mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-09 22:44:38 +08:00
Guide for MapCell (#1250)
This commit is contained in:
parent
11a73da183
commit
cb0a208274
4 changed files with 708 additions and 0 deletions
|
@ -71,6 +71,13 @@ defmodule Livebook.Notebook.Explore do
|
|||
cover_url: "/images/vega_lite.png"
|
||||
}
|
||||
},
|
||||
%{
|
||||
path: Path.join(__DIR__, "explore/intro_to_maplibre.livemd"),
|
||||
details: %{
|
||||
description: "Learn how to seamlessly plot maps using geospatial and tabular data.",
|
||||
cover_url: "/images/maplibre.png"
|
||||
}
|
||||
},
|
||||
%{
|
||||
ref: :kino_intro,
|
||||
path: Path.join(__DIR__, "explore/kino/intro_to_kino.livemd")
|
||||
|
|
688
lib/livebook/notebook/explore/intro_to_maplibre.livemd
Normal file
688
lib/livebook/notebook/explore/intro_to_maplibre.livemd
Normal file
|
@ -0,0 +1,688 @@
|
|||
# Maps with MapLibre
|
||||
|
||||
```elixir
|
||||
Mix.install([
|
||||
{:maplibre, "~> 0.1.0"},
|
||||
{:kino_maplibre, "~> 0.1.0"}
|
||||
])
|
||||
```
|
||||
|
||||
## Introduction
|
||||
|
||||
To work with maps in Livebook we need two libraries:
|
||||
|
||||
* The [`maplibre`](https://github.com/cristineguadelupe/maplibre)
|
||||
package allows us to define our map style specifications
|
||||
|
||||
* The [`kino_maplibre`](https://github.com/cristineguadelupe/Kino_maplibre) package instructs Livebook how to render our specifications and also provides some initial support for [MapLibre Evented API](https://maplibre.org/maplibre-gl-js-docs/api/events/#evented)
|
||||
|
||||
We will make extensive use of MapLibre's functions, so it's handy to call the module something shorter.
|
||||
|
||||
```elixir
|
||||
alias MapLibre, as: Ml
|
||||
```
|
||||
|
||||
<!-- livebook:{"branch_parent_index":0} -->
|
||||
|
||||
## Map cell
|
||||
|
||||
Map Cell is the [Smart Cell](https://news.livebook.dev/v0.6-automate-and-learn-with-smart-cells-mxJJe) for maps that's comes with `Kino_maplibre`.
|
||||
|
||||
Map Cell helps you plot rich maps without needing to know the Maplibre API's details. Besides being a powerful tool for quickly building complex maps, the generated code is wholly valid, which means you can use it to learn more about the Maplibre library or even convert the Map Cell to an Elixir cell and add advanced features to the map without having to start it from scratch.
|
||||
|
||||
Map Cell accepts `:geojson` data sources as url, [Geo](#geo-package-integration) structs or [tabular data](#tabular-data).
|
||||
|
||||
```elixir
|
||||
urban_areas =
|
||||
"https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_urban_areas.geojson"
|
||||
|
||||
conference = %Geo.Point{coordinates: {-123.3596, 48.4268}, properties: %{year: 2007}}
|
||||
|
||||
earthquakes = %{
|
||||
"coordinates" => ["32.3357, 101.8413", "-9.0665, -71.2103", "52.0779, 178.2851"],
|
||||
"mag" => [5.6, 6.5, 6.3]
|
||||
}
|
||||
```
|
||||
|
||||
<!-- livebook:{"attrs":{"center":null,"layers":[{"coordinates_format":"lng_lat","layer_color":"magenta","layer_id":"urban_areas","layer_opacity":1,"layer_radius":10,"layer_source":"urban_areas","layer_type":"fill","source_coordinates":null,"source_latitude":null,"source_longitude":null,"source_type":null},{"coordinates_format":"lat_lng","layer_color":"black","layer_id":"earthquakes","layer_opacity":1,"layer_radius":5,"layer_source":"earthquakes","layer_type":"circle","source_coordinates":"coordinates","source_latitude":null,"source_longitude":null,"source_type":"table"},{"coordinates_format":"lng_lat","layer_color":"green","layer_id":"conference","layer_opacity":1,"layer_radius":10,"layer_source":"conference","layer_type":"circle","source_coordinates":null,"source_latitude":null,"source_longitude":null,"source_type":"geo"}],"ml_alias":"Elixir.Ml","style":"default","zoom":0},"kind":"Elixir.KinoMapLibre.MapCell","livebook_object":"smart_cell"} -->
|
||||
|
||||
```elixir
|
||||
Ml.new()
|
||||
|> Ml.add_source("urban_areas", type: :geojson, data: urban_areas)
|
||||
|> Ml.add_table_source("earthquakes", earthquakes, {:lat_lng, "coordinates"})
|
||||
|> Ml.add_geo_source("conference", conference)
|
||||
|> Ml.add_layer(
|
||||
id: "urban_areas",
|
||||
source: "urban_areas",
|
||||
type: :fill,
|
||||
paint: [fill_color: "magenta", fill_opacity: 1]
|
||||
)
|
||||
|> Ml.add_layer(
|
||||
id: "earthquakes",
|
||||
source: "earthquakes",
|
||||
type: :circle,
|
||||
paint: [circle_color: "black", circle_radius: 5, circle_opacity: 1]
|
||||
)
|
||||
|> Ml.add_layer(
|
||||
id: "conference",
|
||||
source: "conference",
|
||||
type: :circle,
|
||||
paint: [circle_color: "green", circle_radius: 10, circle_opacity: 1]
|
||||
)
|
||||
```
|
||||
|
||||
<!-- livebook:{"branch_parent_index":0} -->
|
||||
|
||||
## Simple maps
|
||||
|
||||
Calling the `Ml.new/0` function with no arguments will render a simple map with all root properties at their default values and the basic style provided by [MapLibre](https://demotiles.maplibre.org/style.json) that will be automatically loaded for you.
|
||||
|
||||
```elixir
|
||||
Ml.new()
|
||||
```
|
||||
|
||||
You can easily override this initial style by declaring your own style using the `:style` option as well as any other root-level properties you wish to configure. It can be either a URL, a JSON string or a map.
|
||||
|
||||
Even though that's not the most recommended tool, you can start a blank style by passing an empty map to create your style totally from scratch.
|
||||
|
||||
```elixir
|
||||
terrain_style =
|
||||
"https://api.maptiler.com/maps/hybrid/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL"
|
||||
|
||||
Ml.new(style: terrain_style, center: {137.9150899566626, 36.25956997955441}, zoom: 9)
|
||||
```
|
||||
|
||||
```elixir
|
||||
street_style =
|
||||
"https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL"
|
||||
|
||||
Ml.new(style: street_style, zoom: 2)
|
||||
```
|
||||
|
||||
<!-- livebook:{"branch_parent_index":0} -->
|
||||
|
||||
## Sources and Layers
|
||||
|
||||
Quite succinctly, sources specify the map's data while layers define how this data will be displayed.
|
||||
|
||||
> Adding a source isn't enough to make data appear on the map because sources don't contain styling details like color or width. Layers refer to a source and give it a visual representation. This makes it possible to style the same source in different ways, like differentiating between types of roads in a highways layer.
|
||||
>
|
||||
> -- <cite>[Sources documentation](https://maplibre.org/maplibre-gl-js-docs/style-spec/sources/)</cite>
|
||||
|
||||
> A style's layers property lists all the layers available in that style. The type of layer is specified by the "type" property, and must be one of background, fill, line, symbol, raster, circle, fill-extrusion, heatmap, hillshade.
|
||||
>
|
||||
> Except for layers of the background type, each layer needs to refer to a source. Layers take the data that they get from a source, optionally filter features, and then define how those features are styled.
|
||||
>
|
||||
> -- <cite>[Layers documentation](https://maplibre.org/maplibre-gl-js-docs/style-spec/layers/)
|
||||
|
||||
Sources and layers are the core of a map. For this reason, we have a set of functions to make it straightforward to work with them.
|
||||
|
||||
Let's add some coordinate data to render GeoJson lines and polygons.
|
||||
|
||||
```elixir
|
||||
polygon_coordinates = [
|
||||
[
|
||||
{-67.13734351262877, 45.137451890638886},
|
||||
{-66.96466, 44.8097},
|
||||
{-68.03252, 44.3252},
|
||||
{-69.06, 43.98},
|
||||
{-70.11617, 43.68405},
|
||||
{-70.64573401557249, 43.090083319667144},
|
||||
{-70.75102474636725, 43.08003225358635},
|
||||
{-70.79761105007827, 43.21973948828747},
|
||||
{-70.98176001655037, 43.36789581966826},
|
||||
{-70.94416541205806, 43.46633942318431},
|
||||
{-71.08482, 45.3052400000002},
|
||||
{-70.6600225491012, 45.46022288673396},
|
||||
{-70.30495378282376, 45.914794623389355},
|
||||
{-70.00014034695016, 46.69317088478567},
|
||||
{-69.23708614772835, 47.44777598732787},
|
||||
{-68.90478084987546, 47.184794623394396},
|
||||
{-68.23430497910454, 47.35462921812177},
|
||||
{-67.79035274928509, 47.066248887716995},
|
||||
{-67.79141211614706, 45.702585354182816},
|
||||
{-67.13734351262877, 45.137451890638886}
|
||||
]
|
||||
]
|
||||
|
||||
line_coordinates = [
|
||||
{-122.48369693756104, 37.83381888486939},
|
||||
{-122.48348236083984, 37.83317489144141},
|
||||
{-122.48339653015138, 37.83270036637107},
|
||||
{-122.48356819152832, 37.832056363179625},
|
||||
{-122.48404026031496, 37.83114119107971},
|
||||
{-122.48404026031496, 37.83049717427869},
|
||||
{-122.48348236083984, 37.829920943955045},
|
||||
{-122.48356819152832, 37.82954808664175},
|
||||
{-122.48507022857666, 37.82944639795659},
|
||||
{-122.48610019683838, 37.82880236636284},
|
||||
{-122.48695850372314, 37.82931081282506},
|
||||
{-122.48700141906738, 37.83080223556934},
|
||||
{-122.48751640319824, 37.83168351665737},
|
||||
{-122.48803138732912, 37.832158048267786},
|
||||
{-122.48888969421387, 37.83297152392784},
|
||||
{-122.48987674713133, 37.83263257682617},
|
||||
{-122.49043464660643, 37.832937629287755},
|
||||
{-122.49125003814696, 37.832429207817725},
|
||||
{-122.49163627624512, 37.832564787218985},
|
||||
{-122.49223709106445, 37.83337825839438},
|
||||
{-122.49378204345702, 37.83368330777276}
|
||||
]
|
||||
```
|
||||
|
||||
We use `Ml.add_source/3` to make the data available and then `Ml.add_layer/2` to present it visually.
|
||||
|
||||
```elixir
|
||||
Ml.new(center: {-122.486052, 37.830348}, zoom: 15, style: street_style)
|
||||
|> Ml.add_source("route",
|
||||
type: :geojson,
|
||||
data: [
|
||||
type: "Feature",
|
||||
geometry: [
|
||||
type: "LineString",
|
||||
coordinates: line_coordinates
|
||||
]
|
||||
]
|
||||
)
|
||||
|> Ml.add_layer(
|
||||
id: "route",
|
||||
type: :line,
|
||||
source: "route",
|
||||
layout: [
|
||||
line_join: "round",
|
||||
line_cap: "round"
|
||||
],
|
||||
paint: [
|
||||
line_color: "#888",
|
||||
line_width: 8
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
Layers are rendered from the top down following the order they were added. While this solution is ideal for most cases, some layers look better if rendered below the labels. For these cases, use `Ml.add_layer_below_labels/2` function.
|
||||
|
||||
```elixir
|
||||
Ml.new(center: {-68.13734351262877, 45.137451890638886}, zoom: 5, style: street_style)
|
||||
|> Ml.add_source("urban-areas",
|
||||
type: :geojson,
|
||||
data: "https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_50m_urban_areas.geojson"
|
||||
)
|
||||
|> Ml.add_source("maine",
|
||||
type: :geojson,
|
||||
data: [
|
||||
type: "Feature",
|
||||
geometry: [
|
||||
type: "Polygon",
|
||||
coordinates: polygon_coordinates
|
||||
]
|
||||
]
|
||||
)
|
||||
|> Ml.add_layer_below_labels(
|
||||
id: "maine",
|
||||
source: "maine",
|
||||
type: :fill,
|
||||
paint: [
|
||||
fill_color: "#088",
|
||||
fill_opacity: 0.5
|
||||
]
|
||||
)
|
||||
|> Ml.add_layer_below_labels(
|
||||
id: "urban-areas-fill",
|
||||
type: :fill,
|
||||
source: "urban-areas",
|
||||
paint: [
|
||||
fill_color: "#f08",
|
||||
fill_opacity: 0.4
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
<!-- livebook:{"branch_parent_index":0} -->
|
||||
|
||||
## Expressions
|
||||
|
||||
[Expressions](https://maplibre.org/maplibre-gl-js-docs/style-spec/expressions/) are a powerful way to render quite complex maps solely through style specifications.
|
||||
We can use expressions to define a formula for computing the value for any `layout`, `paint` or `filter` property.
|
||||
|
||||
If the layer you want to make more dynamic through expressions already exists on the map, you can use `Ml.update_layer/3` to update the respective layer.
|
||||
|
||||
In the following map, we update the `paint` property of the `building` layer so that its colors change according to the zoom level.
|
||||
|
||||
```elixir
|
||||
Ml.new(
|
||||
center: {-90.73414, 14.55524},
|
||||
zoom: 13,
|
||||
style: "https://api.maptiler.com/maps/basic/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL"
|
||||
)
|
||||
|> Ml.update_layer("building",
|
||||
paint: [
|
||||
fill_color: ["interpolate", ["exponential", 0.5], ["zoom"], 15, "#e2714b", 22, "#eee695"],
|
||||
fill_opacity: ["interpolate", ["exponential", 0.5], ["zoom"], 15, 0, 22, 1]
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
Another great example of using expressions is showing the population density of a region.
|
||||
|
||||
```elixir
|
||||
Ml.new(center: {30.0222, -1.9596}, zoom: 7, style: street_style)
|
||||
|> Ml.add_source("rwanda-provinces",
|
||||
type: :geojson,
|
||||
data: "https://maplibre.org/maplibre-gl-js-docs/assets/rwanda-provinces.geojson"
|
||||
)
|
||||
|> Ml.add_layer_below_labels(
|
||||
id: "rwanda-provinces",
|
||||
type: :fill,
|
||||
source: "rwanda-provinces",
|
||||
paint: [
|
||||
fill_color: [
|
||||
"let",
|
||||
"density",
|
||||
["/", ["get", "population"], ["get", "sq-km"]],
|
||||
[
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
8,
|
||||
[
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["var", "density"],
|
||||
274,
|
||||
["to-color", "#edf8e9"],
|
||||
1551,
|
||||
["to-color", "#006d2c"]
|
||||
],
|
||||
10,
|
||||
[
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["var", "density"],
|
||||
274,
|
||||
["to-color", "#eff3ff"],
|
||||
1551,
|
||||
["to-color", "#08519c"]
|
||||
]
|
||||
]
|
||||
],
|
||||
fill_opacity: 0.7
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
<!-- livebook:{"branch_parent_index":0} -->
|
||||
|
||||
## Heatmap
|
||||
|
||||
```elixir
|
||||
Ml.new(
|
||||
center: {-120, 50},
|
||||
zoom: 2,
|
||||
style: "https://api.maptiler.com/maps/basic/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL"
|
||||
)
|
||||
|> Ml.add_source("earthquakes",
|
||||
type: :geojson,
|
||||
data: "https://maplibre.org/maplibre-gl-js-docs/assets/earthquakes.geojson"
|
||||
)
|
||||
|> Ml.add_layer_below_labels(
|
||||
id: "earthquakes-heat",
|
||||
type: :heatmap,
|
||||
source: "earthquakes",
|
||||
maxzoom: 9,
|
||||
paint: [
|
||||
heatmap_weight: ["interpolate", ["linear"], ["get", "mag"], 0, 0, 6, 1],
|
||||
heatmap_intensity: ["interpolate", ["linear"], ["zoom"], 0, 1, 9, 3],
|
||||
heatmap_color: [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["heatmap-density"],
|
||||
0,
|
||||
"rgba(33,102,172,0)",
|
||||
0.2,
|
||||
"rgb(103,169,207)",
|
||||
0.4,
|
||||
"rgb(209,229,240)",
|
||||
0.6,
|
||||
"rgb(253,219,199)",
|
||||
0.8,
|
||||
"rgb(239,138,98)",
|
||||
1,
|
||||
"rgb(178,24,43)"
|
||||
],
|
||||
heatmap_radius: ["interpolate", ["linear"], ["zoom"], 0, 2, 9, 20],
|
||||
heatmap_opacity: ["interpolate", ["linear"], ["zoom"], 7, 1, 9, 0]
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
<!-- livebook:{"branch_parent_index":0} -->
|
||||
|
||||
## Interactive maps
|
||||
|
||||
As said before, the `MapLibre` package covers all the style specification, but for the interactivity of the [Evented API](https://maplibre.org/maplibre-gl-js-docs/api/events/#evented) we need `Kino.MapLibre`.
|
||||
|
||||
`Kino.MapLibre` supports two types of maps: static and dynamic. Essentially, a dynamic map can be updated on
|
||||
the fly without having to be re-evaluated. To make a map dynamic you need to wrap it in `Kino.MapLibre.new/1`
|
||||
|
||||
Static maps will cover most use cases, and all `Kino.MapLibre` functions are available for both map types.
|
||||
|
||||
`Kino.MapLibre` does not currently cover everything the Evented API can offer, but let's see what is already supported.
|
||||
|
||||
<!-- livebook:{"break_markdown":true} -->
|
||||
|
||||
### Simple markers and navigation controls
|
||||
|
||||
```elixir
|
||||
Ml.new(center: {-68.13734351262877, 45.137451890638886}, zoom: 3)
|
||||
|> Kino.MapLibre.add_marker({-69, 50})
|
||||
|> Kino.MapLibre.add_marker({-68, 45}, color: "red", draggable: true)
|
||||
|> Kino.MapLibre.add_nav_controls(show_compass: false)
|
||||
|> Kino.MapLibre.add_nav_controls(show_zoom: false, position: "top-left")
|
||||
```
|
||||
|
||||
### Hover effect
|
||||
|
||||
```elixir
|
||||
Ml.new(center: {-100.486052, 37.830348}, zoom: 3, style: street_style)
|
||||
|> Ml.add_source("states",
|
||||
type: :geojson,
|
||||
data: "https://maplibre.org/maplibre-gl-js-docs/assets/us_states.geojson"
|
||||
)
|
||||
|> Ml.add_layer_below_labels(
|
||||
id: "state-fills",
|
||||
type: :fill,
|
||||
source: "states",
|
||||
paint: [
|
||||
fill_color: "#627BC1",
|
||||
fill_opacity: ["case", ["boolean", ["feature-state", "hover"], false], 1, 0.5]
|
||||
]
|
||||
)
|
||||
|> Ml.add_layer_below_labels(
|
||||
id: "state-borders",
|
||||
type: :line,
|
||||
source: "states",
|
||||
paint: [
|
||||
line_color: "#627BC1",
|
||||
line_width: 2
|
||||
]
|
||||
)
|
||||
|> Kino.MapLibre.add_hover("state-fills")
|
||||
```
|
||||
|
||||
### Clusters expansion on click
|
||||
|
||||
```elixir
|
||||
Ml.new(style: street_style)
|
||||
|> Ml.add_source("earthquakes",
|
||||
type: :geojson,
|
||||
data: "https://maplibre.org/maplibre-gl-js-docs/assets/earthquakes.geojson",
|
||||
cluster: true,
|
||||
cluster_max_zoom: 14,
|
||||
cluster_radius: 50
|
||||
)
|
||||
|> Ml.add_layer(
|
||||
id: "clusters",
|
||||
type: :circle,
|
||||
source: "earthquakes",
|
||||
filter: ["has", "point_count"],
|
||||
paint: [
|
||||
circle_color: ["step", ["get", "point_count"], "#51bbd6", 100, "#f1f075", 750, "#f28cb1"],
|
||||
circle_radius: ["step", ["get", "point_count"], 20, 100, 30, 750, 40]
|
||||
]
|
||||
)
|
||||
|> Ml.add_layer(
|
||||
id: "cluster-count",
|
||||
type: :symbol,
|
||||
source: "earthquakes",
|
||||
filter: ["has", "point_count"],
|
||||
layout: [
|
||||
text_field: "{point_count_abbreviated}",
|
||||
text_font: ["DIN Offc Pro Medium", "Arial Unicode MS Bold"],
|
||||
text_size: 12
|
||||
]
|
||||
)
|
||||
|> Ml.add_layer(
|
||||
id: "unclustered-point",
|
||||
type: :circle,
|
||||
source: "earthquakes",
|
||||
filter: ["!", ["has", "point_count"]],
|
||||
paint: [
|
||||
circle_color: "#11b4da",
|
||||
circle_radius: 4,
|
||||
circle_stroke_width: 1,
|
||||
circle_stroke_color: "#fff"
|
||||
]
|
||||
)
|
||||
|> Kino.MapLibre.clusters_expansion("clusters")
|
||||
```
|
||||
|
||||
### Show information on click
|
||||
|
||||
```elixir
|
||||
Ml.new(center: {-100.04, 38.907}, zoom: 3, style: street_style)
|
||||
|> Ml.add_source("states",
|
||||
type: :geojson,
|
||||
data:
|
||||
"https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_110m_admin_1_states_provinces_shp.geojson"
|
||||
)
|
||||
|> Ml.add_layer(
|
||||
id: "states",
|
||||
type: :fill,
|
||||
source: "states",
|
||||
paint: [
|
||||
fill_color: "rgba(200, 100, 240, 0.4)",
|
||||
fill_outline_color: "rgba(200, 100, 240, 1)"
|
||||
]
|
||||
)
|
||||
|> Kino.MapLibre.info_on_click("states", "name")
|
||||
```
|
||||
|
||||
### Dynamic maps
|
||||
|
||||
As said before, a dynamic map can be updated without re-evaluating. Let's see how it works!
|
||||
|
||||
```elixir
|
||||
map =
|
||||
Ml.new(center: {-68.13734351262877, 45.137451890638886}, zoom: 3)
|
||||
|> Kino.MapLibre.new()
|
||||
```
|
||||
|
||||
We have wrapped the above map in `Kino.MapLibre.new/1` which makes it dynamic, so now we can update the map on the fly.
|
||||
|
||||
When we evaluate the cell below, the marker will be added without the need to re-evaluate the map cell itself.
|
||||
|
||||
```elixir
|
||||
# Change the marker color or location and reevaluate this cell
|
||||
Kino.MapLibre.add_marker(map, {-68, 45}, color: "purple", draggable: true)
|
||||
```
|
||||
|
||||
You can make a static map dynamic at any moment by wrapping it in `Kino.MapLibre.new/1`
|
||||
|
||||
```elixir
|
||||
map =
|
||||
Ml.new(center: {-68.13734351262877, 45.137451890638886}, zoom: 3)
|
||||
# These markers will appear on the initial map render
|
||||
|> Kino.MapLibre.add_marker({-68, 45}, color: "red", draggable: true)
|
||||
|> Kino.MapLibre.add_marker({-69, 50})
|
||||
# This makes the map dynamic for further interactions
|
||||
|> Kino.MapLibre.new()
|
||||
```
|
||||
|
||||
Let's add navigation controls on the fly!
|
||||
|
||||
```elixir
|
||||
Kino.MapLibre.add_nav_controls(map)
|
||||
```
|
||||
|
||||
<!-- livebook:{"branch_parent_index":0} -->
|
||||
|
||||
## Geo package integration
|
||||
|
||||
You can easily add GeoJSON sources to your map using the [Geo](https://hexdocs.pm/geo/readme.html) package. All geometries and collections are supported.
|
||||
|
||||
Just use the `add_geo_source/3` function by giving the geometry or collection as third argument. The type will be detected automatically.
|
||||
|
||||
```elixir
|
||||
p1 = %Geo.Point{coordinates: {-121.415061, 40.506229}}
|
||||
p2 = %Geo.Point{coordinates: {-121.505184, 40.488084}}
|
||||
p3 = %Geo.Point{coordinates: {-121.354465, 40.488737}}
|
||||
|
||||
pl = %Geo.Polygon{
|
||||
coordinates: [
|
||||
[
|
||||
{-121.353637, 40.584978},
|
||||
{-121.284551, 40.584758},
|
||||
{-121.275349, 40.541646},
|
||||
{-121.246768, 40.541017},
|
||||
{-121.251343, 40.423383},
|
||||
{-121.32687, 40.423768},
|
||||
{-121.360619, 40.43479},
|
||||
{-121.363694, 40.409124},
|
||||
{-121.439713, 40.409197},
|
||||
{-121.439711, 40.423791},
|
||||
{-121.572133, 40.423548},
|
||||
{-121.577415, 40.550766},
|
||||
{-121.539486, 40.558107},
|
||||
{-121.520284, 40.572459},
|
||||
{-121.487219, 40.550822},
|
||||
{-121.446951, 40.56319},
|
||||
{-121.370644, 40.563267},
|
||||
{-121.353637, 40.584978}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
gc = %Geo.GeometryCollection{geometries: [p1, p2, p3, pl]}
|
||||
|
||||
Ml.new(center: {-121.403732, 40.492392}, zoom: 10, style: street_style)
|
||||
|> Ml.add_geo_source("national-park", gc)
|
||||
|> Ml.add_layer(
|
||||
id: "park-boundary",
|
||||
type: :fill,
|
||||
source: "national-park",
|
||||
paint: [fill_color: "#888888", fill_opacity: 0.4],
|
||||
filter: ["==", "$type", "Polygon"]
|
||||
)
|
||||
|> Ml.add_layer(
|
||||
id: "park-volcanoes",
|
||||
type: :circle,
|
||||
source: "national-park",
|
||||
paint: [circle_radius: 6, circle_color: "#B42222"],
|
||||
filter: ["==", "$type", "Point"]
|
||||
)
|
||||
```
|
||||
|
||||
```elixir
|
||||
p1 = %Geo.Point{coordinates: {100.4933, 13.7551}, properties: %{year: 2004}}
|
||||
p2 = %Geo.Point{coordinates: {6.6523, 46.5535}, properties: %{year: 2005}}
|
||||
p3 = %Geo.Point{coordinates: {-123.3596, 48.4268}, properties: %{year: 2007}}
|
||||
|
||||
gc = %Geo.GeometryCollection{geometries: [p1, p2, p3]}
|
||||
|
||||
Ml.new()
|
||||
|> Ml.add_geo_source("conferences", gc)
|
||||
|> Ml.add_layer(
|
||||
id: "conferences",
|
||||
type: :symbol,
|
||||
source: "conferences",
|
||||
layout: [
|
||||
icon_image: "custom-marker",
|
||||
text_field: ["get", "year"],
|
||||
text_offset: [0, 1.25],
|
||||
text_anchor: "top"
|
||||
]
|
||||
)
|
||||
|> Kino.MapLibre.add_custom_image(
|
||||
"custom-marker",
|
||||
"https://maplibre.org/maplibre-gl-js-docs/assets/osgeo-logo.png"
|
||||
)
|
||||
```
|
||||
|
||||
<!-- livebook:{"branch_parent_index":0} -->
|
||||
|
||||
## Tabular data
|
||||
|
||||
You can plot tabular data directly on the map without any extra steps or conversion.
|
||||
|
||||
At this point, only points are supported and your data must have the coordinates information in a column or in a pair of columns. For coordinates combined in a single column, the most common formats are well supported, such as `"-123.3596, 48.4268"`, `"-123.3596; 48.4268"` or even `"POINT(-123.3596 48.4268)"`. In addition, the data source needs to implement the [Table](https://hexdocs.pm/table/Table.html) protocol.
|
||||
|
||||
To put the data as a source on the map, use the `add_table_source/4` function by giving the data as the third argument and a tuple with the coordinates information as the fourth. Geometry properties are supported through the function `add_table_source/5`. Just pass the list of columns you want to add as properties as the last argument.
|
||||
|
||||
```elixir
|
||||
earthquakes = %{
|
||||
"latitude" => [
|
||||
32.3646,
|
||||
32.3357,
|
||||
-9.0665,
|
||||
52.0779,
|
||||
-57.7326,
|
||||
-17.9665,
|
||||
38.0765,
|
||||
30.4155,
|
||||
-8.2486,
|
||||
-22.8588,
|
||||
-14.8628,
|
||||
19.6403333333333,
|
||||
-26.2067,
|
||||
14.0187,
|
||||
-54.1373
|
||||
],
|
||||
"longitude" => [
|
||||
101.8781,
|
||||
101.8413,
|
||||
-71.2103,
|
||||
178.2851,
|
||||
148.6945,
|
||||
-174.9684,
|
||||
-122.0076667,
|
||||
102.9892,
|
||||
127.2072,
|
||||
172.1235,
|
||||
-70.3081,
|
||||
-155.968666666667,
|
||||
178.4435,
|
||||
120.6494,
|
||||
159.0844
|
||||
],
|
||||
"mag" => [5.9, 5.6, 6.5, 6.3, 6.4, 6.3, 4.14, 5.9, 6.2, 6.4, 7.2, 4.67, 6.3, 6.1, 6.9],
|
||||
"place" => [
|
||||
"248 km NW of Tianpeng, China",
|
||||
"248 km NW of Tianpeng, China",
|
||||
"111 km SSW of Tarauacá, Brazil",
|
||||
"Rat Islands, Aleutian Islands, Alaska",
|
||||
"west of Macquarie Island",
|
||||
"128 km NW of Neiafu, Tonga",
|
||||
"7km NW of Bay Point, CA",
|
||||
"45 km W of Linqiong, China",
|
||||
"37 km NE of Lospalos, Timor Leste",
|
||||
"southeast of the Loyalty Islands",
|
||||
"13 km WNW of Azángaro, Peru",
|
||||
"3 km NW of Hōlualoa, Hawaii",
|
||||
"south of the Fiji Islands",
|
||||
"1 km S of Lian, Philippines",
|
||||
"Macquarie Island region"
|
||||
]
|
||||
}
|
||||
|
||||
Ml.new()
|
||||
|> Ml.add_table_source(
|
||||
"earthquakes",
|
||||
earthquakes,
|
||||
{:lat_lng, ["latitude", "longitude"]},
|
||||
["place", "mag"]
|
||||
)
|
||||
|> Ml.add_layer(
|
||||
id: "earthquakes",
|
||||
source: "earthquakes",
|
||||
type: :circle,
|
||||
paint: [circle_color: "brown", circle_opacity: 1, circle_radius: 12]
|
||||
)
|
||||
|> Ml.add_layer(
|
||||
id: "earthquakes_mag",
|
||||
source: "earthquakes",
|
||||
type: :symbol,
|
||||
layout: [text_field: ["get", "mag"], text_size: 10],
|
||||
paint: [text_color: "white"]
|
||||
)
|
||||
|> Kino.MapLibre.info_on_click("earthquakes", "place")
|
||||
```
|
|
@ -19,6 +19,7 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
|||
|
||||
kino_vega_lite = %{name: "kino_vega_lite", dependency: {:kino_vega_lite, "~> 0.1.1"}}
|
||||
kino_db = %{name: "kino_db", dependency: {:kino_db, "~> 0.1.1"}}
|
||||
kino_maplibre = %{name: "kino_maplibre", dependency: {:kino_maplibre, "~> 0.1.0"}}
|
||||
|
||||
@extra_smart_cell_definitions [
|
||||
%{
|
||||
|
@ -60,6 +61,18 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
%{
|
||||
kind: "Elixir.KinoMapLibre.MapCell",
|
||||
name: "Map",
|
||||
requirement: %{
|
||||
variants: [
|
||||
%{
|
||||
name: "Default",
|
||||
packages: [kino_maplibre]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
|
BIN
static/images/maplibre.png
Normal file
BIN
static/images/maplibre.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.8 KiB |
Loading…
Add table
Reference in a new issue