var L = L || exports;

(function () {
"use strict";

/**
 * @fileOverview Leaflet Geometry utilities for distances and linear referencing.
 * @name L.GeometryUtil
 */

L.GeometryUtil = {

    /**
        Shortcut function for planar distance between two {L.LatLng} at current zoom.
        @param {L.Map} map
        @param {L.LatLng} latlngA
        @param {L.LatLng} latlngB
        @returns {Number} in pixels
     */
    distance: function (map, latlngA, latlngB) {
        return map.latLngToLayerPoint(latlngA).distanceTo(map.latLngToLayerPoint(latlngB));
    },

    /**
        Shortcut function for planar distance between a {L.LatLng} and a segment (A-B).
        @param {L.Map} map
        @param {L.LatLng} latlng
        @param {L.LatLng} latlngA
        @param {L.LatLng} latlngB
        @returns {Number} in pixels
    */
    distanceSegment: function (map, latlng, latlngA, latlngB) {
        var p = map.latLngToLayerPoint(latlng),
           p1 = map.latLngToLayerPoint(latlngA),
           p2 = map.latLngToLayerPoint(latlngB);
        return L.LineUtil.pointToSegmentDistance(p, p1, p2);
    },

    /**
        Returns true if the latlng belongs to segment.
        param {L.LatLng} latlng
        @param {L.LatLng} latlngA
        @param {L.LatLng} latlngB
        @param {?Number} [tolerance=0.2]
        @returns {boolean}
     */
    belongsSegment: function(latlng, latlngA, latlngB, tolerance) {
        tolerance = tolerance === undefined ? 0.2 : tolerance;
        var hypotenuse = latlngA.distanceTo(latlngB),
            delta = latlngA.distanceTo(latlng) + latlng.distanceTo(latlngB) - hypotenuse;
        return delta/hypotenuse < tolerance;
    },

    /**
     * Returns total length of line
     * @param {L.Polyline|Array<L.Point>|Array<L.LatLng>}
     * @returns {Number} in meters
     */
    length: function (coords) {
        var accumulated = L.GeometryUtil.accumulatedLengths(coords);
        return accumulated.length > 0 ? accumulated[accumulated.length-1] : 0;
    },

    /**
     * Returns a list of accumulated length along a line.
     * @param {L.Polyline|Array<L.Point>|Array<L.LatLng>}
     * @returns {Number} in meters
     */
    accumulatedLengths: function (coords) {
        if (typeof coords.getLatLngs == 'function') {
            coords = coords.getLatLngs();
        }
        if (coords.length === 0)
            return [];
        var total = 0,
            lengths = [0];
        for (var i = 0, n = coords.length - 1; i< n; i++) {
            total += coords[i].distanceTo(coords[i+1]);
            lengths.push(total);
        }
        return lengths;
    },

    /**
        Returns the closest point of a {L.LatLng} on the segment (A-B)
        @param {L.Map} map
        @param {L.LatLng} latlng
        @param {L.LatLng} latlngA
        @param {L.LatLng} latlngB
        @returns {L.LatLng}
    */
    closestOnSegment: function (map, latlng, latlngA, latlngB) {
        var maxzoom = map.getMaxZoom();
        if (maxzoom === Infinity)
            maxzoom = map.getZoom();
        var p = map.project(latlng, maxzoom),
           p1 = map.project(latlngA, maxzoom),
           p2 = map.project(latlngB, maxzoom),
           closest = L.LineUtil.closestPointOnSegment(p, p1, p2);
        return map.unproject(closest, maxzoom);
    },

    /**
        Returns the closest latlng on layer.
        @param {L.Map} map
        @param {Array<L.LatLng>|L.PolyLine} layer - Layer that contains the result.
        @param {L.LatLng} latlng
        @param {?boolean} [vertices=false] - Whether to restrict to path vertices.
        @returns {L.LatLng}
    */
    closest: function (map, layer, latlng, vertices) {
        if (typeof layer.getLatLngs != 'function')
            layer = L.polyline(layer);

        var latlngs = layer.getLatLngs(),
            mindist = Infinity,
            result = null,
            i, n, distance;

        // Lookup vertices
        if (vertices) {
            for(i = 0, n = latlngs.length; i < n; i++) {
                var ll = latlngs[i];
                distance = L.GeometryUtil.distance(map, latlng, ll);
                if (distance < mindist) {
                    mindist = distance;
                    result = ll;
                    result.distance = distance;
                }
            }
            return result;
        }

        if (layer instanceof L.Polygon) {
            latlngs.push(latlngs[0]);
        }

        // Keep the closest point of all segments
        for (i = 0, n = latlngs.length; i < n-1; i++) {
            var latlngA = latlngs[i],
                latlngB = latlngs[i+1];
            distance = L.GeometryUtil.distanceSegment(map, latlng, latlngA, latlngB);
            if (distance <= mindist) {
                mindist = distance;
                result = L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB);
                result.distance = distance;
            }
        }
        return result;
    },

    /**
        Returns the closest layer to latlng among a list of layers.
        @param {L.Map} map
        @param {Array<L.ILayer>} layers
        @param {L.LatLng} latlng
        @returns {object} with layer, latlng and distance or {null} if list is empty;
    */
    closestLayer: function (map, layers, latlng) {
        var mindist = Infinity,
            result = null,
            ll = null,
            distance = Infinity;

        for (var i = 0, n = layers.length; i < n; i++) {
            var layer = layers[i];
            // Single dimension, snap on points, else snap on closest
            if (typeof layer.getLatLng == 'function') {
                ll = layer.getLatLng();
                distance = L.GeometryUtil.distance(map, latlng, ll);
            }
            else {
                ll = L.GeometryUtil.closest(map, layer, latlng);
                if (ll) distance = ll.distance;  // Can return null if layer has no points.
            }
            if (distance < mindist) {
                mindist = distance;
                result = {layer: layer, latlng: ll, distance: distance};
            }
        }
        return result;
    },

    /**
        Returns the closest position from specified {LatLng} among specified layers,
        with a maximum tolerance in pixels, providing snapping behaviour.
        @param {L.Map} map
        @param {Array<ILayer>} layers - A list of layers to snap on.
        @param {L.LatLng} latlng - The position to snap.
        @param {?Number} [tolerance=Infinity] - Maximum number of pixels.
        @param {?boolean} [withVertices=true] - Snap to layers vertices.
        @returns {object} with snapped {LatLng} and snapped {Layer} or null if tolerance exceeded.
    */
    closestLayerSnap: function (map, layers, latlng, tolerance, withVertices) {
        tolerance = typeof tolerance == 'number' ? tolerance : Infinity;
        withVertices = typeof withVertices == 'boolean' ? withVertices : true;

        var result = L.GeometryUtil.closestLayer(map, layers, latlng);
        if (!result || result.distance > tolerance)
            return null;

        // If snapped layer is linear, try to snap on vertices (extremities and middle points)
        if (withVertices && typeof result.layer.getLatLngs == 'function') {
            var closest = L.GeometryUtil.closest(map, result.layer, result.latlng, true);
            if (closest.distance < tolerance) {
                result.latlng = closest;
                result.distance = L.GeometryUtil.distance(map, closest, latlng);
            }
        }
        return result;
    },

    /**
        Returns the Point located on a segment at the specified ratio of the segment length.
        @param {L.Point} pA
        @param {L.Point} pB
        @param {Number} the length ratio, expressed as a decimal between 0 and 1, inclusive.
        @returns {L.Point} the interpolated point.
    */
    interpolateOnPointSegment: function (pA, pB, ratio) {
        return L.point(
            (pA.x * (1 - ratio)) + (ratio * pB.x),
            (pA.y * (1 - ratio)) + (ratio * pB.y)
        );
    },

    /**
        Returns the coordinate of the point located on a line at the specified ratio of the line length.
        @param {L.Map} map
        @param {Array<L.LatLng>|L.PolyLine} latlngs
        @param {Number} the length ratio, expressed as a decimal between 0 and 1, inclusive
        @returns {Object} an object with latLng ({LatLng}) and predecessor ({Number}), the index of the preceding vertex in the Polyline
        (-1 if the interpolated point is the first vertex)
    */
    interpolateOnLine: function (map, latLngs, ratio) {
        latLngs = (latLngs instanceof L.Polyline) ? latLngs.getLatLngs() : latLngs;
        var n = latLngs.length;
        if (n < 2) {
            return null;
        }

        if (ratio === 0) {
            return {latLng: latLngs[0],
                    predecessor: -1};
        }
        if (ratio == 1) {
            return {latLng: latLngs[latLngs.length -1],
                    predecessor: latLngs.length-2};
        }

        // ensure the ratio is between 0 and 1;
        ratio = Math.max(Math.min(ratio, 1), 0);

        // project the LatLngs as Points,
        // and compute total planar length of the line at max precision
        var maxzoom = map.getMaxZoom();
        if (maxzoom === Infinity)
            maxzoom = map.getZoom();
        var pts = [];
        var lineLength = 0;
        for(var i = 0; i < n; i++) {
            pts[i] = map.project(latLngs[i], maxzoom);
            if(i > 0)
              lineLength += pts[i-1].distanceTo(pts[i]);
        }

        var ratioDist = lineLength * ratio;
        var a = pts[0],
            b = pts[1],
            distA = 0,
            distB = a.distanceTo(b);
        // follow the line segments [ab], adding lengths,
        // until we find the segment where the points should lie on
        var index = 1;
        for (; index < n && distB < ratioDist; index++) {
            a = b;
            distA = distB;
            b = pts[index];
            distB += a.distanceTo(b);
        }
        // compute the ratio relative to the segment [ab]
        var segmentRatio = ((distB - distA) !== 0) ? ((ratioDist - distA) / (distB - distA)) : 0;
        var interpolatedPoint = L.GeometryUtil.interpolateOnPointSegment(a, b, segmentRatio);
        return {
            latLng: map.unproject(interpolatedPoint, maxzoom),
            predecessor: index-2
        };
    },

    /**
        Returns a float between 0 and 1 representing the location of the
        closest point on polyline to the given latlng, as a fraction of total 2d line length.
        (opposite of L.GeometryUtil.interpolateOnLine())
        @param {L.Map} map
        @param {L.PolyLine} polyline
        @param {L.LatLng} latlng
        @returns {Number}
    */
    locateOnLine: function (map, polyline, latlng) {
        var latlngs = polyline.getLatLngs();
        if (latlng.equals(latlngs[0]))
            return 0.0;
        if (latlng.equals(latlngs[latlngs.length-1]))
            return 1.0;

        var point = L.GeometryUtil.closest(map, polyline, latlng, false),
            lengths = L.GeometryUtil.accumulatedLengths(latlngs),
            total_length = lengths[lengths.length-1],
            portion = 0,
            found = false;
        for (var i=0, n = latlngs.length-1; i < n; i++) {
            var l1 = latlngs[i],
                l2 = latlngs[i+1];
            portion = lengths[i];
            if (L.GeometryUtil.belongsSegment(point, l1, l2)) {
                portion += l1.distanceTo(point);
                found = true;
                break;
            }
        }
        if (!found) {
            throw "Could not interpolate " + latlng.toString() + " within " + polyline.toString();
        }
        return portion / total_length;
    },

    /**
        Returns a clone with reversed coordinates.
        @param {L.PolyLine} polyline
        @returns {L.PolyLine}
    */
    reverse: function (polyline) {
        return L.polyline(polyline.getLatLngs().slice(0).reverse());
    },

    /**
        Returns a sub-part of the polyline, from start to end.
        If start is superior to end, returns extraction from inverted line.
        @param {L.Map} map
        @param {L.PolyLine} latlngs
        @param {Number} start ratio, expressed as a decimal between 0 and 1, inclusive
        @param {Number} end ratio, expressed as a decimal between 0 and 1, inclusive
        @returns {Array<L.LatLng>}
     */
    extract: function (map, polyline, start, end) {
        if (start > end) {
            return L.GeometryUtil.extract(map, L.GeometryUtil.reverse(polyline), 1.0-start, 1.0-end);
        }

        // Bound start and end to [0-1]
        start = Math.max(Math.min(start, 1), 0);
        end = Math.max(Math.min(end, 1), 0);

        var latlngs = polyline.getLatLngs(),
            startpoint = L.GeometryUtil.interpolateOnLine(map, polyline, start),
            endpoint = L.GeometryUtil.interpolateOnLine(map, polyline, end);
        // Return single point if start == end
        if (start == end) {
            var point = L.GeometryUtil.interpolateOnLine(map, polyline, end);
            return [point.latLng];
        }
        // Array.slice() works indexes at 0
        if (startpoint.predecessor == -1)
            startpoint.predecessor = 0;
        if (endpoint.predecessor == -1)
            endpoint.predecessor = 0;
        var result = latlngs.slice(startpoint.predecessor+1, endpoint.predecessor+1);
        result.unshift(startpoint.latLng);
        result.push(endpoint.latLng);
        return result;
    },

    /**
        Returns true if first polyline ends where other second starts.
        @param {L.PolyLine} polyline
        @param {L.PolyLine} other
        @returns {bool}
    */
    isBefore: function (polyline, other) {
        if (!other) return false;
        var lla = polyline.getLatLngs(),
            llb = other.getLatLngs();
        return (lla[lla.length-1]).equals(llb[0]);
    },

    /**
        Returns true if first polyline starts where second ends.
        @param {L.PolyLine} polyline
        @param {L.PolyLine} other
        @returns {bool}
    */
    isAfter: function (polyline, other) {
        if (!other) return false;
        var lla = polyline.getLatLngs(),
            llb = other.getLatLngs();
        return (lla[0]).equals(llb[llb.length-1]);
    },

    /**
        Returns true if first polyline starts where second ends or start.
        @param {L.PolyLine} polyline
        @param {L.PolyLine} other
        @returns {bool}
    */
    startsAtExtremity: function (polyline, other) {
        if (!other) return false;
        var lla = polyline.getLatLngs(),
            llb = other.getLatLngs(),
            start = lla[0];
        return start.equals(llb[0]) || start.equals(llb[llb.length-1]);
    },

    /**
        Returns horizontal angle in degres between two points.
        @param {L.Point} a
        @param {L.Point} b
        @returns {float}
     */
    computeAngle: function(a, b) {
        return (Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI);
    },

    /**
       Returns slope (Ax+B) between two points.
        @param {L.Point} a
        @param {L.Point} b
        @returns {Object} with ``a`` and ``b`` properties.
     */
    computeSlope: function(a, b) {
        var s = (b.y - a.y) / (b.x - a.x),
            o = a.y - (s * a.x);
        return {'a': s, 'b': o};
    },
    
    /**
       Returns LatLng of rotated point around specified LatLng center.
        @param {L.LatLng} latlngPoint: point to rotate
        @param {double} angleDeg: angle to rotate in degrees
        @param {L.LatLng} latlngCenter: center of rotation
        @returns {L.LatLng} rotated point
     */
    rotatePoint: function(map, latlngPoint, angleDeg, latlngCenter) {
        var maxzoom = map.getMaxZoom();
        if (maxzoom === Infinity)
            maxzoom = map.getZoom();
        var angleRad = angleDeg*Math.PI/180,
            pPoint = map.project(latlngPoint, maxzoom),
            pCenter = map.project(latlngCenter, maxzoom),
            x2 = Math.cos(angleRad)*(pPoint.x-pCenter.x) - Math.sin(angleRad)*(pPoint.y-pCenter.y) + pCenter.x,
            y2 = Math.sin(angleRad)*(pPoint.x-pCenter.x) + Math.cos(angleRad)*(pPoint.y-pCenter.y) + pCenter.y;
        return map.unproject(new L.Point(x2,y2), maxzoom);
    }
    
};

}());