The StaticImageSource can display an image at an arbitrary extent. You can either pass a URL to the remote image, or provide a texture or an image to display.
index.js
import { AdditiveBlending, Mesh, MeshBasicMaterial, PlaneGeometry, Vector3,} from "three";import { MapControls } from "three/examples/jsm/controls/MapControls.js";import OSM from "ol/source/OSM.js";import Extent from "@giro3d/giro3d/core/geographic/Extent.js";import Instance from "@giro3d/giro3d/core/Instance.js";import Map from "@giro3d/giro3d/entities/Map.js";import ColorLayer from "@giro3d/giro3d/core/layer/ColorLayer.js";import TiledImageSource from "@giro3d/giro3d/sources/TiledImageSource.js";import Inspector from "@giro3d/giro3d/gui/Inspector.js";import StaticImageSource from "@giro3d/giro3d/sources/StaticImageSource.js";function bindTextInput(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 = () => { if (element.checkValidity()) { onChange(element.value); } }; const setValue = (v) => { element.value = v; onChange(element.value); }; const currentValue = element.value; return [setValue, currentValue, element];}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;}const extent = new Extent( "EPSG:3857", -20037508.342789244, 20037508.342789244, -20037508.342789244, 20037508.342789244,);const instance = new Instance({ target: "view", crs: extent.crs, backgroundColor: 0x0a3b59,});const map = new Map({ extent, backgroundColor: "white" });instance.add(map);// 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() }),});map.addLayer(osm);instance.view.camera.position.set(0, 0, 80000000);const controls = new MapControls(instance.view.camera, instance.domElement);controls.enableRotate = false;instance.view.setControls(controls);Inspector.attach("inspector", instance);let url = null;const extentPreview = new Mesh( new PlaneGeometry(1, 1, 1, 1), new MeshBasicMaterial({ color: "white", opacity: 0.1, transparent: true, blending: AdditiveBlending, depthTest: false, }),);instance.scene.add(extentPreview);let topLeftCorner;function drawExtent() { return new Promise((resolve) => { let clickCount = 0; const onMouseMove = (mouseEvent) => { if (topLeftCorner) { const picked = instance.pickObjectsAt(mouseEvent)[0]; if (picked) { const currentPoint = picked.point; const width = Math.abs(topLeftCorner.x - currentPoint.x); const height = Math.abs(topLeftCorner.y - currentPoint.y); extentPreview.scale.set(width, height, 1); const center = new Vector3().lerpVectors( currentPoint, topLeftCorner, 0.5, ); extentPreview.position.copy(center); extentPreview.updateMatrixWorld(true); instance.notifyChange(); } } }; const onClick = (mouseEvent) => { clickCount++; const picked = instance.pickObjectsAt(mouseEvent)[0]; if (picked) { controls.enabled = false; extentPreview.visible = true; const point = picked.point; if (clickCount === 1) { topLeftCorner = point; extentPreview.scale.set(0, 0, 1); } else if (clickCount === 2) { instance.domElement.removeEventListener("mousedown", onClick); instance.domElement.removeEventListener("mousemove", onMouseMove); topLeftCorner = null; const { x, y } = extentPreview.position; const scale = extentPreview.scale; controls.enabled = true; resolve( Extent.fromCenterAndSize( instance.referenceCrs, { x, y }, scale.x, scale.y, ), ); } } }; instance.domElement.addEventListener("mousedown", onClick); instance.domElement.addEventListener("mousemove", onMouseMove); });}let currentImage;const showErrorMessage = (show, message) => { const errorElement = document.getElementById("error"); if (show) { errorElement.innerText = `Failed to load remote image: ${message}`; errorElement.style.display = "block"; } else { errorElement.style.display = "none"; }};const startButton = bindButton("draw", (button) => { button.disabled = true; showErrorMessage(false); drawExtent().then((extent) => { if (currentImage) { map.removeLayer(currentImage, { disposeLayer: true }); } const source = new StaticImageSource({ extent, source: url, }); currentImage = new ColorLayer({ source }); source.addEventListener("loaded", () => (extentPreview.visible = false)); source.addEventListener("error", ({ error }) => { extentPreview.visible = false; showErrorMessage(true, error.message); }); map.addLayer(currentImage); instance.notifyChange(map); button.disabled = false; });});const [setCurrentUrl, currentUrl] = bindTextInput("url", (v) => { url = v; startButton.disabled = !url;});setCurrentUrl(currentUrl);