Skip to content

Commit

Permalink
🏹 Implement Shuttle Route KML Visualization (#996)
Browse files Browse the repository at this point in the history
* update libraries and fix test/config in prep for adding map lib

* add react-leaflet/leaflet and add basic map view to relevant shape pages

* remove comment

* fix integration test

* add initial shape visualization

* add colors for different shape polylines and add end markers

* TEMP

* Revert "TEMP"

This reverts commit c8b7e13.

* format, lint, add layer selection for shape lines

* improve layer selector

* address pr comments
  • Loading branch information
bfauble authored Jul 31, 2024
1 parent 863e229 commit e6eb53c
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 8 deletions.
1 change: 1 addition & 0 deletions assets/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
@import "./header.css";
@import "./icon.css";
@import "./notes.css";
@import "./shapes.css";
49 changes: 49 additions & 0 deletions assets/css/shapes.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
.legend-square {
height: 1rem;
width: 1rem;
display: inline-block;
}

.color-da291c {
background-color: #da291c;
}

.color-003da5 {
background-color: #003da5;
}

.color-ffc72c {
background-color: #ffc72c;
}

.color-00843d {
background-color: #00843d;
}

.color-ed8b00 {
background-color: #ed8b00;
}

.color-7c878e {
background-color: #7c878e;
}

.color-494f5c {
background-color: #494f5c;
}

.color-003383 {
background-color: #003383;
}

.color-80276c {
background-color: #80276c;
}

.color-008eaa {
background-color: #008eaa;
}

.color-52bbc5 {
background-color: #52bbc5;
}
133 changes: 128 additions & 5 deletions assets/src/components/ShapeViewMap.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,141 @@
import React from "react"
import { LatLngExpression } from "leaflet"
import { MapContainer, TileLayer } from "react-leaflet"
import React, { useMemo } from "react"
import { LatLngBoundsExpression, LatLngExpression } from "leaflet"
import {
CircleMarker,
LayersControl,
LayerGroup,
MapContainer,
Polyline,
TileLayer,
} from "react-leaflet"

type Shape = {
name: string
coordinates: number[][]
}

interface ShapeViewMapProps {
shapes: Shape[]
}

const COLORS = [
"da291c",
"003da5",
"ffc72c",
"00843d",
"ed8b00",
"7c878e",
"494f5c",
"003383",
"80276c",
"008eaa",
"52bbc5",
]

const defaultCenter: LatLngExpression = [42.360718, -71.05891]

const ShapeViewMap = () => {
const generateNameField = (name: string, color: string) =>
`<div class="legend-square color-${color}"></div> ${name}`

const PolyLines = ({ shapes }: { shapes: Shape[] }) =>
shapes.map((shape: Shape, index: number) => {
const key = crypto.randomUUID()
return <PolyLine shape={shape} index={index} key={key} keyPrefix={key} />
})

const PolyLine = ({
shape,
index,
keyPrefix,
}: {
shape: Shape
index: number
keyPrefix: string
}) => {
const colorValue = COLORS[index]
const color = `#${colorValue}`
const start = shape.coordinates[0]
const end = shape.coordinates.slice(-1)[0]

return (
<LayersControl.Overlay
checked
name={generateNameField(shape.name, colorValue)}
key={`${keyPrefix}-control-overlay`}
>
<LayerGroup key={`${keyPrefix}-control-group`}>
<Polyline
positions={shape.coordinates as LatLngExpression[]}
color={color}
key={`${keyPrefix}-line`}
/>
<CircleMarker
center={start as LatLngExpression}
pathOptions={{ color }}
radius={10}
key={`${keyPrefix}-line-start`}
/>
<CircleMarker
center={end as LatLngExpression}
pathOptions={{ color, fillColor: color, fillOpacity: 1.0 }}
radius={10}
key={`${keyPrefix}-line-end`}
/>
</LayerGroup>
</LayersControl.Overlay>
)
}

const getMapBounds = (shapes: Shape[]) => {
const shapeLats: number[] = []
const shapeLongs: number[] = []
shapes.forEach((shape: Shape) =>
shape.coordinates.forEach((coordinate) => {
shapeLats.push(coordinate[0])
shapeLongs.push(coordinate[1])
})
)

return [
[Math.max(...shapeLats), Math.max(...shapeLongs)],
[Math.min(...shapeLats), Math.min(...shapeLongs)],
] as LatLngBoundsExpression
}

const ShapeViewMap = ({ shapes }: ShapeViewMapProps) => {
const polyLines = useMemo(() => {
if (shapes && shapes.length > 0) {
return (
<LayersControl
position="bottomright"
key="layer-control"
collapsed={false}
>
<PolyLines shapes={shapes} />
</LayersControl>
)
} else {
return []
}
}, [shapes])

const mapProps = useMemo(() => {
if (shapes && shapes.length > 0) {
return { bounds: getMapBounds(shapes) }
} else {
return { center: defaultCenter }
}
}, [shapes])

return (
<MapContainer
{...mapProps}
data-testid="shape-view-map-container"
style={{ height: "800px" }}
center={defaultCenter}
zoom={13}
scrollWheelZoom={true}
>
{polyLines}
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
Expand Down
2 changes: 1 addition & 1 deletion assets/tests/components/ShapeViewMap.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import ShapeViewMap from "../../src/components/ShapeViewMap"

describe("ShapeViewMap", () => {
test("renders", () => {
const { container } = render(<ShapeViewMap />)
const { container } = render(<ShapeViewMap shapes={[]} />)
expect(container.getElementsByClassName("leaflet-map-pane").length).toBe(1)
expect(
container.getElementsByClassName("leaflet-control-container").length
Expand Down
27 changes: 27 additions & 0 deletions lib/arrow_web/controllers/shape_html.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule ArrowWeb.ShapeView do
use ArrowWeb, :html
alias Arrow.Shuttle.ShapesUpload

embed_templates "shape_html/*"

Expand All @@ -10,4 +11,30 @@ defmodule ArrowWeb.ShapeView do
attr :action, :string, required: true

def shape_form(assigns)

def shapes_map_view(%ShapesUpload{shapes: shapes}) do
%{shapes: Enum.map(shapes, &shape_map_view/1)}
end

def shapes_map_view(%{params: %{"shapes" => shapes}}) do
%{shapes: Enum.map(shapes, &shape_map_view/1)}
end

defp shape_map_view(%{coordinates: coordinates, name: name}) do
%{
coordinates: map_coordinates(coordinates),
name: name
}
end

defp map_coordinates(coordinates) do
Enum.map(coordinates, &process_coordinate_pair/1)
end

defp process_coordinate_pair(coordinate_pair) do
coordinate_pair
|> String.split(",")
|> Enum.map(&String.to_float/1)
|> Enum.reverse()
end
end
2 changes: 1 addition & 1 deletion lib/arrow_web/controllers/shape_html/select.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<:subtitle><%= input_value(@form, :filename) %></:subtitle>
</.header>

<%= react_component("Components.ShapeViewMap", %{}) %>
<%= react_component("Components.ShapeViewMap", shapes_map_view(@form)) %>

<div class="container">
<.simple_form :let={f} for={@form} action={~p"/shapes_upload"} multipart>
Expand Down
2 changes: 1 addition & 1 deletion lib/arrow_web/controllers/shape_html/show.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Shape <%= @shape.id %>
</.header>

<%= react_component("Components.ShapeViewMap", %{}) %>
<%= react_component("Components.ShapeViewMap", shapes_map_view(@shape)) %>

<.list>
<:item title="Name"><%= @shape.name %></:item>
Expand Down

0 comments on commit e6eb53c

Please sign in to comment.