Skip to content

Commit

Permalink
refactor JSON backends
Browse files Browse the repository at this point in the history
  • Loading branch information
mozman committed Mar 5, 2024
1 parent f21c0cc commit 0dfafd0
Showing 1 changed file with 80 additions and 87 deletions.
167 changes: 80 additions & 87 deletions src/ezdxf/addons/drawing/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# License: MIT License
from __future__ import annotations
from typing import Iterable, Sequence, no_type_check, Any, Callable, Dict, List, Tuple
from typing_extensions import TypeAlias
from typing_extensions import TypeAlias, override
import abc
import json

from ezdxf.math import Vec2
Expand Down Expand Up @@ -105,14 +106,70 @@
}
"""


class _JSONBackend(BackendInterface):
def __init__(self) -> None:
self._entities: list[dict[str, Any]] = []
self.max_sagitta = 0.01 # set by configure()
self.min_lineweight = 0.05 # in mm, set by configure()
self.lineweight_scaling = 1.0 # set by configure()
# set fixed lineweight for all strokes:
# set Configuration.min_lineweight to the desired lineweight in 1/300 inch!
# set Configuration.lineweight_scaling to 0
self.fixed_lineweight = 0.0

@abc.abstractmethod
def get_json_data(self) -> Any: ...
def get_string(self, *, indent: int | str = 2) -> str:
"""Returns the result as a JSON string."""
return json.dumps(self.get_json_data(), indent=indent)

@override
def configure(self, config: Configuration) -> None:
if config.min_lineweight:
# config.min_lineweight in 1/300 inch!
min_lineweight_mm = config.min_lineweight * 25.4 / 300
self.min_lineweight = max(0.05, min_lineweight_mm)
self.lineweight_scaling = config.lineweight_scaling
if self.lineweight_scaling == 0.0:
# use a fixed lineweight for all strokes defined by min_lineweight
self.fixed_lineweight = self.min_lineweight
self.max_sagitta = config.max_flattening_distance

@override
def clear(self) -> None:
self._entities.clear()

@override
def draw_image(self, image_data: ImageData, properties: BackendProperties) -> None:
pass

@override
def set_background(self, color: Color) -> None:
pass

@override
def finalize(self) -> None:
pass

@override
def enter_entity(self, entity, properties) -> None:
pass

@override
def exit_entity(self, entity) -> None:
pass


MOVE_TO_ABS = "M"
LINE_TO_ABS = "L"
QUAD_TO_ABS = "Q"
CUBIC_TO_ABS = "C"
CLOSE_PATH = "Z"


class CustomJSONBackend(BackendInterface):
class CustomJSONBackend(_JSONBackend):
"""Creates a JSON-like output with a custom JSON scheme. This scheme supports
curved shapes by a SVG-path like structure and coordinates are not limited in
any way. This backend can be used to send geometries from a web-backend to a
Expand All @@ -135,33 +192,14 @@ class CustomJSONBackend(BackendInterface):
"""

def __init__(self, orient_paths=False) -> None:
self._entities: list[dict[str, Any]] = []
super().__init__()
self.orient_paths = orient_paths
self.min_lineweight = 0.05 # in mm, set by configure()
self.lineweight_scaling = 1.0 # set by configure()
# set fixed lineweight for all strokes:
# set Configuration.min_lineweight to the desired lineweight in 1/300 inch!
# set Configuration.lineweight_scaling to 0
self.fixed_lineweight = 0.0

@override
def get_json_data(self) -> list[dict[str, Any]]:
"""Returns the result as a JSON-like data structure."""
return self._entities

def get_string(self, *, indent: int | str = 2) -> str:
"""Returns the result as a JSON string."""
return json.dumps(self.get_json_data(), indent=indent)

def configure(self, config: Configuration) -> None:
if config.min_lineweight:
# config.min_lineweight in 1/300 inch!
min_lineweight_mm = config.min_lineweight * 25.4 / 300
self.min_lineweight = max(0.05, min_lineweight_mm)
self.lineweight_scaling = config.lineweight_scaling
if self.lineweight_scaling == 0.0:
# use a fixed lineweight for all strokes defined by min_lineweight
self.fixed_lineweight = self.min_lineweight

def add_entity(
self, entity_type: str, geometry: Sequence[Any], properties: BackendProperties
):
Expand All @@ -188,12 +226,15 @@ def make_properties_dict(self, properties: BackendProperties) -> dict[str, Any]:
"layer": properties.layer,
}

@override
def draw_point(self, pos: Vec2, properties: BackendProperties) -> None:
self.add_entity("point", [pos.x, pos.y], properties)

@override
def draw_line(self, start: Vec2, end: Vec2, properties: BackendProperties) -> None:
self.add_entity("lines", [(start.x, start.y, end.x, end.y)], properties)

@override
def draw_solid_lines(
self, lines: Iterable[tuple[Vec2, Vec2]], properties: BackendProperties
) -> None:
Expand All @@ -202,9 +243,11 @@ def draw_solid_lines(
return
self.add_entity("lines", [(s.x, s.y, e.x, e.y) for s, e in lines], properties)

@override
def draw_path(self, path: BkPath2d, properties: BackendProperties) -> None:
self.add_entity("path", make_json_path(path), properties)

@override
def draw_filled_paths(
self, paths: Iterable[BkPath2d], properties: BackendProperties
) -> None:
Expand All @@ -223,6 +266,7 @@ def draw_filled_paths(
if json_paths:
self.add_entity("filled-paths", json_paths, properties)

@override
def draw_filled_polygon(
self, points: BkPoints2d, properties: BackendProperties
) -> None:
Expand All @@ -233,24 +277,6 @@ def draw_filled_polygon(
vertices.append(vertices[0])
self.add_entity("filled-polygon", [(v.x, v.y) for v in vertices], properties)

def clear(self) -> None:
self._entities.clear()

def draw_image(self, image_data: ImageData, properties: BackendProperties) -> None:
pass

def set_background(self, color: Color) -> None:
pass

def finalize(self) -> None:
pass

def enter_entity(self, entity, properties) -> None:
pass

def exit_entity(self, entity) -> None:
pass


@no_type_check
def make_json_path(path: BkPath2d, close=False) -> list[Any]:
Expand Down Expand Up @@ -305,7 +331,7 @@ def properties_maker(color: str, stroke_width: float, layer: str) -> dict[str, A
}


class GeoJSONBackend(BackendInterface):
class GeoJSONBackend(_JSONBackend):
"""Creates a JSON-like output according the `GeoJSON`_ scheme.
GeoJSON uses a geographic coordinate reference system, World Geodetic
System 1984, and units of decimal degrees.
Expand All @@ -321,14 +347,14 @@ class GeoJSONBackend(BackendInterface):
The GeoJSON format supports only straight lines so curved shapes are flattened to
polylines and polygons.
The properties are handled as a foreign member feature and is therefore not defined
in the GeoJSON specs. It is possible to provide a custom function to create these
The properties are handled as a foreign member feature and is therefore not defined
in the GeoJSON specs. It is possible to provide a custom function to create these
property objects.
Default implementation:
.. autofunction:: properties_maker
Args:
properties_maker: function to create a properties dict.
Expand All @@ -343,16 +369,10 @@ class GeoJSONBackend(BackendInterface):
"""

def __init__(self, properties_maker: PropertiesMaker = properties_maker) -> None:
self._entities: list[dict[str, Any]] = []
super().__init__()
self._properties_dict_maker = properties_maker
self.max_sagitta = 0.01 # set by configure()
self.min_lineweight = 0.05 # in mm, set by configure()
self.lineweight_scaling = 1.0 # set by configure()
# set fixed lineweight for all strokes:
# set Configuration.min_lineweight to the desired lineweight in 1/300 inch!
# set Configuration.lineweight_scaling to 0
self.fixed_lineweight = 0.0

@override
def get_json_data(self) -> dict[str, Any]:
"""Returns the result as a JSON-like data structure according the GeoJSON specs."""
if len(self._entities) == 0:
Expand All @@ -363,21 +383,6 @@ def get_json_data(self) -> dict[str, Any]:
else:
return {"type": "GeometryCollection", "geometries": self._entities}

def get_string(self, *, indent: int | str = 2) -> str:
"""Returns the result as a JSON string."""
return json.dumps(self.get_json_data(), indent=indent)

def configure(self, config: Configuration) -> None:
if config.min_lineweight:
# config.min_lineweight in 1/300 inch!
min_lineweight_mm = config.min_lineweight * 25.4 / 300
self.min_lineweight = max(0.05, min_lineweight_mm)
self.lineweight_scaling = config.lineweight_scaling
if self.lineweight_scaling == 0.0:
# use a fixed lineweight for all strokes defined by min_lineweight
self.fixed_lineweight = self.min_lineweight
self.max_sagitta = config.max_flattening_distance

def add_entity(self, entity: dict[str, Any], properties: BackendProperties):
if not entity:
return
Expand All @@ -404,15 +409,18 @@ def make_properties(self, properties: BackendProperties) -> tuple[str, float, st
)
return (properties.color, round(stroke_width, 2), properties.layer)

@override
def draw_point(self, pos: Vec2, properties: BackendProperties) -> None:
self.add_entity(geojson_object("Point", [pos.x, pos.y]), properties)

@override
def draw_line(self, start: Vec2, end: Vec2, properties: BackendProperties) -> None:
self.add_entity(
geojson_object("LineString", [(start.x, start.y), (end.x, end.y)]),
properties,
)

@override
def draw_solid_lines(
self, lines: Iterable[tuple[Vec2, Vec2]], properties: BackendProperties
) -> None:
Expand All @@ -422,12 +430,14 @@ def draw_solid_lines(
json_lines = [((s.x, s.y), (e.x, e.y)) for s, e in lines]
self.add_entity(geojson_object("MultiLineString", json_lines), properties)

@override
def draw_path(self, path: BkPath2d, properties: BackendProperties) -> None:
if len(path) == 0:
return
vertices = [(v.x, v.y) for v in path.flattening(distance=self.max_sagitta)]
self.add_entity(geojson_object("LineString", vertices), properties)

@override
def draw_filled_paths(
self, paths: Iterable[BkPath2d], properties: BackendProperties
) -> None:
Expand All @@ -442,6 +452,7 @@ def draw_filled_paths(
if polygons:
self.add_entity(geojson_object("MultiPolygon", polygons), properties)

@override
def draw_filled_polygon(
self, points: BkPoints2d, properties: BackendProperties
) -> None:
Expand All @@ -455,24 +466,6 @@ def draw_filled_polygon(
geojson_object("Polygon", [[(v.x, v.y) for v in vertices]]), properties
)

def clear(self) -> None:
self._entities.clear()

def draw_image(self, image_data: ImageData, properties: BackendProperties) -> None:
pass

def set_background(self, color: Color) -> None:
pass

def finalize(self) -> None:
pass

def enter_entity(self, entity, properties) -> None:
pass

def exit_entity(self, entity) -> None:
pass


def geojson_object(name: str, coordinates: Any) -> dict[str, Any]:
return {"type": name, "coordinates": coordinates}
Expand Down

0 comments on commit 0dfafd0

Please sign in to comment.