Tellus から標高データを取得する
Mix.install(
[
{:nx, "~> 0.9"},
{:evision, "~> 0.2"},
{:exla, "~> 0.9"},
{:req, "~> 0.5"},
{:kino, "~> 0.14"},
{:kino_maplibre, "~> 0.1"},
{:kino_vega_lite, "~> 0.1"}
],
config: [nx: [default_backend: EXLA.Backend]]
)
著作権表記
Credit: ASTER GDEM is courtesy of METI and NASA
情報の設定
# Tellus のトークンを入力する
token_input = Kino.Input.password("Token")
Tellus Traveler からデータを探す
Tellus Satellite Data Traveler API を使用する
API 仕様: https://www.tellusxdp.com/docs/travelers/
データセットの選択
defmodule TellusTraveler do
@base_path "https://www.tellusxdp.com/api/traveler/v1"
@data_path "#{@base_path}/datasets"
defp get_headers(token) do
%{
"Authorization" => "Bearer #{token}",
"Content-Type" => "application/json"
}
end
def get_datasets(token, is_order_required) do
url = "#{@data_path}/?is_order_required=#{is_order_required}"
headers = get_headers(token)
url
|> Req.get!(headers: headers)
|> then(& &1.body["results"])
end
def get_dataset(token, dataset_id) do
url = "#{@data_path}/#{dataset_id}/"
headers = get_headers(token)
url
|> Req.get!(headers: headers)
|> Map.get(:body)
end
def search(token, dataset_id, coordinates) do
url =
if is_list(dataset_id) do
"#{@base_path}/data-search/"
else
"#{@data_path}/#{dataset_id}/data-search/"
end
headers = get_headers(token)
request_body =
%{
intersects: %{
type: "Polygon",
coordinates: coordinates
},
query: %{},
sortby: [
%{
field: "properties.start_datetime",
direction: "asc"
}
]
}
|> Map.merge(
if is_list(dataset_id) do
%{datasets: dataset_id}
else
%{}
end
)
|> Jason.encode!()
url
|> Req.post!(body: request_body, headers: headers)
|> then(& &1.body["features"])
end
def get_data_files(token, dataset_id, data_id) do
url = "#{@data_path}/#{dataset_id}/data/#{data_id}/files/"
headers = get_headers(token)
url
|> Req.get!(headers: headers)
|> then(& &1.body["results"])
end
defp get_data_file_download_url(token, dataset_id, data_id, file_id) do
url = "#{@data_path}/#{dataset_id}/data/#{data_id}/files/#{file_id}/download-url/"
headers = get_headers(token)
url
|> Req.post!(headers: headers)
|> then(& &1.body["download_url"])
end
def download(token, dataset_id, scene_id, dist \\ "/tmp/") do
[dist, scene_id]
|> Path.join()
|> File.mkdir_p()
token
|> get_data_files(dataset_id, scene_id)
|> Enum.map(fn file ->
file_path = Path.join([dist, scene_id, file["name"]])
unless File.exists?(file_path) do
token
|> get_data_file_download_url(dataset_id, scene_id, file["id"])
|> Req.get!(into: File.stream!(file_path))
end
file_path
end)
end
end
datasets =
token_input
|> Kino.Input.read()
|> TellusTraveler.get_datasets(false)
elevation_datasets =
datasets
|> Enum.filter(fn dataset ->
dataset["permission"]["allow_network_type"] == "global" &&
String.contains?(dataset["description"], "標高")
end)
Kino.DataTable.new(elevation_datasets, keys: ["name", "id", "description"])
dataset_id = "3f865d0b-6410-453f-b124-e0bf48544b45"
シーンの選択
mt_fuji_location = {138.73, 35.36}
mt_fuji_rectangle = [
[
[elem(mt_fuji_location, 0) - 0.001, elem(mt_fuji_location, 1) - 0.001],
[elem(mt_fuji_location, 0) + 0.001, elem(mt_fuji_location, 1) - 0.001],
[elem(mt_fuji_location, 0) + 0.001, elem(mt_fuji_location, 1) + 0.001],
[elem(mt_fuji_location, 0) - 0.001, elem(mt_fuji_location, 1) + 0.001],
[elem(mt_fuji_location, 0) - 0.001, elem(mt_fuji_location, 1) - 0.001]
]
]
scenes_list =
token_input
|> Kino.Input.read()
|> TellusTraveler.search(dataset_id, mt_fuji_rectangle)
mt_fuji_scene_id =
scenes_list
|> Enum.at(0)
|> Map.get("id")
データのダウンロード
token_input
|> Kino.Input.read()
|> TellusTraveler.download(dataset_id, mt_fuji_scene_id)
ダウンロードした画像を表示する
"/tmp/#{mt_fuji_scene_id}"
|> File.ls!()
|> Enum.filter(fn filename -> Path.extname(filename) != ".txt" end)
|> Enum.sort()
|> Enum.map(fn filename ->
["/tmp", mt_fuji_scene_id, filename]
|> Path.join()
# 色が 16bit で格納されているため、 IMREAD_ANYDEPTH と IMREAD_ANYCOLOR を指定する
|> Evision.imread(
flags: Evision.Constant.cv_IMREAD_ANYDEPTH() + Evision.Constant.cv_IMREAD_ANYCOLOR()
)
# 大きすぎるのでリサイズ
|> Evision.resize({640, 640})
end)
|> Kino.Layout.grid(columns: 2)
mt_fuji_dem =
"/tmp/#{mt_fuji_scene_id}"
|> File.ls!()
|> Enum.find(fn filename -> String.ends_with?(filename, "_dem.tif") end)
|> then(&Path.join(["/tmp", mt_fuji_scene_id, &1]))
|> Evision.imread(
flags: Evision.Constant.cv_IMREAD_ANYDEPTH() + Evision.Constant.cv_IMREAD_ANYCOLOR()
)
|> Evision.Mat.to_nx(EXLA.Backend)
{mt_fuji_min_dig, mt_fuji_max_dig} = {
mt_fuji_dem |> Nx.reduce_min() |> Nx.to_number(),
mt_fuji_dem |> Nx.reduce_max() |> Nx.to_number()
}
mt_fuji_dem_u8 =
mt_fuji_dem
|> Nx.subtract(mt_fuji_min_dig)
|> Nx.multiply(255 / (mt_fuji_max_dig - mt_fuji_min_dig))
|> Nx.as_type(:u8)
Evision.resize(mt_fuji_dem_u8, {640, 640})
mt_fuji_dem_color = Evision.applyColorMap(src: mt_fuji_dem_u8, colormap: Evision.Constant.cv_COLORMAP_JET())
Evision.resize(mt_fuji_dem_color, {640, 640})
標高をグラフ化する
display_elevation_at_x = fn dem, x_index, width, max_y ->
plot_data =
dem[[x_index]]
|> Nx.to_flat_list()
|> Enum.with_index()
|> Enum.map(fn {elevation, index} ->
%{
elevation: elevation,
index: index
}
end)
x_scale = %{"domain" => [0, 3601]}
y_scale = %{"domain" => [-500, max_y]}
VegaLite.new(width: width)
|> VegaLite.data_from_values(plot_data)
|> VegaLite.mark(:area)
|> VegaLite.encode_field(:x, "index", type: :quantitative, scale: x_scale)
|> VegaLite.encode_field(:y, "elevation", type: :quantitative, scale: y_scale)
end
display_elevation_at_x.(mt_fuji_dem, 0, 700, 4000)
max_index = mt_fuji_dem |> Nx.argmax() |> Nx.to_number()
max_x_index = div(max_index, 3601)
max_y_index = max_index - max_x_index * 3601
{max_x_index, max_y_index}
mt_fuji_dem[[max_x_index, max_y_index]]
display_elevation_at_x.(mt_fuji_dem, max_x_index, 700, 4000)
グラフをアニメーションにする
x_scale = %{"domain" => [0, 3601]}
y_scale = %{"domain" => [-500, 4000]}
widget =
VegaLite.new(width: 700)
|> VegaLite.mark(:area)
|> VegaLite.encode_field(:x, "index", type: :quantitative, scale: x_scale)
|> VegaLite.encode_field(:y, "elevation", type: :quantitative, scale: y_scale)
|> Kino.VegaLite.new()
animate = fn dem, x_index ->
plot_data =
dem[[x_index]]
|> Nx.to_flat_list()
|> Enum.with_index()
|> Enum.map(fn {elevation, y_index} ->
%{
elevation: elevation,
index: y_index
}
end)
Kino.VegaLite.clear(widget)
Kino.VegaLite.push_many(widget, plot_data)
end
0..3600//20
|> Enum.map(fn x_index ->
animate.(mt_fuji_dem, x_index)
Process.sleep(100)
end)
富士山と大分市を比較する
get_scene_id = fn location ->
rectangle = [
[
[elem(location, 0) - 0.001, elem(location, 1) - 0.001],
[elem(location, 0) + 0.001, elem(location, 1) - 0.001],
[elem(location, 0) + 0.001, elem(location, 1) + 0.001],
[elem(location, 0) - 0.001, elem(location, 1) + 0.001],
[elem(location, 0) - 0.001, elem(location, 1) - 0.001]
]
]
scenes_list =
token_input
|> Kino.Input.read()
|> TellusTraveler.search(dataset_id, rectangle)
scenes_list
|> Enum.at(0)
|> Map.get("id")
end
oita_location = {131.64, 33.20}
oita_scene_id = get_scene_id.(oita_location)
token_input
|> Kino.Input.read()
|> TellusTraveler.download(dataset_id, oita_scene_id)
oita_dem =
"/tmp/#{oita_scene_id}"
|> File.ls!()
|> Enum.find(fn filename -> String.ends_with?(filename, "_dem.tif") end)
|> then(&Path.join(["/tmp", oita_scene_id, &1]))
|> Evision.imread(
flags: Evision.Constant.cv_IMREAD_ANYDEPTH() + Evision.Constant.cv_IMREAD_ANYCOLOR()
)
|> Evision.Mat.to_nx(EXLA.Backend)
oita_dem_u8 =
oita_dem
|> Nx.subtract(mt_fuji_min_dig)
|> Nx.multiply(255 / (mt_fuji_max_dig - mt_fuji_min_dig))
|> Nx.as_type(:u8)
oita_dem_color = Evision.applyColorMap(src: oita_dem_u8, colormap: Evision.Constant.cv_COLORMAP_JET())
[mt_fuji_dem_color, oita_dem_color]
|> Kino.Layout.grid(columns: 2)
oita_max_index = oita_dem |> Nx.argmax() |> Nx.to_number()
oita_max_x_index = div(oita_max_index, 3601)
[
display_elevation_at_x.(mt_fuji_dem, max_x_index, 300, 4000),
display_elevation_at_x.(oita_dem, oita_max_x_index, 300, 4000)
]
|> Kino.Layout.grid(columns: 2)
富士山とエベレストを比較する
mt_everest_location = {86.92, 27.99}
mt_everest_scene_id = get_scene_id.(mt_everest_location)
token_input
|> Kino.Input.read()
|> TellusTraveler.download(dataset_id, mt_everest_scene_id)
mt_everest_dem =
"/tmp/#{mt_everest_scene_id}"
|> File.ls!()
|> Enum.find(fn filename -> String.ends_with?(filename, "_dem.tif") end)
|> then(&Path.join(["/tmp", mt_everest_scene_id, &1]))
|> Evision.imread(
flags: Evision.Constant.cv_IMREAD_ANYDEPTH() + Evision.Constant.cv_IMREAD_ANYCOLOR()
)
|> Evision.Mat.to_nx(EXLA.Backend)
{mt_everest_min_dig, mt_everest_max_dig} = {
mt_everest_dem |> Nx.reduce_min() |> Nx.to_number(),
mt_everest_dem |> Nx.reduce_max() |> Nx.to_number()
}
min_dig = Enum.min([mt_fuji_min_dig, mt_everest_min_dig])
max_dig = Enum.max([mt_fuji_max_dig, mt_everest_max_dig])
{min_dig, max_dig}
get_heatmap = fn dem, min_dig, max_dig ->
dem
|> Nx.subtract(min_dig)
|> Nx.multiply(256 / (max_dig - min_dig))
|> Nx.as_type(:u8)
|> then(&[src: &1, colormap: Evision.Constant.cv_COLORMAP_JET()])
|> Evision.applyColorMap()
end
[
get_heatmap.(mt_everest_dem, min_dig, max_dig),
get_heatmap.(mt_fuji_dem, min_dig, max_dig),
get_heatmap.(oita_dem, min_dig, max_dig)
]
|> Kino.Layout.grid(columns: 3)
mt_everest_max_x_index =
mt_everest_dem
|> Nx.argmax()
|> Nx.to_number()
|> div(3601)
[
display_elevation_at_x.(mt_everest_dem, mt_everest_max_x_index, 200, 9000),
display_elevation_at_x.(mt_fuji_dem, max_x_index, 200, 9000),
display_elevation_at_x.(oita_dem, oita_max_x_index, 200, 9000)
]
|> Kino.Layout.grid(columns: 3)
海面上昇シミュレーション
alpha =
Nx.logical_and(Nx.greater(oita_dem, 0), Nx.less_equal(oita_dem, 10))
|> Nx.select(255, 0)
|> Nx.new_axis(-1)
alpha
|> Nx.as_type(:u8)
|> Evision.Mat.from_nx_2d()
|> Evision.resize({640, 640})
bgra =
[0, 0, 255]
|> Nx.tensor()
|> Nx.tile([3601, 3601, 1])
|> then(&Nx.concatenate([&1, alpha], axis: 2))
|> Nx.as_type(:u8)
|> Evision.Mat.from_nx_2d()
Evision.resize(bgra, {640, 640})
get_data_url = fn mat ->
Evision.imencode(".png", mat)
|> Base.encode64()
|> then(&"data:image/png;base64,#{&1}")
end
center = {131.5, 33.5}
coordinates = [
[131 - 1 / 3600 / 2, 34 - 9 / 3600 / 2],
[132 + 1 / 3600 / 2, 34 - 9 / 3600 / 2],
[132 + 1 / 3600 / 2, 33 - 11 / 3600 / 2],
[131 - 1 / 3600 / 2, 33 - 11 / 3600 / 2]
]
img_base64 = get_data_url.(bgra)
MapLibre.new(center: center, zoom: 8, style: :terrain)
|> MapLibre.add_source("image", type: :image, url: img_base64, coordinates: coordinates)
|> MapLibre.add_layer(
id: "overlay",
source: "image",
type: :raster,
paint: %{"raster-opacity" => 0.5}
)