Skip to content

Instantly share code, notes, and snippets.

@larshei
Created September 19, 2023 19:58
Show Gist options
  • Save larshei/249ebc49b7469c41d8ea4260fe9ad93a to your computer and use it in GitHub Desktop.
Save larshei/249ebc49b7469c41d8ea4260fe9ad93a to your computer and use it in GitHub Desktop.
Leaflet Map as Elixir Phoenix LiveView Component

Note

This was just a quick copy/paste of snippets from a project.

I dod not get to test this version yet.

// Add this to your app.js phoenix application
let Hooks = {}
Hooks.Map = {
mounted(){
const markers = {}
const map = L.map('mapid').setView([51.505, -0.09], 14)
const paths = {}
let geojsonLayer = L.geoJSON().addTo(map).setStyle({color: "#6435c9"})
L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
maxZoom: 18,
id: 'mapbox/streets-v11',
tileSize: 512,
zoomOffset: -1,
accessToken: YOUR_MAPBOX_ACCESS_TOKEN
}).addTo(map)
this.handleEvent("update_marker_position", ({reference, lat, lon, center_view}) => {
markers[reference].setLatLng(L.latLng(lat, lon))
if (center_view) {
map.flyTo(L.latLng(lat, lon))
}
})
this.handleEvent("draw_path", ({reference, coordinates, color}) => {
data = {
"type": "LineString",
"coordinates": coordinates
}
geojsonLayer.addData(data)
})
this.handleEvent("view_init", ({reference, lat, lon, zoom_level = 20}) => {
geojsonLayer.remove()
geojsonLayer = L.geoJSON().addTo(map).setStyle({color: "#6435c9"})
map.setView(L.latLng(lat, lon), zoom_level)
})
this.handleEvent("set_zoom_level", ({zoom_level}) => {
map.setZoom(zoom_level)
})
this.handleEvent("add_marker", ({reference, lat, lon}) => {
// lets not add duplicates for the same marker!
if (markers[reference] == null) {
const marker = L.marker(L.latLng(lat, lon))
marker.addTo(map)
markers[reference] = marker
}
})
this.handleEvent("add_marker_with_popup", ({reference, lat, lon, link}) => {
// lets not add duplicates for the same marker!
if (markers[reference] == null) {
const marker = L.marker(L.latLng(lat, lon))
marker.bindPopup(`<a href=\"${link}\">${reference}</a>`)
marker.addTo(map)
markers[reference] = marker
}
})
this.handleEvent("clear", () => {
geojsonLayer.remove()
geojsonLayer = L.geoJSON().addTo(map)
for (const [reference, value] of Object.entries(markers)) {
marker = markers[reference]
marker.remove()
markers.delete(reference)
}
})
this.handleEvent("remove_marker", ({reference}) => {
if (markers[reference] != null) {
marker = markers[reference]
marker.remove()
markers.delete(reference)
}
geojsonLayer.remove()
})
}
}
defmodule Components.LeafletMap do
use Phoenix.Component
attr :class, :string, default: nil
def map(assigns) do
~H"""
<div style="overflow: hidden" phx-update="ignore" id="mapcontainer">
<div class={@class} phx-hook="Map" id="mapid"></div>
</div>
"""
end
# might be off if the maps shape is not close to a square.
def calculate_initial_map_zoom_level(n, e, s, w) do
lat_to_radiant = fn lat ->
sin = :math.sin(lat * :math.pi() / 180)
radX2 = :math.log((1 + sin) / (1 - sin)) / 2
max(min(radX2, :math.pi()), -:math.pi()) / 2
end
lat_difference = abs(lat_to_radiant.(n) - lat_to_radiant.(s))
lon_difference = abs(e - w)
lat_fraction = lat_difference / :math.pi()
lon_fraction = lon_difference / 360
# Ensure we never get 0 in division. 1.0e-5 was chosen arbitrarily after trying different values.
lat_zoom = :math.log(1 / max(lat_fraction, 1.0e-5)) / :math.log(2)
lon_zoom = :math.log(1 / max(lon_fraction, 1.0e-5)) / :math.log(2)
# Slight zoom out for vertical dimension, because our map view is very wide and not square.
min(lat_zoom - 0.5, lon_zoom)
|> min(20) # Lets not zoom in to infinity
end
def liveview_setup_map(socket, opts \\ []) do
socket
|> assign(selected_address: address)
|> Phoenix.LiveView.push_event("view_init", %{
reference: opts[:reference],
lat: opts[:latitude],
lon: opts[:longitude],
zoom_level: opts[:zoom_level] || 15
})
|> Phoenix.LiveView.push_event("add_marker", %{
reference: opts[:reference],
lat: opts[:latitude],
lon: opts[:longitude]
})
|> Phoenix.LiveView.push_event("update_marker_position", %{
reference: opts[:reference],
lat: address[:latitude],
lon: address[:longitude],
center_view: true
})
end
end
defmodule MyAppWeb.MapLive do
use MyAppWeb, :live_view
@impl true
def mount(%{"id" => id}, _session, socket) do
opts = [latitude: 51.123456, longitude: 7.123456, reference: "main"]
LeafletMap.liveview_set_map_to_address(socket, opts)
end
@impl true
def render(assigns) do
~H"""
<div class="h-screen bg-gray-100">
<div class="flex h-screen justify-center items-center rounded-md shadow-lg">
<LeafletMap.map class="h-80" />
</div>
</div>
"""
end
end
@nomtrosk
Copy link

nomtrosk commented Sep 20, 2023

Great approach to get leaflet working in Elixir!

I spent some time getting this to work: The biggest time consumer was to discover that the flex class in line 15 of map_live would render the map gray. Anyways, here is what else i changed to make it run

  • Removed address variable in liveview_setup_map (looked like some left overs)
  • Called liveview_setup_map instead of liveview_set_map_to_address in map_live.ex
  • Added leaflet CSS and JS imports to <head> in root.html.heex from hosted source https://leafletjs.com/download.html
  • Added alias Components.LeafletMap, as: LeafletMap to map_live.ex
  • Added the hooks to livesocket in app.js by altering this line:
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks})

@larshei
Copy link
Author

larshei commented Sep 20, 2023

@nomtrosk right, sorry for rushing it out like this, but glad you got it to work in the end!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment