Reproject layers with heterogenous coordinate systems.

Parameters
?

Type a projected CRS in the field below, then "Update" to create a scene with various layers. You can search for projection codes on epsg.io. Note: not all projections are supported.

CRS

RGF93 v1 / Lambert-93 -- France
EPSG:2154

France - onshore and offshore, mainland and Corsica (France métropolitaine including Corsica).

See on epsg.io
100% © Mapbox, CRS data provided by epsg.io

A Layer can reproject the images produced by the ImageSource to conform to the CRS of the instance.

index.js
import XYZ from "ol/source/XYZ.js";
import { Stroke, Style } from "ol/style.js";
import { GeoJSON } from "ol/format.js";

import { MapControls } from "three/examples/jsm/controls/MapControls.js";

import Extent from "@giro3d/giro3d/core/geographic/Extent.js";
import Instance from "@giro3d/giro3d/core/Instance.js";
import TiledImageSource from "@giro3d/giro3d/sources/TiledImageSource.js";
import ColorLayer from "@giro3d/giro3d/core/layer/ColorLayer.js";
import Map from "@giro3d/giro3d/entities/Map.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import VectorSource from "@giro3d/giro3d/sources/VectorSource.js";
import GeoTIFFSource from "@giro3d/giro3d/sources/GeoTIFFSource.js";
import { crsToUnit } from "@giro3d/giro3d/core/geographic/Coordinates.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;
}

let instance;

let inspector;

let controls;

let map;

function addMapboxLayer(extent) {
  const apiKey =
    "pk.eyJ1IjoidG11Z3VldCIsImEiOiJjbGJ4dTNkOW0wYWx4M25ybWZ5YnpicHV6In0.KhDJ7W5N3d1z3ArrsDjX_A";

  // Adds a satellite basemap
  const tiledLayer = new ColorLayer({
    name: "basemap",
    extent,
    showTileBorders: true,
    source: new TiledImageSource({
      source: new XYZ({
        url: `https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.webp?access_token=${apiKey}`,
        projection: "EPSG:3857",
      }),
    }),
  });
  map.addLayer(tiledLayer).catch((e) => console.error(e));
}

function addCogLayer() {
  const cogLayer = new ColorLayer({
    name: "cog",
    showTileBorders: true,
    source: new GeoTIFFSource({
      url: "https://3d.oslandia.com/giro3d/rasters/TCI.tif",
      crs: "EPSG:3857",
    }),
  });
  map.addLayer(cogLayer).catch((e) => console.error(e));
}

function addVectorLayer() {
  const outlineStyle = new Style({
    stroke: new Stroke({ color: "red", width: 2 }),
  });

  // Display the countries boundaries.
  const boundaries = new ColorLayer({
    name: "boundaries",
    source: new VectorSource({
      data: {
        url: "https://3d.oslandia.com/giro3d/vectors/countries.geojson",
        format: new GeoJSON(),
      },
      style: outlineStyle,
      dataProjection: "EPSG:4326",
    }),
  });

  map.addLayer(boundaries).catch((e) => console.error(e));
}

function createScene(crs, crsDef, extent) {
  if (instance) {
    map.getLayers().forEach((l) => l.dispose());
    controls.dispose();
    inspector.detach();
    instance.dispose();
  }

  Instance.registerCRS(crs, crsDef);

  instance = new Instance({
    target: "view",
    crs,
    backgroundColor: "grey",
  });

  map = new Map({
    extent,
    segments: 2,
    backgroundColor: "black",
    backgroundOpacity: 0.3,
  });

  instance.add(map);

  addMapboxLayer(extent);

  addCogLayer();

  addVectorLayer();

  const center = extent.centerAsVector3();
  instance.view.camera.position.set(
    center.x,
    center.y - 1,
    extent.dimensions().y * 2,
  );

  controls = new MapControls(instance.view.camera, instance.domElement);
  controls.target = center;
  controls.saveState();
  controls.enableDamping = true;
  controls.dampingFactor = 0.2;
  controls.maxPolarAngle = Math.PI / 2.3;
  instance.view.setControls(controls);

  inspector = Inspector.attach("inspector", instance);
}

async function fetchCrsBbox(crs) {
  const code = crs.split(":")[1];
  const link = `https://epsg.io/${code}`;
  const url = `https://epsg.io/${code}.json?download=1`;
  const res = await fetch(url, { mode: "cors" });
  const json = await res.json();

  const bbox = json.bbox;
  const south = Number.parseFloat(bbox.south_latitude);
  const north = Number.parseFloat(bbox.north_latitude);
  const west = Number.parseFloat(bbox.west_longitude);
  const east = Number.parseFloat(bbox.east_longitude);

  const wgs84Extent = new Extent("EPSG:4326", {
    west,
    east,
    north,
    south,
  });

  document.getElementById("srid").innerText = json.name;
  document.getElementById("name").innerText = crs;
  document.getElementById("description").innerText = json.area;

  document.getElementById("link").href = link;

  if (crsToUnit(crs) === undefined) {
    // Unsupported projection
    throw new Error("unsupported projection (invalid units)");
  } else {
    return wgs84Extent.as(crs);
  }
}

async function fetchCrsDefinition(crs) {
  const code = crs.split(":")[1];
  const url = `https://epsg.io/${code}.proj4?download=1`;
  const res = await fetch(url, { mode: "cors" });
  const def = await res.text();

  Instance.registerCRS(crs, def);

  return def;
}

async function initialize(crs) {
  const error = document.getElementById("message");

  try {
    const def = await fetchCrsDefinition(crs);
    const extent = await fetchCrsBbox(crs);
    const proj = crs;
    error.style.display = "none";

    createScene(proj, def, extent);
  } catch (e) {
    error.style.display = "block";

    if (e instanceof Error) {
      error.innerText = e.message;
    } else {
      error.innerText = `An error occured while fetching CRS definition on epsg.io`;
    }
  }
}

bindButton("create", () => {
  const epsgCodeElt = document.getElementById("code");

  const content = epsgCodeElt.value;

  if (content) {
    initialize(content);
  }
});

initialize("EPSG:2154");
index.html
<!doctype html>
<html lang="en">
  <head>
    <title>Layer reprojection</title>
    <meta charset="UTF-8" />
    <meta name="name" content="layer_reprojection" />
    <meta
      name="description"
      content="Reproject layers with heterogenous coordinate systems."
    />
    <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/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: 22rem">
      <div class="mh-100 overflow-y-auto">
        <div class="card" id="currentCrsSection">
          <!-- CRS code -->
          <div class="card-header">Parameters</div>

          <!-- tooltip -->
          <span
            class="badge bg-secondary position-absolute top-0 end-0 m-2"
            data-bs-toggle="popover"
            data-bs-content="help"
            >?</span
          >
          <p class="card-text d-none" id="help">
            Type a projected CRS in the field below, then "Update" to create a
            scene with various layers. You can search for projection codes on
            <a target="_blank" href="https://epsg.io/?q=epsg">epsg.io</a>.
            <b>Note:</b> not all projections are supported.
          </p>

          <!-- CRS name and area -->
          <div class="card-body">
            <!-- CRS selector -->
            <div class="input-group">
              <span class="input-group-text" id="code-label">CRS</span>
              <input
                type="text"
                class="form-control"
                id="code"
                placeholder="EPSG code (e.g EPSG:3857)"
                value="EPSG:2154"
                aria-label="EpsgCode"
                autocomplete="off"
                aria-describedby="code-label"
              />
              <button class="input-group-text btn btn-primary" id="create">
                Update
              </button>
            </div>

            <hr />
            <h5 class="card-title" id="name">
              RGF93 v1 / Lambert-93 -- France
            </h5>
            <h6 class="card-subtitle text-body-secondary mb-2" id="srid">
              EPSG:2154
            </h6>
            <p class="card-text" id="description">
              France - onshore and offshore, mainland and Corsica (France
              métropolitaine including Corsica).
            </p>
            <a id="link" target="_blank">See on epsg.io</a>
          </div>
        </div>

        <!-- Error message -->
        <div
          class="alert alert-danger mt-3"
          id="message"
          style="display: none"
          role="alert"
        >
          A simple primary alert—check it out!
        </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": "layer_reprojection",
    "dependencies": {
        "@giro3d/giro3d": "0.39.0"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}