pull/1/head
Mike Barry 2021-07-15 06:52:44 -04:00
rodzic 72756310cb
commit f78cc18b01
3 zmienionych plików z 408 dodań i 4 usunięć

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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
)
)));
}
}