Parameters
00:00
12:00
24:00
Display a globe-shaped Map.
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import { TopoJSON } from "ol/format.js";
import OSM from "ol/source/OSM.js";
import XYZ from "ol/source/XYZ.js";
import { Fill, Style } from "ol/style.js";
import {
AmbientLight,
Clock,
DirectionalLight,
DirectionalLightHelper,
Mesh,
MathUtils,
MeshBasicMaterial,
SphereGeometry,
Vector3,
} from "three";
import GlobeControls from "@giro3d/giro3d/controls/GlobeControls.js";
import ColorMap from "@giro3d/giro3d/core/ColorMap.js";
import CoordinateSystem from "@giro3d/giro3d/core/geographic/CoordinateSystem.js";
import Ellipsoid from "@giro3d/giro3d/core/geographic/Ellipsoid.js";
import Extent from "@giro3d/giro3d/core/geographic/Extent.js";
import Sun from "@giro3d/giro3d/core/geographic/Sun.js";
import Instance from "@giro3d/giro3d/core/Instance.js";
import BlendingMode from "@giro3d/giro3d/core/layer/BlendingMode.js";
import ColorLayer from "@giro3d/giro3d/core/layer/ColorLayer.js";
import ElevationLayer from "@giro3d/giro3d/core/layer/ElevationLayer.js";
import Atmosphere from "@giro3d/giro3d/entities/Atmosphere.js";
import Globe from "@giro3d/giro3d/entities/Globe.js";
import Glow from "@giro3d/giro3d/entities/Glow.js";
import SphericalPanorama from "@giro3d/giro3d/entities/SphericalPanorama.js";
import MapboxTerrainFormat from "@giro3d/giro3d/formats/MapboxTerrainFormat.js";
import GlobeControlsInspector from "@giro3d/giro3d/gui/GlobeControlsInspector.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import GeoTIFFSource from "@giro3d/giro3d/sources/GeoTIFFSource.js";
import StaticImageSource from "@giro3d/giro3d/sources/StaticImageSource.js";
import TiledImageSource from "@giro3d/giro3d/sources/TiledImageSource.js";
import VectorSource from "@giro3d/giro3d/sources/VectorSource.js";
function bindButton(id, onClick) {
const element = document.getElementById(id);
if (!(element instanceof HTMLButtonElement)) {
throw new Error(
"invalid binding element: expected HTMLButtonElement, got: " +
element.constructor.name,
);
}
element.onclick = () => {
onClick(element);
};
return element;
}
function bindColorPicker(id, onChange) {
const element = document.getElementById(id);
if (!(element instanceof HTMLInputElement)) {
throw new Error(
"invalid binding element: expected HTMLInputElement, got: " +
element.constructor.name,
);
}
element.oninput = function oninput() {
// Let's change the classification color with the color picker value
const hexColor = element.value;
onChange(new Color(hexColor));
};
const externalFunction = (v) => {
element.value = `#${new Color(v).getHexString()}`;
onChange(element.value);
};
return [externalFunction, new Color(element.value), element];
}
function bindDatePicker(id, onChange) {
const element = document.getElementById(id);
if (!(element instanceof HTMLInputElement)) {
throw new Error(
"invalid binding element: expected HTMLInputElement, got: " +
element.constructor.name,
);
}
element.onchange = () => {
onChange(new Date(element.value));
};
const callback = (v) => {
const clone = new Date(v.getTime());
v.setMinutes(v.getMinutes() - v.getTimezoneOffset());
element.value = clone.toISOString().slice(0, 10);
onChange(new Date(element.value));
};
return [callback, new Date(element.value), element];
}
function bindDropDown(id, onChange) {
const element = document.getElementById(id);
if (!(element instanceof HTMLSelectElement)) {
throw new Error(
"invalid binding element: expected HTMLSelectElement, got: " +
element.constructor.name,
);
}
element.onchange = () => {
onChange(element.value);
};
const callback = (v) => {
element.value = v;
onChange(element.value);
};
const setOptions = (options) => {
element.innerHTML = "";
options.forEach((opt) => {
const optElement = document.createElement("option");
optElement.value = opt.id;
optElement.selected = opt.selected;
optElement.textContent = opt.name;
element.appendChild(optElement);
});
};
return [callback, element.value, element, setOptions];
}
function bindSlider(id, onChange) {
const element = document.getElementById(id);
if (!(element instanceof HTMLInputElement)) {
throw new Error(
"invalid binding element: expected HTMLInputElement, got: " +
element.constructor.name,
);
}
element.oninput = function oninput() {
onChange(element.valueAsNumber);
};
const setValue = (v, min, max, step) => {
if (min != null && max != null) {
element.min = min.toString();
element.max = max.toString();
if (step != null) {
element.step = step;
}
}
element.valueAsNumber = v;
onChange(element.valueAsNumber);
};
const initialValue = element.valueAsNumber;
return [setValue, initialValue, element];
}
function bindToggle(id, onChange) {
const element = document.getElementById(id);
if (!(element instanceof HTMLInputElement)) {
throw new Error(
"invalid binding element: expected HTMLButtonElement, got: " +
element.constructor.name,
);
}
element.oninput = function oninput() {
onChange(element.checked);
};
const callback = (v) => {
element.checked = v;
onChange(element.checked);
};
return [callback, element.checked, element];
}
function makeColorRamp(
preset,
discrete = false,
invert = false,
mirror = false,
) {
let nshades = discrete ? 10 : 256;
const values = colormap({ colormap: preset, nshades });
const colors = values.map((v) => new Color(v));
if (invert) {
colors.reverse();
}
if (mirror) {
const mirrored = [...colors, ...colors.reverse()];
return mirrored;
}
return colors;
}
function updateLabel(id, text) {
const element = document.getElementById(id);
if (!(element instanceof HTMLLabelElement)) {
throw new Error(
"invalid binding element: expected HTMLLabelElement, got: " +
element.constructor.name,
);
}
element.innerText = text;
}
import { CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer.js";
const instance = new Instance({
target: "view",
crs: CoordinateSystem.epsg4978,
backgroundColor: "black",
});
/////////////////////////////// Globe creations ///////////////////////////////////////////////////
const earth = new Globe({
name: "Earth",
lighting: {
enabled: true,
},
graticule: {
enabled: true,
color: "black",
xStep: 10, // In degrees
yStep: 10, // In degrees
xOffset: 0,
yOffset: 0,
opacity: 0.5,
thickness: 0.5, // In degrees
},
backgroundColor: "#001B35",
});
instance.add(earth);
const moon = new Globe({
name: "Moon",
lighting: {
enabled: true,
},
graticule: {
enabled: true,
color: "black",
xStep: 10, // In degrees
yStep: 10, // In degrees
xOffset: 0,
yOffset: 0,
opacity: 0.5,
thickness: 0.5, // In degrees
},
backgroundColor: "grey",
// For the moon we use a custom ellipsoid
ellipsoid: new Ellipsoid({
semiMajorAxis: 1_738_100,
semiMinorAxis: 1_736_000,
}),
});
instance.add(moon);
const moonLayer = new ColorLayer({
source: new GeoTIFFSource({
url: "https://3d.oslandia.com/giro3d/rasters/moon.tif",
crs: CoordinateSystem.epsg4326,
}),
});
moon.addLayer(moonLayer);
const mars = new Globe({
name: "Mars",
lighting: {
enabled: true,
},
graticule: {
enabled: true,
color: "black",
xStep: 10, // In degrees
yStep: 10, // In degrees
xOffset: 0,
yOffset: 0,
opacity: 0.5,
thickness: 0.5, // In degrees
},
backgroundColor: "#C64600",
// For Mars we use a custom ellipsoid
// See https://tharsis.gsfc.nasa.gov/geodesy.html
ellipsoid: new Ellipsoid({
semiMajorAxis: 3_396_200,
semiMinorAxis: 3_376_189,
}),
});
instance.add(mars);
const marsLayer = new ColorLayer({
source: new GeoTIFFSource({
// From https://www.solarsystemscope.com/textures/
url: "https://3d.oslandia.com/giro3d/rasters/8k_mars.tif",
crs: CoordinateSystem.epsg4326,
}),
});
mars.addLayer(marsLayer);
// The sun is so huge that it would be impractical
// to display it in its actual scale.
const SUN_SIZE_FACTOR = 0.1;
const sun = new Globe({
name: "Sun",
lighting: {
enabled: false,
},
graticule: {
enabled: true,
color: "black",
xStep: 10, // In degrees
yStep: 10, // In degrees
xOffset: 0,
yOffset: 0,
opacity: 0.5,
thickness: 0.5, // In degrees
},
backgroundColor: "grey",
// For the sun we use a spherical ellipsoid
ellipsoid: new Ellipsoid({
semiMajorAxis: 696_340_000 * SUN_SIZE_FACTOR,
semiMinorAxis: 696_340_000 * SUN_SIZE_FACTOR,
}),
});
instance.add(sun);
const sunLayer = new ColorLayer({
source: new GeoTIFFSource({
// From https://www.solarsystemscope.com/textures/
url: "https://3d.oslandia.com/giro3d/rasters/8k_sun.tif",
crs: CoordinateSystem.epsg4326,
}),
});
sun.addLayer(sunLayer);
const allGlobes = [earth, moon, mars, sun];
/////////////////////////////// Star background /////////////////////////////////////////////////
const background = new SphericalPanorama({
name: "background",
radius: 10_000_000,
subdivisionThreshold: 0.4,
maxSubdivisionLevel: 3,
depthTest: false,
});
background.renderOrder = -9999;
instance.add(background);
const starLayer = new ColorLayer({
source: new StaticImageSource({
source: "https://3d.oslandia.com/giro3d/images/4k_stars_milky_way.jpg",
extent: Extent.fullEquirectangularProjection,
}),
});
background.addLayer(starLayer);
/////////////////////////////// Earth layers ////////////////////////////////////////////////////
const mapboxApiKey =
"pk.eyJ1IjoiZ2lybzNkIiwiYSI6ImNtZ3Q0NDNlNTAwY2oybHI3Ym1kcW03YmoifQ.Zl7_KZiAhqWSPjlkKDKYnQ";
// Adds a XYZ elevation layer with MapBox terrain RGB tileset
const elevationLayer = new ElevationLayer({
name: "elevation",
preloadImages: true,
colorMap: new ColorMap({
colors: makeColorRamp("greens"),
min: -1500,
max: 6000,
}),
minmax: { min: -500, max: 8000 },
// We dont want the full resolution because the terrain
// mesh has a much lower resolution than the raster image
resolutionFactor: 1 / 8,
source: new TiledImageSource({
retries: 0,
format: new MapboxTerrainFormat(),
source: new XYZ({
url: `https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.pngraw?access_token=${mapboxApiKey}`,
projection: "EPSG:3857",
}),
}),
});
earth.addLayer(elevationLayer).catch(console.error);
const watermask = new ColorLayer({
name: "watermask",
source: new VectorSource({
dataProjection: CoordinateSystem.epsg4326,
data: {
url: "https://3d.oslandia.com/giro3d/vectors/water_mask.topojson",
format: new TopoJSON(),
},
style: new Style({
fill: new Fill({
color: "#22274a",
}),
}),
}),
});
earth.addLayer(watermask);
// Adds a XYZ color layer with MapBox satellite tileset
const satellite = new ColorLayer({
name: "satellite",
preloadImages: true,
source: new TiledImageSource({
source: new XYZ({
url: `https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.webp?access_token=${mapboxApiKey}`,
projection: "EPSG:3857",
crossOrigin: "anonymous",
}),
}),
});
earth.addLayer(satellite).catch((e) => console.error(e));
// Create the OpenStreetMap color layer using an OpenLayers source.
// See https://openlayers.org/en/latest/apidoc/module-ol_source_OSM-OSM.html
// for more informations.
const osm = new ColorLayer({
name: "OSM",
source: new TiledImageSource({ source: new OSM() }),
});
earth.addLayer(osm).catch((e) => console.error(e));
const clouds = new ColorLayer({
name: "clouds",
blendingMode: BlendingMode.Add,
source: new StaticImageSource({
source: "https://3d.oslandia.com/giro3d/images/cloud_cover.webp",
extent: Extent.WGS84,
}),
});
earth.addLayer(clouds).catch(console.error);
/////////////////////////////// Lighting //////////////////////////////////////////////////////
// Let's add a sun in our scene
/* const sunlight = new DirectionalLight('white', 4);
sunlight.name = 'sun';
instance.add(sunlight);
const sun_pos = new Vector3(80_000,80_000,80_000);
sunlight.position.copy(sun_pos);
instance.add(sunlight.target);
sunlight.target.position.set(0, 0, 0);
sunlight.target.updateMatrixWorld(true);
sunlight.updateMatrixWorld(true);
sunlight.updateMatrixWorld(true);
*/
///////////
const DEFAULT_PARAMS = {
automaticSunRotation: true,
fov: 30,
redWavelength: 0.65,
greenWavelength: 0.57,
blueWavelength: 0.475,
thickness: 300_000,
globeColor: "#1e4485",
showSunObject: true,
sunPosition: new Vector3(0.2, 1, 0),
inner: true,
lookAtSun: false,
outer: true,
showSunMarker: false,
showEllipsoidHelper: false,
//observer: new Coordinates(CoordinateSystem.epsg4326, 40, 25, 36_000_000),
//target: new Coordinates(CoordinateSystem.epsg4326, 0, 0, 0),
};
let params = { ...DEFAULT_PARAMS };
const sun1 = new Mesh(
new SphereGeometry(earth.ellipsoid.semiMajorAxis * 0.02),
new MeshBasicMaterial({ color: "yellow" }),
);
const elt = document.createElement("span");
elt.style.width = "15px";
elt.style.height = "15px";
elt.style.backgroundColor = "cyan";
elt.style.display = "inline-block";
elt.style.borderRadius = "50%";
elt.style.borderWidth = "2px";
elt.style.borderStyle = "solid";
elt.style.borderColor = "black";
const sunCSSMarker = new CSS2DObject(elt);
sun1.add(sunCSSMarker);
sun1.name = "Sun";
instance.add(sun1);
const sunlight = new DirectionalLight();
instance.add(sunlight);
instance.add(sunlight.target);
const apparentSunCourseRadius = earth.ellipsoid.semiMajorAxis * 2;
const actualSunCourseRAdius = earth.ellipsoid.semiMajorAxis * 200;
const clock = new Clock();
let time = 0;
const actualSunPosition = new Vector3(0, 0, 0);
const updateSunPosition = () => {
requestAnimationFrame(updateSunPosition);
if (!params.automaticSunRotation) {
clock.stop();
return;
}
if (!clock.running) {
clock.start();
}
const speed = -0.05; //-1
time += clock.getDelta();
const t = speed * time;
const cosT = Math.cos(t);
const sinT = Math.sin(t);
const x = cosT * apparentSunCourseRadius;
const y = sinT * apparentSunCourseRadius;
actualSunPosition.setX(cosT * actualSunCourseRAdius);
actualSunPosition.setY(sinT * actualSunCourseRAdius);
sun1.position.set(x, y, 0);
sun1.material.visible = params.showSunObject;
sun1.updateMatrixWorld(true);
sunlight.position.copy(sun1.position);
sunlight.lookAt(earth.object3d.position);
sunlight.updateMatrixWorld(true);
//skyDome.setSunPosition(sun1.position);
if (params.lookAtSun) {
instance.view.camera.lookAt(actualSunPosition);
}
instance.notifyChange();
};
updateSunPosition();
////////////////////////
const ambientLight = new AmbientLight("white", 0.1);
instance.add(ambientLight);
/////////////////////////////// Atmospheres //////////////////////////////////////////////////
const earthAtmosphere = new Atmosphere({
name: "Earth atmosphere",
ellipsoid: earth.ellipsoid,
});
instance.add(earthAtmosphere);
const marsAtmosphere = new Atmosphere({
name: "Mars atmosphere",
ellipsoid: mars.ellipsoid,
wavelengths: [0.414, 0.443, 0.475], // To give the atmosphere the rusty color of Mars
});
instance.add(marsAtmosphere);
// For the sun we don't use an atmosphere, but a glow
const sunGlow = new Glow({
name: "sun glow",
color: "#ff7800",
ellipsoid: sun.ellipsoid,
});
instance.add(sunGlow);
/////////////////////////////// Camera & controls ///////////////////////////////////////////
const defaultCameraPosition = new Vector3(
35_785_000 + Ellipsoid.WGS84.semiMajorAxis,
0,
0,
);
// Geostationary orbit at 36,000 km
instance.view.camera.position.copy(defaultCameraPosition);
instance.view.camera.lookAt(new Vector3(0, 0, 0));
let controls;
const updateControls = () => {
if (controls) {
controls.update();
instance.notifyChange(allGlobes);
}
requestAnimationFrame(updateControls);
};
updateControls();
/////////////////////////////// Example GUI bindings ///////////////////////////////////////////
const [setGraticule] = bindToggle("graticule", (enabled) => {
allGlobes.forEach((g) => (g.graticule.enabled = enabled));
instance.notifyChange(allGlobes);
});
setGraticule(earth.graticule.enabled);
const [setAtmosphere] = bindToggle("atmosphere", (enabled) => {
earthAtmosphere.visible = enabled && earth.visible;
marsAtmosphere.visible = enabled && mars.visible;
sunGlow.visible = enabled && sun.visible;
instance.notifyChange([earthAtmosphere, marsAtmosphere, sunGlow]);
});
let isRotating = true;
const [setRotation] = bindToggle("earth-rotate", (enabled) => {
isRotating = enabled;
//marsAtmosphere.visible = enabled && mars.visible;
//sunGlow.visible = enabled && sun.visible;
instance.notifyChange();
});
const getActiveGlobe = () => {
return allGlobes.find((g) => g.visible);
};
function update() {
const globe = getActiveGlobe();
if (globe == null) {
return;
}
const { x, y, z } = instance.view.camera.position;
let altitude = globe.ellipsoid.toGeodetic(x, y, z).altitude;
altitude = MathUtils.clamp(altitude, 2, +Infinity);
// Let's adjust the graticule step and thickness so that
// it more or less always look the same when altitude changes.
if (earth.graticule.enabled) {
let step = 0;
if (altitude > 10_000_000) {
step = 10;
} else if (altitude > 3_000_000) {
step = 5;
} else if (altitude > 1_000_000) {
step = 2;
} else if (altitude > 500_000) {
step = 1;
} else {
step = 0.5;
}
const thickness = MathUtils.mapLinear(
altitude,
200,
39_000_000,
0.002,
0.9,
);
earth.graticule.xStep = step;
earth.graticule.yStep = step;
earth.graticule.thickness = thickness;
}
// Let's make the clouds transparent when we zoom in.
const opacity = MathUtils.mapLinear(altitude, 12_000_000, 30_000_000, 0, 1);
clouds.opacity = MathUtils.clamp(opacity, 0, 1);
earthAtmosphere.opacity = clouds.opacity;
// Let's increase the shading on the terrain when we zoom out
const zFactor = MathUtils.mapLinear(altitude, 12_000_000, 30_000_000, 1, 10);
earth.lighting.zFactor = MathUtils.clamp(zFactor, 1, 10);
background.object3d.position.set(x, y, z);
background.object3d.updateMatrixWorld(true);
instance.notifyChange(background);
}
update();
const updateColorMap = () => {
const minmax = earth.getElevationMinMaxForVisibleTiles();
if (minmax != null && isFinite(minmax.min) && isFinite(minmax.max)) {
const colorMap = elevationLayer.colorMap;
colorMap.min = MathUtils.lerp(minmax.min, colorMap.min, 0.8);
colorMap.max = MathUtils.lerp(minmax.max, colorMap.max, 0.8);
instance.notifyChange(elevationLayer);
}
};
setInterval(updateColorMap, 50);
instance.addEventListener("after-camera-update", update);
const [setLighting] = bindToggle("lighting", (enabled) => {
earth.lighting.enabled = enabled;
document.getElementById("lightingParams").style.display = enabled
? "block"
: "none";
instance.notifyChange(earth);
});
/*
const sunParams = {
latitude: 9,
longitude: -41,
};
const updateSunDirection = (latitude, longitude) => {
const position = Ellipsoid.WGS84.toCartesian(
sunParams.latitude,
sunParams.longitude,
50_000_000,
);
sunlight.position.copy(position);
sunlight.target.position.set(0, 0, 0);
sunlight.target.updateMatrixWorld(true);
sunlight.updateMatrixWorld(true);
const normal = Ellipsoid.WGS84.getNormal(sunParams.latitude, sunParams.longitude);
earthAtmosphere.setSunPosition(position);
marsAtmosphere.setSunPosition(position);
};
const [setSunLatitude] = bindSlider('sunLatitude', lat => {
sunParams.latitude = lat;
updateSunDirection(sunParams.latitude, sunParams.longitude);
updateLabel('sunLatitudeLabel', `Lat: ${Math.round(Math.abs(lat))}° ${lat >= 0 ? 'N' : 'S'}`);
});
const [setSunLongitude] = bindSlider('sunLongitude', lon => {
sunParams.longitude = lon;
updateSunDirection(sunParams.latitude, sunParams.longitude);
updateLabel('sunLongitudeLabel', `Lon: ${Math.round(Math.abs(lon))} ${lon >= 0 ? 'E' : 'W'}°`);
});
const [setLighting] = bindToggle('lighting', enabled => {
earth.lighting.enabled = enabled;
document.getElementById('lightingParams').style.display = enabled ? 'block' : 'none';
instance.notifyChange(earth);
});
function setSunPosition(date) {
const sunPosition = Sun.getGeographicPosition(date);
setSunLongitude(sunPosition.longitude);
setSunLatitude(sunPosition.latitude);
}
let date = new Date();
const [setDate] = bindDatePicker('date', newDate => {
setSunPosition(newDate);
});
const [setTime] = bindSlider('time', seconds => {
const h = seconds / 3600;
const wholeH = Math.floor(h);
const m = (h - wholeH) * 60;
const wholeM = Math.floor(m);
date.setUTCHours(wholeH, wholeM);
setSunPosition(date);
document.getElementById('timeLabel').innerText =
`${wholeH.toString().padStart(2, '0')}:${wholeM.toString().padStart(2, '0')} UTC`;
});
const setCurrentDate = newDate => {
setSunPosition(newDate);
setDate(newDate);
setTime(newDate.getUTCHours() * 3600 + newDate.getUTCMinutes() * 60 + newDate.getUTCSeconds());
};
bindButton('now', () => {
date = new Date();
setCurrentDate(date);
});
const [setSunPositionMode] = bindDropDown('sun-position-mode', newMode => {
const datePicker = document.getElementById('date-picker');
const locationPicker = document.getElementById('sun-location');
const timeSlider = document.getElementById('timeContainer');
datePicker.style.display = 'none';
locationPicker.style.display = 'none';
timeSlider.style.display = 'none';
switch (newMode) {
case 'custom-date':
datePicker.style.display = 'block';
timeSlider.style.display = 'block';
break;
case 'custom-location':
locationPicker.style.display = 'block';
break;
}
});
*/
const [setGraticuleColor] = bindColorPicker("graticule-color", (color) => {
allGlobes.forEach((g) => (g.graticule.color = color));
instance.notifyChange(allGlobes);
});
function setLayers(...name) {
for (const layer of earth.getLayers()) {
layer.visible = name.includes(layer.name);
}
}
const [setAmbientIntensity] = bindSlider("ambientIntensity", (intensity) => {
ambientLight.intensity = intensity;
instance.notifyChange();
});
const [setSunIntensity] = bindSlider("sunIntensity", (intensity) => {
sunlight.intensity = intensity;
instance.notifyChange();
});
const [setGlobe] = bindDropDown("globe-selector", (globe) => {
allGlobes.forEach((g) => (g.visible = false));
let entity;
switch (globe) {
case "moon":
moon.visible = true;
entity = moon;
break;
case "sun":
sun.visible = true;
entity = sun;
break;
case "earth":
earth.visible = true;
entity = earth;
break;
case "mars":
mars.visible = true;
entity = mars;
break;
}
controls?.dispose();
instance.view.goTo(entity);
document.getElementById("earth-params").style.display = earth.visible
? "block"
: "none";
document.getElementById("lightingGroup").style.display = sun.visible
? "none"
: "block";
document.getElementById("sunParams").style.display = sun.visible
? "none"
: "block";
earthAtmosphere.visible = earth.visible;
marsAtmosphere.visible = mars.visible;
sunGlow.visible = sun.visible;
instance.notifyChange(entity);
controls = new GlobeControls({
scene: entity.object3d,
ellipsoid: entity.ellipsoid,
camera: instance.view.camera,
domElement: instance.domElement,
});
});
const reset = () => {
setGlobe("earth"); // TODO
setLayers("satellite", "clouds");
setAtmosphere(true);
setGraticule(false);
setRotation(true);
setGraticuleColor(0x000000);
//setSunLatitude(9);
//setSunLongitude(-41);
setAmbientIntensity(0.4);
setSunIntensity(4);
setLighting(true);
//setSunPositionMode('custom-location');
instance.view.camera.position.copy(defaultCameraPosition);
instance.view.camera.lookAt(new Vector3(0, 0, 0));
populateLayerList();
};
bindButton("reset", reset);
function populateLayerList() {
const list = document.getElementById("layer-list");
list.innerHTML = "";
const entries = [
`<li class="list-group-item list-group-item-secondary">Layers</li>`,
];
const createEntry = (name, visible) => {
const entry = `
<li class="list-group-item">
<input id="layer-${name}" class="form-check-input me-1" ${visible ? "checked" : ""} type="checkbox" />
<label class="form-check-label" for="layer-${name}">${name}</label>
</li>
`;
entries.push(entry);
};
for (const layer of earth.getColorLayers().reverse()) {
createEntry(layer.name, layer.visible);
}
for (const layer of earth.getElevationLayers()) {
createEntry(layer.name, layer.visible);
}
list.innerHTML = entries.join("\n");
for (const layer of earth.getLayers()) {
bindToggle(`layer-${layer.name}`, (visible) => {
layer.visible = visible;
instance.notifyChange(earth);
});
}
}
reset();
const inspector = Inspector.attach("inspector", instance);
inspector.addPanel(
new GlobeControlsInspector(inspector.gui, instance, controls),
);
function animate() {
requestAnimationFrame(animate);
if (isRotating) {
earth.object3d.rotation.z += 0.001;
}
controls.update();
instance.render();
}
animate();
<!doctype html>
<html lang="en">
<head>
<title>Globe</title>
<meta charset="UTF-8" />
<meta name="name" content="globe2" />
<meta name="description" content="Display a globe-shaped Map." />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="https://giro3d.org/images/favicon.svg" />
<link
href="https://giro3d.org/assets/bootstrap-custom.css"
rel="stylesheet"
/>
<script src="https://giro3d.org/assets/bootstrap.bundle.min.js"></script>
<link
rel="stylesheet"
type="text/css"
href="https://giro3d.org/latest/examples/css/example.css"
/>
</head>
<body>
<div id="view" class="m-0 p-0 w-100 h-100"></div>
<div
id="inspector"
class="position-absolute top-0 start-0 mh-100 overflow-auto"
></div>
<div class="side-pane-with-status-bar" style="width: 20rem">
<!--Parameters -->
<div class="card">
<div class="card-header">
Parameters
<button
type="button"
id="reset"
class="btn btn-sm btn-primary rounded float-end"
>
reset
</button>
</div>
<div class="card-body" id="top-options">
<!-- Earth/Moon selector -->
<div class="input-group mb-3">
<label class="input-group-text" for="globe-selector">Globe</label>
<select class="form-select" id="globe-selector" autocomplete="off">
<option value="earth" selected>Earth</option>
<option value="moon">Moon</option>
<option value="mars">Mars</option>
<option value="sun">Sun</option>
</select>
</div>
<div id="earth-params">
<ul class="list-group mb-3" id="layer-list">
<!-- Content of this list is generated by the example code -->
<li class="list-group-item">
<input class="form-check-input me-1" type="checkbox" />
<label class="form-check-label" for="firstCheckbox"
>Layer 1</label
>
</li>
</ul>
</div>
<!-- Atmosphere -->
<div class="form-check form-switch mb-1">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="atmosphere"
autocomplete="off"
/>
<label class="form-check-label" for="atmosphere">Atmosphere</label>
</div>
<!-- Rotation -->
<div class="form-check form-switch mb-1">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="earth-rotate"
autocomplete="off"
/>
<label class="form-check-label" for="earth-rotate">Rotation</label>
</div>
<!-- Toggle graticule -->
<div class="form-check form-switch mb-1">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="graticule"
autocomplete="off"
/>
<label class="form-check-label w-100" for="graticule">
<label class="form-check-label w-100" for="graticule">
<div class="row">
<div class="col">Graticule</div>
<div class="col-auto">
<input
type="color"
style="height: 1.5rem"
class="form-control form-control-color float-end"
id="graticule-color"
value="#000000"
title="Graticule color"
/>
</div>
</div> </label
></label>
</div>
<div id="lightingGroup">
<!-- Toggle lighting -->
<div class="form-check form-switch mb-1">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="lighting"
autocomplete="off"
/>
<label class="form-check-label" for="lighting">Lighting</label>
</div>
<div id="lightingParams">
<!-- Sun intensity -->
<div class="row">
<div class="col">
<label
id="sunIntensityLabel"
for="sunIntensity"
class="form-label"
>Sun intensity</label
>
</div>
<div class="col">
<input
type="range"
min="0"
max="10"
step="0.1"
value="4"
class="form-range"
id="sunIntensity"
autocomplete="off"
/>
</div>
</div>
<!-- Ambient intensity -->
<div class="row">
<div class="col">
<label
id="ambientIntensityLabel"
for="ambientIntensity"
class="form-label"
>Ambient light intensity</label
>
</div>
<div class="col">
<input
type="range"
min="0"
max="3"
step="0.1"
value="0.3"
class="form-range"
id="ambientIntensity"
autocomplete="off"
/>
</div>
</div>
</div>
</div>
<div id="sunParams">
<hr />
<!-- Sun position mode -->
<div class="input-group">
<label class="input-group-text" for="sun-position-mode"
>Sun position</label
>
<select
class="form-select"
id="sun-position-mode"
autocomplete="off"
>
<option value="custom-location" selected>By location</option>
<option value="custom-date">By date</option>
</select>
</div>
<!-- Date -->
<div id="date-picker">
<div class="input-group mt-3">
<label class="input-group-text" for="date">Date</label>
<input
class="form-control"
type="date"
id="date"
autocomplete="off"
/>
<div class="input-group-text">
<button class="btn btn-sm btn-primary" id="now">Now</button>
</div>
</div>
</div>
<div id="sun-location" class="mt-3">
<!-- Sun latitude slider -->
<div class="row 1">
<div class="col">
<label
id="sunLatitudeLabel"
for="sunLatitude"
class="form-label"
>Lat: 35° N</label
>
</div>
<div class="col">
<input
type="range"
min="-90"
max="90"
step="1"
value="35"
class="form-range"
id="sunLatitude"
autocomplete="off"
/>
</div>
</div>
<!-- Sun longitude -->
<div class="row">
<div class="col">
<label
id="sunLongitudeLabel"
for="sunLongitude"
class="form-label"
>Lat: 35° N</label
>
</div>
<div class="col">
<input
type="range"
min="-180"
max="180"
step="1"
value="9"
class="form-range"
id="sunLongitude"
autocomplete="off"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="bg-body border"
id="timeContainer"
style="
display: none;
position: absolute;
left: 0;
bottom: 1.3rem;
width: 100%;
height: 7rem;
padding: 1rem;
"
>
<!-- Background opacity slider -->
<label for="time" class="form-label"
><span id="timeLabel" class="badge rounded-pill text-bg-primary"
>12:00 UTC</span
>
</label>
<div class="input-group">
<input
type="range"
min="0"
max="86400"
step="60"
value="43200"
class="form-range"
id="time"
autocomplete="off"
/>
</div>
<div class="row">
<div class="col text-start">
<span id="timeLabel" class="badge rounded-pill text-bg-secondary"
>00:00</span
>
</div>
<div class="col text-center">
<span id="timeLabel" class="badge rounded-pill text-bg-secondary"
>12:00</span
>
</div>
<div class="col text-end">
<span id="timeLabel" class="badge rounded-pill text-bg-secondary"
>24:00</span
>
</div>
</div>
</div>
<script type="module" src="index.js"></script>
<script>
/* activate popovers */
const popoverTriggerList = [].slice.call(
document.querySelectorAll('[data-bs-toggle="popover"]'),
);
popoverTriggerList.map(
// bootstrap is used as script in the template, disable warning about undef
// eslint-disable-next-line no-undef
(popoverTriggerEl) =>
new bootstrap.Popover(popoverTriggerEl, {
trigger: "hover",
placement: "left",
content: document.getElementById(
popoverTriggerEl.getAttribute("data-bs-content"),
).innerHTML,
html: true,
}),
);
</script>
</body>
</html>
{
"name": "globe2",
"dependencies": {
"colormap": "^2.3.2",
"@giro3d/giro3d": "1.0.0"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}
import { defineConfig } from "vite";
export default defineConfig({
build: {
target: 'esnext',
},
optimizeDeps: {
esbuildOptions: {
target: 'esnext',
},
},
})