diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f2c0a3c..52bc5dd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,7 +29,7 @@ jobs: run: | python main.py generate_schema ${HOME}/${{ env.FUNCTION_SCHEMA_FILE_NAME }} - name: Speckle Automate Function - Build and Publish - uses: specklesystems/speckle-automate-github-composite-action@0.7.1 + uses: specklesystems/speckle-automate-github-composite-action@0.7.2 with: speckle_automate_url: ${{ env.SPECKLE_AUTOMATE_URL || 'https://automate.speckle.dev' }} speckle_token: ${{ secrets.SPECKLE_FUNCTION_TOKEN }} diff --git a/main.py b/main.py index 5bb04b6..7c37dd5 100644 --- a/main.py +++ b/main.py @@ -15,6 +15,7 @@ from utils.utils_osm import get_buildings, get_roads from utils.utils_other import RESULT_BRANCH from utils.utils_png import create_image_from_bbox +from utils.utils_server import query_version_info class FunctionInputs(AutomateBase): @@ -56,11 +57,12 @@ def automate_function( """ # the context provides a conveniet way, to receive the triggering version try: - base = automate_context.receive_version() - projInfo = base["info"] + projInfo = query_version_info(automate_context) if not projInfo.speckle_type.endswith("Revit.ProjectInfo"): - automate_context.mark_run_failed("Not a valid 'Revit.ProjectInfo' provided") + automate_context.mark_run_failed( + "Not a valid 'Revit.ProjectInfo' provided" + ) lon = np.rad2deg(projInfo["longitude"]) lat = np.rad2deg(projInfo["latitude"]) @@ -86,6 +88,7 @@ def automate_function( source_data="© OpenStreetMap", source_url="https://www.openstreetmap.org/", ) + r''' roads_line_layer = Collection( elements=roads_lines, units="m", @@ -94,6 +97,7 @@ def automate_function( source_data="© OpenStreetMap", source_url="https://www.openstreetmap.org/", ) + ''' roads_mesh_layer = Collection( elements=roads_meshes, units="m", @@ -105,7 +109,7 @@ def automate_function( # add layers to a commit Collection object commit_obj = Collection( - elements=[building_layer, roads_line_layer, roads_mesh_layer], + elements=[building_layer, roads_mesh_layer], units="m", name="Context", collectionType="ContextLayer", diff --git a/poetry.lock b/poetry.lock index e0a0cad..c3a2c29 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1282,13 +1282,13 @@ files = [ [[package]] name = "specklepy" -version = "2.17.9" +version = "2.17.10" description = "The Python SDK for Speckle 2.0" optional = false python-versions = ">=3.8.0,<4.0" files = [ - {file = "specklepy-2.17.9-py3-none-any.whl", hash = "sha256:d3223e69197b07b28b469d29a2a5baf832a3dea6c3b493d7c20b692da57685e6"}, - {file = "specklepy-2.17.9.tar.gz", hash = "sha256:e0b7d7f46c7a65145f8b8b3bb5c9d5a4f97591db786a68e2c53351c026b92030"}, + {file = "specklepy-2.17.10-py3-none-any.whl", hash = "sha256:e1689f33ff70d83d4e3a0f0c2af984dae8c4a9a9d19444f6b1ad696242fc3398"}, + {file = "specklepy-2.17.10.tar.gz", hash = "sha256:c2623bcd0f62dcfbf3fd496d722296d547939203173c39f8b8044c90b5d58e4c"}, ] [package.dependencies] @@ -1671,4 +1671,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "c38dc2695e724d43f33afe7c90bfbd5c7a7238aae39e2944151630f1f3df78e8" +content-hash = "7d663dbef87272b3f373be87d798782d8b9350aa990da1c1eae1497777c7fc61" diff --git a/pyproject.toml b/pyproject.toml index e5bbedb..c2a0bd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ packages = [{include = "src/speckle_automate_py"}] [tool.poetry.dependencies] python = "^3.11" -specklepy = "2.17.9" +specklepy = "2.17.10" pyproj = "^3.6.1" shapely = "^2.0.2" pypng = "^0.20220715.0" diff --git a/utils/utils_geometry.py b/utils/utils_geometry.py index 6e5eb09..14ef831 100644 --- a/utils/utils_geometry.py +++ b/utils/utils_geometry.py @@ -132,7 +132,11 @@ def to_triangles( else: polygon = Polygon([(v[0], v[1]) for v in vert], holes) - exterior_linearring = polygon.exterior + try: + exterior_linearring = polygon.buffer(-0.001).exterior + except AttributeError: + exterior_linearring = polygon.buffer(-0.0001).exterior + poly_points = np.array(exterior_linearring.coords).tolist() try: @@ -149,7 +153,7 @@ def to_triangles( ).reshape(-1, 2) poly_shapes, _ = voronoi_regions_from_coords( - poly_points, polygon.buffer(0.000001) + poly_points, polygon.buffer(0) ) gdf_poly_voronoi = ( gpd.GeoDataFrame({"geometry": poly_shapes}) @@ -205,10 +209,12 @@ def rotate_pt(coord: dict, angle: float) -> dict: return {"x": x2, "y": y2} -def extrude_building( + +def extrude_building_simple( coords: list[dict], coords_inner: list[list[dict]], height: float ) -> Mesh: """Create a 3d Mesh from the lists of outer and inner coords and height.""" + vertices = [] faces = [] colors = [] @@ -217,141 +223,156 @@ def extrude_building( if len(coords) < 3: return None - # if the building has single outline - if len(coords_inner) == 0: - # bottom - bottom_vert_indices = list(range(len(coords))) - bottom_vertices = [[c["x"], c["y"]] for c in coords] - bottom_vert_indices, clockwise_orientation = fix_orientation( - bottom_vertices, bottom_vert_indices + + # bottom + bottom_vert_indices = list(range(len(coords))) + bottom_vertices = [[c["x"], c["y"]] for c in coords] + bottom_vert_indices, clockwise_orientation = fix_orientation( + bottom_vertices, bottom_vert_indices + ) + for c in coords: + vertices.extend([c["x"], c["y"], 0]) + colors.append(color) + faces.extend([len(coords)] + bottom_vert_indices) + + # top + top_vert_indices = list(range(len(coords), 2 * len(coords))) + for c in coords: + vertices.extend([c["x"], c["y"], height]) + colors.append(color) + + if clockwise_orientation is True: # if facing down originally + top_vert_indices.reverse() + faces.extend([len(coords)] + top_vert_indices) + + # sides + total_vertices = len(colors) + for i, c in enumerate(coords): + if i != len(coords) - 1: + next_coord_index = coords[i + 1] + else: + next_coord_index = coords[0] # 0 + + side_vert_indices = list(range(total_vertices, total_vertices + 4)) + faces.extend([4] + side_vert_indices) + side_vertices = create_side_face( + coords, i, next_coord_index, height, clockwise_orientation ) - for c in coords: - vertices.extend([c["x"], c["y"], 0]) - colors.append(color) - faces.extend([len(coords)] + bottom_vert_indices) + vertices.extend(side_vertices) + colors.extend([color, color, color, color]) + total_vertices += 4 - # top - top_vert_indices = list(range(len(coords), 2 * len(coords))) - for c in coords: - vertices.extend([c["x"], c["y"], height]) - colors.append(color) + obj = Mesh.create(faces=faces, vertices=vertices, colors=colors) + obj.units = "m" - if clockwise_orientation is True: # if facing down originally - top_vert_indices.reverse() - faces.extend([len(coords)] + top_vert_indices) + return obj - # sides - total_vertices = len(colors) - for i, c in enumerate(coords): - if i != len(coords) - 1: - next_coord_index = coords[i + 1] - else: - next_coord_index = coords[0] # 0 - side_vert_indices = list(range(total_vertices, total_vertices + 4)) - faces.extend([4] + side_vert_indices) - side_vertices = create_side_face( - coords, i, next_coord_index, height, clockwise_orientation - ) - # if clockwise_orientation is True: # if facing down originally - # side_vertices.reverse() +def extrude_building( + coords: list[dict], coords_inner: list[list[dict]], height: float +) -> Mesh: + """Create a 3d Mesh from the lists of outer and inner coords and height.""" + vertices = [] + faces = [] + colors = [] - vertices.extend(side_vertices) - colors.extend([color, color, color, color]) - total_vertices += 4 + color = COLOR_BLD # (255<<24) + (100<<16) + (100<<8) + 100 # argb - else: # if outline contains holes and mesh needs to be constructed - # bottom - try: - total_vertices = 0 - triangulated_geom, _ = to_triangles(coords, coords_inner) - except Exception as e: # default to only outer border mesh generation - print(f"Mesh creation failed: {e}") - return extrude_building(coords, [], height) - - if triangulated_geom is None: # default to only outer border mesh generation - return extrude_building(coords, [], height) - - pt_list = [[p[0], p[1], 0] for p in triangulated_geom["vertices"]] - triangle_list = [trg for trg in triangulated_geom["triangles"]] - - for trg in triangle_list: - a = trg[0] - b = trg[1] - c = trg[2] - vertices.extend(pt_list[a] + pt_list[b] + pt_list[c]) - colors.extend([color, color, color]) - total_vertices += 3 - - # all faces are counter-clockwise now (facing up) - # therefore, add vertices in the reverse (clockwise) order (facing down) - faces.extend( - [3, total_vertices - 1, total_vertices - 2, total_vertices - 3] - ) + if len(coords) < 3: + return None - # top - pt_list = [[p[0], p[1], height] for p in triangulated_geom["vertices"]] - - for trg in triangle_list: - a = trg[0] - b = trg[1] - c = trg[2] - # all faces are counter-clockwise now (facing up) - vertices.extend(pt_list[a] + pt_list[b] + pt_list[c]) - colors.extend([color, color, color]) - total_vertices += 3 - faces.extend( - [3, total_vertices - 3, total_vertices - 2, total_vertices - 1] - ) + # bottom + try: + total_vertices = 0 + triangulated_geom, _ = to_triangles(coords, coords_inner) + except Exception as e: # default to only outer border mesh generation + print(f"Mesh creation failed: {e}") + return extrude_building_simple(coords, [], height) + + if triangulated_geom is None: # default to only outer border mesh generation + return extrude_building_simple(coords, [], height) + + pt_list = [[p[0], p[1], 0] for p in triangulated_geom["vertices"]] + triangle_list = [trg for trg in triangulated_geom["triangles"]] + + for trg in triangle_list: + a = trg[0] + b = trg[1] + c = trg[2] + vertices.extend(pt_list[a] + pt_list[b] + pt_list[c]) + colors.extend([color, color, color]) + total_vertices += 3 + + # all faces are counter-clockwise now (facing up) + # therefore, add vertices in the reverse (clockwise) order (facing down) + faces.extend( + [3, total_vertices - 1, total_vertices - 2, total_vertices - 3] + ) - # sides - bottom_vert_indices = list(range(len(coords))) - bottom_vertices = [[c["x"], c["y"]] for c in coords] - bottom_vert_indices, clockwise_orientation = fix_orientation( - bottom_vertices, bottom_vert_indices + # top + pt_list = [[p[0], p[1], height] for p in triangulated_geom["vertices"]] + + for trg in triangle_list: + a = trg[0] + b = trg[1] + c = trg[2] + # all faces are counter-clockwise now (facing up) + vertices.extend(pt_list[a] + pt_list[b] + pt_list[c]) + colors.extend([color, color, color]) + total_vertices += 3 + faces.extend( + [3, total_vertices - 3, total_vertices - 2, total_vertices - 1] ) - for i, c in enumerate(coords): - if i != len(coords) - 1: - next_coord_index = coords[i + 1] + + # sides + bottom_vert_indices = list(range(len(coords))) + bottom_vertices = [[c["x"], c["y"]] for c in coords] + bottom_vert_indices, clockwise_orientation = fix_orientation( + bottom_vertices, bottom_vert_indices + ) + for i, c in enumerate(coords): + if i != len(coords) - 1: + next_coord_index = coords[i + 1] + else: + next_coord_index = coords[0] # 0 + + side_vert_indices = list(range(total_vertices, total_vertices + 4)) + faces.extend([4] + side_vert_indices) + side_vertices = create_side_face( + coords, i, next_coord_index, height, clockwise_orientation + ) + + vertices.extend(side_vertices) + colors.extend([color, color, color, color]) + total_vertices += 4 + + # voids sides + for _, local_coords_inner in enumerate(coords_inner): + bottom_void_vert_indices = list(range(len(local_coords_inner))) + bottom_void_vertices = [[c["x"], c["y"]] for c in local_coords_inner] + bottom_void_vert_indices, clockwise_orientation_void = fix_orientation( + bottom_void_vertices, bottom_void_vert_indices + ) + + for i, c in enumerate(local_coords_inner): + if i != len(local_coords_inner) - 1: + next_coord_index = local_coords_inner[i + 1] else: - next_coord_index = coords[0] # 0 + next_coord_index = local_coords_inner[0] # 0 side_vert_indices = list(range(total_vertices, total_vertices + 4)) faces.extend([4] + side_vert_indices) side_vertices = create_side_face( - coords, i, next_coord_index, height, clockwise_orientation + local_coords_inner, + i, + next_coord_index, + height, + clockwise_orientation_void, ) - vertices.extend(side_vertices) colors.extend([color, color, color, color]) total_vertices += 4 - # voids sides - for _, local_coords_inner in enumerate(coords_inner): - bottom_void_vert_indices = list(range(len(local_coords_inner))) - bottom_void_vertices = [[c["x"], c["y"]] for c in local_coords_inner] - bottom_void_vert_indices, clockwise_orientation_void = fix_orientation( - bottom_void_vertices, bottom_void_vert_indices - ) - - for i, c in enumerate(local_coords_inner): - if i != len(local_coords_inner) - 1: - next_coord_index = local_coords_inner[i + 1] - else: - next_coord_index = local_coords_inner[0] # 0 - - side_vert_indices = list(range(total_vertices, total_vertices + 4)) - faces.extend([4] + side_vert_indices) - side_vertices = create_side_face( - local_coords_inner, - i, - next_coord_index, - height, - clockwise_orientation_void, - ) - vertices.extend(side_vertices) - colors.extend([color, color, color, color]) - total_vertices += 4 obj = Mesh.create(faces=faces, vertices=vertices, colors=colors) obj.units = "m" @@ -436,5 +457,7 @@ def join_roads(coords: list[dict], closed: bool, height: float) -> Polyline: poly = Polyline.from_points(points) poly.closed = closed poly.units = "m" + poly.source_data="© OpenStreetMap", + poly.source_url="https://www.openstreetmap.org/", return poly diff --git a/utils/utils_osm.py b/utils/utils_osm.py index b313564..60fdd93 100644 --- a/utils/utils_osm.py +++ b/utils/utils_osm.py @@ -150,11 +150,26 @@ def get_buildings(lat: float, lon: float, r: float, angle_rad: float) -> list[Ba for k, z in enumerate(ways_part): if k == len(ways_part): break + if rel_outer_ways[n][m]["ref"] == ways_part[k]["id"]: - full_node_list += ways_part[k]["nodes"] + # reverse way part is needed + node_list = ways_part[k]["nodes"].copy() + if ( + len(full_node_list) > 0 + and full_node_list[len(full_node_list) - 1] != node_list[0] + ): + node_list.reverse() + node_list = [ + n + for j, n in enumerate(node_list) + if (n not in full_node_list or j == len(node_list) - 1) + ] + + full_node_list += node_list ways_part.pop(k) # remove used ways_parts k -= 1 # reset index break + for m, y in enumerate(rel_inner_ways[n]): # find ways_parts with corresponding ID local_node_list = [] @@ -162,7 +177,15 @@ def get_buildings(lat: float, lon: float, r: float, angle_rad: float) -> list[Ba if k == len(ways_part): break if rel_inner_ways[n][m]["ref"] == ways_part[k]["id"]: - local_node_list += ways_part[k]["nodes"] + # reverse way part is needed + node_list = ways_part[k]["nodes"].copy() + if ( + len(local_node_list) > 0 + and local_node_list[len(local_node_list) - 1] != node_list[0] + ): + node_list.reverse() + + local_node_list += node_list # ways_part[k]["nodes"] ways_part.pop(k) # remove used ways_parts k -= 1 # reset index break diff --git a/utils/utils_other.py b/utils/utils_other.py index e17c255..b9a1924 100644 --- a/utils/utils_other.py +++ b/utils/utils_other.py @@ -3,7 +3,7 @@ from utils.utils_pyproj import create_crs, reproject_to_crs RESULT_BRANCH = "automate" -COLOR_ROAD = (255 << 24) + (50 << 16) + (50 << 8) + 50 # argb +COLOR_ROAD = (255 << 24) + (20 << 16) + (20 << 8) + 20 # argb COLOR_BLD = (255 << 24) + (230 << 16) + (230 << 8) + 230 # argb COLOR_VISIBILITY = (255 << 24) + (255 << 16) + (10 << 8) + 10 # argb @@ -43,7 +43,7 @@ def fill_list(vals: list, lsts: list) -> list[list]: if len(vals) > 1: lsts.append([]) else: - return + return lsts for i, v in enumerate(vals): if v not in lsts[len(lsts) - 1]: diff --git a/utils/utils_server.py b/utils/utils_server.py new file mode 100644 index 0000000..2f31686 --- /dev/null +++ b/utils/utils_server.py @@ -0,0 +1,57 @@ + + +from specklepy.api.wrapper import StreamWrapper +from gql import gql + +def query_version_info(automate_context): + automation_run_data = automate_context.automation_run_data + # get branch name + query = gql( + """ + query Stream($project_id: String!, $model_id: String!, $version_id: String!) { + project(id:$project_id) { + model(id: $model_id) { + version(id: $version_id) { + referencedObject + } + } + } + } + """ + ) + sw = StreamWrapper( + f"{automation_run_data.speckle_server_url}/projects/{automation_run_data.project_id}" + ) + client = sw.get_client() + params = { + "project_id": automation_run_data.project_id, + "model_id": automation_run_data.model_id, + "version_id": automation_run_data.version_id, + } + project = client.httpclient.execute(query, params) + try: + ref_obj = project["project"]["model"]["version"]["referencedObject"] + # get Project Info + query = gql( + """ + query Stream($project_id: String!, $ref_id: String!) { + stream(id: $project_id){ + object(id: $ref_id){ + data + } + } + } + """ + ) + params = { + "project_id": automation_run_data.project_id, + "ref_id": ref_obj, + } + project = client.httpclient.execute(query, params) + projInfo = project["stream"]["object"]["data"]["info"] + + except KeyError: + base = automate_context.receive_version() + projInfo = base["info"] + + return projInfo