ol-elevation-profile 2.0.0 → 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 +6 -3
- 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,4 +1,4 @@
|
|
|
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
|
(function (global, factory) {
|
|
3
3
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('ol/control/Control.js'), require('ol/Overlay.js'), require('ol/Observable.js'), require('ol/proj.js'), require('ol/TileState.js'), require('ol/sphere.js'), require('ol/extent.js'), require('d3')) :
|
|
4
4
|
typeof define === 'function' && define.amd ? define(['ol/control/Control.js', 'ol/Overlay.js', 'ol/Observable.js', 'ol/proj.js', 'ol/TileState.js', 'ol/sphere.js', 'ol/extent.js', 'd3'], factory) :
|
|
@@ -67,6 +67,8 @@
|
|
|
67
67
|
en: {
|
|
68
68
|
distance: 'Distance', elevation: 'Elevation', slope: 'Slope',
|
|
69
69
|
ascent: 'D+', descent: 'D-', empty: 'Click a track',
|
|
70
|
+
noElevation: 'No elevation data',
|
|
71
|
+
untitled: 'Profile',
|
|
70
72
|
time: 'Time', duration: 'Duration',
|
|
71
73
|
durationUnits: { s: 'sec', m: 'min', h: 'h', d: 'd' },
|
|
72
74
|
zoomStart: 'Set start (A)', zoomEnd: 'Set end (B)', zoomAll: 'Show all',
|
|
@@ -78,6 +80,8 @@
|
|
|
78
80
|
fr: {
|
|
79
81
|
distance: 'Distance', elevation: 'Altitude', slope: 'Pente',
|
|
80
82
|
ascent: 'D+', descent: 'D-', empty: 'Cliquez un tracé',
|
|
83
|
+
noElevation: 'Aucune altimétrie',
|
|
84
|
+
untitled: 'Profil',
|
|
81
85
|
time: 'Temps', duration: 'Durée',
|
|
82
86
|
durationUnits: { s: 'sec', m: 'min', h: 'h', d: 'j' },
|
|
83
87
|
zoomStart: 'Définir le début (A)', zoomEnd: 'Définir la fin (B)', zoomAll: 'Tout voir',
|
|
@@ -89,6 +93,8 @@
|
|
|
89
93
|
es: {
|
|
90
94
|
distance: 'Distancia', elevation: 'Altitud', slope: 'Pendiente',
|
|
91
95
|
ascent: 'D+', descent: 'D-', empty: 'Haga clic en una traza',
|
|
96
|
+
noElevation: 'Sin datos de altitud',
|
|
97
|
+
untitled: 'Perfil',
|
|
92
98
|
time: 'Tiempo', duration: 'Duración',
|
|
93
99
|
durationUnits: { s: 'seg', m: 'min', h: 'h', d: 'd' },
|
|
94
100
|
zoomStart: 'Definir el inicio (A)', zoomEnd: 'Definir el final (B)', zoomAll: 'Ver todo',
|
|
@@ -125,8 +131,11 @@
|
|
|
125
131
|
xTicks: null,
|
|
126
132
|
yTicks: null,
|
|
127
133
|
verticalScale: 'auto', // 'auto' : le profil remplit la hauteur ;
|
|
128
|
-
// un nombre : mètres par centimètre physique
|
|
134
|
+
// un nombre : mètres par centimètre physique ;
|
|
135
|
+
// { exaggeration } : rapport fixe vertical/horizontal
|
|
129
136
|
show: 'click',
|
|
137
|
+
showWithoutElevation: true, // trace sans Z, et aucun MNT n'a pu en fournir :
|
|
138
|
+
// le panneau paraît quand même, avec un message
|
|
130
139
|
collapsable: true,
|
|
131
140
|
collapsed: false,
|
|
132
141
|
followMap: true,
|
|
@@ -141,8 +150,8 @@
|
|
|
141
150
|
stopSpeed: 0.5, // stop threshold in m/s (~1.8 km/h)
|
|
142
151
|
tooltipItems: ['distance', 'elevation'],
|
|
143
152
|
headerItems: ['distance', 'ascent', 'descent', 'minmax'],
|
|
144
|
-
titleProperty: 'name',
|
|
145
|
-
titleLink: null,
|
|
153
|
+
titleProperty: 'name', // ou une liste, essayée dans l'ordre
|
|
154
|
+
titleLink: null, // ou un nom, ou une liste : la première vraie URL gagne
|
|
146
155
|
lang: 'en', // 'en' | 'fr' | 'es' : jeu de libellés livré
|
|
147
156
|
labels: {} // surcharges clé à clé, appliquées par-dessus la langue
|
|
148
157
|
};
|
|
@@ -759,6 +768,15 @@
|
|
|
759
768
|
if (typeof opt === 'function') return Object.assign({}, DEM_DEFAULTS, { sample: opt });
|
|
760
769
|
const raw = (opt === true || typeof opt === 'string') ? { source: opt === true ? 'terrarium' : opt } : Object.assign({}, opt);
|
|
761
770
|
if (typeof raw.sample === 'function') return Object.assign({}, DEM_DEFAULTS, raw);
|
|
771
|
+
// A precomputed profile, held by the application and keyed by the track. It is the one
|
|
772
|
+
// source that answers with the WHOLE track rather than one value per point: no grid to
|
|
773
|
+
// choose, no pixels to decode, and nothing to sample.
|
|
774
|
+
if (raw.track) {
|
|
775
|
+
const trCfg = Object.assign({}, DEM_DEFAULTS, raw);
|
|
776
|
+
trCfg.track = Object.assign({ coords: 'coords', parse: null, fetchOptions: null },
|
|
777
|
+
typeof raw.track === 'string' ? { url: raw.track } : raw.track);
|
|
778
|
+
return trCfg.track.url ? trCfg : null;
|
|
779
|
+
}
|
|
762
780
|
// A point-query source: no tile grid, no pixel decoding.
|
|
763
781
|
if (raw.featureInfo) {
|
|
764
782
|
const fiCfg = Object.assign({}, DEM_DEFAULTS, raw);
|
|
@@ -780,6 +798,25 @@
|
|
|
780
798
|
return (cfg.url || cfg.wms || cfg.olSource) ? cfg : null;
|
|
781
799
|
}
|
|
782
800
|
|
|
801
|
+
/**
|
|
802
|
+
* L'URL du profil d'une trace : un gabarit `{propriete}` lu sur l'entité, ou une fonction.
|
|
803
|
+
*
|
|
804
|
+
* Une propriété absente rend `null` plutôt qu'une URL trouée : demander `/profils/.json`
|
|
805
|
+
* ferait répondre le serveur - souvent par un 404, parfois par autre chose - là où la
|
|
806
|
+
* question n'a simplement pas de sens pour cette trace.
|
|
807
|
+
*/
|
|
808
|
+
function trackUrl(tpl, feature) {
|
|
809
|
+
if (typeof tpl === 'function') return tpl(feature) || null;
|
|
810
|
+
if (typeof tpl !== 'string' || !feature) return null;
|
|
811
|
+
let complet = true;
|
|
812
|
+
const url = tpl.replace(/\{(\w+)\}/g, (_, cle) => {
|
|
813
|
+
const v = feature.get(cle);
|
|
814
|
+
if (v == null || v === '') { complet = false; return ''; }
|
|
815
|
+
return encodeURIComponent(String(v));
|
|
816
|
+
});
|
|
817
|
+
return complet ? url : null;
|
|
818
|
+
}
|
|
819
|
+
|
|
783
820
|
/**
|
|
784
821
|
* Zoom at which to download the model: the finest one that fits within `maxTiles`.
|
|
785
822
|
*
|
|
@@ -817,14 +854,23 @@
|
|
|
817
854
|
* @property {number} [smoothing=0] Elevation smoothing window, in METERS (0 = none).
|
|
818
855
|
* @property {?number} [xTicks=null] X axis ticks (null = auto from width).
|
|
819
856
|
* @property {?number} [yTicks=null] Y axis ticks (null = auto from height).
|
|
820
|
-
* @property {('auto'|number)} [verticalScale='auto'] `'auto'`: the
|
|
821
|
-
* height, which is legible but changes scale from one track to the
|
|
822
|
-
* looks like a wall and two profiles cannot be compared. A number
|
|
823
|
-
* covered per physical centimetre
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
857
|
+
* @property {('auto'|number|{exaggeration:number})} [verticalScale='auto'] `'auto'`: the
|
|
858
|
+
* profile fills the height, which is legible but changes scale from one track to the
|
|
859
|
+
* next, so a 2 % ramp looks like a wall and two profiles cannot be compared. A number
|
|
860
|
+
* fixes the metres covered per physical centimetre, which makes RANGES comparable.
|
|
861
|
+
* `{exaggeration}` fixes the ratio between the two axes instead, which makes SLOPES
|
|
862
|
+
* comparable: the gradient read off the chart is the real one, multiplied by the same
|
|
863
|
+
* factor on every track, and the control recomputes it per track and per A/B crop.
|
|
864
|
+
* Either form is a **floor**, not a cage: a track whose range exceeds what the height
|
|
865
|
+
* can show would spill out of the frame, which is worse than losing comparability, so
|
|
866
|
+
* the scale then widens silently, nothing being drawn on the chart to say so.
|
|
827
867
|
* @property {'click'|'mouseover'} [show='click'] How a track is selected on the map.
|
|
868
|
+
* @property {boolean} [showWithoutElevation=true] What to do with a track that ends up
|
|
869
|
+
* with no elevation at all: none in its geometry, and none the terrain model could
|
|
870
|
+
* supply either (`dem: null`, or a fill that failed). `true` shows the panel with the
|
|
871
|
+
* track's title and the `noElevation` message where the chart would be; `false` hides
|
|
872
|
+
* the panel outright. Either way no chart is drawn: a flat line at zero under a D+ of
|
|
873
|
+
* 0 m is not a missing figure, it is a wrong one.
|
|
828
874
|
* @property {boolean} [hideOnMapClick=true] Click on empty map hides the profile.
|
|
829
875
|
* @property {boolean} [collapsable=true] Show the collapse/expand button.
|
|
830
876
|
* @property {boolean} [collapsed=false] Initial collapsed state.
|
|
@@ -845,8 +891,14 @@
|
|
|
845
891
|
* code falls back to English rather than leaving keys empty.
|
|
846
892
|
* @property {Object} [labels={}] Per-key overrides applied on top of `lang`. They survive
|
|
847
893
|
* a later language change, so a corrected key stays corrected.
|
|
848
|
-
* @property {string} [titleProperty='name'] Feature property used as the title
|
|
849
|
-
*
|
|
894
|
+
* @property {(string|string[])} [titleProperty='name'] Feature property used as the title,
|
|
895
|
+
* or a list of them tried in order - the first one holding a value wins. A feature with
|
|
896
|
+
* none falls back on the `untitled` label.
|
|
897
|
+
* @property {?(string|string[])} [titleLink=null] Feature property holding a URL, which
|
|
898
|
+
* makes the title a link, or a list tried in order - the first one holding an actual
|
|
899
|
+
* URL wins, so a property carrying something else is stepped over rather than rendered
|
|
900
|
+
* as a broken link. `null`, the default, never links the title: a dataset is not asked
|
|
901
|
+
* to explain that its `url` column is not the one meant for the reader.
|
|
850
902
|
* @property {number} [maxPoints=2000] Decimation for render/interaction (stats use full data).
|
|
851
903
|
* @property {?import('ol/proj/Projection').default|string} [dataProjection=null] Projection of the feature coordinates.
|
|
852
904
|
* @property {?(boolean|string|Function|Object)} [dem='terrarium'] Fill missing elevations
|
|
@@ -856,6 +908,11 @@
|
|
|
856
908
|
* `{olSource}` = any `ol/source/TileImage` (XYZ, TileWMS, and so on);
|
|
857
909
|
* `{featureInfo:{url,layers,property}}` = WMS GetFeatureInfo, one request per point,
|
|
858
910
|
* for a greyscale coverage that cannot be decoded from its pixels (slow);
|
|
911
|
+
* `{track:{url,coords,parse,fetchOptions}}` = a profile the application computed
|
|
912
|
+
* beforehand and serves per track, `url` being a `{property}` template read off the
|
|
913
|
+
* feature (or a function). Unlike every other source it answers with the whole track:
|
|
914
|
+
* `[[lon,lat,z],…]` replaces the geometry, so a decimated profile is accepted, while a
|
|
915
|
+
* plain `[z,…]` still lines up with the geometry's own points;
|
|
859
916
|
* a function or `{sample}` `(lonlats, ctx) => number[]|Promise<number[]>` to source them
|
|
860
917
|
* yourself; `null` disables the whole thing.
|
|
861
918
|
* Decoding: `encoding` is `'terrarium'`, `'mapbox'`, or a function `(r,g,b,a) => metres`.
|
|
@@ -894,6 +951,9 @@
|
|
|
894
951
|
this._crops = []; this._off = 0;
|
|
895
952
|
this._zoomA = null; this._zoomB = null; this._armed = null;
|
|
896
953
|
this._demZ = null; this._demFor = null; this._demSeq = 0; this._demLoading = false;
|
|
954
|
+
// Un profil précalculé apporte sa propre géométrie : elle remplace celle de l'entité
|
|
955
|
+
// le temps du calcul, sans jamais la modifier - l'entité appartient à la carte.
|
|
956
|
+
this._trackLines = null; this._linesFor = null;
|
|
897
957
|
this._onResize = () => { if (this._feature && !this._collapsed) this._render(); };
|
|
898
958
|
this._buildDom(element);
|
|
899
959
|
element.style.display = 'none';
|
|
@@ -1149,7 +1209,7 @@
|
|
|
1149
1209
|
setFeature(feature) {
|
|
1150
1210
|
this._feature = feature || null;
|
|
1151
1211
|
this._crops = []; this._off = 0; this._zoomA = null; this._zoomB = null; this._armed = null;
|
|
1152
|
-
if (!feature) { this._fullSamples = this._samples = null; this._demZ = this._demFor = null; this._clear(); return this; }
|
|
1212
|
+
if (!feature) { this._fullSamples = this._samples = null; this._demZ = this._demFor = null; this._trackLines = this._linesFor = null; this._clear(); return this; }
|
|
1153
1213
|
this.element.style.display = '';
|
|
1154
1214
|
this._compute();
|
|
1155
1215
|
// Before the render, not after: it is _fillFromDem that knows whether a fill is
|
|
@@ -1191,7 +1251,7 @@
|
|
|
1191
1251
|
}
|
|
1192
1252
|
if (patch && ('zoom' in patch || 'exportPng' in patch)) this._updateZoomButtons();
|
|
1193
1253
|
if (patch && typeof patch.width !== 'undefined' && typeof patch.width === 'number') this.options.width = patch.width;
|
|
1194
|
-
if (patch && 'dem' in patch) { this._demZ = null; this._demFor = null; }
|
|
1254
|
+
if (patch && 'dem' in patch) { this._demZ = null; this._demFor = null; this._trackLines = null; this._linesFor = null; }
|
|
1195
1255
|
if (this._feature) { this._compute(); this._fillFromDem(this._feature); this._updateZoomButtons(); if (this._collapsed) this._renderTitle(); else this._render(); }
|
|
1196
1256
|
return this;
|
|
1197
1257
|
}
|
|
@@ -1215,19 +1275,22 @@
|
|
|
1215
1275
|
*/
|
|
1216
1276
|
_fillFromDem(feature) {
|
|
1217
1277
|
const cfg = demConfig(this.options.dem);
|
|
1218
|
-
if (!cfg || !feature || this._demFor === feature) return;
|
|
1278
|
+
if (!cfg || !feature || this._demFor === feature || this._linesFor === feature) return;
|
|
1219
1279
|
if (ElevationProfile.featureHasZ(feature)) return; // the track already carries its Z
|
|
1220
1280
|
// Canvas decoding needs a browser; a `sample` function does not, and must stay
|
|
1221
1281
|
// usable where there is no DOM.
|
|
1222
1282
|
if (!cfg.sample && (typeof Image === 'undefined' || typeof document === 'undefined')) return;
|
|
1223
1283
|
|
|
1224
1284
|
const geom = feature.getGeometry && feature.getGeometry();
|
|
1225
|
-
|
|
1226
|
-
const lines = geomLines(geom);
|
|
1285
|
+
const lines = geom ? geomLines(geom) : [];
|
|
1227
1286
|
const dataProj = this.options.dataProjection || (this.getMap() && this.getMap().getView().getProjection()) || 'EPSG:3857';
|
|
1228
1287
|
const lonlats = [];
|
|
1229
1288
|
for (const seg of lines) for (const c of seg) lonlats.push(proj_js.toLonLat(c, dataProj));
|
|
1230
|
-
|
|
1289
|
+
// Un profil précalculé rapporte sa propre géométrie : il lui suffit de savoir DE
|
|
1290
|
+
// QUELLE trace il s'agit. C'est ce qui le rend utilisable sur une entité de tuile
|
|
1291
|
+
// vectorielle, dont les coordonnées sont dans le repère de la tuile et qu'aucune
|
|
1292
|
+
// source échantillonnée ne saurait interroger.
|
|
1293
|
+
if (!lonlats.length && !cfg.track) return;
|
|
1231
1294
|
|
|
1232
1295
|
// Sequence number: a track clicked while another is loading must win, otherwise the
|
|
1233
1296
|
// slowest response would overwrite the profile on screen.
|
|
@@ -1246,6 +1309,7 @@
|
|
|
1246
1309
|
|
|
1247
1310
|
/** @private */
|
|
1248
1311
|
_startFill(cfg, seq, feature, lonlats, dataProj) {
|
|
1312
|
+
if (cfg.track) { this._startTrackFill(cfg, seq, feature, lonlats.length, dataProj); return; }
|
|
1249
1313
|
if (cfg.sample) {
|
|
1250
1314
|
// Wrapped in a promise chain so a function that throws synchronously fails the
|
|
1251
1315
|
// same way as one that rejects - a source that misbehaves must not leave the
|
|
@@ -1265,6 +1329,75 @@
|
|
|
1265
1329
|
}).catch(() => this._demDone(seq, feature, null, lonlats.length, z, 0));
|
|
1266
1330
|
}
|
|
1267
1331
|
|
|
1332
|
+
/**
|
|
1333
|
+
* Va chercher un profil déjà calculé, que l'application tient prêt pour cette trace.
|
|
1334
|
+
*
|
|
1335
|
+
* Les dix autres sources échantillonnent un modèle de terrain aux points de la trace.
|
|
1336
|
+
* Celle-ci ne fait rien de tel : elle demande à l'application le profil qu'elle a déjà,
|
|
1337
|
+
* calculé une fois à l'ingestion sur le modèle qu'elle voulait, décimé comme elle
|
|
1338
|
+
* l'entendait. C'est le cas d'une application qui a fait son altimétrie en amont et ne
|
|
1339
|
+
* veut ni la refaire dans le navigateur ni dépendre d'un service au clic.
|
|
1340
|
+
*
|
|
1341
|
+
* La réponse peut prendre deux formes, et elles ne veulent pas dire la même chose :
|
|
1342
|
+
* un tableau de nombres est une altitude par point de la géométrie, qui rejoint le
|
|
1343
|
+
* chemin ordinaire ; un tableau de triplets `[lon, lat, z]` porte SA propre géométrie
|
|
1344
|
+
* et remplace celle de la trace, ce qui est le seul moyen d'accepter un profil décimé.
|
|
1345
|
+
*
|
|
1346
|
+
* @private
|
|
1347
|
+
*/
|
|
1348
|
+
_startTrackFill(cfg, seq, feature, expected, dataProj) {
|
|
1349
|
+
const url = trackUrl(cfg.track.url, feature);
|
|
1350
|
+
if (!url) { this._demDone(seq, feature, null, expected, null, 0); return; }
|
|
1351
|
+
Promise.resolve()
|
|
1352
|
+
.then(() => fetch(url, cfg.track.fetchOptions || undefined))
|
|
1353
|
+
.then((r) => { if (!r.ok) throw new Error(String(r.status)); return r.json(); })
|
|
1354
|
+
.then((json) => {
|
|
1355
|
+
const data = cfg.track.parse ? cfg.track.parse(json, feature)
|
|
1356
|
+
: (Array.isArray(json) ? json : (json && json[cfg.track.coords]));
|
|
1357
|
+
this._trackDone(seq, feature, data, expected, dataProj);
|
|
1358
|
+
})
|
|
1359
|
+
// Une trace sans profil répond 404, et c'est un fait, pas une panne : elle retombe
|
|
1360
|
+
// sur le même sort qu'un remplissage manqué.
|
|
1361
|
+
.catch(() => this._demDone(seq, feature, null, expected, null, 0));
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Adopte un profil précalculé, sous l'une ou l'autre de ses deux formes.
|
|
1366
|
+
*
|
|
1367
|
+
* Un triplet incomplet fait tout refuser, par la même règle que `_demDone` : un profil
|
|
1368
|
+
* auquel il manque des points n'est pas un profil incomplet, c'est un profil faux.
|
|
1369
|
+
*
|
|
1370
|
+
* @fires demload
|
|
1371
|
+
* @private
|
|
1372
|
+
*/
|
|
1373
|
+
_trackDone(seq, feature, data, expected, dataProj) {
|
|
1374
|
+
const rate = () => this._demDone(seq, feature, null, expected, null, 0);
|
|
1375
|
+
if (!Array.isArray(data) || !data.length) return rate();
|
|
1376
|
+
// Des nombres : une altitude par point, exactement ce qu'un échantillonneur rend.
|
|
1377
|
+
if (typeof data[0] === 'number') return void this._demDone(seq, feature, data, expected, null, 0);
|
|
1378
|
+
|
|
1379
|
+
if (seq !== this._demSeq || this._feature !== feature) return; // un clic plus récent a gagné
|
|
1380
|
+
// `isFinite(null)` vaut true - Number(null) est 0 : sans le test de nullité, une
|
|
1381
|
+
// altitude absente passerait pour une altitude au niveau de la mer. Même règle que
|
|
1382
|
+
// `_demDone`, pour la même raison.
|
|
1383
|
+
const fini = (v) => v != null && isFinite(v);
|
|
1384
|
+
const lignes = [];
|
|
1385
|
+
for (const c of data) {
|
|
1386
|
+
if (!c || c.length < 3 || !fini(c[0]) || !fini(c[1]) || !fini(c[2])) return rate();
|
|
1387
|
+
const xy = proj_js.transform([c[0], c[1]], 'EPSG:4326', dataProj);
|
|
1388
|
+
lignes.push([xy[0], xy[1], c[2]]);
|
|
1389
|
+
}
|
|
1390
|
+
if (lignes.length < 2) return rate();
|
|
1391
|
+
|
|
1392
|
+
this._demLoading = false;
|
|
1393
|
+
this._trackLines = [lignes]; this._linesFor = feature;
|
|
1394
|
+
this._demZ = null; this._demFor = null;
|
|
1395
|
+
this._compute();
|
|
1396
|
+
this._updateZoomButtons();
|
|
1397
|
+
if (!this._collapsed) this._render();
|
|
1398
|
+
this.dispatchEvent({ type: 'demload', ok: true, zoom: null, tiles: 0 });
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1268
1401
|
/**
|
|
1269
1402
|
* Outcome of a fill, whatever produced it: adopt the elevations, drop the spinner,
|
|
1270
1403
|
* redraw, announce.
|
|
@@ -1298,14 +1431,18 @@
|
|
|
1298
1431
|
// ---------- computation -------------------------------------------
|
|
1299
1432
|
_compute() {
|
|
1300
1433
|
const o = this.options;
|
|
1301
|
-
|
|
1302
|
-
const
|
|
1434
|
+
// Un profil précalculé apporte sa géométrie ; sinon, celle de l'entité.
|
|
1435
|
+
const substitue = this._linesFor === this._feature && this._trackLines;
|
|
1436
|
+
const geom = this._feature.getGeometry && this._feature.getGeometry();
|
|
1437
|
+
const lines = substitue ? this._trackLines : (geom ? geomLines(geom) : []);
|
|
1303
1438
|
const dataProj = o.dataProjection || (this.getMap() && this.getMap().getView().getProjection()) || 'EPSG:3857';
|
|
1304
1439
|
|
|
1305
1440
|
// Terrain-model elevations, if they were loaded for THIS feature.
|
|
1306
1441
|
const demZ = (this._demFor === this._feature) ? this._demZ : null;
|
|
1307
1442
|
|
|
1308
|
-
|
|
1443
|
+
// Les horodatages sont indexés sur les points de l'ENTITÉ : sur une géométrie
|
|
1444
|
+
// substituée ils tomberaient à côté, et un temps faux vaut moins que pas de temps.
|
|
1445
|
+
const times = substitue ? null : extractTimes(this._feature, lines);
|
|
1309
1446
|
this._hasTime = !!times;
|
|
1310
1447
|
const ignoreStops = o.ignoreStops !== false; // default: stops are excluded
|
|
1311
1448
|
const stopSpeed = (o.stopSpeed != null ? o.stopSpeed : 0.5); // m/s
|
|
@@ -1377,19 +1514,57 @@
|
|
|
1377
1514
|
* `nice()` n'est pas appliqué en échelle absolue : il arrondit le domaine vers
|
|
1378
1515
|
* l'extérieur, donc il fausserait le rapport qu'on vient de fixer.
|
|
1379
1516
|
*/
|
|
1380
|
-
_yScale(s, zpad, innerH) {
|
|
1381
|
-
const
|
|
1382
|
-
const
|
|
1517
|
+
_yScale(s, zpad, innerH, innerW) {
|
|
1518
|
+
const bas = s.min - zpad, haut = s.max + zpad;
|
|
1519
|
+
const cm = innerH / this._pxPerCm(); // hauteur du graphe, en centimètres
|
|
1520
|
+
const cmH = innerW / this._pxPerCm(); // largeur du graphe, en centimètres
|
|
1521
|
+
// Le rapport entre les deux axes, quel que soit le mode : c'est la grandeur qu'on
|
|
1522
|
+
// tient fixe sous `{ exaggeration }`, et celle qui dérive librement en `'auto'`.
|
|
1523
|
+
// La publier partout est le seul moyen de voir la différence plutôt que d'y croire.
|
|
1524
|
+
const rapport = (etendue) => (cmH > 0 && etendue > 0) ? (s.distance / cmH) / (etendue / cm) : null;
|
|
1525
|
+
|
|
1526
|
+
const vs = this._verticalScaleFor(s, innerW);
|
|
1383
1527
|
if (!(vs > 0)) {
|
|
1384
|
-
|
|
1385
|
-
|
|
1528
|
+
// `'auto'` : la hauteur est remplie, donc c'est l'amplitude qui décide de tout.
|
|
1529
|
+
// `nice()` arrondit le domaine vers l'extérieur, d'où la lecture APRÈS coup.
|
|
1530
|
+
const echelle = d3__namespace.scaleLinear().domain([bas, haut]).range([innerH, 0]).nice();
|
|
1531
|
+
const dom = echelle.domain();
|
|
1532
|
+
this._vScale = null; // aucune échelle absolue demandée
|
|
1533
|
+
this._vExaggeration = rapport(dom[1] - dom[0]);
|
|
1534
|
+
return echelle;
|
|
1386
1535
|
}
|
|
1387
|
-
const cm = innerH / this._pxPerCm(); // hauteur du graphe, en centimètres
|
|
1388
1536
|
const etendue = Math.max(vs * cm, haut - bas); // jamais moins qu'il n'en faut
|
|
1389
1537
|
const milieu = (bas + haut) / 2;
|
|
1390
1538
|
this._vScale = etendue / cm; // m/cm effectivement appliqués
|
|
1539
|
+
// Sous le plancher, le rapport retombe sous celui demandé : c'est la seule façon de
|
|
1540
|
+
// savoir que c'est arrivé.
|
|
1541
|
+
this._vExaggeration = rapport(etendue);
|
|
1391
1542
|
return d3__namespace.scaleLinear().domain([milieu - etendue / 2, milieu + etendue / 2]).range([innerH, 0]);
|
|
1392
1543
|
}
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* Les mètres par centimètre demandés, sous l'une ou l'autre forme de `verticalScale`.
|
|
1547
|
+
*
|
|
1548
|
+
* Une échelle absolue rend les AMPLITUDES comparables, pas les pentes : l'axe
|
|
1549
|
+
* horizontal s'étire toujours sur toute la trace, si bien qu'une même pente de 5 %
|
|
1550
|
+
* paraît trois fois plus raide sur une boucle de 3 km que sur une traversée de 30. Une
|
|
1551
|
+
* exagération fixe le RAPPORT entre les deux axes : la pente lue sur le dessin est
|
|
1552
|
+
* alors la pente réelle, multipliée par un facteur constant d'une trace à l'autre.
|
|
1553
|
+
*
|
|
1554
|
+
* Elle se calcule ici et pas chez l'appelant parce qu'elle demande deux choses que lui
|
|
1555
|
+
* n'a pas : la largeur du graphe une fois les marges retirées, et la distance
|
|
1556
|
+
* réellement affichée - celle du recadrage A/B en cours, non celle de la trace.
|
|
1557
|
+
*
|
|
1558
|
+
* @private
|
|
1559
|
+
*/
|
|
1560
|
+
_verticalScaleFor(s, innerW) {
|
|
1561
|
+
const v = this.options.verticalScale;
|
|
1562
|
+
if (typeof v === 'number') return v;
|
|
1563
|
+
const ex = (v && typeof v === 'object') ? Number(v.exaggeration) : 0;
|
|
1564
|
+
if (!(ex > 0) || !(s.distance > 0) || !(innerW > 0)) return 0;
|
|
1565
|
+
const cm = innerW / this._pxPerCm(); // largeur du graphe, en centimètres
|
|
1566
|
+
return (s.distance / cm) / ex; // horizontale ÷ exagération
|
|
1567
|
+
}
|
|
1393
1568
|
_statsOf(samples, fromTotal) {
|
|
1394
1569
|
let ascent = 0, descent = 0, zmin = Infinity, zmax = -Infinity, maxAbs = 0;
|
|
1395
1570
|
for (let k = 0; k < samples.length; k++) {
|
|
@@ -1483,17 +1658,80 @@
|
|
|
1483
1658
|
* is skipped while collapsed, so without a separate entry point the title would keep
|
|
1484
1659
|
* naming the previous track after a change of feature.
|
|
1485
1660
|
*/
|
|
1661
|
+
/**
|
|
1662
|
+
* Cette trace a-t-elle une altitude, d'où qu'elle vienne ?
|
|
1663
|
+
*
|
|
1664
|
+
* Trois sources possibles, et une seule suffit : le Z de la géométrie, les altitudes
|
|
1665
|
+
* rapportées par un modèle de terrain, ou le profil précalculé d'une source `track`.
|
|
1666
|
+
* Aucune des trois, et il n'y a rien à dessiner - ce qui ne veut pas dire zéro.
|
|
1667
|
+
*
|
|
1668
|
+
* @private
|
|
1669
|
+
*/
|
|
1670
|
+
_hasElevation() {
|
|
1671
|
+
const f = this._feature;
|
|
1672
|
+
if (!f) return false;
|
|
1673
|
+
if (ElevationProfile.featureHasZ(f)) return true;
|
|
1674
|
+
if (this._demFor === f && this._demZ) return true;
|
|
1675
|
+
if (this._linesFor === f && this._trackLines) return true;
|
|
1676
|
+
return false;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/**
|
|
1680
|
+
* Ce qui prend la place du graphe quand la trace n'a aucune altitude.
|
|
1681
|
+
*
|
|
1682
|
+
* Même forme que le spinner, et pour la même raison : la hauteur du graphe est tenue,
|
|
1683
|
+
* si bien que le panneau ne saute pas si un profil finit par arriver. Le titre reste,
|
|
1684
|
+
* lui : c'est la trace cliquée, et l'utilisateur a besoin de savoir laquelle.
|
|
1685
|
+
*
|
|
1686
|
+
* @private
|
|
1687
|
+
*/
|
|
1688
|
+
_renderNoElevation() {
|
|
1689
|
+
if (!this.options.showWithoutElevation) { this.element.style.display = 'none'; return; }
|
|
1690
|
+
this.element.style.display = '';
|
|
1691
|
+
this._applyTheme();
|
|
1692
|
+
this._applyPlacement();
|
|
1693
|
+
this._renderHeader(true);
|
|
1694
|
+
const H = typeof this.options.height === 'number' ? this.options.height : 180;
|
|
1695
|
+
this._body.innerHTML = '';
|
|
1696
|
+
const box = document.createElement('div');
|
|
1697
|
+
box.className = 'oep-noelev';
|
|
1698
|
+
box.style.height = `${H}px`;
|
|
1699
|
+
box.setAttribute('role', 'status');
|
|
1700
|
+
box.textContent = this.options.labels.noElevation;
|
|
1701
|
+
this._body.appendChild(box);
|
|
1702
|
+
this._clearFocus();
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* La première propriété qui répond, parmi celles proposées.
|
|
1707
|
+
*
|
|
1708
|
+
* Un nom de propriété unique reste un nom de propriété unique ; une liste est essayée
|
|
1709
|
+
* dans l'ordre. `garde` filtre ce qu'on accepte : n'importe quelle valeur non vide pour
|
|
1710
|
+
* un titre, une URL véritable pour un lien - sans quoi une propriété `url` contenant
|
|
1711
|
+
* autre chose ferait un lien mort plutôt que pas de lien du tout.
|
|
1712
|
+
*
|
|
1713
|
+
* @private
|
|
1714
|
+
*/
|
|
1715
|
+
_pickProperty(f, spec, garde) {
|
|
1716
|
+
if (!spec || !f || !f.get) return null;
|
|
1717
|
+
for (const cle of (Array.isArray(spec) ? spec : [spec])) {
|
|
1718
|
+
const v = f.get(cle);
|
|
1719
|
+
if (v != null && v !== '' && (!garde || garde(v))) return v;
|
|
1720
|
+
}
|
|
1721
|
+
return null;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1486
1724
|
_renderTitle() {
|
|
1487
1725
|
const o = this.options, f = this._feature;
|
|
1488
1726
|
if (!f) return;
|
|
1489
|
-
const name = (f
|
|
1490
|
-
const linkUrl =
|
|
1727
|
+
const name = this._pickProperty(f, o.titleProperty) || o.labels.untitled;
|
|
1728
|
+
const linkUrl = this._pickProperty(f, o.titleLink, isUrl);
|
|
1491
1729
|
if (isUrl(linkUrl)) this._titleEl.innerHTML = `<a href="${esc(linkUrl)}" target="_blank" rel="noopener">${esc(name)}</a>`;
|
|
1492
1730
|
else this._titleEl.textContent = name;
|
|
1493
1731
|
this._titleEl.setAttribute('title', name);
|
|
1494
1732
|
}
|
|
1495
1733
|
|
|
1496
|
-
_renderHeader() {
|
|
1734
|
+
_renderHeader(sansAltimetrie) {
|
|
1497
1735
|
const o = this.options, s = this._stats, f = this._feature;
|
|
1498
1736
|
this._renderTitle();
|
|
1499
1737
|
// While the elevations are still unknown, every figure would read zero: a D+ of 0 m
|
|
@@ -1507,7 +1745,10 @@
|
|
|
1507
1745
|
const html = [], text = [];
|
|
1508
1746
|
o.headerItems.forEach((it) => {
|
|
1509
1747
|
if (typeof it === 'string') {
|
|
1510
|
-
|
|
1748
|
+
// Sans altitude, il reste ce que la géométrie sait dire : la longueur, la durée.
|
|
1749
|
+
// Le dénivelé et les altitudes extrêmes, eux, vaudraient zéro - on les tait.
|
|
1750
|
+
if (sansAltimetrie && (it === 'ascent' || it === 'descent' || it === 'min' || it === 'max' || it === 'minmax')) return;
|
|
1751
|
+
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)); }
|
|
1511
1752
|
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)}`); }
|
|
1512
1753
|
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)}`); }
|
|
1513
1754
|
else if (it === 'min') { html.push(fmtElevation(s.min, o.units)); text.push(fmtElevation(s.min, o.units)); }
|
|
@@ -1562,6 +1803,10 @@
|
|
|
1562
1803
|
// ---------- rendering ---------------------------------------------
|
|
1563
1804
|
_render() {
|
|
1564
1805
|
const o = this.options, s = this._stats, data = this._samples;
|
|
1806
|
+
// Aucune altitude par aucune voie, et plus rien en chemin : le graphe n'a pas lieu
|
|
1807
|
+
// d'être. Le tracer quand même poserait une ligne plate au niveau zéro sous un D+ de
|
|
1808
|
+
// 0 m, ce qui n'est pas un chiffre manquant mais un chiffre faux.
|
|
1809
|
+
if (!this._demLoading && !this._hasElevation()) { this._renderNoElevation(); return; }
|
|
1565
1810
|
if (!data || !data.length) return;
|
|
1566
1811
|
this._applyTheme();
|
|
1567
1812
|
const mobile = this._applyPlacement();
|
|
@@ -1592,7 +1837,7 @@
|
|
|
1592
1837
|
|
|
1593
1838
|
const x = d3__namespace.scaleLinear().domain([0, s.distance]).range([0, innerW]);
|
|
1594
1839
|
const zpad = (s.max - s.min) * 0.1 || 10;
|
|
1595
|
-
const y = this._yScale(s, zpad, innerH);
|
|
1840
|
+
const y = this._yScale(s, zpad, innerH, innerW);
|
|
1596
1841
|
this._x = x; this._y = y; this._dims = { innerW, innerH };
|
|
1597
1842
|
|
|
1598
1843
|
if (o.grid) g.append('g').attr('class', 'oep-grid').call(d3__namespace.axisLeft(y).ticks(yTicks).tickSize(-innerW).tickFormat(''));
|
|
@@ -2027,7 +2272,10 @@
|
|
|
2027
2272
|
ElevationProfile.DEM_PRESETS = DEM_PRESETS;
|
|
2028
2273
|
ElevationProfile.DemSampler = DemSampler;
|
|
2029
2274
|
ElevationProfile.POSITIONS = POSITIONS;
|
|
2030
|
-
|
|
2275
|
+
// Stamped at build time from package.json - see rollup.config.mjs, which fails the build
|
|
2276
|
+
// if this placeholder ever stops matching. Read from the source rather than the bundle,
|
|
2277
|
+
// it says just that: a development copy, of no released version.
|
|
2278
|
+
ElevationProfile.version = '2.1.0';
|
|
2031
2279
|
|
|
2032
2280
|
return ElevationProfile;
|
|
2033
2281
|
|