aboutsummaryrefslogtreecommitdiff
path: root/content/static/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'content/static/main.js')
-rw-r--r--content/static/main.js331
1 files changed, 331 insertions, 0 deletions
diff --git a/content/static/main.js b/content/static/main.js
new file mode 100644
index 0000000..5da3b24
--- /dev/null
+++ b/content/static/main.js
@@ -0,0 +1,331 @@
1const DEFAULT_ZOOM = 14;
2const DEFAULT_COORDINATES = [38.59104623572979, -9.130882470026634];
3
4const ELEM_ID_MAP = "map";
5const ELEM_ID_BTN_SHAPE_CREATE = "shape-create";
6const ELEM_ID_BTN_SHAPE_DELETE = "shape-delete";
7const ELEM_ID_BTN_SHAPE_BURNED = "shape-kind-burned";
8const ELEM_ID_BTN_SHAPE_UNBURNED = "shape-kind-unburned";
9const ELEM_ID_BTN_SHAPES_UPDATE = "shapes-update";
10
11const SHAPE_KIND_UNBURNED = "unburned";
12const SHAPE_KIND_BURNED = "burned";
13
14/**
15 * A location log
16 * @typedef {Object} LocationLog
17 * @property {number} timestamp
18 * @property {number} latitude
19 * @property {number} longitude
20 * @property {number} accuracy - Accuracy in meters
21 * @property {number} heading - Compass heading in degress [0, 360]
22*/
23
24/**
25 * A shape point
26 * @typedef {Object} ShapePoint
27 * @property {number} latitude
28 * @property {number} longitude
29*/
30
31/**
32 * A shape descriptor
33 * @typedef {Object} ShapeDescriptor
34 * @property {string} kind
35 * @property {[]ShapePoint} points
36*/
37
38/**
39 * A shape
40 * @typedef {Object} Shape
41 * @property {string} kind
42 * @property {[]ShapePoint} points
43 * @property {Object} poly - leaflet polygon, null if points.length < 3
44 * @property {[]Object} poly_points - leaflet circles for each point
45 * @property {number} point_insert_idx - index to start inserting points
46*/
47
48function lib_setup_handler_onclick(elementId, handler) {
49 document.getElementById(elementId).onclick = handler
50}
51
52function lib_setup_map() {
53 var map = L.map(ELEM_ID_MAP).setView(DEFAULT_COORDINATES, DEFAULT_ZOOM);
54 L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
55 maxZoom: 19,
56 attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
57 }).addTo(map);
58 return map;
59}
60
61/**
62 * Fetch location logs
63 * @return {Promise<LocationLog[]>}
64*/
65async function lib_fetch_location_logs() {
66 // const burned = [
67 // [38.592177702929426, -9.145557060034113],
68 // [38.58385651421202, -9.134116290522673],
69 // [38.587516574932266, -9.134999747627804],
70 // [38.59442184182009, -9.13809184749576],
71 // [38.596734957715675, -9.138621921758839],
72 // ];
73 //
74 // const unburned = [
75 // [38.598388277527036, -9.135874396116632],
76 // [38.589731317901276, -9.149692038446165],
77 // [38.58043902375093, -9.138619879692945],
78 // [38.591568658478, -9.12070962376425],
79 // ];
80 //
81 // const location_logs = []
82 // for (const point of burned.concat(unburned)) {
83 // console.log(point)
84 // location_logs.push({
85 // latitude: point[0],
86 // longitude: point[1],
87 // accuracy: 5.8,
88 // timestamp: 0,
89 // heading: 0,
90 // })
91 // }
92 // return Promise.resolve(location_logs);
93
94 const response = await fetch("/api/location");
95 return response.json();
96}
97
98/**
99 * Fetch location logs
100 * @return {Promise<ShapeDescriptor[]>}
101*/
102async function lib_fetch_shape_descriptors() {
103 const response = await fetch("/api/shapes");
104 return response.json();
105}
106
107function lib_add_location_logs_to_map(map, locations) {
108 for (const location of locations) {
109 const len = 0.0002;
110 const lat = Math.sin((location.heading + 90) * 2 * Math.PI / 360) * len;
111 const lon = Math.cos((location.heading + 90) * 2 * Math.PI / 360) * len;
112 const line_coords = [
113 [location.latitude, location.longitude],
114 [location.latitude + lat, location.longitude + lon],
115 ];
116 L.circle([location.latitude, location.longitude], { radius: location.accuracy }).addTo(map);
117 L.polyline(line_coords, { color: 'blue' }).addTo(map)
118 }
119}
120
121function lib_shape_color_for_kind(kind) {
122 if (kind == SHAPE_KIND_BURNED)
123 return 'red';
124 if (kind == SHAPE_KIND_UNBURNED)
125 return 'green';
126 return 'orange';
127}
128
129function lib_shape_create_empty() {
130 return {
131 kind: SHAPE_KIND_UNBURNED,
132 points: [],
133 poly: null,
134 poly_points: [],
135 point_insert_idx: 0,
136 };
137}
138
139function lib_shape_create_from_descriptor(desc) {
140 return {
141 kind: desc.kind,
142 points: desc.points,
143 poly: null,
144 poly_points: [],
145 point_insert_idx: desc.points.length,
146 };
147}
148
149function page_shape__on_shape_create(state) {
150 const shape = lib_shape_create_empty();
151 state.shapes.push(shape);
152 page_shape__ui_shape_select(state, shape);
153}
154
155function page_shape__on_shape_delete(state) {
156 if (state.selected_shape == null)
157 return;
158 page_shape__ui_shape_remove(state, state.selected_shape);
159 state.shapes.splice(state.shapes.indexOf(state.selected_shape), 1);
160 state.selected_shape = null;
161}
162
163function page_shape__on_shape_kind_unburned(state) {
164 if (state.selected_shape == null)
165 return;
166 state.selected_shape.kind = SHAPE_KIND_UNBURNED;
167 page_shape__ui_shape_add(state, state.selected_shape);
168}
169
170function page_shape__on_shape_kind_burned(state) {
171 if (state.selected_shape == null)
172 return;
173 state.selected_shape.kind = SHAPE_KIND_BURNED;
174 page_shape__ui_shape_add(state, state.selected_shape);
175}
176
177function page_shape__on_shapes_update(state) {
178 const shape_descriptors = [];
179 for (const shape of state.shapes) {
180 if (shape.points.length < 3)
181 continue;
182
183 const points = [];
184 for (const point of shape.points) {
185 points.push({
186 latitude: point.latitude,
187 longitude: point.longitude,
188 });
189 }
190
191 shape_descriptors.push({
192 kind: shape.kind,
193 points: points,
194 });
195 }
196
197 fetch("/api/shapes", {
198 method: "POST",
199 body: JSON.stringify(shape_descriptors),
200 }).then(() => {
201 alert("updated");
202 window.location.reload();
203 }).catch((e) => {
204 alert(`failed to update: ${e}`);
205 window.location.reload();
206 });
207}
208
209function page_shape__on_map_click(state, ev) {
210 console.log("clicked on map");
211 if (state.selected_shape == null)
212 return;
213 state.selected_shape.points.splice(state.selected_shape.point_insert_idx, 0, {
214 latitude: ev.latlng.lat,
215 longitude: ev.latlng.lng,
216 });
217 state.selected_shape.point_insert_idx += 1;
218 page_shape__ui_shape_add(state, state.selected_shape);
219}
220
221function page_shape__setup_handlers(state) {
222 lib_setup_handler_onclick(ELEM_ID_BTN_SHAPE_CREATE, () => page_shape__on_shape_create(state));
223 lib_setup_handler_onclick(ELEM_ID_BTN_SHAPE_DELETE, () => page_shape__on_shape_delete(state));
224 lib_setup_handler_onclick(ELEM_ID_BTN_SHAPE_UNBURNED, () => page_shape__on_shape_kind_unburned(state));
225 lib_setup_handler_onclick(ELEM_ID_BTN_SHAPE_BURNED, () => page_shape__on_shape_kind_burned(state));
226 lib_setup_handler_onclick(ELEM_ID_BTN_SHAPES_UPDATE, () => page_shape__on_shapes_update(state));
227
228 state.map.on('click', (ev) => page_shape__on_map_click(state, ev));
229}
230
231function page_shape__ui_shape_remove(state, shape) {
232 for (const circle of shape.poly_points) {
233 state.map.removeLayer(circle);
234 }
235 shape.poly_points = [];
236
237 if (shape.poly != null) {
238 state.map.removeLayer(shape.poly);
239 shape.poly.remove();
240 shape.poly = null;
241 }
242}
243
244function page_shape__ui_shape_select(state, shape) {
245 const prev_shape = state.selected_shape;
246 state.selected_shape = shape;
247 page_shape__ui_shape_add(state, shape);
248 if (prev_shape != null)
249 page_shape__ui_shape_add(state, prev_shape);
250}
251
252function page_shape__ui_shape_add(state, shape) {
253 page_shape__ui_shape_remove(state, shape);
254
255 const color = lib_shape_color_for_kind(shape.kind);
256 const positions = [];
257 for (var i = 0; i < shape.points.length; i += 1) {
258 const point = shape.points[i];
259 const highlight_point = shape.point_insert_idx - 1 == i;
260 const coords = [point.latitude, point.longitude];
261 const circle_color = highlight_point ? 'blue' : 'red';
262 const circle_idx = i;
263 console.assert(point.latitude != null, "invalid point latitude")
264 console.assert(point.longitude != null, "invalid point longitude")
265
266 positions.push(coords);
267 const circle = L.circle(coords, { radius: 15, color: circle_color, bubblingMouseEvents: false })
268 .on('click', (e) => {
269 if (e.originalEvent.shiftKey) {
270 shape.points.splice(circle_idx, 1);
271 shape.point_insert_idx = circle_idx;
272 page_shape__ui_shape_add(state, shape);
273 } else {
274 console.log(`clicked on circle, setting point insert idx to ${circle_idx + 1}`);
275 shape.point_insert_idx = circle_idx + 1;
276 page_shape__ui_shape_add(state, shape);
277 }
278 })
279 .addTo(state.map);
280 shape.poly_points.push(circle);
281 }
282
283 if (positions.length >= 3) {
284 const opacity = state.selected_shape == shape ? 0.2 : 0.04;
285 shape.poly = L.polygon(positions, { color: color, fillOpacity: opacity, bubblingMouseEvents: false })
286 .on('click', () => {
287 page_shape__ui_shape_select(state, shape);
288 })
289 .addTo(state.map);
290 }
291}
292
293async function page_shape__main() {
294 const map = lib_setup_map();
295 const locations = await lib_fetch_location_logs();
296 const shape_descriptors = await lib_fetch_shape_descriptors();
297
298 const state = {
299 map: map,
300 locations: locations,
301 shapes: [],
302 selected_shape: null,
303 };
304 window.state = state; // to allow access from the console
305
306 page_shape__setup_handlers(state);
307 lib_add_location_logs_to_map(state.map, state.locations);
308
309 for (const descriptor of shape_descriptors) {
310 const shape = lib_shape_create_from_descriptor(descriptor);
311 state.shapes.push(shape);
312 page_shape__ui_shape_add(state, shape);
313 }
314}
315
316function page_main__poly_create_from_shape_descriptor(map, shape_descriptor) {
317 const color = lib_shape_color_for_kind(shape_descriptor.kind);
318 const points = []
319 for (const point of shape_descriptor.points) {
320 points.push([point.latitude, point.longitude]);
321 }
322 L.polygon(points, { color: color }).addTo(map);
323}
324
325async function page_index__main() {
326 const map = lib_setup_map();
327 const shape_descriptors = await lib_fetch_shape_descriptors();
328 for (const descriptor of shape_descriptors) {
329 page_main__poly_create_from_shape_descriptor(map, descriptor);
330 }
331}