Files
opengeos--geolibre/python/tests/test_project.py
T
wehub-resource-sync 7df9ebf22c
Sync labels / sync (push) Failing after 1m9s
Publish Container Image / Build and publish container image (push) Has been cancelled
Deploy Website / Deploy to GitHub Pages (push) Has been cancelled
Deploy Website / Build website and demo (push) Has been cancelled
Deploy viewer / Deploy viewer proxy to Cloudflare Workers (push) Has been cancelled
Deploy tiles / Deploy planetary tile proxy to Cloudflare Workers (push) Has been cancelled
Deploy collab / Deploy collaboration relay to Cloudflare Workers (push) Has been cancelled
CI / Dependency audit (push) Has been cancelled
CI / E2E smoke (Playwright) (push) Has been cancelled
CI / Validate CITATION.cff (push) Has been cancelled
CI / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:00 +08:00

351 lines
12 KiB
Python

"""Unit tests for the project/layer builders.
These exercise the pure-Python layer construction without needing a browser or
the bundled web app, so they run in plain CI.
"""
from __future__ import annotations
import json
import pytest
from geolibre import project
POINT_FC = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {"name": "A"},
"geometry": {"type": "Point", "coordinates": [0, 0]},
}
],
}
def test_build_empty_project_defaults():
proj = project.build_empty_project()
assert proj["version"] == project.PROJECT_VERSION
assert proj["mapView"]["center"] == [-100, 40]
assert proj["layers"] == []
# Preferences must be a fresh copy, not the shared default.
assert proj["preferences"] is not project.DEFAULT_PROJECT_PREFERENCES
def test_build_empty_project_overrides():
proj = project.build_empty_project(center=(10, 20), zoom=7, basemap_url="x")
assert proj["mapView"]["center"] == [10.0, 20.0]
assert proj["mapView"]["zoom"] == 7.0
assert proj["basemapStyleUrl"] == "x"
def test_geojson_layer_inlines_data():
layer = project.geojson_layer("Pts", POINT_FC, fillColor="#ff0000")
assert layer["type"] == "geojson"
assert layer["source"] == {"type": "geojson"}
assert layer["geojson"] == POINT_FC
assert layer["style"]["fillColor"] == "#ff0000"
# Unspecified style fields fall back to the defaults.
assert layer["style"]["strokeWidth"] == project.DEFAULT_LAYER_STYLE["strokeWidth"]
def test_geojson_layer_with_source_url():
layer = project.geojson_layer("R", POINT_FC, source_url="https://e/x.geojson")
assert layer["source"]["url"] == "https://e/x.geojson"
assert layer["sourcePath"] == "https://e/x.geojson"
def test_tile_layer_shape():
layer = project.tile_layer("OSM", "https://t/{z}/{x}/{y}.png")
assert layer["type"] == "xyz"
assert layer["source"]["type"] == "raster"
assert layer["source"]["tiles"] == ["https://t/{z}/{x}/{y}.png"]
assert layer["source"]["tileSize"] == 256
assert layer["metadata"]["sourceKind"] == "xyz-url"
def test_cog_layer_restore_shape():
layer = project.cog_layer(
"DEM", "https://e/dem.tif", bands=[1, 2, 3], colormap="terrain"
)
assert layer["type"] == "cog"
assert layer["source"] == {"type": "raster", "url": "https://e/dem.tif"}
md = layer["metadata"]
assert md["sourceKind"] == "maplibre-gl-raster"
assert md["rasterSource"] == "url"
assert md["externalNativeLayer"] is True
assert md["nativeLayerIds"] == [layer["id"]]
assert md["rasterState"]["bands"] == [1, 2, 3]
assert md["rasterState"]["mode"] == "rgb"
assert md["rasterState"]["colormap"] == "terrain"
def test_wms_layer_shape_and_url():
layer = project.wms_layer(
"NAIP",
"https://example.com/wms",
"layer:a,layer:b",
styles="",
image_format="image/png",
)
assert layer["type"] == "wms"
src = layer["source"]
assert src["type"] == "raster"
assert src["tileSize"] == 256
assert src["url"] == "https://example.com/wms"
assert src["layers"] == "layer:a,layer:b"
assert layer["metadata"]["service"] == "wms"
tile = src["tiles"][0]
# The bbox placeholder is preserved verbatim; other values are encoded.
assert "BBOX={bbox-epsg-3857}" in tile
assert "SERVICE=WMS" in tile
assert "REQUEST=GetMap" in tile
assert "LAYERS=layer%3Aa%2Clayer%3Ab" in tile
assert "SRS=EPSG%3A3857" in tile
assert "WIDTH=256" in tile
def test_wms_layer_version_1_3_0_uses_crs():
# A 1.3.0-only server (e.g. the IGN Géoplateforme raster endpoint) rejects
# a 1.1.1 GetMap, so the version must be honored and SRS renamed to CRS.
layer = project.wms_layer("x", "https://e/wms", "a", version="1.3.0")
tile = layer["source"]["tiles"][0]
assert "VERSION=1.3.0" in tile
assert "CRS=EPSG%3A3857" in tile
assert "SRS=" not in tile
assert "BBOX={bbox-epsg-3857}" in tile
assert layer["source"]["version"] == "1.3.0"
def test_wms_layer_version_defaults_to_1_1_1():
layer = project.wms_layer("x", "https://e/wms", "a")
tile = layer["source"]["tiles"][0]
assert "VERSION=1.1.1" in tile
assert "SRS=EPSG%3A3857" in tile
assert "CRS=" not in tile
assert layer["source"]["version"] == "1.1.1"
# An unrecognized version falls back rather than emitting a bad request.
assert project.wms_layer("x", "https://e/wms", "a", version="2.0")[
"source"
]["version"] == "1.1.1"
# A None from an untyped caller must not raise.
assert project.wms_layer("x", "https://e/wms", "a", version=None)[
"source"
]["version"] == "1.1.1"
def test_wms_layer_transparent_false():
layer = project.wms_layer(
"x", "https://e/wms", "a", transparent=False, tile_size=512
)
tile = layer["source"]["tiles"][0]
assert "TRANSPARENT=FALSE" in tile
assert "WIDTH=512" in tile
assert layer["source"]["tileSize"] == 512
def test_append_query_keeps_fragment_after_query():
# A "#fragment" must stay at the end so the query is not pushed past it.
layer = project.wms_layer("x", "https://e/ows#v2", "a")
tile = layer["source"]["tiles"][0]
assert tile.startswith("https://e/ows?SERVICE=WMS")
assert tile.endswith("#v2")
assert tile.index("SERVICE=WMS") < tile.index("#v2")
def test_vector_tiles_layer_warns_on_both_source_layer_args():
with pytest.warns(UserWarning, match="source_layer is ignored"):
project.vector_tiles_layer(
"VT", "https://e/t.json", source_layers=["a"], source_layer="b"
)
def test_wmts_layer_shape():
layer = project.wmts_layer("W", "https://t/{z}/{y}/{x}.png")
assert layer["type"] == "wmts"
assert layer["source"]["tiles"] == ["https://t/{z}/{y}/{x}.png"]
assert layer["source"]["type"] == "raster"
assert layer["metadata"]["service"] == "wmts"
def test_wfs_getfeature_url_v2():
url = project.wfs_getfeature_url(
"https://e/wfs", "topp:states", version="2.0.0", max_features=10
)
assert "typeNames=topp%3Astates" in url
assert "count=10" in url
assert "service=WFS" in url
assert "outputFormat=application%2Fjson" in url
def test_wfs_getfeature_url_v1_uses_legacy_params():
url = project.wfs_getfeature_url(
"https://e/wfs?token=1", "ns:type", version="1.1.0", max_features=5
)
assert "typeName=ns%3Atype" in url
assert "maxFeatures=5" in url
# An existing query string is respected with an "&" separator.
assert "wfs?token=1&" in url
def test_vector_layer_geojson_mode():
layer = project.vector_layer("V", "https://e/data.parquet", data_format="parquet")
assert layer["type"] == "geojson"
assert layer["source"] == {"type": "geojson", "url": "https://e/data.parquet"}
md = layer["metadata"]
assert md["sourceKind"] == "maplibre-gl-vector"
assert md["externalNativeLayer"] is True
assert md["controlOwnsPaint"] is True
assert md["vectorSource"] == "url"
assert md["vectorState"] == {"renderMode": "geojson", "format": "parquet"}
assert layer["sourcePath"] == "https://e/data.parquet"
def test_vector_layer_tiles_mode():
layer = project.vector_layer("V", "https://e/data.fgb", render_mode="tiles")
assert layer["type"] == "vector-tiles"
assert layer["source"]["type"] == "vector"
assert layer["metadata"]["vectorState"]["renderMode"] == "tiles"
def test_vector_layer_invalid_render_mode():
with pytest.raises(ValueError, match="render_mode must be"):
project.vector_layer("V", "https://e/x", render_mode="bogus")
def test_vector_tiles_layer_shape():
layer = project.vector_tiles_layer(
"VT", "https://e/tiles.json", source_layers=["a", "b"]
)
assert layer["type"] == "vector-tiles"
assert layer["source"] == {
"type": "vector",
"url": "https://e/tiles.json",
"sourceLayers": ["a", "b"],
}
def test_pmtiles_layer_vector_shape():
layer = project.pmtiles_layer(
"P", "https://e/tiles.pmtiles", source_layers=["roads"]
)
assert layer["type"] == "pmtiles"
assert layer["source"]["type"] == "vector"
assert layer["source"]["tileType"] == "vector"
assert layer["source"]["sourceId"] == layer["id"]
md = layer["metadata"]
assert md["sourceKind"] == "pmtiles-url"
assert md["externalNativeLayer"] is True
assert md["sourceLayers"] == ["roads"]
# nativeLayerIds must be non-empty or isExternalNativeLayer() skips render.
assert md["nativeLayerIds"] == [layer["id"]]
def test_pmtiles_layer_raster_shape():
layer = project.pmtiles_layer("P", "https://e/r.pmtiles", tile_type="raster")
assert layer["source"]["type"] == "raster"
assert layer["metadata"]["tileType"] == "raster"
# Raster placeholder matches ensurePMTilesExternalLayer's computed fallback.
assert layer["metadata"]["nativeLayerIds"] == [f"{layer['id']}-raster"]
def test_pmtiles_layer_invalid_tile_type():
with pytest.raises(ValueError, match="tile_type must be"):
project.pmtiles_layer("P", "https://e/x.pmtiles", tile_type="bogus")
def test_three_d_tiles_layer_shape():
layer = project.three_d_tiles_layer(
"T", "https://e/tileset.json", altitude_offset=12, request_headers={"k": "v"}
)
assert layer["type"] == "3d-tiles"
assert layer["source"]["url"] == "https://e/tileset.json"
assert layer["source"]["altitudeOffset"] == 12
assert layer["source"]["requestHeaders"] == {"k": "v"}
md = layer["metadata"]
assert md["sourceKind"] == "3d-tiles-url"
assert md["externalNativeLayer"] is True
assert md["customLayerType"] == "3d-tiles"
assert md["nativeLayerIds"] == [layer["id"]]
def test_three_d_tiles_layer_omits_empty_headers():
layer = project.three_d_tiles_layer("T", "https://e/tileset.json")
assert "requestHeaders" not in layer["source"]
def test_video_layer_shape():
corners = [[-122, 38], [-121, 38], [-121, 37], [-122, 37]]
layer = project.video_layer("Vid", ["https://e/a.mp4", "https://e/a.webm"], corners)
assert layer["type"] == "video"
assert layer["source"]["type"] == "video"
assert layer["source"]["urls"] == ["https://e/a.mp4", "https://e/a.webm"]
assert layer["source"]["coordinates"] == corners
assert layer["sourcePath"] == "https://e/a.mp4"
assert layer["metadata"]["sourceKind"] == "video-url"
# bounds is [west, south, east, north] of the four corners.
assert layer["metadata"]["bounds"] == [-122, 37, -121, 38]
def test_video_layer_requires_url():
with pytest.raises(ValueError, match="non-empty URL"):
project.video_layer("Vid", [], [[0, 0], [1, 0], [1, 1], [0, 1]])
def test_video_layer_requires_four_corners():
with pytest.raises(ValueError, match=r"four \[lng, lat\] corners"):
project.video_layer("Vid", ["https://e/a.mp4"], [[0, 0], [1, 1]])
def test_video_layer_rejects_non_https():
with pytest.raises(ValueError, match="https://"):
project.video_layer("Vid", ["http://e/a.mp4"], [[0, 0], [1, 0], [1, 1], [0, 1]])
def test_video_layer_rejects_non_string_url():
with pytest.raises(ValueError, match="non-empty string"):
project.video_layer(
"Vid", ["https://e/a.mp4", None], [[0, 0], [1, 0], [1, 1], [0, 1]]
)
def test_load_featurecollection_passthrough():
assert project.load_featurecollection(POINT_FC) is POINT_FC
def test_load_featurecollection_wraps_feature():
feature = POINT_FC["features"][0]
fc = project.load_featurecollection(feature)
assert fc["type"] == "FeatureCollection"
assert fc["features"] == [feature]
def test_load_featurecollection_wraps_geometry():
fc = project.load_featurecollection({"type": "Point", "coordinates": [1, 2]})
assert fc["features"][0]["geometry"]["coordinates"] == [1, 2]
def test_load_featurecollection_from_json_string():
fc = project.load_featurecollection(json.dumps(POINT_FC))
assert fc["features"][0]["properties"]["name"] == "A"
def test_load_featurecollection_from_file(tmp_path):
path = tmp_path / "pts.geojson"
path.write_text(json.dumps(POINT_FC), encoding="utf-8")
fc = project.load_featurecollection(str(path))
assert fc == POINT_FC
def test_load_featurecollection_geo_interface():
class Fake:
__geo_interface__ = POINT_FC
assert project.load_featurecollection(Fake()) == POINT_FC
def test_load_featurecollection_invalid():
with pytest.raises(ValueError):
project.load_featurecollection(42)