Commit a9b09a55 authored by Rodrigo Tapia-McClung's avatar Rodrigo Tapia-McClung

IDW interpolation with animation and extrusion

parent f580fc93
node_modules
config/dbconfig.json
public/files
cache
tiles
public/tiles
\ No newline at end of file
const aireconfig = require('./config/aire-cdmx.json')
const express = require('express');
const logger = require('morgan');
const cors = require('cors');
const app = express();
const compression = require('compression');
//app.use(compression({ threshold: 6 }));
app.use(compression());
//app.use(logger('dev'));
app.use(logger('combined', {
skip: function (req, res) { return res.statusCode < 400 }
}));
app.use(cors());
app.use('/', express.static(__dirname + '/public'));
/*const {Pool} = require('pg');
const dbconfig = require('./config/dbconfig.json');
const readOnlyPool = new Pool(dbconfig);
readOnlyPool.connect();
const DirCache = require('./utils/dircache.js')
const cache = new DirCache(`./cache/${dbconfig.database?dbconfig.database:process.env.PGDATABASE?process.env.PGDATABASE:''}`);
const mvt = require('./mvt.js')(app, readOnlyPool, cache);
const geojson = require('./geojson.js')(app, readOnlyPool);
const geobuf = require('./geobuf.js')(app, readOnlyPool);
const listLayers = require('./list_layers.js')(app, readOnlyPool);
const query = require('./query.js')(app, readOnlyPool);
const geojsonmvt = require('./geojsonmvt.js')(app, readOnlyPool);
const mbtiles = require('./mbtiles.js')(app, readOnlyPool);
*/
const mbtiles = require('./mbtiles.js')(app);
const server = app.listen(aireconfig.port);
server.setTimeout(600000);
console.log(`airecdmx listening on port ${aireconfig.port}`);
module.exports = app;
{
"port": 8092
}
\ No newline at end of file
{
"host": "host.example.com",
"user": "dbuser",
"password": "dbpassword",
"database": "dbname",
"ssl": true,
"port": 5432,
"max": 20,
"idleTimeoutMillis": 30000,
"connectionTimeoutMillis": 2000
}
\ No newline at end of file
const MBTiles = require('@mapbox/mbtiles');
const p = require("path");
// Enable CORS and set correct mime type/content encoding
let header = {
"Access-Control-Allow-Origin":"*",
"Access-Control-Allow-Headers":"Origin, X-Requested-With, Content-Type, Accept",
"Content-Type":"application/x-protobuf",
"Content-Encoding":"gzip"
};
// Route which handles requests like the following: /<mbtiles-name>/0/1/2.pbf
module.exports = function(app) {
app.get('/:source/mbtiles/:z/:x/:y.pbf', function(req, res) {
new MBTiles(p.join(__dirname, `/public/${req.params.source}.mbtiles`), function(err, mbtiles) {
mbtiles.getTile(req.params.z, req.params.x, req.params.y, function(err, tile, headers) {
if (err) {
res.set({"Content-Type": "text/plain"});
res.status(404).send('Tile rendering error: ' + err + '\n');
} else {
res.set(header);
res.send(tile);
}
});
if (err) console.log("error opening database");
});
});
}
\ No newline at end of file
This diff is collapsed.
{
"name": "aire-cdmx",
"version": "1.0.0",
"description": "",
"main": "aire-cdmx.js",
"scripts": {
"dev": "nodemon aire-cdmx.js",
"start": "node aire-cdmx.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@mapbox/mbtiles": "^0.12.1",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.17.1",
"morgan": "^1.10.0",
"nodemon": "^2.0.7",
"path": "^0.12.7"
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Interactuar con MVTs</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" rel="stylesheet" />
<style>
body { margin: 0; padding: 0; }
#mexmap { position: absolute; top: 0; bottom: 0; width: 100%; }
#play-button { background: #F14E58; padding-right: 26px; border-radius: 3px; border: none; color: white; margin: 0; padding: 0 12px; width: 60px; cursor: pointer; height: 30px; }
#play-button:hover { background-color: #848480; }
.ticks { font-size: 10px; }
.track, .track-inset, .track-overlay { stroke-linecap: round;}
.track { stroke: #000; stroke-opacity: 0.3; stroke-width: 10px; }
.track-inset { stroke: #dcdcdc; stroke-width: 8px; }
.track-overlay { pointer-events: stroke; stroke-width: 50px; stroke: transparent; cursor: crosshair; }
.handle { fill: #fff; stroke: #000; stroke-opacity: 0.5; }
</style>
</head>
<body>
<style>
.map-overlay {
font: 12px/20px "Helvetica Neue", Arial, Helvetica, sans-serif;
position: absolute;
width: 25%;
top: 0;
left: 0;
padding: 10px;
}
.map-overlay .map-overlay-inner {
background-color: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.20);
border-radius: 3px;
padding: 10px;
margin-bottom: 10px;
}
.map-overlay h2 {
line-height: 24px;
display: block;
margin: 0 0 10px;
}
.map-overlay .legend .bar {
height: 10px;
width: 100%;
background: linear-gradient(to right,rgb(0, 228, 0), rgb(255,255,0), rgb(255, 126, 0), rgb(255, 0, 0), rgb(143, 63, 151));
/* 0, 51, 95, 135, 175*/
}
.map-overlay #slider {
background-color: transparent;
display: inline-block;
width: 100%;
position: relative;
margin: 10px auto;
cursor: ew-resize;
}
</style>
<div id="mexmap"></div>
<div class="map-overlay top">
<div class="map-overlay-inner">
<h2>Calidad del aire - O3 en 2020</h2>
<label id="datetime"></label>
<input id="slider" type="range" /> <!--min="0" max="11" step="1" value="0" /> -->
<!--<div id="slider"></div>-->
</div>
<div class="map-overlay-inner">
<div id="legend" class="legend">
<div class="bar"></div>
<div>Ozono (ppm)</div>
</div>
</div>
<button id="play-button">Play</button>
</div>
<script src="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js"></script>
<script src='https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-language/v0.10.1/mapbox-gl-language.js'></script>
<script src="https://unpkg.com/papaparse@latest/papaparse.min.js"></script>
<script src="https://unpkg.com/@turf/turf/turf.min.js"></script>
<script src="../js/functions.js"></script>
</body>
</html>
\ No newline at end of file
/*
* Copyright 2021 - All rights reserved.
* Rodrigo Tapia-McClung
*
* February 2021
*/
/* global mapboxgl, turf */
const baseUrl = new URL(window.location.href).href; // returns "http://localhost:8092/"
// use `${baseUrl}something` - don't need `${baseUrl}/something`
let cdmx, estaciones, ozono, fechas,
slider = document.getElementById("slider"),
datetimeLabel = document.getElementById("datetime"),
playBtn = document.getElementById("play-button"),
playing = false,
animate;
fetch(`${baseUrl}data/cdmx.geojson`)
.then(response => response.json())
.then(d => cdmx = d);
fetch(`${baseUrl}data/estaciones.geojson`)
.then(response => response.json())
.then(d => estaciones = d);
const papaPromise = url => {
return new Promise(function (resolve, reject) {
Papa.parse(url, {
download: true,
header: true,
skipEmptyLines: true,
complete: resolve,
});
});
}
let ozonoPromise = papaPromise(`${baseUrl}data/ozono.csv`); // ozono es en ppb: 10^12
// FIXME: check and fix date order in data
/*const getOzono = fetch(`${baseUrl}ozono.csv`)
.then(response => response.text())
.then(d => Papa.parse(d))
.catch(err => console.log(err))
getOzono.then(d => ozono = d.data);
*/
let map = new mapboxgl.Map({
"container": "mexmap",
"accessToken": "pk.eyJ1IjoiZGV2ZWxvcGdlbyIsImEiOiJja2dwcXFic20wYnJnMzBrbG11d3dwYTkyIn0.4WwFOH6C7hDQXV9obU6mAw",
"style": "mapbox://styles/mapbox/dark-v10",
"center": [-99.17, 19.36],
"zoom": 10,
"maxZoom": 20
});
map.addControl(new mapboxgl.NavigationControl());
// TODO: display mor friendly dates
const dateTimeOptions = {
weekday: "short",
day: "numeric",
month:"short",
year: "numeric",
hour: 'numeric',
minute: 'numeric',
//hour: '2-digit',
//minute: '2-digit',
hour12: false,
timeZone: 'America/Mexico_City'
};
//let myDate = "1577919600000";
let myDate = "1605218400000";
const range = (start, stop, step = 1) =>
Array(Math.ceil((stop - start) / step)).fill(start).map((x, y) => x + y * step);
const intersect = (fc1, fc2) => {
let fc = [];
fc1.features.forEach( f1 => {
fc2.features.forEach( f2 => {
let intersection = turf.intersect(f1, f2, {properties: {value: f1.properties.value}});
if (intersection != undefined) {
fc.push(intersection);
}
});
});
return turf.featureCollection(fc);
}
const makeSurface = date => {
//map.getSource("estaciones").serialize().data
// filter data for given date value
let subdata = ozono.map(d => { return {time: d["time"], [date]: d[date]} });
let maxValue = Math.max.apply(Math, subdata.map( d=> d[date]));
// assign corresponding value to each station
turf.featureEach(estaciones, point => {
let stationData = subdata.filter( i => i.time == point.properties.cve_estac);
point.properties.calidad = stationData[0] && parseFloat(stationData[0][date]) != -99 ? parseFloat(stationData[0][date]) : 0;
});
let options = {gridType: "point", property: "calidad", units: "kilometers", weight: 2};
let grid = turf.interpolate(estaciones, .75, options);
let breaks = range(2, maxValue + 1, 5);
//let lines = turf.isolines(grid, breaks, {zProperty: "calidad"});
let bValues = breaks.map( b => { return {value: b} });
let bands = turf.isobands(grid, breaks, {zProperty: "calidad", breaksProperties: bValues});
//map.getSource("interpolation").setData(lines);
//map.getSource("interpolation").setData(bands);
let intersection = intersect(bands, cdmx);
map.getSource("interpolation").setData(intersection);
}
let dateidx = fechas ? fechas.indexOf(parseInt(slider.value)) : 0;
const run = () => {
if (dateidx == fechas.length) {
clearInterval(animate);
} else {
dateidx++;
makeSurface(fechas[dateidx]);
slider.value = fechas[dateidx];
let date = new Date(fechas[dateidx]);
//datetimeLabel.textContent = new Intl.DateTimeFormat('es-MX', dateTimeOptions).format(date);
datetimeLabel.textContent = date;
}
}
map.on("style.load", async () => {
map.addSource("estaciones", {
"type": "vector",
"tiles": [`${baseUrl}estaciones/mbtiles/{z}/{x}/{y}.pbf`],
promoteId: "cve_estac"
});
map.addSource("interpolation", {
type: "geojson",
data: {
type: "FeatureCollection",
features: []
}
});
slider.addEventListener("input", e => {
var date = parseInt(e.target.value, 10);
//datetimeLabel.textContent = new Intl.DateTimeFormat('es-MX', dateTimeOptions).format(date);
datetimeLabel.textContent = new Date(parseInt(e.target.value));
dateidx = fechas.indexOf(date);
makeSurface(date);
});
playBtn.addEventListener("click", () => {
if (!playing) {
playBtn.textContent = "Pause";
playing = true;
animate = setInterval( run, 500);
} else {
playBtn.textContent = "Play";
playing = false;
clearInterval(animate);
}
})
ozonoPromise.then( results => {
let expression = ["match", ["get", "cve_estac"]];
ozono = results.data;
fechas = Object.keys(ozono[0]).slice(1,-1).map(d => parseInt(d));
slider.min = fechas[0];
slider.max = fechas[fechas.length-1];
slider.step = 3600000;
slider.value = slider.min;
datetimeLabel.textContent = new Date(fechas[0]);
//datetimeLabel.textContent = new Intl.DateTimeFormat('es-MX', dateTimeOptions).format(new Date(fechas[0]));
let maxValue = Math.max.apply(Math, ozono.map( d=> d[myDate])); // FIXME calculate real max value
results.data.forEach( row => {
map.setFeatureState(
{
// source tileset and source layer
source: "estaciones",
sourceLayer: "estaciones",
// unqiue ID row name
id: row.time
},
// Add rows you want to style/interact with
{
date: parseFloat(row[myDate])
//candidate: row.candidate,
}
);
var green = (parseFloat(row[myDate]) / maxValue) * 255;
var color = "rgba(" + 0 + ", " + green + ", " + 0 + ", 1)";
expression.push(row["time"], color);
});
expression.push("rgba(0,0,0,0)");
map.addSource("cdmx", {
"type": "geojson",
//"data": `${baseUrl}estaciones.geojson`
"data": cdmx,
//promoteId: "cve_estac"
});
map.addLayer({
id: "cdmx",
type: "line",
source: "cdmx",
paint: {
"line-color":"#088"
}
});
map.addLayer({
"id": "estaciones-circle",
"type": "circle",
"source": "estaciones",
"source-layer": "estaciones",
"layout": {},
"paint": {
"circle-radius": 4,
//"circle-color": "#088",
"circle-color": expression,
"circle-opacity": 0.5
}
});
map.addLayer({
id: "interpolation",
type: "fill",
source: "interpolation",
paint: {
"fill-color": [
"interpolate",
["linear"],
["get", "value"],
0, "rgb(0, 228, 0)",
51, "rgb(255,255,0)",
95, "rgb(255, 126, 0)",
135, "rgb(255, 0, 0)",
175, "rgb(143, 63, 151)"
],
"fill-opacity": 0,
//"fill-antialias": false // use cotnour lines or not
}
});
map.addLayer({
"id": "extrusion",
"type": "fill-extrusion",
"source": "interpolation",
"paint": {
"fill-extrusion-color": [
"interpolate",
["linear"],
["get", "value"],
0, "rgb(0, 228, 0)",
51, "rgb(255,255,0)",
95, "rgb(255, 126, 0)",
135, "rgb(255, 0, 0)",
175, "rgb(143, 63, 151)"
],
"fill-extrusion-height": ['*', ["get", "value"], 5],
"fill-extrusion-base": 0,
'fill-extrusion-height-transition':{
duration: 500,
delay: 0
},
/*"fill-extrusion-height": [
"interpolate",
["linear"],
["zoom"],
15,
0,
15.05,
["get", "value"]
],
"fill-extrusion-base": [
"interpolate",
["linear"],
["zoom"],
15,
0,
15.05,
["get", "value"]
],*/
"fill-extrusion-opacity": 0.5
}
});
makeSurface(fechas[0]);
// bring circle layer to top
map.moveLayer("estaciones-circle");
});
});
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment