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