diff --git a/examples/manhattan_grid/css/example.css b/examples/manhattan_grid/css/example.css new file mode 100644 index 00000000..fae840c3 --- /dev/null +++ b/examples/manhattan_grid/css/example.css @@ -0,0 +1,44 @@ +body { + margin: 0; + background-repeat: repeat-x; +} +img { + position: fixed; + left: 16px; + top: 16px; + border: 2px solid white; + box-shadow: 0 0 4px black; +} +.marker { + position: absolute; + width: 8px; + height: 8px; + border-radius: 4px; + background: rgb(255, 255, 0); + cursor: pointer; +} +.post { + position: absolute; + width: 2px; + height: 32px; + background-color: white; + box-shadow: 0 0 4px black; + z-index: 1000; +} +.sign { + position: absolute; + padding: 2px 4px; + border: 2px solid white; + background-color: rgba(0, 128, 0, 0.75); + font-family: Lucida Grande, Segoe UI, DejaVu Sans, Lucida Sans Unicode, Helvetica, Arial, sans-serif; + font-size: 16px; + text-align: center; + color: white; + box-shadow: 0 0 4px black; + white-space: nowrap; + z-index: 1001; +} +.latlng { + padding: 2px 0 1px 0; + font-size: 10px; +} diff --git a/examples/manhattan_grid/index.html b/examples/manhattan_grid/index.html new file mode 100644 index 00000000..c0dcd82e --- /dev/null +++ b/examples/manhattan_grid/index.html @@ -0,0 +1,13 @@ + + + + Manhattan Grid Coordinate System + + + + + + + + + \ No newline at end of file diff --git a/examples/manhattan_grid/jpg/earth1024.jpg b/examples/manhattan_grid/jpg/earth1024.jpg new file mode 100644 index 00000000..487b3be0 Binary files /dev/null and b/examples/manhattan_grid/jpg/earth1024.jpg differ diff --git a/examples/manhattan_grid/jpg/v=108&x=11174&y=6114&z=14.jpg b/examples/manhattan_grid/jpg/v=108&x=11174&y=6114&z=14.jpg new file mode 100644 index 00000000..a36f34e7 Binary files /dev/null and b/examples/manhattan_grid/jpg/v=108&x=11174&y=6114&z=14.jpg differ diff --git a/examples/manhattan_grid/jpg/v=108&x=1206&y=1539&z=12.jpg b/examples/manhattan_grid/jpg/v=108&x=1206&y=1539&z=12.jpg new file mode 100644 index 00000000..621dd6ae Binary files /dev/null and b/examples/manhattan_grid/jpg/v=108&x=1206&y=1539&z=12.jpg differ diff --git a/examples/manhattan_grid/jpg/v=108&x=4148&y=2817&z=13.jpg b/examples/manhattan_grid/jpg/v=108&x=4148&y=2817&z=13.jpg new file mode 100644 index 00000000..1d13ca33 Binary files /dev/null and b/examples/manhattan_grid/jpg/v=108&x=4148&y=2817&z=13.jpg differ diff --git a/examples/manhattan_grid/js/example.js b/examples/manhattan_grid/js/example.js new file mode 100644 index 00000000..1e4c129d --- /dev/null +++ b/examples/manhattan_grid/js/example.js @@ -0,0 +1,506 @@ +/* +The idea (a slight variation of a proposal by +Harold Cooper) is to extend the Manhattan +Grid in all directions, so that every point on Earth can be addressed as +"Xth Ave & Yth St".

+The origin of this coordinate system is the intersection of Zero Ave (a.k.a. +Avenue A) and Zero St (a.k.a. Houston St). Avenues east of Zero Ave, just as +Streets south of Zero St, have negative numbers. Broadway, which will split not +only Manhattan but the entire globe into an eastern and a western hemisphere, +retains its orientation, but is adjusted slightly so that it originates at the +intersection of Zero & Zero. From there, Broadway, Zero Ave and Zero Street +continue as perfectly straight equatorial lines. All three will intersect once +more, exactly halfway, in the Indian Ocean, (southwest of Australia), at the +point furthest from Manhattan.

+As subsequent avenues remain exactly parallel to Zero Ave, and subsequent +streets exactly parallel to Zero St, they form smaller and smaller circles +around the globe. The northernmost and southernmost streets are small circles +in Central Asia (east of the Caspian Sea) and the southern Pacific (near Easter +Island), the westernmost and easternmost avenues are circles in the North +Pacific (west of Hawaii) and the South Atlantic (near St. Helena). These four +extreme points are the North Pole, South Pole, West Pole and East pole of the +coordinate system. +*/ + +'use strict'; + +/* +Include the Image module. +*/ +Ox.load('Image', function() { + //'Etoile': {lat: 48.87377, lng: 2.29505} + + /* + Ox.EARTH_CIRCUMFERENCE (40075016.68557849) is a built-in constant + */ + var C = Ox.EARTH_CIRCUMFERENCE, + /* + We need a few points to determine the orientation and spacing of + avenues and streets. + */ + points = { + /* + Columbus Circle, the lower western corner of Central Park + */ + '8 & 59': {lat: 40.76807,lng: -73.98190}, + /* + The upper western corner of Central Park, 51 streets up from + Columbus Circle + */ + '8 & 110': {lat: 40.80058, lng: -73.95818}, + /* + The lower eastern corner of Central Park, 3 avenues east of + Columbus Circle + */ + '5 & 59': {lat: 40.76429, lng: -73.97301}, + }, + /* + Ox.getBearing returns the bearing, in degrees, from one lat/lng pair to + another. To make sure that avenues and streets cross at an exact right + angle, we first calculate the bearing of a line that cuts the upper + western quadrant of Columbus Circle in half, then subtract 45 degrees + for the direction of the avenues and add 45 degrees for the direction + of the streets. + */ + bearing = ( + Ox.getBearing(points['8 & 59'], points['8 & 110']) + + Ox.getBearing(points['5 & 59'], points['8 & 59']) + ) / 2 + 180, + bearings = { + avenues: (bearing + 45) % 360, + streets: (bearing - 45) % 360 + }, + /* + Ox.getDistance returns the distance, in meters, from one lat/lng pair + to another. We use this to determine the spacing between avenues and + between streets. The result is 287 meters between Avenues and 81 meters + between streets, which is not too far from the actual + Plan + of the grid. + */ + distances = { + avenues: Ox.getDistance(points['8 & 59'], points['5 & 59']) / 3, + streets: Ox.getDistance(points['8 & 59'], points['8 & 110']) / 51 + }, + /* + The number of avenues and streets, in each direction, is a quarter of + the Earth's circumference divided by the respective spacing. The result + is 34,966 avenues and 123,582 streets. + */ + numbers = Ox.map(distances, function(distance) { + return C / 4 / distance; + }), + colors = { + broadway: 'rgba(0, 0, 255, 0.5)', + avenues: 'rgba(0, 255, 0, 0.5)', + streets: 'rgba(255, 0, 0, 0.5)' + }, + precision = 8, + step = 10000, + $body = Ox.$('body'), + $post = Ox.$('
').addClass('post').hide().appendTo($body), + $sign = Ox.$('
').addClass('sign').hide().appendTo($body), + $images = [], + lines, mapSize, poles; + /* + Ox.getPoint takes a lat/lng pair, a distance and a bearing, and returns the + resulting point. We use this to construct the origin of the coordinate + system, by moving Columbus Circle by minus 59 streets in the direction of + the avenues and then by minus 8 avenues in the direction of the streets. + The resulting point is on Stanton St between Norfolk St and Suffolk St, + which is pretty close to where we expected it to be. + */ + points['0 & 0'] = Ox.getPoint( + Ox.getPoint( + points['8 & 59'], + -59 * distances.streets, + bearings.avenues + ), + -8 * distances.avenues, + bearings.streets + ); + /* + The second intersection of 0th Ave, 0th St and Broadway is half of the + Earth's circumference away from the first one, in any direction. + */ + points['-0 & -0'] = Ox.getPoint( + points['0 & 0'], + Ox.EARTH_CIRCUMFERENCE / 2, + 0 + ); + /* + Now that we have constructed the origin, we can calculate the bearing of + Broadway, which runs from 0th & 0th through Columbus Circle. + */ + bearings.broadway = Ox.getBearing(points['0 & 0'], points['8 & 59']), + /* + Also, we can construct the poles, each of which is a quarter of Earth's + circumference away from 0th & 0th. + */ + poles = { + north: Ox.getPoint(points['0 & 0'], C / 4, bearings.avenues), + south: Ox.getPoint(points['0 & 0'], -C / 4, bearings.avenues), + west: Ox.getPoint(points['0 & 0'], C / 4, bearings.streets), + east: Ox.getPoint(points['0 & 0'], -C / 4, bearings.streets), + /* + Broadway has two poles as well, and constructing them will make drawing + easier. Ox.mod is the modulo function. Unlike -90 % 360, + which in JavaScript is -90, Ox.mod(-90, 360) returns 270. + */ + westBroadway: Ox.getPoint( + points['0 & 0'], + C / 4, + Ox.mod(bearings.broadway - 90, 360) + ), + eastBroadway: Ox.getPoint( + points['0 & 0'], + C / 4, + Ox.mod(bearings.broadway + 90, 360) + ) + }; + /* + Now we calculate circles for Broadway, Avenues and Streets. Ox.getCircle + returns an array of lat/lng pairs that form a circle around a given point, + with a given radius and a given precision (the circle will have + Math.pow(2, precision) segments). + */ + lines = { + /* + Since there is only one Broadway, this is an array with just one circle + that runs around one of the Broadway Poles, at a distance of a quarter + of the Earth's circumference. + */ + broadway: [Ox.getCircle(poles.westBroadway, C / 4, precision)], + /* + For each 10,000th avenue, we compute a circle around the East Pole. + From there, avenues range from -34,966th to 34,966th, so we start at a + distance of 966 avenues from the pole, stop once the distance is half + of the Earth's circumference (the West Pole), and in each step increase + the distance by 10,000 avenues. + */ + avenues: Ox.range( + distances.avenues * (numbers.avenues % step), + C / 2, + distances.avenues * step + ).map(function(distance) { + return Ox.getCircle(poles.east, distance, precision); + }), + /* + Then we do the same for streets, starting at the South Pole. + */ + streets: Ox.range( + distances.streets * (numbers.streets % step), + C / 2, + distances.streets * step + ).map(function(distance) { + return Ox.getCircle(poles.south, distance, precision); + }) + }; + /* + Print our data to the console. + */ + Ox.print(JSON.stringify({ + bearings: bearings, + distances: distances, + numbers: numbers, + points: points, + poles: poles + }, null, ' ')); + + /* + Before we start drawing, we define a few helper functions. + getXYByLatLng returns screen coordinates for a given point. + We use Ox.getXYByLatLng, which takes a lat/lng pair and returns its x/y + position on a 1x1 Mercator position, with {x: 0, y: 0} at the + bottom left and {x: 1, y: 1} at the top right. + */ + function getXYByLatLng(point) { + return Ox.map(Ox.getXYByLatLng(point), function(v) { + return v * mapSize; + }); + } + + /* + getLatLngByXY is just the inverse, just like Ox.getLatLngByXY. + */ + function getLatLngByXY(xy) { + return Ox.getLatLngByXY(Ox.map(xy, function(v) { + return v / mapSize; + })); + } + + /* + getASByLatLng takes lat/lng and returns avenue/street. To + compute the avenue, we subtract the point's distance from the West Pole, in + avenues, from the total number of avenues, to compute the street, we + subtract the point's distance from the North Pole, in avenues, from the + total number of streets. We also return the bearing of the avenues at this + point (which form a right angle with the line from the point to the West + Pole), the bearing of the streets (at a right angle with the line to the + North Pole) and the hemisphere (east or west of Broadway). + */ + function getASByLatLng(point) { + var n = Ox.getDistance(point, poles.north), + w = Ox.getDistance(point, poles.west); + return { + avenue: numbers.avenues - w / distances.avenues, + street: numbers.streets - n / distances.streets, + bearings: { + avenues: Ox.mod(Ox.getBearing(point, poles.west) + ( + w < C / 4 ? -90 : 90 + ), 360), + streets: Ox.mod(Ox.getBearing(point, poles.north) + ( + n < C / 4 ? -90 : 90 + ), 360) + }, + hemisphere: Ox.getDistance(point, poles.eastBroadway) < C / 4 + ? 'E' : 'W' + }; + } + + /* + getASByXY returns avenue and street at the given screen + coordinates. + */ + function getASByXY(xy) { + return getASByLatLng(getLatLngByXY(xy)); + } + + /* + drawPath draws a path of lat/lng pairs on an image. For each + path segment, we have to check if it crosses the eastern or western edge of + the map that splits the Pacific Ocean. Note that our test (a segment + crosses the edge if it spans more than 180 degrees longitude) is obviously + incorrect, but works in our case, since all segments are sufficiently + short. + */ + function drawPath(image, path, options) { + var n, parts = [[]]; + /* + ... + */ + path.push(path[0]); + n = path.length; + Ox.loop(n, function(i) { + var lat, lng, split; + /* + Append each point to the last part. + */ + Ox.last(parts).push(path[i]); + if (Math.abs(path[i].lng - path[(i + 1) % n].lng) > 180) { + /* + If the next line crosses the edge, get the lat/lng of the + points where the line leaves and enters the map. + */ + lat = Ox.getCenter(path[i], path[i + 1]).lat; + lng = path[i].lng < 0 ? [-180, 180] : [180, -180]; + /* + Append the first point to the last part and create a new part + with the second point. + */ + Ox.last(parts).push({lat: lat, lng: lng[0]}); + parts.push([{lat: lat, lng: lng[1]}]); + } + }); + /* + We draw each part, translating lat/lng to [x, y]. + */ + parts.forEach(function(part) { + image.drawPath(part.map(function(point) { + var xy = getXYByLatLng(point); + return [xy.x, xy.y]; + }), options); + }); + } + + /* + ... + */ + Ox.Image('jpg/earth1024.jpg', function(image) { + + mapSize = image.getSize().width; + drawPath(image, Ox.getCircle(points['0 & 0'], C / 4, 8), { + color: 'rgba(255, 255, 255, 0.25)' + }); + ['streets', 'avenues', 'broadway'].forEach(function(type) { + lines[type].forEach(function(line, i) { + drawPath(image, line, { + color: colors[type], + width: i == lines[type].length / 2 - 0.5 ? 2 : 1 + }); + }); + }); + + $body.css({ + minWidth: mapSize + 'px', + height: mapSize + 'px', + backgroundImage: 'url(' + image.src() + ')' + }) + .bind({ + click: click, + mouseover: mouseover, + mousemove: mousemove, + mouseout: mouseout + }); + + [ + {point: points['0 & 0'], title: 'Manhattan', z: 12}, + {point: {lat: 48.87377, lng: 2.29505}, title: 'Paris', z: 13}, + {point: poles.north, title: 'Uzbekistan', z: 14} + ].forEach(function(marker, i) { + var as = getASByLatLng(marker.point), + g = {s: 256, v: 108, z: marker.z}, + xy = getXYByLatLng(marker.point); + Ox.print(as) + Ox.extend(g, Ox.map(Ox.getXYByLatLng(marker.point), function(v) { + return Math.floor(v * Math.pow(2, g.z)); + })); + Ox.$('
') + .addClass('marker') + .css({ + left: xy.x - 4 + 'px', + top: xy.y - 4 + 'px' + }) + .bind({ + click: function() { + $images.forEach(function($image) { + $image.hide(); + }); + $images[i].show(); + } + }) + .appendTo($body); + Ox.Image(Ox.formatString( + 'jpg/v={v}&x={x}&y={y}&z={z}.jpg', g + ), function(image) { + if (marker.title == 'Uzbekistan') { + Ox.range( + distances.streets * (numbers.streets % 1), + 2000, + distances.streets + ).forEach(function(distance) { + var circle = mapLine(Ox.getCircle( + poles.north, distance, precision + ), g); + image.drawPath(circle, { + close: true, + color: colors.streets + }); + }); + } else { + Ox.loop(-200, 200, function(street) { + var line = getLine(g, marker.point, as, 'streets', street); + image.drawPath(line, { + color: colors.streets, + width: marker.title == 'Paris' || street ? 1 : 2 + }); + }); + } + Ox.loop(-20, 20, function(avenue) { + var line = getLine(g, marker.point, as, 'avenues', avenue); + image.drawPath(line, { + color: colors.avenues, + width: marker.title == 'Paris' || avenue ? 1 : 2 + }); + }); + if (marker.title == 'Manhattan') { + var line = mapLine(Ox.getLine( + Ox.getPoint(marker.point, -10000, bearings.broadway), + Ox.getPoint(marker.point, 10000, bearings.broadway), + 1 + ), g); + image.drawPath(line, {color: 'rgba(0, 0, 255, 0.5)', width: 2}); + } + ['black', 'white'].forEach(function(color, i) { + image.drawText(marker.title, [240 - i, 240 - i], { + color: color, + font: 'bold 16px Lucida Grande, sans-serif', + textAlign: 'right' + }); + }) + $images[i] = Ox.$('') + .attr({src: image.src()}) + .hide() + .appendTo($body); + }); + }); + + }); + + function getLine(g, point, as, type, i) { + point = Ox.getPoint( + point, + i * distances[type], + as.bearings[type == 'avenues' ? 'streets' : 'avenues'] + ); + return mapLine(Ox.getLine( + Ox.getPoint(point, -10000, as.bearings[type]), + Ox.getPoint(point, 10000, as.bearings[type]), + 1 + ), g); + } + + function mapLine(line, g) { + return line.map(function(point) { + var xy = Ox.map(Ox.getXYByLatLng(point), function(value, key) { + return (value * Math.pow(2, g.z) - g[key]) * g.s; + }); + return [xy.x, xy.y]; + }); + } + + function click(e) { + if (e.target.className != 'marker') { + $images.forEach(function($image) { + $image.hide(); + }); + } + } + + function mouseover() { + $post.show(); + $sign.show(); + } + + function mousemove(e) { + if (e.target.tagName == 'IMG') { + mouseout(); + return; + } + var left = window.scrollX, + right = left + window.innerWidth, + top = window.scrollY, + xy = {x: left + e.clientX, y: top + e.clientY}, + latlng = getLatLngByXY(xy), + as = getASByXY(xy), + width, height, invertX, invertY; + $sign.html( + Ox.formatNumber(as.avenue, 0) + 'th Av & ' + + as.hemisphere + ' ' + + Ox.formatNumber(as.street, 0) + 'th St' + + '
' + + Ox.formatDegrees(latlng.lat, 'lat') + ' / ' + + Ox.formatDegrees(((latlng.lng + 180) % 360) - 180, 'lng') + + '
' + ) + width = $sign.width(); + height = $sign.height(); + invertX = xy.x + width > right; + invertY = xy.y - height - 32 < top; + $sign.css({ + left: xy.x + (invertX ? 1 - width : -1) + 'px', + top: xy.y + (invertY ? 32 : -32 - height) + 'px' + }); + $post.css({ + left: xy.x - 1 + 'px', + top: xy.y + (invertY ? 0 : -32 - height) + 'px', + height: $sign.height() + 32 + 'px' + }); + } + + function mouseout() { + $post.hide(); + $sign.hide(); + } + +}); \ No newline at end of file