def _create_split_tasks(x, y, zoom, task) -> list: """ function for splitting a task square geometry into 4 smaller squares :param geom_to_split: {geojson.Feature} the geojson feature to b split :return: list of {geojson.Feature} """ # If the task's geometry doesn't correspond to an OSM tile identified by an # x, y, zoom then we need to take a different approach to splitting if x is None or y is None or zoom is None: return SplitService._create_split_tasks_from_geometry(task) try: task_geojson = False if task and not task.is_square: query = db.session.query(Task.id, Task.geometry.ST_AsGeoJSON().label('geometry')) \ .filter(Task.id == task.id, Task.project_id == task.project_id) task_geojson = geojson.loads(query[0].geometry) split_geoms = [] for i in range(0, 2): for j in range(0, 2): new_x = x * 2 + i new_y = y * 2 + j new_zoom = zoom + 1 new_square = SplitService._create_square( new_x, new_y, new_zoom) feature = geojson.Feature() feature.geometry = new_square feature_shape = shapely_shape(feature.geometry) if task and not task.is_square: intersection = shapely_shape( task_geojson).intersection(feature_shape) multipolygon = MultiPolygon([intersection]) feature.geometry = geojson.loads( geojson.dumps(mapping(multipolygon))) if task and not task.is_square: is_square = False else: is_square = True feature.properties = { 'x': new_x, 'y': new_y, 'zoom': new_zoom, 'isSquare': is_square } if (len(feature.geometry.coordinates) > 0): split_geoms.append(feature) return split_geoms except Exception as e: raise SplitServiceError( f'unhandled error splitting tile: {str(e)}')
def process_shape(self, shape, field_map, level, version): properties = shape["properties"] fields = self.extract_fields(field_map, properties) fields["level"] = level fields["version"] = version if shape["geometry"] is None: print(f"No geometry present for {fields['code']}") return sh = shapely_shape(shape["geometry"]) geometry = GEOSGeometry(sh.wkt) if type(geometry) == Polygon: geo_shape = MultiPolygon([geometry]) else: geo_shape = geometry if fields["parent_code"] is None: process_as_root = input(f"""Geography '{fields["code"]}' does not have a parent. Load it as a root geography? (Y/N)?""") if not process_as_root.lower().startswith('n'): return self.process_root(fields, geo_shape) return else: self.process_node(fields, geo_shape)
def plot_polygon( geometry: Union[Polygon, MultiPolygon], fill: bool = False, axis: Axes = None, show: bool = False, **kwargs, ) -> Axes: """ Plot the given polygon. :param geometry: Shapely polygon (or multipolygon) :param axis: `pyplot` axis to plot to :param show: whether to show the plot """ if axis is None: axis = pyplot.gca() if 'c' not in kwargs: try: color = next(axis._get_lines.color_cycle) except AttributeError: color = 'r' kwargs['c'] = color if isinstance(geometry, dict): geometry = shapely_shape(geometry) if type(geometry) is Polygon: if fill: axis.fill(*geometry.exterior.xy, **kwargs) kwargs['c'] = 'w' for interior in geometry.interiors: axis.fill(*interior.xy, **kwargs) else: axis.plot(*geometry.exterior.xy, **kwargs) for interior in geometry.interiors: axis.plot(*interior.xy, **kwargs) elif type(geometry) is MultiPolygon: for polygon in geometry: plot_polygon(geometry=polygon, axis=axis, fill=fill, show=False, **kwargs) else: if fill: axis.fill(*geometry.xy, **kwargs) else: axis.plot(*geometry.xy, **kwargs) if show: pyplot.show() return axis
def validate_item(item: Dict): _ITEM_SCHEMA.validate(item) # Should be a valid polygon assert "geometry" in item, "Item has no geometry" assert item["geometry"], "Item has blank geometry" with DebugContext(f"Failing shape:\n{pformat(item['geometry'])}"): shape = shapely_shape(item["geometry"]) assert shape.is_valid, f"Item has invalid geometry: {explain_validity(shape)}" assert shape.geom_type in ( "Polygon", "MultiPolygon", ), "Unexpected type of shape" assert_stac_extensions(item)
def _create_split_tasks_from_geometry(task) -> list: """ Splits a task into 4 smaller tasks based purely on the task's geometry rather than an OSM tile identified by x, y, zoom :return: list of {geojson.Feature} """ # Load the task's geometry and calculate its centroid and bbox query = db.session.query( Task.id, Task.geometry.ST_AsGeoJSON().label("geometry")).filter( Task.id == task.id, Task.project_id == task.project_id) task_geojson = geojson.loads(query[0].geometry) geometry = shapely_shape(task_geojson) centroid = geometry.centroid minx, miny, maxx, maxy = geometry.bounds # split geometry in half vertically, then split those halves in half horizontally split_geometries = [] vertical_dividing_line = LineString([(centroid.x, miny), (centroid.x, maxy)]) horizontal_dividing_line = LineString([(minx, centroid.y), (maxx, centroid.y)]) vertical_halves = SplitService._as_halves( split(geometry, vertical_dividing_line), centroid, "x") for half in vertical_halves: split_geometries += SplitService._as_halves( split(half, horizontal_dividing_line), centroid, "y") # convert split geometries into GeoJSON features expected by Task split_features = [] for split_geometry in split_geometries: feature = geojson.Feature() # Tasks expect multipolygons. Convert and use the database to get as GeoJSON multipolygon_geometry = shape.from_shape(split_geometry, 4326) feature.geometry = geojson.loads( db.engine.execute( multipolygon_geometry.ST_AsGeoJSON()).scalar()) feature.properties["x"] = None feature.properties["y"] = None feature.properties["zoom"] = None feature.properties["isSquare"] = False split_features.append(feature) return split_features
def validate_item(item: Dict): validate_document(item, _ITEM_SCHEMA, schema_folder=_ITEM_SCHEMA_PATH.parent) # Should be a valid polygon assert "geometry" in item, "Item has no geometry" assert item["geometry"], "Item has blank geometry" with DebugContext(f"Failing shape:\n{pformat(item['geometry'])}"): shape = shapely_shape(item["geometry"]) assert shape.is_valid, f"Item has invalid geometry: {explain_validity(shape)}" assert shape.geom_type in ( "Polygon", "MultiPolygon", ), "Unexpected type of shape" # href should never be blank if present # -> The jsonschema enforces href as required, but it's not checking for emptiness. # (and we've had empty ones in previous prototypes) for offset, value in research(item, lambda p, k, v: k == "href"): viewable_offset = "→".join(map(repr, offset)) assert value.strip(), f"href has empty value: {repr(viewable_offset)}"
def wrapped_geom(self): if not self.wrapped_geojson['coordinates']: return GeometryCollection() return shapely_shape(self.wrapped_geojson)
def split_task(split_task_dto: SplitTaskDTO) -> TaskDTOs: """ Replaces a task square with 4 smaller tasks at the next OSM tile grid zoom level Validates that task is: - locked for mapping by current user :param split_task_dto: :return: new tasks in a DTO """ # get the task to be split original_task = Task.get(split_task_dto.task_id, split_task_dto.project_id) if original_task is None: raise NotFound() original_geometry = shape.to_shape(original_task.geometry) # check its locked for mapping by the current user if TaskStatus( original_task.task_status) != TaskStatus.LOCKED_FOR_MAPPING: raise SplitServiceError( "Status must be LOCKED_FOR_MAPPING to split") if original_task.locked_by != split_task_dto.user_id: raise SplitServiceError( "Attempting to split a task owned by another user") # create new geometries from the task geometry try: new_tasks_geojson = SplitService._create_split_tasks( original_task.x, original_task.y, original_task.zoom, original_task) except Exception as e: raise SplitServiceError(f"Error splitting task{str(e)}") # create new tasks from the new geojson i = Task.get_max_task_id_for_project(split_task_dto.project_id) new_tasks = [] new_tasks_dto = [] for new_task_geojson in new_tasks_geojson: # Sanity check: ensure the new task geometry intersects the original task geometry new_geometry = shapely_shape(new_task_geojson.geometry) if not new_geometry.intersects(original_geometry): raise InvalidGeoJson( "New split task does not intersect original task") # insert new tasks into database i = i + 1 new_task = Task.from_geojson_feature(i, new_task_geojson) new_task.project_id = split_task_dto.project_id new_task.task_status = TaskStatus.READY.value new_task.create() new_task.task_history.extend(original_task.copy_task_history()) if new_task.task_history: new_task.clear_task_lock() # since we just copied the lock new_task.set_task_history(TaskAction.STATE_CHANGE, split_task_dto.user_id, None, TaskStatus.SPLIT) new_task.set_task_history(TaskAction.STATE_CHANGE, split_task_dto.user_id, None, TaskStatus.READY) new_task.task_status = TaskStatus.READY.value new_tasks.append(new_task) new_task.update() new_tasks_dto.append( new_task.as_dto_with_instructions( split_task_dto.preferred_locale)) # delete original task from the database try: original_task.delete() except Exception: db.session.rollback() # Ensure the new tasks are cleaned up for new_task in new_tasks: new_task.delete() db.session.commit() raise # update project task counts project = Project.get(split_task_dto.project_id) project.total_tasks = project.tasks.count() # update bad imagery because we may have split a bad imagery tile project.tasks_bad_imagery = project.tasks.filter( Task.task_status == TaskStatus.BADIMAGERY.value).count() project.save() # return the new tasks in a DTO task_dtos = TaskDTOs() task_dtos.tasks = new_tasks_dto return task_dtos