Skip to content

Commit 33e4228

Browse files
committed
Workaround for inconsistent casing in Geopackage column names
1 parent 3e61f42 commit 33e4228

3 files changed

Lines changed: 70 additions & 9 deletions

File tree

tiles/src/main/java/com/protomaps/basemap/Basemap.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ static void run(Arguments args) throws IOException {
313313
.addGeoPackageSource("landcover", sourcesDir.resolve("daylight-landcover.gpkg"),
314314
"https://r2-public.protomaps.com/datasets/daylight-landcover.gpkg");
315315
}
316+
316317
Path pgfEncodingZip = sourcesDir.resolve("pgf-encoding.zip");
317318
Path qrankCsv = sourcesDir.resolve("qrank.csv.gz");
318319

tiles/src/main/java/com/protomaps/basemap/layers/Boundaries.java

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.protomaps.basemap.feature.FeatureId;
1313
import java.util.ArrayList;
1414
import java.util.List;
15+
import java.util.Locale;
1516
import java.util.OptionalInt;
1617

1718
@SuppressWarnings("java:S1192")
@@ -25,25 +26,40 @@ public String name() {
2526
return LAYER_NAME;
2627
}
2728

29+
/**
30+
* Reads a Natural Earth attribute, falling back to the upper case spelling of the column.
31+
* <p>
32+
* Column name casing is not consistent across the Natural Earth distributions: the sqlite package is entirely lower
33+
* case, while the GeoPackage package preserves whatever the source shapefiles use. That is lower case for most
34+
* physical layers but upper case for the admin layers this handler reads, and some tables mix both.
35+
*/
36+
private static String getNeString(SourceFeature sf, String key) {
37+
Object value = sf.getTag(key);
38+
if (value == null) {
39+
value = sf.getTag(key.toUpperCase(Locale.ROOT));
40+
}
41+
return value == null ? null : value.toString();
42+
}
43+
2844
public void processNe(SourceFeature sf, FeatureCollector features) {
2945
String sourceLayer = sf.getSourceLayer();
3046
String kind = "";
3147
int adminLevel = 2;
3248
boolean disputed = false;
33-
int themeMinZoom = 0;
3449
int themeMaxZoom = 0;
3550

3651
if (sourceLayer.equals("ne_10m_admin_0_boundary_lines_land") ||
3752
sourceLayer.equals("ne_10m_admin_0_boundary_lines_map_units") ||
3853
sourceLayer.equals("ne_10m_admin_0_boundary_lines_disputed_areas") ||
3954
sourceLayer.equals("ne_10m_admin_1_states_provinces_lines")) {
40-
themeMinZoom = 4;
4155
themeMaxZoom = 5;
4256
kind = "tz_boundary";
4357
}
4458

59+
String featureClass = getNeString(sf, "featurecla");
60+
4561
if (!kind.isEmpty()) {
46-
switch (sf.getString("featurecla", "")) {
62+
switch (featureClass == null ? "" : featureClass) {
4763
case "Disputed (please verify)" -> {
4864
kind = "country";
4965
disputed = true;
@@ -130,19 +146,19 @@ public void processNe(SourceFeature sf, FeatureCollector features) {
130146
}
131147
}
132148

133-
if (sf.canBeLine() && sf.getString("min_zoom") != null && (!kind.isEmpty() && !kind.equals("tz_boundary"))) {
134-
var minZoom = Double.parseDouble(sf.getString("min_zoom")) - 1.0;
149+
String minZoomString = getNeString(sf, "min_zoom");
150+
151+
if (sf.canBeLine() && minZoomString != null && (!kind.isEmpty() && !kind.equals("tz_boundary"))) {
152+
var minZoom = Double.parseDouble(minZoomString) - 1.0;
135153
int sortRank = 289 - (disputed ? 1 : 0);
136154
features.line(this.name())
137155
.setAttr("kind", kind)
138156
.setAttr("kind_detail", adminLevel)
139157
.setAttr("sort_rank", sortRank)
140158
.setSortKey(sortRank)
141159
.setAttr("disputed", disputed ? true : null)
142-
.setAttr("brk_a3", sf.getString("brk_a3"))
143-
.setZoomRange(
144-
sf.getString("min_zoom") == null ? themeMinZoom : (int) minZoom,
145-
themeMaxZoom)
160+
.setAttr("brk_a3", getNeString(sf, "brk_a3"))
161+
.setZoomRange((int) minZoom, themeMaxZoom)
146162
.setMinPixelSize(0)
147163
.setBufferPixels(8);
148164
}

tiles/src/test/java/com/protomaps/basemap/layers/BoundariesTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,48 @@ void testDisputedOsmWay(String key, String value) {
103103
List.of(Map.of("disputed", true)),
104104
collector);
105105
}
106+
107+
// The Natural Earth GeoPackage spells the admin columns upper case and the sqlite package lower case,
108+
// so the same feature has to be read correctly either way.
109+
110+
@Test
111+
void testNeAdmin1LineGeoPackageCasing() {
112+
assertFeatures(5,
113+
List.of(Map.of("kind", "region", "kind_detail", 4, "brk_a3", "USA")),
114+
process(SimpleFeature.create(
115+
newLineString(0, 0, 1, 1),
116+
new HashMap<>(Map.of("FEATURECLA", "Admin-1 boundary", "MIN_ZOOM", 2.0, "BRK_A3", "USA")),
117+
"ne",
118+
"ne_10m_admin_1_states_provinces_lines",
119+
1
120+
)));
121+
}
122+
123+
@Test
124+
void testNeAdmin1LineSqliteCasing() {
125+
assertFeatures(5,
126+
List.of(Map.of("kind", "region", "kind_detail", 4, "brk_a3", "USA")),
127+
process(SimpleFeature.create(
128+
newLineString(0, 0, 1, 1),
129+
new HashMap<>(Map.of("featurecla", "Admin-1 boundary", "min_zoom", "2.0", "brk_a3", "USA")),
130+
"ne",
131+
"ne_10m_admin_1_states_provinces_lines",
132+
1
133+
)));
134+
}
135+
136+
// Regression test for the NullPointerException from switching the "ne" source to the GeoPackage:
137+
// FEATURECLA is present but featurecla is not, which used to blow up on a null switch expression.
138+
@Test
139+
void testNeAdmin1LineNullFeatureClass() {
140+
assertFeatures(5,
141+
List.of(),
142+
process(SimpleFeature.create(
143+
newLineString(0, 0, 1, 1),
144+
new HashMap<>(Map.of("MIN_ZOOM", 2.0)),
145+
"ne",
146+
"ne_10m_admin_1_states_provinces_lines",
147+
1
148+
)));
149+
}
106150
}

0 commit comments

Comments
 (0)