Skip to content

Instantly share code, notes, and snippets.

@jatorre
Created February 3, 2020 21:30
Show Gist options
  • Save jatorre/1ee18a1b50229e2e72f61a05a31275cb to your computer and use it in GitHub Desktop.
Save jatorre/1ee18a1b50229e2e72f61a05a31275cb to your computer and use it in GitHub Desktop.
CREATE TEMP FUNCTION bbox(x NUMERIC, y NUMERIC,zoom NUMERIC )
RETURNS ARRAY<FLOAT64>
LANGUAGE js AS """
var SphericalMercator = (function(){
// Closures including constants and other precalculated values.
var cache = {},
EPSLN = 1.0e-10,
D2R = Math.PI / 180,
R2D = 180 / Math.PI,
// 900913 properties.
A = 6378137.0,
MAXEXTENT = 20037508.342789244;
function isFloat(n){
return Number(n) === n && n % 1 !== 0;
}
// SphericalMercator constructor: precaches calculations
// for fast tile lookups.
function SphericalMercator(options) {
options = options || {};
this.size = options.size || 256;
if (!cache[this.size]) {
var size = this.size;
var c = cache[this.size] = {};
c.Bc = [];
c.Cc = [];
c.zc = [];
c.Ac = [];
for (var d = 0; d < 30; d++) {
c.Bc.push(size / 360);
c.Cc.push(size / (2 * Math.PI));
c.zc.push(size / 2);
c.Ac.push(size);
size *= 2;
}
}
this.Bc = cache[this.size].Bc;
this.Cc = cache[this.size].Cc;
this.zc = cache[this.size].zc;
this.Ac = cache[this.size].Ac;
};
// Convert lon lat to screen pixel value
//
// - `ll` {Array} `[lon, lat]` array of geographic coordinates.
// - `zoom` {Number} zoom level.
SphericalMercator.prototype.px = function(ll, zoom) {
if (isFloat(zoom)) {
var size = this.size * Math.pow(2, zoom);
var d = size / 2;
var bc = (size / 360);
var cc = (size / (2 * Math.PI));
var ac = size;
var f = Math.min(Math.max(Math.sin(D2R * ll[1]), -0.9999), 0.9999);
var x = d + ll[0] * bc;
var y = d + 0.5 * Math.log((1 + f) / (1 - f)) * -cc;
(x > ac) && (x = ac);
(y > ac) && (y = ac);
//(x < 0) && (x = 0);
//(y < 0) && (y = 0);
return [x, y];
} else {
var d = this.zc[zoom];
var f = Math.min(Math.max(Math.sin(D2R * ll[1]), -0.9999), 0.9999);
var x = Math.round(d + ll[0] * this.Bc[zoom]);
var y = Math.round(d + 0.5 * Math.log((1 + f) / (1 - f)) * (-this.Cc[zoom]));
(x > this.Ac[zoom]) && (x = this.Ac[zoom]);
(y > this.Ac[zoom]) && (y = this.Ac[zoom]);
//(x < 0) && (x = 0);
//(y < 0) && (y = 0);
return [x, y];
}
};
// Convert screen pixel value to lon lat
//
// - `px` {Array} `[x, y]` array of geographic coordinates.
// - `zoom` {Number} zoom level.
SphericalMercator.prototype.ll = function(px, zoom) {
if (isFloat(zoom)) {
var size = this.size * Math.pow(2, zoom);
var bc = (size / 360);
var cc = (size / (2 * Math.PI));
var zc = size / 2;
var g = (px[1] - zc) / -cc;
var lon = (px[0] - zc) / bc;
var lat = R2D * (2 * Math.atan(Math.exp(g)) - 0.5 * Math.PI);
return [lon, lat];
} else {
var g = (px[1] - this.zc[zoom]) / (-this.Cc[zoom]);
var lon = (px[0] - this.zc[zoom]) / this.Bc[zoom];
var lat = R2D * (2 * Math.atan(Math.exp(g)) - 0.5 * Math.PI);
return [lon, lat];
}
};
// Convert tile xyz value to bbox of the form `[w, s, e, n]`
//
// - `x` {Number} x (longitude) number.
// - `y` {Number} y (latitude) number.
// - `zoom` {Number} zoom.
// - `tms_style` {Boolean} whether to compute using tms-style.
// - `srs` {String} projection for resulting bbox (WGS84|900913).
// - `return` {Array} bbox array of values in form `[w, s, e, n]`.
SphericalMercator.prototype.bbox = function(x, y, zoom, tms_style, srs) {
// Convert xyz into bbox with srs WGS84
if (tms_style) {
y = (Math.pow(2, zoom) - 1) - y;
}
// Use +y to make sure it's a number to avoid inadvertent concatenation.
var ll = [x * this.size, (+y + 1) * this.size]; // lower left
// Use +x to make sure it's a number to avoid inadvertent concatenation.
var ur = [(+x + 1) * this.size, y * this.size]; // upper right
var bbox = this.ll(ll, zoom).concat(this.ll(ur, zoom));
// If web mercator requested reproject to 900913.
if (srs === '900913') {
return this.convert(bbox, '900913');
} else {
return bbox;
}
};
// Convert bbox to xyx bounds
//
// - `bbox` {Number} bbox in the form `[w, s, e, n]`.
// - `zoom` {Number} zoom.
// - `tms_style` {Boolean} whether to compute using tms-style.
// - `srs` {String} projection of input bbox (WGS84|900913).
// - `@return` {Object} XYZ bounds containing minX, maxX, minY, maxY properties.
SphericalMercator.prototype.xyz = function(bbox, zoom, tms_style, srs) {
// If web mercator provided reproject to WGS84.
if (srs === '900913') {
bbox = this.convert(bbox, 'WGS84');
}
var ll = [bbox[0], bbox[1]]; // lower left
var ur = [bbox[2], bbox[3]]; // upper right
var px_ll = this.px(ll, zoom);
var px_ur = this.px(ur, zoom);
// Y = 0 for XYZ is the top hence minY uses px_ur[1].
var x = [ Math.floor(px_ll[0] / this.size), Math.floor((px_ur[0] - 1) / this.size) ];
var y = [ Math.floor(px_ur[1] / this.size), Math.floor((px_ll[1] - 1) / this.size) ];
var bounds = {
minX: Math.min.apply(Math, x) < 0 ? 0 : Math.min.apply(Math, x),
minY: Math.min.apply(Math, y) < 0 ? 0 : Math.min.apply(Math, y),
maxX: Math.max.apply(Math, x),
maxY: Math.max.apply(Math, y)
};
if (tms_style) {
var tms = {
minY: (Math.pow(2, zoom) - 1) - bounds.maxY,
maxY: (Math.pow(2, zoom) - 1) - bounds.minY
};
bounds.minY = tms.minY;
bounds.maxY = tms.maxY;
}
return bounds;
};
return SphericalMercator;
})();
var merc = new SphericalMercator({
size: 256
});
return merc.bbox(x,y,zoom)
""";
CREATE TEMP FUNCTION xyz(bbox ARRAY<FLOAT64>, zoom NUMERIC)
RETURNS STRUCT<minX NUMERIC,minY NUMERIC,maxX NUMERIC,maxY NUMERIC>
LANGUAGE js AS """
var SphericalMercator = (function(){
// Closures including constants and other precalculated values.
var cache = {},
EPSLN = 1.0e-10,
D2R = Math.PI / 180,
R2D = 180 / Math.PI,
// 900913 properties.
A = 6378137.0,
MAXEXTENT = 20037508.342789244;
function isFloat(n){
return Number(n) === n && n % 1 !== 0;
}
// SphericalMercator constructor: precaches calculations
// for fast tile lookups.
function SphericalMercator(options) {
options = options || {};
this.size = options.size || 256;
if (!cache[this.size]) {
var size = this.size;
var c = cache[this.size] = {};
c.Bc = [];
c.Cc = [];
c.zc = [];
c.Ac = [];
for (var d = 0; d < 30; d++) {
c.Bc.push(size / 360);
c.Cc.push(size / (2 * Math.PI));
c.zc.push(size / 2);
c.Ac.push(size);
size *= 2;
}
}
this.Bc = cache[this.size].Bc;
this.Cc = cache[this.size].Cc;
this.zc = cache[this.size].zc;
this.Ac = cache[this.size].Ac;
};
// Convert lon lat to screen pixel value
//
// - `ll` {Array} `[lon, lat]` array of geographic coordinates.
// - `zoom` {Number} zoom level.
SphericalMercator.prototype.px = function(ll, zoom) {
if (isFloat(zoom)) {
var size = this.size * Math.pow(2, zoom);
var d = size / 2;
var bc = (size / 360);
var cc = (size / (2 * Math.PI));
var ac = size;
var f = Math.min(Math.max(Math.sin(D2R * ll[1]), -0.9999), 0.9999);
var x = d + ll[0] * bc;
var y = d + 0.5 * Math.log((1 + f) / (1 - f)) * -cc;
(x > ac) && (x = ac);
(y > ac) && (y = ac);
//(x < 0) && (x = 0);
//(y < 0) && (y = 0);
return [x, y];
} else {
var d = this.zc[zoom];
var f = Math.min(Math.max(Math.sin(D2R * ll[1]), -0.9999), 0.9999);
var x = Math.round(d + ll[0] * this.Bc[zoom]);
var y = Math.round(d + 0.5 * Math.log((1 + f) / (1 - f)) * (-this.Cc[zoom]));
(x > this.Ac[zoom]) && (x = this.Ac[zoom]);
(y > this.Ac[zoom]) && (y = this.Ac[zoom]);
//(x < 0) && (x = 0);
//(y < 0) && (y = 0);
return [x, y];
}
};
// Convert screen pixel value to lon lat
//
// - `px` {Array} `[x, y]` array of geographic coordinates.
// - `zoom` {Number} zoom level.
SphericalMercator.prototype.ll = function(px, zoom) {
if (isFloat(zoom)) {
var size = this.size * Math.pow(2, zoom);
var bc = (size / 360);
var cc = (size / (2 * Math.PI));
var zc = size / 2;
var g = (px[1] - zc) / -cc;
var lon = (px[0] - zc) / bc;
var lat = R2D * (2 * Math.atan(Math.exp(g)) - 0.5 * Math.PI);
return [lon, lat];
} else {
var g = (px[1] - this.zc[zoom]) / (-this.Cc[zoom]);
var lon = (px[0] - this.zc[zoom]) / this.Bc[zoom];
var lat = R2D * (2 * Math.atan(Math.exp(g)) - 0.5 * Math.PI);
return [lon, lat];
}
};
// Convert tile xyz value to bbox of the form `[w, s, e, n]`
//
// - `x` {Number} x (longitude) number.
// - `y` {Number} y (latitude) number.
// - `zoom` {Number} zoom.
// - `tms_style` {Boolean} whether to compute using tms-style.
// - `srs` {String} projection for resulting bbox (WGS84|900913).
// - `return` {Array} bbox array of values in form `[w, s, e, n]`.
SphericalMercator.prototype.bbox = function(x, y, zoom, tms_style, srs) {
// Convert xyz into bbox with srs WGS84
if (tms_style) {
y = (Math.pow(2, zoom) - 1) - y;
}
// Use +y to make sure it's a number to avoid inadvertent concatenation.
var ll = [x * this.size, (+y + 1) * this.size]; // lower left
// Use +x to make sure it's a number to avoid inadvertent concatenation.
var ur = [(+x + 1) * this.size, y * this.size]; // upper right
var bbox = this.ll(ll, zoom).concat(this.ll(ur, zoom));
// If web mercator requested reproject to 900913.
if (srs === '900913') {
return this.convert(bbox, '900913');
} else {
return bbox;
}
};
// Convert bbox to xyx bounds
//
// - `bbox` {Number} bbox in the form `[w, s, e, n]`.
// - `zoom` {Number} zoom.
// - `tms_style` {Boolean} whether to compute using tms-style.
// - `srs` {String} projection of input bbox (WGS84|900913).
// - `@return` {Object} XYZ bounds containing minX, maxX, minY, maxY properties.
SphericalMercator.prototype.xyz = function(bbox, zoom, tms_style, srs) {
// If web mercator provided reproject to WGS84.
if (srs === '900913') {
bbox = this.convert(bbox, 'WGS84');
}
var ll = [bbox[0], bbox[1]]; // lower left
var ur = [bbox[2], bbox[3]]; // upper right
var px_ll = this.px(ll, zoom);
var px_ur = this.px(ur, zoom);
// Y = 0 for XYZ is the top hence minY uses px_ur[1].
var x = [ Math.floor(px_ll[0] / this.size), Math.floor((px_ur[0] - 1) / this.size) ];
var y = [ Math.floor(px_ur[1] / this.size), Math.floor((px_ll[1] - 1) / this.size) ];
var bounds = {
minX: Math.min.apply(Math, x) < 0 ? 0 : Math.min.apply(Math, x),
minY: Math.min.apply(Math, y) < 0 ? 0 : Math.min.apply(Math, y),
maxX: Math.max.apply(Math, x),
maxY: Math.max.apply(Math, y)
};
if (tms_style) {
var tms = {
minY: (Math.pow(2, zoom) - 1) - bounds.maxY,
maxY: (Math.pow(2, zoom) - 1) - bounds.minY
};
bounds.minY = tms.minY;
bounds.maxY = tms.maxY;
}
return bounds;
};
return SphericalMercator;
})();
var merc = new SphericalMercator({
size: 256
});
return merc.xyz(bbox,zoom)
""";
--Area in Brooklyn not very big (4 tiles at zoom 14.
--GENERTE a SERIES OF TILES FOR THE BBOX (minLon,minLat,maxLon,maxLat) at zoom 14
WITH
tiles_on_aoi_limits AS (SELECT xyz([-73.955397,40.724201,-73.924341,40.743828],14) as xyzbounds),
xes AS (SELECT x FROM UNNEST(GENERATE_ARRAY((SELECT xyzbounds.minX FROM tiles_on_aoi_limits), (SELECT xyzbounds.maxX FROM tiles_on_aoi_limits))) AS x),
tiles AS (SELECT x,y,14 as zoom FROM xes,UNNEST(GENERATE_ARRAY((SELECT xyzbounds.minY FROM tiles_on_aoi_limits), (SELECT xyzbounds.maxY FROM tiles_on_aoi_limits))) AS y),
--Calculate the bbox for each tile
tilebbox AS (SELECT bbox(x,y,zoom) as bbox,x,y,zoom FROM tiles)
--Get an array of grometries for all the features intersecting a tile
SELECT x,y,zoom,ARRAY_AGG(geom) as geoms FROM `carto-do-public-data.usa_carto.geography_usa_blockgroup_2015`, tilebbox WHERE
ST_INTERSECTSBOX(geom,bbox[OFFSET(0)],bbox[OFFSET(1)],bbox[OFFSET(2)],bbox[OFFSET(3)])
GROUP by x,y,zoom
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment