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/6] 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/6] 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/6] 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/6] 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/6] 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/6] 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);