mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-24 07:37:46 -05:00
Compare commits
146 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb5da76834 | ||
|
|
ad922f0667 | ||
|
|
773b35d9ce | ||
|
|
a3347c9d62 | ||
|
|
da671b8dd3 | ||
|
|
6d877e13e4 | ||
|
|
8ab1d7170c | ||
|
|
1f75d722cd | ||
|
|
11bd4b2cec | ||
|
|
dcc03da237 | ||
|
|
295c00ea55 | ||
|
|
8d6756d57d | ||
|
|
71acd28b74 | ||
|
|
e79c1168ff | ||
|
|
9833159fa8 | ||
|
|
88ace5ba82 | ||
|
|
0ed82d15ff | ||
|
|
0f525a6c48 | ||
|
|
a91a5ce52e | ||
|
|
cd3b1db90d | ||
|
|
6e3e34c642 | ||
|
|
8ce7f5cae2 | ||
|
|
fae3bb2038 | ||
|
|
9490aa7110 | ||
|
|
66a27d19f3 | ||
|
|
09cf6cb087 | ||
|
|
4d23c916a9 | ||
|
|
fec5de1de1 | ||
|
|
89957ef738 | ||
|
|
a8e9bcd9eb | ||
|
|
0c3e3b0c35 | ||
|
|
78f9b7162c | ||
|
|
600a294ab2 | ||
|
|
1b8bedcd6d | ||
|
|
1b7b5121e6 | ||
|
|
e469ce83e5 | ||
|
|
ef68e6039e | ||
|
|
2ad673f8aa | ||
|
|
5b55087337 | ||
|
|
cb8a81823d | ||
|
|
742950b62c | ||
|
|
9bb8825ab7 | ||
|
|
a0c41290cd | ||
|
|
85240f0145 | ||
|
|
7cc50d7127 | ||
|
|
3d0ebc0b85 | ||
|
|
4e83e37d61 | ||
|
|
94030c010c | ||
|
|
6b287c4084 | ||
|
|
1f6fe04b7d | ||
|
|
91d5ce02e2 | ||
|
|
22bd9ed9e8 | ||
|
|
19142e0b59 | ||
|
|
06fe347c73 | ||
|
|
adead1ac3c | ||
|
|
f722ae5d7a | ||
|
|
33a14c581c | ||
|
|
b80ddf8851 | ||
|
|
848745359d | ||
|
|
5d47ca2e3a | ||
|
|
c28a5382d4 | ||
|
|
f8bdae78cd | ||
|
|
9891cf8e88 | ||
|
|
172e66fe15 | ||
|
|
4a925d10bd | ||
|
|
c7fc0a34ed | ||
|
|
9780f6d2c0 | ||
|
|
f6afb2a8cb | ||
|
|
b0371c1b20 | ||
|
|
2625acffda | ||
|
|
7418748c0f | ||
|
|
3c3edf9ab4 | ||
|
|
d52ec0c63f | ||
|
|
eebb060f1a | ||
|
|
19c0f311ad | ||
|
|
9e280d3150 | ||
|
|
0998ed1f67 | ||
|
|
d7a644cb78 | ||
|
|
f2d98f9d82 | ||
|
|
09d6647ec0 | ||
|
|
1d3e3417aa | ||
|
|
20ae25cf8a | ||
|
|
087178193b | ||
|
|
60d54c989b | ||
|
|
c0555e7965 | ||
|
|
49c2fb3494 | ||
|
|
c1ec46917e | ||
|
|
ac11cddd42 | ||
|
|
6267e897d4 | ||
|
|
8a0224707b | ||
|
|
09ded65b4e | ||
|
|
c8f333ce89 | ||
|
|
4989aedd8b | ||
|
|
4beb1f92ad | ||
|
|
f24b7d1c2c | ||
|
|
094a26980f | ||
|
|
098f815ac9 | ||
|
|
6c51693b8d | ||
|
|
a360c71ecb | ||
|
|
b9da72e449 | ||
|
|
b0aeb8af98 | ||
|
|
1867d1bf7a | ||
|
|
31fcee97e1 | ||
|
|
de8fd364f4 | ||
|
|
e3f271be5d | ||
|
|
99a2540398 | ||
|
|
85173b438b | ||
|
|
c288883572 | ||
|
|
c8f949da01 | ||
|
|
fe33dca1bc | ||
|
|
4fb5090e9b | ||
|
|
d9b8bf382a | ||
|
|
d69456dfd0 | ||
|
|
4da2a273c7 | ||
|
|
8e622c881d | ||
|
|
89b2175d89 | ||
|
|
3c30481821 | ||
|
|
385353689d | ||
|
|
7f9c838b9d | ||
|
|
205814e6f6 | ||
|
|
f30ae4a720 | ||
|
|
a6117d3484 | ||
|
|
f650def803 | ||
|
|
09ec0d1635 | ||
|
|
cfc98209a1 | ||
|
|
17dbab4659 | ||
|
|
f4ed7f7397 | ||
|
|
c26253fe44 | ||
|
|
1c7cc32427 | ||
|
|
4ff944c4e4 | ||
|
|
2ec437a14b | ||
|
|
ae05d44649 | ||
|
|
a0f90a8c94 | ||
|
|
e1b7463490 | ||
|
|
27cbb5e208 | ||
|
|
5526e5d97c | ||
|
|
fc92ee9cdc | ||
|
|
f20fab965c | ||
|
|
9146e7dab8 | ||
|
|
54936b6e1a | ||
|
|
f3245dc29b | ||
|
|
90968029ad | ||
|
|
97cab1d007 | ||
|
|
0d41fb2685 | ||
|
|
2c0a5085ab | ||
|
|
9e80270a78 |
1
.github/workflows/apikeys-ci.xml
vendored
1
.github/workflows/apikeys-ci.xml
vendored
@@ -4,4 +4,5 @@
|
||||
<string name="goingelectric_key" translatable="false">ci</string>
|
||||
<string name="chargeprice_key" translatable="false">ci</string>
|
||||
<string name="openchargemap_key" translatable="false">ci</string>
|
||||
<string name="fronyx_key" translatable="false">ci</string>
|
||||
</resources>
|
||||
|
||||
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@@ -31,6 +31,7 @@ jobs:
|
||||
CHARGEPRICE_API_KEY: ${{ secrets.CHARGEPRICE_API_KEY }}
|
||||
MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }}
|
||||
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
|
||||
KEYSTORE_ALIAS_PASSWORD: ${{ secrets.KEYSTORE_ALIAS_PASSWORD }}
|
||||
|
||||
65
_img/app_logo.svg
Normal file
65
_img/app_logo.svg
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 663.6 219.8" style="enable-background:new 0 0 663.6 219.8;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFB300;}
|
||||
.st1{fill:#90A4AE;}
|
||||
.st2{fill:#546E7A;}
|
||||
.st3{fill:#00E676;}
|
||||
.st4{fill:#FFFFFF;fill-opacity:0.2;}
|
||||
.st5{fill:#3E2723;fill-opacity:0.2;}
|
||||
.st6{opacity:0.45;enable-background:new ;}
|
||||
.st7{enable-background:new ;}
|
||||
.st8{fill:#1D1D1B;}
|
||||
</style>
|
||||
<g id="Ebene_2_1_">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0"
|
||||
d="M19.4,161.7l-4-35.1l-6.1,0.6l4,35.1L19.4,161.7z M41.2,159.1l-4-35.1l-6.1,0.6l4,35.1L41.2,159.1z" />
|
||||
<path class="st1" d="M52.6,206.9c-1.9,2.3-3.4,3.8-3.6,4c-5.5,4.4-9.9,5.7-13.5,4c-6.3-3.2-5.9-15-5.7-16.3l4.4,0.2
|
||||
c-0.2,3.4,0.4,10.6,3.4,12c1.7,0.8,4.6-0.2,8.5-3.4l0,0c0,0,12.3-12.3,9.7-22c-3-11.6,10.6-28.3,15-34l0.6-0.6l3.6,2.7l-0.6,0.8
|
||||
c-13.7,16.9-15.2,25.6-14.2,30C62.3,192.9,56.6,202,52.6,206.9z" />
|
||||
<path class="st1"
|
||||
d="M5.9,161.2l1.7,14.4l13.3,8.9l18-1.9l11-11.6l-1.7-14.4L5.9,161.2z" />
|
||||
<g>
|
||||
<path class="st2" d="M38.6,182.6l-18,1.9l3.8,15.8l14.2-1.7V182.6L38.6,182.6z M51.5,144.5l1.5,13.1l-51.5,5.9L0,150.4
|
||||
L51.5,144.5z" />
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st3" d="M91.9,0c-37,0-67,30-67,67c0,50.5,56.4,76.9,63.2,148.9c0.2,2.1,1.9,3.6,4,3.6s3.8-1.5,4-3.6
|
||||
c6.8-72,63.2-98.4,63.2-148.9C158.8,29.8,128.8,0,91.9,0z" />
|
||||
<path class="st4" d="M91.9,1.5c36.8,0,66.5,29.6,67,66.1c0-0.2,0-0.4,0-0.6c0-37-30-67-67-67s-67,29.8-67,67c0,0.2,0,0.4,0,0.6
|
||||
C25.3,31.1,55.1,1.5,91.9,1.5L91.9,1.5z" />
|
||||
<path class="st5" d="M95.9,214.3c-0.2,2.1-1.9,3.6-4,3.6s-3.8-1.5-4-3.6c-6.5-71.8-62.5-98.2-63-148.1c0,0.4,0,0.6,0,1.1
|
||||
c0,50.5,56.4,76.9,63.2,148.9c0.2,2.1,1.9,3.6,4,3.6s3.8-1.5,4-3.6c6.8-72,63.2-98.4,63.2-148.9c0-0.4,0-0.6,0-1.1
|
||||
C158.4,116,102.4,142.4,95.9,214.3L95.9,214.3z" />
|
||||
</g>
|
||||
<path class="st6"
|
||||
d="M76.5,34.3v40.6h11v33.2l25.8-44.4H98.5l14.8-29.6C113.4,34.3,76.5,34.3,76.5,34.3z" />
|
||||
</g>
|
||||
</g>
|
||||
<g class="st7">
|
||||
<path class="st8"
|
||||
d="M307.9,102.9h-45.4v39.6h52.2v6.9h-60.4V52.3h60.1v6.9h-51.9v36.7h45.4V102.9z" />
|
||||
<path class="st8"
|
||||
d="M361.2,137.4l0.5,2.1l0.6-2.1l30.5-85.1h9l-36.1,97.1h-7.9l-36.1-97.1h8.9L361.2,137.4z" />
|
||||
<path class="st8" d="M427,52.4l35.8,85.7l35.9-85.7h10.9v97.1h-8.2v-42.3l0.7-43.3L466,149.5h-6.3l-36-85.3l0.7,42.7v42.5h-8.2
|
||||
V52.3H427V52.4z" />
|
||||
<path class="st8" d="M578,149.4c-0.8-2.3-1.3-5.6-1.5-10.1c-2.8,3.6-6.4,6.5-10.7,8.4c-4.3,2-8.9,3-13.8,3
|
||||
c-6.9,0-12.5-1.9-16.8-5.8s-6.4-8.8-6.4-14.7c0-7,2.9-12.6,8.8-16.7c5.8-4.1,14-6.1,24.4-6.1h14.5v-8.2c0-5.2-1.6-9.2-4.8-12.2
|
||||
s-7.8-4.4-13.9-4.4c-5.6,0-10.2,1.4-13.8,4.3c-3.6,2.8-5.5,6.3-5.5,10.3l-8-0.1c0-5.7,2.7-10.7,8-14.9s11.9-6.3,19.7-6.3
|
||||
c8,0,14.4,2,19,6s7,9.6,7.2,16.8v34.1c0,7,0.7,12.2,2.2,15.7v0.8H578V149.4z M552.9,143.7c5.3,0,10.1-1.3,14.3-3.9
|
||||
s7.3-6,9.2-10.3v-15.9h-14.3c-8,0.1-14.2,1.5-18.7,4.4c-4.5,2.8-6.7,6.7-6.7,11.6c0,4,1.5,7.4,4.5,10.1S548.1,143.7,552.9,143.7z
|
||||
" />
|
||||
<path class="st8" d="M663.6,114.1c0,11.2-2.5,20.2-7.5,26.8s-11.6,9.9-20,9.9c-9.9,0-17.4-3.5-22.7-10.4v36.8h-7.9V77.3h7.4
|
||||
l0.4,10.2C618.5,79.8,626,76,635.9,76c8.6,0,15.4,3.3,20.3,9.8c4.9,6.5,7.4,15.6,7.4,27.2V114.1z M655.6,112.7
|
||||
c0-9.2-1.9-16.5-5.7-21.8s-9-8-15.8-8c-4.9,0-9.1,1.2-12.6,3.5c-3.5,2.4-6.2,5.8-8.1,10.3v34.6c1.9,4.1,4.6,7.3,8.2,9.5
|
||||
c3.6,2.2,7.8,3.3,12.6,3.3c6.7,0,11.9-2.7,15.7-8C653.7,130.6,655.6,122.9,655.6,112.7z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
64
_img/powered_by_fronyx.svg
Normal file
64
_img/powered_by_fronyx.svg
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 566.9 254.9" style="enable-background:new 0 0 566.9 254.9;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#000044;}
|
||||
</style>
|
||||
<path class="st0" d="M60.8,86.3c-5.6,0-10,0.5-13.3,1.4c-3.3,0.9-5.7,2.7-7.1,5.2c-0.4,0.6-0.7,1.3-0.9,2c-1.1,3.3-1.4,6.8-1.4,10.2
|
||||
c0,2.1,0,4.2,0,6.2c0,0.1,0,0.2,0,0.3h22.3v20.9H38.1v71.2v7.6H14.5v-7.6v-71.2H0v-20.9h14.5V104c0-13.4,3.9-23.8,11.2-31.3
|
||||
c7.3-7.4,19-10.8,35.1-10V86.3 M157.1,161.2c0-15.5,11.2-27.3,25.7-27.3c14.5,0,25.5,11.8,25.5,27.3c0,15.6-11,27.3-25.5,27.3
|
||||
C168.3,188.5,157.1,176.9,157.1,161.2 M182.8,110.1c-27.9,0-49.7,22.2-49.7,51.1c0,29.1,21.8,51.3,49.7,51.3
|
||||
c27.9,0,49.4-22.2,49.4-51.3C232.1,132.3,210.7,110.1,182.8,110.1 M541.4,161.5c14.1,0,25.6-11.5,25.6-25.6
|
||||
c0-14.1-11.5-25.6-25.6-25.6c-14.1,0-25.6,11.5-25.6,25.6C515.8,150,527.2,161.5,541.4,161.5 M129.6,110.5c-2.2-0.2-4.3-0.4-6.6-0.4
|
||||
c-4.4,0-9.1,0.6-13.9,1.9c-4.8,1.3-8.5,3.4-10.7,6.5v-7H74.7V211h23.8v-58.2c0-5.4,1.4-9.5,4.1-12.3c4.4-4.5,10.6-6.5,16.7-6.8
|
||||
c1.7-0.1,3.5,0,5.3,0.1c1.6,0.2,3.2,0.5,4.7,0.7c0-1,0-2,0-3c0-2,0-3.9,0-5.9c0-2.2,0-4.5,0-6.7c0-1.9,0.2-3.8,0.2-5.7
|
||||
c0-0.9,0.2-1.7,0.2-2.6C129.6,110.6,129.6,110.5,129.6,110.5z M475.8,160.6l29.7-49h-28.7l-16.3,31.7l-16.5-31.7H415l30.1,49
|
||||
l-30.7,50.6h28.7l17.3-33.1l17.7,33.1h28.7L475.8,160.6z M356.9,254.8c14.9,0.5,26.7-3.1,34.7-10.9c7.6-7.4,11.6-18.1,12-33l0,0
|
||||
v-2.4v-97.1h-24.5c0,0,0,14.3,0,28.8v14.9v0.1v14.5c0,5.3-1.2,9.7-5.8,13.8c-4.3,3.9-10.9,5.1-15.4,5.1c-2.7,0-10.7-0.9-15.4-7.5
|
||||
c-2.4-3.4-3.9-9.1-4.2-14c-0.3-5-0.3-7.4-0.6-14.7c-1.1-30.2-26-47.6-55-40.3c-5,1.3-9,3.4-11.2,6.5v-7.2h-23.8V211h23.8v-52v-6
|
||||
c0-5.4,1.5-9.5,4.3-12.3c2.8-2.7,5.8-4.2,9.5-5.1c3.7-0.9,6.6-1,8.5-1c2.9,0,6.5,0.8,8,1.2c2.8,0.8,6.3,3.3,8.3,6.2
|
||||
c3.7,5.3,3.3,12.8,3.5,18.9c0.2,7.6,0.4,15.3,2.7,22.7c3.2,10.3,9.7,20,19.7,24.6c9,4.1,20,5.2,29.7,3.4c3-0.6,6.3-1.5,9.1-3
|
||||
c1.6-0.9,3.2-1.9,4.5-3.2c0.4,10.7,0.6,17.6-6,22.5c-1.8,1.4-4.3,2.5-6.5,2.9c-3.1,0.6-6.5,1-9.7,0.9L356.9,254.8z" />
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M97.6,89.8V39.2c0-1.6-0.1-3.2-0.2-4.8c-0.1-1.7-0.3-3.3-0.5-4.9h6.6l0.9,10h-1c0.9-3.3,2.7-5.9,5.5-7.9
|
||||
c2.7-1.9,6-2.9,9.8-2.9c3.8,0,7.1,0.9,9.9,2.6c2.8,1.7,4.9,4.2,6.5,7.5c1.6,3.2,2.4,7.2,2.4,11.8c0,4.5-0.8,8.4-2.3,11.7
|
||||
c-1.5,3.2-3.7,5.8-6.5,7.5c-2.8,1.8-6.1,2.6-9.9,2.6c-3.8,0-7-1-9.7-2.9c-2.7-1.9-4.6-4.5-5.5-7.8h0.9v28.1H97.6z M117.3,66.8
|
||||
c4,0,7.2-1.4,9.6-4.2c2.4-2.8,3.6-6.8,3.6-12.1c0-5.4-1.2-9.5-3.6-12.2c-2.4-2.8-5.6-4.2-9.6-4.2c-4,0-7.2,1.4-9.5,4.2
|
||||
c-2.4,2.8-3.6,6.8-3.6,12.2c0,5.3,1.2,9.4,3.6,12.1C110.2,65.4,113.4,66.8,117.3,66.8z" />
|
||||
<path class="st0" d="M165.4,72.4c-4.1,0-7.6-0.9-10.6-2.6c-3-1.8-5.3-4.3-6.9-7.6c-1.6-3.3-2.4-7.2-2.4-11.6
|
||||
c0-4.5,0.8-8.4,2.4-11.7c1.6-3.2,3.9-5.8,6.9-7.5c3-1.8,6.5-2.6,10.5-2.6c4.1,0,7.6,0.9,10.6,2.6c3,1.8,5.3,4.3,7,7.5
|
||||
c1.7,3.2,2.5,7.1,2.5,11.7c0,4.5-0.8,8.4-2.5,11.6c-1.7,3.3-4,5.8-7,7.6C172.9,71.5,169.4,72.4,165.4,72.4z M165.4,66.8
|
||||
c4,0,7.1-1.4,9.5-4.2c2.4-2.8,3.5-6.8,3.5-12.1c0-5.4-1.2-9.5-3.6-12.2c-2.4-2.8-5.6-4.2-9.5-4.2c-4,0-7.1,1.4-9.5,4.2
|
||||
c-2.4,2.8-3.5,6.8-3.5,12.2c0,5.3,1.2,9.4,3.5,12.1C158.2,65.4,161.4,66.8,165.4,66.8z" />
|
||||
<path class="st0" d="M207.2,71.6l-15.6-42.2h7.2l13,37.1h-2.2l13.4-37.1h5.9l13.2,37.1h-2.1l13.1-37.1h6.9l-15.7,42.2h-6.6
|
||||
l-13.6-37.7h3.4l-13.7,37.7H207.2z" />
|
||||
<path class="st0" d="M287.7,72.4c-6.6,0-11.8-1.9-15.6-5.8c-3.8-3.8-5.7-9.2-5.7-16c0-4.4,0.8-8.3,2.5-11.5c1.7-3.3,4-5.8,7.1-7.6
|
||||
c3-1.8,6.5-2.7,10.4-2.7c3.9,0,7.1,0.8,9.7,2.4c2.6,1.6,4.6,3.9,6,6.9c1.4,3,2.1,6.5,2.1,10.6v2.5h-32.7v-4.3h28.2l-1.4,1.1
|
||||
c0-4.5-1-8-3-10.5c-2-2.5-5-3.8-9-3.8c-4.2,0-7.5,1.5-9.8,4.4C274.2,41.1,273,45,273,50v0.8c0,5.3,1.3,9.3,3.9,12
|
||||
c2.6,2.7,6.3,4.1,11,4.1c2.5,0,4.9-0.4,7.1-1.1c2.2-0.8,4.3-2,6.3-3.7l2.4,4.8c-1.8,1.8-4.2,3.2-7,4.2
|
||||
C293.8,71.9,290.8,72.4,287.7,72.4z" />
|
||||
<path class="st0" d="M314.5,71.6v-32c0-1.7,0-3.4-0.1-5.1c-0.1-1.7-0.2-3.4-0.4-5h6.6l0.8,10.2l-1.2,0.1c0.6-2.5,1.5-4.6,2.9-6.2
|
||||
c1.4-1.6,3.1-2.8,5-3.7c1.9-0.8,3.9-1.2,6-1.2c0.8,0,1.6,0,2.2,0.1c0.6,0.1,1.2,0.2,1.8,0.4l-0.1,6c-0.8-0.3-1.6-0.5-2.3-0.5
|
||||
s-1.5-0.1-2.4-0.1c-2.5,0-4.6,0.6-6.4,1.8c-1.8,1.2-3.2,2.7-4.1,4.5s-1.4,3.8-1.4,5.9v24.9H314.5z" />
|
||||
<path class="st0" d="M363.9,72.4c-6.6,0-11.8-1.9-15.6-5.8c-3.8-3.8-5.7-9.2-5.7-16c0-4.4,0.8-8.3,2.5-11.5c1.7-3.3,4-5.8,7.1-7.6
|
||||
c3-1.8,6.5-2.7,10.4-2.7c3.9,0,7.1,0.8,9.7,2.4c2.6,1.6,4.6,3.9,6,6.9c1.4,3,2.1,6.5,2.1,10.6v2.5h-32.7v-4.3H376l-1.4,1.1
|
||||
c0-4.5-1-8-3-10.5c-2-2.5-5-3.8-9-3.8c-4.2,0-7.5,1.5-9.8,4.4c-2.4,2.9-3.5,6.9-3.5,11.9v0.8c0,5.3,1.3,9.3,3.9,12
|
||||
c2.6,2.7,6.3,4.1,11,4.1c2.5,0,4.9-0.4,7.1-1.1c2.2-0.8,4.3-2,6.3-3.7l2.4,4.8c-1.8,1.8-4.2,3.2-7,4.2
|
||||
C370,71.9,367,72.4,363.9,72.4z" />
|
||||
<path class="st0" d="M406.8,72.4c-3.7,0-6.9-0.9-9.7-2.6c-2.8-1.8-5-4.3-6.5-7.5c-1.5-3.2-2.3-7.1-2.3-11.7
|
||||
c0-4.6,0.8-8.5,2.3-11.8c1.5-3.2,3.7-5.7,6.5-7.5c2.8-1.7,6-2.6,9.7-2.6c3.8,0,7.1,1,9.9,2.9c2.8,1.9,4.6,4.5,5.6,7.7h-1V9.8h6.8
|
||||
v61.8h-6.7V61.5h0.9c-0.9,3.4-2.7,6-5.5,7.9C413.9,71.4,410.6,72.4,406.8,72.4z M408.2,66.8c4,0,7.2-1.4,9.6-4.2
|
||||
c2.4-2.8,3.6-6.8,3.6-12.1c0-5.4-1.2-9.5-3.6-12.2c-2.4-2.8-5.6-4.2-9.6-4.2c-4,0-7.2,1.4-9.5,4.2c-2.4,2.8-3.6,6.8-3.6,12.2
|
||||
c0,5.3,1.2,9.4,3.6,12.1C401.1,65.4,404.3,66.8,408.2,66.8z" />
|
||||
<path class="st0" d="M484.3,72.4c-3.8,0-7.1-1-9.8-2.9c-2.7-1.9-4.6-4.6-5.5-7.9h0.9v10.1h-6.7V9.8h6.8v29.5h-1
|
||||
c1-3.2,2.8-5.8,5.5-7.7c2.7-1.9,6-2.9,9.8-2.9c3.8,0,7.1,0.9,9.9,2.6c2.8,1.8,4.9,4.3,6.5,7.5c1.5,3.2,2.3,7.1,2.3,11.7
|
||||
s-0.8,8.4-2.4,11.7c-1.6,3.2-3.7,5.8-6.5,7.5C491.4,71.5,488.1,72.4,484.3,72.4z M482.9,66.8c4,0,7.2-1.4,9.6-4.1
|
||||
c2.4-2.7,3.6-6.8,3.6-12.2s-1.2-9.5-3.6-12.2c-2.4-2.8-5.6-4.2-9.6-4.2c-4,0-7.2,1.4-9.5,4.2c-2.4,2.8-3.6,6.8-3.6,12.2
|
||||
c0,5.3,1.2,9.4,3.6,12.1C475.8,65.4,478.9,66.8,482.9,66.8z" />
|
||||
<path class="st0" d="M511.9,90.7l-1.6-5.6c2.6-0.6,4.8-1.3,6.6-2.1c1.8-0.8,3.2-1.9,4.4-3.2c1.2-1.3,2.2-3,3-5l2.2-5l-0.2,2.9
|
||||
l-18.4-43.1h7.4l15.2,37h-2.2l15-37h7.1L531,75.1c-1.1,2.7-2.4,4.9-3.7,6.8c-1.3,1.8-2.8,3.3-4.3,4.5c-1.5,1.1-3.2,2.1-5.1,2.7
|
||||
S514.1,90.3,511.9,90.7z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -10,19 +10,23 @@ apply plugin: 'androidx.navigation.safeargs.kotlin'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
apply plugin: 'de.timfreiheit.resourceplaceholders'
|
||||
|
||||
def supportedLocales = "en,de,fr,nb-rNO"
|
||||
|
||||
android {
|
||||
compileSdkVersion 32
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 31
|
||||
targetSdkVersion 33
|
||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||
versionCode 104
|
||||
versionName "1.3.9"
|
||||
versionCode 148
|
||||
versionName "1.4.3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs supportedLocales.split(",")
|
||||
buildConfigField("String", "supportedLocales", '"' + supportedLocales + '"')
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -93,6 +97,7 @@ android {
|
||||
}
|
||||
lint {
|
||||
disable 'NullSafeMutableLiveData'
|
||||
warning 'MissingTranslation'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
@@ -102,6 +107,7 @@ android {
|
||||
resourcePlaceholders {
|
||||
files = ['xml/shortcuts.xml']
|
||||
}
|
||||
namespace 'net.vonforst.evmap'
|
||||
|
||||
// add API keys from environment variable if not set in apikeys.xml
|
||||
applicationVariants.all { variant ->
|
||||
@@ -135,6 +141,13 @@ android {
|
||||
if (chargepriceKey != null) {
|
||||
variant.resValue "string", "chargeprice_key", chargepriceKey
|
||||
}
|
||||
def fronyxKey = env.FRONYX_API_KEY ?: project.findProperty("FRONYX_API_KEY")
|
||||
if (fronyxKey == null && project.hasProperty("FRONYX_API_KEY_ENCRYPTED")) {
|
||||
fronyxKey = decode(project.findProperty("FRONYX_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
|
||||
}
|
||||
if (fronyxKey != null) {
|
||||
variant.resValue "string", "fronyx_key", fronyxKey
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -146,29 +159,26 @@ configurations {
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
||||
implementation 'androidx.core:core-ktx:1.8.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||
implementation "androidx.activity:activity-ktx:1.5.1"
|
||||
implementation "androidx.fragment:fragment-ktx:1.5.1"
|
||||
implementation "androidx.activity:activity-ktx:1.6.1"
|
||||
implementation "androidx.fragment:fragment-ktx:1.5.4"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.7.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.browser:browser:1.4.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:dd0167dbff'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f4f641aab5'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0'
|
||||
implementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
|
||||
implementation 'com.squareup.moshi:moshi-adapters:1.13.0'
|
||||
implementation 'com.github.johan12345:jsonapi:50d72e7e55' // patched version for jsonapi-adapters
|
||||
implementation('com.markomilos.jsonapi:jsonapi-retrofit:1.0.1') {
|
||||
exclude group: 'com.markomilos.jsonapi', module: 'jsonapi-adapters'
|
||||
}
|
||||
implementation 'com.markomilos.jsonapi:jsonapi-retrofit:1.1.0'
|
||||
implementation 'io.coil-kt:coil:1.1.0'
|
||||
implementation 'com.github.johan12345:StfalconImageViewer:5082ebd392'
|
||||
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
|
||||
@@ -180,7 +190,7 @@ dependencies {
|
||||
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
|
||||
|
||||
// Android Auto
|
||||
def carAppVersion = '1.2.0-rc01'
|
||||
def carAppVersion = '1.3.0-beta01'
|
||||
googleImplementation "androidx.car.app:app:$carAppVersion"
|
||||
googleNormalImplementation "androidx.car.app:app-projected:$carAppVersion"
|
||||
googleAutomotiveImplementation "androidx.car.app:app-automotive:$carAppVersion"
|
||||
@@ -200,7 +210,7 @@ dependencies {
|
||||
implementation 'com.github.johan12345:mapbox-events-android:a21c324501'
|
||||
|
||||
// Google Places
|
||||
googleImplementation 'com.google.android.libraries.places:places:2.6.0'
|
||||
googleImplementation 'com.google.android.libraries.places:places:2.7.0'
|
||||
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.1'
|
||||
|
||||
// Mapbox Geocoding
|
||||
@@ -244,15 +254,15 @@ dependencies {
|
||||
|
||||
// testing for car app
|
||||
testGoogleImplementation "androidx.car.app:app-testing:$carAppVersion"
|
||||
testGoogleImplementation 'org.robolectric:robolectric:4.8.1'
|
||||
testGoogleImplementation 'androidx.test:core:1.4.0'
|
||||
testGoogleImplementation 'org.robolectric:robolectric:4.9'
|
||||
testGoogleImplementation 'androidx.test:core:1.5.0'
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
|
||||
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.13.0"
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
|
||||
}
|
||||
|
||||
private static String decode(String s, String key) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="net.vonforst.evmap">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
|
||||
|
||||
@@ -7,16 +7,10 @@ import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.gms.maps.MapsInitializer
|
||||
import com.google.android.libraries.places.api.Places
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.utils.LocaleContextWrapper
|
||||
|
||||
fun init(context: Context) {
|
||||
Places.initialize(context, context.getString(R.string.google_maps_key))
|
||||
|
||||
val localeContext = LocaleContextWrapper.wrap(
|
||||
context.applicationContext, PreferenceDataSource(context).language
|
||||
)
|
||||
MapsInitializer.initialize(localeContext, MapsInitializer.Renderer.LATEST, null)
|
||||
MapsInitializer.initialize(context, MapsInitializer.Renderer.LATEST, null)
|
||||
}
|
||||
|
||||
fun checkPlayServices(activity: Activity): Boolean {
|
||||
|
||||
@@ -31,6 +31,7 @@ import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.location.FusionEngine
|
||||
import net.vonforst.evmap.location.LocationEngine
|
||||
import net.vonforst.evmap.location.Priority
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.utils.checkFineLocationPermission
|
||||
|
||||
|
||||
@@ -111,27 +112,49 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
|
||||
carContext.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
}
|
||||
|
||||
private val prefs: PreferenceDataSource by lazy {
|
||||
PreferenceDataSource(carContext)
|
||||
}
|
||||
|
||||
init {
|
||||
lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
override fun onCreateScreen(intent: Intent): Screen {
|
||||
val mapScreen = MapScreen(carContext, this)
|
||||
val screens = mutableListOf<Screen>(mapScreen)
|
||||
|
||||
if (!prefs.dataSourceSet) {
|
||||
screens.add(
|
||||
ChooseDataSourceScreen(
|
||||
carContext,
|
||||
ChooseDataSourceScreen.Type.CHARGER_DATA_SOURCE,
|
||||
initialChoice = true,
|
||||
extraDesc = R.string.data_sources_description
|
||||
)
|
||||
)
|
||||
}
|
||||
if (!locationPermissionGranted()) {
|
||||
val screenManager = carContext.getCarService(ScreenManager::class.java)
|
||||
screenManager.push(mapScreen)
|
||||
return PermissionScreen(
|
||||
carContext,
|
||||
R.string.auto_location_permission_needed,
|
||||
listOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
screens.add(
|
||||
PermissionScreen(
|
||||
carContext,
|
||||
R.string.auto_location_permission_needed,
|
||||
listOf(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return mapScreen
|
||||
if (screens.size > 1) {
|
||||
val screenManager = carContext.getCarService(ScreenManager::class.java)
|
||||
for (i in 0 until screens.size - 1) {
|
||||
screenManager.push(screens[i])
|
||||
}
|
||||
}
|
||||
|
||||
return screens.last()
|
||||
}
|
||||
|
||||
private fun locationPermissionGranted() = carContext.checkFineLocationPermission()
|
||||
|
||||
@@ -22,13 +22,20 @@ import jsonapi.ResourceIdentifier
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.chargeprice.*
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.currency
|
||||
import net.vonforst.evmap.ui.time
|
||||
import java.io.IOException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(ctx) {
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
@@ -41,6 +48,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
}
|
||||
private var prices: List<ChargePrice>? = null
|
||||
private var meta: ChargepriceChargepointMeta? = null
|
||||
private var chargepoint: Chargepoint? = null
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
@@ -62,17 +70,52 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
if (prices == null && errorMessage == null) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
setSingleList(ItemList.Builder().apply {
|
||||
setNoItemsMessage(
|
||||
errorMessage ?: carContext.getString(R.string.chargeprice_no_tariffs_found)
|
||||
)
|
||||
prices?.take(maxRows)?.forEach { price ->
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(formatProvider(price))
|
||||
addText(formatPrice(price))
|
||||
}.build())
|
||||
val header = meta?.let { meta ->
|
||||
chargepoint?.let { chargepoint ->
|
||||
"${
|
||||
nameForPlugType(
|
||||
carContext.stringProvider(),
|
||||
chargepoint.type
|
||||
)
|
||||
} ${chargepoint.formatPower()} ${
|
||||
carContext.getString(
|
||||
R.string.chargeprice_stats,
|
||||
meta.energy,
|
||||
time(meta.duration.roundToInt()),
|
||||
meta.energy / meta.duration * 60
|
||||
)
|
||||
}"
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
val myTariffs = prefs.chargepriceMyTariffs
|
||||
val myTariffsAll = prefs.chargepriceMyTariffsAll
|
||||
|
||||
val prices = prices?.take(maxRows)
|
||||
if (prices != null && prices.isNotEmpty() && !myTariffsAll && myTariffs != null) {
|
||||
val (myPrices, otherPrices) = prices.partition { price -> price.tariffId in myTariffs }
|
||||
val myPricesList = buildPricesList(myPrices)
|
||||
val otherPricesList = buildPricesList(otherPrices)
|
||||
addSectionedList(
|
||||
SectionedItemList.create(
|
||||
myPricesList,
|
||||
(header?.let { it + "\n" } ?: "") +
|
||||
carContext.getString(R.string.chargeprice_header_my_tariffs)
|
||||
)
|
||||
)
|
||||
addSectionedList(
|
||||
SectionedItemList.create(
|
||||
otherPricesList,
|
||||
carContext.getString(R.string.chargeprice_header_other_tariffs)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
val list = buildPricesList(prices)
|
||||
if (header != null) {
|
||||
addSectionedList(SectionedItemList.create(list, header))
|
||||
} else {
|
||||
setSingleList(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
setActionStrip(
|
||||
ActionStrip.Builder().addAction(
|
||||
@@ -101,11 +144,14 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
carContext.startActivity(intent)
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
if (BuildConfig.FLAVOR_automotive != "automotive") {
|
||||
// only show the toast "opened on phone" if we're running on a phone
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
@@ -119,6 +165,21 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun buildPricesList(prices: List<ChargePrice>?): ItemList {
|
||||
return ItemList.Builder().apply {
|
||||
setNoItemsMessage(
|
||||
errorMessage
|
||||
?: carContext.getString(R.string.chargeprice_no_tariffs_found)
|
||||
)
|
||||
prices?.forEach { price ->
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(formatProvider(price))
|
||||
addText(formatPrice(price))
|
||||
}.build())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun formatProvider(price: ChargePrice): String {
|
||||
if (!price.tariffName.startsWith(price.provider)) {
|
||||
return price.provider + " " + price.tariffName
|
||||
@@ -128,19 +189,21 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
}
|
||||
|
||||
private fun formatPrice(price: ChargePrice): String {
|
||||
val amount = price.chargepointPrices.first().price
|
||||
?: return "${carContext.getString(R.string.chargeprice_price_not_available)} (${price.chargepointPrices.first().noPriceReason})"
|
||||
val totalPrice = carContext.getString(
|
||||
R.string.charge_price_format,
|
||||
price.chargepointPrices.first().price,
|
||||
amount,
|
||||
currency(price.currency)
|
||||
)
|
||||
val kwhPrice = if (price.chargepointPrices.first().price > 0f) {
|
||||
val kwhPrice = if (amount > 0f) {
|
||||
carContext.getString(
|
||||
if (price.chargepointPrices[0].priceDistribution.isOnlyKwh) {
|
||||
R.string.charge_price_kwh_format
|
||||
} else {
|
||||
R.string.charge_price_average_format
|
||||
},
|
||||
price.chargepointPrices.get(0).price / meta!!.energy,
|
||||
amount / meta!!.energy,
|
||||
currency(price.currency)
|
||||
)
|
||||
} else null
|
||||
@@ -179,6 +242,14 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
try {
|
||||
val car = determineVehicle(manufacturer, modelName)
|
||||
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
|
||||
|
||||
if (cpStation.chargePoints.isEmpty()) {
|
||||
errorMessage =
|
||||
carContext.getString(R.string.chargeprice_no_compatible_connectors)
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
|
||||
val result = api.getChargePrices(
|
||||
ChargepriceRequest(
|
||||
dataAdapter = dataAdapter,
|
||||
@@ -189,7 +260,8 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
|
||||
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
|
||||
currency = prefs.chargepriceCurrency,
|
||||
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad
|
||||
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad,
|
||||
showPriceUnavailable = true
|
||||
),
|
||||
relationships = if (!prefs.chargepriceMyTariffsAll) {
|
||||
val myTariffs = prefs.chargepriceMyTariffs ?: emptySet()
|
||||
@@ -213,10 +285,14 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
|
||||
val myTariffs = prefs.chargepriceMyTariffs
|
||||
|
||||
// choose the highest power chargepoint compatible with the car
|
||||
val chargepoint = cpStation.chargePoints.filterIndexed { i, cp ->
|
||||
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
|
||||
}.maxByOrNull { it.power }
|
||||
// choose the highest power chargepoint
|
||||
// (we have already filtered so that only compatible ones are included)
|
||||
val chargepoint = cpStation.chargePoints.maxByOrNull { it.power }
|
||||
|
||||
val index = cpStation.chargePoints.indexOf(chargepoint)
|
||||
this@ChargepriceScreen.chargepoint =
|
||||
charger.chargepoints.filter { equivalentPlugTypes(it.type).any { it in car.compatibleEvmapConnectors } }[index]
|
||||
|
||||
if (chargepoint == null) {
|
||||
errorMessage =
|
||||
carContext.getString(R.string.chargeprice_no_compatible_connectors)
|
||||
@@ -226,11 +302,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
|
||||
val metaMapped =
|
||||
result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!!
|
||||
meta = metaMapped.chargePoints.filterIndexed { i, cp ->
|
||||
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
|
||||
}.maxByOrNull {
|
||||
it.power
|
||||
}
|
||||
meta = metaMapped.chargePoints.maxByOrNull { it.power }
|
||||
|
||||
prices = result.data!!.map { cp ->
|
||||
val filteredPrices =
|
||||
@@ -245,7 +317,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
)
|
||||
}
|
||||
}.filterNotNull()
|
||||
.sortedBy { it.chargepointPrices.first().price }
|
||||
.sortedBy { it.chargepointPrices.first().price ?: Double.MAX_VALUE }
|
||||
.sortedByDescending {
|
||||
prefs.chargepriceMyTariffsAll ||
|
||||
myTariffs != null && it.tariffId in myTariffs
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.net.Uri
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.car.app.CarContext
|
||||
@@ -27,22 +27,27 @@ import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.iconForPlugType
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Cost
|
||||
import net.vonforst.evmap.model.FaultReport
|
||||
import net.vonforst.evmap.model.Favorite
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||
import net.vonforst.evmap.ui.availabilityText
|
||||
import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import net.vonforst.evmap.viewmodel.getReferenceData
|
||||
import net.vonforst.evmap.viewmodel.awaitFinished
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : Screen(ctx) {
|
||||
var charger: ChargeLocation? = null
|
||||
var photo: Bitmap? = null
|
||||
@@ -50,10 +55,8 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val api by lazy {
|
||||
createApi(prefs.dataSource, ctx)
|
||||
}
|
||||
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
|
||||
private val repo =
|
||||
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
||||
|
||||
private val imageSize = 128 // images should be 128dp according to docs
|
||||
private val imageSizeLarge = 480 // images should be 480 x 480 dp according to docs
|
||||
@@ -71,9 +74,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
private var favoriteUpdateJob: Job? = null
|
||||
|
||||
init {
|
||||
referenceData.observe(this) {
|
||||
loadCharger()
|
||||
}
|
||||
loadCharger()
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
@@ -99,29 +100,29 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
).build()
|
||||
)
|
||||
.setTitle(carContext.getString(R.string.navigate))
|
||||
.setFlags(Action.FLAG_PRIMARY)
|
||||
.setBackgroundColor(CarColor.PRIMARY)
|
||||
.setOnClickListener {
|
||||
navigateToCharger(charger)
|
||||
}
|
||||
.build())
|
||||
charger.chargepriceData?.country?.let { country ->
|
||||
if (ChargepriceApi.isCountrySupported(country, charger.dataSource)) {
|
||||
addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_chargeprice
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setTitle(carContext.getString(R.string.auto_prices))
|
||||
if (ChargepriceApi.isChargerSupported(charger)) {
|
||||
addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_chargeprice
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setTitle(carContext.getString(R.string.auto_prices))
|
||||
.setOnClickListener {
|
||||
screenManager.push(ChargepriceScreen(carContext, charger))
|
||||
}
|
||||
.build())
|
||||
}
|
||||
}
|
||||
} ?: setLoading(true)
|
||||
}.build()
|
||||
).apply {
|
||||
@@ -244,15 +245,9 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
val operatorText = generateOperatorText(charger)
|
||||
setTitle(operatorText)
|
||||
|
||||
charger.cost?.let { addText(it.getStatusText(carContext, emoji = true)) }
|
||||
charger.cost?.let { addText(generateCostStatusText(it)) }
|
||||
charger.faultReport?.let { fault ->
|
||||
addText(
|
||||
carContext.getString(
|
||||
R.string.auto_fault_report_date,
|
||||
fault.created?.atZone(ZoneId.systemDefault())
|
||||
?.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
|
||||
)
|
||||
)
|
||||
addText(generateFaultReportTitle(fault))
|
||||
}
|
||||
}.build())
|
||||
} else {
|
||||
@@ -267,20 +262,14 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
val operatorText = generateOperatorText(charger)
|
||||
setTitle(operatorText)
|
||||
charger.cost?.let {
|
||||
addText(it.getStatusText(carContext, emoji = true))
|
||||
addText(generateCostStatusText(it))
|
||||
it.getDetailText()?.let { addText(it) }
|
||||
}
|
||||
}.build())
|
||||
// row 3: fault report (if exists)
|
||||
charger.faultReport?.let { fault ->
|
||||
rows.add(Row.Builder().apply {
|
||||
setTitle(
|
||||
carContext.getString(
|
||||
R.string.auto_fault_report_date,
|
||||
fault.created?.atZone(ZoneId.systemDefault())
|
||||
?.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
|
||||
)
|
||||
)
|
||||
setTitle(generateFaultReportTitle(fault))
|
||||
fault.description?.let {
|
||||
addText(
|
||||
HtmlCompat.fromHtml(
|
||||
@@ -305,18 +294,79 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
return rows
|
||||
}
|
||||
|
||||
private fun generateCostStatusText(cost: Cost): CharSequence {
|
||||
val string = SpannableString(cost.getStatusText(carContext, emoji = true))
|
||||
// replace emoji with CarIcon
|
||||
string.indexOf('⚡').takeIf { it >= 0 }?.let { index ->
|
||||
string.setSpan(
|
||||
CarIconSpan.create(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_lightning
|
||||
)
|
||||
).setTint(CarColor.YELLOW).build()
|
||||
), index, index + 1, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
string.indexOf('\uD83C').takeIf { it >= 0 }?.let { index ->
|
||||
string.setSpan(
|
||||
CarIconSpan.create(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_parking
|
||||
)
|
||||
).setTint(CarColor.BLUE).build()
|
||||
), index, index + 2, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
|
||||
private fun generateFaultReportTitle(fault: FaultReport): CharSequence {
|
||||
val string = SpannableString(
|
||||
carContext.getString(
|
||||
R.string.auto_fault_report_date,
|
||||
fault.created?.atZone(ZoneId.systemDefault())
|
||||
?.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
|
||||
)
|
||||
)
|
||||
// replace emoji with CarIcon
|
||||
string.setSpan(
|
||||
CarIconSpan.create(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_fault_report
|
||||
)
|
||||
).setTint(CarColor.YELLOW).build()
|
||||
), 0, 1, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
return string
|
||||
}
|
||||
|
||||
private fun generateChargepointsText(charger: ChargeLocation): SpannableStringBuilder {
|
||||
val chargepointsText = SpannableStringBuilder()
|
||||
charger.chargepointsMerged.forEachIndexed { i, cp ->
|
||||
if (i > 0) chargepointsText.append(" · ")
|
||||
chargepointsText.append(
|
||||
"${cp.count}× ${
|
||||
nameForPlugType(
|
||||
carContext.stringProvider(),
|
||||
cp.type
|
||||
)
|
||||
} ${cp.formatPower()}"
|
||||
)
|
||||
"${cp.count}× "
|
||||
).append(
|
||||
nameForPlugType(carContext.stringProvider(), cp.type),
|
||||
CarIconSpan.create(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
iconForPlugType(cp.type)
|
||||
)
|
||||
).setTint(
|
||||
CarColor.createCustom(Color.WHITE, Color.BLACK)
|
||||
).build()
|
||||
),
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
).append(" ").append(cp.formatPower())
|
||||
availability?.status?.get(cp)?.let { status ->
|
||||
chargepointsText.append(
|
||||
" (${availabilityText(status)}/${cp.count})",
|
||||
@@ -356,24 +406,23 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
}
|
||||
|
||||
private fun loadCharger() {
|
||||
val referenceData = referenceData.value ?: return
|
||||
lifecycleScope.launch {
|
||||
favorite = db.favoritesDao().findFavorite(chargerSparse.id, chargerSparse.dataSource)
|
||||
|
||||
val response = api.getChargepointDetail(referenceData, chargerSparse.id)
|
||||
val response = repo.getChargepointDetail(chargerSparse.id).awaitFinished()
|
||||
if (response.status == Status.SUCCESS) {
|
||||
val charger = response.data!!
|
||||
this@ChargerDetailScreen.charger = charger
|
||||
invalidate()
|
||||
|
||||
val photo = charger.photos?.firstOrNull()
|
||||
photo?.let {
|
||||
val density = carContext.resources.displayMetrics.density
|
||||
val url = if (largeImageSupported) {
|
||||
photo.getUrl(size = (imageSizeLarge * density).roundToInt())
|
||||
} else {
|
||||
photo.getUrl(size = (imageSize * density).roundToInt())
|
||||
}
|
||||
val size =
|
||||
(density * if (largeImageSupported) imageSizeLarge else imageSize).roundToInt()
|
||||
val url = photo.getUrl(size = size)
|
||||
val request = ImageRequest.Builder(carContext).data(url).build()
|
||||
var img =
|
||||
val img =
|
||||
(carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap
|
||||
|
||||
// draw icon on top of image
|
||||
@@ -383,21 +432,32 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
multi = charger.isMulti()
|
||||
)
|
||||
|
||||
img = img.copy(Bitmap.Config.ARGB_8888, true)
|
||||
val outImg = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
||||
val iconSmall = icon.scale(
|
||||
(img.height * 0.4 / icon.height * icon.width).roundToInt(),
|
||||
(img.height * 0.4).roundToInt()
|
||||
(size * 0.4 / icon.height * icon.width).roundToInt(),
|
||||
(size * 0.4).roundToInt()
|
||||
)
|
||||
val canvas = Canvas(outImg)
|
||||
|
||||
val m = Matrix()
|
||||
m.setRectToRect(
|
||||
RectF(0f, 0f, img.width.toFloat(), img.height.toFloat()),
|
||||
RectF(0f, 0f, size.toFloat(), size.toFloat()),
|
||||
Matrix.ScaleToFit.CENTER
|
||||
)
|
||||
canvas.drawBitmap(
|
||||
img.copy(Bitmap.Config.ARGB_8888, false), m, null
|
||||
)
|
||||
val canvas = Canvas(img)
|
||||
canvas.drawBitmap(
|
||||
iconSmall,
|
||||
0f,
|
||||
(img.height - iconSmall.height * 1.1).toFloat(),
|
||||
(size - iconSmall.height * 1.1).toFloat(),
|
||||
null
|
||||
)
|
||||
this@ChargerDetailScreen.photo = img
|
||||
this@ChargerDetailScreen.photo = outImg
|
||||
}
|
||||
this@ChargerDetailScreen.charger = charger
|
||||
|
||||
invalidate()
|
||||
|
||||
availability = getAvailability(charger).data
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.car.app.CarContext
|
||||
@@ -11,6 +13,7 @@ import androidx.car.app.model.*
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.map
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.model.*
|
||||
@@ -24,15 +27,24 @@ import kotlin.math.roundToInt
|
||||
class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(ctx)
|
||||
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
|
||||
private val filterProfiles: LiveData<List<FilterProfile>> by lazy {
|
||||
db.filterProfileDao().getProfiles(prefs.dataSource)
|
||||
}
|
||||
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
|
||||
private var page = 0
|
||||
|
||||
init {
|
||||
filterProfiles.observe(this) {
|
||||
val filterStatus = prefs.filterStatus
|
||||
if (filterStatus in listOf(FILTERS_DISABLED, FILTERS_FAVORITES, FILTERS_CUSTOM)) {
|
||||
page = 0
|
||||
} else {
|
||||
page = paginateProfiles(it).indexOfFirst { it.any { it.id == filterStatus } }
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
@@ -40,37 +52,27 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
override fun onGetTemplate(): Template {
|
||||
val filterStatus = prefs.filterStatus
|
||||
return ListTemplate.Builder().apply {
|
||||
var title = carContext.getString(R.string.menu_filter)
|
||||
|
||||
filterProfiles.value?.let {
|
||||
setSingleList(buildFilterProfilesList(it, filterStatus))
|
||||
val paginatedProfiles = paginateProfiles(it)
|
||||
setSingleList(buildFilterProfilesList(paginatedProfiles, filterStatus))
|
||||
|
||||
val numPages = paginatedProfiles.size
|
||||
if (numPages > 1) {
|
||||
title += " " + carContext.getString(
|
||||
R.string.auto_multipage,
|
||||
page + 1,
|
||||
numPages
|
||||
)
|
||||
}
|
||||
} ?: setLoading(true)
|
||||
setTitle(carContext.getString(R.string.menu_filter))
|
||||
|
||||
setTitle(title)
|
||||
|
||||
setHeaderAction(Action.BACK)
|
||||
setActionStrip(
|
||||
ActionStrip.Builder().apply {
|
||||
addAction(Action.Builder().apply {
|
||||
setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
||||
R.drawable.ic_search_off
|
||||
} else {
|
||||
R.drawable.ic_search
|
||||
}
|
||||
)
|
||||
).build()
|
||||
|
||||
)
|
||||
setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
||||
prefs.placeSearchResultAndroidAutoName = null
|
||||
prefs.placeSearchResultAndroidAuto = null
|
||||
screenManager.pop()
|
||||
} else {
|
||||
screenManager.push(PlaceSearchScreen(carContext, session))
|
||||
}
|
||||
})
|
||||
}.build())
|
||||
addAction(Action.Builder().apply {
|
||||
setIcon(
|
||||
CarIcon.Builder(
|
||||
@@ -79,7 +81,6 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
R.drawable.ic_edit
|
||||
)
|
||||
).build()
|
||||
|
||||
)
|
||||
setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
lifecycleScope.launch {
|
||||
@@ -94,47 +95,140 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun paginateProfiles(filterProfiles: List<FilterProfile>): List<List<FilterProfile>> {
|
||||
val filterStatus = prefs.filterStatus
|
||||
val extraRows = if (FILTERS_CUSTOM == filterStatus) 3 else 2
|
||||
return filterProfiles.paginate(
|
||||
maxRows - extraRows,
|
||||
maxRows - extraRows - 1,
|
||||
maxRows - 2,
|
||||
maxRows - 1
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildFilterProfilesList(
|
||||
profiles: List<FilterProfile>,
|
||||
paginatedProfiles: List<List<FilterProfile>>,
|
||||
filterStatus: Long
|
||||
): ItemList {
|
||||
val extraRows = if (FILTERS_CUSTOM == filterStatus) 3 else 2
|
||||
val profilesToShow =
|
||||
profiles.sortedByDescending { it.id == filterStatus }.take(maxRows - extraRows)
|
||||
return ItemList.Builder().apply {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.no_filters))
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.filter_favorites))
|
||||
}.build())
|
||||
profilesToShow.forEach {
|
||||
if (page > 0) {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(
|
||||
CarText.Builder(
|
||||
carContext.getString(R.string.auto_multipage_goto, page)
|
||||
).build()
|
||||
)
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_arrow_back
|
||||
)
|
||||
).build(),
|
||||
Row.IMAGE_TYPE_ICON
|
||||
)
|
||||
setOnClickListener {
|
||||
page -= 1
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
|
||||
if (page == 0) {
|
||||
addItem(Row.Builder().apply {
|
||||
val active = filterStatus == FILTERS_DISABLED
|
||||
setTitle(carContext.getString(R.string.no_filters))
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_close
|
||||
)
|
||||
).setTint(if (active) CarColor.SECONDARY else CarColor.DEFAULT)
|
||||
.build(),
|
||||
Row.IMAGE_TYPE_ICON
|
||||
)
|
||||
setOnClickListener { onItemClick(FILTERS_DISABLED) }
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
val active = filterStatus == FILTERS_FAVORITES
|
||||
setTitle(carContext.getString(R.string.filter_favorites))
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_fav
|
||||
)
|
||||
).setTint(if (active) CarColor.SECONDARY else CarColor.DEFAULT)
|
||||
.build(),
|
||||
Row.IMAGE_TYPE_ICON
|
||||
)
|
||||
setOnClickListener { onItemClick(FILTERS_FAVORITES) }
|
||||
}.build())
|
||||
if (FILTERS_CUSTOM == filterStatus) {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.filter_custom))
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_checkbox_checked
|
||||
)
|
||||
).setTint(CarColor.PRIMARY).build(),
|
||||
Row.IMAGE_TYPE_ICON
|
||||
)
|
||||
setOnClickListener { onItemClick(FILTERS_CUSTOM) }
|
||||
}.build())
|
||||
}
|
||||
}
|
||||
paginatedProfiles[page].forEach {
|
||||
addItem(Row.Builder().apply {
|
||||
val name =
|
||||
it.name.ifEmpty { carContext.getString(R.string.unnamed_filter_profile) }
|
||||
val active = filterStatus == it.id
|
||||
setTitle(name)
|
||||
setImage(
|
||||
if (active)
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_check
|
||||
)
|
||||
).setTint(CarColor.SECONDARY).build() else emptyCarIcon,
|
||||
Row.IMAGE_TYPE_ICON
|
||||
)
|
||||
setOnClickListener { onItemClick(it.id) }
|
||||
}.build())
|
||||
}
|
||||
if (FILTERS_CUSTOM == filterStatus) {
|
||||
if (page < paginatedProfiles.size - 1) {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.filter_custom))
|
||||
}.build())
|
||||
}
|
||||
setSelectedIndex(when (filterStatus) {
|
||||
FILTERS_DISABLED -> 0
|
||||
FILTERS_FAVORITES -> 1
|
||||
FILTERS_CUSTOM -> profilesToShow.size + 2
|
||||
else -> profilesToShow.indexOfFirst { it.id == filterStatus } + 2
|
||||
})
|
||||
setOnSelectedListener { index ->
|
||||
onItemClick(
|
||||
when (index) {
|
||||
0 -> FILTERS_DISABLED
|
||||
1 -> FILTERS_FAVORITES
|
||||
profilesToShow.size + 2 -> FILTERS_CUSTOM
|
||||
else -> profilesToShow[index - 2].id
|
||||
setTitle(
|
||||
CarText.Builder(
|
||||
carContext.getString(R.string.auto_multipage_goto, page + 2)
|
||||
).build()
|
||||
)
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_arrow_forward
|
||||
)
|
||||
).build(),
|
||||
Row.IMAGE_TYPE_ICON
|
||||
)
|
||||
setOnClickListener {
|
||||
page += 1
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}.build())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
@@ -153,8 +247,13 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
|
||||
private var page = 0
|
||||
private var paginatedFilters = vm.filtersWithValue.map {
|
||||
it?.paginate(maxRows, maxRows - 1, maxRows - 2, maxRows - 1)
|
||||
}
|
||||
|
||||
init {
|
||||
vm.filtersWithValue.observe(this) {
|
||||
paginatedFilters.observe(this) {
|
||||
vm.filterProfile.observe(this) {
|
||||
invalidate()
|
||||
}
|
||||
@@ -165,18 +264,28 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val currentProfileName = vm.filterProfile.value?.name
|
||||
|
||||
return ListTemplate.Builder().apply {
|
||||
vm.filtersWithValue.value?.let { filtersWithValue ->
|
||||
setSingleList(buildFiltersList(filtersWithValue.take(maxRows)))
|
||||
paginatedFilters.value?.let { paginatedFilters ->
|
||||
setSingleList(buildFiltersList(paginatedFilters))
|
||||
} ?: setLoading(true)
|
||||
|
||||
setTitle(currentProfileName?.let {
|
||||
var title = currentProfileName?.let {
|
||||
carContext.getString(
|
||||
R.string.edit_filter_profile,
|
||||
it
|
||||
it,
|
||||
)
|
||||
} ?: carContext.getString(R.string.menu_filter))
|
||||
} ?: carContext.getString(R.string.menu_filter)
|
||||
val numPages = paginatedFilters.value?.size ?: 0
|
||||
if (numPages > 1) {
|
||||
title += " " + carContext.getString(
|
||||
R.string.auto_multipage,
|
||||
page + 1,
|
||||
numPages
|
||||
)
|
||||
}
|
||||
setTitle(title)
|
||||
|
||||
setHeaderAction(Action.BACK)
|
||||
|
||||
setActionStrip(ActionStrip.Builder().apply {
|
||||
val currentProfile = vm.filterProfile.value
|
||||
if (currentProfile != null) {
|
||||
@@ -218,29 +327,61 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
).build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
val textPromptScreen = TextPromptScreen(
|
||||
carContext,
|
||||
R.string.save_as_profile,
|
||||
R.string.save_profile_enter_name,
|
||||
currentProfileName
|
||||
)
|
||||
screenManager.pushForResult(textPromptScreen) { name ->
|
||||
if (name == null) return@pushForResult
|
||||
lifecycleScope.launch {
|
||||
vm.saveAsProfile(name as String)
|
||||
screenManager.popTo(MapScreen.MARKER)
|
||||
val textPromptScreen = TextPromptScreen(
|
||||
carContext,
|
||||
R.string.save_as_profile,
|
||||
R.string.save_profile_enter_name,
|
||||
currentProfileName
|
||||
)
|
||||
screenManager.pushForResult(textPromptScreen) { name ->
|
||||
if (name == null) return@pushForResult
|
||||
var saveSuccess = false
|
||||
lifecycleScope.launch {
|
||||
saveSuccess = vm.saveAsProfile(name as String)
|
||||
screenManager.popTo(MapScreen.MARKER)
|
||||
}
|
||||
if (!saveSuccess) return@pushForResult
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
.build()
|
||||
.build()
|
||||
)
|
||||
}.build())
|
||||
}
|
||||
.build())
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun buildFiltersList(filters: List<FilterWithValue<out FilterValue>>): ItemList {
|
||||
private fun buildFiltersList(paginatedFilters: List<FilterValues>): ItemList {
|
||||
|
||||
return ItemList.Builder().apply {
|
||||
filters.forEach {
|
||||
if (page > 0) {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(
|
||||
CarText.Builder(
|
||||
carContext.getString(R.string.auto_multipage_goto, page)
|
||||
).build()
|
||||
)
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_arrow_back
|
||||
)
|
||||
).build(),
|
||||
Row.IMAGE_TYPE_ICON
|
||||
)
|
||||
setOnClickListener {
|
||||
page -= 1
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
|
||||
paginatedFilters[page].forEach {
|
||||
val filter = it.filter
|
||||
val value = it.value
|
||||
addItem(Row.Builder().apply {
|
||||
@@ -294,6 +435,33 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
|
||||
if (page < paginatedFilters.size - 1) {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(
|
||||
CarText.Builder(
|
||||
carContext.getString(R.string.auto_multipage_goto, page + 2)
|
||||
).build()
|
||||
)
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_arrow_forward
|
||||
)
|
||||
).build(),
|
||||
Row.IMAGE_TYPE_ICON
|
||||
)
|
||||
setOnClickListener {
|
||||
page += 1
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,24 @@ package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.info.CarInfo
|
||||
import androidx.car.app.hardware.info.CarSensors
|
||||
import androidx.car.app.hardware.info.Compass
|
||||
import androidx.car.app.hardware.info.EnergyLevel
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.*
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.car2go.maps.model.LatLng
|
||||
import kotlinx.coroutines.*
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
@@ -24,19 +30,25 @@ import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.FILTERS_FAVORITES
|
||||
import net.vonforst.evmap.model.FilterValue
|
||||
import net.vonforst.evmap.model.FilterWithValue
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.availabilityText
|
||||
import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.utils.bearingBetween
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import net.vonforst.evmap.utils.headingDiff
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import net.vonforst.evmap.viewmodel.awaitFinished
|
||||
import net.vonforst.evmap.viewmodel.filtersWithValue
|
||||
import net.vonforst.evmap.viewmodel.getFilterValues
|
||||
import net.vonforst.evmap.viewmodel.getReferenceData
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.collections.set
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -60,33 +72,33 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
private var location: Location? = null
|
||||
private var lastDistanceUpdateTime: Instant? = null
|
||||
private var chargers: List<ChargeLocation>? = null
|
||||
private var loadingError = false
|
||||
private var locationError = false
|
||||
private var prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val api by lazy {
|
||||
createApi(prefs.dataSource, ctx)
|
||||
}
|
||||
private val repo =
|
||||
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
||||
private val searchRadius = 5 // kilometers
|
||||
private val distanceUpdateThreshold = Duration.ofSeconds(15)
|
||||
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
|
||||
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus?>> =
|
||||
HashMap()
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST)
|
||||
min(
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST),
|
||||
25
|
||||
)
|
||||
} else 6
|
||||
|
||||
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
|
||||
private val filterStatus = MutableLiveData<Long>().apply {
|
||||
value = prefs.filterStatus
|
||||
}
|
||||
private val filterValues = db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
|
||||
private val filters =
|
||||
Transformations.map(referenceData) { api.getFilters(it, carContext.stringProvider()) }
|
||||
private val filtersWithValue = filtersWithValue(filters, filterValues)
|
||||
private var filterStatus = prefs.filterStatus
|
||||
private var filtersWithValue: List<FilterWithValue<FilterValue>>? = null
|
||||
|
||||
private val hardwareMan: CarHardwareManager by lazy {
|
||||
ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
private val carInfo: CarInfo by lazy {
|
||||
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
|
||||
}
|
||||
private val carSensors: CarSensors by lazy { carContext.patchedCarSensors }
|
||||
private var energyLevel: EnergyLevel? = null
|
||||
private var heading: Compass? = null
|
||||
private val permissions = if (BuildConfig.FLAVOR_automotive == "automotive") {
|
||||
listOf(
|
||||
"android.car.permission.CAR_ENERGY",
|
||||
@@ -107,32 +119,36 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
session.requestLocationUpdates()
|
||||
|
||||
session.mapScreen = this
|
||||
return PlaceListMapTemplate.Builder().apply {
|
||||
setTitle(
|
||||
prefs.placeSearchResultAndroidAutoName?.let {
|
||||
carContext.getString(R.string.auto_chargers_near_location, it)
|
||||
} ?: carContext.getString(
|
||||
if (filterStatus.value == FILTERS_FAVORITES) {
|
||||
if (filterStatus == FILTERS_FAVORITES) {
|
||||
R.string.auto_favorites
|
||||
} else {
|
||||
R.string.auto_chargers_closeby
|
||||
}
|
||||
)
|
||||
)
|
||||
searchLocation?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).apply {
|
||||
if (prefs.placeSearchResultAndroidAutoName != null) {
|
||||
setMarker(
|
||||
PlaceMarker.Builder()
|
||||
.setColor(CarColor.PRIMARY)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
} ?: setLoading(true)
|
||||
if (prefs.placeSearchResultAndroidAutoName != null) {
|
||||
searchLocation?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).apply {
|
||||
if (prefs.placeSearchResultAndroidAutoName != null) {
|
||||
setMarker(
|
||||
PlaceMarker.Builder()
|
||||
.setColor(CarColor.PRIMARY)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
} else {
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).build())
|
||||
}
|
||||
}
|
||||
chargers?.take(maxRows)?.let { chargerList ->
|
||||
val builder = ItemList.Builder()
|
||||
// only show the city if not all chargers are in the same city
|
||||
@@ -142,7 +158,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
}
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(
|
||||
if (filterStatus.value == FILTERS_FAVORITES) {
|
||||
if (filterStatus == FILTERS_FAVORITES) {
|
||||
R.string.auto_no_favorites_found
|
||||
} else {
|
||||
R.string.auto_no_chargers_found
|
||||
@@ -151,21 +167,38 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
)
|
||||
builder.setOnItemsVisibilityChangedListener(this@MapScreen)
|
||||
setItemList(builder.build())
|
||||
} ?: setLoading(true)
|
||||
} ?: run {
|
||||
if (loadingError) {
|
||||
val builder = ItemList.Builder()
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(R.string.connection_error)
|
||||
)
|
||||
setItemList(builder.build())
|
||||
} else if (locationError) {
|
||||
val builder = ItemList.Builder()
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(R.string.location_error)
|
||||
)
|
||||
setItemList(builder.build())
|
||||
} else {
|
||||
setLoading(true)
|
||||
}
|
||||
}
|
||||
setCurrentLocationEnabled(true)
|
||||
setHeaderAction(Action.APP_ICON)
|
||||
val filtersCount = if (filterStatus.value == FILTERS_FAVORITES) 1 else {
|
||||
filtersWithValue.value?.count {
|
||||
val filtersCount = if (filterStatus == FILTERS_FAVORITES) 1 else {
|
||||
filtersWithValue?.count {
|
||||
!it.value.hasSameValueAs(it.filter.defaultValue())
|
||||
}
|
||||
}
|
||||
|
||||
setActionStrip(
|
||||
ActionStrip.Builder()
|
||||
.addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_settings
|
||||
)
|
||||
@@ -176,6 +209,42 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
session.mapScreen = null
|
||||
}
|
||||
.build())
|
||||
.addAction(Action.Builder().apply {
|
||||
setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
||||
R.drawable.ic_search_off
|
||||
} else {
|
||||
R.drawable.ic_search
|
||||
}
|
||||
)
|
||||
).build()
|
||||
|
||||
)
|
||||
setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
||||
prefs.placeSearchResultAndroidAutoName = null
|
||||
prefs.placeSearchResultAndroidAuto = null
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
chargers = null
|
||||
loadChargers()
|
||||
}
|
||||
} else {
|
||||
screenManager.pushForResult(
|
||||
PlaceSearchScreen(
|
||||
carContext,
|
||||
session
|
||||
)
|
||||
) {
|
||||
chargers = null
|
||||
loadChargers()
|
||||
}
|
||||
session.mapScreen = null
|
||||
}
|
||||
})
|
||||
}.build())
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
@@ -189,14 +258,16 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
.build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
screenManager.pushForResult(FilterScreen(carContext, session)) {
|
||||
filterStatus.value = prefs.filterStatus
|
||||
}
|
||||
screenManager.push(FilterScreen(carContext, session))
|
||||
session.mapScreen = null
|
||||
}
|
||||
.build())
|
||||
.build())
|
||||
setOnContentRefreshListener(this@MapScreen)
|
||||
if (carContext.carAppApiLevel >= 5 ||
|
||||
(BuildConfig.FLAVOR_automotive == "automotive" && carContext.carAppApiLevel >= 4)
|
||||
) {
|
||||
setOnContentRefreshListener(this@MapScreen)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
@@ -289,10 +360,10 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
) {
|
||||
return
|
||||
}
|
||||
val previousLocation = this.location
|
||||
this.location = location
|
||||
if (updateCoroutine != null) {
|
||||
// don't update while still loading last update
|
||||
return
|
||||
if (previousLocation == null) {
|
||||
loadChargers()
|
||||
}
|
||||
|
||||
val now = Instant.now()
|
||||
@@ -307,17 +378,22 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
|
||||
private fun loadChargers() {
|
||||
val location = location ?: return
|
||||
val referenceData = referenceData.value ?: return
|
||||
val filters = filtersWithValue.value ?: return
|
||||
|
||||
val searchLocation =
|
||||
prefs.placeSearchResultAndroidAuto ?: LatLng.fromLocation(location)
|
||||
this.searchLocation = searchLocation
|
||||
|
||||
updateCoroutine = lifecycleScope.launch {
|
||||
loadingError = false
|
||||
try {
|
||||
filterStatus = prefs.filterStatus
|
||||
val filterValues =
|
||||
db.filterValueDao().getFilterValuesAsync(filterStatus, prefs.dataSource)
|
||||
val filters = repo.getFiltersAsync(carContext.stringProvider())
|
||||
filtersWithValue = filtersWithValue(filters, filterValues)
|
||||
|
||||
// load chargers
|
||||
if (filterStatus.value == FILTERS_FAVORITES) {
|
||||
if (filterStatus == FILTERS_FAVORITES) {
|
||||
chargers =
|
||||
db.favoritesDao().getAllFavoritesAsync().map { it.charger }.sortedBy {
|
||||
distanceBetween(
|
||||
@@ -326,60 +402,102 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val response = api.getChargepointsRadius(
|
||||
referenceData,
|
||||
searchLocation,
|
||||
searchRadius,
|
||||
zoom = 16f,
|
||||
filters
|
||||
)
|
||||
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
chargers?.let {
|
||||
if (it.size < maxRows) {
|
||||
// try again with larger radius
|
||||
val response = api.getChargepointsRadius(
|
||||
referenceData,
|
||||
searchLocation,
|
||||
searchRadius * 10,
|
||||
zoom = 16f,
|
||||
filters
|
||||
)
|
||||
chargers =
|
||||
response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
// try multiple search radii until we have enough chargers
|
||||
var chargers: List<ChargeLocation>? = null
|
||||
for (radius in listOf(searchRadius, searchRadius * 10, searchRadius * 50)) {
|
||||
val response = repo.getChargepointsRadius(
|
||||
searchLocation,
|
||||
radius,
|
||||
zoom = 16f,
|
||||
filtersWithValue
|
||||
).awaitFinished()
|
||||
if (response.status == Status.ERROR) {
|
||||
loadingError = true
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
chargers = headingFilter(
|
||||
response.data?.filterIsInstance(ChargeLocation::class.java),
|
||||
searchLocation
|
||||
)
|
||||
if (chargers == null || chargers.size >= maxRows) {
|
||||
break
|
||||
}
|
||||
}
|
||||
this@MapScreen.chargers = chargers
|
||||
}
|
||||
|
||||
updateCoroutine = null
|
||||
lastDistanceUpdateTime = Instant.now()
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
loadingError = true
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters by heading if heading available and enabled
|
||||
*/
|
||||
private fun headingFilter(
|
||||
chargers: List<ChargeLocation>?,
|
||||
searchLocation: LatLng
|
||||
): List<ChargeLocation>? =
|
||||
heading?.orientations?.value?.get(0)?.let { heading ->
|
||||
if (!prefs.showChargersAheadAndroidAuto) return@let chargers
|
||||
|
||||
chargers?.filter {
|
||||
val bearing = bearingBetween(
|
||||
searchLocation.latitude,
|
||||
searchLocation.longitude,
|
||||
it.coordinates.lat,
|
||||
it.coordinates.lng
|
||||
)
|
||||
val diff = headingDiff(bearing, heading.toDouble())
|
||||
abs(diff) < 30
|
||||
}
|
||||
} ?: chargers
|
||||
|
||||
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
|
||||
val isUpdate = this.energyLevel == null
|
||||
this.energyLevel = energyLevel
|
||||
if (isUpdate) invalidate()
|
||||
}
|
||||
|
||||
private fun onCompassUpdated(compass: Compass) {
|
||||
this.heading = compass
|
||||
}
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
setupListeners()
|
||||
session.requestLocationUpdates()
|
||||
locationError = false
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
if (location == null) {
|
||||
locationError = true
|
||||
invalidate()
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
// Reloading chargers in onStart does not seem to count towards content limit.
|
||||
// So let's do this so the user gets fresh chargers when re-entering the app.
|
||||
invalidate()
|
||||
filtersWithValue.observe(this@MapScreen) {
|
||||
loadChargers()
|
||||
if (prefs.dataSource != repo.api.value?.id) {
|
||||
repo.api.value = createApi(prefs.dataSource, carContext)
|
||||
}
|
||||
invalidate()
|
||||
loadChargers()
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
carSensors.addCompassListener(
|
||||
CarSensors.UPDATE_RATE_NORMAL,
|
||||
exec,
|
||||
::onCompassUpdated
|
||||
)
|
||||
}
|
||||
if (!permissions.all {
|
||||
ContextCompat.checkSelfPermission(
|
||||
carContext,
|
||||
@@ -390,8 +508,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
println("Setting up energy level listener")
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,13 +519,15 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
// (i.e. onGetTemplate is not called while the old data is still there)
|
||||
chargers = null
|
||||
availabilities.clear()
|
||||
location = null
|
||||
removeListeners()
|
||||
}
|
||||
|
||||
private fun removeListeners() {
|
||||
if (supportsCarApiLevel3(carContext)) {
|
||||
println("Removing energy level listener")
|
||||
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
carSensors.removeCompassListener(::onCompassUpdated)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,15 +105,15 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
addText(text)
|
||||
}
|
||||
|
||||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
val placeDetails = getDetails(place.id)
|
||||
prefs.placeSearchResultAndroidAuto = placeDetails.latLng
|
||||
prefs.placeSearchResultAndroidAutoName =
|
||||
place.primaryText.toString()
|
||||
screenManager.popTo(MapScreen.MARKER)
|
||||
}
|
||||
setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
val placeDetails = getDetails(place.id)
|
||||
prefs.placeSearchResultAndroidAuto = placeDetails.latLng
|
||||
prefs.placeSearchResultAndroidAutoName =
|
||||
place.primaryText.toString()
|
||||
screenManager.popTo(MapScreen.MARKER)
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
|
||||
@@ -19,6 +19,8 @@ import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class SettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.auto_settings))
|
||||
@@ -73,6 +75,14 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
}
|
||||
.build()
|
||||
)
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_chargers_ahead))
|
||||
.setToggle(Toggle.Builder {
|
||||
prefs.showChargersAheadAndroidAuto = it
|
||||
}.setChecked(prefs.showChargersAheadAndroidAuto).build())
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
}.build()
|
||||
@@ -82,12 +92,9 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
val db = AppDatabase.getInstance(ctx)
|
||||
|
||||
val dataSourceNames = carContext.resources.getStringArray(R.array.pref_data_source_names)
|
||||
val dataSourceValues = carContext.resources.getStringArray(R.array.pref_data_source_values)
|
||||
val dataSourceDescriptions = listOf(
|
||||
carContext.getString(R.string.data_source_goingelectric_desc),
|
||||
carContext.getString(R.string.data_source_openchargemap_desc)
|
||||
)
|
||||
val searchProviderNames =
|
||||
carContext.resources.getStringArray(R.array.pref_search_provider_names)
|
||||
val searchProviderValues =
|
||||
@@ -108,14 +115,9 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
screenManager.push(
|
||||
ChooseDataSourceScreen(
|
||||
carContext,
|
||||
R.string.pref_data_source,
|
||||
dataSourceNames,
|
||||
dataSourceValues,
|
||||
prefs.dataSource,
|
||||
dataSourceDescriptions
|
||||
) {
|
||||
prefs.dataSource = it
|
||||
})
|
||||
ChooseDataSourceScreen.Type.CHARGER_DATA_SOURCE
|
||||
)
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
@@ -129,13 +131,9 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
screenManager.push(
|
||||
ChooseDataSourceScreen(
|
||||
carContext,
|
||||
R.string.pref_search_provider,
|
||||
searchProviderNames,
|
||||
searchProviderValues,
|
||||
prefs.searchProvider
|
||||
) {
|
||||
prefs.searchProvider = it
|
||||
})
|
||||
ChooseDataSourceScreen.Type.SEARCH_PROVIDER
|
||||
)
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
addItem(Row.Builder().apply {
|
||||
@@ -158,34 +156,85 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
|
||||
class ChooseDataSourceScreen(
|
||||
ctx: CarContext,
|
||||
@StringRes val title: Int,
|
||||
val names: Array<String>,
|
||||
val values: Array<String>,
|
||||
val currentValue: String,
|
||||
val descriptions: List<String>? = null,
|
||||
val callback: (String) -> Unit
|
||||
val type: Type,
|
||||
val initialChoice: Boolean = false,
|
||||
@StringRes val extraDesc: Int? = null
|
||||
) : Screen(ctx) {
|
||||
enum class Type {
|
||||
CHARGER_DATA_SOURCE, SEARCH_PROVIDER
|
||||
}
|
||||
|
||||
val prefs = PreferenceDataSource(carContext)
|
||||
val title = when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> R.string.pref_data_source
|
||||
Type.SEARCH_PROVIDER -> R.string.pref_search_provider
|
||||
}
|
||||
val names = when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> carContext.resources.getStringArray(R.array.pref_data_source_names)
|
||||
Type.SEARCH_PROVIDER -> carContext.resources.getStringArray(R.array.pref_search_provider_names)
|
||||
}
|
||||
val values = when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> carContext.resources.getStringArray(R.array.pref_data_source_values)
|
||||
Type.SEARCH_PROVIDER -> carContext.resources.getStringArray(R.array.pref_search_provider_values)
|
||||
}
|
||||
val currentValue: String = when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> prefs.dataSource
|
||||
Type.SEARCH_PROVIDER -> prefs.searchProvider
|
||||
}
|
||||
val descriptions = when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> listOf(
|
||||
carContext.getString(R.string.data_source_goingelectric_desc),
|
||||
carContext.getString(R.string.data_source_openchargemap_desc)
|
||||
)
|
||||
Type.SEARCH_PROVIDER -> null
|
||||
}
|
||||
val callback: (String) -> Unit = when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> { it ->
|
||||
prefs.dataSourceSet = true
|
||||
prefs.dataSource = it
|
||||
}
|
||||
Type.SEARCH_PROVIDER -> { it ->
|
||||
prefs.searchProvider = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(title))
|
||||
setHeaderAction(Action.BACK)
|
||||
setSingleList(ItemList.Builder().apply {
|
||||
setHeaderAction(if (initialChoice) Action.APP_ICON else Action.BACK)
|
||||
|
||||
val list = ItemList.Builder().apply {
|
||||
for (i in names.indices) {
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(names[i])
|
||||
descriptions?.let { addText(it[i]) }
|
||||
if (initialChoice) {
|
||||
setBrowsable(true)
|
||||
setOnClickListener {
|
||||
itemSelected(i)
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
setOnSelectedListener {
|
||||
callback(values[it])
|
||||
screenManager.pop()
|
||||
if (!initialChoice) {
|
||||
setOnSelectedListener {
|
||||
itemSelected(it)
|
||||
}
|
||||
setSelectedIndex(values.indexOf(currentValue))
|
||||
}
|
||||
setSelectedIndex(values.indexOf(currentValue))
|
||||
}.build())
|
||||
}.build()
|
||||
if (extraDesc != null) {
|
||||
addSectionedList(SectionedItemList.create(list, carContext.getString(extraDesc)))
|
||||
} else {
|
||||
setSingleList(list)
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun itemSelected(i: Int) {
|
||||
callback(values[i])
|
||||
screenManager.pop()
|
||||
}
|
||||
}
|
||||
|
||||
class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
|
||||
@@ -3,13 +3,13 @@ package net.vonforst.evmap.auto
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.hardware.common.CarUnit
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.Distance
|
||||
import androidx.car.app.model.*
|
||||
import androidx.car.app.versioning.CarAppApiLevels
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
@@ -35,11 +35,13 @@ val CarContext.constraintManager
|
||||
|
||||
fun Bitmap.asCarIcon(): CarIcon = CarIcon.Builder(IconCompat.createWithBitmap(this)).build()
|
||||
|
||||
val emptyCarIcon = Bitmap.createBitmap(
|
||||
1,
|
||||
1,
|
||||
Bitmap.Config.ARGB_8888
|
||||
).asCarIcon()
|
||||
val emptyCarIcon: CarIcon by lazy {
|
||||
Bitmap.createBitmap(
|
||||
1,
|
||||
1,
|
||||
Bitmap.Config.ARGB_8888
|
||||
).asCarIcon()
|
||||
}
|
||||
|
||||
private const val kmPerMile = 1.609344
|
||||
private const val ftPerMile = 5280
|
||||
@@ -134,6 +136,40 @@ private fun roundToMultipleOf(num: Double, step: Double): Double {
|
||||
return (num / step).roundToInt() * step
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginates data based on specific limits for each page.
|
||||
* If the data fits on a single page, this page can have a maximum size nSingle. Otherwise, the
|
||||
* first page has maximum nFirst items, the last page nLast items, and all intermediate pages nOther
|
||||
* items.
|
||||
*/
|
||||
fun <T> List<T>.paginate(nSingle: Int, nFirst: Int, nOther: Int, nLast: Int): List<List<T>> {
|
||||
if (nOther > nLast) {
|
||||
throw IllegalArgumentException("nLast has to be larger than or equal to nOther")
|
||||
}
|
||||
return if (size <= nSingle) {
|
||||
listOf(this)
|
||||
} else {
|
||||
val result = mutableListOf<List<T>>()
|
||||
var i = 0
|
||||
var page = 0
|
||||
while (true) {
|
||||
val remaining = size - i
|
||||
if (page == 0) {
|
||||
result.add(subList(i, i + nFirst))
|
||||
i += nFirst
|
||||
} else if (remaining <= nLast) {
|
||||
result.add(subList(i, size))
|
||||
break
|
||||
} else {
|
||||
result.add(subList(i, i + nOther))
|
||||
i += nOther
|
||||
}
|
||||
page++
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fun getAndroidAutoVersion(ctx: Context): List<String> {
|
||||
val info = ctx.packageManager.getPackageInfo("com.google.android.projection.gearhead", 0)
|
||||
return info.versionName.split(".")
|
||||
@@ -152,4 +188,17 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
class DummyReturnScreen(ctx: CarContext) : Screen(ctx) {
|
||||
/*
|
||||
Dummy screen to get around template refresh limitations.
|
||||
It immediately pops back to the previous screen.
|
||||
*/
|
||||
override fun onGetTemplate(): Template {
|
||||
screenManager.pop()
|
||||
return MessageTemplate.Builder(carContext.getString(R.string.loading)).setLoading(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,8 +28,12 @@
|
||||
<string name="auto_chargeprice_vehicle_unavailable">EVMap konnte das Fahrzeugmodell nicht erkennen.</string>
|
||||
<string name="auto_chargeprice_vehicle_unknown">Keins der in der App ausgewählten Fahrzeuge passt zu diesem Fahrzeug (%1$s %2$s).</string>
|
||||
<string name="auto_chargeprice_vehicle_ambiguous">Mehrere der in der App ausgewählten Fahrzeuge passen zu diesem Fahrzeug (%1$s %2$s).</string>
|
||||
<string name="auto_chargers_ahead">Nur Ladestationen in Fahrtrichtung</string>
|
||||
<string name="settings_android_auto_chargeprice_range">Ladebereich für Preisvergleich</string>
|
||||
<string name="data_sources_hint">In den Einstellungen kannst du auch zwischen Google Maps und OpenStreetMap (Mapbox) für die Kartendaten wechseln.</string>
|
||||
<string name="selecting_all">alle Einträge ausgewählt</string>
|
||||
<string name="selecting_none">alle Einträge abgewählt</string>
|
||||
<string name="loading">Lade…</string>
|
||||
<string name="auto_multipage_goto">Seite %d</string>
|
||||
<string name="auto_multipage">(%d/%d)</string>
|
||||
</resources>
|
||||
@@ -34,4 +34,5 @@
|
||||
<string name="auto_no_refresh_possible">D\'autres mises à jour ne sont pas possibles. Veuillez revenir en arrière et redémarrer.</string>
|
||||
<string name="settings_android_auto_chargeprice_range">Plage de charge pour la comparaison des prix</string>
|
||||
<string name="welcome_android_auto_detail">Vous pouvez également utiliser EVMap à partir d\'Android Auto sur les voitures prises en charge. Il suffit de sélectionner l\'application EVMap dans le menu Android Auto.</string>
|
||||
<string name="loading">Chargement…</string>
|
||||
</resources>
|
||||
@@ -34,4 +34,6 @@
|
||||
<string name="data_sources_hint">I innstillingene kan du også bytte mellom Google Maps og OpenStreetMap (Mapbox) for kartdata.</string>
|
||||
<string name="selecting_all">valgte alle elementene</string>
|
||||
<string name="sounds_cool">den er grei</string>
|
||||
<string name="auto_chargers_ahead">Kun ladere i kjøreretningen</string>
|
||||
<string name="loading">Laster inn …</string>
|
||||
</resources>
|
||||
@@ -28,8 +28,12 @@
|
||||
<string name="auto_chargeprice_vehicle_unavailable">EVMap could not determine your vehicle model.</string>
|
||||
<string name="auto_chargeprice_vehicle_unknown">None of the vehicles selected in the app matches this vehicle (%1$s %2$s).</string>
|
||||
<string name="auto_chargeprice_vehicle_ambiguous">Multiple vehicles selected in the app match this vehicle (%1$s %2$s).</string>
|
||||
<string name="auto_chargers_ahead">Only chargers along driving direction</string>
|
||||
<string name="settings_android_auto_chargeprice_range">Charging range for price comparison</string>
|
||||
<string name="data_sources_hint">In the settings you can also switch between Google Maps and OpenStreetMap (Mapbox) for the map data.</string>
|
||||
<string name="selecting_all">selected all items</string>
|
||||
<string name="selecting_none">deselected all items</string>
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="auto_multipage_goto">Page %d</string>
|
||||
<string name="auto_multipage">(%d/%d)</string>
|
||||
</resources>
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="net.vonforst.evmap">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.car.permission.CAR_INFO" />
|
||||
<uses-permission android:name="android.car.permission.CAR_ENERGY" />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="net.vonforst.evmap">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
@@ -15,6 +15,9 @@
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="google.navigation" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.support.customtabs.action.CustomTabsService" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
@@ -24,7 +27,8 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppTheme"
|
||||
android:localeConfig="@xml/locales_config">
|
||||
|
||||
<meta-data
|
||||
android:name="com.mapbox.ACCESS_TOKEN"
|
||||
@@ -252,6 +256,10 @@
|
||||
android:host="www.goingelectric.de"
|
||||
android:pathPattern="/stromtankstellen/Ungarn/..*/..*/..*/"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="openchargemap.org"
|
||||
android:pathPattern="/site/poi/details/..*"
|
||||
android:scheme="https" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
@@ -261,6 +269,15 @@
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="autoStoreLocales"
|
||||
android:value="true" />
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -3,6 +3,7 @@ package net.vonforst.evmap
|
||||
import android.app.Application
|
||||
import com.facebook.stetho.Stetho
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.updateAppLocale
|
||||
import net.vonforst.evmap.ui.updateNightMode
|
||||
import org.acra.config.dialog
|
||||
import org.acra.config.limiter
|
||||
@@ -13,7 +14,16 @@ import org.acra.ktx.initAcra
|
||||
class EvMapApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
updateNightMode(PreferenceDataSource(this))
|
||||
val prefs = PreferenceDataSource(this)
|
||||
updateNightMode(prefs)
|
||||
|
||||
// Convert to new AppCompat storage for app language
|
||||
val lang = prefs.language
|
||||
if (lang != null && lang !in listOf("", "default")) {
|
||||
updateAppLocale(lang)
|
||||
prefs.language = null
|
||||
}
|
||||
|
||||
Stetho.initializeWithDefaults(this);
|
||||
init(applicationContext)
|
||||
|
||||
|
||||
@@ -2,15 +2,16 @@ package net.vonforst.evmap
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsClient
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.splashscreen.SplashScreen
|
||||
@@ -32,7 +33,6 @@ import net.vonforst.evmap.fragment.MapFragmentArgs
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.navigation.NavHostFragment
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.utils.LocaleContextWrapper
|
||||
import net.vonforst.evmap.utils.getLocationFromIntent
|
||||
|
||||
|
||||
@@ -53,14 +53,6 @@ class MapsActivity : AppCompatActivity(),
|
||||
var fragmentCallback: FragmentCallback? = null
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
return super.attachBaseContext(
|
||||
LocaleContextWrapper.wrap(
|
||||
newBase, PreferenceDataSource(newBase).language
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val splashScreen = installSplashScreen()
|
||||
@@ -141,6 +133,37 @@ class MapsActivity : AppCompatActivity(),
|
||||
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
|
||||
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
|
||||
if (id != null) {
|
||||
if (prefs.dataSource != "goingelectric") {
|
||||
prefs.dataSource = "goingelectric"
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(
|
||||
R.string.data_source_switched_to,
|
||||
getString(R.string.data_source_goingelectric)
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
|
||||
.createPendingIntent()
|
||||
}
|
||||
} else if (intent?.scheme == "https" && intent?.data?.host == "openchargemap.org") {
|
||||
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
|
||||
if (id != null) {
|
||||
if (prefs.dataSource != "openchargemap") {
|
||||
prefs.dataSource = "openchargemap"
|
||||
Toast.makeText(
|
||||
this,
|
||||
getString(
|
||||
R.string.data_source_switched_to,
|
||||
getString(R.string.data_source_openchargemap)
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
@@ -206,6 +229,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
}
|
||||
|
||||
fun openUrl(url: String) {
|
||||
val pkg = CustomTabsClient.getPackageName(this, null)
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
@@ -213,6 +237,11 @@ class MapsActivity : AppCompatActivity(),
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
pkg?.let {
|
||||
// prefer to open URL in custom tab, even if native app
|
||||
// available (such as EVMap itself)
|
||||
intent.intent.setPackage(pkg)
|
||||
}
|
||||
try {
|
||||
intent.launchUrl(this, Uri.parse(url))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
|
||||
@@ -6,11 +6,6 @@ import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.*
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
|
||||
fun Bundle.optDouble(name: String): Double? {
|
||||
@@ -85,25 +80,6 @@ fun max(a: Int?, b: Int?): Int? {
|
||||
|
||||
fun <T> List<T>.containsAny(vararg values: T) = values.any { this.contains(it) }
|
||||
|
||||
public suspend fun <T> LiveData<T>.await(): T {
|
||||
return withContext(Dispatchers.Main.immediate) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val observer = object : Observer<T> {
|
||||
override fun onChanged(value: T) {
|
||||
removeObserver(this)
|
||||
continuation.resume(value, null)
|
||||
}
|
||||
}
|
||||
|
||||
observeForever(observer)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.isDarkMode() =
|
||||
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
|
||||
val binding = holder.binding as ItemConnectorButtonBinding
|
||||
binding.enabled = enabledConnectors?.let { item.type in it } ?: true
|
||||
val root = binding.root as CheckableConstraintLayout
|
||||
root.setOnCheckedChangeListener { _, _ -> }
|
||||
root.isChecked = checkedItem == position
|
||||
root.setOnClickListener {
|
||||
root.isChecked = true
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||
|
||||
class SingleViewAdapter(val view: View) : Adapter<SingleViewAdapter.ViewHolder>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view)
|
||||
|
||||
override fun getItemCount() = 1
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,8 @@ interface ChargepointApi<out T : ReferenceData> {
|
||||
|
||||
fun getFilters(referenceData: ReferenceData, sp: StringProvider): List<Filter<FilterValue>>
|
||||
|
||||
fun getName(): String
|
||||
val name: String
|
||||
val id: String
|
||||
}
|
||||
|
||||
interface StringProvider {
|
||||
|
||||
@@ -132,7 +132,8 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
|
||||
data class ChargeLocationStatus(
|
||||
val status: Map<Chargepoint, List<ChargepointStatus>>,
|
||||
val source: String
|
||||
val source: String,
|
||||
val evseIds: Map<Chargepoint, List<String>>? = null
|
||||
) {
|
||||
fun applyFilters(connectors: Set<String>?, minPower: Int?): ChargeLocationStatus {
|
||||
val statusFiltered = status.filterKeys {
|
||||
|
||||
@@ -152,13 +152,14 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
|
||||
val connectorStatus = details.flatMap { it.chargePoints }.flatMap { cp ->
|
||||
cp.connectors.map { connector ->
|
||||
connector to cp.status
|
||||
Triple(connector, cp.status, cp.evseId)
|
||||
}
|
||||
}
|
||||
|
||||
val enbwConnectors = mutableMapOf<Long, Pair<Double, String>>()
|
||||
val enbwStatus = mutableMapOf<Long, ChargepointStatus>()
|
||||
connectorStatus.forEachIndexed { index, (connector, statusStr) ->
|
||||
val enbwEvseId = mutableMapOf<Long, String>()
|
||||
connectorStatus.forEachIndexed { index, (connector, statusStr, evseId) ->
|
||||
val id = index.toLong()
|
||||
val power = connector.maxPowerInKw ?: 0.0
|
||||
val type = when (connector.plugTypeName) {
|
||||
@@ -179,17 +180,22 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
"UNSPECIFIED" -> ChargepointStatus.UNKNOWN
|
||||
else -> ChargepointStatus.UNKNOWN
|
||||
}
|
||||
enbwConnectors.put(id, power to type)
|
||||
enbwStatus.put(id, status)
|
||||
enbwConnectors[id] = power to type
|
||||
enbwStatus[id] = status
|
||||
evseId?.let { enbwEvseId[id] = it }
|
||||
}
|
||||
|
||||
val match = matchChargepoints(enbwConnectors, location.chargepointsMerged)
|
||||
val chargepointStatus = match.mapValues { entry ->
|
||||
entry.value.map { enbwStatus[it]!! }
|
||||
}
|
||||
val evseIds = if (enbwEvseId.size == enbwStatus.size) match.mapValues { entry ->
|
||||
entry.value.map { enbwEvseId[it]!! }
|
||||
} else null
|
||||
return ChargeLocationStatus(
|
||||
chargepointStatus,
|
||||
"EnBW"
|
||||
"EnBW",
|
||||
evseIds
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -130,13 +130,14 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
}
|
||||
val connectorStatus = details.flatMap { it.evses }.flatMap { evse ->
|
||||
evse.connectors.map { connector ->
|
||||
connector to evse.status
|
||||
Triple(connector, evse.status, evse.evseId)
|
||||
}
|
||||
}
|
||||
|
||||
val nmConnectors = mutableMapOf<Long, Pair<Double, String>>()
|
||||
val nmStatus = mutableMapOf<Long, ChargepointStatus>()
|
||||
connectorStatus.forEach { (connector, statusStr) ->
|
||||
val nmEvseId = mutableMapOf<Long, String>()
|
||||
connectorStatus.forEach { (connector, statusStr, evseId) ->
|
||||
val id = connector.uid
|
||||
val power = connector.electricalProperties.getPower()
|
||||
val type = when (connector.connectorType.toLowerCase(Locale.ROOT)) {
|
||||
@@ -161,15 +162,20 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
}
|
||||
nmConnectors.put(id, power to type)
|
||||
nmStatus.put(id, status)
|
||||
evseId?.let { nmEvseId[id] = it }
|
||||
}
|
||||
|
||||
val match = matchChargepoints(nmConnectors, location.chargepointsMerged)
|
||||
val chargepointStatus = match.mapValues { entry ->
|
||||
entry.value.map { nmStatus[it]!! }
|
||||
}
|
||||
val evseIds = if (nmEvseId.size == nmStatus.size) match.mapValues { entry ->
|
||||
entry.value.map { nmEvseId[it]!! }
|
||||
} else null
|
||||
return ChargeLocationStatus(
|
||||
chargepointStatus,
|
||||
"NewMotion"
|
||||
"NewMotion",
|
||||
evseIds
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -112,18 +112,47 @@ interface ChargepriceApi {
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a charger is supported by Chargeprice.
|
||||
*
|
||||
* This function just applies some heuristics on the charger's data without making API
|
||||
* calls. If it returns true, that is not a guarantee that Chargeprice will have information
|
||||
* on this charger. But if it is false, it is pretty unlikely that Chargeprice will have
|
||||
* useful data, so we do not show the price comparison button in this case.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun isCountrySupported(country: String, dataSource: String): Boolean = when (dataSource) {
|
||||
// list of countries updated 2021/08/24
|
||||
"goingelectric" -> country in listOf(
|
||||
"Deutschland",
|
||||
"Österreich",
|
||||
"Schweiz",
|
||||
"Frankreich",
|
||||
"Belgien",
|
||||
"Niederlande",
|
||||
"Luxemburg",
|
||||
"Dänemark",
|
||||
fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||
val dataSourceSupported = charger.dataSource in listOf("goingelectric", "openchargemap")
|
||||
val countrySupported =
|
||||
charger.chargepriceData?.country?.let { isCountrySupported(it, charger.dataSource) }
|
||||
?: false
|
||||
val networkSupported = charger.chargepriceData?.network?.let {
|
||||
if (charger.dataSource == "openchargemap") {
|
||||
it !in listOf(
|
||||
"1", // unknown operator
|
||||
"44", // private residence/individual
|
||||
"45" // business owner at location
|
||||
)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} ?: false
|
||||
val powerAvailable = charger.chargepoints.all { it.hasKnownPower() }
|
||||
return dataSourceSupported && countrySupported && networkSupported && powerAvailable
|
||||
}
|
||||
|
||||
private fun isCountrySupported(country: String, dataSource: String): Boolean =
|
||||
when (dataSource) {
|
||||
"goingelectric" -> country in listOf(
|
||||
// list of countries according to Chargeprice.app, 2021/08/24
|
||||
"Deutschland",
|
||||
"Österreich",
|
||||
"Schweiz",
|
||||
"Frankreich",
|
||||
"Belgien",
|
||||
"Niederlande",
|
||||
"Luxemburg",
|
||||
"Dänemark",
|
||||
"Norwegen",
|
||||
"Schweden",
|
||||
"Slowenien",
|
||||
@@ -133,9 +162,28 @@ interface ChargepriceApi {
|
||||
"Italien",
|
||||
"Spanien",
|
||||
"Großbritannien",
|
||||
"Irland"
|
||||
"Irland",
|
||||
// additional countries found 2022/09/17, https://github.com/johan12345/EVMap/issues/234
|
||||
"Finnland",
|
||||
"Lettland",
|
||||
"Litauen",
|
||||
"Estland",
|
||||
"Liechtenstein",
|
||||
"Rumänien",
|
||||
"Slowakei",
|
||||
"Slowenien",
|
||||
"Polen",
|
||||
"Serbien",
|
||||
"Bulgarien",
|
||||
"Kosovo",
|
||||
"Montenegro",
|
||||
"Albanien",
|
||||
"Griechenland",
|
||||
"Portugal",
|
||||
"Island"
|
||||
)
|
||||
"openchargemap" -> country in listOf(
|
||||
// list of countries according to Chargeprice.app, 2021/08/24
|
||||
"DE",
|
||||
"AT",
|
||||
"CH",
|
||||
@@ -153,7 +201,25 @@ interface ChargepriceApi {
|
||||
"IT",
|
||||
"ES",
|
||||
"GB",
|
||||
"IE"
|
||||
"IE",
|
||||
// additional countries found 2022/09/17, https://github.com/johan12345/EVMap/issues/234
|
||||
"FI",
|
||||
"LV",
|
||||
"LT",
|
||||
"EE",
|
||||
"LI",
|
||||
"RO",
|
||||
"SK",
|
||||
"SI",
|
||||
"PL",
|
||||
"RS",
|
||||
"BG",
|
||||
"XK",
|
||||
"ME",
|
||||
"AL",
|
||||
"GR",
|
||||
"PT",
|
||||
"IS"
|
||||
)
|
||||
else -> false
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package net.vonforst.evmap.api.chargeprice
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.util.Patterns
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import jsonapi.*
|
||||
import kotlinx.parcelize.Parceler
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.WriteWith
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
@@ -77,7 +80,9 @@ data class ChargepriceOptions(
|
||||
val currency: String? = null,
|
||||
@Json(name = "start_time") val startTime: Int? = null,
|
||||
@Json(name = "allow_unbalanced_load") val allowUnbalancedLoad: Boolean? = null,
|
||||
@Json(name = "provider_customer_tariffs") val providerCustomerTariffs: Boolean? = null
|
||||
@Json(name = "provider_customer_tariffs") val providerCustomerTariffs: Boolean? = null,
|
||||
@Json(name = "show_price_unavailable") val showPriceUnavailable: Boolean? = null,
|
||||
@Json(name = "show_all_brand_restricted_tariffs") val showAllBrandRestrictedTariffs: Boolean? = null
|
||||
)
|
||||
|
||||
@Resource("tariff")
|
||||
@@ -178,9 +183,12 @@ data class ChargePrice(
|
||||
@Json(name = "branding")
|
||||
val branding: ChargepriceBranding? = null,
|
||||
|
||||
@ToOne("tariff")
|
||||
val tariffId: String?
|
||||
@RelationshipsObject
|
||||
val relationships: @WriteWith<RelationshipsParceler>() Relationships? = null,
|
||||
) : Equatable, Cloneable, Parcelable {
|
||||
val tariffId: String?
|
||||
get() = (relationships?.get("tariff") as? Relationship.ToOne)?.data?.id
|
||||
|
||||
fun formatMonthlyFees(ctx: Context): String {
|
||||
return listOfNotNull(
|
||||
if (totalMonthlyFee > 0) {
|
||||
@@ -193,12 +201,76 @@ data class ChargePrice(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parceler implementation for the Relationships object.
|
||||
* Note that this ignores certain fields that we don't need (links, meta, etc.)
|
||||
*/
|
||||
internal object RelationshipsParceler : Parceler<Relationships?> {
|
||||
override fun create(parcel: Parcel): Relationships? {
|
||||
if (parcel.readInt() == 0) return null
|
||||
|
||||
val nMembers = parcel.readInt()
|
||||
val members = (0 until nMembers).map { _ ->
|
||||
val key = parcel.readString()!!
|
||||
val value = if (parcel.readInt() == 0) {
|
||||
val type = parcel.readString()
|
||||
val id = parcel.readString()
|
||||
val ri = if (type != null && id != null) {
|
||||
ResourceIdentifier(type, id)
|
||||
} else null
|
||||
Relationship.ToOne(ri)
|
||||
} else {
|
||||
val size = parcel.readInt()
|
||||
val ris = (0 until size).map { _ ->
|
||||
val type = parcel.readString()!!
|
||||
val id = parcel.readString()!!
|
||||
ResourceIdentifier(type, id)
|
||||
}
|
||||
Relationship.ToMany(ris)
|
||||
}
|
||||
key to value
|
||||
}.toMap()
|
||||
|
||||
return Relationships(members)
|
||||
}
|
||||
|
||||
override fun Relationships?.write(parcel: Parcel, flags: Int) {
|
||||
if (this == null) {
|
||||
parcel.writeInt(0)
|
||||
return
|
||||
} else {
|
||||
parcel.writeInt(1)
|
||||
}
|
||||
|
||||
parcel.writeInt(members.size)
|
||||
for (member in this.members) {
|
||||
parcel.writeString(member.key)
|
||||
when (val value = member.value) {
|
||||
is Relationship.ToOne -> {
|
||||
parcel.writeInt(0)
|
||||
parcel.writeString(value.data?.type)
|
||||
parcel.writeString(value.data?.id)
|
||||
}
|
||||
is Relationship.ToMany -> {
|
||||
parcel.writeInt(1)
|
||||
parcel.writeInt(value.data.size)
|
||||
for (ri in value.data) {
|
||||
parcel.writeString(ri.type)
|
||||
parcel.writeString(ri.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
data class ChargepointPrice(
|
||||
val power: Double,
|
||||
val plug: String,
|
||||
val price: Double,
|
||||
val price: Double?,
|
||||
@Json(name = "price_distribution") val priceDistribution: PriceDistribution,
|
||||
@Json(name = "blocking_fee_start") val blockingFeeStart: Int?,
|
||||
@Json(name = "no_price_reason") var noPriceReason: String?
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package net.vonforst.evmap.api.fronyx
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.ToJson
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
internal class ZonedDateTimeAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String): ZonedDateTime? = ZonedDateTime.parse(value)
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: ZonedDateTime): String = value.toString()
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package net.vonforst.evmap.api.fronyx
|
||||
|
||||
import android.content.Context
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
private interface FronyxApiRetrofit {
|
||||
@GET("predictions/evse-id/{evseId}")
|
||||
suspend fun getPredictionsForEvseId(
|
||||
@Path("evseId") evseId: String,
|
||||
@Query("timeframe") timeframe: Int? = null
|
||||
): FronyxEvseIdResponse
|
||||
|
||||
@GET("predictions/evses")
|
||||
suspend fun getPredictionsForEvseIds(
|
||||
@Query("evseIds", encoded = true) evseIds: String // comma-separated
|
||||
): List<FronyxEvseIdResponse>
|
||||
|
||||
companion object {
|
||||
private val cacheSize = 1L * 1024 * 1024 // 1MB
|
||||
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(ZonedDateTimeAdapter())
|
||||
.build()
|
||||
|
||||
fun create(
|
||||
apikey: String,
|
||||
baseurl: String = "https://api.fronyx.io/api/",
|
||||
context: Context? = null
|
||||
): FronyxApiRetrofit {
|
||||
val client = OkHttpClient.Builder().apply {
|
||||
addInterceptor { chain ->
|
||||
// add API key to every request
|
||||
val original = chain.request()
|
||||
val new = original.newBuilder()
|
||||
.header("X-API-Token", apikey)
|
||||
.header("Content-Type", "application/json")
|
||||
.build()
|
||||
chain.proceed(new)
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
addNetworkInterceptor(StethoInterceptor())
|
||||
}
|
||||
if (context != null) {
|
||||
cache(Cache(context.getCacheDir(), cacheSize))
|
||||
}
|
||||
}.build()
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseurl)
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.client(client)
|
||||
.build()
|
||||
return retrofit.create(FronyxApiRetrofit::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FronyxApi(
|
||||
apikey: String,
|
||||
baseurl: String = "https://api.fronyx.io/api/",
|
||||
context: Context? = null
|
||||
) {
|
||||
private val api = FronyxApiRetrofit.create(apikey, baseurl, context)
|
||||
|
||||
suspend fun getPredictionsForEvseId(
|
||||
evseId: String,
|
||||
timeframe: Int? = null
|
||||
): FronyxEvseIdResponse = api.getPredictionsForEvseId(evseId, timeframe)
|
||||
|
||||
suspend fun getPredictionsForEvseIds(
|
||||
evseIds: List<String>
|
||||
): List<FronyxEvseIdResponse> = api.getPredictionsForEvseIds(evseIds.joinToString(","))
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Checks if a chargepoint is supported by Fronyx.
|
||||
*
|
||||
* This function just applies some heuristics on the charger's data without making API
|
||||
* calls. If it returns true, that is not a guarantee that Fronyx will have information
|
||||
* on this chargepoint. But if it is false, it is pretty unlikely that Fronyx will have
|
||||
* useful data, so we do not try to load the data in this case.
|
||||
*/
|
||||
fun isChargepointSupported(charger: ChargeLocation, chargepoint: Chargepoint): Boolean {
|
||||
if (charger.address?.country !in listOf("Deutschland", "Germany")) {
|
||||
// fronyx only predicts for chargers in Germany for now
|
||||
return false
|
||||
}
|
||||
if (chargepoint.type !in listOf(
|
||||
Chargepoint.CCS_UNKNOWN,
|
||||
Chargepoint.CCS_TYPE_2,
|
||||
Chargepoint.CHADEMO
|
||||
)
|
||||
) {
|
||||
// fronyx only predicts DC chargers for now
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.vonforst.evmap.api.fronyx
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FronyxEvseIdResponse(
|
||||
val evseId: String,
|
||||
val predictions: List<FronyxPrediction>,
|
||||
val locationId: String?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FronyxPrediction(
|
||||
val timestamp: ZonedDateTime,
|
||||
val status: FronyxStatus
|
||||
)
|
||||
|
||||
enum class FronyxStatus {
|
||||
AVAILABLE, UNAVAILABLE
|
||||
}
|
||||
@@ -129,7 +129,8 @@ class GoingElectricApiWrapper(
|
||||
private val clusterThreshold = 11f
|
||||
val api = GoingElectricApi.create(apikey, baseurl, context)
|
||||
|
||||
override fun getName() = "GoingElectric.de"
|
||||
override val name = "GoingElectric.de"
|
||||
override val id = "going_electric"
|
||||
|
||||
override suspend fun getChargepoints(
|
||||
referenceData: ReferenceData,
|
||||
|
||||
@@ -108,7 +108,8 @@ class OpenChargeMapApiWrapper(
|
||||
private val clusterThreshold = 11
|
||||
val api = OpenChargeMapApi.create(apikey, baseurl, context)
|
||||
|
||||
override fun getName() = "OpenChargeMap.org"
|
||||
override val name = "OpenChargeMap.org"
|
||||
override val id = "open_charge_map"
|
||||
|
||||
private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) =
|
||||
if (value == null || value.all) null else value.values.joinToString(",")
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.color.MaterialColors
|
||||
@@ -24,10 +25,12 @@ import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.ChargepriceAdapter
|
||||
import net.vonforst.evmap.adapter.CheckableChargepriceCarAdapter
|
||||
import net.vonforst.evmap.adapter.CheckableConnectorAdapter
|
||||
import net.vonforst.evmap.adapter.SingleViewAdapter
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
|
||||
import net.vonforst.evmap.databinding.FragmentChargepriceHeaderBinding
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
|
||||
@@ -37,6 +40,7 @@ import java.text.NumberFormat
|
||||
|
||||
class ChargepriceFragment : Fragment() {
|
||||
private lateinit var binding: FragmentChargepriceBinding
|
||||
private lateinit var headerBinding: FragmentChargepriceHeaderBinding
|
||||
private var connectionErrorSnackbar: Snackbar? = null
|
||||
|
||||
private val vm: ChargepriceViewModel by viewModels(factoryProducer = {
|
||||
@@ -91,8 +95,14 @@ class ChargepriceFragment : Fragment() {
|
||||
inflater,
|
||||
R.layout.fragment_chargeprice, container, false
|
||||
)
|
||||
headerBinding = DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.fragment_chargeprice_header, container, false
|
||||
)
|
||||
binding.lifecycleOwner = this
|
||||
binding.vm = vm
|
||||
headerBinding.lifecycleOwner = this
|
||||
headerBinding.vm = vm
|
||||
|
||||
binding.toolbar.inflateMenu(R.menu.chargeprice)
|
||||
binding.toolbar.setTitle(R.string.chargeprice_title)
|
||||
@@ -117,7 +127,7 @@ class ChargepriceFragment : Fragment() {
|
||||
}
|
||||
|
||||
val vehicleAdapter = CheckableChargepriceCarAdapter()
|
||||
binding.vehicleSelection.adapter = vehicleAdapter
|
||||
headerBinding.vehicleSelection.adapter = vehicleAdapter
|
||||
val vehicleObserver: Observer<ChargepriceCar> = Observer {
|
||||
vehicleAdapter.setCheckedItem(it)
|
||||
}
|
||||
@@ -133,8 +143,12 @@ class ChargepriceFragment : Fragment() {
|
||||
(requireActivity() as MapsActivity).openUrl(it.url)
|
||||
}
|
||||
}
|
||||
val joinedAdapter = ConcatAdapter(
|
||||
SingleViewAdapter(headerBinding.root),
|
||||
chargepriceAdapter
|
||||
)
|
||||
binding.chargePricesList.apply {
|
||||
adapter = chargepriceAdapter
|
||||
adapter = joinedAdapter
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
addItemDecoration(
|
||||
@@ -152,6 +166,9 @@ class ChargepriceFragment : Fragment() {
|
||||
vm.myTariffsAll.observe(viewLifecycleOwner) {
|
||||
chargepriceAdapter.myTariffsAll = it
|
||||
}
|
||||
vm.chargePricesForChargepoint.observe(viewLifecycleOwner) {
|
||||
it?.data?.let { chargepriceAdapter.submitList(it) }
|
||||
}
|
||||
|
||||
val connectorsAdapter = CheckableConnectorAdapter()
|
||||
|
||||
@@ -170,7 +187,7 @@ class ChargepriceFragment : Fragment() {
|
||||
plugs?.flatMap { plug -> equivalentPlugTypes(plug) }
|
||||
}
|
||||
|
||||
binding.connectorsList.apply {
|
||||
headerBinding.connectorsList.apply {
|
||||
adapter = connectorsAdapter
|
||||
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||
}
|
||||
@@ -183,12 +200,12 @@ class ChargepriceFragment : Fragment() {
|
||||
findNavController().navigate(R.id.action_chargeprice_to_chargepriceSettingsFragment)
|
||||
}
|
||||
|
||||
binding.batteryRange.setLabelFormatter { value: Float ->
|
||||
headerBinding.batteryRange.setLabelFormatter { value: Float ->
|
||||
val fmt = NumberFormat.getNumberInstance()
|
||||
fmt.maximumFractionDigits = 0
|
||||
fmt.format(value.toDouble()) + "%"
|
||||
}
|
||||
binding.batteryRange.setOnTouchListener { _: View, motionEvent: MotionEvent ->
|
||||
headerBinding.batteryRange.setOnTouchListener { _: View, motionEvent: MotionEvent ->
|
||||
when (motionEvent.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> vm.batteryRangeSliderDragging.value = true
|
||||
MotionEvent.ACTION_UP -> vm.batteryRangeSliderDragging.value = false
|
||||
|
||||
@@ -27,7 +27,7 @@ class DataSourceSelectDialog : MaterialDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
override fun createView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
|
||||
@@ -56,8 +56,7 @@ import com.google.android.material.transition.MaterialContainerTransform
|
||||
import com.google.android.material.transition.MaterialFadeThrough
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.*
|
||||
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
|
||||
import com.stfalcon.imageviewer.StfalconImageViewer
|
||||
import io.michaelrocks.bimap.HashBiMap
|
||||
@@ -72,7 +71,6 @@ import net.vonforst.evmap.adapter.ConnectorAdapter
|
||||
import net.vonforst.evmap.adapter.DetailsAdapter
|
||||
import net.vonforst.evmap.adapter.GalleryAdapter
|
||||
import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.autocomplete.ApiUnavailableException
|
||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.bold
|
||||
@@ -132,7 +130,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
val state = bottomSheetBehavior.state
|
||||
if (state != STATE_COLLAPSED && state != STATE_HIDDEN) {
|
||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||
if (bottomSheetCollapsible) {
|
||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||
} else {
|
||||
vm.chargerSparse.value = null
|
||||
}
|
||||
} else if (state == STATE_COLLAPSED) {
|
||||
vm.chargerSparse.value = null
|
||||
} else if (state == STATE_HIDDEN) {
|
||||
@@ -238,6 +240,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
return binding.root
|
||||
}
|
||||
|
||||
val bottomSheetCollapsible
|
||||
get() = resources.getBoolean(R.bool.bottom_sheet_collapsible)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||
|
||||
@@ -253,6 +258,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.detailView.topPart.doOnNextLayout {
|
||||
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom
|
||||
}
|
||||
bottomSheetBehavior.isCollapsible = bottomSheetCollapsible
|
||||
|
||||
setupObservers()
|
||||
setupClickListeners()
|
||||
@@ -370,12 +376,25 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
null, extras
|
||||
)
|
||||
}
|
||||
binding.detailView.imgPredictionSource.setOnClickListener {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.fronyx_url))
|
||||
}
|
||||
binding.detailView.btnPredictionHelp.setOnClickListener {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(getString(R.string.prediction_help))
|
||||
.setPositiveButton(R.string.ok) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
binding.detailView.topPart.setOnClickListener {
|
||||
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
|
||||
}
|
||||
setupSearchAutocomplete()
|
||||
binding.detailAppBar.toolbar.setNavigationOnClickListener {
|
||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||
if (bottomSheetCollapsible) {
|
||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||
} else {
|
||||
vm.chargerSparse.value = null
|
||||
}
|
||||
}
|
||||
binding.detailAppBar.toolbar.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
@@ -394,7 +413,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val charger = vm.charger.value?.data
|
||||
if (charger?.editUrl != null) {
|
||||
(activity as? MapsActivity)?.openUrl(charger.editUrl)
|
||||
if (vm.apiType == GoingElectricApiWrapper::class.java) {
|
||||
if (vm.apiId.value == "going_electric") {
|
||||
// instructions specific to GoingElectric
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
@@ -554,7 +573,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
vm.chargerSparse.observe(viewLifecycleOwner, Observer {
|
||||
if (it != null) {
|
||||
if (vm.bottomSheetState.value != BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT) {
|
||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||
bottomSheetBehavior.state =
|
||||
if (bottomSheetCollapsible) STATE_COLLAPSED else STATE_ANCHOR_POINT
|
||||
}
|
||||
removeSearchFocus()
|
||||
binding.fabDirections.show()
|
||||
@@ -970,7 +990,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
lifecycleScope.launch {
|
||||
val address = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
Geocoder(requireContext()).getFromLocationName(locationName, 1).getOrNull(0)
|
||||
Geocoder(requireContext()).getFromLocationName(locationName, 1)
|
||||
?.getOrNull(0)
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class MultiSelectDialog : MaterialDialogFragment() {
|
||||
private lateinit var items: List<MultiSelectItem>
|
||||
private lateinit var binding: DialogMultiSelectBinding
|
||||
|
||||
override fun onCreateView(
|
||||
override fun createView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.transition.MaterialFadeThrough
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
||||
@@ -44,6 +45,14 @@ class AboutFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
"contributors" -> {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.about_contributors)
|
||||
.setMessage(getString(R.string.about_contributors_text) + "\n\n" + getString(R.string.about_contributors_list))
|
||||
.setPositiveButton(R.string.ok) { _, _ -> }
|
||||
.show()
|
||||
true
|
||||
}
|
||||
"github_link" -> {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.github_link))
|
||||
true
|
||||
|
||||
@@ -1,28 +1,62 @@
|
||||
package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.ui.getAppLocale
|
||||
import net.vonforst.evmap.ui.updateAppLocale
|
||||
import net.vonforst.evmap.ui.updateNightMode
|
||||
|
||||
class UiSettingsFragment : BaseSettingsFragment() {
|
||||
override val isTopLevel = false
|
||||
lateinit var langPref: ListPreference
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings_ui, rootKey)
|
||||
|
||||
langPref = findPreference("language")!!
|
||||
langPref.setOnPreferenceChangeListener { _, newValue ->
|
||||
updateAppLocale(newValue as String)
|
||||
true
|
||||
}
|
||||
|
||||
val appLinkPref = findPreference<Preference>("applink_associate")!!
|
||||
appLinkPref.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
langPref.value = getAppLocale()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
when (key) {
|
||||
"language" -> {
|
||||
activity?.let {
|
||||
it.finish();
|
||||
it.startActivity(it.intent);
|
||||
}
|
||||
}
|
||||
"darkmode" -> {
|
||||
updateNightMode(prefs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
when (preference.key) {
|
||||
"applink_associate" -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val context = context ?: return false
|
||||
val intent = Intent(
|
||||
Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,
|
||||
Uri.parse("package:${context.packageName}")
|
||||
)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import net.vonforst.evmap.ui.MaterialDialogFragment
|
||||
class OpensourceDonationsDialogFragment : MaterialDialogFragment() {
|
||||
private lateinit var binding: DialogOpensourceDonationsBinding
|
||||
|
||||
override fun onCreateView(
|
||||
override fun createView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
|
||||
@@ -26,7 +26,9 @@ abstract class LocationEngine(protected val context: Context) {
|
||||
*/
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
|
||||
fun requestLocationUpdates(priority: Priority, intervalMs: Long, listener: LocationListener) {
|
||||
requests.add(LocationRequest(priority, intervalMs, listener))
|
||||
if (!requests.any { it.listener == listener }) {
|
||||
requests.add(LocationRequest(priority, intervalMs, listener))
|
||||
}
|
||||
enable()
|
||||
}
|
||||
|
||||
|
||||
@@ -374,7 +374,21 @@ data class Address(
|
||||
val street: String?
|
||||
) : Parcelable {
|
||||
override fun toString(): String {
|
||||
return "${street ?: ""}, ${postcode ?: ""} ${city ?: ""}"
|
||||
// TODO: the order here follows a German-style format (i.e. street, postcode city).
|
||||
// in principle this should be country-dependent (e.g. UK has postcode after city)
|
||||
return buildString {
|
||||
street?.let {
|
||||
append(it)
|
||||
append(", ")
|
||||
}
|
||||
postcode?.let {
|
||||
append(it)
|
||||
append(" ")
|
||||
}
|
||||
city?.let {
|
||||
append(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +1,125 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import androidx.lifecycle.*
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.api.StringProvider
|
||||
import net.vonforst.evmap.api.goingelectric.GEReferenceData
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import net.vonforst.evmap.viewmodel.await
|
||||
|
||||
@Dao
|
||||
interface ChargeLocationsDao {
|
||||
abstract class ChargeLocationsDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg locations: ChargeLocation)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertBlocking(vararg locations: ChargeLocation)
|
||||
abstract suspend fun insert(vararg locations: ChargeLocation)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg locations: ChargeLocation)
|
||||
abstract suspend fun delete(vararg locations: ChargeLocation)
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM chargelocation")
|
||||
fun getAllChargeLocations(): LiveData<List<ChargeLocation>>
|
||||
/**
|
||||
* The ChargeLocationsRepository wraps the ChargepointApi and the DB to provide caching
|
||||
* functionality.
|
||||
*/
|
||||
class ChargeLocationsRepository(
|
||||
api: ChargepointApi<ReferenceData>, private val scope: CoroutineScope,
|
||||
private val db: AppDatabase, private val prefs: PreferenceDataSource
|
||||
) {
|
||||
val api = MutableLiveData<ChargepointApi<ReferenceData>>().apply { value = api }
|
||||
|
||||
@Query("SELECT * FROM chargelocation")
|
||||
suspend fun getAllChargeLocationsAsync(): List<ChargeLocation>
|
||||
val referenceData = this.api.switchMap { api ->
|
||||
when (api) {
|
||||
is GoingElectricApiWrapper -> {
|
||||
GEReferenceDataRepository(
|
||||
api,
|
||||
scope,
|
||||
db.geReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
is OpenChargeMapApiWrapper -> {
|
||||
OCMReferenceDataRepository(
|
||||
api,
|
||||
scope,
|
||||
db.ocmReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
else -> {
|
||||
throw RuntimeException("no reference data implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM chargelocation")
|
||||
fun getAllChargeLocationsBlocking(): List<ChargeLocation>
|
||||
private val chargeLocationsDao = db.chargeLocationsDao()
|
||||
|
||||
@Query("SELECT * FROM chargelocation WHERE lat >= :lat1 AND lat <= :lat2 AND lng >= :lng1 AND lng <= :lng2")
|
||||
suspend fun getChargeLocationsInBoundsAsync(
|
||||
lat1: Double,
|
||||
lat2: Double,
|
||||
lng1: Double,
|
||||
lng2: Double
|
||||
): List<ChargeLocation>
|
||||
fun getChargepoints(
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
filters: FilterValues?
|
||||
): LiveData<Resource<List<ChargepointListItem>>> {
|
||||
return liveData {
|
||||
val refData = referenceData.await()
|
||||
val result = api.value!!.getChargepoints(refData, bounds, zoom, filters)
|
||||
|
||||
emit(result)
|
||||
}
|
||||
}
|
||||
|
||||
fun getChargepointsRadius(
|
||||
location: LatLng,
|
||||
radius: Int,
|
||||
zoom: Float,
|
||||
filters: FilterValues?
|
||||
): LiveData<Resource<List<ChargepointListItem>>> {
|
||||
return liveData {
|
||||
val refData = referenceData.await()
|
||||
val result = api.value!!.getChargepointsRadius(refData, location, radius, zoom, filters)
|
||||
|
||||
emit(result)
|
||||
}
|
||||
}
|
||||
|
||||
fun getChargepointDetail(
|
||||
id: Long
|
||||
): LiveData<Resource<ChargeLocation>> {
|
||||
return liveData {
|
||||
emit(Resource.loading(null))
|
||||
val refData = referenceData.await()
|
||||
val result = api.value!!.getChargepointDetail(refData, id)
|
||||
emit(result)
|
||||
}
|
||||
}
|
||||
|
||||
fun getFilters(sp: StringProvider) = MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||
addSource(referenceData) { refData: ReferenceData? ->
|
||||
refData?.let { value = api.value!!.getFilters(refData, sp) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getFiltersAsync(sp: StringProvider): List<Filter<FilterValue>> {
|
||||
val refData = referenceData.await()
|
||||
return api.value!!.getFilters(refData, sp)
|
||||
}
|
||||
|
||||
val chargeCardMap by lazy {
|
||||
referenceData.map { refData: ReferenceData? ->
|
||||
if (refData is GEReferenceData) {
|
||||
refData.chargecards.associate {
|
||||
it.id to it.convert()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,28 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.room.*
|
||||
import net.vonforst.evmap.await
|
||||
import net.vonforst.evmap.model.*
|
||||
|
||||
@Dao
|
||||
abstract class FilterValueDao {
|
||||
@Query("SELECT * FROM booleanfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract fun getBooleanFilterValues(
|
||||
protected abstract suspend fun getBooleanFilterValues(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): LiveData<List<BooleanFilterValue>>
|
||||
): List<BooleanFilterValue>
|
||||
|
||||
@Query("SELECT * FROM multiplechoicefiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract fun getMultipleChoiceFilterValues(
|
||||
protected abstract suspend fun getMultipleChoiceFilterValues(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): LiveData<List<MultipleChoiceFilterValue>>
|
||||
): List<MultipleChoiceFilterValue>
|
||||
|
||||
@Query("SELECT * FROM sliderfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract fun getSliderFilterValues(
|
||||
protected abstract suspend fun getSliderFilterValues(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): LiveData<List<SliderFilterValue>>
|
||||
): List<SliderFilterValue>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract suspend fun insert(vararg values: BooleanFilterValue)
|
||||
@@ -54,27 +51,23 @@ abstract class FilterValueDao {
|
||||
dataSource: String
|
||||
)
|
||||
|
||||
open fun getFilterValues(filterStatus: Long, dataSource: String): LiveData<List<FilterValue>> =
|
||||
open suspend fun getFilterValuesAsync(
|
||||
filterStatus: Long,
|
||||
dataSource: String
|
||||
): List<FilterValue> =
|
||||
if (filterStatus == FILTERS_DISABLED || filterStatus == FILTERS_FAVORITES) {
|
||||
MutableLiveData(emptyList())
|
||||
emptyList()
|
||||
} else {
|
||||
MediatorLiveData<List<FilterValue>>().apply {
|
||||
val sources = listOf(
|
||||
getBooleanFilterValues(filterStatus, dataSource),
|
||||
getMultipleChoiceFilterValues(filterStatus, dataSource),
|
||||
getBooleanFilterValues(filterStatus, dataSource) +
|
||||
getMultipleChoiceFilterValues(filterStatus, dataSource) +
|
||||
getSliderFilterValues(filterStatus, dataSource)
|
||||
)
|
||||
for (source in sources) {
|
||||
addSource(source) {
|
||||
val values = sources.map { it.value }
|
||||
if (values.all { it != null }) {
|
||||
value = values.filterNotNull().flatten()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun getFilterValues(filterStatus: Long, dataSource: String) = liveData {
|
||||
emit(null)
|
||||
emit(getFilterValuesAsync(filterStatus, dataSource))
|
||||
}
|
||||
|
||||
@Transaction
|
||||
open suspend fun insert(vararg values: FilterValue) {
|
||||
values.forEach {
|
||||
@@ -98,7 +91,7 @@ abstract class FilterValueDao {
|
||||
if (filterStatus == FILTERS_CUSTOM) return
|
||||
|
||||
deleteFilterValuesForProfile(FILTERS_CUSTOM, dataSource)
|
||||
val values = getFilterValues(filterStatus, dataSource).await().onEach {
|
||||
val values = getFilterValuesAsync(filterStatus, dataSource).onEach {
|
||||
it.profile = FILTERS_CUSTOM
|
||||
}
|
||||
insert(*values.toTypedArray())
|
||||
|
||||
@@ -87,6 +87,7 @@ class GEReferenceDataRepository(
|
||||
val networks = dao.getAllNetworks()
|
||||
val chargeCards = dao.getAllChargeCards()
|
||||
return MediatorLiveData<GEReferenceData>().apply {
|
||||
value = null
|
||||
listOf(chargeCards, networks, plugs).map { source ->
|
||||
addSource(source) { _ ->
|
||||
val p = plugs.value ?: return@addSource
|
||||
|
||||
@@ -79,6 +79,7 @@ class OCMReferenceDataRepository(
|
||||
val countries = dao.getAllCountries()
|
||||
val operators = dao.getAllOperators()
|
||||
return MediatorLiveData<OCMReferenceData>().apply {
|
||||
value = null
|
||||
listOf(countries, connectionTypes, operators).map { source ->
|
||||
addSource(source) { _ ->
|
||||
val ct = connectionTypes.value
|
||||
|
||||
@@ -75,8 +75,15 @@ class PreferenceDataSource(val context: Context) {
|
||||
}
|
||||
|
||||
|
||||
val language: String
|
||||
get() = sp.getString("language", "default")!!
|
||||
/**
|
||||
* Sets app language. Will be removed and set to null with the next update because storage is
|
||||
* handled by AppCompat.
|
||||
*/
|
||||
var language: String?
|
||||
get() = sp.getString("language", null)
|
||||
set(lang) {
|
||||
sp.edit().putString("language", lang).apply()
|
||||
}
|
||||
|
||||
val darkmode: String
|
||||
get() = sp.getString("darkmode", "default")!!
|
||||
@@ -239,4 +246,13 @@ class PreferenceDataSource(val context: Context) {
|
||||
set(value) {
|
||||
sp.edit().putString("place_search_result_android_auto_name", value).apply()
|
||||
}
|
||||
|
||||
var showChargersAheadAndroidAuto: Boolean
|
||||
get() = sp.getBoolean("show_chargers_ahead_android_auto", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("show_chargers_ahead_android_auto", value).apply()
|
||||
}
|
||||
|
||||
val predictionEnabled: Boolean
|
||||
get() = sp.getBoolean("prediction_enabled", true)
|
||||
}
|
||||
@@ -32,7 +32,7 @@ fun View.exitCircularReveal(block: () -> Unit) {
|
||||
duration = 350
|
||||
interpolator = DecelerateInterpolator(1f)
|
||||
addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
visibility = View.INVISIBLE
|
||||
block()
|
||||
super.onAnimationEnd(animation)
|
||||
|
||||
352
app/src/main/java/net/vonforst/evmap/ui/BarGraphView.kt
Normal file
352
app/src/main/java/net/vonforst/evmap/ui/BarGraphView.kt
Normal file
@@ -0,0 +1,352 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.text.Layout
|
||||
import android.text.SpannableString
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import net.vonforst.evmap.R
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.*
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||
private val dp = context.resources.displayMetrics.density
|
||||
private val sp = context.resources.displayMetrics.scaledDensity
|
||||
var zeroHeight = 4 * dp
|
||||
var barWidth = (16 * dp).roundToInt()
|
||||
var barMargin = (2 * dp).roundToInt()
|
||||
var legendWidth = 12 * dp
|
||||
var legendLineLength = 4 * dp
|
||||
var legendLineWidth = 1 * dp
|
||||
var dashLength = 4 * dp
|
||||
var bubbleTextSize = (12 * sp).roundToInt()
|
||||
var bubblePadding = (6 * dp).roundToInt()
|
||||
var selectedBar: Int = 0
|
||||
var bubbleStrokeWidth = 1 * dp
|
||||
|
||||
var barDrawable =
|
||||
AppCompatResources.getDrawable(context, R.drawable.bar_graph)!!
|
||||
var colorAvailable = ContextCompat.getColor(context, R.color.available)
|
||||
var colorUnavailable = ContextCompat.getColor(context, R.color.unavailable)
|
||||
|
||||
var data: Map<ZonedDateTime, Int>? = null
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
var maxValue: Int? = null
|
||||
set(value) {
|
||||
field = value
|
||||
invalidate()
|
||||
}
|
||||
|
||||
var activeAlpha = 0.87f
|
||||
var inactiveAlpha = 0.60f
|
||||
|
||||
private val legendPaint = Paint().apply {
|
||||
val ta = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorControlNormal))
|
||||
color = ta.getColor(0, 0)
|
||||
strokeWidth = legendLineWidth
|
||||
textSize = legendWidth - legendLineLength
|
||||
}
|
||||
private val legendDashedPaint = Paint().apply {
|
||||
set(legendPaint)
|
||||
alpha = (inactiveAlpha * 255).roundToInt()
|
||||
style = Paint.Style.STROKE
|
||||
pathEffect = DashPathEffect(floatArrayOf(dashLength, dashLength), 0f)
|
||||
strokeWidth = 1f
|
||||
}
|
||||
private val bubblePaint = Paint().apply {
|
||||
set(legendPaint)
|
||||
alpha = (inactiveAlpha * 255).roundToInt()
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = bubbleStrokeWidth
|
||||
}
|
||||
private val bubbleTextPaint = TextPaint().apply {
|
||||
set(legendPaint)
|
||||
textSize = bubbleTextSize.toFloat()
|
||||
}
|
||||
|
||||
private var graphBounds: Rect? = null
|
||||
private var bubbleBounds: Rect? = null
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
val bottom = (paddingBottom + legendWidth).roundToInt()
|
||||
val left = (paddingLeft + legendWidth).roundToInt()
|
||||
val right = (paddingRight + legendWidth).roundToInt()
|
||||
val top = (paddingTop + bubbleStrokeWidth / 2).roundToInt()
|
||||
|
||||
val bubbleTextHeight = bubbleTextPaint.fontMetrics.run { descent - ascent }
|
||||
val bubbleHeight = (bubbleTextHeight + 3 * bubblePadding).roundToInt()
|
||||
val bubbleLeft = (paddingLeft + bubbleStrokeWidth / 2).roundToInt()
|
||||
val bubbleRight = (paddingRight + bubbleStrokeWidth / 2).roundToInt()
|
||||
|
||||
graphBounds = Rect(left, top + bubbleHeight, w - right, h - bottom)
|
||||
bubbleBounds = Rect(bubbleLeft, top, w - bubbleRight, top + bubbleHeight)
|
||||
}
|
||||
|
||||
private val timeFormat = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
if (isInEditMode && data == null) {
|
||||
// show sample data
|
||||
val now = ZonedDateTime.now().run {
|
||||
val minutesRound = ((minute / 15) + 1) * 15
|
||||
plusMinutes((minutesRound - minute).toLong())
|
||||
}
|
||||
data = (0..20).associate {
|
||||
now.plusMinutes(15L * it) to (Math.random() * 8).roundToInt()
|
||||
}
|
||||
maxValue = 8
|
||||
}
|
||||
val data = data?.toSortedMap() ?: return
|
||||
if (data.isEmpty()) return
|
||||
val maxValue = maxValue ?: data.maxOf { it.value }
|
||||
|
||||
drawGraph(canvas, data, maxValue)
|
||||
drawBubble(canvas, data, maxValue)
|
||||
}
|
||||
|
||||
private fun drawGraph(
|
||||
canvas: Canvas,
|
||||
data: SortedMap<ZonedDateTime, Int>,
|
||||
maxValue: Int
|
||||
) {
|
||||
val graphBounds = graphBounds ?: return
|
||||
|
||||
canvas.apply {
|
||||
drawLine(
|
||||
graphBounds.left.toFloat(),
|
||||
graphBounds.top.toFloat(),
|
||||
graphBounds.right.toFloat(),
|
||||
graphBounds.top.toFloat(),
|
||||
legendDashedPaint
|
||||
)
|
||||
|
||||
legendPaint.textAlign = Paint.Align.CENTER
|
||||
data.entries.forEachIndexed { i, (t, v) ->
|
||||
val height =
|
||||
zeroHeight + (graphBounds.height() - zeroHeight) * v.toFloat() / maxValue
|
||||
val left = graphBounds.left + (barWidth + barMargin) * i
|
||||
|
||||
if (left + barWidth > graphBounds.right) return@forEachIndexed
|
||||
|
||||
barDrawable.setBounds(
|
||||
left,
|
||||
graphBounds.bottom - height.roundToInt(),
|
||||
left + barWidth,
|
||||
graphBounds.bottom
|
||||
)
|
||||
barDrawable.alpha =
|
||||
((if (i == selectedBar) activeAlpha else inactiveAlpha) * 255).roundToInt()
|
||||
barDrawable.setTint(getColor(v, maxValue))
|
||||
barDrawable.draw(canvas)
|
||||
|
||||
val center = left.toFloat() + barWidth / 2
|
||||
if (t.minute == 0) {
|
||||
drawLine(
|
||||
center, graphBounds.bottom.toFloat(),
|
||||
center, graphBounds.bottom + legendLineLength, legendPaint
|
||||
)
|
||||
drawText(
|
||||
t.withZoneSameInstant(ZoneId.systemDefault()).format(timeFormat),
|
||||
center, graphBounds.bottom + legendWidth, legendPaint
|
||||
)
|
||||
}
|
||||
|
||||
if (i == selectedBar) {
|
||||
drawLine(
|
||||
center,
|
||||
graphBounds.bottom - height,
|
||||
center,
|
||||
graphBounds.top.toFloat(),
|
||||
legendDashedPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
drawLine(
|
||||
graphBounds.left.toFloat(),
|
||||
graphBounds.bottom.toFloat(),
|
||||
graphBounds.right.toFloat(),
|
||||
graphBounds.bottom.toFloat(),
|
||||
legendPaint
|
||||
)
|
||||
drawLine(
|
||||
graphBounds.left.toFloat(),
|
||||
graphBounds.bottom.toFloat(),
|
||||
graphBounds.right.toFloat(),
|
||||
graphBounds.bottom.toFloat(),
|
||||
legendPaint
|
||||
)
|
||||
|
||||
legendPaint.textAlign = Paint.Align.LEFT
|
||||
drawText(
|
||||
this@BarGraphView.maxValue.toString(),
|
||||
graphBounds.right.toFloat() + legendLineLength,
|
||||
graphBounds.top + (legendWidth - legendLineLength) / 3,
|
||||
legendPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getColor(v: Int, maxValue: Int) =
|
||||
if (v < maxValue) colorAvailable else colorUnavailable
|
||||
|
||||
private fun drawBubble(canvas: Canvas, data: SortedMap<ZonedDateTime, Int>, maxValue: Int) {
|
||||
val bubbleBounds = bubbleBounds ?: return
|
||||
val graphBounds = graphBounds ?: return
|
||||
val data = data.toList()
|
||||
|
||||
if (data.size <= selectedBar) return
|
||||
canvas.apply {
|
||||
val center = graphBounds.left + selectedBar * (barWidth + barMargin) + barWidth * 0.5f
|
||||
val (t, v) = data[selectedBar]
|
||||
val tformat = context.getString(
|
||||
R.string.prediction_time_colon,
|
||||
t.withZoneSameInstant(ZoneId.systemDefault()).format(timeFormat)
|
||||
)
|
||||
val availableformat = context.resources.getQuantityString(
|
||||
R.plurals.prediction_number_available,
|
||||
maxValue - v,
|
||||
maxValue - v,
|
||||
maxValue
|
||||
)
|
||||
val text = SpannableString("$tformat $availableformat").apply {
|
||||
setSpan(
|
||||
ForegroundColorSpan(getColor(v, maxValue)),
|
||||
0,
|
||||
tformat.length + 1,
|
||||
SpannableString.SPAN_INCLUSIVE_INCLUSIVE
|
||||
)
|
||||
setSpan(
|
||||
StyleSpan(Typeface.BOLD),
|
||||
0,
|
||||
tformat.length + 1,
|
||||
SpannableString.SPAN_INCLUSIVE_INCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
val bubbleTextWidth = StaticLayout.getDesiredWidth(text, bubbleTextPaint)
|
||||
val bubbleWidth = bubbleTextWidth + 2 * bubblePadding
|
||||
val bubbleLeft = max(
|
||||
min(center - bubbleWidth / 2, bubbleBounds.right - bubbleWidth),
|
||||
bubbleBounds.left.toFloat()
|
||||
)
|
||||
|
||||
val bubblePath = generateBubblePath(
|
||||
center,
|
||||
bubbleBounds.bottom.toFloat(),
|
||||
bubbleLeft,
|
||||
bubbleBounds.top.toFloat(),
|
||||
bubbleLeft + bubbleWidth,
|
||||
(bubbleBounds.bottom - bubblePadding).toFloat(),
|
||||
bubblePadding.toFloat()
|
||||
)
|
||||
drawPath(bubblePath, bubblePaint)
|
||||
|
||||
val layout = StaticLayout(
|
||||
text,
|
||||
bubbleTextPaint,
|
||||
ceil(bubbleTextWidth).toInt(),
|
||||
Layout.Alignment.ALIGN_NORMAL,
|
||||
1f,
|
||||
0f,
|
||||
false
|
||||
)
|
||||
canvas.save()
|
||||
canvas.translate(bubbleLeft + bubblePadding, bubbleBounds.top + bubblePadding.toFloat())
|
||||
layout.draw(canvas)
|
||||
canvas.restore()
|
||||
//drawText(text, 0, text.length, bubbleLeft + bubblePadding, bubbleBounds.top + 2f * bubblePadding, bubbleTextPaint)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
val graphBounds = graphBounds ?: return super.onTouchEvent(event)
|
||||
val x = event.x.roundToInt()
|
||||
val y = event.y.roundToInt()
|
||||
if (graphBounds.contains(x, y) && event.action == MotionEvent.ACTION_DOWN) {
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
updateSelectedBar(x)
|
||||
return true
|
||||
} else if (event.action == MotionEvent.ACTION_MOVE && x > graphBounds.left && y < graphBounds.right) {
|
||||
updateSelectedBar(x)
|
||||
return true
|
||||
} else if (event.action == MotionEvent.ACTION_UP) {
|
||||
parent.requestDisallowInterceptTouchEvent(false)
|
||||
return true
|
||||
}
|
||||
return super.onTouchEvent(event)
|
||||
}
|
||||
|
||||
private fun updateSelectedBar(x: Int) {
|
||||
val graphBounds = graphBounds ?: return
|
||||
val bar = (x - graphBounds.left) / (barWidth + barMargin)
|
||||
if (bar != selectedBar) {
|
||||
selectedBar = bar
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a path that represents a "speech bubble" with tip position at tipX, tipY,
|
||||
* bubble bounds left, top, right bottom and corner radius cornerRadius.
|
||||
*/
|
||||
private fun generateBubblePath(
|
||||
tipX: Float,
|
||||
tipY: Float,
|
||||
left: Float,
|
||||
top: Float,
|
||||
right: Float,
|
||||
bottom: Float,
|
||||
cornerRadius: Float
|
||||
): Path {
|
||||
val tipWidth = tipY - bottom
|
||||
return Path().apply {
|
||||
moveTo(tipX, tipY)
|
||||
lineTo(min(tipX + tipWidth, right - cornerRadius), bottom)
|
||||
lineTo(right - cornerRadius, bottom)
|
||||
arcTo(
|
||||
right - cornerRadius * 2,
|
||||
bottom - cornerRadius * 2,
|
||||
right,
|
||||
bottom,
|
||||
90f,
|
||||
-90f,
|
||||
false
|
||||
)
|
||||
lineTo(right, top + cornerRadius)
|
||||
arcTo(right - cornerRadius * 2, top, right, top + cornerRadius * 2, 0f, -90f, false)
|
||||
lineTo(left + cornerRadius, top)
|
||||
arcTo(left, top, left + cornerRadius * 2, top + cornerRadius * 2, 270f, -90f, false)
|
||||
lineTo(left, bottom - cornerRadius)
|
||||
arcTo(
|
||||
left,
|
||||
bottom - cornerRadius * 2,
|
||||
left + cornerRadius * 2,
|
||||
bottom,
|
||||
180f,
|
||||
-90f,
|
||||
false
|
||||
)
|
||||
lineTo(max(tipX - tipWidth, left + cornerRadius), bottom)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
fun updateNightMode(prefs: PreferenceDataSource) {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (prefs.darkmode) {
|
||||
"on" -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
"off" -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun updateAppLocale(language: String) {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
if (language in listOf("", "default")) {
|
||||
LocaleListCompat.getEmptyLocaleList()
|
||||
} else {
|
||||
LocaleListCompat.forLanguageTags(language)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun getAppLocale(): String? {
|
||||
val locales = AppCompatDelegate.getApplicationLocales()
|
||||
return if (locales.isEmpty) {
|
||||
"default"
|
||||
} else {
|
||||
val arr = Array(locales.size()) { locales.get(it)!!.toLanguageTag() }
|
||||
LocaleListCompat.forLanguageTags(BuildConfig.supportedLocales).getFirstMatch(arr)
|
||||
?.toLanguageTag()
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ abstract class MaterialDialogFragment : AppCompatDialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext(), theme).apply {
|
||||
dialogView =
|
||||
onCreateView(LayoutInflater.from(requireContext()), null, savedInstanceState)!!
|
||||
createView(LayoutInflater.from(requireContext()), null, savedInstanceState)
|
||||
|
||||
setView(dialogView)
|
||||
}.create()
|
||||
@@ -77,6 +77,12 @@ abstract class MaterialDialogFragment : AppCompatDialogFragment() {
|
||||
return dialog
|
||||
}
|
||||
|
||||
abstract fun createView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View
|
||||
|
||||
abstract fun initView(view: View, savedInstanceState: Bundle?)
|
||||
|
||||
override fun getView(): View {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
fun updateNightMode(prefs: PreferenceDataSource) {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (prefs.darkmode) {
|
||||
"on" -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
"off" -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package net.vonforst.evmap.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import java.util.*
|
||||
|
||||
|
||||
class LocaleContextWrapper(base: Context?) : ContextWrapper(base) {
|
||||
companion object {
|
||||
fun wrap(context: Context, language: String): ContextWrapper {
|
||||
val sysConfig: Configuration = context.applicationContext.resources.configuration
|
||||
val appConfig: Configuration = context.resources.configuration
|
||||
|
||||
if (language == "" || language == "default") {
|
||||
// set default locale
|
||||
Locale.setDefault(ConfigurationCompat.getLocales(sysConfig)[0])
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
appConfig.setLocales(sysConfig.locales)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
appConfig.locale = sysConfig.locale
|
||||
}
|
||||
} else {
|
||||
// set selected locale
|
||||
val locale = if (language.contains("-")) {
|
||||
val split = language.split("-")
|
||||
Locale(split[0], split[1])
|
||||
} else {
|
||||
Locale(language)
|
||||
}
|
||||
Locale.setDefault(locale)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
appConfig.setLocale(locale)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
appConfig.locale = locale
|
||||
}
|
||||
}
|
||||
|
||||
return LocaleContextWrapper(context.createConfigurationContext(appConfig))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,25 @@ fun distanceBetween(
|
||||
}
|
||||
|
||||
|
||||
fun bearingBetween(startLat: Double, startLng: Double, endLat: Double, endLng: Double): Double {
|
||||
val dLon = Math.toRadians(-endLng) - Math.toRadians(-startLng)
|
||||
val originLat = Math.toRadians(startLat)
|
||||
val destinationLat = Math.toRadians(endLat)
|
||||
|
||||
return Math.toDegrees(
|
||||
atan2(
|
||||
sin(dLon) * cos(destinationLat),
|
||||
cos(originLat) * sin(destinationLat) - sin(originLat) * cos(destinationLat) * cos(dLon)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun headingDiff(h1: Double, h2: Double): Double {
|
||||
return (h1 - h2 + 540) % 360 - 180
|
||||
}
|
||||
|
||||
|
||||
fun getLocationFromIntent(intent: Intent): List<Double>? {
|
||||
val pos = intent.data?.schemeSpecificPart?.split("?")?.get(0)
|
||||
var coords = stringToCoords(pos)
|
||||
|
||||
@@ -166,7 +166,7 @@ class ChargepriceViewModel(
|
||||
)
|
||||
}
|
||||
}.filterNotNull()
|
||||
.sortedBy { it.chargepointPrices.first().price }
|
||||
.sortedBy { it.chargepointPrices.first().price ?: Double.MAX_VALUE }
|
||||
.sortedByDescending {
|
||||
prefs.chargepriceMyTariffsAll ||
|
||||
myTariffs != null && it.tariffId in myTariffs
|
||||
@@ -243,6 +243,12 @@ class ChargepriceViewModel(
|
||||
}
|
||||
|
||||
val cpStation = ChargepriceStation.fromEvmap(charger, compatibleConnectors)
|
||||
if (cpStation.chargePoints.isEmpty()) {
|
||||
// no compatible connectors
|
||||
chargePrices.value = Resource.success(emptyList())
|
||||
chargePriceMeta.value = Resource.success(ChargepriceMeta(emptyList()))
|
||||
return
|
||||
}
|
||||
|
||||
loadPricesJob?.cancel()
|
||||
loadPricesJob = viewModelScope.launch {
|
||||
@@ -257,7 +263,8 @@ class ChargepriceViewModel(
|
||||
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
|
||||
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
|
||||
currency = prefs.chargepriceCurrency,
|
||||
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad
|
||||
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad,
|
||||
showPriceUnavailable = true
|
||||
),
|
||||
relationships = if (!myTariffsAll) {
|
||||
Relationships(
|
||||
|
||||
@@ -1,71 +1,45 @@
|
||||
package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.*
|
||||
import androidx.lifecycle.switchMap
|
||||
import net.vonforst.evmap.model.Filter
|
||||
import net.vonforst.evmap.model.FilterValue
|
||||
import net.vonforst.evmap.model.FilterValues
|
||||
import net.vonforst.evmap.model.FilterWithValue
|
||||
import net.vonforst.evmap.storage.FilterValueDao
|
||||
import kotlin.reflect.full.cast
|
||||
|
||||
fun ChargepointApi<ReferenceData>.getReferenceData(
|
||||
scope: CoroutineScope,
|
||||
ctx: Context
|
||||
): LiveData<out ReferenceData> {
|
||||
val db = AppDatabase.getInstance(ctx)
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
return when (this) {
|
||||
is GoingElectricApiWrapper -> {
|
||||
GEReferenceDataRepository(
|
||||
this,
|
||||
scope,
|
||||
db.geReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
is OpenChargeMapApiWrapper -> {
|
||||
OCMReferenceDataRepository(
|
||||
this,
|
||||
scope,
|
||||
db.ocmReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
else -> {
|
||||
throw RuntimeException("no reference data implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun filtersWithValue(
|
||||
filters: LiveData<List<Filter<FilterValue>>>,
|
||||
filterValues: LiveData<List<FilterValue>>
|
||||
): MediatorLiveData<FilterValues> =
|
||||
MediatorLiveData<FilterValues>().apply {
|
||||
filterValues: LiveData<List<FilterValue>?>
|
||||
): MediatorLiveData<FilterValues?> =
|
||||
MediatorLiveData<FilterValues?>().apply {
|
||||
listOf(filters, filterValues).forEach {
|
||||
addSource(it) {
|
||||
val f = filters.value ?: return@addSource
|
||||
val values = filterValues.value ?: return@addSource
|
||||
value = f.map { filter ->
|
||||
val value =
|
||||
values.find { it.key == filter.key } ?: filter.defaultValue()
|
||||
FilterWithValue(filter, filter.valueClass.cast(value))
|
||||
val f = filters.value ?: run {
|
||||
value = null
|
||||
return@addSource
|
||||
}
|
||||
val values = filterValues.value ?: run {
|
||||
value = null
|
||||
return@addSource
|
||||
}
|
||||
value = filtersWithValue(f, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun filtersWithValue(
|
||||
filters: List<Filter<FilterValue>>,
|
||||
values: List<FilterValue>
|
||||
) = filters.map { filter ->
|
||||
val value =
|
||||
values.find { it.key == filter.key } ?: filter.defaultValue()
|
||||
FilterWithValue(filter, filter.valueClass.cast(value))
|
||||
}
|
||||
|
||||
fun FilterValueDao.getFilterValues(filterStatus: LiveData<Long>, dataSource: String) =
|
||||
MediatorLiveData<List<FilterValue>>().apply {
|
||||
var source: LiveData<List<FilterValue>>? = null
|
||||
addSource(filterStatus) { status ->
|
||||
source?.let { removeSource(it) }
|
||||
source = getFilterValues(status, dataSource)
|
||||
addSource(source!!) { result ->
|
||||
value = result
|
||||
}
|
||||
}
|
||||
filterStatus.switchMap {
|
||||
getFilterValues(it, dataSource)
|
||||
}
|
||||
@@ -8,27 +8,23 @@ import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
||||
import net.vonforst.evmap.storage.FilterProfile
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
|
||||
class FilterViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
private var api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
|
||||
private val db = AppDatabase.getInstance(application)
|
||||
private val prefs = PreferenceDataSource(application)
|
||||
private val api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
|
||||
private val repo = ChargeLocationsRepository(api, viewModelScope, db, prefs)
|
||||
private val filters = repo.getFilters(application.stringProvider())
|
||||
|
||||
private val referenceData = api.getReferenceData(viewModelScope, application)
|
||||
private val filters = MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||
addSource(referenceData) { data ->
|
||||
value = api.getFilters(data, application.stringProvider())
|
||||
}
|
||||
}
|
||||
|
||||
private val filterValues: LiveData<List<FilterValue>> by lazy {
|
||||
private val filterValues: LiveData<List<FilterValue>?> by lazy {
|
||||
db.filterValueDao().getFilterValues(FILTERS_CUSTOM, prefs.dataSource)
|
||||
}
|
||||
|
||||
val filtersWithValue: LiveData<FilterValues> by lazy {
|
||||
val filtersWithValue: LiveData<FilterValues?> by lazy {
|
||||
filtersWithValue(filters, filterValues)
|
||||
}
|
||||
|
||||
@@ -65,9 +61,10 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
|
||||
prefs.filterStatus = FILTERS_CUSTOM
|
||||
}
|
||||
|
||||
suspend fun saveAsProfile(name: String) {
|
||||
suspend fun saveAsProfile(name: String): Boolean {
|
||||
// get or create profile
|
||||
var profileId = db.filterProfileDao().getProfileByName(name, prefs.dataSource)?.id
|
||||
|
||||
if (profileId == null) {
|
||||
profileId = db.filterProfileDao().getNewId(prefs.dataSource)
|
||||
db.filterProfileDao().insert(FilterProfile(name, prefs.dataSource, profileId))
|
||||
@@ -85,6 +82,8 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
|
||||
|
||||
// set selected profile
|
||||
prefs.filterStatus = profileId
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun deleteCurrentProfile() {
|
||||
|
||||
@@ -6,30 +6,36 @@ import androidx.lifecycle.*
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.AvailabilityDetectorException
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.api.fronyx.FronyxApi
|
||||
import net.vonforst.evmap.api.fronyx.FronyxEvseIdResponse
|
||||
import net.vonforst.evmap.api.fronyx.FronyxStatus
|
||||
import net.vonforst.evmap.api.goingelectric.GEChargepoint
|
||||
import net.vonforst.evmap.api.goingelectric.GEReferenceData
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.openchargemap.OCMConnection
|
||||
import net.vonforst.evmap.api.openchargemap.OCMReferenceData
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
||||
import net.vonforst.evmap.storage.FilterProfile
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.cluster
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
@Parcelize
|
||||
data class MapPosition(val bounds: LatLngBounds, val zoom: Float) : Parcelable
|
||||
@@ -44,21 +50,43 @@ internal fun getClusterDistance(zoom: Float): Int? {
|
||||
|
||||
class MapViewModel(application: Application, private val state: SavedStateHandle) :
|
||||
AndroidViewModel(application) {
|
||||
val apiType: Class<ChargepointApi<ReferenceData>>
|
||||
get() = api.value!!.javaClass
|
||||
val apiName: String
|
||||
get() = api.value!!.getName()
|
||||
private val db = AppDatabase.getInstance(application)
|
||||
private val prefs = PreferenceDataSource(application)
|
||||
private val repo = ChargeLocationsRepository(
|
||||
createApi(prefs.dataSource, application),
|
||||
viewModelScope,
|
||||
db,
|
||||
prefs
|
||||
)
|
||||
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
private var api = MutableLiveData<ChargepointApi<ReferenceData>>().apply {
|
||||
value = createApi(prefs.dataSource, application)
|
||||
val apiId = repo.api.map { it.id }
|
||||
|
||||
init {
|
||||
// necessary so that apiId is updated
|
||||
apiId.observeForever { }
|
||||
}
|
||||
|
||||
val apiName = repo.api.map { it.name }
|
||||
|
||||
val bottomSheetState: MutableLiveData<Int> by lazy {
|
||||
state.getLiveData("bottomSheetState")
|
||||
}
|
||||
|
||||
val bottomSheetExpanded = MediatorLiveData<Boolean>().apply {
|
||||
addSource(bottomSheetState) {
|
||||
when (it) {
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED,
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN -> {
|
||||
value = false
|
||||
}
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_EXPANDED,
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT -> {
|
||||
value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
|
||||
val mapPosition: MutableLiveData<MapPosition> by lazy {
|
||||
state.getLiveData("mapPosition")
|
||||
}
|
||||
@@ -71,47 +99,28 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
}
|
||||
}
|
||||
private val filterValues: LiveData<List<FilterValue>> =
|
||||
private val filterValues: LiveData<List<FilterValue>?> = repo.api.switchMap {
|
||||
db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
|
||||
private val referenceData =
|
||||
Transformations.switchMap(api) { it.getReferenceData(viewModelScope, application) }
|
||||
private val filters = Transformations.map(referenceData) {
|
||||
api.value!!.getFilters(
|
||||
it,
|
||||
application.stringProvider()
|
||||
)
|
||||
}
|
||||
private val filters = repo.getFilters(application.stringProvider())
|
||||
|
||||
private val filtersWithValue: LiveData<FilterValues> by lazy {
|
||||
private val filtersWithValue: LiveData<FilterValues?> by lazy {
|
||||
filtersWithValue(filters, filterValues)
|
||||
}
|
||||
|
||||
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
|
||||
val filterProfiles: LiveData<List<FilterProfile>> = repo.api.switchMap {
|
||||
db.filterProfileDao().getProfiles(prefs.dataSource)
|
||||
}
|
||||
|
||||
val chargeCardMap: LiveData<Map<Long, ChargeCard>> by lazy {
|
||||
MediatorLiveData<Map<Long, ChargeCard>>().apply {
|
||||
value = null
|
||||
addSource(referenceData) { data ->
|
||||
value = if (data is GEReferenceData) {
|
||||
data.chargecards.map {
|
||||
it.id to it.convert()
|
||||
}.toMap()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val chargeCardMap = repo.chargeCardMap
|
||||
|
||||
val filtersCount: LiveData<Int> by lazy {
|
||||
MediatorLiveData<Int>().apply {
|
||||
value = 0
|
||||
addSource(filtersWithValue) { filtersWithValue ->
|
||||
value = filtersWithValue.count {
|
||||
value = filtersWithValue?.count {
|
||||
!it.value.hasSameValueAs(it.filter.defaultValue())
|
||||
}
|
||||
} ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,7 +130,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
value = Resource.loading(emptyList())
|
||||
// this is not automatically updated with mapPosition, as we only want to update
|
||||
// when map is idle.
|
||||
listOf(filtersWithValue, referenceData).forEach {
|
||||
listOf(filtersWithValue, repo.api).forEach {
|
||||
addSource(it) {
|
||||
reloadChargepoints()
|
||||
}
|
||||
@@ -141,28 +150,17 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
val chargerSparse: MutableLiveData<ChargeLocation> by lazy {
|
||||
state.getLiveData("chargerSparse")
|
||||
}
|
||||
val chargerDetails: MediatorLiveData<Resource<ChargeLocation>> by lazy {
|
||||
MediatorLiveData<Resource<ChargeLocation>>().apply {
|
||||
value = state["chargerDetails"]
|
||||
listOf(chargerSparse, referenceData).forEach {
|
||||
addSource(it) { _ ->
|
||||
val charger = chargerSparse.value
|
||||
val refData = referenceData.value
|
||||
if (charger != null && refData != null) {
|
||||
if (charger.id != value?.data?.id) {
|
||||
loadChargerDetails(charger, refData)
|
||||
}
|
||||
} else {
|
||||
value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
observeForever {
|
||||
// persist data in case fragment gets recreated
|
||||
state["chargerDetails"] = it
|
||||
}
|
||||
val chargerDetails: LiveData<Resource<ChargeLocation>> = chargerSparse.switchMap { charger ->
|
||||
charger?.id?.let {
|
||||
repo.getChargepointDetail(it)
|
||||
}
|
||||
}.apply {
|
||||
observeForever { chargerDetail ->
|
||||
// persist data in case fragment gets recreated
|
||||
state["chargerDetails"] = chargerDetail
|
||||
}
|
||||
}
|
||||
|
||||
val charger: MediatorLiveData<Resource<ChargeLocation>> by lazy {
|
||||
MediatorLiveData<Resource<ChargeLocation>>().apply {
|
||||
addSource(chargerDetails) {
|
||||
@@ -196,15 +194,15 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
val location: MutableLiveData<LatLng> by lazy {
|
||||
MutableLiveData<LatLng>()
|
||||
}
|
||||
val availability: MediatorLiveData<Resource<ChargeLocationStatus>> by lazy {
|
||||
MediatorLiveData<Resource<ChargeLocationStatus>>().apply {
|
||||
addSource(chargerSparse) { charger ->
|
||||
if (charger != null) {
|
||||
viewModelScope.launch {
|
||||
loadAvailability(charger)
|
||||
private val triggerAvailabilityRefresh = MutableLiveData<Boolean>(true)
|
||||
val availability: LiveData<Resource<ChargeLocationStatus>> by lazy {
|
||||
chargerSparse.switchMap { charger ->
|
||||
charger?.let {
|
||||
triggerAvailabilityRefresh.switchMap {
|
||||
liveData {
|
||||
emit(Resource.loading(null))
|
||||
emit(getAvailability(charger))
|
||||
}
|
||||
} else {
|
||||
value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,6 +228,123 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
addSource(filteredMinPower, callback)
|
||||
}
|
||||
}
|
||||
|
||||
val predictionApi = FronyxApi(application.getString(R.string.fronyx_key))
|
||||
|
||||
val prediction: LiveData<Resource<List<FronyxEvseIdResponse>>> by lazy {
|
||||
availability.switchMap { av ->
|
||||
if (!prefs.predictionEnabled) return@switchMap null
|
||||
|
||||
av.data?.evseIds?.let { evseIds ->
|
||||
liveData {
|
||||
emit(Resource.loading(null))
|
||||
|
||||
val charger = charger.value?.data ?: return@liveData
|
||||
val allEvseIds =
|
||||
evseIds.filterKeys {
|
||||
FronyxApi.isChargepointSupported(charger, it) &&
|
||||
filteredConnectors.value?.let { filtered ->
|
||||
equivalentPlugTypes(
|
||||
it.type
|
||||
).any { filtered.contains(it) }
|
||||
} ?: true
|
||||
}.flatMap { it.value }
|
||||
try {
|
||||
val result = predictionApi.getPredictionsForEvseIds(allEvseIds)
|
||||
if (result.size == allEvseIds.size) {
|
||||
emit(Resource.success(result))
|
||||
} else {
|
||||
emit(Resource.error("not all EVSEIDs found", null))
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
emit(Resource.error(e.message, null))
|
||||
e.printStackTrace()
|
||||
} catch (e: HttpException) {
|
||||
emit(Resource.error(e.message, null))
|
||||
e.printStackTrace()
|
||||
} catch (e: AvailabilityDetectorException) {
|
||||
emit(Resource.error(e.message, null))
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
} ?: liveData { emit(Resource.success(null)) }
|
||||
}
|
||||
}
|
||||
|
||||
val predictionGraph: LiveData<Map<ZonedDateTime, Int>?> by lazy {
|
||||
prediction.map {
|
||||
it.data?.let { responses ->
|
||||
if (responses.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
val evseIds = responses.map { it.evseId }
|
||||
val groupByTimestamp = responses.flatMap { response ->
|
||||
response.predictions.map {
|
||||
Triple(
|
||||
it.timestamp,
|
||||
response.evseId,
|
||||
it.status
|
||||
)
|
||||
}
|
||||
}
|
||||
.groupBy { it.first } // group by timestamp
|
||||
.mapValues { it.value.map { it.second to it.third } } // only keep EVSEID and status
|
||||
.filterValues { it.map { it.first } == evseIds } // remove values where status is not given for all EVSEs
|
||||
.filterKeys { it > ZonedDateTime.now() } // only show predictions in the future
|
||||
|
||||
groupByTimestamp.mapValues {
|
||||
it.value.count {
|
||||
it.second == FronyxStatus.UNAVAILABLE
|
||||
}
|
||||
}.ifEmpty { null }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val predictedChargepoints = charger.map {
|
||||
it.data?.let { charger ->
|
||||
charger.chargepoints.filter {
|
||||
FronyxApi.isChargepointSupported(charger, it) &&
|
||||
filteredConnectors.value?.let { filtered ->
|
||||
equivalentPlugTypes(it.type).any {
|
||||
filtered.contains(
|
||||
it
|
||||
)
|
||||
}
|
||||
} ?: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val predictionMaxValue: LiveData<Int> by lazy {
|
||||
predictedChargepoints.map {
|
||||
it?.sumOf { it.count } ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
val predictionDescription: LiveData<String?> by lazy {
|
||||
predictedChargepoints.map { predictedChargepoints ->
|
||||
if (predictedChargepoints == null) return@map null
|
||||
val allChargepoints = charger.value?.data?.chargepoints ?: return@map null
|
||||
|
||||
val predictedChargepointTypes = predictedChargepoints.map { it.type }.distinct()
|
||||
if (allChargepoints == predictedChargepoints) {
|
||||
null
|
||||
} else if (predictedChargepointTypes.size == 1) {
|
||||
application.getString(
|
||||
R.string.prediction_only,
|
||||
nameForPlugType(application.stringProvider(), predictedChargepointTypes[0])
|
||||
)
|
||||
} else {
|
||||
application.getString(
|
||||
R.string.prediction_only,
|
||||
application.getString(R.string.prediction_dc_plugs_only)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val myLocationEnabled: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>()
|
||||
}
|
||||
@@ -267,7 +382,9 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
|
||||
fun reloadPrefs() {
|
||||
filterStatus.value = prefs.filterStatus
|
||||
api.value = createApi(prefs.dataSource, getApplication())
|
||||
if (prefs.dataSource != apiId.value) {
|
||||
repo.api.value = createApi(prefs.dataSource, getApplication())
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleFilters() {
|
||||
@@ -307,8 +424,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
fun reloadChargepoints() {
|
||||
val pos = mapPosition.value ?: return
|
||||
val filters = filtersWithValue.value ?: return
|
||||
val referenceData = referenceData.value ?: return
|
||||
chargepointLoader(Triple(pos, filters, referenceData))
|
||||
chargepointLoader(pos to filters)
|
||||
}
|
||||
|
||||
private val miniMarkerThreshold = 13f
|
||||
@@ -335,17 +451,16 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
|
||||
private var chargepointsInternal: LiveData<Resource<List<ChargepointListItem>>>? = null
|
||||
private var chargepointLoader =
|
||||
throttleLatest(
|
||||
500L,
|
||||
viewModelScope
|
||||
) { data: Triple<MapPosition, FilterValues, ReferenceData> ->
|
||||
) { data: Pair<MapPosition, FilterValues> ->
|
||||
chargepoints.value = Resource.loading(chargepoints.value?.data)
|
||||
|
||||
val mapPosition = data.first
|
||||
val filters = data.second
|
||||
val api = api.value!!
|
||||
val refData = data.third
|
||||
|
||||
if (filterStatus.value == FILTERS_FAVORITES) {
|
||||
// load favorites from local DB
|
||||
@@ -368,96 +483,57 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
|
||||
return@throttleLatest
|
||||
}
|
||||
|
||||
if (api is GoingElectricApiWrapper) {
|
||||
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
|
||||
filteredChargeCards.value =
|
||||
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }
|
||||
.toSet()
|
||||
val result = repo.getChargepoints(mapPosition.bounds, mapPosition.zoom, filters)
|
||||
chargepointsInternal?.let { chargepoints.removeSource(it) }
|
||||
chargepointsInternal = result
|
||||
chargepoints.addSource(result) {
|
||||
val apiId = apiId.value
|
||||
when (apiId) {
|
||||
"going_electric" -> {
|
||||
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
|
||||
filteredChargeCards.value =
|
||||
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }
|
||||
.toSet()
|
||||
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
|
||||
filteredConnectors.value =
|
||||
if (connectorsVal.all) null else connectorsVal.values.map {
|
||||
GEChargepoint.convertTypeFromGE(it)
|
||||
}.toSet()
|
||||
filteredMinPower.value = filters.getSliderValue("min_power")
|
||||
} else if (api is OpenChargeMapApiWrapper) {
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
|
||||
filteredConnectors.value =
|
||||
if (connectorsVal.all) null else connectorsVal.values.map {
|
||||
OCMConnection.convertConnectionTypeFromOCM(
|
||||
it.toLong(),
|
||||
refData as OCMReferenceData
|
||||
)
|
||||
}.toSet()
|
||||
filteredMinPower.value = filters.getSliderValue("min_power")
|
||||
} else {
|
||||
filteredConnectors.value = null
|
||||
filteredMinPower.value = null
|
||||
filteredChargeCards.value = null
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
|
||||
filteredConnectors.value =
|
||||
if (connectorsVal.all) null else connectorsVal.values.map {
|
||||
GEChargepoint.convertTypeFromGE(it)
|
||||
}.toSet()
|
||||
filteredMinPower.value = filters.getSliderValue("min_power")
|
||||
}
|
||||
"open_charge_map" -> {
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
|
||||
filteredConnectors.value =
|
||||
if (connectorsVal.all) null else connectorsVal.values.map {
|
||||
OCMConnection.convertConnectionTypeFromOCM(
|
||||
it.toLong(),
|
||||
repo.referenceData.value!! as OCMReferenceData
|
||||
)
|
||||
}.toSet()
|
||||
filteredMinPower.value = filters.getSliderValue("min_power")
|
||||
}
|
||||
else -> {
|
||||
filteredConnectors.value = null
|
||||
filteredMinPower.value = null
|
||||
filteredChargeCards.value = null
|
||||
}
|
||||
}
|
||||
|
||||
chargepoints.value = it
|
||||
}
|
||||
|
||||
var result = api.getChargepoints(refData, mapPosition.bounds, mapPosition.zoom, filters)
|
||||
if (result.status == Status.ERROR && result.data == null) {
|
||||
// keep old results if new data could not be loaded
|
||||
result = Resource.error(result.message, chargepoints.value?.data)
|
||||
}
|
||||
|
||||
chargepoints.value = result
|
||||
}
|
||||
|
||||
private suspend fun loadAvailability(charger: ChargeLocation) {
|
||||
availability.value = Resource.loading(null)
|
||||
availability.value = getAvailability(charger)
|
||||
}
|
||||
|
||||
fun reloadAvailability() {
|
||||
val charger = chargerSparse.value ?: return
|
||||
viewModelScope.launch {
|
||||
loadAvailability(charger)
|
||||
}
|
||||
}
|
||||
|
||||
private var chargerLoadingTask: Job? = null
|
||||
|
||||
private fun loadChargerDetails(charger: ChargeLocation, referenceData: ReferenceData) {
|
||||
chargerDetails.value = Resource.loading(null)
|
||||
chargerLoadingTask?.cancel()
|
||||
chargerLoadingTask = viewModelScope.launch {
|
||||
try {
|
||||
val chargerDetail = api.value!!.getChargepointDetail(referenceData, charger.id)
|
||||
chargerDetails.value = chargerDetail
|
||||
if (favorites.value?.any { it.charger.id == chargerDetail.data?.id } == true) {
|
||||
// update data of stored favorite
|
||||
db.chargeLocationsDao().insert(charger)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
chargerDetails.value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
triggerAvailabilityRefresh.value = true
|
||||
}
|
||||
|
||||
fun loadChargerById(chargerId: Long) {
|
||||
chargerDetails.value = Resource.loading(null)
|
||||
chargerSparse.value = null
|
||||
referenceData.observeForever(object : Observer<ReferenceData> {
|
||||
override fun onChanged(refData: ReferenceData) {
|
||||
referenceData.removeObserver(this)
|
||||
viewModelScope.launch {
|
||||
val response = api.value!!.getChargepointDetail(refData, chargerId)
|
||||
chargerDetails.value = response
|
||||
if (response.status == Status.SUCCESS) {
|
||||
chargerSparse.value = response.data
|
||||
|
||||
if (response.data != null && favorites.value?.any { it.charger.id == response.data.id } == true) {
|
||||
// update data of stored favorite
|
||||
db.chargeLocationsDao().insert(response.data)
|
||||
}
|
||||
} else {
|
||||
chargerSparse.value = null
|
||||
}
|
||||
}
|
||||
repo.getChargepointDetail(chargerId).observeForever { response ->
|
||||
if (response.status == Status.SUCCESS) {
|
||||
chargerSparse.value = response.data
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,7 @@ import android.os.Parcelable
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.lifecycle.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.RawValue
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -107,4 +104,41 @@ fun <T> throttleLatest(
|
||||
waitingParam = param
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public suspend fun <T> LiveData<T>.await(): T {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val observer = object : Observer<T> {
|
||||
override fun onChanged(value: T?) {
|
||||
if (value == null) return
|
||||
removeObserver(this)
|
||||
continuation.resume(value, null)
|
||||
}
|
||||
}
|
||||
|
||||
observeForever(observer)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public suspend fun <T> LiveData<Resource<T>>.awaitFinished(): Resource<T> {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val observer = object : Observer<Resource<T>> {
|
||||
override fun onChanged(value: Resource<T>) {
|
||||
if (value.status != Status.LOADING) {
|
||||
removeObserver(this)
|
||||
continuation.resume(value, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observeForever(observer)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
8
app/src/main/res/drawable/bar_graph.xml
Normal file
8
app/src/main/res/drawable/bar_graph.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@android:color/black" />
|
||||
<corners
|
||||
android:topLeftRadius="2dp"
|
||||
android:topRightRadius="2dp" />
|
||||
</shape>
|
||||
10
app/src/main/res/drawable/ic_lightning.xml
Normal file
10
app/src/main/res/drawable/ic_lightning.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11,21h-1l1,-7H7.5c-0.58,0 -0.57,-0.32 -0.38,-0.66 0.19,-0.34 0.05,-0.08 0.07,-0.12C8.48,10.94 10.42,7.54 13,3h1l-1,7h3.5c0.49,0 0.56,0.33 0.47,0.51l-0.07,0.15C12.96,17.55 11,21 11,21z" />
|
||||
</vector>
|
||||
@@ -1,8 +1,8 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="233.8dp"
|
||||
android:height="368.4dp"
|
||||
android:viewportHeight="368.4"
|
||||
android:viewportWidth="233.8"
|
||||
android:viewportHeight="368.4">
|
||||
android:width="28dp"
|
||||
android:height="44.11976dp">
|
||||
<path
|
||||
android:pathData="M117,367.4c-0.4,-0.3 -0.8,-0.6 -1.2,-0.9c-1.6,-1.2 -3.1,-2.3 -4.2,-3.7c-2.9,-26.9 -9.6,-51.7 -20.1,-74c-12.4,-27.3 -30.1,-52.4 -47.1,-75.8c-8.7,-12 -19.8,-27.9 -28.8,-45.2C2.3,143.6 -2.1,115.9 3.2,89.9c4.3,-20.4 15,-40 30.3,-55.2C53.6,15.1 81.5,2.8 109.9,1l13.5,0c34.4,1.9 66.9,18.9 86.9,45.4c12.8,16.3 20.8,37.5 22.5,59.8l0,8c-0.7,38.8 -23.7,70.9 -45.9,101.9c-1.7,2.3 -3.3,4.6 -5,6.9c-24.4,34.5 -50.3,76.1 -57.3,123.3c-0.5,2 -0.7,4.3 -0.9,6.5C123.3,359 122.8,364.9 117,367.4z"
|
||||
android:fillColor="#FFFFFF" />
|
||||
|
||||
10
app/src/main/res/drawable/ic_parking.xml
Normal file
10
app/src/main/res/drawable/ic_parking.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector android:height="24dp"
|
||||
android:tint="#000000"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M13,3L6,3v18h4v-6h3c3.31,0 6,-2.69 6,-6s-2.69,-6 -6,-6zM13.2,11L10,11L10,7h3.2c1.1,0 2,0.9 2,2s-0.9,2 -2,2z" />
|
||||
</vector>
|
||||
36
app/src/main/res/drawable/ic_powered_by_fronyx.xml
Normal file
36
app/src/main/res/drawable/ic_powered_by_fronyx.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="566.9dp"
|
||||
android:height="254.9dp"
|
||||
android:viewportWidth="566.9"
|
||||
android:viewportHeight="254.9">
|
||||
<path
|
||||
android:pathData="M60.8,86.3c-5.6,0 -10,0.5 -13.3,1.4c-3.3,0.9 -5.7,2.7 -7.1,5.2c-0.4,0.6 -0.7,1.3 -0.9,2c-1.1,3.3 -1.4,6.8 -1.4,10.2c0,2.1 0,4.2 0,6.2c0,0.1 0,0.2 0,0.3h22.3v20.9H38.1v71.2v7.6H14.5v-7.6v-71.2H0v-20.9h14.5V104c0,-13.4 3.9,-23.8 11.2,-31.3c7.3,-7.4 19,-10.8 35.1,-10V86.3M157.1,161.2c0,-15.5 11.2,-27.3 25.7,-27.3c14.5,0 25.5,11.8 25.5,27.3c0,15.6 -11,27.3 -25.5,27.3C168.3,188.5 157.1,176.9 157.1,161.2M182.8,110.1c-27.9,0 -49.7,22.2 -49.7,51.1c0,29.1 21.8,51.3 49.7,51.3c27.9,0 49.4,-22.2 49.4,-51.3C232.1,132.3 210.7,110.1 182.8,110.1M541.4,161.5c14.1,0 25.6,-11.5 25.6,-25.6c0,-14.1 -11.5,-25.6 -25.6,-25.6c-14.1,0 -25.6,11.5 -25.6,25.6C515.8,150 527.2,161.5 541.4,161.5M129.6,110.5c-2.2,-0.2 -4.3,-0.4 -6.6,-0.4c-4.4,0 -9.1,0.6 -13.9,1.9c-4.8,1.3 -8.5,3.4 -10.7,6.5v-7H74.7V211h23.8v-58.2c0,-5.4 1.4,-9.5 4.1,-12.3c4.4,-4.5 10.6,-6.5 16.7,-6.8c1.7,-0.1 3.5,0 5.3,0.1c1.6,0.2 3.2,0.5 4.7,0.7c0,-1 0,-2 0,-3c0,-2 0,-3.9 0,-5.9c0,-2.2 0,-4.5 0,-6.7c0,-1.9 0.2,-3.8 0.2,-5.7c0,-0.9 0.2,-1.7 0.2,-2.6C129.6,110.6 129.6,110.5 129.6,110.5zM475.8,160.6l29.7,-49h-28.7l-16.3,31.7l-16.5,-31.7H415l30.1,49l-30.7,50.6h28.7l17.3,-33.1l17.7,33.1h28.7L475.8,160.6zM356.9,254.8c14.9,0.5 26.7,-3.1 34.7,-10.9c7.6,-7.4 11.6,-18.1 12,-33l0,0v-2.4v-97.1h-24.5c0,0 0,14.3 0,28.8v14.9v0.1v14.5c0,5.3 -1.2,9.7 -5.8,13.8c-4.3,3.9 -10.9,5.1 -15.4,5.1c-2.7,0 -10.7,-0.9 -15.4,-7.5c-2.4,-3.4 -3.9,-9.1 -4.2,-14c-0.3,-5 -0.3,-7.4 -0.6,-14.7c-1.1,-30.2 -26,-47.6 -55,-40.3c-5,1.3 -9,3.4 -11.2,6.5v-7.2h-23.8V211h23.8v-52v-6c0,-5.4 1.5,-9.5 4.3,-12.3c2.8,-2.7 5.8,-4.2 9.5,-5.1c3.7,-0.9 6.6,-1 8.5,-1c2.9,0 6.5,0.8 8,1.2c2.8,0.8 6.3,3.3 8.3,6.2c3.7,5.3 3.3,12.8 3.5,18.9c0.2,7.6 0.4,15.3 2.7,22.7c3.2,10.3 9.7,20 19.7,24.6c9,4.1 20,5.2 29.7,3.4c3,-0.6 6.3,-1.5 9.1,-3c1.6,-0.9 3.2,-1.9 4.5,-3.2c0.4,10.7 0.6,17.6 -6,22.5c-1.8,1.4 -4.3,2.5 -6.5,2.9c-3.1,0.6 -6.5,1 -9.7,0.9L356.9,254.8z"
|
||||
android:fillColor="#000044" />
|
||||
<path
|
||||
android:pathData="M97.6,89.8V39.2c0,-1.6 -0.1,-3.2 -0.2,-4.8c-0.1,-1.7 -0.3,-3.3 -0.5,-4.9h6.6l0.9,10h-1c0.9,-3.3 2.7,-5.9 5.5,-7.9c2.7,-1.9 6,-2.9 9.8,-2.9c3.8,0 7.1,0.9 9.9,2.6c2.8,1.7 4.9,4.2 6.5,7.5c1.6,3.2 2.4,7.2 2.4,11.8c0,4.5 -0.8,8.4 -2.3,11.7c-1.5,3.2 -3.7,5.8 -6.5,7.5c-2.8,1.8 -6.1,2.6 -9.9,2.6c-3.8,0 -7,-1 -9.7,-2.9c-2.7,-1.9 -4.6,-4.5 -5.5,-7.8h0.9v28.1H97.6zM117.3,66.8c4,0 7.2,-1.4 9.6,-4.2c2.4,-2.8 3.6,-6.8 3.6,-12.1c0,-5.4 -1.2,-9.5 -3.6,-12.2c-2.4,-2.8 -5.6,-4.2 -9.6,-4.2c-4,0 -7.2,1.4 -9.5,4.2c-2.4,2.8 -3.6,6.8 -3.6,12.2c0,5.3 1.2,9.4 3.6,12.1C110.2,65.4 113.4,66.8 117.3,66.8z"
|
||||
android:fillColor="#000044" />
|
||||
<path
|
||||
android:pathData="M165.4,72.4c-4.1,0 -7.6,-0.9 -10.6,-2.6c-3,-1.8 -5.3,-4.3 -6.9,-7.6c-1.6,-3.3 -2.4,-7.2 -2.4,-11.6c0,-4.5 0.8,-8.4 2.4,-11.7c1.6,-3.2 3.9,-5.8 6.9,-7.5c3,-1.8 6.5,-2.6 10.5,-2.6c4.1,0 7.6,0.9 10.6,2.6c3,1.8 5.3,4.3 7,7.5c1.7,3.2 2.5,7.1 2.5,11.7c0,4.5 -0.8,8.4 -2.5,11.6c-1.7,3.3 -4,5.8 -7,7.6C172.9,71.5 169.4,72.4 165.4,72.4zM165.4,66.8c4,0 7.1,-1.4 9.5,-4.2c2.4,-2.8 3.5,-6.8 3.5,-12.1c0,-5.4 -1.2,-9.5 -3.6,-12.2c-2.4,-2.8 -5.6,-4.2 -9.5,-4.2c-4,0 -7.1,1.4 -9.5,4.2c-2.4,2.8 -3.5,6.8 -3.5,12.2c0,5.3 1.2,9.4 3.5,12.1C158.2,65.4 161.4,66.8 165.4,66.8z"
|
||||
android:fillColor="#000044" />
|
||||
<path
|
||||
android:pathData="M207.2,71.6l-15.6,-42.2h7.2l13,37.1h-2.2l13.4,-37.1h5.9l13.2,37.1h-2.1l13.1,-37.1h6.9l-15.7,42.2h-6.6l-13.6,-37.7h3.4l-13.7,37.7H207.2z"
|
||||
android:fillColor="#000044" />
|
||||
<path
|
||||
android:pathData="M287.7,72.4c-6.6,0 -11.8,-1.9 -15.6,-5.8c-3.8,-3.8 -5.7,-9.2 -5.7,-16c0,-4.4 0.8,-8.3 2.5,-11.5c1.7,-3.3 4,-5.8 7.1,-7.6c3,-1.8 6.5,-2.7 10.4,-2.7c3.9,0 7.1,0.8 9.7,2.4c2.6,1.6 4.6,3.9 6,6.9c1.4,3 2.1,6.5 2.1,10.6v2.5h-32.7v-4.3h28.2l-1.4,1.1c0,-4.5 -1,-8 -3,-10.5c-2,-2.5 -5,-3.8 -9,-3.8c-4.2,0 -7.5,1.5 -9.8,4.4C274.2,41.1 273,45 273,50v0.8c0,5.3 1.3,9.3 3.9,12c2.6,2.7 6.3,4.1 11,4.1c2.5,0 4.9,-0.4 7.1,-1.1c2.2,-0.8 4.3,-2 6.3,-3.7l2.4,4.8c-1.8,1.8 -4.2,3.2 -7,4.2C293.8,71.9 290.8,72.4 287.7,72.4z"
|
||||
android:fillColor="#000044" />
|
||||
<path
|
||||
android:pathData="M314.5,71.6v-32c0,-1.7 0,-3.4 -0.1,-5.1c-0.1,-1.7 -0.2,-3.4 -0.4,-5h6.6l0.8,10.2l-1.2,0.1c0.6,-2.5 1.5,-4.6 2.9,-6.2c1.4,-1.6 3.1,-2.8 5,-3.7c1.9,-0.8 3.9,-1.2 6,-1.2c0.8,0 1.6,0 2.2,0.1c0.6,0.1 1.2,0.2 1.8,0.4l-0.1,6c-0.8,-0.3 -1.6,-0.5 -2.3,-0.5s-1.5,-0.1 -2.4,-0.1c-2.5,0 -4.6,0.6 -6.4,1.8c-1.8,1.2 -3.2,2.7 -4.1,4.5s-1.4,3.8 -1.4,5.9v24.9H314.5z"
|
||||
android:fillColor="#000044" />
|
||||
<path
|
||||
android:pathData="M363.9,72.4c-6.6,0 -11.8,-1.9 -15.6,-5.8c-3.8,-3.8 -5.7,-9.2 -5.7,-16c0,-4.4 0.8,-8.3 2.5,-11.5c1.7,-3.3 4,-5.8 7.1,-7.6c3,-1.8 6.5,-2.7 10.4,-2.7c3.9,0 7.1,0.8 9.7,2.4c2.6,1.6 4.6,3.9 6,6.9c1.4,3 2.1,6.5 2.1,10.6v2.5h-32.7v-4.3H376l-1.4,1.1c0,-4.5 -1,-8 -3,-10.5c-2,-2.5 -5,-3.8 -9,-3.8c-4.2,0 -7.5,1.5 -9.8,4.4c-2.4,2.9 -3.5,6.9 -3.5,11.9v0.8c0,5.3 1.3,9.3 3.9,12c2.6,2.7 6.3,4.1 11,4.1c2.5,0 4.9,-0.4 7.1,-1.1c2.2,-0.8 4.3,-2 6.3,-3.7l2.4,4.8c-1.8,1.8 -4.2,3.2 -7,4.2C370,71.9 367,72.4 363.9,72.4z"
|
||||
android:fillColor="#000044" />
|
||||
<path
|
||||
android:pathData="M406.8,72.4c-3.7,0 -6.9,-0.9 -9.7,-2.6c-2.8,-1.8 -5,-4.3 -6.5,-7.5c-1.5,-3.2 -2.3,-7.1 -2.3,-11.7c0,-4.6 0.8,-8.5 2.3,-11.8c1.5,-3.2 3.7,-5.7 6.5,-7.5c2.8,-1.7 6,-2.6 9.7,-2.6c3.8,0 7.1,1 9.9,2.9c2.8,1.9 4.6,4.5 5.6,7.7h-1V9.8h6.8v61.8h-6.7V61.5h0.9c-0.9,3.4 -2.7,6 -5.5,7.9C413.9,71.4 410.6,72.4 406.8,72.4zM408.2,66.8c4,0 7.2,-1.4 9.6,-4.2c2.4,-2.8 3.6,-6.8 3.6,-12.1c0,-5.4 -1.2,-9.5 -3.6,-12.2c-2.4,-2.8 -5.6,-4.2 -9.6,-4.2c-4,0 -7.2,1.4 -9.5,4.2c-2.4,2.8 -3.6,6.8 -3.6,12.2c0,5.3 1.2,9.4 3.6,12.1C401.1,65.4 404.3,66.8 408.2,66.8z"
|
||||
android:fillColor="#000044" />
|
||||
<path
|
||||
android:pathData="M484.3,72.4c-3.8,0 -7.1,-1 -9.8,-2.9c-2.7,-1.9 -4.6,-4.6 -5.5,-7.9h0.9v10.1h-6.7V9.8h6.8v29.5h-1c1,-3.2 2.8,-5.8 5.5,-7.7c2.7,-1.9 6,-2.9 9.8,-2.9c3.8,0 7.1,0.9 9.9,2.6c2.8,1.8 4.9,4.3 6.5,7.5c1.5,3.2 2.3,7.1 2.3,11.7s-0.8,8.4 -2.4,11.7c-1.6,3.2 -3.7,5.8 -6.5,7.5C491.4,71.5 488.1,72.4 484.3,72.4zM482.9,66.8c4,0 7.2,-1.4 9.6,-4.1c2.4,-2.7 3.6,-6.8 3.6,-12.2s-1.2,-9.5 -3.6,-12.2c-2.4,-2.8 -5.6,-4.2 -9.6,-4.2c-4,0 -7.2,1.4 -9.5,4.2c-2.4,2.8 -3.6,6.8 -3.6,12.2c0,5.3 1.2,9.4 3.6,12.1C475.8,65.4 478.9,66.8 482.9,66.8z"
|
||||
android:fillColor="#000044" />
|
||||
<path
|
||||
android:pathData="M511.9,90.7l-1.6,-5.6c2.6,-0.6 4.8,-1.3 6.6,-2.1c1.8,-0.8 3.2,-1.9 4.4,-3.2c1.2,-1.3 2.2,-3 3,-5l2.2,-5l-0.2,2.9l-18.4,-43.1h7.4l15.2,37h-2.2l15,-37h7.1L531,75.1c-1.1,2.7 -2.4,4.9 -3.7,6.8c-1.3,1.8 -2.8,3.3 -4.3,4.5c-1.5,1.1 -3.2,2.1 -5.1,2.7S514.1,90.3 511.9,90.7z"
|
||||
android:fillColor="#000044" />
|
||||
</vector>
|
||||
@@ -5,6 +5,10 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="java.util.Map" />
|
||||
|
||||
<import type="java.time.ZonedDateTime" />
|
||||
|
||||
<import type="net.vonforst.evmap.model.ChargeLocation" />
|
||||
|
||||
<import type="net.vonforst.evmap.model.Chargepoint" />
|
||||
@@ -39,6 +43,18 @@
|
||||
name="availability"
|
||||
type="Resource<ChargeLocationStatus>" />
|
||||
|
||||
<variable
|
||||
name="predictionGraph"
|
||||
type="Map<ZonedDateTime, Integer>" />
|
||||
|
||||
<variable
|
||||
name="predictionMaxValue"
|
||||
type="Integer" />
|
||||
|
||||
<variable
|
||||
name="predictionDescription"
|
||||
type="String" />
|
||||
|
||||
<variable
|
||||
name="filteredAvailability"
|
||||
type="Resource<ChargeLocationStatus>" />
|
||||
@@ -61,7 +77,8 @@
|
||||
|
||||
</data>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="@dimen/detail_corner_radius"
|
||||
@@ -80,15 +97,15 @@
|
||||
android:id="@+id/txtName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="30dp"
|
||||
android:ellipsize="end"
|
||||
android:hyphenationFrequency="normal"
|
||||
android:maxLines="@{expanded ? 3 : 1}"
|
||||
android:text="@{charger.data.name}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toStartOf="@+id/txtAvailability"
|
||||
app:layout_constraintEnd_toStartOf="@+id/imgFaultReport"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Parkhaus" />
|
||||
@@ -136,8 +153,8 @@
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="@android:color/white"
|
||||
app:backgroundTintAvailability="@{BindingAdaptersKt.flatten(filteredAvailability.data.status.values())}"
|
||||
app:invisibleUnlessAnimated="@{filteredAvailability.data != null && !expanded}"
|
||||
app:invisibleUnless="@{filteredAvailability.data != null && !expanded}"
|
||||
app:invisibleUnlessAnimated="@{filteredAvailability.data != null && !expanded}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintTop_toTopOf="@+id/txtName"
|
||||
tools:backgroundTint="@color/available"
|
||||
@@ -175,10 +192,11 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/connectors"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnRefreshLiveData"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtConnectors" />
|
||||
|
||||
@@ -201,10 +219,10 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@{charger.data.amenities}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:autoLink="web"
|
||||
android:linksClickable="true"
|
||||
android:text="@{charger.data.amenities}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
app:goneUnless="@{charger.data.amenities != null}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
@@ -231,10 +249,10 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@{charger.data.generalInformation}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:autoLink="web"
|
||||
android:linksClickable="true"
|
||||
android:text="@{charger.data.generalInformation}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
app:goneUnless="@{charger.data.generalInformation != null}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
@@ -260,13 +278,12 @@
|
||||
android:id="@+id/details"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:data="@{DetailsAdapterKt.buildDetails(charger.data, chargeCards, filteredChargeCards, context)}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btnChargeprice"
|
||||
app:layout_constraintTop_toBottomOf="@+id/divider3"
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/item_detail" />
|
||||
|
||||
@@ -287,17 +304,17 @@
|
||||
android:text="@{@string/source(apiName)}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView4" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView4"
|
||||
tools:text="Source: DataSource" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView13"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="right|end"
|
||||
android:text="@{availability.status == Status.SUCCESS ? @string/realtime_data_source(availability.data.source) : availability.status == Status.LOADING ? @string/realtime_data_loading : @string/realtime_data_unavailable}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnRefreshLiveData"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/connectors"
|
||||
tools:text="Echtzeitdaten nicht verfügbar" />
|
||||
@@ -306,8 +323,8 @@
|
||||
android:id="@+id/topPart"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:text="TextView"
|
||||
android:layout_marginBottom="-10dp"
|
||||
android:text="TextView"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
@@ -321,43 +338,138 @@
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/go_to_chargeprice"
|
||||
android:transitionName="@string/shared_element_chargeprice"
|
||||
app:goneUnless="@{charger.data != null && charger.data.chargepriceData != null && charger.data.chargepriceData.country != null && ChargepriceApi.isCountrySupported(charger.data.chargepriceData.country, charger.data.dataSource)}"
|
||||
app:goneUnless="@{charger.data != null && ChargepriceApi.isChargerSupported(charger.data)}"
|
||||
app:icon="@drawable/ic_chargeprice"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/divider1" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="?android:attr/listDivider"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView13" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider3"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="?android:attr/listDivider"
|
||||
app:goneUnless="@{charger.data != null && ChargepriceApi.isChargerSupported(charger.data)}"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btnChargeprice" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView8"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/utilization_prediction"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:goneUnless="@{predictionGraph != null}"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/divider2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView29"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{predictionDescription}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:goneUnless="@{predictionGraph != null}"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/textView8"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnPredictionHelp"
|
||||
app:layout_constraintStart_toEndOf="@+id/textView8"
|
||||
tools:text="(DC plugs only)" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnPredictionHelp"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/help"
|
||||
app:goneUnless="@{predictionGraph != null}"
|
||||
app:icon="@drawable/ic_help"
|
||||
app:iconTint="?android:textColorSecondary"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/textView8"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/textView8" />
|
||||
|
||||
<net.vonforst.evmap.ui.BarGraphView
|
||||
android:id="@+id/prediction"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="100dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:data="@{predictionGraph}"
|
||||
app:goneUnless="@{predictionGraph != null}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView8"
|
||||
app:maxValue="@{predictionMaxValue}"
|
||||
tools:itemCount="3"
|
||||
tools:layoutManager="LinearLayoutManager"
|
||||
tools:listitem="@layout/item_connector"
|
||||
tools:orientation="horizontal" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgPredictionSource"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:scaleType="fitCenter"
|
||||
app:goneUnless="@{predictionGraph != null}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/prediction"
|
||||
app:srcCompat="@drawable/ic_powered_by_fronyx"
|
||||
app:tint="@color/logo_tint_night" />
|
||||
|
||||
<View
|
||||
android:id="@+id/divider1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="?android:attr/listDivider"
|
||||
app:goneUnless="@{predictionGraph != null}"
|
||||
app:layout_constraintTop_toBottomOf="@+id/imgPredictionSource" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgVerified"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="@string/verified"
|
||||
app:tooltipTextCompat="@{@string/verified_desc(apiName)}"
|
||||
app:goneUnless="@{ charger.data.verified }"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/txtName"
|
||||
app:layout_constraintEnd_toStartOf="@+id/txtAvailability"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toEndOf="@+id/imgFaultReport"
|
||||
app:layout_constraintTop_toTopOf="@+id/txtName"
|
||||
app:srcCompat="@drawable/ic_verified"
|
||||
app:tint="@color/available"
|
||||
app:tooltipTextCompat="@{@string/verified_desc(apiName)}"
|
||||
tools:targetApi="o" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgFaultReport"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:contentDescription="@string/fault_report"
|
||||
app:tooltipTextCompat="@{@string/fault_report}"
|
||||
app:goneUnless="@{ charger.data.faultReport != null }"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/txtName"
|
||||
app:layout_constraintEnd_toStartOf="@+id/imgVerified"
|
||||
app:layout_constraintStart_toEndOf="@+id/txtName"
|
||||
app:layout_constraintTop_toTopOf="@+id/txtName"
|
||||
app:srcCompat="@drawable/ic_map_marker_fault"
|
||||
app:tooltipTextCompat="@{@string/fault_report}"
|
||||
tools:targetApi="o" />
|
||||
|
||||
<TextView
|
||||
@@ -365,31 +477,31 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:breakStrategy="balanced"
|
||||
android:text="@{charger.data.license}"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textStyle="italic"
|
||||
android:text="@{charger.data.license}"
|
||||
android:breakStrategy="balanced"
|
||||
app:goneUnless="@{charger.data.license != null}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/sourceButton"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:text="The data is provided under the National Oman Open Data LicensE (NOODLE), Version 3.14, and may be used for any purpose whatsoever." />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnRefreshLiveData"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Widget.App.Button.OutlinedButton.IconOnly.Small"
|
||||
android:contentDescription="@string/refresh_live_data"
|
||||
android:enabled="@{availability.status != Status.LOADING}"
|
||||
app:icon="@drawable/ic_refresh"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/textView13"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/connectors" />
|
||||
app:iconTint="?android:textColorSecondary"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/textView7"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/textView7" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</layout>
|
||||
@@ -14,7 +14,7 @@
|
||||
type="ChargepriceViewModel" />
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/linearLayout5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -54,192 +54,87 @@
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/charge_prices_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar_container"
|
||||
tools:itemCount="1"
|
||||
tools:listitem="@layout/fragment_chargeprice_preview" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar5"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="280dp"
|
||||
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.LOADING || vm.vehicles.status == Status.LOADING}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/charge_prices_list"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/chargeprice_select_connector"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/vehicle_selection" />
|
||||
<TextView
|
||||
android:id="@+id/textView8"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="280dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_no_tariffs_found"
|
||||
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS && vm.chargePricesForChargepoint.data.size() == 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/connectors_list"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:data="@{vm.charger.chargepointsMerged}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
tools:itemCount="3"
|
||||
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_connector_button"
|
||||
tools:orientation="horizontal" />
|
||||
<TextView
|
||||
android:id="@+id/textView9"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="280dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_no_compatible_connectors"
|
||||
app:goneUnless="@{vm.noCompatibleConnectors}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@{String.format(@string/chargeprice_battery_range, vm.batteryRange[0], vm.batteryRange[1])}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/connectors_list"
|
||||
tools:text="Charge from 20% to 80%" />
|
||||
<TextView
|
||||
android:id="@+id/textView3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="280dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_select_car_first"
|
||||
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS && vm.vehicles.data.size() == 0}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnSettings"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSettings"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/settings"
|
||||
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS && vm.vehicles.data.size() == 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView3" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView4"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{@string/chargeprice_stats(vm.chargepriceMetaForChargepoint.data.energy, BindingAdaptersKt.time((int) Math.round(vm.chargepriceMetaForChargepoint.data.duration)), vm.chargepriceMetaForChargepoint.data.energy / vm.chargepriceMetaForChargepoint.data.duration * 60)}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:invisibleUnlessAnimated="@{!vm.batteryRangeSliderDragging && vm.chargepriceMetaForChargepoint.status == Status.SUCCESS}"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView2"
|
||||
tools:text="(18 kWh, approx. 23 min, ⌀ 50 kW)" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvVehicleHeader"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/chargeprice_vehicle"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:goneUnless="@{vm.vehicles.data != null && vm.vehicles.data.size() > 1}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/vehicle_selection"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvVehicleHeader"
|
||||
app:data="@{vm.vehicles.data}"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:goneUnless="@{vm.vehicles.data != null && vm.vehicles.data.size() > 1}"
|
||||
android:orientation="horizontal"
|
||||
tools:listitem="@layout/item_chargeprice_vehicle_chip" />
|
||||
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/battery_range"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:valueFrom="0.0"
|
||||
android:valueTo="100.0"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView4"
|
||||
app:values="@={vm.batteryRange}" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/charge_prices_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:data="@{vm.chargePricesForChargepoint.data}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/battery_range"
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/item_chargeprice" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView8"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_no_tariffs_found"
|
||||
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS && vm.chargePricesForChargepoint.data.size() == 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView9"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_no_compatible_connectors"
|
||||
app:goneUnless="@{vm.noCompatibleConnectors}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_select_car_first"
|
||||
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS && vm.vehicles.data.size() == 0}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnSettings"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar5"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.LOADING || vm.vehicles.status == Status.LOADING}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/charge_prices_list"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSettings"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/settings"
|
||||
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS && vm.vehicles.data.size() == 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView3" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
|
||||
119
app/src/main/res/layout/fragment_chargeprice_header.xml
Normal file
119
app/src/main/res/layout/fragment_chargeprice_header.xml
Normal file
@@ -0,0 +1,119 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.ChargepriceViewModel" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.Status" />
|
||||
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<variable
|
||||
name="vm"
|
||||
type="ChargepriceViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/chargeprice_select_connector"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/vehicle_selection" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/connectors_list"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:data="@{vm.charger.chargepointsMerged}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
tools:itemCount="3"
|
||||
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_connector_button"
|
||||
tools:orientation="horizontal" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@{String.format(@string/chargeprice_battery_range, vm.batteryRange[0], vm.batteryRange[1])}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/connectors_list"
|
||||
tools:text="Charge from 20% to 80%" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView4"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{@string/chargeprice_stats(vm.chargepriceMetaForChargepoint.data.energy, BindingAdaptersKt.time((int) Math.round(vm.chargepriceMetaForChargepoint.data.duration)), vm.chargepriceMetaForChargepoint.data.energy / vm.chargepriceMetaForChargepoint.data.duration * 60)}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:invisibleUnlessAnimated="@{!vm.batteryRangeSliderDragging && vm.chargepriceMetaForChargepoint.status == Status.SUCCESS}"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView2"
|
||||
tools:text="(18 kWh, approx. 23 min, ⌀ 50 kW)" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvVehicleHeader"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/chargeprice_vehicle"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:textColor="?colorPrimary"
|
||||
app:goneUnless="@{vm.vehicles.data != null && vm.vehicles.data.size() > 1}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/vehicle_selection"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvVehicleHeader"
|
||||
app:data="@{vm.vehicles.data}"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:goneUnless="@{vm.vehicles.data != null && vm.vehicles.data.size() > 1}"
|
||||
android:orientation="horizontal"
|
||||
tools:listitem="@layout/item_chargeprice_vehicle_chip" />
|
||||
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/battery_range"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:valueFrom="0.0"
|
||||
android:valueTo="100.0"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView4"
|
||||
app:values="@={vm.batteryRange}" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
18
app/src/main/res/layout/fragment_chargeprice_preview.xml
Normal file
18
app/src/main/res/layout/fragment_chargeprice_preview.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/fragment_chargeprice_header" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
|
||||
<include layout="@layout/item_chargeprice" />
|
||||
</LinearLayout>
|
||||
@@ -19,7 +19,8 @@
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/map"
|
||||
@@ -142,7 +143,7 @@
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/gallery_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="@dimen/map_toolbar_width"
|
||||
android:layout_height="@dimen/gallery_height_with_margin"
|
||||
android:background="?android:colorBackground"
|
||||
app:layout_behavior="@string/BackDropBottomSheetBehavior">
|
||||
@@ -174,11 +175,11 @@
|
||||
app:backgroundTint="?android:colorBackground"
|
||||
app:borderWidth="0dp"
|
||||
app:isFabActive="@{ vm.myLocationEnabled }"
|
||||
app:layout_behavior="net.vonforst.evmap.ui.HideOnScrollFabBehavior" />
|
||||
app:layout_behavior="@string/hide_on_scroll_fab_behavior" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/bottom_sheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="@dimen/map_toolbar_width"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
android:orientation="vertical"
|
||||
@@ -187,6 +188,7 @@
|
||||
app:behavior_peekHeight="@dimen/peek_height"
|
||||
app:bottomsheetbehavior_defaultState="stateHidden"
|
||||
app:layout_behavior="@string/BottomSheetBehaviorGoogleMapsLike"
|
||||
android:clipToPadding="false"
|
||||
tools:bottomsheetbehavior_defaultState="stateCollapsed">
|
||||
|
||||
<include
|
||||
@@ -195,10 +197,13 @@
|
||||
app:charger="@{vm.charger}"
|
||||
app:availability="@{vm.availability}"
|
||||
app:filteredAvailability="@{vm.filteredAvailability}"
|
||||
app:predictionGraph="@{vm.predictionGraph}"
|
||||
app:predictionMaxValue="@{vm.predictionMaxValue}"
|
||||
app:predictionDescription="@{vm.predictionDescription}"
|
||||
app:chargeCards="@{vm.chargeCardMap}"
|
||||
app:filteredChargeCards="@{vm.filteredChargeCards}"
|
||||
app:distance="@{vm.chargerDistance}"
|
||||
app:expanded="@{vm.bottomSheetState != BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED && vm.bottomSheetState != BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN}"
|
||||
app:expanded="@{vm.bottomSheetExpanded}"
|
||||
app:apiName="@{vm.apiName}" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@@ -211,6 +216,7 @@
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_directions"
|
||||
android:translationX="@dimen/directions_fab_translationx"
|
||||
app:layout_anchor="@id/bottom_sheet"
|
||||
app:layout_anchorGravity="top|right|end"
|
||||
app:layout_behavior="@string/ScrollAwareFABBehavior"
|
||||
@@ -218,7 +224,7 @@
|
||||
|
||||
<com.mahc.custombottomsheetbehavior.MergedAppBarLayout
|
||||
android:id="@+id/detail_app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="@dimen/map_toolbar_width"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="@string/MergedAppBarLayoutBehavior"
|
||||
android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar" />
|
||||
@@ -237,7 +243,7 @@
|
||||
app:borderWidth="0dp"
|
||||
app:fabSize="mini"
|
||||
app:srcCompat="@drawable/ic_layers"
|
||||
app:layout_behavior="net.vonforst.evmap.ui.HideOnExpandFabBehavior"
|
||||
app:layout_behavior="@string/hide_on_scroll_fab_behavior"
|
||||
android:theme="@style/NoElevationOverlay" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="end"
|
||||
android:text="@{String.format(@string/charge_price_format, item.chargepointPrices.get(0).price, BindingAdaptersKt.currency(item.currency))}"
|
||||
android:text="@{item.chargepointPrices.get(0).price != null ? String.format(@string/charge_price_format, item.chargepointPrices.get(0).price, BindingAdaptersKt.currency(item.currency)) : @string/chargeprice_price_not_available}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintBottom_toTopOf="@+id/txtAveragePrice"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@@ -125,8 +125,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:gravity="end"
|
||||
android:text="@{String.format(item.chargepointPrices.get(0).priceDistribution.isOnlyKwh ? @string/charge_price_kwh_format : @string/charge_price_average_format, item.chargepointPrices.get(0).price / meta.energy, BindingAdaptersKt.currency(item.currency))}"
|
||||
app:goneUnless="@{item.chargepointPrices.get(0).price > 0}"
|
||||
android:text="@{item.chargepointPrices.get(0).price != null ? String.format(item.chargepointPrices.get(0).priceDistribution.isOnlyKwh ? @string/charge_price_kwh_format : @string/charge_price_average_format, item.chargepointPrices.get(0).price / meta.energy, BindingAdaptersKt.currency(item.currency)) : item.chargepointPrices.get(0).noPriceReason}"
|
||||
app:goneUnless="@{item.chargepointPrices.get(0).price > 0 || item.chargepointPrices.get(0).price == null}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:layout_constraintBottom_toTopOf="@id/txtPriceDetails"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@@ -162,7 +162,7 @@
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="8dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:goneUnless="@{item.branding.logoUrl != null}"
|
||||
app:invisibleUnless="@{item.branding.logoUrl != null}"
|
||||
app:imageUrl="@{item.branding.logoUrl}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline5"
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="38dp"
|
||||
android:layout_marginTop="38dp"
|
||||
android:text="@{String.format("× %d", item.chargepoint.count)}"
|
||||
android:text="@{String.format("\u00D7 %d", item.chargepoint.count)}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
app:layout_constraintStart_toStartOf="@+id/imageView"
|
||||
app:layout_constraintTop_toTopOf="@+id/imageView"
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<string name="app_name">EVMap</string>
|
||||
<string name="title_activity_maps">EVMap</string>
|
||||
<string name="connectors">Anschlüsse</string>
|
||||
<string name="no_maps_app_found">Keine Navigations-App gefunden</string>
|
||||
<string name="no_browser_app_found">Kein Webbrowser gefunden</string>
|
||||
<string name="no_maps_app_found">Bitte installiere eine Navigations-App</string>
|
||||
<string name="no_browser_app_found">Bitte installiere einen Webbrowser</string>
|
||||
<string name="address">Adresse</string>
|
||||
<string name="operator">Betreiber</string>
|
||||
<string name="network">Verbund</string>
|
||||
@@ -23,8 +23,8 @@
|
||||
<string name="charging_paid">Kostenpflichtig</string>
|
||||
<string name="parking_free">Kostenlos</string>
|
||||
<string name="parking_paid">Kostenpflichtig</string>
|
||||
<string name="amenities">Ladeweile</string>
|
||||
<string name="general_info">Allgemeine Hinweise</string>
|
||||
<string name="amenities">Ausstattung</string>
|
||||
<string name="general_info">Allgemein</string>
|
||||
<string name="realtime_data_unavailable">Echtzeitstatus nicht verfügbar</string>
|
||||
<string name="realtime_data_loading">Prüfe Echtzeitstatus…</string>
|
||||
<string name="realtime_data_source">Quelle Echtzeitdaten (beta): %s</string>
|
||||
@@ -37,7 +37,7 @@
|
||||
<string name="about">Über EVMap</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="github_link_title">Quellcode</string>
|
||||
<string name="oss_licenses">Open Source-Lizenzen</string>
|
||||
<string name="oss_licenses">Lizenzen</string>
|
||||
<string name="settings">Einstellungen</string>
|
||||
<string name="settings_ui">Oberfläche</string>
|
||||
<string name="settings_map">Karte</string>
|
||||
@@ -45,10 +45,10 @@
|
||||
<string name="copyright_summary">©2020–2022 Johan von Forstner</string>
|
||||
<string name="other">Sonstiges</string>
|
||||
<string name="privacy">Datenschutzerklärung</string>
|
||||
<string name="fav_add">Zu Favoriten hinzufügen</string>
|
||||
<string name="fav_add">Als Favorit speichern</string>
|
||||
<string name="fav_remove">Aus Favoriten entfernen</string>
|
||||
<string name="pref_navigate_use_maps">Navigation sofort starten</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigationsbutton startet direkt Google Maps-Navigation</string>
|
||||
<string name="pref_navigate_use_maps">Sofort navigieren</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigationsbutton startet Routenführung mit Google Maps</string>
|
||||
<string name="pref_navigate_use_maps_off">Navigationsbutton startet Karten-App mit Position der Ladesäule</string>
|
||||
<string name="coordinates">Koordinaten</string>
|
||||
<string name="share">Teilen</string>
|
||||
@@ -59,7 +59,7 @@
|
||||
<string name="filter_connectors">Anschlüsse</string>
|
||||
<string name="plug_type_1">Typ 1</string>
|
||||
<string name="plug_type_2">Typ 2</string>
|
||||
<string name="plug_type_3">Typ 3a</string>
|
||||
<string name="plug_type_3">Typ 3A</string>
|
||||
<string name="plug_ccs">CCS</string>
|
||||
<string name="plug_schuko">Schuko</string>
|
||||
<string name="plug_chademo">CHAdeMO</string>
|
||||
@@ -71,18 +71,17 @@
|
||||
<string name="none">keine</string>
|
||||
<string name="show_more">mehr…</string>
|
||||
<string name="show_less">weniger…</string>
|
||||
<string name="favorites_empty_state">Wenn du Ladestationen als Favorit markierst, tauchen sie hier auf.</string>
|
||||
<string name="favorites_empty_state">Als Favorit gespeicherte Ladestationen tauchen hier auf</string>
|
||||
<string name="donate">Spenden</string>
|
||||
<string name="donation_successful">Vielen Dank! ❤️</string>
|
||||
<string name="donation_failed">Etwas ist schiefgelaufen. 😕</string>
|
||||
<string name="donation_successful">Vielen Dank ❤️</string>
|
||||
<string name="donation_failed">Etwas ist schiefgelaufen 😕</string>
|
||||
<string name="map_type_normal">Standard</string>
|
||||
<string name="map_type_satellite">Satellit</string>
|
||||
<string name="map_type_terrain">Gelände</string>
|
||||
<string name="map_type">Kartentyp</string>
|
||||
<string name="map_details">Kartendetails</string>
|
||||
<string name="map_traffic">Verkehr</string>
|
||||
<string name="faq">FAQ</string>
|
||||
<string name="faq_desc">Häufig gestellte Fragen</string>
|
||||
<string name="faq">Häufig gestellte Fragen</string>
|
||||
<string name="menu_filters_active">Filter aktiv</string>
|
||||
<string name="filters_activated">Filter aktiviert</string>
|
||||
<string name="filters_deactivated">Filter deaktiviert</string>
|
||||
@@ -99,11 +98,10 @@
|
||||
<string name="edit">bearbeiten</string>
|
||||
<string name="cancel">Abbrechen</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="pref_language">Sprache</string>
|
||||
<string name="pref_language_summary">App-Sprache ändern</string>
|
||||
<string name="pref_language">App-Sprache</string>
|
||||
<string name="pref_darkmode">Dunkles Design</string>
|
||||
<string name="pref_darkmode_summary">Einstellen, wann der Nachtmodus genutzt wird</string>
|
||||
<string name="connection_error">Ladesäulen konnten nicht geladen werden</string>
|
||||
<string name="location_error">Standort nicht erkannt. Bitte Systemeinstellungen prüfen</string>
|
||||
<string name="retry">Wiederholen</string>
|
||||
<string name="filter_open_247">24 Stunden geöffnet</string>
|
||||
<string name="filter_barrierfree">Ohne Vertrag / Registrierung nutzbar</string>
|
||||
@@ -151,17 +149,17 @@
|
||||
<string name="reorder">Reihenfolge ändern</string>
|
||||
<string name="delete">Löschen</string>
|
||||
<string name="save_as_profile">Als Profil speichern</string>
|
||||
<string name="save_profile_enter_name">Geben Sie den Namen des Filterprofils ein:</string>
|
||||
<string name="filterprofiles_empty_state">Du hast noch keine Filterprofile gespeichert.</string>
|
||||
<string name="save_profile_enter_name">Gib den Namen des Filterprofils ein:</string>
|
||||
<string name="filterprofiles_empty_state">Du hast keine Filterprofile gespeichert</string>
|
||||
<string name="welcome_to_evmap">Willkommen bei EVMap</string>
|
||||
<string name="welcome_1">Finde Ladestationen für Elektroautos in deiner Nähe.</string>
|
||||
<string name="welcome_1">Finde Ladestationen für Elektroautos in deiner Nähe</string>
|
||||
<string name="welcome_2_title">Auf die Leistung kommt es an</string>
|
||||
<string name="welcome_2">Die Farbe einer Ladestation auf der Karte zeigt dir die maximale Ladeleistung.</string>
|
||||
<string name="welcome_2_detail">Du kannst die Farben im Menü unter “Über EVMap → FAQ” erneut ansehen)</string>
|
||||
<string name="welcome_2">Die Farbe einer Ladestation zeigt dir die maximale Ladeleistung</string>
|
||||
<string name="welcome_2_detail">Die Farben kannst du unter “Über EVMap → Häufig gestellte Fragen” erneut ansehen</string>
|
||||
<string name="donation_dialog_title">Danke, dass du EVMap nutzt!</string>
|
||||
<string name="donation_dialog_detail">EVMap ist kostenlos und Open Source, ich entwickle es in meiner Freizeit. Über GitHub kann jeder zur Weiterentwicklung der App beitragen. Durch die steigende Beliebtheit der App müssen allerdings auch laufende Kosten, z.B. für den Zugriff auf die Datenquellen, gedeckt werden. Daher freue ich mich auch über Spenden in der App oder über GitHub Sponsors.</string>
|
||||
<string name="donation_dialog_detail">EVMap ist kostenlos und Open Source. Über GitHub kann jeder zur Weiterentwicklung der App beitragen. Um die laufenden Kosten für den für die Datenquellen zu decken, freue ich mich auch über Spenden in der App oder über GitHub Sponsors.</string>
|
||||
<string name="chargeprice_donation_dialog_title">Du bist ein richtiger Sparfuchs!</string>
|
||||
<string name="chargeprice_donation_dialog_detail">Es sieht so aus, als wenn du den Preisvergleich sehr gern nutzt. Für den Zugang zu den Preisinformationen muss der Entwickler von EVMap eine monatliche Gebühr an die Datenquelle Chargeprice.app zahlen. Um diesen Dienst weiter anbieten zu können, würde ich mich sehr über Spenden freuen.</string>
|
||||
<string name="chargeprice_donation_dialog_detail">Anscheinend nutzt du den Preisvergleich sehr gern. Mit einer Spende für EVMap kannst du helfen, die Kosten für den Datenzugriff zu decken.</string>
|
||||
<string name="deleted_filterprofile">„%s” gelöscht</string>
|
||||
<string name="undo">Rückgängig</string>
|
||||
<string name="rename">Umbenennen</string>
|
||||
@@ -172,7 +170,7 @@
|
||||
</plurals>
|
||||
<string name="navigate">Navigieren</string>
|
||||
<string name="verified">Verifiziert</string>
|
||||
<string name="verified_desc">Verifiziert von der %s Community – nicht zwangsläufig auch aktuell verfügbar.</string>
|
||||
<string name="verified_desc">Ladestation wurde mindestens einmal von einem Mitglied der %s Community getestet</string>
|
||||
<string name="charge_price_format">%1$.2f %2$s</string>
|
||||
<string name="charge_price_average_format">⌀ %1$.2f %2$s/kWh</string>
|
||||
<string name="charge_price_kwh_format">%1$.2f %2$s/kWh</string>
|
||||
@@ -182,27 +180,28 @@
|
||||
<string name="chargeprice_session_fee">Startgebühr</string>
|
||||
<string name="chargeprice_per_kwh">pro kWh</string>
|
||||
<string name="chargeprice_per_minute">pro min</string>
|
||||
<string name="chargeprice_blocking_fee">Blockiergeb. >%s</string>
|
||||
<string name="chargeprice_no_tariffs_found">Keine geeigneten Tarife für diese Ladestation bei Chargeprice.app gefunden.</string>
|
||||
<string name="chargeprice_blocking_fee">Blockiergeb. >%s</string>
|
||||
<string name="chargeprice_no_tariffs_found">Keine Tarife für diese Ladestation bei Chargeprice.app gefunden</string>
|
||||
<string name="powered_by_chargeprice">powered by Chargeprice</string>
|
||||
<string name="chargeprice_base_fee">Fixkosten: %1$.2f %2$s/Monat</string>
|
||||
<string name="chargeprice_min_spend">Mindestumsatz: %1$.2f %2$s/Monat</string>
|
||||
<string name="settings_chargeprice">Preisvergleich</string>
|
||||
<string name="pref_my_vehicle">Meine Fahrzeuge</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Nur Tarife ohne monatliche Gebühren</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Tarife mit monatlichen Gebühren ausschließen</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs">Exklusive Energiekunden-Tarife anzeigen</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Einige Anbieter bieten für ihre Kunden (z.B. Haushaltsstrom, Gas) günstigere Tarife an</string>
|
||||
<string name="chargeprice_select_car_first">Bitte wähle zuerst dein Auto in den Einstellungen aus.</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Einige Energieversorger bieten für ihre Kunden spezielle Tarife an</string>
|
||||
<string name="chargeprice_select_car_first">Bitte wähle zuerst dein Auto in den Einstellungen aus</string>
|
||||
<string name="chargeprice_battery_range">Laden von %1$.0f%% bis %2$.0f%%</string>
|
||||
<string name="chargeprice_battery_range_from">Laden von</string>
|
||||
<string name="chargeprice_battery_range_to">bis</string>
|
||||
<string name="chargeprice_stats">(%1$.0f kWh, ca. %2$s, ⌀ %3$.0f kW)</string>
|
||||
<string name="chargeprice_vehicle">Fahrzeug</string>
|
||||
<string name="edit_on_goingelectric_info">Falls hier nur eine leere Seite erscheint, logge dich bitte zuerst bei GoingElectric.de ein.</string>
|
||||
<string name="close">schließen</string>
|
||||
<string name="chargeprice_price_not_available">Preis nicht verfügbar</string>
|
||||
<string name="edit_on_goingelectric_info">Logge dich zuerst bei GoingElectric.de ein, falls hier nur eine leere Seite erscheint</string>
|
||||
<string name="close">Schließen</string>
|
||||
<string name="chargeprice_title">Preise</string>
|
||||
<string name="chargeprice_connection_error">Preise konnten nicht geladen werden</string>
|
||||
<string name="chargeprice_no_compatible_connectors">Keiner der Anschlüsse dieser Ladestation ist mit deinem Fahrzeug kompatibel.</string>
|
||||
<string name="chargeprice_no_compatible_connectors">Kein kompatibler Anschluss an dieser Ladestation</string>
|
||||
<string name="pref_chargeprice_currency">Währung</string>
|
||||
<string name="pref_my_tariffs">Meine Tarife</string>
|
||||
<string name="chargeprice_all_tariffs_selected">alle Tarife ausgewählt</string>
|
||||
@@ -218,20 +217,20 @@
|
||||
<item quantity="other">%d Tarife ausgewählt</item>
|
||||
</plurals>
|
||||
<string name="unknown_operator">Unbekannter Betreiber</string>
|
||||
<string name="data_sources_description">EVMap unterstützt verschiedene Datenquellen für Ladestationen. Bitte wähle aus, welche du nutzen möchtest. Du kannst sie später in den Einstellungen der App ändern.</string>
|
||||
<string name="data_sources_description">Bitte wähle eine Datenquelle für Ladestationen aus. Du kannst sie später in den Einstellungen der App ändern.</string>
|
||||
<string name="data_source_goingelectric">GoingElectric.de</string>
|
||||
<string name="data_source_openchargemap">Open Charge Map</string>
|
||||
<string name="data_source_goingelectric_desc">Sehr gute Abdeckung in Deutschland, Österreich, Schweiz und vielen angrenzenden Ländern. Beschreibungen in Deutsch. Von der Community gepflegt.</string>
|
||||
<string name="data_source_openchargemap_desc"><![CDATA[Weltweite Abdeckung mit variierender Qualität. Beschreibungen in Englisch oder Landessprache. Von der Community gepflegt & offizielle Verzeichnisse einiger Länder (z.B. Nordamerika, UK, Frankreich, Norwegen).]]></string>
|
||||
<string name="data_source_goingelectric_desc">Sehr gute Abdeckung in den deutschsprachigen Ländern. Beschreibungen in Deutsch. Von der Community gepflegt.</string>
|
||||
<string name="data_source_openchargemap_desc"><![CDATA[Weltweite Abdeckung mit variierender Qualität. Beschreibungen in Englisch oder Landessprache. Von der Community gepflegt und offizielle Verzeichnisse einiger Länder (z.B. Nordamerika, UK, Frankreich, Norwegen).]]></string>
|
||||
<string name="next">weiter</string>
|
||||
<string name="get_started">Los geht\'s</string>
|
||||
<string name="got_it">Alles klar</string>
|
||||
<string name="lets_go">Und los</string>
|
||||
<string name="crash_report_text">Sorry, anscheinend ist EVMap abgestürzt. Bitte schicke einen Fehlerbericht an den Entwickler.</string>
|
||||
<string name="crash_report_text">EVMap ist abgestürzt. Bitte schicke einen Fehlerbericht an den Entwickler.</string>
|
||||
<string name="crash_report_comment_prompt">Du kannst unten noch einen Kommentar hinzufügen:</string>
|
||||
<string name="powered_by_mapbox">powered by Mapbox</string>
|
||||
<string name="pref_search_provider">Anbieter für Ortssuche</string>
|
||||
<string name="pref_search_provider_info"><![CDATA[Die Daten für die Ortssuche, vor allem von Google Maps, sind relativ teuer. Wenn du diese Funktion häufig nutzt, würde ich mich über eine Spende unter \"Über EVMap -> Spenden\" sehr freuen.]]></string>
|
||||
<string name="pref_search_provider_info">Die Daten für die Ortssuche, vor allem von Google Maps, sind relativ teuer. Über eine Spende unter \"Über EVMap -> Spenden\" würde ich mich sehr freuen.</string>
|
||||
<string name="github_sponsors">GitHub Sponsors</string>
|
||||
<string name="donate_desc">Unterstütze die Weiterentwicklung von EVMap mit einer einmaligen Spende</string>
|
||||
<string name="github_sponsors_desc">Unterstütze EVMap über GitHub Sponsors</string>
|
||||
@@ -247,10 +246,10 @@
|
||||
<string name="help">Hilfe</string>
|
||||
<string name="settings_android_auto">Android Auto</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load">Schieflast erlauben</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load_summary"><![CDATA[Erlaubt das Laden mit >4.5 kW an AC-Stationen für Autos mit 1-phasigem Lader]]></string>
|
||||
<string name="pref_map_rotate_gestures_enabled">Kartenrotation erlauben</string>
|
||||
<string name="pref_map_rotate_gestures_on">Karte kann mit Zweifingergeste rotiert werden</string>
|
||||
<string name="pref_map_rotate_gestures_off">Karte bleibt fest nach Norden ausgerichtet</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load_summary">Einphasiges Laden mit mehr als 4.5 kW erlauben</string>
|
||||
<string name="pref_map_rotate_gestures_enabled">Kartenrotation</string>
|
||||
<string name="pref_map_rotate_gestures_on">Karte mit zwei Fingern rotieren</string>
|
||||
<string name="pref_map_rotate_gestures_off">Karte immer nach Norden ausrichten</string>
|
||||
<string name="refresh_live_data">Echtzeitstatus aktualisieren</string>
|
||||
<string name="autocomplete_connection_error">Vorschläge konnten nicht geladen werden</string>
|
||||
<string name="pref_language_device_default">Gerätesprache verwenden</string>
|
||||
@@ -271,4 +270,22 @@
|
||||
<string name="pref_chargeprice_currency_usd">US-Dollar (USD)</string>
|
||||
<string name="pref_provider_google_maps">Google Maps</string>
|
||||
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
|
||||
</resources>
|
||||
<string name="about_contributors">Mitwirkende</string>
|
||||
<string name="about_contributors_text">Dank an alle Mitwirkenden für ihre Beiträge von Code und Übersetzungen für EVMap:</string>
|
||||
<string name="utilization_prediction">Auslastungsprognose</string>
|
||||
<string name="prediction_help">Die Prognose basiert auf Faktoren wie Wochentag, Uhrzeit und Nutzung in der Vergangenheit. So kannst du stark ausgelastete Ladesäulen vermeiden. Keine Garantie.</string>
|
||||
<string name="prediction_time_colon">%s Uhr:</string>
|
||||
<plurals name="prediction_number_available">
|
||||
<item quantity="one">%1$d/%2$d verfügbar</item>
|
||||
<item quantity="other">%1$d/%2$d verfügbar</item>
|
||||
</plurals>
|
||||
<string name="pref_prediction_enabled">Auslastungsprognosen anzeigen</string>
|
||||
<string name="pref_prediction_enabled_summary">für unterstützte Ladestationen\n(momentan nur Schnellader in Deutschland)</string>
|
||||
<string name="prediction_only">(nur %s)</string>
|
||||
<string name="prediction_dc_plugs_only">DC-Anschlüsse</string>
|
||||
<string name="data_source_switched_to">Datenquelle zu %s umgeschaltet</string>
|
||||
<string name="pref_applink_associate">Unterstützte Links öffnen</string>
|
||||
<string name="pref_applink_associate_summary">von goingelectric.de und openchargemap.org</string>
|
||||
<string name="chargeprice_header_my_tariffs">Meine Tarife</string>
|
||||
<string name="chargeprice_header_other_tariffs">Andere Tarife</string>
|
||||
</resources>
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- tools:ignore="MissingQuantity" is temporary until Weblate 4.14 is released -->
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingQuantity">
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="app_name">EVMap</string>
|
||||
<string name="title_activity_maps">EVMap</string>
|
||||
<string name="connectors">Connecteurs</string>
|
||||
<string name="no_maps_app_found">Aucune application de navigation trouvée</string>
|
||||
<string name="no_browser_app_found">Aucun navigateur web trouvé</string>
|
||||
<string name="no_maps_app_found">Installez d\'abord une application de navigation</string>
|
||||
<string name="no_browser_app_found">Installez d\'abord un navigateur web</string>
|
||||
<string name="address">Adresse</string>
|
||||
<string name="operator">Opérateur</string>
|
||||
<string name="network">Réseau</string>
|
||||
@@ -22,34 +22,32 @@
|
||||
<string name="menu_favs">Favoris</string>
|
||||
<string name="menu_filter">Filtre</string>
|
||||
<string name="not_implemented">pas encore mis en œuvre</string>
|
||||
<string name="about">À propos d\'EVMap</string>
|
||||
<string name="about">À propos</string>
|
||||
<string name="github_link_title">Code source</string>
|
||||
<string name="settings_ui">Interface utilisateur</string>
|
||||
<string name="privacy">Politique de confidentialité</string>
|
||||
<string name="fav_add">Ajouter aux favoris</string>
|
||||
<string name="pref_navigate_use_maps">Démarrer la navigation immédiatement</string>
|
||||
<string name="settings_ui">Interface</string>
|
||||
<string name="privacy">Confidentialité</string>
|
||||
<string name="fav_add">Enregistrer comme favori</string>
|
||||
<string name="pref_navigate_use_maps">Naviguer maintenant</string>
|
||||
<string name="coordinates">Coordonnées</string>
|
||||
<string name="pref_navigate_use_maps_on">Le bouton de navigation lance immédiatement la navigation Google Maps</string>
|
||||
<string name="pref_navigate_use_maps_on">Le bouton de navigation démarre le guidage d\'itinéraire avec Google Maps</string>
|
||||
<string name="share">Partager</string>
|
||||
<string name="plug_chademo">CHAdeMO</string>
|
||||
<string name="plug_supercharger">Superchargeur Tesla</string>
|
||||
<string name="show_less">moins…</string>
|
||||
<string name="favorites_empty_state">Si vous ajoutez des chargeurs à vos favoris, ils apparaîtront ici.</string>
|
||||
<string name="favorites_empty_state">Les chargeurs sauvegardés apparaissent ici</string>
|
||||
<string name="donate">Faire un don</string>
|
||||
<string name="map_type_satellite">Satellite</string>
|
||||
<string name="map_type_terrain">Terrain</string>
|
||||
<string name="map_type">Type de carte</string>
|
||||
<string name="map_details">Détails de la carte</string>
|
||||
<string name="map_traffic">Trafic</string>
|
||||
<string name="faq">FAQ</string>
|
||||
<string name="faq_desc">Foire aux questions</string>
|
||||
<string name="faq">Foire aux questions</string>
|
||||
<string name="menu_filters_active">Filtres actifs</string>
|
||||
<string name="filters_activated">Filtres activés</string>
|
||||
<string name="filters_deactivated">Filtres désactivés</string>
|
||||
<string name="menu_manage_filter_profiles">Gérer les profils de filtrage</string>
|
||||
<string name="edit">modifier</string>
|
||||
<string name="pref_language">Langue</string>
|
||||
<string name="pref_language_summary">Changer la langue de l\'application</string>
|
||||
<string name="pref_language">Langue de l\'application</string>
|
||||
<string name="connection_error">Impossible de charger les stations de recharge</string>
|
||||
<string name="retry">Réessayer</string>
|
||||
<string name="filter_open_247">Disponible 24h/24 et 7j/7</string>
|
||||
@@ -72,10 +70,10 @@
|
||||
<string name="category_zoo">Zoo</string>
|
||||
<string name="menu_apply">Appliquer les filtres</string>
|
||||
<string name="save_as_profile">Enregistrer en tant que profil</string>
|
||||
<string name="welcome_1">Trouvez des chargeurs de véhicules électriques autour de vous.</string>
|
||||
<string name="welcome_2">La couleur d\'un chargeur sur la carte vous indique sa puissance de charge maximale.</string>
|
||||
<string name="welcome_2_detail">(Vous pouvez vérifier à nouveau les couleurs sous \"À propos d\'EVMap → FAQ\" dans le menu)</string>
|
||||
<string name="donation_dialog_title">Merci d\'utiliser EVMap !</string>
|
||||
<string name="welcome_1">Trouvez des chargeurs de véhicules électriques autour de vous</string>
|
||||
<string name="welcome_2">La couleur d\'un chargeur sur la carte vous indique sa puissance de charge maximale</string>
|
||||
<string name="welcome_2_detail">Cela peut également être vu dans \"À propos\" → \"Foire aux questions\"</string>
|
||||
<string name="donation_dialog_title">Merci d\'utiliser EVMap</string>
|
||||
<string name="chargeprice_donation_dialog_title">Vous êtes un vrai chasseur de bonnes affaires !</string>
|
||||
<string name="deleted_filterprofile">\"%s\" supprimé</string>
|
||||
<string name="undo">Annuler</string>
|
||||
@@ -83,21 +81,22 @@
|
||||
<string name="verified">vérifié</string>
|
||||
<plurals name="charge_cards_compatible_num">
|
||||
<item quantity="one">%d mode de paiement compatible</item>
|
||||
<item quantity="many">%d modes de paiement compatibles</item>
|
||||
<item quantity="other">%d modes de paiement compatibles</item>
|
||||
</plurals>
|
||||
<string name="verified_desc">Chargeur vérifié par un membre de la communauté %s - ne fonctionne pas forcément en ce moment.</string>
|
||||
<string name="verified_desc">Le fonctionnement du chargeur a été confirmé au moins une fois par un membre de la communauté %s</string>
|
||||
<string name="percent_format">%.0f%%</string>
|
||||
<string name="chargeprice_session_fee">frais de session</string>
|
||||
<string name="chargeprice_per_kwh">par kWh</string>
|
||||
<string name="chargeprice_per_minute">par min</string>
|
||||
<string name="chargeprice_blocking_fee">Frais de blocage >%s</string>
|
||||
<string name="chargeprice_no_tariffs_found">Chargeprice.app n\'a trouvé aucun tarif de recharge compatible avec ce chargeur.</string>
|
||||
<string name="chargeprice_no_tariffs_found">Aucun tarif de recharge pour ce chargeur sur Chargeprice.app</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs">Afficher les tarifs exclusifs aux clients</string>
|
||||
<string name="chargeprice_battery_range">Charge de %1$.0f%% à %2$.0f%%</string>
|
||||
<string name="chargeprice_battery_range_from">Charge de</string>
|
||||
<string name="chargeprice_stats">(%1$.0f kWh, approx. %2$s, ⌀ %3$.0f kW)</string>
|
||||
<string name="chargeprice_vehicle">Véhicule</string>
|
||||
<string name="close">fermer</string>
|
||||
<string name="close">Fermer</string>
|
||||
<string name="chargeprice_title">Prix</string>
|
||||
<string name="pref_chargeprice_currency">Devise</string>
|
||||
<string name="data_source_goingelectric">GoingElectric.de</string>
|
||||
@@ -105,6 +104,7 @@
|
||||
<string name="pref_data_source">Source des données</string>
|
||||
<plurals name="chargeprice_some_tariffs_selected">
|
||||
<item quantity="one">%d tarif sélectionné</item>
|
||||
<item quantity="many">%d tarifs sélectionnés</item>
|
||||
<item quantity="other">%d tarifs sélectionnés</item>
|
||||
</plurals>
|
||||
<string name="data_source_openchargemap">Open Charge Map</string>
|
||||
@@ -121,8 +121,8 @@
|
||||
<string name="pref_search_delete_recent">Supprimer les résultats de recherche récents</string>
|
||||
<string name="settings_android_auto">Android Auto</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load">Permettre une charge déséquilibrée</string>
|
||||
<string name="pref_map_rotate_gestures_enabled">Activer la rotation de la carte</string>
|
||||
<string name="pref_map_rotate_gestures_off">La carte reste orientée vers le nord</string>
|
||||
<string name="pref_map_rotate_gestures_enabled">Rotation de la carte</string>
|
||||
<string name="pref_map_rotate_gestures_off">Rotation désactivée (nord toujours en haut)</string>
|
||||
<string name="refresh_live_data">rafraîchir le statut en temps réel</string>
|
||||
<string name="pref_language_device_default">Utiliser la langue de l\'appareil</string>
|
||||
<string name="pref_darkmode_device_default">Utiliser le réglage de l\'appareil</string>
|
||||
@@ -141,22 +141,22 @@
|
||||
<string name="general_info">Informations générales</string>
|
||||
<string name="realtime_data_loading">Vérification du statut en temps réel…</string>
|
||||
<string name="plug_ccs">CCS</string>
|
||||
<string name="donation_successful">Merci ! ❤️</string>
|
||||
<string name="donation_failed">Quelque chose s\'est mal passé. 😕</string>
|
||||
<string name="donation_successful">Merci ❤️</string>
|
||||
<string name="donation_failed">Quelque chose s\'est mal passé 😕</string>
|
||||
<string name="category_supermarket">Supermarché</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="oss_licenses">Licences Open Source</string>
|
||||
<string name="oss_licenses">Licences</string>
|
||||
<string name="realtime_data_source">Source du statut en temps réel (bêta) : %s</string>
|
||||
<string name="plug_type_2">Type 2</string>
|
||||
<string name="plug_type_3">Type 3a</string>
|
||||
<string name="plug_type_3">Type 3A</string>
|
||||
<string name="plug_cee_rot">CEE Rouge</string>
|
||||
<string name="all">tous</string>
|
||||
<string name="fault_report_date">Rapport d\'anomalie (dernière mise à jour : %s)</string>
|
||||
<string name="menu_report_new_charger">Signaler un nouveau chargeur</string>
|
||||
<string name="menu_report_new_charger">Nouveau chargeur</string>
|
||||
<string name="filter_connectors">Connecteurs</string>
|
||||
<string name="copyright_summary">©2020-2022 Johan von Forstner</string>
|
||||
<string name="other">Autre</string>
|
||||
<string name="pref_navigate_use_maps_off">Le bouton de navigation lance l’application de cartes avec l’emplacement du chargeur</string>
|
||||
<string name="pref_navigate_use_maps_off">Le bouton de navigation lance l’application de cartes à l’emplacement du chargeur</string>
|
||||
<string name="settings_map">Carte</string>
|
||||
<string name="fault_report">Rapport d\'anomalie</string>
|
||||
<string name="filter_free">Uniquement des chargeurs gratuits</string>
|
||||
@@ -179,14 +179,13 @@
|
||||
<string name="number_selected">%d sélectionné</string>
|
||||
<string name="cancel">Annuler</string>
|
||||
<string name="filter_operators">Opérateurs</string>
|
||||
<string name="chargeprice_donation_dialog_detail">Il semble que vous appréciez beaucoup la fonction de comparaison des prix. Pour accéder aux données de tarification, le développeur d\'EVMap doit payer une redevance mensuelle au fournisseur de données Chargeprice.app. Par conséquent, veuillez envisager de soutenir EVMap par un don.</string>
|
||||
<string name="pref_darkmode_summary">Définir lorsque le mode sombre est activé</string>
|
||||
<string name="chargeprice_donation_dialog_detail">Vous faites bon usage de la fonction de comparaison des prix. Aidez-nous à couvrir les coûts de ces données en soutenant EVMap par un don.</string>
|
||||
<string name="and_n_others">et %d autres</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="pref_map_provider">Fournisseur de cartes</string>
|
||||
<string name="twitter">Twitter</string>
|
||||
<string name="category_petrol_station">Station-service</string>
|
||||
<string name="edit_on_goingelectric_info">Si seule une page vide s\'affiche ici, veuillez d\'abord vous connecter à GoingElectric.de.</string>
|
||||
<string name="edit_on_goingelectric_info">Veuillez vous connecter à GoingElectric.de si cette page est vide</string>
|
||||
<string name="settings_chargeprice">Comparaison des prix</string>
|
||||
<string name="category_service_on_motorway">Aire de service (sur autoroute)</string>
|
||||
<string name="category_railway_station">Gare ferroviaire</string>
|
||||
@@ -199,7 +198,7 @@
|
||||
<string name="reorder">réorganiser</string>
|
||||
<string name="delete">Supprimer</string>
|
||||
<string name="save_profile_enter_name">Saisissez le nom du profil de filtrage :</string>
|
||||
<string name="donation_dialog_detail">EVMap est un logiciel libre et open source que je développe pendant mon temps libre. Les contributions de codage sur GitHub sont très appréciées. Cependant, en raison de la popularité croissante de l\'application, je dois également couvrir certains coûts de fonctionnement, par exemple pour l\'accès aux sources de données. Par conséquent, veuillez envisager de soutenir l\'application par un don ou via les sponsors GitHub.</string>
|
||||
<string name="donation_dialog_detail">EVMap est un logiciel libre et gratuit. Les contributions de codage sur GitHub sont très appréciées. Pour aider à couvrir les frais de fonctionnement de l\'accès aux sources de données, veuillez envisager de faire un don du montant de votre choix au développeur.</string>
|
||||
<string name="charging_barrierfree">Utilisable sans enregistrement</string>
|
||||
<string name="chargeprice_battery_range_to">à</string>
|
||||
<string name="category_service_off_motorway">Aire de service (hors autoroute)</string>
|
||||
@@ -211,23 +210,24 @@
|
||||
<string name="category_holiday_home">Maison de vacances</string>
|
||||
<string name="category_caravan_site">Emplacement pour caravanes</string>
|
||||
<string name="filter_custom">Filtre modifié</string>
|
||||
<string name="filterprofiles_empty_state">Vous n\'avez pas encore enregistré de profils de filtrage.</string>
|
||||
<string name="filterprofiles_empty_state">Vous n\'avez aucun profil de filtrage enregistré</string>
|
||||
<string name="welcome_to_evmap">Bienvenue sur EVMap</string>
|
||||
<string name="chargeprice_provider_customer_tariff">Uniquement pour les clients du fournisseur</string>
|
||||
<string name="powered_by_chargeprice">alimenté par Chargeprice</string>
|
||||
<string name="pref_my_vehicle">Mes véhicules</string>
|
||||
<string name="pref_my_tariffs">Mes tarifs de recharge</string>
|
||||
<string name="license">Licence</string>
|
||||
<string name="autocomplete_connection_error">Les suggestions n\'ont pas pu être chargées</string>
|
||||
<string name="autocomplete_connection_error">Impossible de charger les suggestions</string>
|
||||
<string name="chargeprice_select_connector">Choisir le connecteur</string>
|
||||
<string name="chargeprice_select_car_first">Veuillez d\'abord sélectionner le modèle de votre voiture dans les paramètres.</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Certains fournisseurs offrent des tarifs moins chers exclusivement à leurs clients (par exemple, électricité domestique, gaz)</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Afficher uniquement les tarifs sans frais mensuels</string>
|
||||
<string name="chargeprice_no_compatible_connectors">Aucun des connecteurs de cette station de charge n\'est compatible avec votre véhicule.</string>
|
||||
<string name="chargeprice_select_car_first">Veuillez d\'abord sélectionner le modèle de votre voiture dans les paramètres</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Certains fournisseurs d\'énergie offrent des tarifs moins chers exclusivement à leurs clients</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Exclure les tarifs avec frais mensuels</string>
|
||||
<string name="chargeprice_no_compatible_connectors">Pas de connecteurs compatibles dans cette station de recharge</string>
|
||||
<string name="chargeprice_connection_error">Impossible de charger les prix</string>
|
||||
<string name="pref_search_provider">Fournisseur de recherche de lieux</string>
|
||||
<plurals name="pref_my_tariffs_summary">
|
||||
<item quantity="one" tools:ignore="ImpliedQuantity">(sera mis en évidence dans la comparaison des prix)</item>
|
||||
<item quantity="many">(seront mis en évidence dans la comparaison des prix)</item>
|
||||
<item quantity="other">(seront mis en évidence dans la comparaison des prix)</item>
|
||||
</plurals>
|
||||
<string name="deleted_recent_search_results">Les résultats de recherche récents ont été supprimés</string>
|
||||
@@ -238,26 +238,26 @@
|
||||
<string name="got_it">J\'ai compris</string>
|
||||
<string name="powered_by_mapbox">propulsé par Mapbox</string>
|
||||
<string name="lets_go">Allons-y</string>
|
||||
<string name="crash_report_text">Désolé, il semble que EVMap ait planté. Veuillez envoyer un rapport de plantage au développeur.</string>
|
||||
<string name="crash_report_text">EVMap a planté. Veuillez envoyer un rapport de plantage au développeur.</string>
|
||||
<string name="unknown_operator">Opérateur inconnu</string>
|
||||
<string name="data_source_goingelectric_desc">Très bonne couverture en Allemagne, en Autriche et en Suisse et dans de nombreux pays voisins. Descriptions en allemand. Maintenu par la communauté.</string>
|
||||
<string name="data_source_goingelectric_desc">Idéal dans les pays germanophones. Descriptions en allemand. Maintenu par la communauté.</string>
|
||||
<string name="data_source_openchargemap_desc">Couverture mondiale avec une qualité variable. Descriptions en anglais ou dans la langue locale. Données ouvertes maintenues par la communauté et provenant de sources gouvernementales dans certains pays (par exemple, Amérique du Nord, Royaume-Uni, France, Norvège).</string>
|
||||
<string name="faq_link">https://evmap.vonforst.net/en/faq.html</string>
|
||||
<string name="chargeprice_faq_link">https://evmap.vonforst.net/en/chargeprice_faq.html</string>
|
||||
<string name="settings_data_sources">Sources de données</string>
|
||||
<string name="data_sources_description">EVMap supporte plusieurs sources de données pour les stations de recharge. Veuillez sélectionner celle que vous souhaitez utiliser. Vous pourrez toujours la modifier ultérieurement dans les paramètres de l\'application.</string>
|
||||
<string name="pref_search_provider_info">Les données pour la recherche de lieux, en particulier celles de Google Maps, sont relativement coûteuses. Si vous utilisez souvent cette fonctionnalité, veuillez envisager de faire un don via \"À propos d’EVMap -> Faire un don\".</string>
|
||||
<string name="data_sources_description">Veuillez choisir une source de données pour les stations de recharge. Vous pourrez la modifier ultérieurement dans les paramètres de l\'application.</string>
|
||||
<string name="pref_search_provider_info">Les données pour la recherche de lieux, en particulier celles de Google Maps, sont relativement coûteuses à récupérer. Veuillez envisager de faire un don via \"À propos\" -> \"Faire un don\".</string>
|
||||
<string name="pref_chargeprice_currency_hrk">Kuna croate (HRK)</string>
|
||||
<string name="help">Aide</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load_summary">Autoriser la charge avec >4,5 kW aux stations AC pour les voitures avec chargeur monophasé</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load_summary">Autoriser la charge en courant alternatif monophasé de plus de 4,5 kW</string>
|
||||
<string name="pref_chargeprice_currency_huf">Forint hongrois (HUF)</string>
|
||||
<string name="pref_chargeprice_currency_pln">Złoty polonais (PLN)</string>
|
||||
<string name="pref_map_rotate_gestures_on">La carte peut être pivotée avec un geste à deux doigts</string>
|
||||
<string name="pref_map_rotate_gestures_on">Utilisez deux doigts pour faire pivoter la carte</string>
|
||||
<string name="pref_chargeprice_currency_chf">Franc suisse (CHF)</string>
|
||||
<string name="pref_chargeprice_currency_usd">Dollar américain (USD)</string>
|
||||
<string name="pref_chargeprice_currency_sek">Couronne suédoise (SEK)</string>
|
||||
<string name="cost_detail_charging"><b>recharge %s</b></string>
|
||||
<string name="cost_detail_parking"><b>stationnement %s</b></string>
|
||||
<string name="cost_detail_charging"><b>Recharge %s</b></string>
|
||||
<string name="cost_detail_parking"><b>Stationnement %s</b></string>
|
||||
<string name="navigate">Naviguer vers</string>
|
||||
<string name="charge_price_format">%1$.2f %2$s</string>
|
||||
<string name="charge_price_average_format">⌀ %1$.2f %2$s/kWh</string>
|
||||
@@ -266,9 +266,11 @@
|
||||
<string name="chargeprice_min_spend">Dépenses minimales : %1$.2f %2$s/mois</string>
|
||||
<string name="welcome_2_title">Visualisez la puissance</string>
|
||||
<string name="pref_provider_google_maps">Google Maps</string>
|
||||
<string name="parking_free">Gratuit</string>
|
||||
<string name="parking_paid">Payant</string>
|
||||
<string name="parking_free">gratuit</string>
|
||||
<string name="parking_paid">payant</string>
|
||||
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
|
||||
<string name="charging_paid">Payante</string>
|
||||
<string name="charging_free">Gratuite</string>
|
||||
<string name="charging_paid">payante</string>
|
||||
<string name="charging_free">gratuite</string>
|
||||
<string name="about_contributors">Contributeurs</string>
|
||||
<string name="about_contributors_text">Merci à tous les contributeurs pour leur contribution au codage et à la traduction d\'EVMap :</string>
|
||||
</resources>
|
||||
@@ -1,29 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">EVMap</string>
|
||||
<string name="no_maps_app_found">Fant ingen navigeringsprogrammer</string>
|
||||
<string name="no_maps_app_found">Installer et navigeringsprogram først</string>
|
||||
<string name="closed"><b>Stengt</b></string>
|
||||
<string name="open_closesat"><b>Åpen</b> · Stenger %s</string>
|
||||
<string name="holiday">Ferie</string>
|
||||
<string name="cost">Kostnad</string>
|
||||
<string name="general_info">Generell info</string>
|
||||
<string name="menu_filter">Filter</string>
|
||||
<string name="about">Om EVMap</string>
|
||||
<string name="about">Om</string>
|
||||
<string name="version">Versjon</string>
|
||||
<string name="settings">Innstillinger</string>
|
||||
<string name="settings_map">Kart</string>
|
||||
<string name="fav_add">Legg til som favoritt</string>
|
||||
<string name="fav_add">Lagre som favoritt</string>
|
||||
<string name="fav_remove">Fjern fra favoritter</string>
|
||||
<string name="share">Del</string>
|
||||
<string name="filter_free">Kun gratisladere</string>
|
||||
<string name="faq">O-S-S</string>
|
||||
<string name="faq_desc">Ofte stilte spørsmål</string>
|
||||
<string name="faq">Ofte stilte spørsmål</string>
|
||||
<string name="menu_edit_filters">Rediger filtre</string>
|
||||
<string name="edit">rediger</string>
|
||||
<string name="cancel">Avbryt</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="pref_language">Språk</string>
|
||||
<string name="pref_language_summary">Endre programspråket</string>
|
||||
<string name="pref_language">Programspråk</string>
|
||||
<string name="and_n_others">og %d andre</string>
|
||||
<string name="pref_map_provider">Karttilbyder</string>
|
||||
<string name="twitter">Twitter</string>
|
||||
@@ -35,7 +33,7 @@
|
||||
<string name="filter_favorites">Favoritter</string>
|
||||
<string name="delete">Slett</string>
|
||||
<string name="save_as_profile">Lagre som profil</string>
|
||||
<string name="donation_dialog_title">Takk for at du bruker EVMap!</string>
|
||||
<string name="donation_dialog_title">Takk for at du bruker EVMap</string>
|
||||
<string name="license">Lisens</string>
|
||||
<string name="pref_data_source">Datakilder</string>
|
||||
<string name="required">påkrevd</string>
|
||||
@@ -43,9 +41,9 @@
|
||||
<string name="help">Hjelp</string>
|
||||
<string name="hours">Åpningstider</string>
|
||||
<string name="open_247"><b>Døgnåpen</b></string>
|
||||
<string name="settings_ui">Brukergrensesnitt</string>
|
||||
<string name="settings_ui">Grensesnitt</string>
|
||||
<string name="title_activity_maps">EVMap</string>
|
||||
<string name="no_browser_app_found">Fant ingen nettlesere</string>
|
||||
<string name="no_browser_app_found">Installer en nettleser først</string>
|
||||
<string name="address">Adresse</string>
|
||||
<string name="network">Nettverk</string>
|
||||
<string name="closed_unfmt">Stengt</string>
|
||||
@@ -61,12 +59,12 @@
|
||||
<string name="search">Søk</string>
|
||||
<string name="not_implemented">ikke implementert enda</string>
|
||||
<string name="github_link_title">Kildekode</string>
|
||||
<string name="oss_licenses">Frie lisenser</string>
|
||||
<string name="oss_licenses">Lisenser</string>
|
||||
<string name="copyright">Opphavsrett</string>
|
||||
<string name="coordinates">Koordinater</string>
|
||||
<string name="fault_report">Feilrapport</string>
|
||||
<string name="privacy">Personvernsmerknad</string>
|
||||
<string name="pref_navigate_use_maps">Start navigasjon umiddelbart</string>
|
||||
<string name="privacy">Personvern</string>
|
||||
<string name="pref_navigate_use_maps">Umiddelbar navigasjon</string>
|
||||
<string name="charge_cards">Betalingsmetoder</string>
|
||||
<string name="go_to_chargeprice">Sammenlign priser</string>
|
||||
<string name="filter_networks">Nettverk</string>
|
||||
@@ -76,7 +74,7 @@
|
||||
<string name="pref_chargeprice_currency">Valuta</string>
|
||||
<string name="next">neste</string>
|
||||
<string name="github_sponsors">GitHub-sponsorer</string>
|
||||
<string name="menu_report_new_charger">Rapporter ny lader</string>
|
||||
<string name="menu_report_new_charger">Ny lader</string>
|
||||
<string name="category_private_charger">Privat lader</string>
|
||||
<string name="category_restaurant">Restaurant</string>
|
||||
<string name="category_museum">Museum</string>
|
||||
@@ -96,9 +94,9 @@
|
||||
<string name="other">Andre</string>
|
||||
<string name="cost_detail"><b>Lading:</b> %1$s · <b>Parkering:</b> %2$s</string>
|
||||
<string name="copyright_summary">©2020–2022 Johan von Forstner</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigasjonsnkappen starter Google Maps-navigasjon umiddelbart</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigasjonsnkappen starter ruteveiledning på Google Maps</string>
|
||||
<string name="filter_free_parking">Kun ladere med gratis parkering</string>
|
||||
<string name="filter_min_power">Minimumseffekt</string>
|
||||
<string name="filter_min_power">Min. effekt</string>
|
||||
<string name="plug_type_1">Type 1</string>
|
||||
<string name="plug_chademo">CHAdeMO</string>
|
||||
<string name="plug_schuko">Schuko</string>
|
||||
@@ -113,7 +111,7 @@
|
||||
<string name="map_type">Karttype</string>
|
||||
<string name="map_details">Kartdetaljer</string>
|
||||
<string name="map_traffic">Trafikk</string>
|
||||
<string name="favorites_empty_state">Ladere lagret som favoritter vises her.</string>
|
||||
<string name="favorites_empty_state">Lagrede ladere vises her</string>
|
||||
<string name="plug_cee_rot">CEE rød</string>
|
||||
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
|
||||
<string name="menu_filters_active">Aktive filtre</string>
|
||||
@@ -145,13 +143,13 @@
|
||||
<string name="category_parking_underground">Parkeringsgarasje under bakken</string>
|
||||
<string name="reorder">endre rekkefølge</string>
|
||||
<string name="save_profile_enter_name">Skriv inn navnet på filterprofilen:</string>
|
||||
<string name="filterprofiles_empty_state">Du har ikke lagret noen filterprofiler.</string>
|
||||
<string name="filterprofiles_empty_state">Du har ikke noen lagrede filterprofiler</string>
|
||||
<string name="chargeprice_donation_dialog_title">Du er en sann gjerrigknark.</string>
|
||||
<string name="deleted_filterprofile">Slettet «%s»</string>
|
||||
<string name="charging_barrierfree">Kan brukes uten registrering</string>
|
||||
<string name="welcome_1">Finn kjøretøyladere der du er.</string>
|
||||
<string name="welcome_2">Maksimal ladeeffekt er angitt ved forskjellige farger på respektive ladere i kartet.</string>
|
||||
<string name="welcome_2_detail">(Du kan sjekke fargene igjen i «Om EVMap → O-S-S» i menyen)</string>
|
||||
<string name="welcome_1">Finn kjøretøyladere der du er</string>
|
||||
<string name="welcome_2">Hver laders farge samsvarer med dens høyeste ladeeffekt</string>
|
||||
<string name="welcome_2_detail">Dette er også å finne i «Om» → «O-S-S» i menyen</string>
|
||||
<string name="verified_desc">Lader bekreftet av et medlem av %s-gemenskapen. Dette betyr ikke at den virker nå.</string>
|
||||
<string name="charge_price_format">%2$s%1$.2f</string>
|
||||
<string name="charge_price_average_format">⌀ %2$s%1$.2f/kWt</string>
|
||||
@@ -164,11 +162,11 @@
|
||||
<string name="pref_my_vehicle">Mine kjøretøy</string>
|
||||
<string name="chargeprice_battery_range_to">til</string>
|
||||
<string name="chargeprice_stats">(%1$.0f kWt, omtrentlig. %2$s, ⌀ %3$.0f kW)</string>
|
||||
<string name="chargeprice_select_car_first">Velg bilen din i innstillingene først.</string>
|
||||
<string name="chargeprice_select_car_first">Velg bilmodellen din i innstillingene først</string>
|
||||
<string name="chargeprice_battery_range">Lad fra %1$.0f%% til %2$.0f%%</string>
|
||||
<string name="chargeprice_battery_range_from">Lad fra</string>
|
||||
<string name="chargeprice_vehicle">Kjøretøy</string>
|
||||
<string name="close">lukk</string>
|
||||
<string name="close">Lukk</string>
|
||||
<string name="chargeprice_title">Priser</string>
|
||||
<string name="chargeprice_connection_error">Kunne ikke laste inn priser</string>
|
||||
<plurals name="pref_my_tariffs_summary">
|
||||
@@ -182,7 +180,7 @@
|
||||
<string name="github_sponsors_desc">Støtt EVMap med GitHub-sponsorer</string>
|
||||
<string name="donate_desc">Støtt utviklingen av EVMap med en engangsdonasjon</string>
|
||||
<string name="settings_android_auto">Android Auto</string>
|
||||
<string name="pref_map_rotate_gestures_enabled">Skru på kartrotasjon</string>
|
||||
<string name="pref_map_rotate_gestures_enabled">Kartrotasjon</string>
|
||||
<string name="deleted_recent_search_results">Nylige søkeresultater slettet</string>
|
||||
<string name="autocomplete_connection_error">Kunne ikke laste inn forslag</string>
|
||||
<string name="pref_language_device_default">Enhetsforvalg</string>
|
||||
@@ -190,20 +188,20 @@
|
||||
<string name="pref_chargeprice_currency_czk">Tsjekkiske kroner (CZK)</string>
|
||||
<string name="pref_chargeprice_currency_dkk">Danske kroner (DKK)</string>
|
||||
<string name="pref_chargeprice_currency_hrk">Kroatiske kroner (HRK)</string>
|
||||
<string name="pref_map_rotate_gestures_on">Kartet kan roteres med to fingre</string>
|
||||
<string name="pref_map_rotate_gestures_off">Kartet vil orienteres mot nord</string>
|
||||
<string name="pref_map_rotate_gestures_on">Bruk to fingre for å rotere kartet</string>
|
||||
<string name="pref_map_rotate_gestures_off">Rotasjon avslått (nord er alltid oppover)</string>
|
||||
<string name="refresh_live_data">oppdater sanntidsstatus</string>
|
||||
<string name="pref_chargeprice_currency_isk">Islandske kroner (ISK)</string>
|
||||
<string name="pref_chargeprice_currency_pln">Polske zloty (PLN)</string>
|
||||
<string name="pref_chargeprice_currency_usd">Amerikanske dollar (USD)</string>
|
||||
<string name="filters_deactivated">Filtre deaktivert</string>
|
||||
<string name="pref_navigate_use_maps_off">Navigasjonsknapp starter kartprogram med laderposisjon</string>
|
||||
<string name="pref_navigate_use_maps_off">Navigasjonsknapp åpner kartprogrammet med laderposisjon</string>
|
||||
<string name="show_more">flere …</string>
|
||||
<string name="filters_activated">Filtre aktivert</string>
|
||||
<string name="donate">Doner</string>
|
||||
<string name="donation_successful">Takk. ❤️</string>
|
||||
<string name="donation_successful">Takk ❤️</string>
|
||||
<string name="map_type_normal">Forvalg</string>
|
||||
<string name="donation_failed">Noe gikk galt. 😕</string>
|
||||
<string name="donation_failed">Noe gikk galt 😕</string>
|
||||
<string name="filter_custom">Endret filter</string>
|
||||
<string name="welcome_to_evmap">Velkommen til EVMap</string>
|
||||
<string name="rename">Gi nytt navn</string>
|
||||
@@ -221,48 +219,47 @@
|
||||
<string name="plug_cee_blau">CEE blå</string>
|
||||
<string name="filter_open_247">Døgnåpent</string>
|
||||
<string name="pref_chargeprice_currency_huf">Ungarske forint (HUF)</string>
|
||||
<string name="donation_dialog_detail">Fri programvare utviklet på fritiden. Kodebidrag mottas med takk. Siden programmet er mer og mer populært må driftskostnader dekkes. Overvei å gi din støtte gjennom GitHub-sponsorer.</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load_summary">Tillat lading over 4.5 kW på vekselstrømsstasjoner med enfaselader</string>
|
||||
<string name="donation_dialog_detail">EVMap er fri programvare. Kodebidrag mottas med takk. Overvei å gi din støtte gjennom GitHub-sponsorer for å dekke løpende utgifter.</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load_summary">Tillat enfaselading over 4.5 kW</string>
|
||||
<string name="connectors">Tilkobling</string>
|
||||
<string name="operator">Operatør</string>
|
||||
<string name="amenities">Tilleggstjenester</string>
|
||||
<string name="filter_min_connectors">Min. antall tilkoblinger</string>
|
||||
<string name="filter_connectors">Tilkoblinger</string>
|
||||
<string name="filter_operators">Operatører</string>
|
||||
<string name="pref_darkmode_summary">Når mørk drakt er iført</string>
|
||||
<string name="category_service_off_motorway">Rasteplass (ikke på motorvei)</string>
|
||||
<string name="welcome_2_title">Effekten til veies tilgjengeliggjøres</string>
|
||||
<string name="chargeprice_donation_dialog_detail">Du bruker prissammenligningen en del. Dette bekostes av utvikleren som månedlig avgift til Chargeprice.app-datatilbyderen.
|
||||
\nOvervei å støtte EVMap med en donasjon.</string>
|
||||
<string name="chargeprice_donation_dialog_detail">Du bruker prissammenligningen en del.
|
||||
\nOvervei å dekke kostnadene ved å støtte EVMap med en donasjon.</string>
|
||||
<string name="navigate">Navigasjon</string>
|
||||
<string name="chargeprice_session_fee">startgebyr</string>
|
||||
<string name="powered_by_chargeprice">tilbudt av Chargeprice</string>
|
||||
<string name="chargeprice_base_fee">Grunnkostnad: %2$s%1$.2f/måned</string>
|
||||
<string name="chargeprice_no_tariffs_found">Chargeprice.app fant ikke noen ladeabonnementer kompatible med denne laderen.</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Kun vis abonnementer uten månedlige gebyr</string>
|
||||
<string name="chargeprice_no_tariffs_found">Ingen ladeabonnement for denne laderen på Chargeprice.app</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Utelat abonnementer med månedlige gebyr</string>
|
||||
<string name="chargeprice_select_connector">Velg tilkobling</string>
|
||||
<string name="chargeprice_provider_customer_tariff">Kun for tilbyderkunder</string>
|
||||
<string name="chargeprice_provider_customer_tariff">Kun for kundekoblingssalg</string>
|
||||
<string name="chargeprice_blocking_fee">Blokkeringsgebyr >%s</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs">Vis abonnementer fra kundekoblingssalg</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Noen tilbydere gir billigere abonnementer til sine kunder (f.eks. husstandselektrisitet, gass, osv)</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs">Inkluder kundekoblingssalg</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Strømselskaper tilbyr noen ganger billigere abonnementer til sine kunder</string>
|
||||
<string name="pref_my_tariffs">Mine ladeabonnementer</string>
|
||||
<string name="chargeprice_no_compatible_connectors">Ingen av tilkoblingene på denne ladestasjonen er kompatible med ditt kjøretøy.</string>
|
||||
<string name="data_sources_description">Flere datakilder støttes for innhenting av stasjoner. Velg den du ønsker å bruke og gjør endringer senere i innstillingene hvis nødvendig.</string>
|
||||
<string name="chargeprice_no_compatible_connectors">Ingen kompatible ladere på denne ladestasjonen</string>
|
||||
<string name="data_sources_description">Velg en datakilde for ladestasjoner. (Kan endres senere i programinnstillingene.)</string>
|
||||
<string name="unknown_operator">Ukjent operatør</string>
|
||||
<plurals name="chargeprice_some_tariffs_selected">
|
||||
<item quantity="one">%d plan valgt</item>
|
||||
<item quantity="other">%d planer valgt</item>
|
||||
</plurals>
|
||||
<string name="data_source_goingelectric_desc">Veldig god dekning i Tyskland, Østerrike, Sveits, og mange land i nærheten. Beskrivelser på tysk. Gemenskapsdrevet.</string>
|
||||
<string name="data_source_goingelectric_desc">Storartet i tyskspråklige land. Beskrivelser på tysk. Gemenskapsdrevet.</string>
|
||||
<string name="powered_by_mapbox">tilbudt av Mapbox</string>
|
||||
<string name="pref_search_provider_info">Data for stedssøk. Spesielt for Google Maps er dette relativt kostbart. Hvis du bruker dette ofte bes du om å donere gjennom «Om EVMap → Doner».</string>
|
||||
<string name="data_source_openchargemap_desc">Verdensomspennende dekning med varierende kvalitet. Beskrivelser på engelsk eller lokalt språk. Gemenskapsdrevet og åpen data fra myndigheter i noen land (f.eks. Nord-Amerika, Storbritannia, Frankrike, Norge.)</string>
|
||||
<string name="pref_search_provider_info">Data for søk er dyre å hente, spesielt fra Google Maps. Overvei å donere gjennom «Om» → «Doner».</string>
|
||||
<string name="data_source_openchargemap_desc">Verdensomspennende, med varierende kvalitet. Beskrivelser på engelsk eller det lokale språket. Gemenskapsdrevet og åpen myndighetsdata i noen land (f.eks. Nord-Amerika, Storbritannia, Frankrike, og Norge.)</string>
|
||||
<string name="lets_go">Begynn</string>
|
||||
<string name="crash_report_text">EVMap har krasjet. Send en rapport til utvikleren.</string>
|
||||
<string name="crash_report_text">EVMap krasjet. Send en rapport til utvikleren.</string>
|
||||
<string name="chargeprice_all_tariffs_selected">alle planer valgt</string>
|
||||
<string name="pref_search_provider">Stedssøkstilbyder</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load">Tillat skjev-belastning</string>
|
||||
<string name="edit_on_goingelectric_info">Hvis en tom side vises her må du logge inn på GoingElectric.de først.</string>
|
||||
<string name="pref_search_provider">Søketilbyder</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load">Tillat skjev last</string>
|
||||
<string name="edit_on_goingelectric_info">Logg inn på GoingElectric.de hvis denne siden er tom</string>
|
||||
<string name="pref_provider_google_maps">Google Maps</string>
|
||||
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
|
||||
<string name="charging_free">Gratis</string>
|
||||
@@ -272,4 +269,19 @@
|
||||
<string name="privacy_link">https://evmap.vonforst.net/en/privacy.html</string>
|
||||
<string name="faq_link">https://evmap.vonforst.net/en/faq.html</string>
|
||||
<string name="chargeprice_faq_link">https://evmap.vonforst.net/en/chargeprice_faq.html</string>
|
||||
<string name="about_contributors">Bidragsytere</string>
|
||||
<string name="about_contributors_text">Takk til alle som har kodet og oversatt EVMap:</string>
|
||||
<plurals name="prediction_number_available">
|
||||
<item quantity="one">%1$d/%2$d tilgjengelig</item>
|
||||
<item quantity="other">%1$d/%2$d tilgjengelig</item>
|
||||
</plurals>
|
||||
<string name="prediction_help">Basert på hvilken dag det er, tid på dagen, og tidligere bruk, slik at du kan finne ledige ladere. Ingen garanti dog.</string>
|
||||
<string name="prediction_time_colon">%s:</string>
|
||||
<string name="prediction_dc_plugs_only">Likestrømmsstøpsel</string>
|
||||
<string name="location_error">Klarte ikke å fastsette posisjon. Sjekk systeminnstillingene.</string>
|
||||
<string name="utilization_prediction">Bruksprognose</string>
|
||||
<string name="prediction_only">(kun %s)</string>
|
||||
<string name="pref_prediction_enabled">Vis bruksprognoser</string>
|
||||
<string name="pref_prediction_enabled_summary">for støttede ladere
|
||||
\n(foreløpig kun for likestrøm i Tyskland)</string>
|
||||
</resources>
|
||||
@@ -2,4 +2,5 @@
|
||||
<resources>
|
||||
<color name="background">#121212</color>
|
||||
<color name="my_tariff_background">#1FFFFFFF</color>
|
||||
<color name="logo_tint_night">#FFFFFF</color>
|
||||
</resources>
|
||||
4
app/src/main/res/values-w960dp/bools.xml
Normal file
4
app/src/main/res/values-w960dp/bools.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="bottom_sheet_collapsible">false</bool>
|
||||
</resources>
|
||||
@@ -2,4 +2,5 @@
|
||||
<resources>
|
||||
<dimen name="map_toolbar_width">500dp</dimen>
|
||||
<dimen name="layers_fab_top_padding">20dp</dimen>
|
||||
<dimen name="directions_fab_translationx">-44dp</dimen>
|
||||
</resources>
|
||||
4
app/src/main/res/values-w960dp/donottranslate.xml
Normal file
4
app/src/main/res/values-w960dp/donottranslate.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="hide_on_scroll_fab_behavior">@null</string>
|
||||
</resources>
|
||||
4
app/src/main/res/values/bools.xml
Normal file
4
app/src/main/res/values/bools.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="bottom_sheet_collapsible">true</bool>
|
||||
</resources>
|
||||
@@ -28,4 +28,5 @@
|
||||
<color name="my_tariff_background">#1F000000</color>
|
||||
<color name="background">#FFFFFF</color>
|
||||
<color name="pager_unselected">#1F000000</color>
|
||||
<color name="logo_tint_night">@null</color>
|
||||
</resources>
|
||||
|
||||
@@ -11,4 +11,5 @@
|
||||
<item name="match_parent" type="dimen">-1</item>
|
||||
<dimen name="map_toolbar_width">@dimen/match_parent</dimen>
|
||||
<dimen name="layers_fab_top_padding">100dp</dimen>
|
||||
<dimen name="directions_fab_translationx">0dp</dimen>
|
||||
</resources>
|
||||
@@ -8,8 +8,19 @@
|
||||
<string name="goingelectric_forum_url"><![CDATA[https://www.goingelectric.de/forum/viewtopic.php?f=5&t=56342]]></string>
|
||||
<string name="github_sponsors_link">https://github.com/sponsors/johan12345/</string>
|
||||
<string name="chargeprice_api_url">https://api.chargeprice.app/v1/</string>
|
||||
<string name="fronyx_url">https://fronyx.io/</string>
|
||||
<string name="pref_language_en">English</string>
|
||||
<string name="pref_language_de">Deutsch</string>
|
||||
<string name="pref_language_fr">Français</string>
|
||||
<string name="pref_language_nb_rNO">Norsk Bokmål</string>
|
||||
<string name="about_contributors_list">
|
||||
Danilo Bargen\n
|
||||
Altonss\n
|
||||
Allan Nordhøy\n
|
||||
Maximilian Goldschmidt\n
|
||||
Licaon_Kter\n
|
||||
pt2121\n
|
||||
nautilusx
|
||||
</string>
|
||||
<string name="hide_on_scroll_fab_behavior">net.vonforst.evmap.ui.HideOnScrollFabBehavior</string>
|
||||
</resources>
|
||||
@@ -2,8 +2,8 @@
|
||||
<string name="app_name">EVMap</string>
|
||||
<string name="title_activity_maps">EVMap</string>
|
||||
<string name="connectors">Connectors</string>
|
||||
<string name="no_maps_app_found">No navigation app found</string>
|
||||
<string name="no_browser_app_found">No web browser found</string>
|
||||
<string name="no_maps_app_found">Install a navigation app first</string>
|
||||
<string name="no_browser_app_found">Install a web browser first</string>
|
||||
<string name="address">Address</string>
|
||||
<string name="operator">Operator</string>
|
||||
<string name="network">Network</string>
|
||||
@@ -23,7 +23,7 @@
|
||||
<string name="parking_free">Free</string>
|
||||
<string name="parking_paid">Paid</string>
|
||||
<string name="amenities">Amenities</string>
|
||||
<string name="general_info">General information</string>
|
||||
<string name="general_info">General info</string>
|
||||
<string name="realtime_data_unavailable">Real-time status unavailable</string>
|
||||
<string name="realtime_data_loading">Checking real-time status…</string>
|
||||
<string name="realtime_data_source">Real-time status source (beta): %s</string>
|
||||
@@ -33,32 +33,32 @@
|
||||
<string name="menu_favs">Favorites</string>
|
||||
<string name="menu_filter">Filter</string>
|
||||
<string name="not_implemented">not implemented yet</string>
|
||||
<string name="about">About EVMap</string>
|
||||
<string name="about">About</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="github_link_title">Source code</string>
|
||||
<string name="oss_licenses">Open Source Licenses</string>
|
||||
<string name="oss_licenses">Licenses</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="settings_ui">User Interface</string>
|
||||
<string name="settings_ui">Interface</string>
|
||||
<string name="settings_map">Map</string>
|
||||
<string name="copyright">Copyright</string>
|
||||
<string name="copyright_summary">©2020–2022 Johan von Forstner</string>
|
||||
<string name="other">Other</string>
|
||||
<string name="privacy">Privacy Notice</string>
|
||||
<string name="fav_add">Add to favorites</string>
|
||||
<string name="privacy">Privacy</string>
|
||||
<string name="fav_add">Save as favorite</string>
|
||||
<string name="fav_remove">Remove from favorites</string>
|
||||
<string name="pref_navigate_use_maps">Start navigation immediately</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigation button starts Google Maps navigation immediately</string>
|
||||
<string name="pref_navigate_use_maps_off">Navigation button launches maps app with charger location</string>
|
||||
<string name="pref_navigate_use_maps">Immediate navigation</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigation button starts route guidance with Google Maps</string>
|
||||
<string name="pref_navigate_use_maps_off">Navigation button opens the maps app with charger location</string>
|
||||
<string name="coordinates">Coordinates</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="filter_free">Only free chargers</string>
|
||||
<string name="filter_min_power">Minimum power</string>
|
||||
<string name="filter_min_power">Min power</string>
|
||||
<string name="filter_free_parking">Only chargers with free parking</string>
|
||||
<string name="filter_min_connectors">Minimum number of connectors</string>
|
||||
<string name="filter_min_connectors">Min number of connectors</string>
|
||||
<string name="filter_connectors">Connectors</string>
|
||||
<string name="plug_type_1">Type 1</string>
|
||||
<string name="plug_type_2">Type 2</string>
|
||||
<string name="plug_type_3">Type 3a</string>
|
||||
<string name="plug_type_3">Type 3A</string>
|
||||
<string name="plug_ccs">CCS</string>
|
||||
<string name="plug_schuko">Schuko</string>
|
||||
<string name="plug_chademo">CHAdeMO</string>
|
||||
@@ -70,18 +70,17 @@
|
||||
<string name="none">none</string>
|
||||
<string name="show_more">more…</string>
|
||||
<string name="show_less">less…</string>
|
||||
<string name="favorites_empty_state">If you add chargers as favorites, they will show up here.</string>
|
||||
<string name="favorites_empty_state">Saved chargers show up here</string>
|
||||
<string name="donate">Donate</string>
|
||||
<string name="donation_successful">Thank you! ❤️</string>
|
||||
<string name="donation_failed">Something went wrong. 😕</string>
|
||||
<string name="donation_successful">Thank you ❤️</string>
|
||||
<string name="donation_failed">Something went wrong 😕</string>
|
||||
<string name="map_type_normal">Default</string>
|
||||
<string name="map_type_satellite">Satellite</string>
|
||||
<string name="map_type_terrain">Terrain</string>
|
||||
<string name="map_type">Map type</string>
|
||||
<string name="map_details">Map details</string>
|
||||
<string name="map_traffic">Traffic</string>
|
||||
<string name="faq">FAQ</string>
|
||||
<string name="faq_desc">Frequently asked questions</string>
|
||||
<string name="faq">Frequently asked questions</string>
|
||||
<string name="menu_filters_active">Filters active</string>
|
||||
<string name="filters_activated">Filters activated</string>
|
||||
<string name="filters_deactivated">Filters deactivated</string>
|
||||
@@ -98,11 +97,10 @@
|
||||
<string name="edit">edit</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="pref_language">Language</string>
|
||||
<string name="pref_language_summary">Change the app language</string>
|
||||
<string name="pref_language">App language</string>
|
||||
<string name="pref_darkmode">Dark mode</string>
|
||||
<string name="pref_darkmode_summary">Set when dark mode is activated</string>
|
||||
<string name="connection_error">Could not load charging stations</string>
|
||||
<string name="location_error">Failed to detect location. Please check system settings</string>
|
||||
<string name="retry">Retry</string>
|
||||
<string name="filter_open_247">Available 24/7</string>
|
||||
<string name="filter_barrierfree">Usable without registration</string>
|
||||
@@ -113,7 +111,7 @@
|
||||
<string name="twitter">Twitter</string>
|
||||
<string name="goingelectric_forum">Forum thread at GoingElectric.de</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="menu_report_new_charger">Report new charger</string>
|
||||
<string name="menu_report_new_charger">New charger</string>
|
||||
<string name="edit_at_datasource">edit at %s</string>
|
||||
<string name="categories">Categories</string>
|
||||
<string name="category_car_dealership">Car Dealership</string>
|
||||
@@ -131,7 +129,7 @@
|
||||
<string name="category_church">Church</string>
|
||||
<string name="category_hospital">Hospital</string>
|
||||
<string name="category_museum">Museum</string>
|
||||
<string name="category_parking_multi">Multi-storey car park</string>
|
||||
<string name="category_parking_multi">Parking garage</string>
|
||||
<string name="category_parking">Car park</string>
|
||||
<string name="category_private_charger">Private charger</string>
|
||||
<string name="category_rest_area">Rest area</string>
|
||||
@@ -151,16 +149,16 @@
|
||||
<string name="delete">Delete</string>
|
||||
<string name="save_as_profile">Save as profile</string>
|
||||
<string name="save_profile_enter_name">Enter the name of the filter profile:</string>
|
||||
<string name="filterprofiles_empty_state">You have not yet saved any filter profiles.</string>
|
||||
<string name="filterprofiles_empty_state">You have no filter profiles saved</string>
|
||||
<string name="welcome_to_evmap">Welcome to EVMap</string>
|
||||
<string name="welcome_1">Find electric vehicle chargers around you.</string>
|
||||
<string name="welcome_1">Find electric vehicle chargers around you</string>
|
||||
<string name="welcome_2_title">You\'ve got the power</string>
|
||||
<string name="welcome_2">The color of a charger on the map shows you its maximum charging power.</string>
|
||||
<string name="welcome_2_detail">(You can check the colors again under “About EVMap → FAQ” in the menu)</string>
|
||||
<string name="donation_dialog_title">Thank you for using EVMap!</string>
|
||||
<string name="donation_dialog_detail">EVMap is free and Open Source software that I develop in my spare time. Coding contributions on GitHub are very much appreciated. However, due to increasing popularity of the app, I also need to cover some running costs, e.g. for access to the data sources. Therefore, please consider supporting the app through a donation or via GitHub Sponsors.</string>
|
||||
<string name="welcome_2">Each charger\'s color corresponds to its max charging power</string>
|
||||
<string name="welcome_2_detail">This can also be seen in “About” → “Frequently Asked Questions”</string>
|
||||
<string name="donation_dialog_title">Thank you for using EVMap</string>
|
||||
<string name="donation_dialog_detail">EVMap is libre and free of charge. Code contributions on GitHub are much appreciated. To help cover the running costs for data access, please consider donating an amount of your choice to the developer.</string>
|
||||
<string name="chargeprice_donation_dialog_title">You\'re a real bargain hunter!</string>
|
||||
<string name="chargeprice_donation_dialog_detail">It seems like you like the price comparison feature a lot. To access the pricing data, the developer of EVMap needs to pay a monthly fee to the data provider Chargeprice.app. Therefore, please consider supporting EVMap through a donation.</string>
|
||||
<string name="chargeprice_donation_dialog_detail">You make great use of the price comparison feature. Please help cover the costs for this data by supporting EVMap with a donation.</string>
|
||||
<string name="deleted_filterprofile">Deleted “%s”</string>
|
||||
<string name="undo">Undo</string>
|
||||
<string name="rename">Rename</string>
|
||||
@@ -171,42 +169,43 @@
|
||||
</plurals>
|
||||
<string name="navigate">Navigate</string>
|
||||
<string name="verified">verified</string>
|
||||
<string name="verified_desc">Charger verified by a member at the %s community — not necessarily working right now.</string>
|
||||
<string name="verified_desc">Charger has once been confirmed to work by a member of the %s community</string>
|
||||
<string name="charge_price_format">%2$s%1$.2f</string>
|
||||
<string name="charge_price_average_format">⌀ %2$s%1$.2f/kWh</string>
|
||||
<string name="charge_price_kwh_format">%2$s%1$.2f/kWh</string>
|
||||
<string name="chargeprice_select_connector">Choose connector</string>
|
||||
<string name="chargeprice_provider_customer_tariff">Only for provider customers</string>
|
||||
<string name="edit_on_goingelectric_info">If only an empty page is showing here, please first log in to GoingElectric.de.</string>
|
||||
<string name="chargeprice_provider_customer_tariff">Only for tie-in customers</string>
|
||||
<string name="edit_on_goingelectric_info">Please log in at GoingElectric.de if this page is empty</string>
|
||||
<string name="percent_format">%.0f%%</string>
|
||||
<string name="chargeprice_session_fee">session fee</string>
|
||||
<string name="chargeprice_per_kwh">per kWh</string>
|
||||
<string name="chargeprice_per_minute">per min</string>
|
||||
<string name="chargeprice_blocking_fee">Blocking fee >%s</string>
|
||||
<string name="chargeprice_no_tariffs_found">Chargeprice.app found no charging plans compatible with this charger.</string>
|
||||
<string name="chargeprice_no_tariffs_found">No charging plans for this charger on Chargeprice.app</string>
|
||||
<string name="powered_by_chargeprice">powered by Chargeprice</string>
|
||||
<string name="chargeprice_base_fee">Base fee: %2$s%1$.2f/month</string>
|
||||
<string name="chargeprice_min_spend">Minimum spend: %2$s%1$.2f/month</string>
|
||||
<string name="settings_chargeprice">Price comparison</string>
|
||||
<string name="pref_my_vehicle">My vehicles</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Only show plans with no monthly fees</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs">Show customer-exclusive plans</string>
|
||||
<string name="chargeprice_select_car_first">Please first select your car model in the settings.</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Exclude plans with monthly fees</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs">Include tie-in plans</string>
|
||||
<string name="chargeprice_select_car_first">Please select your car model in the settings first</string>
|
||||
<string name="chargeprice_battery_range">Charge from %1$.0f%% to %2$.0f%%</string>
|
||||
<string name="chargeprice_battery_range_from">Charge from</string>
|
||||
<string name="chargeprice_battery_range_to">to</string>
|
||||
<string name="chargeprice_stats">(%1$.0f kWh, approx. %2$s, ⌀ %3$.0f kW)</string>
|
||||
<string name="chargeprice_vehicle">Vehicle</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Some providers offer cheaper plans exclusively to their customers (e.g., household electricity, gas)</string>
|
||||
<string name="close">close</string>
|
||||
<string name="chargeprice_price_not_available">Price not available</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Utility companies sometimes offer special plans for their customers</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="chargeprice_title">Prices</string>
|
||||
<string name="chargeprice_connection_error">Could not load prices</string>
|
||||
<string name="chargeprice_no_compatible_connectors">None of the connectors on this charging station is compatible with your vehicle.</string>
|
||||
<string name="chargeprice_no_compatible_connectors">No compatible connectors at this charging station</string>
|
||||
<string name="pref_chargeprice_currency">Currency</string>
|
||||
<string name="pref_my_tariffs">My charging plans</string>
|
||||
<plurals name="pref_my_tariffs_summary">
|
||||
<item quantity="one">(will be highlighted in price comparison)</item>
|
||||
<item quantity="other">(will be highlighted in price comparison)</item>
|
||||
<item quantity="one">(will be highlighted in the price comparison)</item>
|
||||
<item quantity="other">(will be highlighted in the price comparison)</item>
|
||||
</plurals>
|
||||
<string name="chargeprice_all_tariffs_selected">all plans selected</string>
|
||||
<string name="license">License</string>
|
||||
@@ -217,20 +216,20 @@
|
||||
<item quantity="other">%d plans selected</item>
|
||||
</plurals>
|
||||
<string name="unknown_operator">Unknown operator</string>
|
||||
<string name="data_sources_description">EVMap supports multiple data sources for charging stations. Please select the one you would like to use. You can always change it later in the app\'s settings.</string>
|
||||
<string name="data_sources_description">Please pick a data source for charging stations. It can later be changed in the app settings.</string>
|
||||
<string name="data_source_goingelectric">GoingElectric.de</string>
|
||||
<string name="data_source_openchargemap">Open Charge Map</string>
|
||||
<string name="data_source_goingelectric_desc">Very good coverage in Germany, Austria and Switzerland and many neighboring countries. Descriptions in German. Community-maintained.</string>
|
||||
<string name="data_source_openchargemap_desc"><![CDATA[Worldwide coverage with varying quality. Descriptions in English or local language. Community-maintained & government open data in some countries (e.g. North America, UK, France, Norway).]]></string>
|
||||
<string name="data_source_goingelectric_desc">Great in the German-speaking countries. Descriptions in German. Community-maintained.</string>
|
||||
<string name="data_source_openchargemap_desc"><![CDATA[Worldwide, with varying quality. Descriptions in English or the local language. Community-maintained and open government data in some countries (e.g. North America, UK, France, Norway).]]></string>
|
||||
<string name="next">next</string>
|
||||
<string name="get_started">Get started</string>
|
||||
<string name="got_it">Got it</string>
|
||||
<string name="lets_go">Let\'s go</string>
|
||||
<string name="crash_report_text">Sorry, it seems that EVMap has crashed. Please send a crash report to the developer.</string>
|
||||
<string name="crash_report_text">EVMap crashed. Please send a crash report to the developer.</string>
|
||||
<string name="crash_report_comment_prompt">You can add a comment below:</string>
|
||||
<string name="powered_by_mapbox">powered by Mapbox</string>
|
||||
<string name="pref_search_provider">Place search provider</string>
|
||||
<string name="pref_search_provider_info"><![CDATA[Data for place search, especially from Google Maps, is relatively expensive. If you use this feature often, please consider making a donation through \"About EVMap -> Donate\".]]></string>
|
||||
<string name="pref_search_provider">Search provider</string>
|
||||
<string name="pref_search_provider_info"><![CDATA[Data for searches is expensive to fetch, especially from Google Maps. Please consider donating through “About” → “Donate”.]]></string>
|
||||
<string name="github_sponsors">GitHub Sponsors</string>
|
||||
<string name="donate_desc">Support EVMap\'s development with a one-time donation</string>
|
||||
<string name="github_sponsors_desc">Support EVMap on GitHub Sponsors</string>
|
||||
@@ -245,13 +244,13 @@
|
||||
<string name="settings_data_sources">Data sources</string>
|
||||
<string name="help">Help</string>
|
||||
<string name="settings_android_auto">Android Auto</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load">Enable unbalanced load</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load_summary"><![CDATA[Allow charging with >4.5 kW at AC stations for cars with single-phase charger]]></string>
|
||||
<string name="pref_map_rotate_gestures_enabled">Enable map rotation</string>
|
||||
<string name="pref_map_rotate_gestures_on">Map can be rotated with two-finger gesture</string>
|
||||
<string name="pref_map_rotate_gestures_off">Map will be fixed to north-up</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load">Allow unbalanced load</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load_summary">Allow single-phase AC charging with more than 4.5 kW</string>
|
||||
<string name="pref_map_rotate_gestures_enabled">Map rotation</string>
|
||||
<string name="pref_map_rotate_gestures_on">Use two fingers to rotate the map</string>
|
||||
<string name="pref_map_rotate_gestures_off">Rotation off (north always up)</string>
|
||||
<string name="refresh_live_data">refresh real-time status</string>
|
||||
<string name="autocomplete_connection_error">Suggestions could not be loaded</string>
|
||||
<string name="autocomplete_connection_error">Could not load suggestions</string>
|
||||
<string name="pref_language_device_default">Device default</string>
|
||||
<string name="pref_darkmode_device_default">Device default</string>
|
||||
<string name="pref_darkmode_always_on">always on</string>
|
||||
@@ -270,4 +269,22 @@
|
||||
<string name="pref_chargeprice_currency_usd">US dollar (USD)</string>
|
||||
<string name="pref_provider_google_maps">Google Maps</string>
|
||||
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
|
||||
<string name="about_contributors">Contributors</string>
|
||||
<string name="about_contributors_text">Thanks to all contributors for their coding and translation contributions to EVMap:</string>
|
||||
<string name="utilization_prediction">Utilization prediction</string>
|
||||
<string name="prediction_help">The prediction is based on factors like day of the week, time of day and past usage, so that you can avoid overcrowded chargers. No guarantee.</string>
|
||||
<string name="prediction_time_colon">%s:</string>
|
||||
<plurals name="prediction_number_available">
|
||||
<item quantity="one">%1$d/%2$d available</item>
|
||||
<item quantity="other">%1$d/%2$d available</item>
|
||||
</plurals>
|
||||
<string name="pref_prediction_enabled">Show utilization predictions</string>
|
||||
<string name="pref_prediction_enabled_summary">for supported chargers\n(currently only DC in Germany)</string>
|
||||
<string name="prediction_only">(%s only)</string>
|
||||
<string name="prediction_dc_plugs_only">DC plugs</string>
|
||||
<string name="data_source_switched_to">Data source switched to %s</string>
|
||||
<string name="pref_applink_associate">Open supported links</string>
|
||||
<string name="pref_applink_associate_summary">from goingelectric.de and openchargemap.org</string>
|
||||
<string name="chargeprice_header_my_tariffs">My plans</string>
|
||||
<string name="chargeprice_header_other_tariffs">Other plans</string>
|
||||
</resources>
|
||||
|
||||
@@ -14,10 +14,13 @@
|
||||
android:title="@string/copyright"
|
||||
android:summary="@string/copyright_summary" />
|
||||
|
||||
<Preference
|
||||
android:key="contributors"
|
||||
android:title="@string/about_contributors" />
|
||||
|
||||
<Preference
|
||||
android:key="faq"
|
||||
android:title="@string/faq"
|
||||
android:summary="@string/faq_desc" />
|
||||
android:title="@string/faq" />
|
||||
|
||||
<Preference
|
||||
android:key="donate"
|
||||
|
||||
7
app/src/main/res/xml/locales_config.xml
Normal file
7
app/src/main/res/xml/locales_config.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<locale android:name="en" />
|
||||
<locale android:name="de" />
|
||||
<locale android:name="fr" />
|
||||
<locale android:name="nb-NO" />
|
||||
</locale-config>
|
||||
@@ -9,6 +9,12 @@
|
||||
android:entryValues="@array/pref_data_source_values"
|
||||
android:defaultValue="goingelectric"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="prediction_enabled"
|
||||
android:title="@string/pref_prediction_enabled"
|
||||
android:defaultValue="true"
|
||||
android:summary="@string/pref_prediction_enabled_summary" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/settings_map">
|
||||
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<ListPreference
|
||||
android:key="language"
|
||||
android:persistent="false"
|
||||
android:title="@string/pref_language"
|
||||
android:entries="@array/pref_language_names"
|
||||
android:entryValues="@array/pref_language_values"
|
||||
android:defaultValue="default"
|
||||
android:summary="@string/pref_language_summary" />
|
||||
android:summary="%s" />
|
||||
|
||||
<ListPreference
|
||||
android:key="darkmode"
|
||||
@@ -14,7 +15,7 @@
|
||||
android:entries="@array/pref_darkmode_names"
|
||||
android:entryValues="@array/pref_darkmode_values"
|
||||
android:defaultValue="default"
|
||||
android:summary="@string/pref_darkmode_summary" />
|
||||
android:summary="%s" />
|
||||
<CheckBoxPreference
|
||||
android:key="map_rotate_gestures_enabled"
|
||||
android:title="@string/pref_map_rotate_gestures_enabled"
|
||||
@@ -27,4 +28,8 @@
|
||||
android:summaryOn="@string/pref_navigate_use_maps_on"
|
||||
android:summaryOff="@string/pref_navigate_use_maps_off"
|
||||
android:defaultValue="true" />
|
||||
<Preference
|
||||
android:key="applink_associate"
|
||||
android:title="@string/pref_applink_associate"
|
||||
android:summary="@string/pref_applink_associate_summary" />
|
||||
</PreferenceScreen>
|
||||
@@ -0,0 +1,89 @@
|
||||
package net.vonforst.evmap.api.fronyx
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.vonforst.evmap.okResponse
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.net.HttpURLConnection
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
class FronyxApiTest {
|
||||
val webServer = MockWebServer()
|
||||
val fronyx: FronyxApi
|
||||
|
||||
init {
|
||||
webServer.start()
|
||||
|
||||
val apikey = ""
|
||||
fronyx = FronyxApi(
|
||||
apikey,
|
||||
webServer.url("/").toString()
|
||||
)
|
||||
|
||||
webServer.dispatcher = object : Dispatcher() {
|
||||
val notFoundResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
|
||||
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
val segments = request.requestUrl!!.pathSegments
|
||||
val urlHead = segments.subList(0, 2).joinToString("/")
|
||||
when (urlHead) {
|
||||
"predictions/evse-id" -> {
|
||||
val id = segments[2]
|
||||
return okResponse("/fronyx/${id.replace("*", "_")}.json")
|
||||
}
|
||||
"predictions/evses" -> {
|
||||
val ids = request.requestUrl!!.queryParameter("evseIds")!!.split(",")
|
||||
return okResponse(
|
||||
"/fronyx/${
|
||||
ids.map { it.replace("*", "_") }.joinToString(",")
|
||||
}.json"
|
||||
)
|
||||
}
|
||||
else -> return notFoundResponse
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun apiTestSingle() {
|
||||
val evseId = "DE*ION*E202102"
|
||||
|
||||
runBlocking {
|
||||
val result = fronyx.getPredictionsForEvseId(evseId)
|
||||
assertEquals(result.evseId, evseId)
|
||||
assertEquals(25, result.predictions.size)
|
||||
assertEquals(
|
||||
ZonedDateTime.of(2022, 9, 18, 13, 45, 0, 0, ZoneOffset.UTC),
|
||||
result.predictions[0].timestamp
|
||||
)
|
||||
assertEquals(FronyxStatus.AVAILABLE, result.predictions[0].status)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun apiTestMultiple() {
|
||||
val evseIds = listOf("DE*ION*E202101", "DE*ION*E202102")
|
||||
|
||||
runBlocking {
|
||||
val results = fronyx.getPredictionsForEvseIds(evseIds)
|
||||
results.forEachIndexed { i, result ->
|
||||
assertEquals(result.evseId, evseIds[i])
|
||||
assertEquals(25, result.predictions.size)
|
||||
assertEquals(
|
||||
ZonedDateTime.of(2022, 11, 16, 18, 0, 0, 0, ZoneOffset.UTC),
|
||||
result.predictions[0].timestamp
|
||||
)
|
||||
assertEquals(
|
||||
if (i == 0) FronyxStatus.UNAVAILABLE else FronyxStatus.AVAILABLE,
|
||||
result.predictions[0].status
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.vonforst.evmap.model
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class ChargersModelTest {
|
||||
@Test
|
||||
fun testAddressToString() {
|
||||
assertEquals("Berlin", Address("Berlin", null, null, null).toString())
|
||||
assertEquals("12345 Berlin", Address("Berlin", null, "12345", null).toString())
|
||||
assertEquals(
|
||||
"Pariser Platz 1, Berlin",
|
||||
Address("Berlin", null, null, "Pariser Platz 1").toString()
|
||||
)
|
||||
assertEquals(
|
||||
"Pariser Platz 1, 12345 Berlin",
|
||||
Address("Berlin", null, "12345", "Pariser Platz 1").toString()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
214
app/src/test/resources/fronyx/DE_ION_E202101,DE_ION_E202102.json
Normal file
214
app/src/test/resources/fronyx/DE_ION_E202101,DE_ION_E202102.json
Normal file
@@ -0,0 +1,214 @@
|
||||
[
|
||||
{
|
||||
"evseId": "DE*ION*E202101",
|
||||
"locationId": "DE-ION-03e2876e-0fd0-4e9d-abc1-d2caa1473947",
|
||||
"predictions": [
|
||||
{
|
||||
"timestamp": "2022-11-16T18:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T18:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T18:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T18:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-17T00:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"evseId": "DE*ION*E202102",
|
||||
"locationId": "DE-ION-03e2876e-0fd0-4e9d-abc1-d2caa1473947",
|
||||
"predictions": [
|
||||
{
|
||||
"timestamp": "2022-11-16T18:00:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T18:15:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T18:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T18:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T19:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T20:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:15:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:30:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T21:45:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:00:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:15:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:30:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T22:45:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:00:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:15:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:30:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-16T23:45:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-11-17T00:00:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
105
app/src/test/resources/fronyx/DE_ION_E202102.json
Normal file
105
app/src/test/resources/fronyx/DE_ION_E202102.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"evseId": "DE*ION*E202102",
|
||||
"predictions": [
|
||||
{
|
||||
"timestamp": "2022-09-18T13:45:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T14:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T14:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T14:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T14:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T15:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T15:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T15:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T15:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T16:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T16:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T16:30:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T16:45:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T17:00:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T17:15:00.000Z",
|
||||
"status": "UNAVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T17:30:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T17:45:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T18:00:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T18:15:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T18:30:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T18:45:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T19:00:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T19:15:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T19:30:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
},
|
||||
{
|
||||
"timestamp": "2022-09-18T19:45:00.000Z",
|
||||
"status": "AVAILABLE"
|
||||
}
|
||||
]
|
||||
}
|
||||
40
app/src/testGoogle/java/net/vonforst/evmap/auto/UtilsTest.kt
Normal file
40
app/src/testGoogle/java/net/vonforst/evmap/auto/UtilsTest.kt
Normal file
@@ -0,0 +1,40 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class UtilsTest {
|
||||
@Test
|
||||
fun testPaginate() {
|
||||
var (nSingle, nFirst, nOther, nLast) = listOf(6, 5, 4, 5)
|
||||
for (i in 0..30) {
|
||||
paginateTest(i, nSingle, nFirst, nOther, nLast)
|
||||
}
|
||||
nSingle = 4; nFirst = 4; nOther = 6; nLast = 6
|
||||
for (i in 0..30) {
|
||||
paginateTest(i, nSingle, nFirst, nOther, nLast)
|
||||
}
|
||||
}
|
||||
|
||||
private fun paginateTest(
|
||||
i: Int,
|
||||
nSingle: Int,
|
||||
nFirst: Int,
|
||||
nOther: Int,
|
||||
nLast: Int
|
||||
) {
|
||||
val list = (0..i).toList()
|
||||
val paginated = list.paginate(nSingle, nFirst, nOther, nLast)
|
||||
assertEquals(list, paginated.flatten())
|
||||
assert(paginated.all { it.isNotEmpty() })
|
||||
if (paginated.size == 1) {
|
||||
assert(paginated.first().size <= nSingle)
|
||||
} else {
|
||||
assert(paginated.first().size == nFirst)
|
||||
for (j in 1 until paginated.size - 1) {
|
||||
assert(paginated[j].size == nOther)
|
||||
}
|
||||
assert(paginated.last().size <= nLast)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,14 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
ext.about_libs_version = '8.9.4'
|
||||
ext.nav_version = '2.5.1'
|
||||
ext.nav_version = '2.5.3'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.2'
|
||||
classpath 'com.android.tools.build:gradle:7.3.1'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
|
||||
|
||||
@@ -26,6 +26,9 @@ be put into the app in the form of a resource file called `apikeys.xml` under
|
||||
<string name="openchargemap_key" translatable="false">
|
||||
insert your OpenChargeMap key here
|
||||
</string>
|
||||
<string name="fronyx_key" translatable="false">
|
||||
insert your Fronyx key here
|
||||
</string>
|
||||
</resources>
|
||||
```
|
||||
|
||||
@@ -169,3 +172,18 @@ In case you want to pay for access to the full Chargeprice API, check out their
|
||||
[sales@chargeprice.net](mailto:sales@chargeprice.net).
|
||||
</details>
|
||||
|
||||
Availability data providers
|
||||
---------------------------
|
||||
|
||||
### fronyx
|
||||
|
||||
[fronyx](https://fronyx.io/) provides us predictions of charging station availability.
|
||||
|
||||
<details>
|
||||
<summary>How to obtain an API key</summary>
|
||||
|
||||
The API is not publically available, contact [fronyx](https://fronyx.io/contact-us/) to get an API
|
||||
key and documentation.
|
||||
|
||||
If you don't want to test this functionality, simply leave the API key blank.
|
||||
</details>
|
||||
2
fastlane/metadata/android/de-DE/changelogs/106.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/106.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Wechseln der Datenquelle nicht möglich
|
||||
8
fastlane/metadata/android/de-DE/changelogs/116.txt
Normal file
8
fastlane/metadata/android/de-DE/changelogs/116.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Verbesserungen:
|
||||
- Einige Texte vereinfacht
|
||||
- Unterstützung für Sprachauswahl pro App von Android 13
|
||||
|
||||
Fehler behoben:
|
||||
- Filtermenü ließ sich nicht öffnen
|
||||
- Abstürze / Inkonsistenzen nach Wechsel der Datenquelle
|
||||
- Abstürze unter Android Auto
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user