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
351 lines
12 KiB
Python
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)
|