@conorpai/tilestrata-postgismvt
Version:
A TileStrata plugin for reading Mapbox Vector Tiles from a PostGIS database
251 lines (233 loc) • 9.01 kB
JavaScript
var pg = require('pg');
var zlib = require('zlib');
const SQL = require('sql-template-strings');
module.exports = function(options) {
var pgPool;
var lyr;
/**
* Initializes the layer config and the PostgreSQL datasource.
*
* @param {TileServer} server
* @param {function} callback(err, fn)
* @return {void}
*/
function initialize(server, callback) {
lyr = options.lyr;
pgPool = new pg.Pool(options.pgConfig);
pgPool.on('error', function (err, client) {
console.error(err.message, err.stack);
var err = new Error('idle client error');
err.statusCode = 500;
callback(err);
});
if ((lyr.mode === 'cluster' || lyr.mode === 'cluster_aio' || lyr.mode === 'cluster_fields' || (typeof lyr.mode === 'function')) && lyr.type != 'circle') {
var err = new Error('Clustering and mode functions can only be used in conjunction with point data');
err.statusCode = 422;
callback(err);
} else {
callback(null);
}
}
function tileBBox(z, x, y, srid) {
if (srid == 3857) {
var max = 20037508.34;
var res = (max * 2) / Math.pow(2, z);
var minx = -max + (x * res);
var miny = max - (y * res) - res;
var maxx = -max + (x * res) + res;
var maxy = max - (y * res);
var minlng = minx / max * 180;
var minlat = 180 / Math.PI * (2 * Math.atan(Math.exp((miny / max * 180) * Math.PI / 180)) - Math.PI / 2);
var maxlng = maxx / max * 180;
var maxlat = 180 / Math.PI * (2 * Math.atan(Math.exp((maxy / max * 180) * Math.PI / 180)) - Math.PI / 2);
return [minlng, minlat, maxlng, maxlat];
} else {
var orix = -180;
var oriy = 90;
var res = 1.40625 * 256 / Math.pow(2, z);
var minlng = orix + (x * res);
var minlat = oriy - (y * res) - res;
var maxlng = orix + (x * res) + res;
var maxlat = oriy - (y * res);
return [minlng, minlat, maxlng, maxlat];
}
}
/**
* Creates a tile and returns the result as a Mapbox Vector Tile,
* plus the headers that should accompany it.
*
* @param {TileServer} server
* @param {TileRequest} tile
* @param {function} callback(err, buffer, headers)
* @return {void}
*/
function serveMVT(server, tile, callback) {
if (tile.z < lyr.minZoom || tile.z > lyr.maxZoom) {
err = new Error('Request out of zoom level bounds');
err.statusCode = 204;
return callback(err);
}
if (lyr.extent != undefined) {
var reqExtent = tileBBox(tile.z, tile.x, tile.y, lyr.srid);
if (reqExtent[2] < lyr.extent[0]
|| reqExtent[0] > lyr.extent[2]
|| reqExtent[3] < lyr.extent[1]
|| reqExtent[1] > lyr.extent[3]) {
err = new Error('Request out of data bounds');
return callback(err);
}
}
var fields = lyr.fields ? ', ' + lyr.fields.split(' ') : '';
var clip_geom = (lyr.buffer > 0) ? true : false;
var resolution = (typeof lyr.resolution === 'function') ? lyr.resolution(server, tile) : lyr.resolution;
var mode = (typeof lyr.mode === 'function') ? lyr.mode(server, tile) : lyr.mode;
var datasrid = lyr.datasrid ? lyr.datasrid : lyr.srid;
var layername = lyr.name ? lyr.name : tile.layer;
var tileBBoxFunc = lyr.srid == 3857 ? 'TileBBox_3857' : 'TileBBox';
var query;
switch (mode) {
case "cluster":
var agg_q_name = 'mvt_geo';
query = `
SELECT ST_AsMVT(q, '${layername}', ${resolution}, 'geom') AS mvt FROM (
WITH ${agg_q_name} AS (
SELECT 1 cnt, ST_AsMVTGeom(ST_Transform(${lyr.table}.${lyr.geometry}, ${lyr.srid}), ${tileBBoxFunc}(${tile.z}, ${tile.x}, ${tile.y}, ${lyr.srid}), ${resolution}, ${lyr.buffer}, ${clip_geom}) geom
FROM ${lyr.table}
WHERE ST_Intersects(${tileBBoxFunc}(${tile.z}, ${tile.x}, ${tile.y}, ${datasrid}), ${lyr.table}.${lyr.geometry})
)
SELECT COUNT(${agg_q_name}.cnt), ${agg_q_name}.geom
FROM ${agg_q_name}
GROUP BY ${agg_q_name}.geom
) AS q
`;
break;
case "cluster_aio":
var agg_q_name = 'mvt_geo';
query = `
SELECT ST_AsMVT(q, '${layername}', ${resolution}, 'geom') AS mvt FROM (
WITH ${agg_q_name} AS (
SELECT 1 cnt, ${lyr.table}.${lyr.geometry} geom
FROM ${lyr.table}
WHERE ST_Intersects(${tileBBoxFunc}(${tile.z}, ${tile.x}, ${tile.y}, ${datasrid}), ${lyr.table}.${lyr.geometry})
)
SELECT COUNT(${agg_q_name}.cnt), ST_AsMVTGeom(ST_Centroid(ST_Extent(${agg_q_name}.geom)), ${tileBBoxFunc}(${tile.z}, ${tile.x}, ${tile.y}, ${lyr.srid}), ${resolution}, ${lyr.buffer}, ${clip_geom}) geom
FROM ${agg_q_name}
GROUP BY cnt
) AS q
`;
break;
case "to_point":
query = `
SELECT ST_AsMVT(q, '${layername}', ${resolution}, 'geom') AS mvt FROM (
WITH a AS (
SELECT ST_AsMVTGeom(
ST_Transform(ST_Centroid(${lyr.table}.${lyr.geometry}), ${lyr.srid}),
${tileBBoxFunc}(${tile.z}, ${tile.x}, ${tile.y}, ${lyr.srid}),
${resolution},
${lyr.buffer},
${clip_geom} ) geom ${fields}
FROM ${lyr.table}
WHERE ST_Intersects(${tileBBoxFunc}(${tile.z}, ${tile.x}, ${tile.y}, ${datasrid}), ${lyr.table}.${lyr.geometry})
)
SELECT * FROM a WHERE geom IS NOT NULL
) AS q
`;
break;
case "cluster_fields":
var agg_q_name = 'mvt_geo';
var fieldsAgg = '';
if (lyr.fields) {
lyr.fields.split(' ').forEach(function(field) {
fieldsAgg += ', string_agg('+agg_q_name+'.' + field + '::text, \',\') AS ' + field;
});
}
query = `
SELECT ST_AsMVT(q, '${layername}', ${resolution}, 'geom') AS mvt FROM (
WITH ${agg_q_name} AS (
SELECT 1 cnt, ST_AsMVTGeom(ST_Transform(${lyr.table}.${lyr.geometry}, ${lyr.srid}), ${tileBBoxFunc}(${tile.z}, ${tile.x}, ${tile.y}, ${lyr.srid}), ${resolution}, ${lyr.buffer}, ${clip_geom}) geom ${fields}
FROM ${lyr.table}
WHERE ST_Intersects(${tileBBoxFunc}(${tile.z}, ${tile.x}, ${tile.y}, ${datasrid}), ${lyr.table}.${lyr.geometry})
)
SELECT COUNT(${agg_q_name}.cnt), ${agg_q_name}.geom ${fieldsAgg}
FROM ${agg_q_name}
GROUP BY ${agg_q_name}.geom
) AS q
`;
break;
case "simplify":
query = `
SELECT ST_AsMVT(q, '${layername}', ${resolution}, 'geom') AS mvt FROM (
WITH a AS (
SELECT ST_AsMVTGeom(
ST_SimplifyPreserveTopology(ST_Transform(${lyr.table}.${lyr.geometry}, ${lyr.srid}), 2 * 1.40625 / pow(2, ${tile.z})),
${tileBBoxFunc}(${tile.z}, ${tile.x}, ${tile.y}, ${lyr.srid}),
${resolution},
${lyr.buffer},
${clip_geom} ) geom ${fields}
FROM ${lyr.table}
WHERE ST_Intersects(${tileBBoxFunc}(${tile.z}, ${tile.x}, ${tile.y}, ${datasrid}), ${lyr.table}.${lyr.geometry})
)
SELECT * FROM a WHERE geom IS NOT NULL
) AS q
`;
break;
case "bylevel":
query = `
SELECT ST_AsMVT(q, '${layername}', ${resolution}, 'geom') AS mvt FROM (
WITH a AS (
SELECT ST_AsMVTGeom(
ST_Transform(${lyr.table}_${tile.z}.${lyr.geometry}, ${lyr.srid}),
${tileBBoxFunc}(${tile.z}, ${tile.x}, ${tile.y}, ${lyr.srid}),
${resolution},
${lyr.buffer},
${clip_geom} ) geom ${fields}
FROM ${lyr.table}_${tile.z}
WHERE ST_Intersects(${tileBBoxFunc}(${tile.z}, ${tile.x}, ${tile.y}, ${datasrid}), ${lyr.table}_${tile.z}.${lyr.geometry})
)
SELECT * FROM a WHERE geom IS NOT NULL
) AS q
`;
break;
default:
query = `
SELECT ST_AsMVT(q, '${layername}', ${resolution}, 'geom') AS mvt FROM (
WITH a AS (
SELECT ST_AsMVTGeom(
ST_Transform(${lyr.table}.${lyr.geometry}, ${lyr.srid}),
${tileBBoxFunc}(${tile.z}, ${tile.x}, ${tile.y}, ${lyr.srid}),
${resolution},
${lyr.buffer},
${clip_geom} ) geom ${fields}
FROM ${lyr.table}
WHERE ST_Intersects(${tileBBoxFunc}(${tile.z}, ${tile.x}, ${tile.y}, ${datasrid}), ${lyr.table}.${lyr.geometry})
)
SELECT * FROM a WHERE geom IS NOT NULL
) AS q
`;
break;
}
pgPool.query(query, function(err, result) {
if (err) {
console.log(query, err.message, err.stack)
var err = new Error('An error occurred');
err.statusCode = 500;
return callback(err);
}
if (!result.rows[0].mvt) {
err = new Error('No data');
err.statusCode = 204;
return callback(err);
}
zlib.gzip(result.rows[0].mvt, function(err, result) {
if (!err) {
callback(null, result, {'Content-Type': 'application/x-protobuf', 'Content-Encoding': 'gzip'});
}
});
});
}
return {
name: 'postgismvt',
init: initialize,
serve: serveMVT
};
};