ol-elevation-profile 2.0.1 → 2.1.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 +3 -0
- package/dist/css.d.ts +4 -0
- package/dist/ol-elevation-profile.css +6 -0
- package/dist/ol-elevation-profile.d.ts +947 -0
- package/dist/ol-elevation-profile.esm.js +283 -35
- package/dist/ol-elevation-profile.esm.min.js +3 -3
- package/dist/ol-elevation-profile.js +282 -34
- package/dist/ol-elevation-profile.min.js +2 -2
- package/package.json +13 -6
- package/src/css.d.ts +4 -0
- package/src/ol-elevation-profile.css +6 -0
- package/src/ol-elevation-profile.js +281 -33
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/*! ol-elevation-profile | MIT License | https://github.com/lc-4918/ol-elevation-profile */
|
|
1
|
+
/*! ol-elevation-profile 2.1.0 | MIT License | https://github.com/lc-4918/ol-elevation-profile */
|
|
2
2
|
import Control from 'ol/control/Control.js';
|
|
3
3
|
import Overlay from 'ol/Overlay.js';
|
|
4
4
|
import { unByKey } from 'ol/Observable.js';
|
|
5
|
-
import { toLonLat,
|
|
5
|
+
import { toLonLat, transform, get, fromLonLat } from 'ol/proj.js';
|
|
6
6
|
import TileState from 'ol/TileState.js';
|
|
7
7
|
import { getDistance } from 'ol/sphere.js';
|
|
8
8
|
import { boundingExtent } from 'ol/extent.js';
|
|
@@ -51,6 +51,8 @@ import * as d3 from 'd3';
|
|
|
51
51
|
en: {
|
|
52
52
|
distance: 'Distance', elevation: 'Elevation', slope: 'Slope',
|
|
53
53
|
ascent: 'D+', descent: 'D-', empty: 'Click a track',
|
|
54
|
+
noElevation: 'No elevation data',
|
|
55
|
+
untitled: 'Profile',
|
|
54
56
|
time: 'Time', duration: 'Duration',
|
|
55
57
|
durationUnits: { s: 'sec', m: 'min', h: 'h', d: 'd' },
|
|
56
58
|
zoomStart: 'Set start (A)', zoomEnd: 'Set end (B)', zoomAll: 'Show all',
|
|
@@ -62,6 +64,8 @@ import * as d3 from 'd3';
|
|
|
62
64
|
fr: {
|
|
63
65
|
distance: 'Distance', elevation: 'Altitude', slope: 'Pente',
|
|
64
66
|
ascent: 'D+', descent: 'D-', empty: 'Cliquez un tracé',
|
|
67
|
+
noElevation: 'Aucune altimétrie',
|
|
68
|
+
untitled: 'Profil',
|
|
65
69
|
time: 'Temps', duration: 'Durée',
|
|
66
70
|
durationUnits: { s: 'sec', m: 'min', h: 'h', d: 'j' },
|
|
67
71
|
zoomStart: 'Définir le début (A)', zoomEnd: 'Définir la fin (B)', zoomAll: 'Tout voir',
|
|
@@ -73,6 +77,8 @@ import * as d3 from 'd3';
|
|
|
73
77
|
es: {
|
|
74
78
|
distance: 'Distancia', elevation: 'Altitud', slope: 'Pendiente',
|
|
75
79
|
ascent: 'D+', descent: 'D-', empty: 'Haga clic en una traza',
|
|
80
|
+
noElevation: 'Sin datos de altitud',
|
|
81
|
+
untitled: 'Perfil',
|
|
76
82
|
time: 'Tiempo', duration: 'Duración',
|
|
77
83
|
durationUnits: { s: 'seg', m: 'min', h: 'h', d: 'd' },
|
|
78
84
|
zoomStart: 'Definir el inicio (A)', zoomEnd: 'Definir el final (B)', zoomAll: 'Ver todo',
|
|
@@ -109,8 +115,11 @@ import * as d3 from 'd3';
|
|
|
109
115
|
xTicks: null,
|
|
110
116
|
yTicks: null,
|
|
111
117
|
verticalScale: 'auto', // 'auto' : le profil remplit la hauteur ;
|
|
112
|
-
// un nombre : mètres par centimètre physique
|
|
118
|
+
// un nombre : mètres par centimètre physique ;
|
|
119
|
+
// { exaggeration } : rapport fixe vertical/horizontal
|
|
113
120
|
show: 'click',
|
|
121
|
+
showWithoutElevation: true, // trace sans Z, et aucun MNT n'a pu en fournir :
|
|
122
|
+
// le panneau paraît quand même, avec un message
|
|
114
123
|
collapsable: true,
|
|
115
124
|
collapsed: false,
|
|
116
125
|
followMap: true,
|
|
@@ -125,8 +134,8 @@ import * as d3 from 'd3';
|
|
|
125
134
|
stopSpeed: 0.5, // stop threshold in m/s (~1.8 km/h)
|
|
126
135
|
tooltipItems: ['distance', 'elevation'],
|
|
127
136
|
headerItems: ['distance', 'ascent', 'descent', 'minmax'],
|
|
128
|
-
titleProperty: 'name',
|
|
129
|
-
titleLink: null,
|
|
137
|
+
titleProperty: 'name', // ou une liste, essayée dans l'ordre
|
|
138
|
+
titleLink: null, // ou un nom, ou une liste : la première vraie URL gagne
|
|
130
139
|
lang: 'en', // 'en' | 'fr' | 'es' : jeu de libellés livré
|
|
131
140
|
labels: {} // surcharges clé à clé, appliquées par-dessus la langue
|
|
132
141
|
};
|
|
@@ -743,6 +752,15 @@ import * as d3 from 'd3';
|
|
|
743
752
|
if (typeof opt === 'function') return Object.assign({}, DEM_DEFAULTS, { sample: opt });
|
|
744
753
|
const raw = (opt === true || typeof opt === 'string') ? { source: opt === true ? 'terrarium' : opt } : Object.assign({}, opt);
|
|
745
754
|
if (typeof raw.sample === 'function') return Object.assign({}, DEM_DEFAULTS, raw);
|
|
755
|
+
// A precomputed profile, held by the application and keyed by the track. It is the one
|
|
756
|
+
// source that answers with the WHOLE track rather than one value per point: no grid to
|
|
757
|
+
// choose, no pixels to decode, and nothing to sample.
|
|
758
|
+
if (raw.track) {
|
|
759
|
+
const trCfg = Object.assign({}, DEM_DEFAULTS, raw);
|
|
760
|
+
trCfg.track = Object.assign({ coords: 'coords', parse: null, fetchOptions: null },
|
|
761
|
+
typeof raw.track === 'string' ? { url: raw.track } : raw.track);
|
|
762
|
+
return trCfg.track.url ? trCfg : null;
|
|
763
|
+
}
|
|
746
764
|
// A point-query source: no tile grid, no pixel decoding.
|
|
747
765
|
if (raw.featureInfo) {
|
|
748
766
|
const fiCfg = Object.assign({}, DEM_DEFAULTS, raw);
|
|
@@ -764,6 +782,25 @@ import * as d3 from 'd3';
|
|
|
764
782
|
return (cfg.url || cfg.wms || cfg.olSource) ? cfg : null;
|
|
765
783
|
}
|
|
766
784
|
|
|
785
|
+
/**
|
|
786
|
+
* L'URL du profil d'une trace : un gabarit `{propriete}` lu sur l'entité, ou une fonction.
|
|
787
|
+
*
|
|
788
|
+
* Une propriété absente rend `null` plutôt qu'une URL trouée : demander `/profils/.json`
|
|
789
|
+
* ferait répondre le serveur - souvent par un 404, parfois par autre chose - là où la
|
|
790
|
+
* question n'a simplement pas de sens pour cette trace.
|
|
791
|
+
*/
|
|
792
|
+
function trackUrl(tpl, feature) {
|
|
793
|
+
if (typeof tpl === 'function') return tpl(feature) || null;
|
|
794
|
+
if (typeof tpl !== 'string' || !feature) return null;
|
|
795
|
+
let complet = true;
|
|
796
|
+
const url = tpl.replace(/\{(\w+)\}/g, (_, cle) => {
|
|
797
|
+
const v = feature.get(cle);
|
|
798
|
+
if (v == null || v === '') { complet = false; return ''; }
|
|
799
|
+
return encodeURIComponent(String(v));
|
|
800
|
+
});
|
|
801
|
+
return complet ? url : null;
|
|
802
|
+
}
|
|
803
|
+
|
|
767
804
|
/**
|
|
768
805
|
* Zoom at which to download the model: the finest one that fits within `maxTiles`.
|
|
769
806
|
*
|
|
@@ -801,14 +838,23 @@ import * as d3 from 'd3';
|
|
|
801
838
|
* @property {number} [smoothing=0] Elevation smoothing window, in METERS (0 = none).
|
|
802
839
|
* @property {?number} [xTicks=null] X axis ticks (null = auto from width).
|
|
803
840
|
* @property {?number} [yTicks=null] Y axis ticks (null = auto from height).
|
|
804
|
-
* @property {('auto'|number)} [verticalScale='auto'] `'auto'`: the
|
|
805
|
-
* height, which is legible but changes scale from one track to the
|
|
806
|
-
* looks like a wall and two profiles cannot be compared. A number
|
|
807
|
-
* covered per physical centimetre
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
841
|
+
* @property {('auto'|number|{exaggeration:number})} [verticalScale='auto'] `'auto'`: the
|
|
842
|
+
* profile fills the height, which is legible but changes scale from one track to the
|
|
843
|
+
* next, so a 2 % ramp looks like a wall and two profiles cannot be compared. A number
|
|
844
|
+
* fixes the metres covered per physical centimetre, which makes RANGES comparable.
|
|
845
|
+
* `{exaggeration}` fixes the ratio between the two axes instead, which makes SLOPES
|
|
846
|
+
* comparable: the gradient read off the chart is the real one, multiplied by the same
|
|
847
|
+
* factor on every track, and the control recomputes it per track and per A/B crop.
|
|
848
|
+
* Either form is a **floor**, not a cage: a track whose range exceeds what the height
|
|
849
|
+
* can show would spill out of the frame, which is worse than losing comparability, so
|
|
850
|
+
* the scale then widens silently, nothing being drawn on the chart to say so.
|
|
811
851
|
* @property {'click'|'mouseover'} [show='click'] How a track is selected on the map.
|
|
852
|
+
* @property {boolean} [showWithoutElevation=true] What to do with a track that ends up
|
|
853
|
+
* with no elevation at all: none in its geometry, and none the terrain model could
|
|
854
|
+
* supply either (`dem: null`, or a fill that failed). `true` shows the panel with the
|
|
855
|
+
* track's title and the `noElevation` message where the chart would be; `false` hides
|
|
856
|
+
* the panel outright. Either way no chart is drawn: a flat line at zero under a D+ of
|
|
857
|
+
* 0 m is not a missing figure, it is a wrong one.
|
|
812
858
|
* @property {boolean} [hideOnMapClick=true] Click on empty map hides the profile.
|
|
813
859
|
* @property {boolean} [collapsable=true] Show the collapse/expand button.
|
|
814
860
|
* @property {boolean} [collapsed=false] Initial collapsed state.
|
|
@@ -829,8 +875,14 @@ import * as d3 from 'd3';
|
|
|
829
875
|
* code falls back to English rather than leaving keys empty.
|
|
830
876
|
* @property {Object} [labels={}] Per-key overrides applied on top of `lang`. They survive
|
|
831
877
|
* a later language change, so a corrected key stays corrected.
|
|
832
|
-
* @property {string} [titleProperty='name'] Feature property used as the title
|
|
833
|
-
*
|
|
878
|
+
* @property {(string|string[])} [titleProperty='name'] Feature property used as the title,
|
|
879
|
+
* or a list of them tried in order - the first one holding a value wins. A feature with
|
|
880
|
+
* none falls back on the `untitled` label.
|
|
881
|
+
* @property {?(string|string[])} [titleLink=null] Feature property holding a URL, which
|
|
882
|
+
* makes the title a link, or a list tried in order - the first one holding an actual
|
|
883
|
+
* URL wins, so a property carrying something else is stepped over rather than rendered
|
|
884
|
+
* as a broken link. `null`, the default, never links the title: a dataset is not asked
|
|
885
|
+
* to explain that its `url` column is not the one meant for the reader.
|
|
834
886
|
* @property {number} [maxPoints=2000] Decimation for render/interaction (stats use full data).
|
|
835
887
|
* @property {?import('ol/proj/Projection').default|string} [dataProjection=null] Projection of the feature coordinates.
|
|
836
888
|
* @property {?(boolean|string|Function|Object)} [dem='terrarium'] Fill missing elevations
|
|
@@ -840,6 +892,11 @@ import * as d3 from 'd3';
|
|
|
840
892
|
* `{olSource}` = any `ol/source/TileImage` (XYZ, TileWMS, and so on);
|
|
841
893
|
* `{featureInfo:{url,layers,property}}` = WMS GetFeatureInfo, one request per point,
|
|
842
894
|
* for a greyscale coverage that cannot be decoded from its pixels (slow);
|
|
895
|
+
* `{track:{url,coords,parse,fetchOptions}}` = a profile the application computed
|
|
896
|
+
* beforehand and serves per track, `url` being a `{property}` template read off the
|
|
897
|
+
* feature (or a function). Unlike every other source it answers with the whole track:
|
|
898
|
+
* `[[lon,lat,z],…]` replaces the geometry, so a decimated profile is accepted, while a
|
|
899
|
+
* plain `[z,…]` still lines up with the geometry's own points;
|
|
843
900
|
* a function or `{sample}` `(lonlats, ctx) => number[]|Promise<number[]>` to source them
|
|
844
901
|
* yourself; `null` disables the whole thing.
|
|
845
902
|
* Decoding: `encoding` is `'terrarium'`, `'mapbox'`, or a function `(r,g,b,a) => metres`.
|
|
@@ -878,6 +935,9 @@ import * as d3 from 'd3';
|
|
|
878
935
|
this._crops = []; this._off = 0;
|
|
879
936
|
this._zoomA = null; this._zoomB = null; this._armed = null;
|
|
880
937
|
this._demZ = null; this._demFor = null; this._demSeq = 0; this._demLoading = false;
|
|
938
|
+
// Un profil précalculé apporte sa propre géométrie : elle remplace celle de l'entité
|
|
939
|
+
// le temps du calcul, sans jamais la modifier - l'entité appartient à la carte.
|
|
940
|
+
this._trackLines = null; this._linesFor = null;
|
|
881
941
|
this._onResize = () => { if (this._feature && !this._collapsed) this._render(); };
|
|
882
942
|
this._buildDom(element);
|
|
883
943
|
element.style.display = 'none';
|
|
@@ -1133,7 +1193,7 @@ import * as d3 from 'd3';
|
|
|
1133
1193
|
setFeature(feature) {
|
|
1134
1194
|
this._feature = feature || null;
|
|
1135
1195
|
this._crops = []; this._off = 0; this._zoomA = null; this._zoomB = null; this._armed = null;
|
|
1136
|
-
if (!feature) { this._fullSamples = this._samples = null; this._demZ = this._demFor = null; this._clear(); return this; }
|
|
1196
|
+
if (!feature) { this._fullSamples = this._samples = null; this._demZ = this._demFor = null; this._trackLines = this._linesFor = null; this._clear(); return this; }
|
|
1137
1197
|
this.element.style.display = '';
|
|
1138
1198
|
this._compute();
|
|
1139
1199
|
// Before the render, not after: it is _fillFromDem that knows whether a fill is
|
|
@@ -1175,7 +1235,7 @@ import * as d3 from 'd3';
|
|
|
1175
1235
|
}
|
|
1176
1236
|
if (patch && ('zoom' in patch || 'exportPng' in patch)) this._updateZoomButtons();
|
|
1177
1237
|
if (patch && typeof patch.width !== 'undefined' && typeof patch.width === 'number') this.options.width = patch.width;
|
|
1178
|
-
if (patch && 'dem' in patch) { this._demZ = null; this._demFor = null; }
|
|
1238
|
+
if (patch && 'dem' in patch) { this._demZ = null; this._demFor = null; this._trackLines = null; this._linesFor = null; }
|
|
1179
1239
|
if (this._feature) { this._compute(); this._fillFromDem(this._feature); this._updateZoomButtons(); if (this._collapsed) this._renderTitle(); else this._render(); }
|
|
1180
1240
|
return this;
|
|
1181
1241
|
}
|
|
@@ -1199,19 +1259,22 @@ import * as d3 from 'd3';
|
|
|
1199
1259
|
*/
|
|
1200
1260
|
_fillFromDem(feature) {
|
|
1201
1261
|
const cfg = demConfig(this.options.dem);
|
|
1202
|
-
if (!cfg || !feature || this._demFor === feature) return;
|
|
1262
|
+
if (!cfg || !feature || this._demFor === feature || this._linesFor === feature) return;
|
|
1203
1263
|
if (ElevationProfile.featureHasZ(feature)) return; // the track already carries its Z
|
|
1204
1264
|
// Canvas decoding needs a browser; a `sample` function does not, and must stay
|
|
1205
1265
|
// usable where there is no DOM.
|
|
1206
1266
|
if (!cfg.sample && (typeof Image === 'undefined' || typeof document === 'undefined')) return;
|
|
1207
1267
|
|
|
1208
1268
|
const geom = feature.getGeometry && feature.getGeometry();
|
|
1209
|
-
|
|
1210
|
-
const lines = geomLines(geom);
|
|
1269
|
+
const lines = geom ? geomLines(geom) : [];
|
|
1211
1270
|
const dataProj = this.options.dataProjection || (this.getMap() && this.getMap().getView().getProjection()) || 'EPSG:3857';
|
|
1212
1271
|
const lonlats = [];
|
|
1213
1272
|
for (const seg of lines) for (const c of seg) lonlats.push(toLonLat(c, dataProj));
|
|
1214
|
-
|
|
1273
|
+
// Un profil précalculé rapporte sa propre géométrie : il lui suffit de savoir DE
|
|
1274
|
+
// QUELLE trace il s'agit. C'est ce qui le rend utilisable sur une entité de tuile
|
|
1275
|
+
// vectorielle, dont les coordonnées sont dans le repère de la tuile et qu'aucune
|
|
1276
|
+
// source échantillonnée ne saurait interroger.
|
|
1277
|
+
if (!lonlats.length && !cfg.track) return;
|
|
1215
1278
|
|
|
1216
1279
|
// Sequence number: a track clicked while another is loading must win, otherwise the
|
|
1217
1280
|
// slowest response would overwrite the profile on screen.
|
|
@@ -1230,6 +1293,7 @@ import * as d3 from 'd3';
|
|
|
1230
1293
|
|
|
1231
1294
|
/** @private */
|
|
1232
1295
|
_startFill(cfg, seq, feature, lonlats, dataProj) {
|
|
1296
|
+
if (cfg.track) { this._startTrackFill(cfg, seq, feature, lonlats.length, dataProj); return; }
|
|
1233
1297
|
if (cfg.sample) {
|
|
1234
1298
|
// Wrapped in a promise chain so a function that throws synchronously fails the
|
|
1235
1299
|
// same way as one that rejects - a source that misbehaves must not leave the
|
|
@@ -1249,6 +1313,75 @@ import * as d3 from 'd3';
|
|
|
1249
1313
|
}).catch(() => this._demDone(seq, feature, null, lonlats.length, z, 0));
|
|
1250
1314
|
}
|
|
1251
1315
|
|
|
1316
|
+
/**
|
|
1317
|
+
* Va chercher un profil déjà calculé, que l'application tient prêt pour cette trace.
|
|
1318
|
+
*
|
|
1319
|
+
* Les dix autres sources échantillonnent un modèle de terrain aux points de la trace.
|
|
1320
|
+
* Celle-ci ne fait rien de tel : elle demande à l'application le profil qu'elle a déjà,
|
|
1321
|
+
* calculé une fois à l'ingestion sur le modèle qu'elle voulait, décimé comme elle
|
|
1322
|
+
* l'entendait. C'est le cas d'une application qui a fait son altimétrie en amont et ne
|
|
1323
|
+
* veut ni la refaire dans le navigateur ni dépendre d'un service au clic.
|
|
1324
|
+
*
|
|
1325
|
+
* La réponse peut prendre deux formes, et elles ne veulent pas dire la même chose :
|
|
1326
|
+
* un tableau de nombres est une altitude par point de la géométrie, qui rejoint le
|
|
1327
|
+
* chemin ordinaire ; un tableau de triplets `[lon, lat, z]` porte SA propre géométrie
|
|
1328
|
+
* et remplace celle de la trace, ce qui est le seul moyen d'accepter un profil décimé.
|
|
1329
|
+
*
|
|
1330
|
+
* @private
|
|
1331
|
+
*/
|
|
1332
|
+
_startTrackFill(cfg, seq, feature, expected, dataProj) {
|
|
1333
|
+
const url = trackUrl(cfg.track.url, feature);
|
|
1334
|
+
if (!url) { this._demDone(seq, feature, null, expected, null, 0); return; }
|
|
1335
|
+
Promise.resolve()
|
|
1336
|
+
.then(() => fetch(url, cfg.track.fetchOptions || undefined))
|
|
1337
|
+
.then((r) => { if (!r.ok) throw new Error(String(r.status)); return r.json(); })
|
|
1338
|
+
.then((json) => {
|
|
1339
|
+
const data = cfg.track.parse ? cfg.track.parse(json, feature)
|
|
1340
|
+
: (Array.isArray(json) ? json : (json && json[cfg.track.coords]));
|
|
1341
|
+
this._trackDone(seq, feature, data, expected, dataProj);
|
|
1342
|
+
})
|
|
1343
|
+
// Une trace sans profil répond 404, et c'est un fait, pas une panne : elle retombe
|
|
1344
|
+
// sur le même sort qu'un remplissage manqué.
|
|
1345
|
+
.catch(() => this._demDone(seq, feature, null, expected, null, 0));
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Adopte un profil précalculé, sous l'une ou l'autre de ses deux formes.
|
|
1350
|
+
*
|
|
1351
|
+
* Un triplet incomplet fait tout refuser, par la même règle que `_demDone` : un profil
|
|
1352
|
+
* auquel il manque des points n'est pas un profil incomplet, c'est un profil faux.
|
|
1353
|
+
*
|
|
1354
|
+
* @fires demload
|
|
1355
|
+
* @private
|
|
1356
|
+
*/
|
|
1357
|
+
_trackDone(seq, feature, data, expected, dataProj) {
|
|
1358
|
+
const rate = () => this._demDone(seq, feature, null, expected, null, 0);
|
|
1359
|
+
if (!Array.isArray(data) || !data.length) return rate();
|
|
1360
|
+
// Des nombres : une altitude par point, exactement ce qu'un échantillonneur rend.
|
|
1361
|
+
if (typeof data[0] === 'number') return void this._demDone(seq, feature, data, expected, null, 0);
|
|
1362
|
+
|
|
1363
|
+
if (seq !== this._demSeq || this._feature !== feature) return; // un clic plus récent a gagné
|
|
1364
|
+
// `isFinite(null)` vaut true - Number(null) est 0 : sans le test de nullité, une
|
|
1365
|
+
// altitude absente passerait pour une altitude au niveau de la mer. Même règle que
|
|
1366
|
+
// `_demDone`, pour la même raison.
|
|
1367
|
+
const fini = (v) => v != null && isFinite(v);
|
|
1368
|
+
const lignes = [];
|
|
1369
|
+
for (const c of data) {
|
|
1370
|
+
if (!c || c.length < 3 || !fini(c[0]) || !fini(c[1]) || !fini(c[2])) return rate();
|
|
1371
|
+
const xy = transform([c[0], c[1]], 'EPSG:4326', dataProj);
|
|
1372
|
+
lignes.push([xy[0], xy[1], c[2]]);
|
|
1373
|
+
}
|
|
1374
|
+
if (lignes.length < 2) return rate();
|
|
1375
|
+
|
|
1376
|
+
this._demLoading = false;
|
|
1377
|
+
this._trackLines = [lignes]; this._linesFor = feature;
|
|
1378
|
+
this._demZ = null; this._demFor = null;
|
|
1379
|
+
this._compute();
|
|
1380
|
+
this._updateZoomButtons();
|
|
1381
|
+
if (!this._collapsed) this._render();
|
|
1382
|
+
this.dispatchEvent({ type: 'demload', ok: true, zoom: null, tiles: 0 });
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1252
1385
|
/**
|
|
1253
1386
|
* Outcome of a fill, whatever produced it: adopt the elevations, drop the spinner,
|
|
1254
1387
|
* redraw, announce.
|
|
@@ -1282,14 +1415,18 @@ import * as d3 from 'd3';
|
|
|
1282
1415
|
// ---------- computation -------------------------------------------
|
|
1283
1416
|
_compute() {
|
|
1284
1417
|
const o = this.options;
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1418
|
+
// Un profil précalculé apporte sa géométrie ; sinon, celle de l'entité.
|
|
1419
|
+
const substitue = this._linesFor === this._feature && this._trackLines;
|
|
1420
|
+
const geom = this._feature.getGeometry && this._feature.getGeometry();
|
|
1421
|
+
const lines = substitue ? this._trackLines : (geom ? geomLines(geom) : []);
|
|
1287
1422
|
const dataProj = o.dataProjection || (this.getMap() && this.getMap().getView().getProjection()) || 'EPSG:3857';
|
|
1288
1423
|
|
|
1289
1424
|
// Terrain-model elevations, if they were loaded for THIS feature.
|
|
1290
1425
|
const demZ = (this._demFor === this._feature) ? this._demZ : null;
|
|
1291
1426
|
|
|
1292
|
-
|
|
1427
|
+
// Les horodatages sont indexés sur les points de l'ENTITÉ : sur une géométrie
|
|
1428
|
+
// substituée ils tomberaient à côté, et un temps faux vaut moins que pas de temps.
|
|
1429
|
+
const times = substitue ? null : extractTimes(this._feature, lines);
|
|
1293
1430
|
this._hasTime = !!times;
|
|
1294
1431
|
const ignoreStops = o.ignoreStops !== false; // default: stops are excluded
|
|
1295
1432
|
const stopSpeed = (o.stopSpeed != null ? o.stopSpeed : 0.5); // m/s
|
|
@@ -1361,19 +1498,57 @@ import * as d3 from 'd3';
|
|
|
1361
1498
|
* `nice()` n'est pas appliqué en échelle absolue : il arrondit le domaine vers
|
|
1362
1499
|
* l'extérieur, donc il fausserait le rapport qu'on vient de fixer.
|
|
1363
1500
|
*/
|
|
1364
|
-
_yScale(s, zpad, innerH) {
|
|
1365
|
-
const
|
|
1366
|
-
const
|
|
1501
|
+
_yScale(s, zpad, innerH, innerW) {
|
|
1502
|
+
const bas = s.min - zpad, haut = s.max + zpad;
|
|
1503
|
+
const cm = innerH / this._pxPerCm(); // hauteur du graphe, en centimètres
|
|
1504
|
+
const cmH = innerW / this._pxPerCm(); // largeur du graphe, en centimètres
|
|
1505
|
+
// Le rapport entre les deux axes, quel que soit le mode : c'est la grandeur qu'on
|
|
1506
|
+
// tient fixe sous `{ exaggeration }`, et celle qui dérive librement en `'auto'`.
|
|
1507
|
+
// La publier partout est le seul moyen de voir la différence plutôt que d'y croire.
|
|
1508
|
+
const rapport = (etendue) => (cmH > 0 && etendue > 0) ? (s.distance / cmH) / (etendue / cm) : null;
|
|
1509
|
+
|
|
1510
|
+
const vs = this._verticalScaleFor(s, innerW);
|
|
1367
1511
|
if (!(vs > 0)) {
|
|
1368
|
-
|
|
1369
|
-
|
|
1512
|
+
// `'auto'` : la hauteur est remplie, donc c'est l'amplitude qui décide de tout.
|
|
1513
|
+
// `nice()` arrondit le domaine vers l'extérieur, d'où la lecture APRÈS coup.
|
|
1514
|
+
const echelle = d3.scaleLinear().domain([bas, haut]).range([innerH, 0]).nice();
|
|
1515
|
+
const dom = echelle.domain();
|
|
1516
|
+
this._vScale = null; // aucune échelle absolue demandée
|
|
1517
|
+
this._vExaggeration = rapport(dom[1] - dom[0]);
|
|
1518
|
+
return echelle;
|
|
1370
1519
|
}
|
|
1371
|
-
const cm = innerH / this._pxPerCm(); // hauteur du graphe, en centimètres
|
|
1372
1520
|
const etendue = Math.max(vs * cm, haut - bas); // jamais moins qu'il n'en faut
|
|
1373
1521
|
const milieu = (bas + haut) / 2;
|
|
1374
1522
|
this._vScale = etendue / cm; // m/cm effectivement appliqués
|
|
1523
|
+
// Sous le plancher, le rapport retombe sous celui demandé : c'est la seule façon de
|
|
1524
|
+
// savoir que c'est arrivé.
|
|
1525
|
+
this._vExaggeration = rapport(etendue);
|
|
1375
1526
|
return d3.scaleLinear().domain([milieu - etendue / 2, milieu + etendue / 2]).range([innerH, 0]);
|
|
1376
1527
|
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Les mètres par centimètre demandés, sous l'une ou l'autre forme de `verticalScale`.
|
|
1531
|
+
*
|
|
1532
|
+
* Une échelle absolue rend les AMPLITUDES comparables, pas les pentes : l'axe
|
|
1533
|
+
* horizontal s'étire toujours sur toute la trace, si bien qu'une même pente de 5 %
|
|
1534
|
+
* paraît trois fois plus raide sur une boucle de 3 km que sur une traversée de 30. Une
|
|
1535
|
+
* exagération fixe le RAPPORT entre les deux axes : la pente lue sur le dessin est
|
|
1536
|
+
* alors la pente réelle, multipliée par un facteur constant d'une trace à l'autre.
|
|
1537
|
+
*
|
|
1538
|
+
* Elle se calcule ici et pas chez l'appelant parce qu'elle demande deux choses que lui
|
|
1539
|
+
* n'a pas : la largeur du graphe une fois les marges retirées, et la distance
|
|
1540
|
+
* réellement affichée - celle du recadrage A/B en cours, non celle de la trace.
|
|
1541
|
+
*
|
|
1542
|
+
* @private
|
|
1543
|
+
*/
|
|
1544
|
+
_verticalScaleFor(s, innerW) {
|
|
1545
|
+
const v = this.options.verticalScale;
|
|
1546
|
+
if (typeof v === 'number') return v;
|
|
1547
|
+
const ex = (v && typeof v === 'object') ? Number(v.exaggeration) : 0;
|
|
1548
|
+
if (!(ex > 0) || !(s.distance > 0) || !(innerW > 0)) return 0;
|
|
1549
|
+
const cm = innerW / this._pxPerCm(); // largeur du graphe, en centimètres
|
|
1550
|
+
return (s.distance / cm) / ex; // horizontale ÷ exagération
|
|
1551
|
+
}
|
|
1377
1552
|
_statsOf(samples, fromTotal) {
|
|
1378
1553
|
let ascent = 0, descent = 0, zmin = Infinity, zmax = -Infinity, maxAbs = 0;
|
|
1379
1554
|
for (let k = 0; k < samples.length; k++) {
|
|
@@ -1467,17 +1642,80 @@ import * as d3 from 'd3';
|
|
|
1467
1642
|
* is skipped while collapsed, so without a separate entry point the title would keep
|
|
1468
1643
|
* naming the previous track after a change of feature.
|
|
1469
1644
|
*/
|
|
1645
|
+
/**
|
|
1646
|
+
* Cette trace a-t-elle une altitude, d'où qu'elle vienne ?
|
|
1647
|
+
*
|
|
1648
|
+
* Trois sources possibles, et une seule suffit : le Z de la géométrie, les altitudes
|
|
1649
|
+
* rapportées par un modèle de terrain, ou le profil précalculé d'une source `track`.
|
|
1650
|
+
* Aucune des trois, et il n'y a rien à dessiner - ce qui ne veut pas dire zéro.
|
|
1651
|
+
*
|
|
1652
|
+
* @private
|
|
1653
|
+
*/
|
|
1654
|
+
_hasElevation() {
|
|
1655
|
+
const f = this._feature;
|
|
1656
|
+
if (!f) return false;
|
|
1657
|
+
if (ElevationProfile.featureHasZ(f)) return true;
|
|
1658
|
+
if (this._demFor === f && this._demZ) return true;
|
|
1659
|
+
if (this._linesFor === f && this._trackLines) return true;
|
|
1660
|
+
return false;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* Ce qui prend la place du graphe quand la trace n'a aucune altitude.
|
|
1665
|
+
*
|
|
1666
|
+
* Même forme que le spinner, et pour la même raison : la hauteur du graphe est tenue,
|
|
1667
|
+
* si bien que le panneau ne saute pas si un profil finit par arriver. Le titre reste,
|
|
1668
|
+
* lui : c'est la trace cliquée, et l'utilisateur a besoin de savoir laquelle.
|
|
1669
|
+
*
|
|
1670
|
+
* @private
|
|
1671
|
+
*/
|
|
1672
|
+
_renderNoElevation() {
|
|
1673
|
+
if (!this.options.showWithoutElevation) { this.element.style.display = 'none'; return; }
|
|
1674
|
+
this.element.style.display = '';
|
|
1675
|
+
this._applyTheme();
|
|
1676
|
+
this._applyPlacement();
|
|
1677
|
+
this._renderHeader(true);
|
|
1678
|
+
const H = typeof this.options.height === 'number' ? this.options.height : 180;
|
|
1679
|
+
this._body.innerHTML = '';
|
|
1680
|
+
const box = document.createElement('div');
|
|
1681
|
+
box.className = 'oep-noelev';
|
|
1682
|
+
box.style.height = `${H}px`;
|
|
1683
|
+
box.setAttribute('role', 'status');
|
|
1684
|
+
box.textContent = this.options.labels.noElevation;
|
|
1685
|
+
this._body.appendChild(box);
|
|
1686
|
+
this._clearFocus();
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
/**
|
|
1690
|
+
* La première propriété qui répond, parmi celles proposées.
|
|
1691
|
+
*
|
|
1692
|
+
* Un nom de propriété unique reste un nom de propriété unique ; une liste est essayée
|
|
1693
|
+
* dans l'ordre. `garde` filtre ce qu'on accepte : n'importe quelle valeur non vide pour
|
|
1694
|
+
* un titre, une URL véritable pour un lien - sans quoi une propriété `url` contenant
|
|
1695
|
+
* autre chose ferait un lien mort plutôt que pas de lien du tout.
|
|
1696
|
+
*
|
|
1697
|
+
* @private
|
|
1698
|
+
*/
|
|
1699
|
+
_pickProperty(f, spec, garde) {
|
|
1700
|
+
if (!spec || !f || !f.get) return null;
|
|
1701
|
+
for (const cle of (Array.isArray(spec) ? spec : [spec])) {
|
|
1702
|
+
const v = f.get(cle);
|
|
1703
|
+
if (v != null && v !== '' && (!garde || garde(v))) return v;
|
|
1704
|
+
}
|
|
1705
|
+
return null;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1470
1708
|
_renderTitle() {
|
|
1471
1709
|
const o = this.options, f = this._feature;
|
|
1472
1710
|
if (!f) return;
|
|
1473
|
-
const name = (f
|
|
1474
|
-
const linkUrl =
|
|
1711
|
+
const name = this._pickProperty(f, o.titleProperty) || o.labels.untitled;
|
|
1712
|
+
const linkUrl = this._pickProperty(f, o.titleLink, isUrl);
|
|
1475
1713
|
if (isUrl(linkUrl)) this._titleEl.innerHTML = `<a href="${esc(linkUrl)}" target="_blank" rel="noopener">${esc(name)}</a>`;
|
|
1476
1714
|
else this._titleEl.textContent = name;
|
|
1477
1715
|
this._titleEl.setAttribute('title', name);
|
|
1478
1716
|
}
|
|
1479
1717
|
|
|
1480
|
-
_renderHeader() {
|
|
1718
|
+
_renderHeader(sansAltimetrie) {
|
|
1481
1719
|
const o = this.options, s = this._stats, f = this._feature;
|
|
1482
1720
|
this._renderTitle();
|
|
1483
1721
|
// While the elevations are still unknown, every figure would read zero: a D+ of 0 m
|
|
@@ -1491,7 +1729,10 @@ import * as d3 from 'd3';
|
|
|
1491
1729
|
const html = [], text = [];
|
|
1492
1730
|
o.headerItems.forEach((it) => {
|
|
1493
1731
|
if (typeof it === 'string') {
|
|
1494
|
-
|
|
1732
|
+
// Sans altitude, il reste ce que la géométrie sait dire : la longueur, la durée.
|
|
1733
|
+
// Le dénivelé et les altitudes extrêmes, eux, vaudraient zéro - on les tait.
|
|
1734
|
+
if (sansAltimetrie && (it === 'ascent' || it === 'descent' || it === 'min' || it === 'max' || it === 'minmax')) return;
|
|
1735
|
+
if (it === 'distance') { if (!(s && s.distance > 0)) return; html.push(`<b>${fmtDistance(s.distance, o.units)}</b>`); text.push(fmtDistance(s.distance, o.units)); }
|
|
1495
1736
|
else if (it === 'ascent') { html.push(`<span class="oep-up">${o.labels.ascent} ${fmtElevation(s.ascent, o.units)}</span>`); text.push(`${o.labels.ascent} ${fmtElevation(s.ascent, o.units)}`); }
|
|
1496
1737
|
else if (it === 'descent') { html.push(`<span class="oep-down">${o.labels.descent} ${fmtElevation(s.descent, o.units)}</span>`); text.push(`${o.labels.descent} ${fmtElevation(s.descent, o.units)}`); }
|
|
1497
1738
|
else if (it === 'min') { html.push(fmtElevation(s.min, o.units)); text.push(fmtElevation(s.min, o.units)); }
|
|
@@ -1546,6 +1787,10 @@ import * as d3 from 'd3';
|
|
|
1546
1787
|
// ---------- rendering ---------------------------------------------
|
|
1547
1788
|
_render() {
|
|
1548
1789
|
const o = this.options, s = this._stats, data = this._samples;
|
|
1790
|
+
// Aucune altitude par aucune voie, et plus rien en chemin : le graphe n'a pas lieu
|
|
1791
|
+
// d'être. Le tracer quand même poserait une ligne plate au niveau zéro sous un D+ de
|
|
1792
|
+
// 0 m, ce qui n'est pas un chiffre manquant mais un chiffre faux.
|
|
1793
|
+
if (!this._demLoading && !this._hasElevation()) { this._renderNoElevation(); return; }
|
|
1549
1794
|
if (!data || !data.length) return;
|
|
1550
1795
|
this._applyTheme();
|
|
1551
1796
|
const mobile = this._applyPlacement();
|
|
@@ -1576,7 +1821,7 @@ import * as d3 from 'd3';
|
|
|
1576
1821
|
|
|
1577
1822
|
const x = d3.scaleLinear().domain([0, s.distance]).range([0, innerW]);
|
|
1578
1823
|
const zpad = (s.max - s.min) * 0.1 || 10;
|
|
1579
|
-
const y = this._yScale(s, zpad, innerH);
|
|
1824
|
+
const y = this._yScale(s, zpad, innerH, innerW);
|
|
1580
1825
|
this._x = x; this._y = y; this._dims = { innerW, innerH };
|
|
1581
1826
|
|
|
1582
1827
|
if (o.grid) g.append('g').attr('class', 'oep-grid').call(d3.axisLeft(y).ticks(yTicks).tickSize(-innerW).tickFormat(''));
|
|
@@ -2011,6 +2256,9 @@ import * as d3 from 'd3';
|
|
|
2011
2256
|
ElevationProfile.DEM_PRESETS = DEM_PRESETS;
|
|
2012
2257
|
ElevationProfile.DemSampler = DemSampler;
|
|
2013
2258
|
ElevationProfile.POSITIONS = POSITIONS;
|
|
2014
|
-
|
|
2259
|
+
// Stamped at build time from package.json - see rollup.config.mjs, which fails the build
|
|
2260
|
+
// if this placeholder ever stops matching. Read from the source rather than the bundle,
|
|
2261
|
+
// it says just that: a development copy, of no released version.
|
|
2262
|
+
ElevationProfile.version = '2.1.0';
|
|
2015
2263
|
|
|
2016
2264
|
export { ElevationProfile as default };
|