first commit
This commit is contained in:
commit
5d69d2cad7
15 changed files with 56893 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
.make.*
|
||||
error.txt
|
||||
data
|
||||
elm-stuff
|
2
.watchmakerc
Normal file
2
.watchmakerc
Normal file
|
@ -0,0 +1,2 @@
|
|||
extensions:
|
||||
- .elm
|
11
Makefile
Normal file
11
Makefile
Normal file
|
@ -0,0 +1,11 @@
|
|||
DIRNAME=$(notdir $(CURDIR))
|
||||
|
||||
ELMS=$(wildcard src/*.elm)
|
||||
|
||||
app.js: src/App.elm $(ELMS)
|
||||
-elm make $< --output=$@ 2> error.txt
|
||||
@cat error.txt
|
||||
|
||||
upload: app.js index.html style.css
|
||||
rsync -avz . clpland:~/domains/somethingorotherwhatever.com/html/$(DIRNAME)
|
||||
@echo "Uploaded to https://somethingorotherwhatever.com/$(DIRNAME)"
|
17
cgi-bin/save_data.py
Executable file
17
cgi-bin/save_data.py
Executable file
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import cgi
|
||||
import cgitb
|
||||
from pathlib import Path
|
||||
cgitb.enable()
|
||||
|
||||
form = cgi.FieldStorage()
|
||||
|
||||
print('Content-Type: text/plain\n')
|
||||
|
||||
content = form.getfirst('content')
|
||||
|
||||
with open(Path('..') / 'data' / 'markers.json', 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print(content)
|
25
elm.json
Normal file
25
elm.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"type": "application",
|
||||
"source-directories": [
|
||||
"src"
|
||||
],
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"elm/browser": "1.0.2",
|
||||
"elm/core": "1.0.5",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"elm-community/list-extra": "8.7.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/time": "1.0.0",
|
||||
"elm/url": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.3"
|
||||
}
|
||||
},
|
||||
"test-dependencies": {
|
||||
"direct": {},
|
||||
"indirect": {}
|
||||
}
|
||||
}
|
46989
emoji_metadata.json
Normal file
46989
emoji_metadata.json
Normal file
File diff suppressed because it is too large
Load diff
30
index.html
Normal file
30
index.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Elm app by clp</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
<script src="https://unpkg.com/protomaps-leaflet@4.0.1/dist/protomaps-leaflet.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<h1>Elm app by clp</h1>
|
||||
</header>
|
||||
<main>
|
||||
<p>This is an app which will either load succesfully, and you'll wonder whether you saw this text at all, or fail ignominiously, showing you only this text.</p>
|
||||
<p>On balance of probabilities: I'm sorry I couldn't be bothered to make this work for you.</p>
|
||||
</main>
|
||||
<footer>Made by <a href="https://somethingorotherwhatever.com">clp</a></footer>
|
||||
|
||||
<script src="app.js"></script>
|
||||
<script src="load-app.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
194
load-app.js
Normal file
194
load-app.js
Normal file
|
@ -0,0 +1,194 @@
|
|||
import show_error from './show-error.mjs';
|
||||
import * as marked from './marked.js';
|
||||
|
||||
console.clear();
|
||||
|
||||
window.marked = marked;
|
||||
|
||||
class MarkdownElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const shadowRoot = this.attachShadow({mode:'open'});
|
||||
|
||||
const markdown_changed = () => {
|
||||
const html = marked.parse(this.textContent);
|
||||
shadowRoot.innerHTML = html;
|
||||
for(let a of shadowRoot.querySelectorAll('a')) {
|
||||
a.setAttribute('target','_blank');
|
||||
}
|
||||
}
|
||||
const observer = new MutationObserver(markdown_changed);
|
||||
observer.observe(this, {characterData: true, subtree: true});
|
||||
markdown_changed();
|
||||
}
|
||||
}
|
||||
customElements.define('mark-down', MarkdownElement);
|
||||
|
||||
class LeafletElement extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
addStylesheet(url) {
|
||||
const linkElem = document.createElement("link");
|
||||
linkElem.setAttribute("rel", "stylesheet");
|
||||
linkElem.setAttribute("href", url);
|
||||
this.shadowRoot.append(linkElem);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const shadowRoot = this.attachShadow({mode:'open'});
|
||||
|
||||
this.addStylesheet("https://unpkg.com/leaflet@1.9.4/dist/leaflet.css");
|
||||
this.addStylesheet('map.css');
|
||||
|
||||
const div = this.div = document.createElement('div');
|
||||
div.style.height = '100%';
|
||||
shadowRoot.append(div);
|
||||
const map = this.map = L.map(div);
|
||||
|
||||
this.markers = [];
|
||||
|
||||
this.update_view();
|
||||
this.update_markers();
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
map.on('move', () => {
|
||||
const {lat,lng} = map.getCenter();
|
||||
})
|
||||
|
||||
map.on('click', e => {
|
||||
const ce = new CustomEvent('mapclick', {detail: {latlng: e.latlng}});
|
||||
this.dispatchEvent(ce);
|
||||
})
|
||||
|
||||
map.on('move', e => {
|
||||
this.dispatchEvent(new CustomEvent('mapmove'));
|
||||
})
|
||||
|
||||
}
|
||||
static get observedAttributes() { return ['lat', 'lon', 'markers'] };
|
||||
|
||||
update_view() {
|
||||
const centre = this.getAttribute('centre') == 'true';
|
||||
const home = ['lat','lon'].map(a => parseFloat(this.getAttribute(a)) || 0);
|
||||
|
||||
if(centre) {
|
||||
this.map.setView(home, this.map.getZoom() || 13);
|
||||
}
|
||||
}
|
||||
|
||||
update_markers() {
|
||||
this.markers.forEach(m => {
|
||||
m.marker.remove();
|
||||
})
|
||||
|
||||
const data = JSON.parse(this.getAttribute('markers'));
|
||||
this.markers = data.map(({id,pos,icon}) => {
|
||||
const marker = L.marker(
|
||||
pos,
|
||||
{
|
||||
icon: L.divIcon({html: `<span id="${id}">${icon}</span>`}),
|
||||
iconSize: [20,20]
|
||||
}
|
||||
);
|
||||
marker.addTo(this.map);
|
||||
|
||||
marker.on('click', e => {
|
||||
this.dispatchEvent(new CustomEvent('markerclick', {detail: {id}}));
|
||||
})
|
||||
|
||||
return {pos, marker};
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if(!this.map) {
|
||||
return;
|
||||
}
|
||||
switch(name) {
|
||||
case 'lat':
|
||||
case 'lon':
|
||||
case 'centre':
|
||||
this.update_view();
|
||||
break;
|
||||
case 'markers':
|
||||
this.update_markers();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
set html(value) {
|
||||
this.shadowRoot.innerHTML = value;
|
||||
}
|
||||
}
|
||||
customElements.define('leaflet-map', LeafletElement);
|
||||
|
||||
let opfs = await navigator.storage.getDirectory();
|
||||
|
||||
async function init_app() {
|
||||
const compilation_error = await show_error;
|
||||
if(compilation_error) {
|
||||
return;
|
||||
}
|
||||
|
||||
let markers = [];
|
||||
|
||||
try {
|
||||
markers = await (await fetch('data/markers.json')).json();
|
||||
|
||||
} catch(e) {
|
||||
try {
|
||||
const fh = await opfs.getFileHandle('markers.json');
|
||||
const f = await fh.getFile();
|
||||
markers = JSON.parse(await f.text())
|
||||
} catch(e) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
const emoji = await (await fetch('emoji_metadata.json')).json();
|
||||
|
||||
const app = Elm.App.init({node: document.body, flags: {emoji, markers}});
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
|
||||
navigator.geolocation.watchPosition(
|
||||
(r) => {
|
||||
app.ports.receive_position.send(r.coords);
|
||||
},
|
||||
(e) => console.error(e),
|
||||
{enableHighAccuracy: true}
|
||||
);
|
||||
|
||||
const send_value_handlers = {
|
||||
save: async ({markers}) => {
|
||||
const f = await opfs.getFileHandle('markers.json', {create:true});
|
||||
const w = await f.createWritable();
|
||||
await w.write(JSON.stringify(markers));
|
||||
await w.close();
|
||||
|
||||
const fd = new FormData();
|
||||
fd.set('content', JSON.stringify(markers));
|
||||
fetch('cgi-bin/save_data.py', {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
app.ports.send_value.subscribe(msg => {
|
||||
send_value_handlers[msg.type](msg);
|
||||
});
|
||||
}
|
||||
|
||||
init_app();
|
28
map.css
Normal file
28
map.css
Normal file
|
@ -0,0 +1,28 @@
|
|||
.leaflet-div-icon {
|
||||
margin: 0 !important;
|
||||
width: revert !important;
|
||||
height: revert !important;
|
||||
background: none;
|
||||
border: none;
|
||||
|
||||
& span {
|
||||
--icon-size: 1.6rem;
|
||||
background: white;
|
||||
border: thin solid black;
|
||||
font-size: calc(0.7 * var(--icon-size));
|
||||
-webkit-text-stroke: 1px blue;
|
||||
border-radius: 50%;
|
||||
line-height: var(--icon-size);
|
||||
width: var(--icon-size) !important;
|
||||
height: var(--icon-size) !important;
|
||||
text-align: center;
|
||||
margin-left: calc(-0.5 * var(--icon-size)) !important;
|
||||
margin-top: calc(-0.5 * var(--icon-size)) !important;
|
||||
display: block;
|
||||
|
||||
&#current-position {
|
||||
background: black;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
21
show-error.mjs
Normal file
21
show-error.mjs
Normal file
|
@ -0,0 +1,21 @@
|
|||
export default fetch('/error.txt').then(r=>{
|
||||
if(r.ok) {
|
||||
return r.text();
|
||||
} else {
|
||||
throw('');
|
||||
}
|
||||
}).then(text => {
|
||||
if(!text) {
|
||||
return false;
|
||||
}
|
||||
document.body.innerHTML = '';
|
||||
const error_show = document.createElement('pre');
|
||||
error_show.setAttribute('id','build-error');
|
||||
error_show.style.background = 'black';
|
||||
error_show.style.color = 'white';
|
||||
error_show.style.padding = '1em';
|
||||
error_show.style['font-size'] = '16px';
|
||||
error_show.textContent = text;
|
||||
document.body.appendChild(error_show);
|
||||
return true;
|
||||
}).catch(e => false);
|
426
src/App.elm
Normal file
426
src/App.elm
Normal file
|
@ -0,0 +1,426 @@
|
|||
port module App exposing (..)
|
||||
|
||||
import Browser
|
||||
import Dict exposing (Dict)
|
||||
import Html as H exposing (Html)
|
||||
import Html.Attributes as HA
|
||||
import Html.Events as HE
|
||||
import Json.Decode as JD
|
||||
import Json.Encode as JE
|
||||
import LatLonDistance exposing (lat_lon_distance, LatLon)
|
||||
import List.Extra as LE
|
||||
import Tuple exposing (pair, first, second)
|
||||
|
||||
port receive_position : (JD.Value -> msg) -> Sub msg
|
||||
|
||||
port send_value : JD.Value -> Cmd msg
|
||||
|
||||
main = Browser.document
|
||||
{ init = init
|
||||
, update = update
|
||||
, subscriptions = subscriptions
|
||||
, view = view
|
||||
}
|
||||
|
||||
type alias Marker =
|
||||
{ pos : LatLon
|
||||
, icon : String
|
||||
, name : String
|
||||
, note : String
|
||||
}
|
||||
|
||||
type Selection
|
||||
= NoSelection
|
||||
| SelectedPosition LatLon
|
||||
| SelectedMarker Int
|
||||
| EditingMarker Int Marker
|
||||
|
||||
blank_marker =
|
||||
{ pos = {lat = 0, lon = 0}
|
||||
, icon = "📌"
|
||||
, name = ""
|
||||
, note = ""
|
||||
}
|
||||
|
||||
type MapCentre
|
||||
= NoCentre
|
||||
| CurrentPositionCentre
|
||||
| PositionCentre LatLon
|
||||
|
||||
type alias Model =
|
||||
{ markers : List Marker
|
||||
, emoji : List Emoji
|
||||
, current_position : LatLon
|
||||
, selection : Selection
|
||||
, new_marker : Marker
|
||||
, accuracy : Float
|
||||
, centre : MapCentre
|
||||
}
|
||||
|
||||
init_model =
|
||||
{ markers = []
|
||||
, emoji = []
|
||||
, current_position = {lat = 55.05155870729228, lon = -1.4652193740914812}
|
||||
, selection = NoSelection
|
||||
, new_marker = blank_marker
|
||||
, accuracy = 0
|
||||
, centre = CurrentPositionCentre
|
||||
}
|
||||
|
||||
type Msg
|
||||
= ReceivePosition JD.Value
|
||||
| MapClicked LatLon
|
||||
| MarkerClicked Int
|
||||
| AddMarker
|
||||
| UpdateNewMarker Marker
|
||||
| UpdateExistingMarker Int Marker
|
||||
| EditMarker Int Marker
|
||||
| RemoveMarker Int
|
||||
| ClickCurrentPosition
|
||||
|
||||
type alias Emoji =
|
||||
{ emoji : String
|
||||
, description : String
|
||||
}
|
||||
|
||||
decode_emoji =
|
||||
JD.map2 Emoji
|
||||
(JD.field "emoji" JD.string)
|
||||
(JD.field "description" JD.string)
|
||||
|
||||
type alias Flags =
|
||||
{ emoji : List Emoji
|
||||
, markers : List Marker
|
||||
}
|
||||
|
||||
decode_flags =
|
||||
JD.map2 Flags
|
||||
(JD.field "emoji" (JD.list decode_emoji))
|
||||
(JD.field "markers" (JD.list decode_marker))
|
||||
|
||||
init : (JD.Value) -> (Model, Cmd msg)
|
||||
init vflags =
|
||||
(case JD.decodeValue decode_flags vflags of
|
||||
Err _ -> init_model
|
||||
Ok flags -> { init_model | emoji = flags.emoji, markers = flags.markers }
|
||||
) |> nocmd
|
||||
|
||||
nocmd m = (m, Cmd.none)
|
||||
|
||||
save : Model -> (Model, Cmd Msg)
|
||||
save model =
|
||||
(model
|
||||
, send_value
|
||||
<| JE.object
|
||||
[ ("type", JE.string "save")
|
||||
, ("markers", JE.list identity <| List.indexedMap (\i m -> encode_marker (String.fromInt i) m) model.markers)
|
||||
]
|
||||
)
|
||||
|
||||
update msg model =
|
||||
case msg of
|
||||
ReceivePosition v -> case JD.decodeValue decode_position v of
|
||||
Ok (pos, accuracy) -> { model | current_position = pos, accuracy = accuracy } |> nocmd
|
||||
_ -> model |> nocmd
|
||||
|
||||
MapClicked pos -> case model.selection of
|
||||
NoSelection ->
|
||||
let
|
||||
m = model.new_marker
|
||||
in
|
||||
{ model | selection = SelectedPosition pos, new_marker = { m | pos = pos } } |> nocmd
|
||||
_ -> { model | selection = NoSelection } |> nocmd
|
||||
|
||||
MarkerClicked i ->
|
||||
let
|
||||
marker = LE.getAt i model.markers
|
||||
pos = marker |> Maybe.map .pos
|
||||
centre = case pos of
|
||||
Just p -> PositionCentre p
|
||||
Nothing -> model.centre
|
||||
in
|
||||
{ model | selection = SelectedMarker i, centre = centre } |> nocmd
|
||||
|
||||
AddMarker -> add_marker model |> save
|
||||
|
||||
UpdateNewMarker marker -> { model | new_marker = marker } |> nocmd
|
||||
|
||||
UpdateExistingMarker i marker -> { model | markers = LE.setAt i marker model.markers, selection = SelectedMarker i } |> save
|
||||
|
||||
EditMarker i marker -> { model | selection = EditingMarker i marker } |> nocmd
|
||||
|
||||
RemoveMarker i -> { model | markers = LE.removeAt i model.markers, selection = NoSelection } |> save
|
||||
|
||||
ClickCurrentPosition -> { model | centre = CurrentPositionCentre, selection = NoSelection } |> nocmd
|
||||
|
||||
add_marker model = case model.selection of
|
||||
SelectedPosition pos ->
|
||||
let
|
||||
nm = model.new_marker
|
||||
marker = { nm | pos = pos }
|
||||
in
|
||||
{ model | markers = marker::model.markers, selection = SelectedMarker 0, new_marker = blank_marker }
|
||||
|
||||
_ -> model
|
||||
|
||||
-- For coordinates from the geolocation API
|
||||
decode_latlon : JD.Decoder LatLon
|
||||
decode_latlon =
|
||||
JD.map2 LatLon
|
||||
(JD.field "latitude" JD.float)
|
||||
(JD.field "longitude" JD.float)
|
||||
|
||||
-- For coordinates from leafletjs
|
||||
decode_latlng : JD.Decoder LatLon
|
||||
decode_latlng =
|
||||
JD.map2 LatLon
|
||||
(JD.field "lat" JD.float)
|
||||
(JD.field "lng" JD.float)
|
||||
|
||||
decode_position : JD.Decoder (LatLon, Float)
|
||||
decode_position = JD.map2 pair decode_latlon (JD.field "accuracy" JD.float)
|
||||
|
||||
decode_map_click =
|
||||
JD.at ["detail", "latlng"] decode_latlng
|
||||
|> JD.map MapClicked
|
||||
|
||||
encode_latlng : LatLon -> JE.Value
|
||||
encode_latlng pos =
|
||||
JE.object
|
||||
[ ("lat", JE.float pos.lat)
|
||||
, ("lng", JE.float pos.lon)
|
||||
]
|
||||
|
||||
decode_marker : JD.Decoder Marker
|
||||
decode_marker =
|
||||
JD.map4 Marker
|
||||
(JD.field "pos" decode_latlng)
|
||||
(JD.field "icon" JD.string)
|
||||
(JD.field "name" JD.string)
|
||||
(JD.field "note" JD.string)
|
||||
|
||||
encode_marker : String -> Marker -> JD.Value
|
||||
encode_marker id marker =
|
||||
JE.object
|
||||
[ ("id", JE.string id)
|
||||
, ("pos", encode_latlng marker.pos)
|
||||
, ("icon", JE.string marker.icon)
|
||||
, ("note", JE.string marker.note)
|
||||
, ("name", JE.string marker.name)
|
||||
]
|
||||
|
||||
splitAt : String -> String -> (String, String)
|
||||
splitAt sep str =
|
||||
if str == "" then ("","") else
|
||||
if String.left (String.length sep) str == sep then
|
||||
("", String.dropLeft (String.length sep) str)
|
||||
else
|
||||
let
|
||||
(a,b) = splitAt sep (String.dropLeft 1 str)
|
||||
in
|
||||
((String.left 1 str)++a, b)
|
||||
|
||||
|
||||
decode_marker_click =
|
||||
JD.at ["detail","id"] JD.string
|
||||
|> JD.andThen
|
||||
(\id ->
|
||||
let
|
||||
(l,r) = splitAt "-" id
|
||||
in
|
||||
case id of
|
||||
"current-position" -> JD.succeed ClickCurrentPosition
|
||||
_ -> case l of
|
||||
"marker" -> case String.toInt r of
|
||||
Just i -> JD.succeed (MarkerClicked i)
|
||||
Nothing -> JD.fail <| "Invalid int "++r
|
||||
_ -> JD.fail <| "Invalid marker type "++l
|
||||
)
|
||||
|
||||
subscriptions model =
|
||||
Sub.batch
|
||||
[ receive_position ReceivePosition
|
||||
]
|
||||
|
||||
display_distance : Float -> String
|
||||
display_distance d =
|
||||
let
|
||||
digits = max 1 (floor (logBase 10 d))
|
||||
magnitude = (10^digits)
|
||||
in
|
||||
if digits < 4 then (d/(toFloat magnitude) |> round |> (*) magnitude |> String.fromInt) ++ "m"
|
||||
else ((d/1000) |> round |> String.fromInt) ++ "km"
|
||||
|
||||
string_from_coords : LatLon -> String
|
||||
string_from_coords c = (String.fromFloat c.lat) ++ "," ++ (String.fromFloat c.lon)
|
||||
|
||||
marker_link label i m =
|
||||
H.a
|
||||
[ HE.onClick (MarkerClicked i)
|
||||
, HA.href <| "#marker-"++(String.fromInt i)
|
||||
]
|
||||
[ H.text label ]
|
||||
|
||||
space = H.text " "
|
||||
|
||||
view : Model -> Browser.Document Msg
|
||||
view model =
|
||||
let
|
||||
new_marker = model.new_marker
|
||||
|
||||
selected_marker : List (String, Marker)
|
||||
selected_marker = case model.selection of
|
||||
SelectedPosition pos -> [("",{ new_marker | pos = pos })]
|
||||
_ -> []
|
||||
|
||||
existing_markers : List (String, Marker)
|
||||
existing_markers = List.indexedMap (\i m -> ("marker-"++(String.fromInt i), m)) model.markers
|
||||
|
||||
current_position_marker = [("current-position", { blank_marker | icon = "😀", pos = model.current_position })]
|
||||
|
||||
markers =
|
||||
(current_position_marker ++ existing_markers ++ selected_marker)
|
||||
|> JE.list (\(id,m) -> encode_marker id m)
|
||||
|> JE.encode 0
|
||||
|
||||
edit_marker_form marker update_msg submit extra_elements =
|
||||
H.form
|
||||
[ HE.onSubmit submit
|
||||
, HA.id "marker-form"
|
||||
, HA.class "marker-detail"
|
||||
]
|
||||
([ H.p [] [H.text <| Debug.toString marker.pos]
|
||||
, H.p
|
||||
[]
|
||||
[ H.label [ HA.for "marker-name" ] [ H.text "Name" ]
|
||||
, H.input
|
||||
[ HA.type_ "text"
|
||||
, HA.id "marker-name"
|
||||
, HA.value marker.name
|
||||
, HE.onInput (\s -> update_msg {marker | name = s })
|
||||
]
|
||||
[]
|
||||
, H.label [ HA.for "marker-icon" ] [ H.text "Icon" ]
|
||||
, H.input
|
||||
[ HA.type_ "text"
|
||||
, HA.id "marker-icon"
|
||||
, HA.list "emoji"
|
||||
, HA.value marker.icon
|
||||
, HE.onInput (\s -> update_msg {marker | icon = s })
|
||||
]
|
||||
[]
|
||||
]
|
||||
, H.p
|
||||
[]
|
||||
[ H.label [ HA.for "marker-note" ] [ H.text "Note" ]
|
||||
, H.textarea
|
||||
[ HA.id "marker-note"
|
||||
, HA.value marker.note
|
||||
, HE.onInput (\s -> update_msg {marker | note = s })
|
||||
]
|
||||
[]
|
||||
]
|
||||
, H.datalist
|
||||
[ HA.id "emoji" ]
|
||||
(model.emoji |> List.map (\e -> H.option [HA.value e.emoji] [H.text <| e.emoji ++ " " ++ e.description]))
|
||||
|
||||
]++extra_elements)
|
||||
|
||||
form = case model.selection of
|
||||
SelectedPosition pos ->
|
||||
edit_marker_form
|
||||
model.new_marker
|
||||
UpdateNewMarker
|
||||
AddMarker
|
||||
[ H.p [] [H.button [ HA.type_ "submit" ] [ H.text "Add a marker here" ]]
|
||||
]
|
||||
|
||||
SelectedMarker i -> case LE.getAt i model.markers of
|
||||
Just marker ->
|
||||
H.div
|
||||
[ HA.class "marker-detail" ]
|
||||
[ H.h2
|
||||
[]
|
||||
[ H.button
|
||||
[ HA.type_ "button"
|
||||
, HE.onClick (EditMarker i marker)
|
||||
]
|
||||
[ H.text "Edit" ]
|
||||
, space
|
||||
, H.span [ HA.class "icon" ] [H.text marker.icon ]
|
||||
, space
|
||||
, H.text marker.name
|
||||
]
|
||||
, H.node "mark-down"
|
||||
[]
|
||||
[ H.text marker.note ]
|
||||
]
|
||||
|
||||
Nothing -> H.div [] [H.text ""]
|
||||
|
||||
EditingMarker i marker ->
|
||||
edit_marker_form
|
||||
marker
|
||||
(EditMarker i)
|
||||
(UpdateExistingMarker i marker)
|
||||
[ H.p
|
||||
[]
|
||||
[ H.button [ HA.type_ "submit" ] [ H.text "Update" ]
|
||||
, space
|
||||
, H.button [ HA.type_ "button", HE.onClick (RemoveMarker i) ] [ H.text "Delete" ]
|
||||
, space
|
||||
, marker_link "Cancel" i marker
|
||||
]
|
||||
]
|
||||
|
||||
NoSelection ->
|
||||
H.div
|
||||
[ HA.id "menu"
|
||||
, HA.class "marker-detail"
|
||||
]
|
||||
[ H.h1 [] [ H.text "Closest markers" ]
|
||||
, H.ul
|
||||
[]
|
||||
(List.map (\(i,m) ->
|
||||
H.li []
|
||||
[ marker_link (m.icon ++ " " ++ m.name) i m ]
|
||||
)
|
||||
(closest_markers |> List.take 3)
|
||||
)
|
||||
]
|
||||
|
||||
closest_markers =
|
||||
model.markers
|
||||
|> List.indexedMap pair
|
||||
|> List.sortBy (second >> .pos >> lat_lon_distance model.current_position)
|
||||
|
||||
selection_str = case model.selection of
|
||||
NoSelection -> "none"
|
||||
SelectedPosition _ -> "position"
|
||||
SelectedMarker _ -> "viewing-marker"
|
||||
EditingMarker _ _ -> "editing-marker"
|
||||
|
||||
(centre_mode, centre) = case model.centre of
|
||||
NoCentre -> (False, model.current_position)
|
||||
CurrentPositionCentre -> (True, model.current_position)
|
||||
PositionCentre pos -> (True, pos)
|
||||
in
|
||||
{
|
||||
title = "CLP's map",
|
||||
body =
|
||||
[ H.main_
|
||||
[ HA.attribute "data-selection" selection_str
|
||||
]
|
||||
[ form
|
||||
, H.node "leaflet-map"
|
||||
[ HA.attribute "markers" <| markers
|
||||
, HA.attribute "lat" <| String.fromFloat centre.lat
|
||||
, HA.attribute "lon" <| String.fromFloat centre.lon
|
||||
, HA.attribute "centre" <| if centre_mode then "true" else "false"
|
||||
, HE.on "mapclick" decode_map_click
|
||||
, HE.on "markerclick" decode_marker_click
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
}
|
50
src/LatLonDistance.elm
Normal file
50
src/LatLonDistance.elm
Normal file
|
@ -0,0 +1,50 @@
|
|||
module LatLonDistance exposing (lat_lon_distance, LatLon)
|
||||
|
||||
type alias LatLon = { lat : Float, lon : Float }
|
||||
|
||||
lat_lon_distance : LatLon -> LatLon -> Float
|
||||
lat_lon_distance p1 p2 =
|
||||
let
|
||||
a = 6378137.0
|
||||
b = 6356752.314245
|
||||
f = 1 / 298.257223563
|
||||
lat1 = degrees p1.lat
|
||||
lat2 = degrees p2.lat
|
||||
lon1 = degrees p1.lon
|
||||
lon2 = degrees p2.lon
|
||||
|
||||
dlon = (lon2 - lon1)
|
||||
|
||||
tanU1 = (1-f) * (tan lat1)
|
||||
cosU1 = 1 / (sqrt (1 + tanU1 * tanU1))
|
||||
sinU1 = tanU1 * cosU1
|
||||
|
||||
tanU2 = (1-f) * (tan lat2)
|
||||
cosU2 = 1 / (sqrt (1 + tanU2*tanU2))
|
||||
sinU2 = tanU2 * cosU2
|
||||
|
||||
approx lon =
|
||||
let
|
||||
sinlon = sin lon
|
||||
coslon = cos lon
|
||||
sinSqsigma = (cosU2*sinlon) * (cosU2*sinlon) + (cosU1*sinU2-sinU1*cosU2*coslon) ^ 2
|
||||
sinsigma = sqrt sinSqsigma
|
||||
cossigma = sinU1*sinU2 + cosU1*cosU2*coslon
|
||||
sigma = atan2 sinsigma cossigma
|
||||
sinalpha = cosU1 * cosU2 * sinlon / sinsigma
|
||||
cosSqalpha = 1 - sinalpha*sinalpha
|
||||
cos2sigma_m = cossigma - 2*sinU1*sinU2/cosSqalpha
|
||||
c = f/16*cosSqalpha*(4+f*(4-3*cosSqalpha))
|
||||
lon_ = dlon + (1-c) * f * sinalpha * (sigma + c*sinsigma*(cos2sigma_m+c*cossigma*(-1+2*cos2sigma_m*cos2sigma_m)))
|
||||
|
||||
uSq = cosSqalpha * (a*a - b*b) / (b*b)
|
||||
biga = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)))
|
||||
bigb = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)))
|
||||
deltasigma = bigb*sinsigma*(cos2sigma_m+bigb/4*(cossigma*(-1+2*cos2sigma_m*cos2sigma_m)-bigb/6*cos2sigma_m*(-3+4*sinsigma*sinsigma)*(-3+4*cos2sigma_m*cos2sigma_m)))
|
||||
|
||||
s = b*biga*(sigma-deltasigma)
|
||||
|
||||
in
|
||||
if abs (lon - lon_) > 1e-12 then (approx lon_) else s
|
||||
in
|
||||
approx dlon
|
76
style.css
Normal file
76
style.css
Normal file
|
@ -0,0 +1,76 @@
|
|||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:is(h1,h2,h3):first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
body > main {
|
||||
display: grid;
|
||||
grid-template:
|
||||
"text" auto
|
||||
"map" 1fr
|
||||
;
|
||||
width: 100svw;
|
||||
height: 100svh;
|
||||
margin: 0;
|
||||
|
||||
}
|
||||
|
||||
.marker-detail {
|
||||
padding: 0 1em;
|
||||
max-height: 70svh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#marker-form {
|
||||
& label {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
& input {
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
& textarea {
|
||||
width: 100%;
|
||||
height: 5em;
|
||||
resize: none;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
& #marker-icon {
|
||||
width: 5em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) {
|
||||
body > main {
|
||||
grid-template:
|
||||
"text map" 1fr
|
||||
/ min(20em,50svw) 1fr
|
||||
;
|
||||
}
|
||||
.marker-detail {
|
||||
max-height: 100svh;
|
||||
}
|
||||
|
||||
#marker-form {
|
||||
& textarea {
|
||||
height: 70svh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#menu {
|
||||
|
||||
& li ~ li {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
leaflet-map {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue