From d6a06734bec3f49544feb7cc2f4a418379375c04 Mon Sep 17 00:00:00 2001 From: KatKatKateryna Date: Thu, 22 Aug 2024 16:05:54 +0100 Subject: [PATCH] support ICurves and new GisFeatures, and (possible) hatches --- pygeoapi/provider/speckle.py | 4 +- .../provider/speckle_utils/converter_utils.py | 456 +++++++++++++----- .../provider/speckle_utils/display_utils.py | 28 +- 3 files changed, 368 insertions(+), 120 deletions(-) diff --git a/pygeoapi/provider/speckle.py b/pygeoapi/provider/speckle.py index dc2deb2..33cc3cc 100644 --- a/pygeoapi/provider/speckle.py +++ b/pygeoapi/provider/speckle.py @@ -370,7 +370,7 @@ class SpeckleProvider(BaseProvider): def traverse_data(self, commit_obj) -> Dict: """Traverse Speckle commit and return geojson with features.""" - from specklepy.objects.geometry import Point, Line, Polyline, Curve, Mesh, Brep + from specklepy.objects.geometry import Point, Line, Curve, Arc, Circle, Ellipse, Polyline, Polycurve, Mesh, Brep from specklepy.objects.GIS.layers import VectorLayer from specklepy.objects.GIS.geometry import GisPolygonElement from specklepy.objects.GIS.GisFeature import GisFeature @@ -382,7 +382,7 @@ class SpeckleProvider(BaseProvider): from pygeoapi.provider.speckle_utils.feature_utils import create_features from pygeoapi.provider.speckle_utils.display_utils import set_default_color - supported_classes = [GisFeature, GisPolygonElement, Mesh, Brep, Point, Line, Polyline, Curve] + supported_classes = [GisFeature, GisPolygonElement, Mesh, Brep, Point, Line, Polyline, Curve, Arc, Circle, Ellipse, Polycurve] supported_types = [y().speckle_type for y in supported_classes] supported_types.extend([ "Objects.Other.Revit.RevitInstance", diff --git a/pygeoapi/provider/speckle_utils/converter_utils.py b/pygeoapi/provider/speckle_utils/converter_utils.py index 81f9979..59f6f2a 100644 --- a/pygeoapi/provider/speckle_utils/converter_utils.py +++ b/pygeoapi/provider/speckle_utils/converter_utils.py @@ -1,13 +1,248 @@ -from typing import Dict, List +import math +from typing import Dict, List, Tuple -def assign_geometry(feature: Dict, f_base) -> ( List[List[List[float]]], List[List[None| List[int]]] ): +def convert_point(f_base: "Point", coords, coord_counts): + """Convert Point.""" + + coords.append([f_base.x, f_base.y, f_base.z]) + coord_counts.append([1]) + +def convert_line(f_base: "Line", coords, coord_counts): + """Convert Line.""" + + start = [f_base.start.x, f_base.start.y, f_base.start.z] + end = [f_base.end.x, f_base.end.y, f_base.end.z] + + coords.extend([start, end]) + coord_counts.append([2]) + +def convert_polyline(f_base: "Polyline", coords, coord_counts): + """Convert Polyline.""" + + coord_counts.append([]) + local_poly_count = 0 + + for pt in f_base.as_points(): + coords.append([pt.x, pt.y, pt.z]) + local_poly_count += 1 + + # closing point + if local_poly_count>2 and f_base.closed is True and coords[0] != coords[-1]: + coords.append(coords[0]) + local_poly_count += 1 + coord_counts[-1].append(local_poly_count) + +def convert_arc(f_base: "Arc", coords, coord_counts): + """Convert Arc.""" + + if f_base.plane is None or f_base.plane.normal.z == 0: + normal = 1 + else: + normal = f_base.plane.normal.z + + # calculate angles and interval + interval, angle1, angle2 = getArcRadianAngle(f_base) + + if (angle1 > angle2 and normal == -1) or (angle2 > angle1 and normal == 1): + pass + if angle1 > angle2 and normal == 1: + interval = abs((2 * math.pi - angle1) + angle2) + if angle2 > angle1 and normal == -1: + interval = abs((2 * math.pi - angle2) + angle1) + + # set a (random) point density: 24 per 1 rad + pointsNum = math.floor(abs(interval)) * 24 + if pointsNum < 4: + pointsNum = 4 + + # assign coordinates + coord_counts.append([]) + local_poly_count = 0 + + for i in range(0, pointsNum + 1): + k = i / pointsNum # reset values to fraction + angle = angle1 + k * interval * normal + + x=f_base.plane.origin.x + f_base.radius * math.cos(angle) + y=f_base.plane.origin.y + f_base.radius * math.sin(angle) + z=f_base.plane.origin.z + + coords.append([x, y, z]) + local_poly_count += 1 + coord_counts[-1].append(local_poly_count) + +def convert_circle(f_base: "Circle", coords, coord_counts): + """Convert Circle.""" + + if f_base.plane is None or f_base.plane.normal.z == 0: + normal = 1 + else: + normal = f_base.plane.normal.z + + # set a (random) point density: 24 per 1 rad + interval = 2 * math.pi + pointsNum = math.floor(abs(interval)) * 24 + if pointsNum < 4: + pointsNum = 4 + + # assign coordinates + coord_counts.append([]) + local_poly_count = 0 + + for i in range(0, pointsNum + 1): + k = i / pointsNum # reset values to fraction + angle = k * interval * normal + + x=f_base.plane.origin.x + f_base.radius * math.cos(angle) + y=f_base.plane.origin.y + f_base.radius * math.sin(angle) + z=f_base.plane.origin.z + + coords.append([x, y, z]) + local_poly_count += 1 + coord_counts[-1].append(local_poly_count) + +def convert_polycurve(f_base: "Polycurve", coords, coord_counts): + """Convert Polycurve.""" + + flat_coords = [] + flat_coord_count = [0] + + # put together results from all segment conversions + for segm in f_base.segments: + convert_icurve(segm, coords, coord_counts) + if len(coord_counts)==0: + continue + flat_coords.extend(coords) + flat_coord_count[-1] += coord_counts[-1][-1] + + coords = flat_coords + coord_counts = flat_coord_count + +def convert_curve(f_base: "Curve", coords, coord_counts): + """Convert Curve using its Polyline displayValue.""" + + return convert_polyline(f_base.displayValue, coords, coord_counts) + +def convert_icurve(f_base: "Base", coords, coord_counts): + """Convert any ICurve.""" + + from specklepy.objects.geometry import Line, Polyline, Arc, Curve, Circle, Polycurve, Mesh, Brep + + if isinstance(f_base, Line): + convert_line(f_base, coords, coord_counts) + + elif isinstance(f_base, Polyline): + convert_polyline(f_base, coords, coord_counts) + + elif isinstance(f_base, Curve): + convert_curve(f_base, coords, coord_counts) + + elif isinstance(f_base, Arc): + convert_arc(f_base, coords, coord_counts) + + elif isinstance(f_base, Circle): + convert_circle(f_base, coords, coord_counts) + + elif isinstance(f_base, Polycurve): + convert_polycurve(f_base, coords, coord_counts) + +def convert_mesh_or_brep(f_base: "Base", coords, coord_counts): + """Convert Mesh object or Mesh derived from Brep display value.""" + from specklepy.objects.geometry import Mesh, Brep + + faces = [] + vertices = [] + + # get faces and vertices + if isinstance(f_base, Mesh): + faces = f_base.faces + vertices = f_base.vertices + elif isinstance(f_base, Brep): + if f_base.displayValue is None or ( + isinstance(f_base.displayValue, list) + and len(f_base.displayValue) == 0 + ): + geometry = {} + return + elif isinstance(f_base.displayValue, list): + faces = f_base.displayValue[0].faces + vertices = f_base.displayValue[0].vertices + else: + faces = f_base.displayValue.faces + vertices = f_base.displayValue.vertices + + # add coordinates + count: int = 0 + for i, pt_count in enumerate(faces): + if i != count: + continue + + # old encoding + if pt_count == 0: + pt_count = 3 + elif pt_count == 1: + pt_count = 4 + coord_counts.append([pt_count]) + + for vertex_index in faces[count + 1 : count + 1 + pt_count]: + x = vertices[vertex_index * 3] + y = vertices[vertex_index * 3 + 1] + z = vertices[vertex_index * 3 + 2] + coords.append([x, y, z]) + + count += pt_count + 1 + +def convert_polygon(polygon: "Base", coords, coord_counts): + """Convert GisPolygonGeometry.""" + + coord_counts.append([]) + boundary_count = 0 + + for pt in polygon.boundary.as_points(): + coords.append([pt.x, pt.y, pt.z]) + boundary_count += 1 + coord_counts[-1].append(boundary_count) + + for void in polygon.voids: + void_count = 0 + for pt_void in void.as_points(): + coords.append([pt_void.x, pt_void.y, pt_void.z]) + void_count += 1 + coord_counts[-1].append(void_count) + +def convert_hatch(hatch: "Base", coords, coord_counts): + """Convert Hatch.""" + + coord_counts.append([]) + boundary_count = 0 + + loops: list = hatch["loops"] + boundary = None + voids = [] + for loop in loops: + print(loop) + print(loop.get_member_names()) + + if len(loops)==1 or loop["type"] == "Outer": + boundary = loop["curve"] + else: + voids.append(loop["curve"]) + if boundary is None: + return + + # record coordinates + convert_icurve(boundary, coords, coord_counts) + for void in voids: + convert_icurve(void, coords, coord_counts) + + +def assign_geometry(feature: Dict, f_base) -> Tuple[ List[List[List[float]]], List[List[None| List[int]]] ]: """Assign geom type and convert object coords into flat lists of coordinates and schema.""" - from specklepy.objects.geometry import Point, Line, Polyline, Curve, Mesh, Brep + from specklepy.objects.geometry import Point, Line, Polyline, Arc, Curve, Circle, Polycurve, Mesh, Brep from specklepy.objects.GIS.geometry import GisPolygonGeometry - from specklepy.objects.GIS.GisFeature import GisFeature geometry = feature["geometry"] coords = [] @@ -15,131 +250,132 @@ def assign_geometry(feature: Dict, f_base) -> ( List[List[List[float]]], List[Li if isinstance(f_base, Point): geometry["type"] = "MultiPoint" - coord_counts.append(None) + coord_counts.append(None) # as an indicator of a Multi..type + convert_point(f_base, coords, coord_counts) - coords.append([f_base.x, f_base.y, f_base.z]) - coord_counts.append([1]) + elif (isinstance(f_base, Line) or + isinstance(f_base, Polyline) or + isinstance(f_base, Curve) or + isinstance(f_base, Arc) or + isinstance(f_base, Circle) or + isinstance(f_base, Polycurve)): + + geometry["type"] = "LineString" + convert_icurve(f_base, coords, coord_counts) + + elif f_base.speckle_type.endswith(".Hatch"): + geometry["type"] = "MultiPolygon" + coord_counts.append(None) + convert_hatch(f_base, coords, coord_counts) elif isinstance(f_base, Mesh) or isinstance(f_base, Brep): - geometry["type"] = "MultiPolygon" - coord_counts.append(None) # as an indicator of a MultiPolygon + geometry["type"] = "MultiPolygon" + coord_counts.append(None) # as an indicator of a Multi..type + convert_mesh_or_brep(f_base, coords, coord_counts) - faces = [] - vertices = [] - if isinstance(f_base, Mesh): - faces = f_base.faces - vertices = f_base.vertices - elif isinstance(f_base, Brep): - if f_base.displayValue is None or ( - isinstance(f_base.displayValue, list) - and len(f_base.displayValue) == 0 - ): - geometry = {} - return - elif isinstance(f_base.displayValue, list): - faces = f_base.displayValue[0].faces - vertices = f_base.displayValue[0].vertices - else: - faces = f_base.displayValue.faces - vertices = f_base.displayValue.vertices - - count: int = 0 - for i, pt_count in enumerate(faces): - if i != count: - continue - - # old encoding - if pt_count == 0: - pt_count = 3 - elif pt_count == 1: - pt_count = 4 - coord_counts.append([pt_count]) - - for vertex_index in faces[count + 1 : count + 1 + pt_count]: - x = vertices[vertex_index * 3] - y = vertices[vertex_index * 3 + 1] - z = vertices[vertex_index * 3 + 2] - coords.append([x, y, z]) - - count += pt_count + 1 - - elif f_base.speckle_type.endswith(".GisFeature") and len(f_base["geometry"]) > 0: # isinstance(f_base, GisFeature) and len(f_base.geometry) > 0: + elif f_base.speckle_type.endswith("Feature") and len(f_base["geometry"]) > 0: # isinstance(f_base, GisFeature) and len(f_base.geometry) > 0: # GisFeature doesn't deserialize properly, need to check for speckle_type - if isinstance(f_base.geometry[0], Point): + if isinstance(f_base["geometry"][0], Point): geometry["type"] = "MultiPoint" - coord_counts.append(None) + coord_counts.append(None) # as an indicator of a Multi..type - for geom in f_base.geometry: - coords.append([geom.x, geom.y, geom.z]) - coord_counts.append([1]) + for geom in f_base["geometry"]: + convert_point(geom, coords, coord_counts) - elif isinstance(f_base.geometry[0], Polyline): + elif isinstance(f_base["geometry"][0], Polyline): geometry["type"] = "MultiLineString" coord_counts.append(None) - for geom in f_base.geometry: - coord_counts.append([]) - local_poly_count = 0 + for geom in f_base["geometry"]: + convert_polyline(geom, coords, coord_counts) - for pt in geom.as_points(): - coords.append([pt.x, pt.y, pt.z]) - local_poly_count += 1 - if len(coords)>2 and geom.closed is True and coords[0] != coords[-1]: - coords.append(coords[0]) - local_poly_count += 1 - - coord_counts[-1].append(local_poly_count) - - elif isinstance(f_base.geometry[0], GisPolygonGeometry): + elif isinstance(f_base["geometry"][0], GisPolygonGeometry): geometry["type"] = "MultiPolygon" coord_counts.append(None) - for polygon in f_base.geometry: - coord_counts.append([]) - boundary_count = 0 - for pt in polygon.boundary.as_points(): - coords.append([pt.x, pt.y, pt.z]) - boundary_count += 1 - - coord_counts[-1].append(boundary_count) - - for void in polygon.voids: - void_count = 0 - for pt_void in void.as_points(): - coords.append([pt_void.x, pt_void.y, pt_void.z]) - void_count += 1 - - coord_counts[-1].append(void_count) - - elif isinstance(f_base, Line): - geometry["type"] = "LineString" - start = [f_base.start.x, f_base.start.y, f_base.start.z] - end = [f_base.end.x, f_base.end.y, f_base.end.z] - - coords.extend([start, end]) - coord_counts.append([2]) - - elif isinstance(f_base, Polyline): - geometry["type"] = "LineString" - for pt in f_base.as_points(): - coords.append([pt.x, pt.y, pt.z]) - if len(coords)>2 and f_base.closed is True and coords[0] != coords[-1]: - coords.append(coords[0]) - - coord_counts.append([len(coords)]) - - elif isinstance(f_base, Curve): - geometry["type"] = "LineString" - for pt in f_base.displayValue.as_points(): - coords.append([pt.x, pt.y, pt.z]) - if len(coords)>2 and f_base.displayValue.closed is True and coords[0] != coords[-1]: - coords.append(coords[0]) - - coord_counts.append([len(coords)]) + for geom in f_base["geometry"]: + convert_polygon(geom, coords, coord_counts) else: geometry = {} # print(f"Unsupported geometry type: {f_base.speckle_type}") return coords, coord_counts + + +def getArcRadianAngle(arc: "Arc") -> List[float]: + """Calculate start & end angle, and interval of an Arc.""" + + interval = None + normal = arc.plane.normal.z + angle1, angle2 = getArcAngles(arc) + if angle1 is None or angle2 is None: + return None + interval = abs(angle2 - angle1) + + if (angle1 > angle2 and normal == -1) or (angle2 > angle1 and normal == 1): + pass + if angle1 > angle2 and normal == 1: + interval = abs((2 * math.pi - angle1) + angle2) + if angle2 > angle1 and normal == -1: + interval = abs((2 * math.pi - angle2) + angle1) + return interval, angle1, angle2 + + +def getArcAngles(poly: "Arc") -> Tuple[float | None]: + + if poly.startPoint.x == poly.plane.origin.x: + angle1 = math.pi / 2 + else: + angle1 = math.atan( + abs( + (poly.startPoint.y - poly.plane.origin.y) + / (poly.startPoint.x - poly.plane.origin.x) + ) + ) # between 0 and pi/2 + + if ( + poly.plane.origin.x < poly.startPoint.x + and poly.plane.origin.y > poly.startPoint.y + ): + angle1 = 2 * math.pi - angle1 + if ( + poly.plane.origin.x > poly.startPoint.x + and poly.plane.origin.y > poly.startPoint.y + ): + angle1 = math.pi + angle1 + if ( + poly.plane.origin.x > poly.startPoint.x + and poly.plane.origin.y < poly.startPoint.y + ): + angle1 = math.pi - angle1 + + if poly.endPoint.x == poly.plane.origin.x: + angle2 = math.pi / 2 + else: + angle2 = math.atan( + abs( + (poly.endPoint.y - poly.plane.origin.y) + / (poly.endPoint.x - poly.plane.origin.x) + ) + ) # between 0 and pi/2 + + if ( + poly.plane.origin.x < poly.endPoint.x + and poly.plane.origin.y > poly.endPoint.y + ): + angle2 = 2 * math.pi - angle2 + if ( + poly.plane.origin.x > poly.endPoint.x + and poly.plane.origin.y > poly.endPoint.y + ): + angle2 = math.pi + angle2 + if ( + poly.plane.origin.x > poly.endPoint.x + and poly.plane.origin.y < poly.endPoint.y + ): + angle2 = math.pi - angle2 + + return angle1, angle2 + diff --git a/pygeoapi/provider/speckle_utils/display_utils.py b/pygeoapi/provider/speckle_utils/display_utils.py index b253fb2..fce5604 100644 --- a/pygeoapi/provider/speckle_utils/display_utils.py +++ b/pygeoapi/provider/speckle_utils/display_utils.py @@ -6,7 +6,7 @@ DEFAULT_COLOR = (255 << 24) + (150 << 16) + (150 << 8) + 150 def find_display_obj(obj) -> Tuple["Base", "Base"]: """Get displayable object.""" - from specklepy.objects.geometry import Base, Mesh + from specklepy.objects.geometry import Point, Line, Arc, Circle, Curve, Polycurve, Mesh, Brep displayVal = obj displayValForColor = obj @@ -52,8 +52,20 @@ def find_display_obj(obj) -> Tuple["Base", "Base"]: displayVal = displayValForColor - # if not searching for colored object, return GisFeatures as is - if obj.speckle_type.endswith(".GisFeature"): + # keep reading color from GisFeature Meshes + if not obj.speckle_type.endswith("Feature"): + displayValForColor = obj + + # return known types as is + if (obj.speckle_type.endswith("Feature") or + isinstance(obj, Point) or + isinstance(obj, Line) or + isinstance(obj, Arc) or + isinstance(obj, Circle) or + isinstance(obj, Curve) or + isinstance(obj, Polycurve) or + isinstance(obj, Mesh) or + isinstance(obj, Brep)): displayVal = obj return displayVal, displayValForColor @@ -107,14 +119,14 @@ def assign_color(obj_display, props) -> None: color = DEFAULT_COLOR try: - if hasattr(obj_display, 'renderMaterial'): - color = obj_display['renderMaterial']['diffuse'] - elif hasattr(obj_display, '@renderMaterial'): - color = obj_display['@renderMaterial']['diffuse'] - elif hasattr(obj_display, 'displayStyle'): + if hasattr(obj_display, 'displayStyle'): color = obj_display['displayStyle']['color'] elif hasattr(obj_display, '@displayStyle'): color = obj_display['@displayStyle']['color'] + elif hasattr(obj_display, 'renderMaterial'): + color = obj_display['renderMaterial']['diffuse'] + elif hasattr(obj_display, '@renderMaterial'): + color = obj_display['@renderMaterial']['diffuse'] elif isinstance(obj_display, Mesh) and isinstance(obj_display.colors, List): sameColors = True color1 = obj_display.colors[0]