@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
103 lines (102 loc) • 3.8 kB
JavaScript
const MAPS_API_LOADED_CALLBACK = 'girafeGoogleMapsApiLoaded';
let mapsApiLoading = null;
/**
* Loads the Google Maps JavaScript API once, when the first Google StreetView is displayed.
* The API is only usable once it calls back the application: the load event of the script fires
* before the API has finished loading.
* The API key is part of the script URL, and therefore visible to the user: it must be restricted
* to the domain of the application in the Google Cloud Console.
*/
function loadMapsApi(apiKey) {
mapsApiLoading ??= new Promise((resolve, reject) => {
window[MAPS_API_LOADED_CALLBACK] = () => {
delete window[MAPS_API_LOADED_CALLBACK];
resolve();
};
const script = document.createElement('script');
const params = new URLSearchParams({
key: apiKey,
v: 'weekly',
loading: 'async',
callback: MAPS_API_LOADED_CALLBACK
});
script.src = `https://maps.googleapis.com/maps/api/js?${params}`;
script.async = true;
script.addEventListener('error', () => reject(new Error('The Google Maps JavaScript API could not be loaded')));
document.head.appendChild(script);
});
return mapsApiLoading;
}
function isZeroResults(error) {
return error?.code === 'ZERO_RESULTS';
}
/**
* The Google Street View footage, displayed by a StreetViewPanorama.
* This module is the only place where the Google Maps API is used. It is loaded on demand
* by the StreetView Component, so that the API stays out of the application as long as
* no Google StreetView is displayed.
*/
export default class GoogleStreetViewProvider {
apiKey;
service;
panorama;
nearest;
constructor(apiKey) {
this.apiKey = apiKey;
}
async initialize(target, onFootageChanged) {
await loadMapsApi(this.apiKey);
const library = (await google.maps.importLibrary('streetView'));
this.nearest = library.StreetViewPreference.NEAREST;
this.service = new library.StreetViewService();
this.panorama = new library.StreetViewPanorama(target, {
visible: false,
pov: { heading: 0, pitch: 0 }
});
const notifyFootageChanged = () => {
const footage = this.getFootage();
if (footage) {
onFootageChanged(footage);
}
};
this.panorama.addListener('position_changed', notifyFootageChanged);
this.panorama.addListener('pov_changed', notifyFootageChanged);
}
getFootage() {
const position = this.panorama.getPosition();
if (!position) {
return null;
}
const pov = this.panorama.getPov();
return { lonLat: [position.lng(), position.lat()], heading: pov.heading, pitch: pov.pitch };
}
async showClosestFootage(lonLat, radius, pov) {
try {
const response = await this.service.getPanorama({
location: { lng: lonLat[0], lat: lonLat[1] },
radius: radius,
preference: this.nearest
});
// The API does not respond when it is in error (invalid key, ...)
const pano = response?.data?.location?.pano;
if (!pano) {
return false;
}
this.panorama.setPano(pano);
if (pov) {
this.panorama.setPov(pov);
}
this.panorama.setVisible(true);
return true;
}
catch (error) {
if (isZeroResults(error)) {
return false;
}
throw error;
}
}
setVisible(visible) {
this.panorama?.setVisible(visible && this.panorama.getPosition() !== null);
}
}