diff --git a/baremaps-core/src/main/java/org/apache/baremaps/tasks/UpdateOsmDatabase.java b/baremaps-core/src/main/java/org/apache/baremaps/tasks/UpdateOsmDatabase.java index 51d125a02..5ab92f0b7 100644 --- a/baremaps-core/src/main/java/org/apache/baremaps/tasks/UpdateOsmDatabase.java +++ b/baremaps-core/src/main/java/org/apache/baremaps/tasks/UpdateOsmDatabase.java @@ -158,12 +158,20 @@ static void execute( new ChangeEntitiesHandler(buildRelationGeometry.andThen(reprojectRelationGeometry)); var importRelations = new ChangeElementsImporter<>(Relation.class, relationRepository); + var buildBoundaryGeometry = new RelationBoundaryBuilder(coordinateMap, referenceMap); + var reprojectBoundaryGeometry = new EntityProjectionTransformer(4326, databaseSrid); + var prepareBoundaryGeometry = + new ChangeEntitiesHandler(buildBoundaryGeometry.andThen(reprojectBoundaryGeometry)); + var importBoundaries = new ChangeElementsImporter<>(Relation.class, relationRepository); + var entityProcessor = prepareNodeGeometry .andThen(importNodes) .andThen(prepareWayGeometry) .andThen(importWays) .andThen(prepareRelationGeometry) - .andThen(importRelations); + .andThen(importRelations) + .andThen(prepareBoundaryGeometry) + .andThen(importBoundaries); try (var changeInputStream = new GZIPInputStream(new BufferedInputStream(changeUrl.openStream()))) { diff --git a/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/function/EntityGeometryBuilder.java b/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/function/EntityGeometryBuilder.java index 7c1080d58..423dc5d5b 100644 --- a/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/function/EntityGeometryBuilder.java +++ b/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/function/EntityGeometryBuilder.java @@ -34,6 +34,7 @@ public class EntityGeometryBuilder implements Consumer { private final Consumer nodeGeometryBuilder; private final Consumer wayGeometryBuilder; private final Consumer relationMultiPolygonBuilder; + private final Consumer relationBoundaryBuilder; /** * Constructs a consumer that uses the provided caches to create and set geometries. @@ -47,6 +48,7 @@ public EntityGeometryBuilder( this.nodeGeometryBuilder = new NodeGeometryBuilder(); this.wayGeometryBuilder = new WayGeometryBuilder(coordinateMap); this.relationMultiPolygonBuilder = new RelationMultiPolygonBuilder(coordinateMap, referenceMap); + this.relationBoundaryBuilder = new RelationBoundaryBuilder(coordinateMap, referenceMap); } /** @@ -71,7 +73,7 @@ public void accept(Entity entity) { } else if (entity instanceof Way way) { wayGeometryBuilder.accept(way); } else if (entity instanceof Relation relation && isMultiPolygon(relation)) { - relationMultiPolygonBuilder.accept(relation); + relationMultiPolygonBuilder.andThen(relationBoundaryBuilder).accept(relation); } } diff --git a/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/function/RelationBoundaryBuilder.java b/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/function/RelationBoundaryBuilder.java new file mode 100644 index 000000000..47bcdd8a4 --- /dev/null +++ b/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/function/RelationBoundaryBuilder.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.baremaps.openstreetmap.function; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.apache.baremaps.openstreetmap.model.Entity; +import org.apache.baremaps.openstreetmap.model.Relation; +import org.apache.baremaps.openstreetmap.utils.GeometryUtils; +import org.locationtech.jts.geom.*; +import org.locationtech.jts.geom.util.GeometryCombiner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RelationBoundaryBuilder implements Consumer { + + private static final Logger logger = LoggerFactory.getLogger(RelationBoundaryBuilder.class); + + private final Map coordinateMap; + private final Map> referenceMap; + + public RelationBoundaryBuilder(Map coordinateMap, + Map> referenceMap) { + this.coordinateMap = coordinateMap; + this.referenceMap = referenceMap; + } + + @Override + public void accept(final Entity entity) { + if (!(entity instanceof Relation relation)) { + return; + } + if (!relation.getTags().containsKey("boundary")) { + return; + } + try { + long start = System.currentTimeMillis(); + buildBoundary(relation); + long end = System.currentTimeMillis(); + long duration = end - start; + if (duration > 60 * 1000) { + logger.debug("Relation #{} processed in {} ms", relation.getId(), duration); + } + } catch (Exception e) { + logger.error("Error processing relation #" + relation.getId(), e); + } + } + + public void buildBoundary(Relation relation) { + List geometries = relation.getMembers().stream() + .map(member -> switch (member.type()) { + case NODE -> { + Coordinate coord = coordinateMap.get(member.ref()); + yield (coord != null) ? GeometryUtils.GEOMETRY_FACTORY_WGS84.createPoint(coord) : null; + } + case WAY -> { + List nodeIds = referenceMap.get(member.ref()); + if (nodeIds == null || nodeIds.isEmpty()) { + yield null; + } + Coordinate[] coords = nodeIds.stream() + .map(coordinateMap::get) + .filter(Objects::nonNull) + .toArray(Coordinate[]::new); + if (coords.length < 2) { + yield null; + } + if (coords[0].equals2D(coords[coords.length - 1]) && coords.length >= 4) { + LinearRing ring = GeometryUtils.GEOMETRY_FACTORY_WGS84.createLinearRing(coords); + yield GeometryUtils.GEOMETRY_FACTORY_WGS84.createPolygon(ring); + } else { + yield GeometryUtils.GEOMETRY_FACTORY_WGS84.createLineString(coords); + } + } + case RELATION -> null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + Geometry finalGeometry; + if (geometries.isEmpty()) { + finalGeometry = GeometryUtils.GEOMETRY_FACTORY_WGS84.createGeometryCollection(new Geometry[]{}); + } else { + Geometry combinedGeometry = GeometryCombiner.combine(geometries); + if (combinedGeometry instanceof Polygon) { + finalGeometry = GeometryUtils.GEOMETRY_FACTORY_WGS84.createMultiPolygon(new Polygon[]{(Polygon) combinedGeometry}); + } else if (combinedGeometry instanceof LineString) { + finalGeometry = GeometryUtils.GEOMETRY_FACTORY_WGS84.createMultiLineString(new LineString[]{(LineString) combinedGeometry}); + } else if (combinedGeometry instanceof GeometryCollection && combinedGeometry.getNumGeometries() == 1) { + Geometry single = combinedGeometry.getGeometryN(0); + if (single instanceof Polygon) { + finalGeometry = GeometryUtils.GEOMETRY_FACTORY_WGS84.createMultiPolygon(new Polygon[]{(Polygon) single}); + } else if (single instanceof LineString) { + finalGeometry = GeometryUtils.GEOMETRY_FACTORY_WGS84.createMultiLineString(new LineString[]{(LineString) single}); + } else { + finalGeometry = combinedGeometry; + } + } else if (combinedGeometry instanceof MultiPolygon || combinedGeometry instanceof MultiLineString) { + finalGeometry = combinedGeometry; + } else { + finalGeometry = GeometryUtils.GEOMETRY_FACTORY_WGS84.createGeometryCollection(new Geometry[]{combinedGeometry}); + } + } + relation.setGeometry(finalGeometry); + } +} diff --git a/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/function/RelationMultiPolygonBuilder.java b/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/function/RelationMultiPolygonBuilder.java index e406d5924..c0f08d518 100644 --- a/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/function/RelationMultiPolygonBuilder.java +++ b/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/function/RelationMultiPolygonBuilder.java @@ -62,6 +62,9 @@ public RelationMultiPolygonBuilder( @Override public void accept(Entity entity) { if (entity instanceof Relation relation) { + if (relation.getTags().containsKey("boundary")) { + return; + } try { var start = System.currentTimeMillis(); diff --git a/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/model/Relation.java b/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/model/Relation.java index 03496cc31..5b94914e8 100644 --- a/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/model/Relation.java +++ b/baremaps-openstreetmap/src/main/java/org/apache/baremaps/openstreetmap/model/Relation.java @@ -17,8 +17,6 @@ package org.apache.baremaps.openstreetmap.model; - - import java.util.List; import java.util.Map; import java.util.Objects; diff --git a/basemap/layers/boundary/line.js b/basemap/layers/boundary/line.js index 4d2572c04..f2cb1fa8d 100644 --- a/basemap/layers/boundary/line.js +++ b/basemap/layers/boundary/line.js @@ -55,7 +55,24 @@ export default asLayerObject(withSortKeys(directives), { visibility: 'visible', }, paint: { - 'line-dasharray': [4, 1, 1, 1], + 'line-width': [ + 'interpolate', + ['exponential', 1], + ['zoom'], + 0, 0.5, + 10, [ + 'case', + ['==', ['get', 'maritime'], 'yes'], + 2, // If maritime is 'yes' + [ + 'case', + ['==', ['get', 'admin_level'], '4'], + 1, // If admin_level is '4' + 3 // For all other cases + ] + ], + 20, 5 + ] }, filter: ['==', ["geometry-type"], 'LineString'], }); diff --git a/basemap/layers/boundary/tileset.js b/basemap/layers/boundary/tileset.js index 66a990ca2..d885e4486 100644 --- a/basemap/layers/boundary/tileset.js +++ b/basemap/layers/boundary/tileset.js @@ -18,10 +18,36 @@ export default { id: 'boundary', queries: [ { - minzoom: 13, + minzoom: 7, maxzoom: 20, sql: - "SELECT id, tags, geom FROM osm_boundary", + "SELECT id, tags, ST_SimplifyPreserveTopology(geom, 0.01) AS geom FROM osm_boundary", }, + { + minzoom: 0, + maxzoom: 20, + sql: + "SELECT id, tags, ST_SimplifyPreserveTopology(geom, 0.01) AS geom " + + "FROM osm_boundary " + + "WHERE tags ->> 'admin_level' = '2' " + + "AND COALESCE(tags ->> 'maritime', '') != 'yes';", + }, + { + minzoom: 3.5, + maxzoom: 20, + sql: + "SELECT id, tags, ST_SimplifyPreserveTopology(geom, 0.01) AS geom " + + "FROM osm_boundary " + + "WHERE tags ->> 'admin_level' = '2' " + + "AND tags ->> 'maritime' = 'yes';", + }, + { + minzoom: 3.5, + maxzoom: 20, + sql: + "SELECT id, tags, ST_SimplifyPreserveTopology(geom, 0.01) AS geom " + + "FROM osm_boundary " + + "WHERE tags ->> 'admin_level' = '4';" + } ], -} +} \ No newline at end of file