def merge_to_multi_polygon(feature_collection: str, dissolve: bool) -> geojson.MultiPolygon: """ Merge all geometries to a single multipolygon :param feature_collection: geojson feature collection str containing features :param dissolve: flag for wther to to dissolve internal boundaries. :return: geojson.MultiPolygon """ parsed_geojson = GridService._to_shapely_geometries( json.dumps(feature_collection)) multi_polygon = GridService._convert_to_multipolygon(parsed_geojson) if dissolve: multi_polygon = GridService._dissolve(multi_polygon) aoi_multi_polygon_geojson = geojson.loads( json.dumps(mapping(multi_polygon))) # validate the geometry if type(aoi_multi_polygon_geojson) is not geojson.MultiPolygon: raise InvalidGeoJson( "Area Of Interest: geometry must be a MultiPolygon") is_valid_geojson = geojson.is_valid(aoi_multi_polygon_geojson) if is_valid_geojson["valid"] == "no": raise InvalidGeoJson( f"Area of Interest: Invalid MultiPolygon - {is_valid_geojson['message']}" ) return aoi_multi_polygon_geojson
def _attach_tasks_to_project(draft_project: Project, tasks_geojson): """ Validates then iterates over the array of tasks and attach them to the draft project :param draft_project: Draft project in scope :param tasks_geojson: GeoJSON feature collection of mapping tasks :raises InvalidGeoJson, InvalidData """ tasks = geojson.loads(json.dumps(tasks_geojson)) if type(tasks) is not geojson.FeatureCollection: raise InvalidGeoJson( "Tasks: Invalid GeoJson must be FeatureCollection") is_valid_geojson = geojson.is_valid(tasks) if is_valid_geojson["valid"] == "no": raise InvalidGeoJson( f"Tasks: Invalid FeatureCollection - {is_valid_geojson['message']}" ) task_count = 1 for feature in tasks["features"]: try: task = Task.from_geojson_feature(task_count, feature) except (InvalidData, InvalidGeoJson) as e: raise e draft_project.tasks.append(task) task_count += 1 task_count -= 1 # Remove last increment before falling out loop draft_project.total_tasks = task_count
def from_dict(cls, area_poly: dict): """ Create a new Priority Area from dictionary """ pa_geojson = geojson.loads(json.dumps(area_poly)) if type(pa_geojson) is not geojson.Polygon: raise InvalidGeoJson("Priority Areas must be supplied as Polygons") is_valid_geojson = geojson.is_valid(pa_geojson) if is_valid_geojson["valid"] == "no": raise InvalidGeoJson( f"Priority Area: Invalid Polygon - {is_valid_geojson['message']}" ) pa = cls() valid_geojson = geojson.dumps(pa_geojson) pa.geometry = ST_SetSRID(ST_GeomFromGeoJSON(valid_geojson), 4326) return pa
def from_geojson_feature(cls, task_id, task_feature): """ Constructs and validates a task from a GeoJson feature object :param task_id: Unique ID for the task :param task_feature: A geoJSON feature object :raises InvalidGeoJson, InvalidData """ if type(task_feature) is not geojson.Feature: raise InvalidGeoJson("Task: Invalid GeoJson should be a feature") task_geometry = task_feature.geometry if type(task_geometry) is not geojson.MultiPolygon: raise InvalidGeoJson("Task: Geometry must be a MultiPolygon") is_valid_geojson = geojson.is_valid(task_geometry) if is_valid_geojson["valid"] == "no": raise InvalidGeoJson( f"Task: Invalid MultiPolygon - {is_valid_geojson['message']}" ) task = cls() try: task.x = task_feature.properties["x"] task.y = task_feature.properties["y"] task.zoom = task_feature.properties["zoom"] task.is_square = task_feature.properties["isSquare"] except KeyError as e: raise InvalidData(f"Task: Expected property not found: {str(e)}") if "extra_properties" in task_feature.properties: task.extra_properties = json.dumps( task_feature.properties["extra_properties"] ) task.id = task_id task_geojson = geojson.dumps(task_geometry) task.geometry = ST_SetSRID(ST_GeomFromGeoJSON(task_geojson), 4326) return task
def _to_shapely_geometries(input: str) -> list: """ Parses the input geojson and returns a list of geojson.Feature objects with their geometries adapted to shapely geometries :param input: string of geojson :return: list of geojson.Feature objects with their geometries adapted to shapely geometries """ collection = geojson.loads(input, object_hook=geojson.GeoJSON.to_instance) if not hasattr(collection, "features") or len(collection.features) < 1: raise InvalidGeoJson("Geojson does not contain any features") shapely_features = list((filter( lambda x: x is not None, map(GridService._adapt_feature_geometry, collection.features), ))) return shapely_features
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