diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/autoComplete.ts | 115 | ||||
-rw-r--r-- | src/lib/base.ts | 32 | ||||
-rw-r--r-- | src/lib/button.ts | 29 | ||||
-rw-r--r-- | src/lib/color.ts | 36 | ||||
-rw-r--r-- | src/lib/contextMenu.ts | 35 | ||||
-rw-r--r-- | src/lib/dom.ts | 6 | ||||
-rw-r--r-- | src/lib/form.ts | 54 | ||||
-rw-r--r-- | src/lib/h.ts | 31 | ||||
-rw-r--r-- | src/lib/icons.ts | 66 | ||||
-rw-r--r-- | src/lib/layout.ts | 15 | ||||
-rw-r--r-- | src/lib/modal.ts | 28 | ||||
-rw-r--r-- | src/main.ts | 3 | ||||
-rw-r--r-- | src/map.ts | 131 | ||||
-rw-r--r-- | src/marker.ts | 171 | ||||
-rw-r--r-- | src/markerForm.ts | 116 | ||||
-rw-r--r-- | src/serialization.ts | 44 | ||||
-rw-r--r-- | src/serialization/utils.ts | 9 | ||||
-rw-r--r-- | src/serialization/v0.ts | 122 | ||||
-rw-r--r-- | src/state.ts | 65 | ||||
-rw-r--r-- | src/types/leaflet.d.ts | 95 |
20 files changed, 0 insertions, 1203 deletions
diff --git a/src/lib/autoComplete.ts b/src/lib/autoComplete.ts deleted file mode 100644 index b0a79eb..0000000 --- a/src/lib/autoComplete.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { h, Children, concatClassName } from 'lib/h' -import * as Button from 'lib/button' - -export function create( - attrs: object, - id: string, - keys: string[], - renderEntry: (entry: string) => Element, - onInput: (value: string) => void -): Element { - const completion = h('div', {}) - - const updateCompletion = (target: EventTarget, value: string) => { - const entries = search(value, keys) - mountOn( - completion, - renderCompletion( - renderEntry, - selected => { - (target as HTMLInputElement).value = selected - completion.remove - removeChildren(completion) - onInput(selected) - }, - entries - ) - ) - } - - const input = h('input', - concatClassName( - { ...attrs, - id, - autocomplete: 'off', - onfocus: (e: Event) => { - if (e.target !== null) { - const target = e.target as HTMLInputElement - updateCompletion(target, target.value) - } - }, - oninput: (e: Event) => { - if (e.target !== null) { - const target = e.target as HTMLInputElement - updateCompletion(target, target.value) - onInput(target.value) - } - } - }, - 'g-AutoComplete__Input' - ) - ) as HTMLInputElement - - input.addEventListener('blur', (e: MouseEvent) => { - if (e.relatedTarget === null) { - removeChildren(completion) - } - }) - - return h('div', - { className: 'g-AutoComplete' }, - input, - completion, - Button.raw( - { className: 'g-AutoComplete__Clear', - type: 'button', - onclick: () => { - onInput('') - input.value = '' - input.focus() - } - }, - 'x' - ) - ) -} - -function renderCompletion( - renderEntry: (entry: string) => Element, - onSelect: (entry: string) => void, - entries: string[] -): Element { - return h('div', - { className: 'g-AutoComplete__Completion' }, - ...entries.map(c => - Button.raw( - { className: 'g-AutoComplete__Entry', - type: 'button', - onclick: (e: Event) => { - e.stopPropagation() - e.preventDefault() - onSelect(c) - } - }, - renderEntry(c) - ) - ) - ) -} - -function search(s: string, xs: string[]): string[] { - return xs.filter(x => x.includes(s)) -} - -function mountOn(base: Element, ...children: Element[]) { - removeChildren(base) - children.forEach(child => base.appendChild(child)) -} - -function removeChildren(base: Element) { - const firstChild = base.firstChild - if (firstChild !== null) { - base.removeChild(firstChild) - removeChildren(base) - } -} diff --git a/src/lib/base.ts b/src/lib/base.ts deleted file mode 100644 index 59c91cc..0000000 --- a/src/lib/base.ts +++ /dev/null @@ -1,32 +0,0 @@ -export const b2: string[] = - '01'.split('') - -export const b16: string[] = - '0123456789abcdef'.split('') - -export const b62: string[] = - '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('') - -export function encode(n: bigint, charset: string[]): string { - const base = BigInt(charset.length) - - if (n == BigInt(0)) { - return '0' - } else { - var xs = [] - while (n > BigInt(0)) { - xs.push(charset[Number(n % base)]) - n = n / base - } - return xs.reverse().join('') - } -} - -export function decode(xs: string, charset: string[]): bigint { - const base = BigInt(charset.length) - - return xs - .split('') - .reverse() - .reduce((acc, x, i) => acc + (BigInt(charset.indexOf(x)) * (base ** BigInt(i))), BigInt(0)) -} diff --git a/src/lib/button.ts b/src/lib/button.ts deleted file mode 100644 index 794df35..0000000 --- a/src/lib/button.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { h, Children, concatClassName } from 'lib/h' - -export function raw(attrs: object, ...children: Children): Element { - return h('button', - concatClassName(attrs, 'g-Button__Raw'), - ...children - ) -} - -export function text(attrs: object, ...children: Children): Element { - return h('button', - concatClassName(attrs, 'g-Button__Text'), - ...children - ) -} - -export function action(attrs: object, ...children: Children): Element { - return h('button', - concatClassName(attrs, 'g-Button__Action'), - ...children - ) -} - -export function cancel(attrs: object, ...children: Children): Element { - return h('button', - concatClassName(attrs, 'g-Button__Cancel'), - ...children - ) -} diff --git a/src/lib/color.ts b/src/lib/color.ts deleted file mode 100644 index 59b320d..0000000 --- a/src/lib/color.ts +++ /dev/null @@ -1,36 +0,0 @@ -interface Color { - red: number, - green: number, - blue: number, -} - -export function parse(str: string): Color { - return { - red: parseInt(str.slice(1,3), 16), - green: parseInt(str.slice(3,5), 16), - blue: parseInt(str.slice(5,7), 16), - } -} - -// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrastratio -export function contrastRatio(c1: Color, c2: Color): number { - const r1 = relativeLuminance(c1) - const r2 = relativeLuminance(c2) - - return r1 > r2 - ? (r1 + 0.05) / (r2 + 0.05) - : (r2 + 0.05) / (r1 + 0.05) -} - -function relativeLuminance(c: Color): number { - return ( - 0.2126 * fromSRGB(c.red / 255) - + 0.7152 * fromSRGB(c.green / 255) - + 0.0722 * fromSRGB(c.blue / 255)) -} - -function fromSRGB(sRGB: number): number { - return sRGB <= 0.03928 - ? sRGB / 12.92 - : Math.pow(((sRGB + 0.055) / 1.055), 2.4) -} diff --git a/src/lib/contextMenu.ts b/src/lib/contextMenu.ts deleted file mode 100644 index 6edd567..0000000 --- a/src/lib/contextMenu.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { h } from 'lib/h' - -interface Action { - label: string, - action: () => void -} - -export function show(event: MouseEvent, actions: Action[]) { - const menu = h('div', - { id: 'g-ContextMenu', - style: `left: ${event.pageX.toString()}px; top: ${event.pageY.toString()}px` - }, - ...actions.map(({ label, action }) => - h('div', - { className: 'g-ContextMenu__Entry', - onclick: () => action() - }, - label - ) - ) - ) - - document.body.appendChild(menu) - - // Remove on click or context menu - setTimeout(() => { - const f = () => { - document.body.removeChild(menu) - document.body.removeEventListener('click', f) - document.body.removeEventListener('contextmenu', f) - } - document.body.addEventListener('click', f) - document.body.addEventListener('contextmenu', f) - }, 0) -} diff --git a/src/lib/dom.ts b/src/lib/dom.ts deleted file mode 100644 index 2ab4de5..0000000 --- a/src/lib/dom.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function replaceChildren(parent: Element, ...newChildren: Element[]) { - while (parent.lastChild) { - parent.removeChild(parent.lastChild) - } - newChildren.forEach(c => parent.appendChild(c)) -} diff --git a/src/lib/form.ts b/src/lib/form.ts deleted file mode 100644 index 04a2654..0000000 --- a/src/lib/form.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { h } from 'lib/h' -import * as Layout from 'lib/layout' -import * as Button from 'lib/button' - -interface InputParams { - label: string, - attrs: object, -} - -export function input({ label, attrs }: InputParams): Element { - return h('label', - { className: 'g-Form__Field' }, - label, - h('input', attrs), - ) -} - -interface ColorInputParams { - colors: string[], - label: string, - initValue: string, - onInput: (value: string) => void, -} - -export function colorInput({ colors, label, initValue, onInput }: ColorInputParams): Element { - const input = h('input', - { value: initValue, - type: 'color', - oninput: (e: Event) => { - if (e.target !== null) { - onInput((e.target as HTMLInputElement).value) - } - } - } - ) as HTMLInputElement - return h('label', - { className: 'g-Form__Field' }, - label, - Layout.line( - {}, - input, - ...colors.map(color => - Button.raw({ className: 'g-Form__Color', - style: `background-color: ${color}`, - type: 'button', - onclick: () => { - input.value = color - onInput(color) - } - }) - ) - ) - ) -} diff --git a/src/lib/h.ts b/src/lib/h.ts deleted file mode 100644 index 7e93311..0000000 --- a/src/lib/h.ts +++ /dev/null @@ -1,31 +0,0 @@ -type Child = Element | Text | string | number - -export type Children = Child[] - -export function h(tagName: string, attrs: object, ...children: Children): Element { - let elem = document.createElement(tagName) - elem = Object.assign(elem, attrs) - appendChildren(elem, ...children) - return elem -} - -export function s(tagName: string, attrs: object, ...children: Children): Element { - let elem = document.createElementNS('http://www.w3.org/2000/svg', tagName) - Object.entries(attrs).forEach(([key, value]) => elem.setAttribute(key, value)) - appendChildren(elem, ...children) - return elem -} - -function appendChildren(elem: Element, ...children: Children) { - for (const child of children) { - if (typeof child === 'number') - elem.append(child.toString()) - else - elem.append(child) - } -} - -export function concatClassName(attrs: any, className: string): object { - const existingClassName = 'className' in attrs ? attrs['className'] : undefined - return { ...attrs, className: `${className} ${existingClassName}` } -} diff --git a/src/lib/icons.ts b/src/lib/icons.ts deleted file mode 100644 index 8db4e17..0000000 --- a/src/lib/icons.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { h, s } from 'lib/h' - -export function get(key: string, attrs: object = {}): Element { - const elem = fromKey(key) - if (elem !== undefined) { - Object.entries(attrs).forEach(([key, value]) => { - elem.setAttribute(key, value) - }) - return elem - } else { - return h('span', {}) - } -} - -// https://yqnn.github.io/svg-path-editor/ -function fromKey(key: string): Element | undefined { - if (key == 'house') { - return s('svg', - { viewBox: '0 0 10 10' }, - s('g', { 'stroke': 'none' }, - s('path', { d: 'M0 4V5H1.5V10H4V7C4.4 6.5 5.6 6.5 6 7V10H8.5V5H10V4L5 0Z' }) - ) - ) - } else if (key == 'music') { - return s('svg', - { viewBox: '0 0 10 10' }, - s('g', { 'stroke': 'none' }, - s('ellipse', { cx: '2', cy: '8.5', rx: '2', ry: '1.5' }), - s('ellipse', { cx: '8', cy: '7', rx: '2', ry: '1.5' }), - s('path', { d: 'M2.5 8.5 H4 V4.5 L8.5 3 V7 H10 V0 L2.5 2.5 Z' }), - ) - ) - } else if (key == 'shopping-cart') { - return s('svg', - { viewBox: '0 0 10 10' }, - s('circle', { cx: '3.3', cy: '8.5', r: '0.8' }), - s('circle', { cx: '7.3', cy: '8.5', r: '0.8' }), - s('path', { d: 'M.5.6C1.3.6 1.8.7 2.1 1L2.3 6H8.5', fill: 'transparent' }), - s('path', { d: 'M2.3 1.9H9.4L8.6 4H2.4' }), - ) - } else if (key == 'medical') { - return s('svg', - { viewBox: '0 0 10 10' }, - s('path', { d: 'M5 1V9M1 5H9', style: 'stroke-width: 3' }), - ) - } else if (key == 'envelope') { - return s('svg', - { viewBox: '0 0 10 10' }, - s('path', { d: 'M.5 2.5H9.5V7.5H.5ZM.5 3.4 3.5 5Q5 5.8 6.6 5L9.5 3.4', style: 'fill: transparent' }), - ) - } -} - -// Good to add: -// - loisir / cinéma / piscine -// - école -// - gare -// - bus -export function keys(): string[] { - return [ 'house', - 'music', - 'shopping-cart', - 'medical', - 'envelope', - ] -} diff --git a/src/lib/layout.ts b/src/lib/layout.ts deleted file mode 100644 index 1e38bfd..0000000 --- a/src/lib/layout.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { h, Children, concatClassName } from 'lib/h' - -export function section(attrs: object, ...children: Children): Element { - return h('div', - concatClassName(attrs, 'g-Layout__Section'), - ...children - ) -} - -export function line(attrs: object, ...children: Children): Element { - return h('div', - concatClassName(attrs, 'g-Layout__Line'), - ...children - ) -} diff --git a/src/lib/modal.ts b/src/lib/modal.ts deleted file mode 100644 index 8454e1c..0000000 --- a/src/lib/modal.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { h } from 'lib/h' -import * as Button from 'lib/button' - -export function show(content: Element) { - document.body.appendChild(h('div', - { id: 'g-Modal' }, - h('div', - { className: 'g-Modal__Curtain', - onclick: () => hide() - } - ), - h('div', - { className: 'g-Modal__Window' }, - Button.raw( - { className: 'g-Modal__Close', - onclick: () => hide() - }, - 'x' - ), - content - ) - )) -} - -export function hide() { - const modal = document.querySelector('#g-Modal') - modal && document.body.removeChild(modal) -} diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 36b1143..0000000 --- a/src/main.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as Map from 'map' - -document.body.appendChild(Map.view()) diff --git a/src/map.ts b/src/map.ts deleted file mode 100644 index 04a1351..0000000 --- a/src/map.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { h } from 'lib/h' -import * as Button from 'lib/button' -import * as ContextMenu from 'lib/contextMenu' -import * as Layout from 'lib/layout' -import * as Modal from 'lib/modal' -import * as Marker from 'marker' -import * as MarkerForm from 'markerForm' -import * as State from 'state' -import * as Serialization from 'serialization' -const L = window.L - -export function view() { - // Wait for elements to be on page before installing map - window.setTimeout(installMap, 0) - - return layout() -} - -const mapId: string = 'g-Map__Content' - -function layout(): Element { - return h('div', - { className: 'g-Layout__Page' }, - h('div', - { className: 'g-Layout__Header' }, - h('a', - { className: 'g-Layout__Home', - href: '#' - }, - 'Map' - ) - ) - , h('div', - { className: 'g-Map' }, - h('div', { id: mapId}) - ) - ) -} - -function installMap(): object { - - const map = L.map(mapId, { - center: [51.505, -0.09], - zoom: 2, - attributionControl: false - }) - - map.addLayer(L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png')) - - const mapMarkers = L.featureGroup() - map.addLayer(mapMarkers) - - map.addEventListener('contextmenu', e => { - ContextMenu.show( - e.originalEvent, - [ { label: 'Add a marker', - action: () => { - const pos = e.latlng - const lastMarker = State.last() - Modal.show(MarkerForm.view({ - onValidate: (color: string, icon: string, name: string, radius: number) => { - const id = State.add({ pos, color, icon, name, radius }) - Marker.add({ - id, - pos, - color, - icon, - name, - radius, - addToMap: marker => mapMarkers.addLayer(marker), - removeFromMap: marker => mapMarkers.removeLayer(marker) - }) - Modal.hide() - }, - onCancel: () => Modal.hide(), - color: lastMarker ? lastMarker.color : '#3F92CF', - icon: lastMarker ? lastMarker.icon : '', - name: '', - radius: 0, - })) - } - } - ] - ) - }) - - // Init from hash - const hash = window.location.hash.substr(1) - const state = Serialization.decode(hash) - State.reset(state) - addMarkers({ map, mapMarkers, state, isInit: true }) - - // Reload from hash - window.addEventListener('popstate', _ => reloadFromHash(map, mapMarkers)) - - return map -} - -export function reloadFromHash(map: L.Map, mapMarkers: L.FeatureGroup) { - const state = Serialization.decode(window.location.hash.substr(1)) - mapMarkers.clearLayers() - addMarkers({ map, mapMarkers, state, isInit: false }) -} - -interface AddMarkersOptions { - map: L.Map, - mapMarkers: L.FeatureGroup, - state: State.State, - isInit: boolean, -} - -function addMarkers({ map, mapMarkers, state, isInit }: AddMarkersOptions) { - state.forEach((marker, id) => { - const { pos, color, icon, name, radius } = marker - Marker.add({ - id, - pos, - color, - icon, - name, - radius, - addToMap: marker => mapMarkers.addLayer(marker), - removeFromMap: marker => mapMarkers.removeLayer(marker) - }) - }) - - // Focus - if (state.length > 0 && (isInit || !map.getBounds().contains(mapMarkers.getBounds()))) { - map.fitBounds(mapMarkers.getBounds(), { padding: [ 50, 50 ] }) - } -} diff --git a/src/marker.ts b/src/marker.ts deleted file mode 100644 index 9f59497..0000000 --- a/src/marker.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { h, s } from 'lib/h' -import * as Color from 'lib/color' -import * as ContextMenu from 'lib/contextMenu' -import * as MarkerForm from 'markerForm' -import * as Icons from 'lib/icons' -import * as Modal from 'lib/modal' -import * as State from 'state' -const L = window.L - -interface CreateParams { - id: State.Index, - pos: L.Pos, - color: string, - icon: string, - name: string, - radius: number, - addToMap: (layer: L.Layer | L.FeatureGroup) => void, - removeFromMap: (layer: L.Layer | L.FeatureGroup) => void, -} - -export function add({ id, pos, color, icon, name, radius, addToMap, removeFromMap }: CreateParams) { - const marker = L.marker(pos, { - draggable: true, - autoPan: true, - icon: divIcon({ icon, color, name }), - }) - - const circle = - radius !== 0 - ? L.circle(pos, { radius, color, fillColor: color }) - : undefined - - const layer = - circle !== undefined - ? L.featureGroup([ marker, circle ]) - : L.featureGroup([ marker ]) - - const onUpdate = () => - Modal.show(MarkerForm.view({ - onValidate: (color: string, icon: string, name: string, radius: number) => { - removeFromMap(layer) - add({ id, pos, color, icon, name, radius, addToMap, removeFromMap }) - State.update(id, { pos, color, icon, name, radius }) - Modal.hide() - }, - onCancel: () => Modal.hide(), - color, - icon, - name, - radius, - })) - - marker.addEventListener('contextmenu', e => { - ContextMenu.show( - e.originalEvent, - [ { label: 'Modify', - action: onUpdate, - } - , { label: 'Remove', - action: () => { - removeFromMap(layer) - State.remove(id) - } - } - ] - ) - }) - - marker.addEventListener('drag', e => { - circle && circle.setLatLng(marker.getLatLng()) - }) - - marker.addEventListener('dragend', e => { - const pos = marker.getLatLng() - removeFromMap(layer) - add({ id, pos, color, icon, name, radius, addToMap, removeFromMap }) - State.update(id, { pos, color, icon, name, radius }) - }) - - marker.addEventListener('dblclick', onUpdate) - - addToMap(layer) -} - -interface CreateIconParams { - icon: string, - color: string, - name: string, -} - -function divIcon({ icon, color, name }: CreateIconParams): L.Icon { - const c = Color.parse(color) - const crBlack = Color.contrastRatio({ red: 0, green: 0, blue: 0 }, c) - const crWhite = Color.contrastRatio({ red: 255, green: 255, blue: 255 }, c) - const textCol = crBlack > crWhite ? 'black' : 'white' - const width = 10 - const height = 15 - const stroke = 'black' - const strokeWidth = 0.6 - // Triangle - const t = [ - { x: width * 0.15, y: 7.46 }, - { x: width / 2, y: height }, - { x: width * 0.85, y: 7.46 } - ] - return L.divIcon( - { className: '' - , popupAnchor: [ 0, -34 ] - , html: - h('div', - { className: 'g-Marker' }, - s('svg', - { viewBox: `0 0 ${width} ${height}`, - class: 'g-Marker__Base' - }, - s('circle', - { cx: width / 2, - cy: width / 2, - r: (width - 2 * strokeWidth) / 2, - stroke, - 'stroke-width': strokeWidth, - fill: color - } - ), - s('polygon', - { points: `${t[0].x},${t[0].y} ${t[1].x},${t[1].y} ${t[2].x},${t[2].y}`, - fill: color - } - ), - s('line', - { x1: t[0].x, - y1: t[0].y, - x2: t[1].x, - y2: t[1].y, - stroke, - 'stroke-width': strokeWidth - } - ), - s('line', - { x1: t[1].x, - y1: t[1].y, - x2: t[2].x, - y2: t[2].y, - stroke, - 'stroke-width': strokeWidth - } - ), - ), - Icons.get( - icon, - { class: 'g-Marker__Icon' - , style: `fill: ${textCol}; stroke: ${textCol}` - } - ), - h('div', - { className: 'g-Marker__Title', - style: `color: black; text-shadow: ${textShadow('white', 1, 1)}` - }, - name - ) - ) - } - ) -} - -function textShadow(color: string, w: number, blurr: number): string { - return [[-w, -w], [-w, 0], [-w, w], [0, -w], [0, w], [w, -w], [w, 0], [w, w]] - .map(xs => `${color} ${xs[0]}px ${xs[1]}px ${blurr}px`) - .join(', ') -} - diff --git a/src/markerForm.ts b/src/markerForm.ts deleted file mode 100644 index 54670ae..0000000 --- a/src/markerForm.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { h } from 'lib/h' -import * as AutoComplete from 'lib/autoComplete' -import * as Button from 'lib/button' -import * as Dom from 'lib/dom' -import * as Form from 'lib/form' -import * as Icons from 'lib/icons' -import * as Layout from 'lib/layout' -import * as State from 'state' - -interface FormParams { - onValidate: (color: string, icon: string, name: string, radius: number) => void, - onCancel: () => void, - color: string, - icon: string, - name: string, - radius: number, -} - -export function view({ onValidate, onCancel, color, icon, name, radius }: FormParams): Element { - var radiusStr = radius.toString() - const onSubmit = () => onValidate(color, icon, name, parseInt(radiusStr) || 0) - const domIcon = h('div', - { className: 'g-MarkerForm__Icon' }, - Icons.get(icon, { fill: 'black', stroke: 'black' }) - ) - return h('div', - {}, - Layout.section( - {}, - h('form', - { className: 'g-MarkerForm', - onsubmit: (e: Event) => { - e.preventDefault() - onSubmit() - } - }, - Layout.section( - {}, - Form.input({ - label: 'Name', - attrs: { - oninput: (e: Event) => { - if (e.target !== null) { - name = (e.target as HTMLInputElement).value - } - }, - onblur: (e: Event) => { - if (e.target !== null) { - name = (e.target as HTMLInputElement).value.trim() - } - }, - value: name - } - }), - Form.colorInput({ - colors: State.colors(), - label: 'Color', - initValue: color, - onInput: newColor => color = newColor - }), - h('div', - { className: 'g-Form__Field' }, - h('div', - { className: 'g-Form__Label' }, - h('label', { for: 'g-MarkerForm__IconInput' }, 'Icon') - ), - Layout.line( - { className: 'g-MarkerForm__AutoCompleteAndIcon' }, - AutoComplete.create( - { value: icon, - className: 'g-MarkerForm__AutoComplete' - }, - 'g-MarkerForm__IconInput', - Icons.keys().sort(), - iconKey => h('div', - { className: 'g-MarkerForm__IconEntry' }, - h('div', { className: 'g-MarkerForm__IconElem' }, Icons.get(iconKey, { fill: 'black', stroke: 'black' }) ), - iconKey - ), - newIcon => { - icon = newIcon - Dom.replaceChildren(domIcon, Icons.get(icon, { fill: 'black', stroke: 'black' })) - }), - domIcon - ) - ), - Form.input({ - label: 'Radius (m)', - attrs: { oninput: (e: Event) => { - if (e.target !== null) { - radiusStr = (e.target as HTMLInputElement).value - } - }, - value: radiusStr, - type: 'number', - } - }) - ), - ), - Layout.line( - {}, - Button.action({ onclick: () => onSubmit() }, 'Save'), - Button.cancel( - { onclick: () => onCancel(), - type: 'button' - }, - 'Cancel' - ) - ) - ) - ) -} - -function restrictCharacters(str: string, chars: string): string { - return str.split('').filter(c => chars.indexOf(c) != -1).join('') -} diff --git a/src/serialization.ts b/src/serialization.ts deleted file mode 100644 index 4289b36..0000000 --- a/src/serialization.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as Base from 'lib/base' -import * as State from 'state' -import * as Utils from 'serialization/utils' -import * as V0 from 'serialization/v0' - -// Encoding - -const lastVersion = 0 // max is 62 - -export function encode(s: State.State): string { - if (s.length == 0) { - return '' - } else { - const version = Base.encode(BigInt(lastVersion), Base.b62) - const xs = V0.encode(s).map(binaryToBase62).join('-') - return `${version}${xs}` - } -} - -function binaryToBase62(str: string): string { - // Prepend 1 so that we don’t loose leading 0s - return Base.encode(Base.decode('1' + str, Base.b2), Base.b62) -} - -// Decoding - -export function decode(encoded: string): State.State { - if (encoded == '') { - return [] - } else { - const version = Number(Base.decode(encoded.slice(0, 1), Base.b62)) - if (version == 0) return V0.decode(encoded.slice(1).split('-').map(base62ToBinary)) - else { - console.error(`Unknown decoder version ${version} in order to decode state.`) - return [] - } - } -} - -function base62ToBinary(str: string): string { - // Remove prepended 1 - return Base.encode(Base.decode(str, Base.b62), Base.b2).slice(1) -} - diff --git a/src/serialization/utils.ts b/src/serialization/utils.ts deleted file mode 100644 index c94f199..0000000 --- a/src/serialization/utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as Base from 'lib/base' - -export function encodeNumber(n: bigint, length: number): string { - return Base.encode(n, Base.b2).padStart(length, '0') -} - -export function mod(a: number, b: number): number { - return ((a % b) + b) % b -} diff --git a/src/serialization/v0.ts b/src/serialization/v0.ts deleted file mode 100644 index f90eb66..0000000 --- a/src/serialization/v0.ts +++ /dev/null @@ -1,122 +0,0 @@ -import * as Base from 'lib/base' -import * as Icons from 'lib/icons' -import * as State from 'state' -import * as Utils from 'serialization/utils' - -const posPrecision: number = 5 -const latLength: number = Base.encode(BigInt(`180${'0'.repeat(posPrecision)}`), Base.b2).length -const lngLength: number = Base.encode(BigInt(`360${'0'.repeat(posPrecision)}`), Base.b2).length -const colorLength: number = Base.encode(Base.decode('ffffff', Base.b16), Base.b2).length -const iconLength: number = 8 // At most 255 icons -const radiusLength: number = 5 - -// Encoding - -export function encode(s: State.State): string[] { - return s.map(encodeMarker) -} - -function encodeMarker({ pos, name, color, icon, radius }: State.Marker): string { - const lat = encodeLatOrLng(pos.lat, latLength, 180) // [-90; 90] - const lng = encodeLatOrLng(pos.lng, lngLength, 360) // [-180; 180] - return lat + lng + encodeColor(color) + encodeIcon(icon) + encodeRadius(radius) + encodeName(name) -} - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent -export const uriComponentBase: string[] = - '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.!~*\'()%'.split('') - -function encodeLatOrLng(n: number, length: number, range: number): string { - const [a, b] = Utils.mod(n + range / 2, range).toFixed(posPrecision).split('.') - return Utils.encodeNumber(BigInt(a + b), length) -} - -function encodeColor(color: string): string { - return Utils.encodeNumber(Base.decode(color.slice(1).toLowerCase(), Base.b16), colorLength) -} - -function encodeIcon(icon: string): string { - return icon == '' - ? '0' - : `1${Utils.encodeNumber(BigInt(Icons.keys().indexOf(icon) + 1), iconLength)}` -} - -function encodeRadius(radius: number): string { - if (radius == 0) { - return '0' - } else { - const binary = Base.encode(BigInt(radius), Base.b2) - const binaryLength = Utils.encodeNumber(BigInt(binary.length), radiusLength) - return `1${binaryLength}${binary}` - } -} - -function encodeName(str: string): string { - return str == '' - ? '' - : Base.encode(Base.decode(encodeURIComponent(str), uriComponentBase), Base.b2) -} - -// Decoding - -export function decode(encoded: string[]): State.State { - return encoded.map(binary => { - const [ lat, i1 ] = decodeLatOrLng(binary, 0, latLength, 180) - const [ lng, i2 ] = decodeLatOrLng(binary, i1, lngLength, 360) - const [ color, i3 ] = decodeColor(binary, i2) - const [ icon, i4 ] = decodeIcon(binary, i3) - const [ radius, i5 ] = decodeRadius(binary, i4) - const name = decodeName(binary, i5) - - return { - pos: { lat, lng }, - name, - color, - icon, - radius, - } - }) -} - -function decodeLatOrLng(encoded: string, i: number, length: number, range: number): [number, number] { - const slice = encoded.slice(i, i + length) - const digits = Base.decode(slice, Base.b2).toString() - const latOrLng = parseFloat(`${digits.slice(0, -posPrecision)}.${digits.slice(-posPrecision)}`) - range / 2 - return [ latOrLng, i + length ] -} - -function decodeColor(encoded: string, i: number): [ string, number ] { - const slice = encoded.slice(i, i + colorLength) - const color = `#${Base.encode(Base.decode(slice, Base.b2), Base.b16)}` - return [ color, i + colorLength ] -} - -function decodeIcon(encoded: string, i: number): [ string, number ] { - if (encoded.slice(i, i + 1) == '0') { - return [ '', i + 1 ] - } else { - const slice = encoded.slice(i + 1, i + 1 + iconLength) - const iconIndex = Number(Base.decode(slice, Base.b2)) - 1 - const icon = iconIndex < 0 ? '' : Icons.keys()[iconIndex] - return [ icon, i + 1 + iconLength ] - } -} - -function decodeRadius(encoded: string, i: number): [ number, number ] { - if (encoded.slice(i, i + 1) == '0') { - return [ 0, i + 1 ] - } else { - const binaryLength = encoded.slice(i + 1, i + 1 + radiusLength) - const length = Number(Base.decode(binaryLength, Base.b2)) - const binary = encoded.slice(i + 1 + radiusLength, i + 1 + radiusLength + length) - const radius = Number(Base.decode(binary, Base.b2)) - return [ radius, i + 1 + radiusLength + length ] - } -} - -function decodeName(encoded: string, i: number): string { - const slice = encoded.slice(i) - return slice == '' - ? '' - : decodeURIComponent(Base.encode(Base.decode(slice, Base.b2), uriComponentBase)) -} diff --git a/src/state.ts b/src/state.ts deleted file mode 100644 index 634319a..0000000 --- a/src/state.ts +++ /dev/null @@ -1,65 +0,0 @@ -import * as Serialization from 'serialization' - -const L = window.L - -// State - -var nextIndex: Index = 0 - -export type State = Marker[] -export type Index = number - -var state: State = [] - -export interface Marker { - pos: L.Pos, - name: string, - color: string, - icon: string, - radius: number, -} - -export function reset(s: State) { - state = s - nextIndex = s.length -} - -// CRUD - -export function add(marker: Marker): Index { - const index = nextIndex - state[index] = marker - nextIndex += 1 - pushState() - return index -} - -export function update(index: Index, marker: Marker) { - state[index] = marker - pushState() -} - -export function remove(index: Index) { - delete state[index] - pushState() -} - -// History - -function pushState() { - const encoded = Serialization.encode(Object.values(state)) - history.pushState('', '', `#${encoded}`) -} - -// Inspection - -export function colors() { - return [...new Set(Object.values(state).map(({ color }) => color))] -} - -export function last(): Marker | undefined { - const nonempty = Object.values(state) - return nonempty.length > 0 - ? nonempty.slice(-1)[0] - : undefined -} diff --git a/src/types/leaflet.d.ts b/src/types/leaflet.d.ts deleted file mode 100644 index c1eef16..0000000 --- a/src/types/leaflet.d.ts +++ /dev/null @@ -1,95 +0,0 @@ -export as namespace L - -// Map - -export function map(element: string, options?: MapOptions): Map - -export interface MapOptions { - center: number[], - zoom: number, - attributionControl: boolean, -} - -export interface Map { - addLayer: (layer: Layer | FeatureGroup) => void, - removeLayer: (layer: Layer | FeatureGroup) => void, - addEventListener: (name: string, fn: (e: MapEvent) => void) => void, - getBounds: () => LatLngBounds, - fitBounds: (bounds: LatLngBounds, options: { padding: [number, number] } | undefined) => void, -} - -// LatLngBounds - -export interface LatLngBounds { - contains: (otherBounds: LatLngBounds) => boolean, -} - -// Feature group - -export interface FeatureGroup { - clearLayers: () => void, - addLayer: (layer: Layer | FeatureGroup) => void, - removeLayer: (layer: Layer | FeatureGroup) => void, - getBounds: () => LatLngBounds, -} - -export function featureGroup(xs?: Layer[]): L.FeatureGroup - -// Layer - -export interface Layer { - addEventListener: (name: string, fn: (e: MapEvent) => void) => void, - getLatLng: () => Pos, - setLatLng: (pos: Pos) => void, -} - -export function tileLayer(url: string): Layer - -// Marker - -export function marker( - pos: Pos, - options: { - draggable: boolean, - autoPan: boolean, - icon: Icon, - } -): Layer - -// Circle - -export function circle( - pos: Pos, - options: { - radius: number, - color: string, - fillColor: string, - }, -): Layer - -// Icon - -export interface Icon {} - -export function divIcon( - params: { - className: string, - popupAnchor: number[], - html: Element, - } -): Icon - -// Pos - -export interface Pos { - lat: number, - lng: number, -} - -// MapEvent - -interface MapEvent { - originalEvent: MouseEvent, - latlng: {lat: number, lng: number}, -} - |