/** * @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, }); } }