File : core/lib/SvgProfileBuilder.js

1
/*
2
Copyright - 2017 2023 - wwwouaiebe - Contact: https://www.ouaie.be/
3
4
This  program is free software;
5
you can redistribute it and/or modify it under the terms of the
6
GNU General Public License as published by the Free Software Foundation;
7
either version 3 of the License, or any later version.
8
9
This program is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
GNU General Public License for more details.
13
14
You should have received a copy of the GNU General Public License
15
along with this program; if not, write to the Free Software
16
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
*/
18
/*
19
Changes:
20
    - v4.0.0:
21
        - created from v3.6.0
22
Doc reviewed 202208
23
 */
24
25
import { SVG_NS, ZERO, TWO, DISTANCE } from '../../main/Constants.js';
26
27
/* ------------------------------------------------------------------------------------------------------------------------- */
28
/**
29
This class provides methods to build a Route profile
30
*/
31
/* ------------------------------------------------------------------------------------------------------------------------- */
32
33
class SvgProfileBuilder {
34
35
    /**
36
    A reference to the route for witch the profile is createDistanceTexts
37
    @type {Route}
38
    */
39
40
    #route;
41
42
    /**
43
    The created svg with the profile
44
    @type {SVGElement}
45
    */
46
47
    #svg;
48
49
    /**
50
    The scale used in the Y direction
51
    @type {Number}
52
    */
53
54
    #YScale;
55
56
    /**
57
    The scale used in the Y direction
58
    @type {Number}
59
    */
60
61
    #XScale;
62
63
    /**
64
    The smallest elevation
65
    @type {Number}
66
    */
67
68
    #minElev;
69
70
    /**
71
    The greatest elevation
72
    @type {Number}
73
    */
74
75
    #maxElev;
76
77
    /**
78
    The delta between the max and the min elevation
79
    @type {Number}
80
    */
81
82
    #deltaElev;
83
84
    /**
85
    The margin around the profile
86
    @type {Number}
87
    */
88
89
    // eslint-disable-next-line no-magic-numbers
90
    static get PROFILE_MARGIN ( ) { return 100; }
91
92
    /**
93
    The height of the profile
94
    @type {Number}
95
    */
96
97
    // eslint-disable-next-line no-magic-numbers
98
    static get PROFILE_HEIGHT ( ) { return 500; }
99
100
    /**
101
    The width of the profile
102
    @type {Number}
103
    */
104
105
    // eslint-disable-next-line no-magic-numbers
106
    static get PROFILE_WIDTH ( ) { return 1000; }
107
108
    /**
109
    The horizontal distance between the texts and the vertical line of the flag
110
    @type {Number}
111
    */
112
113
    // eslint-disable-next-line no-magic-numbers
114
    static get X_DELTA_TEXT ( ) { return 10; }
115
116
    /**
117
    The vertical distance between texts of the flag
118
    @type {Number}
119
    */
120
121
    // eslint-disable-next-line no-magic-numbers
122
    static get Y_DELTA_TEXT ( ) { return 30; }
123
124
    /**
125
    The possible X scales for the elevation
126
    @type {Number}
127
    */
128
129
    // eslint-disable-next-line no-magic-numbers
130
    static get #X_SCALES ( ) { return [ 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000 ]; }
131
132
    /**
133
    The possible Y scales for the elevation
134
    @type {Number}
135
    */
136
137
    // eslint-disable-next-line no-magic-numbers
138
    static get #Y_SCALES ( ) { return [ 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000 ]; } // vScales
139
140
    /**
141
    The maximum number of legends in the X direction
142
    @type {Number}
143
    */
144
145
    // eslint-disable-next-line no-magic-numbers
146
    static get #MAX_X_LEGEND_NUMBER ( ) { return 8; }
147
148
    /**
149
    The maximum number of legends in the Y direction
150
    @type {Number}
151
    */
152
153
    // eslint-disable-next-line no-magic-numbers
154
    static get #MAX_Y_LEGEND_NUMBER ( ) { return 4; }
155
156
    /**
157
    The left position of the profile inside the svg
158
    @type {String}
159
    */
160
161
    static get #LEFT_PROFILE ( ) { return SvgProfileBuilder.PROFILE_MARGIN.toFixed ( ZERO ); }
162
163
    /**
164
    The right position of the profile inside the svg
165
    @type {String}
166
    */
167
168
    static get #RIGHT_PROFILE ( ) {
169
        return ( SvgProfileBuilder.PROFILE_MARGIN + SvgProfileBuilder.PROFILE_WIDTH ).toFixed ( ZERO );
170
    }
171
172
    /**
173
    The top position of the profile inside the svg
174
    @type {String}
175
    */
176
177
    static get #TOP_PROFILE ( ) { return SvgProfileBuilder.PROFILE_MARGIN.toFixed ( ZERO ); }
178
179
    /**
180
    The bottom position of the profile inside the svg
181
    @type {String}
182
    */
183
184
    static get #BOTTOM_PROFILE ( ) {
185
        return ( SvgProfileBuilder.PROFILE_MARGIN + SvgProfileBuilder.PROFILE_HEIGHT ).toFixed ( ZERO );
186
    }
187
188
    /**
189
    The left position of the texts inside the svg
190
    @type {String}
191
    */
192
193
    static get #LEFT_TEXT_PROFILE ( ) {
194
        return ( SvgProfileBuilder.PROFILE_MARGIN - SvgProfileBuilder.X_DELTA_TEXT ).toFixed ( ZERO );
195
    }
196
197
    /**
198
    The right position of the texts inside the svg
199
    @type {String}
200
    */
201
202
    static get #RIGHT_TEXT_PROFILE ( ) {
203
        return (
204
            SvgProfileBuilder.PROFILE_MARGIN +
205
            SvgProfileBuilder.PROFILE_WIDTH +
206
            SvgProfileBuilder.X_DELTA_TEXT
207
        ).toFixed ( ZERO );
208
    }
209
210
    /**
211
    The bottom position of the texts inside the svg
212
    @type {Number}
213
    */
214
215
    static get #BOTTOM_TEXT_PROFILE ( ) {
216
        return SvgProfileBuilder.PROFILE_MARGIN +
217
            SvgProfileBuilder.PROFILE_HEIGHT +
218
            ( SvgProfileBuilder.PROFILE_MARGIN / TWO );
219
    }
220
221
    /**
222
    This method creates the profile polyline in the svg element
223
    */
224
225
    #createProfilePolyline ( ) {
226
        let pointsAttribute = '';
227
        let distance = ZERO;
228
        let xPolyline = ZERO;
229
        let yPolyline = ZERO;
230
        this.#route.itinerary.itineraryPoints.forEach (
231
            itineraryPoint => {
232
                xPolyline = ( SvgProfileBuilder.PROFILE_MARGIN + ( this.#XScale * distance ) ).toFixed ( ZERO );
233
                yPolyline =
234
                    (
235
                        SvgProfileBuilder.PROFILE_MARGIN +
236
                        ( this.#YScale * ( this.#maxElev - itineraryPoint.elev ) )
237
                    )
238
                        .toFixed ( ZERO );
239
                pointsAttribute += xPolyline + ',' + yPolyline + ' ';
240
                distance += itineraryPoint.distance;
241
            }
242
        );
243
        const polyline = document.createElementNS ( SVG_NS, 'polyline' );
244
        polyline.setAttributeNS ( null, 'points', pointsAttribute );
245
        polyline.setAttributeNS ( null, 'class', 'TravelNotes-Route-SvgProfile-profilePolyline' );
246
        this.#svg.appendChild ( polyline );
247
    }
248
249
    /**
250
    This method creates the frame polyline in the svg element
251
    */
252
253
    #createFramePolyline ( ) {
254
        const pointsAttribute =
255
            SvgProfileBuilder.#LEFT_PROFILE + ',' +
256
            SvgProfileBuilder.#TOP_PROFILE + ' ' + SvgProfileBuilder.#LEFT_PROFILE + ',' +
257
            SvgProfileBuilder.#BOTTOM_PROFILE + ' ' + SvgProfileBuilder.#RIGHT_PROFILE + ',' +
258
            SvgProfileBuilder.#BOTTOM_PROFILE + ' ' + SvgProfileBuilder.#RIGHT_PROFILE + ',' +
259
            SvgProfileBuilder.#TOP_PROFILE;
260
        const polyline = document.createElementNS ( SVG_NS, 'polyline' );
261
        polyline.setAttributeNS ( null, 'points', pointsAttribute );
262
        polyline.setAttributeNS ( null, 'class', 'TravelNotes-Route-SvgProfile-framePolyline' );
263
        this.#svg.appendChild ( polyline );
264
    }
265
266
    /**
267
    This method creates the distance texts in the svg element
268
    */
269
270
    #createDistanceTexts ( ) {
271
        let minDelta = Number.MAX_VALUE;
272
        let selectedScale = 0;
273
        SvgProfileBuilder.#X_SCALES.forEach (
274
            scale => {
275
                const currentDelta = Math.abs ( ( this.#route.distance / SvgProfileBuilder.#MAX_X_LEGEND_NUMBER ) - scale );
276
                if ( currentDelta < minDelta ) {
277
                    minDelta = currentDelta;
278
                    selectedScale = scale;
279
                }
280
            }
281
        );
282
        let distance = Math.ceil ( this.#route.chainedDistance / selectedScale ) * selectedScale;
283
        while ( distance < this.#route.distance + this.#route.chainedDistance ) {
284
            const distanceText = document.createElementNS ( SVG_NS, 'text' );
285
286
            distanceText.appendChild (
287
                document.createTextNode (
288
                    DISTANCE.metersInKm < selectedScale || ZERO < this.#route.chainedDistance
289
                        ?
290
                        ( distance / DISTANCE.metersInKm ) + ' km'
291
                        :
292
                        distance + ' m '
293
                )
294
            );
295
            distanceText.setAttributeNS ( null, 'class', 'TravelNotes-Route-SvgProfile-distLegend' );
296
            distanceText.setAttributeNS (
297
                null,
298
                'x',
299
                SvgProfileBuilder.PROFILE_MARGIN + ( ( distance - this.#route.chainedDistance ) * this.#XScale )
300
            );
301
            distanceText.setAttributeNS ( null, 'y', SvgProfileBuilder.#BOTTOM_TEXT_PROFILE );
302
            distanceText.setAttributeNS ( null, 'text-anchor', 'start' );
303
            this.#svg.appendChild ( distanceText );
304
            distance += selectedScale;
305
        }
306
    }
307
308
    /**
309
    This method creates the elevation texts in the svg element
310
    */
311
312
    #createElevTexts ( ) {
313
        let minDelta = Number.MAX_VALUE;
314
        let selectedScale = ZERO;
315
        SvgProfileBuilder.#Y_SCALES.forEach (
316
            scale => {
317
                const currentDelta = Math.abs ( ( this.#deltaElev / SvgProfileBuilder.#MAX_Y_LEGEND_NUMBER ) - scale );
318
                if ( currentDelta < minDelta ) {
319
                    minDelta = currentDelta;
320
                    selectedScale = scale;
321
                }
322
            }
323
        );
324
        let elev = Math.ceil ( this.#minElev / selectedScale ) * selectedScale;
325
        while ( elev < this.#maxElev ) {
326
            const elevTextY = SvgProfileBuilder.PROFILE_MARGIN + ( ( this.#maxElev - elev ) * this.#YScale );
327
            const rightElevText = document.createElementNS ( SVG_NS, 'text' );
328
            rightElevText.appendChild ( document.createTextNode ( elev.toFixed ( ZERO ) ) );
329
            rightElevText.setAttributeNS ( null, 'class', 'TravelNotes-Route-SvgProfile-elevLegend' );
330
            rightElevText.setAttributeNS ( null, 'x', SvgProfileBuilder.#RIGHT_TEXT_PROFILE );
331
            rightElevText.setAttributeNS ( null, 'y', elevTextY );
332
            rightElevText.setAttributeNS ( null, 'text-anchor', 'start' );
333
            this.#svg.appendChild ( rightElevText );
334
            const leftElevText = document.createElementNS ( SVG_NS, 'text' );
335
            leftElevText.appendChild ( document.createTextNode ( elev.toFixed ( ZERO ) ) );
336
            leftElevText.setAttributeNS ( null, 'class', 'TravelNotes-Route-SvgProfile-elevLegend' );
337
            leftElevText.setAttributeNS ( null, 'x', SvgProfileBuilder.#LEFT_TEXT_PROFILE );
338
            leftElevText.setAttributeNS ( null, 'y', elevTextY );
339
            leftElevText.setAttributeNS ( null, 'text-anchor', 'end' );
340
            this.#svg.appendChild ( leftElevText );
341
            elev += selectedScale;
342
        }
343
    }
344
345
    /**
346
    This method creates the svg element
347
    */
348
349
    #createSvgElement ( ) {
350
        this.#svg = document.createElementNS ( SVG_NS, 'svg' );
351
        this.#svg.setAttributeNS (
352
            null,
353
            'viewBox',
354
            '0 0 ' + ( SvgProfileBuilder.PROFILE_WIDTH + ( TWO * SvgProfileBuilder.PROFILE_MARGIN ) ) +
355
            ' ' + ( SvgProfileBuilder.PROFILE_HEIGHT + ( TWO * SvgProfileBuilder.PROFILE_MARGIN ) )
356
        );
357
        this.#svg.setAttributeNS ( null, 'class', 'TravelNotes-Route-SvgProfile' );
358
    }
359
360
    /**
361
    The constructor
362
    */
363
364
    constructor ( ) {
365
        Object.freeze ( this );
366
    }
367
368
    /**
369
    this method creates the svg with the Route profile. This svg is displayed in the profile window and in the roadbook
370
    @param {Route} route The route for witch the svg must be created
371
    @return {SVGElement} the svg element with the profile
372
    */
373
374
    createSvg ( route ) {
375
376
        // Doing some computations for min and max elev and scale...
377
        this.#route = route;
378
        this.#minElev = Number.MAX_VALUE;
379
        this.#maxElev = ZERO;
380
        this.#route.itinerary.itineraryPoints.forEach (
381
            itineraryPoint => {
382
                this.#maxElev = Math.max ( this.#maxElev, itineraryPoint.elev );
383
                this.#minElev = Math.min ( this.#minElev, itineraryPoint.elev );
384
            }
385
        );
386
        this.#deltaElev = this.#maxElev - this.#minElev;
387
        this.#YScale = SvgProfileBuilder.PROFILE_HEIGHT / this.#deltaElev;
388
        this.#XScale = SvgProfileBuilder.PROFILE_WIDTH / this.#route.distance;
389
390
        // ... then creates the svg
391
        this.#createSvgElement ( );
392
        this.#createProfilePolyline ( );
393
        this.#createFramePolyline ( );
394
        this.#createElevTexts ( );
395
        this.#createDistanceTexts ( );
396
397
        return this.#svg;
398
    }
399
}
400
401
export default SvgProfileBuilder;
402
403
/* --- End of file --------------------------------------------------------------------------------------------------------- */
404