Display a globe-shaped Map.

Parameters

00:00
12:00
24:00
100% © OpenStreetMap contributors, © NASA, © Mapbox, Made with Natural Earth., SolarSystemsScope
index.js
/*
 * 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();
index.html
<!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>
package.json
{
    "name": "globe2",
    "dependencies": {
        "colormap": "^2.3.2",
        "@giro3d/giro3d": "1.0.0"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}
vite.config.js
import { defineConfig } from "vite";

export default defineConfig({
  build: {
    target: 'esnext',
  },
  optimizeDeps: {
    esbuildOptions: {
      target: 'esnext',
    },
  },
})