Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
389 changes: 389 additions & 0 deletions examples/3DTiles-11-styling.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,389 @@
<html>
<head>
<title>Itowns - 3D Tiles styling</title>

<script type="importmap">
{
"imports": {
"itowns": "../dist/itowns.js",
"debug": "../dist/debug.js",
"dat": "https://unpkg.com/dat.gui@0.7.9/build/dat.gui.module.js",
"GuiTools": "./jsm/GUI/GuiTools.js",
"LoadingScreen": "./jsm/GUI/LoadingScreen.js",
"itowns_widgets": "../dist/itowns_widgets.js",
"three": "https://unpkg.com/three@0.170.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.170.0/examples/jsm/"
}
}
</script>

<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

<link rel="stylesheet" type="text/css" href="css/example.css">
<link rel="stylesheet" type="text/css" href="css/LoadingScreen.css">

<style type="text/css">
#description {
z-index: 2;
left: 10px;
max-width: 420px;
}

#description .marg {
margin: 10px;
}

/* ---- tileset input ---- */
#tileset-input-row {
display: flex;
align-items: center;
gap: 4px;
margin: 6px 0 4px 0;
}

#tileset-url {
flex: 1;
min-width: 0;
font-size: 11px;
padding: 2px 4px;
height: 22px;
box-sizing: border-box;
}

#load-btn {
flex-shrink: 0;
font-size: 12px;
padding: 2px 6px;
height: 22px;
cursor: pointer;
}

/* ---- feature color panel ---- */
.color-row {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
font-size: 12px;
}

.color-row label {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.color-row input[type="color"] {
width: 30px;
height: 22px;
padding: 0;
border: 1px solid #999;
cursor: pointer;
background: none;
}

.color-reset-btn {
font-size: 11px;
padding: 3px 10px;
margin-bottom: 6px;
cursor: pointer;
}
</style>

</head>
<body>
<div id="viewerDiv"></div>
<div id="description">
<b>Tileset</b>
<div id="tileset-input-row">
<input type="text" id="tileset-url" placeholder="Enter tileset URL" />
<button id="load-btn">Load</button>
</div>
<hr />
<p><b>Feature Colors</b></p>
<div id="featureColors" class="marg">
<em>Load a tileset to see feature types</em>
</div>
</div>


<script type="module">
import * as THREE from 'three';
import setupLoadingScreen from 'LoadingScreen';
import * as itowns from 'itowns';
import * as debug from 'debug';

import {
AmbientLight,
PMREMGenerator,
} from 'three';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
import {
zoomToLayer,
} from './jsm/OGC3DTilesHelper.js';

const {
TMSSource, WMTSSource, OGC3DTilesSource,
ColorLayer, ElevationLayer, OGC3DTilesLayer, PNTS_SIZE_MODE,
GlobeView, Coordinates, Fetcher,
} = itowns;

// ── Current layer state ───────────────────────────────────────────────────
let currentLayer = null;
let layerCounter = 0;
let rebuildTimer = null;

const uri = new URL(location);

// ── GlobeView ─────────────────────────────────────────────────────────────

const placement = {
coord: new Coordinates('EPSG:4326', 2.351323, 48.856712),
range: 12500000,
};

const viewerDiv = document.getElementById('viewerDiv');

const view = new GlobeView(viewerDiv, placement, {
controls: { minDistance: 100 },
});

itowns.enableDracoLoader('./libs/draco/');
itowns.enableKtx2Loader('./lib/basis/', view.renderer);
itowns.enableMeshoptDecoder(MeshoptDecoder);

const light = new AmbientLight(0x404040, 40);
view.scene.add(light);

const environment = new RoomEnvironment();
const pmremGenerator = new PMREMGenerator(view.renderer);
view.scene.environment = pmremGenerator.fromScene(environment).texture;
pmremGenerator.dispose();

setupLoadingScreen(viewerDiv, view);

// ── Basemap ───────────────────────────────────────────────────────────────

Fetcher.json('./layers/JSONLayers/OPENSM.json').then((config) => {
const colorLayer = new ColorLayer('Ortho', {
...config,
source: new TMSSource(config.source),
});
view.addLayer(colorLayer);
});

function addElevationLayerFromConfig(config) {
config.source = new itowns.WMTSSource(config.source);
var elevationLayer = new itowns.ElevationLayer(config.id, config);
view.addLayer(elevationLayer);
}
itowns.Fetcher.json('./layers/JSONLayers/IGN_MNT_HIGHRES.json').then(addElevationLayerFromConfig);
itowns.Fetcher.json('./layers/JSONLayers/WORLD_DTM.json').then(addElevationLayerFromConfig);

// ── Load / remove tileset ─────────────────────────────────────────────────

function removeCurrentLayer() {
if (!currentLayer) return;
clearTimeout(rebuildTimer);
rebuildTimer = null;
try { view.removeLayer(currentLayer.layerId); } catch (e) { console.warn('removeLayer:', e); }
try { currentLayer.layer.delete(); } catch (e) { console.warn('layer.delete:', e); }
currentLayer = null;
document.getElementById('featureColors').innerHTML =
'<em>Load a tileset to see feature types</em>';
}

function loadTileset(url) {
removeCurrentLayer();

const layerId = '3DTiles_' + (++layerCounter);

const source = new OGC3DTilesSource({ url });

const layer = new OGC3DTilesLayer(layerId, {
source,
pntsSizeMode: PNTS_SIZE_MODE.ATTENUATED,
});

// ── Feature Colors ───────────────────────────────────────
const typeColors = new Map();
const originalColors = new Map();
const featureColorsDiv = document.getElementById('featureColors');

function getFeatureType(feature) {
let type;
if (feature.userData?.CityObjectType) {
type = feature.userData.CityObjectType;
} else {
try {
const bt = feature.getInfo?.()?.batchTable;
if (bt) {
type = bt.surface_type || bt.CityObjectType || bt.type;
}
} catch (e) { console.warn('getFeatureType batch table lookup:', e); }
}
if (!type) return 'Unknown';
type = type.replace(/\0/g, '');
if (type === 'BuildingPart') return 'Building';
return type;
}

function applyTypeColors() {
if (typeColors.size === 0) {
layer.style = null;
} else {
layer.style = {
fill: {
color: (feature) => {
const t = getFeatureType(feature);
return typeColors.get(t) || originalColors.get(t) || '#ffffff';
},
opacity: 1.0,
},
};
}
view.notifyChange();
}

function getColorForType(typeName) {
for (const [, featuresMap] of layer._tileFeatures) {
for (const [, feature] of featuresMap) {
if (getFeatureType(feature) !== typeName) continue;
const geom = feature.object3d.geometry;
const colorAttr = geom?.getAttribute('color');
if (colorAttr && feature.groups?.length) {
const indexAttr = geom.getIndex();
const startIdx = feature.groups[0].start;
const vertexIdx = indexAttr ? indexAttr.getX(startIdx) : startIdx;
const r = colorAttr.getX(vertexIdx);
const g = colorAttr.getY(vertexIdx);
const b = colorAttr.getZ(vertexIdx);
const c = new THREE.Color(r, g, b);
return '#' + c.getHexString();
}
}
}
return '#ffffff';
}

const knownTypes = new Set();

function rebuildColorList() {
const types = new Set();
for (const [, featuresMap] of layer._tileFeatures) {
for (const [, feature] of featuresMap) {
types.add(getFeatureType(feature));
}
}

if (types.size === 0) {
featureColorsDiv.innerHTML = '<em>No features loaded</em>';
knownTypes.clear();
return;
}

const newTypes = [...types].filter(t => !knownTypes.has(t)).sort();
if (newTypes.length === 0) return;

if (knownTypes.size === 0) {
featureColorsDiv.innerHTML = '';
const resetBtn = document.createElement('button');
resetBtn.className = 'color-reset-btn';
resetBtn.textContent = 'Reset All';
resetBtn.addEventListener('click', () => {
typeColors.clear();
originalColors.clear();
knownTypes.clear();
rebuildColorList();
applyTypeColors();
});
featureColorsDiv.appendChild(resetBtn);
}

for (const typeName of newTypes) {
knownTypes.add(typeName);

if (!originalColors.has(typeName)) {
originalColors.set(typeName, getColorForType(typeName));
}

const row = document.createElement('div');
row.className = 'color-row';

const lbl = document.createElement('label');
lbl.textContent = typeName;
lbl.title = typeName;

const picker = document.createElement('input');
picker.type = 'color';
picker.value = typeColors.get(typeName) || originalColors.get(typeName);
picker.title = 'Click to open color picker';

function onPickerChange() {
typeColors.set(typeName, picker.value);
applyTypeColors();
}
picker.addEventListener('change', onPickerChange);

row.appendChild(lbl);
row.appendChild(picker);
featureColorsDiv.appendChild(row);
}
}

layer.addEventListener('load-model', () => {
clearTimeout(rebuildTimer);
rebuildTimer = setTimeout(rebuildColorList, 500);
});

currentLayer = { layerId, layer, url };

view.addLayer(layer).then((_layer) => {
zoomToLayer(view, _layer);
});

// Update URL bar
uri.searchParams.set('3dtiles', url);
history.replaceState(null, null, `?${uri.searchParams.toString()}`);
}

// ── UI wiring ─────────────────────────────────────────────────────────────

const urlInput = document.getElementById('tileset-url');
const loadBtn = document.getElementById('load-btn');

loadBtn.addEventListener('click', () => {
const u = urlInput.value.trim();
if (u) loadTileset(u);
});

urlInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const u = urlInput.value.trim();
if (u) loadTileset(u);
}
});

// ── Expose globals ────────────────────────────────────────────────────────

window.view = view;
window.itowns = itowns;
window.THREE = THREE;

// ── Load URL from query string ────────────────────────────────────────────

(function loadFromQueryString() {
const passedUrl = uri.searchParams.get('3dtiles');
if (passedUrl) {
urlInput.value = passedUrl;
loadTileset(passedUrl);
}
})();

</script>
</body>
</html>
Loading
Loading