Commit 8670b050 authored by Rodrigo Tapia-McClung's avatar Rodrigo Tapia-McClung

pgserver tiles and leaflet

parent 4f4e017d
body {
margin:0;
padding:0;
}
#mapmex {
position:absolute;
top:0;
bottom:0;
width:100%;
}
.picker {
left: 50px;
top: 45px;
position: absolute;
z-index: 400;
background: #cbddf3;
border: 1px solid #999;
padding: 4px;
border-radius: 5px;
}
#date-initial, #date-final {
text-align: center;
}
#datePickers{
/*display: none;*/
position: absolute;
/* width: 285px; */
z-index: 401;
background: #cbddf3;
border: 1px solid #999;
padding: 4px;
border-radius: 5px;
/*box-shadow: 0px 0px 15px #999;*/
top: 13px;
left: 50px;
}
.ui-datepicker-calendar {
display: none;
}
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Leaflet JS Example</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 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="./js/functions.js"></script>
</body>
</html>
/*
* 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;
// 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 currentTiles = {};
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(userDates => setupMap(userDates))
.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: "&#x3c;Ant",
nextText: "Sig&#x3e;",
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&eacute;rcoles", "Jueves", "Viernes", "S&aacute;bado"],
dayNamesShort: ["Dom", "Lun", "Mar", "Mi&eacute;", "Juv", "Vie", "S&aacute;b"],
dayNamesMin: ["Do", "Lu", "Ma", "Mi", "Ju", "Vi", "S&aacute;"],
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.forEach(time => {
userFiles.push(timeFormat(time));
});
resolve({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) => {
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);
map.createPane("wb-Tiles");
map.getPane("wb-Tiles").style.zIndex = 450;
// 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);
let cols = [];
indicators.forEach( (indicator) => {
cols.push("min(" + indicator + ") as min" + indicator);
cols.push("max(" + indicator + ") as max" + indicator);
});
// query db to get min/max values per month and indicator and store them in an object
userFiles.forEach( (table) => {
let minmaxQuery = "http://localhost:8090/data/query/" + table + "?columns=" + cols.join(", ");
d3.json(minmaxQuery).then(minmax => {
indicators.forEach( (indicator) => {
minIndicators[indicator] = Math.min(minIndicators[indicator], minmax[0]["min"+indicator]);
maxIndicators[indicator] = Math.max(maxIndicators[indicator], minmax[0]["max"+indicator]);
});
});
});
timeDimensionControl.addTo(map);
return new Promise( resolve => {
resolve(map);
});
}
const populateMap = (map) => {
// create mvt layers
userFiles.forEach( f => {
//f = mvtLayer(f, "area");
f = mvtLayer(f);
//abril_2018.addTo(map);
});
// style currentTiles
let option = $("#indicatorSelect").val(); // option selected from dropdrown
styleTiles(option);
let timeLayer = L.timeDimension.layer.Tile(currentTiles[userFiles[0]], {
updateTimeDimension: true,
updateTimeDimensionMode: "replace",
waitForReady: true,
duration: "P1M"
});
timeLayer.addTo(map);
Object.keys(currentTiles).forEach(layer => {
if (layer !== userFiles[0]) {
currentTiles[layer].getContainer().style.display = "none";
}
});
}
// define MVT layer for given month table and indicator
//const mvtLayer = (monthYear, indicator) => {
// define MVT layer for given month table
const mvtLayer = (monthYear) => {
let tiles = "http://localhost:8090/data/" + monthYear + "/mvt/{z}/{x}/{y}?geom_column=geom&columns=" + indicators.join();
let pbfLayer = L.vectorGrid.protobuf(tiles, {
pane: "wb-Tiles",
rendererFactory: L.canvas.tile,
/*vectorTileLayerStyles: {
[monthYear]: properties => { // use [var] to use a computed property name for monthYear...
let currentValue = properties[indicator];
let scale = chroma.scale("PuBu").padding([0.5, 0]).domain([minIndicators[indicator], maxIndicators[indicator]]).classes(5);
return {
fill: true,
fillOpacity: 0.7,
stroke: true,
weight: 1,
opacity: 0.2,
fillColor: scale(currentValue).hex(),
color: scale(currentValue).hex()
}
}
},*/
maxZoom: 22,
tolerance: 5,
extent: 4096,
buffer: 64,
debug: 0,
indexMaxZoom: 5,
indexMaxPoints: 100000,
interactive: true,
getFeatureId: f => {
return f.properties.id;
}
}).on('load', () => { // check when layer has loaded
// return promise to check when all layers have
// loaded and then can remove loader mask
//resolve("Tiles loaded!");
});
currentTiles[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);
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(currentTiles).forEach(layer => {
if (layer !== monthYear) {
currentTiles[layer].getContainer().style.display = "none";
} else {
this._baseLayer = currentTiles[layer];
this._baseLayer.getContainer().style.display = "block";
}
});
//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)
// .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 => {
// define color scale domain based on min-max values for selected indicator
let domain = [minIndicators[option], maxIndicators[option]];
let scale = chroma.scale("PuBu").padding([0.5, 0]).domain(domain).classes(5);
Object.keys(currentTiles).forEach(layer => { // change style for each tile layer
currentTiles[layer].options.vectorTileLayerStyles[layer] = (properties, zoom) => {
let selectedIndicator = properties[option]; // choose column to style with
return {
fill: true,
fillOpacity: 0.5,
stroke: true,
weight: 1,
opacity: 0.5,
fillColor: scale(selectedIndicator).hex(),
color: scale(selectedIndicator).hex()
}
}
// redraw() causes additional tile requests... not good!
currentTiles[layer].redraw();
currentTiles[layer].addTo(map).setZIndex(4); // add tiles to map
});
return Promise.resolve(scale);
}
// TODO: add ordered date selector based on what's available on the DB
// TODO: add layer control
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