Maps
Mix.install([
{:vega_lite, "~> 0.1.11"},
{:kino_vega_lite, "~> 0.1.8"},
{:req, "~> 0.5.0"},
{:sweet_xml, "~> 0.7.5"},
{:floki, "~> 0.37.0"},
{:tucan, "~> 0.4.1"},
{:math, "~> 0.7.0"},
])
Some module
Finland data exploration
In this simple example I’m building a small app to retrieve statistics’data from the finnish open data. And then we just do fun things
defmodule WFServer do
def api_req(url, params) do
Req.get!(url, params: params)
end
end
defmodule GeoFi do
# Module to access and plot map data from Finland geoserver
@url_base "http://geo.stat.fi/geoserver/wfs"
def check_api_access() do
# check
resp = Req.get!(@url_base)
_status = Map.get(resp, :status, "Unknown")
case resp do
%{status: 200} -> IO.puts("API is active")
_ -> IO.puts("API is not active :(")
end
end
def get_municipalities() do
params = [
service: "WFS",
version: "2.0.0",
request: "GetCapabilities",
outputFormat: "json"
]
resp = Req.get!(@url_base, params: params)
_body = Map.get(resp, :body)
end
def do_request(year) do
#tilastointialueet:kunta4500k
params = [
service: "WFS",
version: "2.0.0",
request: "getFeature",
typeName: "vaestoalue:avi_vaki" <> year,
outputFormat: "json",
srsName: "EPSG:4326"
]
resp = Req.get!(@url_base, params: params)
body = Map.get(resp, :body)
body
end
def get_population(year) do
body = GeoFi.do_request(year)
%{"features" => features} = body
data_man =
Enum.map(features, fn %{"properties" => %{"miehet" => man_pop, "name" => name}} ->
%{"gender" => "Male", "pop" => man_pop, "muni" => name}
end)
data_women =
Enum.map(
features,
fn %{"properties" => %{"naiset" => naiset, "name" => name}} ->
%{"gender" => "Female", "pop" => naiset, "muni" => name}
end
)
data_man ++ data_women
end
def plot_pop(year_val) do
data = GeoFi.get_population(year_val)
b =
Tucan.bar(
data,
"muni",
"pop",
color_by: "gender",
mode: :grouped,
orient: :horizontal
)
|> Tucan.set_size(400, 400)
|> Tucan.set_title("Population Finland per Municipalities - " <> year_val)
|> Tucan.Axes.set_xy_titles("Population", "Municipality")
b
end
def geo_plot(year) do
body = GeoFi.do_request(year)
body
end
def get_grid_population() do
:ok
end
def convert_coordinates( lat, lon) do
{}
end
end
#data = GeoFi.get_population("2023")
alias Tucan
years = [
"2019",
"2020",
"2021",
"2022",
"2023"
]
year_select = Kino.Input.select("Select Year", Enum.map(years, fn x -> {x, x} end))
frame = Kino.Frame.new()
Kino.listen(year_select, fn event ->
%{value: val} = event
chart = GeoFi.plot_pop(val)
Kino.Frame.render(frame, chart)
end)
Kino.Layout.grid([year_select, frame])
Geo plots
Let’s try to geo plots using Vega
# Some modules to get the features
defmodule GeoProjection do
# constants
def fin2coord(input_e, input_n) do
# Flattening of the ellipsoid
f = 1 / 298.257222101
# Semi-major axis
a = 6_378_137
# Central meridian (rad), 27 degrees
lambda_nolla = 0.471238898
# Scale factor
k_nolla = 0.9996
# Easting coordinate
e_nolla = 500_000
# Computation
n = f / (2 - f)
a1 = a / (1 + n) * (1 + n ** 2 / 4 + n ** 4 / 64)
e_squared = 2 * f - f ** 2
h1 = 1 / 2 * n - 2 / 3 * n ** 2 + 37 / 96 * n ** 3 - 1 / 360 * n ** 4
h2 = 1 / 48 * n ** 2 + 1 / 15 * n ** 3 - 437 / 1440 * n ** 4
h3 = 17 / 480 * n ** 3 - 37 / 840 * n ** 4
h4 = 4397 / 161_280 * n ** 4
zeeta = input_n / (a1 * k_nolla)
eeta = (input_e - e_nolla) / (a1 * k_nolla)
zeeta1_pilkku = h1 * Math.sin(2 * zeeta) * Math.cosh(2 * eeta)
zeeta2_pilkku = h2 * Math.sin(4 * zeeta) * Math.cosh(4 * eeta)
zeeta3_pilkku = h3 * Math.sin(6 * zeeta) * Math.cosh(6 * eeta)
zeeta4_pilkku = h4 * Math.sin(8 * zeeta) * Math.cosh(8 * eeta)
eeta1_pilkku = h1 * Math.cos(2 * zeeta) * Math.sinh(2 * eeta)
eeta2_pilkku = h2 * Math.cos(4 * zeeta) * Math.sinh(4 * eeta)
eeta3_pilkku = h3 * Math.cos(6 * zeeta) * Math.sinh(6 * eeta)
eeta4_pilkku = h4 * Math.cos(8 * zeeta) * Math.sinh(8 * eeta)
zeeta_pilkku = zeeta - (zeeta1_pilkku + zeeta2_pilkku + zeeta3_pilkku + zeeta4_pilkku)
eeta_pilkku = eeta - (eeta1_pilkku + eeta2_pilkku + eeta3_pilkku + eeta4_pilkku)
beeta = Math.asin(1 / Math.cosh(eeta_pilkku) * Math.sin(zeeta_pilkku))
l = Math.asin(Math.tanh(eeta_pilkku) / Math.cos(beeta))
qq = Math.asinh(Math.tan(beeta))
q_pilkku = qq + Math.sqrt(e_squared) * Math.atanh(Math.sqrt(e_squared) * Math.tanh(qq))
q_pilkku = GeoProjection.update_q(q_pilkku, qq, e_squared)
lat = Math.rad2deg(Math.atan(Math.sinh(q_pilkku)))
lng = Math.rad2deg(lambda_nolla + l)
[lat, lng]
end
@spec update_q(number, number, number) :: {number, number, number}
def update_q(q_pilkku, qq, e_squared) do
Enum.reduce(1..4, q_pilkku, fn _, acc ->
# * Math.atanh(Math.sqrt(e_squared) * Math.tanh(acc))
qq + Math.sqrt(e_squared) * Math.atanh(Math.sqrt(e_squared) * Math.tanh(acc))
end)
end
end
defmodule ParsePairs do
def parse(nested_list, custom_func) do
update_in(nested_list, [Access.all()], fn item ->
case item do
# Apply function to pairs
[x, y] when is_number(x) and is_number(y) -> custom_func.(x, y)
# Recurse deeper
sublist when is_list(sublist) -> parse(sublist, custom_func)
# Keep other elements unchanged
other -> other
end
end)
end
def c_fun(x, y) do
[x + 1, y + 2]
end
end
defmodule ParsePairs2 do
def apply_function(nested_list) do
Enum.map(nested_list, fn
[x, y] when is_number(x) and is_number(y) -> GeoProjection.fin2coord(y,x)
sublist when is_list(sublist) -> apply_function(sublist)
other -> other
end)
end
def swap(nested_list) do
Enum.map(nested_list, fn
[x, y] when is_number(x) and is_number(y) -> [y,x]
sublist when is_list(sublist) -> swap(sublist)
other -> other
end)
end
def c_fun(x, y) do
[x, y]
end
end
aa = [
[2, 5.0, [1, 1]],
[[13.0, 17.0], [25.0, 0.0], [0, 0]],
[2, 2]
]
ParsePairs2.apply_function(aa )
alias VegaLite, as: Vl
# Initialize the specification, optionally with some top-level properties
data = GeoFi.geo_plot("2020")
%{"features" => feats} = data
length(feats)
# Vl.new(width: 400, height: 400)
# |> Vl.data(data)
data_to_map = %{"features" => feats , "format" => "json" }
#Vl.new(width: 400, height: 400)
#|> Vl.data_from_values(data_to_map, format: %{"property" => "features"})
#|> Vl.mark(:geoshape)
data
guy = Enum.at(feats, 5)
%{"geometry" => %{"coordinates" => coords}} = guy
newguy = update_in(guy, ["geometry", "coordinates"], &ParsePairs2.apply_function/1)
new_data =
update_in(data, ["features"], fn feat ->
Enum.map(
feat,
fn x -> update_in(x, ["geometry", "coordinates"], &ParsePairs2.apply_function/1) end
)
end)
new_data
new_data2 =
update_in(new_data, ["features"], fn feat ->
Enum.map(
feat,
fn x -> update_in(x, ["geometry", "coordinates"], &ParsePairs2.swap/1) end
)
end)
swaped_data =
update_in(data, ["features"], fn feat ->
Enum.map(
feat,
fn x -> update_in(x, ["geometry", "coordinates"], &ParsePairs2.swap/1) end
)
end)
swaped_data
new_data2
vl =
VegaLite.new(
title: "Finland",
width: 900,
height: 900
)
|> VegaLite.data(values: new_data, format: %{"type" => "json", "property" => "features"})
|> VegaLite.repeat(
[row: ["properties.naiset", "properties.miehet"]],
VegaLite.new()
|> VegaLite.projection(type: :mercator)
|> VegaLite.mark(:geoshape)
|> VegaLite.encode_repeat(:color, :row,
type: :quantitative,
legend: [title: "Population"],
title: [field: [repeat: :row], title: "Population of "]
)
)
# |> VegaLite.mark(:geoshape)
# |> VegaLite.encode_field(:color , "properties.naiset", type: :quantitative)
defmodule MapEncoding do
def swapped_encoding do
%{
longitude: %{
field: "geometry.1",
type: "quantitative"
},
latitude: %{
field: "geometry.0",
type: "quantitative"
}
}
end
def standard_encoding do
%{
longitude: %{
field: "geometry.0",
type: "quantitative"
},
latitude: %{
field: "geometry.1",
type: "quantitative"
}
}
end
end
vl =
VegaLite.new(
title: "Finland",
width: 500,
height: 500
)
|> VegaLite.data(values: data, format: %{"type" => "json", "property" => "features"})
|> VegaLite.mark(:geoshape)
|> VegaLite.projection(type: :mercator, rotate: [45, 0, 0 ], center: [1, 1 ,1 ])
Enum.map(1..4, fn x -> IO.puts(x) end)
geojson_map = %{
"type" => "FeatureCollection",
"features" => [
%{
"type" => "Feature",
"geometry" => %{
"type" => "Point",
"coordinates" => [102.0, 0.5]
},
"properties" => %{
"prop0" => "value0"
}
},
%{
"type" => "Feature",
"geometry" => %{
"type" => "LineString",
"coordinates" => [
[102.0, 0.0],
[103.0, 1.0],
[104.0, 0.0],
[150.0, 1.0]
]
},
"properties" => %{
"prop0" => "value0",
"prop1" => 0.0
}
},
%{
"type" => "Feature",
"geometry" => %{
"type" => "Polygon",
"coordinates" => [
[
[100.0, 0.0],
[101.0, 0.0],
[501.0, 2.0],
[100.0, 1.0],
[100.0, 0.0]
]
]
},
"properties" => %{
"prop0" => "value0",
"prop1" => %{"this" => "that"}
}
}
]
}
vl =
VegaLite.new()
|> VegaLite.data_from_url(
"https://raw.githubusercontent.com/mattijn/datasets/master/NL_outline_geo.json",
format: :json
)
|> VegaLite.projection(type: :mercator)
|> VegaLite.mark(:geoshape)
qq = 1
Enum.reduce(1..4, qq, fn(x, acc) -> x + acc end)