• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geosolutions-it / MapStore2 / 12831531306

17 Jan 2025 03:01PM UTC coverage: 77.182% (+0.07%) from 77.115%
12831531306

Pull #10746

github

web-flow
Merge 501dbaeea into 4e4dabc03
Pull Request #10746: Fix #10739 Changing correctly resolutions limits when switching map CRS

30373 of 47156 branches covered (64.41%)

34 of 43 new or added lines in 2 files covered. (79.07%)

126 existing lines in 15 files now uncovered.

37769 of 48935 relevant lines covered (77.18%)

35.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.29
/web/client/plugins/StreetView/components/CyclomediaView/CyclomediaView.js
1
import React, {useState, useEffect, useRef} from 'react';
2
import Message from '../../../../components/I18N/Message';
3
import { isProjectionAvailable } from '../../../../utils/ProjectionUtils';
4
import { reproject } from '../../../../utils/CoordinatesUtils';
5

6

7
import { getCredentials as getStoredCredentials, setCredentials as setStoredCredentials } from '../../../../utils/SecurityUtils';
8
import { CYCLOMEDIA_CREDENTIALS_REFERENCE } from '../../constants';
9
import { Alert, Button } from 'react-bootstrap';
10

11
import CyclomediaCredentials from './Credentials';
12
import EmptyStreetView from '../EmptyStreetView';
13
const PROJECTION_NOT_AVAILABLE = "Projection not available";
1✔
14
const isInvalidCredentials = (error) => {
1✔
15
    return error?.message?.indexOf?.("code 401");
15✔
16
};
17
/**
18
 * Parses the error message to show to the user in the alert an user friendly message
19
 * @private
20
 * @param {object|string} error the error to parse
21
 * @returns {string|JSX.Element} the error message
22
 */
23
const getErrorMessage = (error, msgParams = {}) => {
1!
24
    if (isInvalidCredentials(error) >= 0) {
10!
25
        return <Message msgId="streetView.cyclomedia.errors.invalidCredentials" msgParams={msgParams} />;
×
26
    }
27
    if (error?.message?.indexOf?.(PROJECTION_NOT_AVAILABLE) >= 0) {
10✔
28
        return <Message msgId="streetView.cyclomedia.errors.projectionNotAvailable" msgParams={msgParams} />;
2✔
29
    }
30
    return error?.message ?? "Unknown error";
8✔
31
};
32

33
/**
34
 * EmptyView component. It shows a message when the API is not initialized or the map point are not visible.
35
 * @private
36
 * @param {object} props the component props
37
 * @param {object} props.style the style of the component
38
 * @param {boolean} props.initializing true if the API is initializing
39
 * @param {boolean} props.initialized true if the API is initialized
40
 * @param {object} props.StreetSmartApi the StreetSmartApi object
41
 * @param {boolean} props.mapPointVisible true if the map point are visible at the current level of zoom.
42
 * @returns {JSX.Element} the component rendering
43
 */
44
const EmptyView = ({initializing, initialized, StreetSmartApi, mapPointVisible}) => {
1✔
45
    if (initialized && !mapPointVisible) {
6!
UNCOV
46
        return (
×
47
            <EmptyStreetView description={<Message msgId="streetView.cyclomedia.zoomIn" />} />
48
        );
49
    }
50
    if (initialized) {
6!
51
        return (
×
52
            <EmptyStreetView />
53
        );
54
    }
55
    if (initializing) {
6✔
56
        return (
1✔
57
            <EmptyStreetView loading description={<Message msgId="streetView.cyclomedia.initializing" />} />
58

59
        );
60
    }
61
    if (!StreetSmartApi) {
5✔
62
        return (
3✔
63
            <EmptyStreetView loading description={<Message msgId="streetView.cyclomedia.loadingAPI" />} />
64
        );
65
    }
66
    return null;
2✔
67
};
68

69
/**
70
 * CyclomediaView component. It uses the Cyclomedia API to show the street view.
71
 * API Documentation at https://streetsmart.cyclomedia.com/api/v23.14/documentation/
72
 * This component is a wrapper of the Cyclomedia API. It uses an iframe to load the API, because actually the API uses and initializes react-dnd,
73
 * that must be unique in the application and it is already created and initialized by MapStore.
74
 * @param {object} props the component props
75
 * @param {string} props.apiKey the Cyclomedia API key
76
 * @param {object} props.style the style of the component
77
 * @param {object} props.location the location of the street view. It contains the latLng and the properties of the feature
78
 * @param {object} props.location.latLng the latLng of the street view. It contains the lat and lng properties
79
 * @param {object} props.location.properties the properties of the feature. It contains the `imageId` that can be used as query
80
 * @param {function} props.setPov the function to call when the point of view changes. It receives the new point of view as parameter (an object with `heading` and `pitch` properties)
81
 * @param {function} props.setLocation the function to call when the location changes. It receives the new location as parameter (an object with `latLng` and `properties` properties)
82
 * @param {boolean} props.mapPointVisible true if the map point are visible at the current level of zoom. It is used to show a message to zoom in when the map point are not visible.
83
 * @param {object} props.providerSettings the settings of the provider. It contains the `StreetSmartApiURL` property that is the URL of the Cyclomedia API
84
 * @param {function} props.refreshLayer the function to call to refresh the layer. It is used to refresh the layer when the credentials are changed.
85
 * @returns {JSX.Element} the component rendering
86
 */
87

88
const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLocation = () => {}, mapPointVisible, providerSettings = {}, refreshLayer = () => {}}) => {
1!
89
    const StreetSmartApiURL = providerSettings?.StreetSmartApiURL ?? "https://streetsmart.cyclomedia.com/api/v23.7/StreetSmartApi.js";
10!
90
    const scripts = providerSettings?.scripts ?? `
10!
91
    <script type="text/javascript" src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"></script>
92
    <script type="text/javascript" src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
93
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
94
    `;
95
    const initOptions = providerSettings?.initOptions ?? {};
10✔
96
    const srs = providerSettings?.srs ?? 'EPSG:4326'; // for measurement tool and oblique tool 'EPSG:7791' is one of the supported SRS
10✔
97
    // location contains the latLng and the properties of the feature
98
    // properties contains the `imageId` that can be used as query
99
    const {properties} = location;
10✔
100
    const {imageId} = properties ?? {};
10✔
101

102
    // variables to store the API and the target element for the API
103
    const [StreetSmartApi, setStreetSmartApi] = useState();
10✔
104
    const [targetElement, setTargetElement] = useState();
10✔
105
    const viewer = useRef(null); // reference for the iframe that will contain the viewer
10✔
106

107
    // variables to store the state of the API
108
    const [initializing, setInitializing] = useState(false);
10✔
109
    const [initialized, setInitialized] = useState(false);
10✔
110
    const [reload, setReload] = useState(1);
10✔
111
    const [error, setError] = useState(null);
10✔
112

113
    // gets the credentials from the storage
114
    const initialCredentials = getStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE);
10✔
115
    const [credentials, setCredentials] = useState(initialCredentials);
10✔
116
    const [showCredentialsForm, setShowCredentialsForm] = useState(!credentials?.username || !credentials?.password); // determines to show the credentials form
10✔
117
    const {username, password} = credentials ?? {};
10!
118
    const resetCredentials = () => {
10✔
119
        if (getStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE)) {
4!
120
            setStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE, undefined);
4✔
121
        }
122
    };
123
    // setting a custom srs enables the measurement tool (where present) and other tools, but coordinates of the click
124
    // will be managed in the SRS used, so we need to convert them to EPSG:4326.
125
    // So we need to make sure that the SRS is available for coordinates conversion
126
    useEffect(() => {
10✔
127
        if (!isProjectionAvailable(srs)) {
3✔
128
            console.error(`Cyclomedia API: init: error: projection ${srs} is not available`);
1✔
129
            setError(new Error(PROJECTION_NOT_AVAILABLE));
1✔
130
        }
131
    }, [srs]);
132

133

134
    /**
135
     * Utility function to open an image in street smart viewer (it must be called after the API is initialized)
136
     * @param {string} query query for StreetSmartApi.open
137
     * @param {string} srs SRS for StreetSmartApi.open
138
     * @returns {Promise} a promise that resolves with the panoramaViewer
139
     */
140
    const openImage = (query) => {
10✔
141
        const viewerType = StreetSmartApi.ViewerType.PANORAMA;
1✔
142
        const options = {
1✔
143
            viewerType: viewerType,
144
            srs,
145
            panoramaViewer: {
146
                closable: false,
147
                maximizable: true,
148
                replace: true,
149
                recordingsVisible: true,
150
                navbarVisible: true,
151
                timeTravelVisible: true,
152
                measureTypeButtonVisible: true,
153
                measureTypeButtonStart: true,
154
                measureTypeButtonToggle: true
155
            },
156
            obliqueViewer: {
157
                closable: true,
158
                maximizable: true,
159
                navbarVisible: false
160
            }
161
        };
162
        return StreetSmartApi.open(query, options);
1✔
163
    };
164

165
    // initialize API
166
    useEffect(() => {
10✔
167
        if (!StreetSmartApi || !username || !password || !apiKey || !isProjectionAvailable(srs)) return () => {};
5✔
168
        setInitializing(true);
1✔
169
        StreetSmartApi.init({
1✔
170
            targetElement,
171
            username,
172
            password,
173
            apiKey,
174
            loginOauth: false,
175
            srs: srs,
176
            locale: 'en-us',
177
            ...initOptions
178
        }).then(function() {
179
            setInitializing(false);
1✔
180
            setInitialized(true);
1✔
181
            setError(null);
1✔
182
        }).catch(function(err) {
183
            setInitializing(false);
×
184
            setError(err);
×
185
            if (err) {console.error('Cyclomedia API: init: error: ' + err);}
×
186
        });
187
        return () => {
1✔
188
            try {
1✔
189
                setInitialized(false);
1✔
190
                StreetSmartApi?.destroy?.({targetElement});
1✔
191
            } catch (e) {
192
                console.error(e);
×
193
            }
194

195
        };
196
    }, [StreetSmartApi, username, password, apiKey, reload]);
197
    // update credentials in the storage (for layer and memorization)
198
    useEffect(() => {
10✔
199
        const invalid = isInvalidCredentials(error);
5✔
200
        if (initialized && username && password && !invalid && initialized) {
5✔
201
            setStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE, credentials);
1✔
202
            refreshLayer();
1✔
203
        } else {
204
            resetCredentials();
4✔
205
            refreshLayer();
4✔
206
        }
207
    }, [initialized, username, password, username, password, error, initialized]);
208
    const changeView = (_, {detail} = {}) => {
10!
209
        const {yaw: heading, pitch} = detail ?? {};
×
210
        setPov({heading, pitch});
×
211
    };
212
    const changeRecording = (_, {detail} = {}) => {
10!
213
        const {recording} = detail ?? {};
×
214
        // extract coordinates lat long from `xyz` of `recording` and `imageId` from recording `id` property
215
        if (recording?.xyz && recording?.id) {
×
216
            const {x: lng, y: lat} = reproject([recording?.xyz?.[0], recording?.xyz?.[1]], srs, 'EPSG:4326');
×
217
            setLocation({
×
218
                latLng: {
219
                    lat,
220
                    lng,
221
                    h: recording?.xyz?.[2] || 0
×
222
                },
223
                properties: {
224
                    ...recording,
225
                    imageId: recording?.id
226
                }
227
            });
228
        }
229
    };
230
    // open image when the imageId changes
231
    useEffect(() => {
10✔
232
        if (!StreetSmartApi || !imageId || !initialized) return () => {};
7✔
233
        let panoramaViewer;
234
        let viewChangeHandler;
235
        let recordingClickHandler;
236
        openImage(imageId)
1✔
237
            .then((result) => {
238
                if (result && result[0]) {
1!
239
                    panoramaViewer = result[0];
×
240
                    viewChangeHandler = (...args) => changeView(panoramaViewer, ...args);
×
241
                    recordingClickHandler = (...args) => changeRecording(panoramaViewer, ...args);
×
242
                    panoramaViewer.on(StreetSmartApi.Events.panoramaViewer.VIEW_CHANGE, viewChangeHandler);
×
243
                    panoramaViewer.on(StreetSmartApi.Events.panoramaViewer.RECORDING_CLICK, recordingClickHandler);
×
244
                }
245

246
            })
247
            .catch((err) => {
248
                setError(err);
×
249
                console.error('Cyclomedia API: open: error: ' + err);
×
250
            });
251
        return () => {
1✔
252
            if (panoramaViewer && viewChangeHandler) {
1!
253
                panoramaViewer.off(StreetSmartApi.Events.panoramaViewer.VIEW_CHANGE, viewChangeHandler);
×
254
                panoramaViewer.off(StreetSmartApi.Events.panoramaViewer.RECORDING_CLICK, recordingClickHandler);
×
255
            }
256
        };
257
    }, [StreetSmartApi, initialized, imageId]);
258

259
    // flag to show the panorama viewer
260
    const showPanoramaViewer = StreetSmartApi && initialized && imageId && !showCredentialsForm && !error;
10✔
261
    // flag to show the empty view
262
    const showEmptyView = initializing || !showCredentialsForm && !showPanoramaViewer && !error;
10✔
263
    const showError = error && !showCredentialsForm && !showPanoramaViewer && !initializing;
10✔
264

265
    // create the iframe content
266
    const srcDoc = `<html>
10✔
267
        <head>
268
            <style>
269
                html, body, #ms-street-smart-viewer-container {
270
                    width: 100%;
271
                    height: 100%;
272
                    margin: 0;
273
                    padding: 0;
274
                    overflow: hidden;
275
                }
276
            </style>
277
            ${scripts}
278
            <script type="text/javascript" src="${StreetSmartApiURL}" ></script>
279
            <script>
280
                    window.StreetSmartApi = StreetSmartApi
281
            </script>
282
            </head>
283
            <body>
284
                <div key="main" id="ms-street-smart-viewer-container" />
285

286
            </body>
287
        </html>`;
288
    return (<>
10✔
289
        {<CyclomediaCredentials
290
            key="credentials"
291
            showCredentialsForm={showCredentialsForm}
292
            setShowCredentialsForm={setShowCredentialsForm}
293
            credentials={credentials}
294
            setCredentials={(newCredentials) => {
295
                setCredentials(newCredentials);
×
296
            }}/>}
297
        {showEmptyView ? <EmptyView key="empty-view" StreetSmartApi={StreetSmartApi} style={style} initializing={initializing} initialized={initialized}  mapPointVisible={mapPointVisible}/> : null}
10✔
298
        <iframe key="iframe" ref={viewer} onLoad={() => {
299
            setTargetElement(viewer.current?.contentDocument.querySelector('#ms-street-smart-viewer-container'));
2✔
300
            setStreetSmartApi(viewer.current?.contentWindow.StreetSmartApi);
2✔
301
        }} style={{ ...style, display: showPanoramaViewer ? 'block' : 'none'}}  srcDoc={srcDoc}>
10✔
302

303
        </iframe>
304
        <Alert bsStyle="danger" style={{...style, textAlign: 'center', alignContent: 'center', display: showError ? 'block' : 'none'}} key="error">
10✔
305
            <Message msgId="streetView.cyclomedia.errorOccurred" />
306
            {getErrorMessage(error, {srs})}
307
            {initialized ? <div><Button
10✔
308
                onClick={() => {
309
                    setError(null);
×
310
                    try {
×
311
                        setReload(reload + 1);
×
312
                    } catch (e) {
313
                        console.error(e);
×
314
                    }
315
                }}>
316
                <Message msgId="streetView.cyclomedia.reloadAPI"/>
317
            </Button></div> : null}
318
        </Alert>
319
    </>);
320
};
321

322
export default CyclomediaView;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc