kopia lustrzana https://github.com/onthegomap/planetiler
cities
rodzic
72756310cb
commit
f78cc18b01
|
@ -264,12 +264,12 @@ public class FeatureCollector implements Iterable<FeatureCollector.Feature> {
|
|||
return ZoomFunction.applyAsIntOrElse(labelGridLimit, zoom, DEFAULT_LABEL_GRID_LIMIT);
|
||||
}
|
||||
|
||||
private Feature setLabelGridPixelSizeFunction(ZoomFunction<Number> labelGridSize) {
|
||||
public Feature setLabelGridPixelSizeFunction(ZoomFunction<Number> labelGridSize) {
|
||||
this.labelGridPixelSize = labelGridSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
private Feature setLabelGridLimitFunction(ZoomFunction<Number> labelGridLimit) {
|
||||
public Feature setLabelGridLimitFunction(ZoomFunction<Number> labelGridLimit) {
|
||||
this.labelGridLimit = labelGridLimit;
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -1,26 +1,42 @@
|
|||
package com.onthegomap.flatmap.openmaptiles.layers;
|
||||
|
||||
import static com.onthegomap.flatmap.collections.FeatureGroup.Z_ORDER_BITS;
|
||||
import static com.onthegomap.flatmap.collections.FeatureGroup.Z_ORDER_MAX;
|
||||
import static com.onthegomap.flatmap.collections.FeatureGroup.Z_ORDER_MIN;
|
||||
import static com.onthegomap.flatmap.openmaptiles.Utils.coalesce;
|
||||
import static com.onthegomap.flatmap.openmaptiles.Utils.nullIfEmpty;
|
||||
import static com.onthegomap.flatmap.openmaptiles.Utils.nullOrEmpty;
|
||||
|
||||
import com.carrotsearch.hppc.LongIntHashMap;
|
||||
import com.carrotsearch.hppc.LongIntMap;
|
||||
import com.onthegomap.flatmap.Arguments;
|
||||
import com.onthegomap.flatmap.FeatureCollector;
|
||||
import com.onthegomap.flatmap.Parse;
|
||||
import com.onthegomap.flatmap.SourceFeature;
|
||||
import com.onthegomap.flatmap.Translations;
|
||||
import com.onthegomap.flatmap.VectorTileEncoder;
|
||||
import com.onthegomap.flatmap.ZoomFunction;
|
||||
import com.onthegomap.flatmap.geo.GeoUtils;
|
||||
import com.onthegomap.flatmap.geo.GeometryException;
|
||||
import com.onthegomap.flatmap.geo.PointIndex;
|
||||
import com.onthegomap.flatmap.geo.PolygonIndex;
|
||||
import com.onthegomap.flatmap.monitoring.Stats;
|
||||
import com.onthegomap.flatmap.openmaptiles.LanguageUtils;
|
||||
import com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile;
|
||||
import com.onthegomap.flatmap.openmaptiles.generated.OpenMapTilesSchema;
|
||||
import com.onthegomap.flatmap.openmaptiles.generated.Tables;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.DoubleStream;
|
||||
import java.util.stream.Stream;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -31,7 +47,9 @@ public class Place implements
|
|||
Tables.OsmCountryPoint.Handler,
|
||||
Tables.OsmStatePoint.Handler,
|
||||
Tables.OsmIslandPoint.Handler,
|
||||
Tables.OsmIslandPolygon.Handler {
|
||||
Tables.OsmIslandPolygon.Handler,
|
||||
Tables.OsmCityPoint.Handler,
|
||||
OpenMapTilesProfile.FeaturePostProcessor {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Place.class);
|
||||
|
||||
|
@ -44,8 +62,18 @@ public class Place implements
|
|||
}
|
||||
}
|
||||
|
||||
private static record NaturalEarthPoint(String name, String wikidata, int scaleRank, Set<String> names) {}
|
||||
|
||||
private PolygonIndex<NaturalEarthRegion> countries = PolygonIndex.create();
|
||||
private PolygonIndex<NaturalEarthRegion> states = PolygonIndex.create();
|
||||
private PointIndex<NaturalEarthPoint> cities = PointIndex.create();
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
countries = null;
|
||||
states = null;
|
||||
cities = null;
|
||||
}
|
||||
|
||||
public Place(Translations translations, Arguments args, Stats stats) {
|
||||
this.translations = translations;
|
||||
|
@ -72,6 +100,15 @@ public class Place implements
|
|||
));
|
||||
}
|
||||
}
|
||||
case "ne_10m_populated_places" -> cities.put(feature.worldGeometry(), new NaturalEarthPoint(
|
||||
feature.getString("name"),
|
||||
feature.getString("wikidataid"),
|
||||
(int) feature.getLong("scalerank"),
|
||||
Stream.of("name", "namealt", "meganame", "gn_ascii", "nameascii").map(feature::getString)
|
||||
.filter(Objects::nonNull)
|
||||
.map(s -> s.toLowerCase(Locale.ROOT))
|
||||
.collect(Collectors.toSet())
|
||||
));
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
LOGGER
|
||||
|
@ -201,4 +238,133 @@ public class Place implements
|
|||
.setAttr(Fields.RANK, 7)
|
||||
.setZoomRange(12, 14);
|
||||
}
|
||||
|
||||
private static final Set<String> majorCityPlaces = Set.of("city", "town", "village");
|
||||
private static final double CITY_JOIN_DISTANCE = GeoUtils.metersToPixelAtEquator(0, 50_000) / 256d;
|
||||
|
||||
enum PlaceType {
|
||||
CITY("city"),
|
||||
TOWN("town"),
|
||||
VILLAGE("village"),
|
||||
HAMLET("hamlet"),
|
||||
SUBURB("suburb"),
|
||||
QUARTER("quarter"),
|
||||
NEIGHBORHOOD("neighbourhood"),
|
||||
ISOLATED_DWELLING("isolated_dwelling"),
|
||||
UNKNOWN("unknown");
|
||||
|
||||
private final String name;
|
||||
private static final Map<String, PlaceType> byName = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (PlaceType place : values()) {
|
||||
byName.put(place.name, place);
|
||||
}
|
||||
}
|
||||
|
||||
PlaceType(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public static PlaceType forName(String name) {
|
||||
return byName.getOrDefault(name, UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int Z_ORDER_RANK_BITS = 4;
|
||||
private static final int Z_ORDER_PLACE_BITS = 4;
|
||||
private static final int Z_ORDER_LENGTH_BITS = 5;
|
||||
private static final int Z_ORDER_POPULATION_BITS = Z_ORDER_BITS -
|
||||
(Z_ORDER_RANK_BITS + Z_ORDER_PLACE_BITS + Z_ORDER_LENGTH_BITS);
|
||||
private static final int Z_ORDER_POPULATION_RANGE = (1 << Z_ORDER_POPULATION_BITS) - 1;
|
||||
private static final double LOG_MAX_POPULATION = Math.log(100_000_000d);
|
||||
|
||||
// order by rank asc, place asc, population desc, name.length asc
|
||||
static int getZorder(Integer rank, PlaceType place, long population, String name) {
|
||||
int zorder = rank == null ? 0 : Math.max(1, 15 - rank);
|
||||
zorder = (zorder << Z_ORDER_PLACE_BITS) | (place == null ? 0 : Math.max(1, 15 - place.ordinal()));
|
||||
double logPop = Math.min(LOG_MAX_POPULATION, Math.log(population));
|
||||
zorder = (zorder << Z_ORDER_POPULATION_BITS) | Math.max(0, Math.min(Z_ORDER_POPULATION_RANGE,
|
||||
(int) Math.round(logPop * Z_ORDER_POPULATION_RANGE / LOG_MAX_POPULATION)));
|
||||
zorder = (zorder << Z_ORDER_LENGTH_BITS) | (name == null ? 0 : Math.max(1, 31 - name.length()));
|
||||
|
||||
return zorder + Z_ORDER_MIN;
|
||||
}
|
||||
|
||||
private static final ZoomFunction<Number> LABEL_GRID_LIMITS = ZoomFunction.fromMaxZoomThresholds(Map.of(
|
||||
8, 4,
|
||||
9, 8,
|
||||
10, 12,
|
||||
12, 14
|
||||
), 0);
|
||||
|
||||
@Override
|
||||
public void process(Tables.OsmCityPoint element, FeatureCollector features) {
|
||||
Integer rank = null;
|
||||
if (majorCityPlaces.contains(element.place())) {
|
||||
try {
|
||||
Point point = element.source().worldGeometry().getCentroid();
|
||||
List<NaturalEarthPoint> neCities = cities.getWithin(point, CITY_JOIN_DISTANCE);
|
||||
String rawName = coalesce(element.name(), "");
|
||||
String name = coalesce(rawName, "").toLowerCase(Locale.ROOT);
|
||||
String nameEn = coalesce(element.nameEn(), "").toLowerCase(Locale.ROOT);
|
||||
String normalizedName = StringUtils.stripAccents(rawName);
|
||||
String wikidata = element.source().getString("wikidata", "");
|
||||
for (var neCity : neCities) {
|
||||
if (wikidata.equals(neCity.wikidata) ||
|
||||
neCity.names.contains(name) ||
|
||||
neCity.names.contains(nameEn) ||
|
||||
normalizedName.equals(neCity.name)) {
|
||||
rank = neCity.scaleRank <= 5 ? neCity.scaleRank + 1 : neCity.scaleRank;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (GeometryException e) {
|
||||
LOGGER.warn("Unable to get area for OSM city " + element.source().id() + ": " + e);
|
||||
}
|
||||
}
|
||||
|
||||
String capital = element.capital();
|
||||
|
||||
PlaceType placeType = PlaceType.forName(element.place());
|
||||
|
||||
int minzoom = rank != null && rank == 1 ? 2 :
|
||||
rank != null && rank <= 8 ? Math.max(3, rank - 1) :
|
||||
placeType.ordinal() <= PlaceType.TOWN.ordinal() ? 7 :
|
||||
placeType.ordinal() <= PlaceType.VILLAGE.ordinal() ? 8 :
|
||||
placeType.ordinal() <= PlaceType.SUBURB.ordinal() ? 11 : 14;
|
||||
|
||||
var feature = features.point(LAYER_NAME).setBufferPixels(BUFFER_SIZE)
|
||||
.setAttrs(LanguageUtils.getNames(element.source().properties(), translations))
|
||||
.setAttr(Fields.CLASS, element.place())
|
||||
.setAttr(Fields.RANK, rank)
|
||||
.setZoomRange(minzoom, 14)
|
||||
.setZorder(getZorder(rank, placeType, element.population(), element.name()))
|
||||
.setLabelGridPixelSize(12, 128);
|
||||
|
||||
if (rank == null) {
|
||||
feature.setLabelGridLimitFunction(LABEL_GRID_LIMITS);
|
||||
}
|
||||
|
||||
if ("2".equals(capital) || "yes".equals(capital)) {
|
||||
feature.setAttr(Fields.CAPITAL, 2);
|
||||
} else if ("4".equals(capital)) {
|
||||
feature.setAttr(Fields.CAPITAL, 4);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VectorTileEncoder.Feature> postProcess(int zoom,
|
||||
List<VectorTileEncoder.Feature> items) throws GeometryException {
|
||||
LongIntMap groupCounts = new LongIntHashMap();
|
||||
for (int i = items.size() - 1; i >= 0; i--) {
|
||||
VectorTileEncoder.Feature feature = items.get(i);
|
||||
int gridrank = groupCounts.getOrDefault(feature.group(), 1);
|
||||
groupCounts.put(feature.group(), gridrank + 1);
|
||||
if (!feature.attrs().containsKey(Fields.RANK)) {
|
||||
feature.attrs().put(Fields.RANK, 10 + gridrank);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,16 @@ package com.onthegomap.flatmap.openmaptiles.layers;
|
|||
|
||||
import static com.onthegomap.flatmap.TestUtils.newPoint;
|
||||
import static com.onthegomap.flatmap.TestUtils.rectangle;
|
||||
import static com.onthegomap.flatmap.collections.FeatureGroup.Z_ORDER_MAX;
|
||||
import static com.onthegomap.flatmap.collections.FeatureGroup.Z_ORDER_MIN;
|
||||
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.NATURAL_EARTH_SOURCE;
|
||||
import static com.onthegomap.flatmap.openmaptiles.OpenMapTilesProfile.OSM_SOURCE;
|
||||
import static com.onthegomap.flatmap.openmaptiles.layers.Place.getZorder;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import com.onthegomap.flatmap.geo.GeoUtils;
|
||||
import com.onthegomap.flatmap.geo.GeometryException;
|
||||
import com.onthegomap.flatmap.read.ReaderFeature;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -200,7 +206,7 @@ public class PlaceTest extends AbstractLayerTest {
|
|||
|
||||
"_type", "point",
|
||||
"_minzoom", 12
|
||||
)), process(lineFeature(
|
||||
)), process(pointFeature(
|
||||
Map.of(
|
||||
"place", "island",
|
||||
"name", "Nantucket",
|
||||
|
@ -245,4 +251,236 @@ public class PlaceTest extends AbstractLayerTest {
|
|||
"name:en", "Nantucket"
|
||||
))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlaceZorderRanking() {
|
||||
int[] zorders = new int[]{
|
||||
// max
|
||||
getZorder(0, Place.PlaceType.CITY, 1_000_000_000, "name"),
|
||||
|
||||
getZorder(0, Place.PlaceType.CITY, 1_000_000_000, "name longer"),
|
||||
getZorder(0, Place.PlaceType.CITY, 1_000_000_000, "x".repeat(32)),
|
||||
|
||||
getZorder(0, Place.PlaceType.CITY, 10_000_000, "name"),
|
||||
getZorder(0, Place.PlaceType.CITY, 0, "name"),
|
||||
|
||||
getZorder(0, Place.PlaceType.TOWN, 1_000_000_000, "name"),
|
||||
getZorder(0, Place.PlaceType.ISOLATED_DWELLING, 1_000_000_000, "name"),
|
||||
getZorder(0, null, 1_000_000_000, "name"),
|
||||
|
||||
getZorder(1, Place.PlaceType.CITY, 1_000_000_000, "name"),
|
||||
getZorder(10, Place.PlaceType.CITY, 1_000_000_000, "name"),
|
||||
getZorder(null, Place.PlaceType.CITY, 1_000_000_000, "name"),
|
||||
|
||||
// min
|
||||
getZorder(null, null, 0, null),
|
||||
};
|
||||
for (int i = 0; i < zorders.length; i++) {
|
||||
if (zorders[i] < Z_ORDER_MIN) {
|
||||
fail("Item at index " + i + " is < " + Z_ORDER_MIN + ": " + zorders[i]);
|
||||
}
|
||||
if (zorders[i] > Z_ORDER_MAX) {
|
||||
fail("Item at index " + i + " is > " + Z_ORDER_MAX + ": " + zorders[i]);
|
||||
}
|
||||
}
|
||||
assertDescending(zorders);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCountryCapital() {
|
||||
process(new ReaderFeature(
|
||||
newPoint(0, 0),
|
||||
Map.of(
|
||||
"name", "Washington, D.C.",
|
||||
"scalerank", 0,
|
||||
"wikidataid", "Q61"
|
||||
),
|
||||
NATURAL_EARTH_SOURCE,
|
||||
"ne_10m_populated_places",
|
||||
0
|
||||
));
|
||||
assertFeatures(7, List.of(Map.of(
|
||||
"_layer", "place",
|
||||
"class", "city",
|
||||
"name", "Washington, D.C.",
|
||||
"rank", 1,
|
||||
"capital", 2,
|
||||
"_labelgrid_limit", 0,
|
||||
"_labelgrid_size", 128d,
|
||||
|
||||
"_type", "point",
|
||||
"_minzoom", 2
|
||||
)), process(pointFeature(
|
||||
Map.of(
|
||||
"place", "city",
|
||||
"name", "Washington, D.C.",
|
||||
"population", "672228",
|
||||
"wikidata", "Q61",
|
||||
"capital", "yes"
|
||||
))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStateCapital() {
|
||||
process(new ReaderFeature(
|
||||
newPoint(0, 0),
|
||||
Map.of(
|
||||
"name", "Boston",
|
||||
"scalerank", 2,
|
||||
"wikidataid", "Q100"
|
||||
),
|
||||
NATURAL_EARTH_SOURCE,
|
||||
"ne_10m_populated_places",
|
||||
0
|
||||
));
|
||||
assertFeatures(0, List.of(Map.of(
|
||||
"_layer", "place",
|
||||
"class", "city",
|
||||
"name", "Boston",
|
||||
"rank", 3,
|
||||
"capital", 4,
|
||||
|
||||
"_type", "point",
|
||||
"_minzoom", 3
|
||||
)), process(pointFeature(
|
||||
Map.of(
|
||||
"place", "city",
|
||||
"name", "Boston",
|
||||
"population", "667137",
|
||||
"capital", "4"
|
||||
))));
|
||||
// no match when far away
|
||||
assertFeatures(0, List.of(Map.of(
|
||||
"_layer", "place",
|
||||
"class", "city",
|
||||
"name", "Boston",
|
||||
"rank", "<null>"
|
||||
)), process(new ReaderFeature(
|
||||
newPoint(1, 1),
|
||||
Map.of(
|
||||
"place", "city",
|
||||
"name", "Boston",
|
||||
"wikidata", "Q100",
|
||||
"population", "667137",
|
||||
"capital", "4"
|
||||
),
|
||||
OSM_SOURCE,
|
||||
null,
|
||||
0
|
||||
)));
|
||||
// unaccented name match
|
||||
assertFeatures(0, List.of(Map.of(
|
||||
"_layer", "place",
|
||||
"class", "city",
|
||||
"rank", 3
|
||||
)), process(pointFeature(
|
||||
Map.of(
|
||||
"place", "city",
|
||||
"name", "Böston",
|
||||
"population", "667137",
|
||||
"capital", "4"
|
||||
))));
|
||||
// wikidata only match
|
||||
assertFeatures(0, List.of(Map.of(
|
||||
"_layer", "place",
|
||||
"class", "city",
|
||||
"rank", 3
|
||||
)), process(pointFeature(
|
||||
Map.of(
|
||||
"place", "city",
|
||||
"name", "Other name",
|
||||
"population", "667137",
|
||||
"wikidata", "Q100",
|
||||
"capital", "4"
|
||||
))));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testCityWithoutNaturalEarthMatch() {
|
||||
assertFeatures(7, List.of(Map.of(
|
||||
"_layer", "place",
|
||||
"class", "city",
|
||||
"rank", "<null>",
|
||||
"_minzoom", 7,
|
||||
"_labelgrid_limit", 4,
|
||||
"_labelgrid_size", 128d
|
||||
)), process(pointFeature(
|
||||
Map.of(
|
||||
"place", "city",
|
||||
"name", "City name"
|
||||
))));
|
||||
assertFeatures(13, List.of(Map.of(
|
||||
"_layer", "place",
|
||||
"class", "isolated_dwelling",
|
||||
"rank", "<null>",
|
||||
"_labelgrid_limit", 0,
|
||||
"_labelgrid_size", 0d,
|
||||
"_minzoom", 14
|
||||
)), process(pointFeature(
|
||||
Map.of(
|
||||
"place", "isolated_dwelling",
|
||||
"name", "City name"
|
||||
))));
|
||||
assertFeatures(12, List.of(Map.of(
|
||||
"_layer", "place",
|
||||
"class", "isolated_dwelling",
|
||||
"rank", "<null>",
|
||||
"_labelgrid_limit", 14,
|
||||
"_labelgrid_size", 128d,
|
||||
"_minzoom", 14
|
||||
)), process(pointFeature(
|
||||
Map.of(
|
||||
"place", "isolated_dwelling",
|
||||
"name", "City name"
|
||||
))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCitySetRankFromGridrank() throws GeometryException {
|
||||
var layerName = Place.LAYER_NAME;
|
||||
assertEquals(List.of(), profile.postProcessLayerFeatures(layerName, 13, List.of()));
|
||||
|
||||
assertEquals(List.of(pointFeature(
|
||||
layerName,
|
||||
Map.of("rank", 11),
|
||||
1
|
||||
)), profile.postProcessLayerFeatures(layerName, 13, List.of(pointFeature(
|
||||
layerName,
|
||||
Map.of(),
|
||||
1
|
||||
))));
|
||||
|
||||
assertEquals(List.of(
|
||||
pointFeature(
|
||||
layerName,
|
||||
Map.of("rank", 12, "name", "a"),
|
||||
1
|
||||
), pointFeature(
|
||||
layerName,
|
||||
Map.of("rank", 11, "name", "b"),
|
||||
1
|
||||
), pointFeature(
|
||||
layerName,
|
||||
Map.of("rank", 11, "name", "c"),
|
||||
2
|
||||
)
|
||||
), profile.postProcessLayerFeatures(layerName, 13, List.of(
|
||||
pointFeature(
|
||||
layerName,
|
||||
Map.of("name", "a"),
|
||||
1
|
||||
),
|
||||
pointFeature(
|
||||
layerName,
|
||||
Map.of("name", "b"),
|
||||
1
|
||||
),
|
||||
pointFeature(
|
||||
layerName,
|
||||
Map.of("name", "c"),
|
||||
2
|
||||
)
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue