const origin_options = ["sw", "se", "nw", "ne", "center", "centroid"]

"""
    transform_rotate([geojson::T[, angle::Real], pivot::Point=nothing, mutate::Bool=false) where {T <: AbstractGeometry}

Rotates any geojson Feature or Geometry of a specified angle, around its `centroid` or a given `pivot` point;
all rotations follow the right-hand rule.

# Examples
```jldoctest
julia> point = Point([-75.69926351308823,45.43145021122502])
Point([-75.6993, 45.4315])

julia> transform_rotate(geojson=point, angle=80., pivot=Point([-75.6, 45.3]))
Point([-75.433, 45.3915])
```
"""
function transform_rotate(; geojson::T, angle::Real, pivot::Point=nothing, mutate::Bool=false) where {T <: AbstractGeometry}
    if angle === 0
        return geojson
    end

    geo = []

    if mutate
        geo = deepcopy(geojson)
    else
        geo = geojson
    end

    type = geotype(geo)

    pivot === nothing && (pivot = centroid(geo))
    coords = geo.coordinates

    if type === :Point

        initAngle = rhumb_bearing(pivot.coordinates, coords)
        finalAngle = initAngle + angle
        dist = rhumb_distance(pivot.coordinates, coords)
        newCoords = rhumb_destination(pivot.coordinates, dist, finalAngle).coordinates
        coords[1] = newCoords[1]
        coords[2] = newCoords[2]

    elseif type === :Polygon || type === :MultiLineString

        for i in eachindex(coords)
            for j in eachindex(coords[i])
                initAngle = rhumb_bearing(pivot.coordinates, coords[i][j])
                finalAngle = initAngle + angle
                dist = rhumb_distance(pivot.coordinates, coords[i][j])
                newCoords = rhumb_destination(pivot.coordinates, dist, finalAngle).coordinates
                coords[i][j][1] = newCoords[1]
                coords[i][j][2] = newCoords[2]
            end
        end
    elseif type === :LineString

        for i in eachindex(coords)
            initAngle = rhumb_bearing(pivot.coordinates, coords[i])
            finalAngle = initAngle + angle
            dist = rhumb_distance(pivot.coordinates, coords[i])
            newCoords = rhumb_destination(pivot.coordinates, dist, finalAngle).coordinates
            coords[i][1] = newCoords[1]
            coords[i][2] = newCoords[2]
        end
    end

    return geo
end

transform_rotate!(; geojson::T, angle::Real, pivot::Point=nothing) where {T <: AbstractGeometry} = transform_rotate(geojson=geojson, angle=angle, pivot=pivot, mutate=true)

"""
    transform_translate([geojson::T[, distance::R[, direction::R]]], vertical::R=0, mutate::Bool=false, units::String="kilometers") where {T <: Union{AbstractFeature, AbstractGeometry}, R <: Real}

Moves any geojson Feature or Geometry of a specified distance along a Rhumb Line
on the provided direction angle.

# Examples
```jldoctest
julia> poly = Polygon([[[0, 29], [3.5, 29], [2.5, 32], [0, 29]]])
Polygon(Array{Array{Float64,1},1}[[[0.0, 29.0], [3.5, 29.0], [2.5, 32.0], [0.0, 29.0]]])

julia> transform_translate(poly, 300, 70)
Polygon(Array{Array{Float64,1},1}[[[2.91184, 29.9228], [6.41184, 29.9228], [5.50479, 32.9228], [2.91184, 29.9228]]])
```
"""
function transform_translate(geojson::T, distance::R, direction::R, vertical::R=0, mutate::Bool=false, units::String="kilometers") where {T <: Union{AbstractFeature, AbstractGeometry}, R <: Real}
    (distance == 0 && vertical == 0) && return geojson

    if distance < 0
        distance  = -distance
        direction = -direction
    end

    !mutate && (geojson = deepcopy(geojson))

    coords = []
    type = nothing

    if typeof(geojson) <: AbstractGeometry
        coords = geojson.coordinates
        type = geotype(geojson)
    else
        coords = geojson.geometry.coordinates
        type = geotype(geojson.geometry)
    end

    if type === :Point
        newCoords = rhumb_destination(coords, distance, direction, units).coordinates
        coords[1] = newCoords[1]
        coords[2] = newCoords[2]
        (vertical != 0 && length(coords) === 3) && (coords[3] += vertical)

    elseif type === :Polygon || type === :MultiLineString
        for i in eachindex(coords)
            for j in eachindex(coords[i])
                newCoords = rhumb_destination(coords[i][j], distance, direction, units).coordinates
                coords[i][j][1] = newCoords[1]
                coords[i][j][2] = newCoords[2]
                (vertical != 0 && length(coords[i][j]) === 3) && (coords[i][j][3] += vertical)
            end
        end
    elseif type === :LineString
        for i in eachindex(coords)
            newCoords = rhumb_destination(coords[i], distance, direction, units).coordinates
            coords[i][1] = newCoords[1]
            coords[i][2] = newCoords[2]
            (vertical != 0 && length(coords[i]) === 3) && (coords[i][3] += vertical)
        end
    end

    return geojson
end

transform_translate!(geojson::T, distance::R, direction::R, vertical::R=0, units::String="kilometers") where {T <: Union{AbstractFeature, AbstractGeometry}, R <: Real} = transform_translate(geojson, distance, direction, vertical, true, units)

"""
    transform_scale([geojson::T[, factor::Float64]], origin::String="centroid") where {T <: AbstractFeatureCollection}

Scale a GeoJson from a given point by a factor of scaling (ex: factor=2 would make the GeoJson 200% larger).
If a FeatureCollection is provided, the origin point will be calculated based on each individual Feature.

# Examples
```jldoctest
julia> coll = FeatureCollection([Feature(Point([-75.69926351308823,45.43145021122502])), Feature(Polygon([[[0, 29], [3.5, 29], [2.5, 32], [0, 29]]]))])
FeatureCollection{Feature}(Feature[Feature(Point([-75.6993, 45.4315]), Dict{String,Any}()), Feature(Polygon(Array{Array{Float64,1},1}[[[0.0, 29.0], [3.5, 29.0], [2.5, 32.0], [0.0, 29.0]]]), Dict{String,Any}())], nothing, nothing)

julia> transform_scale(coll, 0.1)
FeatureCollection{Feature}(Feature[Feature(Point([-75.6993, 45.4315]), Dict{String,Any}()), Feature(Polygon(Array{Array{Float64,1},1}[[[1.3495, 29.675], [1.70067, 29.675], [1.59896, 29.975], [1.3495, 29.675]]]), Dict{String,Any}())], nothing, nothing)
```
"""
function transform_scale(geojson::T, factor::Real, origin::String="centroid", mutate::Bool=false) where {T <: AbstractFeatureCollection}

    geo = []
    if !mutate
        geo = deepcopy(geojson)
    else
        geo = geojson
    end

    for (i, feat) in enumerate(geo.features)
        geo.features[i] = scale(feat, factor, origin)
    end

    return geo
end

transform_scale!(geojson::T, factor::Real, origin::String="centroid") where {T <: AbstractFeatureCollection} = transform_scale(geojson, factor, origin, true)

"""
    scale([feature::Feature[, factor::Real]], origin::String="centroid")

Scale a Feature.

# Examples
```jldoctest
julia> feature = Feature(Polygon([[[0, 29], [3.5, 29], [2.5, 32], [0, 29]]]))
Feature(Polygon(Array{Array{Float64,1},1}[[[0.0, 29.0], [3.5, 29.0], [2.5, 32.0], [0.0, 29.0]]]), Dict{String,Any}())

julia> scale(feature, 0.1)
Feature(Polygon(Array{Array{Float64,1},1}[[[1.3495, 29.675], [1.70067, 29.675], [1.59896, 29.975], [1.3495, 29.675]]]), Dict{String,Any}())
```
"""
function scale(feature::Feature, factor::Real, origin::String="centroid")

    if !(origin in origin_options)
        throw(error("'$(origin)' is not a valid option. Allowed values are: $(origin_options)"))
    end

    (geotype(feature.geometry) === :Point || factor === 1) && return feature

    type = geotype(feature.geometry)
    coords = feature.geometry.coordinates
    center = getOrigin(feature, origin)

    if type === :LineString

        for i in eachindex(coords)
            start = rhumb_distance(center.coordinates, coords[i])
            bearing = rhumb_bearing(center.coordinates, coords[i])
            distance = start * factor

            newCoords = rhumb_destination(center.coordinates, distance, bearing).coordinates

            coords[i][1] = newCoords[1]
            coords[i][2] = newCoords[2]

            if length(coords) === 3
                coords[i][3] *= factor
            end
        end
    elseif type === :Polygon || type === :MultiLineString

        for i in eachindex(coords)
            for j in eachindex(coords[i])
                start = rhumb_distance(center.coordinates, coords[i][j])
                bearing = rhumb_bearing(center.coordinates, coords[i][j])
                distance = start * factor

                newCoords = rhumb_destination(center.coordinates, distance, bearing).coordinates

                coords[i][j][1] = newCoords[1]
                coords[i][j][2] = newCoords[2]

                if length(coords) === 3
                    coords[i][j][3] *= factor
                end
            end
        end
    end

    return feature

end

function getOrigin(geojson::Feature, origin::String)
    if origin == nothing || origin === ""
        origin = "centroid"
    end

    sw = ["sw", "southwest", "westsouth", "bottomleft"]
    se = ["se", "southeast", "eastsouth", "bottomright"]
    nw = ["nw", "northwest", "westnorth", "topleft"]
    ne = ["ne", "northeast", "eastnorth", "topright"]

    box = bbox(geojson)
    west = box[1]
    south = box[2]
    east = box[3]
    north = box[4]

    if origin in sw
        return Point([west, south])
    elseif origin in se
        return Point([east, south])
    elseif origin in nw
        return Point([west, north])
    elseif origin in ne
        return Point([east, north])
    elseif origin === "center"
        return center(geojson.geometry)
    elseif origin === "centroid"
        return centroid(geojson.geometry)
    else
        throw(error("Invalid origin."))
    end
end

"""
    explode([geojson::T], pointsOnly::Bool=false)::FeatureCollection where {T <: Union{AbstractFeatureCollection, AbstractGeometry}}

Takes a Geometry or a FeatureCollection and returns all positions as Points.

# Examples
```jldoctest
julia> poly = Polygon([[[100, 0], [101, 0], [101, 1], [100, 1], [100, 0]]])
Polygon(Array{Array{Float64,1},1}[[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]])

julia> explode(poly, true)
5-element Array{Point,1}:
 Point([100.0, 0.0])
 Point([101.0, 0.0])
 Point([101.0, 1.0])
 Point([100.0, 1.0])
 Point([100.0, 0.0])

```
"""
function explode(geojson::T, pointsOnly::Bool=false) where {T <: Union{AbstractFeatureCollection, AbstractGeometry}}
    points::Vector{Point} = []

    if geotype(geojson) === :FeatureCollection
        for feat in geojson.features
            geom = feat.geometry

            if geotype(geom) === :Point
                push!(points, geom)

            elseif geotype(geom) === :Polygon || geotype(geom) === :MultiLineString

                for i in eachindex(geom.coordinates)
                    for j in eachindex(geom.coordinates[i])
                        push!(points, Point(geom.coordinates[i][j]))
                    end
                end
            elseif geotype(geom) === :LineString

                for i in eachindex(geom.coordinates)
                    push!(points, Point(geom.coordinates[i]))
                end

            end
        end
    else
        if geotype(geojson) === :Point
            push!(points, geojson)

        elseif geotype(geojson) === :Polygon || geotype(geojson) === :MultiLineString

            for i in eachindex(geojson.coordinates)
                for j in eachindex(geojson.coordinates[i])
                    push!(points, Point(geojson.coordinates[i][j]))
                end
            end
        elseif geotype(geojson) === :LineString

            for i in eachindex(geojson.coordinates)
                push!(points, Point(geojson.coordinates[i]))
            end

        end

    end

    pointsOnly && return points
    return FeatureCollection([Feature(x) for x in points])
end

"""
    flip([geojson::T], mutate::Bool=false) where {T <: Union{AbstractFeature, AbstractGeometry}}

Take input Features and Geometries and flips all of their coordinates from `[x, y]` to `[y, x]`.

# Examples
```jldoctest
julia> point = Point([77.34374999999999,43.58039085560784,3000])
Point([77.3437, 43.5804, 3000.0])

julia> flip(point)
Point([43.5804, 77.3437, 3000.0])
```
"""
function flip(geojson::T, mutate::Bool=false) where {T <: Union{AbstractFeature, AbstractGeometry}}
    type = geotype(geojson)
    coords = []
    geom = nothing

    geo = []
    if !mutate
        geo = deepcopy(geojson)
    else
        geo = geojson
    end

    if type === :Feature
        geom = geo.geom
        coords = geo.geometry.coordinates
    else
        geom = geo
        coords = geo.coordinates
    end

    if geotype(geom) === :Point
        x = coords[1]
        y = coords[2]
        coords[1] = y
        coords[2] = x
    elseif geotype(geom) === :Polygon || geotype(geom) === :MultiLineString
        for i in eachindex(coords)
            x = coords[i][1]
            y = coords[i][2]
            coords[i][1] = y
            coords[i][2] = x
        end
    elseif geotype(geom) === :LineString
        for i in eachindex(coords[1])
            x = coords[1][i][1]
            y = coords[1][i][2]
            coords[1][i][1] = y
            coords[1][i][2] = x
        end
    end

    return geo

end

flip!(geojson::T) where {T <: Union{AbstractFeature, AbstractGeometry}} = flip(geojson, true)

"""
Cohen-Sutherland line clippign algorithm, adapted to efficiently to
handle polylines rather than just segments
"""
function lineclip(points::Vector{P}, bbox::Vector{T}) where {T <: Real, P <: AbstractPoint}
    len = length(points)
    codeA = bitcode(points[1], bbox)
    parts = []
    results = []
    let a, b, codeB, lastCode end

    for i in 2:len
        a = points[i - 1]
        b = points[i]

        codeB = lastCode = bitcode(b, bbox)

        while true
            if !(codeA | codeB)
                push!(parts, a)

                if codeB !== lastCode
                    push!(parts, b)

                    if i < len - 1
                        push!(results, parts)
                        parts = []
                    end
                elseif i === len - 1
                    push!(parts, b)
                end

                break
            elseif (codeA & codeB)
                break

            elseif codeA
                a = intersection(a, b, codeB, bbox)
                codeA = bitcode(b, bbox)

            else
                b = intersection(a, b, codeB, bbox)
                codeB = bitcode(b, bbox)
            end
        end
        codeA = lastCode
    end
    length(part) > 0 && push!(results, parts)

    return results
end

"""
Sutherland-Hodgeman polygon clipping algorithm.
"""
function polygonclip(points::Vector{P}, bbox::Vector{T}) where {P <: AbstractPoint, T <: Real}
    let result, edge, prev, pInside, p, inside end
    ranges = [1, 2, 4, 8]

    for edge in ranges
        result = []

        prev = points[length(points) - 1]
        pInside = !(bitcode(prev, bbox) & edge)

        for i in 1:length(points)
            p = points[i]
            inside = !(bitcode(p, bbox) & edge)

            inside !== pInside && push!(result, intersection(prev, p, edge, bbox))

            inside && push!(result, p)

            prev = p
            pInside = inside
        end

        points = result
        length(points) <= 0 && break
    end

    return result
end

"""
Intersect a segment against one of the 4 lines that make up the bbox
"""
function intersection(a::Point, b::Point, edge, bbox::Vector{T}) where {T <: Real}
    return edge & 8 ? [a[1] + (b[1] - a[1]) * (bbox[4] - a[1]) / (b[1] - a[1]), bbox[4]] : # top
           edge & 4 ? [a[1] + (b[1] - a[1]) * (bbox[2] - a[2]) / (b[2] - a[2]), bbox[2]] : # bottom
           edge & 2 ? [bbox[3], a[2] + (b[2] - a[2]) * (bbox[3] - a[1]) / (b[1] - a[1])] : # right
           edge & 1 ? [bbox[1], a[2] + (b[2] - a[2]) * (bbox[1] - a[1]) / (b[1] - a[1])] : # left
           nothing
end

@inline function bitcode(p::Point, bbox::Vector{T}) where {T <: Real}
    code = 0

    p[1] < bbox[1] && (code |= 1)
    p[1] > bbox[3] && (code |= 2)

    p[2] < bbox[2] && (code |= 4)
    p[2] > bbox[4] && (code |= 8)

    return code
end

"""
    polygon_tangents(pt::Point, poly::Polygon)

Finds the tangents of a Polygon from a Point.

# Examples
```jldoctest
julia> point = Point([92.46093749999999,54.67383096593114])
Point([92.4609, 54.6738])

julia> poly = Polygon([[[100, 0], [101, 0], [101, 1], [100, 1], [100, 0]]])
Polygon(Array{Array{Float64,1},1}[[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]])

julia> poly = Polygon([[[48.1641, 20.6328], [76.6406, 20.6328], [76.6406, 38.8226], [48.1641, 38.8226], [48.1641, 20.6328]]])
Polygon(Array{Array{Float64,1},1}[[[48.1641, 20.6328], [76.6406, 20.6328], [76.6406, 38.8226], [48.1641, 38.8226], [48.1641, 20.6328]]])

julia> polygon_tangents(point, poly)
FeatureCollection{Feature}(Feature[Feature(Point([48.1641, 38.8226]), Dict{String,Any}()), Feature(Point([76.6406, 20.6328]), Dict{String,Any}())], nothing, nothing)
 ```
"""
function polygon_tangents(pt::Point, poly::Polygon)
    ptCoords = pt.coordinates
    polyCoords = poly.coordinates

    box = bbox(poly)
    nearest = nothing
    nearestIndex = 1

    if ptCoords[1] > box[1] && ptCoords[1] < box[3] && ptCoords[2] > box[2] && ptCoords[2] < box[4]
        nearest = nearestpoint(pt, explode(poly, true))
        nearestIndex = findfirst(x -> x == nearest, explode(poly, true))
    end

    rtan = polyCoords[1][nearestIndex]
    ltan = polyCoords[1][1]

    if nearest != nothing
        nearest.coordinates[2] < ptCoords[2] && (ltan = polyCoords[1][nearestIndex])
    end

    enext = nothing
    eprev = isleft(polyCoords[1][1], polyCoords[1][length(polyCoords[1]) - 1], ptCoords)
    out = process(polyCoords[1], ptCoords, eprev, enext, rtan, ltan)

    rtan = out[1]
    ltan = out[2]

    return FeatureCollection([Feature(Point(rtan)), Feature(Point(ltan))])
end

function isleft(point1, point2, point3)
    return (point2[1] - point1[1]) * (point3[2] - point1[2]) - (point3[1] - point1[1]) * (point2[2] - point1[2])
end

function isabove(point1, point2, point3)
    return isleft(point1, point2, point3) > 0
end

function isbelow(point1, point2, point3)
    return isleft(point1, point2, point3) < 0
end

function process(polyCoords, ptCoords, eprev, enext, rtan, ltan)
    for i in 1:length(polyCoords) - 1
        curr = polyCoords[i]
        next = polyCoords[i + 1]

        (i === length(polyCoords) - 1) && (next = polyCoords[1])

        enext = isleft(curr, next, ptCoords)
        if eprev <= 0 && enext > 0
            !(isbelow(ptCoords, curr, rtan)) && (rtan = curr)
        elseif eprev > 0 && enext <= 0
            !(isabove(ptCoords, curr, ltan)) && (ltan = curr)
        end
        eprev = enext
    end
    return [rtan, ltan]
end

"""
    polygon_to_line([poly::Polygon])

Converts a Polygon to LineString or MultiLineString

# Examples
```jldoctest
julia> poly = Polygon([[[-2.275543, 53.464547],[-2.275543, 53.489271],[-2.215118, 53.489271],[-2.215118, 53.464547],[-2.275543, 53.464547]]])
Polygon(Array{Array{Float64,1},1}[[[-2.27554, 53.4645], [-2.27554, 53.4893], [-2.21512, 53.4893], [-2.21512, 53.4645], [-2.27554, 53.4645]]])

julia> polygon_to_line(poly)
LineString(Array{Float64,1}[[-2.27554, 53.4645], [-2.27554, 53.4893], [-2.21512, 53.4893], [-2.21512, 53.4645], [-2.27554, 53.4645]])
```
"""
function polygon_to_line(poly::Polygon)
    return coordinatesToLine(poly.coordinates)
end

function coordinatesToLine(coords::Vector{Vector{Position}})
    length(coords) > 1 && return MultiLineString(coords)
    return LineString(coords[1])
end

"""
    convert_to([geojson::AbstractGeometry[, projection::String]], mutate::Bool=false)

Convert a GeoJSON geojsonect to the defined `projection`.
"""
function convert_to(geojson::AbstractGeometry, projection::String, mutate::Bool=false)
    allowed_proj = ["mercator", "wgs84"]

    !(projection in allowed_proj) && throw(error("$(projection) is not a valid option."))

    type = geotype(geojson)

    type === :Point && return Point((projection === "mercator") ? to_mercator(geojson) : to_WGS84(geojson))

    geo = []
    if !mutate
        geo = deepcopy(geojson)
    else
        geo = geojson
    end

    coords = geo.coordinates
    if type === :LineString
        for i in eachindex(coords)
            newCoords = (projection === "mercator") ? to_mercator(Point(coords[i])) : to_WGS84(Point(coords[i]))
            coords[i][1] = newCoords[1]
            coords[i][2] = newCoords[2]
        end
    elseif type === :Polygon
        for i in eachindex(coords[1])
            newCoords = (projection === "mercator") ? to_mercator(Point(coords[1][i])) : to_WGS84(Point(coords[1][i]))
            coords[1][i][1] = newCoords[1]
            coords[1][i][2] = newCoords[2]
        end
    else
        throw(error("$(type) is not supported."))
    end

    return geo
end

convert_to(geojson::Feature, projection::String, mutate::Bool=false) = convert_to(geojson.geometry, projection, mutate)
convert_to!(geojson::Feature, projection::String) = convert_to(geojson.geometry, projection, true)
convert_to!(geojson::AbstractGeometry, projection::String) = convert_to(geojson, projection, true)

"""
    combine([ft::FeatureCollection])

Combine a FeatureCollection of Point, LineString, or Polygon features
into MultiPoint, MultiLineString, or MultiPolygon features.

# Examples
```jldoctest
julia> l1 = LineString([[102.0,-10.0],[130.0,4.0]])
LineString(Array{Float64,1}[[102.0, -10.0], [130.0, 4.0]])

julia> l2 = LineString([[40.0,-20.0],[150.0,18.0]])
LineString(Array{Float64,1}[[40.0, -20.0], [150.0, 18.0]])

julia> combine(FeatureCollection([Feature(l1), Feature(l2)]))
FeatureCollection{Feature}(Feature[Feature(MultiLineString(Array{Array{Float64,1},1}[[[102.0, -10.0], [130.0, 4.0], [40.0, -20.0], [150.0, 18.0]]]), Dict{String,Any}())], nothing, nothing)
```
"""
function combine(ft::FeatureCollection)

    groups = Dict{String, Any}([
        "MultiPoint" => [[], Dict{String, Any}()],
        "MultiLineString" => [[], Dict{String, Any}()],
        "MultiPolygon" => [[], Dict{String, Any}()]]
    )

    function add_to_group(ft::Feature, key::String, multi::Bool)
        if multi
            if geotype(ft.geometry) === :MultiPoint
                for i in eachindex(ft.geometry.coordinates)
                    push!(groups[key][1], ft.geometry.coordinates[i])
                end
            elseif geotype(ft.geometry) === :MultiLineString
                for i in eachindex(ft.geometry.coordinates[1])
                    push!(groups[key][1], ft.geometry.coordinates[1][i])
                end
            else
                for i in eachindex(ft.geometry.coordinates[1][1])
                    push!(groups[key][1], ft.geometry.coordinates[1][1][i])
                end
            end
        else
            if geotype(ft.geometry) === :Point
                groups[key][1] = vcat(groups[key][1], [ft.geometry.coordinates])
            elseif geotype(ft.geometry) === :LineString
                groups[key][1] = vcat(groups[key][1], ft.geometry.coordinates)
            elseif geotype(ft.geometry) === :Polygon
                groups[key][1] = vcat(groups[key][1], ft.geometry.coordinates...)
            end
        end # function

        merge!(groups[key][2], ft.properties)
    end

    for feat in ft.features
        type = geotype(feat.geometry)
        try
            add_to_group(feat, string(type), true)
        catch e
            if isa(e, KeyError)
                newType = "Multi" * string(type)
                add_to_group(feat, newType, false)
            else
                throw(e)
            end
        end
    end

    features::Vector{Feature} = []

    groups = filter( x -> length(groups[x.first][1]) > 0, groups)

    for g in keys(groups)
        if g === "MultiPoint"
            point = MultiPoint(values(groups[g][1]))
            push!(features, Feature(point, groups[g][2]))
        elseif g === "MultiLineString"
            point = MultiLineString([values(groups[g][1])])
            push!(features, Feature(point, groups[g][2]))
        elseif g === "MultiPolygon"
            point = MultiPolygon([[values(groups[g][1])]])
            push!(features, Feature(point, groups[g][2]))
        end
    end

    return FeatureCollection(features)
end

"""
    tag(fc1::FeatureCollection, fc2::FeatureCollection, in_field::String, out_field::String)::FeatureCollection

Take a set of Points and a set of Polygons and performs a spatial join.
"""
function tag(fc1::FeatureCollection, fc2::FeatureCollection, in_field::String, out_field::String)::FeatureCollection
    points = deepcopy(fc1)
    polys = deepcopy(fc2)

    for feat in points.features
        geotype(feat.geometry) !== :Point && throw(error("$(feat.geometry) is not a supported geometry."))
        for feat2 in polys.features
            geotype(feat2.geometry) !== :Polygon && throw(error("$(feat2.geometry) is not a supported geometry."))

            if point_in_polygon(feat.geometry, feat2.geometry)
                feat.properties[out_field] = feat2.properties[in_field]
            end
        end
    end
    return points
end

"""
    simplify(geojson::FeatureCollection, tolerance::Real=1., hq::Bool=false, mutate::Bool=false)

Take a FeatureCollection and return a simplified version.
Internally uses an adaptation of [simplify-js](http://mourner.github.io/simplify-js/) to perform simplification using the Ramer-Douglas-Peucker algorithm.

# Examples
```jldoctest
julia> poly = Polygon([[
    [-70.603637, -33.399918],
    [-70.614624, -33.395332],
    [-70.639343, -33.392466],
    [-70.659942, -33.394759],
    [-70.683975, -33.404504],
    [-70.697021, -33.419406],
    [-70.701141, -33.434306],
    [-70.700454, -33.446339],
    [-70.694274, -33.458369],
    [-70.682601, -33.465816],
    [-70.668869, -33.472117],
    [-70.646209, -33.473835],
    [-70.624923, -33.472117],
    [-70.609817, -33.468107],
    [-70.595397, -33.458369],
    [-70.587158, -33.442901],
    [-70.587158, -33.426283],
    [-70.590591, -33.414248],
    [-70.594711, -33.406224],
    [-70.603637, -33.399918]]])

julia> simplify(poly, 0.01)
Polygon(Array{Array{Float64,1},1}[[[-70.6036, -33.3999], [-70.684, -33.4045], [-70.7011, -33.4343], [-70.6943, -33.4584], [-70.6689, -33.4721], [-70.6098, -33.4681], [-70.5872, -33.4429], [-70.6036, -33.3999]]])
```
"""
function simplify(geojson::FeatureCollection, tolerance::Real=1., hq::Bool=false, mutate::Bool=false)
    tolerance < 0 && throw(error("Invalid tolerance. It must be a positive number!"))

    !mutate && (geojson = deepcopy(geojson))

    for feat in geojson.features
        geometry = feat.geometry

        simplify_geometry(geometry, tolerance, hq)
    end

    return geojson
end

"""
    simplify(geojson::FeatureCollection, tolerance::Real=1., hq::Bool=false)

Take a FeatureCollection and return a simplified version, modifying the original FeatureCollection.
Internally uses an adaptation of [simplify-js](http://mourner.github.io/simplify-js/) to perform simplification using the Ramer-Douglas-Peucker algorithm.
"""
simplify!(geojson::FeatureCollection, tolerance::Real=1., hq::Bool=false) = simplify(geojson, tolerance, hq, true)

"""
    simplify(geojson::AbstractGeometry, tolerance::Real=1., hq::Bool=false, mutate::Bool=false)

Take a GeoJSON Geometry and return a simplified version.
Internally uses an adaptation of [simplify-js](http://mourner.github.io/simplify-js/) to perform simplification using the Ramer-Douglas-Peucker algorithm.
"""
function simplify(geojson::AbstractGeometry, tolerance::Real=1., hq::Bool=false, mutate::Bool=false)
    !mutate && (geojson = deepcopy(geojson))
    return simplify_geometry(geojson, tolerance, hq)
end

"""
    simplify(geojson::AbstractGeometry, tolerance::Real=1., hq::Bool=false)

Take a GeoJSON Geometry and return a simplified version, modifying the original Geometry.
Internally uses an adaptation of [simplify-js](http://mourner.github.io/simplify-js/) to perform simplification using the Ramer-Douglas-Peucker algorithm.
"""
simplify!(geojson::AbstractGeometry, tolerance::Real=1., hq::Bool=false) = simplify(geojson, tolerance, hq, true)

"""
    simplify(geojson::AbstractFeature, tolerance::Real=1., hq::Bool=false, mutate::Bool=false)

Take a GeoJSON Feature and return a simplified version.
Internally uses an adaptation of [simplify-js](http://mourner.github.io/simplify-js/) to perform simplification using the Ramer-Douglas-Peucker algorithm.
"""
simplify(geojson::AbstractFeature, tolerance::Real=1., hq::Bool=false, mutate::Bool=false) = simplify(geojson.geometry, tolerance, hq, mutate)

"""
    simplify(geojson::AbstractFeature, tolerance::Real=1., hq::Bool=false)

Take a GeoJSON Feature and return a simplified version, modifying the original Feature.
Internally uses an adaptation of [simplify-js](http://mourner.github.io/simplify-js/) to perform simplification using the Ramer-Douglas-Peucker algorithm.
"""
simplify!(geojson::AbstractFeature, tolerance::Real=1., hq::Bool=false) = simplify(geojson, tolerance, hq, true)

function simplify_geometry(geom::AbstractGeometry, tol::Real, hq::Bool)
    type = geotype(geom)

    isequal(type, :Point) || isequal(type, :MultiPoint) && return geom

    clean!(geom)

    coords = geom.coordinates

    if isequal(type, :LineString)
        geom.coordinates = simplify_line(coords, tol, hq)
    elseif isequal(type, :MultiLineString)
        geom.coordinates = map(lines -> simplify_line(lines, tol, hq), coords[1])

    elseif isequal(type, :Polygon)
        geom.coordinates = simplify_polygon(coords, tol, hq)
    else
        geom.coordinates = map(rings -> simplify_polygon(rings, tol, hq), coords[1])
    end

    return geom
end

function simplify_line(coords, tol::Real, hq::Bool)
    return map(coords -> (coords[3]) ? [coords[1], coords[2], coords[3]] : [coords[1], coords[2]],
        DP_simplify(map(coord -> [coord[1], coord[2], coord[3]], coords), tol, hq))
end

function simplify_polygon(coords, tol::Real, hq::Bool)
    return map(ring -> begin
        pts = map(coord -> [coord[1], coord[2]], ring)

        length(pts) < 4 && throw(error("Invalid Polygon."))

        simple = map(p -> [p.coordinates[1], p.coordinates[2]], DP_simplify([Point(p) for p in pts], tol, hq))

        while !valid(simple)
            tol -= tol * 0.01
            simple = map(p -> [p.coordinates[1], p.coordinates[2]], DP_simplify([Point(p) for p in pts], tol, hq))
        end

        ((simple[length(simple)][1] != simple[1][1]) ||
            (simple[length(simple)][2] != simple[1][2])) && push!(simple, simple[1])

        return simple

        end, coords)
end

function valid(ring)
    length(ring) < 3 && return false

    return !(isequal(length(ring), 3) && (isequal(ring[3][1], ring[1][1]) && isequal(ring[3][2], ring[1][2])))
end

function dist²(p1::Point, p2::Point)
    dx = p1.coordinates[1] - p2.coordinates[1]
    dy = p1.coordinates[2] - p2.coordinates[2]

    return dx * dx + dy * dy
end

function segdist²(p::Point, l1::Point, l2::Point)
    x, y = l1.coordinates
    dx = l2.coordinates[1] - x
    dy = l2.coordinates[2] - y

    if !iszero(dx) || !iszero(dy)
        t = ((p.coordinates[1] - x) * dx + (p.coordinates[2] - y) * dy) / (dx * dx + dy * dy)

        if t > 1
            x = l2.coordinates[1]
            y = l2.coordinates[2]
        elseif t > 0
            x += dx * t
            y += dy * t
        end
    end

    dx = p.coordinates[1] - x
    dy = p.coordinates[2] - y

    return dx * dx + dy * dy
end

function simplify_radial_distance(points::Vector, tol::Real)
    previous = points[1]
    new_points = [previous]
    point = nothing

    for i in eachindex(points)
        point = points[i]
        if dist²(point, previous) > tol
            push!(new_points, point)
            previous = point
        end
    end

    !isequal(previous, point) && push!(new_points, point)

    return new_points
end

function simplify_DP_step(points::Vector, first::Integer, last::Integer, tol::Real, simplified)
    max_dist = tol
    index = 0

    for i = first+1:last
        dist = segdist²(points[i], points[first], points[last])

        if dist > max_dist
            index = i
            max_dist = dist
        end
    end

    if max_dist > tol
        (index - first > 1) && simplify_DP_step(points, first, index, tol, simplified)
        push!(simplified, points[index])
        (last - index > 1) && simplify_DP_step(points, index, last, tol, simplified)
    end
end

function douglas_peucker(points::Vector, tol::Real)
    last = length(points)

    simplified = [points[1]]
    simplify_DP_step(points, 1, last, tol, simplified)
    push!(simplified, points[last])

    return simplified
end

function DP_simplify(points::Vector, tol::Real, hq::Bool)
    length(points) <= 2 && return points
    sq_tol = tol * tol

    points = hq ? points : simplify_radial_distance(points, sq_tol)
    points = douglas_peucker(points, sq_tol)

    return points
end


#   Unit "crossspectra" of the FourierAnalysis Package for julia language
#
#   MIT License
#   Copyright (c) 2019, Marco Congedo, CNRS, Grenobe, France:
#   https://sites.google.com/site/marcocongedo/home

# ? CONTENTS :
#   This unit implements Welch cross-spectral estimates using FFTW

#############################################################################
# Generic constructors of CrossSpectra objects:
#
# Enable construction giving only `y`, `sr`, `wl`, `DC`, `taper`
# and `non-linear` argument.
# `flabels` is generated automatically, `smoothing` is set to `noSmoother`
# and `tril` is set to false
CrossSpectra(y, sr, wl, DC, taper, nonlinear) =
        CrossSpectra(y, sr, wl, DC, taper, fdf(sr, wl; DC=DC), nonlinear, noSmoother, false)
#
# As above, but setting by default also `DC` and `nonlinear` to false
CrossSpectra(y, sr, wl, taper) =
    CrossSpectra(y, sr, wl, false, taper, fdf(sr, wl; DC=false), false, noSmoother, false)
#############################################################################

"""
```
(1)
function crossSpectra( X    :: Matrix{T},
                       sr   :: Int,
                       wl   :: Int;
                  tapering  :: Union{Taper, TaperKind} = harris4,
                  planner   :: Planner                 = getplanner,
                  slide     :: Int                     = 0,
                  DC        :: Bool                    = false,
                  nonlinear :: Bool                    = false,
                  smoothing :: Smoother                = noSmoother,
                  tril      :: Bool                    = false,
                  ⏩       :: Bool                    = true) where T<:Real

(2)
function crossSpectra( 𝐗    :: Vector{Matrix{T}},
              < same argument sr, ..., ⏩ of method (1) > where T<:Real

```

(1)

Construct a [CrossSpectra](@ref) objects from real multivariate data
using the Welch method.

Given sampling rate `sr` and epoch length `wl`, compute the cross-spectral
matrices of dimension ``n``x``n`` for a multivariate data matrix
`X` of dimension ``t``x``n``, where ``t`` is the number of samples (rows) and
`n>1` is the number of time-series (columns).

The cross-spectral matrices are hold in the `.y` vector field of the created
object. The length of `.y` depends upon
the `wl` argument and `DC` optional keyword argument (see below).

**Optional Keyword Arguments**:

`sr`, `wl`, `tapering`, `planner` and `slide` have the same meaning as for the
[`spectra`](@ref) function.

`DC`: if true the cross-spectral matrix of the DC level is returned
in the first position of `y` (see the fields of the [CrossSpectra](@ref)
object), otherwise (default) the matrices in `y` start with the first positive
discrete frequency, that is, ``sr/wl`` Hz.

`nonlinear`: if true, the amplitude information is eliminated from the
DFT coefficients, that is, they are normalized by their modulus before being
averaged.
This leads to non-linear estimates (Congedo, 2018;
Pascual-Marqui 2007) where the diagonal elements of the cross-spectral matrices
(the spectra) are 1.0 for all frequencies. By default, it is false.

`smoothing`: apply a smoothing function of type [Smoother](@ref)
to the cross-spectral matrices across frequencies.
By default no smoothing is applied.

`tril`: if false (default), the whole cross-spectra matrices will be computed,
otherwise only their lower triangular part (see below).

`⏩`: if true (default), the method is run in multi-threaded mode
across the series in `X` if the number of series is at least twice
the number of threads Julia is instructed to use. See [Threads](@ref).

**Return**

If `tril` is false (default), the output is of type `Array{Hermitian,1}`,
which is the `ℍVector` type used in package [PosDefManifold](https://github.com/Marco-Congedo/PosDefManifold.jl).
Since cross-spectral estimates are Hermitian
positive definite, they can be straightaway used as
argument to PosDefManifold's functions, e.g., for computing matrix moves on
[geodesics](https://marco-congedo.github.io/PosDefManifold.jl/dev/riemannianGeometry/#Geodesic-equations-1),
matrix [distances](https://marco-congedo.github.io/PosDefManifold.jl/dev/riemannianGeometry/#PosDefManifold.distance),
etc. and the the whole vector output to compute
matrix [means](https://marco-congedo.github.io/PosDefManifold.jl/dev/riemannianGeometry/#Means-1),
[spectral embedding](https://marco-congedo.github.io/PosDefManifold.jl/dev/riemannianGeometry/#PosDefManifold.spectralEmbedding)
and more.

If `tril` is true, the output is of type `Array{LowerTriangular,1}`,
which is the `𝕃Vector` type used in PosDefManifold, that is, only the
lower triangle of the cross-spectra is computed in order to save time
and memory.

(2)

Construct a [CrossSpectraVector](@ref) object from a vector of
real multivariate data matrices. Compute the cross-spectral matrices
using the Welch method as per method (1) for all ``k`` data
matrices in `𝐗`.

The ``k`` matrices in `𝐗` must have the same number of columns
(i.e., the same number of time-series), but may have any number of (at least
`wl`) rows (samples).
All other arguments have the same meaning as in method (1),
with the following difference:

`⏩`: if true (default), the method is run in multi-threaded mode across the
``k`` data matrices if ``k`` is at least twice the number of threads Julia
is instructed to use, otherwise this method attempts to run each cross-spectral
estimation in multi-threaded mode across series as per method (1).
See [Threads](@ref).

If a `planner` is not explicitly passed as an argument,
the FFTW plan is computed once and applied for all cross-spectral
estimations.

**Return**

If `tril` is false, the output is of type `Array{Array{Hermitian,1},1}`,
which is the `ℍVector₂` type used in
[PosDefManifold](https://github.com/Marco-Congedo/PosDefManifold.jl).

If `tril` is true, the output is of type `Array{Array{LowerTriangular,1},1}`,
which is the `𝕃Vector₂` type used in PosDefManifold.

**See**: [CrossSpectra](@ref).

**See also**: [`spectra`](@ref), [`coherence`](@ref).

**Examples**:
```
using FourierAnalysis, Plots, LinearAlgebra

function generateSomeData(sr::Int, t::Int; noise::Real=1.)
    # four sinusoids of length t samples and sr sampling rate
    # peak amplitude: 0.7, 0.6, 0.5, 0.4
    # frequency:        5,   7,  13,  27
    # phase:            0, π/4, π/2,   π
    v1=sinusoidal(0.7, 5,  sr, t, 0)
    v2=sinusoidal(0.6, 7,  sr, t, π/4)
    v3=sinusoidal(0.5, 13, sr, t, π/2)
    v4=sinusoidal(0.4, 27, sr, t, π)
    return hcat(v1, v2, v3, v4) + (randn(t, 4)*noise)
end

sr, wl, t = 128, 512, 8192

# (1)

X=generateSomeData(sr, t) # multivariate data matrix 8192x4

# cross-spectra using default harris4 tapering window
S=crossSpectra(X, sr, wl)

# check the cross-spectral matrix at frequency 5Hz
S.y[f2b(5, sr, wl)]

# check only the diagonal part of this matrix as a vector
diag(Diagonal(S.y[f2b(5, sr, wl)]))

# cross-spectra using hann tapering window
S=crossSpectra(X, sr, wl; tapering=hann)

# using Slepian's multi-tapering
S=crossSpectra(X, sr, wl; tapering=slepians(sr, wl))

# compute non-linear cross-spectra
S=crossSpectra(X, sr, wl; tapering=slepians(sr, wl), nonlinear=true)

# compute only the lower triangle of cross-spectral matrices
S=crossSpectra(X, sr, wl; tapering=slepians(sr, wl), tril=true)

# smooth a-posteriori the cross-spectra
S2=smooth(blackmanSmoother, S)

# or compute cross-spectra already smoothed
S=crossSpectra(X, sr, wl;
               tapering=slepians(sr, wl), tril=true, smoothing=blackmanSmoother)

# mean cross-spectral matrix in 8Hz-12Hz range
M=mean(S, (8, 12)) # or also M=mean(S, (8.0, 12.0))

# extract all cross-spectral matrices in 8Hz-12Hz range
E=extract(S, (8, 12))

# cross-spectral matrices averaged in 2Hz band-pass regions
B=bands(S, 2)

# Get the spectra from a CrossSpectra object
PowerSpectra=Spectra(S)

# Get the amplitude spectra from a CrossSpectra object
AmpSpectra=Spectra(S, func=√)

# Get the log10-spectra from a CrossSpectra object
log10Spectra=Spectra(S, func=log10)

# plot the spectra (see recipes.jl)
plot(AmpSpectra; fmax=32, xspace=4, ytitle="Amplitude")

# (2)
# generate 3 multivariate data matrices 8192x4
X=[generateSomeData(sr, t) for i=1:3]

# The examples here below use exactly the same syntax as the previous method.
# However, since the input here is a vector of data matrices
# and not a single data matrix, the examples here below create a vector
# of the object created by the examples above.
# For example:

# cross-spectra using the default harris4 tapering window
# this creates a CrossSpectraVector object
S=crossSpectra(X, sr, wl)

# check the cross-spectral matrix at fr. 5Hz for the first CrossSpectra object
S[1].y[f2b(5, sr, wl)]

# check only the diagonal part of this matrix as a vector
diag(Diagonal(S[1].y[f2b(5, sr, wl)]))

# cross-spectra using Hamming's tapering window
S=crossSpectra(X, sr, wl; tapering=hamming)

# using Slepian's multi-tapering
S=crossSpectra(X, sr, wl; tapering=slepians(sr, wl))

# compute non-linear cross-spectra
S=crossSpectra(X, sr, wl; tapering=slepians(sr, wl), nonlinear=true)

# compute only the lower triangle of cross-spectral matrices
S=crossSpectra(X, sr, wl; tapering=slepians(sr, wl), tril=true)

# smooth a-posteriori all CrossSpectra objects in S
S2=smooth(blackmanSmoother, S)

# or compute them all already smoothed
S=crossSpectra(X, sr, wl; tapering=parzen, smoothing=blackmanSmoother)

# mean cross-spectral matrix in 8Hz-12Hz range for all CrossSpectra (CS) objects
M=mean(S, (8, 12)) # or also M=mean(S, (8.0, 12.0))

# grand-average mean of the above across all CS objects
meanM=mean(mean(S, (8, 12)))

# extract all cross-spectral matrices in 8Hz-12Hz range for all CS objects
E=extract(S, (8, 12))

# grand average of cross-spectral matrices in 8Hz-12Hz range for all CS objects
meanE=mean(extract(S, (8, 12)))

# cross-spectral matrices averaged in 2Hz band-pass regions for all CS objects
B=bands(S, 2)

# Get and plot the spectra from a CrossSpectra object
plot(Spectra(S[1]); fmax=32, xspace=4)

# Pre-compute a FFT planner and pass it as argument
# (this interesting if the function is to be called repeatedly).
plan=Planner(plan_exhaustive, 10.0, wl, eltype(X[1])) # wait 10s
S=crossSpectra(X, sr, wl; planner=plan)

# how faster is this?
using BenchmarkTools
@benchmark(crossSpectra(X, sr, wl))
@benchmark(crossSpectra(X, sr, wl; planner=plan))
...
...

```
"""
function crossSpectra( X    :: Matrix{T},
                       sr   :: Int,
                       wl   :: Int;
                  tapering  :: Union{Taper, TaperKind} = harris4,
                  planner   :: Planner                 = getplanner,
                  slide     :: Int                     = 0,
                  DC        :: Bool                    = false,
                  nonlinear :: Bool                    = false,
                  smoothing :: Smoother                = noSmoother,
                  tril      :: Bool                    = false,
                  ⏩       :: Bool                    = true) where T<:Real

    # function to set all parameters
    n, 𝚙, 𝜏, s, e, 𝜎, η, wl½, wl½_, cT = _paramSP!(X, tapering, wl, planner, slide, DC)

    # allocate memory for DFTs (𝙵)
    𝙵 = [zeros(cT, wl½_, n) for i=1:𝜏.n]

    # allocate memory for cross-spectra and initialize them with zeros (𝙎)
    𝙎 = Vector{LowerTriangular}([LowerTriangular(zeros(cT, n, n)) for i=1:wl½_])

    # compute the descrete Fourier transform (DFT) of a Vector or of the column vectors
    # of matrix `X` and put it into the columns of `𝙵`. The DFT is computed on the epoch
    # of duration `t` samples starting at sample `at`. It does not return anything.
    # `𝙵` is a `n𝜏`-Vector of DFT matrices, holding a DFT matrix (# discr. freq. x time-series),
    # where `n𝜏` is the number of tapering window (>1 only for Slepian tapers).
    # ARGUMENTS:
    # `η` is a calibration Vector to be applied to the FFT coefficients, computed
    #    by function `_fftNormalization`. It is applied element-wise to the FFT
    #    coefficient vector(s) to obtain spectral estimates in correct physical units.
    #    It is applied only if `nonlinear` is false.
    # `n` (Int) is the number of columns in `X`; it must be 1 if `X` is a vector.
    # `𝜏` is a tapering window, ``rectangular` if no tapering is applied
    # `𝚙` is the FFTW plan performing the FFT.
    # `DC` (bool): if true the DC level(s) is(are) computed and stored in the first matrix of 𝚂
    _dft!(at, 𝚗) =
        if nonlinear
            𝜏.kind == rectangular ? ( 𝙵[1][:, 𝚗] = ((𝚙*X[at:at+wl-1, 𝚗])[1+!DC:end]) ) : # no tapering
                                    ( for i=1:𝜏.n 𝙵[i][:, 𝚗] = (𝚙*(𝜏.y[:, i].*X[at:at+wl-1, 𝚗]))[1+!DC:end] end ) # with tapering
        else
            𝜏.kind == rectangular ? ( 𝙵[1][:, 𝚗] = η.*((𝚙*X[at:at+wl-1, 𝚗])[1+!DC:end]) ) : # no tapering
                                    ( for i=1:𝜏.n 𝙵[i][:, 𝚗] = η.*(𝚙*(𝜏.y[:, i].*X[at:at+wl-1, 𝚗]))[1+!DC:end] end ) # with tapering
        end

    # function computing DFTs (discrete Fourier transform)
    dft!(𝚎, 𝚗) = _dft!((𝚎*s)+1, 𝚗)

    # function cumulating the avarage cross-spectra. The mean is over tapering windows (>1 only for Slepians)
    cs!(𝚏) = for j=1:n, i=j:n 𝙎[𝚏][i, j] += mean(𝙵[l][𝚏, i] * conj(𝙵[l][𝚏, j]) for l=1:𝜏.n) end

    # average cross-spectra across sliding windows
    for 𝚎=0:e-1
        _thread(⏩, n) ? (@threads for 𝚗=1:n dft!(𝚎, 𝚗) end) : (for 𝚗=1:n dft!(𝚎, 𝚗) end)
        nonlinear ? (for i=1:length(𝙵) 𝙵[i]./=(abs.(𝙵[i])*𝜎) end) : nothing
        _thread(⏩, wl½_) ? (@threads for 𝚏=1:wl½_ cs!(𝚏) end) : (for 𝚏=1:wl½_ cs!(𝚏) end)
    end

    # if `tril`=false return the full matrices, otherwise the lower-triangular matrices
    # Typecasting output: 𝔸=Array; 𝕄=Matrix; ℍ=Hermitian; 𝕃=LowerTriangular
    H=Hermitian; V=Vector; M=Matrix; CS=CrossSpectra; sm=smoothing
    args=(sr, wl, DC, taperinfo(𝜏), fdf(sr, wl; DC=DC), nonlinear, sm, tril)
    return tril ? smooth(sm, CS(𝙎, args...)) :
                  smooth(sm, CS(V{H}([H(M(𝙎[𝚏]), :L) for 𝚏=1:wl½_]), args...))
end

function crossSpectra( 𝐗    :: Vector{Matrix{T}},
                       sr   :: Int,
                       wl   :: Int;
                  tapering  :: Union{Taper, TaperKind} = harris4,
                  planner   :: Planner                 = getplanner,
                  slide     :: Int                     = 0,
                  DC        :: Bool                    = false,
                  nonlinear :: Bool                    = false,
                  smoothing :: Smoother                = noSmoother,
                  tril      :: Bool                    = false,
                  ⏩       :: Bool                    = true) where T<:Real

    # set some parameters. k=# input data matrices, n=# time series
    k, n, wl½, thr, cT = length(𝐗), size(𝐗[1], 2), (wl>>1), nthreads(), typeof(Complex(T(0)))

    # get the FFTW plan (for one time-series)
    planner ≠ getplanner ? plan=planner : plan = _Planner(plan_rfft(zeros(T, wl)))

    # get the tapering window if not provided
    tapering isa Taper ? 𝜏=tapering : 𝜏=taper(tapering, wl)

    # allocate memory for cross-spectra
    𝓢 = CrossSpectraVector(undef, k)

    # define internal function to compute cross-spectra for the ith input data matrix `𝐗[i]`
    cs!(i, ⏩) = 𝓢[i]=crossSpectra(𝐗[i],
                                   sr,
                                   wl;
                               tapering  = 𝜏,
                               slide     = slide,
                               DC        = DC,
                               planner   = plan,
                               nonlinear = nonlinear,
                               smoothing = smoothing,
                               tril      = tril,
                               ⏩       = ⏩)

    # threads over k if k is large, otherwise let crossSpectra function decide if to thread over n
    _thread(⏩, k) ? (@threads for i=1:k cs!(i, false) end) : (for i=1:k cs!(i, ⏩) end)

    return 𝓢
end

# ++++++++++++++++++++  Show override  +++++++++++++++++++ # (REPL output)
function Base.show(io::IO, ::MIME{Symbol("text/plain")}, S::CrossSpectra)
    r=size(S.y[1], 1)
    l=length(S.flabels)
    ll=length(S.y)
    println(io, titleFont, "▦ ⋯ ▦ CrossSpectra type; $l freq. x $(r)²-matrices")
    #println(io, "□  □    □      □        □           □", defaultFont)
    println(io, separatorFont, "⭒  ⭒    ⭒      ⭒        ⭒           ⭒", defaultFont)
    println(io, "sampling rate    (.sr): $(S.sr)")
    println(io, "epoch length     (.wl): $(S.wl)")
    println(io, "DC level         (.DC): $(S.DC)")
    println(io, "taper kind (.tapering): $(S.taper)")
    println(io, "freq. lab.  (.flabels): $(l)-", typeof(S.flabels))
    println(io, "data              (.y): $(l)-", typeof(S.y))
    # println(io, S.y)
    println(io, "non-linear(.nonlinear): ", string(S.nonlinear))
    println(io, "smoother  (.smoothing): ", string(S.smoothing))
    println(io, "triang. form   (.tril): ", string(S.tril))
    ll≠l && @warn "number of frequency labels does not match the CrossSpectra data size" l ll
end

function Base.show(io::IO, ::MIME{Symbol("text/plain")}, 𝐒::CrossSpectraVector)
    println(io, titleFont, "▦ ⋯ ▦ CrossSpectraVector Type")
    println(io, titleFont, "  ⋯")
    println(io, separatorFont, "⭒  ⭒    ⭒      ⭒        ⭒           ⭒", defaultFont)
    println(io, "$(length(𝐒))-element Vector{CrossSpectra}")
end

# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #


### A Pluto.jl notebook ###
# v0.14.5

using Markdown
using InteractiveUtils

# ╔═╡ dcab9a18-5d58-4632-a325-d16af9c79344
using Pkg, DrWatson

# ╔═╡ c95f3f02-f179-4f81-9c53-6e6b87f25613
begin
	@quickactivate "NumericalMethodsforEngineers"
	using NumericalMethodsforEngineers, Symbolics, SymbolicUtils
	using StaticArrays, DataFrames, Plots
end

# ╔═╡ 75361cb8-3c45-4475-94dc-bf8093114613
md" ##### Building a Lagrangian polynomial in expression form."

# ╔═╡ c1acd2f9-a762-4beb-b44a-feb74fba31fb
@variables x[1:4] y[1:4];

# ╔═╡ 15eaabdd-ed7b-42ec-bd19-96b927cbbdd8
@variables g(x::Vector{Symbolics.Num}, y::Vector{Symbolics.Num});

# ╔═╡ a240a3be-f978-4f27-985c-b997b8cb75e4
begin
	z = Vector{AbstractString}(undef, length(x))
	for i in 1:length(x)
		for j in 1:length(x)
			if j == 1 && i == 1
				z[i] = "$(y[1])"
			elseif j == 1
				z[i] = "((x - $(x[j])) / ($(x[i]) - $(x[j])))"
			elseif i == j
				z[i] = "$(z[i]) * $(y[i])"
			elseif i !== j
				z[i] = "$(z[i]) * ((x - $(x[j])) / ($(x[i]) - $(x[j])))"
			end
		end
	end
	z
end

# ╔═╡ 9a626dae-2a2e-4d17-9813-dcb1564848de
typeof(z)

# ╔═╡ db65cf7d-d33e-49cd-bd03-464b4096c8d6
ze = Meta.parse.(z)

# ╔═╡ fe586e50-f0e4-41e2-b7f0-f408c9038464
begin
	dct = Dict(x[1]=>1, x[2]=>2, x[3]=>3, x[4]=>4,
		y[1]=>1, y[2]=>4, y[3]=>9, y[4]=>16)
	L(dct)
end

# ╔═╡ 53d5c163-2d9a-4de8-9f37-1698aa142b69
substitute(ze[1], Dict(x[1]=>1, x[2]=>2, x[3]=>3, x[4]=>4, y[1]=>1, y[2]=>4, y[3]=>9, y[4]=>16))

# ╔═╡ 7147c00f-c9ea-4493-929b-8bf1f0354f28
substitute(ze[2], dct)

# ╔═╡ b3ee083c-9002-4273-aab1-b898348c2d0a
begin
	@variables u v
	to_compute = [u^2 + v, v^2 + u]
end;

# ╔═╡ 386da460-ee76-4506-8aaf-3d02658720f9
f_expr = build_function(to_compute, [u, v]);

# ╔═╡ 77c31df8-cab7-4bb8-986a-8d67b56b2b2b
begin
	myf = eval(f_expr[1])
	myf(SA[2.0, 3.0])
end

# ╔═╡ 162d30d7-d520-457e-8f1e-91ee85eb5573
ze_expr = build_function(ze, [x, y]);

# ╔═╡ a29d1a22-bdb9-4253-88ac-ed8e4ccb34e7
zef = eval(ze_expr[1])

# ╔═╡ 2fc2edef-203d-4822-b7a8-80a9c410ab01
zef(SA[1,2,3,4], SA[1,4, 9, 16])

# ╔═╡ Cell order:
# ╠═dcab9a18-5d58-4632-a325-d16af9c79344
# ╠═c95f3f02-f179-4f81-9c53-6e6b87f25613
# ╠═75361cb8-3c45-4475-94dc-bf8093114613
# ╠═c1acd2f9-a762-4beb-b44a-feb74fba31fb
# ╠═15eaabdd-ed7b-42ec-bd19-96b927cbbdd8
# ╠═a240a3be-f978-4f27-985c-b997b8cb75e4
# ╠═9a626dae-2a2e-4d17-9813-dcb1564848de
# ╠═db65cf7d-d33e-49cd-bd03-464b4096c8d6
# ╠═fe586e50-f0e4-41e2-b7f0-f408c9038464
# ╠═53d5c163-2d9a-4de8-9f37-1698aa142b69
# ╠═7147c00f-c9ea-4493-929b-8bf1f0354f28
# ╠═b3ee083c-9002-4273-aab1-b898348c2d0a
# ╠═386da460-ee76-4506-8aaf-3d02658720f9
# ╠═77c31df8-cab7-4bb8-986a-8d67b56b2b2b
# ╠═162d30d7-d520-457e-8f1e-91ee85eb5573
# ╠═a29d1a22-bdb9-4253-88ac-ed8e4ccb34e7
# ╠═2fc2edef-203d-4822-b7a8-80a9c410ab01


# Based on the the "Contour Following Algorithm" described in Chapter 5 of
# Costa, L., Cesar, Jr., R., Laplante, P. (2009). Shape Classification and Analysis. Boca Raton: CRC Press, https://doi.org/10.1201/9780849379406
function trace_boundary(t::CostaOuter, labels::AbstractArray, N::Int = 0)
    N = N == 0 ? maximum(labels) : N
    boundary = [Vector{CartesianIndex{2}}(undef, 0) for n = 1:N]
    # By padding we will avoid having to do out-of-bounds checking when we
    # consider a pixel's neighbourhood.
    labels_padded = padarray(labels, Fill(0, (2,2)))
    # Labeled neighbourhood used by the contour-tracking algorithm which follows
    # the Freeman chain-code convention.
    # 3 2 1
    # 4 P 0
    # 5 6 7
    N₈ = OffsetVector(CartesianIndex.([(0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1), (1, 0), (1, 1)]), 0:7)
    invert = OffsetVector([4, 5, 6, 7, 0, 1, 2, 3], 0:7)
    isfirst = fill(true, N)
    nrow, ncol = size(labels)
    for p in CartesianIndices((0:nrow + 1, 0:ncol + 1))
        l = labels_padded[p]
        l₀ = labels_padded[p + N₈[0]]
        if l == 0 && l₀ != 0 && isfirst[l₀]
            B₁ = p
            push!(boundary[l₀], B₁)
            isfirst[l₀] = false
            pₙ₊₁, curr2next  = find_second_outer(p, N₈, labels_padded, l₀)
            while pₙ₊₁ != B₁
                push!(boundary[l₀], pₙ₊₁)
                prev2curr = curr2next
                pₙ₊₁, curr2next = find_next_outer(pₙ₊₁, prev2curr, N₈, labels_padded, l₀, invert)
            end
        end
    end
    boundary
end

function find_second_outer(p, N₈, labels, l)
    # Because we are tracing an outer contour, once we find the first pixel
    # there *must* be second pixel so we can be sure that this function will
    # always return from within the "if" statement.
    for r = 4:9
        pₙ₊₁ = p + N₈[mod(r, 8)]
        pₙ₊₂ = p + N₈[mod(r + 1, 8)]
        if labels[pₙ₊₁] == 0 && labels[pₙ₊₂] == l
            return pₙ₊₁, r
        end
    end
end

function find_next_outer(p, prev2curr, N₈, labels, l, invert)
    # These initial values will be overwritten in the loop.
    pₙ₊₁ = p
    curr2next = prev2curr

    curr2prev = invert[prev2curr]
    for r = 0:6
        dₑ =  mod(curr2prev + r, 8)
        dₗ =  mod(curr2prev + r + 1, 8)
        pₑ = p + N₈[dₑ]
        pₗ = p + N₈[dₗ]
        if labels[pₑ] == 0 && labels[pₗ] == l
            pₙ₊₁ =  pₑ
            curr2next = dₑ
        end
    end
    pₙ₊₁, curr2next
end


# FIXME: Add some condition to guarantee that: TR <: Union(T, complex(T)) !

# T the numeric type of the transfer function
# TR the type of the roots
struct SisoZpk{T,TR<:Number} <: SisoTf{T}
    z::Vector{TR}
    p::Vector{TR}
    k::T
    function SisoZpk{T,TR}(z::Vector{TR}, p::Vector{TR}, k::T) where  {T<:Number, TR<:Number}
        if k == zero(T)
            p = TR[]
            z = TR[]
        end
        if TR <: Complex && T <: Real
            @assert check_real(z) "zpk model should be real-valued, but zeros do not come in conjugate pairs."
            @assert check_real(p) "zpk model should be real-valued, but poles do not come in conjugate pairs."
        end
        new{T,TR}(z, p, k)
    end
end
function SisoZpk{T,TR}(z::Vector, p::Vector, k::Number) where  {T<:Number, TR<:Number}
    SisoZpk{T,TR}(Vector{TR}(z), Vector{TR}(p), T(k))
end
function SisoZpk{T}(z::Vector, p::Vector, k::Number) where T
    TR = complex(T)
    SisoZpk{T,TR}(Vector{TR}(z), Vector{TR}(p), T(k))
end
function SisoZpk(z::AbstractVector{TZ}, p::AbstractVector{TP}, k::T) where {T<:Number, TZ<:Number, TP<:Number} # NOTE: is this constructor really needed?
    TR = promote_type(TZ,TP)
    # Could check if complex roots come with their conjugates,
    # i.e., if the SisoZpk corresponds to a real-valued system

    if TR <: Complex && T <: Real
        @assert check_real(z) "zpk model should be real-valued, but zeros do not come in conjugate pairs."
        @assert check_real(p) "zpk model should be real-valued, but poles do not come in conjugate pairs."
    end
    SisoZpk{T,TR}(Vector{TR}(z), Vector{TR}(p), k)
end

Base.zero(::Type{SisoZpk{T}}) where T = SisoZpk{T}(T[], T[], zero(T))
Base.one(::Type{SisoZpk{T}}) where T = SisoZpk{T}(T[], T[], one(T))

Base.zero(::Type{SisoZpk{T,TR}}) where {T, TR} = SisoZpk{T,TR}(TR[], TR[], zero(T))
Base.one(::Type{SisoZpk{T,TR}}) where {T, TR} = SisoZpk{T,TR}(TR[], TR[], one(T))

Base.one(f::SisoZpk) = one(typeof(f))
Base.zero(f::SisoZpk) = zero(typeof(f))

# tzero is not meaningful for transfer function element? But both zero and zeros are taken...
tzero(f::SisoZpk) = f.z # Do minreal first?,
pole(f::SisoZpk) = f.p # Do minreal first?

numpoly(f::SisoZpk{<:Real}) = f.k*prod(roots2real_poly_factors(f.z))
denpoly(f::SisoZpk{<:Real}) = prod(roots2real_poly_factors(f.p))

numpoly(f::SisoZpk) = f.k*prod(roots2poly_factors(f.z))
denpoly(f::SisoZpk) = prod(roots2poly_factors(f.p))

numvec(f::SisoZpk) = reverse(coeffs(numpoly(f))) # FIXME: reverse?!
denvec(f::SisoZpk) = reverse(coeffs(denpoly(f))) # FIXME: reverse?!

isproper(f::SisoZpk) = (length(f.z) <= length(f.p))

# Will not be able to create complex polynomials without type instability

function minreal(sys::SisoZpk{T,TR}, eps::Real) where {T, TR}
    if length(sys.p) == 0
        return sys
    end

    newZ = copy(sys.z)
    newP = Vector{TR}()

    pidx = 1
    p = sys.p[pidx]
    while true
        if isempty(newZ)
            push!(newP, p)
        else
            distance, zidx = findmin(abs.(p .- newZ))

            if distance < eps
                if imag(p) == 0 && imag(newZ[zidx]) != 0
                    newZ[zidx+1] = real(newZ[zidx+1])
                end
                if imag(newZ[zidx]) == 0 && imag(p) != 0
                    pidx += 1
                    p = real(sys.p[pidx])
                    deleteat!(newZ, zidx)
                    continue
                end
                deleteat!(newZ, zidx)
            else
                push!(newP, p)
            end
        end

        pidx += 1
        if pidx > length(sys.p)
            break
        end
        p = sys.p[pidx]
    end
    SisoZpk{T, TR}(newZ, newP, sys.k)
end

# FIXME: Perhaps move to some other file with auxilliary functions,
# the name could also be imporoved. Perhaps this functionality can be found in some other package.
""" If TR is Complex and T is Real, check that every pole is matched to its conjugate
    this assumes that the compelx poles are ordered as they are output by the LAPACK
    routines that return complex-conjugated values, i.e., (x+iy) is followed by (x-iy)"""
function check_real(r_vec::AbstractVector{<:Complex})
    k = 1
    while k <= length(r_vec)
        if isreal(r_vec[k])
            # Do nothing
        else
            if k == length(r_vec) # Element is complex and there is no more elements to match up with
                return false
            elseif conj(r_vec[k]) == r_vec[k+1] # Element is complex and succeeding element is its conjugate
                k=k+1
            else
                return false
            end
        end
        k += 1
    end
    return true
end

function evalfr(f::SisoZpk{T1,TR}, s::T2) where {T1<:Number, TR<:Number, T2<:Number}
    T0 = promote_type(T2, TR)
    T = promote_type(T1, Base.promote_op(/, T0, T0))
    den = mapreduce(a -> s-a, *, f.p, init = one(T0)) # Default to one

    if den == zero(T0)
        return convert(T, Inf)
    else
        num = mapreduce(b -> s-b, *, f.z, init=one(T0))
        return f.k*num/den
    end
end

function poly_factors2string(poly_factors::AbstractArray{<:Poly{T}}, var) where T
    if length(poly_factors) == 0
        str = sprint(printpolyfun(var), Poly(one(T)))
    elseif length(poly_factors) == 1
        str = sprint(printpolyfun(var), poly_factors[1])
    else
        str = prod(z -> "("*sprint(printpolyfun(var), z)*")", poly_factors)
    end
end

""" Heurisitc function that tries to add parentheses when printing the coeffcient
    for systems on zpk form. Should at least handle the following types
    Measurment, Dual, Sym. """
function _printcoefficient(nbr::Number)
    # Print type information as in 1.0f0 for Float32
    # showcompact might be better, but is not consistent with polynomials
    nbr_string = sprint(show,nbr)
    if occursin(" + ", nbr_string) || occursin(" - ", nbr_string) || occursin(" ± ", nbr_string)
        return "(" * nbr_string * ")" # Add parens
    else
        return nbr_string
    end
end

function print_siso(io::IO, t::SisoZpk, var=:s)
    if typeof(t.k) <: Real
        numstr = poly_factors2string(roots2real_poly_factors(t.z), var)
        denstr = poly_factors2string(roots2real_poly_factors(t.p), var)
    else
        numstr = poly_factors2string(roots2poly_factors(t.z), var)
        denstr = poly_factors2string(roots2poly_factors(t.p), var)
    end
    # Figure out the length of the separating line
    len_num = length(numstr)
    len_den = length(denstr)
    dashcount = max(len_num, len_den)

    # Center the numerator or denominator
    if len_num < dashcount
        numstr = "$(repeat(" ", div(dashcount - len_num, 2)))$numstr"
    else
        denstr = "$(repeat(" ", div(dashcount - len_den, 2)))$denstr"
    end

    gainstr = _printcoefficient(t.k)
    #Add spaces to account for gain string
    numstr = " "^(length(gainstr))*numstr
    denstr = " "^(length(gainstr))*denstr
    println(io, numstr)
    println(io, gainstr*repeat("-", dashcount))
    println(io, denstr)
end

==(f1::SisoZpk, f2::SisoZpk) = (f1-f2).k == 0
function isapprox(f1::SisoZpk, f2::SisoZpk; rtol::Real=sqrt(eps()), atol::Real=sqrt(eps()))
    fdiff = f1 - f2
    isapprox(fdiff.k, 0, atol=atol, rtol=rtol)
end

function +(f1::SisoZpk{T1,TR1}, f2::SisoZpk{T2,TR2}) where {T1<:Number,T2<:Number,TR1<:Number,TR2<:Number}
    numPoly = numpoly(f1)*denpoly(f2) + numpoly(f2)*denpoly(f1)

    TRtmp = promote_type(TR1, TR2)
    # Calculating roots can make integers floats
    TRnew = Base.promote_op(/, TRtmp, TRtmp)
    z = convert(Vector{TRnew}, roots(numPoly))
    #TODO gains could remain integers, but numerical precision inhibits this
    Ttmp = promote_type(T1,T2)
    Tnew = Base.promote_op(/, Ttmp, Ttmp)
    if length(numPoly) > 0
        k = convert(Tnew, numPoly[end])
        p = convert(Vector{TRnew}, [f1.p;f2.p])
    else
        k = zero(Tnew)
        p = TRnew[]
    end

    # FIXME:
    # Threshold for pole-zero cancellation should depend on the roots of the system
    # Note the difference between continuous and discrete-time systems...
    minreal(SisoZpk(z,p,k), sqrt(eps()))
end

+(f::SisoZpk, n::T) where {T<:Number} = f + SisoZpk{T,T}(T[],T[],n)
+(n::Number, f::SisoZpk) = f + n
#.+(t::SisoZpk, n::Number) = t + n
#.+(n::Number, t::SisoZpk) = t + n

-(t1::SisoZpk, t2::SisoZpk) = +(t1,-t2)
-(n::T, f::SisoZpk) where {T<:Number} = SisoZpk{T,T}(T[],T[],n) - f
-(t::SisoZpk, n::Number) = +(t, -n)
#.-(t::SisoZpk, n::Number) = -(t, n)
#.-(n::Number, t::SisoZpk) = -(n, t)

-(f::SisoZpk) = SisoZpk(f.z, f.p, -f.k)

*(f1::SisoZpk, f2::SisoZpk) = SisoZpk([f1.z;f2.z], [f1.p;f2.p], f1.k*f2.k)

*(f::SisoZpk, n::Number) = SisoZpk(f.z, f.p, f.k*n)
*(n::Number, f::SisoZpk) = *(f, n)
#.*(f1::SisoZpk, f2::SisoZpk) = *(f1, f2)
#.*(t::SisoZpk, n::Number) = *(t, n)
#.*(n::Number, t::SisoZpk) = *(t, n)

/(n::Number, f::SisoZpk) = SisoZpk(f.p, f.z, n/f.k)
/(f::SisoZpk, n::Number) = SisoZpk(f.z, f.p, f.k/n)
/(f1::SisoZpk, f2::SisoZpk) = f1*(1/f2)


# identity matrix
eye(T::Type, m, n) = Matrix{T}(LinearAlgebra.I, m, n)
eye(n) = eye(Float64, n, n)

# vectorization fucntion
vec(X) = vcat(X...)


# MSF_ploty_v3.jl
# ==================

# master stability functions for the Roessler system with different coupling terms

using DrWatson
@quickactivate "NonlinearDynamicsTextbook"
include(srcdir("style.jl"))
#using DynamicalSystems
using PyPlot, OrdinaryDiffEq
using StaticArrays
using LinearAlgebra

# coupling matrix
# ---------------

wmat =  [0   7    0    5    0  ;
         7   0    3    0    4  ;
         0   3    0    0    6  ;
         5   0    0    0    0  ;
         0   4    6    0    0 ]

gmat = - wmat

dg = sum(wmat,dims=2)

for i = 1:5
    gmat[i,i] = dg[i]
end

#check row sum
# println( sum(gmat, dims=2) )

# eigenvalues
evvec = eigvals(gmat)
# println(evvec)

# load results
# -------------
filepath = datadir("master_stability_function.jld")

alpha_vec = load(filepath,"alpha_vec")

LE_vec_xx = load(filepath,"LE_vec_xx")
LE_vec_xy = load(filepath,"LE_vec_xy")
LE_vec_xz = load(filepath,"LE_vec_xz")

LE_vec_yx = load(filepath,"LE_vec_yx")
LE_vec_yy = load(filepath,"LE_vec_yy")
LE_vec_yz = load(filepath,"LE_vec_yz")

LE_vec_zx = load(filepath,"LE_vec_zx")
LE_vec_zy = load(filepath,"LE_vec_zy")
LE_vec_zz = load(filepath,"LE_vec_zz")

# plot version selection
# ======================
fig = figure("MSF selection 1",figsize=(0.5*figx,1.1*figy))

# coupling signal x

# Jh = [1 0 0 ; 0 0 0 ; 0 0 0 ]   # x drives x-ODE
plot(alpha_vec,LE_vec_xx, color = COLORS[1], linestyle="-")

# Jh = [0 0 0 ; 1 0 0 ; 0 0 0 ]  # x drives y-ODE
# plot(alpha_vec,LE_vec_xy, color = COLORS[1], linestyle=":")

# Jh = [0 0 0 ; 0 0 0 ; 1 0 0 ]  # x drives z-ODE
plot(alpha_vec,LE_vec_xz, color = COLORS[2], linestyle="--")

# coupling signal y

# Jh = [0 1 0 ; 0 0 0 ; 0 0 0 ]   # y drives x-ODE
# plot(alpha_vec,LE_vec_yx, color = COLORS[4])

# Jh = [0 0 0 ; 0 1 0 ; 0 0 0 ]  # y drives y-ODE
plot(alpha_vec,LE_vec_yy, color = COLORS[4], linestyle=":")

# Jh = [0 0 0 ; 0 0 0 ; 0 1 0 ]  # y drives z-ODE
# plot(alpha_vec,LE_vec_yz, color = COLORS[4], linestyle="--")

# coupling signal z

# Jh = [0 0 1 ; 0 0 0 ; 0 0 0 ] # z drives x-ODE
# plot(alpha_vec,LE_vec_zx, color = COLORS[2])

# Jh = [0 0 0 ; 0 0 1 ; 0 0 0 ]  # z drives y-ODE
# plot(alpha_vec,LE_vec_zy, color = COLORS[2], linestyle=":")

# Jh = [0 0 0 ; 0 0 0 ; 0 0 1 ]  # z drives z-ODE
plot(alpha_vec,LE_vec_zz, color = COLORS[3], linestyle="--")

xlabel(L"$\alpha$";labelpad = -20)
ylabel(L"$\lambda_1$")

LE_min = -0.3
LE_max = 0.3

ax = gca()
ax.set_xlim([alpha_vec[1], alpha_vec[end]])
ax.set_ylim([LE_min,LE_max])
ax.legend([L"x \to \dot x"; L"x \to \dot z"; L"y \to \dot y"; L"z \to \dot z" ], fontsize = 28,
           loc = "upper right", ncol = 2, handlelength = 1. , handletextpad = 0.5)

# zero line
plot(alpha_vec, 0 .* LE_vec_xx, linewidth=1.0,linestyle="-",color="k" )
ylabel(L"$\lambda_{1}$") #

# plot eigenvalues
sigma = 0.15
scatter(sigma*evvec, 0 .* evvec,color="k",s = 40. )

#add_identifiers!(fig)
fig.tight_layout(pad=0.3)

wsave(plotsdir("10", "MSF_Roessler_selection"), fig)


# Utility functions

type NoArgCheck end

function allfinite{T<:Real}(x::Array{T})
	for i = 1 : length(x)
		if !(isfinite(x[i]))
			return false
		end
	end
	return true
end

function allzeros{T<:Real}(x::Array{T})
	for i = 1 : length(x)
		if !(x == zero(T))
			return false
		end
	end
	return true
end

function isprobvec(p::Vector{Float64})
    s = 0.
    for i = 1:length(p)
        pi = p[i]
        s += pi
        if pi < 0
            return false
        end
    end
    return abs(s - 1.0) <= 1.0e-12
end

function pnormalize!{T<:FloatingPoint}(v::AbstractVector{T})
	s = 0.
	n = length(v)
	for i = 1:n
		@inbounds s += v[i]
	end
	for i = 1:n
		@inbounds v[i] /= s
	end
	v
end

function add!(x::AbstractArray, y::AbstractArray)
	n = length(x)
	length(y) == n || throw(DimensionMismatch("Inconsistent array lengths."))
	for i = 1:n
		x[i] += y[i]
	end
	x
end

function multiply!(x::AbstractArray, c::Number)
	for i = 1:length(x)
		@inbounds x[i] *= c
	end
	x
end

function exp!(x::AbstractArray)
	for i = 1:length(x)
		@inbounds x[i] = exp(x[i])
	end
	x
end

# macros for generating functions for support handling
#
# Both lb & ub should be compile-time constants
# otherwise, one should manually specify the methods
#

macro continuous_distr_support(D, lb, ub)
	if isfinite(eval(lb)) && isfinite(eval(ub))  # [lb, ub]
		esc(quote
			isupperbounded(::Union($D, Type{$D})) = true
			islowerbounded(::Union($D, Type{$D})) = true
			isbounded(::Union($D, Type{$D})) = true
			minimum(::Union($D, Type{$D})) = $lb
			maximum(::Union($D, Type{$D})) = $ub
			insupport(::Union($D, Type{$D}), x::Real) = ($lb <= x <= $ub)
		end)

	elseif isfinite(eval(lb))  # [lb, inf)
		esc(quote
			isupperbounded(::Union($D, Type{$D})) = false
			islowerbounded(::Union($D, Type{$D})) = true
			isbounded(::Union($D, Type{$D})) = false
			minimum(::Union($D, Type{$D})) = $lb
			maximum(::Union($D, Type{$D})) = $ub
			insupport(::Union($D, Type{$D}), x::Real) = (isfinite(x) && x >= $lb)
		end)

	elseif isfinite(eval(ub))  # (-inf, ub]
		esc(quote
			isupperbounded(::Union($D, Type{$D})) = true
			islowerbounded(::Union($D, Type{$D})) = false
			isbounded(::Union($D, Type{$D})) = false
			minimum(::Union($D, Type{$D})) = $lb
			maximum(::Union($D, Type{$D})) = $ub
			insupport(::Union($D, Type{$D}), x::Real) = (isfinite(x) && x <= $ub)
		end)

	else   # (-inf, inf)
		esc(quote
			isupperbounded(::Union($D, Type{$D})) = false
			islowerbounded(::Union($D, Type{$D})) = false
			isbounded(::Union($D, Type{$D})) = false
			minimum(::Union($D, Type{$D})) = $lb
			maximum(::Union($D, Type{$D})) = $ub
			insupport(::Union($D, Type{$D}), x::Real) = isfinite(x)
		end)

	end
end

# for checking the input range of quantile functions
# comparison with NaN is always false, so no explicit check is required
macro checkquantile(p,ex)
    :(zero($p) <= $p <= one($p) ? $ex : NaN)
end
macro checkinvlogcdf(lp,ex)
    :($lp <= zero($lp) ? $ex : NaN)
end


# %% Carbon cycle extinction events
# model of Rothman, 10.1073/pnas.1905164116
# main dynamic rule, eqs.(11, 12) of paper
using DrWatson
@quickactivate "NonlinearDynamicsTextbook"
include(srcdir("style.jl"))
using DynamicalSystems, PyPlot, Statistics, DelimitedFiles
function excitable_carbon(u, p, t)
    c, w = u
    μ, b, θ, cₓ, cₚ, ν, w₀, γ, f₀, c_f, β = p
    s = excitable_carbon_s(c, cₚ, γ)
    sbar = 1 - excitable_carbon_s(c, cₓ, γ)
    f = excitable_carbon_f(c, f₀, β, c_f)
    cdot = μ*(1 - b*s - θ*sbar - ν) + w - w₀
    cdot *= f
    wdot = μ*(1 - b*s + θ*sbar + ν) - w + w₀
    return SVector(cdot, wdot)
end
# functions used in equations of motion
excitable_carbon_f(c, f₀, β, c_f) = f₀*(c^β / (c^β + c_f^β))
excitable_carbon_s(c, cₚ, γ) = c^γ/(c^γ + cₚ^γ)

u0 = [100.0, 2800.0] # random, from Fig. 4

# Parameter default values in Table 1 of appendix, but cₓ same as Fig. 5
p0 = [250, 4, 5, 55, 110, 0, 2000, 4, 0.694, 43.9, 1.7]
# μ, b, θ, cₓ, cₚ, ν, w₀, γ, f₀, c_f, β = p0

ec = ContinuousDynamicalSystem(excitable_carbon, u0, p0)

# streamplot:
xgrid = 0:5:190
ygrid = 2000:20:3500
ux = zeros(length(xgrid), length(ygrid))
uy = copy(ux)
for (i, x) in enumerate(xgrid)
    for (j, y) in enumerate(ygrid)
        ux[i, j], uy[i, j] = ec.f(SVector(x, y), ec.p, 0)
    end
end

fig, axs = subplots(1,2; figsize = (0.66figx, figy))

axs[1].streamplot(Vector(xgrid), Vector(ygrid) ./1000 , ux', uy' ./ 1000;
    linewidth = 1.0, density = 0.25, color = "C4", arrowsize = 1.0
)

# plot trajectories
c_stable = 83.58 # approximate value
for (i, u) in enumerate(([c_stable, 2075.0], [c_stable, 2102.0], ))
    tr = trajectory(ec, 100.0, u)
    c, w = columns(tr)
    w ./= 1000
    for ax in axs
        ax.plot(c, w; color = "C$(i-1)")
    end
end
ax = axs[1]
ax.set_xlabel("\$c\$"; labelpad = -15)
ax.set_ylabel("\$w \\times 10^{-3}\$")
ax.set_xticks(0:60:180)

zbox = ((65, 2.05), (100, 2.25))

# Plot fixed point and arrows
fp = trajectory(ec, 10.0, [83.56, 2075]; Ttr = 1000)[end]
axs[2].plot([fp[1]], [fp[2]/1000]; marker = "o", mew=4, ms = 15, mec="C2", zorder = 99, mfc="white")

# axs[2].arrow(
#     fp[1], fp[2]/1000, 0,  2.1003 - fp[2]/1000,
#     zorder = 99, width = 0.01,
# )

# axs[2].arrow(
#     fp[1], 2.102, 0, fp[2]/1000 - 2.102;
#     zorder = 99, width = 0.01, length_includes_head = true,
#     # head_width = 2,
#     head_starts_at_zero = true
# )

axis_zoomin!(axs[2], axs[1], zbox, zbox, "C3"; α = 0.75)
axs[2].axis("off")
fig.tight_layout(pad = 0.3)
fig.subplots_adjust(wspace = 0.1)

# add_identifiers!(fig)
wsave(plotsdir("12", "carboncycle"), fig)


"""
    subtract(x, y)

Perform subtraction between two floating point numbers.

# Arguments
- `a::Float64`: Number 1.
- `b::Float64`: Number 2.

# Returns
- `a - b::Float64`

# Examples
```julia-repl
julia> subtract(1.0, 2.0)
-1.0
```
"""
subtract(a::Float64, b::Float64) = a - b