Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Animated maps for Dora

race_car_gps.livemd

Animated maps for Dora

Animated maps for Dora

Mix.install([{:kino, "~> 0.5.0"}, {:csv, "~> 2.4"}])
defmodule Rectangle do
  @type point :: {float, float}
  defstruct centre: {0.0, 0.0}, angle: 0.0, length: 1.0, width: 1.0
  @type t :: %Rectangle{centre: point, angle: float, length: float, width: float}

  def angled_vector(a, d), do: {:math.cos(a) * d, :math.sin(a) * d}
  def plus({x, y}, {x_d, y_d}), do: {x + x_d, y + y_d}
  def minus({x, y}, {x_d, y_d}), do: {x - x_d, y - y_d}

  @spec bounds(t) :: [point]
  def bounds(%Rectangle{centre: p, angle: a, length: length, width: width}) do
    l = angled_vector(a, 0.5 * length)
    w = angled_vector(a + 0.5 * :math.pi(), 0.5 * width)
    # front  left
    [
      p |> plus(l) |> minus(w),
      # front right
      p |> plus(l) |> plus(w),
      # back right
      p |> minus(l) |> plus(w),
      # back  left
      p |> minus(l) |> minus(w)
    ]
  end

  @spec move(t, float) :: t
  def move(rectangle = %Rectangle{centre: centre, angle: theta}, distance) do
    %Rectangle{rectangle | centre: centre |> plus(angled_vector(theta, distance))}
  end

  @spec turn(t, float) :: t
  def turn(rectangle = %Rectangle{angle: angle}, delta) do
    %Rectangle{rectangle | angle: angle + delta}
  end
end
%Rectangle{angle: :math.pi()}
|> Rectangle.move(10.0)
|> Rectangle.turn(:math.pi() / 2.0)
|> Rectangle.move(10.0)
defmodule Kino.Leaflet do
  use Kino.JS
  use Kino.JS.Live

  def new(center, zoom) do
    Kino.JS.Live.new(__MODULE__, {normalize_location(center), zoom})
  end

  def add_marker(kino, name, location) do
    Kino.JS.Live.cast(kino, {:add_marker, name, normalize_location(location)})
  end

  def move_marker(kino, name, location) do
    Kino.JS.Live.cast(kino, {:move_marker, name, normalize_location(location)})
  end

  def add_polygon(kino, name, corners, options \\ %{color: 'blue'}) do
    Kino.JS.Live.cast(
      kino,
      {:add_polygon, name, Enum.map(corners, &normalize_location/1), %{color: 'blue'}}
    )
  end

  def move_polygon(kino, name, corners) do
    Kino.JS.Live.cast(kino, {:move_polygon, name, Enum.map(corners, &normalize_location/1)})
  end

  @impl true
  def init({center, zoom}, ctx) do
    {:ok, assign(ctx, center: center, zoom: zoom, locations: %{}, polygons: %{})}
  end

  @impl true
  def handle_connect(ctx) do
    data = %{
      center: ctx.assigns.center,
      zoom: ctx.assigns.zoom,
      locations: ctx.assigns.locations,
      polygons: ctx.assigns.polygons
    }

    {:ok, data, ctx}
  end

  @impl true
  def handle_cast({:add_marker, name, location}, ctx) do
    broadcast_event(ctx, "add_marker", [name, location])
    ctx = update(ctx, :locations, &Map.put_new(&1, name, location))
    {:noreply, ctx}
  end

  @impl true
  def handle_cast({:move_marker, name, location}, ctx) do
    broadcast_event(ctx, "move_marker", [name, location])
    ctx = update(ctx, :locations, &Map.put(&1, name, location))
    {:noreply, ctx}
  end

  @impl true
  def handle_cast({:add_polygon, name, corners, options}, ctx) do
    broadcast_event(ctx, "add_polygon", [name, corners, options])
    ctx = update(ctx, :polygons, &Map.put_new(&1, name, corners))
    {:noreply, ctx}
  end

  @impl true
  def handle_cast({:move_polygon, name, corners}, ctx) do
    broadcast_event(ctx, "move_polygon", [name, corners])
    ctx = update(ctx, :polygons, &Map.put(&1, name, corners))
    {:noreply, ctx}
  end

  defp normalize_location({lat, lng}), do: [lat, lng]
  defp normalize_location(x = [_, _]), do: x
  defp normalize_location(_), do: raise(ArgumentError, "not tuple or list")

  asset "main.js" do
    """
    import * as L from "https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet-src.esm.js";

    export async function init(ctx, data) {
      ctx.root.style.height = "400px";
      const markers = new Map();
      const polygons = new Map();

      // Leaflet requires styles to be present before creating the map,
      // so we await for the import to finish
      await ctx.importCSS("https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet.css");

      const { center, zoom, locations, rectangles } = data;
      const map = L.map(ctx.root, { center, zoom });

      L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '© OpenStreetMap contributors'
      }).addTo(map);

      //locations.forEach( location => { L.marker(location).addTo(map); });

      const path = 'http://127.0.0.1:8887/car_icon.jpg';
      const icon = L.icon({iconUrl: path, iconSize: [10, 20]});
      const add_marker = ([name, location]) => {
        const marker = L.marker(location, {title: name});
        markers.set(name, marker);
        marker.addTo(map);
      };

      const move_marker = ([name, location]) => {
        markers.get(name).setLatLng(location);
      };

      const add_polygon = ([name, corners, options]) => {
        const polygon = L.polygon(corners, {color: "blue"});
        polygons.set(name, polygon);
        polygon.addTo(map);
      };

      const move_polygon = ([name, corners]) => {
        polygons.get(name).setLatLngs(corners);
      };
      const sleep = millis => new Promise(r => setTimeout(r, millis));

      ctx.handleEvent("add_marker", add_marker);
      ctx.handleEvent("move_marker", move_marker);
      ctx.handleEvent("add_polygon", add_polygon);
      ctx.handleEvent("move_polygon", move_polygon);
    }
    """
  end
end
lat_input = Kino.Input.text("latitude") |> Kino.render()
lng_input = Kino.Input.text("longitude")
defmodule LoadCSV do
  def load_csv(filename \\ "livebooks/THillEast.csv") do
    "../#{filename}"
    |> Path.expand(__DIR__)
    |> File.stream!()
    |> CSV.decode!(headers: true)
  end
end

rows = LoadCSV.load_csv()

rows |> Enum.fetch!(2) |> Map.get(" longitude") |> String.trim() |> String.to_float()
#   39.5407175693864
lat = Kino.Input.read(lat_input) |> String.to_float()
# -122.33136040624719
lng = Kino.Input.read(lng_input) |> String.to_float()
start = {lat, lng}
# 1 meter (at the equator)
m = 0.00001 / 1.11
# 1 degree
d = :math.pi() / 180.0
cycle = 33

move = fn d, a -> fn r -> r |> Rectangle.turn(a) |> Rectangle.move(d) end end
pause = fn n -> Process.sleep(n * 33) end
car = %Rectangle{centre: start, angle: 90 * d, length: 10 * m, width: 3 * m}
step = move.(10 * m, 10 * d)

circle = Stream.iterate(car, step) |> Enum.take(37) |> Enum.drop(1)
map = Kino.Leaflet.new(start, 15) |> Kino.render()
# create_car = fn car -> Kino.Leaflet.add_polygon(map, "car", Rectangle.bounds(car)) end
# move_car = fn car -> Kino.Leaflet.move_polygon(map, "car", Rectangle.bounds(car)) end
create_car = fn point -> Kino.Leaflet.add_marker(map, "car", point) end
move_car = fn point -> Kino.Leaflet.move_marker(map, "car", point) end
get_f = fn row, key -> row[" #{key}"] |> String.trim() |> String.to_float() end

read_row = fn row -> {get_f.(row, "latitude") / 60.0, -get_f.(row, "longitude") / 60.0} end
rows = rows |> Enum.map(read_row)

create_car.(rows |> List.first())
Enum.each(rows, fn row -> move_car.(row) end)
# read_row.(rows |> List.first)