From f32ab8383beab1e3d040c1614365edd10ba14693 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 11 Jul 2022 16:51:03 +0200 Subject: [PATCH 001/106] gipy : initial commit --- apps/gipy/README.md | 13 ++++++ apps/gipy/app.js | 85 ++++++++++++++++++++++++++++++++++++++++ apps/gipy/gipy.img | Bin 0 -> 1156 bytes apps/gipy/gipy.png | Bin 0 -> 1606 bytes apps/gipy/metadata.json | 22 +++++++++++ 5 files changed, 120 insertions(+) create mode 100644 apps/gipy/README.md create mode 100644 apps/gipy/app.js create mode 100644 apps/gipy/gipy.img create mode 100644 apps/gipy/gipy.png create mode 100644 apps/gipy/metadata.json diff --git a/apps/gipy/README.md b/apps/gipy/README.md new file mode 100644 index 000000000..f28a2ed46 --- /dev/null +++ b/apps/gipy/README.md @@ -0,0 +1,13 @@ +# Gipy + +Development still in progress. Follow compressed gpx traces. +Warns you before reaching intersections and tries to turn off gps. + +## Usage + +WIP. + + +## Creator + +frederic.wagner@imag.fr diff --git a/apps/gipy/app.js b/apps/gipy/app.js new file mode 100644 index 000000000..92ec0ccfc --- /dev/null +++ b/apps/gipy/app.js @@ -0,0 +1,85 @@ + +// screen size is 172x172 +// we want to show 100 meters ahead +// 86 pixels is 100 meters +// 10 meters is 8.6 pixels +// 1 integer unit is 1.1 meter +// 8.6 pixels is 10 / 1.1 integers +// int = 8.6 pix * 1.1 / 10 +// int = 0.946 pixels + +var lat = null; +var lon = null; + +class Path { + constructor(filename) { + let buffer = require("Storage").readArrayBuffer(filename); + this.points_number = (buffer.byteLength - 2*8)/4; + this.view = DataView(buffer); + this.min_lon = this.view.getFloat64(0); + this.min_lat = this.view.getFloat64(8); + this.current_start = 0; // index of first point to be displayed + this.current_x = 0; + this.current_y = 0; + } + + get len() { + return this.points_number; + } +} + +class Point { + constructor(lon, lat) { + this.lon = lon; + this.lat = lat; + } + screen_x() { + return 192/2 + Math.round((this.lon - lon) * 100000.0); + } + screen_y() { + return 192/2 + Math.round((this.lat - lat) * 100000.0); + } +} + +function display(path) { + g.clear(); + let previous_point = null; + let current_x = path.current_x; + let current_y = path.current_y; + for (let i = path.current_start ; i < path.len ; i++) { + current_x += path.view.getInt16(2*8+4*i); + current_y += path.view.getInt16(2*8+4*i+2); + let point = new Point(current_x/100000.0 + path.min_lon, current_y/100000.0 + path.min_lat); + + if (previous_point !== null) { + g.drawLine( + previous_point.screen_x(), + previous_point.screen_y(), + point.screen_x(), + point.screen_y() + ); + } + previous_point = point; + } + g.setColor(1.0, 0.0, 0.0); + g.fillCircle(192/2, 192/2, 5); +} + +let path = new Path("test.gpc"); +lat = path.min_lat; +lon = path.min_lon; + +function set_coordinates(data) { + if (!isNaN(data.lat)) { + lat = data.lat; + } + if (!isNaN(data.lon)) { + lon = data.lon; + } +} + +Bangle.setGPSPower(true, "gipy"); +Bangle.on('GPS', set_coordinates); + +setInterval(display, 1000, path); + diff --git a/apps/gipy/gipy.img b/apps/gipy/gipy.img new file mode 100644 index 0000000000000000000000000000000000000000..9d6a7a9217c6ab57c6e0b17cb8535e24c90b03ef GIT binary patch literal 1156 zcmchVI}*Y$3`E^9xlWFfCikH81#)mKX?JbQ$qWrpFz~#eNY1`rSL%#4(mNw3t2scvQ2IQ3e>p z0Xz<%`|Xbn&~+G?24HufHEWh+$xB8yZ#2ztY8n7^U3aCOKT`lu0l1YZC3upF;7)A?#Cl%ocKpz5V6hfT1bn)f|y}rK9s&=X? zr3(L>2Y|tm$kiZ#`6|An5eWG5_Yg2qEyz>+)$4aU(Oum-ZalHr0U$Xl)W#&IE=k|p z6rCgNb`tdFWyCWN&{?PE0pN*qX`d44S4HUeYvH!fR5`W_kR)D@ALZ`li)W$tSNP_Pu);1oko7EpIdRp1dF6+)21mEspF+eR zStb*n2ILnNi}F`hd%dP3Cje2QHl&)QSfq`PXd0>7{N_n)x=UJ-jdWUQx0f%p52B^a4*+O8G zcYF2Ki}$eOFA8or!z{-F_}i%l6EdaLz72Ri)12hr(+J!_Wx<>w zQpy(CU72FdG^Dm^^WLnjy=EiF8p;p2Uj~5UjONc(@t`*^gGp-!*w&d=ffEs??2?*w ze_A11Dxm5}cF%A)asq%R$u+bjea|@+FR;inmY6?=we5x({vyUCe@)$2iHHfG0iAVm zGaPBatxa4H!QoBYw&iFl?&8q8eIS|zK-#u^H+nIlC9uddHW4vl0jUR%=>)VBqRi91 zuf*N<7KPSr1>$duZ6JjhyDPo3$C!~!r%+jI)@Z@J4?y%UE?Ka9$xF@3QxXFd^m!Tp zo^H>x(&BX?jP$~)4PUFGaYvx)NOn)bya}=90YH$XhO*Xd<@!$m|9d&&0ppZ2)9t~X zr4-v%&7J$ODyl5t*1R`1!Tj*jLvUoX-jb=@*XZ^2Z7f6{0JiNp7Eig7jgTr}I)wrU z-9-!v#0eJ_k8BDi>N()^Y<0S4ih@BJ2Kp=OerewrcM4UuOzOsrQWL>t!NaerPEU71 zu&lFT(hM3)_+#|ru_M}zr679LM6kOdwYL-XN}5c#mRp8I%(AL1psdB+7eosZ3NpNb zL*pMerO?$ImJKEXpW#@^b+eFn-8W{oj14**jFt!{UsKACBjMSF=M zcwbDDil?8-=>%|1B0XjbRX4fgZrpBPsifO7Y0{kr-VoiWEFks#wL$;`Er<@dktPaM z0X;W287ynjL1zvCjRm3gKSejj6c9W-p$~v%)^&OLs@949+qdYtF1mIV4Irp4YVCt? zDI(dY3dIy)7)Ai#E;i&-G>(+r`4TM5>6~`{%oYZ{=D?~7VTO34WX*cz8N~ANTDfQ{ zTR6M7OJEm3!|eN1B20u_f2eMAJVJ+EFa;ORejb8$GSDIGJ_G-NXj)NdUCheU9(D@& zK3*W;%l9x)gV?`@oPn`NxWaGwD>t6)5WNR@fcqH#0oLvGD9P1zN&o-=07*qoM6N<$ Ef?_oAlK=n! literal 0 HcmV?d00001 diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json new file mode 100644 index 000000000..9f31726d8 --- /dev/null +++ b/apps/gipy/metadata.json @@ -0,0 +1,22 @@ +{ + "id": "gipy", + "name": "Gipy", + "shortName": "Gipy", + "version": "0.01", + "description": "Follow gpx files", + "allow_emulator":false, + "icon": "gipy.img", + "type": "app", + "tags": "tool,outdoors,gps", + "screenshots": [{"url":"screenshot_gipy.png"}], + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"gipy.app.js","url":"app.js"}, + {"name":"gipy.img","url":"gipy-icon.js","evaluate":true}, + {"name":"gipy.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"gipy.gpc"} + ] +} From 1b0bd0d014135147e933fefc6153f29dde81ce89 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 11 Jul 2022 17:08:00 +0200 Subject: [PATCH 002/106] test icon --- apps/gipy/metadata.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 9f31726d8..7fbc756b8 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -8,15 +8,13 @@ "icon": "gipy.img", "type": "app", "tags": "tool,outdoors,gps", - "screenshots": [{"url":"screenshot_gipy.png"}], + "screenshots": [], "supports": ["BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"gipy.app.js","url":"app.js"}, - {"name":"gipy.img","url":"gipy-icon.js","evaluate":true}, - {"name":"gipy.settings.js","url":"settings.js"} + {"name":"gipy.img", "url":"gipy.img"} ], "data": [ - {"name":"gipy.gpc"} ] } From afa1dea6287a4ac8d1ae03b31bb916b47a3c76f1 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 11 Jul 2022 17:17:55 +0200 Subject: [PATCH 003/106] gipy: trying adding icon --- apps/gipy/app-icon.js | 1 + apps/gipy/gipy.img | Bin 1156 -> 0 bytes apps/gipy/metadata.json | 4 ++-- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 apps/gipy/app-icon.js delete mode 100644 apps/gipy/gipy.img diff --git a/apps/gipy/app-icon.js b/apps/gipy/app-icon.js new file mode 100644 index 000000000..0fc51609f --- /dev/null +++ b/apps/gipy/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBiIA/AE8VqoAGCy1RiN3CyYuBi93uIXJIBV3AAIuMBY4XjQ5YXPRAIAEOwIABPBC4LF54wGF6IwFC5jWGIwxIJC4xJFgDuJJAxJFC6TEIJBzEHGCIYPGA5JQC44YPGBBJKY4gwRfQL4DGCL4GGCAXPGAxGBAAJIMGAwWCGCoWGC55HHJB5HIC8pGDSChfXC5AWIL5ynOC45GJC4h3IIyYwCFxwADgB1SC44uSC4guSAH4Ab")) diff --git a/apps/gipy/gipy.img b/apps/gipy/gipy.img deleted file mode 100644 index 9d6a7a9217c6ab57c6e0b17cb8535e24c90b03ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1156 zcmchVI}*Y$3`E^9xlWFfCikH81#)mKX?JbQ$qWrpFz~#eNY1`rSL%#4(mNw3t2scvQ2 Date: Tue, 12 Jul 2022 16:45:37 +0200 Subject: [PATCH 004/106] gipy: path following - gps simulation - back to simple but large file format - find nearest segment - avoid printing all segments testing all segments --- apps/gipy/TODO | 20 +++ apps/gipy/app.js | 162 +++++++++++++++++----- apps/gipy/gpconv/.gitignore | 1 + apps/gipy/gpconv/Cargo.lock | 257 +++++++++++++++++++++++++++++++++++ apps/gipy/gpconv/Cargo.toml | 10 ++ apps/gipy/gpconv/src/main.rs | 208 ++++++++++++++++++++++++++++ 6 files changed, 627 insertions(+), 31 deletions(-) create mode 100644 apps/gipy/TODO create mode 100644 apps/gipy/gpconv/.gitignore create mode 100644 apps/gipy/gpconv/Cargo.lock create mode 100644 apps/gipy/gpconv/Cargo.toml create mode 100644 apps/gipy/gpconv/src/main.rs diff --git a/apps/gipy/TODO b/apps/gipy/TODO new file mode 100644 index 000000000..44ef08e23 --- /dev/null +++ b/apps/gipy/TODO @@ -0,0 +1,20 @@ +- plugins +- do not redraw if no coordinates changed +- display distance to target + +- detect reached waypoints +- beep when reaching waypoint +- display distance to next waypoint +- display average speed +- turn off gps when moving to next waypoint +- beep when moving away from path +- dynamic map rescale +- display scale (100m) + +- store several tracks + +- map rotation to match direction +- water points + +- compress path +- avoid display of all segments diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 92ec0ccfc..3b71c7f6a 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -14,17 +14,35 @@ var lon = null; class Path { constructor(filename) { let buffer = require("Storage").readArrayBuffer(filename); - this.points_number = (buffer.byteLength - 2*8)/4; - this.view = DataView(buffer); - this.min_lon = this.view.getFloat64(0); - this.min_lat = this.view.getFloat64(8); - this.current_start = 0; // index of first point to be displayed - this.current_x = 0; - this.current_y = 0; + this.points = Float64Array(buffer); + } + + point(index) { + let lon = this.points[2*index]; + let lat = this.points[2*index+1]; + return new Point(lon, lat); + } + + // return index of segment which is nearest from point + nearest_segment(point, start, end) { + let previous_point = null; + let min_index = 0; + let min_distance = Number.MAX_VALUE; + for(let i = Math.max(0, start) ; i < Math.min(this.len, end) ; i++) { + let current_point = this.point(i); + if (previous_point !== null) { + let distance = point.distance_to_segment(previous_point, current_point); + if (distance <= min_distance) { + min_distance = distance; + min_index = i-1; + } + } + previous_point = current_point; + } + return min_index; } - get len() { - return this.points_number; + return this.points.length /2; } } @@ -34,52 +52,134 @@ class Point { this.lat = lat; } screen_x() { - return 192/2 + Math.round((this.lon - lon) * 100000.0); + return 172/2 + Math.round((this.lon - lon) * 100000.0); } screen_y() { - return 192/2 + Math.round((this.lat - lat) * 100000.0); + return 172/2 + Math.round((this.lat - lat) * 100000.0); + } + minus(other_point) { + let xdiff = this.lon - other_point.lon; + let ydiff = this.lat - other_point.lat; + return new Point(xdiff, ydiff); + } + plus(other_point) { + return new Point(this.lon + other_point.lon, this.lat + other_point.lat); + } + length_squared(other_point) { + let d = this.minus(other_point); + return (d.lon*d.lon + d.lat*d.lat); + } + times(scalar) { + return new Point(this.lon * scalar, this.lat * scalar); + } + dot(other_point) { + return this.lon * other_point.lon + this.lat * other_point.lat; + } + distance(other_point) { + return Math.sqrt(this.length_squared(other_point)); + } + distance_to_segment(v, w) { + // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment + // Return minimum distance between line segment vw and point p + let l2 = v.length_squared(w); // i.e. |w-v|^2 - avoid a sqrt + if (l2 == 0.0) { + return this.distance(v); // v == w case + } + // Consider the line extending the segment, parameterized as v + t (w - v). + // We find projection of point p onto the line. + // It falls where t = [(p-v) . (w-v)] / |w-v|^2 + // We clamp t from [0,1] to handle points outside the segment vw. + let t = Math.max(0, Math.min(1, (this.minus(v)).dot(w.minus(v)) / l2)); + let projection = v.plus((w.minus(v)).times(t)); // Projection falls on the segment + return this.distance(projection); } } function display(path) { g.clear(); + g.setColor(g.theme.fg); + let next_segment = path.nearest_segment(new Point(lon, lat), current_segment-2, current_segment+3); + if (next_segment < current_segment) { + console.log("error going from", current_segment, "back to", next_segment, "at", lon, lat); + console.log("we are at", fake_gps_point); + let previous_point = null; + for (let i = 0 ; i < current_segment+2 ; i++) { + let point = path.point(i); + if (previous_point !== null) { + let distance = new Point(lon, lat).distance_to_segment(previous_point, point); + console.log(i, distance); + } + previous_point = point; + } + } + current_segment = next_segment; + //current_segment = path.nearest_segment(new Point(lon, lat), 0, path.len); let previous_point = null; - let current_x = path.current_x; - let current_y = path.current_y; - for (let i = path.current_start ; i < path.len ; i++) { - current_x += path.view.getInt16(2*8+4*i); - current_y += path.view.getInt16(2*8+4*i+2); - let point = new Point(current_x/100000.0 + path.min_lon, current_y/100000.0 + path.min_lat); + let start = Math.max(current_segment - 4, 0); + let end = Math.min(current_segment + 5, path.len); + for (let i=start ; i < end ; i++) { + let point = path.point(i); + let px = point.screen_x(); + let py = point.screen_y(); if (previous_point !== null) { + if (i == current_segment + 1) { + g.setColor(0.0, 1.0, 0.0); + } else { + g.setColor(1.0, 0.0, 0.0); + } g.drawLine( previous_point.screen_x(), previous_point.screen_y(), - point.screen_x(), - point.screen_y() + px, + py ); } + g.setColor(g.theme.fg2); + g.fillCircle(px, py, 4); + g.setColor(g.theme.fg); + g.fillCircle(px, py, 3); previous_point = point; } - g.setColor(1.0, 0.0, 0.0); - g.fillCircle(192/2, 192/2, 5); + g.setColor(g.theme.fgH); + g.fillCircle(172/2, 172/2, 5); } let path = new Path("test.gpc"); lat = path.min_lat; lon = path.min_lon; +console.log("len is", path.len); +var current_segment = path.nearest_segment(new Point(lon, lat), 0, Number.MAX_VALUE); -function set_coordinates(data) { - if (!isNaN(data.lat)) { - lat = data.lat; - } - if (!isNaN(data.lon)) { - lon = data.lon; +// function set_coordinates(data) { +// if (!isNaN(data.lat)) { +// lat = data.lat; +// } +// if (!isNaN(data.lon)) { +// lon = data.lon; +// } +// } +// Bangle.setGPSPower(true, "gipy"); +// Bangle.on('GPS', set_coordinates); + + + +let fake_gps_point = 0.0; +function simulate_gps(path) { + let point_index = Math.floor(fake_gps_point); + if (point_index >= path.len) { + return; } + let p1 = path.point(point_index); + let p2 = path.point(point_index+1); + let alpha = fake_gps_point - point_index; + + lon = (1-alpha)*p1.lon + alpha*p2.lon; + lat = (1-alpha)*p1.lat + alpha*p2.lat; + fake_gps_point += 0.2; + display(path); } -Bangle.setGPSPower(true, "gipy"); -Bangle.on('GPS', set_coordinates); - -setInterval(display, 1000, path); +setInterval(simulate_gps, 500, path); +//// setInterval(display, 1000, path); diff --git a/apps/gipy/gpconv/.gitignore b/apps/gipy/gpconv/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/apps/gipy/gpconv/.gitignore @@ -0,0 +1 @@ +/target diff --git a/apps/gipy/gpconv/Cargo.lock b/apps/gipy/gpconv/Cargo.lock new file mode 100644 index 000000000..fbe549955 --- /dev/null +++ b/apps/gipy/gpconv/Cargo.lock @@ -0,0 +1,257 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "assert_approx_eq" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c07dab4369547dbe5114677b33fbbf724971019f3818172d59a97a61c774ffd" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "backtrace", + "version_check", +] + +[[package]] +name = "geo-types" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9805fbfcea97de816e6408e938603241879cc41eea3fba3f84f122f4f6f9c54" +dependencies = [ + "num-traits", +] + +[[package]] +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + +[[package]] +name = "gpconv" +version = "0.1.0" +dependencies = [ + "gpx", + "itertools", +] + +[[package]] +name = "gpx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b03599b85866c88fd0125db7ca7a683be1550724918682c736c7893a399dc5e" +dependencies = [ + "assert_approx_eq", + "error-chain", + "geo-types", + "thiserror", + "time", + "xml-rs", +] + +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "miniz_oxide" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +dependencies = [ + "adler", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.28.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" +dependencies = [ + "memchr", +] + +[[package]] +name = "proc-macro2" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "syn" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" +dependencies = [ + "itoa", + "libc", + "num_threads", +] + +[[package]] +name = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" diff --git a/apps/gipy/gpconv/Cargo.toml b/apps/gipy/gpconv/Cargo.toml new file mode 100644 index 000000000..69891c99c --- /dev/null +++ b/apps/gipy/gpconv/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "gpconv" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +gpx="*" +itertools="*" \ No newline at end of file diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs new file mode 100644 index 000000000..a60924a53 --- /dev/null +++ b/apps/gipy/gpconv/src/main.rs @@ -0,0 +1,208 @@ +use itertools::Itertools; +use std::fs::File; +use std::io::{BufReader, Write}; +use std::path::Path; + +use gpx::read; +use gpx::{Gpx, Track, TrackSegment}; + +fn points(filename: &str) -> impl Iterator { + // This XML file actually exists — try it for yourself! + let file = File::open(filename).unwrap(); + let reader = BufReader::new(file); + + // read takes any io::Read and gives a Result. + let mut gpx: Gpx = read(reader).unwrap(); + eprintln!("we have {} tracks", gpx.tracks.len()); + + gpx.tracks + .pop() + .unwrap() + .segments + .into_iter() + .flat_map(|segment| segment.linestring().points().collect::>()) + .map(|point| (point.x(), point.y())) +} + +// returns distance from point p to line passing through points p1 and p2 +// see https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line +fn distance_to_line(p: &(f64, f64), p1: &(f64, f64), p2: &(f64, f64)) -> f64 { + let (x0, y0) = *p; + let (x1, y1) = *p1; + let (x2, y2) = *p2; + let dx = x2 - x1; + let dy = y2 - y1; + (dx * (y1 - y0) - dy * (x1 - x0)).abs() / (dx * dx + dy * dy).sqrt() +} + +fn rdp(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { + if points.len() <= 2 { + points.iter().copied().collect() + } else { + let (index_farthest, farthest_distance) = points + .iter() + .map(|p| distance_to_line(p, points.first().unwrap(), points.last().unwrap())) + .enumerate() + .max_by(|(_, d1), (_, d2)| d1.partial_cmp(d2).unwrap()) + .unwrap(); + if farthest_distance <= epsilon { + vec![ + points.first().copied().unwrap(), + points.last().copied().unwrap(), + ] + } else { + let (start, end) = points.split_at(index_farthest); + let mut res = rdp(start, epsilon); + res.append(&mut rdp(end, epsilon)); + res + } + } +} + +fn convert_coordinates(points: &[(f64, f64)]) -> (f64, f64, Vec<(i32, i32)>) { + let xmin = points + .iter() + .map(|(x, _)| x) + .min_by(|x1, x2| x1.partial_cmp(x2).unwrap()) + .unwrap(); + + let ymin = points + .iter() + .map(|(_, y)| y) + .min_by(|y1, y2| y1.partial_cmp(y2).unwrap()) + .unwrap(); + + // 0.00001 is 1 meter + // max distance is 1000km + // so we need at most 10^6 + ( + *xmin, + *ymin, + points + .iter() + .map(|(x, y)| { + eprintln!("x {} y {}", x, y); + let r = ( + ((*x - xmin) * 100_000.0) as i32, + ((*y - ymin) * 100_000.0) as i32, + ); + eprintln!( + "again x {} y {}", + xmin + r.0 as f64 / 100_000.0, + ymin + r.1 as f64 / 100_000.0 + ); + r + }) + .collect(), + ) +} + +fn compress_coordinates(points: &[(i32, i32)]) -> Vec<(i16, i16)> { + // we could store the diffs such that + // diffs are either 8bits or 16bits nums + // we store how many nums are 16bits + // then all their indices (compressed with diffs) + // then all nums as either 8 or 16bits + let xdiffs = std::iter::once(0).chain( + points + .iter() + .map(|(x, _)| x) + .tuple_windows() + .map(|(x1, x2)| (x2 - x1) as i16), + ); + + let ydiffs = std::iter::once(0).chain( + points + .iter() + .map(|(_, y)| y) + .tuple_windows() + .map(|(y1, y2)| (y2 - y1) as i16), + ); + + xdiffs.zip(ydiffs).collect() +} + +fn save_coordinates>( + path: P, + //xmin: f64, + //ymin: f64, + // points: &[(i32, i32)], + points: &[(f64, f64)], +) -> std::io::Result<()> { + let mut writer = std::io::BufWriter::new(File::create(path)?); + + eprintln!("saving {} points", points.len()); + // writer.write_all(&xmin.to_be_bytes())?; + // writer.write_all(&ymin.to_be_bytes())?; + points + .iter() + .flat_map(|(x, y)| [x, y]) + .try_for_each(|c| writer.write_all(&c.to_le_bytes()))?; + + Ok(()) +} + +fn save_json>(path: P, points: &[(f64, f64)]) -> std::io::Result<()> { + let mut writer = std::io::BufWriter::new(File::create(path)?); + + eprintln!("saving {} points", points.len()); + writeln!(&mut writer, "[")?; + points + .iter() + .map(|(x, y)| format!("{{\"lat\": {}, \"lon\":{}}}", y, x)) + .intersperse_with(|| ",\n".to_string()) + .try_for_each(|s| write!(&mut writer, "{}", s))?; + write!(&mut writer, "]")?; + + Ok(()) +} + +fn main() { + let input_file = std::env::args().nth(2).unwrap_or("m.gpx".to_string()); + let p = points(&input_file).collect::>(); + let rp = rdp(&p, 0.001); + // let rp = rdp(&p, 0.0001); + save_coordinates("test.gpc", &rp).unwrap(); + return; + eprintln!("we go from {} to {}", p.len(), rp.len()); + + //TODO: assert we don't wrap around the globe + let (xmin, ymin, p) = convert_coordinates(&rp); + // let diffs = compress_coordinates(&p); + + // save_coordinates("test.gpc", xmin, ymin, &p).unwrap(); + + // // compress_coordinates(&p); + // let (xmin, xmax) = p + // .iter() + // .map(|&(x, _)| x) + // .minmax_by(|a, b| a.partial_cmp(b).unwrap()) + // .into_option() + // .unwrap(); + + // let (ymin, ymax) = p + // .iter() + // .map(|&(_, y)| y) + // .minmax_by(|a, b| a.partial_cmp(b).unwrap()) + // .into_option() + // .unwrap(); + + // println!( + // "", + // xmin, + // ymin, + // xmax - xmin, + // ymax - ymin + // ); + // print!( + // "", + // xmin, + // ymin, + // xmax - xmin, + // ymax - ymin + // ); + // print!(""); + // println!(""); +} From 46c6f8cd4defdedbdb7a92d582b21f74cdb3a4a2 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 12 Jul 2022 16:57:34 +0200 Subject: [PATCH 005/106] gipy: testing new code --- apps/gipy/TODO | 2 -- apps/gipy/app.js | 85 ++++++++++++++++++++++++------------------------ 2 files changed, 42 insertions(+), 45 deletions(-) diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 44ef08e23..47019376c 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,5 +1,3 @@ -- plugins -- do not redraw if no coordinates changed - display distance to target - detect reached waypoints diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 3b71c7f6a..6a9b53500 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -10,6 +10,7 @@ var lat = null; var lon = null; +var refresh_needed = false; class Path { constructor(filename) { @@ -96,27 +97,17 @@ class Point { } function display(path) { + if (!refresh_needed) { + return; + } + refresh_needed = false; g.clear(); g.setColor(g.theme.fg); - let next_segment = path.nearest_segment(new Point(lon, lat), current_segment-2, current_segment+3); - if (next_segment < current_segment) { - console.log("error going from", current_segment, "back to", next_segment, "at", lon, lat); - console.log("we are at", fake_gps_point); - let previous_point = null; - for (let i = 0 ; i < current_segment+2 ; i++) { - let point = path.point(i); - if (previous_point !== null) { - let distance = new Point(lon, lat).distance_to_segment(previous_point, point); - console.log(i, distance); - } - previous_point = point; - } - } - current_segment = next_segment; - //current_segment = path.nearest_segment(new Point(lon, lat), 0, path.len); + // let next_segment = path.nearest_segment(new Point(lon, lat), current_segment-2, current_segment+3); + current_segment = path.nearest_segment(new Point(lon, lat), 0, path.len); let previous_point = null; - let start = Math.max(current_segment - 4, 0); - let end = Math.min(current_segment + 5, path.len); + let start = Math.max(current_segment - 5, 0); + let end = Math.min(current_segment + 7, path.len); for (let i=start ; i < end ; i++) { let point = path.point(i); @@ -143,43 +134,51 @@ function display(path) { } g.setColor(g.theme.fgH); g.fillCircle(172/2, 172/2, 5); + Bangle.drawWidgets(); } +Bangle.loadWidgets() let path = new Path("test.gpc"); lat = path.min_lat; lon = path.min_lon; -console.log("len is", path.len); var current_segment = path.nearest_segment(new Point(lon, lat), 0, Number.MAX_VALUE); -// function set_coordinates(data) { -// if (!isNaN(data.lat)) { -// lat = data.lat; -// } -// if (!isNaN(data.lon)) { -// lon = data.lon; + +// let fake_gps_point = 0.0; +// function simulate_gps(path) { +// let point_index = Math.floor(fake_gps_point); +// if (point_index >= path.len) { +// return; // } +// let p1 = path.point(point_index); +// let p2 = path.point(point_index+1); +// let alpha = fake_gps_point - point_index; + +// lon = (1-alpha)*p1.lon + alpha*p2.lon; +// lat = (1-alpha)*p1.lat + alpha*p2.lat; +// fake_gps_point += 0.2; +// display(path); // } -// Bangle.setGPSPower(true, "gipy"); -// Bangle.on('GPS', set_coordinates); + +// setInterval(simulate_gps, 500, path); - -let fake_gps_point = 0.0; -function simulate_gps(path) { - let point_index = Math.floor(fake_gps_point); - if (point_index >= path.len) { - return; +function set_coordinates(data) { + let old_lat = lat; + if (!isNaN(data.lat)) { + lat = data.lat; + } + let old_lon = lon; + if (!isNaN(data.lon)) { + lon = data.lon; + } + if ((old_lat != lat)||(old_lon != lon)) { + refresh_needed = true; } - let p1 = path.point(point_index); - let p2 = path.point(point_index+1); - let alpha = fake_gps_point - point_index; - - lon = (1-alpha)*p1.lon + alpha*p2.lon; - lat = (1-alpha)*p1.lat + alpha*p2.lat; - fake_gps_point += 0.2; - display(path); } +Bangle.setGPSPower(true, "gipy"); +Bangle.on('GPS', set_coordinates); -setInterval(simulate_gps, 500, path); -//// setInterval(display, 1000, path); + +setInterval(display, 1000, path); From ed84d25846c17519d25e8f39a9c3dc385982340c Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 12 Jul 2022 17:01:35 +0200 Subject: [PATCH 006/106] gipy: change res --- apps/gipy/app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 6a9b53500..6acf35312 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -53,10 +53,10 @@ class Point { this.lat = lat; } screen_x() { - return 172/2 + Math.round((this.lon - lon) * 100000.0); + return 172/2 + Math.round((this.lon - lon) * 20000.0); } screen_y() { - return 172/2 + Math.round((this.lat - lat) * 100000.0); + return 172/2 + Math.round((this.lat - lat) * 20000.0); } minus(other_point) { let xdiff = this.lon - other_point.lon; From a1705964c24ec44e3a313a2bbbc75426665c3044 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 12 Jul 2022 20:07:25 +0200 Subject: [PATCH 007/106] fix in gpconv --- apps/gipy/app.js | 4 +--- apps/gipy/gpconv/src/main.rs | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 6acf35312..d2068d1d2 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -137,10 +137,8 @@ function display(path) { Bangle.drawWidgets(); } -Bangle.loadWidgets() +Bangle.loadWidgets(); let path = new Path("test.gpc"); -lat = path.min_lat; -lon = path.min_lon; var current_segment = path.nearest_segment(new Point(lon, lat), 0, Number.MAX_VALUE); diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index a60924a53..a19f0530d 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -158,7 +158,8 @@ fn save_json>(path: P, points: &[(f64, f64)]) -> std::io::Result< } fn main() { - let input_file = std::env::args().nth(2).unwrap_or("m.gpx".to_string()); + let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); + eprintln!("input is {}", input_file); let p = points(&input_file).collect::>(); let rp = rdp(&p, 0.001); // let rp = rdp(&p, 0.0001); From 81226a34d64d293f9a228982e668c35bf282ee02 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 13 Jul 2022 11:53:53 +0200 Subject: [PATCH 008/106] gipy : compute distances --- apps/gipy/app.js | 169 ++++++++++++++++++++++++++--------------------- 1 file changed, 93 insertions(+), 76 deletions(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index d2068d1d2..0a6df824f 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -10,36 +10,48 @@ var lat = null; var lon = null; -var refresh_needed = false; class Path { constructor(filename) { let buffer = require("Storage").readArrayBuffer(filename); this.points = Float64Array(buffer); + let total_distance = 0.0; + this.on_segments(function (p1, p2, i) { + total_distance += p1.distance(p2); + }, 0, this.len-1); + this.total_distance = total_distance; } - + + // start is index of first wanted segment + // end is 1 after index of last wanted segment + on_segments(op, start, end) { + let previous_point = null; + for(let i = start ; i < end + 1 ; i++) { + let point = new Point(this.points[2*i], this.points[2*i+1]); + if (previous_point !== null) { + op(previous_point, point, i); + } + previous_point = point; + } + } + point(index) { let lon = this.points[2*index]; let lat = this.points[2*index+1]; return new Point(lon, lat); } - + // return index of segment which is nearest from point nearest_segment(point, start, end) { - let previous_point = null; let min_index = 0; let min_distance = Number.MAX_VALUE; - for(let i = Math.max(0, start) ; i < Math.min(this.len, end) ; i++) { - let current_point = this.point(i); - if (previous_point !== null) { - let distance = point.distance_to_segment(previous_point, current_point); + this.on_segments(function (p1, p2, i) { + let distance = point.fake_distance_to_segment(p1, p2); if (distance <= min_distance) { min_distance = distance; min_index = i-1; } - } - previous_point = current_point; - } + }, start, end); return min_index; } get len() { @@ -77,9 +89,24 @@ class Point { return this.lon * other_point.lon + this.lat * other_point.lat; } distance(other_point) { + //see https://www.movable-type.co.uk/scripts/latlong.html + const R = 6371e3; // metres + const phi1 = this.lat * Math.PI/180; + const phi2 = other_point.lat * Math.PI/180; + const deltaphi = (other_point.lat-this.lat) * Math.PI/180; + const deltalambda = (other_point.lon-this.lon) * Math.PI/180; + + const a = Math.sin(deltaphi/2) * Math.sin(deltaphi/2) + + Math.cos(phi1) * Math.cos(phi2) * + Math.sin(deltalambda/2) * Math.sin(deltalambda/2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + + return R * c; // in metres + } + fake_distance(other_point) { return Math.sqrt(this.length_squared(other_point)); } - distance_to_segment(v, w) { + fake_distance_to_segment(v, w) { // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment // Return minimum distance between line segment vw and point p let l2 = v.length_squared(w); // i.e. |w-v|^2 - avoid a sqrt @@ -87,96 +114,86 @@ class Point { return this.distance(v); // v == w case } // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. + // We find projection of point p onto the line. // It falls where t = [(p-v) . (w-v)] / |w-v|^2 // We clamp t from [0,1] to handle points outside the segment vw. let t = Math.max(0, Math.min(1, (this.minus(v)).dot(w.minus(v)) / l2)); let projection = v.plus((w.minus(v)).times(t)); // Projection falls on the segment - return this.distance(projection); + return this.fake_distance(projection); } } function display(path) { - if (!refresh_needed) { - return; - } - refresh_needed = false; g.clear(); g.setColor(g.theme.fg); // let next_segment = path.nearest_segment(new Point(lon, lat), current_segment-2, current_segment+3); - current_segment = path.nearest_segment(new Point(lon, lat), 0, path.len); - let previous_point = null; + current_segment = path.nearest_segment(new Point(lon, lat), 0, path.len-1); let start = Math.max(current_segment - 5, 0); - let end = Math.min(current_segment + 7, path.len); - for (let i=start ; i < end ; i++) { - let point = path.point(i); - - let px = point.screen_x(); - let py = point.screen_y(); - if (previous_point !== null) { - if (i == current_segment + 1) { - g.setColor(0.0, 1.0, 0.0); - } else { - g.setColor(1.0, 0.0, 0.0); - } - g.drawLine( - previous_point.screen_x(), - previous_point.screen_y(), - px, - py - ); + let end = Math.min(current_segment + 7, path.len-1); + path.on_segments(function(p1, p2, i) { + let px = p2.screen_x(); + let py = p2.screen_y(); + if (i == current_segment + 1) { + g.setColor(0.0, 1.0, 0.0); + } else { + g.setColor(1.0, 0.0, 0.0); } - g.setColor(g.theme.fg2); - g.fillCircle(px, py, 4); + g.drawLine( + p1.screen_x(), + p1.screen_y(), + px, + py + ); g.setColor(g.theme.fg); + g.fillCircle(px, py, 4); + g.setColor(g.theme.bg); g.fillCircle(px, py, 3); - previous_point = point; - } + }, 0, path.len-1); + g.setColor(g.theme.fgH); g.fillCircle(172/2, 172/2, 5); + g.setFont("6x8:2").drawString(("distance "+(Math.round(path.total_distance/100)/10))+" km",0,30); Bangle.drawWidgets(); } Bangle.loadWidgets(); let path = new Path("test.gpc"); -var current_segment = path.nearest_segment(new Point(lon, lat), 0, Number.MAX_VALUE); +var current_segment = path.nearest_segment(new Point(lon, lat), 0, path.len-1); -// let fake_gps_point = 0.0; -// function simulate_gps(path) { -// let point_index = Math.floor(fake_gps_point); -// if (point_index >= path.len) { -// return; -// } -// let p1 = path.point(point_index); -// let p2 = path.point(point_index+1); -// let alpha = fake_gps_point - point_index; - -// lon = (1-alpha)*p1.lon + alpha*p2.lon; -// lat = (1-alpha)*p1.lat + alpha*p2.lat; -// fake_gps_point += 0.2; -// display(path); -// } - -// setInterval(simulate_gps, 500, path); - - -function set_coordinates(data) { - let old_lat = lat; - if (!isNaN(data.lat)) { - lat = data.lat; - } - let old_lon = lon; - if (!isNaN(data.lon)) { - lon = data.lon; - } - if ((old_lat != lat)||(old_lon != lon)) { - refresh_needed = true; +let fake_gps_point = 0.0; +function simulate_gps(path) { + let point_index = Math.floor(fake_gps_point); + if (point_index >= path.len) { + return; } + let p1 = path.point(point_index); + let p2 = path.point(point_index+1); + let alpha = fake_gps_point - point_index; + + lon = (1-alpha)*p1.lon + alpha*p2.lon; + lat = (1-alpha)*p1.lat + alpha*p2.lat; + fake_gps_point += 0.2; + display(path); } -Bangle.setGPSPower(true, "gipy"); -Bangle.on('GPS', set_coordinates); + +setInterval(simulate_gps, 500, path); -setInterval(display, 1000, path); +// function set_coordinates(data) { +// let old_lat = lat; +// if (!isNaN(data.lat)) { +// lat = data.lat; +// } +// let old_lon = lon; +// if (!isNaN(data.lon)) { +// lon = data.lon; +// } +// if ((old_lat != lat)||(old_lon != lon)) { +// display(path); +// } +// } +// Bangle.setGPSPower(true, "gipy"); +// Bangle.on('GPS', set_coordinates); + From 14b273a8341a994b31b0d6589354affb49e59183 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 13 Jul 2022 13:10:42 +0200 Subject: [PATCH 009/106] gipy: distances --- apps/gipy/app.js | 108 ++++++++++++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 49 deletions(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 0a6df824f..6b70c41a4 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,12 +1,4 @@ -// screen size is 172x172 -// we want to show 100 meters ahead -// 86 pixels is 100 meters -// 10 meters is 8.6 pixels -// 1 integer unit is 1.1 meter -// 8.6 pixels is 10 / 1.1 integers -// int = 8.6 pix * 1.1 / 10 -// int = 0.946 pixels var lat = null; var lon = null; @@ -15,11 +7,18 @@ class Path { constructor(filename) { let buffer = require("Storage").readArrayBuffer(filename); this.points = Float64Array(buffer); - let total_distance = 0.0; - this.on_segments(function (p1, p2, i) { - total_distance += p1.distance(p2); - }, 0, this.len-1); - this.total_distance = total_distance; + this.total_distance = this.segments_length(0, this.len-1); + } + + // return cumulated length of wanted segments (in km). + // start is index of first wanted segment + // end is 1 after index of last wanted segment + segments_length(start, end) { + let total = 0.0; + this.on_segments(function(p1, p2, i) { + total += p1.distance(p2); + }, start, end); + return total; } // start is index of first wanted segment @@ -126,8 +125,17 @@ class Point { function display(path) { g.clear(); g.setColor(g.theme.fg); - // let next_segment = path.nearest_segment(new Point(lon, lat), current_segment-2, current_segment+3); - current_segment = path.nearest_segment(new Point(lon, lat), 0, path.len-1); + let next_segment = path.nearest_segment(new Point(lon, lat), 0, path.len-1); + let diff; + if (next_segment != current_segment) { + if (next_segment > current_segment) { + diff = path.segments_length(current_segment+1, next_segment+1); + } else { + diff = -path.segments_length(next_segment+1, current_segment+1); + } + remaining_distance -= diff; + current_segment = next_segment; + } let start = Math.max(current_segment - 5, 0); let end = Math.min(current_segment + 7, path.len-1); path.on_segments(function(p1, p2, i) { @@ -152,48 +160,50 @@ function display(path) { g.setColor(g.theme.fgH); g.fillCircle(172/2, 172/2, 5); - g.setFont("6x8:2").drawString(("distance "+(Math.round(path.total_distance/100)/10))+" km",0,30); + let real_remaining_distance = remaining_distance + path.point(current_segment+1).distance(new Point(lon, lat)); + let rounded_distance = Math.round(real_remaining_distance/100)/10; + let total = Math.round(path.total_distance/100)/10; + g.setFont("6x8:2").drawString("d. "+rounded_distance + "/" + total, 0, 30); + g.drawString("seg." + (current_segment+1) + "/" + path.len, 0, 48); Bangle.drawWidgets(); } Bangle.loadWidgets(); let path = new Path("test.gpc"); var current_segment = path.nearest_segment(new Point(lon, lat), 0, path.len-1); +var remaining_distance = path.total_distance - path.segments_length(0, 1); - -let fake_gps_point = 0.0; -function simulate_gps(path) { - let point_index = Math.floor(fake_gps_point); - if (point_index >= path.len) { - return; - } - let p1 = path.point(point_index); - let p2 = path.point(point_index+1); - let alpha = fake_gps_point - point_index; - - lon = (1-alpha)*p1.lon + alpha*p2.lon; - lat = (1-alpha)*p1.lat + alpha*p2.lat; - fake_gps_point += 0.2; - display(path); -} - -setInterval(simulate_gps, 500, path); - - -// function set_coordinates(data) { -// let old_lat = lat; -// if (!isNaN(data.lat)) { -// lat = data.lat; -// } -// let old_lon = lon; -// if (!isNaN(data.lon)) { -// lon = data.lon; -// } -// if ((old_lat != lat)||(old_lon != lon)) { -// display(path); +// let fake_gps_point = 0.0; +// function simulate_gps(path) { +// let point_index = Math.floor(fake_gps_point); +// if (point_index >= path.len) { +// return; // } +// let p1 = path.point(point_index); +// let p2 = path.point(point_index+1); +// let alpha = fake_gps_point - point_index; +// +// lon = (1-alpha)*p1.lon + alpha*p2.lon; +// lat = (1-alpha)*p1.lat + alpha*p2.lat; +// fake_gps_point += 0.2; +// display(path); // } -// Bangle.setGPSPower(true, "gipy"); -// Bangle.on('GPS', set_coordinates); +// +// setInterval(simulate_gps, 500, path); +function set_coordinates(data) { + let old_lat = lat; + if (!isNaN(data.lat)) { + lat = data.lat; + } + let old_lon = lon; + if (!isNaN(data.lon)) { + lon = data.lon; + } + if ((old_lat != lat)||(old_lon != lon)) { + display(path); + } +} +Bangle.setGPSPower(true, "gipy"); +Bangle.on('GPS', set_coordinates); From 20712747c731051feb3314903b547191426d07ed Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 13 Jul 2022 13:11:07 +0200 Subject: [PATCH 010/106] gipy: bump version --- apps/gipy/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 96a068194..753fc7f7f 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.01", + "version": "0.02", "description": "Follow gpx files", "allow_emulator":false, "icon": "gipy.png", From 1f76505f5f3a993a2dee428f46bf1d7d4e29619e Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sat, 16 Jul 2022 14:42:48 +0200 Subject: [PATCH 011/106] gipy: prog dyn path simplification --- apps/gipy/gpconv/src/main.rs | 341 +++++++++++++++++++++++++++++------ 1 file changed, 287 insertions(+), 54 deletions(-) diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index a19f0530d..4f2d4a079 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -1,4 +1,5 @@ use itertools::Itertools; +use std::collections::HashMap; use std::fs::File; use std::io::{BufReader, Write}; use std::path::Path; @@ -6,6 +7,14 @@ use std::path::Path; use gpx::read; use gpx::{Gpx, Track, TrackSegment}; +fn squared_distance_between(p1: &(f64, f64), p2: &(f64, f64)) -> f64 { + let (x1, y1) = *p1; + let (x2, y2) = *p2; + let dx = x2 - x1; + let dy = y2 - y1; + dx * dx + dy * dy +} + fn points(filename: &str) -> impl Iterator { // This XML file actually exists — try it for yourself! let file = File::open(filename).unwrap(); @@ -22,6 +31,7 @@ fn points(filename: &str) -> impl Iterator { .into_iter() .flat_map(|segment| segment.linestring().points().collect::>()) .map(|point| (point.x(), point.y())) + .dedup() } // returns distance from point p to line passing through points p1 and p2 @@ -32,33 +42,193 @@ fn distance_to_line(p: &(f64, f64), p1: &(f64, f64), p2: &(f64, f64)) -> f64 { let (x2, y2) = *p2; let dx = x2 - x1; let dy = y2 - y1; + //TODO: remove this division by computing fake distances (dx * (y1 - y0) - dy * (x1 - x0)).abs() / (dx * dx + dy * dy).sqrt() } +fn acceptable_angles(p1: &(f64, f64), p2: &(f64, f64), epsilon: f64) -> (f64, f64) { + // first, convert p2's coordinates for p1 as origin + let (x1, y1) = *p1; + let (x2, y2) = *p2; + let (x, y) = (x2 - x1, y2 - y1); + // rotate so that (p1, p2) ends on x axis + let theta = y.atan2(x); + let rx = x * theta.cos() - y * theta.sin(); + let ry = x * theta.sin() + y * theta.cos(); + assert!(ry.abs() <= std::f64::EPSILON); + + // now imagine a line at an angle alpha. + // we want the distance d from (rx, 0) to our line + // we have sin(alpha) = d / rx + // limiting d to epsilon, we solve + // sin(alpha) = e / rx + // and get + // alpha = arcsin(e/rx) + let alpha = (epsilon / rx).asin(); + + // now we just need to rotate back + let a1 = theta + alpha.abs(); + let a2 = theta - alpha.abs(); + assert!(a1 >= a2); + (a1, a2) +} + +// this is like ramer douglas peucker algorithm +// except that we advance from the start without knowing the end. +// each point we meet constrains the chosen segment's angle +// a bit more. +// +fn simplify(mut points: &[(f64, f64)]) -> Vec<(f64, f64)> { + let mut remaining_points = Vec::new(); + while !points.is_empty() { + let (sx, sy) = points.first().unwrap(); + let i = match points + .iter() + .enumerate() + .map(|(i, (x, y))| todo!("compute angles")) + .try_fold( + (0.0f64, std::f64::consts::FRAC_2_PI), + |(amin, amax), (i, (amin2, amax2))| -> Result<(f64, f64), usize> { + let new_amax = amax.min(amax2); + let new_amin = amin.max(amin2); + if new_amin >= new_amax { + Err(i) + } else { + Ok((new_amin, new_amax)) + } + }, + ) { + Err(i) => i, + Ok(_) => points.len(), + }; + remaining_points.push(points.first().cloned().unwrap()); + points = &points[i..]; + } + remaining_points +} + +fn extract_prog_dyn_solution( + points: &[(f64, f64)], + start: usize, + end: usize, + cache: &HashMap<(usize, usize), (Option, usize)>, +) -> Vec<(f64, f64)> { + if let Some(choice) = cache.get(&(start, end)).unwrap().0 { + let mut v1 = extract_prog_dyn_solution(points, start, choice + 1, cache); + let mut v2 = extract_prog_dyn_solution(points, choice, end, cache); + v1.pop(); + v1.append(&mut v2); + v1 + } else { + vec![points[start], points[end - 1]] + } +} + +fn simplify_prog_dyn( + points: &[(f64, f64)], + start: usize, + end: usize, + epsilon: f64, + cache: &mut HashMap<(usize, usize), (Option, usize)>, +) -> usize { + if let Some(val) = cache.get(&(start, end)) { + val.1 + } else { + let res = if end - start <= 2 { + assert_eq!(end - start, 2); + (None, end - start) + } else { + let first_point = &points[start]; + let last_point = &points[end - 1]; + if points[(start + 1)..end] + .iter() + .map(|p| distance_to_line(p, first_point, last_point)) + .all(|d| d <= epsilon) + { + (None, 2) + } else { + // now we test all possible cutting points + ((start + 1)..(end - 1)) //TODO: take middle min + .map(|i| { + let v1 = simplify_prog_dyn(points, start, i + 1, epsilon, cache); + let v2 = simplify_prog_dyn(points, i, end, epsilon, cache); + (Some(i), v1 + v2 - 1) + }) + .min_by_key(|(_, v)| *v) + .unwrap() + } + }; + cache.insert((start, end), res); + res.1 + } +} + fn rdp(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { if points.len() <= 2 { points.iter().copied().collect() } else { - let (index_farthest, farthest_distance) = points - .iter() - .map(|p| distance_to_line(p, points.first().unwrap(), points.last().unwrap())) - .enumerate() - .max_by(|(_, d1), (_, d2)| d1.partial_cmp(d2).unwrap()) - .unwrap(); - if farthest_distance <= epsilon { - vec![ - points.first().copied().unwrap(), - points.last().copied().unwrap(), - ] - } else { - let (start, end) = points.split_at(index_farthest); + if points.first().unwrap() == points.last().unwrap() { + let first = points.first().unwrap(); + let index_farthest = points + .iter() + .enumerate() + .skip(1) + .max_by(|(_, p1), (_, p2)| { + squared_distance_between(first, p1) + .partial_cmp(&squared_distance_between(first, p2)) + .unwrap() + }) + .map(|(i, _)| i) + .unwrap(); + + let start = &points[..(index_farthest + 1)]; + let end = &points[index_farthest..]; let mut res = rdp(start, epsilon); + res.pop(); res.append(&mut rdp(end, epsilon)); res + } else { + let (index_farthest, farthest_distance) = points + .iter() + .map(|p| distance_to_line(p, points.first().unwrap(), points.last().unwrap())) + .enumerate() + .max_by(|(_, d1), (_, d2)| { + if d1.is_nan() { + std::cmp::Ordering::Greater + } else { + if d2.is_nan() { + std::cmp::Ordering::Less + } else { + d1.partial_cmp(d2).unwrap() + } + } + }) + .unwrap(); + if farthest_distance <= epsilon { + vec![ + points.first().copied().unwrap(), + points.last().copied().unwrap(), + ] + } else { + let start = &points[..(index_farthest + 1)]; + let end = &points[index_farthest..]; + let mut res = rdp(start, epsilon); + res.pop(); + res.append(&mut rdp(end, epsilon)); + res + } } } } +fn simplify_path(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { + if points.len() <= 600 { + optimal_simplification(points, epsilon) + } else { + hybrid_simplification(points, epsilon) + } +} + fn convert_coordinates(points: &[(f64, f64)]) -> (f64, f64, Vec<(i32, i32)>) { let xmin = points .iter() @@ -157,53 +327,116 @@ fn save_json>(path: P, points: &[(f64, f64)]) -> std::io::Result< Ok(()) } +fn optimal_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { + let mut cache = HashMap::new(); + simplify_prog_dyn(&points, 0, points.len(), epsilon, &mut cache); + extract_prog_dyn_solution(&points, 0, points.len(), &cache) +} + +fn hybrid_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { + if points.len() <= 300 { + optimal_simplification(points, epsilon) + } else { + if points.first().unwrap() == points.last().unwrap() { + let first = points.first().unwrap(); + let index_farthest = points + .iter() + .enumerate() + .skip(1) + .max_by(|(_, p1), (_, p2)| { + squared_distance_between(first, p1) + .partial_cmp(&squared_distance_between(first, p2)) + .unwrap() + }) + .map(|(i, _)| i) + .unwrap(); + + let start = &points[..(index_farthest + 1)]; + let end = &points[index_farthest..]; + let mut res = hybrid_simplification(start, epsilon); + res.pop(); + res.append(&mut hybrid_simplification(end, epsilon)); + res + } else { + let (index_farthest, farthest_distance) = points + .iter() + .map(|p| distance_to_line(p, points.first().unwrap(), points.last().unwrap())) + .enumerate() + .max_by(|(_, d1), (_, d2)| { + if d1.is_nan() { + std::cmp::Ordering::Greater + } else { + if d2.is_nan() { + std::cmp::Ordering::Less + } else { + d1.partial_cmp(d2).unwrap() + } + } + }) + .unwrap(); + if farthest_distance <= epsilon { + vec![ + points.first().copied().unwrap(), + points.last().copied().unwrap(), + ] + } else { + let start = &points[..(index_farthest + 1)]; + let end = &points[index_farthest..]; + let mut res = hybrid_simplification(start, epsilon); + res.pop(); + res.append(&mut hybrid_simplification(end, epsilon)); + res + } + } + } +} + fn main() { let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); eprintln!("input is {}", input_file); let p = points(&input_file).collect::>(); - let rp = rdp(&p, 0.001); - // let rp = rdp(&p, 0.0001); + eprintln!("initialy we have {} points", p.len()); + //eprintln!("opt is {}", optimal_simplification(&p, 0.0005).len()); + let start = std::time::Instant::now(); + let rp = hybrid_simplification(&p, 0.0005); + eprintln!("hybrid took {:?}", start.elapsed()); + eprintln!("we now have {} points", rp.len()); + let start = std::time::Instant::now(); + eprintln!("rdp would have had {}", rdp(&p, 0.0005).len()); + eprintln!("rdp took {:?}", start.elapsed()); + // let rp = rdp(&p, 0.001); save_coordinates("test.gpc", &rp).unwrap(); - return; - eprintln!("we go from {} to {}", p.len(), rp.len()); - //TODO: assert we don't wrap around the globe - let (xmin, ymin, p) = convert_coordinates(&rp); - // let diffs = compress_coordinates(&p); + let (xmin, xmax) = rp + .iter() + .map(|&(x, _)| x) + .minmax_by(|a, b| a.partial_cmp(b).unwrap()) + .into_option() + .unwrap(); - // save_coordinates("test.gpc", xmin, ymin, &p).unwrap(); + let (ymin, ymax) = rp + .iter() + .map(|&(_, y)| y) + .minmax_by(|a, b| a.partial_cmp(b).unwrap()) + .into_option() + .unwrap(); - // // compress_coordinates(&p); - // let (xmin, xmax) = p - // .iter() - // .map(|&(x, _)| x) - // .minmax_by(|a, b| a.partial_cmp(b).unwrap()) - // .into_option() - // .unwrap(); - - // let (ymin, ymax) = p - // .iter() - // .map(|&(_, y)| y) - // .minmax_by(|a, b| a.partial_cmp(b).unwrap()) - // .into_option() - // .unwrap(); - - // println!( - // "", - // xmin, - // ymin, - // xmax - xmin, - // ymax - ymin - // ); - // print!( - // "", - // xmin, - // ymin, - // xmax - xmin, - // ymax - ymin - // ); - // print!(""); - // println!(""); + println!( + "", + xmin, + ymin, + xmax - xmin, + ymax - ymin + ); + print!( + "", + xmin, + ymin, + xmax - xmin, + ymax - ymin + ); + print!(""); + println!(""); } From 531d403ff381060e896bfd9e18ed9529ae08205a Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sat, 16 Jul 2022 15:20:41 +0200 Subject: [PATCH 012/106] working on rotation --- apps/gipy/app.js | 95 ++++++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 44 deletions(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 6b70c41a4..90ac10b1f 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -2,6 +2,9 @@ var lat = null; var lon = null; +var direction = 0; +var cos_direction = Math.cos(direction); +var sin_direction = Math.sin(direction); class Path { constructor(filename) { @@ -63,11 +66,12 @@ class Point { this.lon = lon; this.lat = lat; } - screen_x() { - return 172/2 + Math.round((this.lon - lon) * 20000.0); - } - screen_y() { - return 172/2 + Math.round((this.lat - lat) * 20000.0); + coordinates() { + let x = (this.lon - lon) * 20000.0; + let y = (this.lat - lat) * 20000.0; + let rotated_x = x*cos_direction - y*sin_direction; + let rotated_y = x*sin_direction + y*cos_direction; + return [g.getWidth()/2 + Math.round(rotated_x), g.getHeight()/2 + Math.round(rotated_y)]; } minus(other_point) { let xdiff = this.lon - other_point.lon; @@ -139,23 +143,20 @@ function display(path) { let start = Math.max(current_segment - 5, 0); let end = Math.min(current_segment + 7, path.len-1); path.on_segments(function(p1, p2, i) { - let px = p2.screen_x(); - let py = p2.screen_y(); + let c2 = p2.coordinates(); if (i == current_segment + 1) { g.setColor(0.0, 1.0, 0.0); } else { g.setColor(1.0, 0.0, 0.0); } + let c1 = p1.coordinates(); g.drawLine( - p1.screen_x(), - p1.screen_y(), - px, - py + c1[0], c1[1], c2[0], c2[1] ); g.setColor(g.theme.fg); - g.fillCircle(px, py, 4); + g.fillCircle(c2[0], c2[1], 4); g.setColor(g.theme.bg); - g.fillCircle(px, py, 3); + g.fillCircle(c2[0], c2[1], 3); }, 0, path.len-1); g.setColor(g.theme.fgH); @@ -173,37 +174,43 @@ let path = new Path("test.gpc"); var current_segment = path.nearest_segment(new Point(lon, lat), 0, path.len-1); var remaining_distance = path.total_distance - path.segments_length(0, 1); -// let fake_gps_point = 0.0; -// function simulate_gps(path) { -// let point_index = Math.floor(fake_gps_point); -// if (point_index >= path.len) { -// return; -// } -// let p1 = path.point(point_index); -// let p2 = path.point(point_index+1); -// let alpha = fake_gps_point - point_index; -// -// lon = (1-alpha)*p1.lon + alpha*p2.lon; -// lat = (1-alpha)*p1.lat + alpha*p2.lat; -// fake_gps_point += 0.2; -// display(path); -// } -// -// setInterval(simulate_gps, 500, path); - - -function set_coordinates(data) { - let old_lat = lat; - if (!isNaN(data.lat)) { - lat = data.lat; +let fake_gps_point = 0.0; +function simulate_gps(path) { + let point_index = Math.floor(fake_gps_point); + if (point_index >= path.len) { + return; } + let p1 = path.point(point_index); + let p2 = path.point(point_index+1); + let alpha = fake_gps_point - point_index; + let old_lon = lon; - if (!isNaN(data.lon)) { - lon = data.lon; - } - if ((old_lat != lat)||(old_lon != lon)) { - display(path); - } + let old_lat = lat; + lon = (1-alpha)*p1.lon + alpha*p2.lon; + lat = (1-alpha)*p1.lat + alpha*p2.lat; + fake_gps_point += 0.2; + direction = Math.atan2(lat-old_lat, lon-old_lon); + direction = 0.0; + cos_direction = Math.cos(direction); + sin_direction = Math.sin(direction); + display(path); } -Bangle.setGPSPower(true, "gipy"); -Bangle.on('GPS', set_coordinates); + +setInterval(simulate_gps, 500, path); + + +// function set_coordinates(data) { +// let old_lat = lat; +// if (!isNaN(data.lat)) { +// lat = data.lat; +// } +// let old_lon = lon; +// if (!isNaN(data.lon)) { +// lon = data.lon; +// } +// if ((old_lat != lat)||(old_lon != lon)) { +// display(path); +// } +// } +// Bangle.setGPSPower(true, "gipy"); +// Bangle.on('GPS', set_coordinates); From db46d75c6adc4c2eebde00eeb94b29fad4b86280 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sat, 16 Jul 2022 16:23:01 +0200 Subject: [PATCH 013/106] orientation --- apps/gipy/app.js | 378 ++++++++++++++++++++-------------------- apps/gipy/metadata.json | 2 +- 2 files changed, 194 insertions(+), 186 deletions(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 90ac10b1f..d6444803d 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,216 +1,224 @@ - - var lat = null; var lon = null; -var direction = 0; -var cos_direction = Math.cos(direction); -var sin_direction = Math.sin(direction); +var cos_direction; +var sin_direction; + +let simulated = false; class Path { - constructor(filename) { - let buffer = require("Storage").readArrayBuffer(filename); - this.points = Float64Array(buffer); - this.total_distance = this.segments_length(0, this.len-1); - } + constructor(filename) { + let buffer = require("Storage").readArrayBuffer(filename); + this.points = Float64Array(buffer); + this.total_distance = this.segments_length(0, this.len - 1); + } - // return cumulated length of wanted segments (in km). - // start is index of first wanted segment - // end is 1 after index of last wanted segment - segments_length(start, end) { - let total = 0.0; - this.on_segments(function(p1, p2, i) { - total += p1.distance(p2); - }, start, end); - return total; - } + // return cumulated length of wanted segments (in km). + // start is index of first wanted segment + // end is 1 after index of last wanted segment + segments_length(start, end) { + let total = 0.0; + this.on_segments(function(p1, p2, i) { + total += p1.distance(p2); + }, start, end); + return total; + } - // start is index of first wanted segment - // end is 1 after index of last wanted segment - on_segments(op, start, end) { - let previous_point = null; - for(let i = start ; i < end + 1 ; i++) { - let point = new Point(this.points[2*i], this.points[2*i+1]); - if (previous_point !== null) { - op(previous_point, point, i); - } - previous_point = point; - } - } - - point(index) { - let lon = this.points[2*index]; - let lat = this.points[2*index+1]; - return new Point(lon, lat); - } - - // return index of segment which is nearest from point - nearest_segment(point, start, end) { - let min_index = 0; - let min_distance = Number.MAX_VALUE; - this.on_segments(function (p1, p2, i) { - let distance = point.fake_distance_to_segment(p1, p2); - if (distance <= min_distance) { - min_distance = distance; - min_index = i-1; + // start is index of first wanted segment + // end is 1 after index of last wanted segment + on_segments(op, start, end) { + let previous_point = null; + for (let i = start; i < end + 1; i++) { + let point = new Point(this.points[2 * i], this.points[2 * i + 1]); + if (previous_point !== null) { + op(previous_point, point, i); + } + previous_point = point; } - }, start, end); - return min_index; - } - get len() { - return this.points.length /2; - } + } + + point(index) { + let lon = this.points[2 * index]; + let lat = this.points[2 * index + 1]; + return new Point(lon, lat); + } + + // return index of segment which is nearest from point + nearest_segment(point, start, end) { + let min_index = 0; + let min_distance = Number.MAX_VALUE; + this.on_segments(function(p1, p2, i) { + let distance = point.fake_distance_to_segment(p1, p2); + if (distance <= min_distance) { + min_distance = distance; + min_index = i - 1; + } + }, start, end); + return min_index; + } + get len() { + return this.points.length / 2; + } } class Point { - constructor(lon, lat) { - this.lon = lon; - this.lat = lat; - } - coordinates() { - let x = (this.lon - lon) * 20000.0; - let y = (this.lat - lat) * 20000.0; - let rotated_x = x*cos_direction - y*sin_direction; - let rotated_y = x*sin_direction + y*cos_direction; - return [g.getWidth()/2 + Math.round(rotated_x), g.getHeight()/2 + Math.round(rotated_y)]; - } - minus(other_point) { - let xdiff = this.lon - other_point.lon; - let ydiff = this.lat - other_point.lat; - return new Point(xdiff, ydiff); - } - plus(other_point) { - return new Point(this.lon + other_point.lon, this.lat + other_point.lat); - } - length_squared(other_point) { - let d = this.minus(other_point); - return (d.lon*d.lon + d.lat*d.lat); - } - times(scalar) { - return new Point(this.lon * scalar, this.lat * scalar); - } - dot(other_point) { - return this.lon * other_point.lon + this.lat * other_point.lat; - } - distance(other_point) { - //see https://www.movable-type.co.uk/scripts/latlong.html - const R = 6371e3; // metres - const phi1 = this.lat * Math.PI/180; - const phi2 = other_point.lat * Math.PI/180; - const deltaphi = (other_point.lat-this.lat) * Math.PI/180; - const deltalambda = (other_point.lon-this.lon) * Math.PI/180; + constructor(lon, lat) { + this.lon = lon; + this.lat = lat; + } + coordinates() { + let x = (this.lon - lon) * 20000.0; + let y = (this.lat - lat) * 20000.0; + let rotated_x = x * cos_direction - y * sin_direction; + let rotated_y = x * sin_direction + y * cos_direction; + return [g.getWidth() / 2 - Math.round(rotated_x), // x is inverted + g.getHeight() / 2 + Math.round(rotated_y) + ]; + } + minus(other_point) { + let xdiff = this.lon - other_point.lon; + let ydiff = this.lat - other_point.lat; + return new Point(xdiff, ydiff); + } + plus(other_point) { + return new Point(this.lon + other_point.lon, this.lat + other_point.lat); + } + length_squared(other_point) { + let d = this.minus(other_point); + return (d.lon * d.lon + d.lat * d.lat); + } + times(scalar) { + return new Point(this.lon * scalar, this.lat * scalar); + } + dot(other_point) { + return this.lon * other_point.lon + this.lat * other_point.lat; + } + distance(other_point) { + //see https://www.movable-type.co.uk/scripts/latlong.html + const R = 6371e3; // metres + const phi1 = this.lat * Math.PI / 180; + const phi2 = other_point.lat * Math.PI / 180; + const deltaphi = (other_point.lat - this.lat) * Math.PI / 180; + const deltalambda = (other_point.lon - this.lon) * Math.PI / 180; - const a = Math.sin(deltaphi/2) * Math.sin(deltaphi/2) + + const a = Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) + Math.cos(phi1) * Math.cos(phi2) * - Math.sin(deltalambda/2) * Math.sin(deltalambda/2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + Math.sin(deltalambda / 2) * Math.sin(deltalambda / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c; // in metres - } - fake_distance(other_point) { - return Math.sqrt(this.length_squared(other_point)); - } - fake_distance_to_segment(v, w) { - // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment - // Return minimum distance between line segment vw and point p - let l2 = v.length_squared(w); // i.e. |w-v|^2 - avoid a sqrt - if (l2 == 0.0) { - return this.distance(v); // v == w case - } - // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. - // It falls where t = [(p-v) . (w-v)] / |w-v|^2 - // We clamp t from [0,1] to handle points outside the segment vw. - let t = Math.max(0, Math.min(1, (this.minus(v)).dot(w.minus(v)) / l2)); - let projection = v.plus((w.minus(v)).times(t)); // Projection falls on the segment - return this.fake_distance(projection); - } + return R * c; // in metres + } + fake_distance(other_point) { + return Math.sqrt(this.length_squared(other_point)); + } + fake_distance_to_segment(v, w) { + // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment + // Return minimum distance between line segment vw and point p + let l2 = v.length_squared(w); // i.e. |w-v|^2 - avoid a sqrt + if (l2 == 0.0) { + return this.distance(v); // v == w case + } + // Consider the line extending the segment, parameterized as v + t (w - v). + // We find projection of point p onto the line. + // It falls where t = [(p-v) . (w-v)] / |w-v|^2 + // We clamp t from [0,1] to handle points outside the segment vw. + let t = + Math.max(0, Math.min(1, (this.minus(v)).dot(w.minus(v)) / l2)); + let projection = v.plus((w.minus(v)).times(t)); // Projection falls on the segment + return this.fake_distance(projection); + } } function display(path) { - g.clear(); - g.setColor(g.theme.fg); - let next_segment = path.nearest_segment(new Point(lon, lat), 0, path.len-1); - let diff; - if (next_segment != current_segment) { - if (next_segment > current_segment) { - diff = path.segments_length(current_segment+1, next_segment+1); - } else { - diff = -path.segments_length(next_segment+1, current_segment+1); - } - remaining_distance -= diff; - current_segment = next_segment; - } - let start = Math.max(current_segment - 5, 0); - let end = Math.min(current_segment + 7, path.len-1); - path.on_segments(function(p1, p2, i) { - let c2 = p2.coordinates(); - if (i == current_segment + 1) { - g.setColor(0.0, 1.0, 0.0); - } else { - g.setColor(1.0, 0.0, 0.0); - } - let c1 = p1.coordinates(); - g.drawLine( - c1[0], c1[1], c2[0], c2[1] - ); + g.clear(); g.setColor(g.theme.fg); - g.fillCircle(c2[0], c2[1], 4); - g.setColor(g.theme.bg); - g.fillCircle(c2[0], c2[1], 3); - }, 0, path.len-1); + let next_segment = + path.nearest_segment(new Point(lon, lat), 0, path.len - 1); + let diff; + if (next_segment != current_segment) { + if (next_segment > current_segment) { + diff = path.segments_length(current_segment + 1, next_segment + 1); + } else { + diff = -path.segments_length(next_segment + 1, current_segment + 1); + } + remaining_distance -= diff; + current_segment = next_segment; + } + let start = Math.max(current_segment - 5, 0); + let end = Math.min(current_segment + 7, path.len - 1); + path.on_segments(function(p1, p2, i) { + let c2 = p2.coordinates(); + if (i == current_segment + 1) { + g.setColor(0.0, 1.0, 0.0); + } else { + g.setColor(1.0, 0.0, 0.0); + } + let c1 = p1.coordinates(); + g.drawLine(c1[0], c1[1], c2[0], c2[1]); + g.setColor(g.theme.fg); + g.fillCircle(c2[0], c2[1], 4); + g.setColor(g.theme.bg); + g.fillCircle(c2[0], c2[1], 3); + }, 0, path.len - 1); - g.setColor(g.theme.fgH); - g.fillCircle(172/2, 172/2, 5); - let real_remaining_distance = remaining_distance + path.point(current_segment+1).distance(new Point(lon, lat)); - let rounded_distance = Math.round(real_remaining_distance/100)/10; - let total = Math.round(path.total_distance/100)/10; - g.setFont("6x8:2").drawString("d. "+rounded_distance + "/" + total, 0, 30); - g.drawString("seg." + (current_segment+1) + "/" + path.len, 0, 48); - Bangle.drawWidgets(); + g.setColor(g.theme.fgH); + g.fillCircle(172 / 2, 172 / 2, 5); + let real_remaining_distance = + remaining_distance + path.point(current_segment + 1).distance(new Point(lon, lat)); + let rounded_distance = Math.round(real_remaining_distance / 100) / 10; + let total = Math.round(path.total_distance / 100) / 10; + g.setFont("6x8:2").drawString("d. " + rounded_distance + "/" + total, 0, g.getHeight() - 32); + g.drawString("seg." + (current_segment + 1) + "/" + path.len, 0, g.getHeight() - 15); + Bangle.drawWidgets(); } Bangle.loadWidgets(); let path = new Path("test.gpc"); -var current_segment = path.nearest_segment(new Point(lon, lat), 0, path.len-1); +var current_segment = path.nearest_segment(new Point(lon, lat), 0, path.len - 1); var remaining_distance = path.total_distance - path.segments_length(0, 1); +function set_coordinates(data) { + let old_lat = lat; + if (!isNaN(data.lat)) { + lat = data.lat; + } + let old_lon = lon; + if (!isNaN(data.lon)) { + lon = data.lon; + } + if ((old_lat != lat) || (old_lon != lon)) { + let direction = data.course * Math.PI / 180.0; + cos_direction = Math.cos(-direction - Math.PI / 2.0); + sin_direction = Math.sin(-direction - Math.PI / 2.0); + display(path); + } +} + let fake_gps_point = 0.0; function simulate_gps(path) { - let point_index = Math.floor(fake_gps_point); - if (point_index >= path.len) { - return; - } - let p1 = path.point(point_index); - let p2 = path.point(point_index+1); - let alpha = fake_gps_point - point_index; + let point_index = Math.floor(fake_gps_point); + if (point_index >= path.len) { + return; + } + let p1 = path.point(point_index); + let p2 = path.point(point_index + 1); + let alpha = fake_gps_point - point_index; - let old_lon = lon; - let old_lat = lat; - lon = (1-alpha)*p1.lon + alpha*p2.lon; - lat = (1-alpha)*p1.lat + alpha*p2.lat; - fake_gps_point += 0.2; - direction = Math.atan2(lat-old_lat, lon-old_lon); - direction = 0.0; - cos_direction = Math.cos(direction); - sin_direction = Math.sin(direction); - display(path); + let old_lon = lon; + let old_lat = lat; + lon = (1 - alpha) * p1.lon + alpha * p2.lon; + lat = (1 - alpha) * p1.lat + alpha * p2.lat; + fake_gps_point += 0.05; + let direction = Math.atan2(lat - old_lat, lon - old_lon); + cos_direction = Math.cos(-direction - Math.PI / 2.0); + sin_direction = Math.sin(-direction - Math.PI / 2.0); + display(path); } -setInterval(simulate_gps, 500, path); +if (simulated) { + setInterval(simulate_gps, 500, path); -// function set_coordinates(data) { -// let old_lat = lat; -// if (!isNaN(data.lat)) { -// lat = data.lat; -// } -// let old_lon = lon; -// if (!isNaN(data.lon)) { -// lon = data.lon; -// } -// if ((old_lat != lat)||(old_lon != lon)) { -// display(path); -// } -// } -// Bangle.setGPSPower(true, "gipy"); -// Bangle.on('GPS', set_coordinates); +} else { + Bangle.setGPSPower(true, "gipy"); + Bangle.on('GPS', set_coordinates); +} diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 753fc7f7f..6a3e7bc8d 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.02", + "version": "0.03", "description": "Follow gpx files", "allow_emulator":false, "icon": "gipy.png", From 9fb484a6b119c371da732df78bd42502d2dd627e Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 18 Jul 2022 17:01:04 +0200 Subject: [PATCH 014/106] gipy: refactoring --- apps/gipy/TODO | 5 +- apps/gipy/app.js | 222 +++++++++++++++++++++++++++-------------------- 2 files changed, 130 insertions(+), 97 deletions(-) diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 47019376c..360ae0da5 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,4 +1,3 @@ -- display distance to target - detect reached waypoints - beep when reaching waypoint @@ -11,8 +10,8 @@ - store several tracks -- map rotation to match direction - water points - compress path -- avoid display of all segments + +- add a version number to gpc files diff --git a/apps/gipy/app.js b/apps/gipy/app.js index d6444803d..a5d111f03 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,28 +1,113 @@ -var lat = null; -var lon = null; -var cos_direction; -var sin_direction; -let simulated = false; +let simulated = true; + +class Status { + constructor(path) { + this.path = path; + this.position = null; // where we are + this.cos_direction = null; // cos of where we look at + this.sin_direction = null; // sin of where we look at + this.current_segment = null; // which segment is closest + + let r = [0]; + // let's do a reversed prefix computations on all distances: + // loop on all segments in reversed order + let previous_point = null; + for (let i = this.path.len - 1; i >= 0; i--) { + let point = this.path.point(i); + if (previous_point !== null) { + r.unshift(r[0] + point.distance(previous_point)); + } + previous_point = point; + } + this.remaining_distances = r; // how much distance remains at start of each segment + } + update_position(new_position, direction) { + if (Bangle.isLocked() && this.position !== null && new_position.lon == this.position.lon && new_position.lat == this.position.lat) { + return; + } + this.cos_direction = Math.cos(-direction - Math.PI / 2.0); + this.sin_direction = Math.sin(-direction - Math.PI / 2.0); + this.position = new_position; + if (this.is_lost()) { + this.current_segment = this.path.nearest_segment(this.position, 0, path.len - 1); + } else { + this.current_segment = this.path.nearest_segment(this.position, Math.max(0, current_segment-1), Math.min(current_segment+2, path.len - 1)); + } + this.display(); + } + remaining_distance() { + return this.remaining_distances[this.current_segment+1] + this.position.distance(this.path.point(this.current_segment+1)); + } + is_lost() { + let distance_to_nearest = this.position.fake_distance_to_segment(this.path.point(this.current_segment), this.path.point(this.current_segment+1)); + // TODO: if more than something then we are lost + return true; + } + display() { + g.clear(); + this.display_map(); + this.display_stats(); + Bangle.drawWidgets(); + } + display_stats() { + let rounded_distance = Math.round(this.remaining_distance() / 100) / 10; + let total = Math.round(this.remaining_distances[0] / 100) / 10; + let now = new Date(); + let minutes = now.getMinutes().toString(); + if (minutes.length < 2) { + minutes = '0' + minutes; + } + let hours = now.getHours().toString(); + g.setFont("6x8:2").drawString(hours + ":" + minutes, 0, g.getHeight() - 49); + g.drawString("d. " + rounded_distance + "/" + total, 0, g.getHeight() - 32); + g.drawString("seg." + (this.current_segment + 1) + "/" + path.len, 0, g.getHeight() - 15); + } + display_map() { + // don't display all segments, only those neighbouring current segment + // this is most likely to be the correct display + // while lowering the cost a lot + let start = Math.max(this.current_segment - 5, 0); + let end = Math.min(this.current_segment + 6, this.path.len - 1); + let pos = this.position; + let cos = this.cos_direction; + let sin = this.sin_direction; + + // segments + this.path.on_segments(function(p1, p2, i) { + if (i == this.current_segment + 1) { + g.setColor(0.0, 1.0, 0.0); + } else { + g.setColor(1.0, 0.0, 0.0); + } + let c1 = p1.coordinates(pos, cos, sin); + let c2 = p2.coordinates(pos, cos, sin); + g.drawLine(c1[0], c1[1], c2[0], c2[1]); + }, start, end); + + // waypoints + for (let i = start ; i < end + 1 ; i++) { + let p = this.path.point(i); + let c = p.coordinates(pos, cos, sin); + g.setColor(g.theme.fg); + g.fillCircle(c[0], c[1], 4); + g.setColor(g.theme.bg); + g.fillCircle(c[0], c[1], 3); + } + + // now display ourselves + g.setColor(g.theme.fgH); + g.fillCircle(g.getWidth() / 2, g.getHeight() / 2, 5); + } +} class Path { constructor(filename) { let buffer = require("Storage").readArrayBuffer(filename); this.points = Float64Array(buffer); - this.total_distance = this.segments_length(0, this.len - 1); - } - - // return cumulated length of wanted segments (in km). - // start is index of first wanted segment - // end is 1 after index of last wanted segment - segments_length(start, end) { - let total = 0.0; - this.on_segments(function(p1, p2, i) { - total += p1.distance(p2); - }, start, end); - return total; } + // execute op on all segments. // start is index of first wanted segment // end is 1 after index of last wanted segment on_segments(op, start, end) { @@ -36,6 +121,7 @@ class Path { } } + // return point at given index point(index) { let lon = this.points[2 * index]; let lat = this.points[2 * index + 1]; @@ -65,12 +151,12 @@ class Point { this.lon = lon; this.lat = lat; } - coordinates() { - let x = (this.lon - lon) * 20000.0; - let y = (this.lat - lat) * 20000.0; - let rotated_x = x * cos_direction - y * sin_direction; - let rotated_y = x * sin_direction + y * cos_direction; - return [g.getWidth() / 2 - Math.round(rotated_x), // x is inverted + coordinates(current_position, cos_direction, sin_direction) { + let translated = this.minus(current_position).times(20000.0); + let rotated_x = translated.lon * cos_direction - translated.lat * sin_direction; + let rotated_y = translated.lon * sin_direction + translated.lat * cos_direction; + return [ + g.getWidth() / 2 - Math.round(rotated_x), // x is inverted g.getHeight() / 2 + Math.round(rotated_y) ]; } @@ -128,97 +214,45 @@ class Point { } } -function display(path) { - g.clear(); - g.setColor(g.theme.fg); - let next_segment = - path.nearest_segment(new Point(lon, lat), 0, path.len - 1); - let diff; - if (next_segment != current_segment) { - if (next_segment > current_segment) { - diff = path.segments_length(current_segment + 1, next_segment + 1); - } else { - diff = -path.segments_length(next_segment + 1, current_segment + 1); - } - remaining_distance -= diff; - current_segment = next_segment; - } - let start = Math.max(current_segment - 5, 0); - let end = Math.min(current_segment + 7, path.len - 1); - path.on_segments(function(p1, p2, i) { - let c2 = p2.coordinates(); - if (i == current_segment + 1) { - g.setColor(0.0, 1.0, 0.0); - } else { - g.setColor(1.0, 0.0, 0.0); - } - let c1 = p1.coordinates(); - g.drawLine(c1[0], c1[1], c2[0], c2[1]); - g.setColor(g.theme.fg); - g.fillCircle(c2[0], c2[1], 4); - g.setColor(g.theme.bg); - g.fillCircle(c2[0], c2[1], 3); - }, 0, path.len - 1); - - g.setColor(g.theme.fgH); - g.fillCircle(172 / 2, 172 / 2, 5); - let real_remaining_distance = - remaining_distance + path.point(current_segment + 1).distance(new Point(lon, lat)); - let rounded_distance = Math.round(real_remaining_distance / 100) / 10; - let total = Math.round(path.total_distance / 100) / 10; - g.setFont("6x8:2").drawString("d. " + rounded_distance + "/" + total, 0, g.getHeight() - 32); - g.drawString("seg." + (current_segment + 1) + "/" + path.len, 0, g.getHeight() - 15); - Bangle.drawWidgets(); -} Bangle.loadWidgets(); let path = new Path("test.gpc"); -var current_segment = path.nearest_segment(new Point(lon, lat), 0, path.len - 1); -var remaining_distance = path.total_distance - path.segments_length(0, 1); +let status = new Status(path); function set_coordinates(data) { - let old_lat = lat; - if (!isNaN(data.lat)) { - lat = data.lat; - } - let old_lon = lon; - if (!isNaN(data.lon)) { - lon = data.lon; - } - if ((old_lat != lat) || (old_lon != lon)) { + let valid_coordinates = !isNaN(data.lat) && !isNaN(data.lon); + if (valid_coordinates) { let direction = data.course * Math.PI / 180.0; - cos_direction = Math.cos(-direction - Math.PI / 2.0); - sin_direction = Math.sin(-direction - Math.PI / 2.0); - display(path); + let position = new Point(data.lon, data.lat); + status.update_position(position, direction); } } let fake_gps_point = 0.0; -function simulate_gps(path) { +function simulate_gps(status) { let point_index = Math.floor(fake_gps_point); if (point_index >= path.len) { return; } let p1 = path.point(point_index); let p2 = path.point(point_index + 1); - let alpha = fake_gps_point - point_index; - let old_lon = lon; - let old_lat = lat; - lon = (1 - alpha) * p1.lon + alpha * p2.lon; - lat = (1 - alpha) * p1.lat + alpha * p2.lat; - fake_gps_point += 0.05; - let direction = Math.atan2(lat - old_lat, lon - old_lon); - cos_direction = Math.cos(-direction - Math.PI / 2.0); - sin_direction = Math.sin(-direction - Math.PI / 2.0); - display(path); + let alpha = fake_gps_point - point_index; + let pos = p1.times(1-alpha).plus(p2.times(alpha)); + let old_pos = status.position; + + fake_gps_point += 0.05; // advance simulation + let direction = Math.atan2(pos.lat - old_pos.lat, pos.lon - old_pos.lon); + status.update_position(pos, direction); } if (simulated) { - setInterval(simulate_gps, 500, path); - + status.position = new Point(status.path.point(0)); + setInterval(simulate_gps, 500, status); } else { - Bangle.setGPSPower(true, "gipy"); - Bangle.on('GPS', set_coordinates); + Bangle.setGPSPower(true, "gipy"); + Bangle.on('GPS', set_coordinates); } + + From a93fc07bd84fef37059203a978debce975c1589e Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 18 Jul 2022 17:11:41 +0200 Subject: [PATCH 015/106] minor stuff --- apps/gipy/README.md | 2 +- apps/gipy/app.js | 2 +- apps/gipy/metadata.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/gipy/README.md b/apps/gipy/README.md index f28a2ed46..f7fd04233 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -1,7 +1,7 @@ # Gipy Development still in progress. Follow compressed gpx traces. -Warns you before reaching intersections and tries to turn off gps. +Will warn you before reaching intersections and try to turn off gps. ## Usage diff --git a/apps/gipy/app.js b/apps/gipy/app.js index a5d111f03..ebbb37965 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,5 +1,5 @@ -let simulated = true; +let simulated = false; class Status { constructor(path) { diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 6a3e7bc8d..9e05dec6e 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.03", + "version": "0.04", "description": "Follow gpx files", "allow_emulator":false, "icon": "gipy.png", From ab5f4ed3de22e77a1ae33730d5d146bc0d39b34b Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 19 Jul 2022 10:05:34 +0200 Subject: [PATCH 016/106] buzz --- apps/gipy/ChangeLog | 4 +++ apps/gipy/TODO | 18 +++++------- apps/gipy/app.js | 65 ++++++++++++++++++++++++++++++++--------- apps/gipy/metadata.json | 2 +- 4 files changed, 63 insertions(+), 26 deletions(-) create mode 100644 apps/gipy/ChangeLog diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog new file mode 100644 index 000000000..f3c377e06 --- /dev/null +++ b/apps/gipy/ChangeLog @@ -0,0 +1,4 @@ +0.01: Initial code +0.05: We now buzz before reaching a waypoint. Display is only updated when not +locked. We detect leaving path and finding path again. We display remaining distance to +next point. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 360ae0da5..a5d534b63 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,17 +1,13 @@ +- nearest_segment : use highest_completed_segment to disambiguate +- gps direction is weak when speed is low +- add a version number to gpc files +- water points -- detect reached waypoints -- beep when reaching waypoint -- display distance to next waypoint -- display average speed +- store several tracks - turn off gps when moving to next waypoint -- beep when moving away from path + +- display average speed - dynamic map rescale - display scale (100m) -- store several tracks - -- water points - - compress path - -- add a version number to gpc files diff --git a/apps/gipy/app.js b/apps/gipy/app.js index ebbb37965..a4c005073 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -4,10 +4,14 @@ let simulated = false; class Status { constructor(path) { this.path = path; + this.on_path = false; // are we on the path or lost ? this.position = null; // where we are this.cos_direction = null; // cos of where we look at this.sin_direction = null; // sin of where we look at this.current_segment = null; // which segment is closest + this.highest_completed_segment = -1; // remember what we already acomplished to disambiguate nearest path when some segments are takend in both directions + this.reaching = null; // which waypoint are we reaching ? + this.distance_to_next_point = null; // how far are we from next point ? let r = [0]; // let's do a reversed prefix computations on all distances: @@ -23,26 +27,61 @@ class Status { this.remaining_distances = r; // how much distance remains at start of each segment } update_position(new_position, direction) { + if (Bangle.isLocked() && this.position !== null && new_position.lon == this.position.lon && new_position.lat == this.position.lat) { return; } + this.cos_direction = Math.cos(-direction - Math.PI / 2.0); this.sin_direction = Math.sin(-direction - Math.PI / 2.0); this.position = new_position; - if (this.is_lost()) { - this.current_segment = this.path.nearest_segment(this.position, 0, path.len - 1); - } else { - this.current_segment = this.path.nearest_segment(this.position, Math.max(0, current_segment-1), Math.min(current_segment+2, path.len - 1)); + + // detect segment we are on now + let next_segment = this.path.nearest_segment(this.position, Math.max(0, this.current_segment-1), Math.min(this.current_segment+2, path.len - 1)); + + if (this.is_lost(next_segment)) { + // it did not work, try anywhere + next_segment = this.path.nearest_segment(this.position, 0, path.len - 1); + } + // now check if we strayed away from path or back to it + let lost = this.is_lost(next_segment); + if (this.on_path == lost) { + if (lost) { + Bangle.buzz(); // we lost path + setTimeout(()=>Bangle.buzz(), 300); + } else { + Bangle.buzz(); // we found path back + } + this.on_path = !lost; + } + + if (this.current_segment != next_segment) { + if (this.current_segment == next_segment - 1) { + this.highest_completed_segment = this.current_segment; + } + } + this.current_segment = next_segment; + + // check if we are nearing the next point on our path and alert the user + let next_point = this.current_segment + 1; + this.distance_to_next_point = Math.ceil(this.position.distance(this.path.point(next_point))); + if (this.reaching != next_point && this.distance_to_next_point <= 20) { + this.reaching = next_point; + Bangle.buzz(); + } + + // re-display unless locked + if (!Bangle.isLocked()) { + this.display(); } - this.display(); } remaining_distance() { return this.remaining_distances[this.current_segment+1] + this.position.distance(this.path.point(this.current_segment+1)); } - is_lost() { - let distance_to_nearest = this.position.fake_distance_to_segment(this.path.point(this.current_segment), this.path.point(this.current_segment+1)); - // TODO: if more than something then we are lost - return true; + is_lost(segment) { + let distance_to_nearest = this.position.fake_distance_to_segment(this.path.point(segment), this.path.point(segment+1)); + let meters = 6371e3 * distance_to_nearest; + return (meters > 20); } display() { g.clear(); @@ -61,7 +100,7 @@ class Status { let hours = now.getHours().toString(); g.setFont("6x8:2").drawString(hours + ":" + minutes, 0, g.getHeight() - 49); g.drawString("d. " + rounded_distance + "/" + total, 0, g.getHeight() - 32); - g.drawString("seg." + (this.current_segment + 1) + "/" + path.len, 0, g.getHeight() - 15); + g.drawString("seg." + (this.current_segment + 1) + "/" + path.len + " " + this.distance_to_next_point + "m", 0, g.getHeight() - 15); } display_map() { // don't display all segments, only those neighbouring current segment @@ -207,8 +246,8 @@ class Point { // We find projection of point p onto the line. // It falls where t = [(p-v) . (w-v)] / |w-v|^2 // We clamp t from [0,1] to handle points outside the segment vw. - let t = - Math.max(0, Math.min(1, (this.minus(v)).dot(w.minus(v)) / l2)); + let t = Math.max(0, Math.min(1, (this.minus(v)).dot(w.minus(v)) / l2)); + let projection = v.plus((w.minus(v)).times(t)); // Projection falls on the segment return this.fake_distance(projection); } @@ -254,5 +293,3 @@ if (simulated) { Bangle.setGPSPower(true, "gipy"); Bangle.on('GPS', set_coordinates); } - - diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 9e05dec6e..f5baf2eeb 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.04", + "version": "0.05", "description": "Follow gpx files", "allow_emulator":false, "icon": "gipy.png", From c0164d249318311800b2f7217dcd95f3f1fb6d56 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 19 Jul 2022 14:10:12 +0200 Subject: [PATCH 017/106] gpconv: bugfix + osm - we now use distances to segments - better svg - we can now fetch osm data --- apps/gipy/gpconv/Cargo.lock | 975 +++++++++++++++++++++++++++++++++++ apps/gipy/gpconv/Cargo.toml | 5 +- apps/gipy/gpconv/src/main.rs | 385 +++++++++----- 3 files changed, 1247 insertions(+), 118 deletions(-) diff --git a/apps/gipy/gpconv/Cargo.lock b/apps/gipy/gpconv/Cargo.lock index fbe549955..da2fdaac5 100644 --- a/apps/gipy/gpconv/Cargo.lock +++ b/apps/gipy/gpconv/Cargo.lock @@ -44,6 +44,30 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + [[package]] name = "cc" version = "1.0.73" @@ -56,12 +80,37 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "either" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + [[package]] name = "error-chain" version = "0.12.4" @@ -72,6 +121,85 @@ dependencies = [ "version_check", ] +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + [[package]] name = "geo-types" version = "0.7.6" @@ -93,6 +221,9 @@ version = "0.1.0" dependencies = [ "gpx", "itertools", + "lazy_static", + "openstreetmap-api", + "tokio", ] [[package]] @@ -109,6 +240,147 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "h2" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + [[package]] name = "itertools" version = "0.10.3" @@ -124,18 +396,64 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +[[package]] +name = "js-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + [[package]] name = "miniz_oxide" version = "0.5.3" @@ -145,6 +463,36 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -154,6 +502,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_threads" version = "0.1.6" @@ -172,6 +530,120 @@ dependencies = [ "memchr", ] +[[package]] +name = "once_cell" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" + +[[package]] +name = "openssl" +version = "0.10.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "openstreetmap-api" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0964095cdc40d448c6291d079dc98cc5d094c92af5b5fbfcda24a4c9637fc6" +dependencies = [ + "log", + "quick-xml", + "reqwest", + "serde", + "serde_derive", + "serde_urlencoded", + "url", + "urlencoding", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + [[package]] name = "proc-macro2" version = "1.0.40" @@ -181,6 +653,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" version = "1.0.20" @@ -190,12 +672,183 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rustc-demangle" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +[[package]] +name = "ryu" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" + +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" + +[[package]] +name = "serde_derive" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "syn" version = "1.0.98" @@ -207,6 +860,20 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.31" @@ -238,18 +905,326 @@ dependencies = [ "num_threads", ] +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57aec3cfa4c296db7255446efb4928a6be304b431a806216105542a67b6ca82e" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + [[package]] name = "unicode-ident" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" +[[package]] +name = "unicode-normalization" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" + +[[package]] +name = "web-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "xml-rs" version = "0.8.4" diff --git a/apps/gipy/gpconv/Cargo.toml b/apps/gipy/gpconv/Cargo.toml index 69891c99c..b6053a3ce 100644 --- a/apps/gipy/gpconv/Cargo.toml +++ b/apps/gipy/gpconv/Cargo.toml @@ -7,4 +7,7 @@ edition = "2021" [dependencies] gpx="*" -itertools="*" \ No newline at end of file +itertools="*" +openstreetmap-api="*" +tokio={version="1", features=["full"]} +lazy_static="*" diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index 4f2d4a079..74acaa8e5 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -1,11 +1,91 @@ use itertools::Itertools; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fs::File; -use std::io::{BufReader, Write}; +use std::io::{BufReader, BufWriter, Write}; use std::path::Path; use gpx::read; -use gpx::{Gpx, Track, TrackSegment}; +use gpx::Gpx; + +use lazy_static::lazy_static; +use openstreetmap_api::{ + types::{BoundingBox, Credentials}, + Openstreetmap, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +enum Interest { + Bakery, + DrinkingWater, + Toilets, + BikeShop, + ChargingStation, + Bank, + Supermarket, + Table, + // TourismOffice, + Artwork, + Pharmacy, +} + +#[derive(Debug, PartialEq)] +struct InterestPoint { + lat: f64, + lon: f64, + interest: Interest, +} + +impl Eq for InterestPoint {} +impl std::hash::Hash for InterestPoint { + fn hash(&self, state: &mut H) { + unsafe { std::mem::transmute::(self.lat) }.hash(state); + unsafe { std::mem::transmute::(self.lon) }.hash(state); + self.interest.hash(state); + } +} + +lazy_static! { + static ref INTERESTS: HashMap<(&'static str, &'static str), Interest> = { + [ + (("shop", "bakery"), Interest::Bakery), + (("amenity", "drinking_water"), Interest::DrinkingWater), + (("amenity", "toilets"), Interest::Toilets), + (("shop", "bicycle"), Interest::BikeShop), + (("amenity", "charging_station"), Interest::ChargingStation), + (("amenity", "bank"), Interest::Bank), + (("shop", "supermarket"), Interest::Supermarket), + (("leisure", "picnic_table"), Interest::Table), + // (("tourism", "information"), Interest::TourismOffice), + (("tourism", "artwork"), Interest::Artwork), + (("amenity", "pharmacy"), Interest::Pharmacy), + ] + .into_iter() + .collect() + }; +} + +impl Interest { + fn new(key: &str, value: &str) -> Option { + INTERESTS.get(&(key, value)).cloned() + } +} + +impl InterestPoint { + fn color(&self) -> &'static str { + match self.interest { + Interest::Bakery => "red", + Interest::DrinkingWater => "blue", + Interest::Toilets => "brown", + Interest::BikeShop => "purple", + Interest::ChargingStation => "green", + Interest::Bank => "black", + Interest::Supermarket => "red", + Interest::Table => "pink", + Interest::Artwork => "orange", + Interest::Pharmacy => "chartreuse", + } + } +} fn squared_distance_between(p1: &(f64, f64), p2: &(f64, f64)) -> f64 { let (x1, y1) = *p1; @@ -34,79 +114,90 @@ fn points(filename: &str) -> impl Iterator { .dedup() } -// returns distance from point p to line passing through points p1 and p2 -// see https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line -fn distance_to_line(p: &(f64, f64), p1: &(f64, f64), p2: &(f64, f64)) -> f64 { - let (x0, y0) = *p; - let (x1, y1) = *p1; - let (x2, y2) = *p2; - let dx = x2 - x1; - let dy = y2 - y1; - //TODO: remove this division by computing fake distances - (dx * (y1 - y0) - dy * (x1 - x0)).abs() / (dx * dx + dy * dy).sqrt() -} - -fn acceptable_angles(p1: &(f64, f64), p2: &(f64, f64), epsilon: f64) -> (f64, f64) { - // first, convert p2's coordinates for p1 as origin - let (x1, y1) = *p1; - let (x2, y2) = *p2; - let (x, y) = (x2 - x1, y2 - y1); - // rotate so that (p1, p2) ends on x axis - let theta = y.atan2(x); - let rx = x * theta.cos() - y * theta.sin(); - let ry = x * theta.sin() + y * theta.cos(); - assert!(ry.abs() <= std::f64::EPSILON); - - // now imagine a line at an angle alpha. - // we want the distance d from (rx, 0) to our line - // we have sin(alpha) = d / rx - // limiting d to epsilon, we solve - // sin(alpha) = e / rx - // and get - // alpha = arcsin(e/rx) - let alpha = (epsilon / rx).asin(); - - // now we just need to rotate back - let a1 = theta + alpha.abs(); - let a2 = theta - alpha.abs(); - assert!(a1 >= a2); - (a1, a2) -} - -// this is like ramer douglas peucker algorithm -// except that we advance from the start without knowing the end. -// each point we meet constrains the chosen segment's angle -// a bit more. -// -fn simplify(mut points: &[(f64, f64)]) -> Vec<(f64, f64)> { - let mut remaining_points = Vec::new(); - while !points.is_empty() { - let (sx, sy) = points.first().unwrap(); - let i = match points - .iter() - .enumerate() - .map(|(i, (x, y))| todo!("compute angles")) - .try_fold( - (0.0f64, std::f64::consts::FRAC_2_PI), - |(amin, amax), (i, (amin2, amax2))| -> Result<(f64, f64), usize> { - let new_amax = amax.min(amax2); - let new_amin = amin.max(amin2); - if new_amin >= new_amax { - Err(i) - } else { - Ok((new_amin, new_amax)) - } - }, - ) { - Err(i) => i, - Ok(_) => points.len(), - }; - remaining_points.push(points.first().cloned().unwrap()); - points = &points[i..]; +fn distance_to_segment(p: &(f64, f64), v: &(f64, f64), w: &(f64, f64)) -> f64 { + let l2 = squared_distance_between(v, w); + if l2 == 0.0 { + return squared_distance_between(p, v).sqrt(); } - remaining_points + // Consider the line extending the segment, parameterized as v + t (w - v). + // We find projection of point p onto the line. + // It falls where t = [(p-v) . (w-v)] / |w-v|^2 + // We clamp t from [0,1] to handle points outside the segment vw. + let x0 = p.0 - v.0; + let y0 = p.1 - v.1; + let x1 = w.0 - v.0; + let y1 = w.1 - v.1; + let dot = x0 * x1 + y0 * y1; + let t = (dot / l2).min(1.0).max(0.0); + + let proj_x = v.0 + x1 * t; + let proj_y = v.1 + y1 * t; + + squared_distance_between(&(proj_x, proj_y), p).sqrt() } +// // NOTE: this angles idea could maybe be use to get dp from n^3 to n^2 +// fn acceptable_angles(p1: &(f64, f64), p2: &(f64, f64), epsilon: f64) -> (f64, f64) { +// // first, convert p2's coordinates for p1 as origin +// let (x1, y1) = *p1; +// let (x2, y2) = *p2; +// let (x, y) = (x2 - x1, y2 - y1); +// // rotate so that (p1, p2) ends on x axis +// let theta = y.atan2(x); +// let rx = x * theta.cos() - y * theta.sin(); +// let ry = x * theta.sin() + y * theta.cos(); +// assert!(ry.abs() <= std::f64::EPSILON); +// +// // now imagine a line at an angle alpha. +// // we want the distance d from (rx, 0) to our line +// // we have sin(alpha) = d / rx +// // limiting d to epsilon, we solve +// // sin(alpha) = e / rx +// // and get +// // alpha = arcsin(e/rx) +// let alpha = (epsilon / rx).asin(); +// +// // now we just need to rotate back +// let a1 = theta + alpha.abs(); +// let a2 = theta - alpha.abs(); +// assert!(a1 >= a2); +// (a1, a2) +// } +// +// // this is like ramer douglas peucker algorithm +// // except that we advance from the start without knowing the end. +// // each point we meet constrains the chosen segment's angle +// // a bit more. +// // +// fn simplify(mut points: &[(f64, f64)]) -> Vec<(f64, f64)> { +// let mut remaining_points = Vec::new(); +// while !points.is_empty() { +// let (sx, sy) = points.first().unwrap(); +// let i = match points +// .iter() +// .enumerate() +// .map(|(i, (x, y))| todo!("compute angles")) +// .try_fold( +// (0.0f64, std::f64::consts::FRAC_2_PI), +// |(amin, amax), (i, (amin2, amax2))| -> Result<(f64, f64), usize> { +// let new_amax = amax.min(amax2); +// let new_amin = amin.max(amin2); +// if new_amin >= new_amax { +// Err(i) +// } else { +// Ok((new_amin, new_amax)) +// } +// }, +// ) { +// Err(i) => i, +// Ok(_) => points.len(), +// }; +// remaining_points.push(points.first().cloned().unwrap()); +// points = &points[i..]; +// } +// remaining_points +// } + fn extract_prog_dyn_solution( points: &[(f64, f64)], start: usize, @@ -140,9 +231,10 @@ fn simplify_prog_dyn( } else { let first_point = &points[start]; let last_point = &points[end - 1]; + if points[(start + 1)..end] .iter() - .map(|p| distance_to_line(p, first_point, last_point)) + .map(|p| distance_to_segment(p, first_point, last_point)) .all(|d| d <= epsilon) { (None, 2) @@ -190,7 +282,7 @@ fn rdp(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { } else { let (index_farthest, farthest_distance) = points .iter() - .map(|p| distance_to_line(p, points.first().unwrap(), points.last().unwrap())) + .map(|p| distance_to_segment(p, points.first().unwrap(), points.last().unwrap())) .enumerate() .max_by(|(_, d1), (_, d2)| { if d1.is_nan() { @@ -299,7 +391,7 @@ fn save_coordinates>( // points: &[(i32, i32)], points: &[(f64, f64)], ) -> std::io::Result<()> { - let mut writer = std::io::BufWriter::new(File::create(path)?); + let mut writer = BufWriter::new(File::create(path)?); eprintln!("saving {} points", points.len()); // writer.write_all(&xmin.to_be_bytes())?; @@ -312,21 +404,6 @@ fn save_coordinates>( Ok(()) } -fn save_json>(path: P, points: &[(f64, f64)]) -> std::io::Result<()> { - let mut writer = std::io::BufWriter::new(File::create(path)?); - - eprintln!("saving {} points", points.len()); - writeln!(&mut writer, "[")?; - points - .iter() - .map(|(x, y)| format!("{{\"lat\": {}, \"lon\":{}}}", y, x)) - .intersperse_with(|| ",\n".to_string()) - .try_for_each(|s| write!(&mut writer, "{}", s))?; - write!(&mut writer, "]")?; - - Ok(()) -} - fn optimal_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { let mut cache = HashMap::new(); simplify_prog_dyn(&points, 0, points.len(), epsilon, &mut cache); @@ -360,7 +437,7 @@ fn hybrid_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> } else { let (index_farthest, farthest_distance) = points .iter() - .map(|p| distance_to_line(p, points.first().unwrap(), points.last().unwrap())) + .map(|p| distance_to_segment(p, points.first().unwrap(), points.last().unwrap())) .enumerate() .max_by(|(_, d1), (_, d2)| { if d1.is_nan() { @@ -391,52 +468,126 @@ fn hybrid_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> } } -fn main() { - let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); - eprintln!("input is {}", input_file); - let p = points(&input_file).collect::>(); - eprintln!("initialy we have {} points", p.len()); - //eprintln!("opt is {}", optimal_simplification(&p, 0.0005).len()); - let start = std::time::Instant::now(); - let rp = hybrid_simplification(&p, 0.0005); - eprintln!("hybrid took {:?}", start.elapsed()); - eprintln!("we now have {} points", rp.len()); - let start = std::time::Instant::now(); - eprintln!("rdp would have had {}", rdp(&p, 0.0005).len()); - eprintln!("rdp took {:?}", start.elapsed()); - // let rp = rdp(&p, 0.001); - save_coordinates("test.gpc", &rp).unwrap(); +async fn get_openstreetmap_data(points: &[(f64, f64)]) -> HashSet { + let osm = Openstreetmap::new("https://openstreetmap.org", Credentials::None); + let mut interest_points = HashSet::new(); + let border = 0.0001; + for (&(x1, y1), &(x2, y2)) in points.iter().tuple_windows() { + let left = x1.min(x2) - border; + let right = x1.max(x2) + border; + let bottom = y1.min(y2) - border; + let top = y1.max(y2) + border; + if let Ok(map) = osm + .map(&BoundingBox { + bottom, + left, + top, + right, + }) + .await + { + let points = map.nodes.iter().flat_map(|n| { + n.tags.iter().filter_map(|t| { + let latlon = n.lat.and_then(|lat| n.lon.map(|lon| (lat, lon))); + latlon.and_then(|(lat, lon)| { + Interest::new(&t.k, &t.v).map(|i| InterestPoint { + lat, + lon, + interest: i, + }) + }) + }) + }); + interest_points.extend(points) + } else { + eprintln!("failed retrieving osm data") + } + } + interest_points +} - let (xmin, xmax) = rp +fn save_path(writer: &mut W, p: &[(f64, f64)], stroke: &str) -> std::io::Result<()> { + write!( + writer, + "")?; + Ok(()) +} + +fn save_svg>( + filename: P, + p: &[(f64, f64)], + rp: &[(f64, f64)], + interest_points: &HashSet, +) -> std::io::Result<()> { + let mut writer = BufWriter::new(std::fs::File::create(filename)?); + let (xmin, xmax) = p .iter() .map(|&(x, _)| x) .minmax_by(|a, b| a.partial_cmp(b).unwrap()) .into_option() .unwrap(); - let (ymin, ymax) = rp + let (ymin, ymax) = p .iter() .map(|&(_, y)| y) .minmax_by(|a, b| a.partial_cmp(b).unwrap()) .into_option() .unwrap(); - println!( + writeln!( + &mut writer, "", xmin, ymin, xmax - xmin, ymax - ymin - ); - print!( + )?; + write!( + &mut writer, "", xmin, ymin, xmax - xmin, ymax - ymin - ); - print!(""); - println!(""); + )?; + + save_path(&mut writer, &p, "red")?; + save_path(&mut writer, &rp, "black")?; + + for point in interest_points { + writeln!( + &mut writer, + "", + point.lon, + point.lat, + point.color(), + )?; + } + writeln!(&mut writer, "")?; + Ok(()) +} + +#[tokio::main] +async fn main() { + let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); + eprintln!("input is {}", input_file); + let p = points(&input_file).collect::>(); + eprintln!("initialy we have {} points", p.len()); + let start = std::time::Instant::now(); + let rp = simplify_path(&p, 0.00015); + eprintln!("we took {:?}", start.elapsed()); + eprintln!("we now have {} points", rp.len()); + let start = std::time::Instant::now(); + eprintln!("rdp would have had {}", rdp(&p, 0.00015).len()); + eprintln!("rdp took {:?}", start.elapsed()); + + save_coordinates("test.gpc", &rp).unwrap(); + // let i = get_openstreetmap_data(&rp).await; + let i = HashSet::new(); + save_svg("test.svg", &p, &rp, &i).unwrap(); } From e3b5a6bba66702d6573c639a9e8c64bb6ffadf82 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 20 Jul 2022 10:52:00 +0200 Subject: [PATCH 018/106] gipy: steep turns --- apps/gipy/TODO | 3 + apps/gipy/app.js | 34 ++++- apps/gipy/gpconv/src/main.rs | 274 ++++++++++++++--------------------- apps/gipy/gpconv/src/osm.rs | 130 +++++++++++++++++ 4 files changed, 271 insertions(+), 170 deletions(-) create mode 100644 apps/gipy/gpconv/src/osm.rs diff --git a/apps/gipy/TODO b/apps/gipy/TODO index a5d534b63..8149d2636 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -2,6 +2,9 @@ - gps direction is weak when speed is low - add a version number to gpc files - water points + ---> we group them following path by groups of cst_size and record segments ids marking limits + +- gps wait screen - store several tracks - turn off gps when moving to next waypoint diff --git a/apps/gipy/app.js b/apps/gipy/app.js index a4c005073..2196246c8 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -38,7 +38,7 @@ class Status { // detect segment we are on now let next_segment = this.path.nearest_segment(this.position, Math.max(0, this.current_segment-1), Math.min(this.current_segment+2, path.len - 1)); - + if (this.is_lost(next_segment)) { // it did not work, try anywhere next_segment = this.path.nearest_segment(this.position, 0, path.len - 1); @@ -67,7 +67,12 @@ class Status { this.distance_to_next_point = Math.ceil(this.position.distance(this.path.point(next_point))); if (this.reaching != next_point && this.distance_to_next_point <= 20) { this.reaching = next_point; - Bangle.buzz(); + if (Bangle.isLocked()) { + if (this.path.is_waypoint(next_point)) { + Bangle.buzz(); + Bangle.setLocked(false); + } + } } // re-display unless locked @@ -128,6 +133,12 @@ class Status { for (let i = start ; i < end + 1 ; i++) { let p = this.path.point(i); let c = p.coordinates(pos, cos, sin); + if (this.path.is_waypoint(i)) { + g.setColor(g.theme.fg); + g.fillCircle(c[0], c[1], 6); + g.setColor(g.theme.bg); + g.fillCircle(c[0], c[1], 5); + } g.setColor(g.theme.fg); g.fillCircle(c[0], c[1], 4); g.setColor(g.theme.bg); @@ -146,6 +157,24 @@ class Path { this.points = Float64Array(buffer); } + // if start, end or steep direction change + // we are buzzing and displayed specially + is_waypoint(point_index) { + if ((point_index == 0)||(point_index == this.len -1)) { + return true; + } else { + let p1 = this.point(point_index-1); + let p2 = this.point(point_index); + let p3 = this.point(point_index+1); + let d1 = p2.minus(p1); + let d2 = p3.minus(p2); + let a1 = Math.atan2(d1.lat, d1.lon); + let a2 = Math.atan2(d2.lat, d2.lon); + let direction_change = Math.abs(a2-a1); + return (direction_change > Math.PI / 3.0); + } + } + // execute op on all segments. // start is index of first wanted segment // end is 1 after index of last wanted segment @@ -293,3 +322,4 @@ if (simulated) { Bangle.setGPSPower(true, "gipy"); Bangle.on('GPS', set_coordinates); } + diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index 74acaa8e5..a6033bfb2 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -7,95 +7,55 @@ use std::path::Path; use gpx::read; use gpx::Gpx; -use lazy_static::lazy_static; -use openstreetmap_api::{ - types::{BoundingBox, Credentials}, - Openstreetmap, -}; +mod osm; +use osm::InterestPoint; -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -enum Interest { - Bakery, - DrinkingWater, - Toilets, - BikeShop, - ChargingStation, - Bank, - Supermarket, - Table, - // TourismOffice, - Artwork, - Pharmacy, +#[derive(Debug, PartialEq, Clone, Copy)] +pub struct Point { + x: f64, + y: f64, } -#[derive(Debug, PartialEq)] -struct InterestPoint { - lat: f64, - lon: f64, - interest: Interest, -} - -impl Eq for InterestPoint {} -impl std::hash::Hash for InterestPoint { +impl Eq for Point {} +impl std::hash::Hash for Point { fn hash(&self, state: &mut H) { - unsafe { std::mem::transmute::(self.lat) }.hash(state); - unsafe { std::mem::transmute::(self.lon) }.hash(state); - self.interest.hash(state); + unsafe { std::mem::transmute::(self.x) }.hash(state); + unsafe { std::mem::transmute::(self.y) }.hash(state); } } -lazy_static! { - static ref INTERESTS: HashMap<(&'static str, &'static str), Interest> = { - [ - (("shop", "bakery"), Interest::Bakery), - (("amenity", "drinking_water"), Interest::DrinkingWater), - (("amenity", "toilets"), Interest::Toilets), - (("shop", "bicycle"), Interest::BikeShop), - (("amenity", "charging_station"), Interest::ChargingStation), - (("amenity", "bank"), Interest::Bank), - (("shop", "supermarket"), Interest::Supermarket), - (("leisure", "picnic_table"), Interest::Table), - // (("tourism", "information"), Interest::TourismOffice), - (("tourism", "artwork"), Interest::Artwork), - (("amenity", "pharmacy"), Interest::Pharmacy), - ] - .into_iter() - .collect() - }; -} - -impl Interest { - fn new(key: &str, value: &str) -> Option { - INTERESTS.get(&(key, value)).cloned() +impl Point { + fn squared_distance_between(&self, other: &Point) -> f64 { + let dx = other.x - self.x; + let dy = other.y - self.y; + dx * dx + dy * dy } -} - -impl InterestPoint { - fn color(&self) -> &'static str { - match self.interest { - Interest::Bakery => "red", - Interest::DrinkingWater => "blue", - Interest::Toilets => "brown", - Interest::BikeShop => "purple", - Interest::ChargingStation => "green", - Interest::Bank => "black", - Interest::Supermarket => "red", - Interest::Table => "pink", - Interest::Artwork => "orange", - Interest::Pharmacy => "chartreuse", + fn distance_to_segment(&self, v: &Point, w: &Point) -> f64 { + let l2 = v.squared_distance_between(w); + if l2 == 0.0 { + return self.squared_distance_between(v).sqrt(); } + // Consider the line extending the segment, parameterized as v + t (w - v). + // We find projection of point p onto the line. + // It falls where t = [(p-v) . (w-v)] / |w-v|^2 + // We clamp t from [0,1] to handle points outside the segment vw. + let x0 = self.x - v.x; + let y0 = self.y - v.y; + let x1 = w.x - v.x; + let y1 = w.y - v.y; + let dot = x0 * x1 + y0 * y1; + let t = (dot / l2).min(1.0).max(0.0); + + let proj = Point { + x: v.x + x1 * t, + y: v.y + y1 * t, + }; + + proj.squared_distance_between(self).sqrt() } } -fn squared_distance_between(p1: &(f64, f64), p2: &(f64, f64)) -> f64 { - let (x1, y1) = *p1; - let (x2, y2) = *p2; - let dx = x2 - x1; - let dy = y2 - y1; - dx * dx + dy * dy -} - -fn points(filename: &str) -> impl Iterator { +fn points(filename: &str) -> impl Iterator { // This XML file actually exists — try it for yourself! let file = File::open(filename).unwrap(); let reader = BufReader::new(file); @@ -112,28 +72,7 @@ fn points(filename: &str) -> impl Iterator { .flat_map(|segment| segment.linestring().points().collect::>()) .map(|point| (point.x(), point.y())) .dedup() -} - -fn distance_to_segment(p: &(f64, f64), v: &(f64, f64), w: &(f64, f64)) -> f64 { - let l2 = squared_distance_between(v, w); - if l2 == 0.0 { - return squared_distance_between(p, v).sqrt(); - } - // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. - // It falls where t = [(p-v) . (w-v)] / |w-v|^2 - // We clamp t from [0,1] to handle points outside the segment vw. - let x0 = p.0 - v.0; - let y0 = p.1 - v.1; - let x1 = w.0 - v.0; - let y1 = w.1 - v.1; - let dot = x0 * x1 + y0 * y1; - let t = (dot / l2).min(1.0).max(0.0); - - let proj_x = v.0 + x1 * t; - let proj_y = v.1 + y1 * t; - - squared_distance_between(&(proj_x, proj_y), p).sqrt() + .map(|(x, y)| Point { x, y }) } // // NOTE: this angles idea could maybe be use to get dp from n^3 to n^2 @@ -199,11 +138,11 @@ fn distance_to_segment(p: &(f64, f64), v: &(f64, f64), w: &(f64, f64)) -> f64 { // } fn extract_prog_dyn_solution( - points: &[(f64, f64)], + points: &[Point], start: usize, end: usize, cache: &HashMap<(usize, usize), (Option, usize)>, -) -> Vec<(f64, f64)> { +) -> Vec { if let Some(choice) = cache.get(&(start, end)).unwrap().0 { let mut v1 = extract_prog_dyn_solution(points, start, choice + 1, cache); let mut v2 = extract_prog_dyn_solution(points, choice, end, cache); @@ -216,7 +155,7 @@ fn extract_prog_dyn_solution( } fn simplify_prog_dyn( - points: &[(f64, f64)], + points: &[Point], start: usize, end: usize, epsilon: f64, @@ -234,7 +173,7 @@ fn simplify_prog_dyn( if points[(start + 1)..end] .iter() - .map(|p| distance_to_segment(p, first_point, last_point)) + .map(|p| p.distance_to_segment(first_point, last_point)) .all(|d| d <= epsilon) { (None, 2) @@ -255,7 +194,7 @@ fn simplify_prog_dyn( } } -fn rdp(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { +fn rdp(points: &[Point], epsilon: f64) -> Vec { if points.len() <= 2 { points.iter().copied().collect() } else { @@ -266,8 +205,9 @@ fn rdp(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { .enumerate() .skip(1) .max_by(|(_, p1), (_, p2)| { - squared_distance_between(first, p1) - .partial_cmp(&squared_distance_between(first, p2)) + first + .squared_distance_between(p1) + .partial_cmp(&first.squared_distance_between(p2)) .unwrap() }) .map(|(i, _)| i) @@ -282,7 +222,7 @@ fn rdp(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { } else { let (index_farthest, farthest_distance) = points .iter() - .map(|p| distance_to_segment(p, points.first().unwrap(), points.last().unwrap())) + .map(|p| p.distance_to_segment(points.first().unwrap(), points.last().unwrap())) .enumerate() .max_by(|(_, d1), (_, d2)| { if d1.is_nan() { @@ -313,7 +253,7 @@ fn rdp(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { } } -fn simplify_path(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { +fn simplify_path(points: &[Point], epsilon: f64) -> Vec { if points.len() <= 600 { optimal_simplification(points, epsilon) } else { @@ -384,13 +324,7 @@ fn compress_coordinates(points: &[(i32, i32)]) -> Vec<(i16, i16)> { xdiffs.zip(ydiffs).collect() } -fn save_coordinates>( - path: P, - //xmin: f64, - //ymin: f64, - // points: &[(i32, i32)], - points: &[(f64, f64)], -) -> std::io::Result<()> { +fn save_coordinates>(path: P, points: &[Point]) -> std::io::Result<()> { let mut writer = BufWriter::new(File::create(path)?); eprintln!("saving {} points", points.len()); @@ -398,19 +332,19 @@ fn save_coordinates>( // writer.write_all(&ymin.to_be_bytes())?; points .iter() - .flat_map(|(x, y)| [x, y]) + .flat_map(|p| [p.x, p.y]) .try_for_each(|c| writer.write_all(&c.to_le_bytes()))?; Ok(()) } -fn optimal_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { +fn optimal_simplification(points: &[Point], epsilon: f64) -> Vec { let mut cache = HashMap::new(); simplify_prog_dyn(&points, 0, points.len(), epsilon, &mut cache); extract_prog_dyn_solution(&points, 0, points.len(), &cache) } -fn hybrid_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> { +fn hybrid_simplification(points: &[Point], epsilon: f64) -> Vec { if points.len() <= 300 { optimal_simplification(points, epsilon) } else { @@ -421,8 +355,9 @@ fn hybrid_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> .enumerate() .skip(1) .max_by(|(_, p1), (_, p2)| { - squared_distance_between(first, p1) - .partial_cmp(&squared_distance_between(first, p2)) + first + .squared_distance_between(p1) + .partial_cmp(&first.squared_distance_between(p2)) .unwrap() }) .map(|(i, _)| i) @@ -437,7 +372,7 @@ fn hybrid_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> } else { let (index_farthest, farthest_distance) = points .iter() - .map(|p| distance_to_segment(p, points.first().unwrap(), points.last().unwrap())) + .map(|p| p.distance_to_segment(points.first().unwrap(), points.last().unwrap())) .enumerate() .max_by(|(_, d1), (_, d2)| { if d1.is_nan() { @@ -468,73 +403,36 @@ fn hybrid_simplification(points: &[(f64, f64)], epsilon: f64) -> Vec<(f64, f64)> } } -async fn get_openstreetmap_data(points: &[(f64, f64)]) -> HashSet { - let osm = Openstreetmap::new("https://openstreetmap.org", Credentials::None); - let mut interest_points = HashSet::new(); - let border = 0.0001; - for (&(x1, y1), &(x2, y2)) in points.iter().tuple_windows() { - let left = x1.min(x2) - border; - let right = x1.max(x2) + border; - let bottom = y1.min(y2) - border; - let top = y1.max(y2) + border; - if let Ok(map) = osm - .map(&BoundingBox { - bottom, - left, - top, - right, - }) - .await - { - let points = map.nodes.iter().flat_map(|n| { - n.tags.iter().filter_map(|t| { - let latlon = n.lat.and_then(|lat| n.lon.map(|lon| (lat, lon))); - latlon.and_then(|(lat, lon)| { - Interest::new(&t.k, &t.v).map(|i| InterestPoint { - lat, - lon, - interest: i, - }) - }) - }) - }); - interest_points.extend(points) - } else { - eprintln!("failed retrieving osm data") - } - } - interest_points -} - -fn save_path(writer: &mut W, p: &[(f64, f64)], stroke: &str) -> std::io::Result<()> { +fn save_path(writer: &mut W, p: &[Point], stroke: &str) -> std::io::Result<()> { write!( writer, "")?; Ok(()) } fn save_svg>( filename: P, - p: &[(f64, f64)], - rp: &[(f64, f64)], + p: &[Point], + rp: &[Point], interest_points: &HashSet, + waypoints: &HashSet, ) -> std::io::Result<()> { let mut writer = BufWriter::new(std::fs::File::create(filename)?); let (xmin, xmax) = p .iter() - .map(|&(x, _)| x) + .map(|p| p.x) .minmax_by(|a, b| a.partial_cmp(b).unwrap()) .into_option() .unwrap(); let (ymin, ymax) = p .iter() - .map(|&(_, y)| y) + .map(|p| p.y) .minmax_by(|a, b| a.partial_cmp(b).unwrap()) .into_option() .unwrap(); @@ -563,15 +461,54 @@ fn save_svg>( writeln!( &mut writer, "", - point.lon, - point.lat, + point.point.x, + point.point.y, point.color(), )?; } + + let rpoints = rp.iter().cloned().collect::>(); + waypoints.difference(&rpoints).try_for_each(|p| { + writeln!( + &mut writer, + "", + p.x, p.y, + ) + })?; + waypoints.intersection(&rpoints).try_for_each(|p| { + writeln!( + &mut writer, + "", + p.x, p.y, + ) + })?; + writeln!(&mut writer, "")?; Ok(()) } +fn detect_waypoints(points: &[Point]) -> HashSet { + points + .first() + .into_iter() + .chain(points.iter().tuple_windows().filter_map(|(p1, p2, p3)| { + let x1 = p2.x - p1.x; + let y1 = p2.y - p1.y; + let a1 = y1.atan2(x1); + let x2 = p3.x - p2.x; + let y2 = p3.y - p2.y; + let a2 = y2.atan2(x2); + if (a2 - a1).abs() <= std::f64::consts::PI / 3.0 { + None + } else { + Some(p2) + } + })) + .chain(points.last().into_iter()) + .copied() + .collect::>() +} + #[tokio::main] async fn main() { let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); @@ -580,6 +517,7 @@ async fn main() { eprintln!("initialy we have {} points", p.len()); let start = std::time::Instant::now(); let rp = simplify_path(&p, 0.00015); + let waypoints = detect_waypoints(&rp); eprintln!("we took {:?}", start.elapsed()); eprintln!("we now have {} points", rp.len()); let start = std::time::Instant::now(); @@ -589,5 +527,5 @@ async fn main() { save_coordinates("test.gpc", &rp).unwrap(); // let i = get_openstreetmap_data(&rp).await; let i = HashSet::new(); - save_svg("test.svg", &p, &rp, &i).unwrap(); + save_svg("test.svg", &p, &rp, &i, &waypoints).unwrap(); } diff --git a/apps/gipy/gpconv/src/osm.rs b/apps/gipy/gpconv/src/osm.rs new file mode 100644 index 000000000..a70e7ff5d --- /dev/null +++ b/apps/gipy/gpconv/src/osm.rs @@ -0,0 +1,130 @@ +use super::Point; +use lazy_static::lazy_static; +use openstreetmap_api::{ + types::{BoundingBox, Credentials}, + Openstreetmap, +}; +use std::collections::{HashMap, HashSet}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Interest { + Bakery, + DrinkingWater, + Toilets, + BikeShop, + ChargingStation, + Bank, + Supermarket, + Table, + // TourismOffice, + Artwork, + Pharmacy, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct InterestPoint { + pub point: Point, + pub interest: Interest, +} + +lazy_static! { + static ref INTERESTS: HashMap<(&'static str, &'static str), Interest> = { + [ + (("shop", "bakery"), Interest::Bakery), + (("amenity", "drinking_water"), Interest::DrinkingWater), + (("amenity", "toilets"), Interest::Toilets), + (("shop", "bicycle"), Interest::BikeShop), + (("amenity", "charging_station"), Interest::ChargingStation), + (("amenity", "bank"), Interest::Bank), + (("shop", "supermarket"), Interest::Supermarket), + (("leisure", "picnic_table"), Interest::Table), + // (("tourism", "information"), Interest::TourismOffice), + (("tourism", "artwork"), Interest::Artwork), + (("amenity", "pharmacy"), Interest::Pharmacy), + ] + .into_iter() + .collect() + }; +} + +impl Interest { + fn new(key: &str, value: &str) -> Option { + INTERESTS.get(&(key, value)).cloned() + } +} + +impl InterestPoint { + pub fn color(&self) -> &'static str { + match self.interest { + Interest::Bakery => "red", + Interest::DrinkingWater => "blue", + Interest::Toilets => "brown", + Interest::BikeShop => "purple", + Interest::ChargingStation => "green", + Interest::Bank => "black", + Interest::Supermarket => "red", + Interest::Table => "pink", + Interest::Artwork => "orange", + Interest::Pharmacy => "chartreuse", + } + } +} + +async fn get_openstreetmap_data(points: &[(f64, f64)]) -> HashSet { + let osm = Openstreetmap::new("https://openstreetmap.org", Credentials::None); + let mut interest_points = HashSet::new(); + let border = 0.0001; + let mut boxes = Vec::new(); + let max_size = 0.005; + points.iter().fold( + (std::f64::MAX, std::f64::MIN, std::f64::MAX, std::f64::MIN), + |in_box, &(x, y)| { + let (mut xmin, mut xmax, mut ymin, mut ymax) = in_box; + xmin = xmin.min(x); + xmax = xmax.max(x); + ymin = ymin.min(y); + ymax = ymax.max(y); + if (xmax - xmin > max_size) || (ymax - ymin > max_size) { + boxes.push(in_box); + (x, x, y, y) + } else { + (xmin, xmax, ymin, ymax) + } + }, + ); + eprintln!("we need {} requests to openstreetmap", boxes.len()); + for (xmin, xmax, ymin, ymax) in boxes { + let left = xmin - border; + let right = xmax + border; + let bottom = ymin - border; + let top = ymax + border; + match osm + .map(&BoundingBox { + bottom, + left, + top, + right, + }) + .await + { + Ok(map) => { + let points = map.nodes.iter().flat_map(|n| { + n.tags.iter().filter_map(|t| { + let latlon = n.lat.and_then(|lat| n.lon.map(|lon| (lat, lon))); + latlon.and_then(|(lat, lon)| { + Interest::new(&t.k, &t.v).map(|i| InterestPoint { + point: Point { x: lon, y: lat }, + interest: i, + }) + }) + }) + }); + interest_points.extend(points) + } + Err(e) => { + eprintln!("failed retrieving osm data: {:?}", e); + } + } + } + interest_points +} From f1c204c77a95518707f872c03e2a0eb27c6b3382 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 20 Jul 2022 12:00:51 +0200 Subject: [PATCH 019/106] gipy 0.06 --- apps/gipy/ChangeLog | 14 +++++++++++--- apps/gipy/TODO | 4 +--- apps/gipy/app.js | 28 +++++++++++++++++++--------- apps/gipy/gpconv/src/main.rs | 4 +++- apps/gipy/metadata.json | 2 +- 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index f3c377e06..0410d07e7 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -1,4 +1,12 @@ 0.01: Initial code -0.05: We now buzz before reaching a waypoint. Display is only updated when not -locked. We detect leaving path and finding path again. We display remaining distance to -next point. + +0.05: + * We now buzz before reaching a waypoint. + * Display is only updated when not locked. + * We detect leaving path and finding path again. + * We display remaining distance to next point. + +0.06: + * Special display for points with steep turns + * Buzz on points with steep turns and unlock + * Losing gps is now displayed diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 8149d2636..96642608f 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,11 +1,9 @@ -- nearest_segment : use highest_completed_segment to disambiguate +- nearest_segment : use direction to disambiguate - gps direction is weak when speed is low - add a version number to gpc files - water points ---> we group them following path by groups of cst_size and record segments ids marking limits -- gps wait screen - - store several tracks - turn off gps when moving to next waypoint diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 2196246c8..85d8cb11c 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -9,7 +9,6 @@ class Status { this.cos_direction = null; // cos of where we look at this.sin_direction = null; // sin of where we look at this.current_segment = null; // which segment is closest - this.highest_completed_segment = -1; // remember what we already acomplished to disambiguate nearest path when some segments are takend in both directions this.reaching = null; // which waypoint are we reaching ? this.distance_to_next_point = null; // how far are we from next point ? @@ -55,11 +54,6 @@ class Status { this.on_path = !lost; } - if (this.current_segment != next_segment) { - if (this.current_segment == next_segment - 1) { - this.highest_completed_segment = this.current_segment; - } - } this.current_segment = next_segment; // check if we are nearing the next point on our path and alert the user @@ -74,7 +68,6 @@ class Status { } } } - // re-display unless locked if (!Bangle.isLocked()) { this.display(); @@ -171,7 +164,7 @@ class Path { let a1 = Math.atan2(d1.lat, d1.lon); let a2 = Math.atan2(d2.lat, d2.lon); let direction_change = Math.abs(a2-a1); - return (direction_change > Math.PI / 3.0); + return ((direction_change > Math.PI / 3.0)&&(direction_change < Math.PI * 5.0/3.0)); } } @@ -276,7 +269,7 @@ class Point { // It falls where t = [(p-v) . (w-v)] / |w-v|^2 // We clamp t from [0,1] to handle points outside the segment vw. let t = Math.max(0, Math.min(1, (this.minus(v)).dot(w.minus(v)) / l2)); - + let projection = v.plus((w.minus(v)).times(t)); // Projection falls on the segment return this.fake_distance(projection); } @@ -287,13 +280,22 @@ Bangle.loadWidgets(); let path = new Path("test.gpc"); let status = new Status(path); +let frame = 0; function set_coordinates(data) { + frame += 1; let valid_coordinates = !isNaN(data.lat) && !isNaN(data.lon); if (valid_coordinates) { let direction = data.course * Math.PI / 180.0; let position = new Point(data.lon, data.lat); status.update_position(position, direction); } + let gps_status_color; + if ((frame % 2 == 0)||valid_coordinates) { + gps_status_color = g.theme.bg; + } else { + gps_status_color = g.theme.fg; + } + g.setColor(gps_status_color).setFont("6x8:2").drawString("gps", g.getWidth() - 40, 30); } let fake_gps_point = 0.0; @@ -319,6 +321,14 @@ if (simulated) { status.position = new Point(status.path.point(0)); setInterval(simulate_gps, 500, status); } else { + // let's display start while waiting for gps signal + let p1 = status.path.point(0); + let p2 = status.path.point(1); + let diff = p2.minus(p1); + let direction = Math.atan2(diff.lat, diff.lon); + Bangle.setLocked(false); + status.update_position(p1, direction); + Bangle.setGPSPower(true, "gipy"); Bangle.on('GPS', set_coordinates); } diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index a6033bfb2..3b6f7e9f2 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -498,9 +498,11 @@ fn detect_waypoints(points: &[Point]) -> HashSet { let x2 = p3.x - p2.x; let y2 = p3.y - p2.y; let a2 = y2.atan2(x2); - if (a2 - a1).abs() <= std::f64::consts::PI / 3.0 { + let a = (a2 - a1).abs(); + if a <= std::f64::consts::PI / 3.0 || a >= std::f64::consts::PI * 5.0 / 3.0 { None } else { + eprintln!("we have {}", (a2 - a1).abs()); Some(p2) } })) diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index f5baf2eeb..5fb8c4944 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.05", + "version": "0.06", "description": "Follow gpx files", "allow_emulator":false, "icon": "gipy.png", From e6b76cde3130ff676a13ca70d0c15e77a41bbd27 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 20 Jul 2022 15:09:25 +0200 Subject: [PATCH 020/106] gipy: v007 --- apps/gipy/ChangeLog | 5 ++++ apps/gipy/TODO | 5 ++-- apps/gipy/app.js | 48 +++++++++++++++++++++++++++--------- apps/gipy/gpconv/src/main.rs | 9 ++++--- 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 0410d07e7..1a9e16c05 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -10,3 +10,8 @@ * Special display for points with steep turns * Buzz on points with steep turns and unlock * Losing gps is now displayed + +0.07: + * We now use orientation to detect current segment + when segments overlap going in both directions. + * File format is now versioned. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 96642608f..e229268b2 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,12 +1,11 @@ -- nearest_segment : use direction to disambiguate - gps direction is weak when speed is low -- add a version number to gpc files + - water points ---> we group them following path by groups of cst_size and record segments ids marking limits -- store several tracks - turn off gps when moving to next waypoint +- store several tracks - display average speed - dynamic map rescale - display scale (100m) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 85d8cb11c..233680df7 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,5 +1,7 @@ -let simulated = false; +let simulated = true; +let code_version = 7; +let code_key = 47490; class Status { constructor(path) { @@ -36,11 +38,11 @@ class Status { this.position = new_position; // detect segment we are on now - let next_segment = this.path.nearest_segment(this.position, Math.max(0, this.current_segment-1), Math.min(this.current_segment+2, path.len - 1)); + let next_segment = this.path.nearest_segment(this.position, Math.max(0, this.current_segment-1), Math.min(this.current_segment+2, path.len - 1), this.cos_direction, this.sin_direction); if (this.is_lost(next_segment)) { // it did not work, try anywhere - next_segment = this.path.nearest_segment(this.position, 0, path.len - 1); + next_segment = this.path.nearest_segment(this.position, 0, path.len - 1, this.cos_direction, this.sin_direction); } // now check if we strayed away from path or back to it let lost = this.is_lost(next_segment); @@ -147,7 +149,15 @@ class Status { class Path { constructor(filename) { let buffer = require("Storage").readArrayBuffer(filename); - this.points = Float64Array(buffer); + let header = Uint16Array(buffer, 0, 3); + let key = header[0]; + let version = header[1]; + let points_number = header[2]; + if ((key != code_key)||(version>code_version)) { + E.showMessage("Invalid gpc file"); + return; + } + this.points = Float64Array(buffer, 3*2, points_number*2); } // if start, end or steep direction change @@ -189,18 +199,32 @@ class Path { return new Point(lon, lat); } - // return index of segment which is nearest from point - nearest_segment(point, start, end) { - let min_index = 0; - let min_distance = Number.MAX_VALUE; + // return index of segment which is nearest from point. + // we need a direction because we need there is an ambiguity + // for overlapping segments which are taken once to go and once to come back. + // (in the other direction). + nearest_segment(point, start, end, cos_direction, sin_direction) { + // we are going to compute two min distances, one for each direction. + let indices = [0, 0]; + let mins = [Number.MAX_VALUE, Number.MAX_VALUE]; this.on_segments(function(p1, p2, i) { + // we use the dot product to figure out if oriented correctly let distance = point.fake_distance_to_segment(p1, p2); - if (distance <= min_distance) { - min_distance = distance; - min_index = i - 1; + let diff = p2.minus(p1); + let dot = cos_direction * diff.lon + sin_direction * diff.lat; + let orientation = + (dot < 0); // index 0 is good orientation + if (distance <= mins[orientation]) { + mins[orientation] = distance; + indices[orientation] = i - 1; } }, start, end); - return min_index; + // by default correct orientation (0) wins + // but if other one is really closer, return other one + if (mins[1] < mins[0] / 10.0) { + return indices[1]; + } else { + return indices[0]; + } } get len() { return this.points.length / 2; diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index 3b6f7e9f2..c003bfb72 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -10,6 +10,9 @@ use gpx::Gpx; mod osm; use osm::InterestPoint; +const KEY: u16 = 47490; +const VERSION: u16 = 7; + #[derive(Debug, PartialEq, Clone, Copy)] pub struct Point { x: f64, @@ -328,8 +331,9 @@ fn save_coordinates>(path: P, points: &[Point]) -> std::io::Resul let mut writer = BufWriter::new(File::create(path)?); eprintln!("saving {} points", points.len()); - // writer.write_all(&xmin.to_be_bytes())?; - // writer.write_all(&ymin.to_be_bytes())?; + writer.write_all(&KEY.to_le_bytes())?; + writer.write_all(&VERSION.to_le_bytes())?; + writer.write_all(&(points.len() as u16).to_le_bytes())?; points .iter() .flat_map(|p| [p.x, p.y]) @@ -502,7 +506,6 @@ fn detect_waypoints(points: &[Point]) -> HashSet { if a <= std::f64::consts::PI / 3.0 || a >= std::f64::consts::PI * 5.0 / 3.0 { None } else { - eprintln!("we have {}", (a2 - a1).abs()); Some(p2) } })) From 8a1e71ce7606299bc6e9e24c8b96309541841e35 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 20 Jul 2022 15:17:22 +0200 Subject: [PATCH 021/106] fix --- apps/gipy/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 233680df7..223766440 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,5 +1,5 @@ -let simulated = true; +let simulated = false; let code_version = 7; let code_key = 47490; From dafca5136511875260e7fc6aaff11033df152410 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 20 Jul 2022 17:02:03 +0200 Subject: [PATCH 022/106] update metadata --- apps/gipy/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 5fb8c4944..16b0a5916 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.06", + "version": "0.07", "description": "Follow gpx files", "allow_emulator":false, "icon": "gipy.png", From b70bebdbacb7a22675ac700691ce1040f430dbfe Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 20 Jul 2022 17:57:38 +0200 Subject: [PATCH 023/106] v008 --- apps/gipy/ChangeLog | 11 +++++--- apps/gipy/TODO | 7 ++++-- apps/gipy/app.js | 49 ++++++++++++++++++++++++++++-------- apps/gipy/gpconv/src/main.rs | 4 +-- apps/gipy/metadata.json | 2 +- 5 files changed, 54 insertions(+), 19 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 1a9e16c05..536692371 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -7,11 +7,16 @@ * We display remaining distance to next point. 0.06: - * Special display for points with steep turns - * Buzz on points with steep turns and unlock - * Losing gps is now displayed + * Special display for points with steep turns. + * Buzz on points with steep turns and unlock. + * Losing gps is now displayed. 0.07: * We now use orientation to detect current segment when segments overlap going in both directions. * File format is now versioned. + +0.08: + * Don't use gps course anymore but figure it from previous positions. + * Bugfix: path colors are back. + * Always buzz when reaching waypoint even if unlocked. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index e229268b2..f0ccbd299 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,10 +1,13 @@ -- gps direction is weak when speed is low - - water points ---> we group them following path by groups of cst_size and record segments ids marking limits - turn off gps when moving to next waypoint +- meters seem to be a bit too long + +- buzzing does not work nicely + -> is_list seems fishy + - store several tracks - display average speed - dynamic map rescale diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 223766440..aa523f005 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,6 +1,6 @@ let simulated = false; -let code_version = 7; +let file_version = 1; let code_key = 47490; class Status { @@ -46,12 +46,10 @@ class Status { } // now check if we strayed away from path or back to it let lost = this.is_lost(next_segment); - if (this.on_path == lost) { + if (this.on_path == lost) { // if status changes if (lost) { Bangle.buzz(); // we lost path - setTimeout(()=>Bangle.buzz(), 300); - } else { - Bangle.buzz(); // we found path back + setTimeout(()=>Bangle.buzz(), 500); } this.on_path = !lost; } @@ -63,9 +61,10 @@ class Status { this.distance_to_next_point = Math.ceil(this.position.distance(this.path.point(next_point))); if (this.reaching != next_point && this.distance_to_next_point <= 20) { this.reaching = next_point; - if (Bangle.isLocked()) { - if (this.path.is_waypoint(next_point)) { - Bangle.buzz(); + let reaching_waypoint = this.path.is_waypoint(next_point); + if (reaching_waypoint) { + Bangle.buzz(); + if (Bangle.isLocked()) { Bangle.setLocked(false); } } @@ -113,8 +112,9 @@ class Status { let sin = this.sin_direction; // segments + let current_segment = this.current_segment; this.path.on_segments(function(p1, p2, i) { - if (i == this.current_segment + 1) { + if (i == current_segment + 1) { g.setColor(0.0, 1.0, 0.0); } else { g.setColor(1.0, 0.0, 0.0); @@ -153,7 +153,7 @@ class Path { let key = header[0]; let version = header[1]; let points_number = header[2]; - if ((key != code_key)||(version>code_version)) { + if ((key != code_key)||(version>file_version)) { E.showMessage("Invalid gpc file"); return; } @@ -305,13 +305,40 @@ let path = new Path("test.gpc"); let status = new Status(path); let frame = 0; +let old_points = []; // remember the at most 3 previous points function set_coordinates(data) { frame += 1; let valid_coordinates = !isNaN(data.lat) && !isNaN(data.lon); if (valid_coordinates) { + // we try to figure out direction by looking at previous points + // instead of the gps course which is not very nice. let direction = data.course * Math.PI / 180.0; let position = new Point(data.lon, data.lat); - status.update_position(position, direction); + if (old_points.length == 0) { + old_points.push(position); + } else { + let last_point = old_points[old_points.length-1]; + if (last_point.x != position.x || last_point.y != position.y) { + if (old_points.length == 4) { + old_points.shift(); + } + old_points.push(position); + } + } + if (old_points.length == 4) { + // let's just take average angle of 3 previous segments + let angles_sum = 0; + for(let i = 0 ; i < 3 ; i++) { + let p1 = old_points[i]; + let p2 = old_points[i+1]; + let diff = p2.minus(p1); + let angle = Math.atan2(diff.lat, diff.lon); + angles_sum += angle; + } + status.update_position(position, angles_sum / 3.0); + } else { + status.update_position(position, direction); + } } let gps_status_color; if ((frame % 2 == 0)||valid_coordinates) { diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index c003bfb72..95b21c63f 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -11,7 +11,7 @@ mod osm; use osm::InterestPoint; const KEY: u16 = 47490; -const VERSION: u16 = 7; +const FILE_VERSION: u16 = 1; #[derive(Debug, PartialEq, Clone, Copy)] pub struct Point { @@ -332,7 +332,7 @@ fn save_coordinates>(path: P, points: &[Point]) -> std::io::Resul eprintln!("saving {} points", points.len()); writer.write_all(&KEY.to_le_bytes())?; - writer.write_all(&VERSION.to_le_bytes())?; + writer.write_all(&FILE_VERSION.to_le_bytes())?; writer.write_all(&(points.len() as u16).to_le_bytes())?; points .iter() diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 16b0a5916..18c692733 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.07", + "version": "0.08", "description": "Follow gpx files", "allow_emulator":false, "icon": "gipy.png", From 2c410ecdfebc920710b67e3288404aae2c36bac9 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Thu, 21 Jul 2022 11:17:57 +0200 Subject: [PATCH 024/106] preparing for interest points --- apps/gipy/TODO | 4 +- apps/gipy/gpconv/Cargo.lock | 297 ++++++++++++++++++++++++++++++++- apps/gipy/gpconv/Cargo.toml | 1 + apps/gipy/gpconv/README_gpc.md | 58 +++++++ apps/gipy/gpconv/src/main.rs | 117 ++++++++++++- apps/gipy/gpconv/src/osm.rs | 51 +++++- 6 files changed, 515 insertions(+), 13 deletions(-) create mode 100644 apps/gipy/gpconv/README_gpc.md diff --git a/apps/gipy/TODO b/apps/gipy/TODO index f0ccbd299..bad276a60 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,12 +1,14 @@ - water points ---> we group them following path by groups of cst_size and record segments ids marking limits +- split on points with comments + - turn off gps when moving to next waypoint - meters seem to be a bit too long - buzzing does not work nicely - -> is_list seems fishy + -> is_lost seems fishy - store several tracks - display average speed diff --git a/apps/gipy/gpconv/Cargo.lock b/apps/gipy/gpconv/Cargo.lock index da2fdaac5..50bc76cb4 100644 --- a/apps/gipy/gpconv/Cargo.lock +++ b/apps/gipy/gpconv/Cargo.lock @@ -17,6 +17,23 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "anyhow" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" + [[package]] name = "assert_approx_eq" version = "1.1.0" @@ -62,12 +79,39 @@ version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "bzip2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cc" version = "1.0.73" @@ -80,6 +124,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time 0.1.44", + "winapi", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -96,6 +153,81 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "darling" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73" +dependencies = [ + "derive_builder_core", + "syn", +] + [[package]] name = "either" version = "1.6.1" @@ -121,6 +253,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "1.7.0" @@ -130,6 +274,16 @@ dependencies = [ "instant", ] +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -209,6 +363,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + [[package]] name = "gimli" version = "0.26.1" @@ -223,6 +388,7 @@ dependencies = [ "itertools", "lazy_static", "openstreetmap-api", + "osmio", "tokio", ] @@ -236,7 +402,7 @@ dependencies = [ "error-chain", "geo-types", "thiserror", - "time", + "time 0.3.11", "xml-rs", ] @@ -259,12 +425,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown 0.11.2", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -345,6 +529,12 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -363,7 +553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", ] [[package]] @@ -381,6 +571,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +[[package]] +name = "iter-progress" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97059d64dd4e3a8e16696f6c0be50c1d5da3a709983f39b73fd7f84f120c5cd4" + [[package]] name = "itertools" version = "0.10.3" @@ -417,6 +613,16 @@ version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +[[package]] +name = "libsqlite3-sys" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.7" @@ -471,7 +677,7 @@ checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -493,6 +699,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -597,6 +813,28 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "osmio" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0db40ae840afac7f6c710abf757bb76c6b95b4a34a20d55811ef70d30b3ea24f" +dependencies = [ + "anyhow", + "byteorder", + "bzip2", + "chrono", + "derive_builder", + "flate2", + "iter-progress", + "protobuf", + "quick-xml", + "rusqlite", + "separator", + "serde", + "serde_json", + "xml-rs", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -653,6 +891,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "protobuf" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70731852eec72c56d11226c8a5f96ad5058a3dab73647ca5f7ee351e464f2571" + [[package]] name = "quick-xml" version = "0.22.0" @@ -727,6 +971,21 @@ dependencies = [ "winreg", ] +[[package]] +name = "rusqlite" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4b1eaf239b47034fb450ee9cdedd7d0226571689d8823030c4b6c2cb407152" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -778,11 +1037,20 @@ dependencies = [ "libc", ] +[[package]] +name = "separator" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f97841a747eef040fcd2e7b3b9a220a7205926e60488e673d9e4926d27772ce5" + [[package]] name = "serde" version = "1.0.139" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_derive" @@ -849,6 +1117,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.98" @@ -894,6 +1168,17 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "time" version = "0.3.11" @@ -1069,6 +1354,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/apps/gipy/gpconv/Cargo.toml b/apps/gipy/gpconv/Cargo.toml index b6053a3ce..681c86c7e 100644 --- a/apps/gipy/gpconv/Cargo.toml +++ b/apps/gipy/gpconv/Cargo.toml @@ -11,3 +11,4 @@ itertools="*" openstreetmap-api="*" tokio={version="1", features=["full"]} lazy_static="*" +osmio="*" diff --git a/apps/gipy/gpconv/README_gpc.md b/apps/gipy/gpconv/README_gpc.md new file mode 100644 index 000000000..a881d01bf --- /dev/null +++ b/apps/gipy/gpconv/README_gpc.md @@ -0,0 +1,58 @@ +This file documents the .gpc file format. + +current version is version 2. + +every number is encoded in little endian order. + +# header + +We start by a header of 5 16bytes unsigned ints. + +- the first int is a marker with value 47490 +- second int is version of this file format +- third int is **NP** the number of points composing the path +- fourth int is **IP** the number of interest points bordering the path +- fifth int is **LP** the number of interest points as encountered when looping through the path (higher than previous int since some points can be met several times) + +# points + +We continue with an array of **2 NP** f64 containing +for each point its x and y coordinate. + +# interest points + +After that comes the storage for interest points. + +- we start by an array of **2 IP** f64 containing +the x and y coordinates of each interest point +- we continue with an **IP** u8 array named **IPOINTS** containing the id of the point's type. +you can see the `Interest` enum in `src/osm.rs` to know what int is what. +for example 0 is a Bakery and 1 is a water point. + +Now we need to store the relationship between segments and points. +The idea is that in a display phase we don't want to loop on all interest points +to figure out if they should appear on the map or not. +We'll use the fact that we now the segments we want to display and therefore we should only +need to display the points bordering these segments. + +- we store an array **LOOP** of **LP** u16 indices of interest points in **IPOINTS** + +while this is a contiguous array it contains points along the path grouped in buckets of 5 points. + +to figure out on which segments they are : + +- we store an array **STARTS** of **ceil(LP/5)** u16 indices of groups of segments. + +Segments are grouped by 3. +This array tells us on which group of segment is the first point of any bucket. + +## display algorithm + +If we want to display the interest points for the segments between 10 and 16 for example we proceed +as follows: + + * segments are grouped by 3 so instead of segment indices of 10..=16 we will look at group indices 10/3 ..= 16/3 so 3..=5 + * we do a binary search of the highest number below 3 in the **STARTS** array. we call *s* the obtained index + * we do a binary search of the smallest number above 5 in the **STARTS** array. we call *e* the obtained index + * we now loop on all buckets between *s* and *e* that is : on all indices *i* in `LOOP[(s*5)..=(e*5)]` + * display `IPOINTS[i]` diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index 95b21c63f..811a4c361 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -8,10 +8,10 @@ use gpx::read; use gpx::Gpx; mod osm; -use osm::InterestPoint; +use osm::{parse_osm_data, InterestPoint}; const KEY: u16 = 47490; -const FILE_VERSION: u16 = 1; +const FILE_VERSION: u16 = 2; #[derive(Debug, PartialEq, Clone, Copy)] pub struct Point { @@ -327,18 +327,57 @@ fn compress_coordinates(points: &[(i32, i32)]) -> Vec<(i16, i16)> { xdiffs.zip(ydiffs).collect() } -fn save_coordinates>(path: P, points: &[Point]) -> std::io::Result<()> { +fn save_gpc>(path: P, points: &[Point], buckets: &[Bucket]) -> std::io::Result<()> { let mut writer = BufWriter::new(File::create(path)?); eprintln!("saving {} points", points.len()); + + let mut unique_interest_points = Vec::new(); + let mut correspondance = HashMap::new(); + let interests_on_path = buckets + .iter() + .flat_map(|b| &b.points) + .map(|p| match correspondance.entry(*p) { + std::collections::hash_map::Entry::Occupied(o) => *o.get(), + std::collections::hash_map::Entry::Vacant(v) => { + let index = unique_interest_points.len(); + unique_interest_points.push(*p); + v.insert(index); + index + } + }) + .collect::>(); + writer.write_all(&KEY.to_le_bytes())?; writer.write_all(&FILE_VERSION.to_le_bytes())?; writer.write_all(&(points.len() as u16).to_le_bytes())?; + writer.write_all(&(unique_interest_points.len() as u16).to_le_bytes())?; + writer.write_all(&(interests_on_path.len() as u16).to_le_bytes())?; points .iter() .flat_map(|p| [p.x, p.y]) .try_for_each(|c| writer.write_all(&c.to_le_bytes()))?; + unique_interest_points + .iter() + .flat_map(|p| [p.point.x, p.point.y]) + .try_for_each(|c| writer.write_all(&c.to_le_bytes()))?; + + unique_interest_points + .iter() + .map(|p| p.interest.into()) + .try_for_each(|i: u8| writer.write_all(&i.to_le_bytes()))?; + + interests_on_path + .iter() + .map(|i| *i as u16) + .try_for_each(|i| writer.write_all(&i.to_le_bytes()))?; + + buckets + .iter() + .map(|b| b.start as u16) + .try_for_each(|i| writer.write_all(&i.to_le_bytes()))?; + Ok(()) } @@ -419,11 +458,11 @@ fn save_path(writer: &mut W, p: &[Point], stroke: &str) -> std::io::Re Ok(()) } -fn save_svg>( +fn save_svg<'a, P: AsRef, I: IntoIterator>( filename: P, p: &[Point], rp: &[Point], - interest_points: &HashSet, + interest_points: I, waypoints: &HashSet, ) -> std::io::Result<()> { let mut writer = BufWriter::new(std::fs::File::create(filename)?); @@ -514,6 +553,58 @@ fn detect_waypoints(points: &[Point]) -> HashSet { .collect::>() } +pub struct Bucket { + points: Vec, + start: usize, +} + +fn position_interests_along_path( + interests: &mut [InterestPoint], + path: &[Point], + d: f64, + buckets_size: usize, // final points are indexed in buckets + groups_size: usize, // how many segments are compacted together +) -> Vec { + interests.sort_unstable_by(|p1, p2| p1.point.x.partial_cmp(&p2.point.x).unwrap()); + // first compute for each segment a vec containing its nearby points + let mut positions = Vec::new(); + for segment in path.windows(2) { + let mut local_interests = Vec::new(); + let x0 = segment[0].x; + let x1 = segment[1].x; + let (xmin, xmax) = if x0 <= x1 { (x0, x1) } else { (x1, x0) }; + let i = interests.partition_point(|p| p.point.x < xmin - d); + let interests = &interests[i..]; + let i = interests.partition_point(|p| p.point.x <= xmax + d); + let interests = &interests[..i]; + for interest in interests { + if interest.point.distance_to_segment(&segment[0], &segment[1]) <= d { + local_interests.push(*interest); + } + } + positions.push(local_interests); + } + // fuse points on chunks of consecutive segments together + let grouped_positions = positions + .chunks(groups_size) + .map(|c| c.iter().flatten().unique().copied().collect::>()) + .collect::>(); + // now, group the points in buckets + let chunks = grouped_positions + .iter() + .enumerate() + .flat_map(|(i, points)| points.iter().map(move |p| (i, p))) + .chunks(buckets_size); + let mut buckets = Vec::new(); + for bucket_points in &chunks { + let mut bucket_points = bucket_points.peekable(); + let start = bucket_points.peek().unwrap().0; + let points = bucket_points.map(|(_, p)| *p).collect(); + buckets.push(Bucket { points, start }); + } + buckets +} + #[tokio::main] async fn main() { let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); @@ -529,8 +620,18 @@ async fn main() { eprintln!("rdp would have had {}", rdp(&p, 0.00015).len()); eprintln!("rdp took {:?}", start.elapsed()); - save_coordinates("test.gpc", &rp).unwrap(); + let mut interests = parse_osm_data("isere.osm.pbf"); + let buckets = position_interests_along_path(&mut interests, &rp, 0.0005, 5, 3); // let i = get_openstreetmap_data(&rp).await; - let i = HashSet::new(); - save_svg("test.svg", &p, &rp, &i, &waypoints).unwrap(); + // let i = HashSet::new(); + save_svg( + "test.svg", + &p, + &rp, + buckets.iter().flat_map(|b| &b.points), + &waypoints, + ) + .unwrap(); + + save_gpc("test.gpc", &rp, &buckets).unwrap(); } diff --git a/apps/gipy/gpconv/src/osm.rs b/apps/gipy/gpconv/src/osm.rs index a70e7ff5d..6f5cdc4bc 100644 --- a/apps/gipy/gpconv/src/osm.rs +++ b/apps/gipy/gpconv/src/osm.rs @@ -1,10 +1,14 @@ use super::Point; +use itertools::Itertools; use lazy_static::lazy_static; use openstreetmap_api::{ types::{BoundingBox, Credentials}, Openstreetmap, }; +use osmio::prelude::*; +use osmio::OSMObjBase; use std::collections::{HashMap, HashSet}; +use std::path::Path; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Interest { @@ -21,7 +25,24 @@ pub enum Interest { Pharmacy, } -#[derive(Debug, PartialEq, Eq, Hash)] +impl Into for Interest { + fn into(self) -> u8 { + match self { + Interest::Bakery => 0, + Interest::DrinkingWater => 1, + Interest::Toilets => 2, + Interest::BikeShop => 3, + Interest::ChargingStation => 4, + Interest::Bank => 5, + Interest::Supermarket => 6, + Interest::Table => 7, + Interest::Artwork => 8, + Interest::Pharmacy => 9, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct InterestPoint { pub point: Point, pub interest: Interest, @@ -128,3 +149,31 @@ async fn get_openstreetmap_data(points: &[(f64, f64)]) -> HashSet } interest_points } + +pub fn parse_osm_data>(path: P) -> Vec { + let reader = osmio::read_pbf(path).ok(); + reader + .map(|mut reader| { + let mut interests = Vec::new(); + for obj in reader.objects() { + match obj { + osmio::obj_types::ArcOSMObj::Node(n) => { + n.lat_lon_f64().map(|(lat, lon)| { + for p in n.tags().filter_map(move |(k, v)| { + Interest::new(k, v).map(|i| InterestPoint { + point: Point { x: lon, y: lat }, + interest: i, + }) + }) { + interests.push(p); + } + }); + } + osmio::obj_types::ArcOSMObj::Way(_) => {} + osmio::obj_types::ArcOSMObj::Relation(_) => {} + } + } + interests + }) + .unwrap_or_default() +} From b2a7b65c5bb4daea6c7730c5413da8570162ddbb Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Thu, 21 Jul 2022 12:35:24 +0200 Subject: [PATCH 025/106] parsing new file format --- apps/gipy/ChangeLog | 3 ++ apps/gipy/app.js | 69 +++++++++++++++++++++++++++++++++++++---- apps/gipy/metadata.json | 2 +- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 536692371..260abaf0e 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -20,3 +20,6 @@ * Don't use gps course anymore but figure it from previous positions. * Bugfix: path colors are back. * Always buzz when reaching waypoint even if unlocked. + +0.09: + * We now display interest points. diff --git a/apps/gipy/app.js b/apps/gipy/app.js index aa523f005..6fe05ee65 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,8 +1,21 @@ let simulated = false; -let file_version = 1; +let file_version = 2; let code_key = 47490; +let interests_colors = [ + 0x780F, // Bakery, purple + 0x001F, // DrinkingWater, blue + 0x07FF, // Toilets, cyan + 0x7BEF, // BikeShop, dark grey + 0xAFE5, // ChargingStation, green yellow + 0x7800, // Bank, maroon + 0xF800, // Supermarket, red + 0xF81F, // Table, pink + 0xFD20, // Artwork, orange + 0x07E0, // Pharmacy, green +]; + class Status { constructor(path) { this.path = path; @@ -70,7 +83,7 @@ class Status { } } // re-display unless locked - if (!Bangle.isLocked()) { + if (!Bangle.isLocked() || simulated) { this.display(); } } @@ -85,9 +98,18 @@ class Status { display() { g.clear(); this.display_map(); + this.display_interest_points(); this.display_stats(); Bangle.drawWidgets(); } + display_interest_points() { + for (let i = 0 ; i < this.path.interests_coordinates.length ; i++) { + let p = this.path.interest_point(i); + let color = this.path.interest_color(i); + let c = p.coordinates(this.position, this.cos_direction, this.sin_direction); + g.setColor(color).fillCircle(c[0], c[1], 5); + } + } display_stats() { let rounded_distance = Math.round(this.remaining_distance() / 100) / 10; let total = Math.round(this.remaining_distances[0] / 100) / 10; @@ -149,7 +171,11 @@ class Status { class Path { constructor(filename) { let buffer = require("Storage").readArrayBuffer(filename); - let header = Uint16Array(buffer, 0, 3); + let offset = 0; + + // header + let header = Uint16Array(buffer, offset, 5); + offset += 5 * 2; let key = header[0]; let version = header[1]; let points_number = header[2]; @@ -157,7 +183,25 @@ class Path { E.showMessage("Invalid gpc file"); return; } - this.points = Float64Array(buffer, 3*2, points_number*2); + + // path points + this.points = Float64Array(buffer, offset, points_number*2); + + // interest points + offset += 8 * points_number * 2; + let interests_number = header[3]; + this.interests_coordinates = Float64Array(buffer, offset, interests_number * 2); + offset += 8 * interests_number * 2; + this.interests_types = Uint8Array(buffer, offset, interests_number); + offset += interests_number; + + // interests on path + let interests_on_path_number = header[4]; + this.interests_on_path = Uint16Array(buffer, offset, interests_on_path_number); + offset += 2 * interests_on_path_number; + let starts_length = Math.ceil(interests_on_path_number / 5.0); + this.interests_starts = Uint16Array(buffer, offset, starts_length); + offset += 2 * starts_length; } // if start, end or steep direction change @@ -199,6 +243,16 @@ class Path { return new Point(lon, lat); } + interest_point(index) { + let lon = this.interests_coordinates[2 * index]; + let lat = this.interests_coordinates[2 * index + 1]; + return new Point(lon, lat); + } + + interest_color(index) { + return interests_colors[this.interests_types[index]]; + } + // return index of segment which is nearest from point. // we need a direction because we need there is an ambiguity // for overlapping segments which are taken once to go and once to come back. @@ -237,7 +291,7 @@ class Point { this.lat = lat; } coordinates(current_position, cos_direction, sin_direction) { - let translated = this.minus(current_position).times(20000.0); + let translated = this.minus(current_position).times(40000.0); let rotated_x = translated.lon * cos_direction - translated.lat * sin_direction; let rotated_y = translated.lon * sin_direction + translated.lat * cos_direction; return [ @@ -351,6 +405,9 @@ function set_coordinates(data) { let fake_gps_point = 0.0; function simulate_gps(status) { + if (fake_gps_point > status.path.len -1) { + return; + } let point_index = Math.floor(fake_gps_point); if (point_index >= path.len) { return; @@ -362,7 +419,7 @@ function simulate_gps(status) { let pos = p1.times(1-alpha).plus(p2.times(alpha)); let old_pos = status.position; - fake_gps_point += 0.05; // advance simulation + fake_gps_point += 0.2; // advance simulation let direction = Math.atan2(pos.lat - old_pos.lat, pos.lon - old_pos.lon); status.update_position(pos, direction); } diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 18c692733..2ff1d3acd 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.08", + "version": "0.09", "description": "Follow gpx files", "allow_emulator":false, "icon": "gipy.png", From d4a755660ed5e124e49f0cf73faf1f1b3bffe3a9 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Thu, 21 Jul 2022 14:19:41 +0200 Subject: [PATCH 026/106] complex algorithm for interest points seems ok --- apps/gipy/app.js | 33 ++++++++++++++++++++++++++++----- apps/gipy/gpconv/src/main.rs | 10 ++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 6fe05ee65..a463f15e9 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,5 +1,5 @@ -let simulated = false; +let simulated = true; let file_version = 2; let code_key = 47490; @@ -16,6 +16,19 @@ let interests_colors = [ 0x07E0, // Pharmacy, green ]; +function binary_search(array, x) { + let start = 0, end = array.length - 1; + + while (start <= end){ + let mid = Math.floor((start + end) / 2); + if (array[mid] < x) + start = mid + 1; + else + end = mid - 1; + } + return start; +} + class Status { constructor(path) { this.path = path; @@ -103,10 +116,18 @@ class Status { Bangle.drawWidgets(); } display_interest_points() { - for (let i = 0 ; i < this.path.interests_coordinates.length ; i++) { - let p = this.path.interest_point(i); + // this is the algorithm in case we have a lot of interest points + // let's draw all points for 5 segments centered on current one + let starting_group = Math.floor(Math.max(this.current_segment-2, 0) / 3); + let ending_group = Math.floor(Math.min(this.current_segment+2, this.path.len-2) / 3); + let starting_bucket = binary_search(this.path.interests_starts, starting_group); + let ending_bucket = binary_search(this.path.interests_starts, ending_group+0.5); + // we have 5 points per bucket + for (let i = starting_bucket*5 ; i <= ending_bucket*5 ; i++) { + let index = this.path.interests_on_path[i]; + let interest_point = this.path.interest_point(index); let color = this.path.interest_color(i); - let c = p.coordinates(this.position, this.cos_direction, this.sin_direction); + let c = interest_point.coordinates(this.position, this.cos_direction, this.sin_direction); g.setColor(color).fillCircle(c[0], c[1], 5); } } @@ -119,7 +140,7 @@ class Status { minutes = '0' + minutes; } let hours = now.getHours().toString(); - g.setFont("6x8:2").drawString(hours + ":" + minutes, 0, g.getHeight() - 49); + g.setFont("6x8:2").setColor(g.theme.fg).drawString(hours + ":" + minutes, 0, g.getHeight() - 49); g.drawString("d. " + rounded_distance + "/" + total, 0, g.getHeight() - 32); g.drawString("seg." + (this.current_segment + 1) + "/" + path.len + " " + this.distance_to_next_point + "m", 0, g.getHeight() - 15); } @@ -441,3 +462,5 @@ if (simulated) { Bangle.on('GPS', set_coordinates); } + + diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index 811a4c361..48da9a76b 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -378,6 +378,9 @@ fn save_gpc>(path: P, points: &[Point], buckets: &[Bucket]) -> st .map(|b| b.start as u16) .try_for_each(|i| writer.write_all(&i.to_le_bytes()))?; + let starts: Vec<_> = buckets.iter().map(|b| b.start).collect(); + eprintln!("starts {:?} of length {}", starts, starts.len()); + Ok(()) } @@ -589,6 +592,13 @@ fn position_interests_along_path( .chunks(groups_size) .map(|c| c.iter().flatten().unique().copied().collect::>()) .collect::>(); + eprintln!( + "group sizes are {:?}", + grouped_positions + .iter() + .map(|g| g.len()) + .collect::>() + ); // now, group the points in buckets let chunks = grouped_positions .iter() From 1bce4cf8e049d7de0ced5b20178a70b6ed343af1 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Thu, 21 Jul 2022 14:20:50 +0200 Subject: [PATCH 027/106] updated todo list --- apps/gipy/TODO | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/gipy/TODO b/apps/gipy/TODO index bad276a60..222177c8e 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,5 +1,3 @@ -- water points - ---> we group them following path by groups of cst_size and record segments ids marking limits - split on points with comments From b1e3db6d81c03f3030d723dd9a89c0e4f79cf648 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Thu, 21 Jul 2022 15:09:16 +0200 Subject: [PATCH 028/106] v009 --- apps/gipy/ChangeLog | 1 + apps/gipy/TODO | 2 +- apps/gipy/app.js | 167 ++++++++++++++++++++--------------- apps/gipy/gpconv/src/main.rs | 8 -- 4 files changed, 100 insertions(+), 78 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 260abaf0e..932e1282c 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -23,3 +23,4 @@ 0.09: * We now display interest points. + * Menu to choose which file to load. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 222177c8e..b740d0d71 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,4 +1,5 @@ +- it is becoming messy - split on points with comments - turn off gps when moving to next waypoint @@ -8,7 +9,6 @@ - buzzing does not work nicely -> is_lost seems fishy -- store several tracks - display average speed - dynamic map rescale - display scale (100m) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index a463f15e9..5f5d9604c 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,5 +1,5 @@ -let simulated = true; +let simulated = false; let file_version = 2; let code_key = 47490; @@ -64,11 +64,11 @@ class Status { this.position = new_position; // detect segment we are on now - let next_segment = this.path.nearest_segment(this.position, Math.max(0, this.current_segment-1), Math.min(this.current_segment+2, path.len - 1), this.cos_direction, this.sin_direction); + let next_segment = this.path.nearest_segment(this.position, Math.max(0, this.current_segment-1), Math.min(this.current_segment+2, this.path.len - 1), this.cos_direction, this.sin_direction); if (this.is_lost(next_segment)) { // it did not work, try anywhere - next_segment = this.path.nearest_segment(this.position, 0, path.len - 1, this.cos_direction, this.sin_direction); + next_segment = this.path.nearest_segment(this.position, 0, this.path.len - 1, this.cos_direction, this.sin_direction); } // now check if we strayed away from path or back to it let lost = this.is_lost(next_segment); @@ -123,7 +123,8 @@ class Status { let starting_bucket = binary_search(this.path.interests_starts, starting_group); let ending_bucket = binary_search(this.path.interests_starts, ending_group+0.5); // we have 5 points per bucket - for (let i = starting_bucket*5 ; i <= ending_bucket*5 ; i++) { + let end_index = Math.min(this.path.interests_types.length - 1, ending_bucket*5); + for (let i = starting_bucket*5 ; i <= end_index ; i++) { let index = this.path.interests_on_path[i]; let interest_point = this.path.interest_point(index); let color = this.path.interest_color(i); @@ -142,7 +143,7 @@ class Status { let hours = now.getHours().toString(); g.setFont("6x8:2").setColor(g.theme.fg).drawString(hours + ":" + minutes, 0, g.getHeight() - 49); g.drawString("d. " + rounded_distance + "/" + total, 0, g.getHeight() - 32); - g.drawString("seg." + (this.current_segment + 1) + "/" + path.len + " " + this.distance_to_next_point + "m", 0, g.getHeight() - 15); + g.drawString("seg." + (this.current_segment + 1) + "/" + this.path.len + " " + this.distance_to_next_point + "m", 0, g.getHeight() - 15); } display_map() { // don't display all segments, only those neighbouring current segment @@ -376,53 +377,7 @@ class Point { Bangle.loadWidgets(); -let path = new Path("test.gpc"); -let status = new Status(path); -let frame = 0; -let old_points = []; // remember the at most 3 previous points -function set_coordinates(data) { - frame += 1; - let valid_coordinates = !isNaN(data.lat) && !isNaN(data.lon); - if (valid_coordinates) { - // we try to figure out direction by looking at previous points - // instead of the gps course which is not very nice. - let direction = data.course * Math.PI / 180.0; - let position = new Point(data.lon, data.lat); - if (old_points.length == 0) { - old_points.push(position); - } else { - let last_point = old_points[old_points.length-1]; - if (last_point.x != position.x || last_point.y != position.y) { - if (old_points.length == 4) { - old_points.shift(); - } - old_points.push(position); - } - } - if (old_points.length == 4) { - // let's just take average angle of 3 previous segments - let angles_sum = 0; - for(let i = 0 ; i < 3 ; i++) { - let p1 = old_points[i]; - let p2 = old_points[i+1]; - let diff = p2.minus(p1); - let angle = Math.atan2(diff.lat, diff.lon); - angles_sum += angle; - } - status.update_position(position, angles_sum / 3.0); - } else { - status.update_position(position, direction); - } - } - let gps_status_color; - if ((frame % 2 == 0)||valid_coordinates) { - gps_status_color = g.theme.bg; - } else { - gps_status_color = g.theme.fg; - } - g.setColor(gps_status_color).setFont("6x8:2").drawString("gps", g.getWidth() - 40, 30); -} let fake_gps_point = 0.0; function simulate_gps(status) { @@ -430,11 +385,11 @@ function simulate_gps(status) { return; } let point_index = Math.floor(fake_gps_point); - if (point_index >= path.len) { + if (point_index >= status.path.len) { return; } - let p1 = path.point(point_index); - let p2 = path.point(point_index + 1); + let p1 = status.path.point(point_index); + let p2 = status.path.point(point_index + 1); let alpha = fake_gps_point - point_index; let pos = p1.times(1-alpha).plus(p2.times(alpha)); @@ -445,22 +400,96 @@ function simulate_gps(status) { status.update_position(pos, direction); } - -if (simulated) { - status.position = new Point(status.path.point(0)); - setInterval(simulate_gps, 500, status); -} else { - // let's display start while waiting for gps signal - let p1 = status.path.point(0); - let p2 = status.path.point(1); - let diff = p2.minus(p1); - let direction = Math.atan2(diff.lat, diff.lon); - Bangle.setLocked(false); - status.update_position(p1, direction); - - Bangle.setGPSPower(true, "gipy"); - Bangle.on('GPS', set_coordinates); +function drawMenu() { + const menu = { + '': { 'title': 'choose trace' } + }; + var files = require("Storage").list(".gpc"); + for (var i=0; i>(path: P, points: &[Point], buckets: &[Bucket]) -> st .try_for_each(|i| writer.write_all(&i.to_le_bytes()))?; let starts: Vec<_> = buckets.iter().map(|b| b.start).collect(); - eprintln!("starts {:?} of length {}", starts, starts.len()); Ok(()) } @@ -592,13 +591,6 @@ fn position_interests_along_path( .chunks(groups_size) .map(|c| c.iter().flatten().unique().copied().collect::>()) .collect::>(); - eprintln!( - "group sizes are {:?}", - grouped_positions - .iter() - .map(|g| g.len()) - .collect::>() - ); // now, group the points in buckets let chunks = grouped_positions .iter() From 9650cb0204870191117254260baf80bf41c2ef6a Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Thu, 21 Jul 2022 17:58:12 +0200 Subject: [PATCH 029/106] faster display --- apps/gipy/ChangeLog | 3 ++ apps/gipy/app.js | 70 ++++++++++++++++++++++++++--------------- apps/gipy/metadata.json | 2 +- 3 files changed, 49 insertions(+), 26 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 932e1282c..25a2e7fba 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -24,3 +24,6 @@ 0.09: * We now display interest points. * Menu to choose which file to load. + +0.10: + * Display performances enhancement. diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 5f5d9604c..339a59116 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -149,44 +149,65 @@ class Status { // don't display all segments, only those neighbouring current segment // this is most likely to be the correct display // while lowering the cost a lot - let start = Math.max(this.current_segment - 5, 0); - let end = Math.min(this.current_segment + 6, this.path.len - 1); + // + // note that all code is inlined here to speed things up from 400ms to 200ms + let start = Math.max(this.current_segment - 3, 0); + let end = Math.min(this.current_segment + 5, this.path.len - 1); let pos = this.position; let cos = this.cos_direction; let sin = this.sin_direction; - - // segments - let current_segment = this.current_segment; - this.path.on_segments(function(p1, p2, i) { - if (i == current_segment + 1) { - g.setColor(0.0, 1.0, 0.0); - } else { - g.setColor(1.0, 0.0, 0.0); + let points = this.path.points; + let cx = pos.lon; + let cy = pos.lat; + let half_width = g.getWidth() / 2; + let half_height = g.getHeight() / 2; + let previous_x = null; + let previous_y = null; + for (let i = start ; i < end ; i++) { + let tx = (points[2*i] - cx) * 40000.0; + let ty = (points[2*i+1] - cy) * 40000.0; + let rotated_x = tx * cos - ty * sin; + let rotated_y = tx * sin + ty * cos; + let x = half_width - Math.round(rotated_x); // x is inverted + let y = half_height + Math.round(rotated_y); + if (previous_x !== null) { + if (i == this.current_segment + 1) { + g.setColor(0.0, 1.0, 0.0); + } else { + g.setColor(1.0, 0.0, 0.0); + } + g.drawLine(previous_x, previous_y, x, y); } - let c1 = p1.coordinates(pos, cos, sin); - let c2 = p2.coordinates(pos, cos, sin); - g.drawLine(c1[0], c1[1], c2[0], c2[1]); - }, start, end); - // waypoints - for (let i = start ; i < end + 1 ; i++) { - let p = this.path.point(i); - let c = p.coordinates(pos, cos, sin); - if (this.path.is_waypoint(i)) { + if (this.path.is_waypoint(i-1)) { g.setColor(g.theme.fg); - g.fillCircle(c[0], c[1], 6); + g.fillCircle(previous_x, previous_y, 6); g.setColor(g.theme.bg); - g.fillCircle(c[0], c[1], 5); + g.fillCircle(previous_x, previous_y, 5); } g.setColor(g.theme.fg); - g.fillCircle(c[0], c[1], 4); + g.fillCircle(previous_x, previous_y, 4); g.setColor(g.theme.bg); - g.fillCircle(c[0], c[1], 3); + g.fillCircle(previous_x, previous_y, 3); + + previous_x = x; + previous_y = y; } + if (this.path.is_waypoint(end-1)) { + g.setColor(g.theme.fg); + g.fillCircle(previous_x, previous_y, 6); + g.setColor(g.theme.bg); + g.fillCircle(previous_x, previous_y, 5); + } + g.setColor(g.theme.fg); + g.fillCircle(previous_x, previous_y, 4); + g.setColor(g.theme.bg); + g.fillCircle(previous_x, previous_y, 3); + // now display ourselves g.setColor(g.theme.fgH); - g.fillCircle(g.getWidth() / 2, g.getHeight() / 2, 5); + g.fillCircle(half_width, half_height, 5); } } @@ -492,4 +513,3 @@ if (files.length <= 1) { } else { drawMenu(); } - diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 2ff1d3acd..3673abb30 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.09", + "version": "0.10", "description": "Follow gpx files", "allow_emulator":false, "icon": "gipy.png", From a255fc5c1efcf4dc1229e40d46a329f64aabd116 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Thu, 21 Jul 2022 20:49:49 +0200 Subject: [PATCH 030/106] waypoints from gmaps --- apps/gipy/TODO | 9 +++- apps/gipy/gpconv/src/main.rs | 99 +++++++++++++++++++++++++----------- apps/gipy/gpconv/src/osm.rs | 50 +++++++++--------- 3 files changed, 100 insertions(+), 58 deletions(-) diff --git a/apps/gipy/TODO b/apps/gipy/TODO index b740d0d71..b63a6b066 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,16 +1,21 @@ -- it is becoming messy +- direction is still shitty on gps - split on points with comments + and encode them as waypoints + +- code is becoming messy - turn off gps when moving to next waypoint +- display distance to next water/toilet - meters seem to be a bit too long - buzzing does not work nicely -> is_lost seems fishy + -> we need a display on top of buzz - display average speed - dynamic map rescale - display scale (100m) -- compress path +- compress path ? diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index e14bffe08..fd2f4bded 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -58,8 +58,7 @@ impl Point { } } -fn points(filename: &str) -> impl Iterator { - // This XML file actually exists — try it for yourself! +fn points(filename: &str) -> Vec> { let file = File::open(filename).unwrap(); let reader = BufReader::new(file); @@ -67,15 +66,38 @@ fn points(filename: &str) -> impl Iterator { let mut gpx: Gpx = read(reader).unwrap(); eprintln!("we have {} tracks", gpx.tracks.len()); - gpx.tracks + let mut points = Vec::new(); + + let mut iter = gpx + .tracks .pop() .unwrap() .segments .into_iter() - .flat_map(|segment| segment.linestring().points().collect::>()) - .map(|point| (point.x(), point.y())) - .dedup() - .map(|(x, y)| Point { x, y }) + .flat_map(|segment| segment.points.into_iter()) + .map(|p| { + let geop = p.point(); + ( + Point { + x: geop.x(), + y: geop.y(), + }, + p.comment.is_some(), + ) + }) + .dedup(); + let mut current_segment = iter.next().map(|(p, _)| p).into_iter().collect::>(); + for (p, is_waypoint) in iter { + if is_waypoint { + points.push(current_segment); + current_segment = Vec::new(); + } + current_segment.push(p); + } + let last_point = current_segment.pop(); + points.push(current_segment); + points.extend(last_point.map(|p| vec![p])); + points } // // NOTE: this angles idea could maybe be use to get dp from n^3 to n^2 @@ -363,6 +385,17 @@ fn save_gpc>(path: P, points: &[Point], buckets: &[Bucket]) -> st .flat_map(|p| [p.point.x, p.point.y]) .try_for_each(|c| writer.write_all(&c.to_le_bytes()))?; + let counts: HashMap<_, usize> = + unique_interest_points + .iter() + .fold(HashMap::new(), |mut h, p| { + *h.entry(p.interest).or_default() += 1; + h + }); + counts.into_iter().for_each(|(interest, count)| { + eprintln!("{:?} appears {} times", interest, count); + }); + unique_interest_points .iter() .map(|p| p.interest.into()) @@ -378,8 +411,6 @@ fn save_gpc>(path: P, points: &[Point], buckets: &[Bucket]) -> st .map(|b| b.start as u16) .try_for_each(|i| writer.write_all(&i.to_le_bytes()))?; - let starts: Vec<_> = buckets.iter().map(|b| b.start).collect(); - Ok(()) } @@ -512,18 +543,10 @@ fn save_svg<'a, P: AsRef, I: IntoIterator>( )?; } - let rpoints = rp.iter().cloned().collect::>(); - waypoints.difference(&rpoints).try_for_each(|p| { + waypoints.iter().try_for_each(|p| { writeln!( &mut writer, - "", - p.x, p.y, - ) - })?; - waypoints.intersection(&rpoints).try_for_each(|p| { - writeln!( - &mut writer, - "", + "", p.x, p.y, ) })?; @@ -611,19 +634,33 @@ fn position_interests_along_path( async fn main() { let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); eprintln!("input is {}", input_file); - let p = points(&input_file).collect::>(); - eprintln!("initialy we have {} points", p.len()); - let start = std::time::Instant::now(); - let rp = simplify_path(&p, 0.00015); - let waypoints = detect_waypoints(&rp); - eprintln!("we took {:?}", start.elapsed()); - eprintln!("we now have {} points", rp.len()); - let start = std::time::Instant::now(); - eprintln!("rdp would have had {}", rdp(&p, 0.00015).len()); - eprintln!("rdp took {:?}", start.elapsed()); + let mut segmented_points = points(&input_file); + let p = segmented_points + .iter() + .flatten() + .copied() + .collect::>(); - let mut interests = parse_osm_data("isere.osm.pbf"); - let buckets = position_interests_along_path(&mut interests, &rp, 0.0005, 5, 3); + eprintln!("initialy we have {} points", p.len()); + + eprintln!("we have {} waypoints", segmented_points.len()); + + let mut waypoints = HashSet::new(); + let mut rp = Vec::new(); + for (i1, i2) in (0..segmented_points.len()).tuple_windows() { + if let [s1, s2] = &mut segmented_points[i1..=i2] { + s1.extend(s2.first().copied()); + waypoints.insert(s1.first().copied().unwrap()); + let mut simplified = simplify_path(&s1, 0.00015); + rp.append(&mut simplified); + rp.pop(); + } + } + rp.extend(segmented_points.last().and_then(|l| l.last()).copied()); + + let mut interests = parse_osm_data("ardeche.osm.pbf"); + // let mut interests = parse_osm_data("isere.osm.pbf"); + let buckets = position_interests_along_path(&mut interests, &rp, 0.001, 5, 3); // let i = get_openstreetmap_data(&rp).await; // let i = HashSet::new(); save_svg( diff --git a/apps/gipy/gpconv/src/osm.rs b/apps/gipy/gpconv/src/osm.rs index 6f5cdc4bc..763b54be8 100644 --- a/apps/gipy/gpconv/src/osm.rs +++ b/apps/gipy/gpconv/src/osm.rs @@ -15,14 +15,14 @@ pub enum Interest { Bakery, DrinkingWater, Toilets, - BikeShop, - ChargingStation, - Bank, - Supermarket, - Table, - // TourismOffice, + // BikeShop, + // ChargingStation, + // Bank, + // Supermarket, + // Table, + // TourismOffice, Artwork, - Pharmacy, + // Pharmacy, } impl Into for Interest { @@ -31,13 +31,13 @@ impl Into for Interest { Interest::Bakery => 0, Interest::DrinkingWater => 1, Interest::Toilets => 2, - Interest::BikeShop => 3, - Interest::ChargingStation => 4, - Interest::Bank => 5, - Interest::Supermarket => 6, - Interest::Table => 7, + // Interest::BikeShop => 3, + // Interest::ChargingStation => 4, + // Interest::Bank => 5, + // Interest::Supermarket => 6, + // Interest::Table => 7, Interest::Artwork => 8, - Interest::Pharmacy => 9, + // Interest::Pharmacy => 9, } } } @@ -54,14 +54,14 @@ lazy_static! { (("shop", "bakery"), Interest::Bakery), (("amenity", "drinking_water"), Interest::DrinkingWater), (("amenity", "toilets"), Interest::Toilets), - (("shop", "bicycle"), Interest::BikeShop), - (("amenity", "charging_station"), Interest::ChargingStation), - (("amenity", "bank"), Interest::Bank), - (("shop", "supermarket"), Interest::Supermarket), - (("leisure", "picnic_table"), Interest::Table), + // (("shop", "bicycle"), Interest::BikeShop), + // (("amenity", "charging_station"), Interest::ChargingStation), + // (("amenity", "bank"), Interest::Bank), + // (("shop", "supermarket"), Interest::Supermarket), + // (("leisure", "picnic_table"), Interest::Table), // (("tourism", "information"), Interest::TourismOffice), (("tourism", "artwork"), Interest::Artwork), - (("amenity", "pharmacy"), Interest::Pharmacy), + // (("amenity", "pharmacy"), Interest::Pharmacy), ] .into_iter() .collect() @@ -80,13 +80,13 @@ impl InterestPoint { Interest::Bakery => "red", Interest::DrinkingWater => "blue", Interest::Toilets => "brown", - Interest::BikeShop => "purple", - Interest::ChargingStation => "green", - Interest::Bank => "black", - Interest::Supermarket => "red", - Interest::Table => "pink", + // Interest::BikeShop => "purple", + // Interest::ChargingStation => "green", + // Interest::Bank => "black", + // Interest::Supermarket => "red", + // Interest::Table => "pink", Interest::Artwork => "orange", - Interest::Pharmacy => "chartreuse", + // Interest::Pharmacy => "chartreuse", } } } From d2b38405185170e9b7de4eb923dd048344f8bd8c Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 22 Jul 2022 07:02:45 +0200 Subject: [PATCH 031/106] waypoints in file --- apps/gipy/ChangeLog | 2 ++ apps/gipy/TODO | 3 +-- apps/gipy/app.js | 47 +++++++++++++++--------------------- apps/gipy/gpconv/src/main.rs | 23 +++++++++++++++--- 4 files changed, 43 insertions(+), 32 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 25a2e7fba..43f112202 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -27,3 +27,5 @@ 0.10: * Display performances enhancement. + * Waypoints information is embedded in file and extracted from comments on + points. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index b63a6b066..f0dac6b2f 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,12 +1,11 @@ - direction is still shitty on gps -- split on points with comments - and encode them as waypoints - code is becoming messy - turn off gps when moving to next waypoint - display distance to next water/toilet +- display distance to next waypoint - meters seem to be a bit too long diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 339a59116..5b4d5c06b 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,6 +1,6 @@ -let simulated = false; -let file_version = 2; +let simulated = true; +let file_version = 3; let code_key = 47490; let interests_colors = [ @@ -177,18 +177,19 @@ class Status { g.setColor(1.0, 0.0, 0.0); } g.drawLine(previous_x, previous_y, x, y); - } - if (this.path.is_waypoint(i-1)) { + if (this.path.is_waypoint(i-1)) { + g.setColor(g.theme.fg); + g.fillCircle(previous_x, previous_y, 6); + g.setColor(g.theme.bg); + g.fillCircle(previous_x, previous_y, 5); + } g.setColor(g.theme.fg); - g.fillCircle(previous_x, previous_y, 6); + g.fillCircle(previous_x, previous_y, 4); g.setColor(g.theme.bg); - g.fillCircle(previous_x, previous_y, 5); + g.fillCircle(previous_x, previous_y, 3); + } - g.setColor(g.theme.fg); - g.fillCircle(previous_x, previous_y, 4); - g.setColor(g.theme.bg); - g.fillCircle(previous_x, previous_y, 3); previous_x = x; previous_y = y; @@ -224,14 +225,19 @@ class Path { let points_number = header[2]; if ((key != code_key)||(version>file_version)) { E.showMessage("Invalid gpc file"); - return; + load(); } // path points this.points = Float64Array(buffer, offset, points_number*2); + offset += 8 * points_number * 2; + + // path waypoints + let waypoints_len = Math.ceil(points_number / 8.0); + this.waypoints = Uint8Array(buffer, offset, waypoints_len); + offset += waypoints_len; // interest points - offset += 8 * points_number * 2; let interests_number = header[3]; this.interests_coordinates = Float64Array(buffer, offset, interests_number * 2); offset += 8 * interests_number * 2; @@ -247,22 +253,8 @@ class Path { offset += 2 * starts_length; } - // if start, end or steep direction change - // we are buzzing and displayed specially is_waypoint(point_index) { - if ((point_index == 0)||(point_index == this.len -1)) { - return true; - } else { - let p1 = this.point(point_index-1); - let p2 = this.point(point_index); - let p3 = this.point(point_index+1); - let d1 = p2.minus(p1); - let d2 = p3.minus(p2); - let a1 = Math.atan2(d1.lat, d1.lon); - let a2 = Math.atan2(d2.lat, d2.lon); - let direction_change = Math.abs(a2-a1); - return ((direction_change > Math.PI / 3.0)&&(direction_change < Math.PI * 5.0/3.0)); - } + return (this.waypoints[Math.floor(point_index / 8)] & (point_index % 8)); } // execute op on all segments. @@ -513,3 +505,4 @@ if (files.length <= 1) { } else { drawMenu(); } + diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index fd2f4bded..df8a5789d 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -11,7 +11,7 @@ mod osm; use osm::{parse_osm_data, InterestPoint}; const KEY: u16 = 47490; -const FILE_VERSION: u16 = 2; +const FILE_VERSION: u16 = 3; #[derive(Debug, PartialEq, Clone, Copy)] pub struct Point { @@ -349,7 +349,12 @@ fn compress_coordinates(points: &[(i32, i32)]) -> Vec<(i16, i16)> { xdiffs.zip(ydiffs).collect() } -fn save_gpc>(path: P, points: &[Point], buckets: &[Bucket]) -> std::io::Result<()> { +fn save_gpc>( + path: P, + points: &[Point], + waypoints: &HashSet, + buckets: &[Bucket], +) -> std::io::Result<()> { let mut writer = BufWriter::new(File::create(path)?); eprintln!("saving {} points", points.len()); @@ -380,6 +385,18 @@ fn save_gpc>(path: P, points: &[Point], buckets: &[Bucket]) -> st .flat_map(|p| [p.x, p.y]) .try_for_each(|c| writer.write_all(&c.to_le_bytes()))?; + let mut waypoints_bits = std::iter::repeat(0u8) + .take(points.len() / 8 + if points.len() % 8 != 0 { 1 } else { 0 }) + .collect::>(); + points.iter().enumerate().for_each(|(i, p)| { + if waypoints.contains(p) { + waypoints_bits[i / 8] |= 1 << (i % 8) + } + }); + waypoints_bits + .iter() + .try_for_each(|byte| writer.write_all(&byte.to_le_bytes()))?; + unique_interest_points .iter() .flat_map(|p| [p.point.x, p.point.y]) @@ -672,5 +689,5 @@ async fn main() { ) .unwrap(); - save_gpc("test.gpc", &rp, &buckets).unwrap(); + save_gpc("test.gpc", &rp, &waypoints, &buckets).unwrap(); } From f4ca3afe3407e3e125b174ef3f3116774340446f Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 22 Jul 2022 07:31:23 +0200 Subject: [PATCH 032/106] minor stuff --- apps/gipy/app.js | 925 +++++++++++++++++++---------------- apps/gipy/gpconv/src/main.rs | 7 +- 2 files changed, 498 insertions(+), 434 deletions(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 5b4d5c06b..a1b2a6453 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,508 +1,567 @@ - let simulated = true; let file_version = 3; let code_key = 47490; let interests_colors = [ - 0x780F, // Bakery, purple - 0x001F, // DrinkingWater, blue - 0x07FF, // Toilets, cyan - 0x7BEF, // BikeShop, dark grey - 0xAFE5, // ChargingStation, green yellow - 0x7800, // Bank, maroon - 0xF800, // Supermarket, red - 0xF81F, // Table, pink - 0xFD20, // Artwork, orange - 0x07E0, // Pharmacy, green + 0x780f, // Bakery, purple + 0x001f, // DrinkingWater, blue + 0x07ff, // Toilets, cyan + 0xfd20, // Artwork, orange ]; function binary_search(array, x) { - let start = 0, end = array.length - 1; + let start = 0, + end = array.length - 1; - while (start <= end){ - let mid = Math.floor((start + end) / 2); - if (array[mid] < x) - start = mid + 1; - else - end = mid - 1; - } - return start; + while (start <= end) { + let mid = Math.floor((start + end) / 2); + if (array[mid] < x) start = mid + 1; + else end = mid - 1; + } + return start; } class Status { - constructor(path) { - this.path = path; - this.on_path = false; // are we on the path or lost ? - this.position = null; // where we are - this.cos_direction = null; // cos of where we look at - this.sin_direction = null; // sin of where we look at - this.current_segment = null; // which segment is closest - this.reaching = null; // which waypoint are we reaching ? - this.distance_to_next_point = null; // how far are we from next point ? + constructor(path) { + this.path = path; + this.on_path = false; // are we on the path or lost ? + this.position = null; // where we are + this.cos_direction = null; // cos of where we look at + this.sin_direction = null; // sin of where we look at + this.current_segment = null; // which segment is closest + this.reaching = null; // which waypoint are we reaching ? + this.distance_to_next_point = null; // how far are we from next point ? - let r = [0]; - // let's do a reversed prefix computations on all distances: - // loop on all segments in reversed order - let previous_point = null; - for (let i = this.path.len - 1; i >= 0; i--) { - let point = this.path.point(i); - if (previous_point !== null) { - r.unshift(r[0] + point.distance(previous_point)); - } - previous_point = point; - } - this.remaining_distances = r; // how much distance remains at start of each segment + let r = [0]; + // let's do a reversed prefix computations on all distances: + // loop on all segments in reversed order + let previous_point = null; + for (let i = this.path.len - 1; i >= 0; i--) { + let point = this.path.point(i); + if (previous_point !== null) { + r.unshift(r[0] + point.distance(previous_point)); + } + previous_point = point; } - update_position(new_position, direction) { - - if (Bangle.isLocked() && this.position !== null && new_position.lon == this.position.lon && new_position.lat == this.position.lat) { - return; - } - - this.cos_direction = Math.cos(-direction - Math.PI / 2.0); - this.sin_direction = Math.sin(-direction - Math.PI / 2.0); - this.position = new_position; - - // detect segment we are on now - let next_segment = this.path.nearest_segment(this.position, Math.max(0, this.current_segment-1), Math.min(this.current_segment+2, this.path.len - 1), this.cos_direction, this.sin_direction); - - if (this.is_lost(next_segment)) { - // it did not work, try anywhere - next_segment = this.path.nearest_segment(this.position, 0, this.path.len - 1, this.cos_direction, this.sin_direction); - } - // now check if we strayed away from path or back to it - let lost = this.is_lost(next_segment); - if (this.on_path == lost) { // if status changes - if (lost) { - Bangle.buzz(); // we lost path - setTimeout(()=>Bangle.buzz(), 500); - } - this.on_path = !lost; - } - - this.current_segment = next_segment; - - // check if we are nearing the next point on our path and alert the user - let next_point = this.current_segment + 1; - this.distance_to_next_point = Math.ceil(this.position.distance(this.path.point(next_point))); - if (this.reaching != next_point && this.distance_to_next_point <= 20) { - this.reaching = next_point; - let reaching_waypoint = this.path.is_waypoint(next_point); - if (reaching_waypoint) { - Bangle.buzz(); - if (Bangle.isLocked()) { - Bangle.setLocked(false); - } - } - } - // re-display unless locked - if (!Bangle.isLocked() || simulated) { - this.display(); - } + this.remaining_distances = r; // how much distance remains at start of each segment + } + update_position(new_position, direction) { + if ( + Bangle.isLocked() && + this.position !== null && + new_position.lon == this.position.lon && + new_position.lat == this.position.lat + ) { + return; } - remaining_distance() { - return this.remaining_distances[this.current_segment+1] + this.position.distance(this.path.point(this.current_segment+1)); + + this.cos_direction = Math.cos(-direction - Math.PI / 2.0); + this.sin_direction = Math.sin(-direction - Math.PI / 2.0); + this.position = new_position; + + // detect segment we are on now + let next_segment = this.path.nearest_segment( + this.position, + Math.max(0, this.current_segment - 1), + Math.min(this.current_segment + 2, this.path.len - 1), + this.cos_direction, + this.sin_direction + ); + + if (this.is_lost(next_segment)) { + // it did not work, try anywhere + next_segment = this.path.nearest_segment( + this.position, + 0, + this.path.len - 1, + this.cos_direction, + this.sin_direction + ); } - is_lost(segment) { - let distance_to_nearest = this.position.fake_distance_to_segment(this.path.point(segment), this.path.point(segment+1)); - let meters = 6371e3 * distance_to_nearest; - return (meters > 20); + // now check if we strayed away from path or back to it + let lost = this.is_lost(next_segment); + if (this.on_path == lost) { + // if status changes + if (lost) { + Bangle.buzz(); // we lost path + setTimeout(() => Bangle.buzz(), 500); + } + this.on_path = !lost; } - display() { - g.clear(); - this.display_map(); - this.display_interest_points(); - this.display_stats(); - Bangle.drawWidgets(); - } - display_interest_points() { - // this is the algorithm in case we have a lot of interest points - // let's draw all points for 5 segments centered on current one - let starting_group = Math.floor(Math.max(this.current_segment-2, 0) / 3); - let ending_group = Math.floor(Math.min(this.current_segment+2, this.path.len-2) / 3); - let starting_bucket = binary_search(this.path.interests_starts, starting_group); - let ending_bucket = binary_search(this.path.interests_starts, ending_group+0.5); - // we have 5 points per bucket - let end_index = Math.min(this.path.interests_types.length - 1, ending_bucket*5); - for (let i = starting_bucket*5 ; i <= end_index ; i++) { - let index = this.path.interests_on_path[i]; - let interest_point = this.path.interest_point(index); - let color = this.path.interest_color(i); - let c = interest_point.coordinates(this.position, this.cos_direction, this.sin_direction); - g.setColor(color).fillCircle(c[0], c[1], 5); + + this.current_segment = next_segment; + + // check if we are nearing the next point on our path and alert the user + let next_point = this.current_segment + 1; + this.distance_to_next_point = Math.ceil( + this.position.distance(this.path.point(next_point)) + ); + if (this.reaching != next_point && this.distance_to_next_point <= 20) { + this.reaching = next_point; + let reaching_waypoint = this.path.is_waypoint(next_point); + if (reaching_waypoint) { + Bangle.buzz(); + if (Bangle.isLocked()) { + Bangle.setLocked(false); } + } } - display_stats() { - let rounded_distance = Math.round(this.remaining_distance() / 100) / 10; - let total = Math.round(this.remaining_distances[0] / 100) / 10; - let now = new Date(); - let minutes = now.getMinutes().toString(); - if (minutes.length < 2) { - minutes = '0' + minutes; - } - let hours = now.getHours().toString(); - g.setFont("6x8:2").setColor(g.theme.fg).drawString(hours + ":" + minutes, 0, g.getHeight() - 49); - g.drawString("d. " + rounded_distance + "/" + total, 0, g.getHeight() - 32); - g.drawString("seg." + (this.current_segment + 1) + "/" + this.path.len + " " + this.distance_to_next_point + "m", 0, g.getHeight() - 15); + // re-display unless locked + if (!Bangle.isLocked() || simulated) { + this.display(); } - display_map() { - // don't display all segments, only those neighbouring current segment - // this is most likely to be the correct display - // while lowering the cost a lot - // - // note that all code is inlined here to speed things up from 400ms to 200ms - let start = Math.max(this.current_segment - 3, 0); - let end = Math.min(this.current_segment + 5, this.path.len - 1); - let pos = this.position; - let cos = this.cos_direction; - let sin = this.sin_direction; - let points = this.path.points; - let cx = pos.lon; - let cy = pos.lat; - let half_width = g.getWidth() / 2; - let half_height = g.getHeight() / 2; - let previous_x = null; - let previous_y = null; - for (let i = start ; i < end ; i++) { - let tx = (points[2*i] - cx) * 40000.0; - let ty = (points[2*i+1] - cy) * 40000.0; - let rotated_x = tx * cos - ty * sin; - let rotated_y = tx * sin + ty * cos; - let x = half_width - Math.round(rotated_x); // x is inverted - let y = half_height + Math.round(rotated_y); - if (previous_x !== null) { - if (i == this.current_segment + 1) { - g.setColor(0.0, 1.0, 0.0); - } else { - g.setColor(1.0, 0.0, 0.0); - } - g.drawLine(previous_x, previous_y, x, y); - - if (this.path.is_waypoint(i-1)) { - g.setColor(g.theme.fg); - g.fillCircle(previous_x, previous_y, 6); - g.setColor(g.theme.bg); - g.fillCircle(previous_x, previous_y, 5); - } - g.setColor(g.theme.fg); - g.fillCircle(previous_x, previous_y, 4); - g.setColor(g.theme.bg); - g.fillCircle(previous_x, previous_y, 3); - - } - - previous_x = x; - previous_y = y; + } + remaining_distance() { + return ( + this.remaining_distances[this.current_segment + 1] + + this.position.distance(this.path.point(this.current_segment + 1)) + ); + } + is_lost(segment) { + let distance_to_nearest = this.position.fake_distance_to_segment( + this.path.point(segment), + this.path.point(segment + 1) + ); + let meters = 6371e3 * distance_to_nearest; + return meters > 20; + } + display() { + g.clear(); + this.display_map(); + this.display_interest_points(); + this.display_stats(); + Bangle.drawWidgets(); + } + display_interest_points() { + // this is the algorithm in case we have a lot of interest points + // let's draw all points for 5 segments centered on current one + let starting_group = Math.floor(Math.max(this.current_segment - 2, 0) / 3); + let ending_group = Math.floor( + Math.min(this.current_segment + 2, this.path.len - 2) / 3 + ); + let starting_bucket = binary_search( + this.path.interests_starts, + starting_group + ); + let ending_bucket = binary_search( + this.path.interests_starts, + ending_group + 0.5 + ); + // we have 5 points per bucket + let end_index = Math.min( + this.path.interests_types.length - 1, + ending_bucket * 5 + ); + for (let i = starting_bucket * 5; i <= end_index; i++) { + let index = this.path.interests_on_path[i]; + let interest_point = this.path.interest_point(index); + let color = this.path.interest_color(i); + let c = interest_point.coordinates( + this.position, + this.cos_direction, + this.sin_direction + ); + g.setColor(color).fillCircle(c[0], c[1], 5); + } + } + display_stats() { + let rounded_distance = Math.round(this.remaining_distance() / 100) / 10; + let total = Math.round(this.remaining_distances[0] / 100) / 10; + let now = new Date(); + let minutes = now.getMinutes().toString(); + if (minutes.length < 2) { + minutes = "0" + minutes; + } + let hours = now.getHours().toString(); + g.setFont("6x8:2") + .setColor(g.theme.fg) + .drawString(hours + ":" + minutes, 0, g.getHeight() - 49); + g.drawString("d. " + rounded_distance + "/" + total, 0, g.getHeight() - 32); + g.drawString( + "seg." + + (this.current_segment + 1) + + "/" + + this.path.len + + " " + + this.distance_to_next_point + + "m", + 0, + g.getHeight() - 15 + ); + } + display_map() { + // don't display all segments, only those neighbouring current segment + // this is most likely to be the correct display + // while lowering the cost a lot + // + // note that all code is inlined here to speed things up from 400ms to 200ms + let start = Math.max(this.current_segment - 3, 0); + let end = Math.min(this.current_segment + 5, this.path.len - 1); + let pos = this.position; + let cos = this.cos_direction; + let sin = this.sin_direction; + let points = this.path.points; + let cx = pos.lon; + let cy = pos.lat; + let half_width = g.getWidth() / 2; + let half_height = g.getHeight() / 2; + let previous_x = null; + let previous_y = null; + for (let i = start; i < end; i++) { + let tx = (points[2 * i] - cx) * 40000.0; + let ty = (points[2 * i + 1] - cy) * 40000.0; + let rotated_x = tx * cos - ty * sin; + let rotated_y = tx * sin + ty * cos; + let x = half_width - Math.round(rotated_x); // x is inverted + let y = half_height + Math.round(rotated_y); + if (previous_x !== null) { + if (i == this.current_segment + 1) { + g.setColor(0.0, 1.0, 0.0); + } else { + g.setColor(1.0, 0.0, 0.0); } + g.drawLine(previous_x, previous_y, x, y); - if (this.path.is_waypoint(end-1)) { - g.setColor(g.theme.fg); - g.fillCircle(previous_x, previous_y, 6); - g.setColor(g.theme.bg); - g.fillCircle(previous_x, previous_y, 5); + if (this.path.is_waypoint(i - 1)) { + g.setColor(g.theme.fg); + g.fillCircle(previous_x, previous_y, 6); + g.setColor(g.theme.bg); + g.fillCircle(previous_x, previous_y, 5); } g.setColor(g.theme.fg); g.fillCircle(previous_x, previous_y, 4); g.setColor(g.theme.bg); g.fillCircle(previous_x, previous_y, 3); + } - // now display ourselves - g.setColor(g.theme.fgH); - g.fillCircle(half_width, half_height, 5); + previous_x = x; + previous_y = y; } + + if (this.path.is_waypoint(end - 1)) { + g.setColor(g.theme.fg); + g.fillCircle(previous_x, previous_y, 6); + g.setColor(g.theme.bg); + g.fillCircle(previous_x, previous_y, 5); + } + g.setColor(g.theme.fg); + g.fillCircle(previous_x, previous_y, 4); + g.setColor(g.theme.bg); + g.fillCircle(previous_x, previous_y, 3); + + // now display ourselves + g.setColor(g.theme.fgH); + g.fillCircle(half_width, half_height, 5); + } } class Path { - constructor(filename) { - let buffer = require("Storage").readArrayBuffer(filename); - let offset = 0; + constructor(filename) { + let buffer = require("Storage").readArrayBuffer(filename); + let offset = 0; - // header - let header = Uint16Array(buffer, offset, 5); - offset += 5 * 2; - let key = header[0]; - let version = header[1]; - let points_number = header[2]; - if ((key != code_key)||(version>file_version)) { - E.showMessage("Invalid gpc file"); - load(); + // header + let header = Uint16Array(buffer, offset, 5); + offset += 5 * 2; + let key = header[0]; + let version = header[1]; + let points_number = header[2]; + if (key != code_key || version > file_version) { + E.showMessage("Invalid gpc file"); + load(); + } + + // path points + this.points = Float64Array(buffer, offset, points_number * 2); + offset += 8 * points_number * 2; + + // path waypoints + let waypoints_len = Math.ceil(points_number / 8.0); + this.waypoints = Uint8Array(buffer, offset, waypoints_len); + offset += waypoints_len; + + // interest points + let interests_number = header[3]; + this.interests_coordinates = Float64Array( + buffer, + offset, + interests_number * 2 + ); + offset += 8 * interests_number * 2; + this.interests_types = Uint8Array(buffer, offset, interests_number); + offset += interests_number; + + // interests on path + let interests_on_path_number = header[4]; + this.interests_on_path = Uint16Array( + buffer, + offset, + interests_on_path_number + ); + offset += 2 * interests_on_path_number; + let starts_length = Math.ceil(interests_on_path_number / 5.0); + this.interests_starts = Uint16Array(buffer, offset, starts_length); + offset += 2 * starts_length; + } + + is_waypoint(point_index) { + return this.waypoints[Math.floor(point_index / 8)] & point_index % 8; + } + + // execute op on all segments. + // start is index of first wanted segment + // end is 1 after index of last wanted segment + on_segments(op, start, end) { + let previous_point = null; + for (let i = start; i < end + 1; i++) { + let point = new Point(this.points[2 * i], this.points[2 * i + 1]); + if (previous_point !== null) { + op(previous_point, point, i); + } + previous_point = point; + } + } + + // return point at given index + point(index) { + let lon = this.points[2 * index]; + let lat = this.points[2 * index + 1]; + return new Point(lon, lat); + } + + interest_point(index) { + let lon = this.interests_coordinates[2 * index]; + let lat = this.interests_coordinates[2 * index + 1]; + return new Point(lon, lat); + } + + interest_color(index) { + return interests_colors[this.interests_types[index]]; + } + + // return index of segment which is nearest from point. + // we need a direction because we need there is an ambiguity + // for overlapping segments which are taken once to go and once to come back. + // (in the other direction). + nearest_segment(point, start, end, cos_direction, sin_direction) { + // we are going to compute two min distances, one for each direction. + let indices = [0, 0]; + let mins = [Number.MAX_VALUE, Number.MAX_VALUE]; + this.on_segments( + function (p1, p2, i) { + // we use the dot product to figure out if oriented correctly + let distance = point.fake_distance_to_segment(p1, p2); + let diff = p2.minus(p1); + let dot = cos_direction * diff.lon + sin_direction * diff.lat; + let orientation = +(dot < 0); // index 0 is good orientation + if (distance <= mins[orientation]) { + mins[orientation] = distance; + indices[orientation] = i - 1; } - - // path points - this.points = Float64Array(buffer, offset, points_number*2); - offset += 8 * points_number * 2; - - // path waypoints - let waypoints_len = Math.ceil(points_number / 8.0); - this.waypoints = Uint8Array(buffer, offset, waypoints_len); - offset += waypoints_len; - - // interest points - let interests_number = header[3]; - this.interests_coordinates = Float64Array(buffer, offset, interests_number * 2); - offset += 8 * interests_number * 2; - this.interests_types = Uint8Array(buffer, offset, interests_number); - offset += interests_number; - - // interests on path - let interests_on_path_number = header[4]; - this.interests_on_path = Uint16Array(buffer, offset, interests_on_path_number); - offset += 2 * interests_on_path_number; - let starts_length = Math.ceil(interests_on_path_number / 5.0); - this.interests_starts = Uint16Array(buffer, offset, starts_length); - offset += 2 * starts_length; - } - - is_waypoint(point_index) { - return (this.waypoints[Math.floor(point_index / 8)] & (point_index % 8)); - } - - // execute op on all segments. - // start is index of first wanted segment - // end is 1 after index of last wanted segment - on_segments(op, start, end) { - let previous_point = null; - for (let i = start; i < end + 1; i++) { - let point = new Point(this.points[2 * i], this.points[2 * i + 1]); - if (previous_point !== null) { - op(previous_point, point, i); - } - previous_point = point; - } - } - - // return point at given index - point(index) { - let lon = this.points[2 * index]; - let lat = this.points[2 * index + 1]; - return new Point(lon, lat); - } - - interest_point(index) { - let lon = this.interests_coordinates[2 * index]; - let lat = this.interests_coordinates[2 * index + 1]; - return new Point(lon, lat); - } - - interest_color(index) { - return interests_colors[this.interests_types[index]]; - } - - // return index of segment which is nearest from point. - // we need a direction because we need there is an ambiguity - // for overlapping segments which are taken once to go and once to come back. - // (in the other direction). - nearest_segment(point, start, end, cos_direction, sin_direction) { - // we are going to compute two min distances, one for each direction. - let indices = [0, 0]; - let mins = [Number.MAX_VALUE, Number.MAX_VALUE]; - this.on_segments(function(p1, p2, i) { - // we use the dot product to figure out if oriented correctly - let distance = point.fake_distance_to_segment(p1, p2); - let diff = p2.minus(p1); - let dot = cos_direction * diff.lon + sin_direction * diff.lat; - let orientation = + (dot < 0); // index 0 is good orientation - if (distance <= mins[orientation]) { - mins[orientation] = distance; - indices[orientation] = i - 1; - } - }, start, end); - // by default correct orientation (0) wins - // but if other one is really closer, return other one - if (mins[1] < mins[0] / 10.0) { - return indices[1]; - } else { - return indices[0]; - } - } - get len() { - return this.points.length / 2; + }, + start, + end + ); + // by default correct orientation (0) wins + // but if other one is really closer, return other one + if (mins[1] < mins[0] / 10.0) { + return indices[1]; + } else { + return indices[0]; } + } + get len() { + return this.points.length / 2; + } } class Point { - constructor(lon, lat) { - this.lon = lon; - this.lat = lat; - } - coordinates(current_position, cos_direction, sin_direction) { - let translated = this.minus(current_position).times(40000.0); - let rotated_x = translated.lon * cos_direction - translated.lat * sin_direction; - let rotated_y = translated.lon * sin_direction + translated.lat * cos_direction; - return [ - g.getWidth() / 2 - Math.round(rotated_x), // x is inverted - g.getHeight() / 2 + Math.round(rotated_y) - ]; - } - minus(other_point) { - let xdiff = this.lon - other_point.lon; - let ydiff = this.lat - other_point.lat; - return new Point(xdiff, ydiff); - } - plus(other_point) { - return new Point(this.lon + other_point.lon, this.lat + other_point.lat); - } - length_squared(other_point) { - let d = this.minus(other_point); - return (d.lon * d.lon + d.lat * d.lat); - } - times(scalar) { - return new Point(this.lon * scalar, this.lat * scalar); - } - dot(other_point) { - return this.lon * other_point.lon + this.lat * other_point.lat; - } - distance(other_point) { - //see https://www.movable-type.co.uk/scripts/latlong.html - const R = 6371e3; // metres - const phi1 = this.lat * Math.PI / 180; - const phi2 = other_point.lat * Math.PI / 180; - const deltaphi = (other_point.lat - this.lat) * Math.PI / 180; - const deltalambda = (other_point.lon - this.lon) * Math.PI / 180; + constructor(lon, lat) { + this.lon = lon; + this.lat = lat; + } + coordinates(current_position, cos_direction, sin_direction) { + let translated = this.minus(current_position).times(40000.0); + let rotated_x = + translated.lon * cos_direction - translated.lat * sin_direction; + let rotated_y = + translated.lon * sin_direction + translated.lat * cos_direction; + return [ + g.getWidth() / 2 - Math.round(rotated_x), // x is inverted + g.getHeight() / 2 + Math.round(rotated_y), + ]; + } + minus(other_point) { + let xdiff = this.lon - other_point.lon; + let ydiff = this.lat - other_point.lat; + return new Point(xdiff, ydiff); + } + plus(other_point) { + return new Point(this.lon + other_point.lon, this.lat + other_point.lat); + } + length_squared(other_point) { + let d = this.minus(other_point); + return d.lon * d.lon + d.lat * d.lat; + } + times(scalar) { + return new Point(this.lon * scalar, this.lat * scalar); + } + dot(other_point) { + return this.lon * other_point.lon + this.lat * other_point.lat; + } + distance(other_point) { + //see https://www.movable-type.co.uk/scripts/latlong.html + const R = 6371e3; // metres + const phi1 = (this.lat * Math.PI) / 180; + const phi2 = (other_point.lat * Math.PI) / 180; + const deltaphi = ((other_point.lat - this.lat) * Math.PI) / 180; + const deltalambda = ((other_point.lon - this.lon) * Math.PI) / 180; - const a = Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) + - Math.cos(phi1) * Math.cos(phi2) * - Math.sin(deltalambda / 2) * Math.sin(deltalambda / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const a = + Math.sin(deltaphi / 2) * Math.sin(deltaphi / 2) + + Math.cos(phi1) * + Math.cos(phi2) * + Math.sin(deltalambda / 2) * + Math.sin(deltalambda / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c; // in metres + return R * c; // in metres + } + fake_distance(other_point) { + return Math.sqrt(this.length_squared(other_point)); + } + fake_distance_to_segment(v, w) { + // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment + // Return minimum distance between line segment vw and point p + let l2 = v.length_squared(w); // i.e. |w-v|^2 - avoid a sqrt + if (l2 == 0.0) { + return this.distance(v); // v == w case } - fake_distance(other_point) { - return Math.sqrt(this.length_squared(other_point)); - } - fake_distance_to_segment(v, w) { - // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment - // Return minimum distance between line segment vw and point p - let l2 = v.length_squared(w); // i.e. |w-v|^2 - avoid a sqrt - if (l2 == 0.0) { - return this.distance(v); // v == w case - } - // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. - // It falls where t = [(p-v) . (w-v)] / |w-v|^2 - // We clamp t from [0,1] to handle points outside the segment vw. - let t = Math.max(0, Math.min(1, (this.minus(v)).dot(w.minus(v)) / l2)); + // Consider the line extending the segment, parameterized as v + t (w - v). + // We find projection of point p onto the line. + // It falls where t = [(p-v) . (w-v)] / |w-v|^2 + // We clamp t from [0,1] to handle points outside the segment vw. + let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2)); - let projection = v.plus((w.minus(v)).times(t)); // Projection falls on the segment - return this.fake_distance(projection); - } + let projection = v.plus(w.minus(v).times(t)); // Projection falls on the segment + return this.fake_distance(projection); + } } - Bangle.loadWidgets(); - let fake_gps_point = 0.0; function simulate_gps(status) { - if (fake_gps_point > status.path.len -1) { - return; - } - let point_index = Math.floor(fake_gps_point); - if (point_index >= status.path.len) { - return; - } - let p1 = status.path.point(point_index); - let p2 = status.path.point(point_index + 1); + if (fake_gps_point > status.path.len - 1) { + return; + } + let point_index = Math.floor(fake_gps_point); + if (point_index >= status.path.len) { + return; + } + let p1 = status.path.point(point_index); + let p2 = status.path.point(point_index + 1); - let alpha = fake_gps_point - point_index; - let pos = p1.times(1-alpha).plus(p2.times(alpha)); - let old_pos = status.position; + let alpha = fake_gps_point - point_index; + let pos = p1.times(1 - alpha).plus(p2.times(alpha)); + let old_pos = status.position; - fake_gps_point += 0.2; // advance simulation - let direction = Math.atan2(pos.lat - old_pos.lat, pos.lon - old_pos.lon); - status.update_position(pos, direction); + fake_gps_point += 0.2; // advance simulation + let direction = Math.atan2(pos.lat - old_pos.lat, pos.lon - old_pos.lon); + status.update_position(pos, direction); } function drawMenu() { const menu = { - '': { 'title': 'choose trace' } + "": { title: "choose trace" }, }; var files = require("Storage").list(".gpc"); - for (var i=0; i Date: Fri, 22 Jul 2022 07:54:54 +0200 Subject: [PATCH 033/106] display lost/turn on screen to debug --- apps/gipy/app.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index a1b2a6453..8a05da4bd 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,4 +1,4 @@ -let simulated = true; +let simulated = false; let file_version = 3; let code_key = 47490; @@ -188,6 +188,17 @@ class Status { 0, g.getHeight() - 15 ); + + if (this.distance_to_next_point <= 20) { + g.setColor(0.0, 1.0, 0.0) + .setFont("6x8:2") + .drawString("turn", g.getWidth() - 40, 35); + } + if (!this.on_path) { + g.setColor(1.0, 0.0, 0.0) + .setFont("6x8:2") + .drawString("lost", g.getWidth() - 40, 60); + } } display_map() { // don't display all segments, only those neighbouring current segment From d3fcc943e6c7bcc10c1b7d236891e9965edb5048 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 22 Jul 2022 09:49:33 +0200 Subject: [PATCH 034/106] small bug fixes --- apps/gipy/ChangeLog | 1 + apps/gipy/TODO | 20 ++++++++++++-------- apps/gipy/app.js | 31 ++++++++++++++++++------------- apps/gipy/gpconv/src/osm.rs | 4 ++-- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 43f112202..19d8f7b55 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -29,3 +29,4 @@ * Display performances enhancement. * Waypoints information is embedded in file and extracted from comments on points. + * Bugfix in map display (last segment was missing + wrong colors). diff --git a/apps/gipy/TODO b/apps/gipy/TODO index f0dac6b2f..923390335 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,20 +1,24 @@ +* bugs + +- waypoints seem wrong - direction is still shitty on gps +- we are always lost +- meters seem to be a bit too long -- code is becoming messy +* additional features - turn off gps when moving to next waypoint - display distance to next water/toilet - display distance to next waypoint - -- meters seem to be a bit too long - -- buzzing does not work nicely - -> is_lost seems fishy - -> we need a display on top of buzz - - display average speed - dynamic map rescale - display scale (100m) - compress path ? + +* misc + +- code is becoming messy + + diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 8a05da4bd..6ed2546e9 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -3,10 +3,10 @@ let file_version = 3; let code_key = 47490; let interests_colors = [ - 0x780f, // Bakery, purple + 0xf800, // Bakery, red 0x001f, // DrinkingWater, blue 0x07ff, // Toilets, cyan - 0xfd20, // Artwork, orange + 0x07e0, // Artwork, green ]; function binary_search(array, x) { @@ -118,12 +118,11 @@ class Status { ); } is_lost(segment) { - let distance_to_nearest = this.position.fake_distance_to_segment( + let distance_to_nearest = this.position.distance_to_segment( this.path.point(segment), this.path.point(segment + 1) ); - let meters = 6371e3 * distance_to_nearest; - return meters > 20; + return distance_to_nearest > 20; } display() { g.clear(); @@ -181,7 +180,7 @@ class Status { "seg." + (this.current_segment + 1) + "/" + - this.path.len + + (this.path.len - 1) + " " + this.distance_to_next_point + "m", @@ -192,12 +191,12 @@ class Status { if (this.distance_to_next_point <= 20) { g.setColor(0.0, 1.0, 0.0) .setFont("6x8:2") - .drawString("turn", g.getWidth() - 40, 35); + .drawString("turn", g.getWidth() - 55, 35); } if (!this.on_path) { g.setColor(1.0, 0.0, 0.0) .setFont("6x8:2") - .drawString("lost", g.getWidth() - 40, 60); + .drawString("lost", g.getWidth() - 55, 35); } } display_map() { @@ -207,7 +206,7 @@ class Status { // // note that all code is inlined here to speed things up from 400ms to 200ms let start = Math.max(this.current_segment - 3, 0); - let end = Math.min(this.current_segment + 5, this.path.len - 1); + let end = Math.min(this.current_segment + 5, this.path.len); let pos = this.position; let cos = this.cos_direction; let sin = this.sin_direction; @@ -436,12 +435,12 @@ class Point { Math.sin(deltalambda / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c; // in metres + return R * c; // in meters } fake_distance(other_point) { return Math.sqrt(this.length_squared(other_point)); } - fake_distance_to_segment(v, w) { + closest_segment_point(v, w) { // from : https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment // Return minimum distance between line segment vw and point p let l2 = v.length_squared(w); // i.e. |w-v|^2 - avoid a sqrt @@ -453,8 +452,14 @@ class Point { // It falls where t = [(p-v) . (w-v)] / |w-v|^2 // We clamp t from [0,1] to handle points outside the segment vw. let t = Math.max(0, Math.min(1, this.minus(v).dot(w.minus(v)) / l2)); - - let projection = v.plus(w.minus(v).times(t)); // Projection falls on the segment + return v.plus(w.minus(v).times(t)); // Projection falls on the segment + } + distance_to_segment(v, w) { + let projection = this.closest_segment_point(v, w); + return this.distance(projection); + } + fake_distance_to_segment(v, w) { + let projection = this.closest_segment_point(v, w); return this.fake_distance(projection); } } diff --git a/apps/gipy/gpconv/src/osm.rs b/apps/gipy/gpconv/src/osm.rs index 763b54be8..17425023a 100644 --- a/apps/gipy/gpconv/src/osm.rs +++ b/apps/gipy/gpconv/src/osm.rs @@ -31,12 +31,12 @@ impl Into for Interest { Interest::Bakery => 0, Interest::DrinkingWater => 1, Interest::Toilets => 2, - // Interest::BikeShop => 3, + // Interest::BikeShop => 8, // Interest::ChargingStation => 4, // Interest::Bank => 5, // Interest::Supermarket => 6, // Interest::Table => 7, - Interest::Artwork => 8, + Interest::Artwork => 3, // Interest::Pharmacy => 9, } } From f276e9e4f2ea91dc3752649642b5290c5d36112f Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 22 Jul 2022 13:50:53 +0200 Subject: [PATCH 035/106] trying some fixes for the various pb --- apps/gipy/ChangeLog | 2 + apps/gipy/TODO | 9 ++-- apps/gipy/app.js | 15 ++---- apps/gipy/gpconv/src/main.rs | 91 +++++++++++++----------------------- apps/gipy/gpconv/src/osm.rs | 27 +++++++++-- 5 files changed, 68 insertions(+), 76 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 19d8f7b55..1664fc513 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -30,3 +30,5 @@ * Waypoints information is embedded in file and extracted from comments on points. * Bugfix in map display (last segment was missing + wrong colors). + * Waypoint detections using OSM + sharp angles + * New algorith for direction detection diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 923390335..179b73b2c 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,19 +1,22 @@ * bugs -- waypoints seem wrong -- direction is still shitty on gps -- we are always lost - meters seem to be a bit too long +- direction is still shitty on gps ? +- we are always lost (done ?) +- waypoints seem wrong ? + * additional features +- display direction to nearest point - turn off gps when moving to next waypoint - display distance to next water/toilet - display distance to next waypoint - display average speed - dynamic map rescale - display scale (100m) +- get waypoints from osm - compress path ? diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 6ed2546e9..c77a40f24 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -541,16 +541,11 @@ function start(fn) { } } if (old_points.length == 4) { - // let's just take average angle of 3 previous segments - let angles_sum = 0; - for (let i = 0; i < 3; i++) { - let p1 = old_points[i]; - let p2 = old_points[i + 1]; - let diff = p2.minus(p1); - let angle = Math.atan2(diff.lat, diff.lon); - angles_sum += angle; - } - status.update_position(position, angles_sum / 3.0); + // let's just take angle of segment between oldest and newest point + let oldest = old_points[0]; + let diff = position.minus(oldest); + let angle = Math.atan2(diff.lat, diff.lon); + status.update_position(position, angle); } else { status.update_position(position, direction); } diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index 57944e1bc..6794b5c2e 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -13,17 +13,26 @@ use osm::{parse_osm_data, InterestPoint}; const KEY: u16 = 47490; const FILE_VERSION: u16 = 3; -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, Clone, Copy)] pub struct Point { x: f64, y: f64, } +impl PartialEq for Point { + fn eq(&self, other: &Self) -> bool { + (self.x - other.x).abs() < 0.0005 && (self.y - other.y).abs() < 0.0005 + } +} impl Eq for Point {} impl std::hash::Hash for Point { fn hash(&self, state: &mut H) { - unsafe { std::mem::transmute::(self.x) }.hash(state); - unsafe { std::mem::transmute::(self.y) }.hash(state); + let x = format!("{:.4}", self.x); + let y = format!("{:.4}", self.y); + x.hash(state); + y.hash(state); + // unsafe { std::mem::transmute::(self.x) }.hash(state); + // unsafe { std::mem::transmute::(self.y) }.hash(state); } } @@ -58,7 +67,7 @@ impl Point { } } -fn points(filename: &str) -> Vec> { +fn points(filename: &str) -> Vec { let file = File::open(filename).unwrap(); let reader = BufReader::new(file); @@ -66,38 +75,15 @@ fn points(filename: &str) -> Vec> { let mut gpx: Gpx = read(reader).unwrap(); eprintln!("we have {} tracks", gpx.tracks.len()); - let mut points = Vec::new(); - - let mut iter = gpx - .tracks + gpx.tracks .pop() .unwrap() .segments .into_iter() - .flat_map(|segment| segment.points.into_iter()) - .map(|p| { - let geop = p.point(); - ( - Point { - x: geop.x(), - y: geop.y(), - }, - p.comment.is_some(), - ) - }) - .dedup(); - let mut current_segment = iter.next().map(|(p, _)| p).into_iter().collect::>(); - for (p, is_waypoint) in iter { - if is_waypoint { - points.push(current_segment); - current_segment = Vec::new(); - } - current_segment.push(p); - } - let last_point = current_segment.pop(); - points.push(current_segment); - points.extend(last_point.map(|p| vec![p])); - points + .flat_map(|segment| segment.linestring()) + .map(|c| c.x_y()) + .map(|(x, y)| Point { x, y }) + .collect() } // // NOTE: this angles idea could maybe be use to get dp from n^3 to n^2 @@ -553,7 +539,7 @@ fn save_svg<'a, P: AsRef, I: IntoIterator>( for point in interest_points { writeln!( &mut writer, - "", + "", point.point.x, point.point.y, point.color(), @@ -563,7 +549,7 @@ fn save_svg<'a, P: AsRef, I: IntoIterator>( waypoints.iter().try_for_each(|p| { writeln!( &mut writer, - "", + "", p.x, p.y, ) })?; @@ -572,11 +558,14 @@ fn save_svg<'a, P: AsRef, I: IntoIterator>( Ok(()) } -fn detect_waypoints(points: &[Point]) -> HashSet { +fn detect_waypoints(points: &[Point], osm_waypoints: &HashSet) -> HashSet { points .first() .into_iter() .chain(points.iter().tuple_windows().filter_map(|(p1, p2, p3)| { + if !osm_waypoints.contains(&p2) { + return None; + } let x1 = p2.x - p1.x; let y1 = p2.y - p1.y; let a1 = y1.atan2(x1); @@ -652,35 +641,21 @@ async fn main() { let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); let osm_file = std::env::args().nth(2); eprintln!("input is {}", input_file); - let mut segmented_points = points(&input_file); - let p = segmented_points - .iter() - .flatten() - .copied() - .collect::>(); + let p = points(&input_file); eprintln!("initialy we have {} points", p.len()); + let rp = simplify_path(&p, 0.00015); + eprintln!("we now have {} points", rp.len()); - eprintln!("we have {} waypoints", segmented_points.len()); - - let mut waypoints = HashSet::new(); - let mut rp = Vec::new(); - for (i1, i2) in (0..segmented_points.len()).tuple_windows() { - if let [s1, s2] = &mut segmented_points[i1..=i2] { - s1.extend(s2.first().copied()); - waypoints.insert(s1.first().copied().unwrap()); - let mut simplified = simplify_path(&s1, 0.00015); - rp.append(&mut simplified); - rp.pop(); - } - } - rp.extend(segmented_points.last().and_then(|l| l.last()).copied()); - - let mut interests = if let Some(osm) = osm_file { + let (mut interests, osm_waypoints) = if let Some(osm) = osm_file { parse_osm_data(osm) } else { - Vec::new() + (Vec::new(), HashSet::new()) }; + + let waypoints = detect_waypoints(&rp, &osm_waypoints); + eprintln!("we found {} waypoints", waypoints.len()); + // let mut interests = parse_osm_data("isere.osm.pbf"); let buckets = position_interests_along_path(&mut interests, &rp, 0.001, 5, 3); // let i = get_openstreetmap_data(&rp).await; diff --git a/apps/gipy/gpconv/src/osm.rs b/apps/gipy/gpconv/src/osm.rs index 17425023a..a862a3638 100644 --- a/apps/gipy/gpconv/src/osm.rs +++ b/apps/gipy/gpconv/src/osm.rs @@ -5,8 +5,8 @@ use openstreetmap_api::{ types::{BoundingBox, Credentials}, Openstreetmap, }; -use osmio::prelude::*; use osmio::OSMObjBase; +use osmio::{prelude::*, ObjId}; use std::collections::{HashMap, HashSet}; use std::path::Path; @@ -150,9 +150,11 @@ async fn get_openstreetmap_data(points: &[(f64, f64)]) -> HashSet interest_points } -pub fn parse_osm_data>(path: P) -> Vec { +pub fn parse_osm_data>(path: P) -> (Vec, HashSet) { let reader = osmio::read_pbf(path).ok(); - reader + let mut crossroads: HashMap = HashMap::new(); + let mut coordinates: HashMap = HashMap::new(); + let interests = reader .map(|mut reader| { let mut interests = Vec::new(); for obj in reader.objects() { @@ -167,13 +169,28 @@ pub fn parse_osm_data>(path: P) -> Vec { }) { interests.push(p); } + coordinates.insert(n.id(), Point { x: lon, y: lat }); }); } - osmio::obj_types::ArcOSMObj::Way(_) => {} + osmio::obj_types::ArcOSMObj::Way(w) => { + if !w.is_area() { + for node in w.nodes() { + *crossroads.entry(*node).or_default() += 1; + } + } + } osmio::obj_types::ArcOSMObj::Relation(_) => {} } } interests }) - .unwrap_or_default() + .unwrap_or_default(); + ( + interests, + crossroads + .iter() + .filter(|&(_, c)| *c >= 3) + .filter_map(|(id, _)| coordinates.get(&id).copied()) + .collect(), + ) } From f35967d5023e739d44aa0f6b28cb8900b366b591 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 22 Jul 2022 17:36:04 +0200 Subject: [PATCH 036/106] new algorithm for waypoints detection --- apps/gipy/gpconv/src/main.rs | 67 ++++++++++++++++++++++-------------- apps/gipy/gpconv/src/osm.rs | 12 +++---- 2 files changed, 47 insertions(+), 32 deletions(-) diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index 6794b5c2e..2b463d312 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -1,4 +1,5 @@ use itertools::Itertools; +use osmio::ObjId; use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io::{BufReader, BufWriter, Write}; @@ -549,7 +550,7 @@ fn save_svg<'a, P: AsRef, I: IntoIterator>( waypoints.iter().try_for_each(|p| { writeln!( &mut writer, - "", + "", p.x, p.y, ) })?; @@ -558,27 +559,40 @@ fn save_svg<'a, P: AsRef, I: IntoIterator>( Ok(()) } -fn detect_waypoints(points: &[Point], osm_waypoints: &HashSet) -> HashSet { +fn detect_waypoints( + points: &[Point], + osm_waypoints: &HashMap>, +) -> HashSet { points .first() .into_iter() - .chain(points.iter().tuple_windows().filter_map(|(p1, p2, p3)| { - if !osm_waypoints.contains(&p2) { - return None; - } - let x1 = p2.x - p1.x; - let y1 = p2.y - p1.y; - let a1 = y1.atan2(x1); - let x2 = p3.x - p2.x; - let y2 = p3.y - p2.y; - let a2 = y2.atan2(x2); - let a = (a2 - a1).abs(); - if a <= std::f64::consts::PI / 3.0 || a >= std::f64::consts::PI * 5.0 / 3.0 { - None - } else { - Some(p2) - } - })) + .chain( + points + .iter() + .filter_map(|p: &Point| -> Option<(&Point, &Vec)> { + osm_waypoints.get(p).map(|l| (p, l)) + }) + .tuple_windows() + .filter_map(|((p1, l1), (p2, _), (p3, l2))| { + if l1.iter().all(|e| !l2.contains(e)) { + let x1 = p2.x - p1.x; + let y1 = p2.y - p1.y; + let a1 = y1.atan2(x1); + let x2 = p3.x - p2.x; + let y2 = p3.y - p2.y; + let a2 = y2.atan2(x2); + let a = (a2 - a1).abs(); + if a <= std::f64::consts::PI / 4.0 || a >= std::f64::consts::PI * 7.0 / 4.0 + { + None + } else { + Some(p2) + } + } else { + None + } + }), + ) .chain(points.last().into_iter()) .copied() .collect::>() @@ -640,19 +654,20 @@ fn position_interests_along_path( async fn main() { let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); let osm_file = std::env::args().nth(2); - eprintln!("input is {}", input_file); - let p = points(&input_file); - - eprintln!("initialy we have {} points", p.len()); - let rp = simplify_path(&p, 0.00015); - eprintln!("we now have {} points", rp.len()); let (mut interests, osm_waypoints) = if let Some(osm) = osm_file { parse_osm_data(osm) } else { - (Vec::new(), HashSet::new()) + (Vec::new(), HashMap::new()) }; + eprintln!("input is {}", input_file); + let p = points(&input_file); + eprintln!("initialy we have {} points", p.len()); + + let rp = simplify_path(&p, 0.00015); + eprintln!("we now have {} points", rp.len()); + let waypoints = detect_waypoints(&rp, &osm_waypoints); eprintln!("we found {} waypoints", waypoints.len()); diff --git a/apps/gipy/gpconv/src/osm.rs b/apps/gipy/gpconv/src/osm.rs index a862a3638..69e417b23 100644 --- a/apps/gipy/gpconv/src/osm.rs +++ b/apps/gipy/gpconv/src/osm.rs @@ -150,9 +150,9 @@ async fn get_openstreetmap_data(points: &[(f64, f64)]) -> HashSet interest_points } -pub fn parse_osm_data>(path: P) -> (Vec, HashSet) { +pub fn parse_osm_data>(path: P) -> (Vec, HashMap>) { let reader = osmio::read_pbf(path).ok(); - let mut crossroads: HashMap = HashMap::new(); + let mut crossroads: HashMap> = HashMap::new(); let mut coordinates: HashMap = HashMap::new(); let interests = reader .map(|mut reader| { @@ -175,7 +175,7 @@ pub fn parse_osm_data>(path: P) -> (Vec, HashSet

{ if !w.is_area() { for node in w.nodes() { - *crossroads.entry(*node).or_default() += 1; + crossroads.entry(*node).or_default().push(w.id()); } } } @@ -188,9 +188,9 @@ pub fn parse_osm_data>(path: P) -> (Vec, HashSet

= 3) - .filter_map(|(id, _)| coordinates.get(&id).copied()) + .into_iter() + .filter(|(_, r)| r.len() >= 2) + .filter_map(|(id, l)| coordinates.get(&id).copied().map(|c| (c, l))) .collect(), ) } From 987a44990c9162fc2d7157ec04a13366493acb3b Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 22 Jul 2022 18:06:24 +0200 Subject: [PATCH 037/106] minor fixes --- apps/gipy/TODO | 3 --- apps/gipy/app.js | 17 +++++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 179b73b2c..7383a0715 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -2,10 +2,7 @@ * bugs - meters seem to be a bit too long - - direction is still shitty on gps ? -- we are always lost (done ?) -- waypoints seem wrong ? * additional features diff --git a/apps/gipy/app.js b/apps/gipy/app.js index c77a40f24..4dc968b96 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -122,7 +122,7 @@ class Status { this.path.point(segment), this.path.point(segment + 1) ); - return distance_to_nearest > 20; + return distance_to_nearest > 30; } display() { g.clear(); @@ -181,7 +181,7 @@ class Status { (this.current_segment + 1) + "/" + (this.path.len - 1) + - " " + + " " + this.distance_to_next_point + "m", 0, @@ -189,9 +189,11 @@ class Status { ); if (this.distance_to_next_point <= 20) { - g.setColor(0.0, 1.0, 0.0) - .setFont("6x8:2") - .drawString("turn", g.getWidth() - 55, 35); + if (this.path.is_waypoint(this.reaching)) { + g.setColor(0.0, 1.0, 0.0) + .setFont("6x8:2") + .drawString("turn", g.getWidth() - 55, 35); + } } if (!this.on_path) { g.setColor(1.0, 0.0, 0.0) @@ -315,7 +317,10 @@ class Path { } is_waypoint(point_index) { - return this.waypoints[Math.floor(point_index / 8)] & point_index % 8; + let i = Math.floor(point_index / 8); + let subindex = point_index % 8; + let r = this.waypoints[i] & (1 << subindex); + return r != 0; } // execute op on all segments. From 3ee3ddc40307015cf0153a60c13408ff4f368cd3 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sat, 23 Jul 2022 08:21:53 +0200 Subject: [PATCH 038/106] better waypoints extraction --- apps/gipy/TODO | 1 + apps/gipy/gpconv/src/main.rs | 82 ++++++++++++++++++++++++++++++------ 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 7383a0715..3316412a4 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -3,6 +3,7 @@ - meters seem to be a bit too long - direction is still shitty on gps ? +- menu is still active below map * additional features diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index 2b463d312..51f4e5f1a 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -68,7 +68,7 @@ impl Point { } } -fn points(filename: &str) -> Vec { +fn points(filename: &str) -> Vec> { let file = File::open(filename).unwrap(); let reader = BufReader::new(file); @@ -76,15 +76,33 @@ fn points(filename: &str) -> Vec { let mut gpx: Gpx = read(reader).unwrap(); eprintln!("we have {} tracks", gpx.tracks.len()); - gpx.tracks + let mut segments = Vec::new(); + let mut current_segment = Vec::new(); + for (p, stop) in gpx + .tracks .pop() .unwrap() .segments .into_iter() - .flat_map(|segment| segment.linestring()) - .map(|c| c.x_y()) - .map(|(x, y)| Point { x, y }) - .collect() + .flat_map(|segment| segment.points.into_iter()) + .map(|p| { + let is_commented = p.comment.is_some(); + let (x, y) = p.point().x_y(); + (Point { x, y }, is_commented) + }) + { + current_segment.push(p); + if stop { + if current_segment.len() > 1 { + segments.push(current_segment); + current_segment = vec![p]; + } + } + } + if current_segment.len() > 1 { + segments.push(current_segment); + } + segments } // // NOTE: this angles idea could maybe be use to get dp from n^3 to n^2 @@ -661,20 +679,60 @@ async fn main() { (Vec::new(), HashMap::new()) }; - eprintln!("input is {}", input_file); + println!("input is {}", input_file); let p = points(&input_file); - eprintln!("initialy we have {} points", p.len()); - let rp = simplify_path(&p, 0.00015); - eprintln!("we now have {} points", rp.len()); + let mut waypoints; + let mut rp; + if p.len() == 1 { + // we don't have any waypoint information + println!("no waypoint information"); + println!("initially we had {} points", p[0].len()); + waypoints = detect_waypoints(&p[0], &osm_waypoints); - let waypoints = detect_waypoints(&rp, &osm_waypoints); - eprintln!("we found {} waypoints", waypoints.len()); + rp = Vec::new(); + let mut current_segment = Vec::new(); + let mut last = None; + for p in &p[0] { + current_segment.push(*p); + if waypoints.contains(p) { + if current_segment.len() > 1 { + let mut s = simplify_path(¤t_segment, 0.00015); + rp.append(&mut s); + last = rp.pop(); + current_segment = vec![*p]; + } + } + } + rp.extend(last); + println!("we now have {} points", rp.len()); + + eprintln!("we found {} waypoints", waypoints.len()); + } else { + println!("we have {} waypoints", p.len() + 1); + println!( + "initially we had {} points", + p.iter().map(|s| s.len()).sum::() - (p.len() - 1) + ); + waypoints = HashSet::new(); + rp = Vec::new(); + let mut last = None; + for segment in &p { + waypoints.insert(segment.first().copied().unwrap()); + waypoints.insert(segment.last().copied().unwrap()); + let mut s = simplify_path(segment, 0.00015); + rp.append(&mut s); + last = rp.pop(); + } + rp.extend(last); + println!("we now have {} points", rp.len()); + } // let mut interests = parse_osm_data("isere.osm.pbf"); let buckets = position_interests_along_path(&mut interests, &rp, 0.001, 5, 3); // let i = get_openstreetmap_data(&rp).await; // let i = HashSet::new(); + let p = p.into_iter().flatten().collect::>(); save_svg( "test.svg", &p, From 2eea84f01d119612a8ae4f621d2256fd4c156270 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sat, 23 Jul 2022 08:41:23 +0200 Subject: [PATCH 039/106] fonts change --- apps/gipy/ChangeLog | 3 +++ apps/gipy/app.js | 12 ++++++++---- apps/gipy/metadata.json | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 1664fc513..eeb484394 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -32,3 +32,6 @@ * Bugfix in map display (last segment was missing + wrong colors). * Waypoint detections using OSM + sharp angles * New algorith for direction detection + +0.11: + * Better fonts (more free space, still readable). diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 4dc968b96..8c8d6af65 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -174,8 +174,12 @@ class Status { let hours = now.getHours().toString(); g.setFont("6x8:2") .setColor(g.theme.fg) - .drawString(hours + ":" + minutes, 0, g.getHeight() - 49); - g.drawString("d. " + rounded_distance + "/" + total, 0, g.getHeight() - 32); + .drawString(hours + ":" + minutes, g.getWidth() - 50, g.getHeight() - 15); + g.setFont("6x15").drawString( + "d. " + rounded_distance + "/" + total, + 0, + g.getHeight() - 32 + ); g.drawString( "seg." + (this.current_segment + 1) + @@ -191,13 +195,13 @@ class Status { if (this.distance_to_next_point <= 20) { if (this.path.is_waypoint(this.reaching)) { g.setColor(0.0, 1.0, 0.0) - .setFont("6x8:2") + .setFont("6x15") .drawString("turn", g.getWidth() - 55, 35); } } if (!this.on_path) { g.setColor(1.0, 0.0, 0.0) - .setFont("6x8:2") + .setFont("6x15") .drawString("lost", g.getWidth() - 55, 35); } } diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 3673abb30..b2357490c 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.10", + "version": "0.11", "description": "Follow gpx files", "allow_emulator":false, "icon": "gipy.png", From 540bf2fef87ec405a774617a5c491e9363cc8512 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sat, 23 Jul 2022 11:00:59 +0200 Subject: [PATCH 040/106] minor improvements display direction to next point if lost bugfix: menu --- apps/gipy/ChangeLog | 1 + apps/gipy/TODO | 2 -- apps/gipy/app.js | 15 +++++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index eeb484394..4a49e98ea 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -35,3 +35,4 @@ 0.11: * Better fonts (more free space, still readable). + * Display direction to nearest point when lost. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 3316412a4..599e1196a 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -3,11 +3,9 @@ - meters seem to be a bit too long - direction is still shitty on gps ? -- menu is still active below map * additional features -- display direction to nearest point - turn off gps when moving to next waypoint - display distance to next water/toilet - display distance to next waypoint diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 8c8d6af65..8ece4aee4 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -211,8 +211,8 @@ class Status { // while lowering the cost a lot // // note that all code is inlined here to speed things up from 400ms to 200ms - let start = Math.max(this.current_segment - 3, 0); - let end = Math.min(this.current_segment + 5, this.path.len); + let start = Math.max(this.current_segment - 4, 0); + let end = Math.min(this.current_segment + 6, this.path.len); let pos = this.position; let cos = this.cos_direction; let sin = this.sin_direction; @@ -268,6 +268,16 @@ class Status { // now display ourselves g.setColor(g.theme.fgH); g.fillCircle(half_width, half_height, 5); + + // display direction to next point if lost + if (!this.on_path) { + let next_point = this.path.point(this.current_segment + 1); + let diff = next_point.minus(this.position); + let angle = Math.atan2(diff.lat, diff.lon); + let x = Math.cos(angle) * 30.0 + half_width; + let y = Math.sin(angle) * 30.0 + half_height; + g.setColor(g.theme.fgH).drawLine(half_width, half_height, x, y); + } } } @@ -511,6 +521,7 @@ function drawMenu() { } function start(fn) { + E.showMenu(); console.log("loading", fn); let path = new Path(fn); From 61d5e46e868ea08a35cb5f9daab3e03d4d76fc18 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sat, 23 Jul 2022 16:25:38 +0200 Subject: [PATCH 041/106] avg speed --- apps/gipy/ChangeLog | 1 + apps/gipy/TODO | 3 +-- apps/gipy/app.js | 17 +++++++++++++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 4a49e98ea..f3a355f3a 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -36,3 +36,4 @@ 0.11: * Better fonts (more free space, still readable). * Display direction to nearest point when lost. + * Display average speed. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 599e1196a..be499a869 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -8,8 +8,7 @@ - turn off gps when moving to next waypoint - display distance to next water/toilet -- display distance to next waypoint -- display average speed + - dynamic map rescale - display scale (100m) - get waypoints from osm diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 8ece4aee4..7e94dd6c6 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,4 +1,4 @@ -let simulated = false; +let simulated = true; let file_version = 3; let code_key = 47490; @@ -44,6 +44,7 @@ class Status { previous_point = point; } this.remaining_distances = r; // how much distance remains at start of each segment + this.starting_time = getTime(); } update_position(new_position, direction) { if ( @@ -173,8 +174,20 @@ class Status { } let hours = now.getHours().toString(); g.setFont("6x8:2") + .setFontAlign(1, -1, 0) .setColor(g.theme.fg) - .drawString(hours + ":" + minutes, g.getWidth() - 50, g.getHeight() - 15); + .drawString(hours + ":" + minutes, g.getWidth(), g.getHeight() - 15); + + let done_distance = + this.remaining_distances[0] - + this.remaining_distances[this.current_segment + 1] - + this.distance_to_next_point; + let done_in = getTime() - this.starting_time; + let approximate_speed = Math.round(done_distance / done_in); + g.setFont("6x15") + .setFontAlign(-1, -1, 0) + .drawString("s." + approximate_speed + "km/h", 0, g.getHeight() - 49); + g.setFont("6x15").drawString( "d. " + rounded_distance + "/" + total, 0, From 054c72e11c3a351c3c707f10c52692e22f90af88 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sat, 23 Jul 2022 16:26:07 +0200 Subject: [PATCH 042/106] forgot simulation --- apps/gipy/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 7e94dd6c6..0f2fb0961 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,4 +1,4 @@ -let simulated = true; +let simulated = false; let file_version = 3; let code_key = 47490; From cb4ee0d8e9eb67ccd35f8ae8edd8999e16044e14 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 26 Jul 2022 08:43:52 +0200 Subject: [PATCH 043/106] debuging direction --- apps/gipy/app.js | 90 ++++++++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 0f2fb0961..59a2009d6 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -45,15 +45,42 @@ class Status { } this.remaining_distances = r; // how much distance remains at start of each segment this.starting_time = getTime(); + this.old_points = []; } - update_position(new_position, direction) { - if ( - Bangle.isLocked() && - this.position !== null && - new_position.lon == this.position.lon && - new_position.lat == this.position.lat - ) { - return; + new_position_reached(position) { + // we try to figure out direction by looking at previous points + // instead of the gps course which is not very nice. + if (this.old_points.length == 0) { + this.old_points.push(position); + } else { + let last_point = this.old_points[this.old_points.length - 1]; + if (last_point.lon != position.lon || last_point.lat != position.lat) { + if (this.old_points.length == 4) { + this.old_points.shift(); + } + this.old_points.push(position); + } else { + return null; + } + } + if (this.old_points.length == 1) { + return null; + } else { + // let's just take angle of segment between oldest and newest point + let oldest = this.old_points[0]; + let diff = position.minus(oldest); + let angle = Math.atan2(diff.lat, diff.lon); + return angle; + } + } + update_position(new_position, maybe_direction) { + let direction = this.new_position_reached(new_position); + if (direction === null) { + if (maybe_direction === null) { + return; + } else { + direction = maybe_direction; + } } this.cos_direction = Math.cos(-direction - Math.PI / 2.0); @@ -63,8 +90,8 @@ class Status { // detect segment we are on now let next_segment = this.path.nearest_segment( this.position, - Math.max(0, this.current_segment - 1), - Math.min(this.current_segment + 2, this.path.len - 1), + Math.max(0, this.current_segment - 2), + Math.min(this.current_segment + 3, this.path.len - 1), this.cos_direction, this.sin_direction ); @@ -123,7 +150,7 @@ class Status { this.path.point(segment), this.path.point(segment + 1) ); - return distance_to_nearest > 30; + return distance_to_nearest > 50; } display() { g.clear(); @@ -282,6 +309,18 @@ class Status { g.setColor(g.theme.fgH); g.fillCircle(half_width, half_height, 5); + // display old points for direction debug + for (let i = 0; i < this.old_points.length; i++) { + let tx = (this.old_points[i].lon - cx) * 40000.0; + let ty = (this.old_points[i].lat - cy) * 40000.0; + let rotated_x = tx * cos - ty * sin; + let rotated_y = tx * sin + ty * cos; + let x = half_width - Math.round(rotated_x); // x is inverted + let y = half_height + Math.round(rotated_y); + g.setColor((i + 1) / 4.0, 0.0, 0.0); + g.fillCircle(x, y, 3); + } + // display direction to next point if lost if (!this.on_path) { let next_point = this.path.point(this.current_segment + 1); @@ -515,8 +554,7 @@ function simulate_gps(status) { let old_pos = status.position; fake_gps_point += 0.2; // advance simulation - let direction = Math.atan2(pos.lat - old_pos.lat, pos.lon - old_pos.lon); - status.update_position(pos, direction); + status.update_position(pos, null); } function drawMenu() { @@ -553,35 +591,11 @@ function start(fn) { status.update_position(p1, direction); let frame = 0; - let old_points = []; // remember the at most 3 previous points let set_coordinates = function (data) { frame += 1; let valid_coordinates = !isNaN(data.lat) && !isNaN(data.lon); if (valid_coordinates) { - // we try to figure out direction by looking at previous points - // instead of the gps course which is not very nice. - let direction = (data.course * Math.PI) / 180.0; - let position = new Point(data.lon, data.lat); - if (old_points.length == 0) { - old_points.push(position); - } else { - let last_point = old_points[old_points.length - 1]; - if (last_point.x != position.x || last_point.y != position.y) { - if (old_points.length == 4) { - old_points.shift(); - } - old_points.push(position); - } - } - if (old_points.length == 4) { - // let's just take angle of segment between oldest and newest point - let oldest = old_points[0]; - let diff = position.minus(oldest); - let angle = Math.atan2(diff.lat, diff.lon); - status.update_position(position, angle); - } else { - status.update_position(position, direction); - } + status.update_position(new Point(data.lon, data.lat), null); } let gps_status_color; if (frame % 2 == 0 || valid_coordinates) { From e022f5894f09329b73cd7d22c941496454d47936 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 26 Jul 2022 09:02:52 +0200 Subject: [PATCH 044/106] another non working waypoints detection algorithm --- apps/gipy/gpconv/src/main.rs | 64 ++++++++++++++++++++++-------------- apps/gipy/gpconv/src/osm.rs | 26 ++++++++------- 2 files changed, 53 insertions(+), 37 deletions(-) diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index 51f4e5f1a..351e8a093 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -14,26 +14,38 @@ use osm::{parse_osm_data, InterestPoint}; const KEY: u16 = 47490; const FILE_VERSION: u16 = 3; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Point { x: f64, y: f64, } -impl PartialEq for Point { - fn eq(&self, other: &Self) -> bool { - (self.x - other.x).abs() < 0.0005 && (self.y - other.y).abs() < 0.0005 - } -} impl Eq for Point {} impl std::hash::Hash for Point { fn hash(&self, state: &mut H) { - let x = format!("{:.4}", self.x); - let y = format!("{:.4}", self.y); + unsafe { std::mem::transmute::(self.x) }.hash(state); + unsafe { std::mem::transmute::(self.y) }.hash(state); + } +} + +#[derive(Debug, Clone, Copy)] +pub struct APoint { + x: f64, + y: f64, +} + +impl PartialEq for APoint { + fn eq(&self, other: &Self) -> bool { + (self.x - other.x).abs() < 0.00005 && (self.y - other.y).abs() < 0.00005 + } +} +impl Eq for APoint {} +impl std::hash::Hash for APoint { + fn hash(&self, state: &mut H) { + let x = format!("{:.5}", self.x); + let y = format!("{:.5}", self.y); x.hash(state); y.hash(state); - // unsafe { std::mem::transmute::(self.x) }.hash(state); - // unsafe { std::mem::transmute::(self.y) }.hash(state); } } @@ -579,7 +591,7 @@ fn save_svg<'a, P: AsRef, I: IntoIterator>( fn detect_waypoints( points: &[Point], - osm_waypoints: &HashMap>, + osm_waypoints: &HashMap>, ) -> HashSet { points .first() @@ -588,24 +600,26 @@ fn detect_waypoints( points .iter() .filter_map(|p: &Point| -> Option<(&Point, &Vec)> { - osm_waypoints.get(p).map(|l| (p, l)) + osm_waypoints + .get(&APoint { x: p.x, y: p.y }) + .map(|l| (p, l)) }) .tuple_windows() .filter_map(|((p1, l1), (p2, _), (p3, l2))| { if l1.iter().all(|e| !l2.contains(e)) { - let x1 = p2.x - p1.x; - let y1 = p2.y - p1.y; - let a1 = y1.atan2(x1); - let x2 = p3.x - p2.x; - let y2 = p3.y - p2.y; - let a2 = y2.atan2(x2); - let a = (a2 - a1).abs(); - if a <= std::f64::consts::PI / 4.0 || a >= std::f64::consts::PI * 7.0 / 4.0 - { - None - } else { - Some(p2) - } + // let x1 = p2.x - p1.x; + // let y1 = p2.y - p1.y; + // let a1 = y1.atan2(x1); + // let x2 = p3.x - p2.x; + // let y2 = p3.y - p2.y; + // let a2 = y2.atan2(x2); + // let a = (a2 - a1).abs(); + // if a <= std::f64::consts::PI / 4.0 || a >= std::f64::consts::PI * 7.0 / 4.0 + // { + // None + // } else { + Some(p2) + // } } else { None } diff --git a/apps/gipy/gpconv/src/osm.rs b/apps/gipy/gpconv/src/osm.rs index 69e417b23..a862c22ca 100644 --- a/apps/gipy/gpconv/src/osm.rs +++ b/apps/gipy/gpconv/src/osm.rs @@ -1,4 +1,4 @@ -use super::Point; +use super::{APoint, Point}; use itertools::Itertools; use lazy_static::lazy_static; use openstreetmap_api::{ @@ -150,10 +150,12 @@ async fn get_openstreetmap_data(points: &[(f64, f64)]) -> HashSet interest_points } -pub fn parse_osm_data>(path: P) -> (Vec, HashMap>) { +pub fn parse_osm_data>( + path: P, +) -> (Vec, HashMap>) { let reader = osmio::read_pbf(path).ok(); let mut crossroads: HashMap> = HashMap::new(); - let mut coordinates: HashMap = HashMap::new(); + let mut coordinates: HashMap = HashMap::new(); let interests = reader .map(|mut reader| { let mut interests = Vec::new(); @@ -169,7 +171,7 @@ pub fn parse_osm_data>(path: P) -> (Vec, HashMap

{ @@ -185,12 +187,12 @@ pub fn parse_osm_data>(path: P) -> (Vec, HashMap

= 2) - .filter_map(|(id, l)| coordinates.get(&id).copied().map(|c| (c, l))) - .collect(), - ) + + let mut osm_waypoints: HashMap> = HashMap::new(); + for (node_id, ways) in crossroads.into_iter().filter(|(_, r)| r.len() >= 2) { + if let Some(c) = coordinates.get(&node_id).copied() { + osm_waypoints.entry(c).or_default().extend(ways) + } + } + (interests, osm_waypoints) } From e26aafc4faad043547b94fca30fb405bba23919a Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 26 Jul 2022 09:09:08 +0200 Subject: [PATCH 045/106] removed waypoints detection code --- apps/gipy/gpconv/src/main.rs | 87 +++--------------------------------- apps/gipy/gpconv/src/osm.rs | 29 +++--------- 2 files changed, 11 insertions(+), 105 deletions(-) diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index 351e8a093..e85c358bc 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -28,27 +28,6 @@ impl std::hash::Hash for Point { } } -#[derive(Debug, Clone, Copy)] -pub struct APoint { - x: f64, - y: f64, -} - -impl PartialEq for APoint { - fn eq(&self, other: &Self) -> bool { - (self.x - other.x).abs() < 0.00005 && (self.y - other.y).abs() < 0.00005 - } -} -impl Eq for APoint {} -impl std::hash::Hash for APoint { - fn hash(&self, state: &mut H) { - let x = format!("{:.5}", self.x); - let y = format!("{:.5}", self.y); - x.hash(state); - y.hash(state); - } -} - impl Point { fn squared_distance_between(&self, other: &Point) -> f64 { let dx = other.x - self.x; @@ -589,47 +568,6 @@ fn save_svg<'a, P: AsRef, I: IntoIterator>( Ok(()) } -fn detect_waypoints( - points: &[Point], - osm_waypoints: &HashMap>, -) -> HashSet { - points - .first() - .into_iter() - .chain( - points - .iter() - .filter_map(|p: &Point| -> Option<(&Point, &Vec)> { - osm_waypoints - .get(&APoint { x: p.x, y: p.y }) - .map(|l| (p, l)) - }) - .tuple_windows() - .filter_map(|((p1, l1), (p2, _), (p3, l2))| { - if l1.iter().all(|e| !l2.contains(e)) { - // let x1 = p2.x - p1.x; - // let y1 = p2.y - p1.y; - // let a1 = y1.atan2(x1); - // let x2 = p3.x - p2.x; - // let y2 = p3.y - p2.y; - // let a2 = y2.atan2(x2); - // let a = (a2 - a1).abs(); - // if a <= std::f64::consts::PI / 4.0 || a >= std::f64::consts::PI * 7.0 / 4.0 - // { - // None - // } else { - Some(p2) - // } - } else { - None - } - }), - ) - .chain(points.last().into_iter()) - .copied() - .collect::>() -} - pub struct Bucket { points: Vec, start: usize, @@ -687,10 +625,10 @@ async fn main() { let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); let osm_file = std::env::args().nth(2); - let (mut interests, osm_waypoints) = if let Some(osm) = osm_file { + let mut interests = if let Some(osm) = osm_file { parse_osm_data(osm) } else { - (Vec::new(), HashMap::new()) + Vec::new() }; println!("input is {}", input_file); @@ -702,25 +640,12 @@ async fn main() { // we don't have any waypoint information println!("no waypoint information"); println!("initially we had {} points", p[0].len()); - waypoints = detect_waypoints(&p[0], &osm_waypoints); - - rp = Vec::new(); - let mut current_segment = Vec::new(); - let mut last = None; - for p in &p[0] { - current_segment.push(*p); - if waypoints.contains(p) { - if current_segment.len() > 1 { - let mut s = simplify_path(¤t_segment, 0.00015); - rp.append(&mut s); - last = rp.pop(); - current_segment = vec![*p]; - } - } - } - rp.extend(last); + rp = simplify_path(&p[0], 0.00015); println!("we now have {} points", rp.len()); + waypoints = HashSet::new(); + waypoints.insert(rp.first().copied().unwrap()); + waypoints.insert(rp.last().copied().unwrap()); eprintln!("we found {} waypoints", waypoints.len()); } else { println!("we have {} waypoints", p.len() + 1); diff --git a/apps/gipy/gpconv/src/osm.rs b/apps/gipy/gpconv/src/osm.rs index a862c22ca..596febb14 100644 --- a/apps/gipy/gpconv/src/osm.rs +++ b/apps/gipy/gpconv/src/osm.rs @@ -1,4 +1,4 @@ -use super::{APoint, Point}; +use super::Point; use itertools::Itertools; use lazy_static::lazy_static; use openstreetmap_api::{ @@ -150,13 +150,9 @@ async fn get_openstreetmap_data(points: &[(f64, f64)]) -> HashSet interest_points } -pub fn parse_osm_data>( - path: P, -) -> (Vec, HashMap>) { +pub fn parse_osm_data>(path: P) -> Vec { let reader = osmio::read_pbf(path).ok(); - let mut crossroads: HashMap> = HashMap::new(); - let mut coordinates: HashMap = HashMap::new(); - let interests = reader + reader .map(|mut reader| { let mut interests = Vec::new(); for obj in reader.objects() { @@ -171,28 +167,13 @@ pub fn parse_osm_data>( }) { interests.push(p); } - coordinates.insert(n.id(), APoint { x: lon, y: lat }); }); } - osmio::obj_types::ArcOSMObj::Way(w) => { - if !w.is_area() { - for node in w.nodes() { - crossroads.entry(*node).or_default().push(w.id()); - } - } - } + osmio::obj_types::ArcOSMObj::Way(w) => {} osmio::obj_types::ArcOSMObj::Relation(_) => {} } } interests }) - .unwrap_or_default(); - - let mut osm_waypoints: HashMap> = HashMap::new(); - for (node_id, ways) in crossroads.into_iter().filter(|(_, r)| r.len() >= 2) { - if let Some(c) = coordinates.get(&node_id).copied() { - osm_waypoints.entry(c).or_default().extend(ways) - } - } - (interests, osm_waypoints) + .unwrap_or_default() } From 84e9d12a097eeaf24754fd09c455228da02c0a84 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 26 Jul 2022 09:44:42 +0200 Subject: [PATCH 046/106] turn gps off --- apps/gipy/ChangeLog | 1 + apps/gipy/TODO | 7 ++----- apps/gipy/app.js | 21 +++++++++++++++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index f3a355f3a..e44322007 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -37,3 +37,4 @@ * Better fonts (more free space, still readable). * Display direction to nearest point when lost. * Display average speed. + * Turn off gps when locked and between points diff --git a/apps/gipy/TODO b/apps/gipy/TODO index be499a869..88b2e3f6c 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -2,16 +2,13 @@ * bugs - meters seem to be a bit too long -- direction is still shitty on gps ? +- segment detection could be better ? * additional features -- turn off gps when moving to next waypoint -- display distance to next water/toilet - +- display distance to next water/toilet ? - dynamic map rescale - display scale (100m) -- get waypoints from osm - compress path ? diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 59a2009d6..c8ca09791 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -124,6 +124,16 @@ class Status { this.distance_to_next_point = Math.ceil( this.position.distance(this.path.point(next_point)) ); + // disable gps when far from next point and locked + if (Bangle.isLocked()) { + let time_to_next_point = this.distance_to_next_point / 9.7; // 30km/h is 8.3 m/s + if (time_to_next_point > 30) { + Bangle.setGPSPower(false, "gipy"); + setTimeout(function () { + Bangle.setGPSPower(true, "gipy"); + }, time_to_next_point); + } + } if (this.reaching != next_point && this.distance_to_next_point <= 20) { this.reaching = next_point; let reaching_waypoint = this.path.is_waypoint(next_point); @@ -134,10 +144,8 @@ class Status { } } } - // re-display unless locked - if (!Bangle.isLocked() || simulated) { - this.display(); - } + // re-display + this.display(); } remaining_distance() { return ( @@ -610,6 +618,11 @@ function start(fn) { Bangle.setGPSPower(true, "gipy"); Bangle.on("GPS", set_coordinates); + Bangle.on("lock", function (on) { + if (!on) { + Bangle.setGPSPower(true, "gipy"); // activate gps when unlocking + } + }); } } From da72a24199341bd51f34b2fe70640a71c6737241 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 26 Jul 2022 16:59:04 +0200 Subject: [PATCH 047/106] bugfix for speed --- apps/gipy/ChangeLog | 3 +++ apps/gipy/TODO | 5 ++++- apps/gipy/app.js | 10 ++++------ apps/gipy/metadata.json | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index e44322007..1bfac278c 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -38,3 +38,6 @@ * Display direction to nearest point when lost. * Display average speed. * Turn off gps when locked and between points + +0.12: + * Bugfix in speed computation. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 88b2e3f6c..ac0b7416b 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,8 +1,11 @@ * bugs -- meters seem to be a bit too long - segment detection could be better ? +-----> we need to display debug info like which nearest point on which segment + very often you jump to next segment while still on current one +- it does not buzz very often on turns + * additional features diff --git a/apps/gipy/app.js b/apps/gipy/app.js index c8ca09791..c266532b1 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -200,7 +200,8 @@ class Status { } } display_stats() { - let rounded_distance = Math.round(this.remaining_distance() / 100) / 10; + let remaining_distance = this.remaining_distance(); + let rounded_distance = Math.round(remaining_distance / 100) / 10; let total = Math.round(this.remaining_distances[0] / 100) / 10; let now = new Date(); let minutes = now.getMinutes().toString(); @@ -213,12 +214,9 @@ class Status { .setColor(g.theme.fg) .drawString(hours + ":" + minutes, g.getWidth(), g.getHeight() - 15); - let done_distance = - this.remaining_distances[0] - - this.remaining_distances[this.current_segment + 1] - - this.distance_to_next_point; + let done_distance = this.remaining_distances[0] - remaining_distance; let done_in = getTime() - this.starting_time; - let approximate_speed = Math.round(done_distance / done_in); + let approximate_speed = Math.round((done_distance * 3.6) / done_in); g.setFont("6x15") .setFontAlign(-1, -1, 0) .drawString("s." + approximate_speed + "km/h", 0, g.getHeight() - 49); diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index b2357490c..2c31b1317 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.11", + "version": "0.12", "description": "Follow gpx files", "allow_emulator":false, "icon": "gipy.png", From b056f85a953c2fbe96471c45fe4a49ff46be09a9 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 27 Jul 2022 15:35:00 +0200 Subject: [PATCH 048/106] bugfix in segment detection --- apps/gipy/ChangeLog | 1 + apps/gipy/TODO | 4 --- apps/gipy/app.js | 74 ++++++++++++++++++++++++++++----------------- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 1bfac278c..bc68d442d 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -41,3 +41,4 @@ 0.12: * Bugfix in speed computation. + * Bugfix in current segment detection. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index ac0b7416b..203105909 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,12 +1,8 @@ * bugs -- segment detection could be better ? ------> we need to display debug info like which nearest point on which segment - very often you jump to next segment while still on current one - it does not buzz very often on turns - * additional features - display distance to next water/toilet ? diff --git a/apps/gipy/app.js b/apps/gipy/app.js index c266532b1..8931ac426 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -26,8 +26,8 @@ class Status { this.path = path; this.on_path = false; // are we on the path or lost ? this.position = null; // where we are - this.cos_direction = null; // cos of where we look at - this.sin_direction = null; // sin of where we look at + this.adjusted_cos_direction = null; // cos of where we look at + this.adjusted_sin_direction = null; // sin of where we look at this.current_segment = null; // which segment is closest this.reaching = null; // which waypoint are we reaching ? this.distance_to_next_point = null; // how far are we from next point ? @@ -82,18 +82,21 @@ class Status { direction = maybe_direction; } } + g.clear(); - this.cos_direction = Math.cos(-direction - Math.PI / 2.0); - this.sin_direction = Math.sin(-direction - Math.PI / 2.0); + this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0); + this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0); + cos_direction = Math.cos(direction); + sin_direction = Math.sin(direction); this.position = new_position; // detect segment we are on now let next_segment = this.path.nearest_segment( this.position, - Math.max(0, this.current_segment - 2), - Math.min(this.current_segment + 3, this.path.len - 1), - this.cos_direction, - this.sin_direction + Math.max(0, this.current_segment - 1), + Math.min(this.current_segment + 2, this.path.len - 1), + cos_direction, + sin_direction ); if (this.is_lost(next_segment)) { @@ -102,8 +105,8 @@ class Status { this.position, 0, this.path.len - 1, - this.cos_direction, - this.sin_direction + cos_direction, + sin_direction ); } // now check if we strayed away from path or back to it @@ -161,7 +164,7 @@ class Status { return distance_to_nearest > 50; } display() { - g.clear(); + //g.clear(); this.display_map(); this.display_interest_points(); this.display_stats(); @@ -193,8 +196,8 @@ class Status { let color = this.path.interest_color(i); let c = interest_point.coordinates( this.position, - this.cos_direction, - this.sin_direction + this.adjusted_cos_direction, + this.adjusted_sin_direction ); g.setColor(color).fillCircle(c[0], c[1], 5); } @@ -260,8 +263,8 @@ class Status { let start = Math.max(this.current_segment - 4, 0); let end = Math.min(this.current_segment + 6, this.path.len); let pos = this.position; - let cos = this.cos_direction; - let sin = this.sin_direction; + let cos = this.adjusted_cos_direction; + let sin = this.adjusted_sin_direction; let points = this.path.points; let cx = pos.lon; let cy = pos.lat; @@ -316,16 +319,16 @@ class Status { g.fillCircle(half_width, half_height, 5); // display old points for direction debug - for (let i = 0; i < this.old_points.length; i++) { - let tx = (this.old_points[i].lon - cx) * 40000.0; - let ty = (this.old_points[i].lat - cy) * 40000.0; - let rotated_x = tx * cos - ty * sin; - let rotated_y = tx * sin + ty * cos; - let x = half_width - Math.round(rotated_x); // x is inverted - let y = half_height + Math.round(rotated_y); - g.setColor((i + 1) / 4.0, 0.0, 0.0); - g.fillCircle(x, y, 3); - } + // for (let i = 0; i < this.old_points.length; i++) { + // let tx = (this.old_points[i].lon - cx) * 40000.0; + // let ty = (this.old_points[i].lat - cy) * 40000.0; + // let rotated_x = tx * cos - ty * sin; + // let rotated_y = tx * sin + ty * cos; + // let x = half_width - Math.round(rotated_x); // x is inverted + // let y = half_height + Math.round(rotated_y); + // g.setColor((i + 1) / 4.0, 0.0, 0.0); + // g.fillCircle(x, y, 3); + // } // display direction to next point if lost if (!this.on_path) { @@ -437,10 +440,25 @@ class Path { this.on_segments( function (p1, p2, i) { // we use the dot product to figure out if oriented correctly - let distance = point.fake_distance_to_segment(p1, p2); + // let distance = point.fake_distance_to_segment(p1, p2); + + let projection = point.closest_segment_point(p1, p2); + let distance = point.fake_distance(projection); + + // let d = projection.minus(point).times(40000.0); + // let rotated_x = d.lon * acos - d.lat * asin; + // let rotated_y = d.lon * asin + d.lat * acos; + // let x = g.getWidth() / 2 - Math.round(rotated_x); // x is inverted + // let y = g.getHeight() / 2 + Math.round(rotated_y); + // let diff = p2.minus(p1); let dot = cos_direction * diff.lon + sin_direction * diff.lat; let orientation = +(dot < 0); // index 0 is good orientation + // g.setColor(0.0, 0.0 + orientation, 1.0 - orientation).fillCircle( + // x, + // y, + // 10 + // ); if (distance <= mins[orientation]) { mins[orientation] = distance; indices[orientation] = i - 1; @@ -522,7 +540,7 @@ class Point { // Return minimum distance between line segment vw and point p let l2 = v.length_squared(w); // i.e. |w-v|^2 - avoid a sqrt if (l2 == 0.0) { - return this.distance(v); // v == w case + return v; // v == w case } // Consider the line extending the segment, parameterized as v + t (w - v). // We find projection of point p onto the line. @@ -559,7 +577,7 @@ function simulate_gps(status) { let pos = p1.times(1 - alpha).plus(p2.times(alpha)); let old_pos = status.position; - fake_gps_point += 0.2; // advance simulation + fake_gps_point += 0.05; // advance simulation status.update_position(pos, null); } From 0dfb187737f45f989ba788f70d6afeece63faafe Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 27 Jul 2022 17:21:51 +0200 Subject: [PATCH 049/106] more fix --- apps/gipy/ChangeLog | 1 + apps/gipy/app.js | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index bc68d442d..10a532e30 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -42,3 +42,4 @@ 0.12: * Bugfix in speed computation. * Bugfix in current segment detection. + * Bugfix : lost direction. diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 8931ac426..6b8cb0b9d 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -82,7 +82,6 @@ class Status { direction = maybe_direction; } } - g.clear(); this.adjusted_cos_direction = Math.cos(-direction - Math.PI / 2.0); this.adjusted_sin_direction = Math.sin(-direction - Math.PI / 2.0); @@ -164,7 +163,7 @@ class Status { return distance_to_nearest > 50; } display() { - //g.clear(); + g.clear(); this.display_map(); this.display_interest_points(); this.display_stats(); @@ -335,8 +334,8 @@ class Status { let next_point = this.path.point(this.current_segment + 1); let diff = next_point.minus(this.position); let angle = Math.atan2(diff.lat, diff.lon); - let x = Math.cos(angle) * 30.0 + half_width; - let y = Math.sin(angle) * 30.0 + half_height; + let x = Math.cos(-angle - Math.PI / 2) * 50.0 + half_width; + let y = Math.sin(-angle - Math.PI / 2) * 50.0 + half_height; g.setColor(g.theme.fgH).drawLine(half_width, half_height, x, y); } } From 70ad2339204ddda1f0f12f0d877cae68801fbac5 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Thu, 28 Jul 2022 08:23:17 +0200 Subject: [PATCH 050/106] better fonts --- apps/gipy/ChangeLog | 2 ++ apps/gipy/app.js | 53 +++++++++++++++++++++------------------------ 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 10a532e30..88d489161 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -43,3 +43,5 @@ * Bugfix in speed computation. * Bugfix in current segment detection. * Bugfix : lost direction. + * Larger fonts. + * Detecting next point correctly when going back. diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 6b8cb0b9d..c75944ce4 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,4 +1,4 @@ -let simulated = false; +let simulated = true; let file_version = 3; let code_key = 47490; @@ -90,23 +90,27 @@ class Status { this.position = new_position; // detect segment we are on now - let next_segment = this.path.nearest_segment( + let res = this.path.nearest_segment( this.position, Math.max(0, this.current_segment - 1), Math.min(this.current_segment + 2, this.path.len - 1), cos_direction, sin_direction ); + let orientation = res[0]; + let next_segment = res[1]; if (this.is_lost(next_segment)) { // it did not work, try anywhere - next_segment = this.path.nearest_segment( + res = this.path.nearest_segment( this.position, 0, this.path.len - 1, cos_direction, sin_direction ); + orientation = res[0]; + next_segment = res[1]; } // now check if we strayed away from path or back to it let lost = this.is_lost(next_segment); @@ -122,13 +126,13 @@ class Status { this.current_segment = next_segment; // check if we are nearing the next point on our path and alert the user - let next_point = this.current_segment + 1; + let next_point = this.current_segment + (1 - orientation); this.distance_to_next_point = Math.ceil( this.position.distance(this.path.point(next_point)) ); // disable gps when far from next point and locked if (Bangle.isLocked()) { - let time_to_next_point = this.distance_to_next_point / 9.7; // 30km/h is 8.3 m/s + let time_to_next_point = this.distance_to_next_point / 9.7; // 35km/h is 9.7 m/s if (time_to_next_point > 30) { Bangle.setGPSPower(false, "gipy"); setTimeout(function () { @@ -136,7 +140,7 @@ class Status { }, time_to_next_point); } } - if (this.reaching != next_point && this.distance_to_next_point <= 20) { + if (this.reaching != next_point && this.distance_to_next_point <= 50) { this.reaching = next_point; let reaching_waypoint = this.path.is_waypoint(next_point); if (reaching_waypoint) { @@ -212,39 +216,32 @@ class Status { } let hours = now.getHours().toString(); g.setFont("6x8:2") - .setFontAlign(1, -1, 0) + .setFontAlign(-1, -1, 0) .setColor(g.theme.fg) - .drawString(hours + ":" + minutes, g.getWidth(), g.getHeight() - 15); + .drawString(hours + ":" + minutes, 0, 30); let done_distance = this.remaining_distances[0] - remaining_distance; - let done_in = getTime() - this.starting_time; + let done_in = now.getTime() / 1000 - this.starting_time; let approximate_speed = Math.round((done_distance * 3.6) / done_in); - g.setFont("6x15") - .setFontAlign(-1, -1, 0) - .drawString("s." + approximate_speed + "km/h", 0, g.getHeight() - 49); - - g.setFont("6x15").drawString( - "d. " + rounded_distance + "/" + total, + g.setFont("6x8:2").drawString( + "" + this.distance_to_next_point + "m", 0, - g.getHeight() - 32 + g.getHeight() - 49 ); - g.drawString( - "seg." + - (this.current_segment + 1) + - "/" + - (this.path.len - 1) + - " " + - this.distance_to_next_point + - "m", + g.setFont("6x8:2") + .setFontAlign(-1, -1, 0) + .drawString("" + approximate_speed + "km/h", 0, g.getHeight() - 32); + g.setFont("6x8:2").drawString( + "" + rounded_distance + "/" + total, 0, g.getHeight() - 15 ); - if (this.distance_to_next_point <= 20) { + if (this.distance_to_next_point <= 50) { if (this.path.is_waypoint(this.reaching)) { g.setColor(0.0, 1.0, 0.0) .setFont("6x15") - .drawString("turn", g.getWidth() - 55, 35); + .drawString("turn", g.getWidth() - 50, 30); } } if (!this.on_path) { @@ -469,9 +466,9 @@ class Path { // by default correct orientation (0) wins // but if other one is really closer, return other one if (mins[1] < mins[0] / 10.0) { - return indices[1]; + return [1, indices[1]]; } else { - return indices[0]; + return [0, indices[0]]; } } get len() { From 9ccc958d1f577b85c10091abfc7bb3d5c2b15492 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Thu, 28 Jul 2022 08:38:04 +0200 Subject: [PATCH 051/106] simulated=false --- apps/gipy/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index c75944ce4..601e87c84 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -1,4 +1,4 @@ -let simulated = true; +let simulated = false; let file_version = 3; let code_key = 47490; From 038ec3ef3fdf8e5bb37b07e0960467075cc777a2 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 14 Aug 2022 15:36:55 +0200 Subject: [PATCH 052/106] fix for lost direction - fix in display for lost direction - we can now debug current segment's point projection - tried loading gpx files but readLine does not work --- apps/gipy/app.js | 142 ++++++++++++++++++++++++++++++----------------- 1 file changed, 90 insertions(+), 52 deletions(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 601e87c84..a0eae054c 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -169,6 +169,7 @@ class Status { display() { g.clear(); this.display_map(); + this.display_interest_points(); this.display_stats(); Bangle.drawWidgets(); @@ -326,65 +327,98 @@ class Status { // g.fillCircle(x, y, 3); // } + // display current-segment's projection for debug + // let projection = pos.closest_segment_point( + // this.path.point(this.current_segment), + // this.path.point(this.current_segment + 1) + // ); + + // let tx = (projection.lon - cx) * 40000.0; + // let ty = (projection.lat - cy) * 40000.0; + // let rotated_x = tx * cos - ty * sin; + // let rotated_y = tx * sin + ty * cos; + // let x = half_width - Math.round(rotated_x); // x is inverted + // let y = half_height + Math.round(rotated_y); + // g.setColor(g.theme.fg); + // g.fillCircle(x, y, 4); + // display direction to next point if lost if (!this.on_path) { let next_point = this.path.point(this.current_segment + 1); let diff = next_point.minus(this.position); let angle = Math.atan2(diff.lat, diff.lon); - let x = Math.cos(-angle - Math.PI / 2) * 50.0 + half_width; - let y = Math.sin(-angle - Math.PI / 2) * 50.0 + half_height; + let tx = Math.cos(angle) * 50.0; + let ty = Math.sin(angle) * 50.0; + let rotated_x = tx * cos - ty * sin; + let rotated_y = tx * sin + ty * cos; + let x = half_width - Math.round(rotated_x); // x is inverted + let y = half_height + Math.round(rotated_y); g.setColor(g.theme.fgH).drawLine(half_width, half_height, x, y); } } } +function load_gpc(filename) { + let buffer = require("Storage").readArrayBuffer(filename); + let offset = 0; + + // header + let header = Uint16Array(buffer, offset, 5); + offset += 5 * 2; + let key = header[0]; + let version = header[1]; + let points_number = header[2]; + if (key != code_key || version > file_version) { + E.showMessage("Invalid gpc file"); + load(); + } + + // path points + let points = Float64Array(buffer, offset, points_number * 2); + offset += 8 * points_number * 2; + + // path waypoints + let waypoints_len = Math.ceil(points_number / 8.0); + let waypoints = Uint8Array(buffer, offset, waypoints_len); + offset += waypoints_len; + + // interest points + let interests_number = header[3]; + let interests_coordinates = Float64Array( + buffer, + offset, + interests_number * 2 + ); + offset += 8 * interests_number * 2; + let interests_types = Uint8Array(buffer, offset, interests_number); + offset += interests_number; + + // interests on path + let interests_on_path_number = header[4]; + let interests_on_path = Uint16Array(buffer, offset, interests_on_path_number); + offset += 2 * interests_on_path_number; + let starts_length = Math.ceil(interests_on_path_number / 5.0); + let interests_starts = Uint16Array(buffer, offset, starts_length); + offset += 2 * starts_length; + + return [ + points, + waypoints, + interests_coordinates, + interests_types, + interests_on_path, + interests_starts, + ]; +} + class Path { - constructor(filename) { - let buffer = require("Storage").readArrayBuffer(filename); - let offset = 0; - - // header - let header = Uint16Array(buffer, offset, 5); - offset += 5 * 2; - let key = header[0]; - let version = header[1]; - let points_number = header[2]; - if (key != code_key || version > file_version) { - E.showMessage("Invalid gpc file"); - load(); - } - - // path points - this.points = Float64Array(buffer, offset, points_number * 2); - offset += 8 * points_number * 2; - - // path waypoints - let waypoints_len = Math.ceil(points_number / 8.0); - this.waypoints = Uint8Array(buffer, offset, waypoints_len); - offset += waypoints_len; - - // interest points - let interests_number = header[3]; - this.interests_coordinates = Float64Array( - buffer, - offset, - interests_number * 2 - ); - offset += 8 * interests_number * 2; - this.interests_types = Uint8Array(buffer, offset, interests_number); - offset += interests_number; - - // interests on path - let interests_on_path_number = header[4]; - this.interests_on_path = Uint16Array( - buffer, - offset, - interests_on_path_number - ); - offset += 2 * interests_on_path_number; - let starts_length = Math.ceil(interests_on_path_number / 5.0); - this.interests_starts = Uint16Array(buffer, offset, starts_length); - offset += 2 * starts_length; + constructor(arrays) { + this.points = arrays[0]; + this.waypoints = arrays[1]; + this.interests_coordinates = arrays[2]; + this.interests_types = arrays[3]; + this.interests_on_path = arrays[4]; + this.interests_starts = arrays[5]; } is_waypoint(point_index) { @@ -566,14 +600,17 @@ function simulate_gps(status) { if (point_index >= status.path.len) { return; } - let p1 = status.path.point(point_index); - let p2 = status.path.point(point_index + 1); + let p1 = status.path.point(0); + let n = status.path.len; + let p2 = status.path.point(n - 1); + //let p1 = status.path.point(point_index); + //let p2 = status.path.point(point_index + 1); let alpha = fake_gps_point - point_index; let pos = p1.times(1 - alpha).plus(p2.times(alpha)); let old_pos = status.position; - fake_gps_point += 0.05; // advance simulation + fake_gps_point += 0.005; // advance simulation status.update_position(pos, null); } @@ -595,7 +632,8 @@ function start(fn) { E.showMenu(); console.log("loading", fn); - let path = new Path(fn); + // let path = new Path(load_gpx("test.gpx")); + let path = new Path(load_gpc(fn)); let status = new Status(path); if (simulated) { From c1276ef64e5dd4a94baf06c06e6310f16dde5bb1 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 14 Aug 2022 15:58:03 +0200 Subject: [PATCH 053/106] display projection to debug --- apps/gipy/ChangeLog | 3 +++ apps/gipy/app.js | 24 ++++++++++++------------ apps/gipy/metadata.json | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 88d489161..2d03a25ba 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -45,3 +45,6 @@ * Bugfix : lost direction. * Larger fonts. * Detecting next point correctly when going back. + +0.13: + * Bugfix in lost direction. diff --git a/apps/gipy/app.js b/apps/gipy/app.js index a0eae054c..63b22598b 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -328,19 +328,19 @@ class Status { // } // display current-segment's projection for debug - // let projection = pos.closest_segment_point( - // this.path.point(this.current_segment), - // this.path.point(this.current_segment + 1) - // ); + let projection = pos.closest_segment_point( + this.path.point(this.current_segment), + this.path.point(this.current_segment + 1) + ); - // let tx = (projection.lon - cx) * 40000.0; - // let ty = (projection.lat - cy) * 40000.0; - // let rotated_x = tx * cos - ty * sin; - // let rotated_y = tx * sin + ty * cos; - // let x = half_width - Math.round(rotated_x); // x is inverted - // let y = half_height + Math.round(rotated_y); - // g.setColor(g.theme.fg); - // g.fillCircle(x, y, 4); + let tx = (projection.lon - cx) * 40000.0; + let ty = (projection.lat - cy) * 40000.0; + let rotated_x = tx * cos - ty * sin; + let rotated_y = tx * sin + ty * cos; + let x = half_width - Math.round(rotated_x); // x is inverted + let y = half_height + Math.round(rotated_y); + g.setColor(g.theme.fg); + g.fillCircle(x, y, 4); // display direction to next point if lost if (!this.on_path) { diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 2c31b1317..0a750559e 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.12", + "version": "0.13", "description": "Follow gpx files", "allow_emulator":false, "icon": "gipy.png", From 87441bad9b44c08505a938aefe71b4aec1ee0fa7 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 15 Aug 2022 08:08:54 +0200 Subject: [PATCH 054/106] minor tweaks --- apps/gipy/ChangeLog | 2 ++ apps/gipy/TODO | 3 ++- apps/gipy/app.js | 8 ++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 2d03a25ba..679f7aefd 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -48,3 +48,5 @@ 0.13: * Bugfix in lost direction. + * Trying buzzing on all turns (when locked only). + * Buzzing 100m ahead instead of 50m. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 203105909..a4c797bae 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -1,7 +1,8 @@ * bugs -- it does not buzz very often on turns +- when exactly on turn, distance to next point is still often 50m + -----> it does not buzz very often on turns * additional features diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 63b22598b..d670a011a 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -133,18 +133,18 @@ class Status { // disable gps when far from next point and locked if (Bangle.isLocked()) { let time_to_next_point = this.distance_to_next_point / 9.7; // 35km/h is 9.7 m/s - if (time_to_next_point > 30) { + if (time_to_next_point > 60) { Bangle.setGPSPower(false, "gipy"); setTimeout(function () { Bangle.setGPSPower(true, "gipy"); }, time_to_next_point); } } - if (this.reaching != next_point && this.distance_to_next_point <= 50) { + if (this.reaching != next_point && this.distance_to_next_point <= 100) { this.reaching = next_point; let reaching_waypoint = this.path.is_waypoint(next_point); + Bangle.buzz(); if (reaching_waypoint) { - Bangle.buzz(); if (Bangle.isLocked()) { Bangle.setLocked(false); } @@ -238,7 +238,7 @@ class Status { g.getHeight() - 15 ); - if (this.distance_to_next_point <= 50) { + if (this.distance_to_next_point <= 100) { if (this.path.is_waypoint(this.reaching)) { g.setColor(0.0, 1.0, 0.0) .setFont("6x15") From 80aba894e715042b84245bb3644fba4fedbd008d Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 22 Aug 2022 17:14:23 +0200 Subject: [PATCH 055/106] sharp turns detection --- apps/gipy/gpconv/src/main.rs | 115 +++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 51 deletions(-) diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index e85c358bc..716887264 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -59,7 +59,7 @@ impl Point { } } -fn points(filename: &str) -> Vec> { +fn points(filename: &str) -> (HashSet, Vec) { let file = File::open(filename).unwrap(); let reader = BufReader::new(file); @@ -67,9 +67,9 @@ fn points(filename: &str) -> Vec> { let mut gpx: Gpx = read(reader).unwrap(); eprintln!("we have {} tracks", gpx.tracks.len()); - let mut segments = Vec::new(); - let mut current_segment = Vec::new(); - for (p, stop) in gpx + let mut waypoints = HashSet::new(); + + let points = gpx .tracks .pop() .unwrap() @@ -79,21 +79,14 @@ fn points(filename: &str) -> Vec> { .map(|p| { let is_commented = p.comment.is_some(); let (x, y) = p.point().x_y(); - (Point { x, y }, is_commented) - }) - { - current_segment.push(p); - if stop { - if current_segment.len() > 1 { - segments.push(current_segment); - current_segment = vec![p]; + let p = Point { x, y }; + if is_commented { + waypoints.insert(p); } - } - } - if current_segment.len() > 1 { - segments.push(current_segment); - } - segments + p + }) + .collect::>(); + (waypoints, points) } // // NOTE: this angles idea could maybe be use to get dp from n^3 to n^2 @@ -495,7 +488,7 @@ fn hybrid_simplification(points: &[Point], epsilon: f64) -> Vec { fn save_path(writer: &mut W, p: &[Point], stroke: &str) -> std::io::Result<()> { write!( writer, - ", I: IntoIterator>( waypoints.iter().try_for_each(|p| { writeln!( &mut writer, - "", + "", p.x, p.y, ) })?; @@ -620,6 +613,38 @@ fn position_interests_along_path( buckets } +fn detect_sharp_turns(path: &[Point], waypoints: &mut HashSet) { + path.iter() + .tuple_windows() + .map(|(a, b, c)| { + let xd1 = b.x - a.x; + let yd1 = b.y - a.y; + let angle1 = yd1.atan2(xd1); + + let xd2 = c.x - b.x; + let yd2 = c.y - b.y; + let angle2 = yd2.atan2(xd2); + let adiff = angle2 - angle1; + let adiff = if adiff < 0.0 { + adiff + std::f64::consts::PI * 2.0 + } else { + adiff + }; + (adiff % std::f64::consts::PI, b) + }) + .filter_map(|(adiff, b)| { + let allowed = 4.0f64; + if adiff > (90.0 - allowed).to_radians() && adiff < (90.0 + allowed).to_radians() { + Some(b) + } else { + None + } + }) + .for_each(|b| { + waypoints.insert(*b); + }); +} + #[tokio::main] async fn main() { let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); @@ -632,46 +657,34 @@ async fn main() { }; println!("input is {}", input_file); - let p = points(&input_file); + let (mut waypoints, p) = points(&input_file); - let mut waypoints; - let mut rp; - if p.len() == 1 { - // we don't have any waypoint information - println!("no waypoint information"); - println!("initially we had {} points", p[0].len()); - rp = simplify_path(&p[0], 0.00015); - println!("we now have {} points", rp.len()); + detect_sharp_turns(&p, &mut waypoints); + waypoints.insert(p.first().copied().unwrap()); + waypoints.insert(p.last().copied().unwrap()); + println!("we have {} waypoints", waypoints.len()); - waypoints = HashSet::new(); - waypoints.insert(rp.first().copied().unwrap()); - waypoints.insert(rp.last().copied().unwrap()); - eprintln!("we found {} waypoints", waypoints.len()); - } else { - println!("we have {} waypoints", p.len() + 1); - println!( - "initially we had {} points", - p.iter().map(|s| s.len()).sum::() - (p.len() - 1) - ); - waypoints = HashSet::new(); - rp = Vec::new(); - let mut last = None; - for segment in &p { - waypoints.insert(segment.first().copied().unwrap()); - waypoints.insert(segment.last().copied().unwrap()); - let mut s = simplify_path(segment, 0.00015); - rp.append(&mut s); - last = rp.pop(); + println!("initially we had {} points", p.len()); + + let mut rp = Vec::new(); + let mut segment = Vec::new(); + for point in &p { + segment.push(*point); + if waypoints.contains(point) { + if segment.len() >= 2 { + let mut s = simplify_path(&segment, 0.00015); + rp.append(&mut s); + segment = rp.pop().into_iter().collect(); + } } - rp.extend(last); - println!("we now have {} points", rp.len()); } + rp.append(&mut segment); + println!("we now have {} points", rp.len()); // let mut interests = parse_osm_data("isere.osm.pbf"); let buckets = position_interests_along_path(&mut interests, &rp, 0.001, 5, 3); // let i = get_openstreetmap_data(&rp).await; // let i = HashSet::new(); - let p = p.into_iter().flatten().collect::>(); save_svg( "test.svg", &p, From ddea6250243d5cb70bf8ec9dd545ab7d7dc73faf Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 23 Aug 2022 07:40:50 +0200 Subject: [PATCH 056/106] todo --- apps/gipy/TODO | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/gipy/TODO b/apps/gipy/TODO index a4c797bae..34d96b568 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -4,8 +4,16 @@ - when exactly on turn, distance to next point is still often 50m -----> it does not buzz very often on turns +- when going backwards we have a tendencing to get a wrong current_segment + * additional features +- average speed becomes invalid if you stop and restart + - pause when not moving + - figure starting point + +- we need to buzz 200m before sharp turns (or even better, 30seconds) + - display distance to next water/toilet ? - dynamic map rescale - display scale (100m) From 5147008f00eada0b11ae1aaa537b90c4a1ec5cb1 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 23 Aug 2022 07:57:10 +0200 Subject: [PATCH 057/106] minor changes --- apps/gipy/ChangeLog | 2 +- apps/gipy/TODO | 1 + apps/gipy/app.js | 7 ++++++- apps/gipy/gpconv/src/main.rs | 14 +++++++++++--- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 679f7aefd..410cf992a 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -48,5 +48,5 @@ 0.13: * Bugfix in lost direction. - * Trying buzzing on all turns (when locked only). * Buzzing 100m ahead instead of 50m. + * Detect sharp turns. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 34d96b568..3681f4db0 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -13,6 +13,7 @@ - figure starting point - we need to buzz 200m before sharp turns (or even better, 30seconds) +(and look at more than next point) - display distance to next water/toilet ? - dynamic map rescale diff --git a/apps/gipy/app.js b/apps/gipy/app.js index d670a011a..11b2220f3 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -119,6 +119,8 @@ class Status { if (lost) { Bangle.buzz(); // we lost path setTimeout(() => Bangle.buzz(), 500); + setTimeout(() => Bangle.buzz(), 1000); + setTimeout(() => Bangle.buzz(), 1500); } this.on_path = !lost; } @@ -143,8 +145,11 @@ class Status { if (this.reaching != next_point && this.distance_to_next_point <= 100) { this.reaching = next_point; let reaching_waypoint = this.path.is_waypoint(next_point); - Bangle.buzz(); if (reaching_waypoint) { + Bangle.buzz(); + setTimeout(() => Bangle.buzz(), 500); + setTimeout(() => Bangle.buzz(), 1000); + setTimeout(() => Bangle.buzz(), 1500); if (Bangle.isLocked()) { Bangle.setLocked(false); } diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index 716887264..a0df30327 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -11,6 +11,9 @@ use gpx::Gpx; mod osm; use osm::{parse_osm_data, InterestPoint}; +const LOWER_SHARP_TURN: f64 = 45.0 * std::f64::consts::PI / 180.0; +const UPPER_SHARP_TURN: f64 = std::f64::consts::PI * 2.0 - LOWER_SHARP_TURN; + const KEY: u16 = 47490; const FILE_VERSION: u16 = 3; @@ -633,8 +636,7 @@ fn detect_sharp_turns(path: &[Point], waypoints: &mut HashSet) { (adiff % std::f64::consts::PI, b) }) .filter_map(|(adiff, b)| { - let allowed = 4.0f64; - if adiff > (90.0 - allowed).to_radians() && adiff < (90.0 + allowed).to_radians() { + if adiff > LOWER_SHARP_TURN && adiff < UPPER_SHARP_TURN { Some(b) } else { None @@ -694,5 +696,11 @@ async fn main() { ) .unwrap(); - save_gpc("test.gpc", &rp, &waypoints, &buckets).unwrap(); + save_gpc( + Path::new(&input_file).with_extension("gpc"), + &rp, + &waypoints, + &buckets, + ) + .unwrap(); } From f533887975d313772053b759f1b42762f84cecd6 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 23 Aug 2022 11:19:19 +0200 Subject: [PATCH 058/106] bugfix in sharp turns detect --- apps/gipy/gpconv/Cargo.lock | 863 +---------------------------------- apps/gipy/gpconv/Cargo.toml | 2 - apps/gipy/gpconv/src/main.rs | 7 +- apps/gipy/gpconv/src/osm.rs | 63 --- 4 files changed, 4 insertions(+), 931 deletions(-) diff --git a/apps/gipy/gpconv/Cargo.lock b/apps/gipy/gpconv/Cargo.lock index 50bc76cb4..c280558cd 100644 --- a/apps/gipy/gpconv/Cargo.lock +++ b/apps/gipy/gpconv/Cargo.lock @@ -61,36 +61,18 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base64" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" - [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bumpalo" -version = "3.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" - [[package]] name = "byteorder" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" -[[package]] -name = "bytes" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" - [[package]] name = "bzip2" version = "0.4.3" @@ -137,22 +119,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" - [[package]] name = "crc32fast" version = "1.3.2" @@ -234,15 +200,6 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" -[[package]] -name = "encoding_rs" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" -dependencies = [ - "cfg-if", -] - [[package]] name = "error-chain" version = "0.12.4" @@ -265,15 +222,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" -[[package]] -name = "fastrand" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" -dependencies = [ - "instant", -] - [[package]] name = "flate2" version = "1.0.24" @@ -290,70 +238,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" -dependencies = [ - "matches", - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" - -[[package]] -name = "futures-sink" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" - -[[package]] -name = "futures-task" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" - -[[package]] -name = "futures-util" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "pin-utils", -] - [[package]] name = "geo-types" version = "0.7.6" @@ -387,9 +271,7 @@ dependencies = [ "gpx", "itertools", "lazy_static", - "openstreetmap-api", "osmio", - "tokio", ] [[package]] @@ -406,25 +288,6 @@ dependencies = [ "xml-rs", ] -[[package]] -name = "h2" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "hashbrown" version = "0.11.2" @@ -434,99 +297,13 @@ dependencies = [ "ahash", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashlink" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" dependencies = [ - "hashbrown 0.11.2", -] - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "http" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" - -[[package]] -name = "httpdate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" - -[[package]] -name = "hyper" -version = "0.14.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper", - "native-tls", - "tokio", - "tokio-native-tls", + "hashbrown", ] [[package]] @@ -535,42 +312,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indexmap" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "ipnet" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" - [[package]] name = "iter-progress" version = "0.8.0" @@ -592,15 +333,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" -[[package]] -name = "js-sys" -version = "0.3.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" -dependencies = [ - "wasm-bindgen", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -623,43 +355,12 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "lock_api" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" -[[package]] -name = "mime" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" - [[package]] name = "miniz_oxide" version = "0.5.3" @@ -669,36 +370,6 @@ dependencies = [ "adler", ] -[[package]] -name = "mio" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", -] - -[[package]] -name = "native-tls" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "num-integer" version = "0.1.45" @@ -718,16 +389,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_threads" version = "0.1.6" @@ -752,67 +413,6 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" -[[package]] -name = "openssl" -version = "0.10.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" -dependencies = [ - "autocfg", - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "openstreetmap-api" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0964095cdc40d448c6291d079dc98cc5d094c92af5b5fbfcda24a4c9637fc6" -dependencies = [ - "log", - "quick-xml", - "reqwest", - "serde", - "serde_derive", - "serde_urlencoded", - "url", - "urlencoding", -] - [[package]] name = "osmio" version = "0.7.0" @@ -835,47 +435,6 @@ dependencies = [ "xml-rs", ] -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-sys", -] - -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" version = "0.3.25" @@ -904,7 +463,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" dependencies = [ "memchr", - "serde", ] [[package]] @@ -916,61 +474,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "redox_syscall" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" -dependencies = [ - "bitflags", -] - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - -[[package]] -name = "reqwest" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-tls", - "ipnet", - "js-sys", - "lazy_static", - "log", - "mime", - "native-tls", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - [[package]] name = "rusqlite" version = "0.25.4" @@ -998,45 +501,6 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" -[[package]] -name = "schannel" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" -dependencies = [ - "lazy_static", - "windows-sys", -] - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "security-framework" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "separator" version = "0.4.1" @@ -1074,49 +538,12 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" -dependencies = [ - "libc", -] - -[[package]] -name = "slab" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" - [[package]] name = "smallvec" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" -[[package]] -name = "socket2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "strsim" version = "0.10.0" @@ -1134,20 +561,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tempfile" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" -dependencies = [ - "cfg-if", - "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", -] - [[package]] name = "thiserror" version = "1.0.31" @@ -1190,148 +603,12 @@ dependencies = [ "num_threads", ] -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" - -[[package]] -name = "tokio" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57aec3cfa4c296db7255446efb4928a6be304b431a806216105542a67b6ca82e" -dependencies = [ - "autocfg", - "bytes", - "libc", - "memchr", - "mio", - "num_cpus", - "once_cell", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "winapi", -] - -[[package]] -name = "tokio-macros" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" -dependencies = [ - "cfg-if", - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" -dependencies = [ - "once_cell", -] - -[[package]] -name = "try-lock" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" - -[[package]] -name = "unicode-bidi" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" - [[package]] name = "unicode-ident" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" -[[package]] -name = "unicode-normalization" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "url" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" -dependencies = [ - "form_urlencoded", - "idna", - "matches", - "percent-encoding", -] - -[[package]] -name = "urlencoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821" - [[package]] name = "vcpkg" version = "0.2.15" @@ -1344,16 +621,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "want" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" -dependencies = [ - "log", - "try-lock", -] - [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -1366,82 +633,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wasm-bindgen" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" -dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" - -[[package]] -name = "web-sys" -version = "0.3.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "winapi" version = "0.3.9" @@ -1464,58 +655,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - -[[package]] -name = "windows_i686_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" - -[[package]] -name = "windows_i686_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" - -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - [[package]] name = "xml-rs" version = "0.8.4" diff --git a/apps/gipy/gpconv/Cargo.toml b/apps/gipy/gpconv/Cargo.toml index 681c86c7e..735eaa6ed 100644 --- a/apps/gipy/gpconv/Cargo.toml +++ b/apps/gipy/gpconv/Cargo.toml @@ -8,7 +8,5 @@ edition = "2021" [dependencies] gpx="*" itertools="*" -openstreetmap-api="*" -tokio={version="1", features=["full"]} lazy_static="*" osmio="*" diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index a0df30327..9b7a19193 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -11,7 +11,7 @@ use gpx::Gpx; mod osm; use osm::{parse_osm_data, InterestPoint}; -const LOWER_SHARP_TURN: f64 = 45.0 * std::f64::consts::PI / 180.0; +const LOWER_SHARP_TURN: f64 = 80.0 * std::f64::consts::PI / 180.0; const UPPER_SHARP_TURN: f64 = std::f64::consts::PI * 2.0 - LOWER_SHARP_TURN; const KEY: u16 = 47490; @@ -633,7 +633,7 @@ fn detect_sharp_turns(path: &[Point], waypoints: &mut HashSet) { } else { adiff }; - (adiff % std::f64::consts::PI, b) + (adiff, b) }) .filter_map(|(adiff, b)| { if adiff > LOWER_SHARP_TURN && adiff < UPPER_SHARP_TURN { @@ -647,8 +647,7 @@ fn detect_sharp_turns(path: &[Point], waypoints: &mut HashSet) { }); } -#[tokio::main] -async fn main() { +fn main() { let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); let osm_file = std::env::args().nth(2); diff --git a/apps/gipy/gpconv/src/osm.rs b/apps/gipy/gpconv/src/osm.rs index 596febb14..4877165a5 100644 --- a/apps/gipy/gpconv/src/osm.rs +++ b/apps/gipy/gpconv/src/osm.rs @@ -1,10 +1,6 @@ use super::Point; use itertools::Itertools; use lazy_static::lazy_static; -use openstreetmap_api::{ - types::{BoundingBox, Credentials}, - Openstreetmap, -}; use osmio::OSMObjBase; use osmio::{prelude::*, ObjId}; use std::collections::{HashMap, HashSet}; @@ -91,65 +87,6 @@ impl InterestPoint { } } -async fn get_openstreetmap_data(points: &[(f64, f64)]) -> HashSet { - let osm = Openstreetmap::new("https://openstreetmap.org", Credentials::None); - let mut interest_points = HashSet::new(); - let border = 0.0001; - let mut boxes = Vec::new(); - let max_size = 0.005; - points.iter().fold( - (std::f64::MAX, std::f64::MIN, std::f64::MAX, std::f64::MIN), - |in_box, &(x, y)| { - let (mut xmin, mut xmax, mut ymin, mut ymax) = in_box; - xmin = xmin.min(x); - xmax = xmax.max(x); - ymin = ymin.min(y); - ymax = ymax.max(y); - if (xmax - xmin > max_size) || (ymax - ymin > max_size) { - boxes.push(in_box); - (x, x, y, y) - } else { - (xmin, xmax, ymin, ymax) - } - }, - ); - eprintln!("we need {} requests to openstreetmap", boxes.len()); - for (xmin, xmax, ymin, ymax) in boxes { - let left = xmin - border; - let right = xmax + border; - let bottom = ymin - border; - let top = ymax + border; - match osm - .map(&BoundingBox { - bottom, - left, - top, - right, - }) - .await - { - Ok(map) => { - let points = map.nodes.iter().flat_map(|n| { - n.tags.iter().filter_map(|t| { - let latlon = n.lat.and_then(|lat| n.lon.map(|lon| (lat, lon))); - latlon.and_then(|(lat, lon)| { - Interest::new(&t.k, &t.v).map(|i| InterestPoint { - point: Point { x: lon, y: lat }, - interest: i, - }) - }) - }) - }); - interest_points.extend(points) - } - Err(e) => { - eprintln!("failed retrieving osm data: {:?}", e); - } - } - } - interest_points -} - pub fn parse_osm_data>(path: P) -> Vec { let reader = osmio::read_pbf(path).ok(); reader From 81118092f08559b7e01dd395ac530981193be04d Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 26 Aug 2022 13:24:53 +0200 Subject: [PATCH 059/106] instant speed --- apps/gipy/ChangeLog | 1 + apps/gipy/TODO | 3 +++ apps/gipy/app.js | 35 ++++++++++++++++++++++++++--------- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 410cf992a..71ed3dc2e 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -50,3 +50,4 @@ * Bugfix in lost direction. * Buzzing 100m ahead instead of 50m. * Detect sharp turns. + * Display instant speed. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 3681f4db0..a72d1a87e 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -8,6 +8,9 @@ * additional features +- config screen + - are we on foot (and should use compass) + - disable gps off - average speed becomes invalid if you stop and restart - pause when not moving - figure starting point diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 11b2220f3..82bd92cf6 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -46,19 +46,24 @@ class Status { this.remaining_distances = r; // how much distance remains at start of each segment this.starting_time = getTime(); this.old_points = []; + this.old_times = []; } new_position_reached(position) { + let now = getTime(); // we try to figure out direction by looking at previous points // instead of the gps course which is not very nice. if (this.old_points.length == 0) { this.old_points.push(position); + this.old_times.push(now); } else { let last_point = this.old_points[this.old_points.length - 1]; if (last_point.lon != position.lon || last_point.lat != position.lat) { if (this.old_points.length == 4) { this.old_points.shift(); + this.old_times.shift(); } this.old_points.push(position); + this.old_times.push(now); } else { return null; } @@ -226,21 +231,33 @@ class Status { .setColor(g.theme.fg) .drawString(hours + ":" + minutes, 0, 30); + let point_time = this.old_times[this.old_times.length - 1]; let done_distance = this.remaining_distances[0] - remaining_distance; - let done_in = now.getTime() / 1000 - this.starting_time; + let done_in = point_time - this.starting_time; let approximate_speed = Math.round((done_distance * 3.6) / done_in); g.setFont("6x8:2").drawString( "" + this.distance_to_next_point + "m", 0, g.getHeight() - 49 ); + + let instant_speed = + this.old_points[0].distance(this.old_points[this.old_points.length - 1]) / + (point_time - this.old_times[0]); + let approximate_instant_speed = Math.round(instant_speed * 3.6); + g.setFont("6x8:2") .setFontAlign(-1, -1, 0) - .drawString("" + approximate_speed + "km/h", 0, g.getHeight() - 32); + .drawString( + "" + approximate_speed + "km/h (in." + approximate_instant_speed + ")", + 0, + g.getHeight() - 15 + ); + g.setFont("6x8:2").drawString( "" + rounded_distance + "/" + total, 0, - g.getHeight() - 15 + g.getHeight() - 32 ); if (this.distance_to_next_point <= 100) { @@ -605,17 +622,17 @@ function simulate_gps(status) { if (point_index >= status.path.len) { return; } - let p1 = status.path.point(0); - let n = status.path.len; - let p2 = status.path.point(n - 1); - //let p1 = status.path.point(point_index); - //let p2 = status.path.point(point_index + 1); + //let p1 = status.path.point(0); + //let n = status.path.len; + //let p2 = status.path.point(n - 1); + let p1 = status.path.point(point_index); + let p2 = status.path.point(point_index + 1); let alpha = fake_gps_point - point_index; let pos = p1.times(1 - alpha).plus(p2.times(alpha)); let old_pos = status.position; - fake_gps_point += 0.005; // advance simulation + fake_gps_point += 0.05; // advance simulation status.update_position(pos, null); } From 37379720d27467d500d52fdcc4c29c44e363c3e8 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 26 Aug 2022 14:25:35 +0200 Subject: [PATCH 060/106] getting reading for detecting stops --- apps/gipy/app.js | 51 +++++++++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 82bd92cf6..074400782 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -45,38 +45,38 @@ class Status { } this.remaining_distances = r; // how much distance remains at start of each segment this.starting_time = getTime(); + this.stopped_at = this.starting_time; // we need to now how long we stop in order to compute real avg speed this.old_points = []; this.old_times = []; } new_position_reached(position) { - let now = getTime(); // we try to figure out direction by looking at previous points // instead of the gps course which is not very nice. - if (this.old_points.length == 0) { - this.old_points.push(position); - this.old_times.push(now); - } else { - let last_point = this.old_points[this.old_points.length - 1]; - if (last_point.lon != position.lon || last_point.lat != position.lat) { - if (this.old_points.length == 4) { - this.old_points.shift(); - this.old_times.shift(); - } - this.old_points.push(position); - this.old_times.push(now); - } else { - return null; - } - } + let now = getTime(); + this.old_points.push(position); + this.old_times.push(now); + if (this.old_points.length == 1) { return null; - } else { - // let's just take angle of segment between oldest and newest point - let oldest = this.old_points[0]; - let diff = position.minus(oldest); - let angle = Math.atan2(diff.lat, diff.lon); - return angle; } + + let last_point = this.old_points[this.old_points.length - 1]; + let oldest = this.old_points[0]; + this.instant_speed = + oldest_point.distance(last_point) / (now - this.old_times[0]); + + if (this.old_points.length == 8) { + this.old_points.shift(); + this.old_times.shift(); + } + // let's just take angle of segment between newest point and a point a bit before + let previous_index = this.old_points.length - 3; + if (previous_index < 0) { + previous_index = 0; + } + let diff = position.minus(this.old_points[previous_index]); + let angle = Math.atan2(diff.lat, diff.lon); + return angle; } update_position(new_position, maybe_direction) { let direction = this.new_position_reached(new_position); @@ -241,10 +241,7 @@ class Status { g.getHeight() - 49 ); - let instant_speed = - this.old_points[0].distance(this.old_points[this.old_points.length - 1]) / - (point_time - this.old_times[0]); - let approximate_instant_speed = Math.round(instant_speed * 3.6); + let approximate_instant_speed = Math.round(this.instant_speed * 3.6); g.setFont("6x8:2") .setFontAlign(-1, -1, 0) From bf937837de57d77e2c8a107a5cd922fb08c23822 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 29 Aug 2022 15:20:04 +0200 Subject: [PATCH 061/106] quickfix --- apps/gipy/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 074400782..f1064d7a3 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -61,7 +61,7 @@ class Status { } let last_point = this.old_points[this.old_points.length - 1]; - let oldest = this.old_points[0]; + let oldest_point = this.old_points[0]; this.instant_speed = oldest_point.distance(last_point) / (now - this.old_times[0]); From c0c6e5c2bdb3078eb04a52908ddf41b0b5033207 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 13 Sep 2022 17:19:42 +0200 Subject: [PATCH 062/106] new instant speed --- apps/gipy/ChangeLog | 1 + apps/gipy/app.js | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 71ed3dc2e..8684daf17 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -51,3 +51,4 @@ * Buzzing 100m ahead instead of 50m. * Detect sharp turns. * Display instant speed. + * New instant speed algorithm. diff --git a/apps/gipy/app.js b/apps/gipy/app.js index f1064d7a3..4acb9f284 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -62,12 +62,16 @@ class Status { let last_point = this.old_points[this.old_points.length - 1]; let oldest_point = this.old_points[0]; - this.instant_speed = - oldest_point.distance(last_point) / (now - this.old_times[0]); - if (this.old_points.length == 8) { + if (this.old_points.length == 6) { + let p1 = this.old_points[0].plus(this.old_points[1]).plus(this.old_points[2]).times(1/3); + let p2 = this.old_points[3].plus(this.old_points[4]).plus(this.old_points[5]).times(1/3); + this.instant_speed = p1.distance(p2) / (this.old_times[4] - this.old_times[1]); this.old_points.shift(); this.old_times.shift(); + } else { + this.instant_speed = + oldest_point.distance(last_point) / (now - this.old_times[0]); } // let's just take angle of segment between newest point and a point a bit before let previous_index = this.old_points.length - 3; From d54e44c869c00f05242eb52b5d1f536edb1de08f Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 14 Sep 2022 17:09:00 +0200 Subject: [PATCH 063/106] speed adjustments --- apps/gipy/app.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 4acb9f284..723f795da 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -63,10 +63,12 @@ class Status { let last_point = this.old_points[this.old_points.length - 1]; let oldest_point = this.old_points[0]; - if (this.old_points.length == 6) { - let p1 = this.old_points[0].plus(this.old_points[1]).plus(this.old_points[2]).times(1/3); - let p2 = this.old_points[3].plus(this.old_points[4]).plus(this.old_points[5]).times(1/3); - this.instant_speed = p1.distance(p2) / (this.old_times[4] - this.old_times[1]); + if (this.old_points.length == 8) { + let p1 = this.old_points[0].plus(this.old_points[1]).plus(this.old_points[2]).plus(this.old_points[3]).times(1/4); + let p2 = this.old_points[4].plus(this.old_points[5]).plus(this.old_points[6]).plus(this.old_points[7]).times(1/4); + let t1 = (this.old_times[1] + this.old_times[2]) / 2; + let t2 = (this.old_times[5] + this.old_times[6]) / 2; + this.instant_speed = p1.distance(p2) / (t2-t1); this.old_points.shift(); this.old_times.shift(); } else { From 29704dad9b3aed8e6057af40a8f43f5719532c7f Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Thu, 15 Sep 2022 15:17:28 +0200 Subject: [PATCH 064/106] going back --- apps/gipy/ChangeLog | 1 + apps/gipy/app.js | 38 +++++++++++++++++++++++++------------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 8684daf17..330512f5f 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -52,3 +52,4 @@ * Detect sharp turns. * Display instant speed. * New instant speed algorithm. + * Bugfix for remaining distance when going back. diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 723f795da..ec5bfb8e9 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -45,7 +45,6 @@ class Status { } this.remaining_distances = r; // how much distance remains at start of each segment this.starting_time = getTime(); - this.stopped_at = this.starting_time; // we need to now how long we stop in order to compute real avg speed this.old_points = []; this.old_times = []; } @@ -64,11 +63,19 @@ class Status { let oldest_point = this.old_points[0]; if (this.old_points.length == 8) { - let p1 = this.old_points[0].plus(this.old_points[1]).plus(this.old_points[2]).plus(this.old_points[3]).times(1/4); - let p2 = this.old_points[4].plus(this.old_points[5]).plus(this.old_points[6]).plus(this.old_points[7]).times(1/4); + let p1 = this.old_points[0] + .plus(this.old_points[1]) + .plus(this.old_points[2]) + .plus(this.old_points[3]) + .times(1 / 4); + let p2 = this.old_points[4] + .plus(this.old_points[5]) + .plus(this.old_points[6]) + .plus(this.old_points[7]) + .times(1 / 4); let t1 = (this.old_times[1] + this.old_times[2]) / 2; let t2 = (this.old_times[5] + this.old_times[6]) / 2; - this.instant_speed = p1.distance(p2) / (t2-t1); + this.instant_speed = p1.distance(p2) / (t2 - t1); this.old_points.shift(); this.old_times.shift(); } else { @@ -167,13 +174,18 @@ class Status { } } // re-display - this.display(); + this.display(orientation); } - remaining_distance() { - return ( + remaining_distance(orientation) { + let remaining_in_correct_orientation = this.remaining_distances[this.current_segment + 1] + - this.position.distance(this.path.point(this.current_segment + 1)) - ); + this.position.distance(this.path.point(this.current_segment + 1)); + + if (orientation == 0) { + return remaining_in_correct_orientation; + } else { + return this.remaining_distances[0] - remaining_in_correct_orientation; + } } is_lost(segment) { let distance_to_nearest = this.position.distance_to_segment( @@ -182,12 +194,12 @@ class Status { ); return distance_to_nearest > 50; } - display() { + display(orientation) { g.clear(); this.display_map(); this.display_interest_points(); - this.display_stats(); + this.display_stats(orientation); Bangle.drawWidgets(); } display_interest_points() { @@ -222,8 +234,8 @@ class Status { g.setColor(color).fillCircle(c[0], c[1], 5); } } - display_stats() { - let remaining_distance = this.remaining_distance(); + display_stats(orientation) { + let remaining_distance = this.remaining_distance(orientation); let rounded_distance = Math.round(remaining_distance / 100) / 10; let total = Math.round(this.remaining_distances[0] / 100) / 10; let now = new Date(); From 8a7c24b473d5875ad949e1f95299f9de0bf76984 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 16 Sep 2022 11:13:48 +0200 Subject: [PATCH 065/106] better avg speed --- apps/gipy/ChangeLog | 3 +++ apps/gipy/TODO | 1 - apps/gipy/app.js | 44 +++++++++++++++++++++++++++++++++++------ apps/gipy/metadata.json | 2 +- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 330512f5f..c7077fe14 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -53,3 +53,6 @@ * Display instant speed. * New instant speed algorithm. * Bugfix for remaining distance when going back. + +0.14: + * Detect starting distance to compute a good average speed. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index a72d1a87e..904e85b87 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -13,7 +13,6 @@ - disable gps off - average speed becomes invalid if you stop and restart - pause when not moving - - figure starting point - we need to buzz 200m before sharp turns (or even better, 30seconds) (and look at more than next point) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index ec5bfb8e9..e4a7658c0 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -44,7 +44,12 @@ class Status { previous_point = point; } this.remaining_distances = r; // how much distance remains at start of each segment - this.starting_time = getTime(); + // we sometimes don't start at the start of the path. + // in order to compute average speed we need to take this into account. + // we record one starting point for each direction (if we suddenly choose to go back). + // we store the distance from start for these points. + this.remaining_distances_at_start = [null, null]; + this.starting_times = [null, null]; // time we start in each direction this.old_points = []; this.old_times = []; } @@ -150,6 +155,25 @@ class Status { this.distance_to_next_point = Math.ceil( this.position.distance(this.path.point(next_point)) ); + + // record start if needed + if (this.on_path) { + if (this.starting_times[orientation] === null) { + this.starting_times[orientation] = + this.old_times[this.old_times.length - 1]; + if (orientation == 0) { + this.remaining_distances_at_start[orientation] = + this.remaining_distances[this.current_segment + 1] + + this.distance_to_next_point; + } else { + this.remaining_distances_at_start[orientation] = + this.remaining_distances[0] - + this.remaining_distances[this.current_segment] + + this.distance_to_next_point; + } + } + } + // disable gps when far from next point and locked if (Bangle.isLocked()) { let time_to_next_point = this.distance_to_next_point / 9.7; // 35km/h is 9.7 m/s @@ -249,16 +273,20 @@ class Status { .setColor(g.theme.fg) .drawString(hours + ":" + minutes, 0, 30); - let point_time = this.old_times[this.old_times.length - 1]; - let done_distance = this.remaining_distances[0] - remaining_distance; - let done_in = point_time - this.starting_time; - let approximate_speed = Math.round((done_distance * 3.6) / done_in); g.setFont("6x8:2").drawString( "" + this.distance_to_next_point + "m", 0, g.getHeight() - 49 ); + let approximate_speed = "??"; + if (this.starting_times[orientation] !== null) { + let point_time = this.old_times[this.old_times.length - 1]; + let done_distance = + this.remaining_distances_at_start[orientation] - remaining_distance; + let done_in = point_time - this.starting_times[orientation]; + approximate_speed = Math.round((done_distance * 3.6) / done_in); + } let approximate_instant_speed = Math.round(this.instant_speed * 3.6); g.setFont("6x8:2") @@ -688,7 +716,11 @@ function start(fn) { let frame = 0; let set_coordinates = function (data) { frame += 1; - let valid_coordinates = !isNaN(data.lat) && !isNaN(data.lon); + // 0,0 coordinates are considered invalid since we sometimes receive them out of nowhere + let valid_coordinates = + !isNaN(data.lat) && + !isNaN(data.lon) && + (data.lat != 0.0 || data.lon != 0.0); if (valid_coordinates) { status.update_position(new Point(data.lon, data.lat), null); } diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 0a750559e..c55a02b13 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.13", + "version": "0.14", "description": "Follow gpx files", "allow_emulator":false, "icon": "gipy.png", From a3a73e423ec663d37b1c9d07f87c8219ea411f9d Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 16 Sep 2022 13:34:23 +0200 Subject: [PATCH 066/106] settings --- apps/gipy/ChangeLog | 1 + apps/gipy/TODO | 3 --- apps/gipy/app.js | 13 +++++++++++-- apps/gipy/metadata.json | 2 ++ apps/gipy/settings.js | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 apps/gipy/settings.js diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index c7077fe14..949698056 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -56,3 +56,4 @@ 0.14: * Detect starting distance to compute a good average speed. + * Settings diff --git a/apps/gipy/TODO b/apps/gipy/TODO index 904e85b87..d78d4c019 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -10,7 +10,6 @@ - config screen - are we on foot (and should use compass) - - disable gps off - average speed becomes invalid if you stop and restart - pause when not moving @@ -26,5 +25,3 @@ * misc - code is becoming messy - - diff --git a/apps/gipy/app.js b/apps/gipy/app.js index e4a7658c0..3d48a8f50 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -2,6 +2,14 @@ let simulated = false; let file_version = 3; let code_key = 47490; +var settings = Object.assign( + { + keep_gps_alive: false, + max_speed: 35, + }, + require("Storage").readJSON("gipy.json", true) || {} +); + let interests_colors = [ 0xf800, // Bakery, red 0x001f, // DrinkingWater, blue @@ -175,8 +183,9 @@ class Status { } // disable gps when far from next point and locked - if (Bangle.isLocked()) { - let time_to_next_point = this.distance_to_next_point / 9.7; // 35km/h is 9.7 m/s + if (Bangle.isLocked() && !settings.keep_gps_alive) { + let time_to_next_point = + (this.distance_to_next_point * 3.6) / settings.max_speed; if (time_to_next_point > 60) { Bangle.setGPSPower(false, "gipy"); setTimeout(function () { diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index c55a02b13..791d5c812 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -13,8 +13,10 @@ "readme": "README.md", "storage": [ {"name":"gipy.app.js","url":"app.js"}, + {"name":"gipy.settings.js","url":"settings.js"}, {"name":"gipy.img","url":"app-icon.js","evaluate":true} ], "data": [ + {"name":"gipy.json"} ] } diff --git a/apps/gipy/settings.js b/apps/gipy/settings.js new file mode 100644 index 000000000..af9cbef22 --- /dev/null +++ b/apps/gipy/settings.js @@ -0,0 +1,38 @@ +(function (back) { + var FILE = "gipy.json"; + // Load settings + var settings = Object.assign( + { + keep_gps_alive: false, + max_speed: 35, + }, + require("Storage").readJSON(FILE, true) || {} + ); + + function writeSettings() { + require("Storage").writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "": { title: "Gipy" }, + "< Back": () => back(), + "keep gps alive": { + value: !!settings.keep_gps_alive, // !! converts undefined to false + format: (v) => (v ? "Yes" : "No"), + onchange: (v) => { + settings.keep_gps_alive = v; + writeSettings(); + }, + }, + "max speed": { + value: 35 | settings.max_speed, // 0| converts undefined to 0 + min: 0, + max: 130, + onchange: (v) => { + settings.max_speed = v; + writeSettings(); + }, + }, + }); +}); From 51a4ad04e400696e9203a5da68faf5e76d133952 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 16 Sep 2022 13:45:19 +0200 Subject: [PATCH 067/106] small bugfix in avg speed --- apps/gipy/app.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 3d48a8f50..dc1b1b35e 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -165,7 +165,7 @@ class Status { ); // record start if needed - if (this.on_path) { + if (this.on_path && this.old_points.length > 1) { if (this.starting_times[orientation] === null) { this.starting_times[orientation] = this.old_times[this.old_times.length - 1]; @@ -294,7 +294,9 @@ class Status { let done_distance = this.remaining_distances_at_start[orientation] - remaining_distance; let done_in = point_time - this.starting_times[orientation]; - approximate_speed = Math.round((done_distance * 3.6) / done_in); + if (done_in != 0) { + approximate_speed = Math.round((done_distance * 3.6) / done_in); + } } let approximate_instant_speed = Math.round(this.instant_speed * 3.6); From 04c6b6f249b2de319d7971246e7e316b4cc29df0 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 16 Sep 2022 16:35:13 +0200 Subject: [PATCH 068/106] breaks --- apps/gipy/ChangeLog | 1 + apps/gipy/TODO | 2 -- apps/gipy/app.js | 19 +++++++++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 949698056..f9f55be15 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -57,3 +57,4 @@ 0.14: * Detect starting distance to compute a good average speed. * Settings + * Account for breaks in average speed. diff --git a/apps/gipy/TODO b/apps/gipy/TODO index d78d4c019..53c3530e2 100644 --- a/apps/gipy/TODO +++ b/apps/gipy/TODO @@ -10,8 +10,6 @@ - config screen - are we on foot (and should use compass) -- average speed becomes invalid if you stop and restart - - pause when not moving - we need to buzz 200m before sharp turns (or even better, 30seconds) (and look at more than next point) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index dc1b1b35e..a918a7992 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -39,6 +39,8 @@ class Status { this.current_segment = null; // which segment is closest this.reaching = null; // which waypoint are we reaching ? this.distance_to_next_point = null; // how far are we from next point ? + this.paused_time = 0.0; // how long did we stop (stops don't count in avg speed) + this.paused_since = getTime(); let r = [0]; // let's do a reversed prefix computations on all distances: @@ -94,6 +96,18 @@ class Status { } else { this.instant_speed = oldest_point.distance(last_point) / (now - this.old_times[0]); + + // update paused time if we are too slow + if (this.instant_speed < 2) { + if (this.paused_since === null) { + this.paused_since = now; + } + } else { + if (this.paused_since !== null) { + this.paused_time += now - this.paused_since; + this.paused_since = null; + } + } } // let's just take angle of segment between newest point and a point a bit before let previous_index = this.old_points.length - 3; @@ -293,8 +307,9 @@ class Status { let point_time = this.old_times[this.old_times.length - 1]; let done_distance = this.remaining_distances_at_start[orientation] - remaining_distance; - let done_in = point_time - this.starting_times[orientation]; - if (done_in != 0) { + let done_in = + point_time - this.starting_times[orientation] - this.paused_time; + if (done_in > 0) { approximate_speed = Math.round((done_distance * 3.6) / done_in); } } From 2beb6c779ffa7138b90a06bb01c49b9ddf4e8d22 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Fri, 23 Sep 2022 17:05:37 +0200 Subject: [PATCH 069/106] changed avg speed --- apps/gipy/app.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index a918a7992..24676548b 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -183,16 +183,7 @@ class Status { if (this.starting_times[orientation] === null) { this.starting_times[orientation] = this.old_times[this.old_times.length - 1]; - if (orientation == 0) { - this.remaining_distances_at_start[orientation] = - this.remaining_distances[this.current_segment + 1] + - this.distance_to_next_point; - } else { - this.remaining_distances_at_start[orientation] = - this.remaining_distances[0] - - this.remaining_distances[this.current_segment] + - this.distance_to_next_point; - } + this.remaining_distances_at_start[orientation] = this.remaining_distance(orientation); } } @@ -308,7 +299,7 @@ class Status { let done_distance = this.remaining_distances_at_start[orientation] - remaining_distance; let done_in = - point_time - this.starting_times[orientation] - this.paused_time; + point_time - this.starting_times[orientation]; // TODO: add pauses if (done_in > 0) { approximate_speed = Math.round((done_distance * 3.6) / done_in); } From ac8a3c35f62ffeab0537cb8f839d2839a7c9b055 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sat, 5 Nov 2022 14:16:07 +0100 Subject: [PATCH 070/106] better avg speed --- apps/gipy/ChangeLog | 4 ++++ apps/gipy/app.js | 45 +++++++++++++++++------------------------ apps/gipy/metadata.json | 2 +- 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index f9f55be15..d9fde597b 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -58,3 +58,7 @@ * Detect starting distance to compute a good average speed. * Settings * Account for breaks in average speed. + +0.15: + * Record traveled distance to get a good average speed. + * Breaks (low speed) will not count in average speed. diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 24676548b..b97716604 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -54,18 +54,16 @@ class Status { previous_point = point; } this.remaining_distances = r; // how much distance remains at start of each segment - // we sometimes don't start at the start of the path. - // in order to compute average speed we need to take this into account. - // we record one starting point for each direction (if we suddenly choose to go back). - // we store the distance from start for these points. - this.remaining_distances_at_start = [null, null]; - this.starting_times = [null, null]; // time we start in each direction + this.starting_time = this.paused_since; // time we start + this.advanced_distance = 0.0; + this.gps_coordinates_counter = 0; // how many coordinates did we receive this.old_points = []; this.old_times = []; } new_position_reached(position) { // we try to figure out direction by looking at previous points // instead of the gps course which is not very nice. + this.gps_coordinates_counter += 1; let now = getTime(); this.old_points.push(position); this.old_times.push(now); @@ -77,6 +75,16 @@ class Status { let last_point = this.old_points[this.old_points.length - 1]; let oldest_point = this.old_points[0]; + // every 8 points we count the distance + if (this.gps_coordinates_counter % 8 == 0) { + let distance = last_point.distance(oldest_point); + console.log(distance); + if (distance < 150.0) { + // to avoid gps glitches + this.advanced_distance += distance; + } + } + if (this.old_points.length == 8) { let p1 = this.old_points[0] .plus(this.old_points[1]) @@ -178,15 +186,6 @@ class Status { this.position.distance(this.path.point(next_point)) ); - // record start if needed - if (this.on_path && this.old_points.length > 1) { - if (this.starting_times[orientation] === null) { - this.starting_times[orientation] = - this.old_times[this.old_times.length - 1]; - this.remaining_distances_at_start[orientation] = this.remaining_distance(orientation); - } - } - // disable gps when far from next point and locked if (Bangle.isLocked() && !settings.keep_gps_alive) { let time_to_next_point = @@ -293,17 +292,11 @@ class Status { g.getHeight() - 49 ); - let approximate_speed = "??"; - if (this.starting_times[orientation] !== null) { - let point_time = this.old_times[this.old_times.length - 1]; - let done_distance = - this.remaining_distances_at_start[orientation] - remaining_distance; - let done_in = - point_time - this.starting_times[orientation]; // TODO: add pauses - if (done_in > 0) { - approximate_speed = Math.round((done_distance * 3.6) / done_in); - } - } + let point_time = this.old_times[this.old_times.length - 1]; + let done_in = point_time - this.starting_time - this.paused_time; + let approximate_speed = Math.round( + (this.advanced_distance * 3.6) / done_in + ); let approximate_instant_speed = Math.round(this.instant_speed * 3.6); g.setFont("6x8:2") diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 791d5c812..9761643fc 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.14", + "version": "0.15", "description": "Follow gpx files", "allow_emulator":false, "icon": "gipy.png", From db85f1ccdd3cae9382a16240b9a2999f0095279b Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sat, 5 Nov 2022 17:17:49 +0100 Subject: [PATCH 071/106] bugfix in avg speed --- apps/gipy/ChangeLog | 1 + apps/gipy/README.md | 29 ++++++++++++++++++++++++++--- apps/gipy/app.js | 7 +++---- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index d9fde597b..3b0d62009 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -62,3 +62,4 @@ 0.15: * Record traveled distance to get a good average speed. * Breaks (low speed) will not count in average speed. + * Bugfix in average speed. diff --git a/apps/gipy/README.md b/apps/gipy/README.md index f7fd04233..07f5dcca4 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -1,11 +1,34 @@ # Gipy -Development still in progress. Follow compressed gpx traces. -Will warn you before reaching intersections and try to turn off gps. +Gipy allows you to follow gpx traces on your watch. + +It is meant for bicycling and not hiking +(it uses your movement to figure out your orientation). + +It provides the following features : + +- display the path with current position from gps +- detects and buzzes if you leave the path +- buzzes before sharp turns +- buzzes before nodes with comments +(for example when you need to turn in https://mapstogpx.com/) +- display instant / average speed +- display distance to next node + +optionally it can also : + +- display additional data from openstreetmap : + - water points + - toilets + - artwork + - bakeries + +- try to turn off gps between crossroads to save battery ## Usage -WIP. +You first need to convert your .gpx file to a .gpc (our custom lightweight trace). +Then launch gipy and select a trace to follow. ## Creator diff --git a/apps/gipy/app.js b/apps/gipy/app.js index b97716604..ae82e5dfb 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -4,7 +4,7 @@ let code_key = 47490; var settings = Object.assign( { - keep_gps_alive: false, + keep_gps_alive: true, max_speed: 35, }, require("Storage").readJSON("gipy.json", true) || {} @@ -75,10 +75,9 @@ class Status { let last_point = this.old_points[this.old_points.length - 1]; let oldest_point = this.old_points[0]; - // every 8 points we count the distance - if (this.gps_coordinates_counter % 8 == 0) { + // every 7 points we count the distance + if (this.gps_coordinates_counter % 7 == 0) { let distance = last_point.distance(oldest_point); - console.log(distance); if (distance < 150.0) { // to avoid gps glitches this.advanced_distance += distance; From 316edcb36908d3b2dfe9dea26de94ca48e034be3 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 6 Nov 2022 05:04:59 +0100 Subject: [PATCH 072/106] getting ready for wasm --- apps/gipy/gpconv/.gitignore | 3 + apps/gipy/gpconv/Cargo.lock | 70 +++ apps/gipy/gpconv/Cargo.toml | 8 +- apps/gipy/gpconv/src/interests.rs | 78 ++++ apps/gipy/gpconv/src/lib.rs | 575 ++++++++++++++++++++++++ apps/gipy/gpconv/src/main.rs | 713 +----------------------------- apps/gipy/gpconv/src/osm.rs | 82 +--- apps/gipy/gpconv/src/svg.rs | 86 ++++ 8 files changed, 835 insertions(+), 780 deletions(-) create mode 100644 apps/gipy/gpconv/src/interests.rs create mode 100644 apps/gipy/gpconv/src/lib.rs create mode 100644 apps/gipy/gpconv/src/svg.rs diff --git a/apps/gipy/gpconv/.gitignore b/apps/gipy/gpconv/.gitignore index ea8c4bf7f..a4f078e1e 100644 --- a/apps/gipy/gpconv/.gitignore +++ b/apps/gipy/gpconv/.gitignore @@ -1 +1,4 @@ /target +*.gpc +*.pbf +*.svg diff --git a/apps/gipy/gpconv/Cargo.lock b/apps/gipy/gpconv/Cargo.lock index c280558cd..6986021d7 100644 --- a/apps/gipy/gpconv/Cargo.lock +++ b/apps/gipy/gpconv/Cargo.lock @@ -67,6 +67,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + [[package]] name = "byteorder" version = "1.4.3" @@ -272,6 +278,7 @@ dependencies = [ "itertools", "lazy_static", "osmio", + "wasm-bindgen", ] [[package]] @@ -355,6 +362,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + [[package]] name = "memchr" version = "2.5.0" @@ -633,6 +649,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + [[package]] name = "winapi" version = "0.3.9" diff --git a/apps/gipy/gpconv/Cargo.toml b/apps/gipy/gpconv/Cargo.toml index 735eaa6ed..afcbec440 100644 --- a/apps/gipy/gpconv/Cargo.toml +++ b/apps/gipy/gpconv/Cargo.toml @@ -4,9 +4,15 @@ version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +osm = ["dep:osmio"] + +# [lib] +# crate-type = ["cdylib"] [dependencies] gpx="*" itertools="*" lazy_static="*" -osmio="*" +osmio={version="*", optional=true} +wasm-bindgen="*" \ No newline at end of file diff --git a/apps/gipy/gpconv/src/interests.rs b/apps/gipy/gpconv/src/interests.rs new file mode 100644 index 000000000..6d078e75d --- /dev/null +++ b/apps/gipy/gpconv/src/interests.rs @@ -0,0 +1,78 @@ +use super::Point; +use lazy_static::lazy_static; +use std::collections::HashMap; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Interest { + Bakery, + DrinkingWater, + Toilets, + // BikeShop, + // ChargingStation, + // Bank, + // Supermarket, + // Table, + // TourismOffice, + Artwork, + // Pharmacy, +} + +impl Into for Interest { + fn into(self) -> u8 { + match self { + Interest::Bakery => 0, + Interest::DrinkingWater => 1, + Interest::Toilets => 2, + // Interest::BikeShop => 8, + // Interest::ChargingStation => 4, + // Interest::Bank => 5, + // Interest::Supermarket => 6, + // Interest::Table => 7, + Interest::Artwork => 3, + // Interest::Pharmacy => 9, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct InterestPoint { + pub point: Point, + pub interest: Interest, +} + +lazy_static! { + static ref INTERESTS: HashMap<(&'static str, &'static str), Interest> = { + [ + (("shop", "bakery"), Interest::Bakery), + (("amenity", "drinking_water"), Interest::DrinkingWater), + (("amenity", "toilets"), Interest::Toilets), + // (("shop", "bicycle"), Interest::BikeShop), + // (("amenity", "charging_station"), Interest::ChargingStation), + // (("amenity", "bank"), Interest::Bank), + // (("shop", "supermarket"), Interest::Supermarket), + // (("leisure", "picnic_table"), Interest::Table), + // (("tourism", "information"), Interest::TourismOffice), + (("tourism", "artwork"), Interest::Artwork), + // (("amenity", "pharmacy"), Interest::Pharmacy), + ] + .into_iter() + .collect() + }; +} + +impl InterestPoint { + pub fn color(&self) -> &'static str { + match self.interest { + Interest::Bakery => "red", + Interest::DrinkingWater => "blue", + Interest::Toilets => "brown", + // Interest::BikeShop => "purple", + // Interest::ChargingStation => "green", + // Interest::Bank => "black", + // Interest::Supermarket => "red", + // Interest::Table => "pink", + Interest::Artwork => "orange", + // Interest::Pharmacy => "chartreuse", + } + } +} diff --git a/apps/gipy/gpconv/src/lib.rs b/apps/gipy/gpconv/src/lib.rs new file mode 100644 index 000000000..a58922c17 --- /dev/null +++ b/apps/gipy/gpconv/src/lib.rs @@ -0,0 +1,575 @@ +use itertools::Itertools; +use std::collections::{HashMap, HashSet}; +use std::fs::File; +use std::io::{BufReader, BufWriter, Read, Write}; +use std::path::Path; +use wasm_bindgen::prelude::*; + +use gpx::read; +use gpx::Gpx; + +mod interests; +use interests::InterestPoint; + +mod svg; + +#[cfg(feature = "osm")] +mod osm; +#[cfg(feature = "osm")] +use osm::{parse_osm_data, InterestPoint}; + +const LOWER_SHARP_TURN: f64 = 80.0 * std::f64::consts::PI / 180.0; +const UPPER_SHARP_TURN: f64 = std::f64::consts::PI * 2.0 - LOWER_SHARP_TURN; + +const KEY: u16 = 47490; +const FILE_VERSION: u16 = 3; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Point { + x: f64, + y: f64, +} + +impl Eq for Point {} +impl std::hash::Hash for Point { + fn hash(&self, state: &mut H) { + unsafe { std::mem::transmute::(self.x) }.hash(state); + unsafe { std::mem::transmute::(self.y) }.hash(state); + } +} + +impl Point { + fn squared_distance_between(&self, other: &Point) -> f64 { + let dx = other.x - self.x; + let dy = other.y - self.y; + dx * dx + dy * dy + } + fn distance_to_segment(&self, v: &Point, w: &Point) -> f64 { + let l2 = v.squared_distance_between(w); + if l2 == 0.0 { + return self.squared_distance_between(v).sqrt(); + } + // Consider the line extending the segment, parameterized as v + t (w - v). + // We find projection of point p onto the line. + // It falls where t = [(p-v) . (w-v)] / |w-v|^2 + // We clamp t from [0,1] to handle points outside the segment vw. + let x0 = self.x - v.x; + let y0 = self.y - v.y; + let x1 = w.x - v.x; + let y1 = w.y - v.y; + let dot = x0 * x1 + y0 * y1; + let t = (dot / l2).min(1.0).max(0.0); + + let proj = Point { + x: v.x + x1 * t, + y: v.y + y1 * t, + }; + + proj.squared_distance_between(self).sqrt() + } +} + +fn points(reader: R) -> (HashSet, Vec) { + // read takes any io::Read and gives a Result. + let mut gpx: Gpx = read(reader).unwrap(); + eprintln!("we have {} tracks", gpx.tracks.len()); + + let mut waypoints = HashSet::new(); + + let points = gpx + .tracks + .pop() + .unwrap() + .segments + .into_iter() + .flat_map(|segment| segment.points.into_iter()) + .map(|p| { + let is_commented = p.comment.is_some(); + let (x, y) = p.point().x_y(); + let p = Point { x, y }; + if is_commented { + waypoints.insert(p); + } + p + }) + .collect::>(); + (waypoints, points) +} + +// // NOTE: this angles idea could maybe be use to get dp from n^3 to n^2 +// fn acceptable_angles(p1: &(f64, f64), p2: &(f64, f64), epsilon: f64) -> (f64, f64) { +// // first, convert p2's coordinates for p1 as origin +// let (x1, y1) = *p1; +// let (x2, y2) = *p2; +// let (x, y) = (x2 - x1, y2 - y1); +// // rotate so that (p1, p2) ends on x axis +// let theta = y.atan2(x); +// let rx = x * theta.cos() - y * theta.sin(); +// let ry = x * theta.sin() + y * theta.cos(); +// assert!(ry.abs() <= std::f64::EPSILON); +// +// // now imagine a line at an angle alpha. +// // we want the distance d from (rx, 0) to our line +// // we have sin(alpha) = d / rx +// // limiting d to epsilon, we solve +// // sin(alpha) = e / rx +// // and get +// // alpha = arcsin(e/rx) +// let alpha = (epsilon / rx).asin(); +// +// // now we just need to rotate back +// let a1 = theta + alpha.abs(); +// let a2 = theta - alpha.abs(); +// assert!(a1 >= a2); +// (a1, a2) +// } +// +// // this is like ramer douglas peucker algorithm +// // except that we advance from the start without knowing the end. +// // each point we meet constrains the chosen segment's angle +// // a bit more. +// // +// fn simplify(mut points: &[(f64, f64)]) -> Vec<(f64, f64)> { +// let mut remaining_points = Vec::new(); +// while !points.is_empty() { +// let (sx, sy) = points.first().unwrap(); +// let i = match points +// .iter() +// .enumerate() +// .map(|(i, (x, y))| todo!("compute angles")) +// .try_fold( +// (0.0f64, std::f64::consts::FRAC_2_PI), +// |(amin, amax), (i, (amin2, amax2))| -> Result<(f64, f64), usize> { +// let new_amax = amax.min(amax2); +// let new_amin = amin.max(amin2); +// if new_amin >= new_amax { +// Err(i) +// } else { +// Ok((new_amin, new_amax)) +// } +// }, +// ) { +// Err(i) => i, +// Ok(_) => points.len(), +// }; +// remaining_points.push(points.first().cloned().unwrap()); +// points = &points[i..]; +// } +// remaining_points +// } + +fn extract_prog_dyn_solution( + points: &[Point], + start: usize, + end: usize, + cache: &HashMap<(usize, usize), (Option, usize)>, +) -> Vec { + if let Some(choice) = cache.get(&(start, end)).unwrap().0 { + let mut v1 = extract_prog_dyn_solution(points, start, choice + 1, cache); + let mut v2 = extract_prog_dyn_solution(points, choice, end, cache); + v1.pop(); + v1.append(&mut v2); + v1 + } else { + vec![points[start], points[end - 1]] + } +} + +fn simplify_prog_dyn( + points: &[Point], + start: usize, + end: usize, + epsilon: f64, + cache: &mut HashMap<(usize, usize), (Option, usize)>, +) -> usize { + if let Some(val) = cache.get(&(start, end)) { + val.1 + } else { + let res = if end - start <= 2 { + assert_eq!(end - start, 2); + (None, end - start) + } else { + let first_point = &points[start]; + let last_point = &points[end - 1]; + + if points[(start + 1)..end] + .iter() + .map(|p| p.distance_to_segment(first_point, last_point)) + .all(|d| d <= epsilon) + { + (None, 2) + } else { + // now we test all possible cutting points + ((start + 1)..(end - 1)) //TODO: take middle min + .map(|i| { + let v1 = simplify_prog_dyn(points, start, i + 1, epsilon, cache); + let v2 = simplify_prog_dyn(points, i, end, epsilon, cache); + (Some(i), v1 + v2 - 1) + }) + .min_by_key(|(_, v)| *v) + .unwrap() + } + }; + cache.insert((start, end), res); + res.1 + } +} + +fn rdp(points: &[Point], epsilon: f64) -> Vec { + if points.len() <= 2 { + points.iter().copied().collect() + } else { + if points.first().unwrap() == points.last().unwrap() { + let first = points.first().unwrap(); + let index_farthest = points + .iter() + .enumerate() + .skip(1) + .max_by(|(_, p1), (_, p2)| { + first + .squared_distance_between(p1) + .partial_cmp(&first.squared_distance_between(p2)) + .unwrap() + }) + .map(|(i, _)| i) + .unwrap(); + + let start = &points[..(index_farthest + 1)]; + let end = &points[index_farthest..]; + let mut res = rdp(start, epsilon); + res.pop(); + res.append(&mut rdp(end, epsilon)); + res + } else { + let (index_farthest, farthest_distance) = points + .iter() + .map(|p| p.distance_to_segment(points.first().unwrap(), points.last().unwrap())) + .enumerate() + .max_by(|(_, d1), (_, d2)| { + if d1.is_nan() { + std::cmp::Ordering::Greater + } else { + if d2.is_nan() { + std::cmp::Ordering::Less + } else { + d1.partial_cmp(d2).unwrap() + } + } + }) + .unwrap(); + if farthest_distance <= epsilon { + vec![ + points.first().copied().unwrap(), + points.last().copied().unwrap(), + ] + } else { + let start = &points[..(index_farthest + 1)]; + let end = &points[index_farthest..]; + let mut res = rdp(start, epsilon); + res.pop(); + res.append(&mut rdp(end, epsilon)); + res + } + } + } +} + +fn simplify_path(points: &[Point], epsilon: f64) -> Vec { + if points.len() <= 600 { + optimal_simplification(points, epsilon) + } else { + hybrid_simplification(points, epsilon) + } +} + +fn save_gpc( + mut writer: W, + points: &[Point], + waypoints: &HashSet, + buckets: &[Bucket], +) -> std::io::Result<()> { + eprintln!("saving {} points", points.len()); + + let mut unique_interest_points = Vec::new(); + let mut correspondance = HashMap::new(); + let interests_on_path = buckets + .iter() + .flat_map(|b| &b.points) + .map(|p| match correspondance.entry(*p) { + std::collections::hash_map::Entry::Occupied(o) => *o.get(), + std::collections::hash_map::Entry::Vacant(v) => { + let index = unique_interest_points.len(); + unique_interest_points.push(*p); + v.insert(index); + index + } + }) + .collect::>(); + + writer.write_all(&KEY.to_le_bytes())?; + writer.write_all(&FILE_VERSION.to_le_bytes())?; + writer.write_all(&(points.len() as u16).to_le_bytes())?; + writer.write_all(&(unique_interest_points.len() as u16).to_le_bytes())?; + writer.write_all(&(interests_on_path.len() as u16).to_le_bytes())?; + points + .iter() + .flat_map(|p| [p.x, p.y]) + .try_for_each(|c| writer.write_all(&c.to_le_bytes()))?; + + let mut waypoints_bits = std::iter::repeat(0u8) + .take(points.len() / 8 + if points.len() % 8 != 0 { 1 } else { 0 }) + .collect::>(); + points.iter().enumerate().for_each(|(i, p)| { + if waypoints.contains(p) { + waypoints_bits[i / 8] |= 1 << (i % 8) + } + }); + waypoints_bits + .iter() + .try_for_each(|byte| writer.write_all(&byte.to_le_bytes()))?; + + unique_interest_points + .iter() + .flat_map(|p| [p.point.x, p.point.y]) + .try_for_each(|c| writer.write_all(&c.to_le_bytes()))?; + + let counts: HashMap<_, usize> = + unique_interest_points + .iter() + .fold(HashMap::new(), |mut h, p| { + *h.entry(p.interest).or_default() += 1; + h + }); + counts.into_iter().for_each(|(interest, count)| { + eprintln!("{:?} appears {} times", interest, count); + }); + + unique_interest_points + .iter() + .map(|p| p.interest.into()) + .try_for_each(|i: u8| writer.write_all(&i.to_le_bytes()))?; + + interests_on_path + .iter() + .map(|i| *i as u16) + .try_for_each(|i| writer.write_all(&i.to_le_bytes()))?; + + buckets + .iter() + .map(|b| b.start as u16) + .try_for_each(|i| writer.write_all(&i.to_le_bytes()))?; + + Ok(()) +} + +fn optimal_simplification(points: &[Point], epsilon: f64) -> Vec { + let mut cache = HashMap::new(); + simplify_prog_dyn(&points, 0, points.len(), epsilon, &mut cache); + extract_prog_dyn_solution(&points, 0, points.len(), &cache) +} + +fn hybrid_simplification(points: &[Point], epsilon: f64) -> Vec { + if points.len() <= 300 { + optimal_simplification(points, epsilon) + } else { + if points.first().unwrap() == points.last().unwrap() { + let first = points.first().unwrap(); + let index_farthest = points + .iter() + .enumerate() + .skip(1) + .max_by(|(_, p1), (_, p2)| { + first + .squared_distance_between(p1) + .partial_cmp(&first.squared_distance_between(p2)) + .unwrap() + }) + .map(|(i, _)| i) + .unwrap(); + + let start = &points[..(index_farthest + 1)]; + let end = &points[index_farthest..]; + let mut res = hybrid_simplification(start, epsilon); + res.pop(); + res.append(&mut hybrid_simplification(end, epsilon)); + res + } else { + let (index_farthest, farthest_distance) = points + .iter() + .map(|p| p.distance_to_segment(points.first().unwrap(), points.last().unwrap())) + .enumerate() + .max_by(|(_, d1), (_, d2)| { + if d1.is_nan() { + std::cmp::Ordering::Greater + } else { + if d2.is_nan() { + std::cmp::Ordering::Less + } else { + d1.partial_cmp(d2).unwrap() + } + } + }) + .unwrap(); + if farthest_distance <= epsilon { + vec![ + points.first().copied().unwrap(), + points.last().copied().unwrap(), + ] + } else { + let start = &points[..(index_farthest + 1)]; + let end = &points[index_farthest..]; + let mut res = hybrid_simplification(start, epsilon); + res.pop(); + res.append(&mut hybrid_simplification(end, epsilon)); + res + } + } + } +} + +pub struct Bucket { + points: Vec, + start: usize, +} + +fn position_interests_along_path( + interests: &mut [InterestPoint], + path: &[Point], + d: f64, + buckets_size: usize, // final points are indexed in buckets + groups_size: usize, // how many segments are compacted together +) -> Vec { + interests.sort_unstable_by(|p1, p2| p1.point.x.partial_cmp(&p2.point.x).unwrap()); + // first compute for each segment a vec containing its nearby points + let mut positions = Vec::new(); + for segment in path.windows(2) { + let mut local_interests = Vec::new(); + let x0 = segment[0].x; + let x1 = segment[1].x; + let (xmin, xmax) = if x0 <= x1 { (x0, x1) } else { (x1, x0) }; + let i = interests.partition_point(|p| p.point.x < xmin - d); + let interests = &interests[i..]; + let i = interests.partition_point(|p| p.point.x <= xmax + d); + let interests = &interests[..i]; + for interest in interests { + if interest.point.distance_to_segment(&segment[0], &segment[1]) <= d { + local_interests.push(*interest); + } + } + positions.push(local_interests); + } + // fuse points on chunks of consecutive segments together + let grouped_positions = positions + .chunks(groups_size) + .map(|c| c.iter().flatten().unique().copied().collect::>()) + .collect::>(); + // now, group the points in buckets + let chunks = grouped_positions + .iter() + .enumerate() + .flat_map(|(i, points)| points.iter().map(move |p| (i, p))) + .chunks(buckets_size); + let mut buckets = Vec::new(); + for bucket_points in &chunks { + let mut bucket_points = bucket_points.peekable(); + let start = bucket_points.peek().unwrap().0; + let points = bucket_points.map(|(_, p)| *p).collect(); + buckets.push(Bucket { points, start }); + } + buckets +} + +fn detect_sharp_turns(path: &[Point], waypoints: &mut HashSet) { + path.iter() + .tuple_windows() + .map(|(a, b, c)| { + let xd1 = b.x - a.x; + let yd1 = b.y - a.y; + let angle1 = yd1.atan2(xd1); + + let xd2 = c.x - b.x; + let yd2 = c.y - b.y; + let angle2 = yd2.atan2(xd2); + let adiff = angle2 - angle1; + let adiff = if adiff < 0.0 { + adiff + std::f64::consts::PI * 2.0 + } else { + adiff + }; + (adiff, b) + }) + .filter_map(|(adiff, b)| { + if adiff > LOWER_SHARP_TURN && adiff < UPPER_SHARP_TURN { + Some(b) + } else { + None + } + }) + .for_each(|b| { + waypoints.insert(*b); + }); +} + +#[wasm_bindgen] +pub fn convert_gpx_strings(input_str: &str) -> Vec { + let mut interests = Vec::new(); + let mut output: Vec = Vec::new(); + convert_gpx(input_str.as_bytes(), &mut output, &mut interests); + output +} + +pub fn convert_gpx_files(input_file: &str, interests: &mut [InterestPoint]) { + let file = File::open(input_file).unwrap(); + let reader = BufReader::new(file); + let output_path = Path::new(&input_file).with_extension("gpc"); + let writer = BufWriter::new(File::create(output_path).unwrap()); + convert_gpx(reader, writer, interests); +} + +fn convert_gpx( + input_reader: R, + output_writer: W, + interests: &mut [InterestPoint], +) { + // load all points composing the trace and mark commented points + // as special waypoints. + let (mut waypoints, p) = points(input_reader); + + // detect sharp turns before path simplification to keep them + detect_sharp_turns(&p, &mut waypoints); + waypoints.insert(p.first().copied().unwrap()); + waypoints.insert(p.last().copied().unwrap()); + println!("we have {} waypoints", waypoints.len()); + + println!("initially we had {} points", p.len()); + + // simplify path + let mut rp = Vec::new(); + let mut segment = Vec::new(); + for point in &p { + segment.push(*point); + if waypoints.contains(point) { + if segment.len() >= 2 { + let mut s = simplify_path(&segment, 0.00015); + rp.append(&mut s); + segment = rp.pop().into_iter().collect(); + } + } + } + rp.append(&mut segment); + println!("we now have {} points", rp.len()); + + // add interest points from open street map if we have any + let buckets = position_interests_along_path(interests, &rp, 0.001, 5, 3); + + // save_svg( + // "test.svg", + // &p, + // &rp, + // buckets.iter().flat_map(|b| &b.points), + // &waypoints, + // ) + // .unwrap(); + + save_gpc(output_writer, &rp, &waypoints, &buckets).unwrap(); +} diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs index 9b7a19193..c804d0889 100644 --- a/apps/gipy/gpconv/src/main.rs +++ b/apps/gipy/gpconv/src/main.rs @@ -1,705 +1,22 @@ -use itertools::Itertools; -use osmio::ObjId; -use std::collections::{HashMap, HashSet}; -use std::fs::File; -use std::io::{BufReader, BufWriter, Write}; -use std::path::Path; - -use gpx::read; -use gpx::Gpx; - -mod osm; -use osm::{parse_osm_data, InterestPoint}; - -const LOWER_SHARP_TURN: f64 = 80.0 * std::f64::consts::PI / 180.0; -const UPPER_SHARP_TURN: f64 = std::f64::consts::PI * 2.0 - LOWER_SHARP_TURN; - -const KEY: u16 = 47490; -const FILE_VERSION: u16 = 3; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Point { - x: f64, - y: f64, -} - -impl Eq for Point {} -impl std::hash::Hash for Point { - fn hash(&self, state: &mut H) { - unsafe { std::mem::transmute::(self.x) }.hash(state); - unsafe { std::mem::transmute::(self.y) }.hash(state); - } -} - -impl Point { - fn squared_distance_between(&self, other: &Point) -> f64 { - let dx = other.x - self.x; - let dy = other.y - self.y; - dx * dx + dy * dy - } - fn distance_to_segment(&self, v: &Point, w: &Point) -> f64 { - let l2 = v.squared_distance_between(w); - if l2 == 0.0 { - return self.squared_distance_between(v).sqrt(); - } - // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. - // It falls where t = [(p-v) . (w-v)] / |w-v|^2 - // We clamp t from [0,1] to handle points outside the segment vw. - let x0 = self.x - v.x; - let y0 = self.y - v.y; - let x1 = w.x - v.x; - let y1 = w.y - v.y; - let dot = x0 * x1 + y0 * y1; - let t = (dot / l2).min(1.0).max(0.0); - - let proj = Point { - x: v.x + x1 * t, - y: v.y + y1 * t, - }; - - proj.squared_distance_between(self).sqrt() - } -} - -fn points(filename: &str) -> (HashSet, Vec) { - let file = File::open(filename).unwrap(); - let reader = BufReader::new(file); - - // read takes any io::Read and gives a Result. - let mut gpx: Gpx = read(reader).unwrap(); - eprintln!("we have {} tracks", gpx.tracks.len()); - - let mut waypoints = HashSet::new(); - - let points = gpx - .tracks - .pop() - .unwrap() - .segments - .into_iter() - .flat_map(|segment| segment.points.into_iter()) - .map(|p| { - let is_commented = p.comment.is_some(); - let (x, y) = p.point().x_y(); - let p = Point { x, y }; - if is_commented { - waypoints.insert(p); - } - p - }) - .collect::>(); - (waypoints, points) -} - -// // NOTE: this angles idea could maybe be use to get dp from n^3 to n^2 -// fn acceptable_angles(p1: &(f64, f64), p2: &(f64, f64), epsilon: f64) -> (f64, f64) { -// // first, convert p2's coordinates for p1 as origin -// let (x1, y1) = *p1; -// let (x2, y2) = *p2; -// let (x, y) = (x2 - x1, y2 - y1); -// // rotate so that (p1, p2) ends on x axis -// let theta = y.atan2(x); -// let rx = x * theta.cos() - y * theta.sin(); -// let ry = x * theta.sin() + y * theta.cos(); -// assert!(ry.abs() <= std::f64::EPSILON); -// -// // now imagine a line at an angle alpha. -// // we want the distance d from (rx, 0) to our line -// // we have sin(alpha) = d / rx -// // limiting d to epsilon, we solve -// // sin(alpha) = e / rx -// // and get -// // alpha = arcsin(e/rx) -// let alpha = (epsilon / rx).asin(); -// -// // now we just need to rotate back -// let a1 = theta + alpha.abs(); -// let a2 = theta - alpha.abs(); -// assert!(a1 >= a2); -// (a1, a2) -// } -// -// // this is like ramer douglas peucker algorithm -// // except that we advance from the start without knowing the end. -// // each point we meet constrains the chosen segment's angle -// // a bit more. -// // -// fn simplify(mut points: &[(f64, f64)]) -> Vec<(f64, f64)> { -// let mut remaining_points = Vec::new(); -// while !points.is_empty() { -// let (sx, sy) = points.first().unwrap(); -// let i = match points -// .iter() -// .enumerate() -// .map(|(i, (x, y))| todo!("compute angles")) -// .try_fold( -// (0.0f64, std::f64::consts::FRAC_2_PI), -// |(amin, amax), (i, (amin2, amax2))| -> Result<(f64, f64), usize> { -// let new_amax = amax.min(amax2); -// let new_amin = amin.max(amin2); -// if new_amin >= new_amax { -// Err(i) -// } else { -// Ok((new_amin, new_amax)) -// } -// }, -// ) { -// Err(i) => i, -// Ok(_) => points.len(), -// }; -// remaining_points.push(points.first().cloned().unwrap()); -// points = &points[i..]; -// } -// remaining_points -// } - -fn extract_prog_dyn_solution( - points: &[Point], - start: usize, - end: usize, - cache: &HashMap<(usize, usize), (Option, usize)>, -) -> Vec { - if let Some(choice) = cache.get(&(start, end)).unwrap().0 { - let mut v1 = extract_prog_dyn_solution(points, start, choice + 1, cache); - let mut v2 = extract_prog_dyn_solution(points, choice, end, cache); - v1.pop(); - v1.append(&mut v2); - v1 - } else { - vec![points[start], points[end - 1]] - } -} - -fn simplify_prog_dyn( - points: &[Point], - start: usize, - end: usize, - epsilon: f64, - cache: &mut HashMap<(usize, usize), (Option, usize)>, -) -> usize { - if let Some(val) = cache.get(&(start, end)) { - val.1 - } else { - let res = if end - start <= 2 { - assert_eq!(end - start, 2); - (None, end - start) - } else { - let first_point = &points[start]; - let last_point = &points[end - 1]; - - if points[(start + 1)..end] - .iter() - .map(|p| p.distance_to_segment(first_point, last_point)) - .all(|d| d <= epsilon) - { - (None, 2) - } else { - // now we test all possible cutting points - ((start + 1)..(end - 1)) //TODO: take middle min - .map(|i| { - let v1 = simplify_prog_dyn(points, start, i + 1, epsilon, cache); - let v2 = simplify_prog_dyn(points, i, end, epsilon, cache); - (Some(i), v1 + v2 - 1) - }) - .min_by_key(|(_, v)| *v) - .unwrap() - } - }; - cache.insert((start, end), res); - res.1 - } -} - -fn rdp(points: &[Point], epsilon: f64) -> Vec { - if points.len() <= 2 { - points.iter().copied().collect() - } else { - if points.first().unwrap() == points.last().unwrap() { - let first = points.first().unwrap(); - let index_farthest = points - .iter() - .enumerate() - .skip(1) - .max_by(|(_, p1), (_, p2)| { - first - .squared_distance_between(p1) - .partial_cmp(&first.squared_distance_between(p2)) - .unwrap() - }) - .map(|(i, _)| i) - .unwrap(); - - let start = &points[..(index_farthest + 1)]; - let end = &points[index_farthest..]; - let mut res = rdp(start, epsilon); - res.pop(); - res.append(&mut rdp(end, epsilon)); - res - } else { - let (index_farthest, farthest_distance) = points - .iter() - .map(|p| p.distance_to_segment(points.first().unwrap(), points.last().unwrap())) - .enumerate() - .max_by(|(_, d1), (_, d2)| { - if d1.is_nan() { - std::cmp::Ordering::Greater - } else { - if d2.is_nan() { - std::cmp::Ordering::Less - } else { - d1.partial_cmp(d2).unwrap() - } - } - }) - .unwrap(); - if farthest_distance <= epsilon { - vec![ - points.first().copied().unwrap(), - points.last().copied().unwrap(), - ] - } else { - let start = &points[..(index_farthest + 1)]; - let end = &points[index_farthest..]; - let mut res = rdp(start, epsilon); - res.pop(); - res.append(&mut rdp(end, epsilon)); - res - } - } - } -} - -fn simplify_path(points: &[Point], epsilon: f64) -> Vec { - if points.len() <= 600 { - optimal_simplification(points, epsilon) - } else { - hybrid_simplification(points, epsilon) - } -} - -fn convert_coordinates(points: &[(f64, f64)]) -> (f64, f64, Vec<(i32, i32)>) { - let xmin = points - .iter() - .map(|(x, _)| x) - .min_by(|x1, x2| x1.partial_cmp(x2).unwrap()) - .unwrap(); - - let ymin = points - .iter() - .map(|(_, y)| y) - .min_by(|y1, y2| y1.partial_cmp(y2).unwrap()) - .unwrap(); - - // 0.00001 is 1 meter - // max distance is 1000km - // so we need at most 10^6 - ( - *xmin, - *ymin, - points - .iter() - .map(|(x, y)| { - eprintln!("x {} y {}", x, y); - let r = ( - ((*x - xmin) * 100_000.0) as i32, - ((*y - ymin) * 100_000.0) as i32, - ); - eprintln!( - "again x {} y {}", - xmin + r.0 as f64 / 100_000.0, - ymin + r.1 as f64 / 100_000.0 - ); - r - }) - .collect(), - ) -} - -fn compress_coordinates(points: &[(i32, i32)]) -> Vec<(i16, i16)> { - // we could store the diffs such that - // diffs are either 8bits or 16bits nums - // we store how many nums are 16bits - // then all their indices (compressed with diffs) - // then all nums as either 8 or 16bits - let xdiffs = std::iter::once(0).chain( - points - .iter() - .map(|(x, _)| x) - .tuple_windows() - .map(|(x1, x2)| (x2 - x1) as i16), - ); - - let ydiffs = std::iter::once(0).chain( - points - .iter() - .map(|(_, y)| y) - .tuple_windows() - .map(|(y1, y2)| (y2 - y1) as i16), - ); - - xdiffs.zip(ydiffs).collect() -} - -fn save_gpc>( - path: P, - points: &[Point], - waypoints: &HashSet, - buckets: &[Bucket], -) -> std::io::Result<()> { - let mut writer = BufWriter::new(File::create(path)?); - - eprintln!("saving {} points", points.len()); - - let mut unique_interest_points = Vec::new(); - let mut correspondance = HashMap::new(); - let interests_on_path = buckets - .iter() - .flat_map(|b| &b.points) - .map(|p| match correspondance.entry(*p) { - std::collections::hash_map::Entry::Occupied(o) => *o.get(), - std::collections::hash_map::Entry::Vacant(v) => { - let index = unique_interest_points.len(); - unique_interest_points.push(*p); - v.insert(index); - index - } - }) - .collect::>(); - - writer.write_all(&KEY.to_le_bytes())?; - writer.write_all(&FILE_VERSION.to_le_bytes())?; - writer.write_all(&(points.len() as u16).to_le_bytes())?; - writer.write_all(&(unique_interest_points.len() as u16).to_le_bytes())?; - writer.write_all(&(interests_on_path.len() as u16).to_le_bytes())?; - points - .iter() - .flat_map(|p| [p.x, p.y]) - .try_for_each(|c| writer.write_all(&c.to_le_bytes()))?; - - let mut waypoints_bits = std::iter::repeat(0u8) - .take(points.len() / 8 + if points.len() % 8 != 0 { 1 } else { 0 }) - .collect::>(); - points.iter().enumerate().for_each(|(i, p)| { - if waypoints.contains(p) { - waypoints_bits[i / 8] |= 1 << (i % 8) - } - }); - waypoints_bits - .iter() - .try_for_each(|byte| writer.write_all(&byte.to_le_bytes()))?; - - unique_interest_points - .iter() - .flat_map(|p| [p.point.x, p.point.y]) - .try_for_each(|c| writer.write_all(&c.to_le_bytes()))?; - - let counts: HashMap<_, usize> = - unique_interest_points - .iter() - .fold(HashMap::new(), |mut h, p| { - *h.entry(p.interest).or_default() += 1; - h - }); - counts.into_iter().for_each(|(interest, count)| { - eprintln!("{:?} appears {} times", interest, count); - }); - - unique_interest_points - .iter() - .map(|p| p.interest.into()) - .try_for_each(|i: u8| writer.write_all(&i.to_le_bytes()))?; - - interests_on_path - .iter() - .map(|i| *i as u16) - .try_for_each(|i| writer.write_all(&i.to_le_bytes()))?; - - buckets - .iter() - .map(|b| b.start as u16) - .try_for_each(|i| writer.write_all(&i.to_le_bytes()))?; - - Ok(()) -} - -fn optimal_simplification(points: &[Point], epsilon: f64) -> Vec { - let mut cache = HashMap::new(); - simplify_prog_dyn(&points, 0, points.len(), epsilon, &mut cache); - extract_prog_dyn_solution(&points, 0, points.len(), &cache) -} - -fn hybrid_simplification(points: &[Point], epsilon: f64) -> Vec { - if points.len() <= 300 { - optimal_simplification(points, epsilon) - } else { - if points.first().unwrap() == points.last().unwrap() { - let first = points.first().unwrap(); - let index_farthest = points - .iter() - .enumerate() - .skip(1) - .max_by(|(_, p1), (_, p2)| { - first - .squared_distance_between(p1) - .partial_cmp(&first.squared_distance_between(p2)) - .unwrap() - }) - .map(|(i, _)| i) - .unwrap(); - - let start = &points[..(index_farthest + 1)]; - let end = &points[index_farthest..]; - let mut res = hybrid_simplification(start, epsilon); - res.pop(); - res.append(&mut hybrid_simplification(end, epsilon)); - res - } else { - let (index_farthest, farthest_distance) = points - .iter() - .map(|p| p.distance_to_segment(points.first().unwrap(), points.last().unwrap())) - .enumerate() - .max_by(|(_, d1), (_, d2)| { - if d1.is_nan() { - std::cmp::Ordering::Greater - } else { - if d2.is_nan() { - std::cmp::Ordering::Less - } else { - d1.partial_cmp(d2).unwrap() - } - } - }) - .unwrap(); - if farthest_distance <= epsilon { - vec![ - points.first().copied().unwrap(), - points.last().copied().unwrap(), - ] - } else { - let start = &points[..(index_farthest + 1)]; - let end = &points[index_farthest..]; - let mut res = hybrid_simplification(start, epsilon); - res.pop(); - res.append(&mut hybrid_simplification(end, epsilon)); - res - } - } - } -} - -fn save_path(writer: &mut W, p: &[Point], stroke: &str) -> std::io::Result<()> { - write!( - writer, - "")?; - Ok(()) -} - -fn save_svg<'a, P: AsRef, I: IntoIterator>( - filename: P, - p: &[Point], - rp: &[Point], - interest_points: I, - waypoints: &HashSet, -) -> std::io::Result<()> { - let mut writer = BufWriter::new(std::fs::File::create(filename)?); - let (xmin, xmax) = p - .iter() - .map(|p| p.x) - .minmax_by(|a, b| a.partial_cmp(b).unwrap()) - .into_option() - .unwrap(); - - let (ymin, ymax) = p - .iter() - .map(|p| p.y) - .minmax_by(|a, b| a.partial_cmp(b).unwrap()) - .into_option() - .unwrap(); - - writeln!( - &mut writer, - "", - xmin, - ymin, - xmax - xmin, - ymax - ymin - )?; - write!( - &mut writer, - "", - xmin, - ymin, - xmax - xmin, - ymax - ymin - )?; - - save_path(&mut writer, &p, "red")?; - save_path(&mut writer, &rp, "black")?; - - for point in interest_points { - writeln!( - &mut writer, - "", - point.point.x, - point.point.y, - point.color(), - )?; - } - - waypoints.iter().try_for_each(|p| { - writeln!( - &mut writer, - "", - p.x, p.y, - ) - })?; - - writeln!(&mut writer, "")?; - Ok(()) -} - -pub struct Bucket { - points: Vec, - start: usize, -} - -fn position_interests_along_path( - interests: &mut [InterestPoint], - path: &[Point], - d: f64, - buckets_size: usize, // final points are indexed in buckets - groups_size: usize, // how many segments are compacted together -) -> Vec { - interests.sort_unstable_by(|p1, p2| p1.point.x.partial_cmp(&p2.point.x).unwrap()); - // first compute for each segment a vec containing its nearby points - let mut positions = Vec::new(); - for segment in path.windows(2) { - let mut local_interests = Vec::new(); - let x0 = segment[0].x; - let x1 = segment[1].x; - let (xmin, xmax) = if x0 <= x1 { (x0, x1) } else { (x1, x0) }; - let i = interests.partition_point(|p| p.point.x < xmin - d); - let interests = &interests[i..]; - let i = interests.partition_point(|p| p.point.x <= xmax + d); - let interests = &interests[..i]; - for interest in interests { - if interest.point.distance_to_segment(&segment[0], &segment[1]) <= d { - local_interests.push(*interest); - } - } - positions.push(local_interests); - } - // fuse points on chunks of consecutive segments together - let grouped_positions = positions - .chunks(groups_size) - .map(|c| c.iter().flatten().unique().copied().collect::>()) - .collect::>(); - // now, group the points in buckets - let chunks = grouped_positions - .iter() - .enumerate() - .flat_map(|(i, points)| points.iter().map(move |p| (i, p))) - .chunks(buckets_size); - let mut buckets = Vec::new(); - for bucket_points in &chunks { - let mut bucket_points = bucket_points.peekable(); - let start = bucket_points.peek().unwrap().0; - let points = bucket_points.map(|(_, p)| *p).collect(); - buckets.push(Bucket { points, start }); - } - buckets -} - -fn detect_sharp_turns(path: &[Point], waypoints: &mut HashSet) { - path.iter() - .tuple_windows() - .map(|(a, b, c)| { - let xd1 = b.x - a.x; - let yd1 = b.y - a.y; - let angle1 = yd1.atan2(xd1); - - let xd2 = c.x - b.x; - let yd2 = c.y - b.y; - let angle2 = yd2.atan2(xd2); - let adiff = angle2 - angle1; - let adiff = if adiff < 0.0 { - adiff + std::f64::consts::PI * 2.0 - } else { - adiff - }; - (adiff, b) - }) - .filter_map(|(adiff, b)| { - if adiff > LOWER_SHARP_TURN && adiff < UPPER_SHARP_TURN { - Some(b) - } else { - None - } - }) - .for_each(|b| { - waypoints.insert(*b); - }); -} +use gpconv::convert_gpx_files; fn main() { let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); - let osm_file = std::env::args().nth(2); + let mut interests; - let mut interests = if let Some(osm) = osm_file { - parse_osm_data(osm) - } else { - Vec::new() - }; - - println!("input is {}", input_file); - let (mut waypoints, p) = points(&input_file); - - detect_sharp_turns(&p, &mut waypoints); - waypoints.insert(p.first().copied().unwrap()); - waypoints.insert(p.last().copied().unwrap()); - println!("we have {} waypoints", waypoints.len()); - - println!("initially we had {} points", p.len()); - - let mut rp = Vec::new(); - let mut segment = Vec::new(); - for point in &p { - segment.push(*point); - if waypoints.contains(point) { - if segment.len() >= 2 { - let mut s = simplify_path(&segment, 0.00015); - rp.append(&mut s); - segment = rp.pop().into_iter().collect(); - } - } + #[cfg(feature = "osm")] + { + let osm_file = std::env::args().nth(2); + let mut interests = if let Some(osm) = osm_file { + interests = parse_osm_data(osm); + } else { + Vec::new() + }; + } + #[cfg(not(feature = "osm"))] + { + interests = Vec::new() } - rp.append(&mut segment); - println!("we now have {} points", rp.len()); - // let mut interests = parse_osm_data("isere.osm.pbf"); - let buckets = position_interests_along_path(&mut interests, &rp, 0.001, 5, 3); - // let i = get_openstreetmap_data(&rp).await; - // let i = HashSet::new(); - save_svg( - "test.svg", - &p, - &rp, - buckets.iter().flat_map(|b| &b.points), - &waypoints, - ) - .unwrap(); - - save_gpc( - Path::new(&input_file).with_extension("gpc"), - &rp, - &waypoints, - &buckets, - ) - .unwrap(); + convert_gpx_files(&input_file, &mut interests); } diff --git a/apps/gipy/gpconv/src/osm.rs b/apps/gipy/gpconv/src/osm.rs index 4877165a5..ebc7071ef 100644 --- a/apps/gipy/gpconv/src/osm.rs +++ b/apps/gipy/gpconv/src/osm.rs @@ -1,3 +1,4 @@ +use super::Interest; use super::Point; use itertools::Itertools; use lazy_static::lazy_static; @@ -6,87 +7,6 @@ use osmio::{prelude::*, ObjId}; use std::collections::{HashMap, HashSet}; use std::path::Path; -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum Interest { - Bakery, - DrinkingWater, - Toilets, - // BikeShop, - // ChargingStation, - // Bank, - // Supermarket, - // Table, - // TourismOffice, - Artwork, - // Pharmacy, -} - -impl Into for Interest { - fn into(self) -> u8 { - match self { - Interest::Bakery => 0, - Interest::DrinkingWater => 1, - Interest::Toilets => 2, - // Interest::BikeShop => 8, - // Interest::ChargingStation => 4, - // Interest::Bank => 5, - // Interest::Supermarket => 6, - // Interest::Table => 7, - Interest::Artwork => 3, - // Interest::Pharmacy => 9, - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct InterestPoint { - pub point: Point, - pub interest: Interest, -} - -lazy_static! { - static ref INTERESTS: HashMap<(&'static str, &'static str), Interest> = { - [ - (("shop", "bakery"), Interest::Bakery), - (("amenity", "drinking_water"), Interest::DrinkingWater), - (("amenity", "toilets"), Interest::Toilets), - // (("shop", "bicycle"), Interest::BikeShop), - // (("amenity", "charging_station"), Interest::ChargingStation), - // (("amenity", "bank"), Interest::Bank), - // (("shop", "supermarket"), Interest::Supermarket), - // (("leisure", "picnic_table"), Interest::Table), - // (("tourism", "information"), Interest::TourismOffice), - (("tourism", "artwork"), Interest::Artwork), - // (("amenity", "pharmacy"), Interest::Pharmacy), - ] - .into_iter() - .collect() - }; -} - -impl Interest { - fn new(key: &str, value: &str) -> Option { - INTERESTS.get(&(key, value)).cloned() - } -} - -impl InterestPoint { - pub fn color(&self) -> &'static str { - match self.interest { - Interest::Bakery => "red", - Interest::DrinkingWater => "blue", - Interest::Toilets => "brown", - // Interest::BikeShop => "purple", - // Interest::ChargingStation => "green", - // Interest::Bank => "black", - // Interest::Supermarket => "red", - // Interest::Table => "pink", - Interest::Artwork => "orange", - // Interest::Pharmacy => "chartreuse", - } - } -} - pub fn parse_osm_data>(path: P) -> Vec { let reader = osmio::read_pbf(path).ok(); reader diff --git a/apps/gipy/gpconv/src/svg.rs b/apps/gipy/gpconv/src/svg.rs new file mode 100644 index 000000000..387a0f7a0 --- /dev/null +++ b/apps/gipy/gpconv/src/svg.rs @@ -0,0 +1,86 @@ +use itertools::Itertools; +use std::{ + collections::HashSet, + io::{BufWriter, Write}, + path::Path, +}; + +use crate::{interests::InterestPoint, Point}; + +fn save_path(writer: &mut W, p: &[Point], stroke: &str) -> std::io::Result<()> { + write!( + writer, + "")?; + Ok(()) +} + +// save svg file from given path and interest points. +// useful for debugging path simplification and previewing traces. +pub fn save_svg<'a, P: AsRef, I: IntoIterator>( + filename: P, + p: &[Point], + rp: &[Point], + interest_points: I, + waypoints: &HashSet, +) -> std::io::Result<()> { + let mut writer = BufWriter::new(std::fs::File::create(filename)?); + let (xmin, xmax) = p + .iter() + .map(|p| p.x) + .minmax_by(|a, b| a.partial_cmp(b).unwrap()) + .into_option() + .unwrap(); + + let (ymin, ymax) = p + .iter() + .map(|p| p.y) + .minmax_by(|a, b| a.partial_cmp(b).unwrap()) + .into_option() + .unwrap(); + + writeln!( + &mut writer, + "", + xmin, + ymin, + xmax - xmin, + ymax - ymin + )?; + write!( + &mut writer, + "", + xmin, + ymin, + xmax - xmin, + ymax - ymin + )?; + + save_path(&mut writer, &p, "red")?; + save_path(&mut writer, &rp, "black")?; + + for point in interest_points { + writeln!( + &mut writer, + "", + point.point.x, + point.point.y, + point.color(), + )?; + } + + waypoints.iter().try_for_each(|p| { + writeln!( + &mut writer, + "", + p.x, p.y, + ) + })?; + + writeln!(&mut writer, "")?; + Ok(()) +} From 17b649560df2a122a10fd4abacfc0de61cc46611 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 6 Nov 2022 06:03:08 +0100 Subject: [PATCH 073/106] uploader --- apps/gipy_uploader/ChangeLog | 1 + apps/gipy_uploader/README.md | 11 ++++++ apps/gipy_uploader/custom.html | 58 ++++++++++++++++++++++++++++++++ apps/gipy_uploader/metadata.json | 13 +++++++ 4 files changed, 83 insertions(+) create mode 100644 apps/gipy_uploader/ChangeLog create mode 100644 apps/gipy_uploader/README.md create mode 100644 apps/gipy_uploader/custom.html create mode 100644 apps/gipy_uploader/metadata.json diff --git a/apps/gipy_uploader/ChangeLog b/apps/gipy_uploader/ChangeLog new file mode 100644 index 000000000..28f11c1c7 --- /dev/null +++ b/apps/gipy_uploader/ChangeLog @@ -0,0 +1 @@ +0.01: Initial Release diff --git a/apps/gipy_uploader/README.md b/apps/gipy_uploader/README.md new file mode 100644 index 000000000..d51c8c0a2 --- /dev/null +++ b/apps/gipy_uploader/README.md @@ -0,0 +1,11 @@ +# Gipy Uploader + +Uploads and convert a gpx file to the watch for use with gipy. + +## Requests + +Reach out to frederic.wagner@imag.fr if you have feature requests or notice bugs. + +## Creator + +Made by [Frederic Wagner](mailto:frederic.wagner@imag.fr) diff --git a/apps/gipy_uploader/custom.html b/apps/gipy_uploader/custom.html new file mode 100644 index 000000000..69c0a6a84 --- /dev/null +++ b/apps/gipy_uploader/custom.html @@ -0,0 +1,58 @@ + + + + + + +

Please select a gpx file to be converted to gpc and loaded.

+ + + + + + + + + diff --git a/apps/gipy_uploader/metadata.json b/apps/gipy_uploader/metadata.json new file mode 100644 index 000000000..0ba1dfd49 --- /dev/null +++ b/apps/gipy_uploader/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "gipy_uploader", + "name": "Gipy uploader", + "version": "0.01", + "description": "uploads and convert gpx files for use with gipy", + "icon": "slidingtext.png", + "type": "app", + "tags": "tool.outdoors,gps", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "custom": "custom.html", + "allow_emulator": false +} From e2d6e3547933057fccbe9434a24d9ceac2b72a32 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 6 Nov 2022 06:12:27 +0100 Subject: [PATCH 074/106] typo --- apps/gipy_uploader/custom.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gipy_uploader/custom.html b/apps/gipy_uploader/custom.html index 69c0a6a84..e8b9f4b77 100644 --- a/apps/gipy_uploader/custom.html +++ b/apps/gipy_uploader/custom.html @@ -44,7 +44,7 @@ sendCustomizedApp({ storage:[ - {name:gpc_filename, content:gpc_file)}, + {name:gpc_filename, content:gpc_file}, ] }); }); From 4ac9560967cac0e2fa1cb06b9c84b316acf5f47f Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 6 Nov 2022 06:38:28 +0100 Subject: [PATCH 075/106] icon --- apps/gipy_uploader/gipy.png | Bin 0 -> 1606 bytes apps/gipy_uploader/metadata.json | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 apps/gipy_uploader/gipy.png diff --git a/apps/gipy_uploader/gipy.png b/apps/gipy_uploader/gipy.png new file mode 100644 index 0000000000000000000000000000000000000000..e9e472f5ce52c633463e5291590a3f8be81abae8 GIT binary patch literal 1606 zcmV-M2D$l(P)IQ3e>p z0Xz<%`|Xbn&~+G?24HufHEWh+$xB8yZ#2ztY8n7^U3aCOKT`lu0l1YZC3upF;7)A?#Cl%ocKpz5V6hfT1bn)f|y}rK9s&=X? zr3(L>2Y|tm$kiZ#`6|An5eWG5_Yg2qEyz>+)$4aU(Oum-ZalHr0U$Xl)W#&IE=k|p z6rCgNb`tdFWyCWN&{?PE0pN*qX`d44S4HUeYvH!fR5`W_kR)D@ALZ`li)W$tSNP_Pu);1oko7EpIdRp1dF6+)21mEspF+eR zStb*n2ILnNi}F`hd%dP3Cje2QHl&)QSfq`PXd0>7{N_n)x=UJ-jdWUQx0f%p52B^a4*+O8G zcYF2Ki}$eOFA8or!z{-F_}i%l6EdaLz72Ri)12hr(+J!_Wx<>w zQpy(CU72FdG^Dm^^WLnjy=EiF8p;p2Uj~5UjONc(@t`*^gGp-!*w&d=ffEs??2?*w ze_A11Dxm5}cF%A)asq%R$u+bjea|@+FR;inmY6?=we5x({vyUCe@)$2iHHfG0iAVm zGaPBatxa4H!QoBYw&iFl?&8q8eIS|zK-#u^H+nIlC9uddHW4vl0jUR%=>)VBqRi91 zuf*N<7KPSr1>$duZ6JjhyDPo3$C!~!r%+jI)@Z@J4?y%UE?Ka9$xF@3QxXFd^m!Tp zo^H>x(&BX?jP$~)4PUFGaYvx)NOn)bya}=90YH$XhO*Xd<@!$m|9d&&0ppZ2)9t~X zr4-v%&7J$ODyl5t*1R`1!Tj*jLvUoX-jb=@*XZ^2Z7f6{0JiNp7Eig7jgTr}I)wrU z-9-!v#0eJ_k8BDi>N()^Y<0S4ih@BJ2Kp=OerewrcM4UuOzOsrQWL>t!NaerPEU71 zu&lFT(hM3)_+#|ru_M}zr679LM6kOdwYL-XN}5c#mRp8I%(AL1psdB+7eosZ3NpNb zL*pMerO?$ImJKEXpW#@^b+eFn-8W{oj14**jFt!{UsKACBjMSF=M zcwbDDil?8-=>%|1B0XjbRX4fgZrpBPsifO7Y0{kr-VoiWEFks#wL$;`Er<@dktPaM z0X;W287ynjL1zvCjRm3gKSejj6c9W-p$~v%)^&OLs@949+qdYtF1mIV4Irp4YVCt? zDI(dY3dIy)7)Ai#E;i&-G>(+r`4TM5>6~`{%oYZ{=D?~7VTO34WX*cz8N~ANTDfQ{ zTR6M7OJEm3!|eN1B20u_f2eMAJVJ+EFa;ORejb8$GSDIGJ_G-NXj)NdUCheU9(D@& zK3*W;%l9x)gV?`@oPn`NxWaGwD>t6)5WNR@fcqH#0oLvGD9P1zN&o-=07*qoM6N<$ Ef?_oAlK=n! literal 0 HcmV?d00001 diff --git a/apps/gipy_uploader/metadata.json b/apps/gipy_uploader/metadata.json index 0ba1dfd49..24c03ab87 100644 --- a/apps/gipy_uploader/metadata.json +++ b/apps/gipy_uploader/metadata.json @@ -3,9 +3,9 @@ "name": "Gipy uploader", "version": "0.01", "description": "uploads and convert gpx files for use with gipy", - "icon": "slidingtext.png", + "icon": "gipy.png", "type": "app", - "tags": "tool.outdoors,gps", + "tags": "tool,outdoors,gps", "supports": ["BANGLEJS2"], "readme": "README.md", "custom": "custom.html", From 7c4a53b01b8b38dc5840eebeb21b7c7239a7d6fc Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 6 Nov 2022 07:17:06 +0100 Subject: [PATCH 076/106] adding missing files --- apps/gipy_uploader/pkg/gpconv.d.ts | 39 +++++ apps/gipy_uploader/pkg/gpconv.js | 184 +++++++++++++++++++++ apps/gipy_uploader/pkg/gpconv_bg.wasm | Bin 0 -> 204497 bytes apps/gipy_uploader/pkg/gpconv_bg.wasm.d.ts | 8 + apps/gipy_uploader/pkg/package.json | 12 ++ 5 files changed, 243 insertions(+) create mode 100644 apps/gipy_uploader/pkg/gpconv.d.ts create mode 100644 apps/gipy_uploader/pkg/gpconv.js create mode 100644 apps/gipy_uploader/pkg/gpconv_bg.wasm create mode 100644 apps/gipy_uploader/pkg/gpconv_bg.wasm.d.ts create mode 100644 apps/gipy_uploader/pkg/package.json diff --git a/apps/gipy_uploader/pkg/gpconv.d.ts b/apps/gipy_uploader/pkg/gpconv.d.ts new file mode 100644 index 000000000..e97d230c3 --- /dev/null +++ b/apps/gipy_uploader/pkg/gpconv.d.ts @@ -0,0 +1,39 @@ +/* tslint:disable */ +/* eslint-disable */ +/** +* @param {string} input_str +* @returns {Uint8Array} +*/ +export function convert_gpx_strings(input_str: string): Uint8Array; + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly convert_gpx_strings: (a: number, b: number, c: number) => void; + readonly __wbindgen_add_to_stack_pointer: (a: number) => number; + readonly __wbindgen_malloc: (a: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number) => number; + readonly __wbindgen_free: (a: number, b: number) => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; +/** +* Instantiates the given `module`, which can either be bytes or +* a precompiled `WebAssembly.Module`. +* +* @param {SyncInitInput} module +* +* @returns {InitOutput} +*/ +export function initSync(module: SyncInitInput): InitOutput; + +/** +* If `module_or_path` is {RequestInfo} or {URL}, makes a request and +* for everything else, calls `WebAssembly.instantiate` directly. +* +* @param {InitInput | Promise} module_or_path +* +* @returns {Promise} +*/ +export default function init (module_or_path?: InitInput | Promise): Promise; diff --git a/apps/gipy_uploader/pkg/gpconv.js b/apps/gipy_uploader/pkg/gpconv.js new file mode 100644 index 000000000..58b6d4f26 --- /dev/null +++ b/apps/gipy_uploader/pkg/gpconv.js @@ -0,0 +1,184 @@ + +let wasm; + +let WASM_VECTOR_LEN = 0; + +let cachedUint8Memory0 = new Uint8Array(); + +function getUint8Memory0() { + if (cachedUint8Memory0.byteLength === 0) { + cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8Memory0; +} + +const cachedTextEncoder = new TextEncoder('utf-8'); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length); + getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len); + + const mem = getUint8Memory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3); + const view = getUint8Memory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +let cachedInt32Memory0 = new Int32Array(); + +function getInt32Memory0() { + if (cachedInt32Memory0.byteLength === 0) { + cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachedInt32Memory0; +} + +function getArrayU8FromWasm0(ptr, len) { + return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); +} +/** +* @param {string} input_str +* @returns {Uint8Array} +*/ +export function convert_gpx_strings(input_str) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(input_str, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + wasm.convert_gpx_strings(retptr, ptr0, len0); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var v1 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 1); + return v1; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +async function load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + + } catch (e) { + if (module.headers.get('Content-Type') != 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + + } else { + return instance; + } + } +} + +function getImports() { + const imports = {}; + imports.wbg = {}; + + return imports; +} + +function initMemory(imports, maybe_memory) { + +} + +function finalizeInit(instance, module) { + wasm = instance.exports; + init.__wbindgen_wasm_module = module; + cachedInt32Memory0 = new Int32Array(); + cachedUint8Memory0 = new Uint8Array(); + + + return wasm; +} + +function initSync(module) { + const imports = getImports(); + + initMemory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + + const instance = new WebAssembly.Instance(module, imports); + + return finalizeInit(instance, module); +} + +async function init(input) { + if (typeof input === 'undefined') { + input = new URL('gpconv_bg.wasm', import.meta.url); + } + const imports = getImports(); + + if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { + input = fetch(input); + } + + initMemory(imports); + + const { instance, module } = await load(await input, imports); + + return finalizeInit(instance, module); +} + +export { initSync } +export default init; diff --git a/apps/gipy_uploader/pkg/gpconv_bg.wasm b/apps/gipy_uploader/pkg/gpconv_bg.wasm new file mode 100644 index 0000000000000000000000000000000000000000..f71a5c38cabe4d54fe866224fc27ee2d46d21274 GIT binary patch literal 204497 zcmdqK3z%P5S?|9uzx!oo=h`%BoBjJWZ6~yW6iU(?Fnes0rll0HDkw^6xy}@5Lge5% zC7D7fqeO`kG+@*LD~*~S0>pk_%qQ(x5hhm>KQq za)Vn-JmV%*fwHc-u{`eWz3sPFEZY^@err$^fp}ay-StX zT`k*HCcG=W>(;8v(6e~E18uv3b=O_Ntxf+>2?bOQWvK5i`^W15x7{_W0wr3tW~qNW z&GVF6N-C_fJPmG*bRB;(o{UT7Ql(UiqEb+bN>Nx^6qc*CkgK>f7za@lgi#ztQCKdO z$t}e^iONYSBx~`Kpc+P{QmGME+EHm~7&e+=^_-Zpjez&5BaFgjyjcoM=Z2JOmcqC^ z7?;cCHg)o^6o!@Z@@grL!WB^zH+YA)qe`gy&nuOJs2&DUTnej699N_0{7M*I5LOy1 z%Yq~=EeOK}R1QqMsM`2PWl@{Zb#uj z6o>V&R_hEz1C_8I@vfd#YSdh!Lkkzi^?IdLr(5KgL!QO_l;T`idKWD6|Jf?n*dr4?+oAm_A<3DsMMj& zYFw{_`V&#G==OKM7*2&7ekD2_{Z@48%b$qG<6nz@?0-c69Q|zkNcfZSFUNly?FyHCB)a66c>Cve3Djo#uq7xA5NVq;e zPMS?cqsa*hu8xmHo!gX7MV;GKLo}MDSt&UZMZ@}29fzZCV?&$Pvx7WcW>1xWFzSYu zKN776TMnANq}kqhv|CMnnwEy5y>XX@vPrTAhN7SwWxM|T!61vX4=Y)fjHY24WuMbc zIXkT6e4qE&H@OKimn5TElGT#0(dJ07J`U0_tBty=Qa0Wg?MA7pUl$3^ljY*ksov#u z@A7zgGz+c{gi-pwH(no4*Fg^zp8%Umj|!{D|NcR0{IV`dkC3Lft&eR0ZeJf8;_rUn z&f2=*t9sN}7fn?i*JJ$5ozm zuTOU?4X+OVR^<*=N7Jb2BjE1`=B#;jaI!|q{Ry8QRT^C#e3o?N?(y;QWPS9Iu1o8q zLq5G%Dg8Vm$S6XYcuLnmcUaJjkC*5=rLy;xt_crvk-l`zqn(AM z4^&1wKS25k?q5Q>hx9_FrP0n0lKwR5OGyute<>*#=v+qnG15f&_0i4{=@+TzWu*5~ z{zlSWq-#jWNxP(v(eCp}4|D$l(!Hc>Nk2n9!=(3;W~2wmf4S1~Xy=DX@237mq{phG zogX4S3DK-1{aM<(oD}ZeSwi}|r^N>eW)eGCvZ=N};5 zsFbd}juaWADt?s9ovqQ%Ye^?bSCjrERa`>)MF2aObPxIBLI-&DD$+^Pt4XIxZzhGt zI-5vO2BV!HBOM;=yq@GJuir`v(RBu)hwCHHO~eOgaGBHIrRS{)0nTGZaOmW zY&yo-@pv@(qWI|%E{wq*&M)@o7bj6Y?9uW3;z)jh^1-9lO0~DgruEGBmK5zR)I49N z*#ZCPmg(4sqqLm7wzU9ewFXw@*iESm2g_QX*L{jAQ6au0Mpw(iO)Jz#p8t2{$KDr}FkP zgOmDemjC<8ulAR(TK?Dnl>8a+R4o5XDnBD1Q33pas{9@vsJ{dv;ZEh`SO`~)&g)pR zOr?`OaVIhkl~o=3mlfO>KHTHJ@U(Kyhou?7@uaQhBiLHnr0J3kMaY#Klqi>~Uy2Yk z8O=hRQ5Rvgsl9kPQ0;G^sHrk|1ea8(%g;0Dq4XkdP-^1I2ALFlz^5nb?*5ohkB~~) zfgwYi_0ew2Qzydb22~lR*IQ+pa}ApK=#=RXXz@9i zs^&1tjujJEUO;hiWttd@jzrx-(>o9x4q5sN*ymqx>Av`C``jiegV2t5hFL!#p*dv z-F@Wl^HM|6GSseMleFw*-40jpYC~oyHMI?CXTw%%s!>O3b#$o3u1voISriMiRrV~* zF7xfU`^eqr?Vt`hz_0YB+ z0y$BWj)Y2&q6*1rV3CG!{@G&bjKzzlM?S@UL-#`e z+MIc7wqF53h?{V^whJeQwV0sOq@*lt{4{;m5FhOIJ;yIp;<^h%Kw0aj6GtJ&lr?)# z)Ql{y*8IL&^E-up^b~DpH3S2pyXV&AZ)Z?rAdyJlEJw{J~B%iZAUZEZ{L{k-)9`fmarH(^*N}k0b`gsF(1Ka{C z?<0hM4lx0zkjg!WLp&S)P|ND#4>hNx8h^-85b>DASp~VNX)$io`Bb@e(R{3;K0mAI z&D=O$Vk%J1D)PF=)rPD4aW%snJ6UdB*vBd`b_>=sgH`B3q4&Wmbl;o$lD623P7l-k zUFABW%T!bORGF#FJH4rV8kjT|(>>Vo{y*)hj0RTm(*IzkH=j)mXg=?+h`cX9D--(i zC!NqsiV3}>n9xg#3B9CmLhr7$R`n*b$97J>t=t837>3dlZ+&v@V3jdhdq=m1#&X+z zI5oOUJ2*Kc1tL@ujTJunuad055$_c8SffvJBx;`XfcgO(MNl z@UuEHcjQeXz*q2-->0weS0-9c)SUJp9#sy@K@bO?Fq3;iMol;HMS&xn4?!GGF9B>^ z!a3M`sv6-+`a&A=0@X-qMiKD&LbV-FRORbyW>tDrqXpI7^|fPJ`T6kgk<(nThpI)T zl37#C>Ir#ho7r+-gTYu!oAbkFjnybxZKmtfk<~%49=mKk(-p&fgrvwxMuB5Q*vre{ z#%YoR{#;HaZR*OjfW085R@_Z;SI+O!^>ziAZCs6U*2WiJRr?pEt33WMPQTCV1s92N ztm4O`?jnuhs@@o0q%mC88^epmhQ6;~uV|@y@XM%7STh%&6@F(8^(!zEqfS4;gT|#- zebjZQ895C{U(IU+7G*^g3B(*J)~so<$Bz(jQ7!W<%WCj}$^D zz8Hu&!=sZ6l$in)9{0V)EgCqz!ZmBX>bZ38m72c4tWL?*hBV5q74Z65o?e>#lb+K9 zOr6)!Ps?r%P)+Jat(;Y*COBq#57VfLA!QirV*_f{@c}sb@=e%zxE?ENQ$F=VtC?VG zoUEi9r0i`F)ySQ$ryDddCo7n)HmNU9FV1z+Y@ET1Glo!ltJiO*k)$@l%JYLvbL2q) zg??+|P&AU)rA8*I(r*Fc48rj7|Pr6V-Iu!FH|&0s6~r6PYmH9>ah!lftxY z8}gfzKZ?>TH4dwjuS#ngFOQ<~i>#No%=0kIE~Ln$xn$&?I1(E1fX>M}tDiI8PH!8D zUe%V@Rt|=8dTX}VmqxNC-=Zze#Vb=eXlc|>q923wTsir<@`;IuyfBt9(+2bD8}!)& zW~bx^kQkqtE0CXAIxYiB1DqmbuHiG&iaAFRK$k2KTYAW+`;|JK-dmF>MMG#IcrHvz zpg)kFfS04}^g!42NtvGqD3-RcCoR=_&mCPBB6xDhoHEfcqL}Fw*@6ykW%Sz@GNDS? z2H9l}<>H~}I6$I?Gp}p2LPaBf<;YFd-viS_(N2h9D4M8CRI?H`)@eGaX=O21rHI4v zfW_)>;i|S;B=-dJ!3u(`EPP0f{dPD@i8GcCI9f%ZnjA=`nbC#XH~AucqhHtbTz>>?+}O4h`+{xDD*?@J z)WB`*XWJV!A@>{G24k1>33V2OfM}*y;;`k-^h(Gk%BQCDN?bDAh|wBWF4T!Y$c6ei zZ}tmy$p(NdJZkbxOqg+%&sm3E*JqoxjJ$Na&Ss^|THY?$jaGCzOEnv-3|+<6b+Svx z2I6uhz`2NqIM&reW=<&AY%{BEZo@5P>7-f&lHG%eeIrF$2Sys(ySI&`6ZdIh`gz&P zAK%oLhPV_{w1aZr*t`O>B)B1cUfLS#w$kTa|1P{(ti4e)+9Aa{9p5Ai(NbNmNMGdj zv>w*4kY4s8e*D;7p`m(FPcNH;S%)G}#sWHqahz`UM1~22JPV%yk)Dv>I1x^xt#%WW zLvYNQ(Wvk93`1;)bU1~_HSS1jiU4o}DXgM8<)ezn(QvPv+Ra7^bA}ZrJ>RfK3}~7^ti=(GpBq_-DKcAUkOjOxW7-yZB8vuj%-=jugL<-T z#BgqUiPzRlNQ-3sy~+88f5l{cRNO!zVSZCk2%yds>wStvHO2jwH!pXoQFKpbfF+S9 zigql_s9ZIB2d(21HRlRhrU&MmO_zy`bIM&NGR`S?nI>bJH>n_@>4w4+dXCN6T8UR| z03`34>SIRsWl*PicJmTFNRZw$JhMyuWX8xhw%2Fw6vhr*aJ&W8l0MFY_i%bM^5rhw zj<-6JlS2^;CarAM9cotc-mD_g1QgDa8^MxIw6xgWIz;1LFu8)hN&2&+&Tb3$v<>a5lf6$js$QpXYEEwz(Z{`9$Go1D5FpcD!sb<7TF>hS3 zQ63}Jze=CSQrS#oG1w0m&JBwpifgTfd7W(^Cy*0CFMMq<&0~+uYf9{qTa`PKuTCGu zfuaDkLIazuv+y0an1dF0mE;!fvyHn-di>RcBI}qX!eAsuaSM#Wyxfq+=uz2de7eP!! zMa=Cx!AxkO&*{F)Rp!Ut#T}h_-;1~{1)tBI?~&Y%&*{Fmdwq9a@iXsx5yeLQ%G|!6 zjAZ0Ir~7Vx14r}D33KLscVT*v4e>{F`+hiTVPHO|`)-Z|>w7|=K)NhfYZcjrCcVku z52csoPjdOF=o5?NuH?&zyT$YJV%qpnsLXLj_*XiVKT{QwgA?&c*txxh2{HpmRYHW) zK6z#m42!g$tIrlcjM>_kVN#svP~U_}aW&y1iV1;66EKl47VM{ag7Vtm9n|9BiN?C% zK6AD0Y<6x?7vJCPT<^P@2d(EFn3nsCm+q^t3!aEvur_8xj{Jcmlf+DatvRZ9rteDp=eJeb80Bs9m$Be1ug8q>UK|Lp2Q(-Fu>az-C-7%v#A0M zSlj^#hBpd-ZkknOPFZWdtqP&NWYA#IoLSdIZbfeG1IXAelfzb3ZJ~eJcUrE^4Ap9i zHCMJf#D|p<1G(1+3?9%uevL_gzssld1{6Bv`G8mR29GzLH~8^}^9D~hI!zaMJewRa ztZ~enoAq9$cwsa>)H@&OfwA^WQMUpTD|nPxl&x>*@u`m!quAi}*~k{q|xO@~T?{28W7NjzU{qrB94s5fO~VRq=7B zg!B5SjCvmA>y|-nYIFzOO9RPkfe80d+6DcE8r!wSRr%1k(5AWVj_o#;#3$U_GjE-#SQy3?yL|#ca_WsB%|vGv$f_74a7d(TpdX3 zw&aQd+jQ?UDpnt(Qg?V*4q8^of6GM7SH$yFt5+p(k@vddfug8ReZK2wY{%i_8}s^{kNC8C`B-_di$G!v$#`(A7dQ5D=jqVg8D zrg;bhtN57X#jayXx2|%A2^(kEKY55NodH=8xwhiMYMj(E1i{xpY#-UqBG@8&HkC^V z7N!M)N0makCu8R?)UYq?ANGa4VK=kI4ST_;-LOwIHJmILG#TC|Ll3NJt+T+ntlcDY zn0D6GnZRpvfekn7AQlJfw1@Sv*w*!pM9(6uJrOY@`0Y$!wJB?`PQY!1*PTki>v+kn zRG($M_Hlkkp=XX)<17a29uMno59_Xyn|fy%R%6(X2HfoFqglv#2F;pd$yptnm_f!& z5iQN+b`kSr-PZW^&b_#e_XD?l1NXA`Dy_+VDqgxK_=F-#b)VL=@$Fl-!YV582a6bR zaJX692gg&)9pk}$! z!<5)8#U3b0Wq3iZGEBkVty+Y1zsewQu~Y^oh&EaDG>0W#PpaU_(Q*cb2%Sn(cZDv6 zw_JJWv9{)}wEN62Vv&CZs}NpdRamc1pC=8fl3gVwD;h6re_AEGR(HYs{oUy_lBtr@ zNVLS|aoZhGn@%>m%T&lMmaa~RUUg7{)6i&)6118e1kENFLA%Md-C~{&%^D;RrkJen z0PNRk3`WS#7d+k2v{;f}=qSL&tHIXm^z?(5M%)Wj#(AFXXI|4H?LVHK*KCXiddF)}-h{8AXB$nOhm?*2$Gw2Bwe)tsg# ziafwxC8hh#RCqCr`oFpCj%-ZWw89o14$M?ou`XK7WM956!c0h+KiW06Ks%%=DcoF4 z{QGTAYeExhWUK)bXcZ5rYB!Kv#O#F$^rkpm0w6!aBTa1EiK=xC(u#)T95A9a z`w#si{)a3Nbe^+=PeS?g1Jv|2zIe7X}5IY{1Q zG0BsgESM^@O%@50r)jeIJ$wJxOqPb3EJtK{n92L3BzL1TS^iiiOT$c--?tZ^fyts> zm~IbClWo_UM~a~)31Z!DO8;fw`iBZaZ>5ts_mOwG;(O5yeA!^2ptXOCB{)3B&T?}|OE z#)275XBpN$*J=MwnKQxdlNt-=nmfy|_8E<5)mSit=`6$Ab4_~F`plJd<2pvKaskN* zlYt-|^WKs(ZqG8T)_=InZv1xgL`{%-`t>v2HeKZA3`|Nob?*!XM2XkcEJn&JV8|SI z42GOZ(j29~`Td0%QR%kx;hAo0m;Wh2r81ig4O$_M+0WQk0|j3x&|RJ;yi64F&Ly`4 zF`CD@^e-k@=i6@R``_<={FvI{j&@*)2IkJDV*t29<($p#b3i$I5nM7+iyLwL&FXzaGN2T&fPjubr#s} zD$!|8`CHSa7U$xWHS`^9C3Jc=>B#Y_BTZbGUD zSkYlpV^jug%V0!ga?as+Vnr7Gr9I$hr+*cORQ;2lVfLisbfzC3tT>ndfr@ka@2}ts zDck8F?zo~M78|nF;n5mMFt3so1Wi8Ac)G=8P9Q-4ThM6mqA`h^_rQjx- z2J8p={WU*eJ{pl{`Z6<~jednk-C^LUwZj#sI323YQk>2flKPY}V+W@jy@QtREO{vVLGwmh}U4 z$oba~1kMy2tH7CNM!$d=$I5lP@XTI|PKwaHwf2pQ$qNgopJnAvdMkIIS-IsRhZBXH((C!*;Em?BE7lE1f;}r3GI(${BHPInRYZZ-O|9jV3O; zL$Ik3b{y{V#i$KpIxtNR_aOF^y7O47BA{%4p5sDJsDR>b4_lT;AK3{n>XKpo%Mq47 z9NZUj;Tib&O8B&T-@9K$zd#Q9a2&M(K<7{KfwH{b7EeKNr-y z+oP*EOA#>2U4g;Xv2wS;nNwVSnLOTxp()6DWXA_a#yK=`V)x|K_>T8?Zi|Q0MroMe z+g_;-WK-c7yiUQ9;Ubb*ZZ>g-Pv1_Is@6VF+n+yEmo?fj{%O)b6K@K ziz98u5bd{(gzrsnqh+hdfMzpF(}BwImbGLT*du?JgeK`krPJ`X0CzhpI?obyq0$}L ztiEQ`M2*zbMk)e%9}`Vab(|zV7f#+Fi-ue=6~dsSP$}DKM2rU`+^h|Wl-@ab4$K49P6!%B_ndDtCVVF^pdJn%b9;?B zztilEyp;o0J-x%Vf_SycA8?gxf3(^<;@iTjC?-8Pltnd&!FjasPbb+#I6PDWp1lXFi zC}}sk4mQok@1BC;+1aK%VOTlLQky!#zjiaRVxSBN0jo*}p@M2%Lj_TCp^9>{*$JTr zx3gFzlSOCX`2$9ENA*}ryoq@evF6~fC%N8RjsV_HTBUz%!k|S*32XoloqL&+kglf> z(MI;JT8DUWBrwaals$?%8fM1=?NUd3WukC)=j!O?Y_%7#F#8kgF(56z}KJX{Q zi~(bFurU(8O3+^#Pc_@s%ia*&}*0=0h?yBiEm)_Uz+WPP?Kr-DvY?h z0V!c<&WO#%Kchk^C#`avYIcI3$0stc-~=geI>a(<9_v{<=S}7s!f@H8%QiQJU$?+l^M;AdgvtJg{kF z^A|PNG1zgKRJPORA&t5rI$YKcUOGM$onRJakC+bZ;bsdEgr4W<1$Z@*Qh0jYvbkVy zPXX$LU6Z-1FLRGwcL(&|0MFojdI45OpYj*(?R()so{8VbRc!+VpY#~gIlr*YqS;67 z+RtY?qUVqFJ%7xu$(+tJZQOQMiDD{cdmKi7Dm3Vvmnq?(TROYfN({k3Bcb4-PocsUxcz8`8xauoUVW zw{8`{(WUT`20x$BCDW3hPb43Y;RDjHm8Bw|NbCO6o~Cd4ME5P%En9QlE#K*!J3N`b zC4I|vf&X{=3Lbs%y8m^uo2!f?q^3k8&U!9s#y5VCd5yL#q97-xWWCR zU^zD6ZxdW!8t;yhx5A4wd@!Ue8S4rTtIxnvJtJlW+i}twq-;7oZKc!jIJi5Njs#7N zQ58R#7l)G{nGMBBd*M_F6s7FTVu^jS*+}CNUUAf+Clc2xX%>Y^U{5p(UO1xO8)xpa z$O)5+lJj7$yvF>Ou(t{)J}{?3N{HcTt<>u1yBaz$!ky)lOXSnS4~9z=ZUgUM!Dpcv{Q8G$$D1m zEQmE}&=S}aO4G+fY`%=KCF`JM!M~UQdN_obsN^_z*-^@bR$@ve?qzGYF&ZS7F+Mbl zbCSQa_x8m)wKJcX2pyPNSN0Sw8-YaGZ~Vn?{6%mhG7^pN4y=OUdaj{U2tawismGh8 z_#Oc@NX$?N%vWsoQVhf%1u`ihUslSX93d6;7C=0}W>q#PL$L@WL|>9%3A7p*}`QMO-Lka;34!s#L;oLz7F(-DmZ zXoI7xdtxBEyxm#}VLECv?1FT9ju4h+f@{T&FtPJI&74gY0ZB6#wNdDbM^B~?^Q>hq7 zX1mN!u%nAZ46BUnFh#4ND=beSp74&S3#NZ~L1vy3w17rW=|Cfdl`>KdIN>tv3b#|a zlrDBOnVo<$bu|0|RiQt4C0|o-LSx92-K6Fo5zpgZs)+9Or2)Zrq#?CVKr2_=c|_;l zALYi_Bq!bLvjCQP=lYmqU2JA!D>4-vvHEWn9OI~J=@$%NM~#DA9aT*jbcfyDopX#< zpcCPrX{Q*GeU)e!^qX8RWjn-ARZ1fwQ#XxnYzLI!0BU7Cj&bj^7^a4&Mj$do@2rg7 zNcb>AxwpcxCzPKqi+0rM>2g}v=@Vo=MsS2F{#H(Wc@pBRBwanc*Hhah-zy>h+$d!) zkDJL~+spgHZXFt-%eOSW+;U2RzU63u4ZF5}ux^TN2U^*U{Js#NIVUxElNRv6B;j;c= zOB1@Dme6}>cBAVnpCP1Wn^L*ZAWH=2zlDDI%bZGpVF7om1rv zoDjEQ<5uBW(1KTkmQ$OIm>$>6z)~%X;vsHf7HRi3JK@1KmM$VeXhf()VuP@E z-cxA^K{Y{wI)XMOP8%8+Fvd>B<5bMX(`2=Z?nKD>!5t43m}R_rw-5@Ve$a`vfI{OK2N6u=0##m4{_CPi|Zj`7{qxkjCPk4QQmY z^%>hZ?$%eNWJo@A8%1y7nHiHkt*_t*6Lb#0ER4Q~ZHmD!ZFfeGU+y)Q@A+lVPV@Y7 zvd>7HpI_!i>V7GCGE%dB2Qf$!ja!Vz6b&OgDmBB5QsCbW%i5-KH7A_-)g1THFT`-< z#JP*i8n)@F@_K7mCHZ@1h%9oX;9rSCu*U7ev?QKURGTwMPjT)tBL^ZQ2;Tex^Jr%W z-#14)#H#7(WzsCg#Hj}o){GWjHNBDxp;-+SL?A^*8lovMocr&*L(r=i9N8{L%V`gR zdt-t(M1ZhAd%)tSMnZ@(8Zlcd;e=g8Xf$}nn#DH>4bAw2~m(@jnUP-L{6=A2K=UgC{1qDn;{|in1~Q)DUw9+TzJc}zTSc)01=(>mL7wDT)Th|J zMh06WUF;TR&@!zZrih;wCc1c^iCE;ImXO0d&2#eE2v8`POh43HN*tw4hwfGC&?}`6 z8t}JtbQBJDlFoNCPT|YZH*lM46LlOk^%dsP-Haj$SEWm!QbP*I+N*vMNinHZY6#}T zHrCqe#8T96MChPQ@!zhZZCn<8cWowH)YL%>S>}RCCT5$fEQcFZa5t~K#{?D!^k~Zq zTz6DlGLRW2E-hKX?vg;KrSN^8Z$;OH@Mk03t{*q*iIUHY_G5d2ITV^wQhOd_OC3n~m$ z>}O0&n?*9E%~o}0uMIIF>(%6&ynJbVy?k)1o=S|sr zF3KIwUqX#Q1dk%7s%Z!E5O&?yt(X-emztc$HN}fJ(`y(IbsTb{1;=0WfzgXp=6w#y zPe$C}{Sc6qo7#kyQ9U6(+RT@Uo~m26Z6yR4$BH}=auOILuB+9$^d$I-aagQHLB)3@ zB)lLUDx#9B0|-goh0m~o2Wpkv$lVy~sMCo>W^>5T5OK62wM?3MKk1pRA<~l*CrojQ z1I4_A7WN2@OGju#L50N8>SYh@RmUpL+ zE6GFVxph-(9p0IqXfNGhmFk0_Gb|W?^d3^|HMIo=ToUIl~H?8HI0GD@&RLvBrm_U#fM~vBb~G64)>#nMEQho$_yKQ+EmdQf)tNdv_n1=_sai4mU7N#bhh4%k zbl+mmWz{>lso@SF>)mNhZj8D}l0&@^Hdc;vpVKA_A8(Xyd4*1N)A}PGS-3{Z75F<( z8Tt~ypf(%X!yyIC)n&)V*?T(ej1d6y9M`pxCfFMz#TT_oF*%YsV3`C^fYUwHIvaPh zN{B-zzhk@5SdJ=nle9K^8LT3Bg&;@kmG6@H3m8&zC`vv{)QG9X*^YOCMo!;I^OJ#f zoHdM~hIzy(XKg3w#Bqp6AS)SS(j@yLI1fF$jPpM068o#ThXX&1-U{LgmzXi6YLBNa zY88b;E!cb-KCMo3uvuzfGioAWy?LMl$DHwFw8$;A2w+>rM3v%tV9;En)REHV0H7tM zi9s224EmTEzIssxxtbOEi_!!Jhb@=A8>!5Z0McBwMD97dfD@LZF*rNtvW;eZJcSrJ zp}rH%Ssaf=OxV@|L+O{&hsFHQY$Y9+({!+93{1e6IVIX8rcM-s4#81Y(jvP0lya0g zv&#gC@mYD}KqkiwIPd9+8TzrU9Jv{XSQukuZp)Y7sl#Sjp)T8WGzHdA*Ph~RxZxu# z73USdCgHSLjfdEOAz~PHNem}yjE#h_0~0cSCgwth;nBF6BS%M6ssk*Jc zL%i8TE?7rj<0McbgLz9>m}+jyCnw=RE8fjw-v^STQ%kaz0plFiv@l$3tNAwuX={fq684itDKHr~ECgz(>>D_$P>>Ma}8&d^9Fmqb48psoF zB|T4GvW9OH)6)3oInFI7FNd3$`-u5)?B<-bI@URdPa0Vb3#1BSJGmMbi#sibY>2IJ zYIyQ-aU-BPW`0&kGgO6QB@$FJ^$weBoJrakh=Ne(C-OJ3(@T9mw-jc3^E=hV0yUi- zMxL$TJeh%%zD>ahVLGbiGhwoZ3EGO1D{P<6^)BVmv*$;@`5+H-fU~2&%Bue_ z8tp#47Z3iLfAOzg5-;CT+8*gU#YbcRonn2Eh`E>Yxl}1Ry+4WISLR=tG6h?P5WVY~ zeQOra+U^C?UM{$=Yag|ca?CeS?U3dC6C4&8?LWbxjm{9aK788MaqZ^C&A#v{JWFx$ z$%_)Zp7oU{F%%>i-$u@+=)t<^O0LMif@G(isl1k}-Jl#-5H%*hZQOBpOw=8q<6C2Y z2CH25P~`H6@AwZ+y?H}g+zh|T2{6|qlnIWsu9)QZ z-xitEF`dwxcdSY?+2NMKksXUF|%IztxgzDpLIV;wOA<)fP)T%QTwqFI^;E%71)w z5yr?Lp(UQ?7Gj z*;Gd6fu=Imu8w!3JS=0sRF3s$n$F!w_3vQE&c+exSLo<>m7$$g+%u4;P$6(F$b9en zC@2<~?%j@49fTvlNxAQ!9fiP``wrT{3&Z^fKII$p-*>x*Im_Y%@52FjK2bb(9|P^} zt##jz`~QmPutLgzNcT;>@8J9S;<@`+>Bn?$^}COn{!(%8K6?6z;@*7}_1ARYR(;yN zUd}$Ndpihdf!#l%dpp=iGl6rjY~LBuNgop>>HCo-EkLN2A6N_ z*!GdA%>=(RJ{onpxV$PB$|bgRJ$wZLZ#8XfcTKs-^Witc9**SKLr%H&WF2}!5lpTT zXAC+J)OaCG>Vi*PNkYDiSJQ;ZxP1=61j$5vRs0$U!z(Euz^tJ)!QH4)ZtB=JeC+hA zgB(A3$x8~5IQ1>ZjqT}ySqT%;lTfrJaG6YQI(1jFED>=QG#0ewFH6Y@^jY3OCyCT- zOOB>Anm)hIN$;!B+6j3uS5>R`xSP>bs=PplkYM2Er(@?$cdY3EX5pF^fmrLj0Zq7f zQ%ccOIZ$2KjVrb;E;^fku29>(=k^*-)z3tH)7G#tQJr=nW?(D^Vs;-y4O$mG$NPU# zum649$DdaJO@1us??2+r`i~^wbCA#J@n6;Jf8Un$r`3NGofx*g@kh+(Rl{fQQ@R} zEFO=~+Hyp^W$UOWy++KQ9T9KBB7e^nG5_A)NM9HVB;zo)axU7x!^^2@ExG(m&k4Qs zb^^hRs2&@B5{A^<+LaForeQuH_`sgi1JWlD`wkp?S`*UrRy@AF35i~wPsnA@@%|$) z^YM4uS)W3HI5wTce3pF*LDkNc#UsyJHTsNJ=>A!nm1$~07NSWg@7v0Yq>YjIy#Lde zr^sfN`?tP#OFr+r%CjRMQD&9L5ma@KqpEbylX`=a6TlyYv+ zVWpVshoTmrnsVa(9LA`pNXR{hkH~8k=lBOG6|CS$doZ74PyT<02NsSUIfCY9I?^^v zQxi%3MVvrhB!e0$ zwci6ZSpdbS8O{(Wt&Vuf64OllOvM6 zYo+gtR>43>tYSgZ10rd=-`KVTV?9Z`s7=hQkOL$D8CRZ8wr&rkm?++j09)uYaEwRf z_Kjm2Vj+DoNXCD1BWgaorq5wxwu(4_*Bs<`az~4Jb%9UZ0M-|yPDl|PJoA`!&N9`M znE|K;{s0&o7cpBDh|{yW?!^)#&sgz%Ru|r5Z}3@X%nNJG)Unl^nZj{bYP}%P#`g={ z6^Bha(_EIAb4NK#jM?*?hL4H#y@Jn{QVf4Ay@M`ECkdmc#9zN9%bfaN!FN{1*L;V4 z6Xm%^e=HV0r|}27dQQWK%kX;zUvGXaedhC{g%p6{m))oBs(9xJ`^RikTpFJ(tY>_- zFh(!dsTof^5pUsZdBis)?`CbLPS|{1A1ve?6~%VoiIe2$G*W%((-lw8x3@-lM{lQ9 zq6hVTJ!$3g1ok&0W6^4<(nv~x2I2EgxpMJ^)oa2(M7zyGTZO2kOQrHq6(R~GE1072 z*%{NfZ#MX1@@Z4s0=hX8bS|K2i)y)Gq>-Q;pc=4nN}6J!qNXXzoum=-QeqYx;y%O_7^T&N1!1OFYlaI9O`T; zW3{E#xObgQf-7M!d?t3*0}?8iimoAqS1EHM(UpfUv(95O(q-V6bQ_y7Qw(Hen+-~C zS2z(z3}qlc@acNlE_!*Szn8WGDM9NO8||=vn`=P*Dm7^O2EVML_1RPSQ^^}1QaI#Jx`nl(i_(j4flq~(1CB68i5qrHW8QT|m{bGCl;?unjYpG<1 zRI*pWB`=MR>7$}J_o?6#tKgE;RX{9CC3{#Uiwd%FMb%~dJU}Cga8C$gh|}i%BWv#& zS^L3s6=7jN@!pZ&`}l(=OWQ}rKk+A%hoZaGP?wKoZp)G%?<|cJENR!;I;x;TF`WT= zXxke7r4}1>DPKkIOg6XTbV5PinT7ycD!7bP++Mqz$TG%e0Au z6A~OBiZx&*xsvVWb1tQ=z&5E*$gncZd3MB=YDWNNjz(+lN z1_8pa`)ga7^Y{?RZVOl87I^jcEi6nYu)x7i978-7H`TQx!vk$m;4o_&aqSD}GT){C zbMO;)T(R2;{=a-PKPztB%5(9gb~>;b*4^H`6;WlOgq#@!U9nN{T+VQdh&^S01c9WW zFHy#}E>-)6W8NHeRVQlO)c-I|HWMzI+_)8o0+Xj|@0PPmZfq~73v7a@jAr}5k(GAx zMkh$Y3+0v%8`vmK&~6&<$6#pyB;Q%x;1bk2QJll&prbByuUh%fq>WFwM>Iq*fFY`2OBlhyLE zZVY_Qbk%(>hPoyqS*ZEKz%tz4pakv2XoyBkUP2;lI=0EfIiGHL$}R}uDTjx-qq#e4 zc*y-v9G+)`F`HI8?bJ=H14OdTomQ~$hKM;ZUm$Y{HS4H-U`p!SQxNnZ7-JUC!Wg;# zi5O!98_$m7Dn@fe`!z&zLGC*LEu`VWFB|E}L=V7|N!jigFdql_1MHD6mM10N)pMr6+g{)T?*SiXz!m)~9 zen1RP#l^c6RNg%wu6w?TkD;54>p6i#qypi^7zW1ZIxBq>=V7L_8LKuBicI?WC7>#> zsgeQ0e7ur)`+`^6zJoculDavMG7EvjWEaa1EJ={dUItp5CAyY~JErhhM>?vTLkCHS z_-ALNB#7zBIagExZEP^$7mdqAl`r7jx0k%Q{%xb5W zLWY$j!%8N@aIYc5aAKK^3be^MEq8Q86(5x2e=f;Sk6-BFnc){Kg54V#WxgC^DuIsi zI|C#ye~uw}`O`!)^A8EniQ?t-0{sWaCXZti4fD}36LdZtquPj!o3S6FW0PKKE(h*aRs|k4RbV89Gn@!Es4q z0!;p;kee(p}*XhX5tuiN;oEALu?Pn=bRai(H5VISy=p- zF$-|~G?<0>zT>-`!JM8`;FwwpW>FTiD9>gVre8~r2*(52f{kdC?DEhS;kBa{?@;!PRqv1S3LVRE@8XcGJ-= zo20#>Tet09-6i=<;D{8qx1^i#OQquH=JTnid|7Kr+L>n>ESSyD*`g+X&bInuh}c)A zA!2J<-}oRQU>q|I5)=FhU6^H%6oViRgyQeK+!Q&z(ShwE({H}9VJmnsHuDQo zNNd^kvOKQ0BRn(>wL87!v|CLXO^rfV6N#V!%lH@xI%ai1HnFxolncw_^a>i&C0EU7 zSsbT%g2gfKlharn&%y*KNgtEREFRP|0c34S*09)s^FlN?5ezjruc(AMMhu)u4442Cqs=aZYzGYAOaaa6Y=_QDKr=Kk zkjiFZV7m~x)*`Ob`ULMKe9eL);*H7nA%bXhbDO=uq!}NwlT%R6jVQYup#;GU z(CvugJRC5O;XpRXA#DJht(Dkvz659m?V+tNNw>2F*(vz~9tvrArZ0d!J$;^m8CDx_ zEy#`*@69Z^aK?L!X56X06ME0Q4M^6T1s#B06V^zuAxb0`5mdob>$VH(rSX0FOXdCw zx%NO>2l&8p59DkQZjEfY%g`A$A6XgIc7)q|p5HXcz8#A52HBrH@!zCA5okZ!4YK!a z>2UPszdRUd;87{#EJ&e+h||!y$i}11oMWD0LdXKIa-f#Br&M^m_U^ZIgRyr@!ZK{^ zwteYnlYK_fwrujT2XW_9T|j#!T&P%~Yz7}Az@3Yg8&_=8l2imNY`6vhg%Z*V#EdV9 z9U1Zg1W;1B>rNWCZv?oOQs>$7t_wQE=KpSl?;rwOu7YM)H~|yMk1`dL&1?w6dd%XjB1Ntw zKAEC+3oeK;pFVjw{m0P+Tc@u&5XlyHJ3b!%xeh}m#odFWm?Y8ByALtB03Oz`g zG&wXLqtp#{wPoJ{MIUD2R(o@z`B2NTvjuX|E+{1AMq4OvxXqD*m;eKGy$LZ;zxEHa zvq|WK6kH-KHf_*^S2wWEjZ_)*xJA#-tMSZ8Hjqh)MPda;%p4aIGjbsseIznH~lQ@1~+MJsk$_F z^9nc!R2~VdiMSs1_tQBAi?Kj18554+b;cmsulpl%CiV?T*f$I~jQWQGL!w8U0B!i= zj5=se7}GEq;-EO8k(SAHr~;QyimE7QJ;PDoUtgDFd@k9&ZWe;((v9sB77*2DpJwff zQAplBjIo6v?uPd5y97$y&~9c2c}KL*IZt;nC$-3z zz-LCFKRe7#C(wZUaymf+H&VP&EEUWc$5H4&&MSh*21Ao!5PzKI%hT(~(XMDKE~XIl zqmRA77j5o{3uwR{Yf`0@rmJoQ3+_b?ZieZpt4k}iwNVKD%f17_1~r7a6>I4MU~S_) z_oAYBv!sQJ_R5Q9d0+BJb}uh9iN zu2LETXhULg!h_*jlQ^(Jh7%2q+3*OGf_`Hn@?hZE5dq5*rIQEH6dy}6{xm(2&up+ZQs$~z5UP4a_#v)OE+cj2YTwtl5BDr+5s$S#G0KCIda;A%1Ogj-ecRicig> z5ayJ;QS03zcUZE%`Jyv~jb$*G93u&nAJshfN9ZuV!ky-cohL<9q~KzoxZ?-L=5t<; zh!dyxB1PbJXpN++COPb(?1{r>jyOQh1_ZTHfQHIV8JNCG&{cS>QImR{644_6VuaWH6jbnUb7R3stkUgU8PjQL%P}TpC~PgJ)oO|{)y6}*h9KG?4Kwt zihWr(Q~rt4qS!Ir9Pv+-7R4Uc%~AhEX;JJE-5mE%lorJv)y)b2L}^j%G2NW>Pm~tL z9@ot&|3qm~Y$v8*HtnA%EsE{ZlW~58M5RTsJ-V6jPm~tL_UUG~f1w%d)t&qr5%Tp(9h3v2L{&nw182N;P&s z#GkoBy6x=Jv4J=U19S&*GJGc}UBpJg*ik-=y%BNXbq^FJ#X(|$6HJP+&WFedu?*8e zMRJtx*Qt1SaSjBkjl3!3^8FTZHU9Owp5=#&h1jh9BLGG^G9PJq(L%_u{1fHR$>J?aci2&5CEBe;Q+1(!C) zIGUfuEQm{=Or$EEb?M@9)2-P*1R8RV*jA>sXa}BK+!MO)njq{pJ0;bJN3I}CN}|Y7 zQdTm4HY*v~qBB%j(4bLh?7Qtg(P^Z)P{Uq!{pH-fh3P_~^Ge?@3veM}TsmEZ!%t#S zdkj87L)}qroygyG(R(S;%+_pN!P3-%yL4(4Us6*{)Ihdq49x3bWbhD+c8B%dNn#VS z=jT%jh@}F&KprO)tj#O*if_xK92J~WAytk9P+ADSBOqC*smeg=yaVuvL&;5_d|)d@ z$5r1L1^h_b_*Q&>q5bjQU-<36y#G}D7HP^cLnP$;vU;=IaL^Ym*>jJ_q80T5457Wc z#!lag`lDu=2Mk*v;0A@sf>bKbVgN=xM`tmJ3&}ls|ARsD_aUZ{?!Mvf z7qBF?@Tylbnj%EeWMcwd)S&A!T2_P5~{3u)hiY zUHoy@+}avk4xh?EG8(AecOdGNL*z%6-ZgT~4~m1hf5{JSb%$#$7&+&)(7=NGvZV|y z)<5-C36bR_N0y?tWv2jM;m3+buda@V#ECfKb#$mCaf{{Rt$g89}$M;MH_930cz9eT;D|~b2(_f_)dI3z?kxzq9bK}}q zJ{NHE&5H#?0^zqdq#T*F4A_I~?Q1$!jzAk63Gbo^%emM)o0uvT4qm+#mz)@figNfb zI%e4H#>{_RiZVH|Ng=F1S&@fa)1dFq{F(VZjRQu6-u&bY;w#V@RFBTGI<@tv@nYK zvTe~{axpb-s4pX^gQ`=XIFsSiX>b=iE}(<^+ztdGjZh(gE##}-VvUxz?1M1c$`Ow> zO-2D9fzeNBL0;bMt6*~A@ac5!m0w&4}D*e8@E;e=fVvx0#JV3O|@jWUuA-?(S#8c4w;o*tG0EI5-_j1gnt zqBg}b#nuj}FL$dih8oTn!+5%E@a+7AZlY~`rg+7)Pt`4pg>)|7& z9CfNz;aS-q1UiCpEKFFeF%EmcX=6Qq&|cnH17Ep(XI^<%gsPBZq|4 z1Z>=4^hGCPoI9oKyEVB-g%2*|=`+?ZwBX_+iWWvrjr>S4(3ya+Q06E7=BQouSz5Q)&Xmy|`Gau5JQ;#5VOL5_bgj8^QZXgA0>G{OPTIm?$wzLk9n z4Kd-tOb6TNkNQ&{g<}J?Fp5j%O0@CVrxgE3OVkDAeq2RUZ8H<5Te8o2^?y4blKrKr!VRdK8GfgQ^o!38Ew6BUkY-EL8?AI2#66;O0Wj71q~8z#%7=aP$bc{;d)(@`o4$!MeYB1~}-^#Ab@a4I2YLxY1Pk;|931E7-0 zwZOJ3q@~WF5E<~Hl^FY!gAg=$_@L9}VlXCZfy$i(s@mQ2ifM5iCcZ6O_T$~ewmiYB zD`Q#~hiVX&90;zO$D-V|4_hU4{PYrnda& zNH4_9WkHaC?;1#|*(sP-IShh8%fx&#?gk=Ca7WAJj&9sa&hX7!+iekI!_MB+gS9ue zTbgQ(?87^5Xq&xFygYKcODOe?G-e2B*zxecO9=*t{i}Qq!xv!Vk}#APcsVi)rD8xT z>pWlrpiMHRvaIyrM0+ z&jSIa#tM+xAlwhp4Q*R_)DX}mJu3QwShSczca*EG-uUT?x(jk;VKB38__`H&kJKc? zp{N$A-TkyA-qPjUV^po4Ld}*v!>Q68YTK*3zL2 z&sO%bK8j(&zb30dGYjG%N>DJ?;0Yu1wHw+HU4K<9D$Q zmUWgw3^Ifkv^dAaUPN>K8bsM*&9P2v5v4z=(hUv-$zDJLJ=^e{%Mqvy$51p(?Ib#C z*uEWB$rkBw+d=#bwp)uy{yIJz1Y3aRNP`=Su4{pPKAmuVw;wD$d0l6zlGk)rD7mG> zClyq@!Ne=^oixon&B8#}(6K4~S`BV(O$W9zLRiZh=2>G)(ZzIyfv2Y&3E(?0IJX1h zg);z~7p4pD)4;2vw8fy!=wg#T!iEfReWVVwsMhmfQ7ga+F!Q1@(lGfOMmpPRXNjQ0 zF#vmFR_L_AIW_rUki8&zzgDrxSSXInKJs})iYa&RWP%K$(TcnIB*{9o;oJ)|505oS;x_KX5{SF zXUwQa1`iUQqwZw}jWYuBa2-WW+e0I8o>FouY|+f3EV;3LWkxLNw(RTwQMTBtMH$B* zD7tizqi$PQ^?KG`&(V2*9=6t#wI*1#LHBEd3oXt5nsrIUz3-drNLtp3&L_gwmH8X% z^hS3>o7AJLeTAzOGuHQh9ag7iAdIVhxqEmyS!;rh{2nrrG=g32my!r}V+dQCIf66q zWLu66l%$9=Q)12~#=kPF0y_rKk0BiA=JvG!$&~tqxOJ_8G?bOc2C9{6y*XkQ5>ymVlWFhlugY9C(o-riU!_sZ9{M{b z=kqicxYr$#Si~@s@EV4l>uZ~ZKXs>))i!T+6 zVCgo|@iK+n7EtW;bHZg4HQ8bK@W{01MUHTsUa4p&uC8#brs89X~t9^5i&qdqZ+xJ(+ z+~Z7>hunOd(wAWT+_M?SF?u}7n3G*qN(b`US7G)s1%EGUy;yD$Xku^Jt-woEZqm3& z_PIZ2Y<2Rqrk6$WOoVbr@r5A0FwP6>qPLoK_eDPx%LuicU3%G$!lzP0syc6_b%}JJeXFUWS3VNy#=G%oO$@IBv&b;CGB>tufTp@`{8H zD$2*fccyJypR+IBEQJ>Nkf9M1D1iq#l>D$mz;0IW$bq2viKQb-L6Z#%7_uR7#LECBZ7E*rnvSqAF6=o~bL4Jnft%RiGcws} z#(w`_7h}&yGv*n4%rP~~(7JKb??jl7=ZCNfk(#d4uQRsnhoX`fVaUNn33BwqC>k(k zF%Kyo?}4+-g98@SYq~;@9Z4Dy&L;w_S`9bNX}D(%E}COLxTSn>`7WyX@DM@qMiY11 zlwl$sr%_GkXFbG5KTk5uFq3CF%o^Kq$iZeH*YhCuq23(#oVjL8O)$v zhKKL&P)FF$==Iry4(aWoV*;3jV77fGoB?Qg1dI z>_>A1_Ku4w4P9H@OLiY9JpafYSS(4aV9J?;QL2$U#DrBT zM->3axT67KQ+84G~0i8setThlq zQybbT#QF^+|HZA&vxDrhZz5I$iB*7@0c!gKyPy0E6VaUK>+XMb*8Qhu-T(Tm`vbG? zKRxUIvwin&C~Xpn0&PT{+ZnrzoZ*xA9AFwwO|7?kA=0H z1rHN^7u?4}8VWF>IT`>=7Y%bZd6JqU`+N@`c&gsZw9GD&b}JrhhQ@h0Z+Rg(W^&l1 za1WQy8f*homcx@wzi8O2*fNbvSt1@`JmlvkKbiO3%^+PGo;RO%db3mx5<%S6;LlNY z`{>(r zbC=^wjk6I(U(bl2Dx3^`hSQZ%bmf(W;k4dJt7ka9Qng7xd}fBz%^LQ8!|4jD zo5ygH?wMOoT+gN|t0p&{BrHzD{^rn<%qEPaY!`K!C&A1nac`bPI}IPgh4}omrm8;$ zJj5ZSyzlh#XmgtFg2u(?6_fkEQiw@Z&6k^oDYDb%8B8~gid5ed5*;#n=FvAbjyppa zJYdbG@^{Rbx8;$0P-0HN8>HzY>=gKb$gIf2PK^FpuiqImf_FM*+~!i{Cd>YGw*KEr$y=hEauyD=XF zrc=MDWfVE>4He*q9{d`%HTke@b}0A5R=M6Xt zj5mBd?eliv zVYuIu`cScqpTZcUHP48y6UEGfrL)k;_Z5wd%(nRMSt?~L^9prEpKS8*;Esc-ZOgvJ zYU%kiCTDZxo`rhrrQbVh)GA|KBLX1*5J=H1+1Mv+Bay~AsNZ_{@L zIjUIq_H`JXcHAE>?pavmzA!zF#G)HHlL*aWla5@0oqw9o3+hvl&XBk^`BnQcq*GDK z*#oL;nQX42$V*j(#Y*?W#k#ZYu(Crg(Vgi_y6ftWHBYs7Och1R-UuMLr&%`cs}kG{ zdM@fe@;CVFe+!H&V%L0$=fMF^>dg>4u#F7{AMwug9|S-4E>8%p)=h%GktSR4SNJAyLBqJCORS(-Xe`k+hG7~!4(z^1iR;ng89mZANA*cV-Ro5H3_5kXX*UH47T1G2@}*heC@{GUftnfkZ0ii?U^#ioCX{+u4PivI{#E%_U`s~~3Zql6&;nby(%8(wKH!%rG#?+|^2{AAP z4ZO(kelPlPWV-VF(M^cFXD3~uJF$ZZc@iN)e72aeqYwf%-&4$iG0;U6< zOP|MB>QEn(5py8Wv#q5mSd!Uldiai&itZr(&}{EJiTZoq%?F9OKS_Z!67#$(oVq*x zUkl?(pK)6Qk@~He(@Hn(8w{#3ld8p;TdK;BRB)1h#!Z82-{9$0wYc#X9?gE*V*i$E za1)>7v2u$c{wcw`MabN83m=mB^vHagnbl2$r~Bm@e)%*r2TkVF_uVuY_}v+P7g<%4 zHL%k56LN3zqeFarWDhFiGZwdT%q-)G+3OA-y7lnU<@|=Z0n;5ZW?;|2<*V`S;^(S4 zeAwc6LyxFL!!T)V5uJ@qRwP;JF8A53F)gx)b-0gh|T6wy1MwwT|%s`@K*%9N|rz27QA z9nqpT_2`urL%|>>54i5;apyO`d06#4Rx|Bq?qVpS`8}57S`@9Gk6v#l3ffTnSLL&_ zKjcb@^Ab@@7C62MFjGf-`(V_iE|5;cgVwtmGx?-jCEELE=jUAjgX;15jJq%7WMycl zgQ3M0kI&AR$F|8hT$2=wsTv|HMpu|tJNk09GHgRZWSEw*{`=AJ;_bx=JYcM9VA@Nw zr*(4VMcpc%>0|iAVVK{KZ#l|BJO>$yK6PQ3#p7Q`w^?#4U{fjxvnagae)*RY|QQ@AIA)s2h*NLImRU9>s2f;Q`$not6qT;dcT(X%05 zaHgxjZF+D51pOB9-*ex-s$(l*to`?ix@r58snUBy8Mf4RoltAV!Q>2fm4!WF_D>BNJTM4X z4#pImsv8^6>|o(HBNbRr1?#20v8vYvJvZJ)=33Bi{}zINF?Hcdf_{;k3%@zg^ZnNc zdTt$DckzDHpyw)zKGGSZ5muSfgO6O6S3J%TcXBIuoZ1{!KzIRQEX%bMyT$Z%#`*x3+Z7RR z9S@^e;z#Y<3D&rK(XE`|Rut#)w!*D?lQii@Xj1s9!b`8BOPEa*Vuw~~Hd9DcYftB% zkCZ>OQlB7yXth2;{?Lkjg8ZRX`vmzzEB6WVhgNTyywL#!DgGY`ZZsyzARPRgVT}@& zk$N!P+vFL-kAr!{xB>!dPl1mMhc+HZHD2At18BUkr-bT7q=^7PVffSk7rBXBs$e3i z>#=b+m{EZ%c)8jLLhtQmSL#@2Dj&qDO!mkqM@Qw$x4SFg4I5@LlLY|-I=x#vco zG(9;Bum04f(Op#baP=-q-DwwPJ6*kthC1p=Qn9my-PcEjLZh|L%3Qqxd?PDU_1FnP zO%79oQyOKGmnJ43@WRy_x-+JxDT4=QsRw4Lx*OEN|1nq{@-QC2FBFXK915*jJoT=itW}5zAU<3j)<|BJn7|sj>vo*ToA<8EPv`w%O5(hdijU;>`5;Fb37gU z4fNGbRB85WGVB40uSqn%fjMCK0j*W4K1kxo_Og1b=*CU~6=|eah7Fldm8?ipIS5S} z+z+TrfLhjNsnzFLC_VU;`)Q;kk3<1(Y>megooY!0*RMO(KIh~(ALP|JolowxyXHy{B^pNsWiUv+B= zoMD3qoWWi0<9R*KJ*tTJ{pwVAN5->Ka4mA7wK_(D5Uk$$M=0{l_2#w+-m0^MXu{tTBM7Z+NLP9s?MkOOKyMw_GUu(+ ze|=CYJ)DD8UT?_V#qpqNM&{w)Ll0qQyHDSpm{g^d`U`MQKVeXnBYD!~SZ?NWf?&k;DG86eZSU|0n@LD{~s?FYwUmDrt(TrihWEM;XV|@P+;LyQ!dy|m`=5Ze3X9tI7`Ke@q z*Z!*t4{Yc4X*wayx-(2Ol)PM^GB%_?#NCHfPv5r8El z0pFOfYO3yBwtG%|GbD;Q6WZy&sR5&O&tm}1;6adbp6C5@R{z&Qk)2r|y{9Y{j~fSF_O|-S z{Q@S~a))B?qN#RLLhi|z`u~~lnFgDPk?t@ecMN@=)6VhP$Fu&e_!^Ly=5w7-iM+*> zrcdLW^BI-BnP- zz@Z{!t$%$fZW>Gn2eJV}{V#zjVHlQ$Ni)Mi>q+k`Eir;Phivgo9$}V<19=UWw&lD3 zvJ>}}Q$NnLsp96-h>4{BTa2=y__XR>Bg3VG|KtzPjkkAs7iSh{or@ieDVpb6&}^=X z3!01i!d6Y;r3ei9Y#pw2bHNzw`+dl($-jzKe|}a`11q)14{KK{(*^i>N;K^ca{_}=PJ9?d;mPd{Yu2eR zOl5AN46R5|n1l#s*wkF-_V(ZVMzjd7uRlptdcjs3R&7EK;}T;*I8z;aX=xVpl^9eq5vd?n+W+r&hUr-_77 zg-``^Uo1|W-+Yo{S5lPFaCSKn0gAgLDZu|K^zNFrr(6{E=N~0h$+Pn;b%c`^eECtn(f%^y&`wfEuek?J2#QEcGUQ{t6-`!W*V; zan*K3(9HRp4*~}^$!fZqA?6?}h7-@+LPH!~7YZb|2{>}WcMrx}w4JCg@V0CswfuW# z8di%hdAo3Titk2(Vev>Lfr5bhnwAv$p>EvEa*#l#;GEN4_FqrsJQbA&X|>r##}6#N^-GWB--D)|o*^ks zkFM8ZNCT^mn8@YS67h-GxJaII4N*4k1czK4gNVQh=3LH2ow~eH*7m@OlBPlXI2n=ls@qti7svE_( zj*Y3F4$_vMr&Jd;=~A-xc|!TCNjE}d`#f#{vL<~jRzj*Lq69bTfh*0k{1xT9wp&a= zm#5vjkiAnZILNutFehFkVediyqyrH)B~vPzX6qxQlRMq!?~0vnD(!T^w`TS1?r;#6 z9?RjdFbg#+BHLz>hy{BkmyK>FK#uy4%}uNv-aAV1Y|IjMz}=X&#Rw=@v8=9;-y^%)Qa#GVx=5EoZFe@6$3rUt67*<7$YW(xDD5|$|BTQ3_RwP zNUV|-v;G&*g~Iqn1FJ@!d26eJn56kR{j`4{F;7np@%5z z+Msn1+63++Ef?Yq9mShiY{+Wpb0(E+BX%H!!NtJhZ&pX9XLwP6Kpvugrru^QXeMF8 zLpKIMibNo-N{>H{ybwx2-F4dc2*ji$!3>_CT|o^cY7@dOS^Yg!Uo>aUS^OEL>boNC z590j!U_(kf9BIFs(ju0x1S9)UKG=9}c6&~*6`(B}F%jb{?`TPJe-Tnr)(jL-6J^YX zN{U1w9AqTnKg1>+A!ryYtN#Nz+cCdKhZOE#6j7);XsICc!n;IJ*f9vJ){0Fu-rQ`X zWW;EP+sVXpODXTS$)nG-fP*h&x)@9T4`# zbIvniL@Tn{$TQi87}Dl*W7nd?&lwSEgTBSElSsRxMait5w)dy6yvJ5~cH2$nAvK~d zO1|XV(+Dncj>M~nWO&vcp;E$nBZm=U6j4FV&C4#^FCUR!h4y%u#U5|gK{SA7!G^3` z8+-h>e~7LC@$cI?S&R=f<}dt-9Ls0>#ZzwRX(uN@_x2sxC{JPpM3lJUi_Bwh&}|?4 zgEAiFk~{kbwpZRF!8o@Ndb{j>9}13Y+pJX7xa0SbRGl;vM7@l!5( z@T`C3VB7Y=X`F~75^1TxR#qgW%;n{yS(D#O`TaHd%gcFF00o2<^o}dgwos%3o#P6m zHu$z^vc?uK`mrNiSh9PtZK>{GTlZf>2aTCuZC~79op$$N{1D`?d%*R|@0Bw%jKqOs%>=PF;*!MTz;aZ8q8TEMCc@JwUcVTDt2=gVOw?D4-YE2z^AfPAj*(XRo5&H zw%uJZ-|*)92A!9}HRsjoGC`>5SOxV)dbIKK?Q`1}Q7Sk~B}*S2Fj_2%L=cgj1}Mxj zLivto&Y>f=62X71giUIU92r#$C|A!MfQIO69{CIrU7uSow;$9;-$ueY^`n0BSWX+anQ(gj&@kcQpN=|?4HMpP!s+$V zHX-$vFxSpOI#vLTyO27OV(eveSZJ6B2v5EbcAHa|S3+EJr=i0%K+CH!!MkN&1=-o? z`rJ^Ray^8JYj-wZU-bpvrivxn-&dWw&l?5g(Lq=yY-X{jwldT4GA&~9NQ7+Ws>?J{ zS1-*~e1MuTp9|&c)2gRBWr4@G&)jsGTM%Hf+VIlYn@IG;==eUGr_e4EourW*0lx zU!{gPtAncuUS!C6RA;1%ytl!6sJBCXmm1k-BfYi}u#_HMiJl`G>9vipcsAdV+8JBC zihGdujWz`-Y8s{HtZnLanhNFu9%riB&T`z5f$H1s-d6U0eB?6BfNWR64w2Evm5c;g zP*kE`Ymnp@Sx)5{8Fv|Z-xEt8(bhlnuA>&ktj_hG~xCi%gr z07<87FbIV+jOr|$Vc2_;GfWp}j%q>`riLZwo|54TGohOY7~Z(Tfa>82W5IN~rn}<* z!Z+dn65s4q>%Dv*$HO(BbGnArGgg&LIDk-DI+m-RpQh|N1F+v+BaL_Kt1sKj-&b80 z_xB`2#n8*9ZKdggNg|7rr_KQ1m}iA zp%r%i*ERbd~X1Tq@$WmmU$lNW$yu)%Eywj9~1#y*1|tdwT#5gFi`T_??2B+^2s0modld zX2#m+dzt!00w*H9~%4SiNRxtb^6^rIJW-N4yworXCN#7HPa#N0Qd88fj`; zHD6;^WAF~C^UVzjhmf=aAu8eky`@62UAC~(-ZCH_Nj=4ogoNk>r5EtJ+$*IgrvGZT zlbu-wszRhM*=~&`lQCOldC)z%)Sxez@2+Dmwc`1N4uNtaq_%#EU6-e=@q~6jm$Ix_ z5RBBO?}Y0xQ=Qr0n0v?@3md`uAzplzXg@~bDu)nKW_n|mAw)oStpPGDX0ZD$jQ!}_^X$NC6RX;&{-OY{j$C;qEz$1Wi@xe~>$;eZPL zm+)Nz5XAzsRRPul9+SrzoidC1*{VI+JBd007%-5iX_M=Lz`wq@Am8OxAj6EX5w#hd zP#R`zN?+vXXS?F~P`Ztq35^N?hWgXmAH3_oYnqcpg~ttP+lFp5k|Mq7UttbgkuDe~ z{CIQPD+?TMqVMk7>iGVY-#qGj!1_p{KF%_&?IbROyKHA|de_lzK0duPox?~pnDojG zl;ZyL78v9^l26()1EA$ohI?-SKd+w?j#JE2w^K3+@RtQwimj@$xg`5w0ABb~6QpCtzid;(j+<%*ZgoDw-dVGg_jISi=a0>(>-rebYxHU~XAIWeT) zCTBKsZZ`+jIytBK%^#369XWTHlLp19t7afcj%@JiLhoeDWK+3U3{-Fxpb`PlO#&Do zAviyKjzoyZH08> z0@HxchZ*Zk=v1WpneF_OISHnOrzvWo z4V5D2%jP7QLKBMmyH@o%u1;wCEY)e2#Nz8wxjl+&X-wvtMHPmP)%ApZu&u{4NUQ2W zpSNB;GXR+$2D-XJU1%<>7NE^qKNLv1PNN=-;rj$|@*z+E#(LVuq0#MOFhi(%9;Z~> zyIx=T+`3Yz)xb?=qpc{QaqI-6@D$kUQT!R9b&em#EinvgT{{Q3`mYS5Z564PYhHpp zrPilb!BF>(AaEYY8in<-)#ZkSP%rrt|D#VAfZCIJrx=%_^9}{?O;2^Frluywf{rHw zpE7r{40}_mFM_`N&vdNMby(~X)vh6 z9S*I2R(k`GP7K51SiyGb$uLal*_IM)tsks>O;&RqOAv=l;IN6-Smu)zsZGEeA;^eC zHg%^Vw+-6e6!WEtWDP#vL{PO|sh4ZM7&9X}y(S}i7-LwIG3ggEmem;tqVX8hnv7^X zhPWmp8kZ?p)7=BnI5h9L3>#9{5xK>MSOL_7PU|(E6j@ zAqmXGjD?lpPiBi#_J>!)F&Loa)u8I2D*TikMNc>;qP`WiUtZsQL|WB9n*`jOAc2UG z2=iD8ZkgxZ0udfY5vLVmK(4Gg;_buvlL+*husoy_~}c zI+ujmqW(1)!&4AO=QgDk5RBGF_i|*6boO-64YmzS-OemSZE6raIX#2ug=B<~M#%q2 zo8_q=PaAkjp5ZFEr z`wQmZ;V&c=!))Aug+F~!VXgXrzYHAbdpO?ugTD}f{Afhd)?zR81akjRJ@~<@q@`V` zYMxIr=l0qTITyU7-j=)KCH1bDkC&!*&r0GI^|D&I^plF*2Eupk34tVX3!4>_ai{PUZYL*?%HYumD$<9u+d8SMq~RbCtE=&QVARnYltW@yjWMHxk|6gWHIBsn-_6}9 z?kN!=a5TeHq$ysGMH@b`Y=kZH~AiC?e6W-lx=Rv`%3EWLL4QW&oYSHv*MIm*U^K>%7q=7MQBnp|} z_@oCv?%>n)1I-$PxW?Tw?tK5h9=#JkTsj?;4z8f#j6qu7lPB7u7ZCR@r=yv{y%upr zXi?wEl%d9G;Lh|E_4_EcGCz=2Cv*YO>!p^^8&3xLk%}`jX?fzkzeNuZMQSE7o^Wr7 zree-uKg>0uj+0Ya5aP9q8&^4tv6g}|*yhNpY6u486cL53BRp0Dd7VU|aV!k*56={{ z`}4SFcX)f;A@v-PygM<}b(>`#hf*_^AV|e^PoMg5T=$etx8`2@G(go5WVb2Fjk+cR zF)!xiGBO5Q>NIi%m4Lo{6foxD@$|s*U!c7&aD4;!8BvN@OVkP3qWCgb{wtd@WMYNk8 zmWQ$CbmZyksMLh8CweqeN_2L3{*-uRyMG86_rG{@zRy-TTq#9&i04WjS7_KIS{1y6 z=VfE;aa_pj<3sT@33xGAMqJf`6ALLGx_$OS;P$G(n4jf1^(RY3Jq68U(}LMLSQ;{y zhsWK7cU+@pmMLu}k~%Dc2zw4cvW7$h1?RhSaa3?7)TJI5gkzcl{CIj^t5+Xi(f+u% z60r|lm)({K)!4e%ka z3HaYxAg4qGVamqMu@ye`xPg0VCMJ<#V07BvD2l;FF$qD9*2RrGj>iqA>w$z94URZ- zuo#@M3&45W5cU+A?NLXm=jrd6#f)%?5o-wxYg{wD@>QCwvi|X)^=YGQYO9l19+Dtz z)c5f%x`o3|?9WBn$fW}?w#EBmuD*~DJV=-pNq`?W2qO-_MV9sb{LV53T&8O*to`0O z$tD`?1_vABd@e~nW|JT9>FpXAa0)}qgj2#5$4m=8wA4Xpio#V^73v|19I?#F1;3yn z`t!2HiEZO~ftWss3$jShJiMjFisoi@_@rm&Z$w)M&_tnbszL-H;|+wzv1nA&+;{x= zA3;#G8G}KL2ye*n+O1M+@MBzq!Z{SqfQB(=oTlyDWYnILVL8x&a)d5vXiMxIkZ9@y zV+S1C5nmZua5%Civ%Ih@QYzab4<@%4p+O$mjNOWvR%(iiF9w}9%~&x)0KqGSRiSKEw|1t&!K3^TA3k)EWy}$iTmV=Z>!}BIM1KyHbUv$JquWo97 zjH1&?bVyB3D+-i>qO@c+v=^7I(cmj>^_m7040iO>!YeH3g0mw;2F@8%1pnMf7h%j} zWx0u@9fjG;;!gVLU#yf5L?x*&^z)uLoP=MOU3p+QM!<=O;kn2GX?3`a8p@P0d!*8X zW{sd$iajn3yEvQO4svVnY=w)l8Q>qoo3mC*Z>=cnWe)(O3U9FL&PN)o`umyU)O6&u z5ggjKOur9rc5TYX%ACmKv@5DCv?Sk6i9_vX?YiRO>&t({@z9J zkf)%u?%XKeRTGrKExS2{CuZ#5qxEx2ODA{ce1H~@#)r2=w9Q{lHWP?jh#iRNSwf@j z!lBqF1=5uRVotshVH*=H-z+Q*FV$$PI*(*W(xPo!2g6V~>mbfyd{N6rF4 zw@_!$BNJdbU?Ax{$G=7^U>zUL_2C&!Jj9}LFw%h{5iq8p|1;LHOdVXguEY0WvvR_= zpb*jzkYUP{(-O^~dw1o7?Me#4yW*i2Whbhp09m#I_sV_{M@PNIT@sObzSjw0Qr=K_ zxDQnTcyW%LmFxtKV=aKTu^U_pdxC4_8;ikH>ZvfvhiiDbf*ZkR-;)!;B%wvW|KAIA z2yU6+!$!G+z=u;-<`mg9s>Bf|FajM?V8kgcMojau$Z(3tuxaH(j*vwWpjyaaP6pBq zu>mGjc45LJ1rOM5uvV7S(jyU5zdk*G9xxyc?i`)p-B6+@hRTCv~QB z%15R*Cbv2K0t=;L#?UT@oh3k6^#d1q!R#L|;2{^mk0RT7fq`z|BQ&npf_XI(-bK&H z`adAPHvQN_tp1T6ala_>aiN=P*v7+;Ng4;@^LJtC_;wTNIZ@lp0N)*rzfIeSmyqW{ z<_|mv>}y;K9}B~llDq22q7V-t9_%-xE91O&VX%(UvPeBySsGBC)DoOj9CmkVv;HTj z&qyhO)6lmV8(i@UO#lW*A^2oLE=Dq7`tgs{hjrL zL$-*OQs3sAK4I5!je5sKKCVua1B(;V|Ge&Ar~8ZlY!Vj^ z_q#y5-SC1^t1iY2+#=LNYG$sVd3gvV9fc6r+IT(xB4`6UTr)O(5n`w0NA5Ph+Mn33 zyskWNow?sZCF-L(7pp_3mipj*xMbZYx91iYTOv!r<&YEotf=v7p-U{+l!a|RB_QD@ zXd4#65(vLd0wEntK>5=o;BI8X>B9tW);6IUCg`_C!j56WL`r}mY3d+$vn9?NCUB9Z z3A=^~(<$NXVFJczOPn`M;ErPxE*vI!wKoY1OKmGSt(w3A{x&64$C)VIrks+>a9FTS z;o^C}Vw-MLHm8)2ucT~ADR5tHnWv-_Puw+QJT;}jsJ1CvQwqUA+LUc61+Jt`IW482 zm~B(GrxbXfHs$n`!s(JW<*6x!9ji?_Bc(jPlJc~a!fw}=sZt6DU)q$plmd}xQwAvo zMz&3PdP;#+YEyQk6r<2IV}C|UVYh8lz9XfmRA|aPGo>6}Ntq`FBpyX$xY!k>uHOxM z<0%_h9x{y!jupfr$#MTf{*AR4CUUq{R%FqvxM0eV}4f5+yBP&wX z&$JvoUsEQjny7>AMtRiUtt_!seX}i>8F2`WdS!R!tdTo7m;%;u2_L}gQd;Y>Z7le4 zT>YdOQ1|P&an9aZRMn>)*DXWsU$dRDv*)S{ILLafg`q9+16kWY+Ozjg6w~|5eRzs2 zOF4FAcCNm=kdu(NodYr}|Em01?1T1|^9eLTbVFQkNc(<@PnpL{@B8&9?E0}LmcHaE zuaCV0s`%FhK5v+VpmX&@DFN2C$A|Exeh+uKp&}fIRjK0~%<$BIdp@_J#!4ht2WTR+ zQm?HM4bJ$mGOaefCBN?YXVO2>uUVhk%3xuu38F#f#yI!^LP267pw)gnX7zsi@E-|t zf#Ct}cDt`XxV?Vt00i@62w$2La>HQ2W7=CDHo*19Ks6dMJe=B$5}Z+zfiV0m*ZLN~z(FN@E_LTV73l z5WQ3@KEHQ4?7P`oo>e~IcrDK=ub3mSLp8njb0;l*&eodOA6WVBJi`Yu?|!^slSQ@B zV}00yJl2PlU}CoTI@}Se=9C)Pie9YUWFLDSorq;zDrERgIBtE!Mh?+Aq>8zK;x3@C zJj)cjG9IwNol)Q{ho3?k1>hM&-e*8u=sn26UuFgno>RP5NeObz0@4eE5+O|H{pI*R z!2k08rzQOGB%`_mVA)t9uyIiD)o6D3c{!38KUbxlT!-Jh7Ry{rAbc|wFv4RbfJiYj z{XZyJ6MeYez+?@S493dn@ zZ~Av6)k3Jt@0%)TP$cA1tp$kC4mFNQNd{ttp7$YP8cy+?c==$Q4vmNE4S8QeM)%OM zkJn{TxT**W%t)br6Fv)m#tU(tq$rKo%i`YWbDk$4dlEGeIG$!Iugl;OBE}Q?J*bl# z3xo>yRxZ{+vnzK!$B%Q%of5ju_M8qhyCzAKDngMsyRoK1E5r*_uj>3DM4di!hUJC$ z7zZe58=XFbSo;FdV{H`esV_$lZH zqV-5J{zbe+%lKbZw1&NgE}cmSHrc7m#m`MO_tS}=lhqavmJtOa&Y|`9*{onfM&-ZD zwl_DFG`);aUJTqwBape4MBF+g5o&55y^%?~Pwv+J{-NO;`{+5vF6V~H;8HNqDsbm) ze!v~>M1|VM=H)L7zW=K^AOT*`opXu{dFnEI0~iy+L$JTH7`*u<>Fc2J6ugjvPJm(v zpUMvx{afPl4?cKausBAZ#gT4?j|Ev=wRU= zavJN=XSZlv@c7fkpyj7~s02S1XzG`aJ1^Ld=e9Vv{fAf2xfdRmKs8L5K0A_Y)q; zTo=p%{sCqtN<0Y7;7SQyS^6I=!tX|PJgVk!CC-cjnpdYVIRRy6x&Ko||GjiQ7$zO0 zH!~eG%Gqdj&P?~a&rn@8%b)pVYjnx-&Iy>vm*Hc}SE0X|a6oM&Fzv}F+nHD2Q@n+{&oR_DjXJ$9_H=eR- z^OmQax^>%W+fRS$8BeR`22bDdjPH2ne43H1^(Q}!@b1D3=kMfWjjJTHwYC1JdE)3g zdB0}f#`W`l$-E8g=e_+TdAFEHJiqnYd+g|m+k3=3Rdeg~^V8-@8Q01CZ|0p~d>=7S zbY17W51Kc*e%^b`<3#3q-yJ#0cW*n%cW*TB1mpY4hsZm@Jbcc)6TtJ&%u|oQ&iH=U zyr-<6_Z#M&pfC5BcLJE*W!?$!>}Sn8!5rOok~(iWN#5r_NS&vyKZd_H?*wD`_wP>f z-Dk~%UtSNF9yD)Fom1}~M?(npMT+hzdZ&L9ou29m4#kDpsz$J2ey|8 z#@LO~0pJQzan4S=7cycs#>s3KrH18VAr8N}FNgP@DnufTjbo!`$6iWW#*Q@8E7FKi z8z%(GD^8MEo+OXB3!%AOug>mC^2Sb*H-3`59-A3T9V3gCfUoKz+Xk;|Vsl9UVwO&A_%0OuePRqg`k>X{&!idr6SOVGwJW6Yor_H#U z8J}A8t9Vn>5jNsr!kV~OaheZ-lZ{2bI3SLa7C}K-yj7vf1?fZ0rX+U~Hc__uZP&U>!3HuYCV|cvwEVQx3q%0SI}MxRS$%Wg zpI5|*cRsMdIKt#t%Ydvp-jm921Q1T7q9$;FqitGV0};e+A-g^}>;qbx-*o&>uRs3Y zMmt}@gv@6+oaMo*_`-F7FfJ9ra{x*3sf~>zBsP4Ye|a2jS@=LNvkbekZ(&Wyqvf+* zz7@g2AIf2pBOWpl!m)64HB$sXO<#C}bBxnIu+{*C7}6ZMnlHP0Tp`HlB4NWQV)=V4rFqXb zZ^I5+4uMeC4Oh6X-YpHU+x8PFj(!F|7!`tB#Gg)|FsAfz;}cmKZK;vD6Io-ygiCIy z1969iYJcNwmkBF{i>P=TnO2K!r|=67aC^$I8YejN1)XP>*6bwUEv7m0ML zT+zm*7gnz;QOiaX%@4^#mR4=A%ZKZT0uu3Q>K)&tm|gPagJ~>=9CnAb4;Y~jLs6sA zyG_FAZRP?-!EWOzVWjeOJt~u|alPsTQtgt=+nMEpPf(fj8473;0lvCLrz|taF<;!D z&&BUYg7?2KpX0^_ERbI;kc6k1Ule-W`;NM$TmLK*$SR6(UEE+1X>Uzwrx?NuY>Bit zrL;|vMjx^t%kiAdu5PE0qRkY>Q%s>xOkpHMATUol*uVSem!3}GadIk-3AXP}R2wEN z3aUA4ilVNTYJId|d&-#2vGlEh!K{~g@th9x68BZ$J+wD0DjOretywjIkWgY7#s@>h z>G3HG$(fB)LM!Ftz9|;$UE}#oiZ@WygHUS?W<9P6q7g2DeHBYP+aA9zHMBOxLTn?G zX8~U}0&nS@RIu|J4D`N)Ql$@*JF|!+;GTi*euAI4R%62R_+8@2@eNK=F(k@HECHho z7;+Kp*?Ziqnfe1Fv{hsmFUZ_cXqI43mYHoU}-o-)a*AZbL?1?eL zgvS1@#%G`2%bWidojfnwm(kxYG;W-*kC)775bR&_JS>G!lD$7v7YO^WW|`DqzyrzO z&-=@E@^}6+k}-f& zRpZ?(C%^`prK$SM6cTPF&tLSJ^bXIq2K=H48IC4OgjckOHG*j-KsM$G^(o8!cr0tD zWqG-C8P_tRNsJh(Gc_xTn1=PP`i~_qTnl^cb@hflY^($o3WkWZ9P6gVIFdw4XJTBU1 zzyJ7b&y23LrDo5bz1@PRr?#x8?V0CD+sbaW4DSxJY0F7#vmgB>vUPqUdzqpD$>W-B z_D9Wj>|{U0Gua-4Y_sn*TX)Zu$~ia~>XmnzwMKN0V94rT?l5zW?B_wgVZ~ozG;O!u zklN)q-!S_#W^Y;5t}&FY`cKSSTQ%F=@Y|1ApqcjuAgrbM_I+k;2CyS)133%6W#@NK z^2rD~+Nv5vi7y|&i!awG70%C8@?jXnCMN&C%^wXKf{Q<>(xUzUnP-$~!)FlZPCC*j zeP_0Hj#<@{8@(%*i-i1h*$iYbn?w0(P#wI=c4pf`FRKXB*`tP5Sp53CkD@nQJsyfl z?T6ra$ex4nPT?awGsC0V_nO@xz_FAKL5|vg1+imo?Q<-^)XD5p8o?gQe*N=f1^cxp zAr2>VG~M>O|7K{ZOW)qeNq5}^y7ieMfADn?h8d)YWYA(yRsk7ovN!jY3$PnfNeC#< z#DuPl-i14sC#a#Zq_Z`%(T~|!5FF@S*pNE2*fvZ8-(M z7NCL-nKm5b;8lb!kSqjGf@C;?hb=yNoJ048q*5f1lr5qtCMqF`ZOG~--8Tc0V?dJs zT1ZZ;K$802UB&cK?f4$u$zqM96_OT5a|$7%zw6&vKlGNP*>2!Shhm8c zGB|}M0uL;lY7@fPXdvU(2K{-P&^zNXC4tT54OOvkP>9blyO_P48k0fqVUDiT>*q|L zAji*<_8Wusa6QQBtwvR@*E?Z%K!c_p&qWM+paFOgt4ZPLnU^bL@W4V6&fyuOJAq6R z=?IA~svej0AX)^qsn0Yl=Ma`~%wGrWxceZ(M;{uw0zy-U{a*Cunsp)B=Bx|M^)iR> zJU;V8AcdoyUxn`{25?*HaAJdAehXmS-vb#Zz~NhuX4pvKx$?S7Ngpt&lah{RDeWVs zjiGrs_!r&0$GV}F`=9vH%Td3{8et-T?H+%jjNpZY z11VPV@3j$~%q5lnF^^?x0fnLadgp~;1!9bTneY1Ihs6~=)yC_)+86wAH9oTS?7{!o zDm9jeF7`z;ow(nmUM?=95_b8$M1JF83F)NwqP;T_Qp$G?Z^lsz!iolXZ(W237>8iv z9C2OpidIe~FFQO9BFC8m&FO!K633mXKTXRoJW*W^hUTD3ejZ;bqGvHU`IFG39bQ!# z!BK%|)6EUBEi+Co$mz7*3;;pohW&y%#z}?~jwkiL8IR2Z>loGzd+66xfH!r_zNC&p zA?(ClPH&Ln!h>zR|G!8%IPuUJ#5Ef`x8uh8!Bas7o+*M$qg;LK=V4R)Ly`IhXZyMz zp~0XA5$+C6lvxb!$1W(W&x&Ip7*E2cFgY>et4%Oia1QLz%B7E}h&9 z5W?Kf{eM}18?1Uc^_ydPcRybcdoK@EcCeNo5c-~Z@}HZIYHur(y>MHiHVOdAit8}* zA^HNO0DHNoE|CuFfHsWj>iOU+U=j#N4ZG!bQ*5g=33-|X4HC8lo;H)vbrJ}}=p?WU ztv?C$vY7;WZf6)yEOXRKb)ds~%`=;6u0XfVyT53!DgDV zxkARVU=4Ej-Twc@q3u+2+X6&09<^?XC>g+z(E#H8|4<*ZZ~g%c?%pvO*-S=78zAVE ze(j$6b{44!y}wIvC;g(i&_Dv0ZRwZ^DfQ}R%=y{_mE40<-=n;n!hGkyKZG>&fLfRYOUK_ed&H0M_C$## zIQ!4%RcF|q3h_90tSUcCZ}^Ck);Q>zUrn5wEi^-e@wI!yUg18Ypt)<<3);oo$KH+h zt1@Z?4r`om=3KG}ay74?qNTB>sytbCnve3Z>5LEGwB|58Zb^lbF$OevN)TluI{akU zBPqC^g2>MbJ!E2V-LMC_m#4txMhsk+A3ElpB*_|cp%Uu=6X~Ah-l#5~1VgF_MfVZ{ zm%3jCWnc_w&{ITK#{+MTXPe7;GMI6k`w$R=M%w|c-7G9tVZvd6*91EpsNU3q@J1{M z;7wX?w2Vhsv!V3V0buFf1GkRQf&fjwscL6du?Z5NHbsF{(&jvM0r~ryEd~BF{AxTg zDo91w(L04BpHu>+AvL|6vo|SLB$S0sYA!1lAdt4{VZNM>WpaXRVnsUOq}jKG7uvK# z<&`bbPax@CC>o;^r>qNv$^}wkVU2|+x|G2rB z+~1YxdKGPnQMCk?tPS;NaT3lHRtqC#cWhIdlfcT*IM(mr=jgA07%E=XANhy0d6bZA zYQ?n;7vWW@pv(Ofg5=W|UEACWd{T9X8*#lE-w8hDJ`9x6EF+X|#a@i9P1R|7TU=cK zx5{Qad7jBVvM=p{q${!352T%KW$L3A|ckl(QG#{{NnCu}kX%wxX|J{`^K_x%Qn zJK5S{Xh%Ds9tP+<<-a#47Ab|8+D5S;~HD1GbYURpNjeK~Fe0!Dvu(Y9OM zBel5PI~VObAGy>?1ufgA<`QwJm2?5EXf_D6Mb(O+K!PGTBQ6(Wc`y&D>2~3VBM_Z} z>?e3Yyd`1LPl2l6frYE?Y&n*vNQ-Bs70pU9?#ej?V<3^MwmraZZ<$Wae&=Vss!Lblh>L|aQx&MdCmqp1CDx2a-FQX8RcG$s)beUv zGH6@?ZLOEz!|XgOb>N}aPUIxq>?h67F~^v6h}Luw!m-+%e?S&Z|+RBmKtie8)2-r7G)*Ieyx6)=?-^NRH%l0JH(!Yv-L9134 zIlgR>&+M!$EBLjM7hLCBhqOTKB!yDS%>FhwcS3~l8xI%DFH@dr9iWwA0m6__E$h+yc)$v< zAldBj-%|mD?@`<$Qkr=U35k}yvMR-yRn&+36 z>wEYn(D{IxU={?Pw=joDL?CxZ14`|_!wdV3ut?nZX}UnPN*c_oxI)BYP@*#y7i=WL z$lOE;jZgoo?z3~ysR@wIgovh#gc@DWOdPe+wSm!SNia9Z^p zYelOCLnzRQPXs+?go!Hf(Az!q&uFYrE5pq*gnKJh$hU?83uGLB&Dwjeki$Qgp=S>_ zn4}+>M6+F^bRh5Hlob_lX$q47CpxBh*Fj|zm-bOf{n$N>;l^-VWl=+T>=CMMq%e$q zt_0kech}>}5QN_OVS4bT{2E+z>s@=@SpYdG9{$ccI?oj)5T*IJf@0hcChN&prNUHz zL_HZ5luZT5fe&XtjQz~OR=wSqP{vw73tDy=hB_o>02Bx~kc9Y;&l0Uu%EO5m@;=!q zI=GW2VoF0E=hm_8Zd^K{VJ$Avy}aUpo|C&grXDjx%?fBHc0VU$^xSr}4*_oc&|>yI z7V>fxE5ZfMkxs5Dbp;G`2U@&5=Kml6F@SlW+X=JI2ZC>mB8WrdhvKAPkrMf_BT*>Y zj3%!LaXxbhtQTcPs`7hRF+FsET*hr}!itT!BLfCVJR26#2w22CI)A372=98Pph=VW4j zY)x&b?1hHDR3RZ^<@oZ62q`p<=3(=dHum#DCb^Pf3W$&^5hOCL|eU(13RjTOPI~xC414uz-qQQo2CiqPXPc1 z0_t@9iTw)2&?TVhpS<~K#&^Nm>Z-uUBC$AW;heRB{W>~PO@Y)=4c2u{yU_Wu%fS|@ zR!+C;2dON1whPb*bLaf=|3}OTd{>z;K;tg5;m%>Css{F#>w&-&M|4YCN9hw7w&7;4 z)0?iQ7blkovy0b(UNZm~^u;48rhUehvohr@MZc|PK*?BO&KSX2lAoQZT|d&!n6+|w zCa`ZNeRiFe5*~_RYgz&yMwhgguPZJwp+KnOqIq5rV>!0*}kR86rowR3H>j4 z!3{Fd%h6H?KkSE^&APzM<<#!>bS9%f_u4sGpVSL(+giU9`KnGZ`zxIQGd1Aig`*e; z@&<@N>5dw$#p)Kpq}1rZaGG}bJ(d-_Z@Uwa9RQ*iTxU~AsDTce)B$JE{jQ!>Tr~hz zR1%6#n?*myr3g+Afg9L*%*`8W(8N1msS_?T>O|MOQYXibqfQ+;=Wwr!%1-A8)@v)| zPZpE!z$zXZK(iwVWd-U0uWUiZHYoKCVFJfiPu$0>Fv04hJ#UO&~nyn zo{x6&iH3vgG-szY?eif{AM(MCei*L6F*5PV{-}FL;_Z3^?P}>6vtls3fhlP{gt8QU z0zJW9OtC^8Gt>%-IKdRf&d{iRX)GK8v9~`pks&_wd^3oja#~gr_>GaqCQ#wTt&|m- z*^Vm%hPz>)vl?hgyQXG5hr{)_22Fz#n_}I$3fzQsDP}MRmMxt=+a`6~%>q|B$eI_Y z`A?kOGbrp&ndMpG1`DdPH2 zFI>?I=N$Bp(2ub*F@2Ot^sOM$a%mU}MFsl}tcd@i&%5R(U(Um%@i^Vh>m7;@ar_-E zoZucS1*3CUP4xdX2dE*PObwBt%$y0ORJ1dz@wqx`B9bz3(~!b@{qEdYRq!=_?FmZ6-rnV zkHs{}CZ6hDV4ElVs!Q;|a?%N$B}%Dj#Kd60yh(rh;gU(gD+e)s^TqQA({ZqD$^kDP zR2IgRr-BT)R#4FbLel@6+}amBi$JGQf2*@byf^jYc)6HG5VXe5@ww;Wcksho<&TFE zuH~b%R;qN_SZnGy-Hr)kDKcB;u^!ceXbF;b$dl;&%`EFhQt=}xWZCjVsH1DhQ{xVC z*$TAzUf@RyQNsd0{SFc+1Vs9e&;}DKl%W6#r-IxqcZ;%#o{E(u2O)Fq9 z)hCZO^?)Yqm#ldclKO-Y*0VJc9if?cKK<4m`Ev> z=Mh44(nNp27M7d&0)CR_1pHNVav=#xbCSl*D??siZvx7JzbkqZfohDpkYM6$Y3NOI z^(NMv^d{EJ<~<#Aq1h`|4j_*60`dyIXmmwoa&Uqu_2C?0ED>40U=};>1tlVS|0^7O z)LTUK28M~91bEw%KOMSiz$g)P0jJAlK`hJa`$I>9I5exq!&5?sARbX~7W=4!=>i}%c@fVAl7K0NzOW6biU5EDB3MS1 z>`jCh!ZB%ztf!6VvcOQK1`SROuu#k6p!he)EC|3}A*jNHn8L0!+!hpH$|BfP*cB)% z4zW}e)MEm|#QQAnEHzq0Q}FHY*XDr#lEj|43bMGO?K-0{t~V4@6#=j=lDA>80Rj&lmNp@QR=)9e0+&3FPynP zM=@gQ86}f`(XjLstzvW2#VdK@qKiABXgnQu@UM+W(25=5FzF0kG_b?%bq1Zd&cG`S z!ks3NgabU$m%6m;m{ zax+$!JiNxD-K)@@TT;9RxODm;G%~CkP+jh^0JzueP5`z(M$_ciA5Q?L(tvo`zbEGu z(hy!TfpMRUAQ}PPp-;;ioDg&ipy6E!pt@)T4DsVy4~WrmnU&k^)92tR!p78IkA!k0 zM$-6Dp)^nlho9A&==F}utDO*X1U977Xw%$1wF16f0^9N56NfPZA3;*Zn1USv7$`9^ zhpU`yNs}w2R8C<8yR3E2l-$w^*9?t1*(MZA8Oh7l?j|SOSaPEtwpsEbZ5^UmGtdDW ze3Blv(HuQr<6pbdvcn7=53F#p&^n2p$hTImXSHuFa~A_!r$$`$^`H-?akX#lIv=&L zL*22SFDqO6_C!Sf2NmIiUgrmrAokZoJohi%H#twk zw*3Kfh=Nus9;g!G)6Wy+YCg*%45`LJRaD6k&-5oGMH=9l z&c+Dhl;wQWj;AcGCyp4;FO?k?aG241`%5s%TNE3q=2j&4# z%iq@T5_T%WM>?q#Z|9x?5KylQi>Ai_p#iqbz6EvzqpW@;fFA&|oiyW+ADK_V!bmHc zI%<5Pv9_id^gSm1PKs$1u)d$?{sAD3{zZAlzBWfYR}G0Ryn#%1sn11d?5cd~?H%H$ zy(`yiVT#E#ZG7|hFLZ8-`f2ZTwHy72M5%*%_AdSLV5xNuEX z{^3v_4t%f{gFeQk#qmRng!egg>*1rz`2o&cKJnGBeEFaL{x3fCM0U_yEo-!J)aEuw zd`|W0Sm)R?69U>!L}?+h|5IgHe@hJ^jF?Z2-(bIkX-*yY`)QRX$!LkX%!yx)6St^D zJcvpsJx^F-F7^;*UWFXAaY8;D@lA3KlYN8>(3dD1^>Ar`!5ul@zx3*=vj;H%K;R|X z)`N+Tt1W3sIKZs;i4X`v+7I|f8o^}>$W=2~LMesTg>aKRxc4DD#7|ol?|#ZQ$T4{ z%yxtz?Dm_5f=X%0Ec69054As6VfWnsA;PevI>)~EJkeNkhPgzWQHby>cuFRVdJ zQ90EV=ZK#l)RVe8NXM$Zy!~;8e+g@?Q-3O3GFn?yD0Q^qb!(KfVo0ekRe5&=6<*aL zLeLW0_lCj5Vs+>U3w+`TjtRkA)XSe! zHbLaBxzYudC5srGgC#ybZSXo#_LzpYed1QA0Or!6r+BudF^;1 zZBc)*c`dyY7EjaWOD40Z|FL;7YDN9|=0y&n{%rHrL@*5XUpKF%W|yM=tEQ*`tRGG< zA+kfgR=_CKWK!t(SyhpI3s-=8B4}28Q3N!BYXi+RQNgB?z0$(kI*a%YC~``oAoEIB_L;v++^wM)Ib~`_B-0h5|S7 zCgm*h#n%;H8yA+9P+G6FFKF)ll8 zdj%VG5&9B92U3M9LY4u6UK<#MByB2t<#0;&GZ>3Eu@cyZ25bbX2-;9~-Xg7l|L`w3 zfN%{hoCUbTH~_ao8mT@a+Hs(#QG2Kk1ZmL;HcDNQT1K~hhXGDGjOhFfG^xEA8(Gnn zayXPIznfypO`Xg1wbxU9D5XuOG&D5iyg#MQL>l_$?3eSw?77(?W=M7@zE=+9FbXv( zj#N=Lt0-kc1fL5+#pqC-^b+#Ia(+2X!c8E!7`zI!AY*86ZVunYNM7?5uVWhU+fP`h zzXw0ve>QkMwl`v$4f9|l+^sfXrsroh%!h@B6$qYVp$_^)iC01qGYE@1&_Etc*L?AU zxn7(8eD6I|cuXAXVB$sarZ7idD&UG&Tg=Zi^X|vVy%7kxHdU%?a2C6v_(+A-_@#Sg zd-|DEqBm%WyO z<85%@l3Jo5{lKCs5W>9yAW|PPj^bIeC#i$hf;LdlYAT&^(DEm*ji5F{`qwDj<0x$>zNr(i1g(Uwm zv*Aqw!`7bmxb2aak72%|S!PcW2YSsWsZf&n(+8OROwR>h92d}5ndp`Y+75LmwJ=bQ zZQY18=|>qNPM6WVhzJUVMYtP$8nJ*${QSMljhLMrHdDmWJ zo(tJnv_p%+6cB_kExn++$Z#lBzn`_7{sKg-7x|^yRMKfK1M;G9^+IWL7B$QV)P9uO;Zc4jVZ?B(fn4; z?|k#StNA@Ie#_m>|5yaFB{YC3vI#s^)E6#IkunBqNvmVyh~`Q+2bpqn#pN+Q!2n-4 zCCWiUg$T}~3B5@Kznwbm{vbmiMV^~8D=VHGR{Ob?i$nYT(# z!=t*ILl-alu|auLRbJ2I{+Gbqy;!isLEZu_A>;+$WH&TxV|sNSVBDEJGdtZ+`u0bn9z%`CK~rZR)YS6L5_q zN*Pb}XBbuT3t#>E{!X%*^h5d!QJo{TMQhSyTGT|*DB8O&mFk<8dHd^?dRUQvWf>@(;yn6v)}7m-M7IBam7c9W z<5v2cNT1uwbescFlQva$H<$*U zBWSbe-xIHBTkV4MvEebm-rbs@VmqLLWy902YJpqGXz!Hkkkf6UZLjSB`f9SLuUqL+ z9SXtGP7cdFv~CqMbvXq{6e@Mmn7XAw9Erklq`w4VKyLPP#W~TZ>g>#VEL40tt9rt@fn-$g#OB^vVFmDB4sV&~E1X=#cFfP!aA#%z zU+|2go2R9|OHcT zj${o$lCHH2iHa%7bgFm4`hS!>G73qnGn5xumUaU&5iz6Og}eLDwtSG7sk_T!EbR~# zVNP~rd|OKm3QxR z8Fd%;e-CY+cm3ks`(Nh0WA%QVqZmW%O@{{Kixc|>Yzg?9AHgRQ2C@FzJ7=z``>)wu zU*t{kuY0YxW)W7cS3iNYxX97d_+NH_CB+n1y_;W5U@~&(ULKG6;@0mqDPR22m-yRV zEUw%>K%aJ?B5Xyz*wpZ}mky@USx?ko-SFKr`C>--{7{?uPfyUmG{*%IEA20PA`)Z$ z+duMrU|6y}aV(4Da)*wucEd{p+W8@Q(ai88@^k2Vg?*eezU|wj^JCg!r@s|iKcxft zrsLIqvKTs>4fTFH-BQvka~lmBdagG}qm!cRkdJ7(4oZ=cG?)D?Mcff3nr$I3wOTZN zlU#+=(fT}vV(>V{oR^vgCp^^4sWGG`==w?3auzwMo-#-#(7ce)-m~L~zWgskX>hzA z@y4MZ5e%yHe#GAQG3%={Ute9v2HfcBkNruruYIsA@)b6(+pu{osfHC8a(d{lg44?q zr&rW@!`1=RxeTfyKZn3uER+kY_&GM$6@IRiIpVeoIJL1+eh%J4V^V~7k8tLQ2*J;x z9{DEuIgU*-`wc($b#^uwxv$yRbD3=!xo<;P-6+2 z1wOqu=p_vkYn9i{gGqn-gR#pXsyt0!z858lEV!!$r4HwBOg|lKJF~YlLkQzjt=D5q z;on@3b1WO`G{%1g6OIj9Zl(*D20tx{9N*1nq}6&Jlwm?dkb5v_0QKdzmn+>j zr{U=M=^1JYS*ziUqL)g_9$ac%T24pD7)lWP{lXNn8&qUe6lsJpK%mBFXSPEuRZa<= z4Nhv8Y!6+KR=H=;&A11N0zFiSsq66)-UW51zW;AoCHJyt5EW_wFP0)a+tCg;d^BfW zLbVb-l^KBoc()Q&TOriM3R}Gs%VcF^2L{1W{qk5bmE6qPMqPmVfU`5H^3ouKoaQV# znH_$pmpOy`L4Hn)Z9<1}r;jP3YiyeKtixntT-US>S>h)=(bda0_G>QaE?pvij=F;@dq!V<99tU)&obrgO z^%TzJ%E+jtqWqxPuDVnk*+4ruLjj{#Jjp}nCiiXK&`-v&;z_Q8rFAbrnZ^KkpRAKd z24a4DfoEsvNzOsP*i3>Lqj+BNB#(=>seb55&dxFPBxg$v+vlh!`FOoAlxrQ9fG2@i z0&#)o45-p-XG`?_AcoG*;X59#F)F_d5t9dQb^8BL zc#@LicUP*B#FVue2INAq*J;!q4&Y>Kk=jX(*G`p<65dR!xWps^$xoU zpXsm82%KZsjjBaO0R3U{XA(px94h-F?QZyb$tKd#WcM_Jv5T71{8i2+YN(A0A4$uW zY!B9+rvb_aNGCq6pa5+%e*gd<#)w?Zx>fZ9*AVFuId&yF?l812t7=Ul*V%Uj2!y)L zhGamC6a|h!UOLF4Mz*C#2EJAL>F8sxLRqraoYw4mjc0Kk6e;4zcN#^g% zwgYiI+frE`t{;qDUyWUb@`ae6$#K!x>q=h$JA$LmA*+<|mY7-i7%Plq5DnSVfIhLr zcV=1rTu#@hc#W4l7z)ZSQC7bb?f~G4$bEdg#ElB4u{RMx^*M1&(RhN-LAEBlY=UEe z36|zHO4qxlXc1sutLVKO;b5Ro8?XJ>yufb0P(M9qHo2DZ9dtW5NDyfGm(aZ2nBu$? z^{vAoGy8G;8Le%6^lBUUUqfI-G5a~bFCD8 zw(i@Xh}*`qVeN;j;qyg@Z73LR8aljAk!&G~gt@5w?6D86RPoqF?`XeDl@*@crLuR3 z$nRi1HvcYu_BFfDy7+zX_|Q8pe%n|6{Cjr(`Q0Bn{EmzN_qSd9%b&mJSA5*_l&637 zclO-t<9(mI{ilEWr+<$}*0HnN*0J%EgHNA-_pP+~xtZ&BT>hiKcoC&v{BMsP{*N>7 z@$rYN7yOssf8P6jeBwa%lRGZ^EgxrI;{QIu;~c9l&4kJugzIaWqz4`)oUa$4iwODQ zcTnW9Ch6y@?B{-NkiCs8Vr6{qcF~|M_H%P!dp7k@kxw@~-*{r!J1`2No}KWgH#Zqe zr|aRgVj-=ljI|`LfEID)z{)sxI*2UZL|VH#ZFCtR^JrV7k~pE#{tsB`*} z96h-_lLN-6C_wo^7U4xODb*^lb;dL*O~LY|JS#N{mCuWAXh#LT!2rG`>T-=!_e6d8-ACCGxWBLoRs?y1Y1Ky+?v&ki(uYgA2Yebw zP}Xv^bxF6HCEy!_frFSZtF3^|AlgF()f_{bn!1S_utM_ThzXR}7=laCiyiS2E`}9* z0b^&I_dFUbDMMkAXi5B`>-Z@M-$K53coj@LPP}wTUQSvGJK4Vp1Y{J88+vK{&@U15)5uMeXYmy}>sO(mFIBA(<{O(Az!OiePPZ~&Gb_eYo6 zV3)=sBmw<`SiX=M0qRj};dEFP+`C(%gqN<(GwW_$ZM~YXtOv$EnEAYZeQ`m)t72A? znyx(}7kj&`e)FvmWKQ!0RbfhK0P=PlbRHt}fKFr^>yNM$B1G2v%m0_XH-WRGs`B`2 zd0VHl6q^JBQtuIRo-a=K@cRjVv=S~<$^DUE1$?xK8l#fQtiE58LV ze5uU1XW52lR9E^z&-$I^w1a0Hg=taA?Y~Cl;7*QS?Q)x|19))QmUN*v9Qcv8a^Ker zRc@AzNWMv`()s79O54UNX`juLWj7)Oc)sZ_IEsN;3M=V*inJElw{D$U;##M6s<*yP zl3$@l1+_w5(p}mHbxILyJ|zd;;yqd-K`+FYB39PKjYcuQ`&u01t-8Q@n4xVjdbEOR z4SZTotT98$2P>IVmD7)ej!W$uU&kJ^w~K$ccf40HXKE%lcxa(XxYoc;E*v#Osi3YV zG_2y1p;6Lg1@;YPQEt)&Brzy)UU;ej`oXJMhm1!yg=-P)VkGixbyO}JmpB=5?|HoenI04L946rc11w4yz_M^gUYSa3>z z50R02q?zLVCOz5Q`s8UndA9#a{(L+=z1#G&B`i+l%bY_9h4J~tda|PR$*L0O>dJ|^ zpj|$83@n5kt1SmN*SM^|14OX+H^#^e#w4q}F%G)IM9O%SuP@2Ue{;;{dmFvgQjRrx z<{t=)dR6{`6%{5W9^-05l4T+z$N>GBRzkxMK0l5!S@{N>`HkO)RkSe+r;D-oD-tIs zL1_2FCEJO3@{2>vE~PuKTS#L zH$t2GY;v3Fn!bHYU(>aeP1S_X-K05$oV4uAKg*m*8&GGqVNTS99C&zFyw&E3^?!fN zuF;rP|BsE?$gZVKwEzDxL)eTPg3dOB?|J3_*qBwimX`k4#_X~fFE~L;A`I?>nUP5( z76-97H#@{*A=LU2#uMy)EUUAFAxA|LPqd(mupccJ+e4Yic*h({vp+fs$TxGxgJrbUt-f0oV<`d!8flF3AxkmZu(PLX-%j#C z%zmoe0y9Yvk7k`=ALO9lQ+lP85zwH%}DCN)}*F!Xtrmh0RllbA@Fe*SW&xWMGH#hI~3h)1s}u zM0k)g#xu**p~JOYzQco0@-68kUz%4pWeZd-Yv}?N zy!9mCl1}oC)#4bR2u|{qJpfRbbds-Jn5>F=m@&aaS5SRA%a_T(KAE6v`6-zUtYNy< z94Qt4u-?$tePkrzS=bGVkxj}-ot|npkIG=e#O#k*Z=5_OLmzNSdMK=3kNJzD&e%qb zxmK(IAxO!X^79RsIenMRQaFx<5Xu+>X^V`qG4Q4i*Jm~iLVJ9T7osVP7KSv_kvt%y zGzfcRMD8YmnPSj}QnYc=FDlx}MfS=lEdPF)*6krwuJdo@XV4Kgg2xoq*ODErRdY1V zOx?+VHe*-`=FmVU&>A064@TYTGUEqW9U1tDD(r{QXwp!4800}c6fi{knso#9yl*97 zI3PfefxEzyM4s%*g6x}Z0_vKX66sy&EBKS1lUx+DCB&lLFxz4oA`k5Z6y_rY!`HN= zj4{jzM8;mQ%uYs;<#%*`y=LS$@{_fV$s_Qujpe<9@LWV;%cv&mX18MM?moYQ7)SY_ z7Z_8um@yoTaPwcyhh6q-RGW;gExUyQtD043*AGK}M6fd#0w^ydtJo0pU|Wk|!)!EK zfd{odu`?_+)f4O`x}ioM?m_BK=vzKUBs5%EGk(e69jW#=b;rb z&hDRB8!I|sK(VKt2Y6dLbAhIQ^jNcBExYjJe8hzZny~?Yxqnhb`uh+B5*sQ?29uCw zn1PFu^UVZ9hgI%SkMefM>X}vuy|5iM=blk>u2~O3rD7s#MOzEhUM-oy54Ai+t`bf` zln3>xg?dlLJjt{UU>(#CnX~N}JQQ6m4|1uAU?C2L29f#;VQ>j-6xd2cOll_o&={SR zey`y{KV(*Oe{pikBQt_Ruj6KG&u>ljAmX zGO?`5joj>lgGWrba~b7AL1Kx<2=E+(BP-v4(GK>9LKb{8 zn%NR;$!60w+bc>W`li;G<#Oy!bakaMp*8?7AvRw|h-=}b=u@?@ze0yWKq;bBdp?|0 z6~J?m%ojV|r1Y}dtWA+NNqlZY(dBB!yOdjPB08Fb4+`}ZI(%8 z3>#)TTfUJ#0n0-9}kWL5=aCaoahj({WbY&*Y-rW)F!$5aOB!=7_xtO~nm<6G* z&|Px&JXMS=60XWOUaSD!xkp|y{Hw5N7v&;U!hLbaLgyZwMg?%H8XPH;qZuPS=9zZe z$|M}!P*!L)+E7<=1X@im0ZtSzDT01Lb4)H6OB4n|+1^lz&Ts()d#-_(h6PFC7p>Jt z^~5|i;T1GpOh!Uco|>?6zpJ9R@YL)FjyyFDI0dxicL^(Q@y)ac_;Oa~cz4d+J8Slr z()_6DtT~`eIctIgAvoo1J`Vcbwjo7KE?wB)e&1@D&peTW%(kqE5OwSgedGqsb{H~e zf~5?KWN)Gc(tt0k|Mm=dC^jls2(SumOe->ts7q4|2gv)hmLP-}0i(f38b+b12_{=| zm+-c!f<>Jg%ezhZVwf3K8*LTV&up3yh}ZC?RFZ8RL(*Y)W`E6r1#pdM2^LotEs%Dq zI&Byx6yK^8SDuB_Z)uWmF-Fy`oAm6^P13lUYC_|NZc%;nmRBZqyMul!}Pv1^zR9HJ8_%hVpt2XWWPpI<8#X*6Id$b+-O z@P8YoVIw|D|LxPa-! zBv=O_OM8>?MkO0PR%FS`iP@+8L$;!*`j%}}ZH>_G^hugh=-dH`THVRw-D!8uGEejI zH!j=h7&k{zx#`MkpyR_v)iBumC8&UgET+xKojOO2{Y~s@VCmz~Xk6F0rAx3(2XvHg zE|dur(EAv2mGobkF!LPxRMZw&v`%?J=V{YLJ4}qdpqg}T2`^lLWJTg;W$wVIt`Yxq zVFNEx#B$emy5#}yqptLyvJy#@jO?E{teVUhHcDh6-%kIOMS4|sud))K>!rTbclOeM z$IA;F@zCGJDzx~7QZKa1E=(SXIDE-$#K;R7_#-i0qGN*oPF9$ZfWo;th6o30N(=1m zq>vT`Llny&z!1?ac+kQS*%Kf=wzZVzN5&Avfd-Vxa;rFd>ld!nLP}wrk-Q)S%6A@$ zdqz@?#K(+5NUsSpB$g$2Zl-~@}_9k zbc4lG9YRoH*%2a8O5gXG!xB$FE3cRQqpZ8-`{lQ3`OUNf`m;Z^pc`?=0;TlzmQ_g8 z%0NRFJP?Jf;f;+tDIK-w_0l~L2;N}qJ&3mJ3~OZ*i2$D9+` zp&7Et0hJwvT~1E+y0=m5diJImnY&;O#we+IIR~kjhLis{bHV8c1;LR4xR6JR?z(Ex z3{*guFYFnR0|b$kBIw$3=l2hK7Yb&8x``&K9a2BF4W}_>z$(-VT)_s^S9*n#B@bN? z7m%=}1c+fk3q&R8ru|iDR|5)<|Q3NGu663_GA22*qs( zL8os%4@cH2&gzN%>RM9q>5AXAK%4xN8t%*n1$hKI;Up_zRXOf%%<_f=9fy+KeK4JuVXF zYnxT(VH+dvSuNLlyhM7ro~u)vqtl^LNLJWGUDf0i`6$tO0fg_<4>T7-PRCzpJvoDj3hTl&U@TjE*vpt zW=q1jqHag|4G-V2)A7(mly}U~1oSxW6(1qWZ{*EH44LUYW}U%J#`0@H0>`PlU0tH9lS&K4qY^thE_E1f_$Ce^+tzL zZW%t(yLMR9RqJHBb4Y!O!KpOMwp5=9lJ zC*$Gc_Og4X_-f?IsN)d;5+{Qt2_%H6F1)aYEUA= z%c8NU4HoN!qFwjhYzsZfpderj=iN4dk*trHy5)OF$?77?nYJRZrl&np+=++R=D)uY zch8qQK753Y5N~FCkQXtsEwO^R`Z<>F+zvVsYrf()cWMf%CCp4Kj}|6Y51{{&b|tuF zOj~EM%yg)X2fs8QlLXu_lF|dhJji#UmT-P|z6aYG@;xSuVNF$o1q^nd4?D%Z-A8(3 zl4g>E44JL-_E)DVhejQ%f&ql?+6+qI0c9jF1tlP;=%GN|6hSP$qN z!cfO3XjXJ#BO*#J-=K`SAh{n}+$GJ-5{niK7!is{Bbr20;KgiO7?5F-rUh(npnZ)h z=tc|77SR(&#bC7v0J?;>jTdnd$rRhCk_}CN{PobM+P&;X;W2h48vS?%eFryPf0we`%pG)(x|{)~M~a@6Tb7)BP~r zq86*=1%ioWlVoM&%%9#ICE}^0QP5)ggJZVZsGcceHQ2z?7bdIzBAR);4S!G(*zg^|`o9=qr4}YD#gS0+6&YeU zCabi~1{YjmWcuA*SU6W7WnMC3TU1%dY!lc>(Fz5KaS)d`C5yR&EK#Ta>D^I=58#un z&5>P3&KN+!N#Ljo=P&+Snm8t1EeOUv`M5djuz#Y`U=7WgHSrP3Dr-No8QBpdS?xk> zP*U0(Wr%%&ftw9?O z2zG~P*r8*S-yjD9N;1;)^nie7xb1K1$CsQ!JRrikpa)n+=Rao6LtnRQ%fd4muT?fn z&B&RiLt7~_#InLYdhou^*Fxhmg4Ejq+%dLrG3hzz2<)bcJnF=n`?wb- zQ0No8Kq(FF-D(o$fxRkU4c6A*&~0?49+UJCi5jB?5eV{ATEKP?lBHMt8WF+;r+ev5 zbv8!xp;1e7_XLgbYbgwu?7B?tSxRQY_&p)N>SZ{^WO(?oL;&9@{H!LZ4249@-j< z1I9Y&8FZ3tk{jt3s4c@H$+9?U7;tqKHNGDmQ(@0@(3Qt9=%f*^Tj+KPA~x~Ix3dPY zGO=e#Dx>y{5+ea@14$;*{opw5(kkI+KpE)~AY#H{K=5Ul(ndSoi$bwX%BYcIQ(1*-5uooB`we^~L81DSSg}}lD&i4K z$+vjKW&$va1#MO7%);%=UCo)*KPX~i1d!lYz={+1K<~050?U-f{&TYeIcy?ZlqU*K zL_3P$7@LaV7<{{_I56h|>eyuFh}MUQyG=HX!?gI%oep(x-F|QiPh3=A+Jd;-`_%ft-ok@qMkn_!rpJjWIWYuOQX~9{Ap=@0ZOvZ# zvo{93lss=r9$F+plq_-hlo@Mfo0CLObHD);v7o)wA%|2OWCh>CC$+!NI%p9Lz&ATWIf%T5eAnZ%JoAUObmi^*G=D8Ur$B#z!gEtD&nhQx8ERjl?VC}2&{ zp#`@D3K;D*X{qRYCu2%hHmr#!PR5v2X--)AnmCG0aRod~*d$BK(Fo_#iP~3_oPls@ zb=}$)a3&x!P}_i*Xvx;6X0pV*UB>+N_AI(R-4HfXqCngn3ZZIP1nZ$L*lx_(Mr}mj zXy^cDz-DK6fTnT~WTIPFJNz08>y*hcbKxjex<^Q(7oSNU;je_m$w1%>a}%CCeC| z78NbBgF+5af(bm-W8)%B#0Ermh#3^j2829x`QYO|vIB^B2$6i5)S8yS0c;s}*zIJ2 z<7G!8G-agX9TuHc8Cd!LD!|R!j#i1ePjpGiyWS0j2*??k%P2{fftGuJ&G!rJTjJ+ z)^dp%PufuL>uqjNrdS=sxqY6tM0=o2MSCzNd^3Rm`gY&eKxAYFf?{{wjHbSFmw(P2F9|5X|n~4NL<1kDuKKdW;6TiyN>DU z-zlT^&{gadS-I3O0uq50#0NiF5k*6eIHdc1Ror?39E^1jxqVulNa|DEUC^MINf4}r zCoZ7M<~(~kRE~tw2wC|z*_!wh0|(TPr8wySdq(poVGwnK^BHOq?1+seB(?-PGn|`V zMNbGVLvOFCWI?|EZbtgSuk1IG_usje`!Zp}rS*O0xuiRPBGL1oe4pDZNBb8G=j5Mx zcGv`-U;SyGkMxhy3+ko~!NM!j z4cL)yiv-%GBvn2GTTrqo2x-AItb8n0F;yuYYwLi_No$B&)8-`;oJfI*OPwG$z(*(O z3KyUG!+Mv=&Sz6BKl3Z(}E$UsJwn$Smqmg2Q(lRq(0LA=Y z^ns@9FJ5Z4h1ISAvp}rnK0UTBIHH?jYt&_gSq=P3a~?{Q1N|i}7$NCx(hNxdnN|bw z&BAK%Np(BwbpNyr82lC8M7FsC13;O~TorA@OXJ?qE%qYb%t^z{iI^t6Pqc+*LaGW^ z852B=SPIwr9>!HoQ)J0g1vXC+3X#_iurgype|{ZuN`7aiw(xHD{?FoXhBJYaDbztG3K<*b8;YBIikpXu z8)kvF@=UHLEx>;IYX;l~Dc|fKY*rCZ7au(DZ`z|J96;f=nuza_uq;*(PV6FT~* zzyx)`#Z_)z7O23c1`Mfc;ir^7iRb=z!seXM<6|z~gWDNjnG66{V&M4DYlTZzj4`lT zO~1_xM(c5!^y9nna4->rjk(jy*p412g-`a#;WISFGbY2h5Avu-cQ$-Vi7Z83lqNt} z0cL=*37=b_qenv53*=7kIh+}fN^X|+mv^SrW1XS=J~|V(cE&b*nQ^t}Z_=4>|HUIS z+KtTjZg}{0+v>~czs^Le$+3II3R*!90z!AY(zvj0IX_b4FvC zjAb|WRszxgpRjCkmv|jrDa=1G0@)zt!02SOIT#Ub3I-3ef(@{mN*1vJ&Dr2LxIZ|` zQ7EAZD+E-GEP$My!N(W^+=blGkO&*qLThbODi~!=d|(6EcT1EeGp`CjAaUo!5hch7 zh*T=r00c`%b?AHKxWL_iQ9s1X)scU@=%LuCrr8e}AZ5G~Q#G<__U!*;M;;4YN0vT% zabkSAza!0IAdlJJiXr+R>c}_$q7fPE`my=fx9zsAevJK_bmK205agurs$WhHF&<7% z{!io8Mz4cH=Y_dUGH)1sK!vEf4q?zlkxhw7CLb;Qk_4IL4T6 zB-yx$bCcxueq#BP6l5)yiCA=IwKkc?o>(xgdad{sv`Eu;oGkncRpR^lsU&e0+9Bp= z^$^cxo8mKd&=j9pf~NS1rh;f*NahL$)3fPVc4OxBnx-Y1(+9e^Ilaxh>k%UwX*8ii zD40q2UpS{v07L(r{u^OflLL+vznBY$b;&o-7JpG6`dtKZnOC?8^3=p>h7t1t&Vte? z7eU$o>%+~i&%(I#4aByLr=VLraJmcq1`>lj1u?g3s)d^(^Hcm2RcbQ`2XIutYZkh- z99~%EpOJv}-;V*(_z7%vcKqrfbQ7KxtyhR)Dcy94c+{4?{1sExL&cj+PZW z_Io9S+}ZliEG#2yK^VoQN~JvHo|~bKB9JRh*=i@w&?Xi#yIl{mQ_$#QL(Sla$z4s| z{9^nv5D~7ihM4pe{Ok|=CrC@J!xAP3k!5VYmJmY6mctNzw!2=tNi`C@X5F{fMJgyM z9H8@)>*5rJ)Nn#^{v`Z@F194dkb=hE%iDlk^!b#2$j+j=d^K9y(x_UY(u~0&7CV06 z84wmDb)sp}a`;lv7);F$4zT?-v6kssIUmU>&~b@%6Eq4fjdVaX;?MZ0p!FbMdE#LhQF^d}4|r5`iZku9=X8-;in0ad|y=-4q0k4E9~u~vev zzy0DySx#vspDaSzh%Kk` z6o(kRH*gUX#;-;fJZMLKC7&CKJpxi%S zt^CKh+vQol_TQjSZJW)Xevk(p`Ksap+=>seAezUN67KlUWvq9^el6Kt*MX;aWK+ZL zcQ@xKNMmvSt!5@(Y?(zEiBI8pS0Rmr=PSadf0?u(oVlFX1*e>pb&;^5i3BL7JB2lx zJ-T-QPPsNPdNu9{ts#k`&0>r^Fo2b6a~=HG)?_-2k;$J*W+GhOP=)Af;Y@EFRqNBn zK0f13WDbFaEp~jWGNpzJtUL&~Fjo30k)~Z=W_ZQppk#%it%&zKWTPFzxP2uKhbMBj z0=7CUz>C8sr#4@e;z$AV7%MLXhGsCC83CHLIbORj(f^z@?^alG&ySWSw#+&O^#5TBm51|yb0T9;AWS)u;eTW!>%~}u(iBU3*b(w0GX&kMxk>G06 zGG0b#6V9ySLT$9$vksnRykjNhMMzK8f(ocg5=7fq}!|W45ZY!G-yChh31gB`V z);k0?ZXC;?I5*^go%U;`FO3;x3ZXNGn1%=HTVOdw%l5Smv3?QK-MVSGU=|EZl zU<@tDiZkT_Dcj@d;`?O<6ou-Zt;HjvJgCR{b8Nd-7dSgqln`h?fDH1w=P4n+^j5yc zl-Z2LAz@A$^w%MwGz1%@?nLi4h6n2#^P_@~*h-h-el($>Mc^E0>tq>KB5GBYTKj;C z(X<#$yQO~KXeXc1ure39_*zk#0T%%03LAhyt6;5YE0?;$KvJ^fHBvZjlR*5Hg&p%@?@2#mw`IAXVW5GYy$)IYm3J3y*xn{P*qI% zmivvb3g`2U+{1E(Uxo7halMwOBOLh}_qyQ7zrBxF+tQNP!I2k43xt%Y4CVBJs8Jao z`2Wr@?ggP-)hIO|HgBY2@apOe8Fv)|H73{9tFy{y8-dUB?9o~O?U`dBlOh=eXO7|P z;0zvKokgp!(z1g}qRgwam^3f1UTrLor?ifiC08cPJuo|rn0UHL0=9iR8`)qEF?f(a zf&dbFF!W;d<|>Eht9P0sAIliPiKdUA{Q7rxyzcrvk0vk9grzjaU7&c$g!Elv^;tuV zgW}kw%y~q$bU9#;Z3nSv+a}vyM}RpD@{9*4jMqTVcsQQtWl{y1O=}s1e#cvR#$)ts z=9K!?%ta$jJM*f*N1L-z-w;zd#0E_r5%thx;=;sQ-eOTjn~Rus*0hk{j0+>ZN}uRE zVW$>Kw*#oUb=+TY;Gso}&=2@xaba9Dz)|AD%rXiG5Dl%*h@Gl9P~4NZ2qop6h|C0s zdq+czum@0=ieQ}h0_Y4)T+^cv|N9KbbA~i#3nT6Z;5{ErKQzFuk(ZK2OsrZLR7{HC z^fv^whvnCGOS94v=G!r+9_i>HaP0WfLPG#@;S|)LORY<;4}++)_-BBSsBD=C)6UWP z7i1eov2C5{N*U0*U+JCHrxv?$@$RR3CmnK#caOZnyIb^5YF*2B;H813f9bG^5E_gY zTtKWhaRJI};(~Y>Sxsyj{7DEGG!#u-AYr&LAV!eK1r?l}3ob+sXISwU9yMfd0@4fw zJ}NpniKn(h(<6$GU(jShIqa&bgZcPDuj1pvhqPA{W<8Flw5I%V!r0)OLn5K z$#0H_9c6nh;M0Gbh$_jVroM#yr(05JAjgYBU;|e{pAb26CLmJZ28w&Q6_IDtuK)&U z76)ZzVXYZ_H9+Vh6_AUfgGv+xrd1mD+nul)akHgFIrA2_tbEEk zkhro?SbB;=kJ?vD7BE?`0T$&ijLSc7{g__BNrAo zhR>2aO$2mp-YQrYK}$XwYUq`ATKH<);8UZe7oxZFYo^koIPK_BR9Om$0MN=*3&#&v_69r$#mVF4H zC@7N!*y2uFwF{{wuX}bkXr@rIh|ox)v5`|DVL?XZJN0-E67 z<1_5gizT5ztS|j77%7-wPIC`_1>2$u8M+ileY))yNsSCc$*sEyR=t2(#y2SLoR1B5EeW9 zFH8t7M9*cBrUem#%x1iBEHO~)ZlG`pUJ{8YWM~8S>3XnEYM|K$Vstw(OrG!#w$J=d z-HpVc&DoV(P;?fbI_4(w(gF6KRn8hHI+}~%+7BXLYd9+%243$@Fl|bD*q3A|D+#;k z1XIMx@H&l9+hA|h29u!G4|>|@@3k?>fRtTCw`bcOZ37?R@SEThV${P+rLCsTI4-W7EL!ZG!| z?3?^M=j8XEqvEwdXxfBpUh4vie`3wnJ#dcPL=XE43C=udz^L*&ZtM&4KK;l*f~|ty zagtu0HI_Oys2Y zNfrb=-Lh1x&I_}YUrjLWqxz_PH9oO;W#K02`kbE92;_y4c4r}v5N-902L;6Y0{_0I z_gPoEG|O7w_x6T9191hFp|(|qqK3$2C~cL&)xoz>^VQKGS;JB{^Cb4}sJHC#n31nv z!w%2;)0yBeo&N7teS|0su{60+py$u(5sa+SD8&7qv^Hab2beCUPe}K%3mbcL%^FHH zvat&nWDT--^&nfwx7o4<+1~yMpYOTQo)l#+FjT26(8ETS!yKsjdEc1n9F0fJ&~=T* zvNb5SL}Ap1*eLH?@+K+2QQrIY?;pgfKwiT$p45t`9*ZY30QH)^aSwp6S!nak`Wga| z&iiE5=iAKMPPLh(?RY-r=n8VMF&OoUt+mp^R{wLw=cf%y=?j)cDtT4)u6v^7U(+&6 z@;<3U9a&>(eNSZVPCCgMzXKb9-d=(LfOBCs>Vy$h!GsmA#@>tTW7(3e$dLeH6f<-; zScEX=a8vW_#j*vt#GNLCh*ORhMTp7aNckfIa6b#O;WMOmumf)b7>rYZ9CWHO($hz} zq@P)Jckaff>-vZklW*cY78e0j`5Htjp_pMDJrU(IvNExA&~G&c^?jiAjJmUIpJkI| zSuKeTFRKl=#W!#%RGqfGKy=Qr+HXBJW8i%2GHo5nvay-Q21bpxfIy7`{z5W7D8yQkJQ-?`dc86#Ws+zF@g-&-CPZ=9#DFl4H7M3R5` zQ$TjN{J|ztUlYQwWYho*7nMu=Hf=EwVXUKKmpd? z{xWE4J44b4v}LBA?X@7Qp8^Jyvih5&RThd~c6Ko)m|_dZa=?T&g14NNrkvL(C+DT< zEM1cKa5{XDuky*R3t;f+;c*|n7VEk?mUeY~gyO;vo%f-xuU~%Wlcj}Sy;t40`j$v! zy$0^$*yV_*v*5RGeSq;|;9a!Tbfj76OL?Da;#5rc5sw9bJuJ-?_GcsVMJE^<@GTif z6d8AxYG66REm2Xrvw@?p4UZ5T8%oI7_^ne4AQGZ69~OaAZF#$9)olnjLkAU0RX{Dj zWrQY2!;p2n72#361*Ab3;f=1Df~Z5>4z*E!b6r6_gAikF%hyX#Tyda+{AKb~2}KEh zm(hKdFQzalA!r|mg3R#KrAs6!#Kn3Wj98HIPp=rMr&oM-!NM#9pj%~Xc}wMJb(|L? zR~I4%GojOFerbBJGHu(m@~kRf5!Gmt*v51+=YUYU-40Y}_P%T}NC1h+0~sPJbUI&l z8A72wVjGvnpvZdCh#eRTK*Pz44mMeNoi(NfJR?GSU3budvwmk|9~0$#2^WXm+W;55 zQsW?y)#FZzcOat;$OS+HK(cHgybwWIec^`;ROjEaH{KGx9$ z@Nu)U#zdD{7Y<}`zVAsRYEfsVxF$KWQ!*hygs{Qcl$O{Eech1DlIBrKh zF~znq+M-6Xvl;-0bEY?TF}s9%Y5O*V#z+R!CYE|KJR3ccpIwfh%NNR7$wp4(XZZ1R zK~oJhv#mSS~b1!zspJp?s@5ix3p-sOBILvmnUB zg68jY{0P`H1uzfFH|V%s<)&}Uil8*jQz0h~1kxYS0fkN)F_JbxD}jkEO{$F&GvoPFDZ~iXK0@bklvUfX z!t_y=?~IAAf;dHs$bONav`0^>AyhBEo4@GjGXl#p#wT#&1MwlYAiG9pNx@PqKxbG41EKJ3D^+B-%wDrX$re7_)HT zV!@|1tL;lVEV`JZYH(5*sZ5qxQ*R_JZksLT?$Sg&;=hww_){nJk}kU&Hq~G2s1_`9 z<+RbvozyY?W1M%Yt0u6A@Ph)uPKM<5h-`j6)*)|ZXCynBg@z+W1j-s^0#+y_d#(P= zv6RwA0nkC1FKOp7pN{BOBPKywDKjzzsD1$H`zA02uRM%a>byQyliH0Io9IN3n zhkIDfc0%K97c*IbB`TfJ_FQZ#KmaY{Hai%_3$Fj7!D^m33804V>sY(XrLU#aCQ;HvC{F`B6kbViQzc}KG7jM!zXgW zVO1ygNRb^bJG*I!7({jK?(l9YCLpVEyy0yGAUI}(R5gdZnGR{IC3PL50vcqyGb$C0 zF&i6SVN}3HjSBp|7!~=W!teV>MSJY9ehlD8Q391P6w)JNoYq36btqFYI!e0n5s&Ad z@!8(SXLRfM?9e|xIM~?u>^OLQUNdNX%GnMRG4<`J@zI7+W*M*1gQ3QUC9Ib5fvFEU zK4!$(_V|QSlL}yi5on7er7!{fO2#Q8*=_{|hy!n%0$>rZhWEb`tSc5=1>iynt+3vT z796a1-Uh4-TKJ=|h75%I25V0Xz%4TwjQq`D2LZF5PG~W^Ew`^;y73*e_L3CXrO;n(b2-jR*FtC z5Y|R4FjMI|Wz%(9@EWFMc#Sv!*26N+AJ>Ec{tbs1>87;me81i&Ks2WU5~c7Pw2i1M zsAop$$R}AZDut?1(LfC;z2P_ILZ}Z?F}Cpr5jy0Xfhq=}w9)brRVX~KN_k-DolerP zJeV3NZP2NKP33{!_p5<`uA1$n(a6z;rHT#9hUM#}x@cmH{#wmfEA?S{*TLjn>H?)I zdApkvC}rwL8&eSgr$Zqru1)${TMMl5-tn4UfN`(T?4(5v)an2?fo=rXa=%8$tVr$y zVez;h+}dEVtv7&s$ZtHj6KQm+zR>94w9GR5yCnkMW^?$mIc$JGu<If+ki7^0 zM(NLD`T&H(@g)A3)b_PG-NxxX4c>xiO^tjw!fj|yAq@ANf{8&P0iEbGlQzfbAu_Ok z4k-a6@d|nIMnTAs*HAT0`^20?91ur6c3>%jjpK7|;1$);M0mu|dgi6`8A9r(N06tX zAhs^I81hD(#3Z)D0|I4`*^6LN85@Vf=_g{Z6*8b7@XquDaD^q6gVh!pF%D*}SK5P2 zRrUa@f+a0;-jPtDsk()Q4psLA;pa)HC9VS)-Pny|J!6U`ETemQUlX;3%nvGM$ZYzH z`?5~En~L;tV$gh9MVTgymDJjFyoL)CyDotyl7ETei_b$9r8(LVhOeY9u&p8xGNBTsb*`I6viPitmU_6U*+aaC`th23pE5Ep7b(FaU$L<- z$an3Z@5627quR*sU>T&jy20tVi|=UcJ3i>$bohDS$lr2S#=ZaV4DJtj%Wh9SaoDa$ zpE*adWA>CMA2hEJ~Z3lrHaw#qt3b5R0&!L}AnDt^&q&pgJy)2V+sX0fdnP4=49 z;N082iXvG^@rx!r9Xn{z#A5tbaxI#aSy8FSU17b!02d!;7%>Jdnv{!06K)#zjbAk3 zv;z)~bj$NchUbDpjl<)!vp|s(KR~K}c>F#t@e#p4Vx59Z7tV27;?h*Q@(ODvNUeID zhd{@#nM4i(@Z@661j^$0iXh))6QEJkQ)Y;oTVxCdk68z9eQ6PVY)P1>?GnGH&1&4U z!(&cu3-s+I{hX2!4E0nhR|U`lkL2CT#RWh?^=!*_7_Yvb9CIpxvl{r*feIv|MwJ zNtxQN;8}I9<~2Gh0Y5MP|1f?o10+JwUarDW;VT#lz6wJ>8anjS?@*}P_NSn#+1n@w z$>^8&7zI2WSjIA(tTs_~v_n;g!OfQ*Rf*NfC$@auL{$!vejmuv9!+TwtOB%akKWg) zN&p=^Q8B*`3_nnpyo85>d)h{!)nzbow8S#v+VFsho;R}qVnP^ zhoQwQkD(Qhp)wDxWz`_B=c_2)%tQI+8|=Z_;sIrio2~{C>MtO1h>I%h5;&EZF;7&Q zUv4c_9{SMXE@QUK)S2REwDM z9#kFdKtgbky+0I)_)R_n42q;^sio$)QUqkDLr;uXTuk zd+q#7?pX9zy$3{?HRN*q2XoBhHfCf&M=OY7O+Fst>;VVr#5b3^ON_ts^DXJp!55IE z!bOK&zy0>VeP_+`M{j;kM_IPpqhJ2`6TiLm-p9VYS&uRvnTs%&uAglFm*0Nx=|?v_ ztv52(1zo@U_I-=LzxIb;{;eM2*oI@#xoRH2rSxr7ZM#I~;EoY>N|Us$wDLn_nLE~g zS_R6=aX6LU!v%N&W%+HRYz>!bt*j}4+$u0WtLx6GQ5%4)!#pLwR0Zsaw048x_}0^iERZ1O zGf7r$tPr|DP5#I*PN^6HQMm(_`NknD3_#;xo{i>r`oDy*Le`gsw@qxFSvUdr$Kr?q zw{8C(Wf+Od)orOG{o6aGxi@QoEYvuy_f>n+U3U@V= zHZRkE>L*rU&C`NdV@Iq^Z7lBf*2K!2u0g~Kf|)=6*2K!2t%)^GPlur1Mx{ok9|;4; zqZnfx)DUdVjd12@ws`fO+5tDf(G1G7cfY@qcZ0UC74M$WJC1&6^|dbEJvqd?WOj2e zK7M04x~Uo@77(VV8rGT9wz!2ne!)M3EPGa;GjfQ!^M;JT{8N5=qm^KPxN99&(C-LAzrzsC?}eGBhz{J8C)oMz zDH01v8)UIulZ2rjCzHudgf!1d8VygsE>=$t2==mdSy(Ogt&XRstWt?Mi{^!S5wcEP9n%mSRcHnP z@t7n?ky-q_d5YqDTrd2NM|x{O6E%u$lDB{Zx1?L(;<~b344K};MF{X`%m$4amsBkzc&-FLbF516Q2=@2h=?#$en>~gar2`VCxARP zMUd8&TpL9djtqc{cgW3CjuD(fV2ipJ$0kFd>wLN0PM|_m;TXw*g61e0%PO@{whu$d z(mQ&+qLGSaVqnDFgl0&Vp~4XQ0G?$EhUd9`=B(^nXdF~1V-dEuKayPcVFX@B&HBruooE2Gc^6B2T}li^tFE0IE2F6!XQnF2b|6^-iFtSj8eUneh;Ny zeYqGmtJ1X>l9(l5w@%|APj&QuxK3axBoqy{lnpb345-*I^ma%|nKe))2|;pc-ow@Q z4DrhW#hA^9&v0k|_~#9Z5pw~RB&(>tnDr#@1tJO@5_QsL;&TH=C0w$9@?7n%gC2DP zp|~I-flrpD9Sp!lSA&KuIo(md-z?zv;wV^EmPbND3Do?N@1RNUOx-QJ_B8m$C$xRa zAJ3)T0r@>-PS26RGjN#Wp9>x?VXn;6W?{@BR;>u8)UR)M4V6uf@-_tX4cCbQ0 zy-9Kk08vQam*(y%+tAU8#=Wgn$4ms$)oz^ZJrcfMZrPNYHSD0TUD1QXFKfnML|hbkIED-=k>!d%pQHP*wB#%dQsewQAgC2aT=iB-?g^c!jVAtGde?pU~+3&gW9Du}q_8OKg9 z=K&jj7<6)pA8Giz1JR{mNFV)VE((Pg*4ZDdrG{=>ra*ds)5TIUlLG5n9SGMJn?#1J({XB(#+^^?9o z*~m!$!Jh%T{?R#^f&~e%(vZf}luSr|qm!NhsFdy5mT?j5gqb~pK3M1%78sGB0H|rLN+w|^dAHcDgaMx3814Z;6|~e?7z7G}0mN-PUAtLB zp5LU4n~UVP=zgPqq+p1ja-S|1&KcRCWI#0(vxp&HFd|e=fJh@^MlnI4y;#K}^U@;{*Sfe|)P?PBe%5s2D-0^BHpy&+Ceq(3u~-UoTq|E>G_R`lg=CE$II9{wWfht`+l|F!^M^ijCrveeB8DAx z8UzjR`OIyow1|JS-K482cYrs5H&dbHpH>N6&O>xf63_Y(DvtGIPmQRI?A8uiBpW#` z@`hAe$!k=yOH^2!2+%baW=-a?uUHPVC~f#U1hbjVS*L z4Ng7E*sSJgQgUjrfh~A&$Rx866OzGLf5Nr!-s(?5Z|g1OLQ=( zZ@2ZqCRz0|dCFii zDaeWmR>|2?g|o(;X-~tN=2WdYd zJpF?qsWp)`FNtaZCXsE_W}3Q~Jn)m~O5O-eithLbl)yn)9u^0UtEUsRDP2!VlqO*Q zOOh@XA|JT?cJ+79{Q0Zsu_6^xyr){PA&N<7S~H|sz}F1Ns{8{@(dSkT#i$*3uwhdh z95)wJWQhx%Tg#!aG0w$vi*JTD4Wg==ip$uUW==hh*ZUBGZ=oV7gzUg`<^{vCR~Ixz z11z3$%+}-oXAJ&dr<6Bae{8_rh#0Gk(gjL6osB_b*vB&YMsO9J)=@~J&mA(CL1rB- z2ql=-go*k$c(iH@I(VcT>?Jm8Mu|?xI8xv>i7oSlraw|XG)vM8=3QJh2cc$5aNr}- z<7RO0*yPipM>qtH8cj2yJ@HC%m)J4fEz5w=#B^ZRS!rm8^ScU++f#+{ay7xlrwE#c z{Gcn=It(+JkGORh^k5nC)h=hq*PSrGUC*4G1igQ76R0bGf3yc|P^Sgfwmh}NsG>Y` zhf>j#C9%3J0Eg*i1gRlUtIg`r!(?dBZK>G?U@TL6-F3!#o| z4kDlyS=k_l>4%i6wc3mzSgTF86o>$c_r@UVG?$3p-i-l;<`f|%1Ep`fkS&>PS0H*H z%to!FL68z-x*ZDtQK}V_{`y!lJx`9{4)`ZU?RQ&;Xgn`(il^sC7q}x+{=ov@ov5=~ z{oQq9Uhc_)xwfhs4tgBg;|a+)hCAxQWWxwvqfX0Ax$&5(Vd|gb3h|`CzR93_K)~$6x(0xUq<5ZcOelmgv2oDY zyoK6%#OKZF;48(8x=yj32F-1gLtPna-ayizN+3RC#`Jtg5S#aO(J6VihDitQ&iANC zIW*J5Oz?*N=D>R+O^zSjQ7D-2PKdZ5y)_zxzWv%H+209ofW;m*e&!*+`8YU+RVSu+ zK2RDO@WD`XLHEJF&Oz$G+i!eDpe zAT`2as3R>ZcBlIY6+=@SBUn$mvOP#o2;9OcfsO3}h_0fZzmP>Fk>kT*vjj8FYn@4l zuRzpn9ufg7w9zALMeJIBb;Z_PLse%)aT<0cqd>RpDaL8Pe8uhkl1g-8^QdA@gXn4D z^kQMwDEcXU1F+t#Bp5XOWZs)38B4{|3}hc<&P|RIYpdzcB@3*9lM_E{~8f zxO9lB1kv>z(M6L)&J81C*jdEmtmrF(g|9cp0uPGQiGbW|Bp>=oITpI4iKhdDY zT$T~SO=LAk3^Q7d7QhDsVlF4v*nu-fGIn;S&j>SdluYl|&8i|G_jTMn8YPD|aoZu8 zLnbPb5n@otm*4BDQN;cLD>KI`kwP)O6Hi6w0#)b~?-8hr;YQA&MuUYk5wmXNG-3qG z&K^fdgr}1;-Z9wxOrGOe7+=&w!2%uuKf=!A4M4UTyo!H&kgjAMnUbImP!F~*UhPa0 zj?s<=IUf2dxUaW`CA$$K58y4>1)}`(pYJmcCJ~XmKCt?1qpC9=K|3WIr67Mv+gGLY zr$@L=p7bN2hA>`)_nWxlk5;aT6 zApsGUPLTB}N?yJCKK3RazL|Docehp#K`i5JBLpL#h_7*e*GO33DX@X zqqbhSNKEbV$UT&(i^1?(H}$1ob1Kp+ozv`rj7HXK1a;F-=E7B-rFE@@{lUZKh|Rc76)kBs21^ac0?Bg*m!B_U@w z5Okd_2Ahl6>wEK7ngMmVkk2BH)`-uv;rbBWl|mBd4CqZ}5-(y6kYa3xBIPOg$e=vN zJ}!WLyw-J~5v=RtZY>&OzUZw)pHU;zlasytP&u6%aCqa7I<20U6bd!j%K#$hMk#!& z?}G=cY|3bvttAcjg=0<4fh*w07*0swDXwIP;PhoM`ME;Kwr!k|LO><1p5~wz_0aJ2 zhA7!Ph&uI85{?<2+^xY(1e{_+QaYy3tLPaZRD+B?SgAuaX>gvQxJG}o9p(NSP#Oq* zH#SGf?m!#}3dfERcOvNxRHB)|1w1;a04!y@IFts-Jimlb9%J#^27gRky8*Qm%x`d( zj}FShS)OE(B<(GuGnA0WO@e*oB z=n96CxzxcEM}!UuHS)qk3gUKjzHDvZRtt>Hmu>}y0t$sdvY*ipoWZ>%AmbOKWJjL6 z=BRcJE3lzS0*V@eeMt++El+l~}2 zi5*3?I(FX3_6yv>LK`GjBbNfiggPZfpLAygevzRTd##=4iCV#(EV!U!>h_LQa%Zy7 znBejK{=h`#VQsR{io8#MsSi)`yqw6x)?ST{Xjm3aRE;Q>@;>#=MZUul)CcCe(tzDs z3>UxA0an(s(IFPYww`COxUf-?EQ~O9V9+LKPNZWbqjc9sJds!s1_PwybV3jU5zjG+fUcDyI>vVU7Y@F{gr%spi)3tXMHB;6WR(=$3hNswIux;T!N}7)%J_7G5J!IkS+1dyTNb=!& zplu!Hd~+~e@mgi_V}Dt3OVm{723yVvbSnEj8ML>7xUTtww`y*!7_N-y3DjH8?+KXR zblR`m=fYY)ncqnFeV!;5x(0`#;w#x>pc*_U&+?G$|*)ME?fR#u$Yd^^`o z;-agY*L?Ypb@Kz(C3VwvScYlW;j!Gg4$H)NoJ9x{8f6s>Sv1n1YIg0d!0rY^MoTqf z#UF*Wt5A(_6{-=(Y5cVq^CW#J)*Hhd$T0WRj-5inmzsJ2uR{NlYCDSHylEhWQPVN} z~C7zvOlLb&7_Ghki#vQ7HA+Cr(NStJAd#D2j*hwTwo>|k-#j2S<^_zS}AY%)UA+0 zbx+-CLxBQry9t4^C>!G)Bqksj1VG_2mIV*2fHt#t*YT{ zGF1~KUyQ=4Eta#)=bP)dmbNq(L1E2B-uYwnDnL=KMx1GLN2JE%so+XwBBI9E)c7j3 zN9rUtMwU~FI!#He3A1UCK9PYSA2OJq-#K`R*al z9ndZ<+tHzssSxaDler@_cOM4Of@J9;v-c@C>>7!CO&=S^-{wUx1g$u7Jk6s)g_ZyhLq%HQ=qh8t^ihHjD;d z=?%i z2Od&mDP5CgF$G=9wtOXQrVCA^R<9}M>xbOo!#-2JO=NICr zjJIpFfQrH_JcHc98Mc`#yuDE+9*rU&vz2m7+?`d|G|B{JggYN49H89Q>Q$7#c77f~ z`dOe)2;P8yCB)?aWmbweOw)nt8Wlf&^a0~fd7<9c$FRg4bQebD#6SDP(OGaUr7Cn{ zl_k=wJolIpCLx7UjLa zzf<$YMyOcGd5|w7orRMuEcC_PM+<4P#and7SNTaGz27crjAH~)=OX``uCN|mxbDT@rjyg)ZCnp32f^+w=?>C6lb&)?8cgOoDhfmf z!Fciy2GkKwxs`^``7R{Z}R+Uv)-R&XV0ECt7qOYMlKDi zJl}__U{usMK6LeElg~bT>g2ie=-{-OQ|AqPZoWzlf=T3WXGy=dC4CL)-CEKYkZvu1CFzcq_jizPZBO5z^bbkD zt|k9&jPt%N>D@^Sf3kBXpI5-F7~@l;6qn^7m^N!#&$P)iW}KH9HcZKQ*26n2SWUR{ zX4dR;9ImY7-SC!o1+0jt8jR!l-uwwa^|udys$XrIG3|`Gljojy5LL}S^PqWi&pc@E zCtF|o6~x-X*18BF?HtDSv`|`rp=y} z&7Iow!MU@hPRS&!-&r$zsOy9{wK1GRUBZEVvw0tw{lOVi zvNNV;M`bgn&dS=S&7=I8v*%BqkxiR5W$L;1Ro`snu1~bLQ;1Q>|OG zJ~;EBDO1mU{aG`5be%SH@(ijCZs9w@<7EDHEz%$2eutLyRorJS>D#y$trq!z!o9|( zNI%WJzURMMSc_|wv@IQxZa;eG(Yucxo9&mqfnH5$pVa=|_V;(p>*(#cXxyrCH;()2 zxO+Q))cH(jG;ybi`%ZlG$?rIM+R42quYAw?_k8O;Kb`#2U zn){2n!_MFD{1eYVd+{e1e|hm^i=*Z3%THQ<&WbBn+_U00t9C29f90r*II2YDm`kXe zYK^o_QfRaZtx*|loTN~%_( zwz#ci#l@K9Ds9!s3Ju$WYFw&SVk?^ZD%DaPR;>I|wNgV4m1~hz62-MrSSwfC!oV6D zhHX*M7S-Cy<-i(UZmR^PN~u(>`u0a{wIGTkKCD(;SHeEBh_0+={n8z^07OhU%N4`#JmiD@U89t2TY z$XKcJEh7Oa4f$)eT37{-X;&ax(#hL=T-Isao+L`8Qcx3W*Qf-f6EOHXs#K~GBj#uj z(kqofchHNWj0F;X&l%p8u;%F}48s8A52BK2xm+m&7?r>v*I=Wbtjgd`7{Uca$kkd@ z4I)Mjs*A~9E_q^$AgVGW38D*iLx9Hc1&loT`NorA2&vKIDkx>B4D3Kz$PpZ3eBlTV zO~Cb94UQnJ0kt*gpcq_%{(#x^+R-b>-$p+{OtA)GFxUX;)*zrNj1%hc$O9JvJfJDq zWC#tHfyY?yqzZX4k3r-KT4I z`s%2Q>yz`3J6zYRSL`~IYv<2abzIH$;@586@I9`F?ESm$;cqEI_XVpTx$xbG%zsO8 z&)eQV^UfRJ61?dX@7sLpPu|kG{7dKV{)L^!PkQEw?`_`o`0Tf?3FosWPn$7y$`RSrDN|?kOwQi?rmSOVDLL)D)6beQd$Pnb3M|&lI;}i` zKMCd{Ed^KUkUuG*Q}}xl{*fX@SEHeIc8v;pFI^b~Klxob`1a*TM!esVzkB$55Yosf zpmRhV^e#aaJK*3j`1(^xu;S2jg5b{CzgfB`xq{$=XVzZ$-hAc(zbX+7?lQWwL%7HE zJEr_{+^k<-w&n-V{o&|e#3zmB$Pm5SJACuM9N6}$$K#jA?KJA3%{_ZQ{ksRgTYIdu z@dxYg+px=`H(VRN^-Am3l|)?^1UDV?$_pC`RF4bIcTTt@0#@E?z3mT z^?^~3{L90$=G?sZ`m^p@H}slRF8@Xr6b`?21~ zemU#8`=5F2^#6KCxv4}AP&QCBUXP-2&^$Dyo~iFY@1QAjC%=F8EP%T8kW4vn=HyvD z*_*SDVA=3AIEcP~kUvwpwi-V&Pn-3@d6qk9=AZux`Olu=vKM6oLi15?lJ zo;v09$y27ZPdQMZ?|b01d8f~tJ#%V1T4Pbpxv#dIc^^E3a;({{1r_C-=gTS5caj!v z73n{4e@aXGzq!}MrFah)4BpU^9zj~LE8ZVZS`(QfJ(YCp_j5=~K`!3UC*4}#V$v%A z@nd#>08&X`}o>9pPu~U zi;Ie&P(=jBzF{`hS_yY0%` zAHVkFFJEKn6(8zZ_Q2`aKY#tpYc2i271w?IlV^|r*NrdVV(IRkM&0rJv$c;t{_+M( z-+J)DQ{TDQ`uqOy@1mq}-13m6zkA8NX`dQ9 z_J)aD9<%hwZwHHi^QDixbmo@LmVRp2?Jk*8``LztTb{P`y~`%eZo7BG@=tAf&eHo& z-Q|f>cmDFfeR0c+TY~)OSHY;tovZHO0{eSTiRZUZKljj+Pj1mfPyd~<_M1HG_Pz1r*r~T*o_rHGO$KPR-;UM@x?=Nn= zX57C&JkyK@EIc$W-+AwSPP%egaFmt4&kGwLI{4}{e|dE<-qP2tyJz{hsXtzSyUBte z_~FzWcfF`~_u@y4R)XO2-RE@uVfVk^_?zG~%YW*BjJbCIX(wJXB9v?hg8%&Ivv-~H ztMCW=gflGt)^nb@{8OKud39IVW9bXKKXl9a=eGUkv~Yo?&l&xr^RC)_$}Q)FODz5M zZ(i}O|Hwc!d&e{uC?|9#pmultv~!&R34>FKLKb^DbQ ze*Ba08cXlB{Ea_6_?!|DerBAzZU|B(Q}sGbyerB z&s=ol$13rQmfnBKyRS{3+2cXhH{d@^i4Xq7FMf7Sdc~2%C{g;;!&hDP!WHR%yffa> zy?^e=Gak*_*Pb2kX6XwbSo4qnaQyh*kHupxeZY^_#+{q%cYH41-_jr7GX5V9TJ`>m z`r<<@o!_>6)u(rU!$UuakFxajV}5$eB|qHdGrx<+Tl&#+9{7HF~Z2*ZK4|#p-^UlVMD` z+Yio;Ix0KtV1!t+zAp?aZy3FkDu|(D>ddoyvYy%5lxg#)O_`dVabEV1Q|HcZ`SvyB zMOSN<)k#{CtLV$I{{7Z%tDo_~vm{eh#d#8{&2Jv#o7VpAiRz^G6lI=9y0wlCq>pWR z|6S6@x1=BU?_Y7eX3n0XLF(8kHIt?&KyBE%-8B2X9e7%*p3+UwprazDROYMerhR zB^h+h#?o}oi68ubihB>ZrkW*iGzp>iA}B>7qM-ENdl3*2X@W`#By(DTplzsa zq;0HiqNA;&qob>%r=zc9pkt_Gq+_gOqN}Z|qpPc{r>n1PplhgWq-(5eqNlB=qo=E< zr>C!Hpl7IOq-U&WqOYy5qpz#4r?0PXpl_&eq;ITmVxVoHW1wrGXP|FjU|?uqWMFJy zVyJDXW2kGWXQ*#zU}$J)WN2(?Vx(=PW29@OXQXdrU}R`yWMph)VytbfW2|eeXRL2* zU~Fh?WNd6~Vgf}p0rDo0wFx9Mfm?rmw>nTaT>vEM{bPHu$H?m0toE?uVX2X!HU6s~ zCqE0w54}HnJcArYjG>|S<~&;q1Agvrcn07|-TAk2(qbY*$y%}G*wBc0czO6atiBxq zanV@i;297`DsB$G55j0%#uF*wWCD#&j0fE=Q^`tT4<*(UPyiE{B+%%BaT!jJAb0}} zG;YG+s`N)5WD1F$Ln+)x3VzOWR(e+MOiDBY5rz&Lmyn2wU`|0ARKFg$qVW#;i~nDL z7@H#}K>*Xk{o5btX8!a8Jr8JlY|w!}!9kN%nwpvcA<1+O>mdN|`4gW)Z5oWhyq9Hd z0epumY6H%=4@jm4;vyabDmdnr*x%wZD3PI55?Lz_7&x665<_N+446qsw*b;14TC%Y z2SKqJJcP!p@orGwMOR^w`Mb@+O|Dcm&o58O|#8UDjb$y@RcXs__wvb9kDv54qW*Xi$? zT2`wAf?j0hE%c-Yk1)^)9?cW?-U z7ZzbYH8pc^ban|Ok+Zh#DZAR!D=fYUZaJ@5wK_0}M9$k*3rQ|reL6TaAuR6TOd_Xc z9Y51?wtryaedfk3l~o;QFJ10^@X)EW{YF<$ue1BI75;%ix%mZ+%`IoocU>M3mzG(z z`peg!GikAFo;(#%q)_D*g4S<1ax}H=jI_*LMaL!X%luZZ4&0D>@={;_qlx#Q<7owS zM#(}=t?HvKXD{~-Jl$=*vsAl4@pfO&jQg^ct9bcD#ZDj#(;Gz6>bVv9)8)A4fG$uAuMc`*EH4kQxPXVK1g{S$ z?BoSN4G0=x(1CIZfqqnw2P-m*+nraAcLnG&WrRHNc3c`G;J$BW9J;fS(}QuwYbfAaU!$%(>j#s zLTPtpg}BpZxYM2reJJBH;!j^Km3E3Rt(#k5wi%ZIk1?MkpAZjSa4y$Mygz?h=4^QZ zX?|CH+Gd{mN+B7%P6a;w!9rdkZtk=yvGmWpSi)i+c$$Y#>)?{(5*5b4HUv}-l&-uu zP=?|JxCL<{crmOvPJ&xfTnal2Cxe?KEYGdLr-)U?M&YAzN4XktXK=l^Tev=DO$HrHNeEL4)<-L24rk0ATPg!}z!OH5I!>wm7@(2peQZTi0SYB0q z{YE+O99g9WmR7Gv#%C^d;R*8>EL7DqHg$4w_3-lXMSU+glpGdKOIn}0xw7u)v2!<% z9Hme@b_6P|<>ta`aD{PUwKUT*6u5Lm(%kd77(2RSD`FJWh1fwV7<0t#G9oQ=3d_yl-N zcm>i8=kS_w$@^kOb%itcgfRruE^c-W70%KYlP;*sSWbXJy!A`eemzZ$pYJZzi>mnY1r{?{(MB%HN8kox>ctMeBP;fXWJ0bbOe%};l&9DYPg z7E@Qh_o;~ce6w2ly={?imTul7$D91kaZRP+75C<>7f)g`UO4S+Da~`UPkHfj!5(S8 z`EqhU;!~53MQx4E|4I-U`aGfibJ_lq)f>n8jvLf257g6WZ=b*9%!x3yEl)BeUm2t& zsUJ>g@X;nrOK;J+esrEkx0&k>nP~Gjrym`*I4ieUxx?jY(&LRwL*97Z7e0DG|8T6r zKy;lR)nZLwPQ>9M>#d~tY@T9F_tw0twHIb9Eh}Iv%U{c!WPK2OWlWeLaUX0*wvY5AL&hYtR$ml55+0nvtE+;L789{CRmw zPjPPg4*sHB@5b$Din7jgY^UE*3_KEhd;)?fO+F!K9%~sE)?8A^tkeIRJMZ0Ewf;7{CW)_u z^ZT)kk>NucW@MkbW6`@Qh70Ch)jPgqGPmPx-(H^c0$G&EwsZS-C8$#t2-rKl7hZ79 z?rj{;xoZzeE3y~gC{bMz7`2@TZ+u3=a#V$pcILy#XS@*~cUSbzyf$tN%onX*7p}r#LP}C&8>7QFt4d!v`Dl0SW7~yp}(5Ndlyyl??vFyuz^*2{3+(-8}=&0o0 zUae%xC$H1N(IoSHs&f(uZI4}_lzzI*!tE|!WB-HeZR-5)?2gzY~^AIXBJssDrFArsI zesHVKdRqPx0CfYph z6~D1dVd}C@s!s9a%zp6!J6~&S&+qc7jL$=d> zVC_<7=zBc$UPctJwPjyNVT8NV3&t|boNWj2Z>sDQSK?mOt@L^+Tx;0Pec6+FM%>J48OF26awM`bmomPodrFCMvPRR6Jyfe?uxJlHfTWybb zYIzy>7PF0+om?{_d7GT z!(!r^O6!JtAA{tRp)CfCbqhMwpE9> z+XiE7D(-H1rX0Ikz3s{~&d$ng$|l;^;VD0-h?2t3t5P5A%Z>72RGJ*szy_uy?<&~D zRbuOTl_x7UDgE=kfz%UqnPzvY*76Nqx%*tV_eM}~KS^Ly;cl0?(+h8;Ry;FXy)l)? zXLtG7htFqhEI;~Gzj;;8fuMm85;REIO4~op*b9Qq4_*(ArSzJmO zO;LVI`s7)%A$#X&(Dt*$_gVg>7u8ap+np}@P`2Z^%-U5{%ayA}Vy=DMyVG`W z@0j{7?rNQQ6~1=utah)=%*9DFT^{Z3IxZvTLnEprWdq-{la+?I0*^>KiWcqaFnnH| zD3_D9@yo7K8OhUDlq~<7Shjcc)a3uZ|;!RgSWDJoSmw93F84*ZckW+cC6$P zJane1)+?Hk!aFKD9sB;WThF`aMXFsA{?2div=4sSI}xmxxB7lt{eZ?A(ny*9MRk+3 zV<&p+O5Bq3+QTo|d`?_a{-`|D=X?3U#7MB5^S3(n!Cd?JlVumPr9YX3>}lyY3TWu> zQM$xXmDl0yjBUw?Hr$~by8aAl&DVW*YO;6gaN+jO1RQzS?shVp&#UJ9k4#)<>gW4m z8WkZm*TQeF{h{0$`ZWG#@R>@B+9zq5v2(xr@9SP-oAgdN&r6zE$@hvrv9aU6r*GKu zxHqekZ){QA9;2dJ82;s{uH%~f(P_pkaTu z-|f=jrB|)uj4N-c@m!zpotQbZt@p8t<3XO#wR~sQ7x|D@V>(Cj6JT8GdUusJ=ZSw{<`i{{wx16wOgl? zK5w;rT(rB#Z;tjPy)EPQU0v;bDbw@Vql)H+o>4C=wpyKO+_mZb`Q0krov-X44YgFy z5)J-ba{An4ps85>q9=#MA4;4$e&+Q=^B1SSS#)2~yW3>Kp1zxRn54hTSx&r|y>qKS z(t)`z+<0x;Y&`Npj^ioY>M`qO%L6Q?1fuaL7WuuBSiV^N;Grg=N3Slt5S9H}P#@p= zcH`@bB|FM#4|hEKbi!9Ok{mv7)c@2bkK3nae|Vc6D&_mOZ~LvGO!iK($yt{c$FB}gI>JGT&I-Nxi)K;P>TbskbiS52o%4`WN^pD|WCE-INMOYvvxdFFP^ z_mh0T^F-U1RR1mE=c?4k)HVG^*Oy1p$7Ncqjh!AAJ(6G1{E_ zVu;(v?P0U#4h@u-ypd73@~xF{EHU_o?s&Fx;5JPOg-3L$9s$nId#kUq2{e71dJa7sU^ z>|RUFTHdpl*Sz8E^v63NCJo3h5!VRVbGhq_X>ISGdltfjrTxkJ8Ocdaxpz7*xaWx} z%(!$1zhBxdW|-A#vfOWR{L`MFr$*=gEV4VKx`{m2LX<3M zIj0tF3cD_!9^`4Ya=if$U(G`O#c4C46<-(j9(#0w_nIgBWWmV^+~XZ5a=(YKdv9ZG zrT+azgI!+|p`q>EsNtf_>OEz`u60>UiT>^7;i=bI|+e;aq2PrM~Ml#lIlpg-}x<_R>h5kdcxD~{r1Z=N2zpcIC zJ+-1hW4?q-hGXr1650FX@TA6xZ}laQluCCj;hdcK>U(B~+J+>5!|^#?32&#jtZeo% zf9U>h1y^)=qSVmTk@xeX1-C1`8oju2Q?dWP(+@^R9P$n8gNFBbF@Ai-HEoh=dN&)_ z{ZKr;=JxlKT+i#~R4m+oq22TD0Z-m_2km$D2RTnT>czfZdT2+M#;a?o8y{-;3Rzdi zQS;d+H9Sv8i+bE}?LNLxv6}br&YRWMj*^ua!UnA*+7;#Nff-j+g|>!a`tr?u3#T+v zzZ{s-71c_^7G*F ze|6xK#`V_lEa$>$Fv_q57Bx9m8Y zeN_&(N9lp5{e=kPuAalI-b^33U{hZh-v6QVw3Y3y_!m=$=I}?i24C7K+ZTRyf6k3Z zqQTwV!>5|t30Kdzar#e4~Vu!U01j#vg&PX(Ob{9>)FFi7xqgm87@6EY;s8DZEB5j$cZHbrSq*sNSpE- z2lwATY+61?ICUgH*K*dkv9W`h4a#`!h+eNbYWXW$>fUax9MIr4x9zM6;+(W`OXlg8 z__W9i>!a=1EzE+`w>E zlNe?0#Hl)A-ujSvYZh*Q6L$?SS#sjqtt&olk;2ECm4ts>+POB^f}4GkD9U49%lh4? zw!Xx#+MTlbbiVEJ!`t(NKPJj|y|@+6sC3QiDOK3 z*NZc)W;(ZmNv|vpC}hoyLr9I5m`QAfxw+3^*&0My+$8hzCX{UaH~Q4>kt(y|G`~h zrMTd@+5464#K%)N+n1_T<@eusyZLbANNbUa!{ND&y&qiC4~@=#68`DC-BR@vJ?R5I zaXCf3Z_{+XO9g%nvOSQ8bE{3xTu@!*uh@$yS!caG$geC1a9oZ0?3)6~<9 zVShuaW4E0_a(qeD_KPZ;w-;k}$DS|BkP$WKZecX4e{D^}Tzzj6`dDI1m$KK`+6Q!x za)q7vz-#ZMzkj*&mVGj7MtuXmX{@=7=6=F4FnHQoS)?fGNdcih}> z7ow>8JW_5d-uiLh`K&B2Ns*?dwvw99TdBu*J{%&ecC$}{^*LzTx8zuT`&p;l zEjaV;sPllytV59nUpx9o+k1Y#dMNLak)C$#gm%^B6Pm@5VOP+ zuh(0yYc*68zVPFWZOTx8(b>Cha*_CiLS2iKkugWP+l{it8qZ&F-fdMiKWmBHl7>sw z-U{)0kJ5TY)z~M`YAfBXC5fGP%$_dG+ewkge7jJGuR(q#Nnqe$oCW-;Wd4n_xX%9F zU|-Qz9Q@KSHc!W4vlCm6u^DFuu7DpVW?%|qlRp8EaMpV89X-sIxvU*3$d+57=LV;#A}*z7_WZ(52>%ff+s7@JxqTG&9*P0G%}*qmmfO)CVf zq$6rEHdhgPETm%edXL2 zD4TIM-~>?Hte$q@F-3b{MTnNcGdTHj*oYw5@9+e7sLW#CRyaz3UFp+8hr7jgBHVe4 z;JBAfs#;@QiR;?F<&F~{A7HG-gAyJKnTy^~{-~C4ZxiN9)O4i4rbAwsvGL{|BK4RQ zhZMZ<+kDP>$+UZSOC1U;<83C^ybB9GxY1)tcB+Ius$*XHZXrcX2b?MKrk3eqzw=jEn|yq`RKxi~^;;KRIw!cl5-JT<-^HEF}W z+e&@gU1*=MziRT+9=@o~1#yByKb!i!mfSS_fa9FcEF>4H+lCBa@7AT2jZY|D{=DII z%WYFP+b#XiveXX0n>_xcnw*pS>~`WtAwAeh*rWAs)BaCB*F*cOdS{#!lP*jzh`zig z`mFip!p>BA73q&D;|?b7j^kT=O&|2Gxi`AA;biyAi7kwsBUf(P<+~5J%N*>GA06|w z;G742ymTzIrmos`Qhr)F$RV&OYMgFad+kku;h@P_^IgVido_uCuOBZ?*auvxCAr>t zE!|wQrsmAUU>8$`w;iH`g*SA5Iu85fo_V~fvhs+e!bzKs_R`S$`e>>88*aP(NH1<_ zjBYS5Dd`&7TpkzPvLWw)xp(i7!rbqi^QYZQ7uf|JT3{w|f9h!V&smQ<_I;FW5gB)` zzx-W8>a50QBfF#+-ADsU$+xv8_cc#9;Jr(DQ#&s>Pk()oQ;_m{fM(4K+eN5yEeE`7Wy6tzUdh_}=Y8Y-eGmmB44tdDp3D z$q%~17bbgJ`BpSuJ5_1yKYQCLY}|^4z4iQoKVIF)rUsr|h;toHA_{h`3_Oag+L8*@z0a0Qw6xHu>IP0znfwk~(7 z$FChc*Q2pirYvEvl`2X2T?prVZBgbj-S(S@%rIo(9f@T6=Va2$4I}mi*9bvx+M>=z z?CPHD-YdCf*M>IQ=u4yU=I}*YF`st%&0Cedg+7*nx&17;J!j3i zBg5~*kBHYDOylvtH!{pVKU`dNo!~?jd2`$^yYQ#J&8EkXQl;{^`E{O*RqI=3EU%l@ zx8HxlacO(W+gs;vycp_}y~-QcFneFjKHd4d&K(xrch}FN*KqH?{lmqaUxsRTY+3 zU=8j%cAdh`^8WEW{W$p$zwL!B_r5N)m91U3Qb{q0*D=z6#p~UEk;jJfk{EhMxjQ?x zG%C&?$&@EG-u1*}vCl{EuAJWUgZfK8(+dGGy&m0X)T3f6qXQr`-2w^hqrT03` zYKJ_UXHsl?YODfAI(nKmODYZ>e7bcw?dp=&9Y)wj!x%kkm(0Zc=WX|EPG#8y*e}5d zo>CZzxxZNK&a0tq^jZGrgsZko4qldhYgefCFygRL>6IMLdF*NN&Qgbt)h0~y#eO}14y5_- z2V~u@b>N);K3?n4x_$m;LEV;&4~7vRs^>}#L?kxE#tjdS}e`_$J}cWN%3##L7FhwP0ZNC#CE@<|#+aNQ8yn$nq5^U#pAawuA9 zo>E%wPW1=g&dwKp9DBkOUAwqEroBb1ZqLIHob%?F8-|LCGPYBj-tu7IBHcEc}d28#c@e8xbpCSV;1cmt> z-+(jT)t{_&wv?E1f7&WhYn&^4#>H=XtYhk4@06&|bX`{X*#OPfx-sM4%x_+cTZ@mg z&$nk1Kdhy%EPDKOlZ6SL$Y>q>Hnv-Q&9@?%cUza-IxR6AaH`#cM2sFQh>s#YQ0%pdHg6J5G?MN!$)pakc=F9lDP|Jb-;<>>kg@1}x!riBV8M}l$0`1t^Ym>WkKE4>x<(x{!>8SyKeE}|PuS`7+9wN! zUAPI?)vFIj9TD4IE$e%U#%@JgE+d|*gxXlrVq>aU|#besN*s zhRoAbcADe75*Lo+dfPs61rCOPD_Z~Ae(C9>#PrWe7h1=j$tgV2mEKn^*IJ<~LyKH| zKZLa7?4FI=4ya!H`KXtRQLuJ>(ox)NgYM(sE&Dp{J!ubC*B>Sx!=4>4t0qlFNp9jZ zm7bF#f8j{Cw+P0-C&q%-dj+s z+I{YEwRDqQ)QYVAf}J1leE2cZzg)iJfPF04?zMl?#g=^~3r;jv4!Mx_OA+HrPLdtZ zj43`5-XCC*?_8L0SpJPl|NCs8sD3lSyYyps z?#+L(X0tDy5kls^ywTY`E8JHj=S$2jiMEa|w97*vYf+%ef3_0!W}^4(Y8k9KYn z7f*f3}wR*c0ftMdVA?!g)9E45-x#mCzsX>-E8jQTEMX{QMfCbMLJ@ysl0*qq;^eZ-tD2> z+doI%u09+f<|~yi+_)r2J;$3D0kH>QozpA=RXx-AfYW{Mj#B(#XZukAT z==O}Y(}IuAn!j!k^uda`b;X3PoD#V+E;#R=dmYyM+1DlJNvnrnGODF=)(E8Qk4!a? zIrc2VZWT7wCbLu5mCNhbIEdZxDCd_DcX~3{ZEzbQSM6=4F5aQ2bEXyRwzzN zWUu<-di?udPlY4e&R12McZPq^d-jB5Kht<{J$Br>RQk~7H#~frHf8SFW5LC$iuvw` z5AbbH-gfbY%Hq?lAFAFT@ax_@GH~+q!$x{|&L{toc*}?j-LSk51IU;%`=aRK8_#z%scM7^!wxcdz%#nEGCQdzmaB%9c_0JW!vk# zH|mh>lZ`Et^vSyw^wjI^=%v_sxx_13>OPY%_C(XSH|-g0`fUC~H!-UDVMexiOT`5R zCB3a@tY&dnZlC|Af6ztm&|9mc^`aV?Rc6wS+yx@m)gN-)x))8(FTA!&;qq|_vE}2# z1WWoCb^Z3^gGo=Ny%$6)*Sc-v*azi%f4$J0dsCLsdGla>e94i4Ota$K3?4F zD{B5=F8x5E#=y|kHM6H>N^;4Ywr?V*tgF)6koV+U+4Q>&buDjymdX{jJ=XC}+Bj7; zx1W5-GF6Gb*yo8s^p4f4dK-gOatew!nfCi~Ii8|@NUbt@AMvquaqec0J<>)EyJL|I zHH|&Tzl2A9mdv1iUnCN;$ZW4|&?oGr52XhR);4Wx{pg%2nYhL_LCdO}wtJpoZJfOF zK%ej(uW6B=V%WOOJ6q?E@*H{-Div^bU!Ap!F5cMilT*)&b^c9jyZUcFvgGcvYkZI{ z_lQ~{J1M|jl%L&F^jMW`|FkeueooVeb=Tt8e-2gb={rAhFjSGKCI6%M(FgNttjmXy zt>)n-W}#=W*JQH!Y$!V=?`?d>#of`Lai*-=<(6~%Z1?JW_SqUs52$TiUgofOWZ%47 zB6nX;K6Wts=$TRfdHXmasInX_W`VHYJa~Wi@>n)kq8e@~v?>>2{l!an*BLq)GWCEh+H$hEN~< zm%Mpzp3f((brO6S6nG{I`^foN$OCnY-LgH~zA~aOVZByAnifmCZe3xM(7Gs8?)iz+ zxn%;~saNpBwTsyHUG_KH_fI*5dh5Nv)+)M4cc9q%@RDSib+za2cJ$Tyr)CE@UB6zR zKk6K}b<9~xTd{_|a^|e>hltf%wW3zf`M&VBxXui9L^G4t5^&*uL4aRo{f3!KUO#lM zEcmQ@^CMle(kC#rrDmOFEA_b5MQsVw8}AH)hHlICsB-MdPNc8u4ZW3?9M-vfj$xor zxnhWJ=Nazj1J?Rg`#VoYU8$b>`B81j{XCEJ1v&bm8>g0^{yFibbXnu=d#C#_fjjoz z(cAC4`|K?W@3J@i=?14duV9vpq-kXfTnPzreQsOzY3iJx3b9S)M*I5q-s{7!OZO)2 zJSE?6oTkpUU+eU3e-$dQ^VGnc+g7n*-&Xk=+L)&FRSJ!-hV7Q+mB-}=alfa`Ypfc3Un48ov}xt58zAz z*EEqN2V~H-63oOrz{L^SnZhzlg6F@-Hvrt79KI}p91o`Ui5^rMC&%*~T4=36LzuNXQTrg$Tilr)T(FdxSC*%xzoX#n4bj0eKWy^Kd{dYu>$jql!c!g)r$~Kv}RYBXv-6wKF+Oh zwbmzkOhi>IUq+qhS$3wv6VprdBHya;!aSJgjd}Lm=g14e#K}qsV;Azx5!B>@?_)r2Dp)&cc^vrBdZP_uzM5cr}G zGA+Ztz!oFlBj6ZF6>LxsLH-z=2bZIu4t_1xP;?R0Q-Dhc^5n%@a4Ca78cXokr_T?^ zjj=)C?*!`$!O7u#_|Tmw z)E@AZ%mpu%8+_N`KEs=b9)T?cdUz~BK$VC2rg(5%n$X~2SOT#EQc)$U_D0A^e@M$f)E+19|E6t4+ zgmwo7m^%9Oi5e+N z@b868kia$wN=GKfn!+_Q26^RTS`Q)P%6KXm8If78!TxnJ3iVGB!MzxmYhwB%#W1U& zd@lf`QQO2bX!KAmZ7>N0gFX{-SST_nG&VBQ(+MLR6GK8sx!CVC^aSq$_*aMXj~HDu z(KK>6^DV*^IXz?DVTa=Akt7l|mKaH~{$-=71Frp`c`?Yb8kCDkhvS;903KMuv)}O* zfdBXPg*s}{;7tXE#ZzO0Q11w$kZJJVnDpbIOo2eh3;@~f{ipQ)@OHv%LuMyr=Ff3| z*vrkb^Zu81)c+~rUoZBLijtG)&?TAVG5(%nOpgR;{zJBd94~vTG)%NLwbcpeZ-6V6 z0wan+BCpcWF=pOoUN~`C!xTC&G$WZ58cT;4NK1|lCsQLy5oBcKOC!=FNYuClfH(jm zJPIR)$I+0{1G+|%VyKiDh!jr_i-DoU%8?EQr6yq?HD4RI=PmZTSR**+{ToMQjD4GLvAR?nbPDm_+t6vu z=WdZSW?a-}5RE0b`NhVda+Cjot_9F>r+U#5AF-oSpaasqk;6_e7IR>x@Fs>Yiz97FD@B4W zEty>%L>>wV1!3Q?i=m=w!VPxnUpM|-7Hgozi4qYRLvmnIVU`WVT5>o!o>@c!#N)gR z8wfKaI3fp9tQ!3O+!mfMiA&PaL4UN_?cqeEMG%-qpM+3i9PEtHlL=I|WDN0%zAQJC zTK|``VgzIx0-gaVXjar^&x+vPl4Fy)3G!VA`L6^(^EibF4FwYb7Q*OB|dogvBZxC`K+!0$oi2=*k?7%_BHQwAk5o*1XPD3|~qM}hqXLpKX1z=DWD zwh0X-(^#XKBL&dtB&Ilo7%`E=e)lX_XvniiI1EnYpNjxSYakSWX9yIH;B0S72m#A7 z=#3!FB}EWH1R+C5xZ@{=LGwy8TovHz3mqTLF@L2NN!0>J8DZpjG708=3ONz+ka$`| zB-5h;xD56eW7_(IY|3=Q%AkZICt={eA_YlTh%>OP*H}R?jR$E9too_($&9~AVPPP! zF(@=fTpSfxKTJlbMleZ$Kn6tP8F6qE1WE=HR0uI3QbNV41SW51Em;XD2Q)#T`{0Qz z8DdeQVTz#AXc{DP09+Du0 zd6!8JN<5{vIR5=#R;#J9-;hF@P{F$>jinj=L5e=9flbM;ZD}+2Fu*(WXi)6&I zGJ-e8w76&bc>xo9A~=&l1mHf)5dRPK*LzE3dA?;fFa;*wWCCZLk(1!{(SWN(gWZ2z zB$`@SwITtZ18;56_G|i`Cgj7Wx3DHNs@&kmrTLWBVfy@Zn+g923q z4*`%qf(rfRH~hCmK#WZP6CMU6NYMRa(NqfAl3?zE*UPE{qRo=PEe)NSVM=fh~p!r2HXJ)!n41+ z1?F%C7^e`7q#_1+9pcJC+-Jb7(fWvkw}B1?;cTYC^iTj3DB>Qb1h~%!epn>o%JIj- zN%tPoA@&Xgpr68D7>jHN%3zkt9M~9FNQ-6%4v5d7UIu-F0la}T>B1<0aSpEyoPN*= zu^>z&{hb&s2-c#pR0=FpnK77i40j;QUqb=VR08-n$yl5=2&PtitHkaiKIL2Ev} ze@Gh#^K&R#k^-e9h>Pln+*_me^ZCX8{xpBAfM<5VyJ2me&f5#ki9Td zx^iQ3S|sGK*{2PgeY(Q%1F;drQjYZG$Yz~z7YUQDh;iB>#!05xGdU+*SiI8V4_=AV z*aCBe8I8#)Q5fArIU_cS!Yr<`9C1kpHj{Lx(!JQ+(Tl}t92qez@f7JBy%9@<;#jYASpjjTX49Ew^ZUJD$O^=? zY*BBrVF?8eOkQQnE9CZOhsRu#WEFO9*JAi@|iz}H9#;36dv zxjCl*>-uAU{Sqw3^qun$eH%15v{)@oO|9RiV=dGqG%a?1(06AO#h7b8)Xx!r2qjWb z{xB&pV6l=whG~Tux`qJ_&l-Bc5T5|?{_p7h8k3tKy&V4z68m#|4TB3M07^g(U&d^ZzBFx}dUgXds#h zM`b|eL*+tq6Dki1BS3r?JwtI3UqyNhi``9wE$%R+X2=C zL;&~$ECnzIkN|kFQVeq*pblUkKrz4;fVBW5fTaKy0NMcZ08#+opl--Ftr@IBlQ26m zCoz4Pj~H>RHr5?Rha!05T!l%-lwz7O{g_V}39Jrw8TOxJ|8rbe(^6?<#Ct$K0!m1( zLa++J8RpEeFmN3OW5gL|Vf1MS;FZbnX$}%G8NR*%VjKnLepY&L;mZn8!@_{K63}yY zkQEI?0eS?wOJgVd?{wNoz=wsqo~DitbNvP*3aKrCkH&^XQjqG3vyL|o6T^fgZx*y_R0=UBi0Ps2FSLRIXA1#;kYwYoXhlkl`TJ}TNlF5TDt0y= zHg?Y5{{Ov__IK@j2y7UTwJ0FZGp7d3&v2HmKLsWg#4r9{8Zs%ItjSWjqB)4EHejO~ z1RYNL9CmsRzU5!$2Z~&_b4kdJM5D71PP#rGmLEJ0j@I{x7jy8Hf5A)G`2ShCnf3lv z_}}tvV6PQUFvKwE9lJvL5ufDX0)V4(B*n%=(CKleT3U&TiJFOen$-AkEk8F`EgcgR zV=dH=nCoPSMxp&ZqAvI;)M7r<`tJrOZ#5tDjp8GzYtj6XN2jNFjt)Yp^*5jun}kK&Aw9E6hnquLSA;p0~NxGkAt(D9$R< z0i+37A0chdzsQo12-|Z|B?4-Qzh;SOBe!}`Fjx_X#sDWlL)Mg_7tLDnu=K%b*0h#wnG!ZYY9v2F;Yd(JI>>KCQ0kDA$utU;(*huquy6lP znv(_QG~|Ki?%yaeTbNU1kc^S`7__tDsL?TRApQclZiPKM=(3m-aOH-p3TSSbQY?I~ z2kl6(RK?8JfEEKqOA-e2k;d8!Q;lJ@{Qt{c5GiFqC`9uFTv$yW#4djjl%L6PRs-#L zDDP|l^!7QpCWK{w!_mHk5x^hXbb^Wsa*zl;04f9ee0YY5W$6|K;fm%RJB}RBio&ip zK}8*&BYK?rdobGrW%#Sj;>pa77^G8x85^xbS+B?&(jqx&HC&Z{(SS7zvlxHSDEtcz zcMd1`GZ{!l1RW{{a|+@kS*{bVNPhcQ??05aS@7%qLwxl9|5beOD+x*gnAc1R8XEs! zykKZc7?v!hM+_YXL2Mi`j!Hw?#otFQQ~L(H)<~H)ANryNMGTImu+|?)AENCJE9qbC zi5>F~Z<+9MEKqK9##9733A!5d6H>n=VQJct_>TqvlaeVxn95q0aOfevhUjs!`ajGD z;|KX@LAu}CokIds2+#iAJN&IUmRF(t&svCdvB|I>Yz3sa^(7`RnA#F6_qdBVOl zYp0CE4PoPuQ=aEg9z#g`Yy5MD8ZZF{g*jNkF$i;gBLG)aH}?J*K^<^3W;ypcdBWFt zn0az=alrq=d;c`t)ImGWK5l^_L7s?*tb;2Ww?qJrt}x^;IBIWnuN2}UJ_VvB^IQW8 zBRLuU(AOx@?^k;}P{YvSGI&M+z_TIZFF2B;y^_I0D~urmI}VB)3Bc4Q!r1%$eiYmX zl^yB#d(m+3e<4-I663UBP{gC-E`IT1m|&p8fcJ=Kdw~Mb9p*+9=G4s^!VVP9wkG13 z&NR>nc|n^qyV^fIb3pyj85l=B&7!Y7-K5vzsy^XicDkT*Q4Glnn zeOp?ZYnhwRvtMTC?eF1WWeHLfCv5_xL-VDE#;+a(lNZ{RfO##1468{p0hBRHbk^9= zhbz(raOObsc|d_flYM1fn19h901j>Tm33kMMSlSP({A+Z=nA+fjsr$Ojz=Hn`R}u1 zdEaLF(jS95yFs0sfEggY)PBI1!LGaQ?}7fOmH2h4<{uVp@P*HC(4R5;4`)r%3gt$6AP!y(VZ;aj7X^T^&7`fL zAjaIU{|0o>Jj&eRgvBpwI|aUN#g@{4+pq$Oghb|~n+uJN=7fJbXK)YlMtU%|R7PZK zf(b-|H)~7Sl?pF`z*fAorVIj!fewG5eM4dx^dS(j|5#+O#x-Qee7{Kd_n)>4>>T~R zgAM9r)~7376IuPK3hIQ`&i`rt|NP>Aos(nD3fnh=ZH1-FXtucxGum~hELSc=C Date: Sun, 6 Nov 2022 07:24:46 +0100 Subject: [PATCH 077/106] metadata --- apps/gipy_uploader/metadata.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/gipy_uploader/metadata.json b/apps/gipy_uploader/metadata.json index 24c03ab87..c0c05e4f4 100644 --- a/apps/gipy_uploader/metadata.json +++ b/apps/gipy_uploader/metadata.json @@ -8,6 +8,7 @@ "tags": "tool,outdoors,gps", "supports": ["BANGLEJS2"], "readme": "README.md", + "storage": [], "custom": "custom.html", "allow_emulator": false } From 37f5121ab2b11d02159f866472d2b644f50ff63f Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 6 Nov 2022 07:39:12 +0100 Subject: [PATCH 078/106] minor change --- apps/gipy_uploader/custom.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/gipy_uploader/custom.html b/apps/gipy_uploader/custom.html index e8b9f4b77..0bbfbf612 100644 --- a/apps/gipy_uploader/custom.html +++ b/apps/gipy_uploader/custom.html @@ -35,16 +35,15 @@ const reader = new FileReader(); reader.onload = function fileReadCompleted() { console.log("reading file completed"); - // when the reader is done, the content is in reader.result. - console.log(reader.result); init().then(() => { let gpc_file = convert_gpx_strings(reader.result); + console.log("uploading"); sendCustomizedApp({ storage:[ - {name:gpc_filename, content:gpc_file}, + {name:gpc_filename, url:'test.gpc', content:gpc_file}, ] }); }); From 8dcc587c4510935a364e99929ef2da2a0ef123f8 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 6 Nov 2022 07:52:47 +0100 Subject: [PATCH 079/106] testing strings instead of vec --- apps/gipy_uploader/pkg/gpconv.d.ts | 4 ++-- apps/gipy_uploader/pkg/gpconv.js | 15 +++++++++------ apps/gipy_uploader/pkg/gpconv_bg.wasm | Bin 204497 -> 204462 bytes 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/gipy_uploader/pkg/gpconv.d.ts b/apps/gipy_uploader/pkg/gpconv.d.ts index e97d230c3..9ded6a888 100644 --- a/apps/gipy_uploader/pkg/gpconv.d.ts +++ b/apps/gipy_uploader/pkg/gpconv.d.ts @@ -2,9 +2,9 @@ /* eslint-disable */ /** * @param {string} input_str -* @returns {Uint8Array} +* @returns {string} */ -export function convert_gpx_strings(input_str: string): Uint8Array; +export function convert_gpx_strings(input_str: string): string; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; diff --git a/apps/gipy_uploader/pkg/gpconv.js b/apps/gipy_uploader/pkg/gpconv.js index 58b6d4f26..7a9d8a777 100644 --- a/apps/gipy_uploader/pkg/gpconv.js +++ b/apps/gipy_uploader/pkg/gpconv.js @@ -74,12 +74,16 @@ function getInt32Memory0() { return cachedInt32Memory0; } -function getArrayU8FromWasm0(ptr, len) { - return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); +const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); } /** * @param {string} input_str -* @returns {Uint8Array} +* @returns {string} */ export function convert_gpx_strings(input_str) { try { @@ -89,11 +93,10 @@ export function convert_gpx_strings(input_str) { wasm.convert_gpx_strings(retptr, ptr0, len0); var r0 = getInt32Memory0()[retptr / 4 + 0]; var r1 = getInt32Memory0()[retptr / 4 + 1]; - var v1 = getArrayU8FromWasm0(r0, r1).slice(); - wasm.__wbindgen_free(r0, r1 * 1); - return v1; + return getStringFromWasm0(r0, r1); } finally { wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(r0, r1); } } diff --git a/apps/gipy_uploader/pkg/gpconv_bg.wasm b/apps/gipy_uploader/pkg/gpconv_bg.wasm index f71a5c38cabe4d54fe866224fc27ee2d46d21274..cf24562a348c0302072b7df36fa145952ee891f5 100644 GIT binary patch delta 323 zcmXZWJxc>Y5C-7cxxEjL6xRvCVmq&~or8Yq_ev&n?z8=BDhV;6bcqWf(h6d zP)KQ!6iPxO+W0H3(4BbIGw<%pFw0r(VOAR-$l!Gz553;xP#y_;<=5@VPun7gFJDOw z4ftd`-SdcG&EvP<(xB7CdUcnkVM}3(KnPqXybqOZip(22HRLd0;~ZAOzXKLX+*G;G zMugW+6%$@^w#UI~Bxg$t&Sr9vZQPS2Y zB%*EW71ZE^=RbU{5yo)gk|c&tdr9^ERNy9pME9B9@7Phb8pAv&{H)~25o36?OqyEj zYJ@|lS%m$P*FpZ`t=E;-OyZj>v{;RFtq^sIQ^x~|#*s*Tbpbp4T=K4@E;;@GT;ixV a@ywyG0tMA?%pWpwY_e06ktdVk`^_J0+f*R{ From d8ca24c817c75e4596c0c60b7d0b1ecbbba81f8d Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 6 Nov 2022 07:56:54 +0100 Subject: [PATCH 080/106] Revert "testing strings instead of vec" This reverts commit 8dcc587c4510935a364e99929ef2da2a0ef123f8. --- apps/gipy_uploader/pkg/gpconv.d.ts | 4 ++-- apps/gipy_uploader/pkg/gpconv.js | 15 ++++++--------- apps/gipy_uploader/pkg/gpconv_bg.wasm | Bin 204462 -> 204497 bytes 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/gipy_uploader/pkg/gpconv.d.ts b/apps/gipy_uploader/pkg/gpconv.d.ts index 9ded6a888..e97d230c3 100644 --- a/apps/gipy_uploader/pkg/gpconv.d.ts +++ b/apps/gipy_uploader/pkg/gpconv.d.ts @@ -2,9 +2,9 @@ /* eslint-disable */ /** * @param {string} input_str -* @returns {string} +* @returns {Uint8Array} */ -export function convert_gpx_strings(input_str: string): string; +export function convert_gpx_strings(input_str: string): Uint8Array; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; diff --git a/apps/gipy_uploader/pkg/gpconv.js b/apps/gipy_uploader/pkg/gpconv.js index 7a9d8a777..58b6d4f26 100644 --- a/apps/gipy_uploader/pkg/gpconv.js +++ b/apps/gipy_uploader/pkg/gpconv.js @@ -74,16 +74,12 @@ function getInt32Memory0() { return cachedInt32Memory0; } -const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); - -cachedTextDecoder.decode(); - -function getStringFromWasm0(ptr, len) { - return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +function getArrayU8FromWasm0(ptr, len) { + return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); } /** * @param {string} input_str -* @returns {string} +* @returns {Uint8Array} */ export function convert_gpx_strings(input_str) { try { @@ -93,10 +89,11 @@ export function convert_gpx_strings(input_str) { wasm.convert_gpx_strings(retptr, ptr0, len0); var r0 = getInt32Memory0()[retptr / 4 + 0]; var r1 = getInt32Memory0()[retptr / 4 + 1]; - return getStringFromWasm0(r0, r1); + var v1 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_free(r0, r1 * 1); + return v1; } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_free(r0, r1); } } diff --git a/apps/gipy_uploader/pkg/gpconv_bg.wasm b/apps/gipy_uploader/pkg/gpconv_bg.wasm index cf24562a348c0302072b7df36fa145952ee891f5..f71a5c38cabe4d54fe866224fc27ee2d46d21274 100644 GIT binary patch delta 367 zcmYL@F-rqM6olWt-Mu@{6zK)oPA#~Bxg$t&Sr9vZQPS2Y zB%*EW71ZE^=RbU{5yo)gk|c&tdr9^ERNy9pME9B9@7Phb8pAv&{H)~25o36?OqyEj zYJ@|lS%m$P*FpZ`t=E;-OyZj>v{;RFtq^sIQ^x~|#*s*Tbpbp4T=K4@E;;@GT;ixV a@ywyG0tMA?%pWpwY_e06ktdVk`^_J0+f*R{ delta 323 zcmXZWJxc>Y5C-7cxxEjL6xRvCVmq&~or8Yq_ev&n?z8=BDhV;6bcqWf(h6d zP)KQ!6iPxO+W0H3(4BbIGw<%pFw0r(VOAR-$l!Gz553;xP#y_;<=5@VPun7gFJDOw z4ftd`-SdcG&EvP<(xB7CdUcnkVM}3(KnPqXybqOZip(22HRLd0;~ZAOzXKLX+*G;G zMugW+6%$@^w#UI Date: Sun, 6 Nov 2022 08:03:08 +0100 Subject: [PATCH 081/106] vec to string ? --- apps/gipy_uploader/custom.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/gipy_uploader/custom.html b/apps/gipy_uploader/custom.html index 0bbfbf612..852fdd32a 100644 --- a/apps/gipy_uploader/custom.html +++ b/apps/gipy_uploader/custom.html @@ -39,11 +39,12 @@ init().then(() => { let gpc_file = convert_gpx_strings(reader.result); + let gpc_string = String.fromCharCode.apply(String, gpc_file); console.log("uploading"); sendCustomizedApp({ storage:[ - {name:gpc_filename, url:'test.gpc', content:gpc_file}, + {name:gpc_filename, url:'test.gpc', content:gpc_string}, ] }); }); From 58591021094e1d9b98003c7030ac800574af21de Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 6 Nov 2022 10:08:47 +0100 Subject: [PATCH 082/106] documentation --- apps/gipy/README.md | 85 ++++++++++++++++++++++++++++++++++++-- apps/gipy/screenshot1.png | Bin 0 -> 2859 bytes apps/gipy/screenshot2.png | Bin 0 -> 3024 bytes 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 apps/gipy/screenshot1.png create mode 100644 apps/gipy/screenshot2.png diff --git a/apps/gipy/README.md b/apps/gipy/README.md index 07f5dcca4..f7fb0d29e 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -2,8 +2,18 @@ Gipy allows you to follow gpx traces on your watch. -It is meant for bicycling and not hiking -(it uses your movement to figure out your orientation). +![Screenshot](screenshot1.png) + + +It is for now meant for bicycling and not hiking +(it uses your movement to figure out your orientation +and walking is too slow). + +It is untested on Banglejs1. If you can try it, you would be welcome. + +This software is not perfect but surprisingly useful. + +## Features It provides the following features : @@ -27,10 +37,77 @@ optionally it can also : ## Usage -You first need to convert your .gpx file to a .gpc (our custom lightweight trace). -Then launch gipy and select a trace to follow. +### Preparing the file +You first need to have a trace file in *gpx* format. +Usually I download from [komoot](https://www.komoot.com/) or I export +from google maps using [mapstogpx](https://mapstogpx.com/). + +Note that *mapstogpx* has a super nice feature in its *advanced settings*. +You can turn on *next turn info* and be warned by the watch when you need to turn. + +Once you have your gpx file you need to convert it to *gpc* which is my custom file format. +They are smaller than gpx and reduce the number of computations left to be done on the watch. + +Two possibilities here : +- easy : use [gipy uploader](BangleApps/?id=gipy_uploader) +- hard : use [gpconv](https://github.com/wagnerf42/gpconv) + * you need to compile *gpconv* yourself (it is some rust code) + * you can download additional openstreetmap data to get interest points along the path + * you need to upload the obtained *gpc* file manually for example with the [ide](https://www.espruino.com/ide/) + +### Starting Gipy + +Once you start gipy you will have a menu for selecting your trace (if more than one). +Choose the one you want and here you go : + +![Screenshot](screenshot2.png) + +On your screen you can see : + +- yourself (the big black dot) +- the path (the top of the screen is in front of you) +- if needed a projection of yourself on the path (small black dot) +- extremities of segments as white dots +- turning points as doubled white dots +- some text on the left (from top to bottom) : + * current time + * left distance till end of current segment + * distance from start of path / path length + * average speed / instant speed +- interest points from openstreetmap (using gpconv) as color dots : + * red : bakery + * deep blue : water point + * cyan : toilets (often doubles as water point) + * green : artwork +- a *turn* indicator on the top right when you reach a turning point +- a *gps* indicator (blinking) on the top right if you lose gps signal +- a *lost* indicator on the top right if you stray too far away from path +- a black segment extending from you when you are lost, indicating the rough direction of where to go + +### Settings + +Few settings for now (feel free to suggest me more) : + +- keep gps alive : if turned off, will try to save battery by turning the gps off on long segments +- max speed : used to compute how long to turn the gps off + +### Caveats + +It is good to use but you should know : + +- the gps might take a long time to start initially (see [assisted gps update](BangleApps/?id=assistedgps)). +- gps signal is noisy : there is therefore a small delay for instant speed. sometimes you may jump somewhere else. +- your gpx trace has been decimated and approximated : the **REAL PATH** might be **A FEW METERS AWAY** +- sometimes the watch will tell you that you are lost but you are in fact on the path. +- battery saving by turning off gps is not very well tested (disabled by default). +- buzzing does not always work: when there is a high load on the watch, the buzzes might just never happen :-(. +- buzzes are not strong enough to be always easily noticed. +- be careful when **GOING DOWNHILL AT VERY HIGH SPEED**. I already missed a few turning points and by the time I realized it, +I had to go back uphill by quite a distance. ## Creator +Feel free to give me feedback : is it useful for you ? what other features would you like ? + frederic.wagner@imag.fr diff --git a/apps/gipy/screenshot1.png b/apps/gipy/screenshot1.png new file mode 100644 index 0000000000000000000000000000000000000000..c7c45fa3b5a4a7d54f7c5e7702c2bede0eb7b3a6 GIT binary patch literal 2859 zcmV+`3)J+9P)kDe;@o!QQ-6A^EJ-b$B*o- z&!z-(lR-WKc-~9`aJ<$49It8D;|Rc?pZ~M=#8A6K&Hln(j(=}V0G>z%0Ncba@a{nj zz&1_I)>~*TonJNbZw8*7`RZ89H^EVH^dJUc3snQQPXc&b?QFLOdY?;j*3h;f24G1= zXL~fji@%bcouQe!OBr~9w1#&1_xAGwvS;7x57g{JnC8)J48T8N7m#bvw}MI;mkeyi z&)?xG0Iz%6)rLt6+&zGQe6)8RsDb?~4taPI!0*}gf9732FJ*%S@D^frA%>8P?Q}PQ zYjw7}5|N8I4ZJJt)WEU8{@H%oTt2pp)Z0codJ(q=u;utPw$eom0M@AJw0Tknc1rcu zoeUhQrqh=d;_WtBy?ynB3|zy@l{hDW(cNpH_x5$K4zOm?_P+RI7b2RsaDP_@*m^11 z0x3VFRkw*2kho4a!*ZUkUeZOlIKd?fA&u;yl-pAz8xrXNiPW*1^U zqv0jQSq@<&18-xu-iapF#?-(EkTH^h4+yh4q;(g<6So8}HLyorM_&uTj$t0rl?;5q zGy`y)=l~q2X6z z0FS6<>hsFL*}z!+TkTCpJUlvGj~Y`Sc<5Z zaVa4ro-2pfT#8EGo2e|yCsF(4IFy7q=ZAPUA9z}7i9{?+LdmO+QsDCw! zxUy5x+MOsyYHV%EaY|L!w8o7*Gv)?>vs4D)EM?#Pax+hs%mAFF zECA0@mJ(uy⪼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 So*tM00000 Date: Sun, 6 Nov 2022 10:18:33 +0100 Subject: [PATCH 083/106] doc fix --- apps/gipy/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/gipy/README.md b/apps/gipy/README.md index f7fb0d29e..32a3018b6 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -43,14 +43,14 @@ You first need to have a trace file in *gpx* format. Usually I download from [komoot](https://www.komoot.com/) or I export from google maps using [mapstogpx](https://mapstogpx.com/). -Note that *mapstogpx* has a super nice feature in its *advanced settings*. -You can turn on *next turn info* and be warned by the watch when you need to turn. +Note that *mapstogpx* has a super nice feature in its advanced settings. +You can turn on 'next turn info' and be warned by the watch when you need to turn. Once you have your gpx file you need to convert it to *gpc* which is my custom file format. They are smaller than gpx and reduce the number of computations left to be done on the watch. Two possibilities here : -- easy : use [gipy uploader](BangleApps/?id=gipy_uploader) +- easy : use [gipy uploader](/BangleApps/?id=gipy_uploader) - hard : use [gpconv](https://github.com/wagnerf42/gpconv) * you need to compile *gpconv* yourself (it is some rust code) * you can download additional openstreetmap data to get interest points along the path @@ -96,7 +96,7 @@ Few settings for now (feel free to suggest me more) : It is good to use but you should know : -- the gps might take a long time to start initially (see [assisted gps update](BangleApps/?id=assistedgps)). +- the gps might take a long time to start initially (see [assisted gps update](/BangleApps/?id=assistedgps)). - gps signal is noisy : there is therefore a small delay for instant speed. sometimes you may jump somewhere else. - your gpx trace has been decimated and approximated : the **REAL PATH** might be **A FEW METERS AWAY** - sometimes the watch will tell you that you are lost but you are in fact on the path. From 1d258b50c442ea43ae27e32f90abab10afea9369 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 6 Nov 2022 10:22:53 +0100 Subject: [PATCH 084/106] trying local links --- apps/gipy/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/gipy/README.md b/apps/gipy/README.md index 32a3018b6..12d12fc7b 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -50,7 +50,7 @@ Once you have your gpx file you need to convert it to *gpc* which is my custom f They are smaller than gpx and reduce the number of computations left to be done on the watch. Two possibilities here : -- easy : use [gipy uploader](/BangleApps/?id=gipy_uploader) +- easy : use [gipy uploader](../gipy_uploader) - hard : use [gpconv](https://github.com/wagnerf42/gpconv) * you need to compile *gpconv* yourself (it is some rust code) * you can download additional openstreetmap data to get interest points along the path @@ -96,7 +96,7 @@ Few settings for now (feel free to suggest me more) : It is good to use but you should know : -- the gps might take a long time to start initially (see [assisted gps update](/BangleApps/?id=assistedgps)). +- the gps might take a long time to start initially (see [assisted gps update](../assistedgps)). - gps signal is noisy : there is therefore a small delay for instant speed. sometimes you may jump somewhere else. - your gpx trace has been decimated and approximated : the **REAL PATH** might be **A FEW METERS AWAY** - sometimes the watch will tell you that you are lost but you are in fact on the path. From c98f6a9c71480e71d265fc9aae54868cb2048ab0 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 6 Nov 2022 10:33:53 +0100 Subject: [PATCH 085/106] more readme --- apps/gipy/README.md | 2 +- apps/gipy_uploader/README.md | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/gipy/README.md b/apps/gipy/README.md index 12d12fc7b..11638503c 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -96,7 +96,7 @@ Few settings for now (feel free to suggest me more) : It is good to use but you should know : -- the gps might take a long time to start initially (see [assisted gps update](../assistedgps)). +- the gps might take a long time to start initially (see the assisted gps update app). - gps signal is noisy : there is therefore a small delay for instant speed. sometimes you may jump somewhere else. - your gpx trace has been decimated and approximated : the **REAL PATH** might be **A FEW METERS AWAY** - sometimes the watch will tell you that you are lost but you are in fact on the path. diff --git a/apps/gipy_uploader/README.md b/apps/gipy_uploader/README.md index d51c8c0a2..f2813ccfb 100644 --- a/apps/gipy_uploader/README.md +++ b/apps/gipy_uploader/README.md @@ -1,10 +1,6 @@ # Gipy Uploader -Uploads and convert a gpx file to the watch for use with gipy. - -## Requests - -Reach out to frederic.wagner@imag.fr if you have feature requests or notice bugs. +Uploads and convert a gpx file to the watch for use with [gipy](../gipy). ## Creator From a73c38794a95d01e7bc2ca71c102d1e79408642e Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Sun, 6 Nov 2022 10:42:20 +0100 Subject: [PATCH 086/106] removing gpconv --- apps/gipy/gpconv/Cargo.lock | 732 ------------------------------ apps/gipy/gpconv/Cargo.toml | 18 - apps/gipy/gpconv/README_gpc.md | 58 --- apps/gipy/gpconv/src/interests.rs | 78 ---- apps/gipy/gpconv/src/lib.rs | 575 ----------------------- apps/gipy/gpconv/src/main.rs | 22 - apps/gipy/gpconv/src/osm.rs | 36 -- apps/gipy/gpconv/src/svg.rs | 86 ---- 8 files changed, 1605 deletions(-) delete mode 100644 apps/gipy/gpconv/Cargo.lock delete mode 100644 apps/gipy/gpconv/Cargo.toml delete mode 100644 apps/gipy/gpconv/README_gpc.md delete mode 100644 apps/gipy/gpconv/src/interests.rs delete mode 100644 apps/gipy/gpconv/src/lib.rs delete mode 100644 apps/gipy/gpconv/src/main.rs delete mode 100644 apps/gipy/gpconv/src/osm.rs delete mode 100644 apps/gipy/gpconv/src/svg.rs diff --git a/apps/gipy/gpconv/Cargo.lock b/apps/gipy/gpconv/Cargo.lock deleted file mode 100644 index 6986021d7..000000000 --- a/apps/gipy/gpconv/Cargo.lock +++ /dev/null @@ -1,732 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "anyhow" -version = "1.0.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" - -[[package]] -name = "assert_approx_eq" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c07dab4369547dbe5114677b33fbbf724971019f3818172d59a97a61c774ffd" - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backtrace" -version = "0.3.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bumpalo" -version = "3.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bzip2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.11+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "cc" -version = "1.0.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" -dependencies = [ - "libc", - "num-integer", - "num-traits", - "time 0.1.44", - "winapi", -] - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "darling" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "derive_builder" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d13202debe11181040ae9063d739fa32cfcaaebe2275fe387703460ae2365b30" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66e616858f6187ed828df7c64a6d71720d83767a7f19740b2d1b6fe6327b36e5" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_builder_macro" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58a94ace95092c5acb1e97a7e846b310cfbd499652f72297da7493f618a98d73" -dependencies = [ - "derive_builder_core", - "syn", -] - -[[package]] -name = "either" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - -[[package]] -name = "error-chain" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" -dependencies = [ - "backtrace", - "version_check", -] - -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] -name = "flate2" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "geo-types" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9805fbfcea97de816e6408e938603241879cc41eea3fba3f84f122f4f6f9c54" -dependencies = [ - "num-traits", -] - -[[package]] -name = "getrandom" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "gimli" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" - -[[package]] -name = "gpconv" -version = "0.1.0" -dependencies = [ - "gpx", - "itertools", - "lazy_static", - "osmio", - "wasm-bindgen", -] - -[[package]] -name = "gpx" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b03599b85866c88fd0125db7ca7a683be1550724918682c736c7893a399dc5e" -dependencies = [ - "assert_approx_eq", - "error-chain", - "geo-types", - "thiserror", - "time 0.3.11", - "xml-rs", -] - -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashlink" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" -dependencies = [ - "hashbrown", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "iter-progress" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97059d64dd4e3a8e16696f6c0be50c1d5da3a709983f39b73fd7f84f120c5cd4" - -[[package]] -name = "itertools" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" - -[[package]] -name = "libsqlite3-sys" -version = "0.22.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d" -dependencies = [ - "pkg-config", - "vcpkg", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "miniz_oxide" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" -dependencies = [ - "adler", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - -[[package]] -name = "object" -version = "0.28.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" - -[[package]] -name = "osmio" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0db40ae840afac7f6c710abf757bb76c6b95b4a34a20d55811ef70d30b3ea24f" -dependencies = [ - "anyhow", - "byteorder", - "bzip2", - "chrono", - "derive_builder", - "flate2", - "iter-progress", - "protobuf", - "quick-xml", - "rusqlite", - "separator", - "serde", - "serde_json", - "xml-rs", -] - -[[package]] -name = "pkg-config" -version = "0.3.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" - -[[package]] -name = "proc-macro2" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "protobuf" -version = "2.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70731852eec72c56d11226c8a5f96ad5058a3dab73647ca5f7ee351e464f2571" - -[[package]] -name = "quick-xml" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rusqlite" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4b1eaf239b47034fb450ee9cdedd7d0226571689d8823030c4b6c2cb407152" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink", - "libsqlite3-sys", - "memchr", - "smallvec", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" - -[[package]] -name = "ryu" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" - -[[package]] -name = "separator" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f97841a747eef040fcd2e7b3b9a220a7205926e60488e673d9e4926d27772ce5" - -[[package]] -name = "serde" -version = "1.0.139" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.139" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "smallvec" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" -dependencies = [ - "itoa", - "libc", - "num_threads", -] - -[[package]] -name = "unicode-ident" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "xml-rs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" diff --git a/apps/gipy/gpconv/Cargo.toml b/apps/gipy/gpconv/Cargo.toml deleted file mode 100644 index afcbec440..000000000 --- a/apps/gipy/gpconv/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "gpconv" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] -osm = ["dep:osmio"] - -# [lib] -# crate-type = ["cdylib"] - -[dependencies] -gpx="*" -itertools="*" -lazy_static="*" -osmio={version="*", optional=true} -wasm-bindgen="*" \ No newline at end of file diff --git a/apps/gipy/gpconv/README_gpc.md b/apps/gipy/gpconv/README_gpc.md deleted file mode 100644 index a881d01bf..000000000 --- a/apps/gipy/gpconv/README_gpc.md +++ /dev/null @@ -1,58 +0,0 @@ -This file documents the .gpc file format. - -current version is version 2. - -every number is encoded in little endian order. - -# header - -We start by a header of 5 16bytes unsigned ints. - -- the first int is a marker with value 47490 -- second int is version of this file format -- third int is **NP** the number of points composing the path -- fourth int is **IP** the number of interest points bordering the path -- fifth int is **LP** the number of interest points as encountered when looping through the path (higher than previous int since some points can be met several times) - -# points - -We continue with an array of **2 NP** f64 containing -for each point its x and y coordinate. - -# interest points - -After that comes the storage for interest points. - -- we start by an array of **2 IP** f64 containing -the x and y coordinates of each interest point -- we continue with an **IP** u8 array named **IPOINTS** containing the id of the point's type. -you can see the `Interest` enum in `src/osm.rs` to know what int is what. -for example 0 is a Bakery and 1 is a water point. - -Now we need to store the relationship between segments and points. -The idea is that in a display phase we don't want to loop on all interest points -to figure out if they should appear on the map or not. -We'll use the fact that we now the segments we want to display and therefore we should only -need to display the points bordering these segments. - -- we store an array **LOOP** of **LP** u16 indices of interest points in **IPOINTS** - -while this is a contiguous array it contains points along the path grouped in buckets of 5 points. - -to figure out on which segments they are : - -- we store an array **STARTS** of **ceil(LP/5)** u16 indices of groups of segments. - -Segments are grouped by 3. -This array tells us on which group of segment is the first point of any bucket. - -## display algorithm - -If we want to display the interest points for the segments between 10 and 16 for example we proceed -as follows: - - * segments are grouped by 3 so instead of segment indices of 10..=16 we will look at group indices 10/3 ..= 16/3 so 3..=5 - * we do a binary search of the highest number below 3 in the **STARTS** array. we call *s* the obtained index - * we do a binary search of the smallest number above 5 in the **STARTS** array. we call *e* the obtained index - * we now loop on all buckets between *s* and *e* that is : on all indices *i* in `LOOP[(s*5)..=(e*5)]` - * display `IPOINTS[i]` diff --git a/apps/gipy/gpconv/src/interests.rs b/apps/gipy/gpconv/src/interests.rs deleted file mode 100644 index 6d078e75d..000000000 --- a/apps/gipy/gpconv/src/interests.rs +++ /dev/null @@ -1,78 +0,0 @@ -use super::Point; -use lazy_static::lazy_static; -use std::collections::HashMap; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum Interest { - Bakery, - DrinkingWater, - Toilets, - // BikeShop, - // ChargingStation, - // Bank, - // Supermarket, - // Table, - // TourismOffice, - Artwork, - // Pharmacy, -} - -impl Into for Interest { - fn into(self) -> u8 { - match self { - Interest::Bakery => 0, - Interest::DrinkingWater => 1, - Interest::Toilets => 2, - // Interest::BikeShop => 8, - // Interest::ChargingStation => 4, - // Interest::Bank => 5, - // Interest::Supermarket => 6, - // Interest::Table => 7, - Interest::Artwork => 3, - // Interest::Pharmacy => 9, - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct InterestPoint { - pub point: Point, - pub interest: Interest, -} - -lazy_static! { - static ref INTERESTS: HashMap<(&'static str, &'static str), Interest> = { - [ - (("shop", "bakery"), Interest::Bakery), - (("amenity", "drinking_water"), Interest::DrinkingWater), - (("amenity", "toilets"), Interest::Toilets), - // (("shop", "bicycle"), Interest::BikeShop), - // (("amenity", "charging_station"), Interest::ChargingStation), - // (("amenity", "bank"), Interest::Bank), - // (("shop", "supermarket"), Interest::Supermarket), - // (("leisure", "picnic_table"), Interest::Table), - // (("tourism", "information"), Interest::TourismOffice), - (("tourism", "artwork"), Interest::Artwork), - // (("amenity", "pharmacy"), Interest::Pharmacy), - ] - .into_iter() - .collect() - }; -} - -impl InterestPoint { - pub fn color(&self) -> &'static str { - match self.interest { - Interest::Bakery => "red", - Interest::DrinkingWater => "blue", - Interest::Toilets => "brown", - // Interest::BikeShop => "purple", - // Interest::ChargingStation => "green", - // Interest::Bank => "black", - // Interest::Supermarket => "red", - // Interest::Table => "pink", - Interest::Artwork => "orange", - // Interest::Pharmacy => "chartreuse", - } - } -} diff --git a/apps/gipy/gpconv/src/lib.rs b/apps/gipy/gpconv/src/lib.rs deleted file mode 100644 index a58922c17..000000000 --- a/apps/gipy/gpconv/src/lib.rs +++ /dev/null @@ -1,575 +0,0 @@ -use itertools::Itertools; -use std::collections::{HashMap, HashSet}; -use std::fs::File; -use std::io::{BufReader, BufWriter, Read, Write}; -use std::path::Path; -use wasm_bindgen::prelude::*; - -use gpx::read; -use gpx::Gpx; - -mod interests; -use interests::InterestPoint; - -mod svg; - -#[cfg(feature = "osm")] -mod osm; -#[cfg(feature = "osm")] -use osm::{parse_osm_data, InterestPoint}; - -const LOWER_SHARP_TURN: f64 = 80.0 * std::f64::consts::PI / 180.0; -const UPPER_SHARP_TURN: f64 = std::f64::consts::PI * 2.0 - LOWER_SHARP_TURN; - -const KEY: u16 = 47490; -const FILE_VERSION: u16 = 3; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Point { - x: f64, - y: f64, -} - -impl Eq for Point {} -impl std::hash::Hash for Point { - fn hash(&self, state: &mut H) { - unsafe { std::mem::transmute::(self.x) }.hash(state); - unsafe { std::mem::transmute::(self.y) }.hash(state); - } -} - -impl Point { - fn squared_distance_between(&self, other: &Point) -> f64 { - let dx = other.x - self.x; - let dy = other.y - self.y; - dx * dx + dy * dy - } - fn distance_to_segment(&self, v: &Point, w: &Point) -> f64 { - let l2 = v.squared_distance_between(w); - if l2 == 0.0 { - return self.squared_distance_between(v).sqrt(); - } - // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. - // It falls where t = [(p-v) . (w-v)] / |w-v|^2 - // We clamp t from [0,1] to handle points outside the segment vw. - let x0 = self.x - v.x; - let y0 = self.y - v.y; - let x1 = w.x - v.x; - let y1 = w.y - v.y; - let dot = x0 * x1 + y0 * y1; - let t = (dot / l2).min(1.0).max(0.0); - - let proj = Point { - x: v.x + x1 * t, - y: v.y + y1 * t, - }; - - proj.squared_distance_between(self).sqrt() - } -} - -fn points(reader: R) -> (HashSet, Vec) { - // read takes any io::Read and gives a Result. - let mut gpx: Gpx = read(reader).unwrap(); - eprintln!("we have {} tracks", gpx.tracks.len()); - - let mut waypoints = HashSet::new(); - - let points = gpx - .tracks - .pop() - .unwrap() - .segments - .into_iter() - .flat_map(|segment| segment.points.into_iter()) - .map(|p| { - let is_commented = p.comment.is_some(); - let (x, y) = p.point().x_y(); - let p = Point { x, y }; - if is_commented { - waypoints.insert(p); - } - p - }) - .collect::>(); - (waypoints, points) -} - -// // NOTE: this angles idea could maybe be use to get dp from n^3 to n^2 -// fn acceptable_angles(p1: &(f64, f64), p2: &(f64, f64), epsilon: f64) -> (f64, f64) { -// // first, convert p2's coordinates for p1 as origin -// let (x1, y1) = *p1; -// let (x2, y2) = *p2; -// let (x, y) = (x2 - x1, y2 - y1); -// // rotate so that (p1, p2) ends on x axis -// let theta = y.atan2(x); -// let rx = x * theta.cos() - y * theta.sin(); -// let ry = x * theta.sin() + y * theta.cos(); -// assert!(ry.abs() <= std::f64::EPSILON); -// -// // now imagine a line at an angle alpha. -// // we want the distance d from (rx, 0) to our line -// // we have sin(alpha) = d / rx -// // limiting d to epsilon, we solve -// // sin(alpha) = e / rx -// // and get -// // alpha = arcsin(e/rx) -// let alpha = (epsilon / rx).asin(); -// -// // now we just need to rotate back -// let a1 = theta + alpha.abs(); -// let a2 = theta - alpha.abs(); -// assert!(a1 >= a2); -// (a1, a2) -// } -// -// // this is like ramer douglas peucker algorithm -// // except that we advance from the start without knowing the end. -// // each point we meet constrains the chosen segment's angle -// // a bit more. -// // -// fn simplify(mut points: &[(f64, f64)]) -> Vec<(f64, f64)> { -// let mut remaining_points = Vec::new(); -// while !points.is_empty() { -// let (sx, sy) = points.first().unwrap(); -// let i = match points -// .iter() -// .enumerate() -// .map(|(i, (x, y))| todo!("compute angles")) -// .try_fold( -// (0.0f64, std::f64::consts::FRAC_2_PI), -// |(amin, amax), (i, (amin2, amax2))| -> Result<(f64, f64), usize> { -// let new_amax = amax.min(amax2); -// let new_amin = amin.max(amin2); -// if new_amin >= new_amax { -// Err(i) -// } else { -// Ok((new_amin, new_amax)) -// } -// }, -// ) { -// Err(i) => i, -// Ok(_) => points.len(), -// }; -// remaining_points.push(points.first().cloned().unwrap()); -// points = &points[i..]; -// } -// remaining_points -// } - -fn extract_prog_dyn_solution( - points: &[Point], - start: usize, - end: usize, - cache: &HashMap<(usize, usize), (Option, usize)>, -) -> Vec { - if let Some(choice) = cache.get(&(start, end)).unwrap().0 { - let mut v1 = extract_prog_dyn_solution(points, start, choice + 1, cache); - let mut v2 = extract_prog_dyn_solution(points, choice, end, cache); - v1.pop(); - v1.append(&mut v2); - v1 - } else { - vec![points[start], points[end - 1]] - } -} - -fn simplify_prog_dyn( - points: &[Point], - start: usize, - end: usize, - epsilon: f64, - cache: &mut HashMap<(usize, usize), (Option, usize)>, -) -> usize { - if let Some(val) = cache.get(&(start, end)) { - val.1 - } else { - let res = if end - start <= 2 { - assert_eq!(end - start, 2); - (None, end - start) - } else { - let first_point = &points[start]; - let last_point = &points[end - 1]; - - if points[(start + 1)..end] - .iter() - .map(|p| p.distance_to_segment(first_point, last_point)) - .all(|d| d <= epsilon) - { - (None, 2) - } else { - // now we test all possible cutting points - ((start + 1)..(end - 1)) //TODO: take middle min - .map(|i| { - let v1 = simplify_prog_dyn(points, start, i + 1, epsilon, cache); - let v2 = simplify_prog_dyn(points, i, end, epsilon, cache); - (Some(i), v1 + v2 - 1) - }) - .min_by_key(|(_, v)| *v) - .unwrap() - } - }; - cache.insert((start, end), res); - res.1 - } -} - -fn rdp(points: &[Point], epsilon: f64) -> Vec { - if points.len() <= 2 { - points.iter().copied().collect() - } else { - if points.first().unwrap() == points.last().unwrap() { - let first = points.first().unwrap(); - let index_farthest = points - .iter() - .enumerate() - .skip(1) - .max_by(|(_, p1), (_, p2)| { - first - .squared_distance_between(p1) - .partial_cmp(&first.squared_distance_between(p2)) - .unwrap() - }) - .map(|(i, _)| i) - .unwrap(); - - let start = &points[..(index_farthest + 1)]; - let end = &points[index_farthest..]; - let mut res = rdp(start, epsilon); - res.pop(); - res.append(&mut rdp(end, epsilon)); - res - } else { - let (index_farthest, farthest_distance) = points - .iter() - .map(|p| p.distance_to_segment(points.first().unwrap(), points.last().unwrap())) - .enumerate() - .max_by(|(_, d1), (_, d2)| { - if d1.is_nan() { - std::cmp::Ordering::Greater - } else { - if d2.is_nan() { - std::cmp::Ordering::Less - } else { - d1.partial_cmp(d2).unwrap() - } - } - }) - .unwrap(); - if farthest_distance <= epsilon { - vec![ - points.first().copied().unwrap(), - points.last().copied().unwrap(), - ] - } else { - let start = &points[..(index_farthest + 1)]; - let end = &points[index_farthest..]; - let mut res = rdp(start, epsilon); - res.pop(); - res.append(&mut rdp(end, epsilon)); - res - } - } - } -} - -fn simplify_path(points: &[Point], epsilon: f64) -> Vec { - if points.len() <= 600 { - optimal_simplification(points, epsilon) - } else { - hybrid_simplification(points, epsilon) - } -} - -fn save_gpc( - mut writer: W, - points: &[Point], - waypoints: &HashSet, - buckets: &[Bucket], -) -> std::io::Result<()> { - eprintln!("saving {} points", points.len()); - - let mut unique_interest_points = Vec::new(); - let mut correspondance = HashMap::new(); - let interests_on_path = buckets - .iter() - .flat_map(|b| &b.points) - .map(|p| match correspondance.entry(*p) { - std::collections::hash_map::Entry::Occupied(o) => *o.get(), - std::collections::hash_map::Entry::Vacant(v) => { - let index = unique_interest_points.len(); - unique_interest_points.push(*p); - v.insert(index); - index - } - }) - .collect::>(); - - writer.write_all(&KEY.to_le_bytes())?; - writer.write_all(&FILE_VERSION.to_le_bytes())?; - writer.write_all(&(points.len() as u16).to_le_bytes())?; - writer.write_all(&(unique_interest_points.len() as u16).to_le_bytes())?; - writer.write_all(&(interests_on_path.len() as u16).to_le_bytes())?; - points - .iter() - .flat_map(|p| [p.x, p.y]) - .try_for_each(|c| writer.write_all(&c.to_le_bytes()))?; - - let mut waypoints_bits = std::iter::repeat(0u8) - .take(points.len() / 8 + if points.len() % 8 != 0 { 1 } else { 0 }) - .collect::>(); - points.iter().enumerate().for_each(|(i, p)| { - if waypoints.contains(p) { - waypoints_bits[i / 8] |= 1 << (i % 8) - } - }); - waypoints_bits - .iter() - .try_for_each(|byte| writer.write_all(&byte.to_le_bytes()))?; - - unique_interest_points - .iter() - .flat_map(|p| [p.point.x, p.point.y]) - .try_for_each(|c| writer.write_all(&c.to_le_bytes()))?; - - let counts: HashMap<_, usize> = - unique_interest_points - .iter() - .fold(HashMap::new(), |mut h, p| { - *h.entry(p.interest).or_default() += 1; - h - }); - counts.into_iter().for_each(|(interest, count)| { - eprintln!("{:?} appears {} times", interest, count); - }); - - unique_interest_points - .iter() - .map(|p| p.interest.into()) - .try_for_each(|i: u8| writer.write_all(&i.to_le_bytes()))?; - - interests_on_path - .iter() - .map(|i| *i as u16) - .try_for_each(|i| writer.write_all(&i.to_le_bytes()))?; - - buckets - .iter() - .map(|b| b.start as u16) - .try_for_each(|i| writer.write_all(&i.to_le_bytes()))?; - - Ok(()) -} - -fn optimal_simplification(points: &[Point], epsilon: f64) -> Vec { - let mut cache = HashMap::new(); - simplify_prog_dyn(&points, 0, points.len(), epsilon, &mut cache); - extract_prog_dyn_solution(&points, 0, points.len(), &cache) -} - -fn hybrid_simplification(points: &[Point], epsilon: f64) -> Vec { - if points.len() <= 300 { - optimal_simplification(points, epsilon) - } else { - if points.first().unwrap() == points.last().unwrap() { - let first = points.first().unwrap(); - let index_farthest = points - .iter() - .enumerate() - .skip(1) - .max_by(|(_, p1), (_, p2)| { - first - .squared_distance_between(p1) - .partial_cmp(&first.squared_distance_between(p2)) - .unwrap() - }) - .map(|(i, _)| i) - .unwrap(); - - let start = &points[..(index_farthest + 1)]; - let end = &points[index_farthest..]; - let mut res = hybrid_simplification(start, epsilon); - res.pop(); - res.append(&mut hybrid_simplification(end, epsilon)); - res - } else { - let (index_farthest, farthest_distance) = points - .iter() - .map(|p| p.distance_to_segment(points.first().unwrap(), points.last().unwrap())) - .enumerate() - .max_by(|(_, d1), (_, d2)| { - if d1.is_nan() { - std::cmp::Ordering::Greater - } else { - if d2.is_nan() { - std::cmp::Ordering::Less - } else { - d1.partial_cmp(d2).unwrap() - } - } - }) - .unwrap(); - if farthest_distance <= epsilon { - vec![ - points.first().copied().unwrap(), - points.last().copied().unwrap(), - ] - } else { - let start = &points[..(index_farthest + 1)]; - let end = &points[index_farthest..]; - let mut res = hybrid_simplification(start, epsilon); - res.pop(); - res.append(&mut hybrid_simplification(end, epsilon)); - res - } - } - } -} - -pub struct Bucket { - points: Vec, - start: usize, -} - -fn position_interests_along_path( - interests: &mut [InterestPoint], - path: &[Point], - d: f64, - buckets_size: usize, // final points are indexed in buckets - groups_size: usize, // how many segments are compacted together -) -> Vec { - interests.sort_unstable_by(|p1, p2| p1.point.x.partial_cmp(&p2.point.x).unwrap()); - // first compute for each segment a vec containing its nearby points - let mut positions = Vec::new(); - for segment in path.windows(2) { - let mut local_interests = Vec::new(); - let x0 = segment[0].x; - let x1 = segment[1].x; - let (xmin, xmax) = if x0 <= x1 { (x0, x1) } else { (x1, x0) }; - let i = interests.partition_point(|p| p.point.x < xmin - d); - let interests = &interests[i..]; - let i = interests.partition_point(|p| p.point.x <= xmax + d); - let interests = &interests[..i]; - for interest in interests { - if interest.point.distance_to_segment(&segment[0], &segment[1]) <= d { - local_interests.push(*interest); - } - } - positions.push(local_interests); - } - // fuse points on chunks of consecutive segments together - let grouped_positions = positions - .chunks(groups_size) - .map(|c| c.iter().flatten().unique().copied().collect::>()) - .collect::>(); - // now, group the points in buckets - let chunks = grouped_positions - .iter() - .enumerate() - .flat_map(|(i, points)| points.iter().map(move |p| (i, p))) - .chunks(buckets_size); - let mut buckets = Vec::new(); - for bucket_points in &chunks { - let mut bucket_points = bucket_points.peekable(); - let start = bucket_points.peek().unwrap().0; - let points = bucket_points.map(|(_, p)| *p).collect(); - buckets.push(Bucket { points, start }); - } - buckets -} - -fn detect_sharp_turns(path: &[Point], waypoints: &mut HashSet) { - path.iter() - .tuple_windows() - .map(|(a, b, c)| { - let xd1 = b.x - a.x; - let yd1 = b.y - a.y; - let angle1 = yd1.atan2(xd1); - - let xd2 = c.x - b.x; - let yd2 = c.y - b.y; - let angle2 = yd2.atan2(xd2); - let adiff = angle2 - angle1; - let adiff = if adiff < 0.0 { - adiff + std::f64::consts::PI * 2.0 - } else { - adiff - }; - (adiff, b) - }) - .filter_map(|(adiff, b)| { - if adiff > LOWER_SHARP_TURN && adiff < UPPER_SHARP_TURN { - Some(b) - } else { - None - } - }) - .for_each(|b| { - waypoints.insert(*b); - }); -} - -#[wasm_bindgen] -pub fn convert_gpx_strings(input_str: &str) -> Vec { - let mut interests = Vec::new(); - let mut output: Vec = Vec::new(); - convert_gpx(input_str.as_bytes(), &mut output, &mut interests); - output -} - -pub fn convert_gpx_files(input_file: &str, interests: &mut [InterestPoint]) { - let file = File::open(input_file).unwrap(); - let reader = BufReader::new(file); - let output_path = Path::new(&input_file).with_extension("gpc"); - let writer = BufWriter::new(File::create(output_path).unwrap()); - convert_gpx(reader, writer, interests); -} - -fn convert_gpx( - input_reader: R, - output_writer: W, - interests: &mut [InterestPoint], -) { - // load all points composing the trace and mark commented points - // as special waypoints. - let (mut waypoints, p) = points(input_reader); - - // detect sharp turns before path simplification to keep them - detect_sharp_turns(&p, &mut waypoints); - waypoints.insert(p.first().copied().unwrap()); - waypoints.insert(p.last().copied().unwrap()); - println!("we have {} waypoints", waypoints.len()); - - println!("initially we had {} points", p.len()); - - // simplify path - let mut rp = Vec::new(); - let mut segment = Vec::new(); - for point in &p { - segment.push(*point); - if waypoints.contains(point) { - if segment.len() >= 2 { - let mut s = simplify_path(&segment, 0.00015); - rp.append(&mut s); - segment = rp.pop().into_iter().collect(); - } - } - } - rp.append(&mut segment); - println!("we now have {} points", rp.len()); - - // add interest points from open street map if we have any - let buckets = position_interests_along_path(interests, &rp, 0.001, 5, 3); - - // save_svg( - // "test.svg", - // &p, - // &rp, - // buckets.iter().flat_map(|b| &b.points), - // &waypoints, - // ) - // .unwrap(); - - save_gpc(output_writer, &rp, &waypoints, &buckets).unwrap(); -} diff --git a/apps/gipy/gpconv/src/main.rs b/apps/gipy/gpconv/src/main.rs deleted file mode 100644 index c804d0889..000000000 --- a/apps/gipy/gpconv/src/main.rs +++ /dev/null @@ -1,22 +0,0 @@ -use gpconv::convert_gpx_files; - -fn main() { - let input_file = std::env::args().nth(1).unwrap_or("m.gpx".to_string()); - let mut interests; - - #[cfg(feature = "osm")] - { - let osm_file = std::env::args().nth(2); - let mut interests = if let Some(osm) = osm_file { - interests = parse_osm_data(osm); - } else { - Vec::new() - }; - } - #[cfg(not(feature = "osm"))] - { - interests = Vec::new() - } - - convert_gpx_files(&input_file, &mut interests); -} diff --git a/apps/gipy/gpconv/src/osm.rs b/apps/gipy/gpconv/src/osm.rs deleted file mode 100644 index ebc7071ef..000000000 --- a/apps/gipy/gpconv/src/osm.rs +++ /dev/null @@ -1,36 +0,0 @@ -use super::Interest; -use super::Point; -use itertools::Itertools; -use lazy_static::lazy_static; -use osmio::OSMObjBase; -use osmio::{prelude::*, ObjId}; -use std::collections::{HashMap, HashSet}; -use std::path::Path; - -pub fn parse_osm_data>(path: P) -> Vec { - let reader = osmio::read_pbf(path).ok(); - reader - .map(|mut reader| { - let mut interests = Vec::new(); - for obj in reader.objects() { - match obj { - osmio::obj_types::ArcOSMObj::Node(n) => { - n.lat_lon_f64().map(|(lat, lon)| { - for p in n.tags().filter_map(move |(k, v)| { - Interest::new(k, v).map(|i| InterestPoint { - point: Point { x: lon, y: lat }, - interest: i, - }) - }) { - interests.push(p); - } - }); - } - osmio::obj_types::ArcOSMObj::Way(w) => {} - osmio::obj_types::ArcOSMObj::Relation(_) => {} - } - } - interests - }) - .unwrap_or_default() -} diff --git a/apps/gipy/gpconv/src/svg.rs b/apps/gipy/gpconv/src/svg.rs deleted file mode 100644 index 387a0f7a0..000000000 --- a/apps/gipy/gpconv/src/svg.rs +++ /dev/null @@ -1,86 +0,0 @@ -use itertools::Itertools; -use std::{ - collections::HashSet, - io::{BufWriter, Write}, - path::Path, -}; - -use crate::{interests::InterestPoint, Point}; - -fn save_path(writer: &mut W, p: &[Point], stroke: &str) -> std::io::Result<()> { - write!( - writer, - "")?; - Ok(()) -} - -// save svg file from given path and interest points. -// useful for debugging path simplification and previewing traces. -pub fn save_svg<'a, P: AsRef, I: IntoIterator>( - filename: P, - p: &[Point], - rp: &[Point], - interest_points: I, - waypoints: &HashSet, -) -> std::io::Result<()> { - let mut writer = BufWriter::new(std::fs::File::create(filename)?); - let (xmin, xmax) = p - .iter() - .map(|p| p.x) - .minmax_by(|a, b| a.partial_cmp(b).unwrap()) - .into_option() - .unwrap(); - - let (ymin, ymax) = p - .iter() - .map(|p| p.y) - .minmax_by(|a, b| a.partial_cmp(b).unwrap()) - .into_option() - .unwrap(); - - writeln!( - &mut writer, - "", - xmin, - ymin, - xmax - xmin, - ymax - ymin - )?; - write!( - &mut writer, - "", - xmin, - ymin, - xmax - xmin, - ymax - ymin - )?; - - save_path(&mut writer, &p, "red")?; - save_path(&mut writer, &rp, "black")?; - - for point in interest_points { - writeln!( - &mut writer, - "", - point.point.x, - point.point.y, - point.color(), - )?; - } - - waypoints.iter().try_for_each(|p| { - writeln!( - &mut writer, - "", - p.x, p.y, - ) - })?; - - writeln!(&mut writer, "")?; - Ok(()) -} From 769ced85e4fc0459c792abcfad77bd652ba718b9 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 7 Nov 2022 14:03:15 +0100 Subject: [PATCH 087/106] testing interface.html --- apps/gipy/interface.html | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 apps/gipy/interface.html diff --git a/apps/gipy/interface.html b/apps/gipy/interface.html new file mode 100644 index 000000000..852fdd32a --- /dev/null +++ b/apps/gipy/interface.html @@ -0,0 +1,58 @@ + + + + + + +

Please select a gpx file to be converted to gpc and loaded.

+ + + + + + + + + From 10d42bae2e246fae19177c50ddc8c230513f944c Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 7 Nov 2022 14:08:39 +0100 Subject: [PATCH 088/106] interface --- apps/gipy/README.md | 4 +- apps/{gipy_uploader => gipy}/pkg/gpconv.d.ts | 0 apps/{gipy_uploader => gipy}/pkg/gpconv.js | 0 .../pkg/gpconv_bg.wasm | Bin .../pkg/gpconv_bg.wasm.d.ts | 0 apps/{gipy_uploader => gipy}/pkg/package.json | 0 apps/gipy_uploader/ChangeLog | 1 - apps/gipy_uploader/README.md | 7 --- apps/gipy_uploader/custom.html | 58 ------------------ apps/gipy_uploader/gipy.png | Bin 1606 -> 0 bytes apps/gipy_uploader/metadata.json | 14 ----- 11 files changed, 2 insertions(+), 82 deletions(-) rename apps/{gipy_uploader => gipy}/pkg/gpconv.d.ts (100%) rename apps/{gipy_uploader => gipy}/pkg/gpconv.js (100%) rename apps/{gipy_uploader => gipy}/pkg/gpconv_bg.wasm (100%) rename apps/{gipy_uploader => gipy}/pkg/gpconv_bg.wasm.d.ts (100%) rename apps/{gipy_uploader => gipy}/pkg/package.json (100%) delete mode 100644 apps/gipy_uploader/ChangeLog delete mode 100644 apps/gipy_uploader/README.md delete mode 100644 apps/gipy_uploader/custom.html delete mode 100644 apps/gipy_uploader/gipy.png delete mode 100644 apps/gipy_uploader/metadata.json diff --git a/apps/gipy/README.md b/apps/gipy/README.md index 11638503c..4a6921933 100644 --- a/apps/gipy/README.md +++ b/apps/gipy/README.md @@ -50,8 +50,8 @@ Once you have your gpx file you need to convert it to *gpc* which is my custom f They are smaller than gpx and reduce the number of computations left to be done on the watch. Two possibilities here : -- easy : use [gipy uploader](../gipy_uploader) -- hard : use [gpconv](https://github.com/wagnerf42/gpconv) +- easy : upload from here +- hard : use an external tool : [gpconv](https://github.com/wagnerf42/gpconv) * you need to compile *gpconv* yourself (it is some rust code) * you can download additional openstreetmap data to get interest points along the path * you need to upload the obtained *gpc* file manually for example with the [ide](https://www.espruino.com/ide/) diff --git a/apps/gipy_uploader/pkg/gpconv.d.ts b/apps/gipy/pkg/gpconv.d.ts similarity index 100% rename from apps/gipy_uploader/pkg/gpconv.d.ts rename to apps/gipy/pkg/gpconv.d.ts diff --git a/apps/gipy_uploader/pkg/gpconv.js b/apps/gipy/pkg/gpconv.js similarity index 100% rename from apps/gipy_uploader/pkg/gpconv.js rename to apps/gipy/pkg/gpconv.js diff --git a/apps/gipy_uploader/pkg/gpconv_bg.wasm b/apps/gipy/pkg/gpconv_bg.wasm similarity index 100% rename from apps/gipy_uploader/pkg/gpconv_bg.wasm rename to apps/gipy/pkg/gpconv_bg.wasm diff --git a/apps/gipy_uploader/pkg/gpconv_bg.wasm.d.ts b/apps/gipy/pkg/gpconv_bg.wasm.d.ts similarity index 100% rename from apps/gipy_uploader/pkg/gpconv_bg.wasm.d.ts rename to apps/gipy/pkg/gpconv_bg.wasm.d.ts diff --git a/apps/gipy_uploader/pkg/package.json b/apps/gipy/pkg/package.json similarity index 100% rename from apps/gipy_uploader/pkg/package.json rename to apps/gipy/pkg/package.json diff --git a/apps/gipy_uploader/ChangeLog b/apps/gipy_uploader/ChangeLog deleted file mode 100644 index 28f11c1c7..000000000 --- a/apps/gipy_uploader/ChangeLog +++ /dev/null @@ -1 +0,0 @@ -0.01: Initial Release diff --git a/apps/gipy_uploader/README.md b/apps/gipy_uploader/README.md deleted file mode 100644 index f2813ccfb..000000000 --- a/apps/gipy_uploader/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Gipy Uploader - -Uploads and convert a gpx file to the watch for use with [gipy](../gipy). - -## Creator - -Made by [Frederic Wagner](mailto:frederic.wagner@imag.fr) diff --git a/apps/gipy_uploader/custom.html b/apps/gipy_uploader/custom.html deleted file mode 100644 index 852fdd32a..000000000 --- a/apps/gipy_uploader/custom.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - -

Please select a gpx file to be converted to gpc and loaded.

- - - - - - - - - diff --git a/apps/gipy_uploader/gipy.png b/apps/gipy_uploader/gipy.png deleted file mode 100644 index e9e472f5ce52c633463e5291590a3f8be81abae8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1606 zcmV-M2D$l(P)IQ3e>p z0Xz<%`|Xbn&~+G?24HufHEWh+$xB8yZ#2ztY8n7^U3aCOKT`lu0l1YZC3upF;7)A?#Cl%ocKpz5V6hfT1bn)f|y}rK9s&=X? zr3(L>2Y|tm$kiZ#`6|An5eWG5_Yg2qEyz>+)$4aU(Oum-ZalHr0U$Xl)W#&IE=k|p z6rCgNb`tdFWyCWN&{?PE0pN*qX`d44S4HUeYvH!fR5`W_kR)D@ALZ`li)W$tSNP_Pu);1oko7EpIdRp1dF6+)21mEspF+eR zStb*n2ILnNi}F`hd%dP3Cje2QHl&)QSfq`PXd0>7{N_n)x=UJ-jdWUQx0f%p52B^a4*+O8G zcYF2Ki}$eOFA8or!z{-F_}i%l6EdaLz72Ri)12hr(+J!_Wx<>w zQpy(CU72FdG^Dm^^WLnjy=EiF8p;p2Uj~5UjONc(@t`*^gGp-!*w&d=ffEs??2?*w ze_A11Dxm5}cF%A)asq%R$u+bjea|@+FR;inmY6?=we5x({vyUCe@)$2iHHfG0iAVm zGaPBatxa4H!QoBYw&iFl?&8q8eIS|zK-#u^H+nIlC9uddHW4vl0jUR%=>)VBqRi91 zuf*N<7KPSr1>$duZ6JjhyDPo3$C!~!r%+jI)@Z@J4?y%UE?Ka9$xF@3QxXFd^m!Tp zo^H>x(&BX?jP$~)4PUFGaYvx)NOn)bya}=90YH$XhO*Xd<@!$m|9d&&0ppZ2)9t~X zr4-v%&7J$ODyl5t*1R`1!Tj*jLvUoX-jb=@*XZ^2Z7f6{0JiNp7Eig7jgTr}I)wrU z-9-!v#0eJ_k8BDi>N()^Y<0S4ih@BJ2Kp=OerewrcM4UuOzOsrQWL>t!NaerPEU71 zu&lFT(hM3)_+#|ru_M}zr679LM6kOdwYL-XN}5c#mRp8I%(AL1psdB+7eosZ3NpNb zL*pMerO?$ImJKEXpW#@^b+eFn-8W{oj14**jFt!{UsKACBjMSF=M zcwbDDil?8-=>%|1B0XjbRX4fgZrpBPsifO7Y0{kr-VoiWEFks#wL$;`Er<@dktPaM z0X;W287ynjL1zvCjRm3gKSejj6c9W-p$~v%)^&OLs@949+qdYtF1mIV4Irp4YVCt? zDI(dY3dIy)7)Ai#E;i&-G>(+r`4TM5>6~`{%oYZ{=D?~7VTO34WX*cz8N~ANTDfQ{ zTR6M7OJEm3!|eN1B20u_f2eMAJVJ+EFa;ORejb8$GSDIGJ_G-NXj)NdUCheU9(D@& zK3*W;%l9x)gV?`@oPn`NxWaGwD>t6)5WNR@fcqH#0oLvGD9P1zN&o-=07*qoM6N<$ Ef?_oAlK=n! diff --git a/apps/gipy_uploader/metadata.json b/apps/gipy_uploader/metadata.json deleted file mode 100644 index c0c05e4f4..000000000 --- a/apps/gipy_uploader/metadata.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "id": "gipy_uploader", - "name": "Gipy uploader", - "version": "0.01", - "description": "uploads and convert gpx files for use with gipy", - "icon": "gipy.png", - "type": "app", - "tags": "tool,outdoors,gps", - "supports": ["BANGLEJS2"], - "readme": "README.md", - "storage": [], - "custom": "custom.html", - "allow_emulator": false -} From 80099590901721bcbad019bd9adddeef2a07249f Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 7 Nov 2022 14:08:52 +0100 Subject: [PATCH 089/106] missing file --- apps/gipy/metadata.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 9761643fc..2d06a7c2d 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -11,6 +11,7 @@ "screenshots": [], "supports": ["BANGLEJS2"], "readme": "README.md", + "interface": "interface.html", "storage": [ {"name":"gipy.app.js","url":"app.js"}, {"name":"gipy.settings.js","url":"settings.js"}, From 61f62f59d05b62d076beed817b8960f3cccdadff Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 7 Nov 2022 14:26:46 +0100 Subject: [PATCH 090/106] gipy: removed linting on wasm file --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index fcbea07f9..4af79d129 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,4 +3,5 @@ apps/banglerun/rollup.config.js apps/schoolCalendar/fullcalendar/main.js apps/authentiwatch/qr_packed.js apps/qrcode/qr-scanner.umd.min.js +apps/gipy/pkg/gpconv.js *.test.js From 6c992653066d497fbee9de241ffe2762e671a2e9 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 7 Nov 2022 14:35:06 +0100 Subject: [PATCH 091/106] gipy: skip tsc on wasm --- tsconfig.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index db3db1fc3..8da08b8e2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,5 +15,9 @@ "strict": true, "typeRoots": ["./typescript/types"] }, - "include": ["./**/*"] + "include": ["./**/*"], + "exclude": [ + "**/gpconv.d.ts", + "**/gpconv_bg.wasm.d.ts" + ] } From b9efa173bf9d9d6719a900617e904ffe831a14fb Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Mon, 7 Nov 2022 21:42:31 +0100 Subject: [PATCH 092/106] gipy: wrong fs file import in interface --- apps/gipy/interface.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/gipy/interface.html b/apps/gipy/interface.html index 852fdd32a..672b0e4f4 100644 --- a/apps/gipy/interface.html +++ b/apps/gipy/interface.html @@ -9,7 +9,7 @@ - + + From 0d43f4ff290c43784b034ebc0608e30dc6125f6d Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Tue, 8 Nov 2022 18:15:04 +0100 Subject: [PATCH 099/106] missing chunksize --- apps/gipy/interface.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/gipy/interface.html b/apps/gipy/interface.html index bd9753e0b..430fafbac 100644 --- a/apps/gipy/interface.html +++ b/apps/gipy/interface.html @@ -17,6 +17,8 @@ + From df756c01034f1d10d7f216cf1f21cced602eb2c3 Mon Sep 17 00:00:00 2001 From: frederic wagner Date: Wed, 9 Nov 2022 20:45:48 +0100 Subject: [PATCH 105/106] gipy : interface works except upload --- apps/gipy/interface.html | 71 ++++++++++++++++++++-------- apps/gipy/pkg/gpconv.d.ts | 22 +++++++-- apps/gipy/pkg/gpconv.js | 75 ++++++++++++++++++++++-------- apps/gipy/pkg/gpconv_bg.wasm | Bin 603943 -> 605249 bytes apps/gipy/pkg/gpconv_bg.wasm.d.ts | 7 +-- 5 files changed, 128 insertions(+), 47 deletions(-) diff --git a/apps/gipy/interface.html b/apps/gipy/interface.html index 57cb9557e..e2c3707bb 100644 --- a/apps/gipy/interface.html +++ b/apps/gipy/interface.html @@ -58,13 +58,15 @@ svg { width:95% }