The DrawTool class allows you to draw geometries, such as points, lines and polygons, on surfaces. A temporary geometry is displayed while you add points, then removed when you're finished. The DrawTool then returns a GeoJSON object of the created geometry.
index.js
import*as turf from'@turf/turf';import XYZ from'ol/source/XYZ.js';import { LineBasicMaterial, MeshBasicMaterial, PointsMaterial, ShapeUtils, Vector2, Vector3,} from'three';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 ElevationLayer from'@giro3d/giro3d/core/layer/ElevationLayer.js';import Map from'@giro3d/giro3d/entities/Map.js';import Inspector from'@giro3d/giro3d/gui/Inspector.js';import GeoTIFFFormat from'@giro3d/giro3d/formats/GeoTIFFFormat.js';import DrawTool, { DrawToolMode, DrawToolState } from'@giro3d/giro3d/interactions/DrawTool.js';import Drawing from'@giro3d/giro3d/interactions/Drawing.js';import DrawingCollection from'@giro3d/giro3d/entities/DrawingCollection.js';import Fetcher from'@giro3d/giro3d/utils/Fetcher';import TiledImageSource from'@giro3d/giro3d/sources/TiledImageSource.js';// Create some functions for measurementfunctiongetPerimeter(drawing) {if (drawing.coordinates.length<6) returnnull;let length =0;for (let i =0; i < drawing.coordinates.length/3-1; i +=1) { length +=newVector3( drawing.coordinates[i *3+0], drawing.coordinates[i *3+1], drawing.coordinates[i *3+2], ).distanceTo(newVector3( drawing.coordinates[(i +1) *3+0], drawing.coordinates[(i +1) *3+1], drawing.coordinates[(i +1) *3+2], ), ); }return length;}functiongetMinMaxAltitudes(drawing) {let min =+Infinity;let max =-Infinity;for (let i =0; i < drawing.coordinates.length/3; i +=1) { min = Math.min(min, drawing.coordinates[i *3+2]); max = Math.max(max, drawing.coordinates[i *3+2]); }return [min, max];}functiongetArea(drawing) {if (drawing.coordinates.length<6) returnnull;constlocalFlatCoords= drawing.localCoordinates;constlocalCoords=newArray(localFlatCoords.length/3);for (let i =0; i < localFlatCoords.length/3; i +=1) { localCoords[i] =newVector2(localFlatCoords[i *3+0], localFlatCoords[i *3+1]); }return Math.abs(ShapeUtils.area(localCoords));}// Initialize Giro3D (see tifftiles for more details)constx=-13602618.385789588;consty=5811042.273912458;constextent=newExtent('EPSG:3857', x -12000, x +13000, y -4000, y +26000);constinstance=newInstance(document.getElementById('viewerDiv'), { crs: extent.crs(), renderer: { clearColor: 0x0a3b59, },});constmap=newMap('planar', { extent, hillshading: true, discardNoData: true, backgroundColor: 'white',});instance.add(map);let footprint;/** * A function that will override the default intersection test for image sources (by default * performing intersection on extents, i.e rectangles). Here we want to exclude tiles that do not * intersect with the GeoJSON footprint of the dataset. * * @param{Extent}tileExtent The extent to test. */functioncustomIntersectionTest(tileExtent) {if (!footprint) {returntrue; }constcorners= [ [tileExtent.topLeft().x, tileExtent.topLeft().y], [tileExtent.topRight().x, tileExtent.topRight().y], [tileExtent.bottomRight().x, tileExtent.bottomRight().y], [tileExtent.bottomLeft().x, tileExtent.bottomLeft().y], ];constextentAsPolygon= turf.helpers.polygon([ [corners[0], corners[1], corners[2], corners[3], corners[0]], ]);constintersects= turf.booleanIntersects(turf.toWgs84(extentAsPolygon), footprint);return intersects;}Fetcher.json('data/MtStHelens-footprint.geojson') .then(geojson=> { footprint = turf.toWgs84(geojson);constsource=newTiledImageSource({ containsFn: customIntersectionTest, // Here we specify our custom intersection test source: newXYZ({ minZoom: 10, maxZoom: 16, url: 'https://3d.oslandia.com/dem/MtStHelens-tiles/{z}/{x}/{y}.tif', }), format: newGeoTIFFFormat(), }); map.addLayer(newElevationLayer({ name: 'osm', extent, source, }), ).catch(e=> console.error(e)); }) .catch(e=> console.error(e));constcenter= extent.centerAsVector3();instance.camera.camera3D.position.set(center.x, center.y -1, 50000);// Instanciates controls// Beware: we need to bind them to *instance.domElement* so we can interact over 2D labels!constcontrols=newMapControls(instance.camera.camera3D, instance.domElement);controls.target.copy(center);instance.useTHREEControls(controls);Inspector.attach(document.getElementById('panelDiv'), instance);// Instanciate drawtoolconstdrawToolOptions= { drawObject3DOptions: { minExtrudeDepth: 40, maxExtrudeDepth: 100, }, enableDragging: document.getElementById('dragging').value ==='0', splicingHitTolerance: document.getElementById('splicingtoleranceEnabled').checked?parseInt(document.getElementById('splicingtolerance').value, 10):undefined, minPoints: document.getElementById('minpointsEnabled').checked?parseInt(document.getElementById('minpoints').value, 10):undefined, maxPoints: document.getElementById('maxpointsEnabled').checked?parseInt(document.getElementById('maxpoints').value, 10):undefined, enableAddPointsOnEdit: document.getElementById('addpointsEnabled').checked, use3Dpoints: document.getElementById('pointsrendering').value ==='0', point2DFactory: point2DFactoryHighlighted,};constdrawTool=newDrawTool(instance, drawToolOptions);// Prevent drawing when the user is interacting (panning, etc.)controls.addEventListener('change', () => drawTool.pause());controls.addEventListener('end', () =>setTimeout(() => drawTool.continue(), 0));// Let's plug our buttons to the drawTool APIconstupdateDragging= () => { drawToolOptions.enableDragging = document.getElementById('dragging').value ==='0'; drawTool.setOptions(drawToolOptions);};constupdateSplicingTolerance= () => { drawToolOptions.splicingHitTolerance = document.getElementById('splicingtoleranceEnabled') .checked?parseInt(document.getElementById('splicingtolerance').value, 10):undefined; drawTool.setOptions(drawToolOptions);};constupdateMinpoints= () => { drawToolOptions.minPoints = document.getElementById('minpointsEnabled').checked?parseInt(document.getElementById('minpoints').value, 10):undefined; drawTool.setOptions(drawToolOptions);};constupdateMaxpoints= () => { drawToolOptions.maxPoints = document.getElementById('maxpointsEnabled').checked?parseInt(document.getElementById('maxpoints').value, 10):undefined; drawTool.setOptions(drawToolOptions);};constupdateAddPoints= () => { drawToolOptions.enableAddPointsOnEdit = document.getElementById('addpointsEnabled').checked; drawTool.setOptions(drawToolOptions);};document.getElementById('dragging').addEventListener('change', updateDragging);document .getElementById('splicingtoleranceEnabled') .addEventListener('change', updateSplicingTolerance);document.getElementById('splicingtolerance').addEventListener('change', updateSplicingTolerance);document.getElementById('minpointsEnabled').addEventListener('change', updateMinpoints);document.getElementById('minpoints').addEventListener('change', updateMinpoints);document.getElementById('maxpointsEnabled').addEventListener('change', updateMaxpoints);document.getElementById('maxpoints').addEventListener('change', updateMaxpoints);document.getElementById('addpointsEnabled').addEventListener('change', updateAddPoints);document.getElementById('addPoint').onclick= () => {if (drawTool.state !== DrawToolState.READY) {// We're already drawing, do something with the current drawingif (drawTool.mode === DrawToolMode.EDIT) drawTool.end();else drawTool.reset(); }// Display helpfor (constoof document.getElementsByClassName('helper')) { o.classList.add('d-none'); } document.getElementById('addPointHelper').classList.remove('d-none'); document.getElementById('options').setAttribute('disabled', true);// Start drawing! drawTool.start('MultiPoint');};document.getElementById('addLine').onclick= () => {if (drawTool.state !== DrawToolState.READY) {if (drawTool.mode === DrawToolMode.EDIT) drawTool.end();else drawTool.reset(); }for (constoof document.getElementsByClassName('helper')) { o.classList.add('d-none'); } document.getElementById('addLineHelper').classList.remove('d-none'); document.getElementById('options').setAttribute('disabled', true); drawTool.start('LineString');};document.getElementById('addPolygon').onclick= () => {if (drawTool.state !== DrawToolState.READY) {if (drawTool.mode === DrawToolMode.EDIT) drawTool.end();else drawTool.reset(); }for (constoof document.getElementsByClassName('helper')) { o.classList.add('d-none'); } document.getElementById('addPolygonHelper').classList.remove('d-none'); document.getElementById('options').setAttribute('disabled', true); drawTool.start('Polygon');};// Hide the help when we're done drawingdrawTool.addEventListener('end', () => {for (constoof document.getElementsByClassName('helper')) { o.classList.add('d-none'); } document.getElementById('mainHelper').classList.remove('d-none'); document.getElementById('options').removeAttribute('disabled');});// When we're done drawing, the drawTool removes the shape.// We want to keep it displayed so we can edit it.// We'll keep track of our shapes, so we can edit their rendering parameters// and optimize the picking on hover&click.constdrawEntity=newDrawingCollection();instance.add(drawEntity);// We'll use different materials for displaying drawn shapesconstdrawnFaceMaterial=newMeshBasicMaterial({ color: 0x433c73, opacity: 0.2,});constdrawnSideMaterial=newMeshBasicMaterial({ color: 0x433c73, opacity: 0.8,});constdrawnLineMaterial=newLineBasicMaterial({ color: 0x252140,});constdrawnPointMaterial=newPointsMaterial({ color: 0x433c73, size: 100,});// If using CSS2DRenderer for points, we define our own (optional) factoryfunctionpoint2DFactory(text) {constpt= document.createElement('div'); pt.style.position ='absolute'; pt.style.borderRadius ='50%'; pt.style.width ='28px'; pt.style.height ='28px'; pt.style.backgroundColor ='#433C73'; pt.style.color ='#ffffff'; pt.style.border ='2px solid #070607'; pt.style.fontSize ='14px'; pt.style.textAlign ='center'; pt.style.pointerEvents ='none'; pt.style.cursor ='pointer'; pt.innerText = text;return pt;}functionpoint2DFactoryHighlighted(text) {constpt= document.createElement('div'); pt.style.position ='absolute'; pt.style.borderRadius ='50%'; pt.style.width ='28px'; pt.style.height ='28px'; pt.style.backgroundColor ='#347330'; pt.style.color ='#ffffff'; pt.style.border ='2px solid #070607'; pt.style.fontSize ='14px'; pt.style.textAlign ='center'; pt.style.pointerEvents ='none'; pt.style.cursor ='pointer'; pt.innerText = text;return pt;}constupdatePointsRendering= () => {// Update existing drawingsfor (constoof drawEntity.children) {if (o.geometryType ==='MultiPoint') { o.use3Dpoints = document.getElementById('pointsrendering').value ==='0'; instance.notifyChange(o); } }};document.getElementById('pointsrendering').addEventListener('change', updatePointsRendering);functionaddShape(geojson) {if (geojson.type ==='LineString'&& geojson.coordinates.length<2) {return; }if (geojson.type ==='Polygon'&& geojson.coordinates[0].length<3) {return; }// Create and show a new object with the same geometry but with different materialsconsto=newDrawing( { faceMaterial: drawnFaceMaterial, sideMaterial: drawnSideMaterial, lineMaterial: drawnLineMaterial, pointMaterial: drawnPointMaterial, minExtrudeDepth: 40, maxExtrudeDepth: 100, use3Dpoints: document.getElementById('pointsrendering').value ==='0', point2DFactory, }, geojson, );// Compute some measurements o.userData.measurements = { minmax: getMinMaxAltitudes(o), nbPoints: o.coordinates.length/3, };if (o.geometryType ==='Polygon'|| o.geometryType ==='LineString') { o.userData.measurements.perimeter =getPerimeter(o); }if (o.geometryType ==='Polygon') { o.userData.measurements.area =getArea(o); }// Add it to our scene drawEntity.add(o); instance.notifyChange(drawEntity);}// Listen to when we are done drawingdrawTool.addEventListener('end', evt=>addShape(evt.geojson));// At this point we:// - can add new shapes,// - have them displayed when done.// Let's add selection & edition!// Display a nice pointer when the user is over a drawn shape// and show measurement infoinstance.domElement.addEventListener('mousemove', evt=> {// Do nothing if we're editing a shapeif (drawTool.state !== DrawToolState.READY) return;// In case we're using points with CSS2DRenderer, instance.pickObjectsAt will take// care of returning the elements corresponding to the points// You can therefore use the same API whatever rendering method you're using for pointsconstpicked= instance .pickObjectsAt(evt, { where: [drawEntity], limit: 1, radius: 5, pickFeatures: true, }) .at(0); instance.domElement.style.cursor = picked ?'pointer':'default';if (picked && picked.drawing) {constmesurements= picked.drawing.userData.measurements;consthoverHelper= document.getElementById('hoverHelper'); hoverHelper.innerText =` Number of points: ${mesurements.nbPoints} Min altitude: ${mesurements.minmax[0].toFixed()}m Max altitude: ${mesurements.minmax[1].toFixed()}m ${mesurements.perimeter?`Perimeter: ${mesurements.perimeter.toFixed()}m`:''} ${mesurements.area?`Area: ${mesurements.area.toFixed()}m²`:''} `; hoverHelper.classList.remove('d-none'); } else { document.getElementById('hoverHelper').classList.add('d-none'); }});// Edit a shape when clicking on itinstance.domElement.addEventListener('click', evt=> {if (drawTool.state !== DrawToolState.READY) return;constpicked= instance.pickObjectsAt(evt, { where: [drawEntity], limit: 1, radius: 5 }).at(0);if (picked) {constdrawing= picked.drawing;for (constoof document.getElementsByClassName('helper')) { o.classList.add('d-none'); } document.getElementById('editHelper').classList.remove('d-none'); document.getElementById('options').setAttribute('disabled', true); instance.domElement.style.cursor ='default';// When editing a Drawing object directly, materials and options are not reset from drawTool drawing.setMaterials({}); // Reset the materials instance.notifyChange(drawEntity); // And notify Giro3D for the changes drawEntity.remove(drawing); drawTool.edit(drawing); // Start the edit }});// Load some shapesFetcher.json('https://3d.oslandia.com/dem/features.json').then(features=> { features.forEach(feature=> {// Mess around with the API drawTool.edit(feature); drawTool.insertPointAt(0, newVector3(0, 0, 0)); drawTool.updatePointAt(0, newVector2(1, 2, 3)); drawTool.deletePoint(0); drawTool.end();// In real use case, we'd call addShape directly });});// Add some shapes via APIdrawTool.start('Polygon');drawTool.addPointAt(newVector3(0, 0, 0));drawTool.addPointAt(newVector3(1, 2, 3));drawTool.addPointAt(newVector3(-13601375.735757545, 5811313.553932933, 2263.4501953125));drawTool.addPointAt(newVector3(-13601183.080786511, 5811718.900814531, 2169.449462890625));drawTool.addPointAt(newVector3(-13601435.612581342, 5811988.171339845, 2113.301025390625));drawTool.addPointAt(newVector3(-13601692.241683275, 5812247.386654718, 2098.3310546875));drawTool.addPointAt(newVector3(-13601229.646728978, 5812162.072989969, 2098.95068359375));drawTool.addPointAt(newVector3(-13601285.151505515, 5812670.5826594215, 2013.54736328125));drawTool.addPointAt(newVector3(-13601435.248480782, 5813288.373160986, 1857.339111328125));drawTool.addPointAt(newVector3(-13601878.109128818, 5813344.696319774, 1802.1083984375));drawTool.addPointAt(newVector3(-13602217.049078688, 5813161.852430431, 1857.4925537109375));drawTool.addPointAt(newVector3(-13602524.077633068, 5812790.909256665, 1967.9317626953125));drawTool.addPointAt(newVector3(-13602730.889452294, 5812173.281410588, 2093.4921875));drawTool.addPointAt(newVector3(-13602525.81417714, 5811603.776026396, 2215.937744140625));drawTool.updatePointAt(0, newVector3(-13601908.711527146, 5811403.3703095, 2241.1591796875));drawTool.deletePoint(1);drawTool.end();
index.html
<!doctypehtml><htmllang="en"> <head> <title>Draw shapes</title> <metacharset="UTF-8" /> <metaname="name"content="drawtool" /> <metaname="description"content="Enables the user to draw points, lines and polygons." /> <metaname="viewport"content="width=device-width, initial-scale=1.0" /> <linkrel="icon"href="https://giro3d.org/images/favicon.svg" /> <linkhref="https://giro3d.org/assets/bootstrap-custom.css"rel="stylesheet" /> <scriptsrc="https://giro3d.org/assets/bootstrap.bundle.min.js"></script> <linkrel="stylesheet"type="text/css"href="https://giro3d.org/latest/css/example.css" /> </head> <body> <divid="viewerDiv"class="m-0 p-0 w-100 h-100"></div> <divid="panelDiv"class="position-absolute top-0 start-0 mh-100 overflow-auto"></div> <divclass="side-pane-with-status-bar"style="width: 20rem"> <divclass="card"> <divclass="card-body"> <divclass="row"> <buttonclass="col btn btn-primary"id="addPoint">Points</button> <buttonclass="col mx-1 btn btn-primary"id="addLine">Line</button> <buttonclass="col btn btn-primary"id="addPolygon">Polygon</button> </div> <divclass="row mt-2"> <divclass="p-2 d-none helper"id="addPointHelper"> Click on the map to add points. Right-click to end the shape. </div> <divclass="p-2 d-none helper"id="addLineHelper"> Click on the map to add points to the line. Right-click to end the shape. </div> <divclass="p-2 d-none helper"id="addPolygonHelper"> Click on the map to add points to the polygon. Click on the first point or right-click to end the shape. </div> <divclass="p-2 d-none helper"id="editHelper"> Click on a point and drag it to move it. Click on an edge to create a new point and drag it to move it. Right-click to end. </div> <divclass="p-2 d-none helper"id="mainHelper"> Click on a drawn shape to edit it. </div> </div> <divclass="row mt-2"> <buttonclass="btn btn-secondary dropdown-toggle"type="button"data-bs-toggle="collapse"data-bs-target="#options"aria-expanded="false"aria-controls="options" > Options </button> <fieldsetid="options"class="p-2 collapse"> <divclass="container"> <divclass="row mt-1"> <divclass="input-group p-0"> <labelclass="input-group-text"for="dragging">Move points</label> <selectclass="form-select"id="dragging"> <optionvalue="0"selected>via drag-and-drop</option> <optionvalue="1">on click</option> </select> </div> </div> <divclass="row mt-1"> <divclass="input-group p-0"> <divclass="input-group-text"> <divclass="form-check form-switch"> <inputclass="form-check-input"type="checkbox"checked="true"role="switch"id="splicingtoleranceEnabled" /> <labelclass="form-check-label"for="splicingtoleranceEnabled"title="Unit in 3D world space (when disabled, auto)" >Splicing hit tolerance</label > </div> </div> <inputtype="number"class="form-control"min="0"max="100"value="15"id="splicingtolerance" /> </div> </div> <divclass="row mt-1"> <divclass="input-group p-0"> <divclass="input-group-text"> <divclass="form-check form-switch"> <inputclass="form-check-input"type="checkbox"checked="true"role="switch"id="minpointsEnabled" /> <labelclass="form-check-label"for="minpointsEnabled"title="Minimum points before the geometry can be finished (when disabled, auto)" >Minimum points</label > </div> </div> <inputtype="number"class="form-control"min="0"max="10"value="0"id="minpoints" /> </div> </div> <divclass="row mt-1"> <divclass="input-group p-0"> <divclass="input-group-text"> <divclass="form-check form-switch"> <inputclass="form-check-input"type="checkbox"checked="true"role="switch"id="maxpointsEnabled" /> <labelclass="form-check-label"for="maxpointsEnabled"title="Number of points at which the geometry is automatically considered as finished" >Maximum points</label > </div> </div> <inputtype="number"class="form-control"min="0"max="10"value="10"id="maxpoints" /> </div> </div> <divclass="row mt-1"> <div> <divclass="form-check form-switch"> <inputclass="form-check-input"type="checkbox"checked="true"role="switch"id="addpointsEnabled" /> <labelclass="form-check-label"for="addpointsEnabled" >Add new points when editing</label > </div> </div> </div> <divclass="row mt-1"> <divclass="input-group p-0"> <labelclass="input-group-text"for="pointsrendering" >Render points</label > <selectclass="form-select"id="pointsrendering"> <optionvalue="0">as 3D objects</option> <optionvalue="1"selected>via CSS2DRenderer</option> </select> </div> </div> </div> </fieldset> </div> </div> </div> </div> <divclass="me-3 mb-5 position-absolute bottom-0 end-0"style="width: 20vw"> <divclass="p-2 card d-none"id="hoverHelper"></div> </div> <scripttype="module"src="index.js"></script> <script>/* activate popovers */constpopoverTriggerList= [].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-undefpopoverTriggerEl=>new bootstrap.Popover(popoverTriggerEl, { trigger: 'hover', placement: 'left', content: document.getElementById( popoverTriggerEl.getAttribute('data-bs-content'), ).innerHTML, html: true, }), ); </script> </body></html>