@here/harp-examples
Version:
harp.gl Examples
318 lines (273 loc) • 10.6 kB
text/typescript
/*
* 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);
}