Skip to content

Commit de7b426

Browse files
authored
Merge pull request #56 from geoscript/ecc-vector-tile
Switch pbf vector tile libraries and fix build for JTS 1.17.0
2 parents f6cd081 + 6ea1a38 commit de7b426

File tree

7 files changed

+71
-132
lines changed

7 files changed

+71
-132
lines changed

examples/vectortiles_generate_world_pbf.groovy

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import static geoscript.GeoScript.unzip
1212

1313
// Download data from natural earth
1414
File dataDir = new File("naturalearth")
15+
dataDir.mkdir()
1516
[
16-
[name: "countries", url: "http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_admin_0_countries.zip"],
17-
[name: "ocean", url: "http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/physical/ne_110m_ocean.zip"]
17+
[name: "countries", url: "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_admin_0_countries.zip"],
18+
[name: "ocean", url: "https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/physical/ne_110m_ocean.zip"]
1819
].each { Map item ->
1920
unzip(download(new URL(item.url), new File(dataDir, "${item.name}.zip"), overwrite: false))
2021
}

examples/vectortiles_world_pbf.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
<!doctype html>
22
<html lang="en">
33
<head>
4-
<link rel="stylesheet" href="https://openlayers.org/en/v4.5.0/css/ol.css" type="text/css">
4+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v4.6.5/css/ol.css" type="text/css">
55
<style>
66
.map {
77
height: 400px;
88
width: 100%;
99
}
1010
</style>
11-
<script src="https://openlayers.org/en/v4.5.0/build/ol.js" type="text/javascript"></script>
11+
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v4.6.5/build/ol.js" type="text/javascript"></script>
1212
<title>GeoScript Vector Tiles</title>
1313
</head>
1414
<body>

pom.xml

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,10 @@
3232
</repositories>
3333
<dependencies>
3434
<dependency>
35-
<groupId>com.wdtinc</groupId>
36-
<artifactId>mapbox-vector-tile</artifactId>
37-
<version>3.0.0</version>
35+
<groupId>no.ecc.vectortile</groupId>
36+
<artifactId>java-vector-tile</artifactId>
37+
<version>1.3.10</version>
3838
<exclusions>
39-
<!-- Exclude JTS so geotools can pull in it's version -->
40-
<exclusion>
41-
<groupId>org.locationtech.jts</groupId>
42-
<artifactId>jts-core</artifactId>
43-
</exclusion>
44-
<!-- Exclude protobuf here so gt-geobuf can pull in a more recent version -->
4539
<exclusion>
4640
<groupId>com.google.protobuf</groupId>
4741
<artifactId>protobuf-java</artifactId>
Lines changed: 38 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,14 @@
11
package geoscript.layer.io
22

3-
import org.locationtech.jts.algorithm.CGAlgorithms
3+
import no.ecc.vectortile.VectorTileDecoder
4+
import no.ecc.vectortile.VectorTileEncoder
5+
import org.geotools.geometry.jts.JTS
6+
import org.geotools.referencing.operation.transform.ProjectiveTransform
7+
import org.geotools.renderer.lite.RendererUtilities
48
import org.locationtech.jts.geom.Coordinate
59
import org.locationtech.jts.geom.Envelope
610
import org.locationtech.jts.geom.Geometry as JtsGeometry
711
import org.locationtech.jts.geom.GeometryFactory
8-
import org.locationtech.jts.geom.LinearRing
9-
import org.locationtech.jts.geom.Polygon
10-
import com.wdtinc.mapbox_vector_tile.VectorTile
11-
import com.wdtinc.mapbox_vector_tile.adapt.jts.IGeometryFilter
12-
import com.wdtinc.mapbox_vector_tile.adapt.jts.JtsAdapter
13-
import com.wdtinc.mapbox_vector_tile.adapt.jts.TagKeyValueMapConverter
14-
import com.wdtinc.mapbox_vector_tile.adapt.jts.TileGeomResult
15-
import com.wdtinc.mapbox_vector_tile.adapt.jts.UserDataKeyValueMapConverter
16-
import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsLayer
17-
import com.wdtinc.mapbox_vector_tile.adapt.jts.model.JtsMvt
18-
import com.wdtinc.mapbox_vector_tile.adapt.jts.MvtReader as JtsMvtReader
19-
import com.wdtinc.mapbox_vector_tile.build.MvtLayerBuild
20-
import com.wdtinc.mapbox_vector_tile.build.MvtLayerParams
21-
import com.wdtinc.mapbox_vector_tile.build.MvtLayerProps
2212
import geoscript.feature.Feature
2313
import geoscript.feature.Field
2414
import geoscript.feature.Schema
@@ -30,6 +20,9 @@ import geoscript.proj.Projection
3020
import geoscript.workspace.Memory
3121
import org.geotools.renderer.crs.ProjectionHandler
3222
import org.geotools.renderer.crs.ProjectionHandlerFinder
23+
import org.opengis.referencing.operation.MathTransform
24+
25+
import java.awt.Rectangle
3326

3427
/**
3528
* A MapBox Vector Tile Reader and Writer
@@ -47,42 +40,39 @@ class Pbf {
4740
* @param b The Bounds
4841
*/
4942
static List<Layer> read(Map options = [:], byte[] bytes, Bounds b) {
43+
44+
ProjectionHandler projectionHandler = ProjectionHandlerFinder.getHandler(b.env, b.proj.crs, true)
5045
Projection proj = options.get("proj", new Projection("EPSG:3857"))
5146
int tileSize = options.get("tileSize", 256)
5247
int extent = options.get("extent", 4096)
53-
List<Layer> layers = []
54-
GeometryFactory geometryFactory = new GeometryFactory()
55-
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes)
56-
JtsMvt jtsMvt = JtsMvtReader.loadMvt(inputStream, geometryFactory, new TagKeyValueMapConverter(), new NonValidatingRingClassifier())
5748

58-
jtsMvt.layers.each { JtsLayer jtsLayer ->
59-
String name = jtsLayer.name
60-
Layer layer
61-
Schema schema
62-
jtsLayer.geometries.eachWithIndex { JtsGeometry jtsGeometry, int i ->
63-
Geometry geometry = fromPixel(Geometry.wrap(jtsGeometry), b, tileSize, extent)
64-
ProjectionHandler projectionHandler = ProjectionHandlerFinder.getHandler(b.env, b.proj.crs, true)
49+
Map<String, Layer> layers = [:]
50+
VectorTileDecoder vectorTileDecoder = new VectorTileDecoder()
51+
VectorTileDecoder.FeatureIterable features = vectorTileDecoder.decode(bytes)
52+
features.iterator().each { VectorTileDecoder.Feature vectorTileFeature ->
53+
String layerName = vectorTileFeature.layerName
54+
Geometry geometry = fromPixel(Geometry.wrap(vectorTileFeature.geometry), b, tileSize, extent)
55+
if (!geometry.isEmpty()) {
6556
if (projectionHandler) {
6657
JtsGeometry processedGeom = projectionHandler.preProcess(geometry.g)
6758
if (processedGeom) {
6859
geometry = Geometry.wrap(processedGeom)
6960
}
7061
}
71-
Map properties = jtsGeometry.userData as Map
72-
properties.put("geometry", geometry)
73-
Feature feature
74-
if (!schema) {
75-
feature = new Feature(properties, "1")
76-
schema = new Schema(name, feature.schema.fields).reproject(proj)
77-
layer = new Memory().create(schema)
78-
} else {
79-
feature = schema.feature(properties)
62+
Map<String, Object> attributes = [geometry: geometry]
63+
attributes.putAll(vectorTileFeature.attributes)
64+
if (!layers.containsKey(layerName)) {
65+
Feature feature = new Feature(attributes, "1")
66+
Schema schema = new Schema(layerName, feature.schema.fields).reproject(proj)
67+
layers[layerName] = new Memory().create(schema)
8068
}
69+
Layer layer = layers[layerName]
70+
Feature feature = layer.schema.feature(attributes)
8171
layer.add(feature)
8272
}
83-
layers.add(layer)
8473
}
85-
layers
74+
75+
layers.values().toList()
8676
}
8777

8878
/**
@@ -126,34 +116,27 @@ class Pbf {
126116
final double bufferHeight = b.env.height * 0.1f
127117
clipEnvelope.expandBy(bufferWidth, bufferHeight)
128118

129-
VectorTile.Tile.Builder tileBuilder = VectorTile.Tile.newBuilder()
130-
MvtLayerParams mvtParams = MvtLayerParams.DEFAULT
131-
GeometryFactory geometryFactory = new GeometryFactory()
132-
IGeometryFilter geometryFilter = new IGeometryFilter() {
133-
@Override
134-
boolean accept(org.locationtech.jts.geom.Geometry geometry) {
135-
true
136-
}
137-
}
138-
119+
VectorTileEncoder encoder = new VectorTileEncoder()
139120
layers.each { Layer layer ->
140121
Bounds projectedBounds = b.reproject(layer.proj)
141122
projectedBounds.expandBy(projectedBounds.width * 0.1f)
142123
Geometry boundsGeom = projectedBounds.geometry
143124
boolean isPoint = layer.schema.geom.typ.equalsIgnoreCase("point")
144-
VectorTile.Tile.Layer.Builder layerBuilder = MvtLayerBuild.newLayerBuilder(layer.name, mvtParams)
145-
MvtLayerProps layerProps = new MvtLayerProps()
125+
// Intersects
146126
List<JtsGeometry> geometries = []
147127
layer.eachFeature(Filter.intersects(layer.schema.geom.name, boundsGeom), { Feature f ->
128+
// Clip
148129
Geometry geom = isPoint ? f.geom : f.geom.intersection(boundsGeom)
149130
if (!geom.empty) {
131+
// Process
150132
ProjectionHandler projectionHandler = ProjectionHandlerFinder.getHandler(b.env, layer.proj.crs, true)
151133
if (projectionHandler) {
152134
JtsGeometry processedGeom = projectionHandler.preProcess(geom.g)
153135
if (processedGeom) {
154136
geom = Geometry.wrap(processedGeom)
155137
}
156138
}
139+
// Reproject
157140
JtsGeometry geometry = Projection.transform(geom, layer.proj, b.proj).g
158141
Map attributes = [:]
159142
layer.schema.fields.each { Field fld ->
@@ -168,72 +151,16 @@ class Pbf {
168151
}
169152
}
170153
geometry.userData = attributes
171-
172-
TileGeomResult tileGeom = JtsAdapter.createTileGeom([geometry], b.env, clipEnvelope, geometryFactory, mvtParams, geometryFilter);
173-
geometries.addAll(tileGeom.mvtGeoms)
154+
// To Screen
155+
MathTransform worldToScreenTransform = ProjectiveTransform.create(RendererUtilities.worldToScreenTransform(b.env, new Rectangle(0,0, tileSize, tileSize)))
156+
geometry = JTS.transform(geometry, worldToScreenTransform)
157+
//Encode
158+
encoder.addFeature(layer.name, attributes, geometry)
174159
}
175160
})
176-
177-
List<VectorTile.Tile.Feature> features = JtsAdapter.toFeatures(geometries, layerProps, new UserDataKeyValueMapConverter())
178-
layerBuilder.addAllFeatures(features);
179-
MvtLayerBuild.writeProps(layerBuilder, layerProps);
180-
181-
VectorTile.Tile.Layer vtLayer = layerBuilder.build()
182-
tileBuilder.addLayers(vtLayer)
183-
}
184-
185-
VectorTile.Tile tile = tileBuilder.build()
186-
byte[] bytes = tile.toByteArray()
187-
bytes
188-
}
189-
190-
private static final class NonValidatingRingClassifier implements com.wdtinc.mapbox_vector_tile.adapt.jts.MvtReader.RingClassifier {
191-
192-
@Override
193-
public List<Polygon> classifyRings(List<LinearRing> rings, GeometryFactory geomFactory) {
194-
195-
final List<Polygon> polygons = new ArrayList<>();
196-
final List<LinearRing> holes = new ArrayList<>();
197-
198-
double outerArea = 0d;
199-
LinearRing outerPoly = null;
200-
201-
for(LinearRing r : rings) {
202-
203-
double area = CGAlgorithms.signedArea(r.getCoordinates());
204-
205-
if(area == 0d) {
206-
continue; // zero-area
207-
}
208-
209-
if(area > 0d) {
210-
if(outerPoly != null) {
211-
polygons.add(geomFactory.createPolygon(outerPoly, holes.toArray(new LinearRing[holes.size()])));
212-
holes.clear();
213-
}
214-
215-
// Pos --> CCW, Outer
216-
outerPoly = r;
217-
outerArea = area;
218-
} else {
219-
220-
if(Math.abs(outerArea) < Math.abs(area)) {
221-
continue; // Holes must have less area, could probably be handled in a isSimple() check
222-
}
223-
224-
// Neg --> CW, Hole
225-
holes.add(r);
226-
}
227-
}
228-
229-
if(outerPoly != null) {
230-
holes.toArray();
231-
polygons.add(geomFactory.createPolygon(outerPoly, holes.toArray(new LinearRing[holes.size()])));
232-
}
233-
234-
return polygons;
235161
}
236162

163+
encoder.encode()
237164
}
238165

239166
}

src/test/groovy/geoscript/geom/GeometryTestCase.groovy

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,18 @@ class GeometryTestCase {
2828
}
2929

3030
@Test void singleSidedBuffer() {
31-
String wkt = "LINESTRING (0.693359375 46.591796875, 5.703125 51.337890625, 9.306640625 48.0419921875, 12.03125 53.0078125, 19.2822265625 45.80078125)"
31+
String wkt = "LINESTRING (0.693359375 46.591796875, 5.703125 51.337890625, 9.306640625 48.0419921875, " +
32+
"12.03125 53.0078125, 19.2822265625 45.80078125)"
3233
Geometry g = Geometry.fromWKT(wkt)
3334
Geometry buffer = g.singleSidedBuffer(-2)
34-
assertEquals "POLYGON ((0.693359375 46.591796875, 5.703125 51.337890625, 9.306640625 48.0419921875, 12.03125 53.0078125, 19.2822265625 45.80078125, 17.8723180343991 44.38227571867897, 12.506275490911024 49.71579679220651, 11.060054369792876 47.07994216823311, 10.832564011572027 46.749104791663626, 10.543576944652168 46.470372468507726, 10.20473972588441 46.25497847602753, 9.82970793845354 46.11160347389942, 9.43359585401137 46.04602566227302, 9.032367307756616 46.06088791274607, 8.642192334992414 46.155591257181634, 8.278795497549897 46.32631902690761, 7.9568221633754534 46.566190669457164, 5.727082494228705 48.605586708310895, 2.0688486708215663 45.139891507188345, 0.693359375 46.591796875))", buffer.wkt
35+
assertEquals "POLYGON ((0.693359375 46.591796875, 5.703125 51.337890625, 9.306640625 48.0419921875, " +
36+
"12.03125 53.0078125, 19.2822265625 45.80078125, 17.8723180343991 44.38227571867897, 12.506275490911024 " +
37+
"49.71579679220651, 11.060054369792876 47.07994216823311, 10.832564011572027 46.749104791663626, " +
38+
"10.543576944652168 46.470372468507726, 10.20473972588441 46.25497847602753, 9.82970793845354 " +
39+
"46.11160347389942, 9.43359585401137 46.04602566227302, 9.032367307756616 46.06088791274607, " +
40+
"8.642192334992412 46.155591257181634, 8.278795497549897 46.32631902690761, 7.9568221633754534 " +
41+
"46.566190669457164, 5.727082494228705 48.605586708310895, 2.0688486708215663 45.139891507188345, " +
42+
"0.693359375 46.591796875))", buffer.wkt
3543
}
3644

3745
@Test void equals() {

src/test/groovy/geoscript/geom/MultiLineStringTestCase.groovy

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,10 @@ class MultiLineStringTestCase {
3636
def lines = new MultiLineString(Geometry.fromWKT(wkt))
3737
def noded = lines.node(5)
3838
assertEquals "MULTILINESTRING ((5.19775390625 51.07421875, 5.6 51.6), (5.6 51.6, 6.4 52.6), " +
39-
"(6.4 52.6, 7.52685546875 53.7548828125), (7.52685546875 53.7548828125, 8.2 53.2), " +
40-
"(8.2 53.2, 9 52.4), (9 52.4, 11.65771484375 49.931640625), (11.65771484375 49.931640625, " +
41-
"7.52685546875 47.20703125), (7.52685546875 47.20703125, 9 52.4), (9 52.4, 9.50439453125 " +
42-
"54.501953125), (9.50439453125 54.501953125, 8.2 53.2), (8.2 53.2, 7.35107421875 52.4365234375), " +
43-
"(7.35107421875 52.4365234375, 6.4 52.6), (6.4 52.6, 4.53857421875 52.65625), (4.53857421875 " +
44-
"52.65625, 5.6 51.6), (5.6 51.6, 6.38427734375 50.634765625))", noded.wkt
39+
"(6.4 52.6, 7.52685546875 53.7548828125, 8.2 53.2), (8.2 53.2, 9 52.4), " +
40+
"(9 52.4, 11.65771484375 49.931640625, 7.52685546875 47.20703125, 9 52.4), " +
41+
"(9 52.4, 9.50439453125 54.501953125, 8.2 53.2), (8.2 53.2, 7.35107421875 52.4365234375, 6.4 52.6), " +
42+
"(6.4 52.6, 4.53857421875 52.65625, 5.6 51.6), (5.6 51.6, 6.38427734375 50.634765625))", noded.wkt
4543
}
4644

4745
@Test void merge() {

src/test/groovy/geoscript/layer/io/PbfTestCase.groovy

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package geoscript.layer.io
22

3+
import geoscript.feature.Feature
34
import geoscript.feature.Field
45
import geoscript.feature.Schema
56
import geoscript.geom.Bounds
@@ -21,6 +22,16 @@ import static org.junit.Assert.*
2122
*/
2223
class PbfTestCase {
2324

25+
@Test void read() {
26+
URL url = getClass().getClassLoader().getResource("pbf/1/1/0.pbf")
27+
28+
Pyramid pyramid = Pyramid.createGlobalMercatorPyramid()
29+
Bounds bounds = pyramid.bounds(new Tile(1, 1, 0))
30+
31+
List<Layer> layers = Pbf.read(url.bytes, bounds)
32+
assertTrue layers.size() > 0
33+
}
34+
2435
@Test void writeRead() {
2536
URL url = getClass().getClassLoader().getResource("states.shp")
2637
Layer layer = new Shapefile(new File(url.toURI()))

0 commit comments

Comments
 (0)