Skip to content

Commit 4acd6c7

Browse files
authored
Generalize stations using discrete isolation (#633)
Closes #555 ## Goal Improve the distribution of stations on the map for lower zoom levels, worldwide, without cluttering the map with too many stations. ## Implementation and documentation Method: *Discrete Isolation* to provide a measure of local importance of a station, compared to a global importance. See: - https://osm2pgsql.org/doc/manual.html#strategy-discrete-isolation - https://blog.jochentopf.com/2022-12-19-selecting-settlements-to-display.html - https://dx.doi.org/10.1007/s42489-021-00079-y ## Changes The *Discrete Isolation* implementation of Osm2pgsql is used. This is experimental but works fine. After the station importance has been determined by the number of routes or rail length (for yards), the discrete isolation algorithm is ran to determine the discrete isolation column values for each station. The discrete isolation are used to filter zooms 4 until 7, instead of just the station importance values. The result is that huge stations close together (e.g. in the same city) do not all show on the map in the same location, because locally one of the stations will be the most important. Worldwide this makes a difference because many stations do not have many routes like the large European stations, but do have more routes tagged than other stations in the area, making them locally more important. Furthermore, the requirement to have a railway reference has been removed. For non-European stations, railway references are often not tagged, which removed them from the map for the low zoom levels. Zooms 3 and lower are unchanged and show no stations. Zoom 4 shows medium and large stations without their references/names, filtered by local importance for zoom 4. Zoom 5 shows medium and large stations, filtered by local importance for zoom 5. Zoom 6 shows medium and large stations, filtered by local importance for zoom 6. Zoom 7 shows all stations, filtered by local importance for zoom 7, but no references/names for small stations. Zoom 8 and higher shows all stations. ## Testing Image below: master branch, https://openrailwaymap.app Image above: this branch, http://localhost:8000 ### Europe Zoom 4 (https://openrailwaymap.app/#view=4/51.14/18.47): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/8e385e5a-c797-4ac0-b603-71fa18da1b8a" /> Zoom 5 (https://openrailwaymap.app/#view=5/50.88/12.49): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/275f9a50-8200-44c2-b8b9-e944308e4ceb" /> Zoom 6 (https://openrailwaymap.app/#view=6/49.388/7.588): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/617b168b-2187-4802-a9ce-a466489e321b" /> Zoom 7 (https://openrailwaymap.app/#view=7/47.657/7.751): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/8ea468da-df71-469a-a209-234e721ff02d" /> ### East Asia Zoom 4 (https://openrailwaymap.app/#view=4/39.06/127.17): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/bba03360-9d9e-482d-a4f4-928a32890c36" /> Zoom 5 (https://openrailwaymap.app/#view=5/38.49/134.36): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/b9b18f3f-c7e7-41ef-b390-8a88850cdd5e" /> Zoom 6 (https://openrailwaymap.app/#view=6/35.741/132.211): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/4ee196a3-ac26-478c-8e1e-f1ba8d9efceb" /> Zoom 7 (https://openrailwaymap.app/#view=7/34.752/134.117): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/08c3939d-a794-44cd-9a88-8ca1b74426e8" /> ### South Asia Zoom 4 (https://openrailwaymap.app/#view=4/19.97/86.77): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/f6a6cf0e-0dc3-4386-8bd7-fdcc46297fe8" /> Zoom 5 (https://openrailwaymap.app/#view=5/21.78/82.24): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/0d9307f8-31d6-420e-be3a-baa095c391b4" /> Zoom 6 (https://openrailwaymap.app/#view=6/25.757/80.629): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/8b919f40-d221-41e8-b3c7-4934a04e217a" /> Zoom 7 (https://openrailwaymap.app/#view=7/22.007/74.38): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/487925aa-cc8f-4e80-9da3-3dc55c8625f1" /> ### South America Zoom 4 (https://openrailwaymap.app/#view=4/-34.87/-58.21): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/5e873a01-7972-4bfb-91cd-35b555c687f1" /> Zoom 5 (https://openrailwaymap.app/#view=5/-34.65/-62.82): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/74b72599-171a-4524-bb57-bebf0f906f21" /> Zoom 6 (https://openrailwaymap.app/#view=6/-33.533/-61.109): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/3495ceed-41de-47cd-ab70-2a4329c5eda3" /> Zoom 7 (https://openrailwaymap.app/#view=7/-34.062/-60.466): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/d2daa8d4-7a4a-4454-b0b9-e028391ff190" /> ### North America Zoom 4 (https://openrailwaymap.app/#view=4/38.16/-88.29): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/e332bcd7-64d2-4cac-b69e-5ca2697daffd" /> Zoom 5 (https://openrailwaymap.app/#view=5/39.85/-80.27): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/c681be9a-9517-4600-84b5-b826457bc3fb" /> Zoom 6 (https://openrailwaymap.app/#view=6/42.238/-83.259): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/975dbfed-8302-4c95-b230-39b0fb4ee0cd" /> Zoom 7 (https://openrailwaymap.app/#view=7/41.893/-85.488): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/dcf14865-3ac3-4b96-b83c-73a1fa9d151b" /> ### Africa Zoom 4 (https://openrailwaymap.app/#view=4/-27.11/29.33): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/cbc5a110-4c12-4f6d-897b-1f00ff3ce8da" /> Zoom 5 (https://openrailwaymap.app/#view=5/-29.81/25.02): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/012b08e7-689f-4392-8542-7a42e09e4709" /> Zoom 6 (https://openrailwaymap.app/#view=6/-27.548/28.778): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/b5409dc7-bcad-46fe-8e98-e4f67ebdb600" /> Zoom 7 (https://openrailwaymap.app/#view=7/-26.916/28.831): <img width="1227" height="1395" alt="image" src="https://github.com/user-attachments/assets/85f95c35-3ae3-4f01-81ab-d48252f9f147" /> ## Future Stations should not be rendered for low zooms if they are not on main/branch lines, and for non-train modalities. There is much missing tagging in some regions of the world. See e.g. #663. With more stop areas and routes tagged, the importance of a station is clearer to determine worldwide. Some future tweaking of the discrete isolation output values might be needed.
1 parent 65aec5f commit 4acd6c7

File tree

10 files changed

+229
-179
lines changed

10 files changed

+229
-179
lines changed

api/test/api.hurl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ jsonpath "$" count == 3
201201
jsonpath "$[0].name" == "Landsberger Allee/Petersburger Straße"
202202
jsonpath "$[0].feature" == "tram_stop"
203203
jsonpath "$[0].state" == "present"
204-
jsonpath "$[0].rank" == 32
204+
jsonpath "$[0].rank" == 34
205205
jsonpath "$[0].osm_ids" count == 4
206206
jsonpath "$[0].osm_ids[0]" == 244129991
207207
jsonpath "$[0].osm_ids[1]" == 271777826

import/docker-startup.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ function create_update_functions_views() {
8484
$PSQL -f sql/api_milestone_functions.sql
8585
$PSQL -f sql/signal_features.sql
8686
$PSQL -f sql/get_station_importance.sql
87+
$PSQL -f sql/update_station_importance.sql
88+
osm2pgsql-gen \
89+
--database gis \
90+
--style openrailwaymap.lua
91+
$PSQL -f sql/stations_clustered.sql
8792
$PSQL -f sql/tile_views.sql
8893
$PSQL -f sql/api_facility_views.sql
8994
}
@@ -92,6 +97,10 @@ function refresh_materialized_views() {
9297
echo "Updating materialized views"
9398
$PSQL -f sql/update_signal_features.sql
9499
$PSQL -f sql/update_station_importance.sql
100+
osm2pgsql-gen \
101+
--database gis \
102+
--style openrailwaymap.lua
103+
$PSQL -f sql/update_stations_clustered.sql
95104
$PSQL -f sql/update_api_views.sql
96105
}
97106

import/openrailwaymap.lua

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1254,7 +1254,7 @@ function osm2pgsql.process_way(object)
12541254
if station_feature then
12551255
for station, _ in pairs(station_type(tags)) do
12561256
stations:insert({
1257-
way = object:as_polygon(),
1257+
way = object.is_closed and object:as_polygon() or object:as_linestring(),
12581258
feature = station_feature,
12591259
state = station_state,
12601260
name = tags.name or tags.short_name,
@@ -1478,3 +1478,16 @@ function osm2pgsql.process_relation(object)
14781478
end
14791479
end
14801480
end
1481+
1482+
function osm2pgsql.process_gen()
1483+
-- Discrete isolation to assign a "local" importance to each station
1484+
osm2pgsql.run_gen('discrete-isolation', {
1485+
name = 'station_importance',
1486+
debug = true,
1487+
src_table = 'stations_with_importance',
1488+
dest_table = 'stations_with_importance',
1489+
geom_column = 'way',
1490+
id_column = 'id',
1491+
importance_column = 'importance',
1492+
})
1493+
end

import/sql/get_station_importance.sql

Lines changed: 12 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -103,69 +103,10 @@ CREATE OR REPLACE VIEW station_nodes_platforms_rel_count AS
103103
) sr
104104
GROUP BY id;
105105

106-
-- Clustered stations without importance
107-
CREATE MATERIALIZED VIEW IF NOT EXISTS stations_clustered AS
108-
SELECT
109-
row_number() over (order by name, station, railway_ref, uic_ref, feature) as id,
110-
name,
111-
station,
112-
railway_ref,
113-
uic_ref,
114-
feature,
115-
state,
116-
array_agg(facilities.id) as station_ids,
117-
ST_Centroid(ST_ConvexHull(ST_RemoveRepeatedPoints(ST_Collect(way)))) as center,
118-
ST_Buffer(ST_ConvexHull(ST_RemoveRepeatedPoints(ST_Collect(way))), 50) as buffered,
119-
ST_NumGeometries(ST_RemoveRepeatedPoints(ST_Collect(way))) as count
120-
FROM (
121-
SELECT
122-
*,
123-
ST_ClusterDBSCAN(way, 400, 1) OVER (PARTITION BY name, station, railway_ref, uic_ref, feature, state) AS cluster_id
124-
FROM (
125-
SELECT
126-
st_collect(any_value(s.way), st_collect(distinct q.way)) as way,
127-
name,
128-
station,
129-
railway_ref,
130-
uic_ref,
131-
feature,
132-
state,
133-
id
134-
FROM stations s
135-
left join stop_areas sa
136-
ON (ARRAY[s.osm_id] <@ sa.node_ref_ids AND s.osm_type = 'N')
137-
OR (ARRAY[s.osm_id] <@ sa.way_ref_ids AND s.osm_type = 'W')
138-
OR (ARRAY[s.osm_id] <@ sa.stop_ref_ids AND s.osm_type = 'N')
139-
left join (
140-
select
141-
sa.osm_id as stop_area_id,
142-
se.way
143-
from stop_areas sa
144-
join station_entrances se
145-
on array[se.osm_id] <@ sa.node_ref_ids
146-
147-
union all
148-
149-
select
150-
sa.osm_id as stop_area_id,
151-
pl.way
152-
from stop_areas sa
153-
join platforms pl
154-
on array[pl.osm_id] <@ sa.platform_ref_ids
155-
) q on q.stop_area_id = sa.osm_id
156-
group by name, station, railway_ref, uic_ref, feature, state, id
157-
) stations_with_entrances
158-
) AS facilities
159-
GROUP BY cluster_id, name, station, railway_ref, uic_ref, feature, state;
160-
161-
CREATE INDEX IF NOT EXISTS stations_clustered_station_ids
162-
ON stations_clustered
163-
USING gin(station_ids);
164-
165-
CREATE MATERIALIZED VIEW IF NOT EXISTS stations_with_importance AS
106+
CREATE OR REPLACE VIEW stations_with_importance_view AS
166107
SELECT
167108
id,
168-
max(importance) as importance
109+
1 + max(importance) as importance
169110
FROM (
170111
SELECT
171112
id,
@@ -191,7 +132,7 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS stations_with_importance AS
191132
SELECT
192133
s.id,
193134
-- The square root and factor are made to align the importance factors of yards
194-
-- with stations. A 320 km yard is equivalent to a station with 140 platforms/routes.
135+
-- with stations. A 320 km yard is equivalent to a station with 140 routes.
195136
SQRT(
196137
SUM(ST_Length(ST_Intersection(ST_Buffer(s.way, 50), l.way)))
197138
) / 4 AS importance
@@ -210,100 +151,12 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS stations_with_importance AS
210151
) all_stations_with_importance
211152
GROUP BY id;
212153

213-
CREATE INDEX IF NOT EXISTS stations_with_importance_idx
214-
ON stations_with_importance
215-
USING btree(id);
216-
217-
-- Final table with station nodes and the number of route relations
218-
-- needs about 3 to 4 minutes for whole Germany
219-
-- or about 20 to 30 minutes for the whole planet
220-
CREATE MATERIALIZED VIEW IF NOT EXISTS grouped_stations_with_importance AS
221-
SELECT
222-
-- Aggregated station columns
223-
array_agg(DISTINCT station_id ORDER BY station_id) as station_ids,
224-
hstore(string_agg(nullif(name_tags::text, ''), ',')) as name_tags,
225-
array_agg(osm_id ORDER BY osm_id) as osm_ids,
226-
array_agg(osm_type ORDER BY osm_id) as osm_types,
227-
array_remove(array_agg(DISTINCT s.operator ORDER BY s.operator), null) as operator,
228-
array_remove(array_agg(DISTINCT s.network ORDER BY s.network), null) as network,
229-
array_remove(string_to_array(array_to_string(array_agg(DISTINCT array_to_string(s.position, U&'\\001E')), U&'\\001E'), U&'\\001E'), null) as position,
230-
array_remove(array_agg(DISTINCT s.wikidata ORDER BY s.wikidata), null) as wikidata,
231-
array_remove(array_agg(DISTINCT s.wikimedia_commons ORDER BY s.wikimedia_commons), null) as wikimedia_commons,
232-
array_remove(array_agg(DISTINCT s.wikimedia_commons_file ORDER BY s.wikimedia_commons_file), null) as wikimedia_commons_file,
233-
array_remove(array_agg(DISTINCT s.wikipedia ORDER BY s.wikipedia), null) as wikipedia,
234-
array_remove(array_agg(DISTINCT s.image ORDER BY s.image), null) as image,
235-
array_remove(array_agg(DISTINCT s.mapillary ORDER BY s.mapillary), null) as mapillary,
236-
array_remove(array_agg(DISTINCT s.note ORDER BY s.note), null) as note,
237-
array_remove(array_agg(DISTINCT s.description ORDER BY s.description), null) as description,
238-
array_remove(string_to_array(array_to_string(array_agg(DISTINCT array_to_string(s.yard_purpose, U&'\\001E')), U&'\\001E'), U&'\\001E'), null) as yard_purpose,
239-
bool_or(s.yard_hump) as yard_hump,
240-
-- Aggregated importance
241-
max(sr.importance) as importance,
242-
-- Re-grouped clustered stations columns
243-
clustered.id as id,
244-
any_value(clustered.center) as center,
245-
any_value(clustered.buffered) as buffered,
246-
any_value(clustered.name) as name,
247-
any_value(clustered.station) as station,
248-
any_value(clustered.railway_ref) as railway_ref,
249-
any_value(clustered.uic_ref) as uic_ref,
250-
any_value(clustered.feature) as feature,
251-
any_value(clustered.state) as state,
252-
any_value(clustered.count) as count
253-
FROM (
254-
SELECT
255-
id,
256-
UNNEST(sc.station_ids) as station_id,
257-
name, station, railway_ref, uic_ref, feature, state, station_ids, center, buffered, count
258-
FROM stations_clustered sc
259-
) clustered
260-
JOIN stations s
261-
ON clustered.station_id = s.id
262-
JOIN stations_with_importance sr
263-
ON clustered.station_id = sr.id
264-
GROUP BY clustered.id;
265-
266-
CREATE INDEX IF NOT EXISTS grouped_stations_with_importance_center_index
267-
ON grouped_stations_with_importance
268-
USING GIST(center);
269-
270-
CREATE INDEX IF NOT EXISTS grouped_stations_with_importance_buffered_index
271-
ON grouped_stations_with_importance
272-
USING GIST(buffered);
273-
274-
CREATE INDEX IF NOT EXISTS grouped_stations_with_importance_osm_ids_index
275-
ON grouped_stations_with_importance
276-
USING GIN(osm_ids);
277-
278-
CLUSTER grouped_stations_with_importance
279-
USING grouped_stations_with_importance_center_index;
280-
281-
CREATE MATERIALIZED VIEW IF NOT EXISTS stop_area_groups_buffered AS
282-
SELECT
283-
sag.osm_id,
284-
ST_Buffer(ST_ConvexHull(ST_RemoveRepeatedPoints(ST_Collect(gs.buffered))), 20) as way
285-
FROM stop_area_groups sag
286-
JOIN stop_areas sa
287-
ON ARRAY[sa.osm_id] <@ sag.stop_area_ref_ids
288-
JOIN stations s
289-
ON (ARRAY[s.osm_id] <@ sa.node_ref_ids AND s.osm_type = 'N')
290-
OR (ARRAY[s.osm_id] <@ sa.way_ref_ids AND s.osm_type = 'W')
291-
OR (ARRAY[s.osm_id] <@ sa.stop_ref_ids AND s.osm_type = 'N')
292-
JOIN (
293-
SELECT
294-
unnest(osm_ids) AS osm_id,
295-
unnest(osm_types) AS osm_type,
296-
buffered
297-
FROM grouped_stations_with_importance
298-
) gs
299-
ON s.osm_id = gs.osm_id and s.osm_type = gs.osm_type
300-
GROUP BY sag.osm_id
301-
-- Only use station area groups that have more than one station area
302-
HAVING COUNT(distinct sa.osm_id) > 1;
303-
304-
CREATE INDEX IF NOT EXISTS stop_area_groups_buffered_index
305-
ON stop_area_groups_buffered
306-
USING GIST(way);
307-
308-
CLUSTER stop_area_groups_buffered
309-
USING stop_area_groups_buffered_index;
154+
-- Not a materialized view because the Osm2Pgsql scripts update the discrete isolation values
155+
CREATE TABLE IF NOT EXISTS stations_with_importance (
156+
id BIGINT NOT NULL PRIMARY KEY,
157+
way GEOMETRY NOT NULL,
158+
importance NUMERIC NOT NULL DEFAULT 0,
159+
discr_iso REAL NOT NULL DEFAULT 0.0, -- Column name is fixed
160+
irank BIGINT NOT NULL DEFAULT 0, -- Column name is fixed
161+
dirank BIGINT NOT NULL DEFAULT 0 -- Column name is fixed
162+
);

0 commit comments

Comments
 (0)