Skip to content

Commit

Permalink
feat: TopoJSON copmbining arcs with same OSM id to single geometries,…
Browse files Browse the repository at this point in the history
… properties replacing weight with speed/distance, fallback for cases where OSM ids are not available, using OSM way geometries (with option to turn off and get only node coordinates (beeline graph) instead)
  • Loading branch information
takb committed Dec 9, 2024
1 parent aff6244 commit efeecf8
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.heigit.ors.api.requests.common.APIRequest;
import org.heigit.ors.api.APIEnums;
import org.heigit.ors.api.requests.common.APIRequest;

import java.util.List;

Expand All @@ -17,6 +17,7 @@ public class ExportApiRequest extends APIRequest {
public static final String PARAM_BBOX = "bbox";
public static final String PARAM_PROFILE = "profile";
public static final String PARAM_FORMAT = "format";
public static final String PARAM_GEOMETRY = "geometry";

public static final String PARAM_DEBUG = "debug";

Expand All @@ -40,6 +41,10 @@ public class ExportApiRequest extends APIRequest {
@JsonProperty(PARAM_FORMAT)
private APIEnums.ExportResponseType responseType = APIEnums.ExportResponseType.JSON;

@Schema(name = PARAM_GEOMETRY, description = "Wether to return the exact geometry of the graph.", example = "true", defaultValue = "true")
@JsonProperty(PARAM_GEOMETRY)
private boolean geometry = true;

@Schema(name = PARAM_DEBUG, hidden = true)
@JsonProperty(PARAM_DEBUG)
private boolean debug;
Expand Down Expand Up @@ -89,4 +94,8 @@ public void setResponseType(APIEnums.ExportResponseType responseType) {
public APIEnums.ExportResponseType getResponseType() {
return responseType;
}

public boolean getGeometry() {
return geometry;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import java.io.Serializable;
import java.util.*;

import static org.heigit.ors.export.ExportResult.TopoGeometry;

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({"type", "transform", "objects", "arcs", "bbox"})
@Getter
Expand All @@ -33,70 +35,98 @@ public class TopoJsonExportResponse implements Serializable {
private List<Double> bbox = new ArrayList<>();

public static TopoJsonExportResponse fromExportResult(ExportResult exportResult, String topologyLayerName) {
List<Double> bbox = initializeBbox();
BBox bbox = new BBox();
LinkedList<Geometry> geometries = new LinkedList<>();
LinkedList<Arc> arcsLocal = new LinkedList<>();
int arcCount = 0;

Map<Integer, Coordinate> nodes = exportResult.getLocations();
for (Map.Entry<Pair<Integer, Integer>, Double> edgeWeight : exportResult.getEdgeWeights().entrySet()) {
Pair<Integer, Integer> fromTo = edgeWeight.getKey();
List<Double> from = getXY(nodes, fromTo.first);
List<Double> to = getXY(nodes, fromTo.second);
bbox = updateBbox(bbox, from, to);

// TODO: Add the correct geometry to the export result
LineString lineString = (LineString) exportResult.getEdgeExtras().get(fromTo).get("geometry");
// TODO: Can we decide to merge LineStrings into a single LineString based on osm_id?
// This would allow us to later just store two arcs for a single LineString for both directions.
long osmId = (long) exportResult.getEdgeExtras().get(fromTo).get("osm_id");

List<List<Double>> coordinates = List.of(from, to);
Arc arc = Arc.builder().coordinates(coordinates).build();
arcsLocal.add(arc);

List<Integer> arcList = List.of(arcCount);
Map<String, Object> properties = new HashMap<>();
properties.put("weight", edgeWeight.getValue());
properties.put("osm_id", osmId);

Geometry geometry = Geometry.builder()
.type("LineString")
.properties(properties)
.arcs(arcList)
.build();
geometries.add(geometry);

arcCount++;
if (exportResult.getTopoGeometries().isEmpty()) {
// If OSM ids are not present, we are creating a simple topology with geometries for every edge
for (Map.Entry<Pair<Integer, Integer>, Double> edgeWeight : exportResult.getEdgeWeights().entrySet()) {
Pair<Integer, Integer> fromTo = edgeWeight.getKey();

LineString lineString = (LineString) exportResult.getEdgeExtras().get(fromTo).get("geometry");
arcsLocal.add(Arc.builder().coordinates(makeCoordinates(lineString, bbox)).build());
arcCount++;

List<Integer> arcList = List.of(arcCount);
Map<String, Object> properties = new HashMap<>();
properties.put("weight", edgeWeight.getValue());
Geometry geometry = Geometry.builder()
.type("LineString")
.properties(properties)
.arcs(arcList)
.build();
geometries.add(geometry);
}
} else {
for (long osmId : exportResult.getTopoGeometries().keySet()) {
TopoGeometry topoGeometry = exportResult.getTopoGeometries().get(osmId);
List<Integer> orsIdList = topoGeometry.getArcs().keySet().stream().sorted().toList();
List<Integer> arcList = new LinkedList<>();
List<Integer> nodeList = new LinkedList<>();
List<Double> distanceList = new LinkedList<>();
for (int orsId : orsIdList) {
arcsLocal.add(Arc.builder().coordinates(makeCoordinates(topoGeometry.getArcs().get(orsId).geometry(), bbox)).build());
arcList.add(arcCount);
if (nodeList.isEmpty()) {
nodeList.add(topoGeometry.getArcs().get(orsId).from());
}
nodeList.add(topoGeometry.getArcs().get(orsId).to());
distanceList.add(topoGeometry.getArcs().get(orsId).length());
arcCount++;
}

Map<String, Object> properties = new HashMap<>();
properties.put("osm_id", osmId);
properties.put("ors_ids", orsIdList);
properties.put("ors_nodes", nodeList);
properties.put("speed", topoGeometry.getSpeed());
properties.put("distances", distanceList);
properties.put("both_directions", topoGeometry.isBothDirections());
if (topoGeometry.isBothDirections()) {
properties.put("speed_reverse", topoGeometry.getSpeedReverse());
}

Geometry geometry = Geometry.builder()
.type("LineString")
.properties(properties)
.arcs(arcList)
.build();
geometries.add(geometry);
}
}

Layer layer = Layer.builder()
.type("GeometryCollection")
.geometries(geometries)
.build();

return TopoJsonExportResponse.builder()
.type("Topology")
.objects(new HashMap<>(Map.of(topologyLayerName, layer)))
.objects(new HashMap<>(Map.of(topologyLayerName, Layer.builder()
.type("GeometryCollection")
.geometries(geometries)
.build())))
.arcs(arcsLocal)
.bbox(bbox)
.bbox(bbox.toList())
.build();
}

private static List<Double> initializeBbox() {
return List.of(Double.MAX_VALUE, Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE);
private static List<List<Double>> makeCoordinates(LineString geometry, BBox bbox) {
List<List<Double>> coordinates = new LinkedList<>();
for (Coordinate coordinate : geometry.getCoordinates()) {
coordinates.add(List.of(coordinate.x, coordinate.y));
bbox.update(coordinate.x, coordinate.y);
}
return coordinates;
}

private static List<Double> getXY(Map<Integer, Coordinate> nodes, int id) {
Coordinate coordinate = nodes.get(id);
return List.of(coordinate.x, coordinate.y);
}
private static class BBox {
private double[] coords = {Double.MAX_VALUE, Double.MAX_VALUE, -Double.MAX_VALUE, -Double.MAX_VALUE};

private static List<Double> updateBbox(List<Double> bbox, List<Double> node1, List<Double> node2) {
double minX = Math.min(bbox.get(0), Math.min(node1.get(0), node2.get(0)));
double minY = Math.min(bbox.get(1), Math.min(node1.get(1), node2.get(1)));
double maxX = Math.max(bbox.get(2), Math.max(node1.get(0), node2.get(0)));
double maxY = Math.max(bbox.get(3), Math.max(node1.get(1), node2.get(1)));
return List.of(minX, minY, maxX, maxY);
public void update(double x, double y) {
coords[0] = Math.min(coords[0], x);
coords[1] = Math.min(coords[1], y);
coords[2] = Math.max(coords[2], x);
coords[3] = Math.max(coords[3], y);
}

public List<Double> toList() {
return List.of(coords[0], coords[1], coords[2], coords[3]);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import com.google.common.primitives.Doubles;
import com.graphhopper.util.shapes.BBox;
import org.heigit.ors.api.config.ApiEngineProperties;
import org.heigit.ors.api.APIEnums;
import org.heigit.ors.api.config.ApiEngineProperties;
import org.heigit.ors.api.config.EndpointsProperties;
import org.heigit.ors.api.requests.export.ExportApiRequest;
import org.heigit.ors.common.StatusCode;
Expand Down Expand Up @@ -61,6 +61,7 @@ private org.heigit.ors.export.ExportRequest convertExportRequest(ExportApiReques
exportRequest.setBoundingBox(convertBBox(exportApiRequest.getBbox()));
exportRequest.setDebug(exportApiRequest.debug());
exportRequest.setTopoJson(exportApiRequest.getResponseType().equals(APIEnums.ExportResponseType.TOPOJSON));
exportRequest.setUseRealGeometry(exportApiRequest.getGeometry());

return exportRequest;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.heigit.ors.apitests.export;

import org.hamcrest.Matchers;
import org.heigit.ors.apitests.common.EndPointAnnotation;
import org.heigit.ors.apitests.common.ServiceTest;
import org.heigit.ors.apitests.common.VersionAnnotation;
Expand All @@ -12,8 +13,6 @@
import static org.hamcrest.Matchers.is;
import static org.heigit.ors.apitests.utils.CommonHeaders.jsonContent;

import org.hamcrest.Matchers;

@EndPointAnnotation(name = "export")
@VersionAnnotation(version = "v2")
class ParamsTest extends ServiceTest {
Expand Down Expand Up @@ -42,6 +41,17 @@ public ParamsTest() {
addParameter("bboxFake", bboxMock);
addParameter("bboxFaulty", bboxFaulty);
addParameter("bboxProper", bboxProper);

JSONArray coord4 = new JSONArray();
coord4.put(8.6280012);
coord4.put(49.3669484);
JSONArray coord5 = new JSONArray();
coord5.put(8.6391592);
coord5.put(49.3732920);
JSONArray bboxCar = new JSONArray();
bboxCar.put(coord4);
bboxCar.put(coord5);
addParameter("bboxCar", bboxCar);
}

@Test
Expand Down Expand Up @@ -152,20 +162,87 @@ void expectTopoJsonFormat() {
.body(body.toString())
.when()
.post(getEndPointPath() + "/{profile}/topojson")
// TODO: remove me before merging to main
.then().log().all()
.then().log().ifValidationFails()
.assertThat()
.body("type", is("Topology"))
.body("objects.size()", is(1))
.body("objects.network.type", is("GeometryCollection"))
.body("bbox.size()", is(4))
// approximations due to float comparison
.body("bbox[0]", is(8.681522F))
.body("bbox[1]", is(49.41491F))
.body("bbox[0]", is(8.681523F))
.body("bbox[1]", is(49.414877F))
.body("bbox[2]", is(8.686507F))
.body("bbox[3]", is(49.41943F))
.body("arcs.size()", is(128))
.body("objects.network.geometries.size()", is(128))
.body("arcs.size()", is(69))
.body("objects.network.geometries.size()", is(35))
.body("objects.network.geometries[0].properties.osm_id", is(4084860))
.body("objects.network.geometries[0].properties.both_directions", is(true))
.body("objects.network.geometries[0].properties.distances.size()", is(2))
.body("objects.network.geometries[0].properties.ors_ids.size()", is(2))
.body("objects.network.geometries[0].properties.speed", is(5.0F))
.body("objects.network.geometries[0].properties.speed_reverse", is(5.0F))
.body("objects.network.geometries[0].properties.ors_nodes.size()", is(3))
.body("objects.network.geometries[0].arcs.size()", is(2)) // Geometry with more than 1 arc exists
.body("arcs[4].size()", is(3)) // Arc with more than 2 coordinates exists
.statusCode(200);
}

@Test
void expectTopoJsonWithSimpleGeometry() {
JSONObject body = new JSONObject();
body.put("bbox", getParameter("bboxProper"));
body.put("geometry", false);
given()
.headers(jsonContent)
.pathParam("profile", "wheelchair")
.body(body.toString())
.when()
.post(getEndPointPath() + "/{profile}/topojson")
.then().log().ifValidationFails()
.assertThat()
.body("type", is("Topology"))
.body("arcs.size()", is(69))
.body("objects.network.geometries.size()", is(35))
.body("arcs[4].size()", is(2)) // Arc 4 now has only 2 coordinates
.statusCode(200);
}

@Test
void expectTopoJsonFormatOneWay() {
JSONObject body = new JSONObject();
body.put("bbox", getParameter("bboxCar"));
given()
.headers(jsonContent)
.pathParam("profile", "driving-car")
.body(body.toString())
.when()
.post(getEndPointPath() + "/{profile}/topojson")
.then().log().ifValidationFails()
.assertThat()
.body("type", is("Topology"))
.body("arcs.size()", is(28))
.body("objects.network.geometries.size()", is(27))
.body("objects.network.geometries[0].properties.both_directions", is(false))
.body("objects.network.geometries[0].properties.containsKey('speed_reverse')", is(false))
.statusCode(200);
}

@Test
void expectTopoJsonFallbackNoOsmIdMode() {
JSONObject body = new JSONObject();
body.put("bbox", getParameter("bboxCar"));
given()
.headers(jsonContent)
.pathParam("profile", "driving-hgv")
.body(body.toString())
.when()
.post(getEndPointPath() + "/{profile}/topojson")
.then().log().ifValidationFails()
.assertThat()
.body("type", is("Topology"))
.body("arcs.size()", is(30))
.body("objects.network.geometries.size()", is(30))
.body("objects.network.geometries[0].properties.containsKey('weight')", is(true))
.statusCode(200);
}
}
1 change: 1 addition & 0 deletions ors-api/src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ ors:
restrictions: true
WaySurfaceType:
Tollways:
OsmId:
Borders:
boundaries: ./src/test/files/borders/borders.geojson
ids: ./src/test/files/borders/ids.csv
Expand Down
Loading

0 comments on commit efeecf8

Please sign in to comment.