File : printRoute/PrintViewsFactory.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 LatLng from '../containers/LatLng.js';
26
import PrintView from './PrintView.js';
27
28
/* ------------------------------------------------------------------------------------------------------------------------- */
29
/**
30
Compute the size of the views for printing
31
*/
32
/* ------------------------------------------------------------------------------------------------------------------------- */
33
34
class PrintViewsFactory {
35
36
    /**
37
    An array with rhe computed views
38
    @type {Array.<PrintView>}
39
    */
40
41
    #printViews;
42
43
    /**
44
    A reference to the printed route
45
    @type {Route}
46
    */
47
48
    #route;
49
50
    /**
51
    the greatest possible view size
52
    @type {ViewSize}
53
    */
54
55
    #maxViewSize;
56
57
    /**
58
    Compute if the line defined by firstItineraryPoint  lastItineraryPoint
59
    is horizontal or vertical. If yes, the intersection of the line and currentView is returned
60
    @param {PrintView} currentView The current view
61
    @param {ItineraryPoint} firstItineraryPoint The first ItineraryPoint
62
    @param {ItineraryPoint} lastItineraryPoint The last ItineraryPoint
63
    @return {LatLng} The coordinates of the intersection or null if the line is not horizontal or vertical.
64
    */
65
66
    #isItineraryHorOrVer ( currentView, firstItineraryPoint, lastItineraryPoint ) {
67
        if ( firstItineraryPoint.lng === lastItineraryPoint.lng ) {
68
69
            // Itinerary is vertical
70
            return new LatLng (
71
                firstItineraryPoint.lat > lastItineraryPoint.lat ? currentView.bottomLeft.lat : currentView.upperRight.lat,
72
                firstItineraryPoint.lng
73
            );
74
        }
75
        if ( firstItineraryPoint.lat === lastItineraryPoint.lat ) {
76
77
            // Itinerary is horizontal
78
            return new LatLng (
79
                firstItineraryPoint.lat,
80
                firstItineraryPoint.lng < lastItineraryPoint.lng ? currentView.upperRight.lng : currentView.bottomLeft.lng
81
            );
82
        }
83
        return null;
84
    }
85
86
    /**
87
    Test if itineraryPoint is on the frame of currentView
88
    @param {PrintView} currentView The current view
89
    @param {ItineraryPoint} itineraryPoint The ItineraryPoint to test
90
    @return {LatLng} The coordinates of itineraryPoint or null if the itinerayPoint is not on the frame
91
    */
92
93
    #isPointOnViewFrame ( currentView, itineraryPoint ) {
94
95
        const LAT_LNG_TOLERANCE = 0.000001;
96
97
        if (
98
            itineraryPoint.lat - currentView.bottomLeft.lat < LAT_LNG_TOLERANCE
99
            ||
100
            currentView.upperRight.lat - itineraryPoint.lat < LAT_LNG_TOLERANCE
101
            ||
102
            itineraryPoint.lng - currentView.bottomLeft.lng < LAT_LNG_TOLERANCE
103
            ||
104
            currentView.upperRight.lng - itineraryPoint.lng < LAT_LNG_TOLERANCE
105
        ) {
106
107
            // itinerary point is really near the frame. we consider the itinerary point as intermediate point
108
            return LatLng.clone ( itineraryPoint );
109
        }
110
        return null;
111
    }
112
113
    /**
114
    Test if currentView is only a point. If yes an intermediatePoint is computed
115
    to extend the view to the maximun possible
116
    @param {PrintView} currentView The current view
117
    @param {ItineraryPoint} firstItineraryPoint The first ItineraryPoint
118
    @param {ItineraryPoint} lastItineraryPoint The last ItineraryPoint
119
    @return {LatLng} The coordinates of the computed intermediate point or null
120
    */
121
122
    #haveViewOnlyOnePoint ( currentView, firstItineraryPoint, lastItineraryPoint ) {
123
        if (
124
            currentView.bottomLeft.lat === currentView.upperRight.lat
125
            &&
126
            currentView.bottomLeft.lng === currentView.upperRight.lng
127
        ) {
128
            const coef = Math.min (
129
                Math.abs ( this.#maxViewSize.height / ( lastItineraryPoint.lat - firstItineraryPoint.lat ) ),
130
                Math.abs ( this.#maxViewSize.width / ( lastItineraryPoint.lng - firstItineraryPoint.lng ) )
131
            );
132
            return new LatLng (
133
                firstItineraryPoint.lat + ( coef * ( lastItineraryPoint.lat - firstItineraryPoint.lat ) ),
134
                firstItineraryPoint.lng + ( coef * ( lastItineraryPoint.lng - firstItineraryPoint.lng ) )
135
            );
136
        }
137
        return null;
138
    }
139
140
    /**
141
    See comments in the code
142
    @param {PrintView} currentView The current view
143
    @param {ItineraryPoint} firstItineraryPoint The first ItineraryPoint
144
    @param {ItineraryPoint} lastItineraryPoint The last ItineraryPoint
145
    @return {LatLng} The computed point
146
    */
147
148
    #computeIntermediatePoint ( currentView, firstItineraryPoint, lastItineraryPoint ) {
149
150
        /*
151
        we have to find the intersection of the line segment 'firstItineraryPoint -> lastItineraryPoint' with
152
        the rectangle defined by currentView.lowerLeft, currentView.upperRight.
153
        We know also that firstItineraryPoint is inside currentView but perhaps on the frame and that
154
        lastItineraryPoint is outside the frame so the intersection is always between firstItineraryPoint
155
        and lastItineraryPoint
156
157
        Equation of the a line :
158
            y = coefA * x + coefB
159
            or
160
            x = ( y - coefB ) / coefA
161
162
        So we have :
163
164
            firstItineraryPoint.lat = coefA * firstItineraryPoint.lng + coefB
165
            and
166
            lastItineraryPoint.lat = coefA * lastItineraryPoint.lng + coefB
167
168
        and after some transformations:
169
            coefA = ( firstItineraryPoint.lat - lastItineraryPoint.lat ) / ( firstItineraryPoint.lng - lastItineraryPoint.lng )
170
            coefB = firstItineraryPoint.lat - ( coefA * firstItineraryPoint.lng )
171
172
        Notice: we have some computing problems when
173
        - currentView.lowerLeft === currentView.upperRight. We cannot find an intersection and we have to compute a
174
        intermediatePoint outside the currentView
175
        - firstItineraryPoint is on the frame (or really near the frame ) of currentView -> the intersection
176
        is the firstItineraryPoint
177
        - the line segment 'firstItineraryPoint -> lastItineraryPoint' is horizontal or vertical
178
        (we have to divide by 0)
179
180
        So we test first the 3 problems and then we compute the intersection if needed
181
        */
182
183
        let intermediatePoint = this.#haveViewOnlyOnePoint ( currentView, firstItineraryPoint, lastItineraryPoint );
184
        if ( intermediatePoint ) {
185
            return intermediatePoint;
186
        }
187
188
        intermediatePoint = this.#isPointOnViewFrame ( currentView, firstItineraryPoint );
189
        if ( intermediatePoint ) {
190
            return intermediatePoint;
191
        }
192
193
        intermediatePoint = this.#isItineraryHorOrVer ( currentView, firstItineraryPoint, lastItineraryPoint );
194
        if ( intermediatePoint ) {
195
            return intermediatePoint;
196
        }
197
198
        const coefA =
199
            ( firstItineraryPoint.lat - lastItineraryPoint.lat )
200
            /
201
            ( firstItineraryPoint.lng - lastItineraryPoint.lng );
202
        const coefB = firstItineraryPoint.lat - ( coefA * firstItineraryPoint.lng );
203
204
        // Searching intersection with the right side of currentView
205
        intermediatePoint = new LatLng ( ( coefA * currentView.upperRight.lng ) + coefB, currentView.upperRight.lng );
206
207
        if (
208
            intermediatePoint.lat <= currentView.upperRight.lat
209
                &&
210
                intermediatePoint.lat >= currentView.bottomLeft.lat
211
                &&
212
                intermediatePoint.lng < lastItineraryPoint.lng
213
        ) {
214
            return intermediatePoint;
215
        }
216
217
        // Searching intersection with the top side of currentView
218
        intermediatePoint = new LatLng ( currentView.upperRight.lat, ( currentView.upperRight.lat - coefB ) / coefA );
219
220
        if (
221
            intermediatePoint.lng >= currentView.bottomLeft.lng
222
                &&
223
                intermediatePoint.lng <= currentView.upperRight.lng
224
                &&
225
                intermediatePoint.lat < lastItineraryPoint.lat
226
        ) {
227
            return intermediatePoint;
228
        }
229
230
        // Searching intersection with the left side of currentView
231
        intermediatePoint = new LatLng ( ( coefA * currentView.bottomLeft.lng ) + coefB, currentView.bottomLeft.lng );
232
233
        if (
234
            intermediatePoint.lat <= currentView.upperRight.lat
235
                &&
236
                intermediatePoint.lat >= currentView.bottomLeft.lat
237
                &&
238
                intermediatePoint.lng > lastItineraryPoint.lng
239
        ) {
240
            return intermediatePoint;
241
        }
242
243
        // Searching intersection with the bottom side of currentView
244
        intermediatePoint = new LatLng ( currentView.bottomLeft.lat, ( currentView.bottomLeft.lat - coefB ) / coefA );
245
246
        if (
247
            intermediatePoint.lng >= currentView.bottomLeft.lng
248
                &&
249
                intermediatePoint.lng <= currentView.upperRight.lng
250
                &&
251
                intermediatePoint.lat > lastItineraryPoint.lat
252
        ) {
253
            return intermediatePoint;
254
        }
255
        throw new Error ( 'intermediate point not found' );
256
    }
257
258
    /**
259
    Compute the different views needed to print the maps
260
    */
261
262
    #computePrintViews ( ) {
263
264
        this.#printViews = [];
265
266
        // Iteration on the route
267
        const itineraryPointsIterator = this.#route.itinerary.itineraryPoints.iterator;
268
        let done = itineraryPointsIterator.done;
269
270
        // First view is created with the first itineraryPoint
271
        // ! Be sure that points in the views are clones and not only references...
272
273
        let currentView = new PrintView ( );
274
        currentView.bottomLeft = LatLng.clone ( itineraryPointsIterator.value );
275
        currentView.upperRight = LatLng.clone ( itineraryPointsIterator.value );
276
        currentView.entryPoint = LatLng.clone ( itineraryPointsIterator.value );
277
278
        let previousItineraryPoint = itineraryPointsIterator.value;
279
280
        let intermediatePoint = LatLng.clone ( itineraryPointsIterator.value );
281
282
        // we go to the next point
283
        done = itineraryPointsIterator.done;
284
        let currentItineraryPoint = itineraryPointsIterator.value;
285
        while ( ! done ) {
286
287
            // a temporary view is created, extending the current view with the current itinerary point
288
            const tmpView = new PrintView ( );
289
            tmpView.bottomLeft = new LatLng (
290
                Math.min ( currentView.bottomLeft.lat, currentItineraryPoint.lat ),
291
                Math.min ( currentView.bottomLeft.lng, currentItineraryPoint.lng )
292
            );
293
            tmpView.upperRight = new LatLng (
294
                Math.max ( currentView.upperRight.lat, currentItineraryPoint.lat ),
295
                Math.max ( currentView.upperRight.lng, currentItineraryPoint.lng )
296
            );
297
            tmpView.entryPoint = LatLng.clone ( currentView.entryPoint );
298
299
            // computing the temporary view size and comparing with the desired max view size
300
            if (
301
                this.#maxViewSize.height > tmpView.upperRight.lat - tmpView.bottomLeft.lat
302
                &&
303
                this.#maxViewSize.width > tmpView.upperRight.lng - tmpView.bottomLeft.lng
304
            ) {
305
306
                // the current itineraryPoint is inside the temporary view.
307
                // the temporary view becomes the current view and we go to the next itinerary point
308
                currentView = tmpView;
309
                previousItineraryPoint = itineraryPointsIterator.value;
310
                intermediatePoint = LatLng.clone ( itineraryPointsIterator.value );
311
                done = itineraryPointsIterator.done;
312
                currentItineraryPoint = itineraryPointsIterator.value;
313
                if ( done ) {
314
                    currentView.exitPoint = intermediatePoint;
315
                    this.#printViews.push ( Object.freeze ( currentView ) );
316
                }
317
            }
318
            else {
319
320
                // the itineraryPoint is outside the view. We have to compute an intermediate
321
                // point (where the route intersect with the max size view).
322
                intermediatePoint = this.#computeIntermediatePoint (
323
                    currentView,
324
                    previousItineraryPoint,
325
                    currentItineraryPoint
326
                );
327
328
                // The view is extended to the intermediate point
329
                currentView.bottomLeft = new LatLng (
330
                    Math.min ( currentView.bottomLeft.lat, intermediatePoint.lat ),
331
                    Math.min ( currentView.bottomLeft.lng, intermediatePoint.lng )
332
                );
333
                currentView.upperRight = new LatLng (
334
                    Math.max ( currentView.upperRight.lat, intermediatePoint.lat ),
335
                    Math.max ( currentView.upperRight.lng, intermediatePoint.lng )
336
                );
337
338
                // exit point is added to the view
339
                currentView.exitPoint = intermediatePoint;
340
341
                // and the view added to the list view
342
                this.#printViews.push ( Object.freeze ( currentView ) );
343
344
                // and a new view is created
345
                currentView = new PrintView ( );
346
                currentView.bottomLeft = LatLng.clone ( intermediatePoint );
347
                currentView.upperRight = LatLng.clone ( intermediatePoint );
348
                currentView.entryPoint = LatLng.clone ( intermediatePoint );
349
            }
350
        } // end of while ( ! done )
351
    }
352
353
    /**
354
    The constructor
355
    @param {Route} route The route to print
356
    @param {ViewSize} maxViewSize The view size
357
    */
358
359
    constructor ( route, maxViewSize ) {
360
        Object.freeze ( this );
361
        this.#route = route;
362
        this.#maxViewSize = maxViewSize;
363
        this.#computePrintViews ( );
364
    }
365
366
    /**
367
    The print views
368
    @type {Array.<PrintView>}
369
    */
370
371
    get printViews ( ) { return this.#printViews; }
372
373
}
374
375
export default PrintViewsFactory;
376
377
/* --- End of file --------------------------------------------------------------------------------------------------------- */
378