-
Notifications
You must be signed in to change notification settings - Fork 1
/
highway_merge.py
1829 lines (1349 loc) · 60.9 KB
/
highway_merge.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
# -*- coding: utf8
# highway_merge.py
# Replace OSM highways or tags with NVDB
# Usage: python highway_merge.py [command] [input_osm.osm] [input_nvdb.osm]
# or: python highway_merge.py [command] [municipality name]
# Commands: - replace: Merge all existing OSM highways with NVDB
# - offset: Include all NVDB highways above an certain average offset
# - new: Include only NVDB highways not found in OSM
# - tagref/taglocal: Update OSM highways with attributes from NVDB (maxspeed, name etc)
# Resulting file will be written to a new version of input file
import sys
import time
import datetime
import math
import copy
import json
import os.path
import urllib.request, urllib.parse, urllib.error
from xml.etree import ElementTree
version = "3.1.0"
request_header = {"User-Agent": "osmno/highway_merge/" + version}
overpass_api = "https://overpass-api.de/api/interpreter" # Overpass endpoint
import_folder = "~/Jottacloud/osm/nvdb/" # Folder containing import highway files (default folder tried first)
nvdb_sweden_site = "https://nvdb-osm-map-data.s3.amazonaws.com/osm/" # All Sweden .osm NVDB files
progress_filename = "progress/nvdb_tagging_progress.json"
country = "Norway" # Argument "-swe" for Sweden
# Paramters for matching
debug = False # True will provide extra keys in output file
debug_gap = False # True will show gap/distance testing in output file
merge_all = False # True will delete excess way from OSM if its NVDB match is already merged
save_progress = False # Save progress report to file in the cloud
margin = 15 # Meters of tolarance for matching nodes
margin_new = 8 # Meters of tolerance for matching nodes, for "new" command
margin_tag = 5 # Meters of tolerance for matching nodes, for "tagref"/"taglocal" command
margin_offset = 5 # Minimum average distance in meters for matching ways (used with "offset" command to filter large offsets)
match_factor = 0.3 # Minimum percent of length of way matched
new_factor = 0.6 # Ditto for "new" command
min_nodes = 2 # Min number of nodes in a way to be matched
# The following lists are for the replace command:
# Do not merge OSM ways with the folowing highway categories
avoid_highway = ["path", "bus_stop", "rest_area", "platform", "construction", "proposed"] # Platform ok for "new" function
# Do not merge OSM ways with the following keys
avoid_tags = ["area", "railway", "piste:type", "snowmobile", "turn:lanes", "turn:lanes:forward", "turn:lanes:backward",
"destination", "destination:forward", "destination:backward", "destination:ref", "destination:ref:forward", "destination:ref:backward",
"destination:symbol", "destination:symbol:forward", "destination:symbol:backward", "mtb:scale", "class:bicycle:mtb"]
# Overwrite with the following tags from NVDB when merging ways, including deletion if not present in NVDB (not used by "-tagref" function)
replace_tags = ["ref", "name", "maxspeed", "oneway", "junction", "foot", "bridge", "tunnel", "layer", "source"]
# Do not consider OSM highways of the following types when updating tags
avoid_highway_tags = ["cycleway", "footway", "steps"]
# Pedestrian highways which should not be mixed with other highway classes for cars
pedestrian_highway = ["footway", "cycleway"]
# Only consider the following highway categories when merging (leave empty [] to merge all)
replace_highway = []
#replace_highway = ["motorway", "trunk", "primary", "secondary", "motorway_link", "trunk_link", "primary_link", "secondary_link"]
#replace_highway = ["primary", "secondary", "primary_link", "secondary_link"]
# + state_highway below
# The folloing lists are for the tagref/taglocal commands:
# Include the following tags from NVDB when marking tags in OSM for consideration
core_tags = ["highway", "ref", "oneway", "lanes", "junction", "name", "maxspeed", "maxheight", "maxweight", "maxlength", "motorroad",
"motor_vehicle", "psv", "foot", "bicycle", "agriculatural", "hgv", "cycleway",
"bridge", "tunnel", "layer", "bridge:description", "tunnel:name", "tunnel:description", "turn"]
# Include the following suffixes to the core tags above when reporting tags to consider
tag_suffixes = ["", ":lanes", ":forward", ":backward", ":lanes:forward", ":lanes:backward", ":left", ":right", ":both"]
# Do not update the following tags for the "-tagref" function
avoid_update_tags = ["ref", "highway", "oneway", "lanes", "surface"]
# Store progress report for the following tags
progress_tags = ["highway", "junction", "name", "maxspeed", "maxweight", "maxlength", "motorroad",
"motor_vehicle", "psv", "foot", "bicycle", "agricultural", "hgv", "cycleway", "bridge", "tunnel", "layer"]
# The following tags will be deleted
delete_tags = ["int_ref", "nvdb:id", "nvdb:date", "attribution", "maxspeed:type", "postal_code"] # Also "source" handled in code
# The following tags will be deleted if the value is "no"
delete_negative_tags = ["sidewalk", "sidewalk:both", "sidewalk:left", "sidewalk:right",
"cycleway", "cycleway:both", "cycleway:right", "cycleway:left",
"oneway", "lit", "island:crossing", "tactile_paving", "lane_markings"]
# Public highways (national/county) which should not be mixed with other highway classes
state_highway = ["motorway", "trunk", "primary", "secondary", "motorway_link", "trunk_link", "primary_link", "secondary_link"] # + "tertiary" included for Sweden
# Municipality highways in OSM to be matched with residential/pedestrian/tertiary/unclassified in NVDB
municipality_highway = ["residential", "unclassified", "tertiary", "tertiary_link", "pedestrian", "busway"]
force_ref = True # True if ref in NVDB and OSM must match, for "-tagref" function
# Output message
def message (line):
sys.stdout.write (line)
sys.stdout.flush()
# Open file/api, try up to 6 times, each time with double sleep time
def open_url (url):
tries = 0
while tries < 6:
try:
return urllib.request.urlopen(url)
except urllib.error.HTTPError as e:
if e.code in [429, 503, 504]: # Too many requests, Service unavailable or Gateway timed out
if tries == 0:
message ("\n")
message ("\rRetry %i... " % (tries + 1))
time.sleep(5 * (2**tries))
tries += 1
error = e
elif e.code in [401, 403]:
message ("\nHTTP error %i: %s\n" % (e.code, e.reason)) # Unauthorized or Blocked
sys.exit()
elif e.code in [400, 409, 412]:
message ("\nHTTP error %i: %s\n" % (e.code, e.reason)) # Bad request, Conflict or Failed precondition
message ("%s\n" % str(e.read()))
sys.exit()
else:
raise
message ("\nHTTP error %i: %s\n" % (error.code, error.reason))
sys.exit()
# Compute approximation of distance between two coordinates, in meters
# Works for short distances
def distance(n1_lat, n1_lon, n2_lat, n2_lon):
lon1, lat1, lon2, lat2 = map(math.radians, [n1_lon, n1_lat, n2_lon, n2_lat])
x = (lon2 - lon1) * math.cos( 0.5*(lat2+lat1) )
y = lat2 - lat1
return 6371000 * math.sqrt( x*x + y*y )
# Compute closest distance from point p3 to line segment [s1, s2]
# Works for short distances
def line_distance(s1_lat, s1_lon, s2_lat, s2_lon, p3_lat, p3_lon):
x1, y1, x2, y2, x3, y3 = map(math.radians, [s1_lon, s1_lat, s2_lon, s2_lat, p3_lon, p3_lat])
# Simplified reprojection of latitude
x1 = x1 * math.cos( y1 )
x2 = x2 * math.cos( y2 )
x3 = x3 * math.cos( y3 )
A = x3 - x1
B = y3 - y1
dx = x2 - x1
dy = y2 - y1
dot = (x3 - x1)*dx + (y3 - y1)*dy
len_sq = dx*dx + dy*dy
if len_sq != 0: # in case of zero length line
param = dot / len_sq
else:
param = -1
if param < 0:
x4 = x1
y4 = y1
elif param > 1:
x4 = x2
y4 = y2
else:
x4 = x1 + param * dx
y4 = y1 + param * dy
# Also compute distance from p to segment
x = x4 - x3
y = y4 - y3
distance = 6371000 * math.sqrt( x*x + y*y ) # In meters
# Project back to longitude/latitude
x4 = x4 / math.cos(y4)
lon = math.degrees(x4)
lat = math.degrees(y4)
return (lat, lon, distance)
# Test if line segments s1 and s2 are crossing.
# Segments have two nodes [(start.x, start.y), (end.x, end.y)].
# Source: https://en.wikipedia.org/wiki/Line–line_intersection#Given_two_points_on_each_line_segment
def crossing_lines (s1, s2):
d1x = s1[1]['lon'] - s1[0]['lon'] # end1.x - start1.x
d1y = s1[1]['lat'] - s1[0]['lat'] # end1.y - start1.y
d2x = s2[1]['lon'] - s2[0]['lon'] # end2.x - start2.x
d2y = s2[1]['lat'] - s2[0]['lat'] # end2.y - start2.y
D = d1x * d2y - d1y * d2x
if abs(D) < 0.0000000001: # s1 and s2 are parallel
return False
A = s1[0]['lat'] - s2[0]['lat'] # start1.y - start2.y
B = s1[0]['lon'] - s2[0]['lon'] # start1.x - start2.x
r1 = (A * d2x - B * d2y) / D
r2 = (A * d1x - B * d1y) / D
if r1 < 0 or r1 > 1 or r2 < 0 or r2 > 1:
return False
'''
# Compute intersection point
x = s1[0]['lon'] + r1 * d1x
y = s1[0]['lon'] + r1 * d1y
intersection = (x, y)
return (intersection)
'''
return True
# Identify municipality name, unless more than one hit
# Returns municipality number, or input parameter if not found
def get_municipality (parameter):
if parameter.isdigit():
return parameter
else:
parameter = parameter
found_id = ""
duplicate = False
for mun_id, mun_name in iter(municipalities.items()):
if parameter.lower() == mun_name.lower():
return mun_id
elif parameter.lower() in mun_name.lower():
if found_id:
duplicate = True
else:
found_id = mun_id
if found_id and not duplicate:
return found_id
else:
return parameter
# Load dict of all municipalities
def load_municipalities(country):
if country == "Sweden":
url = "https://catalog.skl.se/rowstore/dataset/b80d412c-9a81-4de3-a62c-724192295677?_limit=400"
file = urllib.request.urlopen(url)
data = json.load(file)
file.close()
for municipality in data['results']:
municipalities[ municipality['kommunkod'] ] = municipality['kommun']
else: # Default Norway
url = "https://ws.geonorge.no/kommuneinfo/v1/fylkerkommuner?filtrer=fylkesnummer%2Cfylkesnavn%2Ckommuner.kommunenummer%2Ckommuner.kommunenavnNorsk"
file = urllib.request.urlopen(url)
data = json.load(file)
file.close()
for county in data:
for municipality in county['kommuner']:
municipalities[ municipality['kommunenummer'] ] = municipality['kommunenavnNorsk']
# Load OSM or NVDB xml data and build list and dicts for later processing
def load_xml(root, ways):
# Prepare nodes
count_nodes = 0
for node in root.iter("node"):
if not("action" in node.attrib and node.attrib['action'] == "delete"):
nodes[ node.attrib['id'] ] = {
'xml': node,
'used': 0, # Will have a value larger than zero at time of output to avoid deletion
'lat': float(node.attrib['lat']),
'lon': float(node.attrib['lon'])
}
# Remove node tags used by early editors
for tag in node.iter("tag"):
if tag.attrib['k'] == "created_by":
node.remove(tag)
node.set("action", "modify")
count_nodes += 1
# Determine bounding box and length of OSM ways
count_roads = 0
for way in root.iter("way"):
way_id = way.attrib['id']
length = 0
way_nodes = []
tags = {}
highway = None
ref = None
incomplete = False
avoid_match = False
min_lat = 0.0
min_lon = 0.0
max_lat = 0.0
max_lon = 0.0
# Iterate tags to determine if way should be excluded
for tag in way.iter("tag"):
key = tag.attrib['k']
if key in avoid_tags:
avoid_match = True
if key == "highway":
highway = tag.attrib['v']
if highway not in avoid_highway:
count_roads += 1
if key == "ref":
ref = tag.attrib['v']
tags[ key ] = tag.attrib['v']
# Iterate nodes to determine if way is complete
if ways == osm_ways:
for node in way.iter("nd"):
node_id = node.attrib['ref']
if node_id in nodes:
nodes[ node_id ]['used'] += 1
elif not("action" in node.attrib and node.attrib['action'] == "delete"):
incomplete = True
if "action" in way.attrib and way.attrib['action'] == "delete" or "area" in tags and tags['area'] == "yes":
incomplete = True
# Determine bounding box and length of way
if not incomplete:
node_tag = way.find("nd")
node_id = node_tag.attrib['ref']
min_lat = nodes[ node_id ]['lat']
min_lon = nodes[ node_id ]['lon']
max_lat = min_lat
max_lon = min_lon
prev_lat = min_lat
prev_lon = min_lon
for node in way.iter("nd"):
if not("action" in node.attrib and node.attrib['action'] == "delete"):
node_id = node.attrib['ref']
length += distance(prev_lat, prev_lon, nodes[node_id]['lat'], nodes[node_id]['lon'])
# Append node and update bbox
prev_lat = nodes[node_id]['lat']
prev_lon = nodes[node_id]['lon']
way_nodes.append(node_id)
min_lat = min(min_lat, prev_lat)
min_lon = min(min_lon, prev_lon)
max_lat = max(max_lat, prev_lat)
max_lon = max(max_lon, prev_lon)
# Note: Sinple reprojection of bounding box to meters
ways[ way_id ] = {
'xml': way,
'highway': highway,
'ref': ref,
'incomplete': incomplete,
'avoid_tag': avoid_match, # Not used for NVDB
'min_lat': min_lat - margin / 111500.0,
'max_lat': max_lat + margin / 111500.0,
'min_lon': min_lon - margin / (math.cos(math.radians(min_lat)) * 111320.0),
'max_lon': max_lon + margin / (math.cos(math.radians(max_lat)) * 111320.0),
'length': length,
'nodes': way_nodes,
'tags': tags,
'relations': set() # Not used for NVDB
}
# Determine which nodes and ways are used by relation (should be kept)
if ways == osm_ways:
for relation in root.iter("relation"):
for member in relation.iter("member"):
if member.attrib['type'] == "node" and member.attrib['ref'] in nodes:
nodes[ member.attrib['ref'] ]['used'] += 1
elif member.attrib['type'] == "way" and member.attrib['ref'] in ways:
ways[ member.attrib['ref'] ]['relations'].add( relation.attrib['id'] )
return count_roads
# Load files and build data structure for analysis
def load_files (name_osm):
global tree_osm, root_osm, tree_nvdb, root_nvdb, count_osm_roads, filename_osm, filename_nvdb
# Load OSM file
municipality_id = None
if ".osm" not in name_osm.lower():
municipality_id = get_municipality(name_osm)
if municipality_id in municipalities:
message ("Loading municipality %s %s from OSM ... " % (municipality_id, municipalities[municipality_id]))
filename_osm = "nvdb_%s_%s" % (municipality_id, municipalities[municipality_id].replace(" ", "_"))
if country == "Sweden":
filename_nvdb = municipalities[municipality_id] + ".osm"
query = '[timeout:90];(area["ref:scb"=%s][admin_level=7];)->.a;(nwr["highway"](area.a););(._;>;<;);out meta;' % municipality_id
else: # Norway
filename_nvdb = filename_osm + ".osm"
query = '[timeout:90];(area[ref=%s][admin_level=7][place=municipality];)->.a;(nwr["highway"](area.a););(._;>;<;);out meta;' % municipality_id
if command not in ["new", "offset"]:
query = query.replace('nwr["highway"](area.a);', 'nwr["highway"](area.a);nwr["railway"](area.a);nwr["man_made"="bridge"](area.a);')
filename_osm += ".osm"
request = urllib.request.Request(overpass_api + "?data=" + urllib.parse.quote(query), headers=request_header)
file = open_url(request)
data = file.read()
file.close()
root_osm = ElementTree.fromstring(data)
tree_osm = ElementTree.ElementTree(root_osm)
else:
sys.exit("\n*** Municipality '%s' not found\n\n" % name_osm)
else:
message ("Loading file '%s' ... " % name_osm)
if os.path.isfile(name_osm):
tree_osm = ElementTree.parse(name_osm)
root_osm = tree_osm.getroot()
else:
sys.exit("\n*** File '%s' not found\n\n" % name_osm)
count_osm_roads = load_xml(root_osm, osm_ways)
message ("%i highways loaded\n" % count_osm_roads)
# Load NVDB file
if country == "Sweden":
request = urllib.request.Request(nvdb_sweden_site + urllib.parse.quote(filename_nvdb), headers=request_header)
try:
file = urllib.request.urlopen(request)
except urllib.error.HTTPError:
sys.exit("\n*** File '%s' not available\n\n" % (nvdb_sweden_site + filename_nvdb))
message ("Loading file '%s' ... " % filename_nvdb)
data = file.read()
tree_nvdb = ElementTree.ElementTree(ElementTree.fromstring(data))
else: # Norway
full_filename_nvdb = filename_nvdb
if not os.path.isfile(full_filename_nvdb):
test_filename = os.path.expanduser(import_folder + filename_nvdb)
if os.path.isfile(test_filename):
full_filename_nvdb = test_filename
else:
sys.exit("\n*** File '%s' not found\n\n" % filename_nvdb)
message ("Loading file '%s' ... " % full_filename_nvdb)
tree_nvdb = ElementTree.parse(full_filename_nvdb)
root_nvdb = tree_nvdb.getroot()
count_nvdb_roads = load_xml(root_nvdb, nvdb_ways)
message ("%i highways loaded\n" % count_nvdb_roads)
return municipality_id
# Compute length of way in metres.
def way_length(way_nodes):
way_distance = 0.0
if len(way_nodes) > 1:
prev_node = None
for node in way_nodes:
if prev_node:
way_distance += distance(nodes[prev_node]['lat'], nodes[prev_node]['lon'], \
nodes[node]['lat'], nodes[node]['lon'])
prev_node = node
return way_distance
# Compute length of way part in metres.
# Only compute distance between nodes given by index.
def partial_way_length(way_nodes, match_index):
match_nodes = [way_nodes[node] for node in match_index]
way_distance = 0.0
if len(way_nodes) > 1 and len(match_nodes) > 1:
prev_node = None
for node in way_nodes:
if prev_node in match_nodes and node in match_nodes:
way_distance += distance(nodes[prev_node]['lat'], nodes[prev_node]['lon'], \
nodes[node]['lat'], nodes[node]['lon'])
prev_node = node
return way_distance
# Compare part of way 1 with way 2 to determine if they match.
# Include only segments of the ways which are closer than margin parameter.
# Return average distance of matched nodes + index of matched nodes.
def match_ways (way1, way2, start_node, end_node, margin, trim_end = False):
way_distance = 0.0
count_distance = 0
match_nodes = []
match_distances = []
# Iterate all nodes in way1 and identify distance from node to way2
for i, node1 in enumerate(way1['nodes'][ start_node : end_node + 1]):
min_node_distance = margin
prev_node2 = way2['nodes'][0]
for node2 in way2['nodes'][1:]:
line_lat, line_lon, node_distance = line_distance(nodes[prev_node2]['lat'], nodes[prev_node2]['lon'], \
nodes[node2]['lat'], nodes[node2]['lon'], \
nodes[node1]['lat'], nodes[node1]['lon'])
if node_distance < min_node_distance:
min_node_distance = node_distance
min_node_ref = node1
gap_test = {
'lat1': nodes[node1]['lat'],
'lon1': nodes[node1]['lon'],
'lat2': line_lat,
'lon2': line_lon,
'distance': node_distance
}
prev_node2 = node2
# Include node in matched nodes list if closer distance than margin
if min_node_distance < margin:
count_distance += 1
way_distance += min_node_distance
match_nodes.append(i + start_node)
match_distances.append(min_node_distance)
if debug_gap:
test_lines.append(gap_test)
# Remove runoff nodes at each end
if trim_end and count_distance > 0:
for backward in [False, True]:
test_nodes = match_nodes.copy()
test_distances = match_distances.copy()
if backward:
test_nodes.reverse()
test_distances.reverse()
end_length = 0
while test_distances[0] > 0.5 * margin and len(test_distances) > 1: # and test_distances[1] < test_distances[0]:
node = nodes[ way1['nodes'][ test_nodes[1] ]]
last_node = nodes[ way1['nodes'][ test_nodes[0] ]]
end_length += distance(last_node['lat'], last_node['lon'], node['lat'], node['lon'])
if end_length > margin:
break
test_distances.pop(0)
test_nodes.pop(0)
if backward:
test_nodes.reverse()
test_distances.reverse()
if test_nodes != match_nodes:
match_nodes = test_nodes
match_distances = test_distances
count_distance = len(match_nodes)
way_distance = sum(match_distances)
# Return average gap and matched nodes
if count_distance > 0:
average_distance = way_distance / count_distance
else:
average_distance = None
return (average_distance, match_nodes)
# Merge NVDB and OSM highways for the commands "replace", "offset" and "tag"
def merge_highways(command):
message ("Merge highways ...\n")
count = count_osm_roads
count_swap = 0
total_distance = 0
# Pass 1: Match topology
# Iterate OSM ways to identify best match with NVDB way
for osm_id, osm_way in iter(osm_ways.items()):
if not osm_way['incomplete'] and osm_way['highway'] != None and osm_way['highway'] not in avoid_highway:
message ("\r%i " % count)
count -= 1
best_id = None
best_distance = 99999.0
for nvdb_id, nvdb_way in iter(nvdb_ways.items()):
# Avoid ways with no overlapping bbox or with incompatible relative lengths
if nvdb_way['min_lat'] <= osm_way['max_lat'] and nvdb_way['max_lat'] >= osm_way['min_lat'] and \
nvdb_way['min_lon'] <= osm_way['max_lon'] and nvdb_way['max_lon'] >= osm_way['min_lon'] and \
osm_way['length'] > match_factor * nvdb_way['length'] and nvdb_way['length'] > match_factor * osm_way['length']:
# Avoid mixing pedestrian and car highways
if nvdb_way['highway'] in pedestrian_highway and osm_way['highway'] not in pedestrian_highway + ["track"] or \
nvdb_way['highway'] not in pedestrian_highway and osm_way['highway'] in pedestrian_highway:
continue
# Avoid mixing trunk etc with lower highway classes
if nvdb_way['highway'] in state_highway and osm_way['highway'] not in state_highway + ['tertiary'] or \
osm_way['highway'] in state_highway and nvdb_way['highway'] not in state_highway + ['road', 'tertiary']:
continue
# Check if match between OSM and NVDB way, and determine if closest distance between them
match_distance, match_nodes = match_ways(nvdb_way, osm_way, 0, len(nvdb_way['nodes']) - 1, margin)
if len(match_nodes) >= min_nodes and match_distance < best_distance and \
partial_way_length(nvdb_way['nodes'], match_nodes) > match_factor * nvdb_way['length']:
# Also check reverse match
reverse_distance, reverse_nodes = match_ways(osm_way, nvdb_way, 0, len(osm_way['nodes']) - 1, margin)
if len(reverse_nodes) >= min_nodes and reverse_distance < margin and \
partial_way_length(osm_way['nodes'], reverse_nodes) > match_factor * osm_way['length']:
best_id = nvdb_id
best_distance = match_distance
# Store match in data structure, if any match
if best_id is not None:
if command in ["replace", "offset"]:
# Replace earlier match if new match is better
if "osm_id" in nvdb_ways[ best_id ] and nvdb_ways[ best_id ]['distance'] > best_distance:
count_swap -= 1
total_distance -= nvdb_ways[ best_id ]['distance']
del osm_ways[ nvdb_ways[ best_id ]['osm_id'] ]['nvdb_id']
del nvdb_ways[ best_id ]['osm_id']
if "osm_id" not in nvdb_ways[ best_id ]:
count_swap += 1
total_distance += best_distance
osm_ways[ osm_id ]['nvdb_id'] = best_id
nvdb_ways[ best_id ]['osm_id'] = osm_id
nvdb_ways[ best_id ]['order'] = count_swap # Debug
nvdb_ways[ best_id ]['distance'] = best_distance # Debug
elif merge_all:
osm_ways[ osm_id ]['remove'] = True # Remove redundant way if it got a match
elif command == "tag":
count_swap += 1
total_distance += best_distance
osm_ways[ osm_id ]['nvdb_id'] = best_id
osm_ways[ osm_id ]['order'] = count_swap # Debug
osm_ways[ osm_id ]['distance'] = best_distance # Debug
# Pass 2: Match type highway
# Remove matches to be avoided before output
if command == "offset":
for nvdb_id, nvdb_way in iter(nvdb_ways.items()):
if "osm_id" in nvdb_way and (nvdb_way['distance'] < margin_offset or osm_ways[ nvdb_way['osm_id'] ]['highway'] in avoid_highway or \
replace_highway and (osm_way['highway'] not in replace_highway or nvdb_ways[ osm_way['nvdb_id'] ]['highway'] not in replace_highway)):
count_swap -= 1
total_distance -= nvdb_way['distance']
del nvdb_ways[ nvdb_id ]['osm_id']
elif command == "replace":
for osm_id, osm_way in iter(osm_ways.items()):
if "nvdb_id" in osm_way and (osm_way['avoid_tag'] or \
replace_highway and (osm_way['highway'] not in replace_highway or nvdb_ways[ osm_way['nvdb_id'] ]['highway'] not in replace_highway)):
count_swap -= 1
total_distance -= nvdb_ways[ osm_way['nvdb_id'] ]['distance']
del nvdb_ways[ osm_way['nvdb_id'] ]['osm_id']
del osm_ways[ osm_id ]['nvdb_id']
elif command == "tag":
for osm_id, osm_way in iter(osm_ways.items()):
if "nvdb_id" in osm_way and (osm_way['highway'] in avoid_highway or \
replace_highway and (osm_way['highway'] not in replace_highway or nvdb_ways[ osm_way['nvdb_id'] ]['highway'] not in replace_highway)):
count_swap -= 1
total_distance -= osm_way['distance']
del osm_ways[ osm_id ]['nvdb_id']
# Report result
message ("\r \t%i highways matched, %i not matched\n" % (count_swap, count_osm_roads - count_swap))
if command == "replace":
message ("\t%i missing highways added from NVDB\n" % (len(nvdb_ways) - count_swap))
message ("\tAverage offset: %.1f m\n" % (total_distance / count_swap))
# Identify missing NVDB highways, for "new" command.
# NVDB ways may be matched with any and several OSM ways. If match is long enough, the way will not be included as missing.
def add_new_highways():
message ("Add missing highways ...\n")
if "platform" in avoid_highway:
avoid_highway.remove("platform") # highway=platform is ok for this function
count = len(nvdb_ways)
count_missing = 0
# Iterate NVDB ways to check if match with any OSM way.
# Does not identify the best match, but checks how many nodes have a match with any highway.
for nvdb_id, nvdb_way in iter(nvdb_ways.items()):
message ("\r%i " % count)
count -= 1
if not nvdb_way['highway']: # Skip ferries etc
continue
match_nodes = set()
for osm_id, osm_way in iter(osm_ways.items()):
# Avoid testing ways with no overlapping bbox
if not osm_way['incomplete'] and osm_way['highway'] != None and osm_way['highway'] not in avoid_highway and \
osm_way['min_lat'] <= nvdb_way['max_lat'] and osm_way['max_lat'] >= nvdb_way['min_lat'] and \
osm_way['min_lon'] <= nvdb_way['max_lon'] and osm_way['max_lon'] >= nvdb_way['min_lon']:
test_distance, test_match_nodes = match_ways(nvdb_way, osm_way, 0, len(nvdb_way['nodes']) - 1, margin_new)
match_nodes.update(test_match_nodes)
if len(match_nodes) == len(nvdb_way['nodes']): # Break if all nodes already have match
break
# Include way as missing if matching partitions are not long enough
match_length = partial_way_length(nvdb_way['nodes'], match_nodes)
if match_length < new_factor * nvdb_way['length']:
nvdb_ways[ nvdb_id ]['missing'] = "%i" % match_length
count_missing += 1
message ("\r \t%i missing highways\n" % count_missing)
# Find which node in a way which is closest to another given node
def closest_node(way, target_node):
best_node_gap = margin
best_node = None
for i, node in enumerate(way['nodes']):
test_gap = distance(nodes[ node ]['lat'], nodes[ node ]['lon'], nodes[ target_node ]['lat'], nodes[ target_node ]['lon'])
if test_gap < best_node_gap:
best_node_gap = test_gap
best_node = i
return best_node
# Get the new tags, given tags1 as old tags and tags2 as new/target tags.
# Several exceptions to avoid undesired deletions or inclusions.
def update_tags(tags1, tags2):
new_tags = {} # copy.copy(tags1)
target_tags = tags2
if "highway" in tags2 and tags2['highway'] in ["unclassified", "service"]:
target_tags = {}
for key, value in iter(tags2.items()):
if key in ["name", "bridge", "tunnel", "layer", "foot", "bicycle", "motor_vehicle", "psv"]:
target_tags[key] = value
for key, value in iter(target_tags.items()):
if (key not in avoid_update_tags and ":lanes" not in key and "|" not in value and
":forward" not in key and ":backward" not in key and ":left" not in key and ":right" not in key and
not (key == "name" and "name" in tags1 and
("bridge" in tags1 and ("bru" in tags1["name"].lower() or "bro" in tags1["name"].lower()) or
("tunnel" in tags1 and ("tunnel" in tags1["name"] or "port" in tags1['name'].lower() or "lokk" in tags1['name'].lower())))) and
not (key == "bridge:description" and "name" in tags1 and
(tags1['name'] == value and "bru" in value or tags1['name'] == value + " bru")) and
not (key == "tunnel:name" and "name" in tags1 and tags1['name'] == value) and
not (key == "bridge" and "bridge" in tags1) and
not (key == "tunnel" and "tunnel" in tags1) and
not (key == "layer" and "layer" in tags1 and ("-" in value) == ("-" in tags1['layer'])) and
not (key == "maxspeed" and ("maxspeed:forward" in tags1 or "maxspeed:backward" in tags1)) and
not (key == "cycleway" and ("cycleway:right" in tags1 or "cyclway:left" in tags1))):
# not (key == "surface" and value == "asphalt"):
new_tags[ key ] = value
return new_tags
# Update tagging of public highways containing the ref=* tag, without replacing geometry.
# OSM ways are split to match the NVDB ways, then concatenated if tags are equal.
def tag_highways():
global new_id
message ("Match and retag highways ...\n")
# Prepare segments to be matched
for osm_id, osm_way in iter(osm_ways.items()):
if osm_way['highway'] != None and osm_way['highway'] in public_highway and not osm_way['incomplete']:
if osm_way['ref']:
ref = osm_way['ref'].split(";") # May have multiple ref=*
else:
ref = []
segment = {
'id': osm_id,
'highway': osm_way['highway'],
'ref': ref,
'length': osm_way['length'],
'nodes': copy.copy(osm_way['nodes']),
'tags': copy.copy(osm_way['tags']),
'new_tags': copy.copy(osm_way['tags']),
'relations': copy.copy(osm_way['relations'])
}
segments.append(segment)
segment_groups[ osm_id ] = [ segment ] # Will contain split segments in order
# Identify start/end nodes of public highways
osm_public_end_nodes = {} # Used later when combining segments
for osm_id, osm_way in iter(osm_ways.items()):
if osm_way['highway'] != None and osm_way['highway'] in public_highway + state_highway and not osm_way['incomplete']:
for node_id in [ osm_way['nodes'][0], osm_way['nodes'][-1] ]:
if node_id not in osm_public_end_nodes:
osm_public_end_nodes[ node_id ] = 1
else:
osm_public_end_nodes[ node_id ] += 1
nvdb_public_end_nodes = set()
for nvdb_id, nvdb_way in iter(nvdb_ways.items()):
if nvdb_way['highway'] in public_highway and nvdb_way['highway'] != "service": # and nvdb_way['ref'] is not None:
nvdb_public_end_nodes.add(nvdb_way['nodes'][0])
nvdb_public_end_nodes.add(nvdb_way['nodes'][-1])
# Insert end nodes from NVDB to simplify later matching
for end_node in nvdb_public_end_nodes:
for segment in segments:
osm_way = osm_ways[ segment['id'] ]
if osm_way['min_lat'] <= nodes[ end_node ]['lat'] <= osm_way['max_lat'] and \
osm_way['min_lon'] <= nodes[ end_node ]['lon'] <= osm_way['max_lon']:
# Identify position of closest node in OSM segment
best_distance = margin
prev_node = segment['nodes'][0]
for i, node in enumerate(segment['nodes'][1:]):
line_lat, line_lon, node_distance = line_distance(nodes[prev_node]['lat'], nodes[prev_node]['lon'], \
nodes[node]['lat'], nodes[node]['lon'], \
nodes[end_node]['lat'], nodes[end_node]['lon'])
if node_distance < best_distance:
best_distance = node_distance
best_node = i + 1
best_lat = line_lat
best_lon = line_lon
prev_node = node
# Insert node if gap to next existing node is big enough
if best_distance < margin:
node1 = nodes[ segment['nodes'][ best_node - 1 ] ]
node2 = nodes[ segment['nodes'][ best_node ] ]
dist1 = distance(node1['lat'], node1['lon'], best_lat, best_lon)
dist2 = distance(node2['lat'], node2['lon'], best_lat, best_lon)
if dist1 > margin_tag and dist2 > margin_tag:
new_id -= 1
best_lat = round(best_lat, 7)
best_lon = round(best_lon, 7)
nodes[ str(new_id) ] = {
'xml': None, # To be created at the end of this function
'used': 1, # Note: Not maintained for these nodes
'lat': best_lat,
'lon': best_lon
}
segment['nodes'].insert(best_node, str(new_id))
# Remove from NVDB short bridges which crosses a tunnel, or vice versa for tunnels
count = 0
for nvdb_id, nvdb_way in iter(nvdb_ways.items()):
if "tunnel" in nvdb_way['tags'] or "bridge" in nvdb_way['tags'] and nvdb_way['nodes'] and nvdb_way['length'] < 50:
for osm_id, osm_way in iter(osm_ways.items()):
if ("tunnel" in nvdb_way['tags'] and "bridge" in osm_way['tags'] or \
"bridge" in nvdb_way['tags'] and "tunnel" in osm_way['tags']) and \
osm_way['nodes']: # and osm_way['length'] < 50:
if crossing_lines([ nodes[ nvdb_way['nodes'][0] ], nodes[ nvdb_way['nodes'][-1] ]], \
[ nodes[ osm_way['nodes'][0] ], nodes[ osm_way['nodes'][-1] ]]):
osm_way['bridge_tunnel'] = nvdb_id # Mark for later tests
for key in list(nvdb_way['tags']):
if "tunnel" in key or "bridge" in key or "layer" in key:
del nvdb_way['tags'][ key ]
count += 1
message ("\tSwapped %i intersecting bridges/tunnels\n" % count)
# Match segments
count_first_match = 0
segment_index = 0