A Spring Boot application for OpenLR encoding/decoding using PostgreSQL + PostGIS with a minimal OpenLR schema. Includes an interactive web visualization tool for decoding and analyzing OpenLR location references.
- Interactive Web Visualization Tool: Browser-based interface with Leaflet maps, measurement tools, and LRP display
- PostgreSQL + PostGIS: Spatial database backend
- Minimal OpenLR Schema: Two-table design (
local.roadsandlocal.intersections) - Docker Compose Deployment: Containerized with database setup sidecar
- Multiple Decode Formats: JSON POST, form data POST, and GET query parameters
- GeoJSON Responses: GeoJSON FeatureCollection output
- Configurable Decoder: Multiple decoding profiles (default, relaxed, strict)
- Database Setup Sidecar: Data loading with psql and ogr2ogr tools
- Docker and Docker Compose
- (Optional) Java 17+ and Gradle for local development
cd docker
./dc build./dc upThe application will be available at http://localhost:8081
Create a directory with your data files and a dbsetup.sh script:
mkdir ~/my-map-data
cd ~/my-map-data
# Create setup script
cat > dbsetup.sh <<'EOF'
#!/bin/bash
set -e
# Load roads from shapefile
ogr2ogr -f PostgreSQL \
PG:"host=postgres port=5432 dbname=openlr_db user=openlr password=openlrpwd" \
roads.shp \
-nln local.roads \
-lco GEOMETRY_NAME=geom \
-lco SPATIAL_INDEX=GIST \
-t_srs EPSG:4326
EOF
# Run database setup
cd /path/to/webtool/docker
./dc setup ~/my-map-data# JSON POST with single profile
curl -X POST http://localhost:8081/api/v1/decode \
-H "Content-Type: application/json" \
-d '{"openLrCode": "CwV/mSIeQA4kBgFxAJ8OEA==", "props": "default"}'
# JSON POST with fallback profiles (try strict, then relaxed, then default)
curl -X POST http://localhost:8081/api/v1/decode \
-H "Content-Type: application/json" \
-d '{"openLrCode": "CwV/mSIeQA4kBgFxAJ8OEA==", "props": "strict,relaxed,default"}'
# Form data POST with fallback profiles
curl -X POST http://localhost:8081/api/v1/decode \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "openLrCode=CwV/mSIeQA4kBgFxAJ8OEA==" \
-d "props=strict,relaxed,default"
# HTTPie with fallback profiles
http --ignore-stdin -f http://localhost:8081/api/v1/decode \
openLrCode=CwV/mSIeQA4kBgFxAJ8OEA== \
props=strict,relaxed,default
# Returns GeoJSON FeatureCollection with road segments
# The meta.propertySet field indicates which profile succeededSee docker/README.md for complete deployment documentation.
The OpenLR WebTool includes a browser-based visualization interface for interactive OpenLR decoding and map analysis.
The webapp provides:
- Interactive Map Interface: Leaflet-based map for visualizing OpenLR decoding results
- Client-Side OpenLR Parsing: Uses openlr-js library to parse OpenLR binary codes
- Location Reference Point (LRP) Display: Shows LRPs even when backend decoding fails
- Measurement Tools: Distance and bearing measurement tools
- Configurable Decoder: Select between default, strict, and relaxed decoding profiles
- Detailed Diagnostics: Comprehensive error reporting with diagnostic information
The webapp consists of three Docker containers:
- postgres (port 5432): PostGIS database with OpenLR schema
- app (port 8081): Spring Boot backend API
- webapp (port 3000): Node.js/Express server serving static frontend
The frontend uses:
- Leaflet 1.9.4: Interactive map library
- openlr-js v3.x: Client-side OpenLR binary parsing
- Leaflet TextPath: Arrow visualization for bearing measurements
- Docker and Docker Compose
- Map data loaded into PostgreSQL (see Quick Start section above)
cd docker
./dc upThis starts all three containers:
- PostgreSQL + PostGIS (localhost:5432)
- OpenLR API backend (localhost:8081)
- Webapp frontend (localhost:3000)
Open your browser to:
http://localhost:3000
To run the webapp on a different port:
# Run webapp on port 3002
cd docker
WEBAPP_PORT=3002 ./dc up
# Access at http://localhost:3002cd docker
# Build webapp only
./dc build webapp
WEBAPP_PORT=3002 docker-compose build webapp
# Restart webapp only
./dc restart webapp
WEBAPP_PORT=3002 docker-compose restart webapp
# View webapp logs
./dc logs webapp
WEBAPP_PORT=3002 docker-compose logs -f webapp
# Stop webapp only
WEBAPP_PORT=3002 docker-compose stop webapp
# Rebuild and restart webapp
WEBAPP_PORT=3002 docker-compose build webapp && \
WEBAPP_PORT=3002 docker-compose up -d webapp-
Enter OpenLR Code: Paste a base64-encoded OpenLR code in the input field
- Example:
CwV/mSIeQA4kBgFxAJ8OEA==
- Example:
-
Select Decoder Profile: Choose from dropdown:
default: Balanced parameters for general usestrict: Higher matching requirementsrelaxed: More lenient matching
-
Click Decode: The system will:
- Parse the OpenLR binary using openlr-js (client-side)
- Send to backend for map matching
- Display results on the map
When decoding succeeds:
- Decoded Path: Displayed as a blue polyline on the map
- LRP Markers: Location Reference Points shown as numbered markers
- Offset Markers: Start/end points shown with offset adjustments
- Sidebar Information:
- Offsets (absolute and relative percentages)
- Path metadata (length, applied offsets)
- LRP details (coordinates, bearing, FRC, FOW, distance)
- Segment information (FRC, FOW, length)
When decoding fails:
- LRPs Still Displayed: Shows parsed Location Reference Points even without successful map matching
- Error Details: Click the red error banner for comprehensive diagnostics including:
- Error type and category
- Backend response details
- HTTP status codes
- Request details
- Client-side parsing status
- Troubleshooting hints
- Click the ruler button (📏) in the top-left map controls
- Click points on the map to measure distances
- See segment distances and cumulative total
- Double-click to finish measurement
- Click the ruler button again to clear and start new measurement
Display format:
Segment: 123.45m
Total: 456.78m
- Click the compass button (🧭) in the top-left map controls
- Click the first point (origin)
- Click the second point (destination)
- See bearing information:
- Forward bearing (A→B)
- Reverse bearing (B→A)
- Distance between points
- Visual arrow shows direction
- Click the compass button again to clear and start new measurement
Display format:
Bearing: 123.4°
Reverse: 303.4°
Distance: 456.78m
Bearing values:
- 0° = North
- 90° = East
- 180° = South
- 270° = West
-
Click Segment Rows: Click any row in the Segments table to:
- Highlight the segment on the map (yellow highlight)
- Zoom to the segment
- Display segment details in a popup
-
Hover Over Rows: Hover to preview segment location
-
Collapsible Sections: Click section headers to expand/collapse:
- Offsets
- Path Metadata
- Location Reference Points
- Segments
-
Resizable Sidebar: Drag the right edge of the sidebar to resize
-
Toggle Sidebar: Click the arrow button on the left edge to show/hide
When decoding fails, click the red error banner to view:
- Error Basics: Type, message, category
- Connection Errors: Backend unavailable or network issues
- HTTP Errors: Status codes and server responses
- Backend Responses: Detailed error messages from the API
- Request Details: OpenLR code, decoder profile, timestamp
- Client-Side Status: Whether openlr-js successfully parsed the binary
- Stack Trace: Full JavaScript error stack (when available)
- Troubleshooting Hints: Suggested actions based on error type
The webapp container accepts the following environment variables:
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Port webapp listens on (inside container) |
WEBAPP_PORT |
3000 |
Port exposed on host machine |
API_URL |
http://app:8081 |
Backend API URL |
Custom backend API:
cd docker
API_URL=http://custom-api:9000 WEBAPP_PORT=3002 ./dc up webappProduction deployment:
cd docker
WEBAPP_PORT=80 API_URL=http://api.production.com:8081 ./dc up webappThe webapp container includes health checks:
- Interval: Every 30 seconds
- Timeout: 3 seconds
- Retries: 3
- Start Period: 10 seconds
Check webapp health:
docker ps --filter name=openlr-webapp
curl http://localhost:3000/healthwebapp/
├── Dockerfile # Node.js Alpine-based container
├── package.json # Node.js dependencies (express, http-proxy-middleware)
├── server.js # Express server with API proxy
└── public/
├── index.html # Main application page
├── css/
│ └── style.css # Application styles
└── js/
└── app.js # Main application logic
cd webapp
# Install dependencies
npm install
# Set environment variables
export PORT=3000
export API_URL=http://localhost:8081
# Start development server
npm run dev
# Access at http://localhost:3000-
Edit Files: Modify files in
webapp/public/public/index.html- HTML structurepublic/css/style.css- Stylingpublic/js/app.js- Application logic
-
Update Cache Buster: Increment version in
index.html:<script src="/js/app.js?v=24"></script>
-
Rebuild Container:
cd docker WEBAPP_PORT=3002 docker-compose build webapp WEBAPP_PORT=3002 docker-compose up -d webapp -
Verify Changes: Check logs and reload browser
docker logs openlr-webapp
Edit webapp/public/index.html to add CDN libraries:
<!-- Example: Adding a new Leaflet plugin -->
<script src="https://unpkg.com/leaflet-plugin@1.0.0/dist/plugin.js"></script>For Node.js dependencies, edit webapp/package.json and rebuild.
Browser Console: The webapp does not output console logs in production. Use browser DevTools:
- Network tab: Monitor API requests
- Elements tab: Inspect DOM and styles
- Sources tab: Debug JavaScript with breakpoints
Server Logs: View Express server output:
docker logs -f openlr-webappAPI Proxy: The webapp proxies /api/* requests to the backend:
http://localhost:3000/api/v1/decode → http://app:8081/api/v1/decode
# Check container status
docker ps -a | grep openlr-webapp
# View logs
docker logs openlr-webapp
# Check dependencies
docker ps | grep openlr-tool
docker ps | grep openlr-postgres
# Restart with fresh build
cd docker
WEBAPP_PORT=3002 docker-compose stop webapp
WEBAPP_PORT=3002 docker-compose rm -f webapp
WEBAPP_PORT=3002 docker-compose build --no-cache webapp
WEBAPP_PORT=3002 docker-compose up -d webappSymptoms: Decoding fails with "Network/Connection Error"
Solutions:
- Verify backend is running:
docker ps | grep openlr-tool - Check backend health:
curl -f -X POST http://localhost:8081/api/v1/cache/clear - Verify proxy configuration in
webapp/server.js - Check container networking:
docker network inspect openlr_network
Symptoms: Blank map area, no tiles
Solutions:
- Check browser console for errors
- Verify internet connection (map tiles load from external CDN)
- Check if Leaflet CSS loaded: View page source, verify CSS link
- Try different browser or clear browser cache
Symptoms: Decode succeeds but sidebar doesn't appear
Solutions:
- Check if toggle button visible (left edge of map)
- Clear browser cache and hard reload (Ctrl+Shift+R / Cmd+Shift+R)
- Check browser console for JavaScript errors
- Verify cache buster version in URL:
app.js?v=24
Symptoms: Container fails to start, port conflict error
Solutions:
# Use different port
WEBAPP_PORT=3002 ./dc up
# Find process using port 3000
lsof -i :3000
# Stop conflicting container
docker stop <container-using-port># Build the application
./gradlew bootJar
# Run with local PostgreSQL
./gradlew bootRun --args='--spring.config.location=file:config/application.properties'Edit config/application.properties to point to your PostgreSQL instance:
# Database connection
spring.profiles.active=generic_pg_mapdb
spring.datasource.url=jdbc:postgresql://localhost:5432/openlr_db
spring.datasource.username=openlr
spring.datasource.password=openlrpwd
# Server port (default: 8081)
server.port=8081Change listening port:
For local development:
# Via command line
./gradlew bootRun --args='--server.port=9000'
# Or edit config/application.properties
server.port=9000For Docker deployment:
# Use PORT environment variable
PORT=9000 ./docker/dc upThe application uses a minimal two-table schema in PostgreSQL:
Road segments representing the map network.
| Column | Type | Description |
|---|---|---|
id |
bigint | Unique road segment identifier (positive/negative for direction) |
meta |
text | Optional metadata/UUID |
flowdir |
smallint | Flow direction (0=both, 1=forward, 2=backward) |
fow |
smallint | Form of way classification |
frc |
smallint | Functional road class (0-7, 0=motorway, 7=other) |
geom |
geometry(LineString,4326) | Line geometry in WGS84 |
len |
double precision | Length in meters |
from_int |
bigint | Starting intersection ID |
to_int |
bigint | Ending intersection ID |
Junction points where road segments connect.
| Column | Type | Description |
|---|---|---|
id |
bigint | Unique intersection identifier |
meta |
text | Optional metadata/UUID |
geom |
geometry(Point,4326) | Point geometry in WGS84 |
Connection string: postgresql://openlr:openlrpwd@postgres:5432/openlr_db
Decode an OpenLR code to map geometry.
Endpoint: POST /api/v1/decode
Three Supported Formats:
- JSON POST:
curl -X POST http://localhost:8081/api/v1/decode \
-H "Content-Type: application/json" \
-d '{"openLrCode": "CwV/mSIeQA4kBgFxAJ8OEA==", "props": "default"}'- Form Data POST:
curl -X POST http://localhost:8081/api/v1/decode \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "openLrCode=CwV/mSIeQA4kBgFxAJ8OEA==" \
--data-urlencode "props=default"- GET with Query Parameters:
curl "http://localhost:8081/api/v1/decode?openLrCode=CwV/mSIeQA4kBgFxAJ8OEA==&props=default"Parameters:
openLrCode(required): Base64-encoded OpenLR location referenceprops(optional, default: "default"): Decoding profile(s) to try in order. Can be a single profile or comma-separated list. The decoder attempts each profile sequentially until one succeeds.
Multiple Profiles (Sequential Fallback):
Provide comma-separated profiles to try in order until one succeeds:
# JSON POST format
curl -X POST http://localhost:8081/api/v1/decode \
-H "Content-Type: application/json" \
-d '{"openLrCode": "CwV/mSIeQA4kBgFxAJ8OEA==", "props": "strict,relaxed,default"}'
# Form data format
curl -X POST http://localhost:8081/api/v1/decode \
-d "openLrCode=CwV/mSIeQA4kBgFxAJ8OEA==" \
-d "props=strict,relaxed,default"
# HTTPie format
http --ignore-stdin -f http://localhost:8081/api/v1/decode \
openLrCode=CwV/mSIeQA4kBgFxAJ8OEA== \
props=strict,relaxed,defaultResponse: GeoJSON FeatureCollection with decoded road segments. The meta.propertySet field indicates which profile succeeded.
Encode a path of road segments as an OpenLR location reference.
Endpoint: POST /api/v1/encode
curl -X POST http://localhost:8081/api/v1/encode \
-d "path=123&path=456&path=-789" \
-d "props=default"Parameters:
path(required, multiple): List of road segment IDs. Use positive IDs to traverse in the forward direction, negative IDs to traverse in the reverse direction (e.g.,path=123&path=-456travels forward on segment 123, then backward on segment 456)positiveOffset(optional, default: 0): Offset in meters from the start of the pathnegativeOffset(optional, default: 0): Offset in meters from the end of the pathprops(optional, default: "default"): Encoding profile
# Purge caches
curl -X POST http://localhost:8081/api/v1/cache/clear
# Reload configuration
curl -X POST http://localhost:8081/api/v1/properties/reload
# Get count of roads and intersections
curl http://localhost:8081/api/v1/stats
# Find roads within a given radius of lat/lon
curl "http://localhost:8081/api/v1/roads/near?lon=-0.1276&lat=51.5074&distance=100"
# Get roads matching a metadata value
curl "http://localhost:8081/api/v1/roads?meta=road_12345"
# Find details of a road with a given id
curl http://localhost:8081/api/v1/roads/123
# Health check (returns status, road count, and intersection count)
curl http://localhost:8081/api/v1/healthThe props parameter selects which decoder configuration to use:
default- Balanced parameters for general userelaxed- More lenient matching (lower rating requirements)strict- Strict matching (higher rating requirements)
Property files are located in config/decoding_properties/.
From config/decoding_properties/default.properties:
BearingDistance=20- Distance for bearing calculationMaxNodeDistance=100- Maximum distance to search for nodesMinimumAcceptedRating=600- Minimum rating to accept a matchFRC_Variance=2- Allowed functional road class varianceMaxNumberRetries=3- Maximum retry attempts
- Edit property files in
config/decoding_properties/orconfig/encoding_properties/ - Create new profiles by adding new
.propertiesfiles - Restart the application to apply changes
See docker/README.md for detailed configuration documentation.
The dc script in the docker/ directory provides convenient commands:
cd docker
# Build
./dc build
# Start/stop
./dc up
./dc down
./dc restart app
# Logs and monitoring
./dc logs app
./dc ps
# Database setup
./dc setup /path/to/data
# Database operations
./dc exec postgres psql -U openlr -d openlr_db
# Cleanup
./dc cleanSee docker/README.md for complete documentation.
.
├── main/ # Spring Boot application (single Gradle module)
│ └── src/main/kotlin/…/tool/
│ ├── WebtoolApplication.kt
│ ├── controller/ # REST API controllers
│ ├── model/ # Request/response data models
│ └── service/ # OpenLR and map database services
├── database/ # SQL schema (schema.sql)
├── config/ # Application configuration
│ ├── application.properties
│ ├── decoding_properties/
│ └── encoding_properties/
├── webapp/ # Web visualization tool
│ ├── Dockerfile
│ ├── package.json
│ ├── server.js # Node.js/Express server
│ └── public/ # Static frontend files
│ ├── index.html
│ ├── css/
│ └── js/
└── docker/ # Docker Compose deployment
├── docker-compose.yml
├── dc # Management script
├── db-setup/ # Database setup sidecar
└── README.md # Complete deployment docs
# Build all modules
./gradlew build
# Create executable JAR
./gradlew bootJar
# Run tests
./gradlew test
# Build Docker image
cd docker && ./dc build# Run all tests
./gradlew test
# Run specific module tests
./gradlew :main:test# 1. Build containers
cd docker
./dc build
# 2. Start services
PORT=8080 JAVA_OPTS="-Xmx32g" ./dc up
# 3. Load map data
./dc setup /path/to/production-data
# 4. Verify health
./dc ps
curl -X POST http://localhost:8080/api/v1/cache/clearBoth PostgreSQL and the application have health checks:
- PostgreSQL: Every 10s, 5s timeout, 5 retries
- Application: Every 30s, 3s timeout, 3 retries
- Minimum RAM: 40GB recommended (PostgreSQL defaults to ~16GB tuning + 24GB JVM heap)
- PostgreSQL: 4GB shared_buffers, 12GB effective_cache_size
- Application: 24GB heap default (configurable via
JAVA_OPTS)
# Backup PostgreSQL data
docker exec openlr-postgres pg_dump -U openlr openlr_db > backup.sql
# Restore PostgreSQL data
cat backup.sql | docker exec -i openlr-postgres psql -U openlr -d openlr_db- docker/README.md - Complete Docker deployment guide
- docker/db-setup/README.md - Database setup sidecar documentation
- config/README.md - Configuration file documentation