From 9acf9aca2469687127cb69711e7ffe7d81bc57af Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Wed, 3 Dec 2025 14:14:50 +0100 Subject: [PATCH 1/5] feat: enable external point data in custom foot profile --- .../com/graphhopper/routing/ev/PointData.java | 9 ++ .../heigit/ors/routing/RoutingProfile.java | 113 ++++++++++++++++++ .../extensions/OrsEncodedValueFactory.java | 14 +-- .../flagencoders/FootFlagEncoder.java | 3 + 4 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 ors-engine/src/main/java/com/graphhopper/routing/ev/PointData.java diff --git a/ors-engine/src/main/java/com/graphhopper/routing/ev/PointData.java b/ors-engine/src/main/java/com/graphhopper/routing/ev/PointData.java new file mode 100644 index 0000000000..3b6425853e --- /dev/null +++ b/ors-engine/src/main/java/com/graphhopper/routing/ev/PointData.java @@ -0,0 +1,9 @@ +package com.graphhopper.routing.ev; + +public class PointData { + public static final String KEY = "point_data"; + + public static DecimalEncodedValue create() { + return new UnsignedDecimalEncodedValue(KEY, 31, 0.01, false); + } +} diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java index 27894a37aa..6019dc6b3f 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java @@ -15,8 +15,17 @@ import com.graphhopper.config.CHProfile; import com.graphhopper.routing.ev.*; +import com.graphhopper.routing.util.DefaultSnapFilter; +import com.graphhopper.routing.util.EdgeFilter; +import com.graphhopper.routing.util.EncodingManager; +import com.graphhopper.routing.weighting.Weighting; import com.graphhopper.storage.GraphHopperStorage; +import com.graphhopper.storage.IntsRef; import com.graphhopper.storage.StorableProperties; +import com.graphhopper.storage.index.LocationIndex; +import com.graphhopper.storage.index.Snap; +import com.graphhopper.util.EdgeIteratorState; +import com.graphhopper.util.PMap; import org.apache.log4j.Logger; import org.heigit.ors.config.EngineProperties; import org.heigit.ors.config.profile.ExecutionProperties; @@ -26,10 +35,15 @@ import org.heigit.ors.routing.graphhopper.extensions.storages.builders.BordersGraphStorageBuilder; import org.heigit.ors.routing.graphhopper.extensions.storages.builders.GraphStorageBuilder; import org.heigit.ors.routing.pathprocessors.ORSPathProcessorFactory; +import org.heigit.ors.util.ProfileTools; import org.heigit.ors.util.TimeUtility; import org.json.simple.JSONObject; +import org.locationtech.jts.geom.Coordinate; +import java.io.BufferedReader; import java.io.File; +import java.io.FileReader; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -37,6 +51,8 @@ import java.util.*; import java.util.function.Function; +import static com.graphhopper.routing.util.EncodingManager.getKey; + /** * This class generates {@link RoutingProfile} classes and is used by mostly all service classes e.g. *

@@ -75,6 +91,8 @@ public RoutingProfile(String profileName, ProfileProperties profile, EnginePrope astarApproximation = execution.getMethods().getAstar().getApproximation(); if (execution.getMethods().getAstar().getEpsilon() != null) astarEpsilon = execution.getMethods().getAstar().getEpsilon(); + + if (hasExternalData()) loadStaticExternalData(mGraphHopper); } @@ -144,6 +162,101 @@ public ORSGraphHopper initGraphHopper(RoutingProfileLoadContext loadCntx) throws return gh; } + private boolean hasExternalData() { + return true; // TODO: implement properly + } + + private void loadStaticExternalData(ORSGraphHopper gh) throws IOException { + EncodingManager em = gh.getEncodingManager(); + DecimalEncodedValue dev = em.getDecimalEncodedValue(getKey("pedestrian_ors", PointData.KEY)); + if (dev == null) { + LOGGER.error("Could not create EncodedValue %s.".formatted(PointData.KEY)); + return; + } + + Path csvFile = Path.of("external_point_data.csv").toAbsolutePath(); // TODO: parametrize + + try (BufferedReader csvBuffer = new BufferedReader(new FileReader(csvFile.toString()))) { + // Read header line + String row = csvBuffer.readLine(); + String[] columnNames = Arrays.stream(row.split(",")).toArray(String[]::new); + // TODO: check that column names are lon, lat, value + + Snapper snapper = setupSnapper(gh, WeightingMethod.CUSTOM); + + double maxSearchRadius = 300; // TODO: initialize from config + int num_ignored = 0; + int num_not_snapped = 0; + int num_loaded = 0; + + while ((row = csvBuffer.readLine()) != null) { + String[] fields = row.split(",", 4); + if (fields.length != 3) { // ignore rows with too few or too many entires + num_ignored ++; + continue; + } + + float lon = Float.parseFloat(fields[0].trim()); + float lat = Float.parseFloat(fields[1].trim()); + double value = Float.parseFloat(fields[2].trim()); + + Snap snap = snapper.snapToGraphEdge(lon, lat); + if (!snap.isValid() || snap.getQueryDistance() > maxSearchRadius) { + num_not_snapped ++; + continue; + } + + EdgeIteratorState closestEdge = snap.getClosestEdge(); + IntsRef edgeFlags = closestEdge.getFlags(); + + double current = dev.getDecimal(false, edgeFlags); + if (current != dev.getMaxDecimal()) value += current; + + dev.setDecimal(false, edgeFlags, value); + closestEdge.setFlags(edgeFlags); + num_loaded ++; + } + LOGGER.info("External data points: " + + num_loaded + " loaded, " + + num_not_snapped + " not snapped, " + + num_ignored + " ignored."); + } catch (IOException openFileEx) { + LOGGER.error(openFileEx.getStackTrace()); + throw openFileEx; + } + } + + private Snapper setupSnapper(ORSGraphHopper gh, int weightingMethod) { + LocationIndex locationIndex = gh.getLocationIndex(); + int profileType = RoutingProfileType.getFromString(profileName); + String encoderName = RoutingProfileType.getEncoderName(profileType); + String localProfileName = ProfileTools.makeProfileName(encoderName, WeightingMethod.getName(weightingMethod), false); + PMap hintsMap = new PMap(); + ProfileTools.setWeightingMethod(hintsMap, weightingMethod, profileType, false); + ProfileTools.setWeighting(hintsMap, weightingMethod, profileType, false); + Weighting weighting = new ORSWeightingFactory(gh.getGraphHopperStorage(), gh.getEncodingManager()) + .createWeighting(gh.getProfile(localProfileName), hintsMap, false); + BooleanEncodedValue profileSubnetwork = gh.getEncodingManager().getBooleanEncodedValue(Subnetwork.key(localProfileName)); + EdgeFilter snappingEdgeFilter = new DefaultSnapFilter(weighting, profileSubnetwork); + return new Snapper(locationIndex, snappingEdgeFilter); + } + + class Snapper { + LocationIndex locationIndex; + EdgeFilter edgeFilter; + + Snapper(LocationIndex locationIndex, EdgeFilter edgeFilter) { + this.locationIndex = locationIndex; + this.edgeFilter = edgeFilter; + } + + public Snap snapToGraphEdge(Float lon, Float lat) { + Coordinate p = new Coordinate(lon, lat); + return locationIndex.findClosest(p.y, p.x, edgeFilter); + } + + } + public boolean hasCHProfile(String profileName) { boolean hasCHProfile = false; for (CHProfile chProfile : getGraphhopper().getCHPreparationHandler().getCHProfiles()) { diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/OrsEncodedValueFactory.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/OrsEncodedValueFactory.java index 30fd2bed28..57866b8a1a 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/OrsEncodedValueFactory.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/OrsEncodedValueFactory.java @@ -3,6 +3,7 @@ import com.graphhopper.routing.ev.DefaultEncodedValueFactory; import com.graphhopper.routing.ev.EncodedValue; import com.graphhopper.routing.ev.EncodedValueFactory; +import com.graphhopper.routing.ev.PointData; import com.graphhopper.util.Helper; import org.heigit.ors.routing.graphhopper.extensions.ev.DynamicData; @@ -19,17 +20,14 @@ public EncodedValue create(String encodedValueString) { if (Helper.isEmpty(encodedValueString)) throw new IllegalArgumentException("No string provided to load EncodedValue"); - final EncodedValue enc; String name = encodedValueString.split("\\|")[0]; if (name.isEmpty()) throw new IllegalArgumentException("To load EncodedValue a name is required. " + encodedValueString); - if (DynamicData.KEY.equals(name)) { - enc = DynamicData.create(); - } else { - // Fallback to GraphHopper's EncodedValues - enc = defaultEncodedValueFactory.create(encodedValueString); - } - return enc; + return switch (name) { + case DynamicData.KEY -> DynamicData.create(); + case PointData.KEY -> PointData.create(); + default -> defaultEncodedValueFactory.create(encodedValueString); // fall back to GraphHopper's EncodedValues + }; } } diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/flagencoders/FootFlagEncoder.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/flagencoders/FootFlagEncoder.java index 8bd0464b48..9e1fbb9905 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/flagencoders/FootFlagEncoder.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/flagencoders/FootFlagEncoder.java @@ -168,6 +168,9 @@ public void createEncodedValues(List registerNewEncodedValue, Stri registerNewEncodedValue.add(conditionalAccessEncoder); } footRouteEnc = getEnumEncodedValue(RouteNetwork.key("foot"), RouteNetwork.class); + + // TODO: this should be available for all flag encoders + registerNewEncodedValue.add(new UnsignedDecimalEncodedValue(getKey(prefix, PointData.KEY), 31, 0.01, false)); } @Override From 311d667600291ef4779b9a0cc37e46eda31c8b7f Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Wed, 3 Dec 2025 14:26:06 +0100 Subject: [PATCH 2/5] refactor: minor cleanup --- .../com/graphhopper/routing/ev/DynamicData.java | 17 +++++++++++++++++ .../extensions/OrsEncodedValueFactory.java | 2 +- .../graphhopper/extensions/ev/DynamicData.java | 12 ------------ .../flagencoders/VehicleFlagEncoder.java | 2 +- .../org/heigit/ors/routing/DynamicDataTest.java | 2 +- 5 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 ors-engine/src/main/java/com/graphhopper/routing/ev/DynamicData.java delete mode 100644 ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ev/DynamicData.java diff --git a/ors-engine/src/main/java/com/graphhopper/routing/ev/DynamicData.java b/ors-engine/src/main/java/com/graphhopper/routing/ev/DynamicData.java new file mode 100644 index 0000000000..96d6d9f9c0 --- /dev/null +++ b/ors-engine/src/main/java/com/graphhopper/routing/ev/DynamicData.java @@ -0,0 +1,17 @@ +package com.graphhopper.routing.ev; + +/** + * This class is just an example for showing how to use + * encoded values dynamically. + */ +public class DynamicData { + public static final String KEY = "dynamic_data"; + + public static BooleanEncodedValue create() { + return new SimpleBooleanEncodedValue(KEY, false); + } + + private DynamicData() { + // hide implicit constructor + } +} diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/OrsEncodedValueFactory.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/OrsEncodedValueFactory.java index 57866b8a1a..57e0ffc3b3 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/OrsEncodedValueFactory.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/OrsEncodedValueFactory.java @@ -5,7 +5,7 @@ import com.graphhopper.routing.ev.EncodedValueFactory; import com.graphhopper.routing.ev.PointData; import com.graphhopper.util.Helper; -import org.heigit.ors.routing.graphhopper.extensions.ev.DynamicData; +import com.graphhopper.routing.ev.DynamicData; public class OrsEncodedValueFactory implements EncodedValueFactory { DefaultEncodedValueFactory defaultEncodedValueFactory = new DefaultEncodedValueFactory(); diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ev/DynamicData.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ev/DynamicData.java deleted file mode 100644 index e7d7c8c7db..0000000000 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/ev/DynamicData.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.heigit.ors.routing.graphhopper.extensions.ev; - -import com.graphhopper.routing.ev.BooleanEncodedValue; -import com.graphhopper.routing.ev.SimpleBooleanEncodedValue; - -public class DynamicData { - public static final String KEY = "dynamic_data"; - - public static BooleanEncodedValue create() { - return new SimpleBooleanEncodedValue(KEY, false); - } -} diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/flagencoders/VehicleFlagEncoder.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/flagencoders/VehicleFlagEncoder.java index ab4f7ee173..3804617888 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/flagencoders/VehicleFlagEncoder.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/flagencoders/VehicleFlagEncoder.java @@ -26,7 +26,7 @@ import com.graphhopper.util.BitUtil; import com.graphhopper.util.Helper; import com.graphhopper.util.PMap; -import org.heigit.ors.routing.graphhopper.extensions.ev.DynamicData; +import com.graphhopper.routing.ev.DynamicData; import java.util.Arrays; import java.util.HashMap; diff --git a/ors-engine/src/test/java/org/heigit/ors/routing/DynamicDataTest.java b/ors-engine/src/test/java/org/heigit/ors/routing/DynamicDataTest.java index 01a81aa596..cd7138bd2c 100644 --- a/ors-engine/src/test/java/org/heigit/ors/routing/DynamicDataTest.java +++ b/ors-engine/src/test/java/org/heigit/ors/routing/DynamicDataTest.java @@ -15,7 +15,7 @@ import com.graphhopper.storage.IntsRef; import com.graphhopper.util.EdgeIteratorState; import org.heigit.ors.routing.graphhopper.extensions.edgefilters.BooleanEncodedValueEdgeFilter; -import org.heigit.ors.routing.graphhopper.extensions.ev.DynamicData; +import com.graphhopper.routing.ev.DynamicData; import org.heigit.ors.util.ToyGraphCreationUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; From 4fe7b157f4352ea9e1f40348590d1993cdd26c0e Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Mon, 8 Dec 2025 11:55:26 +0100 Subject: [PATCH 3/5] refactor: major cleanup of snapping functionality --- .../matrix/MatrixSearchContextBuilder.java | 1 + .../heigit/ors/routing/RoutingProfile.java | 65 +++++-------------- .../java/org/heigit/ors/snapping/Snapper.java | 52 +++++++++++++++ .../heigit/ors/snapping/SnappingRequest.java | 35 +++------- 4 files changed, 78 insertions(+), 75 deletions(-) create mode 100644 ors-engine/src/main/java/org/heigit/ors/snapping/Snapper.java diff --git a/ors-engine/src/main/java/org/heigit/ors/matrix/MatrixSearchContextBuilder.java b/ors-engine/src/main/java/org/heigit/ors/matrix/MatrixSearchContextBuilder.java index d2fb8996f2..7e210b05f9 100644 --- a/ors-engine/src/main/java/org/heigit/ors/matrix/MatrixSearchContextBuilder.java +++ b/ors-engine/src/main/java/org/heigit/ors/matrix/MatrixSearchContextBuilder.java @@ -119,6 +119,7 @@ private int[] pointIdsOutOfBounds(BBox bounds, Coordinate[] coords) { return idsArray; } + // TODO: refactor using org.heigit.ors.snapping.Snapper instead to match Snapping and loadExternalPointData private void resolveLocations(String profileName, Coordinate[] coords, List queryResults, double maxSearchRadius) { for (Coordinate p : coords) { LocationEntry ld = locationCache.get(p); diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java index 6019dc6b3f..5ad8f2a3dd 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java @@ -15,17 +15,12 @@ import com.graphhopper.config.CHProfile; import com.graphhopper.routing.ev.*; -import com.graphhopper.routing.util.DefaultSnapFilter; -import com.graphhopper.routing.util.EdgeFilter; import com.graphhopper.routing.util.EncodingManager; -import com.graphhopper.routing.weighting.Weighting; import com.graphhopper.storage.GraphHopperStorage; import com.graphhopper.storage.IntsRef; import com.graphhopper.storage.StorableProperties; -import com.graphhopper.storage.index.LocationIndex; import com.graphhopper.storage.index.Snap; import com.graphhopper.util.EdgeIteratorState; -import com.graphhopper.util.PMap; import org.apache.log4j.Logger; import org.heigit.ors.config.EngineProperties; import org.heigit.ors.config.profile.ExecutionProperties; @@ -35,10 +30,9 @@ import org.heigit.ors.routing.graphhopper.extensions.storages.builders.BordersGraphStorageBuilder; import org.heigit.ors.routing.graphhopper.extensions.storages.builders.GraphStorageBuilder; import org.heigit.ors.routing.pathprocessors.ORSPathProcessorFactory; -import org.heigit.ors.util.ProfileTools; +import org.heigit.ors.snapping.Snapper; import org.heigit.ors.util.TimeUtility; import org.json.simple.JSONObject; -import org.locationtech.jts.geom.Coordinate; import java.io.BufferedReader; import java.io.File; @@ -182,17 +176,17 @@ private void loadStaticExternalData(ORSGraphHopper gh) throws IOException { String[] columnNames = Arrays.stream(row.split(",")).toArray(String[]::new); // TODO: check that column names are lon, lat, value - Snapper snapper = setupSnapper(gh, WeightingMethod.CUSTOM); + Snapper snapper = new Snapper(this, WeightingMethod.CUSTOM); double maxSearchRadius = 300; // TODO: initialize from config - int num_ignored = 0; - int num_not_snapped = 0; - int num_loaded = 0; + int numIgnored = 0; + int numNotSnapped = 0; + int numLoaded = 0; while ((row = csvBuffer.readLine()) != null) { String[] fields = row.split(",", 4); if (fields.length != 3) { // ignore rows with too few or too many entires - num_ignored ++; + numIgnored ++; continue; } @@ -200,9 +194,9 @@ private void loadStaticExternalData(ORSGraphHopper gh) throws IOException { float lat = Float.parseFloat(fields[1].trim()); double value = Float.parseFloat(fields[2].trim()); - Snap snap = snapper.snapToGraphEdge(lon, lat); + Snap snap = snapper.snapToGraph(lon, lat); if (!snap.isValid() || snap.getQueryDistance() > maxSearchRadius) { - num_not_snapped ++; + numNotSnapped ++; continue; } @@ -214,49 +208,18 @@ private void loadStaticExternalData(ORSGraphHopper gh) throws IOException { dev.setDecimal(false, edgeFlags, value); closestEdge.setFlags(edgeFlags); - num_loaded ++; + numLoaded ++; } LOGGER.info("External data points: " - + num_loaded + " loaded, " - + num_not_snapped + " not snapped, " - + num_ignored + " ignored."); + + numLoaded + " loaded, " + + numNotSnapped + " not snapped, " + + numIgnored + " ignored."); } catch (IOException openFileEx) { LOGGER.error(openFileEx.getStackTrace()); throw openFileEx; } } - private Snapper setupSnapper(ORSGraphHopper gh, int weightingMethod) { - LocationIndex locationIndex = gh.getLocationIndex(); - int profileType = RoutingProfileType.getFromString(profileName); - String encoderName = RoutingProfileType.getEncoderName(profileType); - String localProfileName = ProfileTools.makeProfileName(encoderName, WeightingMethod.getName(weightingMethod), false); - PMap hintsMap = new PMap(); - ProfileTools.setWeightingMethod(hintsMap, weightingMethod, profileType, false); - ProfileTools.setWeighting(hintsMap, weightingMethod, profileType, false); - Weighting weighting = new ORSWeightingFactory(gh.getGraphHopperStorage(), gh.getEncodingManager()) - .createWeighting(gh.getProfile(localProfileName), hintsMap, false); - BooleanEncodedValue profileSubnetwork = gh.getEncodingManager().getBooleanEncodedValue(Subnetwork.key(localProfileName)); - EdgeFilter snappingEdgeFilter = new DefaultSnapFilter(weighting, profileSubnetwork); - return new Snapper(locationIndex, snappingEdgeFilter); - } - - class Snapper { - LocationIndex locationIndex; - EdgeFilter edgeFilter; - - Snapper(LocationIndex locationIndex, EdgeFilter edgeFilter) { - this.locationIndex = locationIndex; - this.edgeFilter = edgeFilter; - } - - public Snap snapToGraphEdge(Float lon, Float lat) { - Coordinate p = new Coordinate(lon, lat); - return locationIndex.findClosest(p.y, p.x, edgeFilter); - } - - } - public boolean hasCHProfile(String profileName) { boolean hasCHProfile = false; for (CHProfile chProfile : getGraphhopper().getCHPreparationHandler().getCHProfiles()) { @@ -306,6 +269,10 @@ public String name() { return this.profileName; } + public int profileType() { + return RoutingProfileType.getFromString(profileName); + } + public ProfileProperties getProfileProperties() { return this.profileProperties; } diff --git a/ors-engine/src/main/java/org/heigit/ors/snapping/Snapper.java b/ors-engine/src/main/java/org/heigit/ors/snapping/Snapper.java new file mode 100644 index 0000000000..37b128c861 --- /dev/null +++ b/ors-engine/src/main/java/org/heigit/ors/snapping/Snapper.java @@ -0,0 +1,52 @@ +package org.heigit.ors.snapping; + +import com.graphhopper.routing.ev.BooleanEncodedValue; +import com.graphhopper.routing.ev.Subnetwork; +import com.graphhopper.routing.util.DefaultSnapFilter; +import com.graphhopper.routing.util.EdgeFilter; +import com.graphhopper.routing.weighting.Weighting; +import com.graphhopper.storage.index.LocationIndex; +import com.graphhopper.storage.index.Snap; +import com.graphhopper.util.PMap; +import org.heigit.ors.matrix.ResolvedLocation; +import org.heigit.ors.routing.RoutingProfile; +import org.heigit.ors.routing.RoutingProfileType; +import org.heigit.ors.routing.graphhopper.extensions.ORSGraphHopper; +import org.heigit.ors.routing.graphhopper.extensions.ORSWeightingFactory; +import org.heigit.ors.util.ProfileTools; +import org.locationtech.jts.geom.Coordinate; + +public class Snapper { + LocationIndex locationIndex; + EdgeFilter edgeFilter; + private int profileType; + + public Snapper(RoutingProfile rp, int weightingMethod) { + profileType = rp.profileType(); + this.locationIndex = rp.getGraphhopper().getLocationIndex(); + this.edgeFilter = snappingEdgeFilter(rp.getGraphhopper(), weightingMethod); + } + + public Snap snapToGraph(double lon, double lat) { + return locationIndex.findClosest(lat, lon, edgeFilter); + } + + public ResolvedLocation resolveLocation(Coordinate coordinate) { + Snap snap = snapToGraph(coordinate.y, coordinate.x); + Coordinate snappedCoordinates = new Coordinate(snap.getSnappedPoint().getLon(), snap.getSnappedPoint().getLat()); + return new ResolvedLocation(snappedCoordinates, snap.getClosestEdge().getName(), snap.getQueryDistance()); + } + + private EdgeFilter snappingEdgeFilter(ORSGraphHopper gh, int weightingMethod) { + String encoderName = RoutingProfileType.getEncoderName(profileType); + PMap hintsMap = new PMap(); + ProfileTools.setWeightingMethod(hintsMap, weightingMethod, profileType, false); + ProfileTools.setWeighting(hintsMap, weightingMethod, profileType, false); + String effectiveWeighting = hintsMap.getString("weightingMethod", ""); + String localProfileName = ProfileTools.makeProfileName(encoderName, effectiveWeighting, false); + Weighting weighting = new ORSWeightingFactory(gh.getGraphHopperStorage(), gh.getEncodingManager()) + .createWeighting(gh.getProfile(localProfileName), hintsMap, false); + BooleanEncodedValue profileSubnetwork = gh.getEncodingManager().getBooleanEncodedValue(Subnetwork.key(localProfileName)); + return new DefaultSnapFilter(weighting, profileSubnetwork); + } +} diff --git a/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java index 4423d3d76c..784864d849 100644 --- a/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java +++ b/ors-engine/src/main/java/org/heigit/ors/snapping/SnappingRequest.java @@ -1,21 +1,13 @@ package org.heigit.ors.snapping; -import com.graphhopper.GraphHopper; -import com.graphhopper.routing.util.AccessFilter; -import com.graphhopper.routing.util.FlagEncoder; -import com.graphhopper.routing.weighting.Weighting; -import com.graphhopper.storage.GraphHopperStorage; -import com.graphhopper.util.PMap; import org.heigit.ors.common.ServiceRequest; -import org.heigit.ors.matrix.MatrixSearchContext; -import org.heigit.ors.matrix.MatrixSearchContextBuilder; +import org.heigit.ors.matrix.ResolvedLocation; import org.heigit.ors.routing.RoutingProfile; -import org.heigit.ors.routing.RoutingProfileType; import org.heigit.ors.routing.WeightingMethod; -import org.heigit.ors.routing.graphhopper.extensions.ORSWeightingFactory; -import org.heigit.ors.util.ProfileTools; import org.locationtech.jts.geom.Coordinate; +import java.util.stream.Stream; + public class SnappingRequest extends ServiceRequest { private String profileName; private final int profileType; @@ -58,21 +50,12 @@ public int getMaximumLocations() { } public SnappingResult computeResult(RoutingProfile rp) throws Exception { - GraphHopper gh = rp.getGraphhopper(); - String encoderName = RoutingProfileType.getEncoderName(getProfileType()); - FlagEncoder flagEncoder = gh.getEncodingManager().getEncoder(encoderName); - PMap hintsMap = new PMap(); - int weightingMethod = WeightingMethod.RECOMMENDED; // Only needed to create the profile string - ProfileTools.setWeightingMethod(hintsMap, weightingMethod, getProfileType(), false); - ProfileTools.setWeighting(hintsMap, weightingMethod, getProfileType(), false); - String localProfileName = ProfileTools.makeProfileName(encoderName, hintsMap.getString("weighting", ""), false); - GraphHopperStorage ghStorage = gh.getGraphHopperStorage(); - String graphDate = ghStorage.getProperties().get("datareader.import.date"); + Snapper snapper = new Snapper(rp, WeightingMethod.RECOMMENDED); + + ResolvedLocation[] resolvedLocations = Stream.of(getLocations()).map(snapper::resolveLocation).toArray(ResolvedLocation[]::new); - // TODO: replace usage of matrix search context by snapping-specific class - MatrixSearchContextBuilder builder = new MatrixSearchContextBuilder(ghStorage, gh.getLocationIndex(), AccessFilter.allEdges(flagEncoder.getAccessEnc()), true); - Weighting weighting = new ORSWeightingFactory(ghStorage, gh.getEncodingManager()).createWeighting(gh.getProfile(localProfileName), hintsMap, false); - MatrixSearchContext mtxSearchCntx = builder.create(ghStorage.getBaseGraph(), null, weighting, localProfileName, getLocations(), getLocations(), getMaximumSearchRadius()); - return new SnappingResult(mtxSearchCntx.getSources().getLocations(), graphDate); + String graphDate = rp.getGraphhopper().getGraphHopperStorage().getProperties().get("datareader.import.date"); + return new SnappingResult(resolvedLocations, graphDate); } + } From 6fbe2c1819df1a434df7ac993c4c4bc9c7462b8b Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Tue, 9 Dec 2025 12:28:37 +0100 Subject: [PATCH 4/5] feat: extend external point data with point-wise maximum search radius --- .../main/java/org/heigit/ors/routing/RoutingProfile.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java index 5ad8f2a3dd..7e75cf853e 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java @@ -178,21 +178,21 @@ private void loadStaticExternalData(ORSGraphHopper gh) throws IOException { Snapper snapper = new Snapper(this, WeightingMethod.CUSTOM); - double maxSearchRadius = 300; // TODO: initialize from config int numIgnored = 0; int numNotSnapped = 0; int numLoaded = 0; while ((row = csvBuffer.readLine()) != null) { - String[] fields = row.split(",", 4); - if (fields.length != 3) { // ignore rows with too few or too many entires + String[] fields = row.split(",", 5); + if (fields.length != 4) { // ignore rows with too few or too many entires numIgnored ++; continue; } float lon = Float.parseFloat(fields[0].trim()); float lat = Float.parseFloat(fields[1].trim()); - double value = Float.parseFloat(fields[2].trim()); + double value = Double.parseDouble(fields[2].trim()); + double maxSearchRadius = Double.parseDouble(fields[3].trim()); Snap snap = snapper.snapToGraph(lon, lat); if (!snap.isValid() || snap.getQueryDistance() > maxSearchRadius) { From 5bdbad83663a34cea93c2d16653f9b13a4fde6f9 Mon Sep 17 00:00:00 2001 From: Sascha Fendrich Date: Tue, 9 Dec 2025 14:19:36 +0100 Subject: [PATCH 5/5] fix: correct weighting method in Snapper --- .../src/main/java/org/heigit/ors/routing/RoutingProfile.java | 2 +- .../src/main/java/org/heigit/ors/snapping/Snapper.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java index 7e75cf853e..80c1f3553e 100644 --- a/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java +++ b/ors-engine/src/main/java/org/heigit/ors/routing/RoutingProfile.java @@ -174,7 +174,7 @@ private void loadStaticExternalData(ORSGraphHopper gh) throws IOException { // Read header line String row = csvBuffer.readLine(); String[] columnNames = Arrays.stream(row.split(",")).toArray(String[]::new); - // TODO: check that column names are lon, lat, value + // TODO: check that column names are lon, lat, value, maxSearchRadius Snapper snapper = new Snapper(this, WeightingMethod.CUSTOM); diff --git a/ors-engine/src/main/java/org/heigit/ors/snapping/Snapper.java b/ors-engine/src/main/java/org/heigit/ors/snapping/Snapper.java index 37b128c861..8f545dfe34 100644 --- a/ors-engine/src/main/java/org/heigit/ors/snapping/Snapper.java +++ b/ors-engine/src/main/java/org/heigit/ors/snapping/Snapper.java @@ -11,6 +11,7 @@ import org.heigit.ors.matrix.ResolvedLocation; import org.heigit.ors.routing.RoutingProfile; import org.heigit.ors.routing.RoutingProfileType; +import org.heigit.ors.routing.WeightingMethod; import org.heigit.ors.routing.graphhopper.extensions.ORSGraphHopper; import org.heigit.ors.routing.graphhopper.extensions.ORSWeightingFactory; import org.heigit.ors.util.ProfileTools; @@ -42,7 +43,9 @@ private EdgeFilter snappingEdgeFilter(ORSGraphHopper gh, int weightingMethod) { PMap hintsMap = new PMap(); ProfileTools.setWeightingMethod(hintsMap, weightingMethod, profileType, false); ProfileTools.setWeighting(hintsMap, weightingMethod, profileType, false); - String effectiveWeighting = hintsMap.getString("weightingMethod", ""); +// TODO: which effectiveWeighting used at different places is correct? +// String effectiveWeighting = hintsMap.getString("weightingMethod", ""); + String effectiveWeighting = WeightingMethod.getName(weightingMethod); String localProfileName = ProfileTools.makeProfileName(encoderName, effectiveWeighting, false); Weighting weighting = new ORSWeightingFactory(gh.getGraphHopperStorage(), gh.getEncodingManager()) .createWeighting(gh.getProfile(localProfileName), hintsMap, false);