Skip to content

Commit f729dd7

Browse files
committed
merged master
2 parents fc0f400 + 0283cf2 commit f729dd7

14 files changed

Lines changed: 1190 additions & 1189 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ With address search (autocomplete), alternative routes, information along the ro
1212

1313
## Turn-by-Turn navigation
1414

15-
There is [an experimental `navi` branch](https://github.com/graphhopper/graphhopper-maps/tree/navi) that implements turn-by-turn navigation
15+
You can download this bundled as an app for Android [on fdroid](https://f-droid.org/de/packages/com.graphhopper.maps/),
16+
where also a turn-by-turn navigation is implemented via Maplibre Navigation SDK
17+
using a capacitor plugin. Read more about it in [this repository](https://github.com/boldtrn/graphhopper-maps-capacitor/).
18+
19+
There was [an experimental `navi` branch](https://github.com/graphhopper/graphhopper-maps/tree/navi) that implements turn-by-turn navigation
1620
[directly in the browser](https://navi.graphhopper.org).
1721

1822
## Start development:

package-lock.json

Lines changed: 1003 additions & 1018 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,18 @@
2020
"geojson": "^0.5.0",
2121
"ol": "10.6.1",
2222
"ol-mapbox-style": "13.1.0",
23-
"react": "^19.1.1",
24-
"react-dom": "^19.1.1",
25-
"react-responsive": "^10.0.1"
23+
"react": "19.2.7",
24+
"react-dom": "19.2.7",
25+
"react-responsive": "10.0.1"
26+
},
27+
"overrides": {
28+
"js-yaml": "^4.2.0",
29+
"uuid": "^11.1.1"
30+
},
31+
"allowScripts": {
32+
"fsevents": false,
33+
"sharp": false,
34+
"unrs-resolver": false
2635
},
2736
"devDependencies": {
2837
"@svgr/webpack": "^8.1.0",
@@ -35,18 +44,18 @@
3544
"favicons": "^7.2.0",
3645
"favicons-webpack-plugin": "^6.0.1",
3746
"html-webpack-plugin": "^5.6.4",
38-
"jest": "^30.0.5",
39-
"jest-environment-jsdom": "^30.0.5",
47+
"jest": "^30.4.2",
48+
"jest-environment-jsdom": "^30.4.1",
4049
"jest-fetch-mock": "^3.0.3",
4150
"prettier": "3.6.2",
4251
"source-map-loader": "^5.0.0",
4352
"style-loader": "^4.0.0",
44-
"ts-jest": "^29.4.1",
53+
"ts-jest": "^29.4.11",
4554
"ts-loader": "^9.5.2",
4655
"typescript": "^5.9.2",
47-
"webpack": "^5.101.3",
48-
"webpack-cli": "^6.0.1",
49-
"webpack-dev-server": "^5.2.2",
56+
"webpack": "^5.107.2",
57+
"webpack-cli": "^7.0.3",
58+
"webpack-dev-server": "^5.2.5",
5059
"webpack-merge": "^6.0.1"
5160
}
5261
}

src/NavBar.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { coordinateToText } from '@/Converters'
22
import Dispatcher from '@/stores/Dispatcher'
3-
import { ClearPoints, SelectMapLayer, SetBBox, SetQueryPoints, SetVehicleProfile } from '@/actions/Actions'
3+
import {
4+
ClearPoints,
5+
DisableCustomModel,
6+
SelectMapLayer,
7+
SetBBox,
8+
SetCustomModel,
9+
SetQueryPoints,
10+
SetVehicleProfile,
11+
} from '@/actions/Actions'
412
// import the window like this so that it can be mocked during testing
513
import { window } from '@/Window'
614
import QueryStore, { QueryPoint, QueryPointType, QueryStoreState } from '@/stores/QueryStore'
@@ -150,6 +158,10 @@ export default class NavBar {
150158
const parsedLayer = NavBar.parseLayer(url)
151159
if (parsedLayer) Dispatcher.dispatch(new SelectMapLayer(parsedLayer))
152160

161+
const customModelParam = url.searchParams.get('custom_model')
162+
if (customModelParam != null) Dispatcher.dispatch(new SetCustomModel(customModelParam, false))
163+
else Dispatcher.dispatch(new DisableCustomModel())
164+
153165
this.ignoreStateUpdates = false
154166
}
155167

src/actions/Actions.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,6 @@ export class InvalidatePoint implements Action {
9797
}
9898
}
9999

100-
export class SetCustomModelEnabled implements Action {
101-
readonly enabled: boolean
102-
103-
constructor(enabled: boolean) {
104-
this.enabled = enabled
105-
}
106-
}
107-
108100
export class SetCustomModel implements Action {
109101
readonly customModelStr: string
110102
readonly issueRoutingRequest: boolean
@@ -115,6 +107,8 @@ export class SetCustomModel implements Action {
115107
}
116108
}
117109

110+
export class DisableCustomModel implements Action {}
111+
118112
export class RouteRequestSuccess implements Action {
119113
readonly result: RoutingResult
120114
readonly request: RoutingArgs

src/api/Api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ export class ApiImpl implements Api {
106106
url.searchParams.append('osm_tag', '!place:county')
107107
url.searchParams.append('osm_tag', '!boundary')
108108
url.searchParams.append('osm_tag', '!historic')
109+
// display no results for tourist info signs
110+
url.searchParams.append('osm_tag', '!information')
109111

110112
if (additionalOptions) {
111113
for (const key in additionalOptions) {

src/pathDetails/elevationWidget/ChartRenderer.ts

Lines changed: 6 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import {
66
computeDetailYRange,
77
formatDetailTick,
88
} from './axisUtils'
9-
import { getSlopeColor } from './colors'
10-
import { computeWindowedSlopes } from './pathDetailData'
9+
import { computeElevationColorRuns } from './pathDetailData'
1110

1211
const DEFAULT_MARGIN = { top: 10, right: 15, bottom: 26, left: 48 }
1312
const DETAIL_BAR_HEIGHT = 50
@@ -190,7 +189,7 @@ export default class ChartRenderer {
190189
lines.push(`distance: ${formatDistanceLabel(hit.distance, miles)}`)
191190
} else {
192191
// Default: elevation, incline, distance
193-
lines.push(`elevation: ${formatElevationLabel(hit.elevation, miles)}`)
192+
lines.push(`elevation: ${formatElevationLabel(hit.elevation, miles, 1)}`)
194193

195194
// Compute incline between the two surrounding elevation points
196195
const i = hit.elevationIndex
@@ -443,50 +442,14 @@ export default class ChartRenderer {
443442
}
444443

445444
// For long routes the elevation data can have thousands of points, leading to
446-
// hundreds of tiny color-change polygons that create visual noise. To avoid this
447-
// we quantize into bins of at least MIN_BIN_PX pixels wide. Within each bin we
448-
// pick the steepest slope to determine the color — this preserves steep sections
449-
// visually even when they span only a few data points. Adjacent bins with the
450-
// same color are then merged into single polygons to minimize draw calls.
451-
// Slopes are computed over a fixed forward distance (see computeWindowedSlopes)
452-
// so polyline-quantization noise can't paint long bins as steep climbs/descents.
445+
// hundreds of tiny color-change polygons that create visual noise. We quantize
446+
// into bins at least MIN_BIN_PX pixels wide, colored by net grade and merged by
447+
// color (see computeElevationColorRuns), then draw one polygon per run.
453448
const totalDist = elev[elev.length - 1].distance
454449
const plotWidth = xScale(totalDist) - xScale(0)
455450
const MIN_BIN_PX = 1
456451
const minBinDist = (MIN_BIN_PX / plotWidth) * totalDist
457-
const slopes = computeWindowedSlopes(elev)
458-
const bins: { fromIdx: number; toIdx: number; color: string }[] = []
459-
let binStart = 0
460-
let maxAbsSlope = 0
461-
let steepestSlope = 0
462-
463-
for (let i = 0; i < elev.length - 1; i++) {
464-
const slope = slopes[i]
465-
const absSlope = Math.abs(slope)
466-
if (absSlope > maxAbsSlope) {
467-
maxAbsSlope = absSlope
468-
steepestSlope = slope
469-
}
470-
471-
const binSpan = elev[i + 1].distance - elev[binStart].distance
472-
if (binSpan >= minBinDist || i === elev.length - 2) {
473-
bins.push({ fromIdx: binStart, toIdx: i + 1, color: getSlopeColor(steepestSlope) })
474-
binStart = i + 1
475-
maxAbsSlope = 0
476-
steepestSlope = 0
477-
}
478-
}
479-
480-
// Merge consecutive bins with the same color
481-
const runs: { fromIdx: number; toIdx: number; color: string }[] = []
482-
for (const bin of bins) {
483-
const last = runs[runs.length - 1]
484-
if (last && last.color === bin.color) {
485-
last.toIdx = bin.toIdx
486-
} else {
487-
runs.push({ ...bin })
488-
}
489-
}
452+
const runs = computeElevationColorRuns(elev, minBinDist)
490453

491454
// Draw each run as a single filled polygon
492455
for (const run of runs) {

src/pathDetails/elevationWidget/ElevationWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ export default function ElevationWidget({
237237
>
238238
{isExpanded ? '\u25C0' : '\u25B6'}
239239
</button>
240-
{isExpanded && onClose && (
240+
{onClose && (
241241
<button className={styles.closeButton} onClick={onClose} title="Close">
242242
<Cross />
243243
</button>

src/pathDetails/elevationWidget/axisUtils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,12 @@ export function formatDistanceLabel(meters: number, showMiles: boolean): string
5555
/**
5656
* Format an elevation value for the y-axis label.
5757
*/
58-
export function formatElevationLabel(meters: number, showMiles: boolean): string {
58+
export function formatElevationLabel(meters: number, showMiles: boolean, decimals = 0): string {
59+
const round = (n: number) => (decimals > 0 ? n.toFixed(decimals) : Math.round(n))
5960
if (showMiles) {
60-
return `${Math.round(meters / 0.3048)} ft`
61+
return `${round(meters / 0.3048)} ft`
6162
}
62-
return `${Math.round(meters)} m`
63+
return `${round(meters)} m`
6364
}
6465

6566
function formatNumber(n: number): string {

src/pathDetails/elevationWidget/pathDetailData.ts

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,44 @@ export interface PathLike {
2323
distance: number
2424
}
2525

26-
// Distance window (meters) for slope computation. Encoded polylines quantize
27-
// elevation to ~0.01m, which makes ~1m bumps over <20m sub-segments read as
28-
// 10%+ slopes. Computing slope over a fixed forward distance filters that
29-
// noise while preserving real sustained gradients (typical climbs span ≥100m).
30-
export const SLOPE_HORIZON_M = 30
31-
32-
// Per-segment slope (%) using a forward distance window of SLOPE_HORIZON_M.
33-
// Returned array has length elevation.length - 1, indexed by segment start.
34-
export function computeWindowedSlopes(elevation: ElevationPoint[]): number[] {
35-
const n = elevation.length
36-
if (n < 2) return []
37-
const slopes: number[] = new Array(n - 1)
38-
let j = 1
39-
for (let i = 0; i < n - 1; i++) {
40-
if (j < i + 1) j = i + 1
41-
while (j < n - 1 && elevation[j].distance - elevation[i].distance < SLOPE_HORIZON_M) j++
42-
const dist = elevation[j].distance - elevation[i].distance
43-
slopes[i] = dist > 0 ? (100 * (elevation[j].elevation - elevation[i].elevation)) / dist : 0
26+
export interface ElevationColorRun {
27+
fromIdx: number
28+
toIdx: number
29+
color: string
30+
}
31+
32+
// Quantize an elevation profile into colored runs for the area fill. Points are
33+
// grouped into bins at least minBinDist meters wide and each bin is colored by
34+
// its net (distance-weighted) grade — total elevation change over the bin's
35+
// distance. At normal zoom a bin is a single segment, so the color equals the
36+
// grade the hover popup reports there; when many points share one pixel the net
37+
// grade averages them, so a single noisy sample can't paint the whole pixel as a
38+
// steep climb/descent. Consecutive bins with the same color are merged so the
39+
// renderer draws one polygon per run. Indices refer to the input array.
40+
export function computeElevationColorRuns(elevation: ElevationPoint[], minBinDist: number): ElevationColorRun[] {
41+
const bins: ElevationColorRun[] = []
42+
let binStart = 0
43+
for (let i = 0; i < elevation.length - 1; i++) {
44+
const binSpan = elevation[i + 1].distance - elevation[binStart].distance
45+
if (binSpan >= minBinDist || i === elevation.length - 2) {
46+
const slope =
47+
binSpan > 0 ? (100 * (elevation[i + 1].elevation - elevation[binStart].elevation)) / binSpan : 0
48+
bins.push({ fromIdx: binStart, toIdx: i + 1, color: getSlopeColor(slope) })
49+
binStart = i + 1
50+
}
51+
}
52+
53+
// Merge consecutive bins with the same color.
54+
const runs: ElevationColorRun[] = []
55+
for (const bin of bins) {
56+
const last = runs[runs.length - 1]
57+
if (last && last.color === bin.color) {
58+
last.toIdx = bin.toIdx
59+
} else {
60+
runs.push({ ...bin })
61+
}
4462
}
45-
return slopes
63+
return runs
4664
}
4765

4866
export function extractElevationPoints(coordinates: number[][]): ElevationPoint[] {
@@ -319,13 +337,13 @@ export function buildInclineDetail(elevation: ElevationPoint[]): ChartPathDetail
319337
return { key: '_incline', label: 'Incline', type: 'bars', segments: [], legend }
320338
}
321339

322-
// Compute slope between consecutive points and assign incline colors
323-
const slopes = computeWindowedSlopes(elevation)
340+
// Color each segment by its exact grade so the map matches the elevation popup.
324341
const raw: PathDetailSegment[] = []
325342
for (let i = 0; i < elevation.length - 1; i++) {
326343
const p = elevation[i]
327344
const q = elevation[i + 1]
328-
const slopePercent = slopes[i]
345+
const dist = q.distance - p.distance
346+
const slopePercent = dist > 0 ? ((q.elevation - p.elevation) / dist) * 100 : 0
329347
const color = getSlopeColor(slopePercent)
330348
raw.push({
331349
fromDistance: p.distance,

0 commit comments

Comments
 (0)