diff --git a/layers/landmarks/README.md b/layers/landmarks/README.md new file mode 100644 index 0000000..e180344 --- /dev/null +++ b/layers/landmarks/README.md @@ -0,0 +1,10 @@ +## lm + +### Docs +Read the layer documentation at **http://openmaptiles.org/schema#lm** + +### Mapping Diagram +![Mapping diagram for lm](mapping_diagram.png?raw=true) + +### ETL diagram +![ETL diagram for lm](etl_diagram.png?raw=true) diff --git a/layers/landmarks/class.sql b/layers/landmarks/class.sql new file mode 100644 index 0000000..6f8f747 --- /dev/null +++ b/layers/landmarks/class.sql @@ -0,0 +1,19 @@ +CREATE OR REPLACE FUNCTION lm_class_rank(class text) + RETURNS int AS +$$ +SELECT CASE class + WHEN 'forest' THEN 120 + ELSE 1000 + END; +$$ LANGUAGE SQL IMMUTABLE + PARALLEL SAFE; + +CREATE OR REPLACE FUNCTION lm_class(subclass text, mapping_key text) + RETURNS text AS +$$ +SELECT CASE + %%FIELD_MAPPING: class %% + ELSE subclass + END; +$$ LANGUAGE SQL IMMUTABLE + PARALLEL SAFE; diff --git a/layers/landmarks/etl_diagram.png b/layers/landmarks/etl_diagram.png new file mode 100644 index 0000000..bbabf80 Binary files /dev/null and b/layers/landmarks/etl_diagram.png differ diff --git a/layers/landmarks/landmark.yaml b/layers/landmarks/landmark.yaml new file mode 100644 index 0000000..857bc7d --- /dev/null +++ b/layers/landmarks/landmark.yaml @@ -0,0 +1,156 @@ +layer: + id: "lm" + description: | + [Points of interests](http://wiki.openstreetmap.org/wiki/Points_of_interest) containing + a of a variety of OpenStreetMap tags. Mostly contains amenities, sport, shop and tourist POIs. + buffer_size: 64 + srs: +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0.0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over + fields: + name: The OSM [`name`](http://wiki.openstreetmap.org/wiki/Key:name) value of the POI. + name_en: English name `name:en` if available, otherwise `name`. + name_de: German name `name:de` if available, otherwise `name` or `name:en`. + class: + description: | + More general classes of POIs. If there is no more general `class` for the `subclass` + this field will contain the same value as `subclass`. + But for example for schools you only need to style the class `school` to filter the subclasses `school` + and `kindergarten`. Or use the class `shop` to style all shops. + values: + shop: + subclass: ['accessories', 'antiques', 'beauty', 'bed', 'boutique', 'camera', 'carpet', 'charity', 'chemist', + 'coffee', 'computer', 'convenience', 'copyshop', 'cosmetics', 'garden_centre', 'doityourself', + 'erotic', 'electronics', 'fabric', 'florist', 'frozen_food', 'furniture', 'video_games', 'video', + 'general', 'gift', 'hardware', 'hearing_aids', 'hifi', 'ice_cream', 'interior_decoration', + 'jewelry', 'kiosk', 'lamps', 'mall', 'massage', 'motorcycle', 'mobile_phone', 'newsagent', + 'optician', 'outdoor', 'perfumery', 'perfume', 'pet', 'photo', 'second_hand', 'shoes', 'sports', + 'stationery', 'tailor', 'tattoo', 'ticket', 'tobacco', 'toys', 'travel_agency', 'watches', + 'weapons', 'wholesale'] + town_hall: + subclass: ['townhall', 'public_building', 'courthouse', 'community_centre'] + golf: + subclass: ['golf', 'golf_course', 'miniature_golf'] + fast_food: + subclass: ['fast_food', 'food_court'] + park: + subclass: ['park', 'bbq'] + bus: + subclass: ['bus_stop', 'bus_station'] + railway: + - __AND__: + subclass: 'station' + mapping_key: 'railway' + - subclass: ['halt', 'tram_stop', 'subway'] + aerialway: + __AND__: + subclass: 'station' + mapping_key: 'aerialway' + entrance: + subclass: ['subway_entrance', 'train_station_entrance'] + campsite: + subclass: ['camp_site', 'caravan_site'] + laundry: + subclass: ['laundry', 'dry_cleaning'] + grocery: + subclass: ['supermarket', 'deli', 'delicatessen', 'department_store', 'greengrocer', 'marketplace'] + library: + subclass: ['books', 'library'] + college: + subclass: ['university', 'college'] + lodging: + subclass: ['hotel', 'motel', 'bed_and_breakfast', 'guest_house', 'hostel', 'chalet', 'alpine_hut', 'dormitory'] + ice_cream: + subclass: ['chocolate', 'confectionery'] + post: + subclass: ['post_box', 'post_office'] + cafe: + subclass: ['cafe'] + school: + subclass: ['school', 'kindergarten'] + alcohol_shop: + subclass: ['alcohol', 'beverages', 'wine'] + bar: + subclass: ['bar', 'nightclub'] + harbor: + subclass: ['marina', 'dock'] + car: + subclass: ['car', 'car_repair', 'car_parts', 'taxi'] + hospital: + subclass: ['hospital', 'nursing_home', 'clinic'] + cemetery: + subclass: ['grave_yard', 'cemetery'] + attraction: + subclass: ['attraction', 'viewpoint'] + beer: + subclass: ['biergarten', 'pub'] + music: + subclass: ['music', 'musical_instrument'] + stadium: + subclass: ['american_football', 'stadium', 'soccer'] + art_gallery: + subclass: ['art', 'artwork', 'gallery', 'arts_centre'] + clothing_store: + subclass: ['bag', 'clothes'] + swimming: + subclass: ['swimming_area', 'swimming'] + castle: + subclass: ['castle', 'ruins'] + subclass: + description: | + Original value of either the + [`amenity`](http://wiki.openstreetmap.org/wiki/Key:amenity), + [`barrier`](http://wiki.openstreetmap.org/wiki/Key:barrier), + [`historic`](http://wiki.openstreetmap.org/wiki/Key:historic), + [`information`](http://wiki.openstreetmap.org/wiki/Key:information), + [`landuse`](http://wiki.openstreetmap.org/wiki/Key:landuse), + [`leisure`](http://wiki.openstreetmap.org/wiki/Key:leisure), + [`railway`](http://wiki.openstreetmap.org/wiki/Key:railway), + [`shop`](http://wiki.openstreetmap.org/wiki/Key:shop), + [`sport`](http://wiki.openstreetmap.org/wiki/Key:sport), + [`station`](http://wiki.openstreetmap.org/wiki/Key:station), + [`religion`](http://wiki.openstreetmap.org/wiki/Key:religion), + [`tourism`](http://wiki.openstreetmap.org/wiki/Key:tourism), + [`aerialway`](http://wiki.openstreetmap.org/wiki/Key:aerialway), + [`building`](http://wiki.openstreetmap.org/wiki/Key:building), + [`highway`](http://wiki.openstreetmap.org/wiki/Key:highway) + or [`waterway`](http://wiki.openstreetmap.org/wiki/Key:waterway) + tag. Use this to do more precise styling. + rank: | + The POIs are ranked ascending according to their importance within a grid. The `rank` value shows the + local relative importance of a POI within it's cell in the grid. This can be used to reduce label density at *z14*. + Since all POIs already need to be contained at *z14* you can use `less than rank=10` epxression to limit + LMs. At some point like *z17* you can show all LMs. + agg_stop: + description: | + Experimental feature! Indicates main platform of public transport + stops (buses, trams, and subways). Grouping of platforms is + implemented using + [`uic_ref`](http://wiki.openstreetmap.org/wiki/Key:uic_ref) tag that + is not used worldwide. + values: [1] + level: + description: | + Original value of [`level`](http://wiki.openstreetmap.org/wiki/Key:level) tag. + layer: + description: | + Original value of [`layer`](http://wiki.openstreetmap.org/wiki/Key:layer) tag. + indoor: + description: | + Original value of [`indoor`](http://wiki.openstreetmap.org/wiki/Key:indoor) tag. + values: + - 1 + datasource: + geometry_field: geometry + key_field: osm_id + key_field_as_attribute: no + srid: 900913 + query: (SELECT osm_id, geometry, name, name_en, name_de, {name_languages}, class, subclass, agg_stop, layer, level, indoor, rank FROM layer_lm(!bbox!, z(!scale_denominator!), !pixel_width!)) AS t +schema: + - ./public_transport_stop_type.sql + - ./class.sql + - ./lm_stop_agg.sql + - ./update_lm_polygon.sql + - ./update_lm_point.sql + - ./layer.sql +datasources: + - type: imposm3 + mapping_file: ./mapping.yaml diff --git a/layers/landmarks/layer.sql b/layers/landmarks/layer.sql new file mode 100644 index 0000000..b442735 --- /dev/null +++ b/layers/landmarks/layer.sql @@ -0,0 +1,90 @@ +-- etldoc: layer_lm[shape=record fillcolor=lightpink, style="rounded,filled", +-- etldoc: label="layer_lm | z12 | z13 | z14+" ] ; + +CREATE OR REPLACE FUNCTION layer_lm(bbox geometry, zoom_level integer, pixel_width numeric) + RETURNS TABLE + ( + osm_id bigint, + geometry geometry, + name text, + name_en text, + name_de text, + tags hstore, + class text, + subclass text, + agg_stop integer, + layer integer, + level integer, + indoor integer, + "rank" int + ) +AS +$$ +SELECT osm_id_hash AS osm_id, + geometry, + NULLIF(name, '') AS name, + COALESCE(NULLIF(name_en, ''), name) AS name_en, + COALESCE(NULLIF(name_de, ''), name, name_en) AS name_de, + tags, + lm_class(subclass, mapping_key) AS class, + subclass AS subclass, + agg_stop, + NULLIF(layer, 0) AS layer, + "level", + row_number() OVER ( + PARTITION BY LabelGrid(geometry, 100 * pixel_width) + ORDER BY CASE WHEN name = '' THEN 2000 ELSE lm_class_rank(lm_class(subclass, mapping_key)) END ASC + )::int AS "rank" +FROM ( + -- etldoc: osm_lm_point -> layer_lm:z12 + -- etldoc: osm_lm_point -> layer_lm:z13 + SELECT *, + osm_id * 10 AS osm_id_hash + FROM osm_lm_point + WHERE geometry && bbox + AND zoom_level BETWEEN 12 AND 13 + AND ((subclass = 'station' AND mapping_key = 'railway') + OR subclass IN ('halt', 'ferry_terminal')) + + UNION ALL + + -- etldoc: osm_lm_point -> layer_lm:z14_ + SELECT *, + osm_id * 10 AS osm_id_hash + FROM osm_lm_point + WHERE geometry && bbox + AND zoom_level >= 14 + + UNION ALL + + -- etldoc: osm_lm_polygon -> layer_lm:z12 + -- etldoc: osm_lm_polygon -> layer_lm:z13 + SELECT *, + NULL::integer AS agg_stop, + CASE + WHEN osm_id < 0 THEN -osm_id * 10 + 4 + ELSE osm_id * 10 + 1 + END AS osm_id_hash + FROM osm_lm_polygon + WHERE geometry && bbox + AND zoom_level BETWEEN 12 AND 13 + AND ((subclass = 'station' AND mapping_key = 'railway') + OR subclass IN ('halt', 'ferry_terminal')) + + UNION ALL + + -- etldoc: osm_lm_polygon -> layer_lm:z14_ + SELECT *, + NULL::integer AS agg_stop, + CASE + WHEN osm_id < 0 THEN -osm_id * 10 + 4 + ELSE osm_id * 10 + 1 + END AS osm_id_hash + FROM osm_lm_polygon + WHERE geometry && bbox + AND zoom_level >= 14 + ) AS lm_union +ORDER BY "rank" +$$ LANGUAGE SQL STABLE + PARALLEL SAFE; +-- TODO: Check if the above can be made STRICT -- i.e. if pixel_width could be NULL diff --git a/layers/landmarks/lm_stop_agg.sql b/layers/landmarks/lm_stop_agg.sql new file mode 100644 index 0000000..80497c3 --- /dev/null +++ b/layers/landmarks/lm_stop_agg.sql @@ -0,0 +1,31 @@ +DROP MATERIALIZED VIEW IF EXISTS osm_lm_stop_centroid CASCADE; +CREATE MATERIALIZED VIEW osm_lm_stop_centroid AS +( +SELECT uic_ref, + count(*) AS count, + CASE WHEN count(*) > 2 THEN ST_Centroid(ST_UNION(geometry)) END AS centroid +FROM osm_lm_point +WHERE nullif(uic_ref, '') IS NOT NULL + AND subclass IN ('bus_stop', 'bus_station', 'tram_stop', 'subway') +GROUP BY uic_ref +HAVING count(*) > 1 + ) /* DELAY_MATERIALIZED_VIEW_CREATION */; + +DROP MATERIALIZED VIEW IF EXISTS osm_lm_stop_rank CASCADE; +CREATE MATERIALIZED VIEW osm_lm_stop_rank AS +( +SELECT p.osm_id, +-- p.uic_ref, +-- p.subclass, + ROW_NUMBER() + OVER ( + PARTITION BY p.uic_ref + ORDER BY + p.subclass :: public_transport_stop_type NULLS LAST, + ST_Distance(c.centroid, p.geometry) + ) AS rk +FROM osm_lm_point p + INNER JOIN osm_lm_stop_centroid c ON (p.uic_ref = c.uic_ref) +WHERE subclass IN ('bus_stop', 'bus_station', 'tram_stop', 'subway') +ORDER BY p.uic_ref, rk + ) /* DELAY_MATERIALIZED_VIEW_CREATION */; diff --git a/layers/landmarks/mapping.yaml b/layers/landmarks/mapping.yaml new file mode 100644 index 0000000..8858548 --- /dev/null +++ b/layers/landmarks/mapping.yaml @@ -0,0 +1,77 @@ + +# imposm3 mapping file for https://github.com/osm2vectortiles/imposm3 +# Warning: this is not the official imposm3 + +# landuse values , see http://taginfo.openstreetmap.org/keys/landuse#values +def_lm_mapping_landuse: &lm_mapping_landuse + - forest + +def_poi_fields: &lm_fields + - name: osm_id + type: id + - name: geometry + type: geometry + - name: name + key: name + type: string + - name: name_en + key: name:en + type: string + - name: name_de + key: name:de + type: string + - name: tags + type: hstore_tags + - name: subclass + type: mapping_value + - name: mapping_key + type: mapping_key + - name: station + key: station + type: string + - name: funicular + key: funicular + type: string + - name: information + key: information + type: string + - name: uic_ref + key: uic_ref + type: string + - name: religion + key: religion + type: string + - name: level + key: level + type: integer + - name: indoor + key: indoor + type: bool + - name: layer + key: layer + type: integer + - name: sport + key: sport + type: string + +def_lm_mapping: &lm_mapping + landuse: *lm_mapping_landuse + +tables: + # etldoc: imposm3 -> osm_lm_point + lm_point: + type: point + columns: *lm_fields + filters: + require: + name: ["__any__"] + mapping: *lm_mapping + + # etldoc: imposm3 -> osm_lm_polygon + lm_polygon: + type: polygon + columns: *lm_fields + filters: + require: + name: ["__any__"] + mapping: *lm_mapping diff --git a/layers/landmarks/mapping_diagram.png b/layers/landmarks/mapping_diagram.png new file mode 100644 index 0000000..682751c Binary files /dev/null and b/layers/landmarks/mapping_diagram.png differ diff --git a/layers/landmarks/public_transport_stop_type.sql b/layers/landmarks/public_transport_stop_type.sql new file mode 100644 index 0000000..fa37541 --- /dev/null +++ b/layers/landmarks/public_transport_stop_type.sql @@ -0,0 +1,12 @@ +DO +$$ + BEGIN + IF NOT EXISTS(SELECT 1 + FROM pg_type + WHERE typname = 'public_transport_stop_type') THEN + CREATE TYPE public_transport_stop_type AS enum ( + 'subway', 'tram_stop', 'bus_station', 'bus_stop' + ); + END IF; + END +$$; diff --git a/layers/landmarks/update_lm_point.sql b/layers/landmarks/update_lm_point.sql new file mode 100644 index 0000000..3999a45 --- /dev/null +++ b/layers/landmarks/update_lm_point.sql @@ -0,0 +1,96 @@ +DROP TRIGGER IF EXISTS trigger_flag ON osm_lm_point; +DROP TRIGGER IF EXISTS trigger_refresh ON lm_point.updates; + +-- etldoc: osm_lm_point -> osm_lm_point +CREATE OR REPLACE FUNCTION update_osm_lm_point() RETURNS void AS +$$ +BEGIN + UPDATE osm_lm_point + SET subclass = 'subway' + WHERE station = 'subway' + AND subclass = 'station'; + + UPDATE osm_lm_point + SET subclass = 'halt' + WHERE funicular = 'yes' + AND subclass = 'station'; + + UPDATE osm_lm_point + SET tags = update_tags(tags, geometry) + WHERE COALESCE(tags->'name:latin', tags->'name:nonlatin', tags->'name_int') IS NULL; + +END; +$$ LANGUAGE plpgsql; + +SELECT update_osm_lm_point(); + +CREATE OR REPLACE FUNCTION update_osm_lm_point_agg() RETURNS void AS +$$ +BEGIN + UPDATE osm_lm_point p + SET agg_stop = CASE + WHEN p.subclass IN ('bus_stop', 'bus_station', 'tram_stop', 'subway') + THEN 1 + END; + + UPDATE osm_lm_point p + SET agg_stop = ( + CASE + WHEN p.subclass IN ('bus_stop', 'bus_station', 'tram_stop', 'subway') + AND r.rk IS NULL OR r.rk = 1 + THEN 1 + END) + FROM osm_lm_stop_rank r + WHERE p.osm_id = r.osm_id; + +END; +$$ LANGUAGE plpgsql; + +ALTER TABLE osm_lm_point + ADD COLUMN IF NOT EXISTS agg_stop integer DEFAULT NULL; +SELECT update_osm_lm_point_agg(); + +-- Handle updates + +CREATE SCHEMA IF NOT EXISTS lm_point; + +CREATE TABLE IF NOT EXISTS lm_point.updates +( + id serial PRIMARY KEY, + t text, + UNIQUE (t) +); +CREATE OR REPLACE FUNCTION lm_point.flag() RETURNS trigger AS +$$ +BEGIN + INSERT INTO lm_point.updates(t) VALUES ('y') ON CONFLICT(t) DO NOTHING; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION lm_point.refresh() RETURNS trigger AS +$$ +BEGIN + RAISE LOG 'Refresh lm_point'; + PERFORM update_osm_lm_point(); + REFRESH MATERIALIZED VIEW osm_lm_stop_centroid; + REFRESH MATERIALIZED VIEW osm_lm_stop_rank; + PERFORM update_osm_lm_point_agg(); + -- noinspection SqlWithoutWhere + DELETE FROM lm_point.updates; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trigger_flag + AFTER INSERT OR UPDATE OR DELETE + ON osm_lm_point + FOR EACH STATEMENT +EXECUTE PROCEDURE lm_point.flag(); + +CREATE CONSTRAINT TRIGGER trigger_refresh + AFTER INSERT + ON lm_point.updates + INITIALLY DEFERRED + FOR EACH ROW +EXECUTE PROCEDURE lm_point.refresh(); diff --git a/layers/landmarks/update_lm_polygon.sql b/layers/landmarks/update_lm_polygon.sql new file mode 100644 index 0000000..2eb81ae --- /dev/null +++ b/layers/landmarks/update_lm_polygon.sql @@ -0,0 +1,78 @@ +DROP TRIGGER IF EXISTS trigger_flag ON osm_lm_polygon; +DROP TRIGGER IF EXISTS trigger_refresh ON lm_polygon.updates; + +-- etldoc: osm_lm_polygon -> osm_lm_polygon + +CREATE OR REPLACE FUNCTION update_lm_polygon() RETURNS void AS +$$ +BEGIN + UPDATE osm_lm_polygon + SET geometry = + CASE + WHEN ST_NPoints(ST_ConvexHull(geometry)) = ST_NPoints(geometry) + THEN ST_Centroid(geometry) + ELSE ST_PointOnSurface(geometry) + END + WHERE ST_GeometryType(geometry) <> 'ST_Point'; + + UPDATE osm_lm_polygon + SET subclass = 'subway' + WHERE station = 'subway' + AND subclass = 'station'; + + UPDATE osm_lm_polygon + SET subclass = 'halt' + WHERE funicular = 'yes' + AND subclass = 'station'; + + UPDATE osm_lm_polygon + SET tags = update_tags(tags, geometry) + WHERE COALESCE(tags->'name:latin', tags->'name:nonlatin', tags->'name_int') IS NULL; + + ANALYZE osm_lm_polygon; +END; +$$ LANGUAGE plpgsql; + +SELECT update_lm_polygon(); + +-- Handle updates + +CREATE SCHEMA IF NOT EXISTS lm_polygon; + +CREATE TABLE IF NOT EXISTS lm_polygon.updates +( + id serial PRIMARY KEY, + t text, + UNIQUE (t) +); +CREATE OR REPLACE FUNCTION lm_polygon.flag() RETURNS trigger AS +$$ +BEGIN + INSERT INTO lm_polygon.updates(t) VALUES ('y') ON CONFLICT(t) DO NOTHING; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION lm_polygon.refresh() RETURNS trigger AS +$$ +BEGIN + RAISE LOG 'Refresh lm_polygon'; + PERFORM update_lm_polygon(); + -- noinspection SqlWithoutWhere + DELETE FROM lm_polygon.updates; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trigger_flag + AFTER INSERT OR UPDATE OR DELETE + ON osm_lm_polygon + FOR EACH STATEMENT +EXECUTE PROCEDURE lm_polygon.flag(); + +CREATE CONSTRAINT TRIGGER trigger_refresh + AFTER INSERT + ON lm_polygon.updates + INITIALLY DEFERRED + FOR EACH ROW +EXECUTE PROCEDURE lm_polygon.refresh(); diff --git a/layers/poi/mapping.yaml b/layers/poi/mapping.yaml index 4a1eb14..152aa46 100644 --- a/layers/poi/mapping.yaml +++ b/layers/poi/mapping.yaml @@ -95,7 +95,6 @@ def_poi_mapping_landuse: &poi_mapping_landuse - cemetery - reservoir - winter_sports - - forest # leisure values , see http://taginfo.openstreetmap.org/keys/leisure#values def_poi_mapping_leisure: &poi_mapping_leisure diff --git a/openmaptiles.yaml b/openmaptiles.yaml index f4900eb..a1be149 100644 --- a/openmaptiles.yaml +++ b/openmaptiles.yaml @@ -15,6 +15,7 @@ tileset: - layers/place/place.yaml - layers/housenumber/housenumber.yaml - layers/poi/poi.yaml + - layers/landmarks/landmark.yaml - layers/aerodrome_label/aerodrome_label.yaml name: OpenMapTiles version: 3.11.0