Skip to content

Source StreetView

Source StreetView

Cette page est générée automatiquement à partir du dépôt local au moment de la génération de la documentation.

Fichiers inclus

packages/common/src/lib/widgets/streetview/streetview.declaration.ts

packages/common/src/lib/widgets/streetview/streetview.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';
import { type StreetviewConfig, streetViewConfigSchema } from '$lib/widgets/streetview/streetview.config';
import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';
export const declaration = {
factory: () => import('./StreetView.svelte').then((StreetView) => widgetFactorySvelte(StreetView)),
schema: () => streetViewConfigSchema,
} satisfies WidgetDeclaration;
export type StreetViewProps = WidgetProps<StreetviewConfig>;

packages/common/src/lib/widgets/streetview/streetview.config.ts

packages/common/src/lib/widgets/streetview/streetview.config.ts
import { defineWidgetConfig } from '$lib/api/managers/configuration/models/widget/widget-configuration.schema';
import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';
import { z } from 'zod';
import { type I18nRegistry, i18nSchemaFrom } from '$lib/api/managers/i18n';
const streetViewerTranslations = {
'confirm-message': {
fr: 'Conformément aux conditions d\'utilisation de Google, cet outil activera le fond de plan Google. Le fond de plan actuel sera réactivé lorsque vous quitterez "StreetView".',
nl: 'NL - Conformément aux conditions d\'utilisation de Google, cet outil activera le fond de plan Google. Le fond de plan actuel sera réactivé lorsque vous quitterez "StreetView".',
},
} satisfies I18nRegistry;
export const streetViewConfigSchema = defineWidgetConfig({
title: {
fr: 'Street View',
nl: 'NL - Street View',
},
icon: {
geoviewer: 'street-view',
},
inToolbar: inToolbarSchemaFrom({
type: 'button',
}),
onActivate: {
deactivate: {
classes: ['BasemapChooser'],
},
},
i18n: i18nSchemaFrom(streetViewerTranslations),
config: z
.object({
apiKey: z.string().optional().default(''),
googleBaseMapId: z.string().optional().default('GOOGLE'),
closestLocationRadius: z.number().optional().default(500),
initRadius: z.number().optional().default(10000),
enablePinIconRotation: z.boolean().optional().default(false),
pinIconUrl: z
.string()
.optional()
.default('https://geoservices.wallonie.be/geoviewer-api/4.10/spw/widgets/images/gsv_graphic.png'),
})
.prefault({}),
});
export type StreetviewConfig = z.infer<typeof streetViewConfigSchema>;

packages/common/src/lib/widgets/streetview/streetview.models.ts

packages/common/src/lib/widgets/streetview/streetview.models.ts
export interface IconRendering {
heading: number;
x?: number;
y?: number;
}

packages/common/src/lib/widgets/streetview/StreetView.svelte

packages/common/src/lib/widgets/streetview/StreetView.svelte
<script lang="ts">
import { Loader as GoogleLoader } from '@googlemaps/js-api-loader';
import { getMapManager } from '$lib/api/map';
import { onDestroy, onMount } from 'svelte';
import { Projections } from '$lib/api/managers/projection';
import type { ApiPoint } from '$lib/api/geometry';
import type { Unsubscriber } from 'svelte/store';
import { initGraphicMapServiceConfiguration, MapServiceTypes } from '$lib/api/managers/configuration';
import StringUtils from '$lib/api/utils/string.utils';
import { createPicturePointSymbol } from '$lib/api/symbol';
import Loader from '$lib/components/common/Loader.svelte';
import ConfirmDialog from '$lib/components/confirm-dialog/ConfirmDialog.svelte';
import { getI18n } from '$lib/api/managers/i18n';
import { GeoviewerError } from '$lib/api/managers/error/geoviewer-error.model';
import type { ApiBasemap } from '$lib/api/mapservices';
import { Checkbox } from '$lib/components/shadcn/ui/checkbox';
import { Label } from '$lib/components/shadcn/ui/label';
import type { IconRendering } from './streetview.models';
import type { StreetViewProps } from './streetview.declaration';
let { fullConfig, reference }: StreetViewProps = $props();
const { config } = fullConfig;
const i18n = getI18n(fullConfig.i18n);
const mapManager = getMapManager();
const featureFactory = mapManager.tools.featureFactory;
const zoom = mapManager.tools.zoom;
const transform = mapManager.tools.transform;
const scale = mapManager.tools.scale;
const dragAndDrop = mapManager.tools.dragAndDrop;
const basemapList = mapManager.basemapList;
const iconRenderingState = $state<IconRendering>({
heading: 0,
x: undefined,
y: undefined,
});
const googleLoader = new GoogleLoader({
apiKey: config.apiKey,
version: 'weekly',
});
const graphicMapService = mapManager.addGraphicMapService(
initGraphicMapServiceConfiguration({
id: StringUtils.uuid(),
label: 'StreetviewPinMapService',
toc: {
visible: false,
},
}),
);
const dragAndDropUnsubscriber = dragAndDrop.on('dragEnd', graphicMapService, (newPosition) => {
updatePanoramaLocation(newPosition);
});
const rememberMyChoiceLocalStorageKey = 'G5-StreetView-RememberMyChoice';
const rememberMyChoiceValue = 'true';
let panoramaInstance = $state<google.maps.StreetViewPanorama | undefined>();
let streetViewService: google.maps.StreetViewService | undefined;
let panoramaAnchorElement: HTMLElement;
let onClickUnsubscriber: Unsubscriber | undefined;
let confirmDialogOpen = $state<boolean>(false);
let previousBaseMap: ApiBasemap | undefined;
let rememberMyChoice = $state<boolean>(false);
let scaleAtOpening = scale.getCurrentScaleInfo();
let currentScale = 0;
const scaleListenerUnsub = mapManager.tools.events.watch('SCALE', (v) => {
currentScale = v;
});
$effect(() => {
if (iconRenderingState) {
renderPin();
}
});
async function onConfirm() {
await switchToGoogleBaseMap();
await startStreetview();
}
async function switchToGoogleBaseMap() {
if (rememberMyChoice) {
localStorage.setItem(rememberMyChoiceLocalStorageKey, rememberMyChoiceValue);
}
previousBaseMap = basemapList.selected;
const googleBaseMap = findGoogleBaseMap();
if (!googleBaseMap) {
throw new GeoviewerError(
`Unable to find Google basemap with ID ${config.googleBaseMapId} or any other basemap containing a Google map service`,
);
}
basemapList.select(googleBaseMap);
}
async function startStreetview() {
await initializePanorama();
onClickUnsubscriber = mapManager.tools.events.on('click', (point) => updatePanoramaLocation(point));
}
async function updatePanoramaLocation(point: ApiPoint, radius?: number) {
if (!panoramaInstance || !streetViewService) return;
const { x, y } = transform.transformPoint(point, Projections.WGS_1984.wkid);
await streetViewService.getPanorama(
{
location: { lat: y, lng: x },
radius: radius ?? config.closestLocationRadius,
},
(data, status) => {
if (data && status === google.maps.StreetViewStatus.OK && data.location) {
const closestLocation = data.location.latLng;
if (closestLocation) {
panoramaInstance!.setPosition(closestLocation);
iconRenderingState.x = closestLocation.lng();
iconRenderingState.y = closestLocation.lat();
}
} else {
console.log('No Street View panorama found within the specified radius.');
}
},
);
}
function renderPin() {
graphicMapService.removeAll();
const { heading, x, y } = iconRenderingState;
if (x === undefined || y === undefined) {
return;
}
const symbol = createPicturePointSymbol({
source: config.pinIconUrl,
rotation: config.enablePinIconRotation ? 180 + heading : 0,
});
graphicMapService.addFeature(
featureFactory.createPoint({ x, y, wkid: Projections.WGS_1984.wkid, symbol: symbol }),
);
}
async function initializePanorama() {
if (panoramaInstance) return;
const { StreetViewPanorama, StreetViewService } = await googleLoader.importLibrary('streetView');
streetViewService = new StreetViewService();
const apiPoint = transform.transformPoint(mapManager.getMapCenter(), Projections.WGS_1984.wkid);
panoramaInstance = new StreetViewPanorama(panoramaAnchorElement, {
position: { lat: apiPoint.y, lng: apiPoint.x },
pov: {
heading: 0,
pitch: 0,
},
zoom: 1,
});
if (panoramaAnchorElement.parentElement) {
panoramaAnchorElement.parentElement.style.height = '100%';
}
panoramaInstance.addListener('position_changed', () => {
const newPosition = panoramaInstance!.getPosition();
if (newPosition) {
const { lat, lng } = newPosition.toJSON();
if (iconRenderingState.x === undefined && iconRenderingState.y === undefined) {
// At first initialization, we zoom on the pin, as it could be out of current extent
zoom.zoomToPoint({ x: lng, y: lat, wkid: Projections.WGS_1984.wkid });
}
iconRenderingState.x = lng;
iconRenderingState.y = lat;
}
});
panoramaInstance.addListener('pov_changed', () => {
iconRenderingState.heading = panoramaInstance!.getPov().heading;
});
await updatePanoramaLocation(apiPoint, config.initRadius);
if (scaleAtOpening) {
scale.setScale(scaleAtOpening.scaleForDevice);
}
}
function closeWidget() {
reference.deactivate();
}
function findGoogleBaseMap() {
return (
basemapList.list.find((x) => x.id === config.googleBaseMapId) ??
basemapList.list.find((x) => x.services.some((s) => s.type === MapServiceTypes.GOOGLE))
);
}
function isGoogleBasemap() {
return (
basemapList.selected?.id === config.googleBaseMapId ||
basemapList.selected?.services.find((x) => x.type === MapServiceTypes.GOOGLE) != undefined
);
}
onMount(async () => {
if (isGoogleBasemap()) {
await startStreetview();
return;
}
const rememberMyChoice = localStorage.getItem(rememberMyChoiceLocalStorageKey);
if (rememberMyChoice === rememberMyChoiceValue) {
await switchToGoogleBaseMap();
await startStreetview();
} else {
//FIXME: if no time out, the dialog is shown at the same time as the widget container opens,
// resulting in the dialog to move on the screen at first opening
setTimeout(() => {
confirmDialogOpen = true;
}, 1);
}
});
onDestroy(() => {
if (onClickUnsubscriber) {
onClickUnsubscriber();
}
mapManager.removeMapService(graphicMapService.id);
if (previousBaseMap) {
mapManager.basemapList.select(previousBaseMap);
scale.setScale(currentScale);
}
if (dragAndDropUnsubscriber) {
dragAndDropUnsubscriber();
}
scaleListenerUnsub();
});
</script>
{#if !panoramaInstance}
<Loader />
{/if}
<div bind:this={panoramaAnchorElement} class="gv-w-full gv-h-full"></div>
<ConfirmDialog
onCancel={closeWidget}
onClose={closeWidget}
{onConfirm}
title={i18n('common.warning')}
message={i18n('confirm-message')}
bind:open={confirmDialogOpen}
confirmDataTestId="StreetView-Confirm"
cancelDataTestId="StreetView-Cancel"
>
<div class="gv-flex gv-align-middle">
<Checkbox id="rememberMyChoice" bind:checked={rememberMyChoice} data-test-id="StreetView-RememberMe" />
<Label for="rememberMyChoice" class="gv-ml-2 gv-mt-0.5">
{i18n('common.remember')}
</Label>
</div>
</ConfirmDialog>

Aller plus loin