Laget av Bjørn Sandvik
I veiledning 8 konstruerte vi et interaktivt kart over befolkningen i Oslo kommune. Her skal vi vise de samme dataene i 3D med bruk av WebGL og Three.js.
Vi bør være forsiktig med bruk av 3D til å visualisere statistikk. Av og til kan det forsvares for å skape oppmerksomhet og interesse, og det kan være lettere å se geografiske variasjoner ved bruk av en ekstra dimensjon. Samtidig vil søylene blokkere for hverandre, og perspektivet gjør det vanskeligere å sammenligne høydene.
Three.js er et bibliotek som gjør det mye enklere å lage 3D-visualiseringer i nettleseren. Det er ikke laget spesielt for kart, men heldigvis er det lett å konvertere UTM-koordinater til Three.js sitt koordinatsystem.
Vi skal vise søylene oppå det samme bakgrunnskartet som vi brukte i det 2-dimensjonale kartet, men siden Three.js ikke har støtte for kartfliser (tiles), laster vi inn kartet som ett stort bilde. Vi bruker her WMS-tjensten til Kartverket for å definere og laste ned kartbildet. Web Map Service (WMS) er en kjent kartstandard som lar deg laste ned kart i ulike projeksjoner og hvor du selv kan bestemme hva som skal vises på kartet (se oversikt). Det er ikke mulig å laste inn bildet direkte fra Kartverkets server til Three.js, pga. sikkerhetsinnstillingene i nettleseren. I steden lagrer vi en lokal kopi av kartet.
Vi skal vise befolkningsstatistikk for Oslo kommune, og hvis vi tenker oss en firkant rundt den befolkede delen av kommunen vil denne ha følgende koordinater i UTM 33:
Sørvest: 253700, 6637800 - Nordøst: 273800, 6663700
UTM-koordinater er i meter, og dette gir oss et område som er 273 800 - 253 700 = 20 100 meter fra vest til øst, og 6 663 700 - 6 637 800 = 25 900 meter fra nord til sør.
For å hente ut dette kartutsnittet for Oslo kan vi bruke følgende URL:
http://openwms.statkart.no/skwms1/wms.topo2.graatone?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&CRS=EPSG:32633&BBOX=253700,6637800,273800,6663700&WIDTH=2010&HEIGHT=2590&LAYERS=fjellskygge,N50Vannflate,N50Vannkontur,N50Elver,N50Bilveg,N50Bygningsflate&FORMAT=image/jpeg
Her har vi angitt at kartprojeksjonen skal være UTM 33N (CRS=EPSG:32633), utsnittet er definert av koordinatene over (BBOX=253700,6637800,273800,6663700), oppløsningen skal være 10 meter per pixel (WIDTH=2010, HEIGHT=2590), og vi ønsker å vise følgende kartlag (LAYERS): fjellskygge, vann, elver, bilveg og bygninger. URL'en returnerer dette kartet:
Vi kan nå sette opp kartet i Three.js. Først definerer vi noen størrelser:
var bounds = [253700, 6637800, 273800, 6663700], // UTM 33 vest, sør, øst, nord
boundsWidth = bounds[2] - bounds[0],
boundsHeight = bounds[3] - bounds[1],
sceneWidth = 100,
sceneHeight = 100 * (boundsHeight / boundsWidth),
width = window.innerWidth,
height = window.innerHeight;
"Bounds" er utsnittet i UTM-koordinater som vi omtalt over. "Scene" er bredde og høyde på koordinatsystemet i Three.js. Vi definerer også bredde og høyde på 3D-visningen, som skal dekke hele nettleservinduet.
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 20, width / height, 0.1, 1000 );
camera.position.set(0, -200, 120);
var controls = new THREE.TrackballControls(camera);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
Videre oppretter vi en scene med Three.js hvor vi kan legge til elementer. Vi oppretter også et kamera og angir hvor vi ser på 3D-scenen fra. Vi bruker THREE.TrackballControls som gjør at brukeren selv kan styre kameraet og se på visualiseringen fra ulike vinkler. Til slutt spesifiserer vi at kartet skal tegnes ut med WebGL med bredden og høyden definert over.
var geometry = new THREE.PlaneGeometry(sceneWidth, sceneHeight, 1, 1),
material = new THREE.MeshBasicMaterial(),
plane = new THREE.Mesh(geometry, material);
var textureLoader = new THREE.TextureLoader();
textureLoader.load('data/wms_oslo_topo2_graatone.jpg', function(texture) {
material.map = texture;
scene.add(plane);
});
Selve kartbildet lastes inn ved å først opprette en flate (PlaneGeometry) og legge bildet oppå som en tekstur, før det legges til scenen.
function render() {
controls.update();
requestAnimationFrame(render);
renderer.render(scene, camera);
}
render();
Vi lager en egen funksjon som tegner ut scenen kontinuerlig ettersom kameravinkelen endrer seg. Vi får nå et kart i perspektiv som ser slik ut:
Prøv å endre kameraposisjonen ved å justere verdiene for: camera.position.set(0, -200, 120)
Vi er nå klare for å legge på befolkningsdataene fra SSB:
var cellSize = 100,
xCells = boundsWidth / cellSize,
yCells = boundsHeight / cellSize,
boxSize = sceneWidth / xCells,
valueFactor = 0.02;
var colorScale = d3.scale.linear()
.domain([0, 100, 617])
.range(['#fec576', '#f99d1c', '#E31A1C']);
var csv = d3.dsv(' ', 'text/plain');
csv('data/Oslo_bef_100m_2015.csv').get(function(error, data) { // ru250m_2015.csv
for (var i = 0; i < data.length; i++) {
var id = data[i].rute_100m,
utmX = parseInt(id.substring(0, 7)) - 2000000 + cellSize, // First seven digits minus false easting
utmY = parseInt(id.substring(7, 14)) + cellSize, // Last seven digits
sceneX = (utmX - bounds[0]) / (boundsWidth / sceneWidth) - sceneWidth / 2,
sceneY = (utmY - bounds[1]) / (boundsHeight / sceneHeight) - sceneHeight / 2,
value = parseInt(data[i].sum);
var geometry = new THREE.BoxGeometry(boxSize, boxSize, value * valueFactor);
var material = new THREE.MeshBasicMaterial({
color: color(value)
});
var cube = new THREE.Mesh(geometry, material);
cube.position.set(sceneX, sceneY, value * valueFactor / 2);
scene.add(cube);
}
});
Vi definerere størrelsen på rutene (cellSize) til 100 meter, og regner ut hvor mye det tilsvarer i koordinatsystemet til Three.js (boxSize). Vi lager også en lineær fargeskala med D3.js. D3 brukes også til å lese inn dataene, og for hver rute finner UTM-koordinatene fra id'en til ruta (se detaljer i veiledning 8). UTM-koordinatene blir så konvertert verdier som passer med scenen vi har definert over.
Alle ruter får en søyle eller en boks (BoxGeometry) hvor høyden og fargen bestemmes av antall innbyggere. Søylen plasseres på riktig sted, og legges til scenen vår.
Søylene reiser seg nå opp fra kartet, men det er vanskelig å skille søylene fra hverandre. Dette kan vi forbedre ved å legge til et par lyskilder:
var dirLight = new THREE.DirectionalLight(0xcccccc, 1);
dirLight.position.set(-70, -50, 80);
scene.add(dirLight);
var ambLight = new THREE.AmbientLight(0x777777);
scene.add(ambLight);
"Directional light" er lys som kommer fra en retning, som fra sola, mens "ambient light" er overalt og siker at det også er litt lys på "baksiden" av søylene. Vi må også endre "materialet" på søylene til et som reflekterer lys (MeshPhongMaterial):
var material = new THREE.MeshPhongMaterial({
color: colorScale(value)
});
Hvordan synes du visualiserigen ble sammenlignet med det 2-dimensjonale kartet?