Skip to content

Commit 3b9c033

Browse files
authored
Render vector tiles from graph storage (graphhopper#1572)
* use 4 or 16 cells only for LocationIndexTree * make it possible to visualize boundaries of Quadtree and add few notes what's different to usual implementation * implement query(BBox), graphhopper#1324 * test and show how to properly user query(BBox) * make it clear what we can expect from the Visitor functionality; added EdgeVisitor * use query(Shape) instead of query(BBox) * example log: avoid immediateFlush config as it is confusing * make current maximum precision clear in javadoc * show tiles via graph exploration * made vector tiles much faster via loc_index branch * show less details for small zoom numbers and full geometry details for big zoom * make it possible to use speed in JavaScript and use Leaflet > 1.0; see https://gist.github.com/karussell/0720b3d3b297621c8bb10cf8c86b2906 * store name and filter based on speed not edge length * use separate MVTResource * try to fix openjdk12 download URL * Adds alternative z/x/y endpoint. Signed-off-by: easbar <[email protected]> * fix response application type * remove unnecessary isochrone stuff * integrate local mvt into GH maps demo * switch localhost to 127.0.0.1 * do remove roads from omniscale layer * updated main.js * fixed merge conflict for LocationIndexTree * include mvt test that reads tile from /mvt endpoint * revert changes in travis config * include various EncodedValues in response * advertise /mvt endpoint
1 parent 8744f0e commit 3b9c033

File tree

14 files changed

+395
-51
lines changed

14 files changed

+395
-51
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,4 @@ Here is a list of the more detailed features including a link to the documentati
205205
* Do [map matching](https://github.com/graphhopper/map-matching) with GraphHopper
206206
* Calculate [isochrones](./docs/web/api-doc.md#isochrone) with GraphHopper
207207
* Show path details [#1142](https://github.com/graphhopper/graphhopper/pull/1142)
208+
* GraphHopper can produce vector tiles for debugging purposes [#1572](https://github.com/graphhopper/graphhopper/pull/1572)

core/src/main/java/com/graphhopper/routing/util/EncodingManager.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -666,9 +666,6 @@ public void applyWayTags(ReaderWay way, EdgeIteratorState edge) {
666666
}
667667
}
668668

669-
/**
670-
* The returned list is never empty.
671-
*/
672669
public List<FlagEncoder> fetchEdgeEncoders() {
673670
return new ArrayList<FlagEncoder>(edgeEncoders);
674671
}

core/src/main/java/com/graphhopper/storage/index/LocationIndexTree.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ public class LocationIndexTree implements LocationIndex {
6666
private final int MAGIC_INT;
6767
private final NodeAccess nodeAccess;
6868
protected DistanceCalc distCalc = Helper.DIST_PLANE;
69-
protected SpatialKeyAlgo keyAlgo;
70-
int maxRegionSearch = 4;
69+
SpatialKeyAlgo keyAlgo;
70+
private int maxRegionSearch = 4;
7171
private DistanceCalc preciseDistCalc = Helper.DIST_EARTH;
7272
private int[] entries;
7373
private byte[] shifts;
@@ -138,7 +138,7 @@ void prepareAlgo() {
138138
equalNormedDelta = distCalc.calcNormalizedDist(0.1);
139139

140140
// now calculate the necessary maxDepth d for our current bounds
141-
// if we assume a minimum resolution like 0.5km for a leaf-tile
141+
// if we assume a minimum resolution like 0.5km for a leaf-tile
142142
// n^(depth/2) = toMeter(dLon) / minResolution
143143
BBox bounds = graph.getBounds();
144144
if (graph.getNodes() == 0)
@@ -349,8 +349,8 @@ IntArrayList getEntries() {
349349
/**
350350
* This method fills the set with stored node IDs from the given spatial key part (a latitude-longitude prefix).
351351
*/
352-
final void fillIDs(long keyPart, int intIndex, GHIntHashSet set, int depth) {
353-
long pointer = (long) intIndex << 2;
352+
final void fillIDs(long keyPart, int intPointer, GHIntHashSet set, int depth) {
353+
long pointer = (long) intPointer << 2;
354354
if (depth == entries.length) {
355355
int nextIntPointer = dataAccess.getInt(pointer);
356356
if (nextIntPointer < 0) {
@@ -385,7 +385,6 @@ final long createReverseKey(long key) {
385385
/**
386386
* calculate the distance to the nearest tile border for a given lat/lon coordinate in the
387387
* context of a spatial key tile.
388-
* <p>
389388
*/
390389
final double calculateRMin(double lat, double lon) {
391390
return calculateRMin(lat, lon, 0);
@@ -788,7 +787,7 @@ IntArrayList getResults() {
788787

789788
// Space efficient sorted integer set. Suited for only a few entries.
790789
static class SortedIntSet extends IntArrayList {
791-
public SortedIntSet(int capacity) {
790+
SortedIntSet(int capacity) {
792791
super(capacity);
793792
}
794793

core/src/test/java/com/graphhopper/storage/index/LocationIndexTreeTest.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,7 @@
3030
import com.graphhopper.util.shapes.GHPoint;
3131
import org.junit.Test;
3232

33-
import java.util.ArrayList;
34-
import java.util.Arrays;
35-
import java.util.Collections;
36-
import java.util.List;
33+
import java.util.*;
3734

3835
import static org.junit.Assert.*;
3936

isochrone/pom.xml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@
1616
<version>0.13-SNAPSHOT</version>
1717
</parent>
1818

19-
2019
<dependencies>
2120
<dependency>
2221
<groupId>com.graphhopper</groupId>
2322
<artifactId>graphhopper-reader-osm</artifactId>
2423
<version>${project.parent.version}</version>
2524
</dependency>
2625

26+
<dependency>
27+
<groupId>com.wdtinc</groupId>
28+
<artifactId>mapbox-vector-tile</artifactId>
29+
<version>3.1.0</version>
30+
</dependency>
31+
2732
<dependency>
2833
<groupId>org.slf4j</groupId>
2934
<artifactId>slf4j-log4j12</artifactId>
@@ -44,7 +49,6 @@
4449
<scope>test</scope>
4550
</dependency>
4651
</dependencies>
47-
4852
</project>
4953

5054

web-bundle/src/main/java/com/graphhopper/http/GraphHopperBundle.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ protected void configure() {
281281
if (configuration.getBool("web.change_graph.enabled", false)) {
282282
environment.jersey().register(ChangeGraphResource.class);
283283
}
284+
285+
environment.jersey().register(MVTResource.class);
284286
environment.jersey().register(NearestResource.class);
285287
environment.jersey().register(RouteResource.class);
286288
environment.jersey().register(IsochroneResource.class);

web-bundle/src/main/java/com/graphhopper/http/GraphHopperManaged.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public void start() {
7171
graphHopper.importOrLoad();
7272
logger.info("loaded graph at:" + graphHopper.getGraphHopperLocation()
7373
+ ", data_reader_file:" + graphHopper.getDataReaderFile()
74-
+ ", flag_encoders:" + graphHopper.getEncodingManager()
74+
+ ", encoded values:" + graphHopper.getEncodingManager().toEncodedValuesAsString()
7575
+ ", " + graphHopper.getGraphHopperStorage().toDetailsString());
7676
}
7777

web-bundle/src/main/java/com/graphhopper/resources/IsochroneResource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,4 +146,4 @@ private Response jsonSuccessResponse(Object result, float took) {
146146
info.put("took", Math.round(took * 1000));
147147
return Response.ok(json).build();
148148
}
149-
}
149+
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package com.graphhopper.resources;
2+
3+
import com.graphhopper.GraphHopper;
4+
import com.graphhopper.routing.profiles.*;
5+
import com.graphhopper.routing.util.DefaultEdgeFilter;
6+
import com.graphhopper.routing.util.EncodingManager;
7+
import com.graphhopper.storage.NodeAccess;
8+
import com.graphhopper.storage.index.LocationIndexTree;
9+
import com.graphhopper.util.*;
10+
import com.graphhopper.util.shapes.BBox;
11+
import com.wdtinc.mapbox_vector_tile.VectorTile;
12+
import com.wdtinc.mapbox_vector_tile.adapt.jts.IGeometryFilter;
13+
import com.wdtinc.mapbox_vector_tile.adapt.jts.JtsAdapter;
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.build.MvtLayerBuild;
17+
import com.wdtinc.mapbox_vector_tile.build.MvtLayerParams;
18+
import com.wdtinc.mapbox_vector_tile.build.MvtLayerProps;
19+
import org.locationtech.jts.geom.Coordinate;
20+
import org.locationtech.jts.geom.Envelope;
21+
import org.locationtech.jts.geom.GeometryFactory;
22+
import org.locationtech.jts.geom.LineString;
23+
import org.slf4j.Logger;
24+
import org.slf4j.LoggerFactory;
25+
26+
import javax.inject.Inject;
27+
import javax.servlet.http.HttpServletRequest;
28+
import javax.ws.rs.*;
29+
import javax.ws.rs.core.Context;
30+
import javax.ws.rs.core.MediaType;
31+
import javax.ws.rs.core.Response;
32+
import javax.ws.rs.core.UriInfo;
33+
import java.util.HashMap;
34+
import java.util.List;
35+
import java.util.Map;
36+
import java.util.concurrent.atomic.AtomicInteger;
37+
38+
@Path("mvt")
39+
public class MVTResource {
40+
41+
private static final Logger logger = LoggerFactory.getLogger(MVTResource.class);
42+
private static final MediaType PBF = new MediaType("application", "x-protobuf");
43+
private final GraphHopper graphHopper;
44+
private final EncodingManager encodingManager;
45+
46+
@Inject
47+
public MVTResource(GraphHopper graphHopper, EncodingManager encodingManager) {
48+
this.graphHopper = graphHopper;
49+
this.encodingManager = encodingManager;
50+
}
51+
52+
@GET
53+
@Path("{z}/{x}/{y}.mvt")
54+
@Produces("application/x-protobuf")
55+
public Response doGetXyz(
56+
@Context HttpServletRequest httpReq,
57+
@Context UriInfo uriInfo,
58+
@PathParam("z") int zInfo,
59+
@PathParam("x") int xInfo,
60+
@PathParam("y") int yInfo,
61+
@QueryParam(Parameters.DETAILS.PATH_DETAILS) List<String> pathDetails) {
62+
63+
if (zInfo <= 9) {
64+
VectorTile.Tile.Builder mvtBuilder = VectorTile.Tile.newBuilder();
65+
return Response.fromResponse(Response.ok(mvtBuilder.build().toByteArray(), PBF).build())
66+
.header("X-GH-Took", "0")
67+
.build();
68+
}
69+
70+
StopWatch totalSW = new StopWatch().start();
71+
Coordinate nw = num2deg(xInfo, yInfo, zInfo);
72+
Coordinate se = num2deg(xInfo + 1, yInfo + 1, zInfo);
73+
LocationIndexTree locationIndex = (LocationIndexTree) graphHopper.getLocationIndex();
74+
final NodeAccess na = graphHopper.getGraphHopperStorage().getNodeAccess();
75+
EdgeExplorer edgeExplorer = graphHopper.getGraphHopperStorage().createEdgeExplorer(DefaultEdgeFilter.ALL_EDGES);
76+
BBox bbox = new BBox(nw.x, se.x, se.y, nw.y);
77+
if (!bbox.isValid())
78+
throw new IllegalStateException("Invalid bbox " + bbox);
79+
80+
final GeometryFactory geometryFactory = new GeometryFactory();
81+
VectorTile.Tile.Builder mvtBuilder = VectorTile.Tile.newBuilder();
82+
final IGeometryFilter acceptAllGeomFilter = geometry -> true;
83+
final Envelope tileEnvelope = new Envelope(se, nw);
84+
final MvtLayerParams layerParams = new MvtLayerParams(256, 4096);
85+
final UserDataKeyValueMapConverter converter = new UserDataKeyValueMapConverter();
86+
if (!encodingManager.hasEncodedValue(RoadClass.KEY))
87+
throw new IllegalStateException("You need to configure GraphHopper to store road_class, e.g. graph.encoded_values: road_class,max_speed,... ");
88+
89+
final EnumEncodedValue<RoadClass> roadClassEnc = encodingManager.getEnumEncodedValue(RoadClass.KEY, RoadClass.class);
90+
final AtomicInteger edgeCounter = new AtomicInteger(0);
91+
// in toFeatures addTags of the converter is called and layerProps is filled with keys&values => those need to be stored in the layerBuilder
92+
// otherwise the decoding won't be successful and "undefined":"undefined" instead of "speed": 30 is the result
93+
final MvtLayerProps layerProps = new MvtLayerProps();
94+
final VectorTile.Tile.Layer.Builder layerBuilder = MvtLayerBuild.newLayerBuilder("roads", layerParams);
95+
96+
locationIndex.query(bbox, new LocationIndexTree.EdgeVisitor(edgeExplorer) {
97+
@Override
98+
public void onEdge(EdgeIteratorState edge, int nodeA, int nodeB) {
99+
LineString lineString;
100+
RoadClass rc = edge.get(roadClassEnc);
101+
if (zInfo >= 14) {
102+
PointList pl = edge.fetchWayGeometry(3);
103+
lineString = pl.toLineString(false);
104+
} else if (rc == RoadClass.MOTORWAY
105+
|| zInfo > 10 && (rc == RoadClass.PRIMARY || rc == RoadClass.TRUNK)
106+
|| zInfo > 11 && (rc == RoadClass.SECONDARY)
107+
|| zInfo > 12) {
108+
double lat = na.getLatitude(nodeA);
109+
double lon = na.getLongitude(nodeA);
110+
double toLat = na.getLatitude(nodeB);
111+
double toLon = na.getLongitude(nodeB);
112+
lineString = geometryFactory.createLineString(new Coordinate[]{new Coordinate(lon, lat), new Coordinate(toLon, toLat)});
113+
} else {
114+
// skip edge for certain zoom
115+
return;
116+
}
117+
118+
edgeCounter.incrementAndGet();
119+
Map<String, Object> map = new HashMap<>(2);
120+
map.put("name", edge.getName());
121+
for (String str : pathDetails) {
122+
// how to indicate an erroneous parameter?
123+
if (str.contains(",") || !encodingManager.hasEncodedValue(str))
124+
continue;
125+
126+
EncodedValue ev = encodingManager.getEncodedValue(str, EncodedValue.class);
127+
if (ev instanceof EnumEncodedValue)
128+
map.put(ev.getName(), edge.get((EnumEncodedValue) ev).toString());
129+
else if (ev instanceof DecimalEncodedValue)
130+
map.put(ev.getName(), edge.get((DecimalEncodedValue) ev));
131+
else if (ev instanceof BooleanEncodedValue)
132+
map.put(ev.getName(), edge.get((BooleanEncodedValue) ev));
133+
else if (ev instanceof IntEncodedValue)
134+
map.put(ev.getName(), edge.get((IntEncodedValue) ev));
135+
}
136+
137+
lineString.setUserData(map);
138+
139+
// doing some AffineTransformation
140+
TileGeomResult tileGeom = JtsAdapter.createTileGeom(lineString, tileEnvelope, geometryFactory, layerParams, acceptAllGeomFilter);
141+
List<VectorTile.Tile.Feature> features = JtsAdapter.toFeatures(tileGeom.mvtGeoms, layerProps, converter);
142+
layerBuilder.addAllFeatures(features);
143+
}
144+
145+
@Override
146+
public void onTile(BBox bbox, int depth) {
147+
}
148+
});
149+
150+
MvtLayerBuild.writeProps(layerBuilder, layerProps);
151+
mvtBuilder.addLayers(layerBuilder.build());
152+
byte[] bytes = mvtBuilder.build().toByteArray();
153+
totalSW.stop();
154+
logger.debug("took: " + totalSW.getSeconds() + ", edges:" + edgeCounter.get());
155+
return Response.ok(bytes, PBF).header("X-GH-Took", "" + totalSW.getSeconds() * 1000)
156+
.build();
157+
}
158+
159+
Coordinate num2deg(int xInfo, int yInfo, int zoom) {
160+
double n = Math.pow(2, zoom);
161+
double lonDeg = xInfo / n * 360.0 - 180.0;
162+
// unfortunately latitude numbers goes from north to south
163+
double latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * yInfo / n)));
164+
double latDeg = Math.toDegrees(latRad);
165+
return new Coordinate(lonDeg, latDeg);
166+
}
167+
}

web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
}
2525
},
2626
"dependencies": {
27+
"leaflet.vectorgrid": "1.3.0",
2728
"browserify": "16.2.0",
2829
"browserify-swap": "0.2.2",
2930
"d3": "5.9.1",

web/src/main/resources/assets/js/config/tileLayers.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ var lyrk = L.tileLayer('https://tiles.lyrk.org/' + (retinaTiles ? 'lr' : 'ls') +
1616
attribution: osmAttr + ', <a href="https://geodienste.lyrk.de/">Lyrk</a>'
1717
});
1818

19-
var omniscale = L.tileLayer('https://maps.omniscale.net/v2/' +osAPIKey + '/style.default' + (retinaTiles ? '/hq.true' : '') + '/{z}/{x}/{y}.png', {
19+
var omniscale = L.tileLayer('https://maps.omniscale.net/v2/' +osAPIKey + '/style.default/{z}/{x}/{y}.png' + (retinaTiles ? '?hq=true' : ''), {
2020
layers: 'osm',
2121
attribution: osmAttr + ', &copy; <a href="https://maps.omniscale.com/">Omniscale</a>'
2222
});
@@ -85,13 +85,67 @@ var availableTileLayers = {
8585
"OpenStreetMap.de": osmde
8686
};
8787

88+
var overlays;
89+
if(ghenv.environment === 'development') {
90+
var omniscaleGray = L.tileLayer('https://maps.omniscale.net/v2/' +osAPIKey + '/style.grayscale/layers.world,buildings,landusages,labels/{z}/{x}/{y}.png?' + (retinaTiles ? '&hq=true' : ''), {
91+
layers: 'osm',
92+
attribution: osmAttr + ', &copy; <a href="https://maps.omniscale.com/">Omniscale</a>'
93+
});
94+
availableTileLayers["Omniscale Dev"] = omniscaleGray;
95+
96+
require('leaflet.vectorgrid');
97+
overlays = {};
98+
overlays["Local MVT"] = L.vectorGrid.protobuf("http://127.0.0.1:8989/mvt/{z}/{x}/{y}.mvt?details=max_speed&details=road_class&details=road_environment", {
99+
rendererFactory: L.canvas.tile,
100+
maxZoom: 20,
101+
minZoom: 10,
102+
vectorTileLayerStyles: {
103+
'roads': function(properties, zoom) {
104+
var color, opacity = 1, weight = 1, radius = 2, rc = properties.road_class;
105+
// if(properties.speed < 30) console.log(properties)
106+
if(rc == "motorway") {
107+
color = '#dd504b'; // red
108+
weight = 3;
109+
radius = 6;
110+
} else if(rc == "primary" || rc == "trunk") {
111+
color = '#e2a012'; // orange
112+
weight = 2;
113+
radius = 6;
114+
} else if(rc == "secondary") {
115+
weight = 2;
116+
color = '#f7c913'; // yellow
117+
} else {
118+
if(zoom <= 11) {
119+
// color = "white";
120+
color = "#aaa5a7"
121+
opacity = 0.2;
122+
} else {
123+
color = "#aaa5a7"; /* gray */
124+
}
125+
}
126+
return {
127+
weight: weight,
128+
color: color,
129+
opacity: opacity,
130+
radius: radius
131+
}
132+
},
133+
},
134+
interactive: true // use true to make sure that this VectorGrid fires mouse/pointer events
135+
});
136+
}
137+
88138
module.exports.activeLayerName = "Omniscale";
89139
module.exports.defaultLayer = omniscale;
90140

91141
module.exports.getAvailableTileLayers = function () {
92142
return availableTileLayers;
93143
};
94144

145+
module.exports.getOverlays = function () {
146+
return overlays;
147+
};
148+
95149
module.exports.selectLayer = function (layerName) {
96150
var defaultLayer = availableTileLayers[layerName];
97151
if (!defaultLayer)

0 commit comments

Comments
 (0)