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/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/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 27894a37aa..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 @@ -15,8 +15,12 @@ import com.graphhopper.config.CHProfile; import com.graphhopper.routing.ev.*; +import com.graphhopper.routing.util.EncodingManager; import com.graphhopper.storage.GraphHopperStorage; +import com.graphhopper.storage.IntsRef; import com.graphhopper.storage.StorableProperties; +import com.graphhopper.storage.index.Snap; +import com.graphhopper.util.EdgeIteratorState; import org.apache.log4j.Logger; import org.heigit.ors.config.EngineProperties; import org.heigit.ors.config.profile.ExecutionProperties; @@ -26,10 +30,14 @@ 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.snapping.Snapper; import org.heigit.ors.util.TimeUtility; import org.json.simple.JSONObject; +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 +45,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 +85,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 +156,70 @@ 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, maxSearchRadius + + Snapper snapper = new Snapper(this, WeightingMethod.CUSTOM); + + int numIgnored = 0; + int numNotSnapped = 0; + int numLoaded = 0; + + while ((row = csvBuffer.readLine()) != null) { + 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 = Double.parseDouble(fields[2].trim()); + double maxSearchRadius = Double.parseDouble(fields[3].trim()); + + Snap snap = snapper.snapToGraph(lon, lat); + if (!snap.isValid() || snap.getQueryDistance() > maxSearchRadius) { + numNotSnapped ++; + 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); + numLoaded ++; + } + LOGGER.info("External data points: " + + numLoaded + " loaded, " + + numNotSnapped + " not snapped, " + + numIgnored + " ignored."); + } catch (IOException openFileEx) { + LOGGER.error(openFileEx.getStackTrace()); + throw openFileEx; + } + } + public boolean hasCHProfile(String profileName) { boolean hasCHProfile = false; for (CHProfile chProfile : getGraphhopper().getCHPreparationHandler().getCHProfiles()) { @@ -193,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/routing/graphhopper/extensions/OrsEncodedValueFactory.java b/ors-engine/src/main/java/org/heigit/ors/routing/graphhopper/extensions/OrsEncodedValueFactory.java index 30fd2bed28..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 @@ -3,8 +3,9 @@ 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; +import com.graphhopper.routing.ev.DynamicData; public class OrsEncodedValueFactory implements EncodedValueFactory { DefaultEncodedValueFactory defaultEncodedValueFactory = new DefaultEncodedValueFactory(); @@ -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/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/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 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/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..8f545dfe34 --- /dev/null +++ b/ors-engine/src/main/java/org/heigit/ors/snapping/Snapper.java @@ -0,0 +1,55 @@ +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.WeightingMethod; +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); +// 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); + 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); } + } 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;