From 0dfafd0b276f573771ca45eb4fd2ba4f18439d78 Mon Sep 17 00:00:00 2001 From: mozman Date: Tue, 5 Mar 2024 17:45:22 +0100 Subject: [PATCH] refactor JSON backends --- src/ezdxf/addons/drawing/json.py | 167 +++++++++++++++---------------- 1 file changed, 80 insertions(+), 87 deletions(-) diff --git a/src/ezdxf/addons/drawing/json.py b/src/ezdxf/addons/drawing/json.py index efd09e5fe..8bd4293c2 100644 --- a/src/ezdxf/addons/drawing/json.py +++ b/src/ezdxf/addons/drawing/json.py @@ -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 @@ -105,6 +106,62 @@ } """ + + +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" @@ -112,7 +169,7 @@ 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 @@ -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 ): @@ -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: @@ -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: @@ -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: @@ -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]: @@ -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. @@ -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. @@ -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: @@ -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 @@ -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: @@ -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: @@ -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: @@ -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}