ol-elevation-profile 0.5.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  A synchronized, themeable **elevation profile control for [OpenLayers](https://openlayers.org/)**, rendered with [d3](https://d3js.org/).
11
11
 
12
- It reads elevation (**Z**) directly from a track's geometry (`[lon, lat, z]` GPX/GeoJSON), so **no elevation service is queried** — distance, ascent/descent and min/max are computed from the track itself. Clicking (or hovering) a track shows its profile; a marker stays synchronized on both the map and the chart, and a click on the empty map hides it. The control is fully responsive (full-width docked bar on phones), supports slope-class colouring, metric smoothing, an A↔B crop, six themes, transparency, and a track-colour mode.
12
+ It reads elevation (**Z**) directly from a track's geometry (`[lon, lat, z]` GPX/GeoJSON) — distance, ascent/descent and min/max are computed from the track itself, with no service involved. A track that carries **no** Z is filled from keyless [terrain tiles](#terrain-model), which is on by default; `dem: null` turns it off. Clicking (or hovering) a track shows its profile; a marker stays synchronized on both the map and the chart, and a click on the empty map hides it. The control is fully responsive (full-width docked bar on phones), supports slope-class colouring, metric smoothing, an A↔B crop, six themes, transparency, and a track-colour mode.
13
13
 
14
14
  ## Screenshots
15
15
 
@@ -68,29 +68,43 @@ vectorSource.addFeatures(feats)
68
68
  profile.setFeature(feats[0]) // or let a click on the track select it
69
69
  ```
70
70
 
71
+ Any OpenLayers-readable format works (GeoJSON, GPX, KML, …) — the control only consumes OL `Feature`s. Read GPX/KML with `ol.format.GPX` / `ol.format.KML` and pass the line feature to `setFeature`.
72
+
71
73
  Full option reference: see the [documentation](#documentation).
72
74
 
73
75
  ## Features
74
76
 
75
77
  ### Slope classes
76
78
 
77
- With `slope: true`, the profile is split into contiguous portions of the same slope class (width `slopeClassSize`, in %), capped at `maxClasses` (default 8). Colours run from **blue** (flattest class) to **red** (steepest), via cyan/green and a pure yellow, spread across the classes that are actually present not stretched on the real maximum slope. A vertical separator marks each class change, and a legend appears under the title.
79
+ With `slope: true`, the profile is split into contiguous portions of the same slope class (width `slopeClassSize`, in %), capped at `maxClasses` (default 8). Colours run from **blue** (flattest class) to **red** (steepest), via cyan/green and a pure yellow, spread across the classes that are actually present (not stretched on the real maximum slope). A vertical separator marks each class change, and a legend appears under the title.
78
80
 
79
81
  ### Smoothing
80
82
 
81
83
  `smoothing` is a sliding-window average over **metres** of track (`0` = none, the default). Because the unit is metric, the result is independent of GPS point density; it softens both the profile and the slope.
82
84
 
85
+ ### Terrain model
86
+
87
+ A track with **no Z** — drawn by hand, traced over a basemap, exported by a tool that drops the third dimension — would get no profile. The missing elevations are read from [AWS Terrain Tiles](https://registry.opendata.aws/terrain-tiles/) instead, **by default**: PNG tiles carrying elevation in their R/G/B channels, **no API key, no quota, no rate limit**. A 10 000-point track costs a handful of tiles where a free elevation API would cost 100 requests. Pass `dem: null` to disable it and keep the control off the network.
88
+
89
+ Elevation is interpolated bilinearly between the four surrounding pixels — reading the containing pixel would make the profile advance in stairs, and every stair counts as a climb then a descent in the D+. A track is filled **entirely or not at all**: a profile missing a few points dives to sea level and its D+ becomes absurd. Tracks that already carry their own Z are untouched.
90
+
91
+ Accuracy is roughly 30–90 m depending on the region (mean 16 m from IGN's 1 m reference on steep alpine terrain). Any XYZ tile set in `terrarium` or `mapbox` encoding can be used instead. See the [guide](https://lc-4918.github.io/ol-elevation-profile/guide/features#terrain-model).
92
+
83
93
  ### Attributions
84
94
 
85
95
  When the profile occupies the bottom-right corner (or full-width at the bottom), the OpenLayers attribution control is automatically lifted **above** the profile, right-aligned, with a vertical gap equal to the map-edge-to-profile-bottom gap. Other placements leave the attribution untouched.
86
96
 
97
+ ### Time
98
+
99
+ If the track carries time data (`coordTimes` ISO timestamps from GPX `<time>`, `coordinateProperties.times`, or a 4th `M` coordinate), add `'duration'` to `headerItems` for the **total elapsed time** in the title, and `'time'` to `tooltipItems` for the **elapsed time at the cursor**. The unit adapts: `7 sec`, `26 min`, `1 h 48 min`, `2 j 3 h`. By default this is **moving time** (stopped segments below `stopSpeed`, 0.5 m/s, are excluded); set `ignoreStops: false` for raw wall-clock time. Detect availability with `OlElevationProfile.featureHasTime(feature)`.
100
+
87
101
  ## Demo
88
102
 
89
- Live, interactive demo (toggle every option): **https://lc-4918.github.io/ol-elevation-profile/demo/**
103
+ Live, interactive [demo](https://lc-4918.github.io/ol-elevation-profile/demo/) (toggle every option)
90
104
 
91
105
  ## Documentation
92
106
 
93
- Full guide and API (English & French): **https://lc-4918.github.io/ol-elevation-profile/**
107
+ [Full guide and API](https://lc-4918.github.io/ol-elevation-profile/) (English & French)
94
108
 
95
109
  ## License
96
110
 
@@ -21,7 +21,7 @@
21
21
  }
22
22
  .ol-elevation-profile.oep-transparent { backdrop-filter: blur(2px); }
23
23
 
24
- /* ---- 8 ancrages ---- */
24
+ /* ---- 8 anchors ---- */
25
25
  .ol-elevation-profile.oep-pos-bottom { left: 50%; bottom: .6em; transform: translateX(-50%); }
26
26
  .ol-elevation-profile.oep-pos-top { left: 50%; top: .6em; transform: translateX(-50%); }
27
27
  .ol-elevation-profile.oep-pos-left { left: .6em; top: 50%; transform: translateY(-50%); }
@@ -31,12 +31,12 @@
31
31
  .ol-elevation-profile.oep-pos-bottom-left { left: .6em; bottom: .6em; }
32
32
  .ol-elevation-profile.oep-pos-bottom-right { right: .6em; bottom: .6em; }
33
33
 
34
- /* ---- mobile : pleine largeur, collé en haut ou en bas ---- */
34
+ /* ---- mobile: full width, pinned to the top or the bottom ---- */
35
35
  .ol-elevation-profile.oep-mobile { left: 0 !important; right: 0 !important; width: auto !important; transform: none !important; }
36
36
  .ol-elevation-profile.oep-mobile.oep-pos-bottom { bottom: 0; border-radius: 10px 10px 0 0; }
37
37
  .ol-elevation-profile.oep-mobile.oep-pos-top { top: 0; border-radius: 0 0 10px 10px; }
38
38
 
39
- /* ---- entête ---- */
39
+ /* ---- header ---- */
40
40
  .oep-header { display: flex; align-items: center; gap: 8px; width: 100%; padding: 0 2px 2px; }
41
41
  .oep-title { flex: 0 1 auto; min-width: 0; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
42
42
  .oep-title a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
@@ -45,7 +45,7 @@
45
45
  .oep-stats .oep-up { color: #c0392b; }
46
46
  .oep-stats .oep-down { color: #2471a3; }
47
47
 
48
- /* ---- barre d'outils zoom ---- */
48
+ /* ---- zoom toolbar ---- */
49
49
  .oep-toolbar { flex: 0 0 auto; display: inline-flex; gap: 2px; margin-left: auto; }
50
50
  .oep-tbtn {
51
51
  display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 22px;
@@ -65,14 +65,14 @@
65
65
  .oep-toggle:hover { opacity: 1; background: rgba(0, 0, 0, .07); }
66
66
  .oep-toggle svg { width: 15px; height: 15px; display: block; }
67
67
 
68
- /* ---- légende de pente ---- */
68
+ /* ---- slope legend ---- */
69
69
  .oep-legend { display: flex; flex-wrap: wrap; gap: 4px 10px; padding: 0 2px 4px; font-size: 10px; opacity: .9; }
70
70
  .oep-leg-it { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
71
71
  .oep-sw { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
72
72
 
73
73
  .oep-body { overflow: hidden; }
74
74
 
75
- /* ---- mode réduit : on rétracte le contrôle à la largeur Titre + bouton ---- */
75
+ /* ---- collapsed mode: the control shrinks to title + button width ---- */
76
76
  .ol-elevation-profile.oep-collapsed { width: auto !important; }
77
77
  .ol-elevation-profile.oep-collapsed .oep-body,
78
78
  .ol-elevation-profile.oep-collapsed .oep-legend { display: none !important; }
@@ -80,6 +80,22 @@
80
80
  .ol-elevation-profile.oep-collapsed .oep-toolbar { display: none !important; }
81
81
  .ol-elevation-profile.oep-collapsed .oep-header { width: auto; }
82
82
 
83
+ /* ---- terrain-model loading spinner ---- */
84
+ /* Takes --oep-area, so it wears the theme colour - and the track colour under color:'auto'. */
85
+ .oep-loading { display: flex; align-items: center; justify-content: center; }
86
+ .oep-spinner {
87
+ width: 26px; height: 26px; box-sizing: border-box;
88
+ border: 3px solid var(--oep-area);
89
+ border-top-color: transparent;
90
+ border-radius: 50%;
91
+ opacity: .9;
92
+ animation: oep-spin .8s linear infinite;
93
+ }
94
+ @keyframes oep-spin { to { transform: rotate(360deg); } }
95
+ /* A still ring reads as a broken control rather than a wait, so the motion stays -
96
+ slowed to a crawl, which is what the reduced-motion request is really about. */
97
+ @media (prefers-reduced-motion: reduce) { .oep-spinner { animation-duration: 4s; } }
98
+
83
99
  /* ---- chart ---- */
84
100
  .oep-svg { display: block; }
85
101
  .oep-area { fill: var(--oep-area); opacity: .6; }
@@ -108,3 +124,5 @@
108
124
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .35); pointer-events: none;
109
125
  }
110
126
  .ol-elevation-profile.oep-floating { transform: none; }
127
+
128
+ .oep-time{opacity:.85}