UNPKG

@here/harp-examples

Version:
318 lines (273 loc) 10.6 kB
/* * Copyright (C) 2020-2021 HERE Europe B.V. * Licensed under Apache 2.0, see full license in LICENSE * SPDX-License-Identifier: Apache-2.0 */ import "three/examples/js/controls/TrackballControls"; import { Theme } from "@here/harp-datasource-protocol"; import { GeoCoordinates } from "@here/harp-geoutils"; import { MapControls, MapControlsUI } from "@here/harp-map-controls"; import { CopyrightElementHandler, MapView, MapViewEventNames } from "@here/harp-mapview"; import { VectorTileDataSource } from "@here/harp-vectortile-datasource"; import { GUI } from "dat.gui"; import * as THREE from "three"; import { apikey } from "../config"; const SunCalc = require("suncalc"); const FADE_DURATION = 30 * 60 * 1000; // in ms const COLOR_CHANGE_DURATION = 2 * FADE_DURATION; // in ms const TOTAL_FADE_DURATION = FADE_DURATION + COLOR_CHANGE_DURATION; const COLOR_INTENSITY_FACTOR = 1.5; const SUNRISE_COLOR = new THREE.Color("hsl(45, 100%, 75%)"); const SUNSET_COLOR = new THREE.Color("hsl(30, 100%, 60%)"); let map: MapView; let mapControls: MapControls; let trackball: any; let debugCamera: THREE.PerspectiveCamera; let directionalLightHelper: THREE.DirectionalLightHelper; let shadowCameraHelper: THREE.CameraHelper; let sun: THREE.DirectionalLight; let MAX_SUN_INTENSITY: number; const MAIN_SUN_COLOR = new THREE.Color(); const HERE = new GeoCoordinates(52.530932, 13.3849151); const date = new Date(); const guiOptions = { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate(), hours: date.getHours(), minutes: date.getMinutes(), time: date.getHours() + date.getMinutes() / 60, timeIndicator: `${date.getHours()}:${date.getMinutes()}`, debugCamera: false }; // Reference solar noon time is used to calculate time offsets at specific coordinates. const refSolarNoon = SunCalc.getTimes(date, 0, 0).solarNoon; // Main time offset. const refTime = refSolarNoon.getTime() + date.getTimezoneOffset() * 60 * 1000; function swapCamera() { mapControls.enabled = !mapControls.enabled; trackball.enabled = !trackball.enabled; map.pointOfView = mapControls.enabled ? undefined : debugCamera; directionalLightHelper.visible = !directionalLightHelper.visible; shadowCameraHelper.visible = !shadowCameraHelper.visible; } function setupDebugStuff() { const mapCameraHelper = new THREE.CameraHelper(map["m_rteCamera"]); mapCameraHelper.renderOrder = Number.MAX_SAFE_INTEGER; map.scene.add(mapCameraHelper); debugCamera = new THREE.PerspectiveCamera( map.camera.fov, map.canvas.width / map.canvas.height, 100, 100000 ); map.scene.add(debugCamera); debugCamera.position.set(6000, 2000, 1000); trackball = new (THREE as any).TrackballControls(debugCamera, map.canvas); trackball.enabled = false; trackball.addEventListener("start", () => { map.beginAnimation(); }); trackball.addEventListener("end", () => { map.endAnimation(); }); trackball.addEventListener("change", () => { map.update(); }); trackball.staticMoving = true; trackball.rotateSpeed = 3.0; trackball.zoomSpeed = 4.0; trackball.panSpeed = 2.0; directionalLightHelper = new THREE.DirectionalLightHelper(sun, 500); directionalLightHelper.visible = false; map.scene.add(directionalLightHelper); shadowCameraHelper = new THREE.CameraHelper(sun.shadow.camera); shadowCameraHelper.visible = false; map.scene.add(shadowCameraHelper); let lastZoomLevel = map.zoomLevel; map.addEventListener(MapViewEventNames.Render, () => { const trackballTarget = trackball.target as THREE.Vector3; if (lastZoomLevel !== map.zoomLevel) { trackballTarget.set(0, 0, -map.targetDistance); lastZoomLevel = map.zoomLevel; } trackball.update(); const enableCameraHelpers = map.pointOfView !== undefined; if (enableCameraHelpers) { mapCameraHelper.update(); } mapCameraHelper.visible = enableCameraHelpers; directionalLightHelper.update(); shadowCameraHelper.update(); }); } function update() { guiOptions.time = guiOptions.hours + guiOptions.minutes / 60; guiOptions.timeIndicator = `${guiOptions.hours}:${guiOptions.minutes}`; const { latitude, longitude } = map.geoCenter; const lightPos = sun.position; // Dirty time is a time without taking into account the time offset at the specific coordinates. const dirtyTime = new Date( guiOptions.year, guiOptions.month - 1, guiOptions.day, guiOptions.hours, guiOptions.minutes, 0 ); // Calculating time offset at current location. const timeOffset = SunCalc.getTimes(date, latitude, longitude).solarNoon.getTime() - refTime; // Time with corrected offset. const locationDate = new Date(dirtyTime.getTime() + timeOffset); const sunTimes = SunCalc.getTimes(locationDate, latitude, longitude); const sunPosition = SunCalc.getPosition(locationDate, latitude, longitude); const azimuth = sunPosition.azimuth; const altitude = sunPosition.altitude - Math.PI / 2; const r = map.targetDistance; lightPos.setX(r * Math.sin(altitude) * Math.sin(azimuth)); lightPos.setY(r * Math.sin(altitude) * Math.cos(azimuth)); lightPos.setZ(r * Math.cos(altitude) - r); // Resetting the target is important, because this is overriden in the MapView. // This is an ugly hack and HARP-10353 should improve this. sun.target.position.set(0, 0, -r); sun.color.set(MAIN_SUN_COLOR); const location_ms = locationDate.getTime(); const sunriseDiff = location_ms - sunTimes.sunriseEnd.getTime(); const sunsetDiff = sunTimes.sunsetStart.getTime() - location_ms; if (sunriseDiff > 0 && sunsetDiff > 0) { if (sunriseDiff < TOTAL_FADE_DURATION || sunsetDiff < TOTAL_FADE_DURATION) { let color: THREE.Color; let colorDiff: number; if (azimuth < 0) { color = SUNRISE_COLOR; colorDiff = sunriseDiff; } else { color = SUNSET_COLOR; colorDiff = sunsetDiff; } sun.color.lerpHSL( color, THREE.MathUtils.clamp(1 - (colorDiff - FADE_DURATION) / COLOR_CHANGE_DURATION, 0, 1) ); if (colorDiff <= FADE_DURATION) { sun.intensity = THREE.MathUtils.lerp( 0, MAX_SUN_INTENSITY * COLOR_INTENSITY_FACTOR, colorDiff / FADE_DURATION ); } else { sun.intensity = THREE.MathUtils.lerp( MAX_SUN_INTENSITY, MAX_SUN_INTENSITY * COLOR_INTENSITY_FACTOR, THREE.MathUtils.clamp( 1 - (colorDiff - FADE_DURATION) / COLOR_CHANGE_DURATION, 0, 1 ) ); } } else { sun.intensity = MAX_SUN_INTENSITY; } } else { sun.intensity = 0; } map.update(); } function initializeMapView(id: string, theme: Theme): MapView { const canvas = document.getElementById(id) as HTMLCanvasElement; map = new MapView({ canvas, theme, enableShadows: true }); map.renderLabels = false; map.fog.enabled = false; CopyrightElementHandler.install("copyrightNotice", map); mapControls = new MapControls(map); mapControls.maxTiltAngle = 50; const ui = new MapControlsUI(mapControls); canvas.parentElement!.appendChild(ui.domElement); map.lookAt({ target: HERE, zoomLevel: 17 }); map.resize(window.innerWidth, window.innerHeight); window.addEventListener("resize", () => { map.resize(window.innerWidth, window.innerHeight); }); addVectorTileDataSource().then(() => { const light = map.lights.find(item => item instanceof THREE.DirectionalLight) as | THREE.DirectionalLight | undefined; if (light === undefined) { throw new Error("Light for a sun was not found."); } sun = light; MAX_SUN_INTENSITY = sun.intensity; MAIN_SUN_COLOR.copy(sun.color); map.addEventListener(MapViewEventNames.MovementFinished, update); addGuiElements(); setupDebugStuff(); update(); }); return map; } const addVectorTileDataSource = (): Promise<void> => { const omvDataSource = new VectorTileDataSource({ baseUrl: "https://vector.hereapi.com/v2/vectortiles/base/mc", authenticationCode: apikey }); return map.addDataSource(omvDataSource); }; function addGuiElements() { // Control light direction const gui = new GUI({ width: 300 }); gui.add(guiOptions, "year").onChange(update); gui.add(guiOptions, "month").onChange(update); gui.add(guiOptions, "day").onChange(update); const timeSlider = gui.add(guiOptions, "time", 0, 24, 0.01); const timeIndicator = gui.add(guiOptions, "timeIndicator"); timeSlider.onChange(() => { guiOptions.hours = Math.floor(guiOptions.time); guiOptions.minutes = Math.floor((guiOptions.time - guiOptions.hours) * 60); update(); timeIndicator.updateDisplay(); }); timeIndicator.onChange(() => { const time = guiOptions.timeIndicator.split(":"); guiOptions.hours = parseInt(time[0], 10); guiOptions.minutes = parseInt(time[1], 10); update(); timeSlider.updateDisplay(); }); gui.add(guiOptions, "debugCamera").onChange(swapCamera); } export namespace RealTimeShadows { const theme: Theme = { extends: "resources/berlin_tilezen_base.json", lights: [ { type: "ambient", color: "#ffffff", name: "ambientLight", intensity: 0.9 }, { type: "directional", color: "#ffffff", name: "light1", intensity: 1, // Will be overriden immediately, see `update` direction: { x: 0, y: 0.01, z: -1 }, castShadow: true } ], definitions: { // Opaque buildings defaultBuildingColor: { value: "#EDE7E1FF" } } }; initializeMapView("mapCanvas", theme); }