UNPKG

global-mercator

Version:

Tools to help with TMS, Quadkey & Google (XYZ) Tiles

806 lines (749 loc) 21.9 kB
var originShift = 2 * Math.PI * 6378137 / 2.0 var d2r = Math.PI / 180 export function initialResolution (tileSize) { tileSize = tileSize || 256 return 2 * Math.PI * 6378137 / tileSize } /** * Hash tile for unique id key * * @param {Tile} tile [x, y, z] * @returns {number} hash * @example * var id = globalMercator.hash([312, 480, 4]) * //=5728 */ export function hash (tile) { var x = tile[0] var y = tile[1] var z = tile[2] return (1 << z) * ((1 << z) + x) + y } /** * Get the tile for a point at a specified zoom level * https://github.com/mapbox/tilebelt * * @param {[number, number]} lnglat [Longitude, Latitude] * @param {number} zoom Zoom level * @param {boolean} [validate=true] validates LatLng coordinates * @returns {Google} Google (XYZ) Tile * @example * var tile = globalMercator.pointToTile([1, 1], 12) * //= [ 2059, 2036, 12 ] */ export function pointToTile (lnglat, zoom, validate) { var tile = pointToTileFraction(lnglat, zoom, validate) tile[0] = Math.floor(tile[0]) tile[1] = Math.floor(tile[1]) return tile } /** * Get the precise fractional tile location for a point at a zoom level * https://github.com/mapbox/tilebelt * * @name pointToTileFraction * @param {[number, number]} lnglat [Longitude, Latitude] * @param {number} zoom Zoom level * @param {boolean} [validate=true] validates LatLng coordinates * @returns {Google} Google (XYZ) Tile * @example * var tile = globalMercator.pointToTileFraction([1, 1], 12) * //= [ 2059.3777777777777, 2036.6216445333432, 12 ] */ export function pointToTileFraction (lnglat, zoom, validate) { // lnglat = validateLngLat(lnglat, validate) var z = zoom var lon = longitude(lnglat[0]) var lat = latitude(lnglat[1]) var sin = Math.sin(lat * d2r) var z2 = Math.pow(2, z) var x = z2 * (lon / 360 + 0.5) var y = z2 * (0.5 - 0.25 * Math.log((1 + sin) / (1 - sin)) / Math.PI) return validateTile([x, y, z], validate) } /** * Converts BBox to Center * * @param {BBox} bbox - [west, south, east, north] coordinates * @param {number} [decimal=6] - coordinate decimals * @return {LngLat} center * @example * var center = globalMercator.bboxToCenter([90, -45, 85, -50]) * //= [ 87.5, -47.5 ] */ export function bboxToCenter (bbox, decimal = 6) { var west = bbox[0] var south = bbox[1] var east = bbox[2] var north = bbox[3] var lng = (west - east) / 2 + east var lat = (south - north) / 2 + north if (decimal !== undefined && decimal !== null) { lng = Number(lng.toFixed(decimal)) lat = Number(lat.toFixed(decimal)) } return [lng, lat] } /** * Converts LngLat coordinates to Meters coordinates. * * @param {[number, number]} lnglat [Longitude, Latitude] * @param {boolean} [validate=true] validates LatLng coordinates * @param {Object} accuracy - { enable: true, decimal: 6 } * @returns {Meters} Meters coordinates * @example * var meters = globalMercator.lngLatToMeters([126, 37]) * //=[ 14026255.8, 4439106.7 ] */ export function lngLatToMeters (lnglat, validate, accuracy = { enable: true, decimal: 1 }) { lnglat = validateLngLat(lnglat, validate) var lng = lnglat[0] var lat = lnglat[1] var x = lng * originShift / 180.0 var y = Math.log(Math.tan((90 + lat) * Math.PI / 360.0)) / (Math.PI / 180.0) y = y * originShift / 180.0 if (accuracy.enable) { x = Number(x.toFixed(accuracy.decimal)) y = Number(y.toFixed(accuracy.decimal)) } return [x, y] } /** * Converts Meters coordinates to LngLat coordinates. * * @param {Meters} meters Meters in Mercator [x, y] * @param {number} [decimal=6] - coordinate decimals * @returns {LngLat} LngLat coordinates * @example * var lnglat = globalMercator.metersToLngLat([14026255, 4439106]) * //=[ 126, 37 ] */ export function metersToLngLat (meters, decimal = 6) { var x = meters[0] var y = meters[1] var lng = (x / originShift) * 180.0 var lat = (y / originShift) * 180.0 lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180.0)) - Math.PI / 2.0) if (decimal !== undefined && decimal !== null) { lng = Number(lng.toFixed(decimal)) lat = Number(lat.toFixed(decimal)) } return [lng, lat] } /** * Converts Meters coordinates to Pixels coordinates. * * @param {Meters} meters Meters in Mercator [x, y] * @param {number} zoom Zoom level * @param {number} [tileSize=256] Tile size * @returns {Pixels} Pixels coordinates * @example * var pixels = globalMercator.metersToPixels([14026255, 4439106], 13) * //=[ 1782579.1, 1280877.3, 13 ] */ export function metersToPixels (meters, zoom, tileSize) { var x = meters[0] var y = meters[1] var res = resolution(zoom, tileSize) var px = (x + originShift) / res var py = (y + originShift) / res return [px, py, zoom] } /** * Converts LngLat coordinates to TMS Tile. * * @param {[number, number]} lnglat [Longitude, Latitude] * @param {number} zoom Zoom level * @param {boolean} [validate=true] validates LatLng coordinates * @returns {Tile} TMS Tile * @example * var tile = globalMercator.lngLatToTile([126, 37], 13) * //=[ 6963, 5003, 13 ] */ export function lngLatToTile (lnglat, zoom, validate) { lnglat = validateLngLat(lnglat, validate) var meters = lngLatToMeters(lnglat) var pixels = metersToPixels(meters, zoom) return pixelsToTile(pixels) } /** * Converts LngLat coordinates to Google (XYZ) Tile. * * @param {[number, number]} lnglat [Longitude, Latitude] * @param {number} zoom Zoom level * @param {boolean} [validate=true] validates LatLng coordinates * @returns {Google} Google (XYZ) Tile * @example * var google = globalMercator.lngLatToGoogle([126, 37], 13) * //=[ 6963, 3188, 13 ] */ export function lngLatToGoogle (lnglat, zoom, validate) { lnglat = validateLngLat(lnglat, validate) if (zoom === 0) { return [0, 0, 0] } var tile = lngLatToTile(lnglat, zoom) return tileToGoogle(tile) } /** * Converts Meters coordinates to TMS Tile. * * @param {Meters} meters Meters in Mercator [x, y] * @param {number} zoom Zoom level * @returns {Tile} TMS Tile * @example * var tile = globalMercator.metersToTile([14026255, 4439106], 13) * //=[ 6963, 5003, 13 ] */ export function metersToTile (meters, zoom) { if (zoom === 0) { return [0, 0, 0] } var pixels = metersToPixels(meters, zoom) return pixelsToTile(pixels) } /** * Converts Pixels coordinates to Meters coordinates. * * @param {Pixels} pixels Pixels [x, y, zoom] * @param {number} [tileSize=256] Tile size * @param {number} [decimal=1] - coordinate decimals * @returns {Meters} Meters coordinates * @example * var meters = globalMercator.pixelsToMeters([1782579, 1280877, 13]) * //=[ 14026252.0, 4439099.5 ] */ export function pixelsToMeters (pixels, tileSize, decimal = 1) { var px = pixels[0] var py = pixels[1] var zoom = pixels[2] var res = resolution(zoom, tileSize) var mx = px * res - originShift var my = py * res - originShift if (decimal !== undefined && decimal !== null) { mx = Number(mx.toFixed(decimal)) my = Number(my.toFixed(decimal)) } return [mx, my] } /** * Converts Pixels coordinates to TMS Tile. * * @param {Pixels} pixels Pixels [x, y, zoom] * @param {number} [tileSize=256] Tile size * @param {boolean} [validate=true] validates Pixels coordinates * @returns {Tile} TMS Tile * @example * var tile = globalMercator.pixelsToTile([1782579, 1280877, 13]) * //=[ 6963, 5003, 13 ] */ export function pixelsToTile (pixels, tileSize, validate) { tileSize = tileSize || 256 var px = pixels[0] var py = pixels[1] var zoom = pixels[2] if (zoom === 0) return [0, 0, 0] validateZoom(zoom, validate) var tx = Math.ceil(px / tileSize) - 1 var ty = Math.ceil(py / tileSize) - 1 if (tx < 0) tx = 0 if (ty < 0) ty = 0 return [tx, ty, zoom] } /** * Converts TMS Tile to bbox in Meters coordinates. * * @param {Tile} tile Tile [x, y, zoom] * @param {number} x TMS Tile X * @param {number} y TMS Tile Y * @param {number} zoom Zoom level * @param {number} [tileSize=256] Tile size * @param {boolean} [validate=true] validates Tile * @returns {BBox} bbox extent in [minX, minY, maxX, maxY] order * @example * var bbox = globalMercator.tileToBBoxMeters([6963, 5003, 13]) * //=[ 14025277.4, 4437016.6, 14030169.4, 4441908.5 ] */ export function tileToBBoxMeters (tile, tileSize, validate) { validateTile(tile, validate) tileSize = tileSize || 256 var tx = tile[0] var ty = tile[1] var zoom = tile[2] var min = pixelsToMeters([tx * tileSize, ty * tileSize, zoom]) var max = pixelsToMeters([(tx + 1) * tileSize, (ty + 1) * tileSize, zoom]) return [min[0], min[1], max[0], max[1]] } /** * Converts TMS Tile to bbox in LngLat coordinates. * * @param {Tile} tile Tile [x, y, zoom] * @param {number} x TMS Tile X * @param {number} y TMS Tile Y * @param {number} zoom Zoom level * @param {boolean} [validate=true] validates Tile * @returns {BBox} bbox extent in [minX, minY, maxX, maxY] order * @example * var bbox = globalMercator.tileToBBox([6963, 5003, 13]) * //=[ 125.991, 36.985, 126.035, 37.020 ] */ export function tileToBBox (tile, validate) { validateTile(tile, validate) var tx = tile[0] var ty = tile[1] var zoom = tile[2] if (zoom === 0) { return [-180, -85.051129, 180, 85.051129] } var bbox = tileToBBoxMeters([tx, ty, zoom]) var mx1 = bbox[0] var my1 = bbox[1] var mx2 = bbox[2] var my2 = bbox[3] var min = metersToLngLat([mx1, my1, zoom]) var max = metersToLngLat([mx2, my2, zoom]) return [min[0], min[1], max[0], max[1]] } /** * Converts Google (XYZ) Tile to bbox in Meters coordinates. * * @param {Google} google Google [x, y, zoom] * @returns {BBox} bbox extent in [minX, minY, maxX, maxY] order * @example * var bbox = globalMercator.googleToBBoxMeters([6963, 3188, 13]) * //=[ 14025277.4, 4437016.6, 14030169.4, 4441908.5 ] */ export function googleToBBoxMeters (google) { var Tile = googleToTile(google) return tileToBBoxMeters(Tile) } /** * Converts Google (XYZ) Tile to bbox in LngLat coordinates. * * @param {Google} google Google [x, y, zoom] * @returns {BBox} bbox extent in [minX, minY, maxX, maxY] order * @example * var bbox = globalMercator.googleToBBox([6963, 3188, 13]) * //=[ 125.991, 36.985, 126.035, 37.020 ] */ export function googleToBBox (google) { var Tile = googleToTile(google) return tileToBBox(Tile) } /** * Converts TMS Tile to Google (XYZ) Tile. * * @param {Tile} tile Tile [x, y, zoom] * @param {boolean} [validate=true] validates Tile * @returns {Google} Google (XYZ) Tile * @example * var google = globalMercator.tileToGoogle([6963, 5003, 13]) * //=[ 6963, 3188, 13 ] */ export function tileToGoogle (tile, validate) { validateTile(tile, validate) var tx = tile[0] var ty = tile[1] var zoom = tile[2] if (zoom === 0) { return [0, 0, 0] } var x = tx var y = (Math.pow(2, zoom) - 1) - ty return [x, y, zoom] } /** * Converts Google (XYZ) Tile to TMS Tile. * * @param {Google} google Google [x, y, zoom] * @returns {Tile} TMS Tile * @example * var tile = globalMercator.googleToTile([6963, 3188, 13]) * //=[ 6963, 5003, 13 ] */ export function googleToTile (google) { var x = google[0] var y = google[1] var zoom = google[2] var tx = x var ty = Math.pow(2, zoom) - y - 1 return [tx, ty, zoom] } /** * Converts Google (XYZ) Tile to Quadkey. * * @param {Google} google Google [x, y, zoom] * @returns {string} Microsoft's Quadkey schema * @example * var quadkey = globalMercator.googleToQuadkey([6963, 3188, 13]) * //='1321102330211' */ export function googleToQuadkey (google) { var Tile = googleToTile(google) return tileToQuadkey(Tile) } /** * Converts TMS Tile to QuadKey. * * @param {Tile} tile Tile [x, y, zoom] * @param {boolean} [validate=true] validates Tile * @returns {string} Microsoft's Quadkey schema * @example * var quadkey = globalMercator.tileToQuadkey([6963, 5003, 13]) * //='1321102330211' */ export function tileToQuadkey (tile, validate) { validateTile(tile, validate) var tx = tile[0] var ty = tile[1] var zoom = tile[2] // Zoom 0 does not exist for Quadkey if (zoom === 0) { return '' } var quadkey = '' ty = (Math.pow(2, zoom) - 1) - ty range(zoom, 0, -1).map(function (i) { var digit = 0 var mask = 1 << (i - 1) if ((tx & mask) !== 0) { digit += 1 } if ((ty & mask) !== 0) { digit += 2 } quadkey = quadkey.concat(digit) }) return quadkey } /** * Converts Quadkey to TMS Tile. * * @param {string} quadkey Microsoft's Quadkey schema * @returns {Tile} TMS Tile * @example * var tile = globalMercator.quadkeyToTile('1321102330211') * //=[ 6963, 5003, 13 ] */ export function quadkeyToTile (quadkey) { var Google = quadkeyToGoogle(quadkey) return googleToTile(Google) } /** * Converts Quadkey to Google (XYZ) Tile. * * @param {string} quadkey Microsoft's Quadkey schema * @returns {Google} Google (XYZ) Tile * @example * var google = globalMercator.quadkeyToGoogle('1321102330211') * //=[ 6963, 3188, 13 ] */ export function quadkeyToGoogle (quadkey) { var x = 0 var y = 0 var zoom = quadkey.length range(zoom, 0, -1).map(function (i) { var mask = 1 << (i - 1) switch (parseInt(quadkey[zoom - i], 0)) { case 0: break case 1: x += mask break case 2: y += mask break case 3: x += mask y += mask break default: throw new Error('Invalid Quadkey digit sequence') } }) return [x, y, zoom] } /** * Converts BBox from LngLat coordinates to Meters coordinates * * @param {BBox} bbox extent in [minX, minY, maxX, maxY] order * @returns {BBox} bbox extent in [minX, minY, maxX, maxY] order * @example * var meters = globalMercator.bboxToMeters([ 125, 35, 127, 37 ]) * //=[ 13914936.3, 4163881.1, 14137575.3, 4439106.7 ] */ export function bboxToMeters (bbox) { var min = lngLatToMeters([bbox[0], bbox[1]]) var max = lngLatToMeters([bbox[2], bbox[3]]) return [min[0], min[1], max[0], max[1]] } /** * Validates TMS Tile. * * @param {Tile} tile Tile [x, y, zoom] * @param {boolean} [validate=true] validates Tile * @throws {Error} Will throw an error if TMS Tile is not valid. * @returns {Tile} TMS Tile * @example * globalMercator.validateTile([60, 80, 12]) * //=[60, 80, 12] * globalMercator.validateTile([60, -43, 5]) * //= Error: Tile <y> must not be less than 0 * globalMercator.validateTile([25, 60, 3]) * //= Error: Illegal parameters for tile */ export function validateTile (tile, validate) { var tx = tile[0] var ty = tile[1] var zoom = tile[2] if (validate === false) return tile if (zoom === undefined || zoom === null) throw new Error('<zoom> is required') if (tx === undefined || tx === null) throw new Error('<x> is required') if (ty === undefined || ty === null) throw new Error('<y> is required') // Adjust values of tiles to fit within tile scheme zoom = validateZoom(zoom) tile = wrapTile(tile) // // Check to see if tile is valid based on the zoom level // // Currently impossible to hit since WrapTile handles this error // // will keep this test commented out in case it doesnt handle it // var maxCount = Math.pow(2, zoom) // if (tile[0] >= maxCount || tile[1] >= maxCount) throw new Error('Illegal parameters for tile') return tile } /** * Wrap Tile -- Handles tiles which crosses the 180th meridian or 90th parallel * * @param {[number, number, number]} tile Tile * @param {number} zoom Zoom Level * @returns {[number, number, number]} Wrapped Tile * @example * globalMercator.wrapTile([0, 3, 2]) * //= [0, 3, 2] -- Valid Tile X * globalMercator.wrapTile([4, 2, 2]) * //= [0, 2, 2] -- Tile 4 does not exist, wrap around to TileX=0 */ export function wrapTile (tile) { var tx = tile[0] var ty = tile[1] var zoom = tile[2] // Maximum tile allowed // zoom 0 => 1 // zoom 1 => 2 // zoom 2 => 4 // zoom 3 => 8 var maxTile = Math.pow(2, zoom) // Handle Tile X tx = tx % maxTile if (tx < 0) tx = tx + maxTile return [tx, ty, zoom] } /** * Validates Zoom level * * @param {number} zoom Zoom level * @param {boolean} [validate=true] validates Zoom level * @throws {Error} Will throw an error if zoom is not valid. * @returns {number} zoom Zoom level * @example * globalMercator.validateZoom(12) * //=12 * globalMercator.validateZoom(-4) * //= Error: <zoom> cannot be less than 0 * globalMercator.validateZoom(32) * //= Error: <zoom> cannot be greater than 30 */ export function validateZoom (zoom) { if (zoom === false) return zoom if (zoom === undefined || zoom === null) { throw new Error('<zoom> is required') } if (zoom < 0) { throw new Error('<zoom> cannot be less than 0') } if (zoom > 32) { throw new Error('<zoom> cannot be greater than 32') } return zoom } /** * Validates LngLat coordinates * * @param {[number, number]} lnglat [Longitude, Latitude] * @param {boolean} [validate=true] validates LatLng coordinates * @throws {Error} Will throw an error if LngLat is not valid. * @returns {LngLat} LngLat coordinates * @example * globalMercator.validateLngLat([-115, 44]) * //= [ -115, 44 ] * globalMercator.validateLngLat([-225, 44]) * //= Error: LngLat [lng] must be within -180 to 180 degrees */ export function validateLngLat (lnglat, validate) { if (validate === false) return lnglat var lng = longitude(lnglat[0]) var lat = latitude(lnglat[1]) // Global Mercator does not support latitudes within 85 to 90 degrees if (lat > 85) lat = 85 if (lat < -85) lat = -85 return [lng, lat] } /** * Retrieve resolution based on zoom level * * @private * @param {number} zoom zoom level * @param {number} [tileSize=256] Tile size * @returns {number} resolution * @example * var res = globalMercator.resolution(13) * //=19.109257071294063 */ export function resolution (zoom, tileSize) { return initialResolution(tileSize) / Math.pow(2, zoom) } /** * Generate an integer Array containing an arithmetic progression. * * @private * @param {number} [start=0] Start * @param {number} stop Stop * @param {number} [step=1] Step * @returns {number[]} range * @example * globalMercator.range(3) * //=[ 0, 1, 2 ] * globalMercator.range(3, 6) * //=[ 3, 4, 5 ] * globalMercator.range(6, 3, -1) * //=[ 6, 5, 4 ] */ export function range (start, stop, step) { if (stop == null) { stop = start || 0 start = 0 } if (!step) { step = stop < start ? -1 : 1 } var length = Math.max(Math.ceil((stop - start) / step), 0) var range = Array(length) for (var idx = 0; idx < length; idx++, start += step) { range[idx] = start } return range } /** * Maximum extent of BBox * * @param {BBox|BBox[]} array BBox [west, south, east, north] * @returns {BBox} Maximum BBox * @example * var bbox = globalMercator.maxBBox([[-20, -30, 20, 30], [-110, -30, 120, 80]]) * //=[-110, -30, 120, 80] */ export function maxBBox (array) { if (!array) throw new Error('array is required') // Single BBox if (array && array[0] && array.length === 4 && array[0][0] === undefined) { return array } // Multiple BBox if (array && array[0] && array[0][0] !== undefined) { var west = array[0][0] var south = array[0][1] var east = array[0][2] var north = array[0][3] array.map(function (bbox) { if (bbox[0] < west) { west = bbox[0] } if (bbox[1] < south) { south = bbox[1] } if (bbox[2] > east) { east = bbox[2] } if (bbox[3] > north) { north = bbox[3] } }) return [west, south, east, north] } } /** * Valid TMS Tile * * @param {Tile} tile Tile [x, y, zoom] * @returns {boolean} valid tile true/false * @example * globalMercator.validTile([60, 80, 12]) * //= true * globalMercator.validTile([60, -43, 5]) * //= false * globalMercator.validTile([25, 60, 3]) * //= false */ export function validTile (tile) { try { validateTile(tile) return true } catch (e) { return false } } /** * Modifies a Latitude to fit within +/-90 degrees. * * @param {number} lat latitude to modify * @returns {number} modified latitude * @example * globalMercator.latitude(100) * //= -80 */ export function latitude (lat) { if (lat === undefined || lat === null) throw new Error('lat is required') // Latitudes cannot extends beyond +/-90 degrees if (lat > 90 || lat < -90) { lat = lat % 180 if (lat > 90) lat = -180 + lat if (lat < -90) lat = 180 + lat if (lat === 0) lat = 0 } return lat } /** * Modifies a Longitude to fit within +/-180 degrees. * * @param {number} lng longitude to modify * @returns {number} modified longitude * @example * globalMercator.longitude(190) * //= -170 */ export function longitude (lng) { if (lng === undefined || lng === null) throw new Error('lng is required') // lngitudes cannot extends beyond +/-90 degrees if (lng > 180 || lng < -180) { lng = lng % 360 if (lng > 180) lng = -360 + lng if (lng < -180) lng = 360 + lng if (lng === 0) lng = 0 } return lng } /** * Get the smallest tile to cover a bbox * * @param {Array<number>} bbox BBox * @returns {Array<number>} tile Tile * @example * var tile = bboxToTile([-178, 84, -177, 85]) * //=tile */ export function bboxToTile (bboxCoords) { var min = pointToTile([bboxCoords[0], bboxCoords[1]], 32) var max = pointToTile([bboxCoords[2], bboxCoords[3]], 32) var bbox = [min[0], min[1], max[0], max[1]] var z = getBboxZoom(bbox) if (z === 0) return [0, 0, 0] var x = bbox[0] >>> (32 - z) var y = bbox[1] >>> (32 - z) return [x, y, z] } function getBboxZoom (bbox) { var MAX_ZOOM = 28 for (var z = 0; z < MAX_ZOOM; z++) { var mask = 1 << (32 - (z + 1)) if (((bbox[0] & mask) !== (bbox[2] & mask)) || ((bbox[1] & mask) !== (bbox[3] & mask))) { return z } } return MAX_ZOOM }