From 42385b7794e60351a585eb965d5a9ce415161cd3 Mon Sep 17 00:00:00 2001 From: Hans Busch <47081128+HansBusch@users.noreply.github.com> Date: Sat, 19 Aug 2023 13:01:49 +0200 Subject: [PATCH 1/8] Highlight selected measurement. --- visualization/measurements.html | 57 ++++++++++++--------------------- 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/visualization/measurements.html b/visualization/measurements.html index f1194c5..3bd86fc 100644 --- a/visualization/measurements.html +++ b/visualization/measurements.html @@ -337,7 +337,8 @@

Visualisierung: Messwerte

var f = new ol.Feature({ geometry: new ol.geom.LineString( [p1, p2] - ) + ), + parent: feature }); // f.setStyle(styles); arrowLayer.getSource().addFeature(f); @@ -354,43 +355,27 @@

Visualisierung: Messwerte

arrowLayer.setVisible(true); - + function selectNode(feature) { + var prop = feature.getProperties(); + if (prop.hasOwnProperty('parent')) feature = prop.parent; + if (dataSource.hasFeature(feature)) { + var coord = feature.getGeometry().getCoordinates(); + var props = feature.getProperties(); + caption.innerHTML = annotation(feature); + caption.style.alignItems="flex-start"; + console.log(annotation_verbose(feature)); + feature.setStyle(styleFunction(feature, undefined, true)); + } + vectorLayer.getSource().getFeatures().forEach(f=>{ + if (f != feature) f.setStyle(styleFunction(f, undefined, false)); + }); + } map.on('singleclick', function(evt) { - var feature = map.forEachFeatureAtPixel(evt.pixel, function(feature, layer) { - return feature; - }); - if (feature && dataSource.hasFeature(feature)) { - var coord = feature.getGeometry().getCoordinates(); - var props = feature.getProperties(); - caption.innerHTML = annotation(feature); - caption.style.alignItems="flex-start"; - console.log(annotation_verbose(feature)); - } - }); - - - // feature mouse hover handler - var noFeatureActive = false; - - map.on('pointermove', function(evt) { - if (evt.dragging) { - return; - } - if (!noFeatureActive){ - vectorLayer.getSource().getFeatures().forEach(f=>{ - f.setStyle(styleFunction(f, undefined, false)); - }); - noFeatureActive = true; - } - var pixel = map.getEventPixel(evt.originalEvent); - map.forEachFeatureAtPixel(pixel,function(feature) { - if (dataSource.hasFeature(feature)){ - feature.setStyle(styleFunction(feature, undefined, true)); - noFeatureActive = false; - } - return feature; - }); + var sources = vectorLayer.getSource(); + var feature = vectorLayer.getSource().getClosestFeatureToCoordinate(evt.coordinate); + if (feature) selectNode(feature); + return feature; }); paletteUrban.writeLegend('legend_urban', [0, 1.5, 2.5], 'm'); From 42eada0ead8bf76a99f603c4efe9aea8cda22ca2 Mon Sep 17 00:00:00 2001 From: Hans Busch <47081128+HansBusch@users.noreply.github.com> Date: Sat, 19 Aug 2023 14:01:28 +0200 Subject: [PATCH 2/8] Split longer lines into segments of 100 meter. Output distance measurements instead of statistics to enable client side combined statistics including sample count to know number of seconds on segment. --- obs/face/geojson/ExportRoadAnnotations.py | 43 +++++++++++------------ obs/face/osm/DataSource.py | 7 ++-- obs/face/osm/Way.py | 41 +++++++++++++++++++-- 3 files changed, 62 insertions(+), 29 deletions(-) diff --git a/obs/face/geojson/ExportRoadAnnotations.py b/obs/face/geojson/ExportRoadAnnotations.py index e399fd8..f216bd3 100644 --- a/obs/face/geojson/ExportRoadAnnotations.py +++ b/obs/face/geojson/ExportRoadAnnotations.py @@ -43,16 +43,26 @@ def add_measurements(self, measurements): for sample in measurements: self.n_samples += 1 # filter measurements + if not ("OSM_way_id" in sample): + continue; + way_id = sample["OSM_way_id"] + way_orientation = 1 if sample["OSM_way_orientation"] == -1 else 0 + if sample["latitude"] is None or sample["longitude"] is None or sample["distance_overtaker"] is None \ or self.only_confirmed_measurements and (sample["confirmed"] is not True) \ or not sample["has_OSM_annotations"]: + if not (way_id in self.way_statistics): + way = self.map_source.get_way_by_id(way_id) + if way: + self.way_statistics[way_id] = WayStatistics(way_id, way) + if way_id in self.way_statistics and sample["speed"] != 0: + self.way_statistics[way_id].n_ticks[way_orientation] += 1 + continue self.n_valid += 1 - way_id = sample["OSM_way_id"] value = sample["distance_overtaker"] - way_orientation = sample["OSM_way_orientation"] self.map_source.ensure_coverage([sample["latitude"]], [sample["longitude"]]) @@ -68,13 +78,15 @@ def add_measurements(self, measurements): self.n_grouped += 1 else: logging.warning("way not found in map") + self.way_statistics[way_id].n_ticks[way_orientation] += 1 def finalize(self): log.info("%s samples, %s valid", self.n_samples, self.n_valid) features = [] for way_stats in self.way_statistics.values(): way_stats.finalize() - if not any(way_stats.valid): +# if not any(way_stats.valid): + if not any(way_stats.n_ticks): continue for i in range(1 if way_stats.oneway else 2): @@ -91,19 +103,14 @@ def finalize(self): coordinates = [] feature = {"type": "Feature", - "properties": {"distance_overtaker_mean": way_stats.d_mean[i], - "distance_overtaker_median": way_stats.d_median[i], - "distance_overtaker_minimum": way_stats.d_minimum[i], - "distance_overtaker_n": way_stats.n[i], - "distance_overtaker_n_below_limit": way_stats.n_lt_limit[i], - "distance_overtaker_n_above_limit": way_stats.n_geq_limit[i], - "distance_overtaker_limit": way_stats.d_limit, - "distance_overtaker_measurements": way_stats.samples[i], + "properties": {"distance_overtaker_limit": way_stats.d_limit, + "distance_overtaker_measurements": sorted(way_stats.samples[i], key = float), "zone": way_stats.zone, "direction": direction, "name": way_stats.name, "way_id": way_stats.way_id, "valid": way_stats.valid[i], + "ticks": way_stats.n_ticks[i], }, "geometry": {"type": "LineString", "coordinates": coordinates}} @@ -124,12 +131,10 @@ def __init__(self, way_id, way): self.n = [0, 0] self.n_lt_limit = [0, 0] self.n_geq_limit = [0, 0] + self.n_ticks = [0, 0] self.way_id = way_id self.valid = [False, False] - self.d_mean = [0, 0] - self.d_median = [0, 0] - self.d_minimum = [0, 0] self.zone = "unknown" self.oneway = False @@ -156,19 +161,11 @@ def __init__(self, way_id, way): def add_sample(self, sample, orientation): if np.isfinite(sample): - i = 1 if orientation == -1 else 0 - self.samples[i].append(sample) + self.samples[orientation].append(sample) return self def finalize(self): for i in range(2): samples = np.array(self.samples[i]) if len(samples) > 0: - self.n[i] = len(samples) - self.d_mean[i] = np.mean(samples) - self.d_median[i] = np.median(samples) - self.d_minimum[i] = np.min(samples) - if self.d_limit is not None: - self.n_lt_limit[i] = int((samples < self.d_limit).sum()) - self.n_geq_limit[i] = int((samples >= self.d_limit).sum()) self.valid[i] = True diff --git a/obs/face/osm/DataSource.py b/obs/face/osm/DataSource.py index fb536d8..5f4d22c 100644 --- a/obs/face/osm/DataSource.py +++ b/obs/face/osm/DataSource.py @@ -65,9 +65,10 @@ def add_tile(self, tile): # add way objects, and store for way_id, way in ways.items(): if way_id not in self.ways: - w = Way(way_id, way, nodes) - self.ways[way_id] = w - self.way_container.insert(w) + w = Way.create(way_id, way, nodes, 100) + self.ways.update(w) + for id in w: + self.way_container.insert(w[id]) # update tile list self.loaded_tiles.append(tile) diff --git a/obs/face/osm/Way.py b/obs/face/osm/Way.py index 689c742..729051c 100644 --- a/obs/face/osm/Way.py +++ b/obs/face/osm/Way.py @@ -4,7 +4,7 @@ class Way: - def __init__(self, way_id, way, all_nodes): + def __init__(self, way_id, way, nodes_way): self.way_id = way_id if "tags" in way: @@ -13,8 +13,6 @@ def __init__(self, way_id, way, all_nodes): self.tags = {} # determine points - nodes_way = [all_nodes[i] for i in way["nodes"]] - lat = np.array([n["lat"] for n in nodes_way]) lon = np.array([n["lon"] for n in nodes_way]) self.points_lat_lon = np.stack((lat, lon), axis=1) @@ -35,10 +33,47 @@ def __init__(self, way_id, way, all_nodes): # direction dx = np.diff(x) dy = np.diff(y) + self.seg_length = np.hypot(dx, dy) self.direction = np.arctan2(dy, dx) self.directionality_bicycle, self.directionality_motorized = self.get_way_directionality(way) + @staticmethod + def create(way_id, way, all_nodes, max_len): + ways = {} + # determine points + nodes = [all_nodes[i] for i in way["nodes"]] + lat = np.array([n["lat"] for n in nodes]) + lon = np.array([n["lon"] for n in nodes]) + + # bounding box + a = (min(lat), min(lon)) + b = (max(lat), max(lon)) + + # define the local map around the center of the bounding box + lat_0 = (a[0] + b[0]) * 0.5 + lon_0 = (a[1] + b[1]) * 0.5 + local_map = LocalMap(lat_0, lon_0) + x, y = local_map.transfer_to(lat, lon) + dx = np.diff(x) + dy = np.diff(y) + seg_length = np.hypot(dx, dy) + + slen = 0 + first = 0 + if len(dx) > 0: + for i in range(len(seg_length)): + slen += seg_length[i] + if (slen > max_len and i != first): + id = str(way_id)+'.'+str(i) + ways[id] = Way(id, way, nodes[first:i+1]) + first = i + slen = 0 + id = str(way_id) + ways[id] = Way(id, way, nodes[first:]) + return ways + + def get_axis_aligned_bounding_box(self): return self.a, self.b From 14b2a36c200a6020c436e5e7ea3700d25b3f50fa Mon Sep 17 00:00:00 2001 From: Hans Busch <47081128+HansBusch@users.noreply.github.com> Date: Sat, 19 Aug 2023 14:05:25 +0200 Subject: [PATCH 3/8] Show global (city-wide) statistics when nothing selected. Select nearest line instead of exact hit to ease selection with mouse and tablet. Easy street selection plus individual segment selection via shift key. Condensed detail view including measurement time. --- visualization/OBS.js | 11 +- visualization/roads.html | 335 +++++++++++++++++++-------------------- 2 files changed, 168 insertions(+), 178 deletions(-) diff --git a/visualization/OBS.js b/visualization/OBS.js index 7a66aac..b3378bf 100644 --- a/visualization/OBS.js +++ b/visualization/OBS.js @@ -172,11 +172,12 @@ class Palette { paletteUrban = new Palette( { - 0.0: [64, 0, 0, 255], - 1.4999: [196, 0, 0, 255], - 1.5: [196, 196, 0, 255], - 2.0: [0, 196, 0, 255], - 2.55: [0, 255, 0, 255], + 0.0: [196, 0, 0, 255], + 0.8: [196, 0, 0, 255], + 1.3: [245, 141, 0, 255], + 1.5: [94, 188, 7, 255], + 2.0: [94, 188, 7, 255], + 2.55: [0, 196, 0, 255], }, [0, 0, 196, 255] ) diff --git a/visualization/roads.html b/visualization/roads.html index 79921bb..75a50bd 100644 --- a/visualization/roads.html +++ b/visualization/roads.html @@ -43,13 +43,14 @@ .overlay { display: flex; flex-direction: column; - justify-content: space-around; + justify-content: space-between; position: absolute; width: 30%; height: 98%; left: 1%; top: 1%; z-index: 0; + pointer-events: none; } .title { @@ -57,7 +58,7 @@ position: relative; height: 10%; width: 100%; - background-color: #FFFFFF; +# background-color: #FFFFFF; padding: 5px; z-index: 0; } @@ -81,6 +82,7 @@ padding: 5px; align-items: center; z-index: 0; + pointer-events: auto; } .chart { @@ -89,20 +91,16 @@ background-color: #FFFFFF; padding: 5px; z-index: 0; - } - - .legend { - height: 15%; - width: 100%; - background-color: #FFFFFF; - padding: 5px; - z-index: 0; + pointer-events: auto; } .ol-zoom { left: unset; right: 8px; } + td { + padding-left: 10px; + } @@ -113,36 +111,26 @@
+
-
-

Visualisierung: Straßenabschnitte

-
-
- Bitte einen Streckenabschnitt in der Karte (farbige Linien) anklicken um detailierte Informationen zu - erhalten. -
-
- Kartenlegende
- Streckenabschnitte werden wie folgt eingefärbt:
- Anteil der Überholenden unter Minimalabstand: -
- ohne Messungen: grau
From 64d02185c0f23d542d05adee2bae512ec12b0fcc Mon Sep 17 00:00:00 2001 From: Hans Busch <47081128+HansBusch@users.noreply.github.com> Date: Sat, 7 Oct 2023 17:14:16 +0200 Subject: [PATCH 4/8] Add filter for simple visual data queries. --- visualization/measurements.html | 73 ++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/visualization/measurements.html b/visualization/measurements.html index 3bd86fc..d1af8bf 100644 --- a/visualization/measurements.html +++ b/visualization/measurements.html @@ -104,18 +104,14 @@

Visualisierung: Messwerte

Bitte einen Messpunkt in der Karte (farbige Kreise) anklicken um detailierte Informationen zu erhalten.
-
- Kartenlegende
- Ring um Messung: außerorts, innerorts, unbekannt -
- Fläche innerhalb kodiert den Überholabstand: -
Überholabstand außerorts
-
Überholabstand innerorts
-
+
+ Filter
+ +
From fa82c94562bb255035d9e99379f5a6d477ccb73a Mon Sep 17 00:00:00 2001 From: Hans Busch <47081128+HansBusch@users.noreply.github.com> Date: Sun, 8 Oct 2023 16:38:26 +0200 Subject: [PATCH 5/8] Output full GPS coordinates to ease searching of abnormalities. --- visualization/measurements.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/visualization/measurements.html b/visualization/measurements.html index d1af8bf..72b4576 100644 --- a/visualization/measurements.html +++ b/visualization/measurements.html @@ -125,7 +125,7 @@

Visualisierung: Messwerte

if (feature.get('has_OSM_annotations') == true){ s += "Position der Messung (korrigiert):" + feature.get('latitude_projected').toFixed(6) + " " + feature.get('longitude_projected').toFixed(6) + ""; } - s += "Position der Messung (GPS):" + feature.get('latitude_GPS').toFixed(6) + " " + feature.get('longitude_GPS').toFixed(6) + ""; + s += "Position der Messung (GPS):" + feature.get('latitude_GPS') + " " + feature.get('longitude_GPS') + ""; d = feature.get('distance_stationary'); s += "Abstand nach rechts:" + ((d == null)?"n/a":d.toFixed(2)) + " m "; From 245fd890473f21c1aaa9852756b5f9754b24d634 Mon Sep 17 00:00:00 2001 From: Hans Busch <47081128+HansBusch@users.noreply.github.com> Date: Sun, 8 Oct 2023 17:12:45 +0200 Subject: [PATCH 6/8] Handle too many blanks. Add filter keyword tooltips. Map missing distance to large distance. --- visualization/measurements.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/visualization/measurements.html b/visualization/measurements.html index 72b4576..bd68bcd 100644 --- a/visualization/measurements.html +++ b/visualization/measurements.html @@ -116,10 +116,10 @@

Visualisierung: Messwerte

var s = ""; d = feature.get('distance_overtaker'); - s += ""; + s += ""; s += ""; - s += ""; + s += ""; s += ""; if (feature.get('has_OSM_annotations') == true){ @@ -128,15 +128,15 @@

Visualisierung: Messwerte

s += ""; d = feature.get('distance_stationary'); - s += ""; + s += ""; - s += "
Überholabstand:" + ((d == null)?"n/a":d.toFixed(2)) + " m
Überholabstand:
" + ((d == null)?"n/a":d.toFixed(2)) + " m
Zeit/Datum der Messung:" + feature.get('time') + "
Benutzer (pseudonymisiert):" + feature.get('user_id') + "
Benutzer (pseudonymisiert):
" + feature.get('user_id') + "
Messungs-ID (pseudonymisiert):" + feature.get('measurement_id') + "
Position der Messung (GPS):" + feature.get('latitude_GPS') + " " + feature.get('longitude_GPS') + "
Abstand nach rechts:" + ((d == null)?"n/a":d.toFixed(2)) + " m
Abstand nach rechts:
" + ((d == null)?"n/a":d.toFixed(2)) + " m
" + s += "
" if (feature.get("egomotion_is_derived") == true){ s += "Eigenbewegung (aus Pos. berechnet)"; } else { s += "Eigenbewegung (von GPS)"; } - s += "
"; + s += ""; c = feature.get('course'); s += ((c == null)?"n/a":c.toFixed(0)) + " deg, "; @@ -376,10 +376,10 @@

Visualisierung: Messwerte

if (displayFilter[0].val != val) sel = -1; } else { - var fval = parseFloat(val); + var fval = val ? parseFloat(val) : 2; // map missing to 2m distance var fcomp = parseFloat(displayFilter[0].val); - if (displayFilter[0].op == "<") if (fval >= fcomp) sel = -1; - else if (displayFilter[0].op == ">") if (fval <= fcomp) sel = -1; + if (displayFilter[0].op == "<") { if (fval >= fcomp) sel = -1;} + else if (displayFilter[0].op == ">") { if (fval <= fcomp) sel = -1;} } } f.setStyle(styleFunction(f, undefined, sel)); @@ -399,7 +399,7 @@

Visualisierung: Messwerte

if (event.key === "Enter") { event.preventDefault(); displayFilter = [] - var args = input.value.split(' '); + var args = input.value.replace(/\s+/g, ' ').trim().split(' '); if (args.length >= 3) { var f = {key:args[0], op:args[1], val:args[2]} displayFilter.push(f); From 740d622c08ad862ca374e50c571122723bdb70b8 Mon Sep 17 00:00:00 2001 From: Hans Busch <47081128+HansBusch@users.noreply.github.com> Date: Sun, 2 Nov 2025 12:56:25 +0100 Subject: [PATCH 7/8] Add parameter to chop segments. Add intermediate nodes when existing segments exceed the chunk size. --- obs/bin/obs_face.py | 5 ++++ obs/face/osm/DataSource.py | 3 ++- obs/face/osm/Way.py | 49 +++++++++++++++++++++++++++++++------- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/obs/bin/obs_face.py b/obs/bin/obs_face.py index dbee450..9616775 100644 --- a/obs/bin/obs_face.py +++ b/obs/bin/obs_face.py @@ -356,6 +356,9 @@ def main(): parser.add_argument('-v', '--verbose', action='store_true', help='be verbose') + parser.add_argument('-c', '--chunk', required=False, action='store', default=100, type=float, + help='Chop segments on road page to max length in meter.') + args = parser.parse_args() coloredlogs.install(level=logging.DEBUG if args.verbose else logging.INFO, @@ -393,6 +396,8 @@ def main(): if args.annotate or args.collect or args.visualization: logging.info('Loading OpenStreetMap data') map_source = OSMDataSource(cache_dir=args.path_cache) + if args.chunk is not None: + map_source.chunk_size = args.chunk if args.annotate or args.collect: if not args.input: diff --git a/obs/face/osm/DataSource.py b/obs/face/osm/DataSource.py index 5f4d22c..66da862 100644 --- a/obs/face/osm/DataSource.py +++ b/obs/face/osm/DataSource.py @@ -36,6 +36,7 @@ def __init__(self, cache_dir="cache", tile_zoom=14): self.loaded_tiles = [] self.tile_source = TileSource() self.tile_zoom = tile_zoom + self.chunk_size = 100 def ensure_coverage(self, lat, lon, extend=0.0): tiles = self.tile_source.get_required_tiles(lat, lon, self.tile_zoom, extend=extend) @@ -65,7 +66,7 @@ def add_tile(self, tile): # add way objects, and store for way_id, way in ways.items(): if way_id not in self.ways: - w = Way.create(way_id, way, nodes, 100) + w = Way.create(way_id, way, nodes, self.chunk_size) self.ways.update(w) for id in w: self.way_container.insert(w[id]) diff --git a/obs/face/osm/Way.py b/obs/face/osm/Way.py index 729051c..a24c865 100644 --- a/obs/face/osm/Way.py +++ b/obs/face/osm/Way.py @@ -38,6 +38,12 @@ def __init__(self, way_id, way, nodes_way): self.directionality_bicycle, self.directionality_motorized = self.get_way_directionality(way) + @staticmethod + def subid(way_id, nodes): + if len(nodes) == 0: + return way_id + return str(way_id)+'.'+str(len(nodes)) + @staticmethod def create(way_id, way, all_nodes, max_len): ways = {} @@ -58,21 +64,46 @@ def create(way_id, way, all_nodes, max_len): dx = np.diff(x) dy = np.diff(y) seg_length = np.hypot(dx, dy) - slen = 0 + newnodes = [nodes[0]] first = 0 - if len(dx) > 0: - for i in range(len(seg_length)): + # split long segments with intermediate nodes + for i in range(len(seg_length)): + n = math.floor((seg_length[i] - max_len / 2) / max_len) + if n > 0: + for s in range(n): + f1 = (n - s) / (n + 1) + f2 = (s + 1) / (n + 1) + latx = lat[i] * f1 + lat[i+1] * f2 + lonx = lon[i] * f1 + lon[i+1] * f2 + if math.isnan(latx) or math.isnan(lonx): + f1 = f2 + node_id = nodes[i]['id']+ (s+1) * 0x1000000 + node = {'type':'node', 'id':node_id, 'lat':latx, 'lon':lonx} + all_nodes[node_id] = node + newnodes.append(node) + w_id = Way.subid(way_id, ways) + ways[w_id] = Way(w_id, way, newnodes[first:]) + first = len(newnodes) - 1 + slen = 0 + # add last segment + newnodes.append(nodes[i+1]) + w_id = Way.subid(way_id, ways) + ways[w_id] = Way(w_id, way, newnodes[first:]) + first = len(newnodes) - 1 + slen = 0 + else: + newnodes.append(nodes[i+1]) slen += seg_length[i] if (slen > max_len and i != first): - id = str(way_id)+'.'+str(i) - ways[id] = Way(id, way, nodes[first:i+1]) + w_id = Way.subid(way_id, ways) + ways[w_id] = Way(w_id, way, newnodes[first:]) first = i slen = 0 - id = str(way_id) - ways[id] = Way(id, way, nodes[first:]) + if slen > 0: + w_id = Way.subid(way_id, ways) + ways[w_id] = Way(w_id, way, newnodes[first:]) return ways - def get_axis_aligned_bounding_box(self): return self.a, self.b @@ -153,6 +184,8 @@ def get_way_coordinates(self, reverse=False, lateral_offset=0): # then move the point c_i = c[i] + n_i * lateral_offset c_i = self.local_map.transfer_from(c_i[0], c_i[1]) + if math.isnan(c_i[0]) or math.isnan(c_i[1]): + n_next = 0 coordinates.append([c_i[0], c_i[1]]) return coordinates From 4de0a2425833d4d8ca6010f2edd02b8c7cfa3981 Mon Sep 17 00:00:00 2001 From: Hans Busch <47081128+HansBusch@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:31:16 +0100 Subject: [PATCH 8/8] Fix issue in short segment partitioning. --- obs/face/osm/Way.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obs/face/osm/Way.py b/obs/face/osm/Way.py index a24c865..6075051 100644 --- a/obs/face/osm/Way.py +++ b/obs/face/osm/Way.py @@ -98,7 +98,7 @@ def create(way_id, way, all_nodes, max_len): if (slen > max_len and i != first): w_id = Way.subid(way_id, ways) ways[w_id] = Way(w_id, way, newnodes[first:]) - first = i + first = len(newnodes) - 1 slen = 0 if slen > 0: w_id = Way.subid(way_id, ways)