Skip to content

Commit b18c33c

Browse files
committed
Add tool for finding removed buildings
Adds a tool for finding OSM buildings that have been removed from the cadastral registry. From testing in Lillestrøm this is unfortunately not a reliable indicator of the building actually having been removed, so the output format is intentionally not suitable for automatic uploading.
1 parent 10bb126 commit b18c33c

File tree

4 files changed

+174
-2
lines changed

4 files changed

+174
-2
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
FLAKE8_FILES := \
55
filter_buildings.py \
66
find_lifecycle_updates.py \
7+
find_removed.py \
78
shared.py \
89
tests/test_filter.py \
910
tests/test_find_lifecycle_updates.py \
11+
tests/test_find_removed.py \
1012
;
1113

1214

find_removed.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import argparse
2+
import json
3+
4+
import shared
5+
6+
7+
def collect_refs(buildings):
8+
refs = set()
9+
10+
for building in buildings:
11+
try:
12+
tags = building['tags']
13+
except KeyError:
14+
tags = building['properties']
15+
16+
raw_ref = tags['ref:bygningsnr']
17+
for ref in shared.parse_ref(raw_ref):
18+
refs.add(ref)
19+
20+
return refs
21+
22+
23+
def to_output(building):
24+
if building['type'] == 'node':
25+
lon = building['lon']
26+
lat = building['lat']
27+
else:
28+
lon = building['center']['lon']
29+
lat = building['center']['lat']
30+
31+
return {
32+
'type': 'Feature',
33+
'geometry': {
34+
'type': 'Point',
35+
'coordinates': [
36+
lon,
37+
lat,
38+
]
39+
},
40+
'properties': building['tags'],
41+
}
42+
43+
44+
def find_removed(cadastral_raw, osm_raw):
45+
cadastral_buildings = json.loads(cadastral_raw)['features']
46+
osm_buildings = json.loads(osm_raw)['elements']
47+
48+
cadastral_refs = collect_refs(cadastral_buildings)
49+
osm_refs = collect_refs(osm_buildings)
50+
51+
removed_buildings = []
52+
for ref in osm_refs - cadastral_refs:
53+
for osm_building in osm_buildings:
54+
if ref in collect_refs([osm_building]):
55+
try:
56+
removed_buildings.append(to_output(osm_building))
57+
except Exception:
58+
print(osm_building)
59+
raise
60+
61+
return json.dumps({
62+
'type': 'FeatureCollection',
63+
'generator': 'find_removed.py',
64+
'features': removed_buildings,
65+
})
66+
67+
68+
def main():
69+
parser = argparse.ArgumentParser()
70+
parser.add_argument('--input', required=True)
71+
parser.add_argument('--output', required=True)
72+
parser.add_argument('--municipality', required=True)
73+
args = parser.parse_args()
74+
75+
print('Reading cadastral file')
76+
with open(args.input, 'r', encoding='utf-8') as file:
77+
cadastral_raw = file.read()
78+
79+
print('Running Overpass query')
80+
osm_raw = shared.load_building_tags(args.municipality,
81+
with_position=True)
82+
83+
print('Comparing data')
84+
output = find_removed(cadastral_raw, osm_raw)
85+
with open(args.output, 'w', encoding='utf-8') as file:
86+
file.write(output)
87+
88+
89+
if __name__ == '__main__':
90+
main()

shared.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ def run_overpass_query(query):
1717
return request.text
1818

1919

20-
def load_building_tags(municipality_id):
20+
def load_building_tags(municipality_id, with_position=False):
21+
center = 'center' if with_position else ''
2122
query = f'''[out:json][timeout:60];
2223
(area[ref={municipality_id}]
2324
[admin_level=7]
2425
[place=municipality];
2526
) -> .county;
2627
nwr["ref:bygningsnr"](area.county);
27-
out tags noids;
28+
out tags noids {center};
2829
'''
2930
return run_overpass_query(query)

tests/test_find_removed.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import json
2+
import unittest
3+
4+
import find_removed
5+
6+
7+
expected_output_point = {
8+
'type': 'Feature',
9+
'geometry': {
10+
'type': 'Point',
11+
'coordinates': [
12+
11.0,
13+
59.0,
14+
]
15+
},
16+
'properties': {
17+
'ref:bygningsnr': '1',
18+
'building': 'yes',
19+
}
20+
}
21+
22+
23+
def cadastral(ref):
24+
return {'properties': {'ref:bygningsnr': str(ref)}}
25+
26+
27+
def osm_node(ref):
28+
return {
29+
'type': 'node',
30+
'lat': 59.0,
31+
'lon': 11.0,
32+
'tags': {
33+
'building': 'yes',
34+
'ref:bygningsnr': str(ref),
35+
}
36+
}
37+
38+
39+
def osm_way(ref):
40+
return {
41+
'type': 'way',
42+
'center': {
43+
'lat': 59.0,
44+
'lon': 11.0,
45+
},
46+
'tags': {
47+
'building': 'yes',
48+
'ref:bygningsnr': str(ref),
49+
}
50+
}
51+
52+
53+
class TestFindRemoved(unittest.TestCase):
54+
def _find_removed(self, cadastral_buildings, osm_buildings):
55+
cadastral_raw = json.dumps({'features': cadastral_buildings})
56+
osm_raw = json.dumps({'elements': osm_buildings})
57+
58+
raw_output = find_removed.find_removed(cadastral_raw, osm_raw)
59+
output = json.loads(raw_output)
60+
self.assertEqual('FeatureCollection', output['type'])
61+
self.assertEqual('find_removed.py', output['generator'])
62+
self.assertEqual(list, type(output['features']))
63+
return output['features']
64+
65+
def test_ignore_building_still_in_cadastral_data(self):
66+
removed = self._find_removed([cadastral(1)], [osm_node(1)])
67+
self.assertEqual([], removed)
68+
69+
def test_ignore_building_missing_from_osm(self):
70+
removed = self._find_removed([cadastral(1)], [])
71+
self.assertEqual([], removed)
72+
73+
def test_output_removed_building_node(self):
74+
removed = self._find_removed([], [osm_node(1)])
75+
self.assertEqual([expected_output_point], removed)
76+
77+
def test_output_removed_building_way(self):
78+
removed = self._find_removed([], [osm_way(1)])
79+
self.assertEqual([expected_output_point], removed)

0 commit comments

Comments
 (0)