View a bridge on a map
See how a bridge connects the surrounding roads across a river, and switch between street and satellite maps.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>View a bridge on a map</title>
<meta name="description" content="See how a bridge connects the surrounding roads across a river, and switch between street and satellite maps.">
<style>
html, body { height: 100%; margin: 0; }
#example-viewer-mount { position: fixed; inset: 0; }
bim-grid#ifc-viewer { display: block; position: absolute; inset: 0; width: 100%; height: 100%; min-width: 0; min-height: 0; }
.example-ui { position: fixed; z-index: 10; box-sizing: border-box; color: #253041; font: 12px/1.4 system-ui, sans-serif; }
.example-ui.example-task { right: 10px; bottom: 64px; width: 224px; max-width: calc(100vw - 64px); display: grid; gap: 6px; padding: 10px; background: #fff; border: 1px solid #dfe3e8; border-radius: 4px; box-shadow: 0 1px 5px #14203310; }
.example-ui :is(h2, p) { margin: 0; }
.example-ui h2 { font-size: 13px; line-height: 1.3; font-weight: 650; }
.example-ui p { color: #5a6575; }
.example-ui button { box-sizing: border-box; min-width: 0; max-width: 100%; font: inherit; padding: 5px 7px; border: 1px solid #cbd2dc; border-radius: 3px; background: #fff; color: #253041; cursor: pointer; }
.example-ui button:hover { background: #f3f6fa; }
.example-ui button:focus-visible { outline: 2px solid #3568ba; outline-offset: 2px; }
.example-ui [hidden] { display: none; }
.example-ui.example-credit { right: 8px; bottom: 32px; width: max-content; max-width: calc(100vw - 64px); padding: 2px 5px; background: #ffffffdf; border-radius: 2px; font-size: 9px; text-align: right; overflow-wrap: anywhere; }
.example-ui.example-credit a { color: #566171; }
@media (max-width: 600px) {
.example-ui.example-task { right: 8px; padding: 8px; }
.example-ui button { min-height: 30px; }
}
</style>
<!-- generated:example-seo:start -->
<link rel="canonical" href="https://viewer.flinker.app/examples/rotterdam-bridge/index.html">
<meta name="robots" content="index,follow">
<meta property="og:title" content="View a bridge on a map">
<meta property="og:description" content="See how a bridge connects the surrounding roads across a river, and switch between street and satellite maps.">
<meta property="og:url" content="https://viewer.flinker.app/examples/rotterdam-bridge/index.html"><meta property="og:type" content="article">
<meta property="og:site_name" content="Flinker IFC Viewer"><meta property="og:image" content="https://viewer.flinker.app/examples/rotterdam-bridge/thumbnail.webp">
<meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="View a bridge on a map">
<meta name="twitter:description" content="See how a bridge connects the surrounding roads across a river, and switch between street and satellite maps."><meta name="twitter:image" content="https://viewer.flinker.app/examples/rotterdam-bridge/thumbnail.webp">
<script type="application/ld+json">{"@context":"https://schema.org","@type":"TechArticle","headline":"View a bridge on a map","description":"See how a bridge connects the surrounding roads across a river, and switch between street and satellite maps.","url":"https://viewer.flinker.app/examples/rotterdam-bridge/index.html","mainEntityOfPage":"https://viewer.flinker.app/examples/rotterdam-bridge/index.html","image":"https://viewer.flinker.app/examples/rotterdam-bridge/thumbnail.webp","inLanguage":"en","author":{"@type":"Organization","name":"Flinker","url":"https://www.flinker.app/"}}</script>
<!-- generated:example-seo:end -->
<script type="application/json" id="example-data">
{
"category": "Infrastructure",
"formats": [
"IFC"
],
"capabilities": [
"IFC4x3",
"Bridges",
"Maps"
],
"audience": "BIM workflow",
"order": 22,
"viewer": {
"assetIds": [
"van-brienenoord"
],
"presentation": "map"
},
"assets": [
{
"id": "van-brienenoord",
"name": "Van Brienenoordbrug-IFC4X3.ifc",
"format": "IFC",
"bytes": 1319501,
"url": "https://raw.githubusercontent.com/Geonovum/GeoBIM_Georefereren/0fe0f5e89676de3d21fc3219eb5805b36e29e032/data/voorbeeldmodellen_Infra/Van%20Brienenoordbrug-IFC4X3.ifc",
"sourceUrl": "https://github.com/Geonovum/GeoBIM_Georefereren/blob/0fe0f5e89676de3d21fc3219eb5805b36e29e032/data/voorbeeldmodellen_Infra/Van%20Brienenoordbrug-IFC4X3.ifc",
"license": "W3C",
"licenseUrl": "https://github.com/Geonovum/GeoBIM_Georefereren/blob/0fe0f5e89676de3d21fc3219eb5805b36e29e032/LICENSE",
"noticeUrl": "https://viewer.flinker.app/examples/rotterdam-bridge/index.html#example-notices",
"attribution": "Geonovum GeoBIM georeferencing practice dataset and original model authors. W3C Software and Document license as published in the source repository.",
"notes": "The example supplements the missing vertical datum as NAP, inferred from the publisher’s successful height validation against AHN (NAP). Original geometry, map-conversion coordinates and heights are retained. The publisher reports a small XYZ offset against AHN. Copernicus is a 30 m surface model, so this is not a certified survey overlay.",
"crsCorrection": {
"sha256": "fd305a82156910c4c4332cfec826ff39fda506b02c1bd8e8d5c8ab1e32b66a64",
"expectedName": "EPSG:28992",
"verticalDatum": "NAP"
},
"placementSourceUrl": "https://github.com/Geonovum/GeoBIM_Georefereren/blob/0fe0f5e89676de3d21fc3219eb5805b36e29e032/09%20Praktijktoets%20georefereren.md"
}
]
}
</script>
</head>
<body>
<main id="example-viewer-mount" aria-label="Model viewer"><bim-grid id="ifc-viewer"></bim-grid></main>
<aside class="example-ui example-task" aria-label="Bridge on the map">
<h2>Bridge on the map</h2>
<p>Zoom out to see how the bridge connects the roads across the river. Select a component, then click <strong>1 element selected</strong> at the top left to open <strong>Properties</strong>.</p>
<p>To change the map, open <strong>View</strong> at the top right → <strong>Layers</strong>, then choose <strong>Satellite</strong> or <strong>Street</strong>.</p>
<button id="sample-retry" type="button" hidden>Retry sample</button>
</aside>
<div class="example-ui example-credit" aria-label="Sample source and license"><a href="https://github.com/Geonovum/GeoBIM_Georefereren/blob/0fe0f5e89676de3d21fc3219eb5805b36e29e032/data/voorbeeldmodellen_Infra/Van%20Brienenoordbrug-IFC4X3.ifc" target="_blank" rel="noopener noreferrer">Geonovum / W3C</a> · <a href="https://github.com/Geonovum/GeoBIM_Georefereren/blob/0fe0f5e89676de3d21fc3219eb5805b36e29e032/LICENSE" target="_blank" rel="noopener noreferrer">W3C</a></div>
<template id="example-guide">
<h2 data-example-title>View a bridge on a map</h2>
<p data-example-description>See how a bridge connects the surrounding roads across a river, and switch between street and satellite maps.</p>
<h3>Try it</h3><ol><li>Use the map to locate the bridge relative to the roads and river.</li><li>Open View at the top right → Layers and choose Satellite or Street to compare the model with the surrounding area.</li><li>Zoom between the bridge and its wider setting.</li></ol><h2>Developer integration</h2><p>After <code>viewer.add()</code>, call <code>viewer.toggleMap(true)</code> to show the project's map context. The SDK owns map styles and terrain controls. The source placement and height limitations are listed under Model notes.</p><p>Copy this folder and serve it over HTTP, for example <code>python -m http.server 8080</code>. Open <code>http://localhost:8080/</code>; the SDK and sample downloads require internet access.</p>
<p>The host uses <code>await viewer.IfcAPI</code> to open the hash-verified source, writes the projected CRS vertical datum with <code>WriteLine()</code>, and saves the edited bytes with <code>SaveModel()</code>. It closes the native model and disposes WASM before <code>viewer.add()</code>. See the <a href="https://thatopen.github.io/engine_web-ifc/docs/classes/web-ifc.IfcAPI.html">complete web-ifc API reference</a>.</p><h3>Model notes</h3><p>Height datum set to NAP (inferred); geometry retained. The publisher reports a small XYZ offset. Optional terrain is a 30 m surface.</p>
</template>
<template id="example-notices">
<h2>Sources and license notices</h2><p><a href="https://github.com/Geonovum/GeoBIM_Georefereren/blob/0fe0f5e89676de3d21fc3219eb5805b36e29e032/data/voorbeeldmodellen_Infra/Van%20Brienenoordbrug-IFC4X3.ifc" target="_blank" rel="noopener noreferrer">Geonovum and original model authors</a> · <a href="https://github.com/Geonovum/GeoBIM_Georefereren/blob/0fe0f5e89676de3d21fc3219eb5805b36e29e032/LICENSE" target="_blank" rel="noopener noreferrer">W3C</a></p>
<p>The example supplements the missing vertical datum as NAP, inferred from the publisher’s successful height validation against AHN (NAP). Original geometry, map-conversion coordinates and heights are retained. The publisher reports a small XYZ offset against AHN. Copernicus is a 30 m surface model, so this is not a certified survey overlay.</p>
</template>
<script type="module">
import { Viewer } from "https://cdn.flinker.app/ifc-viewer/v4.0.3/ifc-viewer.es.js";
async function withIfcModel(blob, read) {
const api = await viewer.IfcAPI;
await api.Init();
let modelID = -1;
try {
modelID = api.OpenModel(new Uint8Array(await blob.arrayBuffer()));
if (modelID < 0) throw new Error('The native IFC API could not open the model.');
return await read(api, modelID);
} finally {
if (modelID >= 0) api.CloseModel(modelID);
api.Dispose();
}
}
async function correctSampleCrs(file, correction) {
const bytes = await file.arrayBuffer();
const hash = [...new Uint8Array(await crypto.subtle.digest('SHA-256', bytes))].map(byte => byte.toString(16).padStart(2, '0')).join('');
if (hash !== correction.sha256) throw new Error('The source differs from the verified sample; its coordinate correction was not applied.');
return withIfcModel(new Blob([bytes]), (api, modelID) => {
const ids = api.GetLineIDsWithType(modelID, api.GetTypeCodeFromName('IFCPROJECTEDCRS'), true);
let records;
try {
records = api.GetLines(modelID, Array.from({ length: ids.size() }, (_, index) => ids.get(index)))
.filter(line => line.Name?.value === correction.expectedName);
} finally { ids.delete(); }
if (records.length !== 1 || records[0].VerticalDatum !== null)
throw new Error('The sample CRS differs from its published reference.');
const crs = records[0];
crs.VerticalDatum = api.CreateIfcType(modelID, api.GetTypeCodeFromName('IFCIDENTIFIER'), correction.verticalDatum);
api.WriteLine(modelID, crs);
return api.SaveModel(modelID);
});
}
// Example-only download feedback. All file parsing remains in the published SDK.
async function loadExampleFile(name, url, load = blob => viewer.add(name, blob), size = 0) {
const show = stage => viewer.setMessage(name + ': ' + stage, { type: 'loading' });
const chunks = [], chunkSize = 4 * 1024 * 1024;
let offset = 0, attempts = 0, action = 'download', type = '';
try {
await show('Downloading');
while (!size || offset < size) {
try {
const end = Math.min(offset + chunkSize, size || Infinity) - 1;
const response = await fetch(url, {
headers: { Range: 'bytes=' + offset + '-' + end },
cache: 'no-store', signal: AbortSignal.timeout(60000),
});
if (!response.ok) {
await response.body?.cancel();
// GitHub raw hides Content-Range from browser JavaScript: probe EOF.
if (response.status === 416 && offset && !size) break;
throw Object.assign(new Error('HTTP ' + response.status), { retry: [408, 429].includes(response.status) || response.status >= 500 });
}
const range = response.headers.get('content-range')?.match(/^bytes (\d+)-(\d+)\/(\d+)$/);
if (range && Number(range[1]) !== offset) throw new TypeError('Unexpected byte range');
size ||= Number(range?.[3]) || 0;
const bytes = await response.arrayBuffer();
const expected = response.status === 206 ? (size ? Math.min(chunkSize, size - offset) : 0) : size;
if (!bytes.byteLength || (expected && bytes.byteLength !== expected) || (response.status === 206 && bytes.byteLength > chunkSize))
throw new TypeError('Incomplete download at byte ' + offset);
if (response.status === 200) chunks.length = 0; // Server ignored Range.
chunks.push(bytes);
offset = response.status === 200 ? bytes.byteLength : offset + bytes.byteLength;
type = response.headers.get('content-type') || '';
attempts = 0;
if (response.status === 200) break;
await show('Downloading' + (size ? ' ' + Math.round(offset / size * 100) + '%' : ''));
}
catch (error) {
if (error.retry === false || ++attempts === 3) throw error;
await show('Retrying download');
await new Promise(resolve => setTimeout(resolve, 500 * 2 ** (attempts - 1)));
}
}
action = 'open';
const result = await load(new Blob(chunks, { type }), show);
if (result?.isError) throw new Error('The viewer could not open the file');
await viewer.setMessage(null);
return result;
}
catch (error) {
throw new Error('Could not ' + action + ' ' + name + (action === 'download' ? ' from ' + url : '') + ': ' + error.message, { cause: error });
}
}
const { assets } = JSON.parse(document.getElementById("example-data").textContent);
const viewer = new Viewer('#ifc-viewer');
window.exampleViewer = viewer;
document.body.dataset.exampleState = 'loading';
const imported = new Set();
const retry = document.querySelector('#sample-retry');
let running = false;
async function load() {
if (running)
return;
running = true;
retry.hidden = true;
document.body.dataset.exampleState = 'loading';
try {
await viewer.ready;
for (const asset of assets) {
if (imported.has(asset.id))
continue;
await loadExampleFile(asset.name, asset.url, async (blob, stage) => {
await stage('Preparing coordinates');
blob = await correctSampleCrs(blob, asset.crsCorrection);
return viewer.add(asset.name, blob);
}, asset.bytes);
imported.add(asset.id);
}
await viewer.toggleMap(true);
document.body.dataset.exampleState = 'loaded';
}
catch (error) {
document.body.dataset.exampleState = 'error';
await viewer.setMessage(error instanceof Error ? error.message : String(error), { type: 'error' });
retry.hidden = false;
}
finally {
running = false;
}
}
retry.addEventListener('click', () => { void load(); });
void load();
</script>
</body>
</html>
How it works
Try it
- Use the map to locate the bridge relative to the roads and river.
- Open View at the top right → Layers and choose Satellite or Street to compare the model with the surrounding area.
- Zoom between the bridge and its wider setting.
Developer integration
After viewer.add(), call viewer.toggleMap(true) to show the project's map context. The SDK owns map styles and terrain controls. The source placement and height limitations are listed under Model notes.
Copy this folder and serve it over HTTP, for example python -m http.server 8080. Open http://localhost:8080/; the SDK and sample downloads require internet access.
The host uses await viewer.IfcAPI to open the hash-verified source, writes the projected CRS vertical datum with WriteLine(), and saves the edited bytes with SaveModel(). It closes the native model and disposes WASM before viewer.add(). See the complete web-ifc API reference.
Model notes
Height datum set to NAP (inferred); geometry retained. The publisher reports a small XYZ offset. Optional terrain is a 30 m surface.
Sources and license notices
Geonovum and original model authors · W3C
The example supplements the missing vertical datum as NAP, inferred from the publisher’s successful height validation against AHN (NAP). Original geometry, map-conversion coordinates and heights are retained. The publisher reports a small XYZ offset against AHN. Copernicus is a 30 m surface model, so this is not a certified survey overlay.