Spaces:
Sleeping
Sleeping
| /** | |
| * @license | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| /** | |
| * @license | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| /** | |
| * Copyright 2024 Google LLC | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| import { Map3DCameraProps } from '@/components/map-3d'; | |
| import { lookAtWithPadding } from './look-at'; | |
| import { MapMarker, MapRectangularOverlay, useMapStore } from './state'; | |
| type MapControllerDependencies = { | |
| map: google.maps.maps3d.Map3DElement; | |
| maps3dLib: google.maps.Maps3DLibrary; | |
| elevationLib: google.maps.ElevationLibrary; | |
| }; | |
| /** | |
| * A controller class to centralize all interactions with the Google Maps 3D element. | |
| */ | |
| export class MapController { | |
| private map: google.maps.maps3d.Map3DElement; | |
| private maps3dLib: google.maps.Maps3DLibrary; | |
| private elevationLib: google.maps.ElevationLibrary; | |
| constructor(deps: MapControllerDependencies) { | |
| this.map = deps.map; | |
| this.maps3dLib = deps.maps3dLib; | |
| this.elevationLib = deps.elevationLib; | |
| } | |
| /** | |
| * Clears all child elements (like markers and overlays) from the map. | |
| */ | |
| clearMap() { | |
| this.map.innerHTML = ''; | |
| } | |
| /** | |
| * Adds a list of markers to the map. | |
| * @param markers - An array of marker data to be rendered. | |
| */ | |
| addMarkers(markers: MapMarker[]) { | |
| for (const markerData of markers) { | |
| const marker = new this.maps3dLib.Marker3DInteractiveElement({ | |
| position: markerData.position, | |
| altitudeMode: 'RELATIVE_TO_MESH', | |
| label: markerData.showLabel ? markerData.label : null, | |
| title: markerData.label, | |
| drawsWhenOccluded: true, | |
| }); | |
| // Make markers interactive | |
| marker.style.cursor = 'pointer'; | |
| marker.addEventListener('click', () => { | |
| // Prevent the main view from auto-framing all markers again. | |
| useMapStore.getState().setPreventAutoFrame(true); | |
| // Set a new camera target to fly to the clicked marker. | |
| useMapStore.getState().setCameraTarget({ | |
| center: { ...markerData.position, altitude: 200 }, | |
| range: 1000, // Zoom in for a close-up view | |
| tilt: 60, | |
| heading: this.map.heading, // Maintain the current camera heading | |
| roll: 0, | |
| }); | |
| }); | |
| this.map.appendChild(marker); | |
| } | |
| } | |
| /** | |
| * Adds rectangular overlays to the map. | |
| * @param overlays - An array of rectangular overlay data to be rendered. | |
| */ | |
| addRectangularOverlays(overlays: MapRectangularOverlay[]) { | |
| for (const overlayData of overlays) { | |
| const { center, corners, color, label } = overlayData; | |
| // Create corner markers for the rectangle | |
| const cornerPositions = [ | |
| corners.northEast, | |
| corners.northWest, | |
| corners.southEast, | |
| corners.southWest | |
| ]; | |
| // Create markers for each corner | |
| cornerPositions.forEach((corner, index) => { | |
| const cornerMarker = new this.maps3dLib.Marker3DInteractiveElement({ | |
| position: corner, | |
| altitudeMode: 'RELATIVE_TO_MESH', | |
| // label: `Corner ${index + 1}`, | |
| title: `Rectangle Corner ${index + 1}`, | |
| drawsWhenOccluded: true, | |
| }); | |
| // Style corner markers | |
| cornerMarker.style.cursor = 'pointer'; | |
| cornerMarker.style.color = color; | |
| cornerMarker.style.fontSize = '16px'; | |
| cornerMarker.style.fontWeight = 'bold'; | |
| this.map.appendChild(cornerMarker); | |
| }); | |
| // Create center marker | |
| const centerMarker = new this.maps3dLib.Marker3DInteractiveElement({ | |
| position: center, | |
| altitudeMode: 'RELATIVE_TO_MESH', | |
| label: label, | |
| title: label, | |
| drawsWhenOccluded: true, | |
| }); | |
| // Style the center marker to be more prominent | |
| centerMarker.style.cursor = 'pointer'; | |
| centerMarker.style.color = color; | |
| centerMarker.style.fontSize = '20px'; | |
| centerMarker.style.fontWeight = 'bold'; | |
| centerMarker.addEventListener('click', () => { | |
| useMapStore.getState().setPreventAutoFrame(true); | |
| useMapStore.getState().setCameraTarget({ | |
| center: { ...center, altitude: 500 }, | |
| range: Math.max(overlayData.width, overlayData.height) * 2, | |
| tilt: 45, | |
| heading: this.map.heading, | |
| roll: 0, | |
| }); | |
| }); | |
| this.map.appendChild(centerMarker); | |
| } | |
| } | |
| /** | |
| * Animate the camera to a specific set of camera properties. | |
| * @param cameraProps - The target camera position, range, tilt, etc. | |
| */ | |
| flyTo(cameraProps: Map3DCameraProps) { | |
| this.map.flyCameraTo({ | |
| durationMillis: 5000, | |
| endCamera: { | |
| center: { | |
| lat: cameraProps.center.lat, | |
| lng: cameraProps.center.lng, | |
| altitude: cameraProps.center.altitude, | |
| }, | |
| range: cameraProps.range, | |
| heading: cameraProps.heading, | |
| tilt: cameraProps.tilt, | |
| roll: cameraProps.roll, | |
| }, | |
| }); | |
| } | |
| /** | |
| * Calculates the optimal camera view to frame a set of entities and animates to it. | |
| * @param entities - An array of entities to frame (must have a `position` property). | |
| * @param padding - The padding to apply around the entities. | |
| */ | |
| async frameEntities( | |
| entities: { position: { lat: number; lng: number } }[], | |
| padding: [number, number, number, number], | |
| ) { | |
| if (entities.length === 0) return; | |
| const elevator = new this.elevationLib.ElevationService(); | |
| const cameraProps = await lookAtWithPadding( | |
| entities.map(e => e.position), | |
| elevator, | |
| 0, // heading | |
| padding, | |
| ); | |
| this.flyTo({ | |
| center: { | |
| lat: cameraProps.lat, | |
| lng: cameraProps.lng, | |
| altitude: cameraProps.altitude, | |
| }, | |
| range: cameraProps.range + 1000, // Add a bit of extra range | |
| heading: cameraProps.heading, | |
| tilt: cameraProps.tilt, | |
| roll: 0, | |
| }); | |
| } | |
| } |