module Mimi

include("metainfo.jl")
include("clock.jl")

using DataStructures
using DataFrames
using Distributions
using Compat

export
    ComponentState, run_timestep, run, @defcomp, Model, setindex, addcomponent, setparameter,
    connectparameter, setleftoverparameters, getvariable, adder, MarginalModel, getindex,
    getdataframe, components, variables, setbestguess, setrandom, getvpd, unitcheck

import
    Base.getindex, Base.run, Base.show

function lint_helper(ex::Expr, ctx)
    if ex.head == :macrocall
        if ex.args[1] == Symbol("@defcomp")
            push!(ctx.callstack[end].types, ex.args[2])
            return true
        end
    end
    return false
end

abstract ComponentState

abstract Parameter

type CertainScalarParameter <: Parameter
    dependentCompsAndParams::Set{Tuple{ComponentState, Symbol}}
    value

    function CertainScalarParameter(value)
        p = new()
        p.dependentCompsAndParams = Set{Tuple{ComponentState,Symbol}}()
        p.value = value
        return p
    end
end

function setbestguess(p::CertainScalarParameter)
    for (c, name) in p.dependentCompsAndParams
        bg_value = p.value
        setfield!(c.Parameters,name,bg_value)
    end
end

function setrandom(p::CertainScalarParameter)
    for (c, name) in p.dependentCompsAndParams
        bg_value = p.value
        setfield!(c.Parameters,name,bg_value)
    end
end

type UncertainScalarParameter <: Parameter
    dependentCompsAndParams::Set{Tuple{ComponentState,Symbol}}
    value::Distribution

    function UncertainScalarParameter(value)
        p = new()
        p.dependentCompsAndParams = Set{Tuple{ComponentState,Symbol}}()
        p.value = value
        return p
    end
end

function setbestguess(p::UncertainScalarParameter)
    bg_value = mode(p.value)
    for (c, name) in p.dependentCompsAndParams
        setfield!(c.Parameters,name,bg_value)
    end
end

function setrandom(p::UncertainScalarParameter)
    sample = rand(p.value)
    for (c, name) in p.dependentCompsAndParams
        setfield!(c.Parameters,name,sample)
    end
end

type UncertainArrayParameter <: Parameter
    distributions::Array{Distribution, 1}
    values::Array{Float64,1}

    function UncertainArrayParameter(distributions)
        uap = new()
        uap.distributions = distributions
        uap.values = Array(Float64, size(distributions))
        return uap
    end
end

function setbestguess(p::UncertainArrayParameter)
    for i in 1:length(p.distributions)
        p.values[i] = mode(p.distributions[i])
    end
end

function setrandom(p::UncertainArrayParameter)
    for i in 1:length(p.distributions)
        p.values[i] = rand(p.distributions[i])
    end
end

type CertainArrayParameter <: Parameter
    values

    function CertainArrayParameter(values::Array)
        uap = new()
        uap.values = values
        return uap
    end
end

function setbestguess(p::CertainArrayParameter)
end

function setrandom(p::CertainArrayParameter)
end

type ComponentInstanceInfo
    name::Symbol
    component_type::DataType
end

type ParameterVariableConnection
    source_variable_name::Symbol
    source_component_name::Symbol
    target_parameter_name::Symbol
    target_component_name::Symbol
end

type Model
    indices_counts::Dict{Symbol,Int}
    indices_values::Dict{Symbol,Vector{Any}}
    components::OrderedDict{Symbol,ComponentState}
    parameters_that_are_set::Set{Compat.UTF8String}
    parameters::Dict{Symbol,Parameter}
    numberType::DataType
    connections::Array{ParameterVariableConnection, 1}
    components2::OrderedDict{Symbol, ComponentInstanceInfo}

    function Model(numberType::DataType=Float64)
        m = new()
        m.indices_counts = Dict{Symbol,Int}()
        m.indices_values = Dict{Symbol, Vector{Any}}()
        m.components = OrderedDict{Symbol,ComponentState}()
        m.parameters_that_are_set = Set{Compat.UTF8String}()
        m.parameters = Dict{Symbol, Parameter}()
        m.numberType = numberType
        m.connections = Array(ParameterVariableConnection, 0)
        m.components2 = OrderedDict{Symbol, ComponentInstanceInfo}()
        return m
    end
end

type MarginalModel
    base::Model
    marginal::Model
    delta::Float64
end

function setbestguess(m::Model)
    for p in values(m.parameters)
        setbestguess(p)
    end
end

function setrandom(m::Model)
    for p in values(m.parameters)
        setrandom(p)
    end
end

"""
    components(m::Model)

List all the components in model `m`.
"""
function components(m::Model)
    collect(keys(m.components))
end

# Return the MetaComponent for a given component
function getmetainfo(m::Model, componentname::Symbol)
    meta = metainfo.getallcomps()
    meta_module_name = Symbol(supertype(typeof(m.components[componentname])).name.module)
    meta_component_name = Symbol(supertype(typeof(m.components[componentname])).name.name)
    meta[(meta_module_name, meta_component_name)]
end

"""
    variables(m::Model, componentname::Symbol)

List all the variables of `componentname` in model `m`.
"""
function variables(m::Model, componentname::Symbol)
    c = getmetainfo(m, componentname)
    collect(keys(c.variables))
end

function getindex(m::MarginalModel, component::Symbol, name::Symbol)
    return (m.marginal[component,name].-m.base[component,name])./m.delta
end

function setindex(m::Model, name::Symbol, count::Int)
    m.indices_counts[name] = count
    m.indices_values[name] = collect(1:count)
    nothing
end

function setindex{T}(m::Model, name::Symbol, values::Vector{T})
    m.indices_counts[name] = length(values)
    m.indices_values[name] = copy(values)
    nothing
end

"""
Add a component to a model.
"""
function addcomponent(m::Model, t, name::Symbol=Symbol(string(t)); before=nothing,after=nothing)
    if before!=nothing && after!=nothing
        error("Can only specify before or after parameter")
    end

    comp = t(m.numberType, m.indices_counts)

    if before!=nothing
        newcomponents = OrderedDict{Symbol,ComponentState}()
        newcomponents2 = OrderedDict{Symbol, ComponentInstanceInfo}()
        for i in keys(m.components)
            if i==before
                newcomponents[name] = comp
                curr = ComponentInstanceInfo(name, t)
                newcomponents2[name] = curr
            end
            newcomponents[i] = m.components[i]
            newcomponents2[i] = m.components2[i]
        end
        m.components = newcomponents
        m.components2 = newcomponents2
    elseif after!=nothing
        error("Not yet implemented")
    else
        m.components[name] = comp
        this = ComponentInstanceInfo(name, t)
        m.components2[name] = this
    end

    ComponentReference(m, name)
end

"""
Set the parameter of a component in a model to a given value.
"""
function setparameter(m::Model, component::Symbol, name::Symbol, value)
    addparameter(m, name, value)
    connectparameter(m, component, name, name)

    setbestguess(m.parameters[Symbol(lowercase(string(name)))])
    nothing
end

function connectparameter(m::Model, component::Symbol, name::Symbol, parametername::Symbol)
    c = m.components[component]
    p = m.parameters[Symbol(lowercase(string(parametername)))]

    if isa(p, CertainScalarParameter) || isa(p, UncertainScalarParameter)
        push!(p.dependentCompsAndParams, (c, name))
    else
        setfield!(c.Parameters,name,p.values)
    end
    push!(m.parameters_that_are_set, string(component) * string(name))

    nothing
end

function updateparameter(m::Model, name::Symbol, value)
       p = m.parameters[Symbol(lowercase(string(name)))]

       p.value = value

       setbestguess(p)
end

function addparameter(m::Model, name::Symbol, value)
    if isa(value, Distribution)
        p = UncertainScalarParameter(value)
        m.parameters[Symbol(lowercase(string(name)))] = p
    elseif isa(value, AbstractArray)
        if any(x->isa(x, Distribution), value)
            p = UncertainArrayParameter(value)
            m.parameters[Symbol(lowercase(string(name)))] = p
        else
            if !(typeof(value) <: Array{m.numberType})
                # E.g., if model takes Number and given Float64, convert it
                value = convert(Array{m.numberType}, value)
            end

            p = CertainArrayParameter(value)
            m.parameters[Symbol(lowercase(string(name)))] = p
        end
    else
        p = CertainScalarParameter(value)
        m.parameters[Symbol(lowercase(string(name)))] = p
    end
end

function connectparameter(m::Model, target_component::Symbol, target_name::Symbol, source_component::Symbol, source_name::Symbol; ignoreunits::Bool=false)
    c_target = m.components[target_component]
    c_source = m.components[source_component]

    # Check the units, if provided
    if !ignoreunits &&
        !unitcheck(getmetainfo(m, target_component).parameters[target_name].unit,
                   getmetainfo(m, source_component).variables[source_name].unit)
        throw(ErrorException("Units of $source_component.$source_name do not match $target_component.$target_name."))
    end

    setfield!(c_target.Parameters, target_name, getfield(c_source.Variables, source_name))
    push!(m.parameters_that_are_set, string(target_component) * string(target_name))

    this = ParameterVariableConnection(source_name, source_component, target_name, target_component)
    push!(m.connections, this)

    nothing
end

# Default string, string unit check function
function unitcheck(one::AbstractString, two::AbstractString)
    # True if and only if they match
    return one == two
end

"""
Bind the parameter of one component to a variable in another component.

"""
connectparameter

"""
Set all the parameters in a model that don't have a value and are not connected
to some other component to a value from a dictionary.
"""
function setleftoverparameters(m::Model,parameters::Dict{Any,Any})
    for (name, value) in parameters
        addparameter(m, Symbol(name), value)
    end

    for c in m.components
        for name in fieldnames(c[2].Parameters)
            if !in(string(c[1])*string(name), m.parameters_that_are_set)
                connectparameter(m, c[1], name, name)
            end
        end
    end
    nothing
end

function getindex(m::Model, component::Symbol, name::Symbol)
    return getfield(m.components[component].Variables, name)
end

"""
    getdataframe(m::Model, componentname::Symbol, name::Symbol)

Return the values for variable `name` in `componentname` of model `m` as a DataFrame.
"""
function getdataframe(m::Model, componentname::Symbol, name::Symbol)
    comp_type = typeof(m.components[componentname])

    meta_module_name = Symbol(supertype(typeof(m.components[componentname])).name.module)
    meta_component_name = Symbol(supertype(typeof(m.components[componentname])).name.name)

    vardiminfo = getdiminfoforvar((meta_module_name,meta_component_name), name)
    if length(vardiminfo)==0
        return m[componentname, name]
    elseif length(vardiminfo)==1
        df = DataFrame()
        df[vardiminfo[1]] = m.indices_values[vardiminfo[1]]
        df[name] = m[componentname, name]
        return df
    elseif length(vardiminfo)==2
        df = DataFrame()
        dim1 = length(m.indices_values[vardiminfo[1]])
        dim2 = length(m.indices_values[vardiminfo[2]])
        df[vardiminfo[1]] = repeat(m.indices_values[vardiminfo[1]],inner=[dim2])
        df[vardiminfo[2]] = repeat(m.indices_values[vardiminfo[2]],outer=[dim1])
        data = m[componentname, name]
        df[name] = cat(1,[vec(data[i,:]) for i=1:dim1]...)
        return df
    else
        error("Not yet implemented")
    end
end

import Base.show
show(io::IO, a::ComponentState) = print(io, "ComponentState")

"""
    run(m::Model)

Run model `m` once.
"""
function run(m::Model;ntimesteps=typemax(Int))
    clock = Clock(1,min(m.indices_counts[:time],ntimesteps))

    for c in values(m.components)
        resetvariables(c)
        init(c)
    end

    while !finished(clock)
        for c in values(m.components)
            run_timestep(c,gettimestep(clock))
        end
        move_forward(clock)
    end
end

function run_timestep(s, t)
    typeofs = typeof(s)
    println("Generic run_timestep called for $typeofs.")
end

function init(s)
end

function resetvariables(s)
    typeofs = typeof(s)
    println("Generic resetvariables called for $typeofs.")
end

function getdiminfoforvar(s, name)
    meta = metainfo.getallcomps()
    meta[s].variables[name].dimensions
end

function getvpd(s)
    return s.Variables, s.Parameters, s.Dimensions
end

# Helper function for macro: collects all the keyword arguments in a function call to a dictionary.
function collectkw(args::Vector{Any})
    kws = Dict{Symbol, Any}()
    for arg in args
        if isa(arg, Expr) && arg.head == :kw
            kws[arg.args[1]] = arg.args[2]
        end
    end

    kws
end

"""
Define a new component.
"""
macro defcomp(name, ex)
    resetvarsdef = Expr(:block)

    metavardef = Expr(:block)
    metapardef = Expr(:block)
    metadimdef = Expr(:block)

    for line in ex.args
        if line.head==:(=) && line.args[2].head==:call && line.args[2].args[1]==:Index
            dimensionName = line.args[1]

            push!(metadimdef.args, :(metainfo.adddimension(module_name(current_module()), $(Expr(:quote,name)), $(QuoteNode(dimensionName)) )))
        elseif line.head==:(=) && line.args[2].head==:call && line.args[2].args[1]==:Parameter
            if isa(line.args[1], Symbol)
                parameterName = line.args[1]
                parameterType = :Number
            elseif line.args[1].head==:(::)
                parameterName = line.args[1].args[1]
                parameterType = line.args[1].args[2]
            else
                error()
            end

            kws = collectkw(line.args[2].args)

            # Get description and unit, if provided
            description = get(kws, :description, "")
            unit = get(kws, :unit, "")

            if haskey(kws, :index)
                parameterIndex = kws[:index].args

                pardims = Array(Any, 0)
                for l in parameterIndex
                    push!(pardims, l)
                end

                push!(metapardef.args, :(metainfo.addparameter(module_name(current_module()), $(Expr(:quote,name)), $(QuoteNode(parameterName)), $(esc(parameterType)), $(pardims), $(description), $(unit))))
            else
                push!(metapardef.args, :(metainfo.addparameter(module_name(current_module()), $(Expr(:quote,name)), $(QuoteNode(parameterName)), $(esc(parameterType)), [], $(description), $(unit))))
            end
        elseif line.head==:(=) && line.args[2].head==:call && line.args[2].args[1]==:Variable
            if isa(line.args[1], Symbol)
                variableName = line.args[1]
                variableType = :Number
            elseif line.args[1].head==:(::)
                variableName = line.args[1].args[1]
                variableType = line.args[1].args[2]
            else
                error()
            end

            kws = collectkw(line.args[2].args)

            # Get description and unit, if provided
            description = get(kws, :description, "")
            unit = get(kws, :unit, "")

            if haskey(kws, :index)
                variableIndex = kws[:index].args

                vardims = Array(Any, 0)
                for l in variableIndex
                    push!(vardims, l)
                end

                push!(metavardef.args, :(metainfo.addvariable(module_name(current_module()), $(Expr(:quote,name)), $(QuoteNode(variableName)), $(esc(variableType)), $(vardims), $(description), $(unit))))

                if variableType==:Number
                    push!(resetvarsdef.args,:($(esc(Symbol("fill!")))(s.Variables.$(variableName),$(esc(Symbol("NaN"))))))
                end
            else
                push!(metavardef.args, :(metainfo.addvariable(module_name(current_module()), $(Expr(:quote,name)), $(QuoteNode(variableName)), $(esc(variableType)), [], $(description), $(unit))))

                if variableType==:Number
                    push!(resetvarsdef.args,:(s.Variables.$(variableName) = $(esc(Symbol("NaN")))))
                end
            end
        elseif line.head==:line
        else
            error("Unknown expression.")
        end
    end

    module_def = :(eval(current_module(), :(module temporary_name end)))
    module_def.args[3].args[1].args[2] = Symbol(string("_mimi_implementation_", name))

    call_expr = Expr(:call,
        Expr(:curly,
            Expr(:., Expr(:., Expr(:., :Main, QuoteNode(Symbol(current_module()))), QuoteNode(Symbol(string("_mimi_implementation_", name)))), QuoteNode(Symbol(string(name,"Impl")))) ,
            :T),
        :T,
        :indices
        )

    x = quote

        abstract $(esc(Symbol(name))) <: Mimi.ComponentState

        import Mimi.run_timestep
        import Mimi.init
        import Mimi.resetvariables

        function $(esc(Symbol("resetvariables")))(s::$(esc(Symbol(name))))
            $(resetvarsdef)
        end

        metainfo.addcomponent(module_name(current_module()), $(Expr(:quote,name)))
        $(metavardef)
        $(metapardef)
        $(metadimdef)

        $(module_def)

        eval($(esc(Symbol(string("_mimi_implementation_", name)))), metainfo.generate_comp_expressions(module_name(current_module()), $(Expr(:quote,name))))

        function $(esc(Symbol(name))){T}(::Type{T}, indices)
            $(call_expr)
        end

    end

    x
end

@defcomp adder begin
    add = Parameter(index=[time])
    input = Parameter(index=[time])
    output = Variable(index=[time])
end

function run_timestep(s::adder, t::Int)
    v = s.Variables
    p = s.Parameters

    v.output[t] = p.input[t] + p.add[t]
end

#Begin Graph Functionality section

function show(io::IO, m::Model)
    println(io, "showing model component connections:")
    for item in enumerate(keys(m.components2))
        c = item[2]
        i_connections = get_connections(m,c,:incoming)
        o_connections = get_connections(m,c,:outgoing)
        println(io, item[1], ". ", c, " component")
        println(io, "    incoming parameters:")
        if length(i_connections)==0
            println(io, "      none")
        else
            [println(io, "      - ",e.target_parameter_name," from ",e.source_component_name," component") for e in i_connections]
        end
        println(io, "    outgoing variables:")
        if length(o_connections)==0
            println(io, "      none")
        else
            [println(io, "      - ",e.source_variable_name," in ",e.target_component_name, " component") for e in o_connections]
        end
    end
end

function get_connections(m::Model, c::ComponentInstanceInfo, which::Symbol)
    return get_connections(m, c.name, which)
end

function get_connections(m::Model, component_name::Symbol, which::Symbol)
    if which==:all
        f = e -> e.source_component_name==component_name || e.target_component_name==component_name
    elseif which==:incoming
        f = e -> e.target_component_name==component_name
    elseif which==:outgoing
        f = e -> e.source_component_name==component_name
    else
        error("Invalid parameter for the 'which' argument; must be 'all' or 'incoming' or 'outgoing'.")
    end
    return filter(f, m.connections)
end

#End of graph section

include("references.jl")

end # module


# Tests based on the Vector API tutorial found at http://gdal.org/ogr_apitut.html

GU.registerdrivers() do
    GU.read("data/point.geojson") do dataset
        @fact GU.nlayer(dataset) --> 1
        layer = GU.fetchlayer(dataset, 0)
        @fact GU.getname(layer) --> "OGRGeoJSON"
        layerbyname = GU.fetchlayer(dataset, "OGRGeoJSON")
        @fact layerbyname --> layer
        GU.resetreading(layer)

        featuredefn = GU.getlayerdefn(layer)
        @fact GU.nfield(featuredefn) --> 2
        fielddefn = GU.fetchfielddefn(featuredefn, 0)
        @fact GDAL.gettype(fielddefn) --> GDAL.OFTReal
        fielddefn = GU.fetchfielddefn(featuredefn, 1)
        @fact GDAL.gettype(fielddefn) --> GDAL.OFTString

        GU.fetchnextfeature(layer) do feature
            @fact GU.asdouble(feature, 0) --> roughly(2.0)
            @fact GU.asstring(feature, 1) --> "point-a"
        end
        GU.fetchnextfeature(layer) do feature # second feature
            @fact GU.asdouble(feature, 0) --> roughly(3.0)
            @fact GU.asstring(feature, 1) --> "point-b"

            geometry = GU.getgeom(feature)
            @fact GU.getgeomname(geometry) --> "POINT"
            @fact GU.getgeomtype(geometry) --> GDAL.wkbPoint
            @fact GU.nfield(featuredefn) --> 2
            @fact GU.getx(geometry, 0) --> roughly(100.2785)
            @fact GU.gety(geometry, 0) --> roughly(0.0893)
            @fact GU.getpoint(geometry, 0) --> (100.2785,0.0893,0.0)
        end
    end

    pointshapefile = "tmp/point_out"
    GU.create("$pointshapefile.shp", "ESRI Shapefile") do dataset
        layer = GU.createlayer(dataset, "point_out", GDAL.wkbPoint)
        fielddefn = GU.createfielddefn("Name", GDAL.OFTString)
        GU.setwidth(fielddefn, 32)
        GU.createfield(layer, fielddefn, true)
        GU.destroy(fielddefn)

        featuredefn = GU.getlayerdefn(layer)
        @fact GU.getname(featuredefn) --> "point_out"
        GU.createfeature(featuredefn) do feature
            GU.setfield(feature, GU.getfieldindex(feature, "Name"), "myname")
            GU.creategeom(GDAL.wkbPoint) do point
                GU.setpoint(point, 0, 100.123, 0.123)
                GU.setgeom(feature, point)
            end
        end
    end

    rm("$pointshapefile.dbf")
    rm("$pointshapefile.shp")
    rm("$pointshapefile.shx")
end


using JSON
using JLD
using StatsBase
using BenchmarkTools
using Glob
using HTTP
# using Libz
# using Base.LibGit2

# push!(LOAD_PATH, @__DIR__)
# using Check

const cachefolder = joinpath(@__DIR__, "dlcache")
const repobase = "https://raw.githubusercontent.com/JuliaCI/BaseBenchmarkReports/master"

function summarize(benchmarks)
    if isdefined(benchmarks, :data)
        return map(benchmarks.data) do entry
            string(entry.first) => summarize(entry.second)
        end
    elseif isdefined(benchmarks, :times)
        return summarystats(benchmarks.times)
    else
        return nothing
    end
end

datedirname(date) = string("daily_", Dates.year(date), "_", Dates.month(date), "_", Dates.day(date))

function get_daily_data(date)
    dailydir = joinpath(cachefolder, date)
    isdir(dailydir) || mkdir(dailydir)
    filegz = joinpath(dailydir, "data.tar.gz")
    if !isfile(filegz)
        uri = string("$repobase/$date/data.tar.gz")
        datagz = HTTP.get(uri)
        status = HTTP.status(datagz)
        if status != 200
            warn("$uri => $status")
            return nothing # assume there is no data for this day
        end
        write(filegz, HTTP.bytes(datagz))
    end
    run(`tar -xzf $filegz -C $dailydir`)
    files = readdir(glob"data/*_primary.jld", dailydir)
    if isempty(files)
        data = nothing # no data?
    else
        length(files) == 1 || warn("Too many primary.jld files for $date; taking one.")
        data = BenchmarkTools.load(files[1], "results")
    end
    rm(joinpath(dailydir, "data"), recursive = true)
    return data
end

function get_daily_summary(date)
    file = joinpath(cachefolder, "$date.json")
    if !isfile(file)
        data = summarize(get_daily_data(date))
        open(file, "w") do io
            JSON.print(io, data)
        end
    end
    return JSON.parse(readstring(file))
end

# take a Vector{Dict{String, Float64}} and turn it into Dict{String, Vector{Float64}}
# but with nesting and nullable
function transpose_daily_summary(data)
    groups = Set{String}()
    for day in data
        isa(day, Dict) && union!(groups, keys(day))
    end

    if isempty(groups)
        output = Float64[ (day === nothing ? NaN : day) for day in data ]
    else
        output = Dict{String, Any}()
        sizehint!(output, length(groups))
        for group in groups
            values = Any[ (isa(day, Dict) ? get(day, group, nothing) : day) for day in data ]
            output[group] = transpose_daily_summary(values)
        end
    end
    return output
end

# take a Dict{String, Dict{String, ...{String, Vector{Float64}}}
# and flatten it into Dict{String, Vector{Float64}} by name-mangling the paths together
flatten_summary(data) = flatten_summary("", data)
flatten_summary(prefix::String, data::Vector{Float64}) = Dict(prefix => data)
function flatten_summary(prefix::String, data::Dict)
    output = Dict{String, Vector{Float64}}()
    for (k, v) in data
        merge!(output, flatten_summary(isempty(prefix) ? k : "$prefix.$k", v))
    end
    return output
end

if false
    today = trunc(now(), Dates.Day)
    until = today - Dates.Day(90)
    headers = String[]
    data = Any[]
    while today > until
        date = datedirname(today)
        push!(headers, date)
        push!(data, get_daily_summary(date))
        today -= Dates.Day(1)
    end
    missing = data .== nothing
    data = transpose_daily_summary(data)
    data = flatten_summary(data)
end


using CategoricalArrays, XLSX

export shadedmap, myhist, GISturbines_density, aggregate, exportGISturbinedata, landtypes,
        makePixelDataframe, savePixelData, grouppopulationdensity, groupwindspeeds,
        scatter, plot, plot!, Point2f0, RGBA, FRect, plotfix!, heatmap, colorlegend, vbox

const landtypes = ["Evergreen Needleleaf", "Evergreen Broadleaf",
                "Deciduous Needleleaf", "Deciduous Broadleaf", "Mixed Forests",
                "Closed Shrublands", "Open Shrublands", "Woody Savannas", "Savannas", "Grasslands",
                "Permanent Wetlands", "Croplands", "Urban", "Cropland/Natural", "Snow/Ice", "Barren"]

# const landtypes = ["Forest", "Forest", "Forest", "Forest", "Forest", "Shrubland", "Shrubland", "Forest",
#                 "Savanna", "Grassland", "Wetland", "Cropland", "Urban", "Cropland", "Snow/Ice", "Barren"]

function map_protected(; downsample=1, resolutionscale=1, textscale=1, optionlist...)
    options = WindOptions(merge(windoptions(), optionlist))
    @unpack gisregion, res, protected_codes = options
    regions, offshoreregions, regionlist, gridaccess, popdens, topo, land,
        protected, lonrange, latrange = read_datasets(options)

    n2000_full, ex_n2000 = readraster(in_datafolder("natura2000.tif"), :getextent)
    latrange_n2000, lonrange_n2000 = bbox2ranges(extent2bbox(ex_n2000), 100)
    lons_n2000 = (lonrange[1]:lonrange[end]) .- lonrange_n2000[1] .+ 1
    lats_n2000 = latrange .- latrange_n2000[1] .+ 1
    n2000 = n2000_full[lons_n2000, lats_n2000]

    wdpa = zeros(Bool, size(protected))
    for i in protected_codes
        wdpa[protected .== i] .= true
    end

    reg = (regions.>0) .& (regions .< NOREGION)

    mask = zeros(Int16, size(regions))
    mask[reg] .= 1
    mask[wdpa .& reg] .= 2
    mask[reg .& (n2000 .> 0)] .= 3
    mask[reg .& wdpa .& (n2000 .> 0)] .= 4
    mask[regions.==NOREGION] .= NOREGION

    legendtext = ["unprotected", "WDPA", "Natura2000", "both", "wind turbines"]

    mask = mask[1:downsample:end, 1:downsample:end]
    lonrange = lonrange[1:downsample:end]
    latrange = latrange[1:downsample:end]
    nreg = length(legendtext)

    colors = RGBA.([RGB(0.2,0.3,0.4), RGB(0.7,0.7,0.7), RGB(0,.7,0), RGB(0,0,.7), RGB(.9,.9,0), RGB(1,0,0), RGB(0.4,0.4,0.4)], 0.8)

    println("Mapping colors to regions (avoid same color in adjacent regions)...")
    connected = connectedregions(mask, nreg)
    # colorindices = (nreg > 7) ? greedycolor(connected, 1:7, 1:nreg, randseed=randseed) : collect(1:nreg)
    # colors = colorschemes[Symbol("Set2_$nreg")].colors[colorindices]

    println("\nProjecting coordinates (Mollweide)...")
    res = 0.01
    res2 = res/2
    lons = (-180+res2:res:180-res2)[lonrange]         # longitude values (pixel center)
    lats = (90-res2:-res:-90+res2)[latrange]          # latitude values (pixel center)
    source = Projection("+proj=longlat +datum=WGS84")
    dest = Projection("+proj=moll +lon_0=$(mean(lons)) +ellps=WGS84")
    xs, ys = xygrid(lons, lats)
    Proj4.transform!(source, dest, vec(xs), vec(ys))

    cols = [:lon, :lat, :capac, :year, :onshore, :elec2018]
    df = DataFrame(CSV.File(in_datafolder("turbines_DE.csv")))[:, cols[1:4]]
    tx, ty = df.lon, df.lat
    Proj4.transform!(source, dest, vec(tx), vec(ty))

    println("\nOnshore map...")
    createmap("$(gisregion)_protected", mask, legendtext, lons, lats, colors, source, dest, xs, ys,
        [], [], connected, connected; lines=false, labels=false, resolutionscale=resolutionscale,
        textscale=textscale, legend=true, dots=(tx,ty))
end

function makePixelDataframe(; optionlist...)
    cols = [:lon, :lat, :capac, :year, :onshore, :elec2018]
    df_DK = DataFrame(CSV.File(in_datafolder("turbines_DK.csv")))[:, cols]
    df_SE = DataFrame(CSV.File(in_datafolder("turbines_SE.csv")))[:, cols[1:5]]
    df_DE = DataFrame(CSV.File(in_datafolder("turbines_DE.csv")))[:, cols[1:4]]
    df_USA = DataFrame(CSV.File(in_datafolder("turbines_USA.csv")))[:, cols[1:4]]
    df_SE[:, :elec2018] .= missing    # MWh/year
    df_DE[:, :elec2018] .= missing    # MWh/year
    df_DE[:, :onshore] .= missing    # MWh/year
    df_USA[:, :elec2018] .= missing    # MWh/year
    df_USA[:, :onshore] .= missing    # MWh/year
    df_SE[!,:year] = [ismissing(d) ? 2000 : year(d) for d in df_SE[!,:year]]
    turbines = vcat(df_DK, df_SE, df_DE, df_USA)

    options = WindOptions(merge(windoptions(), optionlist))
    @unpack gisregion, res = options
    regions, offshoreregions, regionlist, gridaccess, popdens, topo, land,
        protected, lonrange, latrange = read_datasets(options)

    windatlas = getwindatlas()[lonrange,latrange]
    miuu = zeros(Float32, size(windatlas))

    if gisregion == "SwedenGADM3"
        miuu_raw, ex_miuu = readraster(in_datafolder("miuu_windatlas.tif"), :getextent)
        latrange_miuu, lonrange_miuu = bbox2ranges(extent2bbox(ex_miuu), 100)
        # MIUU has a smaller extent than SwedenGADM3
        lons_miuu = lonrange_miuu .- lonrange[1] .+ 1
        lats_miuu = latrange_miuu .- latrange[1] .+ 1
        miuu[lons_miuu, lats_miuu] = miuu_raw
    end

    n2000, ex_n2000 = readraster(in_datafolder("natura2000.tif"), :getextent)
    latrange_n2000, lonrange_n2000 = bbox2ranges(extent2bbox(ex_n2000), 100)
    lons_n2000 = (lonrange[1]:lonrange[end]) .- lonrange_n2000[1] .+ 1
    lats_n2000 = latrange .- latrange_n2000[1] .+ 1
    n2000 = occursin("USA", gisregion) ? zeros(UInt8, size(windatlas)) : n2000[lons_n2000, lats_n2000]

    nreg = length(regionlist)
    nlon, nlat = size(windatlas)
    turbinecapac = zeros(Int, nlon, nlat)
    nturbines = zeros(Int, nlon, nlat)
    turbineyear = zeros(Int, nlon, nlat)

    for row in eachrow(turbines)
        reg, flon, flat = getregion_and_index(row[:lon], row[:lat], regions, lonrange, latrange)
        if (reg == 0 || reg == NOREGION)
            reg, flon, flat = getregion_and_index(row[:lon], row[:lat], offshoreregions, lonrange, latrange)
            (reg == 0 || reg == NOREGION) && continue
        end
        nturbines[flon,flat] += 1
        turbineyear[flon,flat] = max(turbineyear[flon,flat], row[:year])
        turbinecapac[flon,flat] += row[:capac]
    end

    region = max.(regions, offshoreregions)
    lons = (-180+res/2:res:180-res/2)[lonrange]   # longitude values (pixel center)
    lats = (90-res/2:-res:-90+res/2)[latrange]    # latitude values (pixel center)
    cellarea = rastercellarea.(lats, res)
    lon = repeat(lons, 1, nlat)
    lat = repeat(lats', nlon)
    area = repeat(cellarea', nlon)

    r = (region .> 0) .& (region .!= NOREGION)

    return DataFrame(lat=lat[r], lon=lon[r], munic=region[r],
        area=round.(area[r], sigdigits=4), landtype=land[r], protected=protected[r], natura2000=Int.(n2000[r]),
        popdens=round.(popdens[r], sigdigits=4), windspeed=round.(windatlas[r], sigdigits=4), miuu=round.(miuu[r], sigdigits=4),
        nturbines=nturbines[r], turbinecapac=turbinecapac[r], turbineyear=turbineyear[r])
end

function savePixelData()
    gisregions = ["SwedenGADM3", "Denmark83", "GermanyGADM3", "USA_Texas", "USA_Iowa", "USA_Oklahoma",
        "USA_Kansas", "USA_Illinois", "USA_South Dakota", "USA_North Dakota", "USA_California",
        "USA_Minnesota", "USA_Colorado", "USA_Oregon"]
    countries = ["Sweden", "Denmark", "Germany", "Texas", "Iowa", "Oklahoma", "Kansas", "Illinois",
        "South Dakota", "North Dakota", "California", "Minnesota", "Colorado", "Oregon"]
    println("Building municipality list...")
    munic, nmunic = Dict(), Dict()
    for (gisregion, country) in zip(gisregions, countries)
        _, _, regionlist, _, _ = loadregions(gisregion)
        munic[country] = regionlist
        nmunic[gisregion] = length(regionlist)
    end
    dfmunic = vcat((DataFrame(country=country, municipality=munic[country])
                for country in countries)...)
    insertcols!(dfmunic, 1, :index => 1:nrow(dfmunic))
    CSV.write("D:/GISdata/municipalities.csv", dfmunic)
    println("Building main DataFrame...")
    println(countries[1])
    dfall = makePixelDataframe(gisregion=gisregions[1])
    countrynum = 1
    insertcols!(dfall, 3, :country => countrynum)
    len = nmunic[gisregions[1]]
    for gisregion in gisregions[2:end]
        println(gisregion)
        df = makePixelDataframe(gisregion=gisregion)
        df[!,:munic] .+= len
        countrynum += 1
        insertcols!(df, 3, :country => countrynum)
        dfall = vcat(dfall, df)
        len += nmunic[gisregion]
    end
    println("Saving DataFrame...")
    CSV.write("D:/GISdata/windpixeldata.csv", dfall)
end

extent2bbox(ex) = [ex[2] ex[1]; ex[4] ex[3]]

function protected_vs_natura2000(; optionlist...)
    options = WindOptions(merge(windoptions(), optionlist))
    regions, offshoreregions, regionlist, gridaccess, popdens, topo, land,
        protected, lonrange, latrange = read_datasets(options)

    n2000, ex_n2000 = readraster(in_datafolder("natura2000.tif"), :getextent)
    latrange_n2000, lonrange_n2000 = bbox2ranges(extent2bbox(ex_n2000), 100)
    lons_n2000 = (lonrange[1]:lonrange[end]) .- lonrange_n2000[1] .+ 1
    lats_n2000 = latrange .- latrange_n2000[1] .+ 1
    n2000 = n2000[lons_n2000, lats_n2000]

    onshore = (regions .> 0) .& (regions .!= NOREGION) .& (land .> 0)
    offshore = (offshoreregions .> 0) .& (offshoreregions .!= NOREGION) .& (land .== 0)

    count_onshore = zeros(Int, 11, 4)
    count_offshore = zeros(Int, 11, 4)
    for i = 1:length(protected)
        if onshore[i]
            count_onshore[protected[i]+1, n2000[i]+1] += 1
        elseif offshore[i]
            count_offshore[protected[i]+1, n2000[i]+1] += 1
        end
    end
    display(count_onshore)
    display(count_offshore)

    return protected, n2000
end

function miuu_vs_windatlas()
    options = windoptions()
    options[:gisregion] = "SwedenGADM3"
    regions, offshoreregions, regionlist, gridaccess, popdens, topo, land,
        protected, lonrange, latrange = read_datasets(options)

    windatlas = getwindatlas()[lonrange,latrange]
    onshore = (regions .> 0) .& (regions .!= NOREGION) .& (land .> 0)
    offshore = (offshoreregions .> 0) .& (offshoreregions .!= NOREGION) .& (land .== 0)

    miuu, ex_miuu = readraster(in_datafolder("miuu_windatlas.tif"), :getextent)
    latrange_miuu, lonrange_miuu = bbox2ranges(extent2bbox(ex_miuu), 100)
    # MIUU has a smaller extent than SwedenGADM3
    lons_miuu = lonrange_miuu .- lonrange[1] .+ 1
    lats_miuu = latrange_miuu .- latrange[1] .+ 1

    windatlas = windatlas[lons_miuu, lats_miuu]
    onshore = onshore[lons_miuu, lats_miuu] .& (miuu .> 0)
    offshore = offshore[lons_miuu, lats_miuu] .& (miuu .> 0)

    return miuu, windatlas, onshore, offshore
end

function plotfix!(scene)
    xlabel!(scene, "Global Wind Atlas [m/s]")
    ylabel!(scene, "MIUU [m/s]")
    plot!(scene, [0,14],[0,14], color=:red)
end

#=
mm,ww,on,off = GE.miuu_vs_windatlas();
p = heatmap(reverse(ww.*(mm.>0),dims=2), resolution=(950,950), colorrange=(3,10), highclip=:red, lowclip=:black)
p = heatmap(reverse(mm,dims=2), resolution=(950,950), colorrange=(3,10), highclip=:red, lowclip=:black)
        # cl = colorlegend(p[end], resolution=(950,950)); vbox(p,vl)
opt = (color=RGBA(0,0,0,0.2), markersize=0.01, limits=FRect(0,0,14,14), textsize=10)
p = scatter(Point2f0.(ww[on], mm[on]+randn(sum(on))*.05); opt..., color=RGBA(0,0,0,1)); plotfix!(p)
p = scatter(Point2f0.(ww[off], mm[off]+randn(sum(off))*.05); opt..., color=RGBA(0,0,0,1)); plotfix!(p)

pp,nn = GE.protected_vs_natura2000(gisregion="SwedenGADM3");
p = heatmap(reverse(nn,dims=2), resolution=(950,950), highclip=:red, lowclip=:black)
p = heatmap(reverse(pp,dims=2), resolution=(950,950), highclip=:red, lowclip=:black)

=#

function analyze_protected(; firstyear=1978, lastyear=2021)
    df = CSV.File("D:/GISdata/windpixeldata.csv") |> DataFrame

    df.inyears = (df.turbineyear.>=firstyear) .& (df.turbineyear.<=lastyear)
    countries = ["Sweden", "Denmark", "Germany", "Texas", "Iowa", "Oklahoma", "Kansas", "Illinois",
        "South Dakota", "North Dakota", "California", "Minnesota", "Colorado", "Oregon"]
    df.countryname = countries[df.country]
    df.onshore = df.landtype .> 0
    protected_names = [
       "(Ia) Strict Nature Reserve"
       "(Ib) Wilderness Area"
       "(II) National Park"
       "(III) Natural Monument"
       "(IV) Habitat/Species Management"
       "(V) Protected Landscape/Seascape"
       "(VI) Managed Resource Protected Area"
       "Not Reported"
       "Not Applicable"
       "Not Assigned"
    ]
    protected_type = Dict(i => n for (i,n) in enumerate(protected_names))
    protected_type[0] = "UNPROTECTED"

    gdf = groupby(df, [:countryname, :onshore, :protected])
    cdf = combine(gdf,
            :area => (a -> sum(a)/1000) => :area,
            [:turbinecapac, :inyears] => ((c,y) -> sum(c.*y)/1000) => :capac
        )
    gdf_tot = groupby(cdf, [:countryname, :onshore])
    cdf_tot = combine(gdf_tot, :area => sum, :capac => sum)

    cdf.turbinedensity = cdf.capac ./ cdf.area
    cdf.protected_type = getindex.(Ref(protected_type), cdf.protected)
    cdf_tot.turbinedensity = cdf_tot.capac_sum ./ cdf_tot.area_sum

    sort!(cdf, [order(:countryname, by = x -> findfirst(countries.==x)), :protected, order(:onshore, rev=true)])
    sort!(cdf_tot, [order(:countryname, by = x -> findfirst(countries.==x)), order(:onshore, rev=true)])
    # CSV.write("protectedinfo.csv", cdf)
    # CSV.write("protectedinfo_tot.csv", cdf_tot)

    filename = "protected WDPA.xlsx"
    XLSX.openxlsx(filename, mode=isfile(filename) ? "rw" : "w") do xf
        sheetname = "$firstyear-$lastyear"
        sheet = sheetname in XLSX.sheetnames(xf) ? xf[sheetname] : XLSX.addsheet!(xf, sheetname)
        XLSX.writetable!(sheet, [cdf[:,i] for i=1:size(cdf,2)], names(cdf), anchor_cell=XLSX.CellRef("A4"))
        XLSX.writetable!(sheet, [cdf_tot[:,i] for i=1:size(cdf_tot,2)], names(cdf_tot), anchor_cell=XLSX.CellRef("L4"))
    end

    return cdf, cdf_tot
end

function analyze_natura2000(; firstyear=1978, lastyear=2021)
    df = CSV.File("D:/GISdata/windpixeldata.csv") |> DataFrame

    df.inyears = (df.turbineyear.>=firstyear) .& (df.turbineyear.<=lastyear)
    countries = ["Sweden", "Denmark", "Germany", "Texas", "Iowa", "Oklahoma", "Kansas", "Illinois",
        "South Dakota", "North Dakota", "California", "Minnesota", "Colorado", "Oregon"]
    df.countryname = countries[df.country]
    df.onshore = df.landtype .> 0
    protected_names = [
       "A: SPAs"
       "B: SCIs and SACs"
       "C: both categories A + B"
    ]
    protected_type = Dict(i => n for (i,n) in enumerate(protected_names))
    protected_type[0] = "UNPROTECTED"

    gdf = groupby(df, [:countryname, :onshore, :natura2000])
    cdf = combine(gdf,
            :area => (a -> sum(a)/1000) => :area,
            [:turbinecapac, :inyears] => ((c,y) -> sum(c.*y)/1000) => :capac
        )
    gdf_tot = groupby(cdf, [:countryname, :onshore])
    cdf_tot = combine(gdf_tot, :area => sum, :capac => sum)

    cdf.turbinedensity = cdf.capac ./ cdf.area
    cdf.protected_type = getindex.(Ref(protected_type), cdf.natura2000)
    cdf_tot.turbinedensity = cdf_tot.capac_sum ./ cdf_tot.area_sum

    sort!(cdf, [order(:countryname, by = x -> findfirst(countries.==x)), :natura2000, order(:onshore, rev=true)])
    sort!(cdf_tot, [order(:countryname, by = x -> findfirst(countries.==x)), order(:onshore, rev=true)])

    filename = "protected Natura2000.xlsx"
    XLSX.openxlsx(filename, mode=isfile(filename) ? "rw" : "w") do xf
        sheetname = "$firstyear-$lastyear"
        sheet = sheetname in XLSX.sheetnames(xf) ? xf[sheetname] : XLSX.addsheet!(xf, sheetname)
        XLSX.writetable!(sheet, [cdf[:,i] for i=1:size(cdf,2)], names(cdf), anchor_cell=XLSX.CellRef("A4"))
        XLSX.writetable!(sheet, [cdf_tot[:,i] for i=1:size(cdf_tot,2)], names(cdf_tot), anchor_cell=XLSX.CellRef("L4"))
    end
    # CSV.write("protectedinfo.csv", cdf)
    # CSV.write("protectedinfo_tot.csv", cdf_tot)

    return cdf, cdf_tot
end

function analyze_landtype(; firstyear=1978, lastyear=2021, minspeed=0)
    df = CSV.File("D:/GISdata/windpixeldata.csv") |> DataFrame

    df.inyears = (df.turbineyear.>=firstyear) .& (df.turbineyear.<=lastyear) .& (df.windspeed .>= minspeed)
    countries = ["Sweden", "Denmark", "Germany", "Texas", "Iowa", "Oklahoma", "Kansas", "Illinois",
        "South Dakota", "North Dakota", "California", "Minnesota", "Colorado", "Oregon"]
    df.countryname = countries[df.country]
    landtype_names = [
        "Water"
        "Evergreen Needleleaf Forests"
        "Evergreen Broadleaf Forests"
        "Deciduous Needleleaf Forests"
        "Deciduous Broadleaf Forests"
        "Mixed Forests"
        "Closed Shrublands"
        "Open Shrublands"
        "Woody Savannas"
        "Savannas"
        "Grasslands"
        "Permanent Wetlands"
        "Croplands"
        "Urban"
        "Cropland/Natural"
        "Snow/Ice"
        "Barren"
    ]
    landtype = Dict(i-1 => n for (i,n) in enumerate(landtype_names))

    gdf = groupby(df, [:countryname, :landtype])
    cdf = combine(gdf,
            :area => (a -> sum(a)/1000) => :area,
            [:turbinecapac, :inyears] => ((c,y) -> sum(c.*y)/1000) => :capac
        )
    gdf_tot = groupby(cdf, :countryname)
    cdf_tot = combine(gdf_tot, :area => sum, :capac => sum)
    cdf.turbinedensity = cdf.capac ./ cdf.area
    cdf.landtype_name = getindex.(Ref(landtype), cdf.landtype)
    cdf_tot.turbinedensity = cdf_tot.capac_sum ./ cdf_tot.area_sum

    sort!(cdf, [order(:countryname, by = x -> findfirst(countries.==x)), :landtype])
    sort!(cdf_tot, [order(:countryname, by = x -> findfirst(countries.==x))])

    filename = "landtypes minspeed $minspeed.xlsx"
    XLSX.openxlsx(filename, mode=isfile(filename) ? "rw" : "w") do xf
        sheetname = "$firstyear-$lastyear"
        sheet = sheetname in XLSX.sheetnames(xf) ? xf[sheetname] : XLSX.addsheet!(xf, sheetname)
        XLSX.writetable!(sheet, [cdf[:,i] for i=1:size(cdf,2)], names(cdf), anchor_cell=XLSX.CellRef("A4"))
        XLSX.writetable!(sheet, [cdf_tot[:,i] for i=1:size(cdf_tot,2)], names(cdf_tot), anchor_cell=XLSX.CellRef("L4"))
    end

    return cdf, cdf_tot
end

function grouppopulationdensity(country; firstyear=1978, lastyear=2021)
    df = CSV.File("D:/GISdata/windpixeldata.csv") |> DataFrame
    df = df[df.country .== country, :]
    df.inyears = (df.turbineyear.>=firstyear) .& (df.turbineyear.<=lastyear)
    df.nturbines = df.nturbines .* df.inyears
    df.capac = df.turbinecapac/1000 .* df.inyears
    pops = [[i*10.0^j for j = -1:3 for i in [1,2,5]]; 100_000]
    popmin = [0; pops[1:end-1]]
    df.poprange = CategoricalArrays.cut(df.popdens, pops, extend=true)
    gdf = groupby(df, :poprange)
    cdf = combine(gdf, :nturbines .=> sum, :capac .=> sum, nrow => :pixels, :area .=> sum,
        renamecols = false)
    allranges = CategoricalArrays.cut(popmin, pops, extend=true)
    df_out = similar(cdf, length(allranges))
    df_out.poprange = allranges
    df_out[:,2:end] .= 0
    insertcols!(df_out, 2, :popmin => popmin, :popmax => pops)
    indexes = [findfirst(allranges .== r) for r in cdf.poprange]
    df_out[indexes, 4:end] = cdf[:, 2:end]
    countries = ["Sweden", "Denmark", "Germany", "Texas", "Iowa", "Oklahoma", "Kansas", "Illinois",
        "South Dakota", "North Dakota", "California", "Minnesota", "Colorado", "Oregon"]

    filename = "wind_popdens 1978-2021.xlsx"
    XLSX.openxlsx(filename, mode=isfile(filename) ? "rw" : "w") do xf
        sheetname = "$(countries[country]) $firstyear-$lastyear"
        sheet = sheetname in XLSX.sheetnames(xf) ? xf[sheetname] : XLSX.addsheet!(xf, sheetname)
        XLSX.writetable!(sheet, [df_out[:,i] for i=2:size(df_out,2)], names(df_out)[2:end], anchor_cell=XLSX.CellRef("A1"))
    end
    # CSV.write(filename, df_out[:, 2:end])
end

# just for the histogram data Fredrik wanted
function groupwindspeeds(country; firstyear=1978, lastyear=2021, usemiuu=false)
    df = CSV.File("D:/GISdata/windpixeldata.csv") |> DataFrame
    df = df[df.country .== country, :]
    df.inyears = (df.turbineyear.>=firstyear) .& (df.turbineyear.<=lastyear)
    df.is_onshore = (df.landtype .> 0)
    df.is_offshore = (df.landtype .== 0)
    df.nturbines = df.nturbines .* df.inyears
    df.nturbines_on = df.nturbines .* df.is_onshore
    df.nturbines_off = df.nturbines .* df.is_offshore
    df.capac = df.turbinecapac/1000 .* df.inyears
    df.capac_on = df.capac .* df.is_onshore
    df.capac_off = df.capac .* df.is_offshore
    df.area_on = df.area .* df.is_onshore
    df.area_off = df.area .* df.is_offshore
    df.speed = usemiuu ? df.miuu : df.windspeed
    df.windspeed_range = CategoricalArrays.cut(df.speed, 0:.25:21, extend=true)
    gdf = groupby(df, :windspeed_range)
    cdf = combine(gdf, [:nturbines, :nturbines_on, :nturbines_off] .=> sum,
        [:capac, :capac_on, :capac_off] .=> sum,
        nrow => :pixels, [:is_onshore, :is_offshore] .=> sum .=> [:pixels_on, :pixels_off],
        [:area, :area_on, :area_off] .=> sum,
        renamecols = false)
    allranges = CategoricalArrays.cut(0:.25:20.75, 0:.25:21, extend=true)
    df_out = similar(cdf, length(allranges))
    df_out.windspeed_range = allranges
    df_out[:,2:end] .= 0
    indexes = [findfirst(allranges .== r) for r in cdf.windspeed_range]
    df_out[indexes, :] = cdf
    insertcols!(df_out, 2, :windspeed_low => 0:.25:20.75, :windspeed_high => .25:.25:21)

    countries = ["Sweden", "Denmark", "Germany", "Texas", "Iowa", "Oklahoma", "Kansas", "Illinois",
        "South Dakota", "North Dakota", "California", "Minnesota", "Colorado", "Oregon"]

    filename = "winddata 1978-2021.xlsx"
    XLSX.openxlsx(filename, mode=isfile(filename) ? "rw" : "w") do xf
        sheetname = "$(countries[country]) $(usemiuu ? "MIUU " : "")$firstyear-$lastyear"
        sheet = sheetname in XLSX.sheetnames(xf) ? xf[sheetname] : XLSX.addsheet!(xf, sheetname)
        XLSX.writetable!(sheet, [df_out[:,i] for i=2:size(df_out,2)], names(df_out)[2:end], anchor_cell=XLSX.CellRef("A1"))
    end
    # CSV.write(filename, df_out)
end

#=
# run in REPL with using Plots & plotly()
function plothists(region, windspeed, onshoreturbine, onshore, offshore, years)
    region = replace(region, "GADM" => "")
    region = replace(region, r"\d" => "")
    title = "$region $(years[1]) - $(years[2])"
    histopt = (legend=:none, size=(1200,550), tickfont=12, bins=2.8:.1:11.2,
                    xticks=3:1:11, normalize=true)
    barhist(windspeed[onshoreturbine], title=title, c=:lightgray; histopt...)
    stephist!(windspeed[onshore .& (windspeed.>0)], lw=1.5, c=:blue; histopt...)
    stephist!(windspeed[offshore .& (windspeed.>0)], lw=1.5, c=:red; histopt...)
end

    # groupedhist([windspeed[onshoreturbine]; windspeed[offshoreturbine]];
    #     group=[ones(sum(onshoreturbine)); 2*ones(sum(offshoreturbine))],
    #     bar_position = :stack, c=[:lightgray :red], histopt...)
=#

function exportGISturbinedata(; mincapac=0, minclass=0, firstyear=1978, plotmasks=false, optionlist...)
    cols = [:lon, :lat, :capac, :onshore, :elec2018]
    df_DK = DataFrame(CSV.File(in_datafolder("turbines_DK.csv")))[:, cols]
    df_SE = DataFrame(CSV.File(in_datafolder("turbines_SE.csv")))[:, cols[1:4]]
    df_DE = DataFrame(CSV.File(in_datafolder("turbines_DE.csv")))[:, cols[1:3]]
    df_USA = DataFrame(CSV.File(in_datafolder("turbines_USA.csv")))[:, cols[1:3]]
    df_SE[:, :elec2018] .= missing    # MWh/year
    df_DE[:, :elec2018] .= missing    # MWh/year
    df_DE[:, :onshore] .= missing    # MWh/year
    df_USA[:, :elec2018] .= missing    # MWh/year
    df_USA[:, :onshore] .= missing    # MWh/year
    turbines = vcat(df_DK, df_SE, df_DE, df_USA)
    turbines = turbines[turbines[!,:capac].>=mincapac, :]

    options = WindOptions(merge(windoptions(), optionlist))
    @unpack gisregion, downsample_masks, onshore_density, area_onshore, res = options
    regions, offshoreregions, regionlist, gridaccess, popdens, topo, land,
        protected_mat, lonrange, latrange =
                read_datasets(options)
    mask_onshoreA, mask_onshoreB, mask_offshore,
        gridA, lowpopdens, goodland, protected_area =
            return_wind_masks(options, regions, offshoreregions, gridaccess, popdens,
                            topo, land, protected_mat, lonrange, latrange,
                            plotmasks=plotmasks, downsample=downsample_masks)
    mask_onshore = mask_onshoreA .| mask_onshoreB
    lats = (90-res/2:-res:-90+res/2)[latrange]          # latitude values (pixel center)
    cellarea = rastercellarea.(lats, res)

    windatlas = getwindatlas()[lonrange,latrange]
    onshoreclass, offshoreclass = makewindclasses(options, windatlas)

    nreg = length(regionlist)
    capac, offcapac, elec, pop = zeros(nreg), zeros(nreg), zeros(nreg), zeros(nreg)
    onshore, offshore = zeros(nreg), zeros(nreg)
    n_onshore, n_offshore = zeros(Int, nreg), zeros(Int, nreg)
    landcount, commonland, freq = zeros(Int, nreg, 16), fill("", nreg, 3), zeros(nreg, 3)
    maskedturbines, nogrid, highpop, badland, protected =
        zeros(Int, nreg), zeros(Int, nreg), zeros(Int, nreg), zeros(Int, nreg), zeros(Int, nreg)
    capac_ok, area, area_ok, maxcapac, maxcapac_ok, class, windspeed =
        zeros(nreg), zeros(nreg), zeros(nreg), zeros(nreg), zeros(nreg), zeros(nreg), zeros(nreg)

    for row in eachrow(turbines)
        reg, flon, flat = getregion_and_index(row[:lon], row[:lat], regions, lonrange, latrange)
        if (reg == 0 || reg == NOREGION)
            reg, flon, flat = getregion_and_index(row[:lon], row[:lat], offshoreregions, lonrange, latrange)
            (reg == 0 || reg == NOREGION) && continue
        end
        if ismissing(row[:onshore]) || row[:onshore]
            onshoreclass[flon, flat] < minclass && continue
            capac[reg] += row[:capac] / 1000
            capac_ok[reg] += row[:capac] / 1000 * mask_onshore[flon, flat]
            class[reg] += onshoreclass[flon, flat]
            windspeed[reg] += windatlas[flon, flat]
            onshore[reg] += mask_onshore[flon, flat]
            n_onshore[reg] += 1
            if land[flon, flat] != 0
                landcount[reg, land[flon, flat]] += 1
            end
            pop[reg] += capac[reg]*popdens[flon, flat]  # capacity-weighted
            if !mask_onshore[flon, flat]
                maskedturbines[reg] += 1
                nogrid[reg] += !gridA[flon, flat]
                highpop[reg] += !lowpopdens[flon, flat]
                badland[reg] += !goodland[flon, flat]
                protected[reg] += protected_area[flon, flat]
            end
        else
            offshoreclass[flon, flat] < minclass && continue
            offcapac[reg] += row[:capac] / 1000
            offshore[reg] += mask_offshore[flon, flat]
            n_offshore[reg] += 1
        end
        elec[reg] += coalesce(row[:elec2018] / 1000, 0.0)
    end
    freg = (regions.>0) .& (regions.!=NOREGION)
    all_onshore = sum(mask_onshore[freg]) / sum(freg)
    fregoff = (offshoreregions.>0) .& (offshoreregions.!=NOREGION)
    all_offshore = sum(mask_offshore[fregoff]) / sum(fregoff)
    println("\nTotal onshore OK: ", percentstring(sum(onshore)/sum(n_onshore)),
            " ($(percentstring(all_onshore)))")
    println("Total offshore OK: ", percentstring(sum(offshore)/sum(n_offshore)),
            " ($(percentstring(all_offshore)))")
    regionalpopdens = sum(popdens[freg]) / sum(freg)
    println("Masked turbines: ", percentstring(sum(maskedturbines)/sum(n_onshore)))
    println("\tno grid: ", percentstring(sum(nogrid)/sum(maskedturbines)))
    println("\thigh pop: ", percentstring(sum(highpop)/sum(maskedturbines)))
    println("\tbad land: ", percentstring(sum(badland)/sum(maskedturbines)))
    println("\tprotected: ", percentstring(sum(protected)/sum(maskedturbines)))
    println("Total population density (persons/km2): ", round(sum(pop)/sum(n_onshore)/sum(capac), digits=1),
            " ($(round(regionalpopdens, digits=1)))")
    onshore = round.(onshore ./ n_onshore * 100, digits=1)
    class = class ./ n_onshore
    windspeed = windspeed ./ n_onshore
    offshore = round.(offshore ./ n_offshore * 100, digits=1)
    pop = round.(pop ./ n_onshore ./ capac, digits=1)
    masked = round.(maskedturbines ./ n_onshore * 100, digits=1)
    nogrid = round.(nogrid ./ maskedturbines * 100, digits=1)
    highpop = round.(highpop ./ maskedturbines * 100, digits=1)
    badland = round.(badland ./ maskedturbines * 100, digits=1)
    protected = round.(protected ./ maskedturbines * 100, digits=1)
    println("Calculating results per municipality/county...")
    updateprogress = Progress(nreg, 1)
    for reg = 1:nreg
        area[reg] += sum((regions .== reg) .* cellarea')
        maxcapac[reg] = area[reg] * onshore_density * area_onshore
        area_ok[reg] += sum(((regions .== reg) .& mask_onshore) .* cellarea')
        maxcapac_ok[reg] = area_ok[reg] * onshore_density * area_onshore
        ss = sortslices([landcount[reg, :] collect(1:16)], dims=1, rev=true)
        commonland[reg,:] = getindex.(Ref(landtypes), ss[1:3, 2])
        freq[reg,:] = round.(ss[1:3, 1]/n_onshore[reg] * 100, digits=1)
        next!(updateprogress)
    end
    println("Turbine density of unmasked land (MW/km2): ",
            round(sum(capac_ok)/sum(area_ok), digits=3),
            " ($(round(sum(capac)/sum(area), digits=3)))")
    println("Exploited share of potential capacity (%): ",
            round(sum(capac_ok)/sum(maxcapac_ok)*100, digits=1),
            " ($(round(sum(capac)/sum(maxcapac)*100, digits=1)))")
    ss = sortslices([sum(landcount, dims=1)' collect(1:16)], dims=1, rev=true)
    for i = 1:3
        println("Landtype $i: $(landtypes[ss[i,2]]) ($(percentstring(ss[i,1]/sum(n_onshore))))")
    end
    df = DataFrame(region=regionlist, capac=round.(capac, digits=2),
            offcapac=round.(offcapac, digits=2), elec2018=round.(elec, digits=2),
            onshore=replace(onshore, NaN=>missing), offshore=replace(offshore, NaN=>missing),
            masked=masked, nogrid=nogrid, highpop=highpop, badland=badland, protected=protected,
            dens_tot=round.(capac./area, digits=3), dens_ok=round.(capac_ok./area_ok, digits=3),
            exploit_tot=round.(area_onshore*capac./maxcapac*100, digits=1),
            exploit_ok=round.(area_onshore*capac_ok./maxcapac_ok*100, digits=1),
            popdens=pop, land1=commonland[:,1], freq1=freq[:,1], land2=commonland[:,2], freq2=freq[:,2],
            land3=commonland[:,3], freq3=freq[:,3], area=round.(Int, area), class=class,
            windspeed=windspeed)
    outfile = "regionalwindGIS_$(gisregion)_minturbine=$(mincapac)_maxpopdens=$(Int(options.persons_per_km2)).csv"
    CSV.write(in_datafolder("output", outfile), df)
    df
end

percentstring(x) = "$(round(100*x, digits=1))%"

function turbinecharts(gisregion)
    td, pd = GISturbines_density(gisregion=gisregion);
    nz = (pd.>1) .| (td.>0);
    scene = plot(histogram(nbins = 50), td[td.>0])
    xlabel!("turbine density (MW/km2)")
    # scene = plot(histogram(nbins = 50), log10.(pd[nz]))
    # xlabel!("pop. density (log10 persons/km2)")

    # scene = StatsMakie.scatter(td[nz], log10.(pd[nz]), markersize=0.2)
    # xlabel!("turbine density (MW/km2)")
    # ylabel!("pop. density (log10 persons/km2)")

    scene

    # shadedmap("Denmark5", log.(1 .+tdm), downsample=2, colorscheme=:magma)
    # shadedmap("Denmark5", log10.(1 .+pd), downsample=2, colorscheme=:magma)
end

function myhist(data; normalize=false, args...)
    h = fit(Histogram, data; args...)
    weights = normalize ? h.weights/sum(h.weights) : h.weights
    barplot(h.edges[1][1:end-1], weights)
end

function GISturbines_density(; agg=1, optionlist...)
    options = WindOptions(merge(windoptions(), optionlist))
    @unpack res, gisregion, scenarioyear = options

    println("\nReading regions and population dataset...")
    regions, offshoreregions, regionlist, lonrange, latrange = loadregions(gisregion)
    lats = (90-res/2:-res:-90+res/2)[latrange]          # latitude values (pixel center)
    cellarea = rastercellarea.(lats, res)

    pop = JLD.load(in_datafolder("population_$scenarioyear.jld"), "population")[lonrange,latrange]
    popdens = aggregate_array(pop, agg) ./ aggregate_array(cellarea, agg)'      # persons/km2

    df_DK = DataFrame(CSV.File(in_datafolder("turbines_DK.csv")))[:, [:lon, :lat, :capac]]
    df_USA = DataFrame(CSV.File(in_datafolder("turbines_USA.csv")))[:, [:lon, :lat, :capac]]
    df_SE = DataFrame(CSV.File(in_datafolder("turbines_SE.csv")))[:, [:lon, :lat, :capac]]
    df_UK = DataFrame(CSV.File(in_datafolder("turbines_UK.csv")))[:, [:lon, :lat, :capac]]
    turbines = vcat(df_DK, df_USA, df_SE, df_UK)

    turbinecapacity = aggregate_turbine_capacity(gisregion, turbines, regions, regionlist, lonrange, latrange)
    turbinedensity = aggregate_array(turbinecapacity, agg) ./ aggregate_array(cellarea, agg)'    # MW/km2

    # println("\nSaving...")

    # mkpath(in_datafolder("output"))
    # matopen(in_datafolder("output", "GISdata_turbines_$gisregion.mat"), "w") do file
    #     write(file, "turbinecapacity", turbinecapacity)
    # end
    return turbinedensity, popdens
end

# function aggregate_array(a::AbstractArray, n::Int)
#     sz = size(a) .÷ n
#     out = similar(a, sz)
#     R = CartesianIndices(A)
#     Ifirst, Ilast = first(R), last(R)
#     I1 = oneunit(Ifirst)
#     for I in R
#         n, s = 0, zero(eltype(out))
#         for J in max(Ifirst, I-I1):min(Ilast, I+I1)
#             s += A[J]
#             n += 1
#         end
#         out[I] = s/n
#     end
#     out
# end

function aggregate_array(a::AbstractArray, n::Int)
    n == 1 && return a
    sz = size(a)
    dims = length(sz)
    out = similar(a, size(a) .÷ n)
    R = CartesianIndices(a)[ntuple(i -> 1:n:sz[i]-n+1, dims)...]   # index to every nth element in all dims
    Rsub = CartesianIndices(ntuple(i -> 0:n-1, dims)) #
    for (i, I) in enumerate(R)
        s = zero(eltype(out))
        for J in I .+ Rsub
            s += a[J]
        end
        out[i] = s
    end
    out
end

function aggregate_turbine_capacity(gisregion, turbines, regions, regionlist, lonrange, latrange)
    println("Calculate turbine capacity per pixel...")

    numreg = length(regionlist)
    turbinecapacity = zeros(size(regions))       # MW

    for i = 1:size(turbines,1)
        reg, flon, flat = getregion_and_index(turbines[i,:lon], turbines[i,:lat], regions, lonrange, latrange)
        (reg == 0 || reg == NOREGION) && continue

        turbinecapacity[flon, flat] += turbines[i,:capac]/1000
    end

    return turbinecapacity
end

function getregion_and_index(lon, lat, regions, lonrange=1:36000, latrange=1:18000)
    res = 0.01
    res2 = res/2
    lons = (-180+res2:res:180-res2)[lonrange]         # longitude values (pixel center)
    lats = (90-res2:-res:-90+res2)[latrange]          # latitude values (pixel center)
    flon = findfirst(round(lon-res2, digits=5) .< lons .<= round(lon+res2, digits=5))
    flat = findfirst(round(lat-res2, digits=5) .< lats .<= round(lat+res2, digits=5))
    (flon == nothing || flat == nothing) && return 0, flon, flat
    # println(lons[flon],", ",lats[flat])
    # println(flon, " ", flat)
    if regions[flon, flat] > 0
        return regions[flon, flat], flon, flat
    else
        return NOREGION, flon, flat
    end
end

function shadedmap(gisregion, plotdata, regionlist, lons, lats, colors, source, dest, xs, ys;
                    resolutionscale=1)
    nreg = length(regionlist)
    scale = maximum(size(plotdata))/6500

    # data[regions.==NOREGION] .= nreg + 1

    xmin, xmax = extrema(xs)
    ymin, ymax = extrema(ys)
    aspect_ratio = (ymax - ymin) / (xmax - xmin)
    pngwidth = round(Int, resolutionscale*1.02*size(plotdata,1))     # allow for margins (+1% on both sides)
    pngsize = pngwidth, round(Int, pngwidth * aspect_ratio)     # use aspect ratio after projection transformation

    println("...constructing map...")
    scene = surface(xs, ys; color=float.(plotdata), colormap=colors,
                            shading=false, show_axis=false, scale_plot=false, interpolate=false)
    ga = geoaxis!(scene, lons[1], lons[end], lats[end], lats[1]; crs=(src=source, dest=dest,))[end]
    ga.x.tick.color = RGBA(colorant"black", 0.4)
    ga.y.tick.color = RGBA(colorant"black", 0.4)
    ga.x.tick.width = scale
    ga.y.tick.width = scale

    println("...saving...")
    mkpath(in_datafolder("output"))
    filename = in_datafolder("output", "$(gisregion)_shadedmap.png")
    isfile(filename) && rm(filename)
    Makie.save(filename, scene, resolution=pngsize)
end

function shadedmap(gisregion, plotdata; resolutionscale=1, downsample=1, colorscheme=:viridis)
    regions, offshoreregions, regionlist, lonrange, latrange = loadregions(gisregion)
    plotdata = plotdata[1:downsample:end, 1:downsample:end]
    lonrange = lonrange[1:downsample:end]
    latrange = latrange[1:downsample:end]
    nreg = length(regionlist)

    colors = colorschemes[colorscheme].colors  #[RGB(0.2,0.3,0.4); colorschemes[:viridis].colors; RGB(0.4,0.4,0.4)]

    println("\nProjecting coordinates (Mollweide)...")
    res = 0.01
    res2 = res/2
    lons = (-180+res2:res:180-res2)[lonrange]         # longitude values (pixel center)
    lats = (90-res2:-res:-90+res2)[latrange]          # latitude values (pixel center)
    source = Projection("+proj=longlat +datum=WGS84")
    dest = Projection("+proj=moll +lon_0=$(mean(lons))")
    xs, ys = xygrid(lons, lats)
    Proj4.transform!(source, dest, vec(xs), vec(ys))

    println("\nOnshore map...")
    shadedmap(gisregion, plotdata, regionlist, lons, lats, colors, source, dest, xs, ys,
        resolutionscale=resolutionscale)
    # exit()
    return nothing
end

function return_wind_masks(options, regions, offshoreregions, gridaccess, popdens, topo, land, protected, lonrange, latrange; plotmasks=false, downsample=1)
    @unpack res, gisregion, exclude_landtypes, protected_codes, distance_elec_access, persons_per_km2,
                min_shore_distance, max_depth, classB_threshold = options

    println("Creating masks...")

    goodland = (regions .> 0)
    for i in exclude_landtypes
        goodland[land .== i] .= false
    end
    protected_area = zeros(Bool, size(protected))
    for i in protected_codes
        protected_area[protected .== i] .= true
    end

    # Pixels with electricity access for onshore wind A
    gridA = (gridaccess .> 0)

    # Pixels with electricity access for onshore wind B and offshore wind
    km_per_degree = π*2*6371/360
    disk = diskfilterkernel(distance_elec_access/km_per_degree/res)
    gridB = (imfilter(gridaccess, disk) .> max(1e-9, classB_threshold)) # avoid artifacts if classB_threshold == 0

    # println("MAKE SURE MASKS DON'T OVERLAP! (regions & offshoreregions, mask_*)")

    # all mask conditions
    mask_onshoreA = gridA .& (popdens .< persons_per_km2) .& goodland .& .!protected_area
    mask_onshoreB = (gridB .& .!gridA) .& (popdens .< persons_per_km2) .& goodland .& .!protected_area

    # shoreline mask for offshore wind
    disk = diskfilterkernel(min_shore_distance/km_per_degree/res)
    shore = (imfilter(regions .> 0, disk) .> 1e-6)

    # all mask conditions
    mask_offshore = gridB .& .!shore .& (topo .> -max_depth) .& (offshoreregions .> 0) .& .!protected_area

    if plotmasks != false   # can == :onlymasks as well
        # drawmap(land)
        isregion = (regions .> 0) .& (regions .!= NOREGION)

        # mask values refer to colors in ColorBrewer Set2_7:
        # https://juliagraphics.github.io/ColorSchemes.jl/stable/basics/#colorbrewer-1
        masks = zeros(Int16, size(regions))
        masks[(masks .== 0) .& (popdens .> persons_per_km2)] .= 2
        masks[(masks .== 0) .& protected_area] .= 3
        masks[(masks .== 0) .& .!gridA .& .!gridB] .= 4
        masks[(masks .== 0) .& .!goodland] .= 1
        masks[(masks .== 0) .& .!gridA .& gridB] .= 8
        masks[(masks .== 0) .& isregion] .= 7
        masks[regions .== 0] .= 0
        masks[regions .== NOREGION] .= NOREGION
        legendtext = ["bad land type", "high population", "protected area", "no grid", "", "", "wind plant A", "wind plant B"]
        maskmap("$(gisregion)_masks_wind", masks, legendtext, lonrange, latrange; legend=true, downsample=downsample)

        # drawmap(.!shore .& (offshoreregions .> 0))
        # drawmap((topo .> -max_depth) .& (offshoreregions .> 0))
        # drawmap(.!protected_area .& (offshoreregions .> 0))
        # drawmap(mask_offshore)
    end

    return mask_onshoreA, mask_onshoreB, mask_offshore,
            gridA, (popdens .< persons_per_km2), goodland, protected_area
end

function make_turbineCSV_for_postdocs(; mincapac=0, minclass=0, firstyear=1978, plotmasks=false, optionlist...)
    cols = [:lon, :lat, :capac, :year, :onshore, :elec2018]
    turbines = DataFrame(CSV.File(in_datafolder("turbines_DK.csv")))[:, cols]
    turbines = turbines[turbines[:capac].>=mincapac, :]
    nrows = size(turbines, 1)

    options = WindOptions(merge(windoptions(), optionlist))
    @unpack gisregion, downsample_masks, onshore_density, area_onshore, res = options
    regions, offshoreregions, regionlist, gridaccess, popdens, topo, land,
        protected_mat, lonrange, latrange =
                read_datasets(options)
    mask_onshoreA, mask_onshoreB, mask_offshore,
        gridA, lowpopdens, goodland, protected_area =
            return_wind_masks(options, regions, offshoreregions, gridaccess, popdens,
                            topo, land, protected_mat, lonrange, latrange,
                            plotmasks=plotmasks, downsample=downsample_masks)
    mask_onshore = mask_onshoreA .| mask_onshoreB
    lats = (90-res/2:-res:-90+res/2)[latrange]          # latitude values (pixel center)
    cellarea = rastercellarea.(lats, res)

    windatlas = getwindatlas()[lonrange,latrange]

    nreg = length(regionlist)

    pop, area, area_ok, maxcapac, maxcapac_ok =
        zeros(nreg), zeros(nreg), zeros(nreg), zeros(nreg), zeros(nreg)

    turbines[!, :municipality] = fill("", nrows)
    turbines[!, :avg_windspeed] = fill(0.0, nrows)
    turbines[!, :landtype] = fill("", nrows)
    turbines[!, :popdensity] = fill(0.0, nrows)
    turbines[!, :masks_ok] = fill(false, nrows)

    for row in eachrow(turbines)
        reg, flon, flat = getregion_and_index(row[:lon], row[:lat], regions, lonrange, latrange)
        if (reg == 0 || reg == NOREGION)
            reg, flon, flat = getregion_and_index(row[:lon], row[:lat], offshoreregions, lonrange, latrange)
            (reg == 0 || reg == NOREGION) && continue
        end
        row[:municipality] = string(regionlist[reg])
        row[:avg_windspeed] = windatlas[flon,flat]
        row[:landtype] = land[flon,flat] > 0 ? landtypes[land[flon,flat]] : "Water"
        row[:popdensity] = popdens[flon, flat]
        row[:masks_ok] = row[:onshore] ? mask_onshore[flon, flat] : mask_offshore[flon, flat]
    end

    for reg = 1:nreg
        area[reg] += sum((regions .== reg) .* cellarea')
        area_ok[reg] += sum(((regions .== reg) .& mask_onshore) .* cellarea')
        pop[reg] = sum((regions .== reg) .* popdens) / area[reg]
    end

    munic = DataFrame(region=regionlist, avg_popdens=pop, area=area, area_masks_ok=area_ok)
    CSV.write("turbines_DK.csv", turbines)
    CSV.write("municipalities_DK.csv", munic)
    turbines, munic
end


using CSV
using DataFrames
using DelimitedFiles
using Printf
using Statistics

using SLiDE

"""
    make_uniform(df::DataFrame, cols::Array{Symbol,1})
This function is specific to the verify_datastream.jl file. It makes input DataFrames
uniform (same columns; should add support to standardize types)
"""
function make_uniform(df::DataFrame, cols::Array{Symbol,1})
    if size(propertynames(df)) == size(cols) && (length(setdiff(cols, propertynames(df))) > 0)
        df = edit_with(df, Rename.(propertynames(df), cols))[:,cols]
    end
    df = df[:,cols]

    # !!!! Assumptions about value column name and type: (1) values are stored in a column
    # named :value and (2) values are type FLOAT64. This is a common theme throughout SLiDE;
    # may need to address.
    df[!,:value] .= convert_type.(Float64, df[:,:value])

    df = edit_with(df, Drop(:value, 0.0, "=="));
    return unique(sort(df, propertynames(df)[1:end-1]));
end

# ******************************************************************************************

# PATHS TO DATA LOCATIONS:
# - path_side is general: the contents of this directory should have been created by
#     auto_standardize_data.
# - path_bluenote is specific to where Caroline stored WiNDC windc_datastream output.
#     This line must be changed to be consistent with user-specific file storage organizations.
path_slide = joinpath("data", "input")
path_bluenote = joinpath("data","windc_output","1b_stream_windc_base")

y = read_file(joinpath("dev", "verify_datastream.yml"));
lst = y["FileInput"];

df_attn = Dict()

# Iterate through list of datafiles to compare and shared column names.
for inp in lst[1:end-1]
    println("\n",uppercase(inp.f1));

    # For SLiDE and bluenote data sets, read and save the DataFrames.
    dfs = read_file(joinpath(path_slide, inp.f1));
    global dfs = make_uniform(dfs, inp.colnames);

    dfb = read_file(joinpath(path_bluenote, inp.f2));
    global dfb = make_uniform(dfb, inp.colnames);

    # If comparing SEDS data, compare unstandardized units.
    # if occursin("seds", inp.f1)
    #     x = [Rename.([:units_0, :value_0], [:units, :value]);
    #          Replace.([:source_code, :sector_code], "lower", "upper");
    #          Map("parse/units.csv", [:from], [:to], [:units], [:units])];
    #     global dfs = edit_with(dfs, x)
    #     global dfb = edit_with(dfb, x)
    # end

    global df_keys = compare_keys(copy.([dfs, dfb]), [:slide, :bluenote]);
    # size(df_keys,1) == 0 ? println("All keys are consistent.") : show(df_keys);
    println("");

    # Resolve MINOR discrepancy to compare values for heatrate calculations.
    x = Replace(:units, "btu per kWh generated", "btu per kilowatthour");
    global dfs = edit_with(dfs, x);
    global dfb = edit_with(dfb, x);

    # Compare values. Print a warning if there are discrepancies and
    # save the summary DataFrame in a list of DataFrames in need of attention.
    global df = compare_values(copy.([dfs, dfb]), [:slide, :bluenote]);
    size(df,1) > 0 && (df_attn[inp.f1[1:end-4]] = df)
end

# ******************************************************************************************
# cols = [:year, :state, :sgf_code, :units, :value];
# dfs = read_file(joinpath(path_slide, "sgf.csv"));
# dfs[!,:windc_code] .= ismissing.(dfs[:,:windc_code])
# dfs = unique(sort(dropmissing(dfs, :sgf_code), cols));
# display(first(dfs,4));

# dfb = read_file(joinpath(path_bluenote, "sgf_units.csv"));
# dfb = make_uniform(edit_with(dfb, Replace(:sgf_code, "othtax", "OTHTAX")), cols);
# display(first(dfb,4));


@testset "ASH Model" begin
    @testset "1-var" begin
        @test @timev !isnothing(one_var_test((n_vars, dist) -> ASHModel()))
    end
    @testset "2-var" begin
        @test_broken @timev !isnothing(multi_var_test((n_vars, dist) -> ASHModel()))
    end
    @testset "3-var" begin
        @test_broken @timev !isnothing(multi_var_test((n_vars, dist) -> ASHModel(), n_vars=3))
    end
end


"Values for units for strings found in option line."
const units = Dict{ String, Float64 }(
  "HZ"    =>      1e0,
  "KHZ"   =>      1e3,
  "MHZ"   =>      1e6,
  "GHZ"   =>      1e9
)
"The default unit, if not found in option line."
const default_units = "GHZ"

"Symbols for different parameter formats for strings found in option line"
const parameters = Dict{ String, Symbol }(
  "S" => :ScatteringParameters,
  "Y" => :AdmittanceParameters,
  "Z" => :ImpedanceParameters,
  "H" => :HybridHParameters,
  "G" => :HybridGParameters,
)
"The default parameter format, if not found in option line."
const default_parameter = "S"

"Symbols for data format for strings found in option line."
const formats = Dict{ String, Symbol }(
  "DB" => :DecibelAngle,
  "MA" => :MagnitudeAngle,
  "RI" => :RealImaginary
)
"The default data format, if not found in option line."
const default_format = "MA"
"The default characteristic impedance, if not found in option line."
const default_resistance = 50

"""
    is_commentline( line )

Checks, if a line is a comment line.
"""
is_comment_line( line ) = length( line ) > 0 && line[ 1 ] == '!'
"""
    parse_comment_line( line )

Returns the comment of a comment line.
"""
parse_comment_line( line ) = line[ 2:end ]

"""
    is_option_line( line )

Checks, if a line is a option line.
"""
is_option_line( line ) = length( line ) > 0 && line[ 1 ] == '#'

"""
    is_frequency_unit_option( option )

Checks, if a string is a valid unit in an option line.
"""
is_frequency_unit_option( option ) = haskey( units,  uppercase( option ) )

"""
    parse_frequency_unit_option( option )

Returns the multiplier for a unit string in an option line.
"""
parse_frequency_unit_option( option ) = units[ uppercase( option ) ]

"""
    is_parameter_option( option )

Checks, if a string is a valid parameter format in an option line.
"""
is_parameter_option( option ) = haskey( parameters, uppercase( option ) )

"""
    parse_parameter_option( option )

Returns the symbol for a valid parameter format string in an option line.
"""
parse_parameter_option( option ) = parameters[ uppercase( option ) ]

"""
    is_format_option( option )

Checks, if a string is a valid data format in an option line.
"""
is_format_option( option ) = haskey( formats, uppercase( option ) )

"""
    parse_format_option( option )

Returns the symbol for a valid data format string in an option line.
"""
parse_format_option( option ) = formats[ uppercase( option ) ]

"""
    is_resistance_option( option )

Checks, if a string is a valid resistance in an option line.
"""
is_resistance_option( option ) = uppercase( option ) == "R"

"""
    parse_resistance_option( option )

Returns the value of the characteristic impedance for a valid resistance string in an option line.
"""
parse_resistance_option( option ) = parse( Float64, option )

"""
    is_empy_line( line )

Checks, if a line is empty.
"""
is_empy_line( line ) = length( line ) == 0

"""
    parse_option_line( line )

Returns the option structure for a valid option line.
"""
function parse_option_line( line )
  line = uppercase( line )

  default = Options()
  unit        = default.unit
  parameter   = default.parameter
  format      = default.format
  resistance  = default.resistance

  entries = split( line[ 2:end ] )

  next_is_resistance = false
  for entry in entries
    if next_is_resistance
      resistance = parse_resistance_option( entry )
      next_is_resistance = false
    else
      if is_frequency_unit_option( entry )
        unit = parse_frequency_unit_option( entry )
      elseif is_parameter_option( entry )
        parameter = parse_parameter_option( entry )
      elseif is_format_option( entry )
        format = parse_format_option( entry )
      else
        next_is_resistance =  is_resistance_option( entry )
      end
    end
  end
  Options( unit, parameter, format, resistance )
end

"""
    ma2comp( m, a )

Converts a pair of values in magnitude / angle format to a complex number.
"""
ma2comp( m, a ) =  m * cis( deg2rad( a ) )

dB2Mag( dB ) = 10 ^ ( dB / 20 )

"""
    da2comp( m, a )

Converts a pair of values in dB / angle format to a complex number.
"""
da2comp( d, a ) = ma2comp( dB2Mag( d ), a )

"Holds conversion functions for parameter format symbols when parsing."
const ParseConversions = Dict{ Symbol, Function }(
  :RealImaginary  => complex,
  :MagnitudeAngle => ma2comp,
  :DecibelAngle   => da2comp,
)

"""
    parse_data( line, N, [ options ] )

Returns a data point structure for a valid line of a one-port Touchstone file.

For 3 or more ports, the line should be the concatenation of 3 or more lines from the Touchstone file.
"""
function parse_data( line::String, N, options = Options(), version = 1 )
  vals = map( s -> parse( Float64, s ), split( line ) )
  parse_data( vals, N, options, version )
end

"""
    parse_data( vals, N, [ options ] )

Returns a data point structure for a vector of numbers from a one-port Touchstone file.

For 3 or more ports, the array should contain exactly 2N² + 1 values.
"""
function parse_data( vals::Array{Float64}, N, options = Options(), version = 1, twoPortDataFlipped = true )
  if length( vals ) != 2N * N + 1
    error( "Wrong number of values!" )
  end
  freq = vals[ 1 ]  * options.unit
  pairs = zip( vals[ 2:2:end - 1 ], vals[ 3:2:end ] )
  converted = map( pair -> ParseConversions[ options.format ]( pair... ), pairs )
  # renormalize Z, Y, G and H parameters for version 1.0
  if version == 1
    if options.parameter == :ImpedanceParameters
      converted = map( x -> x * options.resistance, converted )
    end
    if options.parameter == :AdmittanceParameters
      converted = map( x -> x / options.resistance, converted )
    end
    if N == 2
      if options.parameter == :HybridGParameters
        converted[ 1 ] /= options.resistance
        converted[ 4 ] *= options.resistance
      end
      if options.parameter == :HybridHParameters
        converted[ 1 ] *= options.resistance
        converted[ 4 ] /= options.resistance
      end
    end
  end
  res = reshape( converted, N, N )
  if N > 2 || ( N == 2 && !twoPortDataFlipped )
    res = permutedims( res, [ 2, 1 ] )
  end
  return DataPoint( freq, res )
end

function parseNoiseData( vals::Array{Float64}, options = Options(), version = 1 )
  if length( vals ) != 5
    error( "Wrong number of noise data values!" )
  end
  frequency = vals[ 1 ]  * options.unit
  minNoiseFigure = dB2Mag( vals[ 2 ] )
  reflCoeff = ma2comp( vals[ 3 ], vals[ 4 ] )
  effNoiseRes = vals[ 5 ]
  if version == 1
    effNoiseRes *= options.resistance
  end
  return NoiseDataPoint( frequency, minNoiseFigure, reflCoeff, effNoiseRes )
end

function stripCommentFromLine( line )
  comment = ""
  strings = split( line, "!" )
  if length( strings ) > 1
    comment = join( strings[ 2:end ], "!" )
    line = strings[ 1 ]
  end
  return( line, comment)
end

function getValsAndPushComments!( comments, line )
  line, comment = stripCommentFromLine( line )
  if comment != ""
    push!( comments, comment )
  end
  vals = map( s -> parse( Float64, s ), split( line ) )
  return vals
end

"""
    parse_touchstone_stream( stream, [ ports ] )

Returns a TouchstoneData structure for a valid Touchstone data stream.
"""
function parse_touchstone_stream( stream::IO, ports::Integer = 1 )
  version = 1
  options = Options()
  option_line_found = false
  comments = Vector{ String }()
  data = Vector{ DataPoint }()
  noiseData = Vector{ NoiseDataPoint }()
  neededValues = 1 + 2ports * ports
  vals = Vector{ Float64 }()
  keywordparams = Dict{ Symbol, Any }()
  networkdata = false
  noiseDataExpected = false
  twoPortDataFlipped = true
  referenceNextLine = false
  endfound = false
  lastParamFrequency = -1
  for line in eachline( stream )
    if referenceNextLine
      keywordparams[ :Reference ] = parse_reference( split( line ) )
      referenceNextLine = false
    elseif is_empy_line( line )
      ;# nothing
    elseif is_keyword_line( line )
      version = 2
      keyword, params = parse_keyword_line( line )
      keywordparams[ keyword ] = params
      if option_line_found && keyword == :Version
        error( "V2.0: [Version] 2.0 before option line expected." )
      end
      if keyword in unOrderedKeywordSymbols
        keywordString = keywordSymbolStrings[ keyword ]
        if !haskey( keywordparams, :Version ) || keywordparams[ :Version ] != 2
          error( "V2.0: [Version] keyword before [$( keywordString )] keyword expected." )
        end
        if !option_line_found
          error( "V2.0: Option line before [$( keywordString )] keyword expected." )
        end
        if !haskey( keywordparams, :NumberOfPorts )
          error( "V2.0: [Number of Ports] keyword before [$( keywordString )] keyword expected." )
        end
        if haskey( keywordparams, :NetworkData )
          error( "V2.0: [Network Data] keyword after [$( keywordString )] keyword expected." )
        end
      end
      if keyword == :Reference
        if length( keywordparams[ :Reference ] ) == 0
          referenceNextLine = true
        end
      elseif keyword == :NetworkData
        networkdata = true
        if haskey( keywordparams, :NoiseData )
          error( "V2.0: [Noise Data] keyword after [Network Data] keyword expected." )
        end
        if !haskey( keywordparams, :NumberOfFrequencies )
          error( "V2.0: [Number of Frequencies] keyword before [Network Data] keyword expected." )
        end
      elseif keyword == :NumberOfPorts
        ports = keywordparams[ :NumberOfPorts ]
        neededValues = 1 + 2ports * ports
      elseif keyword == :TwoPortDataOrder
        dataOrder = keywordparams[ :TwoPortDataOrder ]
        if dataOrder == "12_21"
          twoPortDataFlipped = true
        elseif dataOrder == "21_12"
          twoPortDataFlipped = false
        end
      elseif keyword == :NoiseData
        noiseDataExpected   = true
        networkdata = false
      elseif keyword == :End
        networkdata = false
        noiseDataExpected   = false
        endfound = true
      end
    elseif is_comment_line( line )
      push!( comments, parse_comment_line( line ) )
    elseif !option_line_found && is_option_line( line )
      if haskey( keywordparams, :Version ) && keywordparams[ :Version ] == 2 && haskey( keywordparams, :NumberOfPorts )
        error( "V2.0: Option line before [Number of Ports] keyword expected." )
      end
      options = parse_option_line( line )
      option_line_found = true
    elseif noiseDataExpected
      vals = getValsAndPushComments!( comments, line )
      push!( noiseData, parseNoiseData( vals, options, version ) )
    elseif !haskey( keywordparams, :Version ) || ( keywordparams[ :Version ] == 2 && networkdata )
      newVals = getValsAndPushComments!( comments, line )
      if version == 1
        # V1.0 Noise Data starts, when frequency decreases.
        if length( vals ) == 0
          if newVals[ 1 ] <= lastParamFrequency
            push!( noiseData, parseNoiseData( newVals, options, 1 ) )
            vals = Vector{Float64}()
            noiseDataExpected = true
          end
          lastParamFrequency = newVals[ 1 ]
        end
        # V1.0: not more then 4 value pairs per line. (#7)
        # The first  line contains the frequency.
        maxVals = length( vals ) == 0 ? 9 : 8
        if length( newVals ) > maxVals
          error( "V1.0: more than four complex values in one parameter line." )
        end
      end
      vals = [ vals; newVals ]
      if length( vals ) >= neededValues
        push!( data, parse_data( vals, ports, options, version, twoPortDataFlipped  ) )
        vals = Vector{Float64}()
      end
    elseif endfound
      error( "V2.0: Non empty or comment line found after [End] keyword." )
    end
  end
  ret = TouchstoneData( data, noiseData, options, comments, keywordparams )
  return ret
end

"""
    parse_touchstone_string( in, [ ports ] )

Returns a TouchstoneData structure for a valid Touchstone data string.
"""
parse_touchstone_string( in::String, ports::Integer = 1 ) = parse_touchstone_stream( IOBuffer( in ), ports )

"""
    parse_touchstone_file( filename, [ ports ] )

Returns a TouchstoneData structure for a valid Touchstone data file.

When called without ports, tries to interpret the file extension, such as ".s2p", or ".S4P".
"""
function parse_touchstone_file( filename::String, ports::Integer = 0 )
  if ports == 0
    extorig =  splitext( filename )[ 2 ][ 2: end ]
    ext = uppercase( extorig )
    if ext[ 1 ] == 'S' && ext[ end ] == 'P'
      ports = parse( Int64, ext[ 2:end - 1 ] )
    else
      error( "File extension ( $( extorig ) ) not recognized, unknown number of ports." )
    end
  end
  open( filename ) do io
    parse_touchstone_stream( open( filename ), ports )
  end
end

# V2.0
"Symbols for different keywords"
const keywordStringSymbols = Dict{ String, Symbol }(
  "VERSION"         => :Version,
  "NUMBER OF PORTS" => :NumberOfPorts,
  "TWO-PORT DATA ORDER"         => :TwoPortDataOrder,
  "NUMBER OF FREQUENCIES"       => :NumberOfFrequencies,
  "NUMBER OF NOISE FREQUENCIES" => :NumberOfNoiseFrequencies,
  "REFERENCE"         => :Reference,
  "MATRIX FORMAT"     => :MatrixFormat,
  "MIXED-MODE ORDER"  => :MixedModeOrder,
  "BEGIN INFORMATION" => :BeginInformation,
  "END INFORMATION"   => :EndInformation,
  "NETWORK DATA"      => :NetworkData,
  "NOISE DATA"        => :NoiseData,
  "END"   => :End,
)

const keywordSymbolStrings = Dict{ Symbol, String }(
  :Version        => "Version",
  :NumberOfPorts  => "Number of Ports",
  :TwoPortDataOrder         => "Two-Port Data Order",
  :NumberOfFrequencies      => "Number of Frequencies",
  :NumberOfNoiseFrequencies => "Number of Noise Frequencies",
  :Reference        => "Reference",
  :MatrixFormat     => "Matrix Format",
  :MixedModeOrder   => "Mixed-Mode Order",
  :BeginInformation => "Begin Information",
  :EndInformation   => "End Information",
  :NetworkData      => "Network Data",
  :NoiseData  => "NoiseData",
  :End        => "End",
)

const unOrderedKeywordSymbols = [
  :TwoPortDataOrder,
  :NumberOfFrequencies,
  :NumberOfNoiseFrequencies,
  :Reference,
  :MatrixFormat,
  :MixedModeOrder,
  :BeginInformation,
  :EndInformation,
]

"""
    is_keyword_line( line )

Checks, if a line is a valid keyword line.
"""
is_keyword_line( line::String ) = length( line ) > 0 && line[ 1 ] == '['

function parse_N_params( params, N::Integer, tp::DataType = String )
  if length( params ) != N
    error( "Exactly $( N ) parameter expected." )
  else
    if tp != String
      params = map( x -> parse( tp, x ), params )
    end
    if N == 1
      params = params[ 1 ]
    end
    return params
  end
end

function parse_N_params( params, tp::DataType )
  N = length( params )
  if tp != String
    params = map( x -> parse( tp, x ), params )
  end
end

function parse_version_params( params )
  versionstring = parse_N_params( params, 1 )
  if versionstring == "2.0"
    return 2
  else
    error( "Unknown version $( versionstring )" )
  end
end

parse_number_of_ports( params ) = parse_N_params( params, 1, Int64 )
parse_two_port_data_order( params )  = parse_N_params( params, 1 )
parse_number_of_frequencies( params )       = parse_N_params( params, 1, Int64 )
parse_number_of_noise_frequencies( params ) = parse_N_params( params, 1, Int64 )
parse_reference( params )         = parse_N_params( params, Float64 )
parse_matrix_format( params )     = []
parse_mixed_mode_order( params )  = []
parse_begin_information( params ) = []
parse_end_information( params )   = []
parse_network_data( params )  = []
parse_noise_data( params )  = []
parse_end( params )  = []

const version_param_parsers = Dict{ Symbol, Function }(
  :Version        => parse_version_params,
  :NumberOfPorts  => parse_number_of_ports,
  :TwoPortDataOrder          => parse_two_port_data_order,
  :NumberOfFrequencies       => parse_number_of_frequencies,
  :NumberOfNoiseFrequencies  => parse_number_of_noise_frequencies,
  :Reference      => parse_reference,
  :MatrixFormat   => parse_matrix_format,
  :MixedModeOrder => parse_mixed_mode_order,
  :BeginInformation => parse_begin_information,
  :EndInformation   => parse_end_information,
  :NetworkData      => parse_network_data,
  :NoiseData  => parse_noise_data,
  :End        => parse_end,
)

"""
    parse_keyword_line( line )

Returns the keyword parameters for a keyword line.
"""
function parse_keyword_line( line )
  keyword, paramstring = split( line[ 2:end ], "]" )
  keyword = uppercase( keyword )
  params = split( paramstring )
  if haskey( keywordStringSymbols, keyword )
    kwSymbol = keywordStringSymbols[ keyword ]
    params = version_param_parsers[ kwSymbol ]( params )
  else
    error( "Not a valid keyword." )
  end
  return kwSymbol, params
end


module SCoBASolver

using StaticArrays
using Parameters
using DataStructures

# Types etc
export
    MODE,
    START,
    FINISH,
    SUCCESS,
    InteractionEvent,
    SearchTree,
    GenericAllocation,
    MRTAEnvironment,
    DecisionNode,
    OutcomeNode,
    TaskUtil

# Solver stuff
export
    generate_search_tree!,
    get_next_attempt_idx,
    SCoBAAlgorithm,
    coordinate_allocation!

include("types.jl")
include("scoba_tree_search.jl")
include("scoba_conflict_resolution.jl")

end


const eidx = 30

```
    ui_advanced(img::AstroImage, indx::Int = 1)

Opens an advanced GUI panel to visualize AstroImages.
```
function ui_advanced(img::AstroImage, indx::Int = 1)              # 3 X eidx grid
    win = Window("Image", 700,700, border_width = 5)
    g = Grid()
    set_gtk_property!(g, :column_homogeneous, true)
    set_gtk_property!(g, :column_spacing, 15)
    push!(win, g)
    img_labels = Array{Tuple{Tuple{Real,Real},String},1}(undef,0)

    # Sets toolbar widget
    toolbar = Toolbar()
    headerbtn = get_button("Header")
    closebtn = get_button("Close")
    labelbtn = get_button("Show Labels")
    savebtn = get_button("Save")
    resetbtn = get_button("Reset")
    map(w->push!(toolbar,w),[headerbtn, labelbtn, savebtn,resetbtn, closebtn])
    g[1:3,1] = toolbar

    # Create canvas to display image
    c = canvas(UserUnit)
    g[1:2,2:eidx-3] = c
    set_gtk_property!(c, :expand, true)

    if(length(img) > 1)
        g[3,6] = get_label("AstroImage index-> 1:"* string.(length(img)))
        g[3,7] = GtkEntry()
        g[3,8] = GtkButton("Change Index")
    end

    # Image Label widgets
    g[3,9] = get_label("Pixel Coordinate X")
    g[3,10] = GtkEntry()
    g[3,11]= get_label("Pixel Coordinate Y")
    g[3,12]= GtkEntry()
    g[3,13]= get_label("Label")
    g[3,14]= GtkEntry()
    g[3,15]= GtkButton("Add Label")

    # Sets labels showing informations
    g[1,eidx-2] = get_label("Physical Coordinate X =")
    g[2,eidx-2] = get_label("Physical Coordinate Y =")
    g[1,eidx-1] = get_label("World Coordinate X =")
    g[2,eidx-1] = get_label("World Coordinate Y =")
    g[1:2,eidx] = get_label("Value = ")
    g[3,eidx] = get_label("Tip: Ctrl+Scroll to zoom")

    fv,zr,surf = form_image(img, indx)

    # interactive signals
    init_zoom_scroll(c,zr)                     # Makes canvas scrollable
    close_window(closebtn, win)                # Handles close button
    draw_canvas(c,zr,surf)                     # Draws canvas on window

    # Mapping mouse motion
    map(c.mouse.motion) do btn
        xu, yu = btn.position.x.val, btn.position.y.val
        world_coord = pix_to_world(img.wcs[indx], [xu, yu])
        GAccessor.text(g[1,eidx-2] ,"Pixel Coordinate X = $(round(xu, digits = 2))")
        GAccessor.text(g[2,eidx-2] ,"Pixel Coordinate Y = $(round(yu, digits = 2))")
        GAccessor.text(g[1,eidx-1] ,"World Coordinate X = $(round(world_coord[1], digits = 2))"*"°")
        GAccessor.text(g[2,eidx-1] ,"World Coordinate Y = $(round(world_coord[2], digits = 2))"*"°")
        if xu >= 1 && yu >= 1
            GAccessor.text(g[1,eidx] ,"Value = $(img.data[indx][Int(round(xu)),Int(round(yu))])")
        else
            GAccessor.text(g[1,eidx] , "Value = ")
        end
    end

    # Handles label adding
    signal_connect(g[3,15], :clicked) do btn
        x = parse(Float64, get_gtk_property(g[3,10], :text, String))
        y = parse(Float64, get_gtk_property(g[3,12], :text, String))
        label = get_gtk_property(g[3,14], :text, String)
        push!(img_labels, ((x,y), label))
        set_gtk_property!(g[3,10], :text, "")
        set_gtk_property!(g[3,12], :text, "")
        set_gtk_property!(g[3,14], :text, "")
    end

    # Handles index changing
    if(length(img) > 1)
        signal_connect(g[3,8], :clicked) do btn
            tmp = parse(Int, get_gtk_property(g[3,7], :text, String))
            if tmp <= length(img)
                indx = tmp
                set_gtk_property!(g[3,7], :text, "")
                fv,zr,surf = form_image(img, indx)
                draw_canvas(c,zr,surf)
                map(c.mouse.motion) do btn
                    xu, yu = btn.position.x.val, btn.position.y.val
                    world_coord = pix_to_world(img.wcs[indx], [xu, yu])
                    GAccessor.text(g[1,eidx-2] ,"Pixel Coordinate X = $(round(xu, digits = 2))")
                    GAccessor.text(g[2,eidx-2] ,"Pixel Coordinate Y = $(round(yu, digits = 2))")
                    GAccessor.text(g[1,eidx-1] ,"World Coordinate X = $(round(world_coord[1], digits = 2))"*"°")
                    GAccessor.text(g[2,eidx-1] ,"World Coordinate Y = $(round(world_coord[2], digits = 2))"*"°")
                    if xu >= 1 && yu >= 1
                        GAccessor.text(g[1,eidx] ,"Value = $(img.data[indx][Int(round(xu)),Int(round(yu))])")
                    else
                        GAccessor.text(g[1,eidx] , "Value = ")
                    end
                end
                init_zoom_scroll(c,zr)
            else
                set_gtk_property!(g[3,7], :text, "Set index within bound")
            end
        end
    end

    # Handles header showing
    signal_connect(headerbtn, :clicked) do widget
        showall(header_window(img.wcs[indx]))
    end

    # Handles Label showing
    signal_connect(labelbtn, :clicked) do widget
        showall(label_window(img_labels))
    end

    # Handles reset button
    signal_connect(resetbtn, :clicked) do widget
        img_labels = Array{Tuple{Tuple{Real,Real},String},1}(undef,0)
    end

    showall(win)
end