Skip to content

Commit 10bb126

Browse files
committed
Add tool for finding lifecycle updates
Adds a tool for finding buildings that are planned or under construction in OSM, but are finished in the cadastral data. Since IG doesn't mean that construction has actually started, planned and construction are treated as the same thing.
1 parent 9ca2792 commit 10bb126

File tree

5 files changed

+212
-29
lines changed

5 files changed

+212
-29
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
FLAKE8_FILES := \
55
filter_buildings.py \
6+
find_lifecycle_updates.py \
7+
shared.py \
68
tests/test_filter.py \
9+
tests/test_find_lifecycle_updates.py \
710
;
811

912

filter_buildings.py

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,7 @@
22
import json
33
import sys
44

5-
import requests
6-
7-
8-
def parse_ref(raw_ref):
9-
return {int(ref) for ref in raw_ref.split(';') if ref}
10-
11-
12-
def run_overpass_query(query):
13-
overpass_url = "https://overpass-api.de/api/interpreter"
14-
params = {'data': query}
15-
version = '0.8.0'
16-
headers = {'User-Agent': 'building2osm/' + version}
17-
request = requests.get(overpass_url,
18-
params=params,
19-
headers=headers)
20-
return request.text
21-
22-
23-
def load_osm_data(municipality_id):
24-
query_fmt = '''[out:json][timeout:60];
25-
(area[ref={}][admin_level=7][place=municipality];)->.county;
26-
nwr["ref:bygningsnr"](area.county);
27-
out tags noids;
28-
'''
29-
query = query_fmt.format(municipality_id)
30-
return run_overpass_query(query)
5+
import shared
316

327

338
def load_osm_refs(osm_raw):
@@ -36,7 +11,7 @@ def load_osm_refs(osm_raw):
3611
osm_refs = set()
3712
for element in elements:
3813
raw_ref = element['tags']['ref:bygningsnr']
39-
osm_refs |= parse_ref(raw_ref)
14+
osm_refs |= shared.parse_ref(raw_ref)
4015

4116
return osm_refs
4217

@@ -47,7 +22,7 @@ def filter_buildings(cadastral_raw, osm_raw):
4722

4823
def in_osm(building):
4924
raw_ref = building['properties']['ref:bygningsnr']
50-
building_refs = parse_ref(raw_ref)
25+
building_refs = shared.parse_ref(raw_ref)
5126
return bool(building_refs & osm_refs)
5227

5328
missing_in_osm = [b for b in cadastral_buildings if not in_osm(b)]
@@ -72,7 +47,7 @@ def main():
7247
cadastral_raw = file.read()
7348

7449
print('Running Overpass query')
75-
osm_raw = load_osm_data(args.municipality)
50+
osm_raw = shared.load_building_tags(args.municipality)
7651

7752
print('Filtering buildings')
7853
output = filter_buildings(cadastral_raw, osm_raw)

find_lifecycle_updates.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import argparse
2+
import json
3+
import re
4+
import sys
5+
6+
import shared
7+
8+
9+
def osm_buildings_by_ref(osm_buildings):
10+
by_ref = {}
11+
for osm_building in osm_buildings:
12+
tags = osm_building['tags']
13+
raw_ref = tags['ref:bygningsnr']
14+
for osm_ref in shared.parse_ref(raw_ref):
15+
try:
16+
by_ref[osm_ref].append(osm_building)
17+
except KeyError:
18+
by_ref[osm_ref] = [osm_building]
19+
20+
return by_ref
21+
22+
23+
def cadastral_construction_finished(building):
24+
tags = building['properties']
25+
if 'STATUS' not in tags:
26+
raise RuntimeError
27+
28+
if re.match('#(RA|IG) .*', tags['STATUS']):
29+
return False
30+
31+
return True
32+
33+
34+
def osm_construction_finished(building):
35+
tags = building['tags']
36+
if 'planned:building' in tags:
37+
return False
38+
elif 'building' in tags and tags['building'] == 'construction':
39+
return False
40+
else:
41+
return True
42+
43+
44+
def has_lifecycle_update(cadastral_building, osm_buildings):
45+
for osm_building in osm_buildings:
46+
cadastral_done = cadastral_construction_finished(cadastral_building)
47+
osm_done = osm_construction_finished(osm_building)
48+
49+
if cadastral_done and not osm_done:
50+
return True
51+
52+
return False
53+
54+
55+
def find_lifecycle_updates(cadastral_raw, osm_raw):
56+
cadastral_buildings = json.loads(cadastral_raw)['features']
57+
58+
osm_buildings = json.loads(osm_raw)['elements']
59+
osm_by_ref = osm_buildings_by_ref(osm_buildings)
60+
61+
updated = []
62+
for cadastral_building in cadastral_buildings:
63+
cadastral_ref = int(cadastral_building['properties']['ref:bygningsnr'])
64+
try:
65+
osm_buildings = osm_by_ref[cadastral_ref]
66+
except KeyError:
67+
# Building is missing from OSM
68+
continue
69+
70+
if has_lifecycle_update(cadastral_building, osm_buildings):
71+
updated.append(cadastral_building)
72+
continue
73+
74+
geojson = {
75+
'type': 'FeatureCollection',
76+
'generator': 'find_lifecycle_updates.py',
77+
'features': updated,
78+
}
79+
return json.dumps(geojson)
80+
81+
82+
def main():
83+
parser = argparse.ArgumentParser()
84+
parser.add_argument('--input', required=True)
85+
parser.add_argument('--output', required=True)
86+
parser.add_argument('--municipality', required=True)
87+
args = parser.parse_args()
88+
89+
print('Reading cadastral file')
90+
with open(args.input, 'r', encoding='utf-8') as file:
91+
cadastral_raw = file.read()
92+
93+
print('Running Overpass query')
94+
osm_raw = shared.load_building_tags(args.municipality)
95+
96+
print('Filtering buildings')
97+
output = find_lifecycle_updates(cadastral_raw, osm_raw)
98+
with open(args.output, 'w', encoding='utf-8') as file:
99+
file.write(output)
100+
101+
return 0
102+
103+
104+
if __name__ == '__main__':
105+
sys.exit(main())

shared.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import requests
2+
3+
4+
def parse_ref(raw_ref):
5+
return {int(ref) for ref in raw_ref.split(';') if ref}
6+
7+
8+
def run_overpass_query(query):
9+
overpass_url = "https://overpass-api.de/api/interpreter"
10+
params = {'data': query}
11+
version = '0.8.0'
12+
headers = {'User-Agent': 'building2osm/' + version}
13+
request = requests.get(overpass_url,
14+
params=params,
15+
headers=headers)
16+
request.raise_for_status()
17+
return request.text
18+
19+
20+
def load_building_tags(municipality_id):
21+
query = f'''[out:json][timeout:60];
22+
(area[ref={municipality_id}]
23+
[admin_level=7]
24+
[place=municipality];
25+
) -> .county;
26+
nwr["ref:bygningsnr"](area.county);
27+
out tags noids;
28+
'''
29+
return run_overpass_query(query)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import json
2+
import unittest
3+
4+
import find_lifecycle_updates
5+
6+
7+
def cadastral(ref, status):
8+
if status == 'MB':
9+
status = '#MB Midlertidig brukstillatelse'
10+
elif status == 'IG':
11+
status = '#IG Igangsettingstillatelse'
12+
else:
13+
raise RuntimeError
14+
15+
return {
16+
'properties': {
17+
'ref:bygningsnr': str(ref),
18+
'STATUS': status,
19+
},
20+
}
21+
22+
23+
def osm(ref, planned=False, construction=False):
24+
tags = {
25+
'ref:bygningsnr': str(ref),
26+
}
27+
28+
if planned:
29+
tags['planned:building'] = 'yes'
30+
elif construction:
31+
tags['building'] = 'construction'
32+
33+
return {'tags': tags}
34+
35+
36+
class TestFindLifecycleUpdate(unittest.TestCase):
37+
def _run_filter(self, cadastral_buildings, osm_buildings):
38+
cadastral_raw = json.dumps({'features': cadastral_buildings})
39+
osm_raw = json.dumps({'elements': osm_buildings})
40+
raw_output = find_lifecycle_updates.find_lifecycle_updates(
41+
cadastral_raw,
42+
osm_raw)
43+
output = json.loads(raw_output)
44+
self.assertEqual('FeatureCollection', output['type'])
45+
self.assertEqual('find_lifecycle_updates.py', output['generator'])
46+
return output['features']
47+
48+
def test_provisional_use_permit_is_update_from_planned(self):
49+
cadastral_buildings = [cadastral(1, status='MB')]
50+
osm_buildings = [osm(1, planned=True)]
51+
output = self._run_filter(cadastral_buildings, osm_buildings)
52+
self.assertEqual(cadastral_buildings, output)
53+
54+
def test_provisional_use_permit_is_update_from_construction(self):
55+
cadastral_buildings = [cadastral(1, status='MB')]
56+
osm_buildings = [osm(1, construction=True)]
57+
output = self._run_filter(cadastral_buildings, osm_buildings)
58+
self.assertEqual(cadastral_buildings, output)
59+
60+
def test_dont_include_construction_permit_when_osm_has_planned(self):
61+
# IG doesn't imply that construction has actually started, so planned
62+
# might still be the correct OSM tagging
63+
cadastral_buildings = [cadastral(1, status='IG')]
64+
osm_buildings = [osm(1, planned=True)]
65+
output = self._run_filter(cadastral_buildings, osm_buildings)
66+
self.assertEqual([], output)
67+
68+
def test_ignore_building_missing_from_osm(self):
69+
cadastral_buildings = [cadastral(1, status='MB')]
70+
output = self._run_filter(cadastral_buildings, [])
71+
self.assertEqual([], output)

0 commit comments

Comments
 (0)