feat(geo): cross-check block heights against Mapbox building data (#25)

Merged PR #25: Mapbox building-height cross-check

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
This commit was merged in pull request #25.
This commit is contained in:
zwitschi authored and zwitschi committed 2026-11-08 16:31:39 +01:00
1 parent 68d8745534
commit 81f7cd3991
10 files changed
+476 -3

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
"""Tests for Geo/tools/mapbox_buildings.py — the pure merge logic.
No network calls here: Tilequery responses are represented as the small dicts
the API returns, and only the selection/comparison/formatting helpers are
exercised. The live call is verified by running the tool.
"""
import json
import os
import sys
import unittest
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
sys.path.insert(0, os.path.join(ROOT, "Geo", "tools"))
import mapbox_buildings as mb # noqa: E402
def feature(distance, height=None, min_height=0, type_="building", layer="building"):
props = {"tilequery": {"distance": distance, "layer": layer}}
if height is not None:
props["height"] = height
props["min_height"] = min_height
props["type"] = type_
return {"properties": props}
class NearestBuildingTest(unittest.TestCase):
def test_picks_the_closest_building(self):
payload = {"features": [feature(6.0, 10.0), feature(1.0, 20.0)]}
best = mb.nearest_building(payload, max_distance=8.0)
self.assertEqual(best["height"], 20.0)
def test_ignores_hits_beyond_max_distance(self):
payload = {"features": [feature(20.0, 10.0)]}
self.assertIsNone(mb.nearest_building(payload, max_distance=8.0))
def test_ignores_other_layers(self):
payload = {"features": [feature(0.0, 10.0, layer="road")]}
self.assertIsNone(mb.nearest_building(payload, max_distance=8.0))
def test_no_features(self):
self.assertIsNone(mb.nearest_building({"features": []}, max_distance=8.0))
class CompareTest(unittest.TestCase):
def test_match_within_half_a_metre(self):
self.assertEqual(mb.compare(14.5, "height tag", 14.2)[0], "match")
def test_close_within_two_metres(self):
verdict, delta = mb.compare(38.4, "levels x 3.2 m", 37.2)
self.assertEqual(verdict, "close")
self.assertAlmostEqual(delta, -1.2)
def test_disagree_beyond_two_metres(self):
verdict, delta = mb.compare(6.4, "GUESS: default 2 levels x 3.2 m", 3.0)
self.assertEqual(verdict, "disagree")
self.assertAlmostEqual(delta, -3.4)
def test_mapbox_only_when_we_have_nothing(self):
self.assertEqual(mb.compare(0.0, "height tag", 12.0)[0], "mapbox-only")
def test_ours_only_when_mapbox_returns_nothing(self):
self.assertEqual(mb.compare(12.0, "height tag", None)[0], "ours-only")
def test_non_building_is_excluded_from_comparison(self):
verdict, delta = mb.compare(0.0, "not a building volume (roof)", 3.0)
self.assertEqual(verdict, "non-building")
self.assertIsNone(delta)
def test_canopy_is_non_building(self):
verdict, _ = mb.compare(0.0, "not a building volume (canopy)", 3.0)
self.assertEqual(verdict, "non-building")
class CentroidTest(unittest.TestCase):
def test_centroid_of_a_square(self):
lat, lon = mb.centroid_lat_lon(
[(34.0, -118.0), (34.0, -117.0), (35.0, -117.0), (35.0, -118.0)])
self.assertAlmostEqual(lat, 34.5)
self.assertAlmostEqual(lon, -117.5)
class RenderTest(unittest.TestCase):
ARGS = type("A", (), {"radius": 25, "max_distance": 8.0, "only_diffs": False})()
def test_disagreements_sort_first(self):
rows = [
{"verdict": "match", "delta_m": 0.1, "way": 1, "name": "", "kind": "yes",
"osm_height_m": 10.0, "osm_source": "height tag", "mapbox_height_m": 10.1,
"mapbox_min_height_m": 0, "mapbox_type": "building",
"mapbox_distance_m": 0.0, "cached": True, "lat": 0, "lon": 0},
{"verdict": "disagree", "delta_m": -3.4, "way": 2, "name": "", "kind": "yes",
"osm_height_m": 6.4, "osm_source": "GUESS", "mapbox_height_m": 3.0,
"mapbox_min_height_m": 0, "mapbox_type": "building",
"mapbox_distance_m": 0.0, "cached": True, "lat": 0, "lon": 0},
]
text = mb.render_markdown(rows, self.ARGS)
self.assertLess(text.index("way 2"), text.index("way 1"))
def test_never_contains_a_token(self):
rows = []
text = mb.render_markdown(rows, self.ARGS)
self.assertNotIn("access_token", text)
self.assertNotIn("sk.", text)
class TokenTest(unittest.TestCase):
def test_explicit_token_wins(self):
self.assertEqual(mb.load_token("abc123"), "abc123")
def test_token_is_stripped(self):
self.assertEqual(mb.load_token(" abc123\n"), "abc123")
if __name__ == "__main__":
unittest.main()