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
+
+