/*
The idea (a slight variation of a proposal by
<a href="http://extendny.com/">Harold Cooper</a>) is to extend the Manhattan
Grid in all directions, so that every point on Earth can be addressed as
"Xth Ave & Yth St".<br><br>
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 Avenue 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.<br><br>
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 small 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() {

        /*
        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 add 45 degrees for
        the direction of the avenues and subtract 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 = {
            // fixme: Ox.mod ?
            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
        <a href="http://en.wikipedia.org/wiki/Commissioners'_Plan_of_1811">Plan</a>
        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.$('<div>').addClass('post').hide().appendTo($body),
        $sign = Ox.$('<div>').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 Zero Avenue, Zero Street 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 Zero & Zero 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 Zero & Zero.
    */
    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 <code>-90 % 360</code>,
        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, so that the circle will have
    <code>Math.pow(2, precision)</code> 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.
    <code>getXYByLatLng</code> 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 <code>{x: 0, y: 0}</code> at the
    bottom left and <code>{x: 1, y: 1}</code> at the top right.
    */
    function getXYByLatLng(point) {
        return Ox.map(Ox.getXYByLatLng(point), function(v) {
            return v * mapSize;
        });
    }

    /*
    <code>getLatLngByXY</code> is just the inverse, just like Ox.getLatLngByXY.
    */
    function getLatLngByXY(xy) {
        return Ox.getLatLngByXY(Ox.map(xy, function(v) {
            return v / mapSize;
        }));
    }

    /*
    <code>getASByLatLng</code> 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'
        };        
    }

    /*
    <code>getASByXY</code> returns avenue and street at the given screen
    coordinates.
    */
    function getASByXY(xy) {
        return getASByLatLng(getLatLngByXY(xy));
    }

    /*
    <code>drawPath</code> 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.$('<div>')
                .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.$('<img>')
                    .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'
            + '<div class="latlng">'
            + Ox.formatDegrees(latlng.lat, 'lat') + ' / '
            + Ox.formatDegrees(((latlng.lng + 180) % 360) - 180, 'lng')
            + '</div>'
        )
        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();
    }

});