Skip to content

Commit

Permalink
feat(pixel): Add pixel conversion functions
Browse files Browse the repository at this point in the history
  • Loading branch information
trevorgerhardt committed Oct 17, 2018
1 parent 7373a47 commit a4e04f1
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 1 deletion.
139 changes: 139 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@
* @property {number} lon
*/

/**
* (type)
*
* Object with x/y number values.
* @typedef {Object} lonlat.types.point
* @property {number} x
* @property {number} y
*/

/**
* (exception type)
*
Expand Down Expand Up @@ -287,6 +296,136 @@ module.exports.toLatFirstString = function toLatFirstString (input) {
return ll.lat + ',' + ll.lon
}

/**
* Pixel conversions and constants taken from
* https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Implementations
*/

/**
* Pixels per tile.
*/
var PIXELS_PER_TILE = module.exports.PIXELS_PER_TILE = 256

// 2^z represents the tile number. Scale that by the number of pixels in each tile.
function zScale (z) {
return Math.pow(2, z) * PIXELS_PER_TILE
}

// Converts from degrees to radians
function toRadians (degrees) {
return degrees * Math.PI / 180
}

// Converts from radians to degrees.
function toDegrees (radians) {
return radians * 180 / Math.PI
}

/**
* Convert a longitude to it's pixel value given a `zoom` level.
*
* @param {number} longitude
* @param {number} zoom
* @return {number} pixel
* @example
* var xPixel = lonlat.longitudeToPixel(-70, 9) //= 40049.77777777778
*/
function longitudeToPixel (longitude, zoom) {
return (longitude + 180) / 360 * zScale(zoom)
}
module.exports.longitudeToPixel = longitudeToPixel

/**
* Convert a latitude to it's pixel value given a `zoom` level.
*
* @param {number} latitude
* @param {number} zoom
* @return {number} pixel
* @example
* var yPixel = lonlat.latitudeToPixel(40, 9) //= 49621.12736343896
*/
function latitudeToPixel (latitude, zoom) {
const latRad = toRadians(latitude)
return (1 -
Math.log(Math.tan(latRad) + (1 / Math.cos(latRad))) /
Math.PI) / 2 * zScale(zoom)
}
module.exports.latitudeToPixel = latitudeToPixel

/**
* Maximum Latitude for valid Mercator projection conversion.
*/
var MAX_LAT = toDegrees(Math.atan(Math.sinh(Math.PI)))

/**
* Convert a coordinate to a pixel.
*
* @param {lonlat.types.input} input
* @param {number} zoom
* @return {Object} An object with `x` and `y` attributes representing pixel coordinates
* @throws {lonlat.types.InvalidCoordinateException}
* @throws {Error} If latitude is above or below `MAX_LAT`
* @throws {Error} If `zoom` is undefined.
* @example
* var pixel = lonlat.toPixel({lon: -70, lat: 40}, 9) //= {x: 40049.77777777778, y:49621.12736343896}
*/
module.exports.toPixel = function toPixel (input, zoom) {
var ll = normalize(input)
if (ll.lat > MAX_LAT || ll.lat < -MAX_LAT) {
throw new Error('Pixel conversion only works between ' + MAX_LAT + 'N and -' + MAX_LAT + 'S')
}

return {
x: longitudeToPixel(ll.lon, zoom),
y: latitudeToPixel(ll.lat, zoom)
}
}

/**
* Convert a pixel to it's longitude value given a zoom level.
*
* @param {number} x
* @param {number} zoom
* @return {number} longitude
* @example
* var lon = lonlat.pixelToLongitude(40000, 9) //= -70.13671875
*/
function pixelToLongitude (x, zoom) {
return x / zScale(zoom) * 360 - 180
}
module.exports.pixelToLongitude = pixelToLongitude

/**
* Convert a pixel to it's latitude value given a zoom level.
*
* @param {number} y
* @param {number} zoom
* @return {number} latitude
* @example
* var lat = lonlat.pixelToLatitude(50000, 9) //= 39.1982053488948
*/
function pixelToLatitude (y, zoom) {
var latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / zScale(zoom))))
return toDegrees(latRad)
}
module.exports.pixelToLatitude = pixelToLatitude

/**
* From pixel.
*
* @param {lonlat.types.point} pixel
* @param {number} zoom
* @return {lonlat.types.output}
* @example
* var ll = lonlat.fromPixel({x: 40000, y: 50000}, 9) //= {lon: -70.13671875, lat: 39.1982053488948}
*/
module.exports.fromPixel = function fromPixel (pixel, zoom) {
return {
lon: pixelToLongitude(pixel.x, zoom),
lat: pixelToLatitude(pixel.y, zoom)
}
}

function floatize (lonlat) {
var lon = parseFloatWithAlternates([lonlat.lon, lonlat.lng, lonlat.longitude])
var lat = parseFloatWithAlternates([lonlat.lat, lonlat.latitude])
Expand Down
20 changes: 20 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const ll = require('./')

const lat = 38.13234
const lon = 70.01232
const Z = 9 // Zoom level to use
const pixel = {x: 91026.70779, y: 50497.02600}
const lonlat = {lon, lat}
const point = {x: lon, y: lat}
const coordinates = [lon, lat]
Expand Down Expand Up @@ -140,6 +142,24 @@ describe('lonlat', () => {
})
})

describe('pixel', () => {
it('can convert to web mercator pixel coordinates', () => {
const p = ll.toPixel({lat, lon}, Z)
expect(Math.round(p.x)).toBe(Math.round(pixel.x))
expect(Math.round(p.y)).toBe(Math.round(pixel.y))
})

it('can convert from web mercator pixel coordinates', () => {
const l = ll.fromPixel(pixel, Z)
expect(Math.round(l.lat)).toBe(Math.round(lat))
expect(Math.round(l.lon)).toBe(Math.round(lon))
})

it('should throw an error if converting a latitude > MAX_LAT', () => {
expect(() => ll.toPixel({lat: 86, lon}, Z)).toThrow()
})
})

describe('issues', () => {
it('#3 - Does not parse coordinates with 0 for lat or lon', () => {
expect(ll({ lat: 0, lng: 0 })).toEqual({ lat: 0, lon: 0 })
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
"scripts": {
"cover": "yarn test --coverage --coverage-paths index.js",
"generate-docs": "documentation readme index.js --section=API --markdown-toc=true",
"jest": "mastarm test",
"lint": "mastarm lint index.js index.test.js",
"lint-docs": "documentation lint index.js",
"pretest": "yarn",
"semantic-release": "semantic-release",
"test": "yarn run lint && yarn run lint-docs && mastarm test"
"test": "yarn run lint && yarn run lint-docs && yarn run jest"
},
"repository": {
"type": "git",
Expand Down

0 comments on commit a4e04f1

Please sign in to comment.