2018-09-30 14:50:09 -04:00
|
|
|
/**
|
2019-02-24 08:34:40 -05:00
|
|
|
* Render tiles
|
2018-09-30 14:50:09 -04:00
|
|
|
*
|
2019-04-27 08:26:03 -04:00
|
|
|
* Use mapnik to render map tiles from the database
|
|
|
|
*
|
|
|
|
* Styles have two sources of truth for colour ranges (could generate from single source?)
|
|
|
|
* - XML style definitions in app/map_styles/polygon.xml
|
|
|
|
* - front-end legend in app/src/frontend/legend.js
|
|
|
|
*
|
|
|
|
* Data is provided by the queries in MAP_STYLE_TABLE_DEFINITIONS below.
|
|
|
|
*
|
2018-09-30 14:50:09 -04:00
|
|
|
*/
|
2018-09-10 05:44:32 -04:00
|
|
|
import path from 'path';
|
|
|
|
import mapnik from 'mapnik';
|
|
|
|
import SphericalMercator from '@mapbox/sphericalmercator';
|
|
|
|
|
2018-09-30 11:18:34 -04:00
|
|
|
// connection details from environment variables
|
2018-09-10 05:44:32 -04:00
|
|
|
const DATASOURCE_CONFIG = {
|
2018-09-30 11:18:34 -04:00
|
|
|
'host': process.env.PGHOST,
|
|
|
|
'dbname': process.env.PGDATABASE,
|
|
|
|
'user': process.env.PGUSER,
|
|
|
|
'password': process.env.PGPASSWORD,
|
|
|
|
'port': process.env.PGPORT,
|
2018-09-10 05:44:32 -04:00
|
|
|
'geometry_field': 'geometry_geom',
|
2019-02-24 14:28:11 -05:00
|
|
|
'extent': '-20005048.4188,-9039211.13765,19907487.2779,17096598.5401',
|
2018-09-10 05:44:32 -04:00
|
|
|
'srid': 3857,
|
|
|
|
'type': 'postgis'
|
|
|
|
}
|
|
|
|
|
|
|
|
const TILE_SIZE = 256
|
|
|
|
const TILE_BUFFER_SIZE = 64
|
|
|
|
const PROJ4_STRING = '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs +over';
|
|
|
|
|
2019-02-24 08:34:40 -05:00
|
|
|
// Mapnik uses table definitions to query geometries and attributes from PostGIS.
|
|
|
|
// The queries here are eventually used as subqueries when Mapnik fetches data to render a
|
2019-05-27 13:26:29 -04:00
|
|
|
// tile - so given a table definition like:
|
|
|
|
// (SELECT geometry_geom FROM geometries) as def
|
2019-02-24 08:34:40 -05:00
|
|
|
// Mapnik will wrap it in a bbox query and PostGIS will eventually see something like:
|
|
|
|
// SELECT AsBinary("geometry") AS geom from
|
2019-05-27 13:26:29 -04:00
|
|
|
// (SELECT geometry_geom FROM geometries) as def
|
2019-02-24 08:34:40 -05:00
|
|
|
// WHERE "geometry" && SetSRID('BOX3D(0,1,2,3)'::box3d, 3857)
|
|
|
|
// see docs: https://github.com/mapnik/mapnik/wiki/OptimizeRenderingWithPostGIS
|
|
|
|
const MAP_STYLE_TABLE_DEFINITIONS = {
|
|
|
|
base_light: `(
|
|
|
|
SELECT
|
|
|
|
b.location_number as location_number,
|
|
|
|
g.geometry_geom
|
|
|
|
FROM
|
|
|
|
geometries as g,
|
|
|
|
buildings as b
|
|
|
|
WHERE
|
|
|
|
g.geometry_id = b.geometry_id
|
|
|
|
) as outline`,
|
|
|
|
base_night: `(
|
|
|
|
SELECT
|
|
|
|
b.location_number as location_number,
|
|
|
|
g.geometry_geom
|
|
|
|
FROM
|
|
|
|
geometries as g,
|
|
|
|
buildings as b
|
|
|
|
WHERE
|
|
|
|
g.geometry_id = b.geometry_id
|
|
|
|
) as outline`,
|
|
|
|
date_year: `(
|
|
|
|
SELECT
|
|
|
|
b.date_year as date_year,
|
|
|
|
g.geometry_geom
|
|
|
|
FROM
|
|
|
|
geometries as g,
|
|
|
|
buildings as b
|
|
|
|
WHERE
|
|
|
|
g.geometry_id = b.geometry_id
|
|
|
|
) as outline`,
|
|
|
|
size_storeys: `(
|
|
|
|
SELECT
|
|
|
|
(
|
|
|
|
coalesce(b.size_storeys_attic, 0) +
|
|
|
|
coalesce(b.size_storeys_core, 0)
|
|
|
|
) as size_storeys,
|
|
|
|
g.geometry_geom
|
|
|
|
FROM
|
|
|
|
geometries as g,
|
|
|
|
buildings as b
|
|
|
|
WHERE
|
|
|
|
g.geometry_id = b.geometry_id
|
|
|
|
) as outline`,
|
|
|
|
location: `(
|
|
|
|
SELECT
|
|
|
|
(
|
|
|
|
case when b.location_name is null then 0 else 1 end +
|
|
|
|
case when b.location_number is null then 0 else 1 end +
|
|
|
|
case when b.location_street is null then 0 else 1 end +
|
|
|
|
case when b.location_line_two is null then 0 else 1 end +
|
|
|
|
case when b.location_town is null then 0 else 1 end +
|
|
|
|
case when b.location_postcode is null then 0 else 1 end +
|
|
|
|
case when b.location_latitude is null then 0 else 1 end +
|
|
|
|
case when b.location_longitude is null then 0 else 1 end +
|
|
|
|
case when b.ref_toid is null then 0 else 1 end +
|
|
|
|
case when b.ref_osm_id is null then 0 else 1 end
|
|
|
|
) as location_info_count,
|
|
|
|
g.geometry_geom
|
|
|
|
FROM
|
|
|
|
geometries as g,
|
|
|
|
buildings as b
|
|
|
|
WHERE
|
|
|
|
g.geometry_id = b.geometry_id
|
|
|
|
) as location`,
|
|
|
|
likes: `(
|
|
|
|
SELECT
|
|
|
|
g.geometry_geom,
|
|
|
|
b.likes_total as likes
|
|
|
|
FROM
|
|
|
|
geometries as g,
|
|
|
|
buildings as b
|
|
|
|
WHERE
|
|
|
|
g.geometry_id = b.geometry_id
|
|
|
|
AND b.likes_total > 0
|
|
|
|
) as location`,
|
|
|
|
conservation_area: `(
|
|
|
|
SELECT
|
|
|
|
g.geometry_geom
|
|
|
|
FROM
|
|
|
|
geometries as g,
|
|
|
|
buildings as b
|
|
|
|
WHERE
|
|
|
|
g.geometry_id = b.geometry_id
|
|
|
|
AND b.planning_in_conservation_area = true
|
|
|
|
) as conservation_area`
|
|
|
|
}
|
|
|
|
|
2018-09-30 11:18:34 -04:00
|
|
|
// register datasource adapters for mapnik database connection
|
2019-05-27 11:20:00 -04:00
|
|
|
if (mapnik.register_default_input_plugins) {
|
|
|
|
mapnik.register_default_input_plugins();
|
|
|
|
}
|
2018-09-30 11:18:34 -04:00
|
|
|
// register fonts for text rendering
|
2018-09-11 18:29:30 -04:00
|
|
|
mapnik.register_default_fonts();
|
2018-09-10 05:44:32 -04:00
|
|
|
|
|
|
|
const mercator = new SphericalMercator({
|
|
|
|
size: TILE_SIZE
|
|
|
|
});
|
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
function getBbox(z, x, y) {
|
2019-02-24 14:23:00 -05:00
|
|
|
return mercator.bbox(x, y, z, false, '900913');
|
|
|
|
}
|
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
function getXYZ(bbox, z) {
|
2019-02-24 14:23:00 -05:00
|
|
|
return mercator.xyz(bbox, z, false, '900913')
|
2019-01-22 17:37:44 -05:00
|
|
|
}
|
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
function renderTile(tileset, z, x, y, geometryId, cb) {
|
|
|
|
const bbox = getBbox(z, x, y)
|
2018-09-10 05:44:32 -04:00
|
|
|
|
|
|
|
const map = new mapnik.Map(TILE_SIZE, TILE_SIZE, PROJ4_STRING);
|
|
|
|
map.bufferSize = TILE_BUFFER_SIZE;
|
|
|
|
const layer = new mapnik.Layer('tile', PROJ4_STRING);
|
2019-02-24 08:34:40 -05:00
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
const tableDefinition = (tileset === 'highlight') ?
|
|
|
|
getHighlightTableDefinition(geometryId)
|
2019-02-24 08:34:40 -05:00
|
|
|
: MAP_STYLE_TABLE_DEFINITIONS[tileset];
|
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
const conf = Object.assign({ table: tableDefinition }, DATASOURCE_CONFIG)
|
2018-09-10 05:44:32 -04:00
|
|
|
|
2018-09-13 18:55:53 -04:00
|
|
|
var postgis;
|
|
|
|
try {
|
|
|
|
postgis = new mapnik.Datasource(conf);
|
|
|
|
layer.datasource = postgis;
|
2019-02-24 08:34:40 -05:00
|
|
|
layer.styles = [tileset]
|
2018-09-10 05:44:32 -04:00
|
|
|
|
2018-09-13 18:55:53 -04:00
|
|
|
map.load(
|
|
|
|
path.join(__dirname, '..', 'map_styles', 'polygon.xml'),
|
|
|
|
{ strict: true },
|
2019-02-24 14:28:11 -05:00
|
|
|
function (err, map) {
|
2019-05-27 11:20:00 -04:00
|
|
|
if (err) {throw err}
|
2018-09-10 05:44:32 -04:00
|
|
|
|
2018-09-13 18:55:53 -04:00
|
|
|
map.add_layer(layer)
|
|
|
|
const im = new mapnik.Image(map.width, map.height)
|
|
|
|
map.extent = bbox
|
2019-02-24 08:49:16 -05:00
|
|
|
map.render(im, {}, (err, rendered) => {
|
2019-05-27 11:20:00 -04:00
|
|
|
if (err) {throw err}
|
2019-02-24 08:49:16 -05:00
|
|
|
rendered.encode('png', cb)
|
|
|
|
});
|
2018-09-13 18:55:53 -04:00
|
|
|
}
|
|
|
|
)
|
2019-02-24 14:28:11 -05:00
|
|
|
} catch (err) {
|
2018-09-13 18:55:53 -04:00
|
|
|
console.error(err);
|
|
|
|
}
|
2018-09-10 05:44:32 -04:00
|
|
|
}
|
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
// highlight single geometry, requires geometryId in the table query
|
|
|
|
function getHighlightTableDefinition(geometryId) {
|
2019-02-24 08:34:40 -05:00
|
|
|
return `(
|
|
|
|
SELECT
|
|
|
|
g.geometry_geom
|
|
|
|
FROM
|
2019-02-24 14:22:18 -05:00
|
|
|
geometries as g
|
2019-02-24 08:34:40 -05:00
|
|
|
WHERE
|
2019-05-27 13:26:29 -04:00
|
|
|
g.geometry_id = ${geometryId}
|
2019-02-24 08:34:40 -05:00
|
|
|
) as highlight`
|
|
|
|
}
|
|
|
|
|
2019-05-27 13:26:29 -04:00
|
|
|
export { getBbox, getXYZ, renderTile, TILE_SIZE };
|