Commit 87e5fa65 authored by Rodrigo Tapia-McClung's avatar Rodrigo Tapia-McClung

pgserver tiles with mapbox-gl-leaflet

parent 8670b050
/*
* Copyright 2019 - All rights reserved.
* Rodrigo Tapia-McClung
*
* August 2019
*/
/* global Promise, chroma */
let timeParse,
timeFormat,
timeDimensionControl,
userFiles = [],
monthArray = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"],
dateArray = [],
dateMin,
dateMax,
minUserDate,
maxUserDate,
userDates,
map,
glmap,
timeLayer;
// define empty objects and indicators
let maxIndicators = {},
minIndicators = {},
indicators = ["area", "perimeter", "costa", "df"],
indicatorsNames = ["Área", "Perímetro", "Desarrollo de la línea de costa", "Dimensión fractal"];
let cols = [];
indicators.forEach( (indicator) => {
cols.push("min(" + indicator + ") as min" + indicator);
cols.push("max(" + indicator + ") as max" + indicator);
});
let currentTiles = {},
allTiles = {};
d3.json("https://unpkg.com/d3-time-format@2.1.1/locale/es-MX.json").then(locale => {
d3.timeFormatDefaultLocale(locale);
timeParse = d3.timeParse("%B_%Y");
timeFormat = d3.timeFormat("%B_%Y");
setupTimeDimensionControl();
setupDates()
.then(dates => populateDates(dates))
.then(userData => setupMap(userData))
.then(map => populateMap(map));
});
const sortInitialDateAscending = (a, b) => {
// Dates will be cast to numbers automagically:
return a - b;
}
// query available dates on DB
const setupDates = () => {
return new Promise( resolve => {
let layersQuery = "http://localhost:8090/data/list_layers";
d3.json(layersQuery).then(layers => {
layers.forEach(layer => {
dateArray.push(timeParse(layer.f_table_name)); // convert filenames to dates
})
dateArray = dateArray.sort(sortInitialDateAscending); // order dates
dateMin = d3.min(dateArray);
dateMax = d3.max(dateArray);
//userFiles = dateArray.map( month => timeFormat(month)); // order table names by date
const dates = {min:dateMin, max:dateMax, dates:dateArray};
resolve(dates);
});
});
}
const populateDates = (dates) => { // fill out date pickers with available dates
return new Promise( resolve => {
$.datepicker.regional["es"] = {
closeText: "Cerrar",
prevText: "<Ant",
nextText: "Sig>",
currentText: "Hoy",
monthNames: ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio",
"Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"
],
monthNamesShort: ["Ene", "Feb", "Mar", "Abr", "May", "Jun",
"Jul", "Ago", "Sep", "Oct", "Nov", "Dic"
],
dayNames: ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"],
dayNamesShort: ["Dom", "Lun", "Mar", "Mié", "Juv", "Vie", "Sáb"],
dayNamesMin: ["Do", "Lu", "Ma", "Mi", "Ju", "Vi", "Sá"],
weekHeader: "Sm",
dateFormat: "yy/mm/dd",
firstDay: 1,
isRTL: false,
showMonthAfterYear: false,
yearSuffix: ""
}
$.datepicker.setDefaults($.datepicker.regional["es"]);
// month pickers
$("#date-initial").datepicker({
minDate: dates.min,
maxDate: dates.max,
defaultDate: dates.min,
changeMonth: true,
changeYear: true,
showButtonPanel: true,
dateFormat: "M yy",
onClose: function() {
let month = $("#ui-datepicker-div .ui-datepicker-month :selected").val();
let year = $("#ui-datepicker-div .ui-datepicker-year :selected").val();
minUserDate = new Date(year, month, 1); // initial date
$(this).datepicker("setDate", minUserDate);
$("#date-final").datepicker("option", {
"minDate": minUserDate,
disabled: false
});
// hack to avoid needing to change date twice in second datepicker
setTimeout(() => { $("#date-final").datepicker("show") }, 10);
},
beforeShow: el => {
$("#ui-datepicker-div").toggleClass("hide-calendar", $(el).is("[data-calendar=\"false\"]"));
}
});
$("#date-final").datepicker({
maxDate: dates.max,
defaultDate: dates.max,
changeMonth: true,
changeYear: true,
showButtonPanel: true,
disabled: true,
dateFormat: "M yy",
onClose: function() {
let month = $("#ui-datepicker-div .ui-datepicker-month :selected").val();
let year = $("#ui-datepicker-div .ui-datepicker-year :selected").val();
maxUserDate = new Date(year, month, 1); // final date
$(this).datepicker("setDate", maxUserDate);
// use .setUTCHours(6,0,0) to adjust DST offset fror some months
let startUserDate = new Date(minUserDate.setUTCHours(6,0,0));
let endUserDate = new Date(maxUserDate.setUTCHours(6,0,0));
// pass new timeinterval to timeDimension player
userDates = L.TimeDimension.Util.explodeTimeRange(startUserDate, endUserDate, "P1M");
userFiles = userDates.map( month => timeFormat(month)); // order table names by date
// TODO: on close trigger update map
if (!map) {
resolve({min: startUserDate, max: endUserDate});
} else {
// FIXME: no need to pass data here?
updateMap({map: map, min: startUserDate, max: endUserDate});
}
},
beforeShow: (el, inst) => {
inst.input.datepicker("refresh");
$("#ui-datepicker-div").toggleClass("hide-calendar", $(el).is("[data-calendar=\"false\"]"));
}
});
})
}
const setupMap = (dates) => {
return new Promise( resolve => {
map = L.map("mapmex", {
center: [17.22, -92.28],
minZoom: 7,
zoom: 7,
timeDimension: true,
timeDimensionOptions: {
times: userDates,
currentTime: dates.min
}
});
let cartoDarkLayer = L.tileLayer("https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: "Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
});
cartoDarkLayer.addTo(map);
// initialize min and max objects to hold values for each indicator
// and add select options
indicators.forEach( (indicator, index) => {
maxIndicators[indicator] = 0;
minIndicators[indicator] = 1e30;
$("#indicatorSelect").append("<option value=\""+ indicator + "\">" + indicatorsNames[index] + "</option>");
});
console.log(userFiles);
// query db to get min/max values per month and indicator and store them in an object
queryFiles().then( minmax => {
minmax.map( minmaxMonth => {
indicators.forEach( (indicator) => {
minIndicators[indicator] = Math.min(minIndicators[indicator], minmaxMonth["min" + indicator]);
maxIndicators[indicator] = Math.max(maxIndicators[indicator], minmaxMonth["max" + indicator]);
});
});
timeDimensionControl.addTo(map);
resolve({"map": map, "minIndicators": minIndicators, "maxIndicators": maxIndicators});
});
});
}
const flatten = arrays => {
return [].concat(arrays);
}
const queryFiles = () => {
return Promise.all(userFiles.map(getMinMax))
// the result is an array of arrays, so we'll flatten them here
.then(flatten);
}
const getMinMax = table => {
return new Promise( resolve => {
let minmaxQuery = "http://localhost:8090/data/query/" + table + "?columns=" + cols.join(", ");
d3.json(minmaxQuery).then( minmax => {
resolve(minmax[0]);
});
});
}
const updateMap = (mapData) => {
//console.log(mapData);
console.log(userFiles);
// ckear tiles
currentTiles = {};
//retrieve or create tiles for current dates
userFiles.forEach( monthYear => {
if (Object.keys(allTiles).includes(monthYear)) {
currentTiles[monthYear] = allTiles[monthYear]; // recover tile if it has already been created
//currentJSONs[monthYear] = allJSONs[monthYear]; // recover json if it has already been created
return; // if file has already been processed, exit
} else { // if file cannot be found in allTiles, then add 1 to the number of files to process
let newTile = mapboxLayer(monthYear);
glmap._glMap.addLayer(newTile);
if (monthYear == userFiles[0]) {
glmap._glMap.setPaintProperty(monthYear, 'fill-opacity', 0.7)
}
}
});
// update timeDimension times
timeLayer._timeDimension.setAvailableTimes(userDates, "replace");
//timeLayer._timeDimension.setCurrentTime(mapData.min);
// clear minmax indicators objects
maxIndicators = {},
minIndicators = {};
indicators.forEach( (indicator) => {
maxIndicators[indicator] = 0;
minIndicators[indicator] = 1e30;
});
// query db for new minmax values then style tiles
new Promise( resolve => {
queryFiles().then( minmax => {
minmax.map( minmaxMonth => {
indicators.forEach( (indicator) => {
minIndicators[indicator] = Math.min(minIndicators[indicator], minmaxMonth["min" + indicator]);
maxIndicators[indicator] = Math.max(maxIndicators[indicator], minmaxMonth["max" + indicator]);
});
});
resolve({"map": map, "minIndicators": minIndicators, "maxIndicators": maxIndicators});
})
}).then( values => { // once we have new minmax values, style all tiles
let option = $("#indicatorSelect").val(), // option selected from dropdrown
min = values.minIndicators,
max = values.maxIndicators;
styleTiles(option, min, max);
});
}
const populateMap = (mapData) => {
let map = mapData.map,
minIndicators = mapData.minIndicators,
maxIndicators = mapData.maxIndicators;
// create mvt layers
userFiles.forEach( f => {
f = mapboxLayer(f);
});
glmap = L.mapboxGL({
accessToken: 'no-token',
style: {
'version': 8,
'sources': {},
'layers': []
}
}).addTo(map);
// after mapboxGL map is ready with styles do this:
glmap._glMap.on("style.load", () => {
userFiles.forEach(monthYear => {
glmap._glMap.addLayer(currentTiles[monthYear]);
});
Object.keys(allTiles).forEach(layer => {
if (layer == userFiles[0]) {
glmap._glMap.setPaintProperty(layer, 'fill-opacity', 0.7)
}
});
// Pass dummy geojson layer to timeDimension in order to register and sync
timeLayer = L.timeDimension.layer.Tile(L.geoJSON(), {
updateTimeDimension: true,
updateTimeDimensionMode: "replace",
waitForReady: true,
duration: "P1M"
});
timeLayer.addTo(map);
// style currentTiles
let option = $("#indicatorSelect").val(); // option selected from dropdrown
styleTiles(option, minIndicators, maxIndicators);
});
}
// define MVT layer for given month table and all indicators
const mapboxLayer = (monthYear) => {
let pbfLayer = {
id: monthYear,
source: {
type: 'vector',
tiles: ["http://localhost:8090/data/" + monthYear + "/mvt/{z}/{x}/{y}?geom_column=geom&columns=" + indicators.join()],
maxzoom: 14,
minzoom: 5
},
'source-layer': monthYear,
type: 'fill',
minzoom: 5,
'paint': {
'fill-opacity': 0,
'fill-color': [
'interpolate',
['linear'],
['get', 'df'],
1, 'rgba(255, 0, 0, 0.5)', // red
1.3, 'rgba(0, 255, 0, 0.5)', // green
]/*,
'fill-outline-color': [
'interpolate',
['linear'],
['get', 'df'],
1, 'rgba(255, 0, 0, 0.6)', // red
1.3, 'rgba(0, 255, 0, 0.6)', // green
]*/
}
}
currentTiles[monthYear] = pbfLayer;
allTiles[monthYear] = pbfLayer;
return pbfLayer;
}
const setupTimeDimensionControl = () => {
L.Control.TimeDimensionCustom = L.Control.TimeDimension.extend({
_getDisplayDateFormat: date => {
let d = new Date(date);
let year = d.getFullYear().toString();
let month = d.getUTCMonth();
return monthArray[month] + " " + year;
}
});
timeDimensionControl = new L.Control.TimeDimensionCustom({
loopButton: true,
/*minSpeed: 1,
maxSpeed: 5,*/
timeSteps: 1,
playReverseButton: true,
//limitSliders: true,
playerOptions: {
//buffer: 5,
//minBufferReady: 5,
transitionTime: 125,
loop: true
},
timeZones: ["Local"]
});
}
L.TimeDimension.Layer.Tile = L.TimeDimension.Layer.extend({
_setAvailableTimes: function () {
if (this.options.times) {
return L.TimeDimension.Util.parseTimesExpression(this.options.times);
} else if (this.options.timeInterval) {
let tiArray = L.TimeDimension.Util.parseTimeInterval(this.options.timeInterval);
let period = this.options.period || "P1D";
let validTimeRange = this.options.validTimeRange || undefined;
//alert("times");
return L.TimeDimension.Util.explodeTimeRange(tiArray[0], tiArray[1], period, validTimeRange);
} else {
return [];
}
},
onAdd: function (map) {
// Don't call prototype so this_update() does not get called
//L.TimeDimension.Layer.prototype.onAdd.call(this, map);
this._map = map;
if (!this._timeDimension && map.timeDimension) {
this._timeDimension = map.timeDimension;
}
this._timeDimension.on("timeloading", this._onNewTimeLoading, this);
this._timeDimension.on("timeload", this._update, this);
this._timeDimension.registerSyncedLayer(this);
// Remove base layer caue mapbox layer is not an L layer...
map.addLayer(this._baseLayer);
// Don't update on add. Rather check @708 and what happens there
//this._update();
},
onRemove: function (map) {
this._timeDimension.unregisterSyncedLayer(this);
this._timeDimension.off("timeloading", this._onNewTimeLoading, this);
this._timeDimension.off("timeload", this._update, this);
this._baseLayer.getContainer().style.display = "none";
//this.eachLayer(map.removeLayer, map);
//this._map = null;
},
isReady: function (time) {
// to be implemented for each type of layer
return true;
},
_update: function () {
if (!this._baseLayer || !this._map) {
return;
}
var time = this._timeDimension.getCurrentTime();
// get data for time
let d = new Date(time),
year = d.getFullYear().toString(),
m = d.getUTCMonth(),
month = monthArray[m].toLowerCase(),
monthYear = month + "_" + year;
// Update title
//let title = $("#title");
//title.html("<h2>Cobertura de agua en la cuenca del r&iacute;o Grijalva en " + month + " de " + year + "</h2>");
// Update graphs only on timeload event
//indicators.forEach( indicator => {
// indicatorVars[indicator].chartData = indicatorVars[indicator].chart.data(); // get chart data
// indicatorVars[indicator].chart.data(indicatorVars[indicator].chartData); // set chart data
//});
//console.time("process");
console.log("data for", monthYear);
//console.log(currentTiles)
Object.keys(allTiles).forEach(layer => {
if (layer !== monthYear) { // hide all other months
glmap._glMap.setPaintProperty(layer, 'fill-opacity', 0);
} else { // except current one
glmap._glMap.setPaintProperty(layer, 'fill-opacity', 0.7);
}
});
//console.timeEnd("process");
}
});
L.timeDimension.layer.Tile = (layer, options) => {
return new L.TimeDimension.Layer.Tile(layer, options);
};
// When selecting indicator from dropdown, style tiles.
$("#indicatorSelect").on("change", function() {
// style currentTiles
let option = this.value; // option selected from dropdrown
styleTiles(option, minIndicators, maxIndicators);
// .then(legend.addTo(map)); // add legend control -> it updates
// FIXME: re-adding control updates its contents... why?
// Highlight plot title according to selected option
//indicators.forEach( indicator => {
// d3.select(indicatorVars[indicator].container).select("svg text.title").classed("active", indicator === option ? true : false);
//});
});
const styleTiles = (option, minIndicators, maxIndicators) => {
// define color scale domain based on min-max values for selected indicator
let domain = [minIndicators[option], maxIndicators[option]];
//console.log(domain)
let scale = chroma.scale("PuBu").padding([0.5, 0]).domain(domain).classes(5);
Object.keys(currentTiles).forEach(layer => {
let color = [
'interpolate',
['linear'],
['get', option],
minIndicators[option], scale(minIndicators[option]).hex(),
maxIndicators[option], scale(maxIndicators[option]).hex()
];
glmap._glMap.setPaintProperty(layer, 'fill-color', color);
});
return Promise.resolve(scale);
}
// TODO: add layer control
// TODO: add charts
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<title>Mapbox GL JS Examples</title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<link rel="stylesheet" type="text/css" href="http://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css" />
<link rel="stylesheet" href="https://cdn.rawgit.com/socib/Leaflet.TimeDimension/master/dist/leaflet.timedimension.control.min.css" />
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.1.0/mapbox-gl.css' rel='stylesheet' />
<link rel="stylesheet" href="./css/map.css" type="text/css">
</head>
<body>
<div id='mapmex'></div>
<div class="picker">
<select id="indicatorSelect"></select>
</div>
<div id="datePickers">
<input type="text" name="date-initial" id="date-initial" readonly="readonly" size="12"
placeholder="Fecha inicial" data-calendar="false" />
<input type="text" name="date-final" id="date-final" readonly="readonly" size="12"
placeholder="Fecha final" data-calendar="true" />
</div>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.vectorgrid@latest/dist/Leaflet.VectorGrid.bundled.js"></script>
<script type="text/javascript" src="https://cdn.rawgit.com/nezasa/iso8601-js-period/master/iso8601.min.js"></script>
<script type="text/javascript" src="https://cdn.rawgit.com/socib/Leaflet.TimeDimension/master/dist/leaflet.timedimension.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.0.3/chroma.min.js"></script>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v1.1.0/mapbox-gl.js'></script>
<script src="https://unpkg.com/mapbox-gl-leaflet/leaflet-mapbox-gl.js"></script>
<script src="./js/functions_mapbox.js"></script>
<!--<script>
var map = new mapboxgl.Map({
'container': 'mapmex',
'zoom': 7,
'center': [-92.28, 17.22], // cuenca Grijalva
'style': {
'version': 8,
'sources': {
'carto-dark': {
'type': 'raster',
'tiles': [
"http://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
"http://b.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
"http://c.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
"http://d.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"
]
}
},
'layers': [{
'id': 'carto-dark-layer',
'type': 'raster',
'source': 'carto-dark',
'minzoom': 0,
'maxzoom': 22
}]
}
});
map.addControl(new mapboxgl.NavigationControl());
map.on('load', function() {
map.addLayer({
id: 'abril_2018-tiles',
source: {
type: 'vector',
// "http://localhost:8090/data/" + monthYear + "/mvt/{z}/{x}/{y}?geom_column=geom&columns=" + indicators.join();
tiles: ['http://localhost:8090/data/abril_2018/mvt/{z}/{x}/{y}?geom_column=geom&columns=df'],
maxzoom: 14,
minzoom: 5
},
'source-layer': 'abril_2018',
type: 'fill',
minzoom: 5,
'paint': {
'fill-opacity': 0.7,
'fill-color': [
'interpolate',
['linear'],
['get', 'df'],
1, 'rgba(255, 0, 0, 0.5)', // red
1.3, 'rgba(0, 255, 0, 0.5)', // green
],
'fill-outline-color': [
'interpolate',
['linear'],
['get', 'df'],
1, 'rgba(255, 0, 0, 0.6)', // red
1.3, 'rgba(0, 255, 0, 0.6)', // green
]
}
})
})
</script>-->
</body>
</html>
\ 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