mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-25 08:07:46 -05:00
Compare commits
194 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
350f18df8e | ||
|
|
dda151abf5 | ||
|
|
a86f1397f4 | ||
|
|
086cc51dd3 | ||
|
|
0de91bc107 | ||
|
|
3436bcd870 | ||
|
|
22c150d557 | ||
|
|
675abb5011 | ||
|
|
af2a2cfcae | ||
|
|
f74526fdd6 | ||
|
|
c5bbca0428 | ||
|
|
6167079c0e | ||
|
|
c3836a92ad | ||
|
|
dccce1a0a0 | ||
|
|
74d79640a8 | ||
|
|
0eb6ece780 | ||
|
|
ae15b13591 | ||
|
|
4962eb7268 | ||
|
|
abe360d7c2 | ||
|
|
2aa1fcf5bd | ||
|
|
221e5f49bc | ||
|
|
df6f26ad56 | ||
|
|
1210efd3b9 | ||
|
|
097be8c92b | ||
|
|
16031884ac | ||
|
|
c0b4c56eda | ||
|
|
9587ee948d | ||
|
|
890eec4419 | ||
|
|
c972c871d4 | ||
|
|
e4da902430 | ||
|
|
7a5d4b4107 | ||
|
|
80642b1731 | ||
|
|
6dab611c1b | ||
|
|
d9fc43af68 | ||
|
|
2fd0fa7e22 | ||
|
|
b04284fb16 | ||
|
|
7b3bd84d18 | ||
|
|
773d052819 | ||
|
|
4e0ad98e17 | ||
|
|
d8e572338a | ||
|
|
ff86eeff95 | ||
|
|
47f57992fb | ||
|
|
0ae59358ca | ||
|
|
576e0b9c42 | ||
|
|
3878b27154 | ||
|
|
2166ac076a | ||
|
|
c489df2aaf | ||
|
|
56712ff1af | ||
|
|
e2cf332f34 | ||
|
|
0b541d498d | ||
|
|
1bdc576300 | ||
|
|
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 | ||
|
|
97cab1d007 | ||
|
|
0d41fb2685 |
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 }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
EVMap [](https://github.com/johan12345/EVMap/actions)
|
||||
EVMap [](https://github.com/ev-map/EVMap/actions)
|
||||
=====
|
||||
|
||||
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/>
|
||||
<img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/>
|
||||
|
||||
Android app to find electric vehicle charging stations.
|
||||
|
||||
@@ -28,7 +28,7 @@ Features
|
||||
Screenshots
|
||||
-----------
|
||||
|
||||
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/screenshots/phone/en/mapbox/01_map.png" width=250 alt="Screenshot 1"/><img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/screenshots/phone/en/mapbox/02_detail.png" width=250 alt="Screenshot 2"/>
|
||||
<img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/screenshots/phone/en/mapbox/01_map.png" width=250 alt="Screenshot 1"/><img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/screenshots/phone/en/mapbox/02_detail.png" width=250 alt="Screenshot 2"/>
|
||||
|
||||
Development setup
|
||||
-----------------
|
||||
|
||||
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 |
@@ -8,21 +8,24 @@ apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'androidx.navigation.safeargs.kotlin'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
apply plugin: 'de.timfreiheit.resourceplaceholders'
|
||||
|
||||
def supportedLocales = "en,de,fr,nb-rNO,nl"
|
||||
|
||||
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 106
|
||||
versionName "1.3.10"
|
||||
versionCode 164
|
||||
versionName "1.4.8"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs supportedLocales.split(',')
|
||||
buildConfigField("String", "supportedLocales", '"' + supportedLocales + '"')
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -93,15 +96,14 @@ android {
|
||||
}
|
||||
lint {
|
||||
disable 'NullSafeMutableLiveData'
|
||||
warning 'MissingTranslation'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.includeAndroidResources true
|
||||
}
|
||||
|
||||
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 +137,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,31 +155,28 @@ 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.1'
|
||||
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.5"
|
||||
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.8.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.browser:browser:1.4.0'
|
||||
implementation 'androidx.browser:browser:1.5.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:dd0167dbff'
|
||||
implementation 'com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b'
|
||||
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.github.ev-map:StfalconImageViewer:5082ebd392'
|
||||
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
|
||||
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
|
||||
implementation 'com.airbnb.android:lottie:4.1.0'
|
||||
@@ -180,28 +186,30 @@ dependencies {
|
||||
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
|
||||
|
||||
// Android Auto
|
||||
def carAppVersion = '1.2.0-rc01'
|
||||
def carAppVersion = '1.3.0-rc01'
|
||||
googleImplementation "androidx.car.app:app:$carAppVersion"
|
||||
googleNormalImplementation "androidx.car.app:app-projected:$carAppVersion"
|
||||
googleAutomotiveImplementation "androidx.car.app:app-automotive:$carAppVersion"
|
||||
|
||||
// AnyMaps
|
||||
def anyMapsVersion = 'a9b3dd7d99'
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
|
||||
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
|
||||
def anyMapsVersion = '7fdcf50fc4'
|
||||
implementation "com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion"
|
||||
googleImplementation "com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion"
|
||||
googleImplementation 'com.google.android.gms:play-services-maps:18.1.0'
|
||||
implementation("com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion") {
|
||||
implementation("com.github.ev-map.AnyMaps:anymaps-mapbox:$anyMapsVersion") {
|
||||
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-accounts'
|
||||
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-telemetry'
|
||||
exclude group: 'com.google.android.gms', module: 'play-services-location'
|
||||
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-core'
|
||||
}
|
||||
// patched version of mapbox-android-core that removes build-time dependency on GMS
|
||||
implementation 'com.github.johan12345:mapbox-events-android:a21c324501'
|
||||
// original version of mapbox-android-core
|
||||
googleImplementation 'com.mapbox.mapboxsdk:mapbox-android-core:2.0.1'
|
||||
// patched version that removes build-time dependency on GMS (-> no Google location services)
|
||||
fossImplementation 'com.github.ev-map:mapbox-events-android:a21c324501'
|
||||
|
||||
// Google Places
|
||||
googleImplementation 'com.google.android.libraries.places:places:2.6.0'
|
||||
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.1'
|
||||
googleImplementation 'com.google.android.libraries.places:places:3.0.0'
|
||||
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4'
|
||||
|
||||
// Mapbox Geocoding
|
||||
implementation 'com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0'
|
||||
@@ -216,13 +224,13 @@ dependencies {
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
|
||||
// room library
|
||||
def room_version = "2.4.3"
|
||||
def room_version = "2.5.0"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
|
||||
// billing library
|
||||
def billing_version = "4.1.0"
|
||||
def billing_version = "5.1.0"
|
||||
googleImplementation "com.android.billingclient:billing:$billing_version"
|
||||
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
|
||||
|
||||
@@ -244,15 +252,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.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
|
||||
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:2.0.2'
|
||||
}
|
||||
|
||||
private static String decode(String s, String key) {
|
||||
|
||||
6
app/src/foss/res/values-nl/strings.xml
Normal file
6
app/src/foss/res/values-nl/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Vond je EVMap nuttig\? Je kan de ontwikkeling ondersteunen door een donatie te sturen naar de ontwikkelaar.</string>
|
||||
<string name="donate_paypal">Doneer via PayPal</string>
|
||||
<string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap (Mapbox).</string>
|
||||
</resources>
|
||||
@@ -2,5 +2,4 @@
|
||||
<resources>
|
||||
<string name="pref_search_provider_default" translatable="false">mapbox</string>
|
||||
<string name="pref_map_provider_default" translatable="false">mapbox</string>
|
||||
<string name="paypal_link" translatable="false">https://paypal.me/johan98</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.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
|
||||
@@ -12,6 +11,7 @@
|
||||
|
||||
<queries>
|
||||
<package android:name="com.google.android.projection.gearhead" />
|
||||
<package android:name="com.google.android.apps.automotive.templates.host" />
|
||||
</queries>
|
||||
|
||||
<application>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
@@ -24,11 +19,17 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
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,9 +42,8 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
}
|
||||
private var prices: List<ChargePrice>? = null
|
||||
private var meta: ChargepriceChargepointMeta? = null
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
private var chargepoint: Chargepoint? = null
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
private var errorMessage: String? = null
|
||||
private val batteryRange = prefs.chargepriceBatteryRangeAndroidAuto
|
||||
|
||||
@@ -62,17 +62,62 @@ 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)
|
||||
if (myPricesList.items.isNotEmpty() && otherPricesList.items.isNotEmpty()) {
|
||||
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 =
|
||||
if (myPricesList.items.isNotEmpty()) myPricesList else otherPricesList
|
||||
if (header != null) {
|
||||
addSectionedList(SectionedItemList.create(list, header))
|
||||
} else {
|
||||
setSingleList(list)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val list = buildPricesList(prices)
|
||||
if (header != null && list.items.isNotEmpty()) {
|
||||
addSectionedList(SectionedItemList.create(list, header))
|
||||
} else {
|
||||
setSingleList(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
setActionStrip(
|
||||
ActionStrip.Builder().addAction(
|
||||
@@ -84,41 +129,28 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
)
|
||||
).build()
|
||||
).setOnClickListener {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(
|
||||
ContextCompat.getColor(
|
||||
carContext,
|
||||
R.color.colorPrimary
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build().intent
|
||||
intent.data =
|
||||
Uri.parse(ChargepriceApi.getPoiUrl(charger))
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
carContext.startActivity(intent)
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.no_browser_app_found,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
openUrl(carContext, ChargepriceApi.getPoiUrl(charger))
|
||||
}.build()
|
||||
).build()
|
||||
)
|
||||
}.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 +160,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
|
||||
@@ -172,13 +206,21 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
}
|
||||
|
||||
private fun loadPrices(model: Model?) {
|
||||
val dataAdapter = ChargepriceApi.getDataAdapter(charger) ?: return
|
||||
val dataAdapter = ChargepriceApi.getDataAdapter(charger)
|
||||
val manufacturer = model?.manufacturer?.value
|
||||
val modelName = getVehicleModel(model?.manufacturer?.value, model?.name?.value)
|
||||
lifecycleScope.launch {
|
||||
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 +231,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 +256,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 +273,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 +288,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
|
||||
@@ -61,9 +64,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
private val iconGen =
|
||||
ChargerIconGenerator(carContext, null, height = imageSize)
|
||||
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PANE)
|
||||
} else 2
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PANE)
|
||||
private val largeImageSupported =
|
||||
ctx.carAppApiLevel >= 4 // since API 4, Row.setImage is supported
|
||||
|
||||
@@ -71,9 +72,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 +98,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 +243,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 +260,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 +292,87 @@ 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
|
||||
chargepointsText.apply {
|
||||
if (i > 0) append(" · ")
|
||||
append("${cp.count}× ")
|
||||
val plugIcon = iconForPlugType(cp.type)
|
||||
if (plugIcon != 0) {
|
||||
append(
|
||||
nameForPlugType(carContext.stringProvider(), cp.type),
|
||||
CarIconSpan.create(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
plugIcon
|
||||
)
|
||||
).setTint(
|
||||
CarColor.createCustom(Color.WHITE, Color.BLACK)
|
||||
).build()
|
||||
),
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
} ${cp.formatPower()}"
|
||||
)
|
||||
} else {
|
||||
append(nameForPlugType(carContext.stringProvider(), cp.type))
|
||||
}
|
||||
append(" ")
|
||||
append(cp.formatPower())
|
||||
}
|
||||
availability?.status?.get(cp)?.let { status ->
|
||||
chargepointsText.append(
|
||||
" (${availabilityText(status)}/${cp.count})",
|
||||
@@ -356,24 +412,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 +438,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,22 @@ 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 val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
private val supportsRefresh = ctx.isAppDrivenRefreshSupported
|
||||
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 +50,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 +79,6 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
R.drawable.ic_edit
|
||||
)
|
||||
).build()
|
||||
|
||||
)
|
||||
setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
lifecycleScope.launch {
|
||||
@@ -94,47 +93,148 @@ 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
|
||||
if (!supportsRefresh) {
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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
|
||||
if (!supportsRefresh) {
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
)
|
||||
}.build())
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
@@ -149,12 +249,16 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||
private val vm = FilterViewModel(carContext.applicationContext as Application)
|
||||
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
private val supportsRefresh = ctx.isAppDrivenRefreshSupported
|
||||
|
||||
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 +269,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 +332,65 @@ 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
|
||||
if (!supportsRefresh) {
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
}
|
||||
|
||||
paginatedFilters[page].forEach {
|
||||
val filter = it.filter
|
||||
val value = it.value
|
||||
addItem(Row.Builder().apply {
|
||||
@@ -294,6 +444,37 @@ 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
|
||||
if (!supportsRefresh) {
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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
|
||||
|
||||
@@ -59,34 +71,34 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
|
||||
private var location: Location? = null
|
||||
private var lastDistanceUpdateTime: Instant? = null
|
||||
private var lastChargersUpdateTime: 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 val chargersUpdateThresholdDistance = 500 // meters
|
||||
private val chargersUpdateThresholdTime = Duration.ofSeconds(30)
|
||||
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)
|
||||
} else 6
|
||||
private val maxRows =
|
||||
min(ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST), 25)
|
||||
private val supportsRefresh = ctx.isAppDrivenRefreshSupported
|
||||
|
||||
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,31 +167,89 @@ 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
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setOnClickListener {
|
||||
screenManager.push(SettingsScreen(carContext))
|
||||
screenManager.push(SettingsScreen(carContext, session))
|
||||
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 {
|
||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
||||
prefs.placeSearchResultAndroidAutoName = null
|
||||
prefs.placeSearchResultAndroidAuto = null
|
||||
if (!supportsRefresh) {
|
||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
||||
chargers = null
|
||||
loadChargers()
|
||||
}
|
||||
} else {
|
||||
chargers = null
|
||||
loadChargers()
|
||||
}
|
||||
} else {
|
||||
screenManager.pushForResult(
|
||||
PlaceSearchScreen(
|
||||
carContext,
|
||||
session
|
||||
)
|
||||
) {
|
||||
chargers = null
|
||||
loadChargers()
|
||||
}
|
||||
session.mapScreen = null
|
||||
}
|
||||
}
|
||||
}.build())
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setIcon(
|
||||
@@ -189,14 +263,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()
|
||||
}
|
||||
|
||||
@@ -259,7 +335,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
availabilities[charger.id]?.second?.let { av ->
|
||||
val status = av.status.values.flatten()
|
||||
val available = availabilityText(status)
|
||||
val total = charger.chargepoints.sumBy { it.count }
|
||||
val total = charger.chargepoints.sumOf { it.count }
|
||||
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append(
|
||||
@@ -289,9 +365,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
|
||||
if (previousLocation == null) {
|
||||
loadChargers()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -303,21 +380,43 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
|
||||
// update displayed distances
|
||||
invalidate()
|
||||
}
|
||||
|
||||
// if chargers are searched around current location, consider app-driven refresh
|
||||
val searchLocation =
|
||||
if (prefs.placeSearchResultAndroidAuto == null) searchLocation else null
|
||||
val distance = searchLocation?.let {
|
||||
distanceBetween(
|
||||
it.latitude, it.longitude, location.latitude, location.longitude
|
||||
)
|
||||
} ?: 0.0
|
||||
if (supportsRefresh && (lastChargersUpdateTime == null ||
|
||||
Duration.between(
|
||||
lastChargersUpdateTime,
|
||||
now
|
||||
) > chargersUpdateThresholdTime) && (distance > chargersUpdateThresholdDistance)
|
||||
) {
|
||||
onContentRefreshRequested()
|
||||
}
|
||||
}
|
||||
|
||||
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 +425,112 @@ 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
|
||||
// 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
|
||||
this@MapScreen.chargers = null
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
if (prefs.placeSearchResultAndroidAutoName == null) {
|
||||
chargers = headingFilter(
|
||||
chargers,
|
||||
searchLocation
|
||||
)
|
||||
chargers =
|
||||
response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
}
|
||||
if (chargers == null || chargers.size >= maxRows) {
|
||||
break
|
||||
}
|
||||
}
|
||||
this@MapScreen.chargers = chargers
|
||||
}
|
||||
|
||||
updateCoroutine = null
|
||||
lastChargersUpdateTime = Instant.now()
|
||||
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>? {
|
||||
// use compass heading if available, otherwise fall back to GPS
|
||||
val location = location
|
||||
val heading = heading?.orientations?.value?.get(0)
|
||||
?: if (location?.hasBearing() == true) location.bearing else null
|
||||
return heading?.let {
|
||||
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 +541,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 +552,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class PermissionScreen(
|
||||
}
|
||||
|
||||
private fun requestPermissions() {
|
||||
carContext.requestPermissions(permissions) { granted, rejected ->
|
||||
carContext.requestPermissions(permissions) { granted, _ ->
|
||||
if (granted.containsAll(permissions)) {
|
||||
screenManager.pop()
|
||||
} else {
|
||||
|
||||
@@ -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())
|
||||
@@ -148,6 +148,7 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
|
||||
}
|
||||
|
||||
private suspend fun loadNewList(query: String) {
|
||||
val location = location?.let { LatLng.fromLocation(it) }
|
||||
for (provider in providers) {
|
||||
try {
|
||||
recentResults.clear()
|
||||
@@ -161,7 +162,7 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
|
||||
}
|
||||
recentResults.addAll(recentPlaces)
|
||||
resultList =
|
||||
recentPlaces.map { it.asAutocompletePlace(LatLng.fromLocation(location)) }
|
||||
recentPlaces.map { it.asAutocompletePlace(location) }
|
||||
invalidate()
|
||||
|
||||
// if we already have enough results or the query is short, stop here
|
||||
@@ -170,7 +171,7 @@ class PlaceSearchScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx
|
||||
// then search online
|
||||
val recentIds = recentPlaces.map { it.id }
|
||||
resultList = withContext(Dispatchers.IO) {
|
||||
(resultList!! + provider.autocomplete(query, LatLng.fromLocation(location))
|
||||
(resultList!! + provider.autocomplete(query, location)
|
||||
.filter { !recentIds.contains(it.id) }).take(maxItems)
|
||||
}
|
||||
invalidate()
|
||||
|
||||
@@ -15,9 +15,7 @@ abstract class MultiSelectSearchScreen<T>(ctx: CarContext) : Screen(ctx),
|
||||
protected var fullList: List<T>? = null
|
||||
private var currentList: List<T> = emptyList()
|
||||
private var query: String = ""
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
protected abstract val isMultiSelect: Boolean
|
||||
protected abstract val shouldShowSelectAll: Boolean
|
||||
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.NameNotFoundException
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorManager
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.annotations.ExperimentalCarApi
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
|
||||
@@ -18,7 +24,10 @@ import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class SettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
@ExperimentalCarApi
|
||||
class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.auto_settings))
|
||||
@@ -69,11 +78,44 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(VehicleDataScreen(carContext))
|
||||
screenManager.push(VehicleDataScreen(carContext, session))
|
||||
}
|
||||
.build()
|
||||
)
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_chargers_ahead))
|
||||
.setToggle(Toggle.Builder {
|
||||
prefs.showChargersAheadAndroidAuto = it
|
||||
}.setChecked(prefs.showChargersAheadAndroidAuto).build())
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_navigation
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.about))
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_about
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(AboutScreen(carContext))
|
||||
}
|
||||
.build()
|
||||
)
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
@@ -82,12 +124,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 +147,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 +163,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,41 +188,90 @@ 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) {
|
||||
val prefs = PreferenceDataSource(carContext)
|
||||
private val maxRows = if (ctx.carAppApiLevel >= 2) {
|
||||
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
} else 6
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
@@ -507,4 +586,165 @@ class SelectChargingRangeScreen(ctx: CarContext) : Screen(ctx) {
|
||||
)
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
class AboutScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
var developerOptionsCounter = 0
|
||||
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.about))
|
||||
setHeaderAction(Action.BACK)
|
||||
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.version))
|
||||
.addText(BuildConfig.VERSION_NAME)
|
||||
.addText(
|
||||
carContext.getString(R.string.copyright) + " " + carContext.getString(
|
||||
R.string.copyright_summary
|
||||
)
|
||||
)
|
||||
.setBrowsable(prefs.developerModeEnabled)
|
||||
.setOnClickListener {
|
||||
if (!prefs.developerModeEnabled) {
|
||||
developerOptionsCounter += 1
|
||||
if (developerOptionsCounter >= 7) {
|
||||
prefs.developerModeEnabled = true
|
||||
invalidate()
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
carContext.getString(R.string.developer_mode_enabled),
|
||||
CarToast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
} else {
|
||||
screenManager.pushForResult(DeveloperOptionsScreen(carContext)) {
|
||||
developerOptionsCounter = 0
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
)
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.faq))
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
openUrl(carContext, carContext.getString(R.string.faq_link))
|
||||
}).build()
|
||||
)
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.donate))
|
||||
.addText(carContext.getString(R.string.donate_desc))
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
if (BuildConfig.FLAVOR_automotive == "automotive") {
|
||||
// we can't open the donation page on the phone in this case
|
||||
openUrl(carContext, carContext.getString(R.string.paypal_link))
|
||||
} else {
|
||||
val intent = Intent(carContext, MapsActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(EXTRA_DONATE, true)
|
||||
carContext.startActivity(intent)
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}).build()
|
||||
)
|
||||
}.build(), carContext.getString(R.string.about)))
|
||||
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
|
||||
addItem(Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.twitter))
|
||||
.addText(carContext.getString(R.string.twitter_handle))
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
openUrl(carContext, carContext.getString(R.string.twitter_url))
|
||||
}).build()
|
||||
)
|
||||
if (maxRows > 6) {
|
||||
addItem(Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.goingelectric_forum))
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
openUrl(
|
||||
carContext,
|
||||
carContext.getString(R.string.goingelectric_forum_url)
|
||||
)
|
||||
}).build()
|
||||
)
|
||||
}
|
||||
}.build(), carContext.getString(R.string.contact)))
|
||||
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
|
||||
addItem(Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.github_link_title))
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
openUrl(carContext, carContext.getString(R.string.github_link))
|
||||
}).build()
|
||||
)
|
||||
addItem(Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.privacy))
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
openUrl(carContext, carContext.getString(R.string.privacy_link))
|
||||
}).build()
|
||||
)
|
||||
}.build(), carContext.getString(R.string.other)))
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
class DeveloperOptionsScreen(ctx: CarContext) : Screen(ctx) {
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
return ListTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.developer_options))
|
||||
setHeaderAction(Action.BACK)
|
||||
setSingleList(ItemList.Builder().apply {
|
||||
addItem(
|
||||
Row.Builder().apply {
|
||||
setTitle("Car app API Level: ${carContext.carAppApiLevel}")
|
||||
val hostPackage = carContext.hostInfo?.packageName
|
||||
val hostVersion = hostPackage?.let {
|
||||
try {
|
||||
carContext.packageManager.getPackageInfoCompat(it).versionName
|
||||
} catch (e: NameNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
addText("$hostPackage $hostVersion")
|
||||
if (BuildConfig.FLAVOR_automotive == "automotive") {
|
||||
addText(
|
||||
"Sensor list: ${
|
||||
(carContext.getSystemService(Context.SENSOR_SERVICE) as SensorManager).getSensorList(
|
||||
Sensor.TYPE_ALL
|
||||
).map { it.type }.joinToString(",")
|
||||
}"
|
||||
)
|
||||
}
|
||||
}.build()
|
||||
)
|
||||
addItem(Row.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.disable_developer_mode))
|
||||
setOnClickListener {
|
||||
prefs.developerModeEnabled = false
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
carContext.getString(R.string.developer_mode_disabled),
|
||||
CarToast.LENGTH_SHORT
|
||||
).show()
|
||||
screenManager.pop()
|
||||
}
|
||||
}.build())
|
||||
}.build())
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,25 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
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.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.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.getPackageInfoCompat
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -33,13 +42,32 @@ fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
|
||||
val CarContext.constraintManager
|
||||
get() = getCarService(CarContext.CONSTRAINT_SERVICE) as ConstraintManager
|
||||
|
||||
fun CarContext.getContentLimit(id: Int) = if (carAppApiLevel >= 2) {
|
||||
constraintManager.getContentLimit(id)
|
||||
} else {
|
||||
when (id) {
|
||||
ConstraintManager.CONTENT_LIMIT_TYPE_GRID -> 6
|
||||
ConstraintManager.CONTENT_LIMIT_TYPE_LIST -> 6
|
||||
ConstraintManager.CONTENT_LIMIT_TYPE_PANE -> 4
|
||||
ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST -> 6
|
||||
ConstraintManager.CONTENT_LIMIT_TYPE_ROUTE_LIST -> 3
|
||||
else -> throw IllegalArgumentException("unknown limit ID")
|
||||
}
|
||||
}
|
||||
|
||||
val CarContext.isAppDrivenRefreshSupported
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
get() = if (carAppApiLevel >= 6) constraintManager.isAppDrivenRefreshEnabled else false
|
||||
|
||||
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,8 +162,42 @@ 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)
|
||||
val info = ctx.packageManager.getPackageInfoCompat("com.google.android.projection.gearhead", 0)
|
||||
return info.versionName.split(".")
|
||||
}
|
||||
|
||||
@@ -152,4 +214,51 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun openUrl(carContext: CarContext, url: String) {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(
|
||||
ContextCompat.getColor(
|
||||
carContext,
|
||||
R.color.colorPrimary
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build().intent
|
||||
intent.data = Uri.parse(url)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
carContext.startActivity(intent)
|
||||
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,
|
||||
R.string.no_browser_app_found,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.car.app.CarContext
|
||||
@@ -16,10 +17,13 @@ import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.ui.CompassNeedle
|
||||
import net.vonforst.evmap.ui.Gauge
|
||||
import net.vonforst.evmap.utils.formatDecimal
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver {
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class VehicleDataScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx),
|
||||
LocationAwareScreen, DefaultLifecycleObserver {
|
||||
private val carInfo =
|
||||
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
|
||||
private val carSensors = carContext.patchedCarSensors
|
||||
@@ -27,6 +31,7 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
|
||||
private var energyLevel: EnergyLevel? = null
|
||||
private var speed: Speed? = null
|
||||
private var heading: Compass? = null
|
||||
private var location: Location? = null
|
||||
private var gauge = Gauge((ctx.resources.displayMetrics.density * 128).roundToInt(), ctx)
|
||||
private var compass =
|
||||
CompassNeedle((ctx.resources.displayMetrics.density * 128).roundToInt(), ctx)
|
||||
@@ -70,7 +75,11 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
|
||||
val energyLevel = energyLevel
|
||||
val model = model
|
||||
val speed = speed
|
||||
val heading = heading
|
||||
val location = location
|
||||
|
||||
val compassHeading = heading?.orientations?.value?.get(0)
|
||||
val gpsHeading = if (location?.hasBearing() == true) location.bearing else null
|
||||
val heading = compassHeading ?: gpsHeading
|
||||
|
||||
return GridTemplate.Builder().apply {
|
||||
setTitle(
|
||||
@@ -192,17 +201,30 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
|
||||
if (heading == null) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
val heading = heading.orientations.value
|
||||
if (heading != null) {
|
||||
setText(
|
||||
"${heading[0].roundToInt()}°"
|
||||
val headingSource =
|
||||
if (compassHeading != null) carContext.getString(R.string.compass) else carContext.getString(
|
||||
R.string.gps
|
||||
)
|
||||
|
||||
} else {
|
||||
setText(carContext.getString(R.string.auto_no_data))
|
||||
}
|
||||
setText("${heading.roundToInt()}° ($headingSource)")
|
||||
setImage(
|
||||
compass.draw(heading?.get(0)).asCarIcon()
|
||||
compass.draw(heading).asCarIcon()
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
addItem(GridItem.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.coordinates))
|
||||
if (location == null) {
|
||||
setLoading(true)
|
||||
} else {
|
||||
val dms = location.formatDecimal(4)
|
||||
setText(dms)
|
||||
setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_location
|
||||
)
|
||||
).setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
}
|
||||
}.build())
|
||||
@@ -229,6 +251,7 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
setupListeners()
|
||||
session.mapScreen = this
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
@@ -253,6 +276,7 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
removeListeners()
|
||||
session.mapScreen = null
|
||||
}
|
||||
|
||||
private fun removeListeners() {
|
||||
@@ -269,4 +293,8 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
|
||||
it
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
this.location = location
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import android.text.style.StyleSpan
|
||||
import com.car2go.maps.google.adapter.AnyMapAdapter
|
||||
import com.car2go.maps.util.SphericalUtil
|
||||
import com.google.android.gms.common.api.ApiException
|
||||
import com.google.android.gms.common.api.CommonStatusCodes
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import com.google.android.gms.maps.model.LatLngBounds
|
||||
import com.google.android.gms.tasks.Tasks.await
|
||||
@@ -19,6 +20,7 @@ import com.google.android.libraries.places.api.net.FindAutocompletePredictionsRe
|
||||
import com.google.android.libraries.places.api.net.PlacesStatusCodes
|
||||
import kotlinx.coroutines.tasks.await
|
||||
import net.vonforst.evmap.R
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@@ -58,6 +60,13 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
|
||||
if (cause is ApiException) {
|
||||
if (cause.statusCode == PlacesStatusCodes.OVER_QUERY_LIMIT) {
|
||||
throw ApiUnavailableException()
|
||||
} else if (cause.statusCode in listOf(
|
||||
CommonStatusCodes.NETWORK_ERROR,
|
||||
CommonStatusCodes.TIMEOUT, CommonStatusCodes.RECONNECTION_TIMED_OUT,
|
||||
CommonStatusCodes.RECONNECTION_TIMED_OUT_DURING_UPDATE
|
||||
)
|
||||
) {
|
||||
throw IOException(cause)
|
||||
}
|
||||
}
|
||||
throw e
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.android.billingclient.api.*
|
||||
import com.android.billingclient.api.BillingClient.ProductType
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
|
||||
@@ -14,6 +15,12 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
.setListener(this)
|
||||
.enablePendingPurchases()
|
||||
.build()
|
||||
|
||||
val products: MutableLiveData<Resource<List<DonationItem>>> by lazy {
|
||||
MutableLiveData<Resource<List<DonationItem>>>().apply {
|
||||
value = Resource.loading(null)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
billingClient.startConnection(object : BillingClientStateListener {
|
||||
@@ -24,10 +31,15 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
loadProducts()
|
||||
|
||||
// consume pending purchases
|
||||
val purchases = billingClient.queryPurchases(BillingClient.SkuType.INAPP)
|
||||
purchases.purchasesList?.forEach {
|
||||
if (!it.isAcknowledged) {
|
||||
consumePurchase(it.purchaseToken, false)
|
||||
billingClient.queryPurchasesAsync(
|
||||
QueryPurchasesParams.newBuilder()
|
||||
.setProductType(ProductType.INAPP)
|
||||
.build()
|
||||
) { _, purchasesList ->
|
||||
purchasesList.forEach {
|
||||
if (!it.isAcknowledged) {
|
||||
consumePurchase(it.purchaseToken, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,26 +48,26 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
}
|
||||
|
||||
private fun loadProducts() {
|
||||
val params = SkuDetailsParams.newBuilder()
|
||||
.setType(BillingClient.SkuType.INAPP)
|
||||
.setSkusList(
|
||||
listOf(
|
||||
"donate_1_eur", "donate_2_eur", "donate_5_eur", "donate_10_eur"
|
||||
) +
|
||||
if (BuildConfig.DEBUG) {
|
||||
listOf(
|
||||
"android.test.purchased", "android.test.canceled",
|
||||
"android.test.item_unavailable"
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val productIds = listOf(
|
||||
"donate_1_eur", "donate_2_eur", "donate_5_eur", "donate_10_eur"
|
||||
) + if (BuildConfig.DEBUG) {
|
||||
listOf(
|
||||
"android.test.purchased", "android.test.canceled",
|
||||
"android.test.item_unavailable"
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val params = QueryProductDetailsParams.newBuilder()
|
||||
.setProductList(productIds.map {
|
||||
QueryProductDetailsParams.Product.newBuilder().setProductType(ProductType.INAPP)
|
||||
.setProductId(it).build()
|
||||
})
|
||||
.build()
|
||||
billingClient.querySkuDetailsAsync(params) { result, details ->
|
||||
if (result.responseCode == BillingClient.BillingResponseCode.OK && details != null) {
|
||||
billingClient.queryProductDetailsAsync(params) { result, details ->
|
||||
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
products.postValue(Resource.success(details
|
||||
.sortedBy { it.priceAmountMicros }
|
||||
.sortedBy { it.oneTimePurchaseOfferDetails!!.priceAmountMicros }
|
||||
.map { DonationItem(it) }
|
||||
))
|
||||
} else {
|
||||
@@ -64,12 +76,6 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
}
|
||||
}
|
||||
|
||||
val products: MutableLiveData<Resource<List<DonationItem>>> by lazy {
|
||||
MutableLiveData<Resource<List<DonationItem>>>().apply {
|
||||
value = Resource.loading(null)
|
||||
}
|
||||
}
|
||||
|
||||
val purchaseSuccessful = SingleLiveEvent<Nothing>()
|
||||
val purchaseFailed = SingleLiveEvent<Nothing>()
|
||||
|
||||
@@ -97,7 +103,13 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
|
||||
fun startPurchase(it: DonationItem, activity: Activity) {
|
||||
val flowParams = BillingFlowParams.newBuilder()
|
||||
.setSkuDetails(it.sku)
|
||||
.setProductDetailsParamsList(
|
||||
listOf(
|
||||
BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||
.setProductDetails(it.product)
|
||||
.build()
|
||||
)
|
||||
)
|
||||
.build()
|
||||
val response = billingClient.launchBillingFlow(activity, flowParams)
|
||||
if (response.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||
@@ -110,4 +122,4 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
}
|
||||
}
|
||||
|
||||
data class DonationItem(val sku: SkuDetails) : Equatable
|
||||
data class DonationItem(val product: ProductDetails) : Equatable
|
||||
@@ -28,7 +28,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{item.sku.title}"
|
||||
android:text="@{item.product.title}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/textView21"
|
||||
@@ -41,7 +41,7 @@
|
||||
android:id="@+id/textView21"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.sku.price}"
|
||||
android:text="@{item.product.oneTimePurchaseOfferDetails.formattedPrice}"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
||||
@@ -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,8 @@
|
||||
<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>
|
||||
<string name="auto_multipage_goto">Side %d</string>
|
||||
<string name="auto_multipage">(%d/%d)</string>
|
||||
</resources>
|
||||
41
app/src/google/res/values-nl/strings.xml
Normal file
41
app/src/google/res/values-nl/strings.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="auto_chargeprice_vehicle_ambiguous">Meerdere voertuigen geselecteerd in de app komen overeen met dit voertuig (%1$s %2$s).</string>
|
||||
<string name="donations_info" formatted="false">Vind je EVMap nuttig\? Je kan de ontwikkeling steunen via een donatie aan de ontwikkelaar.
|
||||
\n
|
||||
\nGoogle houdt 15% in van elke donatie.</string>
|
||||
<string name="auto_location_service">EVMap draait op Android Auto en gebruikt jouw locatie.</string>
|
||||
<string name="auto_no_chargers_found">Geen laadpunten gevonden in de omgeving</string>
|
||||
<string name="auto_no_favorites_found">Geen favorieten gevonden</string>
|
||||
<string name="open_in_app">Open in de app</string>
|
||||
<string name="opened_on_phone">Geopend op de telefoon</string>
|
||||
<string name="auto_location_permission_needed">Om EVMap op Android Auto te gebruiken, moet je toegang geven tot je locatie.</string>
|
||||
<string name="auto_vehicle_data_permission_needed">Voor deze functie heeft EVMap toegang nodig tot de gegevens van je voertuig.</string>
|
||||
<string name="grant_on_phone">Geef toestemming op telefoon</string>
|
||||
<string name="auto_chargers_closeby">Oplaadpunten in de buurt</string>
|
||||
<string name="auto_favorites">Favorieten</string>
|
||||
<string name="auto_chargers_near_location">Nabij %s</string>
|
||||
<string name="auto_fault_report_date">⚠️ Foutrapport (%s)</string>
|
||||
<string name="auto_no_refresh_possible">Verdere updates zijn niet mogelijk. Ga terug en herbegin.</string>
|
||||
<string name="auto_prices">Prijzen</string>
|
||||
<string name="auto_vehicle_data">Voertuiggegevens</string>
|
||||
<string name="auto_charging_level">Laadniveau (SoC)</string>
|
||||
<string name="auto_no_data">Niet beschikbaar</string>
|
||||
<string name="auto_range">Reikwijdte</string>
|
||||
<string name="auto_speed">Snelheid</string>
|
||||
<string name="auto_heading">Richting</string>
|
||||
<string name="auto_settings">Instellingen</string>
|
||||
<string name="welcome_android_auto">Android Auto support</string>
|
||||
<string name="welcome_android_auto_detail">Je kan EVMap ook gebruiken in Android Auto op ondersteunde voertuigen. Selecteer gewoon de EVMap app in het Android Auto menu.</string>
|
||||
<string name="sounds_cool">klinkt cool</string>
|
||||
<string name="auto_chargeprice_vehicle_unavailable">EVMap kon je voertuigtype niet bepalen.</string>
|
||||
<string name="auto_chargers_ahead">Alleen laadpunten in rijrichting</string>
|
||||
<string name="settings_android_auto_chargeprice_range">Laadbereik voor prijsvergelijking</string>
|
||||
<string name="data_sources_hint">In de instellingen kan je ook wisselen tussen Google Maps en OpenStreetMap (Mapbox) voor de kaartgegevens.</string>
|
||||
<string name="selecting_all">alle items geselecteerd</string>
|
||||
<string name="selecting_none">alle items gedeselecteerd</string>
|
||||
<string name="loading">Laden…</string>
|
||||
<string name="auto_multipage_goto">Pagina %d</string>
|
||||
<string name="auto_multipage">(%d/%d)</string>
|
||||
<string name="auto_chargeprice_vehicle_unknown">Geen enkel voertuig geselecteerd in de app komt overeen met dit voertuig (%1$s %2$s).</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" />
|
||||
|
||||
5
app/src/googleAutomotive/res/values-nl/strings.xml
Normal file
5
app/src/googleAutomotive/res/values-nl/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="grant_on_phone">Toestaan</string>
|
||||
<string name="auto_location_permission_needed">Om EVmap te gebruiken in je wagen, moet je toegang geven tot je locatie.</string>
|
||||
</resources>
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ const val EXTRA_CHARGER_ID = "chargerId"
|
||||
const val EXTRA_LAT = "lat"
|
||||
const val EXTRA_LON = "lon"
|
||||
const val EXTRA_FAVORITES = "favorites"
|
||||
const val EXTRA_DONATE = "donate"
|
||||
|
||||
class MapsActivity : AppCompatActivity(),
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
||||
@@ -53,14 +54,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()
|
||||
@@ -83,7 +76,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
val navView = findViewById<NavigationView>(R.id.nav_view)
|
||||
navView.setupWithNavController(navController)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(navView) { v, insets ->
|
||||
ViewCompat.setOnApplyWindowInsetsListener(navView) { _, insets ->
|
||||
val header = navView.getHeaderView(0)
|
||||
header.setPadding(0, insets.getInsets(WindowInsetsCompat.Type.statusBars()).top, 0, 0)
|
||||
insets
|
||||
@@ -141,6 +134,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)
|
||||
@@ -165,6 +189,11 @@ class MapsActivity : AppCompatActivity(),
|
||||
.setGraph(navGraph)
|
||||
.setDestination(R.id.favs)
|
||||
.createPendingIntent()
|
||||
} else if (intent.hasExtra(EXTRA_DONATE)) {
|
||||
deepLink = navController.createDeepLink()
|
||||
.setGraph(navGraph)
|
||||
.setDestination(R.id.donate)
|
||||
.createPendingIntent()
|
||||
}
|
||||
|
||||
deepLink?.send()
|
||||
@@ -206,6 +235,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
}
|
||||
|
||||
fun openUrl(url: String) {
|
||||
val pkg = CustomTabsClient.getPackageName(this, null)
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
@@ -213,6 +243,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) {
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
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? {
|
||||
@@ -77,7 +75,7 @@ fun max(a: Int?, b: Int?): Int? {
|
||||
* otherwise the non-null value or null
|
||||
*/
|
||||
return if (a != null && b != null) {
|
||||
max(a, b)
|
||||
kotlin.math.max(a, b)
|
||||
} else {
|
||||
a ?: b
|
||||
}
|
||||
@@ -85,25 +83,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
|
||||
|
||||
@@ -112,4 +91,11 @@ const val meterPerFt = 0.3048
|
||||
|
||||
fun shouldUseImperialUnits(): Boolean {
|
||||
return Locale.getDefault().country in listOf("US", "GB", "MM", "LR")
|
||||
}
|
||||
}
|
||||
|
||||
fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): PackageInfo =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION") getPackageInfo(packageName, flags)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
@@ -161,11 +160,12 @@ 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
|
||||
}
|
||||
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
|
||||
root.setOnCheckedChangeListener { _, checked: Boolean ->
|
||||
if (checked) {
|
||||
checkedItem = holder.bindingAdapterPosition.takeIf { it != -1 }
|
||||
root.post {
|
||||
@@ -204,7 +204,7 @@ class CheckableChargepriceCarAdapter : DataBindingAdapter<ChargepriceCar>() {
|
||||
root.setOnClickListener {
|
||||
root.isChecked = true
|
||||
}
|
||||
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
|
||||
root.setOnCheckedChangeListener { _, checked: Boolean ->
|
||||
if (checked && item != checkedItem) {
|
||||
checkedItem = item
|
||||
root.post {
|
||||
|
||||
@@ -10,6 +10,8 @@ import net.vonforst.evmap.model.ChargeCardId
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.OpeningHoursDays
|
||||
import net.vonforst.evmap.plus
|
||||
import net.vonforst.evmap.utils.formatDMS
|
||||
import net.vonforst.evmap.utils.formatDecimal
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
|
||||
@@ -25,7 +25,7 @@ class FilterProfilesAdapter(
|
||||
super.bind(holder, item)
|
||||
|
||||
val binding = holder.binding as ItemFilterProfileBinding
|
||||
binding.handle.setOnTouchListener { v, event ->
|
||||
binding.handle.setOnTouchListener { _, event ->
|
||||
if (event?.action == MotionEvent.ACTION_DOWN) {
|
||||
dragHelper.startDrag(holder)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -71,19 +71,19 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
connectors: Map<Long, Pair<Double, String>>,
|
||||
chargepoints: List<Chargepoint>
|
||||
): Map<Chargepoint, Set<Long>> {
|
||||
var chargepoints = chargepoints
|
||||
var cpts = chargepoints
|
||||
|
||||
// iterate over each connector type
|
||||
val types = connectors.map { it.value.second }.distinct().toSet()
|
||||
val equivalentTypes = types.map { equivalentPlugTypes(it).plus(it) }.cartesianProduct()
|
||||
var geTypes = chargepoints.map { it.type }.distinct().toSet()
|
||||
var geTypes = cpts.map { it.type }.distinct().toSet()
|
||||
if (!equivalentTypes.any { it == geTypes } && geTypes.size > 1 && geTypes.contains(
|
||||
Chargepoint.SCHUKO
|
||||
)) {
|
||||
// If charger has household plugs and other plugs, try removing the household plugs
|
||||
// (common e.g. in Hamburg -> 2x Type 2 + 2x Schuko, but NM only lists Type 2)
|
||||
geTypes = geTypes.filter { it != Chargepoint.SCHUKO }.toSet()
|
||||
chargepoints = chargepoints.filter { it.type != Chargepoint.SCHUKO }
|
||||
cpts = cpts.filter { it.type != Chargepoint.SCHUKO }
|
||||
}
|
||||
if (!equivalentTypes.any { it == geTypes }) throw AvailabilityDetectorException("chargepoints do not match")
|
||||
return types.flatMap { type ->
|
||||
@@ -93,14 +93,14 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
val powers = connsOfType.map { it.value.first }.distinct().sorted()
|
||||
// find corresponding powers in GE data
|
||||
val gePowers =
|
||||
chargepoints.filter { equivalentPlugTypes(it.type).any { it == type } }
|
||||
cpts.filter { equivalentPlugTypes(it.type).any { it == type } }
|
||||
.mapNotNull { it.power }.distinct().sorted()
|
||||
|
||||
// if the distinct number of powers is the same, try to match.
|
||||
if (powers.size == gePowers.size) {
|
||||
gePowers.zip(powers).map { (gePower, power) ->
|
||||
val chargepoint =
|
||||
chargepoints.find { equivalentPlugTypes(it.type).any { it == type } && it.power == gePower }!!
|
||||
cpts.find { equivalentPlugTypes(it.type).any { it == type } && it.power == gePower }!!
|
||||
val ids = connsOfType.filter { it.value.first == power }.keys
|
||||
if (chargepoint.count != ids.size) {
|
||||
throw AvailabilityDetectorException("chargepoints do not match")
|
||||
@@ -108,7 +108,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
chargepoint to ids
|
||||
}
|
||||
} else if (powers.size == 1 && gePowers.size == 2
|
||||
&& chargepoints.sumOf { it.count } == connsOfType.size
|
||||
&& cpts.sumOf { it.count } == connsOfType.size
|
||||
) {
|
||||
// special case: dual charger(s) with load balancing
|
||||
// GoingElectric shows 2 different powers, NewMotion just one
|
||||
@@ -116,7 +116,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
var i = 0
|
||||
gePowers.map { gePower ->
|
||||
val chargepoint =
|
||||
chargepoints.find { it.type in equivalentPlugTypes(type) && it.power == gePower }!!
|
||||
cpts.find { it.type in equivalentPlugTypes(type) && it.power == gePower }!!
|
||||
val ids = allIds.subList(i, i + chargepoint.count).toSet()
|
||||
i += chargepoint.count
|
||||
chargepoint to ids
|
||||
@@ -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 {
|
||||
|
||||
@@ -51,7 +51,7 @@ interface ChargecloudApi {
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun create(client: OkHttpClient, baseUrl: String? = null): ChargecloudApi {
|
||||
fun create(client: OkHttpClient, baseUrl: String): ChargecloudApi {
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.addConverterFactory(MoshiConverterFactory.create())
|
||||
|
||||
@@ -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,16 +130,17 @@ 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)) {
|
||||
val type = when (connector.connectorType.lowercase(Locale.ROOT)) {
|
||||
"type3" -> Chargepoint.TYPE_3
|
||||
"type2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||
"type1" -> Chargepoint.TYPE_1
|
||||
@@ -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/ev-map/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/ev-map/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
|
||||
}
|
||||
@@ -71,10 +71,10 @@ internal class JsonObjectOrFalseAdapter<T> private constructor(
|
||||
private val clazz: Class<*>
|
||||
) : JsonAdapter<T>() {
|
||||
|
||||
class Factory() : JsonAdapter.Factory {
|
||||
class Factory : JsonAdapter.Factory {
|
||||
override fun create(
|
||||
type: Type,
|
||||
annotations: Set<Annotation>?,
|
||||
annotations: Set<Annotation>,
|
||||
moshi: Moshi
|
||||
): JsonAdapter<Any>? {
|
||||
val clazz = Types.getRawType(type)
|
||||
|
||||
@@ -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,
|
||||
@@ -398,10 +399,10 @@ class GoingElectricApiWrapper(
|
||||
referenceData: ReferenceData,
|
||||
sp: StringProvider
|
||||
): List<Filter<FilterValue>> {
|
||||
val referenceData = referenceData as GEReferenceData
|
||||
val plugs = referenceData.plugs
|
||||
val networks = referenceData.networks
|
||||
val chargeCards = referenceData.chargecards
|
||||
val refData = referenceData as GEReferenceData
|
||||
val plugs = refData.plugs
|
||||
val networks = refData.networks
|
||||
val chargeCards = refData.chargecards
|
||||
|
||||
val plugMap = plugs.map { plug ->
|
||||
plug to nameForPlugType(sp, GEChargepoint.convertTypeFromGE(plug))
|
||||
|
||||
@@ -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(",")
|
||||
@@ -119,7 +120,7 @@ class OpenChargeMapApiWrapper(
|
||||
zoom: Float,
|
||||
filters: FilterValues?,
|
||||
): Resource<List<ChargepointListItem>> {
|
||||
val referenceData = referenceData as OCMReferenceData
|
||||
val refData = referenceData as OCMReferenceData
|
||||
|
||||
val minPower = filters?.getSliderValue("min_power")?.toDouble()
|
||||
val minConnectors = filters?.getSliderValue("min_connectors")
|
||||
@@ -132,7 +133,7 @@ class OpenChargeMapApiWrapper(
|
||||
}
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
|
||||
val operatorsVal = filters?.getMultipleChoiceValue("operators")!!
|
||||
val operatorsVal = filters?.getMultipleChoiceValue("operators")
|
||||
if (operatorsVal != null && operatorsVal.values.isEmpty() && !operatorsVal.all) {
|
||||
// no operators chosen
|
||||
return Resource.success(emptyList())
|
||||
@@ -159,7 +160,7 @@ class OpenChargeMapApiWrapper(
|
||||
minPower,
|
||||
connectorsVal,
|
||||
minConnectors,
|
||||
referenceData,
|
||||
refData,
|
||||
zoom
|
||||
)
|
||||
return Resource.success(result)
|
||||
@@ -175,7 +176,7 @@ class OpenChargeMapApiWrapper(
|
||||
zoom: Float,
|
||||
filters: FilterValues?
|
||||
): Resource<List<ChargepointListItem>> {
|
||||
val referenceData = referenceData as OCMReferenceData
|
||||
val refData = referenceData as OCMReferenceData
|
||||
|
||||
val minPower = filters?.getSliderValue("min_power")?.toDouble()
|
||||
val minConnectors = filters?.getSliderValue("min_connectors")
|
||||
@@ -213,7 +214,7 @@ class OpenChargeMapApiWrapper(
|
||||
minPower,
|
||||
connectorsVal,
|
||||
minConnectors,
|
||||
referenceData,
|
||||
refData,
|
||||
zoom
|
||||
)
|
||||
return Resource.success(result)
|
||||
@@ -253,11 +254,11 @@ class OpenChargeMapApiWrapper(
|
||||
referenceData: ReferenceData,
|
||||
id: Long
|
||||
): Resource<ChargeLocation> {
|
||||
val referenceData = referenceData as OCMReferenceData
|
||||
val refData = referenceData as OCMReferenceData
|
||||
try {
|
||||
val response = api.getChargepointDetail(id)
|
||||
if (response.isSuccessful && response.body()?.size == 1) {
|
||||
return Resource.success(response.body()!![0].convert(referenceData, true))
|
||||
return Resource.success(response.body()!![0].convert(refData, true))
|
||||
} else {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
@@ -283,10 +284,10 @@ class OpenChargeMapApiWrapper(
|
||||
referenceData: ReferenceData,
|
||||
sp: StringProvider
|
||||
): List<Filter<FilterValue>> {
|
||||
val referenceData = referenceData as OCMReferenceData
|
||||
val refData = referenceData as OCMReferenceData
|
||||
|
||||
val operatorsMap = referenceData.operators.map { it.id.toString() to it.title }.toMap()
|
||||
val plugMap = referenceData.connectionTypes.map { it.id.toString() to it.title }.toMap()
|
||||
val operatorsMap = refData.operators.map { it.id.toString() to it.title }.toMap()
|
||||
val plugMap = refData.connectionTypes.map { it.id.toString() to it.title }.toMap()
|
||||
|
||||
return listOf(
|
||||
// supported by OCM API
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -44,8 +44,7 @@ class FavoritesFragment : Fragment() {
|
||||
private val vm: FavoritesViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory {
|
||||
FavoritesViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.goingelectric_key)
|
||||
requireActivity().application
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -98,6 +98,12 @@ class FilterFragment : Fragment(), MenuProvider {
|
||||
saveProfile()
|
||||
true
|
||||
}
|
||||
R.id.menu_reset -> {
|
||||
lifecycleScope.launch {
|
||||
vm.resetValues()
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@@ -114,7 +120,7 @@ class FilterFragment : Fragment(), MenuProvider {
|
||||
|
||||
dialog.setTitle(R.string.save_as_profile)
|
||||
.setMessage(R.string.save_profile_enter_name)
|
||||
.setPositiveButton(R.string.ok) { di, button ->
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
if (input.text.isBlank()) {
|
||||
saveProfile(true)
|
||||
} else {
|
||||
@@ -124,7 +130,7 @@ class FilterFragment : Fragment(), MenuProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { di, button ->
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,12 +188,12 @@ class FilterProfilesFragment : Fragment() {
|
||||
|
||||
dialog.setTitle(R.string.rename)
|
||||
.setMessage(R.string.save_profile_enter_name)
|
||||
.setPositiveButton(R.string.ok) { di, button ->
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
lifecycleScope.launch {
|
||||
vm.update(fp.copy(name = input.text.toString()))
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { di, button ->
|
||||
.setNegativeButton(R.string.cancel) { _, _ ->
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -93,6 +91,7 @@ import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.contains
|
||||
import kotlin.collections.set
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback, MenuProvider {
|
||||
@@ -132,7 +131,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) {
|
||||
@@ -195,7 +198,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.root
|
||||
) { v, insets ->
|
||||
) { _, insets ->
|
||||
ViewCompat.onApplyWindowInsets(binding.root, insets)
|
||||
|
||||
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
|
||||
@@ -238,6 +241,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 +259,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.detailView.topPart.doOnNextLayout {
|
||||
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom
|
||||
}
|
||||
bottomSheetBehavior.isCollapsible = bottomSheetCollapsible
|
||||
|
||||
setupObservers()
|
||||
setupClickListeners()
|
||||
@@ -370,12 +377,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 +414,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(),
|
||||
@@ -446,7 +466,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
)
|
||||
}
|
||||
binding.search.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus ->
|
||||
binding.search.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
binding.search.keyListener = searchKeyListener
|
||||
binding.search.text = binding.search.text // workaround to fix copy/paste
|
||||
@@ -534,7 +554,17 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
bottomSheetBehavior.addBottomSheetCallback(object :
|
||||
BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
|
||||
if (bottomSheetBehavior.state == STATE_HIDDEN) {
|
||||
map?.setPadding(0, mapTopPadding, 0, 0)
|
||||
} else {
|
||||
val height = binding.root.height - bottomSheet.top
|
||||
map?.setPadding(
|
||||
0,
|
||||
mapTopPadding,
|
||||
0,
|
||||
min(bottomSheetBehavior.peekHeight, height)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
@@ -542,9 +572,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
updateBackPressedCallback()
|
||||
|
||||
if (vm.layersMenuOpen.value!! && newState !in listOf(
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING,
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN,
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
|
||||
STATE_SETTLING,
|
||||
STATE_HIDDEN,
|
||||
STATE_COLLAPSED
|
||||
)
|
||||
) {
|
||||
closeLayersMenu()
|
||||
@@ -554,7 +584,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 +1001,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
|
||||
}
|
||||
@@ -1168,6 +1200,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
)
|
||||
popup.menuInflater.inflate(R.menu.popup_filter, popup.menu)
|
||||
MenuCompat.setGroupDividerEnabled(popup.menu, true)
|
||||
popup.setForceShowIcon(true)
|
||||
popup.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.menu_edit_filters -> {
|
||||
|
||||
@@ -71,7 +71,7 @@ class MultiSelectDialog : MaterialDialogFragment() {
|
||||
binding.btnAll.visibility = if (showAllButton) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
items = data.entries.toList()
|
||||
.sortedBy { it.value.toLowerCase(Locale.getDefault()) }
|
||||
.sortedBy { it.value.lowercase(Locale.getDefault()) }
|
||||
.sortedBy {
|
||||
when {
|
||||
selected.contains(it.key) && commonChoices?.contains(it.key) == true -> 0
|
||||
@@ -117,7 +117,7 @@ private fun search(
|
||||
): List<MultiSelectItem> {
|
||||
return items.filter { item ->
|
||||
// search for string within name
|
||||
text.toLowerCase(Locale.getDefault()) in item.name.toLowerCase(Locale.getDefault())
|
||||
text.lowercase(Locale.getDefault()) in item.name.lowercase(Locale.getDefault())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
|
||||
sealed class ChargepointListItem
|
||||
|
||||
@@ -113,7 +111,7 @@ data class ChargeLocation(
|
||||
|
||||
// check if there is more than one plug for any connector type
|
||||
val chargepointsPerConnector =
|
||||
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumBy { it.count } }
|
||||
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumOf { it.count } }
|
||||
return chargepointsPerConnector.any { it > 1 }
|
||||
}
|
||||
|
||||
@@ -129,13 +127,13 @@ data class ChargeLocation(
|
||||
return variants.map { variant ->
|
||||
val count = chargepoints
|
||||
.filter { it.type == variant.type && it.power == variant.power }
|
||||
.sumBy { it.count }
|
||||
.sumOf { it.count }
|
||||
Chargepoint(variant.type, variant.power, count)
|
||||
}
|
||||
}
|
||||
|
||||
val totalChargepoints: Int
|
||||
get() = chargepoints.sumBy { it.count }
|
||||
get() = chargepoints.sumOf { it.count }
|
||||
|
||||
fun formatChargepoints(sp: StringProvider): String {
|
||||
return chargepointsMerged.map {
|
||||
@@ -343,28 +341,7 @@ data class ChargeLocationCluster(
|
||||
) : ChargepointListItem()
|
||||
|
||||
@Parcelize
|
||||
data class Coordinate(val lat: Double, val lng: Double) : Parcelable {
|
||||
fun formatDMS(): String {
|
||||
return "${dms(lat, false)}, ${dms(lng, true)}"
|
||||
}
|
||||
|
||||
private fun dms(value: Double, lon: Boolean): String {
|
||||
val hemisphere = if (lon) {
|
||||
if (value >= 0) "E" else "W"
|
||||
} else {
|
||||
if (value >= 0) "N" else "S"
|
||||
}
|
||||
val d = abs(value)
|
||||
val degrees = floor(d).toInt()
|
||||
val minutes = floor((d - degrees) * 60).toInt()
|
||||
val seconds = ((d - degrees) * 60 - minutes) * 60
|
||||
return "%d°%02d'%02.1f\"%s".format(Locale.ENGLISH, degrees, minutes, seconds, hemisphere)
|
||||
}
|
||||
|
||||
fun formatDecimal(): String {
|
||||
return "%.6f, %.6f".format(Locale.ENGLISH, lat, lng)
|
||||
}
|
||||
}
|
||||
data class Coordinate(val lat: Double, val lng: Double) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Address(
|
||||
@@ -374,7 +351,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,28 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
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 suspend fun getBooleanFilterValuesAsync(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): List<BooleanFilterValue>
|
||||
|
||||
@Query("SELECT * FROM multiplechoicefiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract suspend fun getMultipleChoiceFilterValuesAsync(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): List<MultipleChoiceFilterValue>
|
||||
|
||||
@Query("SELECT * FROM sliderfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract suspend fun getSliderFilterValuesAsync(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): List<SliderFilterValue>
|
||||
|
||||
@Query("SELECT * FROM booleanfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract fun getBooleanFilterValues(
|
||||
profile: Long,
|
||||
@@ -54,11 +71,24 @@ 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) {
|
||||
emptyList()
|
||||
} else {
|
||||
getBooleanFilterValuesAsync(filterStatus, dataSource) +
|
||||
getMultipleChoiceFilterValuesAsync(filterStatus, dataSource) +
|
||||
getSliderFilterValuesAsync(filterStatus, dataSource)
|
||||
}
|
||||
|
||||
open fun getFilterValues(filterStatus: Long, dataSource: String): LiveData<List<FilterValue>?> =
|
||||
if (filterStatus == FILTERS_DISABLED || filterStatus == FILTERS_FAVORITES) {
|
||||
MutableLiveData(emptyList())
|
||||
} else {
|
||||
MediatorLiveData<List<FilterValue>>().apply {
|
||||
MediatorLiveData<List<FilterValue>?>().apply {
|
||||
value = null
|
||||
val sources = listOf(
|
||||
getBooleanFilterValues(filterStatus, dataSource),
|
||||
getMultipleChoiceFilterValues(filterStatus, dataSource),
|
||||
@@ -98,7 +128,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,19 @@ 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)
|
||||
|
||||
var developerModeEnabled: Boolean
|
||||
get() = sp.getBoolean("dev_mode_enabled", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("dev_mode_enabled", value).apply()
|
||||
}
|
||||
}
|
||||
@@ -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 d = data.toList()
|
||||
|
||||
if (d.size <= selectedBar) return
|
||||
canvas.apply {
|
||||
val center = graphBounds.left + selectedBar * (barWidth + barMargin) + barWidth * 0.5f
|
||||
val (t, v) = d[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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,7 @@ private fun activeTint(
|
||||
}
|
||||
|
||||
@BindingAdapter("data")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> setRecyclerViewData(recyclerView: RecyclerView, items: List<T>?) {
|
||||
if (recyclerView.adapter is ListAdapter<*, *>) {
|
||||
(recyclerView.adapter as ListAdapter<T, *>).submitList(items)
|
||||
@@ -128,6 +129,7 @@ fun <T> setRecyclerViewData(recyclerView: RecyclerView, items: List<T>?) {
|
||||
}
|
||||
|
||||
@BindingAdapter("data")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> setRecyclerViewData(recyclerView: ViewPager2, items: List<T>?) {
|
||||
if (recyclerView.adapter is ListAdapter<*, *>) {
|
||||
(recyclerView.adapter as ListAdapter<T, *>).submitList(items)
|
||||
@@ -325,10 +327,10 @@ fun distance(meters: Number?): String? {
|
||||
}
|
||||
}
|
||||
|
||||
@InverseBindingAdapter(attribute = "app:values")
|
||||
@InverseBindingAdapter(attribute = "values")
|
||||
fun getRangeSliderValue(slider: RangeSlider) = slider.values
|
||||
|
||||
@BindingAdapter("app:valuesAttrChanged")
|
||||
@BindingAdapter("valuesAttrChanged")
|
||||
fun setRangeSliderListeners(slider: RangeSlider, attrChange: InverseBindingListener) {
|
||||
slider.addOnChangeListener { _, _, _ ->
|
||||
attrChange.onChange()
|
||||
@@ -348,7 +350,7 @@ fun colorEnabled(ctx: Context, enabled: Boolean): Int {
|
||||
return color
|
||||
}
|
||||
|
||||
@BindingAdapter("app:tint")
|
||||
@BindingAdapter("tint")
|
||||
fun setImageTintList(view: ImageView, @ColorInt color: Int) {
|
||||
view.imageTintList = ColorStateList.valueOf(color)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -89,12 +89,12 @@ class RangeSliderPreference(context: Context, attrs: AttributeSet) : Preference(
|
||||
slider.valueTo = valueTo
|
||||
stepSize?.let { slider.stepSize = it }
|
||||
|
||||
slider.addOnChangeListener { slider, value, fromUser ->
|
||||
slider.addOnChangeListener { slider, _, fromUser ->
|
||||
if (fromUser && (updatesContinuously || !dragging)) {
|
||||
syncValueInternal(slider)
|
||||
}
|
||||
}
|
||||
slider.setOnTouchListener { v, event ->
|
||||
slider.setOnTouchListener { _, event ->
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> dragging = true
|
||||
MotionEvent.ACTION_UP -> dragging = false
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import android.location.Location
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import net.vonforst.evmap.model.Coordinate
|
||||
import java.util.*
|
||||
import kotlin.math.*
|
||||
|
||||
/**
|
||||
@@ -47,6 +49,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)
|
||||
@@ -92,4 +113,33 @@ fun Context.checkAnyLocationPermission() = ContextCompat.checkSelfPermission(
|
||||
fun Context.checkFineLocationPermission() = ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
fun Coordinate.formatDMS(): String {
|
||||
return "${dms(lat, false)}, ${dms(lng, true)}"
|
||||
}
|
||||
|
||||
fun Location.formatDMS(): String {
|
||||
return "${dms(latitude, false)}, ${dms(longitude, true)}"
|
||||
}
|
||||
|
||||
private fun dms(value: Double, lon: Boolean): String {
|
||||
val hemisphere = if (lon) {
|
||||
if (value >= 0) "E" else "W"
|
||||
} else {
|
||||
if (value >= 0) "N" else "S"
|
||||
}
|
||||
val d = abs(value)
|
||||
val degrees = floor(d).toInt()
|
||||
val minutes = floor((d - degrees) * 60).toInt()
|
||||
val seconds = ((d - degrees) * 60 - minutes) * 60
|
||||
return "%d°%02d'%02.1f\"%s".format(Locale.ENGLISH, degrees, minutes, seconds, hemisphere)
|
||||
}
|
||||
|
||||
fun Coordinate.formatDecimal(accuracy: Int = 6): String {
|
||||
return "%.${accuracy}f, %.${accuracy}f".format(Locale.ENGLISH, lat, lng)
|
||||
}
|
||||
|
||||
fun Location.formatDecimal(accuracy: Int = 6): String {
|
||||
return "%.${accuracy}f, %.${accuracy}f".format(Locale.ENGLISH, latitude, longitude)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import net.vonforst.evmap.model.FavoriteWithDetail
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
|
||||
class FavoritesViewModel(application: Application, geApiKey: String) :
|
||||
class FavoritesViewModel(application: Application) :
|
||||
AndroidViewModel(application) {
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
|
||||
@@ -69,7 +69,7 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
|
||||
FavoritesListItem(
|
||||
favorite,
|
||||
totalAvailable(charger.id),
|
||||
charger.chargepoints.sumBy { it.count },
|
||||
charger.chargepoints.sumOf { it.count },
|
||||
location.value.let { loc ->
|
||||
if (loc == null) null else {
|
||||
distanceBetween(
|
||||
|
||||
@@ -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() {
|
||||
@@ -93,4 +92,8 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
|
||||
prefs.filterStatus = FILTERS_DISABLED
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun resetValues() {
|
||||
db.filterValueDao().deleteFilterValuesForProfile(FILTERS_CUSTOM, prefs.dataSource)
|
||||
}
|
||||
}
|
||||
@@ -6,30 +6,37 @@ 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 com.squareup.moshi.JsonDataException
|
||||
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 +51,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 +100,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 +131,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 +151,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 +195,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 +229,131 @@ 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 }
|
||||
if (allEvseIds.isEmpty()) {
|
||||
emit(Resource.success(emptyList()))
|
||||
return@liveData
|
||||
}
|
||||
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()
|
||||
} catch (e: JsonDataException) {
|
||||
// malformed JSON response from fronyx API
|
||||
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 +391,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 +433,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 +460,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 +492,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
|
||||
@@ -16,7 +13,7 @@ import java.util.concurrent.atomic.AtomicBoolean
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
|
||||
object : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(aClass: Class<T>): T = f() as T
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = f() as T
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -107,4 +104,43 @@ fun <T> throttleLatest(
|
||||
waitingParam = param
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
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_filter_no.xml
Normal file
10
app/src/main/res/drawable/ic_filter_no.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
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="M10.83,8H21V6H8.83L10.83,8zM15.83,13H18v-2h-4.17L15.83,13zM14,16.83V18h-4v-2h3.17l-3,-3H6v-2h2.17l-3,-3H3V6h0.17L1.39,4.22l1.41,-1.41l18.38,18.38l-1.41,1.41L14,16.83z" />
|
||||
</vector>
|
||||
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>
|
||||
10
app/src/main/res/drawable/ic_manage_filter_profiles.xml
Normal file
10
app/src/main/res/drawable/ic_manage_filter_profiles.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
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="M3,10h11v2H3V10zM3,8h11V6H3V8zM3,16h7v-2H3V16zM18.01,12.87l0.71,-0.71c0.39,-0.39 1.02,-0.39 1.41,0l0.71,0.71c0.39,0.39 0.39,1.02 0,1.41l-0.71,0.71L18.01,12.87zM17.3,13.58l-5.3,5.3V21h2.12l5.3,-5.3L17.3,13.58z" />
|
||||
</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,12 +224,13 @@
|
||||
|
||||
<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" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
style="@style/Widget.Material3.FloatingActionButton.Small.Surface"
|
||||
android:id="@+id/fab_layers"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -233,11 +240,9 @@
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginTop="@dimen/layers_fab_top_padding"
|
||||
app:tint="?android:colorControlNormal"
|
||||
app:backgroundTint="?android:colorBackground"
|
||||
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"
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/menu_reset"
|
||||
android:title="@string/menu_reset"
|
||||
android:icon="@drawable/ic_filter_no"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_save_profile"
|
||||
android:title="@string/menu_save_profile"
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
<item
|
||||
android:id="@+id/menu_edit_filters"
|
||||
android:title="@string/menu_edit_filters"
|
||||
android:menuCategory="secondary" />
|
||||
android:menuCategory="secondary"
|
||||
android:icon="@drawable/ic_edit" />
|
||||
<item
|
||||
android:id="@+id/menu_manage_filter_profiles"
|
||||
android:title="@string/menu_manage_filter_profiles"
|
||||
android:menuCategory="secondary" />
|
||||
android:menuCategory="secondary"
|
||||
android:icon="@drawable/ic_manage_filter_profiles" />
|
||||
</menu>
|
||||
@@ -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,18 +37,18 @@
|
||||
<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>
|
||||
<string name="copyright">Copyright</string>
|
||||
<string name="copyright_summary">©2020–2022 Johan von Forstner</string>
|
||||
<string name="copyright_summary">©2020–2023 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>
|
||||
@@ -145,23 +143,24 @@
|
||||
<string name="category_caravan_site">Wohnmobilstellplatz</string>
|
||||
<string name="menu_apply">Filter anwenden</string>
|
||||
<string name="menu_save_profile">Als Profil speichern</string>
|
||||
<string name="menu_reset">Filter zurücksetzen</string>
|
||||
<string name="no_filters">Keine Filter</string>
|
||||
<string name="filter_custom">Verändertes Filterprofil</string>
|
||||
<string name="filter_favorites">Favoriten</string>
|
||||
<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 +171,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 +181,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 +218,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 +247,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>
|
||||
@@ -273,4 +273,26 @@
|
||||
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
|
||||
<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>
|
||||
</resources>
|
||||
<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>
|
||||
<string name="developer_mode_enabled">Entwicklermodus aktiviert</string>
|
||||
<string name="developer_options">Entwicklereinstellungen</string>
|
||||
<string name="disable_developer_mode">Entwicklermodus deaktivieren</string>
|
||||
<string name="developer_mode_disabled">Entwicklermodus deaktiviert</string>
|
||||
<string name="gps">GPS</string>
|
||||
<string name="compass">Kompass</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="copyright_summary">©2020–2023 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>
|
||||
@@ -95,10 +93,10 @@
|
||||
<string name="realtime_data_unavailable">Sanntidsstatus utilgjengelig</string>
|
||||
<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="copyright_summary">©2020–2023 Johan von Forstner</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,31 @@
|
||||
<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>
|
||||
<string name="chargeprice_price_not_available">Pris ikke tilgjengelig</string>
|
||||
<string name="developer_mode_disabled">Utviklermodus avslått</string>
|
||||
<string name="gps">GPS</string>
|
||||
<string name="compass">Kompass</string>
|
||||
<string name="pref_applink_associate">Åpne støttede lenker</string>
|
||||
<string name="pref_applink_associate_summary">fra goingelectric.de og openchargemap.org</string>
|
||||
<string name="chargeprice_header_other_tariffs">Andre ladeabonnementer</string>
|
||||
<string name="disable_developer_mode">Skru av utviklermodus</string>
|
||||
<string name="chargeprice_header_my_tariffs">Mine ladeabonnementer</string>
|
||||
<string name="developer_options">Utvikleralternativer</string>
|
||||
<string name="data_source_switched_to">Datakilde byttet til %s</string>
|
||||
<string name="developer_mode_enabled">Utviklermodus påslått</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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user