Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"permissions": {
"allow": [
"Bash(git checkout:*)",
"Bash(git add:*)",
"Bash(git rm:*)",
"Bash(awk:*)",
"Bash(xargs git add)",
"Bash(xargs git checkout:*)",
"Bash(xargs git rm:*)",
"Bash(git diff:*)",
"Bash(mvn clean:*)",
"Bash(rm:*)",
"Bash(javac:*)",
"Bash(mvn compile:*)",
"Bash(java:*)",
"Bash(mvn test:*)",
"WebSearch",
"Bash(echo:*)",
"Bash(echo $PATH)",
"Bash(mvn:*)",
"Read(//Users/oleksii/.m2/**)",
"Read(//Library/Java/JavaVirtualMachines/**)",
"Read(//Users/oleksii/**)",
"Bash(timeout 60 mvn test:*)",
"Read(//tmp/**)",
"Bash(tee:*)"
],
"deny": [],
"ask": []
}
}
16 changes: 16 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Repository Guidelines

## Project Structure & Module Organization
GraphHopper is a multi-module Maven project. `core/` hosts the routing engine (`core/src/main/java`) and its tests in `core/src/test/java`. `web/` wraps the Dropwizard server, while `web-api/` and `client-hc/` expose the HTTP contract. Import utilities live in `reader-gtfs/`, with domain-specific tools under `map-matching/`, `isochrone/`, and `navigation/`. Shared documentation resides in `docs/`, and scripts such as `graphhopper.sh` sit at the repository root. Keep generated data (e.g., `.osm-gh` directories) out of the tree.

## Build, Test, and Development Commands
Use Maven from the project root: `mvn clean install -DskipTests` compiles all modules and creates `web/target/graphhopper-web-*.jar`. `mvn clean test verify` runs the full test suite and quality checks; run it before every push. For rapid iteration on the server, `mvn -pl web -am package` rebuilds API-facing modules only. During local experiments, `./graphhopper.sh build` packages the web service, and `./graphhopper.sh --action web --config config-example.yml --input berlin-latest.osm.pbf` starts a Dropwizard instance on `localhost:8989`.

## Coding Style & Naming Conventions
Follow IntelliJ defaults with the shared `.editorconfig`: four-space indentation, Unix line endings, and a 100-character line width. Java classes use PascalCase, fields and methods camelCase, and YAML keys kebab-case (`graphhopper.graph.location`). Prefer descriptive names over abbreviations and avoid reformatting import blocks unless necessary.

## Testing Guidelines
JUnit-based tests live beside their modules (for example, `core/src/test/java`). New features need unit coverage plus integration coverage when a public API changes; name classes `*Test` or `*IT`. Execute `mvn clean test` locally and add targeted executions (e.g., `mvn -pl map-matching test`) for module-specific work. Include real-world fixtures under `src/test/resources` and keep them minimal.

## Commit & Pull Request Guidelines
Recent history shows short, imperative commit subjects (e.g., “Set result readonly after writing to it”). Reference relevant issues with `#123` when applicable and limit each commit to one logical change. Pull requests should describe the behaviour change, list validation steps or command outputs, and attach screenshots for UI-facing updates. Confirm `mvn clean test verify` passes before requesting review.
136 changes: 136 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

GraphHopper is a fast and memory-efficient routing engine written in Java that calculates routes, distances, and turn-by-turn instructions using OpenStreetMap data. It supports multiple routing profiles (car, bike, foot, etc.), advanced algorithms like Contraction Hierarchies (CH) for speed optimization, and provides both a Java library and web service.

## Development Commands

### Building and Testing
- **Build entire project**: `mvn clean compile`
- **Run all tests**: `mvn clean test verify`
- **Run single test**: `mvn test -Dtest=TestClassName`
- **Run integration tests**: `mvn verify`
- **Build with dependencies**: `mvn package`

### Code Quality
- **Run checkstyle**: `mvn checkstyle:check`
- **Run forbidden API checks**: `mvn de.thetaphi:forbiddenapis:check`

### Running GraphHopper
- **Start web service**: `java -Ddw.graphhopper.datareader.file=<osm-file> -jar web/target/graphhopper-web-*-jar-with-dependencies.jar server config-example.yml`
- **Using shell script**: `./graphhopper.sh -a web -i <osm-file>`

### Benchmarking
- **Run benchmarks**: `./benchmark/benchmark.sh <graph_dir> <results_dir> <summary_dir> <small_map> <big_map>`
- **Download test maps**: `./benchmark/download_map.sh`

### Individual Module Commands
- **Core module**: `cd core && mvn test`
- **Web module**: `cd web && mvn test`
- **Tools module**: `cd tools && mvn package`

## High-Level Architecture

### Module Structure
GraphHopper is organized as a Maven multi-module project:
- **core/**: Main routing engine, algorithms, graph storage, OSM reading
- **web/**: REST API, web service implementation using Dropwizard
- **web-api/**: API models and interfaces
- **client-hc/**: Java HTTP client for GraphHopper API
- **tools/**: Command-line tools, benchmarking, measurement utilities
- **reader-gtfs/**: Public transit routing with GTFS data
- **map-matching/**: GPS trace to road network matching
- **navigation/**: Turn-by-turn navigation features
- **example/**: Sample applications and usage examples

### Core Components

#### Graph Storage (`core/src/main/java/com/graphhopper/storage/`)
- **BaseGraph**: Core graph data structure storing nodes and edges
- **GraphHopperStorage**: Main storage implementation with memory-mapped or in-memory options
- **DataAccess**: Low-level data storage abstraction (RAM, memory-mapped)
- **LocationIndex**: Spatial index for finding nearby nodes/edges

#### Routing Algorithms (`core/src/main/java/com/graphhopper/routing/`)
- **AStar, Dijkstra**: Classic shortest path algorithms with bidirectional variants
- **Contraction Hierarchies (CH)**: Speed optimization preprocessing
- **Landmarks (LM)**: Alternative speed optimization using A* with landmarks
- **EdgeBasedCH**: Turn-aware routing with edge-based graph preprocessing

#### OSM Data Processing (`core/src/main/java/com/graphhopper/reader/osm/`)
- **OSMReader**: Parses OSM PBF/XML files into graph representation
- **TagParsers**: Extract routing-relevant properties from OSM tags
- **EncodingManager**: Manages vehicle-specific encoded values and parsing

#### Routing Profiles and Weighting (`core/src/main/java/com/graphhopper/routing/`)
- **Profile**: Defines routing behavior (vehicle + weighting + turn costs)
- **CustomModel**: JSON-based routing customization without code changes
- **Weighting**: Calculates edge costs based on distance, time, preferences

### Key Design Patterns

#### Three-Tier Architecture
1. **Import Phase**: OSM data → Graph storage with vehicle-specific encodings
2. **Preprocessing**: Optional speed optimizations (CH/LM preparation)
3. **Query Phase**: Route calculation using prepared graph structures

#### Encoded Values System
- **EncodedValue**: Stores routing properties per edge (speed, access, surface)
- **BooleanEncodedValue**: Binary properties (access allowed/forbidden)
- **DecimalEncodedValue**: Numeric properties (speed limits, weights)
- **EnumEncodedValue**: Categorical values (road class, surface type)

#### Flexible vs Speed vs Hybrid Modes
- **Flexible**: No preprocessing, supports dynamic customization
- **Speed**: CH preprocessing, fastest queries, limited customization
- **Hybrid**: LM preprocessing, balanced speed/flexibility

## Testing Strategy

### Test Organization
- Unit tests in each module's `src/test/java/`
- Integration tests using `*IT.java` naming convention
- GraphHopper uses JUnit 5 for all testing
- Test data in `core/files/` directory

### Key Test Patterns
- **AlgoTester**: Framework for testing routing algorithms with various graph configurations
- **GHUtility**: Test utilities for creating simple test graphs
- **Measurement**: Performance benchmarking and regression testing

## Configuration

### Primary Config File: `config-example.yml`
- **Profiles**: Define vehicle types and routing behavior
- **CH/LM**: Speed optimization settings
- **Import**: OSM data processing options
- **Web**: Server configuration for REST API

### Common Development Settings
- **Memory**: Adjust JVM heap size based on map size (use `-Xmx` flag)
- **Data Access**: Choose between RAM_STORE (fast) or MMAP (memory-efficient)
- **Profiles**: Enable/disable vehicle types and customization options

## Important Development Notes

### Code Style
- Java 8+ required
- 4-space indentation, 100-character line limit
- IntelliJ IDEA defaults, EditorConfig support
- Unix line endings (LF)
- Extensive test coverage expected

### Performance Considerations
- Graph preprocessing (CH/LM) trades memory/startup time for query speed
- Memory-mapped storage enables handling large maps with limited RAM
- Encoded values system allows efficient storage of routing properties
- Contraction and landmarks dramatically improve routing performance

### Common Customization Points
- **Custom Models**: Modify routing behavior via JSON without code changes
- **Tag Parsers**: Extract custom properties from OSM data
- **Weighting**: Implement custom cost functions
- **Encoded Values**: Add new edge properties for routing decisions
75 changes: 22 additions & 53 deletions core/src/main/java/com/graphhopper/GraphHopper.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
import com.graphhopper.config.LMProfile;
import com.graphhopper.config.Profile;
import com.graphhopper.jackson.Jackson;
import com.graphhopper.reader.dem.*;
import com.graphhopper.reader.dem.EdgeElevationInterpolator;
import com.graphhopper.reader.dem.ElevationProvider;
import com.graphhopper.reader.osm.OSMReader;
import com.graphhopper.reader.osm.RestrictionTagParser;
import com.graphhopper.reader.osm.conditional.DateRangeParser;
Expand Down Expand Up @@ -95,6 +96,7 @@ public class GraphHopper {
private final LinkedHashMap<String, String> dataAccessConfig = new LinkedHashMap<>();
private boolean sortGraph = false;
private boolean elevation = false;
private boolean elevationEdgeInterpolationEnabled = true;
private LockFactory lockFactory = new NativeFSLockFactory();
private boolean allowWrites = true;
private boolean fullyLoaded = false;
Expand Down Expand Up @@ -185,6 +187,16 @@ public GraphHopper setElevationProvider(ElevationProvider eleProvider) {
return this;
}

public GraphHopper setElevationEdgeInterpolationEnabled(boolean enabled) {
ensureNotLoaded();
this.elevationEdgeInterpolationEnabled = enabled;
return this;
}

public boolean isElevationEdgeInterpolationEnabled() {
return elevationEdgeInterpolationEnabled;
}

public GraphHopper setPathDetailsBuilderFactory(PathDetailsBuilderFactory pathBuilderFactory) {
this.pathBuilderFactory = pathBuilderFactory;
return this;
Expand Down Expand Up @@ -577,6 +589,7 @@ public GraphHopper init(GraphHopperConfig ghConfig) {
osmReaderConfig.setElevationSmoothingRamerMax(ghConfig.getInt("graph.elevation.edge_smoothing.ramer.max_elevation", osmReaderConfig.getElevationSmoothingRamerMax()));
osmReaderConfig.setLongEdgeSamplingDistance(ghConfig.getDouble("graph.elevation.long_edge_sampling_distance", osmReaderConfig.getLongEdgeSamplingDistance()));
osmReaderConfig.setElevationMaxWayPointDistance(ghConfig.getDouble("graph.elevation.way_point_max_distance", osmReaderConfig.getElevationMaxWayPointDistance()));
elevationEdgeInterpolationEnabled = ghConfig.getBool("graph.elevation.edge_interpolation", elevationEdgeInterpolationEnabled);
routerConfig.setElevationWayPointMaxDistance(ghConfig.getDouble("graph.elevation.way_point_max_distance", routerConfig.getElevationWayPointMaxDistance()));
ElevationProvider elevationProvider = createElevationProvider(ghConfig);
setElevationProvider(elevationProvider);
Expand Down Expand Up @@ -762,56 +775,7 @@ public static Map<String, String> getVehiclesByName(String vehiclesStr, Collecti
}

private static ElevationProvider createElevationProvider(GraphHopperConfig ghConfig) {
String eleProviderStr = toLowerCase(ghConfig.getString("graph.elevation.provider", "noop"));

if (ghConfig.has("graph.elevation.calcmean"))
throw new IllegalArgumentException("graph.elevation.calcmean is deprecated, use graph.elevation.interpolate");

String cacheDirStr = ghConfig.getString("graph.elevation.cache_dir", "");
if (cacheDirStr.isEmpty() && ghConfig.has("graph.elevation.cachedir"))
throw new IllegalArgumentException("use graph.elevation.cache_dir not cachedir in configuration");

ElevationProvider elevationProvider = ElevationProvider.NOOP;
if (eleProviderStr.equalsIgnoreCase("hgt")) {
elevationProvider = new HGTProvider(cacheDirStr);
} else if (eleProviderStr.equalsIgnoreCase("srtm")) {
elevationProvider = new SRTMProvider(cacheDirStr);
} else if (eleProviderStr.equalsIgnoreCase("cgiar")) {
elevationProvider = new CGIARProvider(cacheDirStr);
} else if (eleProviderStr.equalsIgnoreCase("gmted")) {
elevationProvider = new GMTEDProvider(cacheDirStr);
} else if (eleProviderStr.equalsIgnoreCase("srtmgl1")) {
elevationProvider = new SRTMGL1Provider(cacheDirStr);
} else if (eleProviderStr.equalsIgnoreCase("multi")) {
elevationProvider = new MultiSourceElevationProvider(cacheDirStr);
} else if (eleProviderStr.equalsIgnoreCase("skadi")) {
elevationProvider = new SkadiProvider(cacheDirStr);
}

if (elevationProvider instanceof TileBasedElevationProvider) {
TileBasedElevationProvider provider = (TileBasedElevationProvider) elevationProvider;

String baseURL = ghConfig.getString("graph.elevation.base_url", "");
if (baseURL.isEmpty() && ghConfig.has("graph.elevation.baseurl"))
throw new IllegalArgumentException("use graph.elevation.base_url not baseurl in configuration");

DAType elevationDAType = DAType.fromString(ghConfig.getString("graph.elevation.dataaccess", "MMAP"));

boolean interpolate = ghConfig.has("graph.elevation.interpolate")
? "bilinear".equals(ghConfig.getString("graph.elevation.interpolate", "none"))
: ghConfig.getBool("graph.elevation.calc_mean", false);

boolean removeTempElevationFiles = ghConfig.getBool("graph.elevation.cgiar.clear", true);
removeTempElevationFiles = ghConfig.getBool("graph.elevation.clear", removeTempElevationFiles);

provider
.setAutoRemoveTemporaryFiles(removeTempElevationFiles)
.setInterpolate(interpolate)
.setDAType(elevationDAType);
if (!baseURL.isEmpty())
provider.setBaseURL(baseURL);
}
return elevationProvider;
return ElevationProvider.NOOP;
}

private void printInfo() {
Expand Down Expand Up @@ -1241,6 +1205,10 @@ protected void importPublicTransit() {
}

void interpolateBridgesTunnelsAndFerries() {
// Use configuration to skip this step when elevations are already preprocessed (e.g. iOS build pipeline).
if (!elevationEdgeInterpolationEnabled)
return;

if (encodingManager.hasEncodedValue(RoadEnvironment.KEY)) {
EnumEncodedValue<RoadEnvironment> roadEnvEnc = encodingManager.getEnumEncodedValue(RoadEnvironment.KEY, RoadEnvironment.class);
StopWatch sw = new StopWatch().start();
Expand All @@ -1250,10 +1218,11 @@ void interpolateBridgesTunnelsAndFerries() {
new EdgeElevationInterpolator(baseGraph.getBaseGraph(), roadEnvEnc, RoadEnvironment.BRIDGE).execute();
float bridge = sw.stop().getSeconds();
// The SkadiProvider contains bathymetric data. For ferries this can result in bigger elevation changes
// See #2098 for mor information
// See #2098 for more information
sw = new StopWatch().start();
new EdgeElevationInterpolator(baseGraph.getBaseGraph(), roadEnvEnc, RoadEnvironment.FERRY).execute();
logger.info("Bridge interpolation " + (int) bridge + "s, " + "tunnel interpolation " + (int) tunnel + "s, ferry interpolation " + (int) sw.stop().getSeconds() + "s");
logger.info("Bridge interpolation {}s, tunnel interpolation {}s, ferry interpolation {}s",
(int) bridge, (int) tunnel, (int) sw.stop().getSeconds());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;

public class EncodedValueSerializer {
private final static ObjectMapper MAPPER = new ObjectMapper();

static {
MAPPER.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
MAPPER.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
MAPPER.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
}

public static String serializeEncodedValue(EncodedValue encodedValue) {
Expand Down
4 changes: 2 additions & 2 deletions web-api/src/main/java/com/graphhopper/jackson/Jackson.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import com.bedatadriven.jackson.datatype.jts.JtsModule;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;

public class Jackson {

Expand All @@ -31,7 +31,7 @@ public static ObjectMapper newObjectMapper() {
public static ObjectMapper initObjectMapper(ObjectMapper objectMapper) {
objectMapper.registerModule(new GraphHopperModule());
objectMapper.registerModule(new JtsModule());
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return objectMapper;
}
Expand Down
Loading