Scaling and coloring points according to population.
The populated places dataset has a pop_max
field that is used in rendering the style for each place. The interpolate
operator is used to scale the circle-radius
, rendering a point with a radius of 3 for places with a population under 500 thousand, growing to a point with a radius of 10 for places with a population of 10 million or more. The same interpolate
operator is used to generate a color ramp for places based on the same pop_max
field.
import Map from 'ol/Map.js';
import View from 'ol/View.js';
import GeoJSON from 'ol/format/GeoJSON.js';
import Link from 'ol/interaction/Link.js';
import TileLayer from 'ol/layer/Tile.js';
import VectorLayer from 'ol/layer/Vector.js';
import VectorSource from 'ol/source/Vector.js';
import XYZ from 'ol/source/XYZ.js';
const populatedPlaces = new VectorLayer({
source: new VectorSource({
url: 'https://openlayers.org/data/vector/populated-places.json',
format: new GeoJSON(),
}),
style: {
'circle-stroke-color': 'hsl(0 100% 100% / 0.9)',
'circle-stroke-width': 0.75,
'circle-radius': [
'interpolate',
['linear'],
['get', 'pop_max'],
500_000,
3,
10_000_000,
10,
],
'circle-fill-color': [
'interpolate',
['linear'],
['get', 'pop_max'],
1_000_000,
'hsl(210 100% 40% / 0.9)',
10_000_000,
'hsl(0 80% 60% / 0.9)',
],
},
});
const key = 'Get your own API key at https://www.maptiler.com/cloud/';
const map = new Map({
layers: [
new TileLayer({
source: new XYZ({
attributions:
'<a href="https://www.maptiler.com/copyright/" target="_blank">© MapTiler</a> ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank">© OpenStreetMap contributors</a>',
url:
'https://api.maptiler.com/maps/satellite/{z}/{x}/{y}.jpg?key=' + key,
tileSize: 512,
maxZoom: 20,
}),
}),
populatedPlaces,
],
target: 'map',
view: new View({
center: [0, 0],
zoom: 1,
}),
});
map.addInteraction(new Link());
const info = document.getElementById('info');
let currentFeature = null;
function displayFeatureInfo(pixel, width) {
const feature = map.getFeaturesAtPixel(pixel)[0];
if (feature) {
const featurePixel = map.getPixelFromCoordinate(
feature.getGeometry().getCoordinates(),
);
if (featurePixel[0] > width) {
featurePixel[0] = featurePixel[0] % width;
} else if (featurePixel[1] < width) {
featurePixel[0] = width + (featurePixel[0] % width);
}
info.style.top = featurePixel[1] + 'px';
if (featurePixel[0] < width / 2) {
info.style.left = featurePixel[0] + 'px';
info.style.right = 'auto';
} else {
info.style.right = width - featurePixel[0] + 'px';
info.style.left = 'auto';
}
if (feature !== currentFeature) {
info.style.visibility = 'visible';
info.innerHTML =
feature.get('name') + '<br>' + feature.get('pop_max').toLocaleString();
}
} else if (currentFeature) {
info.style.visibility = 'hidden';
}
currentFeature = feature;
}
map.on('pointermove', function (evt) {
if (evt.dragging) {
info.style.visibility = 'hidden';
currentFeature = undefined;
return;
}
displayFeatureInfo(evt.pixel, evt.frameState.size[0]);
});
map.on('click', function (evt) {
displayFeatureInfo(evt.pixel, evt.frameState.size[0]);
});
map.getTargetElement().addEventListener('pointerleave', function () {
currentFeature = undefined;
info.style.visibility = 'hidden';
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Populated Places</title>
<link rel="stylesheet" href="node_modules/ol/ol.css">
<style>
.map {
width: 100%;
height: 400px;
}
#map {
position: relative;
}
#info {
position: absolute;
display: inline-block;
height: auto;
width: auto;
z-index: 100;
background-color: #333;
color: #fff;
text-align: center;
border-radius: 4px;
padding: 5px;
left: 50%;
transform: translateX(3%);
visibility: hidden;
pointer-events: none;
}
</style>
</head>
<body>
<div id="map" class="map">
<div id="info"> </div>
</div>
<script type="module" src="main.js"></script>
</body>
</html>
{
"name": "populated-places",
"dependencies": {
"ol": "10.5.0"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}