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

geosolutions-it / MapStore2 / 19712434894

26 Nov 2025 05:32PM UTC coverage: 76.663% (+0.002%) from 76.661%
19712434894

push

github

web-flow
Update CI.yml

SFTP investigation

32262 of 50209 branches covered (64.26%)

40156 of 52380 relevant lines covered (76.66%)

37.78 hits per line

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

72.87
/web/client/plugins/StreetView/components/CyclomediaView/CyclomediaView.js
1
import React, {useState, useEffect, useRef} from 'react';
2
import {isEmpty} from 'lodash';
3
import Message from '../../../../components/I18N/Message';
4
import HTML from '../../../../components/I18N/HTML';
5

6
import { isProjectionAvailable } from '../../../../utils/ProjectionUtils';
7
import { reproject } from '../../../../utils/CoordinatesUtils';
8

9

10
import { getCredentials as getStoredCredentials, setCredentials as setStoredCredentials } from '../../../../utils/SecurityUtils';
11
import { CYCLOMEDIA_CREDENTIALS_REFERENCE } from '../../constants';
12
import { Alert, Button, Glyphicon } from 'react-bootstrap';
13
import withConfirm from '../../../../components/misc/withConfirm';
14
import withTooltip from '../../../../components/misc/enhancers/tooltip';
15
const CTButton = withConfirm(withTooltip(Button));
1✔
16
import CyclomediaCredentials from './Credentials';
17
import EmptyStreetView from '../EmptyStreetView';
18
const PROJECTION_NOT_AVAILABLE = "Projection not available";
1✔
19
const isInvalidCredentials = (error) => {
1✔
20
    return error?.message?.indexOf?.("code 401");
34✔
21
};
22
/**
23
 * Parses the error message to show to the user in the alert an user friendly message
24
 * @private
25
 * @param {object|string} error the error to parse
26
 * @returns {string|JSX.Element} the error message
27
 */
28
const getErrorMessage = (error, msgParams = {}) => {
1!
29
    if (!error) {
17✔
30
        return null;
12✔
31
    }
32
    if (isInvalidCredentials(error) >= 0) {
5✔
33
        return <Message msgId="streetView.cyclomedia.errors.invalidCredentials" msgParams={msgParams} />;
3✔
34
    }
35
    if (error?.message?.indexOf?.(PROJECTION_NOT_AVAILABLE) >= 0) {
2!
36
        return <Message msgId="streetView.cyclomedia.errors.projectionNotAvailable" msgParams={msgParams} />;
2✔
37
    }
38
    if (error?.message?.indexOf?.("not logged in") >= 0) {
×
39
        return <HTML msgId="streetView.cyclomedia.errors.notLoggedIn" />;
×
40
    }
41
    return error?.message ?? "Unknown error";
×
42
};
43

44
/**
45
 * EmptyView component. It shows a message when the API is not initialized or the map point are not visible.
46
 * @private
47
 * @param {object} props the component props
48
 * @param {object} props.style the style of the component
49
 * @param {boolean} props.initializing true if the API is initializing
50
 * @param {boolean} props.initialized true if the API is initialized
51
 * @param {object} props.StreetSmartApi the StreetSmartApi object
52
 * @param {boolean} props.mapPointVisible true if the map point are visible at the current level of zoom.
53
 * @param {boolean} props.loggingOut true if the user is logging out
54
 * @param {function} props.onClose the function to call when the user closes the component
55
 * @returns {JSX.Element} the component rendering
56
 */
57
const EmptyView = ({initializing, initialized, StreetSmartApi, mapPointVisible, loggingOut, onClose}) => {
1✔
58
    if (initialized && !mapPointVisible) {
10!
59
        return (
×
60
            <EmptyStreetView description={<Message msgId="streetView.cyclomedia.zoomIn" />} />
61
        );
62
    }
63
    if (initialized) {
10!
64
        return (
×
65
            <EmptyStreetView />
66
        );
67
    }
68
    if (initializing) {
10✔
69
        return (
2✔
70
            <EmptyStreetView loading description={<Message msgId="streetView.cyclomedia.initializing" />} />
71

72
        );
73
    }
74
    if (!StreetSmartApi) {
8✔
75
        return (
4✔
76
            <EmptyStreetView loading description={<Message msgId="streetView.cyclomedia.loadingAPI" />} />
77
        );
78
    }
79
    if (loggingOut) {
4!
80
        return (<EmptyStreetView description={<>
×
81
            <div><Message msgId="streetView.cyclomedia.loggingOut" /></div>
82
            <Button onClick={() => {
83
                onClose();
×
84
            }}>Close</Button>
85
        </>} />);
86
    }
87
    return null;
4✔
88
};
89

90
/**
91
 * CyclomediaView component. It uses the Cyclomedia API to show the street view.
92
 * API Documentation at https://streetsmart.cyclomedia.com/api/v23.14/documentation/
93
 * 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,
94
 * that must be unique in the application and it is already created and initialized by MapStore.
95
 * @param {object} props the component props
96
 * @param {string} props.apiKey the Cyclomedia API key
97
 * @param {object} props.style the style of the component
98
 * @param {object} props.location the location of the street view. It contains the latLng and the properties of the feature
99
 * @param {object} props.location.latLng the latLng of the street view. It contains the lat and lng properties
100
 * @param {object} props.location.properties the properties of the feature. It contains the `imageId` that can be used as query
101
 * @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)
102
 * @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)
103
 * @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.
104
 * @param {object} props.providerSettings the settings of the provider. It contains the `StreetSmartApiURL` property that is the URL of the Cyclomedia API
105
 * @param {function} props.refreshLayer the function to call to refresh the layer. It is used to refresh the layer when the credentials are changed.
106
 * @returns {JSX.Element} the component rendering
107
 */
108

109
const CyclomediaView = ({ apiKey, style, location = {}, setPov = () => {}, setLocation = () => {}, mapPointVisible, providerSettings = {}, refreshLayer = () => {}, onClose = () => {}}) => {
1!
110
    const StreetSmartApiURL = providerSettings?.StreetSmartApiURL ?? "https://streetsmart.cyclomedia.com/api/v23.7/StreetSmartApi.js";
17!
111
    const scripts = providerSettings?.scripts ?? `
17!
112
    <script type="text/javascript" src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"></script>
113
    <script type="text/javascript" src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
114
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
115
    `;
116
    const initOptions = providerSettings?.initOptions ?? {};
17✔
117
    const srs = providerSettings?.srs ?? 'EPSG:4326'; // for measurement tool and oblique tool 'EPSG:7791' is one of the supported SRS
17✔
118
    // location contains the latLng and the properties of the feature
119
    // properties contains the `imageId` that can be used as query
120
    const {properties} = location;
17✔
121
    const {imageId} = properties ?? {};
17✔
122

123
    // variables to store the API and the target element for the API
124
    const [StreetSmartApi, setStreetSmartApi] = useState();
17✔
125
    const [targetElement, setTargetElement] = useState();
17✔
126
    const viewer = useRef(null); // reference for the iframe that will contain the viewer
17✔
127

128
    // variables to store the state of the API
129
    const [initializing, setInitializing] = useState(false);
17✔
130
    const [initialized, setInitialized] = useState(false);
17✔
131
    const [reload, setReload] = useState(1);
17✔
132
    const [error, setError] = useState(null);
17✔
133
    const [reloadAllowed, setReloadAllowed] = useState(false);
17✔
134
    const [loggingOut, setLoggingOut] = useState(false);
17✔
135
    // gets the credentials from the storage or from configuration.
136
    const hasConfiguredCredentials = providerSettings?.credentials;
17✔
137
    const isConfiguredOauth = initOptions?.loginOauth;
17✔
138
    const showLogout = providerSettings?.showLogout ?? true;
17✔
139
    const initialCredentials =
140
        isEmpty(getStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE)) ?
17✔
141
            providerSettings?.credentials ?? {} : getStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE);
24✔
142
    const [credentials, setCredentials] = useState(initialCredentials);
17✔
143
    const [showCredentialsForm, setShowCredentialsForm] = useState(!credentials?.username || !credentials?.password); // determines to show the credentials form
17✔
144
    const {username, password} = credentials ?? {};
17!
145
    const resetCredentials = () => {
17✔
146
        if (getStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE)) {
6!
147
            setStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE, undefined);
6✔
148
        }
149
    };
150
    // setting a custom srs enables the measurement tool (where present) and other tools, but coordinates of the click
151
    // will be managed in the SRS used, so we need to convert them to EPSG:4326.
152
    // So we need to make sure that the SRS is available for coordinates conversion
153
    useEffect(() => {
17✔
154
        if (!isProjectionAvailable(srs)) {
4✔
155
            console.error(`Cyclomedia API: init: error: projection ${srs} is not available`);
1✔
156
            setError(new Error(PROJECTION_NOT_AVAILABLE));
1✔
157
        }
158
    }, [srs]);
159

160

161
    /**
162
     * Utility function to open an image in street smart viewer (it must be called after the API is initialized)
163
     * @param {string} query query for StreetSmartApi.open
164
     * @param {string} srs SRS for StreetSmartApi.open
165
     * @returns {Promise} a promise that resolves with the panoramaViewer
166
     */
167
    const openImage = (query) => {
17✔
168
        const viewerType = StreetSmartApi.ViewerType.PANORAMA;
1✔
169
        const options = {
1✔
170
            viewerType: viewerType,
171
            srs,
172
            panoramaViewer: {
173
                closable: false,
174
                maximizable: true,
175
                replace: true,
176
                recordingsVisible: true,
177
                navbarVisible: true,
178
                timeTravelVisible: true,
179
                measureTypeButtonVisible: true,
180
                measureTypeButtonStart: true,
181
                measureTypeButtonToggle: true
182
            },
183
            obliqueViewer: {
184
                closable: true,
185
                maximizable: true,
186
                navbarVisible: false
187
            }
188
        };
189
        return StreetSmartApi.open(query, options);
1✔
190
    };
191

192
    // initialize API
193
    useEffect(() => {
17✔
194
        if (!StreetSmartApi || !username || !password || !apiKey || !isProjectionAvailable(srs)) return () => {};
7✔
195
        setInitializing(true);
2✔
196
        StreetSmartApi.init({
2✔
197
            targetElement,
198

199
            apiKey,
200
            loginOauth: false,
201
            srs: srs,
202
            locale: 'en-us',
203
            ...initOptions,
204
            ...(isConfiguredOauth
2!
205
                ? { }
206
                : {
207
                    username,
208
                    password
209
                } )
210
        }).then(function() {
211
            setInitializing(false);
1✔
212
            setInitialized(true);
1✔
213
            setError(null);
1✔
214
        }).catch(function(err) {
215
            setInitializing(false);
1✔
216
            setError(err);
1✔
217
            setReloadAllowed(true);
1✔
218
            if (isInvalidCredentials(err) >= 0) {
1!
219
                setShowCredentialsForm(true);
1✔
220
            }
221
            if (err) {
1!
222
                console.error('Cyclomedia API: init: error: ' + err);
1✔
223
            }
224

225

226
        });
227
        return () => {
2✔
228
            try {
2✔
229
                setInitialized(false);
2✔
230
                StreetSmartApi?.destroy?.({targetElement});
2✔
231
            } catch (e) {
232
                console.error(e);
×
233
            }
234

235
        };
236
    }, [StreetSmartApi, username, password, apiKey, reload]);
237
    // update credentials in the storage (for layer and memorization)
238
    useEffect(() => {
17✔
239
        const invalid = isInvalidCredentials(error);
7✔
240
        if (initialized && username && password && !invalid && initialized) {
7✔
241
            setStoredCredentials(CYCLOMEDIA_CREDENTIALS_REFERENCE, credentials);
1✔
242
            refreshLayer();
1✔
243
        } else {
244
            resetCredentials();
6✔
245
            refreshLayer();
6✔
246
        }
247
    }, [initialized, username, password, username, password, error, initialized]);
248
    const changeView = (_, {detail} = {}) => {
17!
249
        const {yaw: heading, pitch} = detail ?? {};
×
250
        setPov({heading, pitch});
×
251
    };
252
    const changeRecording = (_, {detail} = {}) => {
17!
253
        const {recording} = detail ?? {};
×
254
        // extract coordinates lat long from `xyz` of `recording` and `imageId` from recording `id` property
255
        if (recording?.xyz && recording?.id) {
×
256
            const {x: lng, y: lat} = reproject([recording?.xyz?.[0], recording?.xyz?.[1]], srs, 'EPSG:4326');
×
257
            setLocation({
×
258
                latLng: {
259
                    lat,
260
                    lng,
261
                    h: recording?.xyz?.[2] || 0
×
262
                },
263
                properties: {
264
                    ...recording,
265
                    imageId: recording?.id
266
                }
267
            });
268
        }
269
    };
270
    // open image when the imageId changes
271
    useEffect(() => {
17✔
272
        if (!StreetSmartApi || !imageId || !initialized) return () => {};
9✔
273
        let panoramaViewer;
274
        let viewChangeHandler;
275
        let recordingClickHandler;
276
        openImage(imageId)
1✔
277
            .then((result) => {
278
                if (result && result[0]) {
1!
279
                    panoramaViewer = result[0];
×
280
                    viewChangeHandler = (...args) => changeView(panoramaViewer, ...args);
×
281
                    recordingClickHandler = (...args) => changeRecording(panoramaViewer, ...args);
×
282
                    panoramaViewer.on(StreetSmartApi.Events.panoramaViewer.VIEW_CHANGE, viewChangeHandler);
×
283
                    panoramaViewer.on(StreetSmartApi.Events.panoramaViewer.RECORDING_CLICK, recordingClickHandler);
×
284
                }
285

286
            })
287
            .catch((err) => {
288
                setError(err);
×
289
                console.error('Cyclomedia API: open: error: ' + err);
×
290
            });
291
        return () => {
1✔
292
            if (panoramaViewer && viewChangeHandler) {
1!
293
                panoramaViewer.off(StreetSmartApi.Events.panoramaViewer.VIEW_CHANGE, viewChangeHandler);
×
294
                panoramaViewer.off(StreetSmartApi.Events.panoramaViewer.RECORDING_CLICK, recordingClickHandler);
×
295
            }
296
        };
297
    }, [StreetSmartApi, initialized, imageId]);
298

299
    // flag to show the panorama viewer
300
    const showPanoramaViewer = StreetSmartApi && initialized && imageId && !showCredentialsForm && !error;
17✔
301
    // flag to show the empty view
302
    const showEmptyView = initializing || !showCredentialsForm && !showPanoramaViewer && !error;
17✔
303
    const showError = error && !showCredentialsForm && !showPanoramaViewer && !initializing;
17✔
304

305
    // create the iframe content
306
    const srcDoc = `<html>
17✔
307
        <head>
308
            <style>
309
                html, body, #ms-street-smart-viewer-container {
310
                    width: 100%;
311
                    height: 100%;
312
                    margin: 0;
313
                    padding: 0;
314
                    overflow: hidden;
315
                }
316
            </style>
317
            ${scripts}
318
            <script type="text/javascript" src="${StreetSmartApiURL}" ></script>
319
            <script>
320
                    window.StreetSmartApi = StreetSmartApi
321
            </script>
322
            </head>
323
            <body>
324
                <div key="main" id="ms-street-smart-viewer-container" />
325

326
            </body>
327
        </html>`;
328
    return (<>
17✔
329
        {!hasConfiguredCredentials && <CyclomediaCredentials
34✔
330
            key="credentials"
331
            showCredentialsForm={showCredentialsForm}
332
            setShowCredentialsForm={setShowCredentialsForm}
333
            credentials={credentials}
334
            isCredentialsInvalid={isInvalidCredentials(error) >= 0}
335
            setCredentials={(newCredentials) => {
336
                setCredentials(newCredentials);
×
337
                setError(null);
×
338
                setReload(prev => prev + 1);
×
339
            }}/>}
340
        {showLogout
36!
341
            && initialized
342
            && isConfiguredOauth
343
            && !error
344
            && (<div style={{textAlign: "right"}}>
345
                <CTButton
346
                    key="logout"
347
                    confirmContent={<Message msgId="streetView.cyclomedia.confirmLogout" />}
348
                    tooltipId="streetView.cyclomedia.logout"
349
                    onClick={() => {
350
                        StreetSmartApi?.destroy?.({targetElement, loginOauth: true});
×
351
                        setInitialized(false);
×
352
                        setLoggingOut(true);
×
353
                    }}>
354
                    <Glyphicon glyph="log-out" />&nbsp;
355
                </CTButton></div>)}
356
        {showEmptyView
17✔
357
            ? <EmptyView key="empty-view"
358
                StreetSmartApi={StreetSmartApi}
359
                style={style}
360
                initializing={initializing}
361
                initialized={initialized}
362
                loggingOut={loggingOut}
363
                onClose={onClose}
364
                mapPointVisible={mapPointVisible}/>
365
            : null}
366
        <iframe key="iframe" ref={viewer} onLoad={() => {
367
            setTargetElement(viewer.current?.contentDocument.querySelector('#ms-street-smart-viewer-container'));
3✔
368
            setStreetSmartApi(viewer.current?.contentWindow.StreetSmartApi);
3✔
369
        }} style={{ ...style, display: showPanoramaViewer ? 'block' : 'none'}}  srcDoc={srcDoc}>
17✔
370

371
        </iframe>
372
        <Alert bsStyle="danger"
373
            style={{...style, textAlign: 'center', alignContent: 'center', display: showError ? 'block' : 'none', overflow: 'auto'}} key="error">
17✔
374
            <Message msgId="streetView.cyclomedia.errorOccurred" />
375
            {getErrorMessage(error, {srs})}
376
            <div style={{ display: "flex", justifyContent: "center", marginTop: 10 }}>
377
                {(initialized || reloadAllowed) && isInvalidCredentials(error) < 0 ? <div><Button
53!
378
                    style={{margin: 10}}
379
                    onClick={() => {
380
                        setError(null);
×
381
                        setReloadAllowed(false);
×
382
                        try {
×
383
                            setReload(reload + 1);
×
384
                        } catch (e) {
385
                            console.error(e);
×
386
                        }
387
                    }}>
388
                    <Message msgId="streetView.cyclomedia.reloadAPI"/>
389
                </Button></div> : null}
390
                {
391
                    isConfiguredOauth
17!
392
                && showLogout
393
                && !showCredentialsForm
394
                && !initialized
395
                && error?.message?.indexOf?.("not logged in") >= 0
396
                && (<CTButton
397
                    style={{margin: 10}}
398
                    key="logout"
399
                    confirmContent={<Message msgId="streetView.cyclomedia.confirmLogout" />}
400
                    tooltipId="streetView.cyclomedia.tryForceLogout"
401
                    onClick={() => {
402
                        StreetSmartApi?.destroy?.({targetElement, loginOauth: true});
×
403
                    }}>
404
                    <Glyphicon glyph="log-out" />&nbsp;<Message msgId="streetView.cyclomedia.logout" />
405
                </CTButton>)
406
                }
407
            </div>
408
        </Alert>
409
    </>);
410
};
411

412
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

© 2025 Coveralls, Inc