import colormap from "colormap" ;
import { Vector3, Color, MathUtils } from "three" ;
import { MapControls } from "three/examples/jsm/controls/MapControls.js" ;
import GeoJSON from "ol/format/GeoJSON.js" ;
import VectorSource from "ol/source/Vector.js" ;
import { createXYZ } from "ol/tilegrid.js" ;
import { tile } from "ol/loadingstrategy.js" ;
import Instance from "@giro3d/giro3d/core/Instance.js" ;
import Extent from "@giro3d/giro3d/core/geographic/Extent.js" ;
import Inspector from "@giro3d/giro3d/gui/Inspector.js" ;
import FeatureCollection from "@giro3d/giro3d/entities/FeatureCollection.js" ;
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 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 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 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 ) => {
const items = options. map (
( opt ) =>
`<option value=${ opt . id } ${ opt . selected ? "selected" : ""}>${ opt . name }</option>` ,
);
element.innerHTML = items. join ( " \n " );
};
return [callback, element.value, element, setOptions];
}
const instance = new Instance ({
target: "view" ,
crs: "EPSG:3857" ,
backgroundColor: null ,
});
const extent = new Extent (
"EPSG:3857" ,
- 20037508.342789244 ,
20037508.342789244 ,
- 20037508.342789244 ,
20037508.342789244 ,
);
const colors = {
"North America" : "#b5a98f" ,
"South America" : "#adc78b" ,
Asia: "#d4d496" ,
Africa: "#db95a5" ,
Oceania: "#c49856" ,
Europe: "#ac96d4" ,
};
let colorMode = "continent" ;
let lineWidth = 1 ;
let fillOpacity = 1 ;
let imageSize = 32 ;
let strokeOpacity = 1 ;
const getContinentColor = ( feature ) => {
const properties = feature. getProperties ();
const continent = properties[ "continent" ];
return colors[continent];
};
const populationColorMap = makeColorRamp ( "bluered" );
const getPopulationColor = ( feature ) => {
const properties = feature. getProperties ();
const population = properties[ "pop_est" ];
const colorIndex = MathUtils. clamp (
Math. log (population * 0.0001 ) * 20 ,
0 ,
255 ,
);
return populationColorMap[Math. round (colorIndex)];
};
const gdpColorRamp = makeColorRamp ( "hot" , false , true );
const getGdpColor = ( feature ) => {
const properties = feature. getProperties ();
const gdp = properties[ "gdp_md" ];
const colorIndex = MathUtils. clamp (Math. log (gdp * 0.0001 ) * 30 , 0 , 255 );
return gdpColorRamp[Math. round (colorIndex)];
};
const countryStyle = ( feature ) => {
const properties = feature. getProperties ();
let fillColor;
let activeColor;
switch (colorMode) {
case "continent" :
fillColor = getContinentColor (feature);
activeColor = "yellow" ;
break ;
case "population" :
fillColor = getPopulationColor (feature);
activeColor = "yellow" ;
break ;
case "gdp" :
fillColor = getGdpColor (feature);
activeColor = "cyan" ;
break ;
}
const hovered = properties.hovered ?? false ;
const clicked = properties.clicked ?? false ;
const fill = clicked ? activeColor : fillColor;
return {
fill: {
color: fill,
depthTest: false ,
renderOrder: 1 ,
opacity: fillOpacity,
},
stroke: {
opacity: strokeOpacity,
color: clicked || hovered ? activeColor : "black" ,
renderOrder: 2 , // To ensure lines are displayed on top of surfaces
lineWidth: clicked ? lineWidth * 2 : lineWidth,
depthTest: false ,
},
};
};
const countries = new FeatureCollection ({
source: new VectorSource ({
format: new GeoJSON (),
url: "https://3d.oslandia.com/giro3d/vectors/countries.geojson" ,
strategy: tile ( createXYZ ({ tileSize: 512 })),
}),
extent,
style: countryStyle,
minLevel: 0 ,
maxLevel: 0 ,
});
countries.name = "countries" ;
instance. add (countries);
const capitalStyle = ( feature ) => {
const image = "https://3d.oslandia.com/giro3d/images/capital.webp" ;
const clicked = feature. get ( "clicked" );
const hovered = feature. get ( "hovered" );
return {
point: {
color: clicked ? "yellow" : hovered ? "orange" : "white" ,
pointSize: clicked ? imageSize * 1.5 : imageSize,
image,
renderOrder: clicked ? 4 : 3 ,
},
};
};
const capitals = new FeatureCollection ({
source: new VectorSource ({
format: new GeoJSON (),
url: "https://3d.oslandia.com/giro3d/vectors/capitals.geojson" ,
strategy: tile ( createXYZ ({ tileSize: 512 })),
}),
extent,
style: capitalStyle,
minLevel: 0 ,
maxLevel: 0 ,
});
capitals.name = "capitals" ;
instance. add (capitals);
instance.view.camera.position. set ( 0 , 5500000 , 50000000 );
const lookAt = new Vector3 ( 0 , 5500000 + 1 , 0 );
instance.view.camera. lookAt (lookAt);
const controls = new MapControls (instance.view.camera, instance.domElement);
controls.enableDamping = true ;
controls.dampingFactor = 0.4 ;
controls.target. copy (lookAt);
controls. saveState ();
instance.view. setControls (controls);
// information on click
const resultTable = document. getElementById ( "results" );
function truncate ( value , length ) {
if (value == null ) {
return null ;
}
const text = `${ value }` ;
if (text. length < length) {
return text;
}
return text. substring ( 0 , length) + "…" ;
}
const filteredAttributes = [
"country" ,
"city" ,
"continent" ,
"name" ,
"gdp_md" ,
"pop_est" ,
];
const gdpFormatter = new Intl. NumberFormat ( undefined , {
style: "currency" ,
currency: "USD" ,
maximumFractionDigits: 0 ,
});
const popFormatter = new Intl. NumberFormat ( undefined , {
style: "decimal" ,
});
function formatValue ( attribute , value ) {
switch (attribute) {
case "gdp_md" :
return gdpFormatter. format (value);
case "pop_est" :
return popFormatter. format (value);
default :
return truncate (value, 18 );
}
}
function fillTable ( objects ) {
resultTable.innerHTML = "" ;
document. getElementById ( "attributes" ).style.display =
objects. length > 0 ? "block" : "none" ;
for ( const obj of objects) {
if ( ! obj.userData.feature) {
continue ;
}
const p = obj.userData.feature. getProperties ();
const entries = [];
for ( const [ key , value ] of Object. entries (p)) {
if (filteredAttributes. includes (key)) {
const entry = `<tr>
<td title="${ key }"><code>${ truncate ( key , 12 ) }</code></td>
<td title="${ value }">${ formatValue ( key , value ) ?? "<code>null</code>"}</td>
</tr>` ;
entries. push (entry);
}
}
resultTable.innerHTML += `
<table class="table table-sm">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Value</th>
</tr>
</thead>
<tbody>
${ entries . join ( "" ) }
</tbody>
</table>
` ;
}
}
const previousHovered = [];
const previousClicked = [];
const objectsToUpdate = [];
function pick ( e , click ) {
const pickedObjects = instance. pickObjectsAt (e, {
where: [capitals, countries],
});
if (click) {
previousClicked. forEach (( obj ) =>
obj.userData.feature. set ( "clicked" , false ),
);
} else {
previousHovered. forEach (( obj ) =>
obj.userData.feature. set ( "hovered" , false ),
);
}
const property = click ? "clicked" : "hovered" ;
objectsToUpdate. length = 0 ;
if (pickedObjects. length > 0 ) {
const picked = pickedObjects[ 0 ];
const obj = picked.object;
const { feature } = obj.userData;
feature. set (property, true );
objectsToUpdate. push (obj);
}
if (click) {
fillTable (objectsToUpdate);
}
// To avoid updating all the objects and lose a lot of performance,
// we only update the objects that have changed.
const updatedObjects = [
... previousHovered,
... previousClicked,
... objectsToUpdate,
];
if (click) {
previousClicked. splice ( 0 , previousClicked. length , ... objectsToUpdate);
} else {
previousHovered. splice ( 0 , previousHovered. length , ... objectsToUpdate);
}
if (updatedObjects. length > 0 ) {
countries. updateStyles (updatedObjects);
capitals. updateStyles (updatedObjects);
}
}
const hover = ( e ) => pick (e, false );
const click = ( e ) => pick (e, true );
instance.domElement. addEventListener ( "mousemove" , hover);
instance.domElement. addEventListener ( "click" , click);
for ( const continent of Object. keys (colors)) {
let timeout;
const [ setColor ] = bindColorPicker (continent, ( c ) => {
colors[continent] = c;
if (timeout) {
clearTimeout (timeout);
}
timeout = setTimeout (() => countries. updateStyles (), 16 );
});
setColor (colors[continent]);
}
const [ setLineWidth ] = bindSlider ( "line-width" , ( v ) => {
lineWidth = v;
countries. updateStyles ();
});
setLineWidth (lineWidth);
const [ setStrokeOpacity ] = bindSlider ( "stroke-opacity" , ( v ) => {
strokeOpacity = v;
countries. updateStyles ();
});
setStrokeOpacity (strokeOpacity);
const [ setFillOpacity ] = bindSlider ( "fill-opacity" , ( v ) => {
fillOpacity = v;
countries. updateStyles ();
});
setFillOpacity (fillOpacity);
const [ setImageSize ] = bindSlider ( "image-size" , ( v ) => {
imageSize = v;
capitals. updateStyles ();
});
setImageSize (imageSize);
bindDropDown ( "color-mode" , ( mode ) => {
colorMode = mode;
countries. updateStyles ();
document. getElementById ( "colors" ).style.display =
colorMode === "continent" ? "block" : "none" ;
});
Inspector. attach ( "inspector" , instance);
<! doctype html >
< html lang = "en" >
< head >
< title >Undraped vectors</ title >
< meta charset = "UTF-8" />
< meta name = "name" content = "undraped_vectors" />
< meta
name = "description"
content = "Display GeoJSON files as meshes and symbols."
/>
< 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"
/>
< style >
#view canvas {
background : rgb ( 132 , 170 , 182 );
background : radial-gradient (
circle ,
rgba ( 132 , 170 , 182 , 1 ) 0 % ,
rgba ( 37 , 44 , 48 , 1 ) 100 %
);
}
</ style >
</ 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 pe-none" style = "width: 20rem" >
<!--Parameters -->
< div class = "card" >
< div class = "card-header" >Style</ div >
< div class = "card-body" >
< div class = "input-group mb-3" >
< label class = "input-group-text" for = "color-mode" >Color</ label >
< select class = "form-select" id = "color-mode" autocomplete = "off" >
< option value = "continent" selected >Per continent</ option >
< option value = "population" >Population</ option >
< option value = "gdp" >GDP</ option >
</ select >
</ div >
< div class = "card mb-3" id = "colors" >
< div class = "card-header" >Colors</ div >
< div class = "card-body" >
<!-- North America -->
< label class = "form-check-label w-100 mb-2" for = "North America" >
< div class = "row" >
< div class = "col" >North America</ div >
< div class = "col" >
< input
type = "color"
class = "form-control form-control-color float-end h-100 w-100"
id = "North America"
value = "#2978b4"
title = "color"
/>
</ div >
</ div >
</ label >
<!-- South America -->
< label class = "form-check-label w-100 mb-2" for = "South America" >
< div class = "row" >
< div class = "col" >South America</ div >
< div class = "col" >
< input
type = "color"
class = "form-control form-control-color float-end h-100 w-100"
id = "South America"
value = "#2978b4"
title = "color"
/>
</ div >
</ div >
</ label >
<!-- Asia -->
< label class = "form-check-label w-100 mb-2" for = "Asia" >
< div class = "row" >
< div class = "col" >Asia</ div >
< div class = "col" >
< input
type = "color"
class = "form-control form-control-color float-end h-100 w-100"
id = "Asia"
value = "#2978b4"
title = "color"
/>
</ div >
</ div >
</ label >
<!-- Europe -->
< label class = "form-check-label w-100 mb-2" for = "Europe" >
< div class = "row" >
< div class = "col" >Europe</ div >
< div class = "col" >
< input
type = "color"
class = "form-control form-control-color float-end h-100 w-100"
id = "Europe"
value = "#2978b4"
title = "color"
/>
</ div >
</ div >
</ label >
<!-- Africa -->
< label class = "form-check-label w-100 mb-2" for = "Africa" >
< div class = "row" >
< div class = "col" >Africa</ div >
< div class = "col" >
< input
type = "color"
class = "form-control form-control-color float-end h-100 w-100"
id = "Africa"
value = "#2978b4"
title = "color"
/>
</ div >
</ div >
</ label >
<!-- Oceania -->
< label class = "form-check-label w-100 mb-2" for = "Oceania" >
< div class = "row" >
< div class = "col" >Oceania</ div >
< div class = "col" >
< input
type = "color"
class = "form-control form-control-color float-end h-100 w-100"
id = "Oceania"
value = "#2978b4"
title = "color"
/>
</ div >
</ div >
</ label >
</ div >
</ div >
<!-- Line width slider -->
< div class = "row" >
< div class = "col" >
< label for = "line-width" class = "form-label" >Line width</ label >
</ div >
< div class = "col" >
< input
type = "range"
min = "0"
max = "20"
step = "1"
value = "2"
class = "form-range"
id = "line-width"
autocomplete = "off"
/>
</ div >
</ div >
<!-- Image size -->
< div class = "row" >
< div class = "col" >
< label for = "image-size" class = "form-label" >Image size</ label >
</ div >
< div class = "col" >
< input
type = "range"
min = "8"
max = "256"
step = "1"
value = "64"
class = "form-range"
id = "image-size"
autocomplete = "off"
/>
</ div >
</ div >
<!-- Stroke opacity -->
< div class = "row" >
< div class = "col" >
< label for = "stroke-opacity" class = "form-label"
>Stroke opacity</ label
>
</ div >
< div class = "col" >
< input
type = "range"
min = "0"
max = "1"
step = "0.01"
value = "1"
class = "form-range"
id = "stroke-opacity"
autocomplete = "off"
/>
</ div >
</ div >
<!-- Fill opacity -->
< div class = "row" >
< div class = "col" >
< label for = "fill-opacity" class = "form-label" >Fill opacity</ label >
</ div >
< div class = "col" >
< input
type = "range"
min = "0"
max = "1"
step = "0.01"
value = "1"
class = "form-range"
id = "fill-opacity"
autocomplete = "off"
/>
</ div >
</ div >
</ div >
</ div >
< div class = "card mt-3" id = "attributes" style = "display: none" >
< div class = "card-header" >Attributes</ div >
< div class = "card-body" >
<!-- Result table -->
< div id = "results" ></ div >
</ 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 >