Skip to content

Commit a4e04f1

Browse files
feat(pixel): Add pixel conversion functions
1 parent 7373a47 commit a4e04f1

File tree

3 files changed

+161
-1
lines changed

3 files changed

+161
-1
lines changed

index.js

+139
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@
2222
* @property {number} lon
2323
*/
2424

25+
/**
26+
* (type)
27+
*
28+
* Object with x/y number values.
29+
* @typedef {Object} lonlat.types.point
30+
* @property {number} x
31+
* @property {number} y
32+
*/
33+
2534
/**
2635
* (exception type)
2736
*
@@ -287,6 +296,136 @@ module.exports.toLatFirstString = function toLatFirstString (input) {
287296
return ll.lat + ',' + ll.lon
288297
}
289298

299+
/**
300+
* Pixel conversions and constants taken from
301+
* https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Implementations
302+
*/
303+
304+
/**
305+
* Pixels per tile.
306+
*/
307+
var PIXELS_PER_TILE = module.exports.PIXELS_PER_TILE = 256
308+
309+
// 2^z represents the tile number. Scale that by the number of pixels in each tile.
310+
function zScale (z) {
311+
return Math.pow(2, z) * PIXELS_PER_TILE
312+
}
313+
314+
// Converts from degrees to radians
315+
function toRadians (degrees) {
316+
return degrees * Math.PI / 180
317+
}
318+
319+
// Converts from radians to degrees.
320+
function toDegrees (radians) {
321+
return radians * 180 / Math.PI
322+
}
323+
324+
/**
325+
* Convert a longitude to it's pixel value given a `zoom` level.
326+
*
327+
* @param {number} longitude
328+
* @param {number} zoom
329+
* @return {number} pixel
330+
* @example
331+
* var xPixel = lonlat.longitudeToPixel(-70, 9) //= 40049.77777777778
332+
*/
333+
function longitudeToPixel (longitude, zoom) {
334+
return (longitude + 180) / 360 * zScale(zoom)
335+
}
336+
module.exports.longitudeToPixel = longitudeToPixel
337+
338+
/**
339+
* Convert a latitude to it's pixel value given a `zoom` level.
340+
*
341+
* @param {number} latitude
342+
* @param {number} zoom
343+
* @return {number} pixel
344+
* @example
345+
* var yPixel = lonlat.latitudeToPixel(40, 9) //= 49621.12736343896
346+
*/
347+
function latitudeToPixel (latitude, zoom) {
348+
const latRad = toRadians(latitude)
349+
return (1 -
350+
Math.log(Math.tan(latRad) + (1 / Math.cos(latRad))) /
351+
Math.PI) / 2 * zScale(zoom)
352+
}
353+
module.exports.latitudeToPixel = latitudeToPixel
354+
355+
/**
356+
* Maximum Latitude for valid Mercator projection conversion.
357+
*/
358+
var MAX_LAT = toDegrees(Math.atan(Math.sinh(Math.PI)))
359+
360+
/**
361+
* Convert a coordinate to a pixel.
362+
*
363+
* @param {lonlat.types.input} input
364+
* @param {number} zoom
365+
* @return {Object} An object with `x` and `y` attributes representing pixel coordinates
366+
* @throws {lonlat.types.InvalidCoordinateException}
367+
* @throws {Error} If latitude is above or below `MAX_LAT`
368+
* @throws {Error} If `zoom` is undefined.
369+
* @example
370+
* var pixel = lonlat.toPixel({lon: -70, lat: 40}, 9) //= {x: 40049.77777777778, y:49621.12736343896}
371+
*/
372+
module.exports.toPixel = function toPixel (input, zoom) {
373+
var ll = normalize(input)
374+
if (ll.lat > MAX_LAT || ll.lat < -MAX_LAT) {
375+
throw new Error('Pixel conversion only works between ' + MAX_LAT + 'N and -' + MAX_LAT + 'S')
376+
}
377+
378+
return {
379+
x: longitudeToPixel(ll.lon, zoom),
380+
y: latitudeToPixel(ll.lat, zoom)
381+
}
382+
}
383+
384+
/**
385+
* Convert a pixel to it's longitude value given a zoom level.
386+
*
387+
* @param {number} x
388+
* @param {number} zoom
389+
* @return {number} longitude
390+
* @example
391+
* var lon = lonlat.pixelToLongitude(40000, 9) //= -70.13671875
392+
*/
393+
function pixelToLongitude (x, zoom) {
394+
return x / zScale(zoom) * 360 - 180
395+
}
396+
module.exports.pixelToLongitude = pixelToLongitude
397+
398+
/**
399+
* Convert a pixel to it's latitude value given a zoom level.
400+
*
401+
* @param {number} y
402+
* @param {number} zoom
403+
* @return {number} latitude
404+
* @example
405+
* var lat = lonlat.pixelToLatitude(50000, 9) //= 39.1982053488948
406+
*/
407+
function pixelToLatitude (y, zoom) {
408+
var latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / zScale(zoom))))
409+
return toDegrees(latRad)
410+
}
411+
module.exports.pixelToLatitude = pixelToLatitude
412+
413+
/**
414+
* From pixel.
415+
*
416+
* @param {lonlat.types.point} pixel
417+
* @param {number} zoom
418+
* @return {lonlat.types.output}
419+
* @example
420+
* var ll = lonlat.fromPixel({x: 40000, y: 50000}, 9) //= {lon: -70.13671875, lat: 39.1982053488948}
421+
*/
422+
module.exports.fromPixel = function fromPixel (pixel, zoom) {
423+
return {
424+
lon: pixelToLongitude(pixel.x, zoom),
425+
lat: pixelToLatitude(pixel.y, zoom)
426+
}
427+
}
428+
290429
function floatize (lonlat) {
291430
var lon = parseFloatWithAlternates([lonlat.lon, lonlat.lng, lonlat.longitude])
292431
var lat = parseFloatWithAlternates([lonlat.lat, lonlat.latitude])

index.test.js

+20
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const ll = require('./')
44

55
const lat = 38.13234
66
const lon = 70.01232
7+
const Z = 9 // Zoom level to use
8+
const pixel = {x: 91026.70779, y: 50497.02600}
79
const lonlat = {lon, lat}
810
const point = {x: lon, y: lat}
911
const coordinates = [lon, lat]
@@ -140,6 +142,24 @@ describe('lonlat', () => {
140142
})
141143
})
142144

145+
describe('pixel', () => {
146+
it('can convert to web mercator pixel coordinates', () => {
147+
const p = ll.toPixel({lat, lon}, Z)
148+
expect(Math.round(p.x)).toBe(Math.round(pixel.x))
149+
expect(Math.round(p.y)).toBe(Math.round(pixel.y))
150+
})
151+
152+
it('can convert from web mercator pixel coordinates', () => {
153+
const l = ll.fromPixel(pixel, Z)
154+
expect(Math.round(l.lat)).toBe(Math.round(lat))
155+
expect(Math.round(l.lon)).toBe(Math.round(lon))
156+
})
157+
158+
it('should throw an error if converting a latitude > MAX_LAT', () => {
159+
expect(() => ll.toPixel({lat: 86, lon}, Z)).toThrow()
160+
})
161+
})
162+
143163
describe('issues', () => {
144164
it('#3 - Does not parse coordinates with 0 for lat or lon', () => {
145165
expect(ll({ lat: 0, lng: 0 })).toEqual({ lat: 0, lon: 0 })

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
"scripts": {
66
"cover": "yarn test --coverage --coverage-paths index.js",
77
"generate-docs": "documentation readme index.js --section=API --markdown-toc=true",
8+
"jest": "mastarm test",
89
"lint": "mastarm lint index.js index.test.js",
910
"lint-docs": "documentation lint index.js",
1011
"pretest": "yarn",
1112
"semantic-release": "semantic-release",
12-
"test": "yarn run lint && yarn run lint-docs && mastarm test"
13+
"test": "yarn run lint && yarn run lint-docs && yarn run jest"
1314
},
1415
"repository": {
1516
"type": "git",

0 commit comments

Comments
 (0)