diff --git a/examples/roads.jGIS b/examples/roads.jGIS new file mode 100644 index 000000000..76ea0f112 --- /dev/null +++ b/examples/roads.jGIS @@ -0,0 +1,165 @@ +{ + "layerTree": [ + "af9e8ee5-c731-4989-86c9-b0ce1652db89" + ], + "layers": { + "af9e8ee5-c731-4989-86c9-b0ce1652db89": { + "name": "Custom GeoJSON Layer", + "parameters": { + "color": { + "circle-fill-color": [ + "case", + [ + "==", + [ + "get", + "expressway" + ], + 0.0 + ], + [ + 125.0, + 0.0, + 179.0, + 1.0 + ], + [ + "==", + [ + "get", + "expressway" + ], + 1.0 + ], + [ + 147.0, + 255.0, + 0.0, + 1.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0 + ] + ], + "fill-color": "#3399CC", + "stroke-color": [ + "interpolate", + [ + "linear" + ], + [ + "get", + "orig_fid" + ], + 4210.0, + [ + 125.0, + 0.0, + 179.0, + 1.0 + ], + 8420.0, + [ + 116.0, + 0.0, + 218.0, + 1.0 + ], + 12630.0, + [ + 98.0, + 74.0, + 237.0, + 1.0 + ], + 16840.0, + [ + 68.0, + 146.0, + 231.0, + 1.0 + ], + 21050.0, + [ + 0.0, + 204.0, + 197.0, + 1.0 + ], + 25260.0, + [ + 0.0, + 247.0, + 146.0, + 1.0 + ], + 29470.0, + [ + 0.0, + 255.0, + 88.0, + 1.0 + ], + 33680.0, + [ + 40.0, + 255.0, + 8.0, + 1.0 + ], + 37890.0, + [ + 147.0, + 255.0, + 0.0, + 1.0 + ] + ], + "stroke-line-cap": "round", + "stroke-line-join": "round", + "stroke-width": 1.25 + }, + "opacity": 1.0, + "source": "d068cd81-937d-43da-bc0f-54c15b732aba", + "symbologyState": { + "colorRamp": "cool", + "method": "color", + "mode": "equal interval", + "nClasses": "9", + "renderType": "Graduated", + "value": "orig_fid" + }, + "type": "line" + }, + "type": "VectorLayer", + "visible": true + } + }, + "metadata": {}, + "options": { + "bearing": 0.0, + "extent": [ + -156165167.61157694, + -19031255.21022601, + -116090150.92599846, + 17090358.99858201 + ], + "latitude": -8.684240841198346, + "longitude": -142.85556912566426, + "pitch": 0.0, + "projection": "EPSG:3857", + "zoom": 1.9113919878434593 + }, + "sources": { + "d068cd81-937d-43da-bc0f-54c15b732aba": { + "name": "Custom GeoJSON Layer Source", + "parameters": { + "path": "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_10m_roads.geojson" + }, + "type": "GeoJSONSource" + } + } +} diff --git a/examples/world.jGIS b/examples/world.jGIS new file mode 100644 index 000000000..08828e173 --- /dev/null +++ b/examples/world.jGIS @@ -0,0 +1,341 @@ +{ + "layerTree": [ + "ef427395-3dad-411d-ae4b-d56190cb2e68" + ], + "layers": { + "ef427395-3dad-411d-ae4b-d56190cb2e68": { + "name": "Custom GeoJSON Layer", + "parameters": { + "blur": 15.0, + "color": { + "0": "#7d00b3", + "1": "#7400da", + "2": "#624aed", + "3": "#4492e7", + "4": "#00ccc5", + "5": "#00f792", + "6": "#00ff58", + "7": "#28ff08", + "8": "#93ff00", + "circle-fill-color": [ + "case", + [ + "==", + [ + "get", + "MAPCOLOR13" + ], + -99.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + [ + "==", + [ + "get", + "MAPCOLOR13" + ], + 1.0 + ], + [ + 58.0, + 0.0, + 0.0, + 1.0 + ], + [ + "==", + [ + "get", + "MAPCOLOR13" + ], + 2.0 + ], + [ + 115.0, + 0.0, + 0.0, + 1.0 + ], + [ + "==", + [ + "get", + "MAPCOLOR13" + ], + 3.0 + ], + [ + 173.0, + 0.0, + 0.0, + 1.0 + ], + [ + "==", + [ + "get", + "MAPCOLOR13" + ], + 4.0 + ], + [ + 230.0, + 0.0, + 0.0, + 1.0 + ], + [ + "==", + [ + "get", + "MAPCOLOR13" + ], + 5.0 + ], + [ + 236.0, + 53.0, + 0.0, + 1.0 + ], + [ + "==", + [ + "get", + "MAPCOLOR13" + ], + 6.0 + ], + [ + 243.0, + 105.0, + 0.0, + 1.0 + ], + [ + "==", + [ + "get", + "MAPCOLOR13" + ], + 7.0 + ], + [ + 249.0, + 158.0, + 0.0, + 1.0 + ], + [ + "==", + [ + "get", + "MAPCOLOR13" + ], + 8.0 + ], + [ + 255.0, + 210.0, + 0.0, + 1.0 + ], + [ + "==", + [ + "get", + "MAPCOLOR13" + ], + 9.0 + ], + [ + 255.0, + 219.0, + 51.0, + 1.0 + ], + [ + "==", + [ + "get", + "MAPCOLOR13" + ], + 10.0 + ], + [ + 255.0, + 228.0, + 102.0, + 1.0 + ], + [ + "==", + [ + "get", + "MAPCOLOR13" + ], + 11.0 + ], + [ + 255.0, + 237.0, + 153.0, + 1.0 + ], + [ + "==", + [ + "get", + "MAPCOLOR13" + ], + 12.0 + ], + [ + 255.0, + 246.0, + 204.0, + 1.0 + ], + [ + "==", + [ + "get", + "MAPCOLOR13" + ], + 13.0 + ], + [ + 255.0, + 255.0, + 255.0, + 1.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0 + ] + ], + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "get", + "MAPCOLOR13" + ], + -86.55555555555556, + [ + 0.0, + 0.0, + 0.0, + 1.0 + ], + -74.11111111111111, + [ + 115.0, + 0.0, + 0.0, + 1.0 + ], + -61.66666666666667, + [ + 230.0, + 0.0, + 0.0, + 1.0 + ], + -49.22222222222223, + [ + 238.0, + 70.0, + 0.0, + 1.0 + ], + -36.777777777777786, + [ + 247.0, + 140.0, + 0.0, + 1.0 + ], + -24.333333333333343, + [ + 255.0, + 210.0, + 0.0, + 1.0 + ], + -11.888888888888898, + [ + 255.0, + 225.0, + 85.0, + 1.0 + ], + 0.5555555555555465, + [ + 255.0, + 240.0, + 170.0, + 1.0 + ], + 13.0, + [ + 255.0, + 255.0, + 255.0, + 1.0 + ] + ] + }, + "feature": "featurecla", + "opacity": 1.0, + "radius": 8.0, + "source": "5c228c74-5167-4437-a38b-f9302be9b80f", + "symbologyState": { + "colorRamp": "hot", + "method": "color", + "mode": "equal interval", + "nClasses": "9", + "renderType": "Graduated", + "value": "MAPCOLOR13" + }, + "type": "fill" + }, + "type": "VectorLayer", + "visible": true + } + }, + "metadata": {}, + "options": { + "bearing": 0.0, + "extent": [ + -174851033.91853222, + -19719973.28964494, + -135266845.8945556, + 15959232.530512594 + ], + "latitude": -16.652148605851878, + "longitude": 47.08184342581217, + "pitch": 0.0, + "projection": "EPSG:3857", + "zoom": 1.9291708506906815 + }, + "sources": { + "5c228c74-5167-4437-a38b-f9302be9b80f": { + "name": "Custom GeoJSON Layer Source", + "parameters": { + "path": "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_110m_admin_0_countries.geojson" + }, + "type": "GeoJSONSource" + } + } +} diff --git a/python/jupytergis_qgis/jupytergis_qgis/qgis_loader.py b/python/jupytergis_qgis/jupytergis_qgis/qgis_loader.py index 97e3a9d0e..385ff1aca 100644 --- a/python/jupytergis_qgis/jupytergis_qgis/qgis_loader.py +++ b/python/jupytergis_qgis/jupytergis_qgis/qgis_loader.py @@ -30,6 +30,12 @@ QgsSingleBandPseudoColorRenderer, QgsVectorLayer, QgsVectorTileLayer, + QgsSingleSymbolRenderer, + QgsCategorizedSymbolRenderer, + QgsRendererCategory, + QgsGraduatedSymbolRenderer, + QgsRendererRange, + Qgis, ) # Prevent any Qt application and event loop to spawn when @@ -151,13 +157,12 @@ def qgis_layer_to_jgis( source_type = "GeoJSONSource" source = layer.source() - components = source.split("/") - - # Get the last component, which should be the file name - file_name = components[-1] - - # Remove any query parameters - file_name = file_name.split("|")[0] + if source.startswith("http://") or source.startswith("https://"): + file_name = source + else: + components = source.split("/") + file_name = components[-1] + file_name = file_name.split("|")[0] source_parameters.update(path=file_name) @@ -177,11 +182,15 @@ def qgis_layer_to_jgis( color["stroke-color"] = symbol.color().name() + alpha if isinstance(symbol, QgsFillSymbol): - color["fill-color"] = symbol.color().name() + alpha + color["fill-color"] = symbol.color().name() + color["stroke-color"] = symbol.color().name() layer_parameters.update(type="fill") layer_parameters.update(color=color) + if isinstance(renderer, QgsSingleSymbolRenderer): + layer_parameters.update(symbologyState={"renderType": "Single Symbol"}) + if isinstance(layer, QgsVectorTileLayer): layer_type = "VectorTileLayer" source_type = "VectorTileSource" @@ -235,7 +244,7 @@ def qgis_layer_to_jgis( layer_parameters.update(color=color) if layer_type is None: - print(f"JUPYTERGIS - Enable to load layer type {type(layer)}") + print(f"JUPYTERGIS - Unable to load layer type {type(layer)}") return layer_id = layer.id() @@ -376,6 +385,137 @@ def import_project_from_qgis(path: str | Path): } +def get_base_symbol(geometry_type, color_params, opacity): + """Returns a base symbol based on geometry type.""" + if geometry_type == "circle": + symbol = QgsMarkerSymbol() + elif geometry_type == "line": + symbol = QgsLineSymbol() + elif geometry_type == "fill": + symbol = QgsFillSymbol() + else: + return None + + symbol.setOpacity(opacity) + symbol.setOutputUnit(Qgis.RenderUnit.Pixels) + symbol_layer = symbol.symbolLayer(0) + + if geometry_type == "circle": + stroke_color = QColor(color_params.get("circle-stroke-color", "#000000")) + symbol_layer.setStrokeColor(stroke_color) + stroke_width = color_params.get("circle-stroke-width", 1) + symbol_layer.setStrokeWidth(stroke_width) + elif geometry_type == "line": + stroke_color = QColor(color_params.get("stroke-color", "#000000")) + symbol_layer.setStrokeColor(stroke_color) + stroke_width = color_params.get("stroke-width", 1) + symbol_layer.setWidth(stroke_width) + elif geometry_type == "fill": + stroke_color = QColor(color_params.get("stroke-color", "#000000")) + symbol_layer.setStrokeColor(stroke_color) + stroke_width = color_params.get("stroke-width", 1) + symbol_layer.setStrokeWidth(stroke_width) + + return symbol + + +def create_categorized_renderer( + symbology_state, geometry_type, color_params, base_symbol +): + """Creates a categorized renderer.""" + fill_color_rules = color_params.get("circle-fill-color", []) + radius_rules = ( + color_params.get("circle-radius", []) if geometry_type == "circle" else [] + ) + + renderer = QgsCategorizedSymbolRenderer(symbology_state.get("value")) + + for i in range(1, len(fill_color_rules) - 1, 2): + condition = fill_color_rules[i] + color = fill_color_rules[i + 1] + + if isinstance(color, list) and len(color) == 4: + r, g, b, a = color + color = [r, g, b, 1.0] + + category_symbol = base_symbol.clone() + category_symbol.setColor( + QColor(int(color[0]), int(color[1]), int(color[2]), int(color[3] * 255)) + ) + + if geometry_type == "circle" and len(radius_rules) > i: + radius = radius_rules[i + 3] + category_symbol.setSize(2 * radius) + + category = QgsRendererCategory(condition[2], category_symbol, str(condition[2])) + renderer.addCategory(category) + + return renderer + + +def create_graduated_renderer( + symbology_state, geometry_type, color_params, base_symbol +): + """Creates a graduated renderer.""" + if geometry_type == "circle": + fill_color_rules = color_params.get("circle-fill-color", []) + radius_rules = color_params.get("circle-radius", []) + elif geometry_type == "fill": + fill_color_rules = color_params.get("fill-color", []) + elif geometry_type == "line": + fill_color_rules = color_params.get("stroke-color", []) + + ranges = [] + previous_value = 0 + last_color = None + last_radius = None + + for i in range(3, len(fill_color_rules) - 2, 2): + lower_value = fill_color_rules[i] + upper_value = fill_color_rules[i + 2] + color = fill_color_rules[i + 1] + + if isinstance(color, list) and len(color) == 4: + r, g, b, a = color + qcolor = QColor(int(r), int(g), int(b), int(a * 255)) + last_color = qcolor + + range_symbol = base_symbol.clone() + range_symbol.setColor(qcolor) + + if geometry_type == "circle" and len(radius_rules) > i + 1: + radius = radius_rules[i + 1] + if isinstance(radius, (int, float)): + range_symbol.setSize(2 * radius) + last_radius = radius + + g_range = QgsRendererRange( + previous_value, + lower_value, + range_symbol, + f"{previous_value} - {lower_value}", + ) + ranges.append(g_range) + previous_value = lower_value + + if last_color: + final_symbol = base_symbol.clone() + final_symbol.setColor(last_color) + + if geometry_type == "circle" and last_radius is not None: + final_symbol.setSize(2 * last_radius) + + g_range = QgsRendererRange( + previous_value, + upper_value, + final_symbol, + f"{previous_value} - {upper_value}", + ) + ranges.append(g_range) + + return QgsGraduatedSymbolRenderer(symbology_state.get("value"), ranges) + + def jgis_layer_to_qgis( layer_id: str, layers: dict[str, dict[str, Any]], @@ -400,6 +540,10 @@ def build_uri(parameters: dict[str, str], source_type: str) -> str | None: layer_config["url"] = url layer_config["type"] = "xyz" + if source_type == "GeoJSONSource": + path = parameters.get("path", None) + return path + if source_type == "RasterSource": layer_config["crs"] = "EPSG:3857" @@ -477,6 +621,113 @@ def build_uri(parameters: dict[str, str], source_type: str) -> str | None: renderer.setStyles(parsed_styles) + if layer_type == "VectorLayer" and source_type == "GeoJSONSource": + source_parameters = source.get("parameters", {}) + uri = build_uri(source_parameters, "GeoJSONSource") + if not uri: + logs["warnings"].append( + f"Layer {layer_id} not exported: invalid GeoJSON source." + ) + return + + # Not checking for file.isValid() since it will eventually fail to load the file (relative path on the original file does not match server root) + map_layer = QgsVectorLayer(uri, layer_name, "ogr") + crs_84 = QgsCoordinateReferenceSystem("EPSG:4326") + map_layer.setCrs(crs_84) + + geometry_type = layer.get("parameters", {}).get("type") + layer_params = layer.get("parameters", {}) + + if geometry_type == "circle": + symbol = QgsMarkerSymbol() + color_params = layer_params.get("color", {}) + opacity = layer_params.get("opacity", 1.0) + symbology_state = layer_params.get("symbologyState", {}) + render_type = symbology_state.get("renderType", "Single Symbol") + + symbol = get_base_symbol(geometry_type, color_params, opacity) + + render_type = symbology_state.get("renderType", "Single Symbol") + + if render_type == "Single Symbol": + fill_color = QColor(color_params.get("circle-fill-color")) + symbol.setColor(fill_color) + radius = color_params.get("circle-radius", 5) + symbol.setSize(2 * radius) + renderer = QgsSingleSymbolRenderer(symbol) + + elif render_type == "Categorized": + renderer = create_categorized_renderer( + symbology_state, geometry_type, color_params, symbol + ) + + elif render_type == "Graduated": + renderer = create_graduated_renderer( + symbology_state, geometry_type, color_params, symbol + ) + + elif geometry_type == "line": + symbol = QgsLineSymbol() + symbol.setOutputUnit(Qgis.RenderUnit.Pixels) + color_params = layer_params.get("color", {}) + + opacity = layer_params.get("opacity") + + symbology_state = layer_params.get("symbologyState", {}) + render_type = symbology_state.get("renderType", "Single Symbol") + + symbol = get_base_symbol(geometry_type, color_params, opacity) + + render_type = symbology_state.get("renderType", "Single Symbol") + + if render_type == "Single Symbol": + fill_color = QColor(color_params.get("stroke-color")) + symbol.setColor(fill_color) + renderer = QgsSingleSymbolRenderer(symbol) + + elif render_type == "Categorized": + renderer = create_categorized_renderer( + symbology_state, geometry_type, color_params, symbol + ) + + elif render_type == "Graduated": + renderer = create_graduated_renderer( + symbology_state, geometry_type, color_params, symbol + ) + + elif geometry_type == "fill": + symbol = QgsFillSymbol() + symbol.setOutputUnit(Qgis.RenderUnit.Pixels) + color_params = layer_params.get("color", {}) + opacity = layer_params.get("opacity", 1.0) + + symbology_state = layer_params.get("symbologyState", {}) + render_type = symbology_state.get("renderType", "Single Symbol") + + symbol = get_base_symbol(geometry_type, color_params, opacity) + + render_type = symbology_state.get("renderType", "Single Symbol") + + if render_type == "Single Symbol": + fill_color = QColor(color_params.get("fill-color")) + stroke_color = QColor(color_params.get("stroke-color")) + symbol.setColor(fill_color) + symbol_layer = symbol.symbolLayer(0) + symbol_layer.setStrokeColor(stroke_color) + renderer = QgsSingleSymbolRenderer(symbol) + + elif render_type == "Categorized": + renderer = create_categorized_renderer( + symbology_state, geometry_type, color_params, symbol + ) + + elif render_type == "Graduated": + renderer = create_graduated_renderer( + symbology_state, geometry_type, color_params, symbol + ) + + map_layer.setRenderer(renderer) + if layer_type == "WebGlLayer" and source_type == "GeoTiffSource": source_parameters = source.get("parameters", {}) # TODO: Support sources with multiple URLs @@ -585,7 +836,7 @@ def build_uri(parameters: dict[str, str], source_type: str) -> str | None: logs["warnings"].append( f"Layer {layer_id} not exported: enable to export layer type {layer_type}" ) - print(f"JUPYTERGIS - Enable to export layer type {layer_type}") + print(f"JUPYTERGIS - Unable to export layer type {layer_type}") return map_layer.setId(layer_id) diff --git a/python/jupytergis_qgis/jupytergis_qgis/tests/test_qgis.py b/python/jupytergis_qgis/jupytergis_qgis/tests/test_qgis.py index 8d4ff1165..952354130 100644 --- a/python/jupytergis_qgis/jupytergis_qgis/tests/test_qgis.py +++ b/python/jupytergis_qgis/jupytergis_qgis/tests/test_qgis.py @@ -125,8 +125,8 @@ def test_qgis_saver(): if os.path.exists(filename): os.remove(filename) - layer_ids = [str(uuid4()), str(uuid4()), str(uuid4()), str(uuid4())] - source_ids = [str(uuid4()), str(uuid4()), str(uuid4()), str(uuid4())] + layer_ids = [str(uuid4()), str(uuid4()), str(uuid4()), str(uuid4()), str(uuid4())] + source_ids = [str(uuid4()), str(uuid4()), str(uuid4()), str(uuid4()), str(uuid4())] jgis = { "options": { "bearing": 0.0, @@ -185,6 +185,21 @@ def test_qgis_saver(): "type": "RasterLayer", "visible": False, }, + layer_ids[4]: { + "name": "Custom GeoJSON Layer", + "parameters": { + "color": { + "fill-color": "#4ea4d0", + "stroke-color": "#4ea4d0", + }, + "opacity": 1.0, + "source": source_ids[4], + "symbologyState": {"renderType": "Single Symbol"}, + "type": "fill", + }, + "type": "VectorLayer", + "visible": True, + }, }, "layerTree": [ layer_ids[0], @@ -196,6 +211,7 @@ def test_qgis_saver(): ], "name": "group0", }, + layer_ids[4], ], "sources": { source_ids[0]: { @@ -234,6 +250,13 @@ def test_qgis_saver(): "minZoom": 0, }, }, + source_ids[4]: { + "name": "Custom GeoJSON Layer Source", + "parameters": { + "path": "https://raw.githubusercontent.com/nvkelso/natural-earth-vector/master/geojson/ne_110m_admin_0_countries.geojson" + }, + "type": "GeoJSONSource", + }, }, "metadata": {}, }