Compare commits

...

137 Commits
1.3.9 ... 1.4.2

Author SHA1 Message Date
johan12345
dcc03da237 Release 1.4.2 2022-11-18 22:27:27 +01:00
johan12345
295c00ea55 prefer to open URLs in custom tab, even if native app available
(such as EVMap itself)
2022-11-18 22:02:09 +01:00
johan12345
8d6756d57d Release 1.4.1 2022-11-13 15:16:15 +01:00
johan12345
71acd28b74 upgrade robolectric 2022-11-13 15:09:50 +01:00
johan12345
e79c1168ff update dependencies 2022-11-13 14:43:02 +01:00
johan12345
9833159fa8 update target SDK to 33 (Android 13) 2022-11-13 14:37:37 +01:00
johan12345
88ace5ba82 Android >= 12: Add link in preferences to enable opening links 2022-11-13 14:19:15 +01:00
johan12345
0ed82d15ff Add support for opening openchargemap.org links in EVMap 2022-11-13 14:14:08 +01:00
johan12345
0f525a6c48 Fix address format when street is not provided
fixes #258
2022-11-12 21:10:03 +01:00
johan12345
a91a5ce52e replace times symbol with escape sequence
refs #257
2022-11-12 20:58:25 +01:00
Maximilian Goldschmidt
cd3b1db90d Added multiple filter pages for Android Auto and AAOS (#251)
* Added multiple filter pages for auto and automotive

* use IMAGE_TYPE_ICON for icons

* implement different approach for multi-page layout using DummyReturnScreen

* revert unnecessary changes

* Added multiple filter pages for auto and automotive

* use IMAGE_TYPE_ICON for icons

* implement different approach for multi-page layout using DummyReturnScreen

* revert unnecessary changes

* reimplement EditFiltersScreen pagination to allow for arbitrary number of rows

* add @lxam97 to contributors list

* move delete button back to EditFilterScreen

* implement pagination for FilterScreen

* Replaced Next and Back with the goto page

* fixes for FilterScreen

* update strings

Co-authored-by: johan12345 <johan.forstner@gmail.com>
2022-11-11 17:25:36 +01:00
johan12345
6e3e34c642 add fronyx API to GH actions release pipeline 2022-11-09 18:34:07 +01:00
johan12345
8ce7f5cae2 Android Auto ChargerDetailScreen: show data even before availability and photo is loaded 2022-11-05 19:01:50 +01:00
johan12345
fae3bb2038 Chargeprice: show plans where the price is not available
fixes #255
2022-11-05 12:53:30 +01:00
johan12345
9490aa7110 donottranslate.xml: split up contributors list into multiple lines 2022-11-04 23:13:59 +01:00
Hosted Weblate
66a27d19f3 Translated using Weblate (French)
Currently translated at 97.0% (33 of 34 strings)

Co-authored-by: Altons <marsupilami450@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/fr/
Translation: EVMap/Android (strings specific to Google Play variant)
2022-11-04 23:08:30 +01:00
Hosted Weblate
09cf6cb087 Translated using Weblate (Norwegian Bokmål)
Currently translated at 82.3% (28 of 34 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/nb_NO/
Translation: EVMap/Android (strings specific to Google Play variant)
2022-11-04 23:08:29 +01:00
johan12345
4d23c916a9 fix repeated call of onCheckedChangeListener 2022-11-01 11:45:23 +01:00
johan12345
fec5de1de1 BarGraphView: don't crash if onDraw is called before onSizeChanged 2022-11-01 11:31:10 +01:00
johan12345
89957ef738 update CustomBottomSheetBehavior
fixes #247 (problem was that layout is not applied in settling state)
2022-10-31 22:38:22 +01:00
johan12345
a8e9bcd9eb improve bottomSheetExpanded LiveData 2022-10-31 22:24:13 +01:00
johan12345
0c3e3b0c35 Another constraint fix
Refs 1b7b5121e6, #253
2022-10-31 22:24:13 +01:00
johan12345
78f9b7162c Fix #252: Pins have wrong color after switching filter 2022-10-31 22:24:13 +01:00
johan12345
600a294ab2 Fix #252: Pins have wrong color after switching filter 2022-10-31 22:01:56 +01:00
johan12345
1b8bedcd6d improve switch between single- and multiline mode for charger name 2022-10-31 21:53:11 +01:00
johan12345
1b7b5121e6 rework constraints for name & icons at top of detail view
fixes #253
2022-10-31 21:43:11 +01:00
johan12345
e469ce83e5 Release 1.4.0 2022-10-30 17:46:29 +01:00
johan12345
ef68e6039e Auto: make navigate button primary action 2022-10-30 17:41:10 +01:00
johan12345
2ad673f8aa Automotive: Don't show "opened on phone" toast when we're in fact opening on the car's browser 2022-10-30 17:41:10 +01:00
johan12345
5b55087337 rework adapters in ChargepriceFragment to increase performance
fixes #222
2022-10-26 22:46:47 +02:00
Hosted Weblate
cb8a81823d Translated using Weblate (Norwegian Bokmål)
Currently translated at 85.1% (230 of 270 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nb_NO/
Translation: EVMap/Android
2022-10-26 21:25:16 +02:00
johan12345
742950b62c Android Auto: fix infinite loading in case location cannot be determined 2022-10-19 07:50:53 +02:00
johan12345
9bb8825ab7 Android Auto: fix a case of infinite loading 2022-10-18 21:36:45 +02:00
johan12345
a0c41290cd Android Auto: add data source selection on first start 2022-10-17 23:27:19 +02:00
johan12345
85240f0145 add possibility to pass fronyx API key with env variables 2022-10-16 12:40:36 +02:00
johan12345
7cc50d7127 Upgrade jsonapi library, implement workaround for relationships 2022-10-15 22:26:31 +02:00
Johan von Forstner
3d0ebc0b85 Merge pull request #244 from johan12345/car-app-library-1.3.0-2
Android Auto/Automotive enhancements
2022-10-13 22:53:14 +02:00
johan12345
4e83e37d61 Android Auto: allow to search for chargers along the current driving direction
#143
2022-10-13 22:38:46 +02:00
johan12345
94030c010c Car App Library 1.3: show prices for other tariffs in disabled state 2022-10-13 21:43:02 +02:00
johan12345
6b287c4084 ChargepriceScreen: fix crash if car is not selected 2022-10-13 21:28:17 +02:00
johan12345
1f6fe04b7d Revert "Revert "upgrade car app library to 1.3.0-beta01""
This reverts commit 087178193b.
2022-10-13 21:07:32 +02:00
johan12345
91d5ce02e2 Revert "Revert "try to use plug icons in detail view""
This reverts commit 20ae25cf8a.
2022-10-13 21:07:32 +02:00
johan12345
22bd9ed9e8 Revert "Revert "use CarIconSpan in row texts""
This reverts commit 1d3e3417aa.
2022-10-13 21:07:31 +02:00
johan12345
19142e0b59 Revert "Revert "check car app API level before setting OnContentRefreshListener""
This reverts commit 09d6647ec0.
2022-10-13 21:07:31 +02:00
johan12345
06fe347c73 Revert "Revert "Android Auto: move search button from filter screen back to map""
This reverts commit f2d98f9d82.
2022-10-13 21:07:31 +02:00
johan12345
adead1ac3c Revert "Revert "Upgrade Car App library""
This reverts commit d7a644cb78.
2022-10-13 21:07:30 +02:00
Johan von Forstner
f722ae5d7a Implement tablet layout for charger detail view (#242)
* prototype tablet layout for detail view

#133

* layout fixes for tablet layout

* make bottom sheet non-collapsible in tablet layout

* Update BottomSheetBehavior library

* change minimum size for tablet layout

* don't hide FABs in tablet layout
2022-10-09 21:08:26 +02:00
johan12345
33a14c581c fix ListIndexOutOfBoundsException 2022-10-09 21:01:07 +02:00
johan12345
b80ddf8851 fix double divider line for chargers not supported by Chargeprice 2022-10-08 15:12:54 +02:00
johan12345
848745359d If connector filter is set, only show prediction for filtered connectors 2022-10-08 15:12:54 +02:00
johan12345
5d47ca2e3a add note that predictions are only for DC plugs
(in case it is relevant)
2022-10-08 15:12:54 +02:00
johan12345
c28a5382d4 add EVSEID support for NewMotionAvailabilityDetector 2022-10-08 15:12:54 +02:00
johan12345
f8bdae78cd add option to disable predictions 2022-10-08 15:12:54 +02:00
johan12345
9891cf8e88 Fronyx predictions: Only load data for supported chargers
(CCS and CHAdeMO located in Germany)
2022-10-08 15:12:54 +02:00
Johan von Forstner
172e66fe15 continue work on prediction UI 2022-10-08 15:12:54 +02:00
Johan von Forstner
4a925d10bd make the bar graph show utilization instead of availability 2022-10-08 15:12:54 +02:00
johan12345
c7fc0a34ed add "powered by fronyx" logo, work on UI 2022-10-08 15:12:54 +02:00
johan12345
9780f6d2c0 implement aggregation of prediction timeseries more elegantly 2022-10-08 15:12:54 +02:00
johan12345
f6afb2a8cb Add some labels to BarGraphView 2022-10-08 15:12:54 +02:00
johan12345
b0371c1b20 implement initial UI for predictions 2022-10-08 15:12:54 +02:00
johan12345
2625acffda Add EVSEID output to EnBwAvailabilityDetector 2022-10-08 15:12:54 +02:00
johan12345
7418748c0f Implement fronyx availability prediction API 2022-10-08 15:12:54 +02:00
Licaon_Kter
3c3edf9ab4 Changelogs mixup (#239)
* mixup locale

* and here
2022-10-07 14:02:02 +02:00
johan12345
d52ec0c63f improve heuristics in Chargeprice code
fixes #238
2022-10-05 21:28:51 +02:00
johan12345
eebb060f1a getChargepointDetail: emit loading state first 2022-10-05 20:52:27 +02:00
johan12345
19c0f311ad add stats to ChargepriceScreen 2022-10-04 19:03:06 +02:00
johan12345
9e280d3150 ChargepriceScreen: fix selection of correct chargepoint 2022-10-04 18:42:57 +02:00
johan12345
0998ed1f67 Release 1.3.14 2022-10-01 10:11:58 +02:00
johan12345
d7a644cb78 Revert "Upgrade Car App library"
This reverts commit 094a26980f.
2022-10-01 09:57:07 +02:00
johan12345
f2d98f9d82 Revert "Android Auto: move search button from filter screen back to map"
This reverts commit f24b7d1c2c.
2022-10-01 09:57:07 +02:00
johan12345
09d6647ec0 Revert "check car app API level before setting OnContentRefreshListener"
This reverts commit 4beb1f92ad.
2022-10-01 09:57:07 +02:00
johan12345
1d3e3417aa Revert "use CarIconSpan in row texts"
This reverts commit 4989aedd8b.
2022-10-01 09:57:07 +02:00
johan12345
20ae25cf8a Revert "try to use plug icons in detail view"
This reverts commit c8f333ce89.
2022-10-01 09:57:06 +02:00
johan12345
087178193b Revert "upgrade car app library to 1.3.0-beta01"
This reverts commit 09ded65b4e.
2022-10-01 09:57:06 +02:00
Johan von Forstner
60d54c989b update version code 2022-09-27 23:13:31 +02:00
Johan von Forstner
c0555e7965 Android Auto: show loading error as list text instead of toast 2022-09-27 23:11:18 +02:00
Johan von Forstner
49c2fb3494 Android Auto location fixes 2022-09-27 23:04:43 +02:00
Johan von Forstner
c1ec46917e add app_logo.svg 2022-09-27 19:33:33 +02:00
Johan von Forstner
ac11cddd42 Release 1.3.13 2022-09-27 19:02:05 +02:00
Johan von Forstner
6267e897d4 observe apiId liveData to fix bug 2022-09-27 18:59:38 +02:00
Johan von Forstner
8a0224707b Release 1.3.12 2022-09-26 16:49:16 +02:00
johan12345
09ded65b4e upgrade car app library to 1.3.0-beta01 2022-09-25 20:10:53 +02:00
johan12345
c8f333ce89 try to use plug icons in detail view
#199, #198
2022-09-25 20:10:53 +02:00
johan12345
4989aedd8b use CarIconSpan in row texts
fixes #199
2022-09-25 20:10:53 +02:00
johan12345
4beb1f92ad check car app API level before setting OnContentRefreshListener 2022-09-25 20:10:52 +02:00
johan12345
f24b7d1c2c Android Auto: move search button from filter screen back to map
This reverts commit 6b6c7da081.
2022-09-25 20:10:52 +02:00
johan12345
094a26980f Upgrade Car App library
This reverts commit 2e8cdb01fd.
2022-09-25 20:10:52 +02:00
Johan von Forstner
098f815ac9 fix german string 2022-09-25 20:10:40 +02:00
johan12345
6c51693b8d rebuild availability loading with switchMap 2022-09-21 20:15:47 +02:00
johan12345
a360c71ecb Upgrade dependencies 2022-09-20 19:54:51 +02:00
johan12345
b9da72e449 add supported countries for Chargeprice
fixes #234
2022-09-20 19:24:33 +02:00
johan12345
b0aeb8af98 Fix crash when changing data source
fixes #232
2022-09-20 19:15:45 +02:00
johan12345
1867d1bf7a Update source button text when API changes
fixes #233
2022-09-18 19:54:19 +02:00
johan12345
31fcee97e1 increase version code 2022-09-14 08:37:34 +02:00
johan12345
de8fd364f4 Android Auto: show loading errors 2022-09-14 08:36:51 +02:00
johan12345
e3f271be5d increase version code 2022-09-12 23:06:40 +02:00
johan12345
99a2540398 MapScreen: move away from LiveData to avoid more refresh bugs 2022-09-12 23:06:08 +02:00
johan12345
85173b438b increase version code 2022-09-12 22:43:42 +02:00
johan12345
c288883572 Android Auto: fix switch between filter settings 2022-09-12 22:42:34 +02:00
johan12345
c8f949da01 increase version code 2022-09-12 08:20:37 +02:00
johan12345
fe33dca1bc Android Auto: fix loading data on first start 2022-09-12 08:19:00 +02:00
johan12345
4fb5090e9b Release 1.3.11 2022-09-11 20:19:21 +02:00
johan12345
d9b8bf382a ChargerDetailScreen: make images square 2022-09-11 19:41:27 +02:00
johan12345
d69456dfd0 fix more issues switching the data source, esp. on Android Auto 2022-09-11 19:14:12 +02:00
johan12345
4da2a273c7 MapScreen: avoid updating chargers twice, which can lead to crashes
due to template update limit
2022-09-11 18:49:28 +02:00
johan12345
8e622c881d Android Auto: limit maxRows in MapScreen to at most 25 2022-09-11 18:38:04 +02:00
johan12345
89b2175d89 fix crashes due to race conditions when changing data source 2022-09-11 18:07:08 +02:00
johan12345
3c30481821 add ChargeLocationRepository
encapsulates logic to load charging stations for future implementation of offline caching (#164, #97)
2022-09-10 19:46:14 +02:00
johan12345
385353689d French: ignore ImpliedQuantity 2022-09-04 15:46:26 +02:00
johan12345
7f9c838b9d remove ignore:MissingQuantity for French 2022-09-04 15:41:30 +02:00
Hosted Weblate
205814e6f6 Translated using Weblate (Norwegian Bokmål)
Currently translated at 85.4% (223 of 261 strings)

Co-authored-by: Johan von Forstner <johan.forstner@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nb_NO/
Translation: EVMap/Android
2022-09-04 15:40:02 +02:00
Hosted Weblate
f30ae4a720 Translated using Weblate (French)
Currently translated at 100.0% (261 of 261 strings)

Co-authored-by: Altons <marsupilami450@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/fr/
Translation: EVMap/Android
2022-09-04 15:40:02 +02:00
johan12345
a6117d3484 string adjustments 2022-09-04 11:05:50 +02:00
Hosted Weblate
f650def803 Translated using Weblate (Norwegian Bokmål)
Currently translated at 84.6% (221 of 261 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nb_NO/
Translation: EVMap/Android
2022-09-04 11:04:33 +02:00
johan12345
09ec0d1635 fix size of marker icon (mainly in intro)
(regression from b82f6f68fb)
2022-09-04 08:43:39 +02:00
Hosted Weblate
cfc98209a1 Translated using Weblate (German)
Currently translated at 100.0% (261 of 261 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Johan von Forstner <johan.forstner@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/de/
Translation: EVMap/Android
2022-09-03 20:24:44 +02:00
johan12345
17dbab4659 remove unnecessary period 2022-09-03 20:19:56 +02:00
johan12345
f4ed7f7397 remove French and Norwegian strings according to simplifications in English strings 2022-09-03 20:10:56 +02:00
johan12345
c26253fe44 adapt some German strings according to changes in English strings 2022-09-03 20:10:56 +02:00
johan12345
1c7cc32427 Further adjustments to @comradekingu's suggestions 2022-09-03 20:10:56 +02:00
Johan von Forstner
4ff944c4e4 Update app/src/main/res/values/strings.xml
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
2022-09-03 20:10:56 +02:00
Allan Nordhøy
2ec437a14b Suggestions, fixes, removals 2022-09-03 20:10:56 +02:00
Allan Nordhøy
ae05d44649 App strings reworked 2022-09-03 20:10:47 +02:00
johan12345
a0f90a8c94 restructure FilterValueDao.getFilterValues
possible fix for #225
2022-09-03 18:14:44 +02:00
johan12345
e1b7463490 rework locale chooser to use Android 13 native app locales 2022-09-03 15:37:58 +02:00
johan12345
27cbb5e208 add nautilusx to contributors 2022-09-01 20:57:03 +02:00
Hosted Weblate
5526e5d97c Translated using Weblate (Norwegian Bokmål)
Currently translated at 79.5% (210 of 264 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nb_NO/
Translation: EVMap/Android
2022-09-01 20:55:53 +02:00
Hosted Weblate
fc92ee9cdc Translated using Weblate (German)
Currently translated at 100.0% (264 of 264 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: nautilusx <translate@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/de/
Translation: EVMap/Android
2022-09-01 20:55:52 +02:00
Hosted Weblate
f20fab965c Translated using Weblate (French)
Currently translated at 100.0% (264 of 264 strings)

Co-authored-by: Altons <marsupilami450@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/fr/
Translation: EVMap/Android
2022-09-01 20:55:52 +02:00
johan12345
9146e7dab8 Merge remote-tracking branch 'weblate/master' 2022-08-25 20:43:12 +02:00
johan12345
54936b6e1a make MissingTranslation lint check a warning 2022-08-25 20:32:31 +02:00
Altons
f3245dc29b Translated using Weblate (French)
Currently translated at 100.0% (264 of 264 strings)

Translation: EVMap/Android
Translate-URL: https://hosted.weblate.org/projects/evmap/android/fr/
2022-08-25 20:27:25 +02:00
johan12345
90968029ad Release 1.3.10 2022-08-25 20:26:44 +02:00
Hosted Weblate
97cab1d007 Translated using Weblate (French)
Currently translated at 99.6% (263 of 264 strings)

Co-authored-by: Altons <marsupilami450@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/fr/
Translation: EVMap/Android
2022-08-25 20:24:19 +02:00
johan12345
0d41fb2685 Release 1.3.10 2022-08-25 20:23:53 +02:00
johan12345
2c0a5085ab Add contributors dialog in About
fixes #223
2022-08-25 20:22:30 +02:00
johan12345
9e80270a78 fix DataSourceSelectDialog OK button not working 2022-08-25 19:56:04 +02:00
113 changed files with 3451 additions and 1187 deletions

View File

@@ -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>

View File

@@ -31,6 +31,7 @@ jobs:
CHARGEPRICE_API_KEY: ${{ secrets.CHARGEPRICE_API_KEY }}
MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }}
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
KEYSTORE_ALIAS_PASSWORD: ${{ secrets.KEYSTORE_ALIAS_PASSWORD }}

65
_img/app_logo.svg Normal file
View 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

View 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

View File

@@ -10,19 +10,23 @@ apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'de.timfreiheit.resourceplaceholders'
def supportedLocales = "en,de,fr,nb-rNO"
android {
compileSdkVersion 32
compileSdkVersion 33
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 31
targetSdkVersion 33
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode 104
versionName "1.3.9"
versionCode 142
versionName "1.4.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs supportedLocales.split(",")
buildConfigField("String", "supportedLocales", '"' + supportedLocales + '"')
}
signingConfigs {
@@ -93,6 +97,7 @@ android {
}
lint {
disable 'NullSafeMutableLiveData'
warning 'MissingTranslation'
}
testOptions {
@@ -102,6 +107,7 @@ android {
resourcePlaceholders {
files = ['xml/shortcuts.xml']
}
namespace 'net.vonforst.evmap'
// add API keys from environment variable if not set in apikeys.xml
applicationVariants.all { variant ->
@@ -135,6 +141,13 @@ android {
if (chargepriceKey != null) {
variant.resValue "string", "chargeprice_key", chargepriceKey
}
def fronyxKey = env.FRONYX_API_KEY ?: project.findProperty("FRONYX_API_KEY")
if (fronyxKey == null && project.hasProperty("FRONYX_API_KEY_ENCRYPTED")) {
fronyxKey = decode(project.findProperty("FRONYX_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
}
if (fronyxKey != null) {
variant.resValue "string", "fronyx_key", fronyxKey
}
}
}
@@ -146,29 +159,26 @@ configurations {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.6.0-rc01'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "androidx.activity:activity-ktx:1.5.1"
implementation "androidx.fragment:fragment-ktx:1.5.1"
implementation "androidx.activity:activity-ktx:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.5.4"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.browser:browser:1.4.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:dd0167dbff'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:3529a5a9f1'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
implementation 'com.squareup.moshi:moshi-adapters:1.13.0'
implementation 'com.github.johan12345:jsonapi:50d72e7e55' // patched version for jsonapi-adapters
implementation('com.markomilos.jsonapi:jsonapi-retrofit:1.0.1') {
exclude group: 'com.markomilos.jsonapi', module: 'jsonapi-adapters'
}
implementation 'com.markomilos.jsonapi:jsonapi-retrofit:1.1.0'
implementation 'io.coil-kt:coil:1.1.0'
implementation 'com.github.johan12345:StfalconImageViewer:5082ebd392'
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
@@ -180,7 +190,7 @@ dependencies {
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
// Android Auto
def carAppVersion = '1.2.0-rc01'
def carAppVersion = '1.3.0-beta01'
googleImplementation "androidx.car.app:app:$carAppVersion"
googleNormalImplementation "androidx.car.app:app-projected:$carAppVersion"
googleAutomotiveImplementation "androidx.car.app:app-automotive:$carAppVersion"
@@ -200,7 +210,7 @@ dependencies {
implementation 'com.github.johan12345:mapbox-events-android:a21c324501'
// Google Places
googleImplementation 'com.google.android.libraries.places:places:2.6.0'
googleImplementation 'com.google.android.libraries.places:places:2.7.0'
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.1'
// Mapbox Geocoding
@@ -244,15 +254,15 @@ dependencies {
// testing for car app
testGoogleImplementation "androidx.car.app:app-testing:$carAppVersion"
testGoogleImplementation 'org.robolectric:robolectric:4.8.1'
testGoogleImplementation 'androidx.test:core:1.4.0'
testGoogleImplementation 'org.robolectric:robolectric:4.9'
testGoogleImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.13.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
}
private static String decode(String s, String key) {

View File

@@ -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" />

View File

@@ -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 {

View File

@@ -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()

View File

@@ -22,13 +22,20 @@ import jsonapi.ResourceIdentifier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.currency
import net.vonforst.evmap.ui.time
import java.io.IOException
import kotlin.math.roundToInt
class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(ctx) {
private val prefs = PreferenceDataSource(ctx)
@@ -41,6 +48,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
}
private var prices: List<ChargePrice>? = null
private var meta: ChargepriceChargepointMeta? = null
private var chargepoint: Chargepoint? = null
private val maxRows = if (ctx.carAppApiLevel >= 2) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
} else 6
@@ -62,7 +70,24 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
if (prices == null && errorMessage == null) {
setLoading(true)
} else {
setSingleList(ItemList.Builder().apply {
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
)
}
}
val myTariffs = prefs.chargepriceMyTariffs
val myTariffsAll = prefs.chargepriceMyTariffsAll
val list = ItemList.Builder().apply {
setNoItemsMessage(
errorMessage ?: carContext.getString(R.string.chargeprice_no_tariffs_found)
)
@@ -70,9 +95,17 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
addItem(Row.Builder().apply {
setTitle(formatProvider(price))
addText(formatPrice(price))
if (carContext.carAppApiLevel >= 5) {
setEnabled(myTariffsAll || myTariffs != null && price.tariffId in myTariffs)
}
}.build())
}
}.build())
}.build()
if (header != null && list.items.isNotEmpty()) {
addSectionedList(SectionedItemList.create(list, header))
} else {
setSingleList(list)
}
}
setActionStrip(
ActionStrip.Builder().addAction(
@@ -101,11 +134,14 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
carContext.startActivity(intent)
CarToast.makeText(
carContext,
R.string.opened_on_phone,
CarToast.LENGTH_LONG
).show()
if (BuildConfig.FLAVOR_automotive != "automotive") {
// only show the toast "opened on phone" if we're running on a phone
CarToast.makeText(
carContext,
R.string.opened_on_phone,
CarToast.LENGTH_LONG
).show()
}
} catch (e: ActivityNotFoundException) {
CarToast.makeText(
carContext,
@@ -128,19 +164,21 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
}
private fun formatPrice(price: ChargePrice): String {
val amount = price.chargepointPrices.first().price
?: return "${carContext.getString(R.string.chargeprice_price_not_available)} (${price.chargepointPrices.first().noPriceReason})"
val totalPrice = carContext.getString(
R.string.charge_price_format,
price.chargepointPrices.first().price,
amount,
currency(price.currency)
)
val kwhPrice = if (price.chargepointPrices.first().price > 0f) {
val kwhPrice = if (amount > 0f) {
carContext.getString(
if (price.chargepointPrices[0].priceDistribution.isOnlyKwh) {
R.string.charge_price_kwh_format
} else {
R.string.charge_price_average_format
},
price.chargepointPrices.get(0).price / meta!!.energy,
amount / meta!!.energy,
currency(price.currency)
)
} else null
@@ -179,6 +217,14 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
try {
val car = determineVehicle(manufacturer, modelName)
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
if (cpStation.chargePoints.isEmpty()) {
errorMessage =
carContext.getString(R.string.chargeprice_no_compatible_connectors)
invalidate()
return@launch
}
val result = api.getChargePrices(
ChargepriceRequest(
dataAdapter = dataAdapter,
@@ -189,7 +235,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 +260,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 +277,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 +292,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

View File

@@ -1,10 +1,10 @@
package net.vonforst.evmap.auto
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.car.app.CarContext
@@ -27,22 +27,27 @@ import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.iconForPlugType
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Cost
import net.vonforst.evmap.model.FaultReport
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.ChargerIconGenerator
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.getReferenceData
import net.vonforst.evmap.viewmodel.awaitFinished
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import kotlin.math.roundToInt
class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : Screen(ctx) {
var charger: ChargeLocation? = null
var photo: Bitmap? = null
@@ -50,10 +55,8 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
val prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
private val api by lazy {
createApi(prefs.dataSource, ctx)
}
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
private val repo =
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
private val imageSize = 128 // images should be 128dp according to docs
private val imageSizeLarge = 480 // images should be 480 x 480 dp according to docs
@@ -71,9 +74,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
private var favoriteUpdateJob: Job? = null
init {
referenceData.observe(this) {
loadCharger()
}
loadCharger()
}
override fun onGetTemplate(): Template {
@@ -99,29 +100,29 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
).build()
)
.setTitle(carContext.getString(R.string.navigate))
.setFlags(Action.FLAG_PRIMARY)
.setBackgroundColor(CarColor.PRIMARY)
.setOnClickListener {
navigateToCharger(charger)
}
.build())
charger.chargepriceData?.country?.let { country ->
if (ChargepriceApi.isCountrySupported(country, charger.dataSource)) {
addAction(Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_chargeprice
)
).build()
)
.setTitle(carContext.getString(R.string.auto_prices))
if (ChargepriceApi.isChargerSupported(charger)) {
addAction(
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_chargeprice
)
).build()
)
.setTitle(carContext.getString(R.string.auto_prices))
.setOnClickListener {
screenManager.push(ChargepriceScreen(carContext, charger))
}
.build())
}
}
} ?: setLoading(true)
}.build()
).apply {
@@ -244,15 +245,9 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
val operatorText = generateOperatorText(charger)
setTitle(operatorText)
charger.cost?.let { addText(it.getStatusText(carContext, emoji = true)) }
charger.cost?.let { addText(generateCostStatusText(it)) }
charger.faultReport?.let { fault ->
addText(
carContext.getString(
R.string.auto_fault_report_date,
fault.created?.atZone(ZoneId.systemDefault())
?.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
)
)
addText(generateFaultReportTitle(fault))
}
}.build())
} else {
@@ -267,20 +262,14 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
val operatorText = generateOperatorText(charger)
setTitle(operatorText)
charger.cost?.let {
addText(it.getStatusText(carContext, emoji = true))
addText(generateCostStatusText(it))
it.getDetailText()?.let { addText(it) }
}
}.build())
// row 3: fault report (if exists)
charger.faultReport?.let { fault ->
rows.add(Row.Builder().apply {
setTitle(
carContext.getString(
R.string.auto_fault_report_date,
fault.created?.atZone(ZoneId.systemDefault())
?.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
)
)
setTitle(generateFaultReportTitle(fault))
fault.description?.let {
addText(
HtmlCompat.fromHtml(
@@ -305,18 +294,79 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
return rows
}
private fun generateCostStatusText(cost: Cost): CharSequence {
val string = SpannableString(cost.getStatusText(carContext, emoji = true))
// replace emoji with CarIcon
string.indexOf('⚡').takeIf { it >= 0 }?.let { index ->
string.setSpan(
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_lightning
)
).setTint(CarColor.YELLOW).build()
), index, index + 1, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
)
}
string.indexOf('\uD83C').takeIf { it >= 0 }?.let { index ->
string.setSpan(
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_parking
)
).setTint(CarColor.BLUE).build()
), index, index + 2, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
)
}
return string
}
private fun generateFaultReportTitle(fault: FaultReport): CharSequence {
val string = SpannableString(
carContext.getString(
R.string.auto_fault_report_date,
fault.created?.atZone(ZoneId.systemDefault())
?.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
)
)
// replace emoji with CarIcon
string.setSpan(
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_fault_report
)
).setTint(CarColor.YELLOW).build()
), 0, 1, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE
)
return string
}
private fun generateChargepointsText(charger: ChargeLocation): SpannableStringBuilder {
val chargepointsText = SpannableStringBuilder()
charger.chargepointsMerged.forEachIndexed { i, cp ->
if (i > 0) chargepointsText.append(" · ")
chargepointsText.append(
"${cp.count}× ${
nameForPlugType(
carContext.stringProvider(),
cp.type
)
} ${cp.formatPower()}"
)
"${cp.count}× "
).append(
nameForPlugType(carContext.stringProvider(), cp.type),
CarIconSpan.create(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
iconForPlugType(cp.type)
)
).setTint(
CarColor.createCustom(Color.WHITE, Color.BLACK)
).build()
),
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
).append(" ").append(cp.formatPower())
availability?.status?.get(cp)?.let { status ->
chargepointsText.append(
" (${availabilityText(status)}/${cp.count})",
@@ -356,24 +406,23 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
}
private fun loadCharger() {
val referenceData = referenceData.value ?: return
lifecycleScope.launch {
favorite = db.favoritesDao().findFavorite(chargerSparse.id, chargerSparse.dataSource)
val response = api.getChargepointDetail(referenceData, chargerSparse.id)
val response = repo.getChargepointDetail(chargerSparse.id).awaitFinished()
if (response.status == Status.SUCCESS) {
val charger = response.data!!
this@ChargerDetailScreen.charger = charger
invalidate()
val photo = charger.photos?.firstOrNull()
photo?.let {
val density = carContext.resources.displayMetrics.density
val url = if (largeImageSupported) {
photo.getUrl(size = (imageSizeLarge * density).roundToInt())
} else {
photo.getUrl(size = (imageSize * density).roundToInt())
}
val size =
(density * if (largeImageSupported) imageSizeLarge else imageSize).roundToInt()
val url = photo.getUrl(size = size)
val request = ImageRequest.Builder(carContext).data(url).build()
var img =
val img =
(carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap
// draw icon on top of image
@@ -383,21 +432,32 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
multi = charger.isMulti()
)
img = img.copy(Bitmap.Config.ARGB_8888, true)
val outImg = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
val iconSmall = icon.scale(
(img.height * 0.4 / icon.height * icon.width).roundToInt(),
(img.height * 0.4).roundToInt()
(size * 0.4 / icon.height * icon.width).roundToInt(),
(size * 0.4).roundToInt()
)
val canvas = Canvas(outImg)
val m = Matrix()
m.setRectToRect(
RectF(0f, 0f, img.width.toFloat(), img.height.toFloat()),
RectF(0f, 0f, size.toFloat(), size.toFloat()),
Matrix.ScaleToFit.CENTER
)
canvas.drawBitmap(
img.copy(Bitmap.Config.ARGB_8888, false), m, null
)
val canvas = Canvas(img)
canvas.drawBitmap(
iconSmall,
0f,
(img.height - iconSmall.height * 1.1).toFloat(),
(size - iconSmall.height * 1.1).toFloat(),
null
)
this@ChargerDetailScreen.photo = img
this@ChargerDetailScreen.photo = outImg
}
this@ChargerDetailScreen.charger = charger
invalidate()
availability = getAvailability(charger).data

View File

@@ -1,6 +1,8 @@
package net.vonforst.evmap.auto
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.car.app.CarContext
@@ -11,6 +13,7 @@ import androidx.car.app.model.*
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.map
import kotlinx.coroutines.launch
import net.vonforst.evmap.R
import net.vonforst.evmap.model.*
@@ -24,15 +27,24 @@ import kotlin.math.roundToInt
class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
private val prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(ctx)
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
private val filterProfiles: LiveData<List<FilterProfile>> by lazy {
db.filterProfileDao().getProfiles(prefs.dataSource)
}
private val maxRows = if (ctx.carAppApiLevel >= 2) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
} else 6
private var page = 0
init {
filterProfiles.observe(this) {
val filterStatus = prefs.filterStatus
if (filterStatus in listOf(FILTERS_DISABLED, FILTERS_FAVORITES, FILTERS_CUSTOM)) {
page = 0
} else {
page = paginateProfiles(it).indexOfFirst { it.any { it.id == filterStatus } }
}
invalidate()
}
}
@@ -40,37 +52,27 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
override fun onGetTemplate(): Template {
val filterStatus = prefs.filterStatus
return ListTemplate.Builder().apply {
var title = carContext.getString(R.string.menu_filter)
filterProfiles.value?.let {
setSingleList(buildFilterProfilesList(it, filterStatus))
val paginatedProfiles = paginateProfiles(it)
setSingleList(buildFilterProfilesList(paginatedProfiles, filterStatus))
val numPages = paginatedProfiles.size
if (numPages > 1) {
title += " " + carContext.getString(
R.string.auto_multipage,
page + 1,
numPages
)
}
} ?: setLoading(true)
setTitle(carContext.getString(R.string.menu_filter))
setTitle(title)
setHeaderAction(Action.BACK)
setActionStrip(
ActionStrip.Builder().apply {
addAction(Action.Builder().apply {
setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
if (prefs.placeSearchResultAndroidAuto != null) {
R.drawable.ic_search_off
} else {
R.drawable.ic_search
}
)
).build()
)
setOnClickListener(ParkedOnlyOnClickListener.create {
if (prefs.placeSearchResultAndroidAuto != null) {
prefs.placeSearchResultAndroidAutoName = null
prefs.placeSearchResultAndroidAuto = null
screenManager.pop()
} else {
screenManager.push(PlaceSearchScreen(carContext, session))
}
})
}.build())
addAction(Action.Builder().apply {
setIcon(
CarIcon.Builder(
@@ -79,7 +81,6 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
R.drawable.ic_edit
)
).build()
)
setOnClickListener(ParkedOnlyOnClickListener.create {
lifecycleScope.launch {
@@ -94,47 +95,140 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
}.build()
}
private fun paginateProfiles(filterProfiles: List<FilterProfile>): List<List<FilterProfile>> {
val filterStatus = prefs.filterStatus
val extraRows = if (FILTERS_CUSTOM == filterStatus) 3 else 2
return filterProfiles.paginate(
maxRows - extraRows,
maxRows - extraRows - 1,
maxRows - 2,
maxRows - 1
)
}
private fun buildFilterProfilesList(
profiles: List<FilterProfile>,
paginatedProfiles: List<List<FilterProfile>>,
filterStatus: Long
): ItemList {
val extraRows = if (FILTERS_CUSTOM == filterStatus) 3 else 2
val profilesToShow =
profiles.sortedByDescending { it.id == filterStatus }.take(maxRows - extraRows)
return ItemList.Builder().apply {
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.no_filters))
}.build())
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.filter_favorites))
}.build())
profilesToShow.forEach {
if (page > 0) {
addItem(Row.Builder().apply {
setTitle(
CarText.Builder(
carContext.getString(R.string.auto_multipage_goto, page)
).build()
)
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_arrow_back
)
).build(),
Row.IMAGE_TYPE_ICON
)
setOnClickListener {
page -= 1
screenManager.pushForResult(DummyReturnScreen(carContext)) {
Handler(Looper.getMainLooper()).post {
invalidate()
}
}
}
}.build())
}
if (page == 0) {
addItem(Row.Builder().apply {
val active = filterStatus == FILTERS_DISABLED
setTitle(carContext.getString(R.string.no_filters))
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_close
)
).setTint(if (active) CarColor.SECONDARY else CarColor.DEFAULT)
.build(),
Row.IMAGE_TYPE_ICON
)
setOnClickListener { onItemClick(FILTERS_DISABLED) }
}.build())
addItem(Row.Builder().apply {
val active = filterStatus == FILTERS_FAVORITES
setTitle(carContext.getString(R.string.filter_favorites))
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_fav
)
).setTint(if (active) CarColor.SECONDARY else CarColor.DEFAULT)
.build(),
Row.IMAGE_TYPE_ICON
)
setOnClickListener { onItemClick(FILTERS_FAVORITES) }
}.build())
if (FILTERS_CUSTOM == filterStatus) {
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.filter_custom))
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_checkbox_checked
)
).setTint(CarColor.PRIMARY).build(),
Row.IMAGE_TYPE_ICON
)
setOnClickListener { onItemClick(FILTERS_CUSTOM) }
}.build())
}
}
paginatedProfiles[page].forEach {
addItem(Row.Builder().apply {
val name =
it.name.ifEmpty { carContext.getString(R.string.unnamed_filter_profile) }
val active = filterStatus == it.id
setTitle(name)
setImage(
if (active)
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_check
)
).setTint(CarColor.SECONDARY).build() else emptyCarIcon,
Row.IMAGE_TYPE_ICON
)
setOnClickListener { onItemClick(it.id) }
}.build())
}
if (FILTERS_CUSTOM == filterStatus) {
if (page < paginatedProfiles.size - 1) {
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.filter_custom))
}.build())
}
setSelectedIndex(when (filterStatus) {
FILTERS_DISABLED -> 0
FILTERS_FAVORITES -> 1
FILTERS_CUSTOM -> profilesToShow.size + 2
else -> profilesToShow.indexOfFirst { it.id == filterStatus } + 2
})
setOnSelectedListener { index ->
onItemClick(
when (index) {
0 -> FILTERS_DISABLED
1 -> FILTERS_FAVORITES
profilesToShow.size + 2 -> FILTERS_CUSTOM
else -> profilesToShow[index - 2].id
setTitle(
CarText.Builder(
carContext.getString(R.string.auto_multipage_goto, page + 2)
).build()
)
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_arrow_forward
)
).build(),
Row.IMAGE_TYPE_ICON
)
setOnClickListener {
page += 1
screenManager.pushForResult(DummyReturnScreen(carContext)) {
Handler(Looper.getMainLooper()).post {
invalidate()
}
}
}
)
}.build())
}
}.build()
}
@@ -153,8 +247,13 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
} else 6
private var page = 0
private var paginatedFilters = vm.filtersWithValue.map {
it?.paginate(maxRows, maxRows - 1, maxRows - 2, maxRows - 1)
}
init {
vm.filtersWithValue.observe(this) {
paginatedFilters.observe(this) {
vm.filterProfile.observe(this) {
invalidate()
}
@@ -165,18 +264,28 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
val currentProfileName = vm.filterProfile.value?.name
return ListTemplate.Builder().apply {
vm.filtersWithValue.value?.let { filtersWithValue ->
setSingleList(buildFiltersList(filtersWithValue.take(maxRows)))
paginatedFilters.value?.let { paginatedFilters ->
setSingleList(buildFiltersList(paginatedFilters))
} ?: setLoading(true)
setTitle(currentProfileName?.let {
var title = currentProfileName?.let {
carContext.getString(
R.string.edit_filter_profile,
it
it,
)
} ?: carContext.getString(R.string.menu_filter))
} ?: carContext.getString(R.string.menu_filter)
val numPages = paginatedFilters.value?.size ?: 0
if (numPages > 1) {
title += " " + carContext.getString(
R.string.auto_multipage,
page + 1,
numPages
)
}
setTitle(title)
setHeaderAction(Action.BACK)
setActionStrip(ActionStrip.Builder().apply {
val currentProfile = vm.filterProfile.value
if (currentProfile != null) {
@@ -218,29 +327,61 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
).build()
)
.setOnClickListener {
val textPromptScreen = TextPromptScreen(
carContext,
R.string.save_as_profile,
R.string.save_profile_enter_name,
currentProfileName
)
screenManager.pushForResult(textPromptScreen) { name ->
if (name == null) return@pushForResult
lifecycleScope.launch {
vm.saveAsProfile(name as String)
screenManager.popTo(MapScreen.MARKER)
val textPromptScreen = TextPromptScreen(
carContext,
R.string.save_as_profile,
R.string.save_profile_enter_name,
currentProfileName
)
screenManager.pushForResult(textPromptScreen) { name ->
if (name == null) return@pushForResult
var saveSuccess = false
lifecycleScope.launch {
saveSuccess = vm.saveAsProfile(name as String)
screenManager.popTo(MapScreen.MARKER)
}
if (!saveSuccess) return@pushForResult
}
invalidate()
}
}
.build()
.build()
)
}.build())
}
.build())
}.build()
}
private fun buildFiltersList(filters: List<FilterWithValue<out FilterValue>>): ItemList {
private fun buildFiltersList(paginatedFilters: List<FilterValues>): ItemList {
return ItemList.Builder().apply {
filters.forEach {
if (page > 0) {
addItem(Row.Builder().apply {
setTitle(
CarText.Builder(
carContext.getString(R.string.auto_multipage_goto, page)
).build()
)
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_arrow_back
)
).build(),
Row.IMAGE_TYPE_ICON
)
setOnClickListener {
page -= 1
screenManager.pushForResult(DummyReturnScreen(carContext)) {
Handler(Looper.getMainLooper()).post {
invalidate()
}
}
}
}.build())
}
paginatedFilters[page].forEach {
val filter = it.filter
val value = it.value
addItem(Row.Builder().apply {
@@ -294,6 +435,33 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
}
}.build())
}
if (page < paginatedFilters.size - 1) {
addItem(Row.Builder().apply {
setTitle(
CarText.Builder(
carContext.getString(R.string.auto_multipage_goto, page + 2)
).build()
)
setImage(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_arrow_forward
)
).build(),
Row.IMAGE_TYPE_ICON
)
setOnClickListener {
page += 1
screenManager.pushForResult(DummyReturnScreen(carContext)) {
Handler(Looper.getMainLooper()).post {
invalidate()
}
}
}
}.build())
}
}.build()
}
}

View File

@@ -2,18 +2,24 @@ package net.vonforst.evmap.auto
import android.content.pm.PackageManager
import android.location.Location
import android.os.Handler
import android.os.Looper
import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.info.CarInfo
import androidx.car.app.hardware.info.CarSensors
import androidx.car.app.hardware.info.Compass
import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.model.*
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.*
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.car2go.maps.model.LatLng
import kotlinx.coroutines.*
import net.vonforst.evmap.BuildConfig
@@ -24,19 +30,25 @@ import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.model.FilterValue
import net.vonforst.evmap.model.FilterWithValue
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.utils.bearingBetween
import net.vonforst.evmap.utils.distanceBetween
import net.vonforst.evmap.utils.headingDiff
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.awaitFinished
import net.vonforst.evmap.viewmodel.filtersWithValue
import net.vonforst.evmap.viewmodel.getFilterValues
import net.vonforst.evmap.viewmodel.getReferenceData
import java.io.IOException
import java.time.Duration
import java.time.Instant
import java.time.ZonedDateTime
import kotlin.collections.set
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
@@ -60,33 +72,33 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
private var location: Location? = null
private var lastDistanceUpdateTime: Instant? = null
private var chargers: List<ChargeLocation>? = null
private var loadingError = false
private var locationError = false
private var prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
private val api by lazy {
createApi(prefs.dataSource, ctx)
}
private val repo =
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
private val searchRadius = 5 // kilometers
private val distanceUpdateThreshold = Duration.ofSeconds(15)
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus?>> =
HashMap()
private val maxRows = if (ctx.carAppApiLevel >= 2) {
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST)
min(
ctx.constraintManager.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST),
25
)
} else 6
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
private val filterStatus = MutableLiveData<Long>().apply {
value = prefs.filterStatus
}
private val filterValues = db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
private val filters =
Transformations.map(referenceData) { api.getFilters(it, carContext.stringProvider()) }
private val filtersWithValue = filtersWithValue(filters, filterValues)
private var filterStatus = prefs.filterStatus
private var filtersWithValue: List<FilterWithValue<FilterValue>>? = null
private val hardwareMan: CarHardwareManager by lazy {
ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
private val carInfo: CarInfo by lazy {
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
}
private val carSensors: CarSensors by lazy { carContext.patchedCarSensors }
private var energyLevel: EnergyLevel? = null
private var heading: Compass? = null
private val permissions = if (BuildConfig.FLAVOR_automotive == "automotive") {
listOf(
"android.car.permission.CAR_ENERGY",
@@ -107,32 +119,36 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
}
override fun onGetTemplate(): Template {
session.requestLocationUpdates()
session.mapScreen = this
return PlaceListMapTemplate.Builder().apply {
setTitle(
prefs.placeSearchResultAndroidAutoName?.let {
carContext.getString(R.string.auto_chargers_near_location, it)
} ?: carContext.getString(
if (filterStatus.value == FILTERS_FAVORITES) {
if (filterStatus == FILTERS_FAVORITES) {
R.string.auto_favorites
} else {
R.string.auto_chargers_closeby
}
)
)
searchLocation?.let {
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).apply {
if (prefs.placeSearchResultAndroidAutoName != null) {
setMarker(
PlaceMarker.Builder()
.setColor(CarColor.PRIMARY)
.build()
)
}
}.build())
} ?: setLoading(true)
if (prefs.placeSearchResultAndroidAutoName != null) {
searchLocation?.let {
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).apply {
if (prefs.placeSearchResultAndroidAutoName != null) {
setMarker(
PlaceMarker.Builder()
.setColor(CarColor.PRIMARY)
.build()
)
}
}.build())
}
} else {
location?.let {
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).build())
}
}
chargers?.take(maxRows)?.let { chargerList ->
val builder = ItemList.Builder()
// only show the city if not all chargers are in the same city
@@ -142,7 +158,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
}
builder.setNoItemsMessage(
carContext.getString(
if (filterStatus.value == FILTERS_FAVORITES) {
if (filterStatus == FILTERS_FAVORITES) {
R.string.auto_no_favorites_found
} else {
R.string.auto_no_chargers_found
@@ -151,21 +167,38 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
)
builder.setOnItemsVisibilityChangedListener(this@MapScreen)
setItemList(builder.build())
} ?: setLoading(true)
} ?: run {
if (loadingError) {
val builder = ItemList.Builder()
builder.setNoItemsMessage(
carContext.getString(R.string.connection_error)
)
setItemList(builder.build())
} else if (locationError) {
val builder = ItemList.Builder()
builder.setNoItemsMessage(
carContext.getString(R.string.location_error)
)
setItemList(builder.build())
} else {
setLoading(true)
}
}
setCurrentLocationEnabled(true)
setHeaderAction(Action.APP_ICON)
val filtersCount = if (filterStatus.value == FILTERS_FAVORITES) 1 else {
filtersWithValue.value?.count {
val filtersCount = if (filterStatus == FILTERS_FAVORITES) 1 else {
filtersWithValue?.count {
!it.value.hasSameValueAs(it.filter.defaultValue())
}
}
setActionStrip(
ActionStrip.Builder()
.addAction(Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
.addAction(
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_settings
)
@@ -176,6 +209,42 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
session.mapScreen = null
}
.build())
.addAction(Action.Builder().apply {
setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
if (prefs.placeSearchResultAndroidAuto != null) {
R.drawable.ic_search_off
} else {
R.drawable.ic_search
}
)
).build()
)
setOnClickListener(ParkedOnlyOnClickListener.create {
if (prefs.placeSearchResultAndroidAuto != null) {
prefs.placeSearchResultAndroidAutoName = null
prefs.placeSearchResultAndroidAuto = null
screenManager.pushForResult(DummyReturnScreen(carContext)) {
chargers = null
loadChargers()
}
} else {
screenManager.pushForResult(
PlaceSearchScreen(
carContext,
session
)
) {
chargers = null
loadChargers()
}
session.mapScreen = null
}
})
}.build())
.addAction(
Action.Builder()
.setIcon(
@@ -189,14 +258,14 @@ 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) {
setOnContentRefreshListener(this@MapScreen)
}
}.build()
}
@@ -289,10 +358,10 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
) {
return
}
val previousLocation = this.location
this.location = location
if (updateCoroutine != null) {
// don't update while still loading last update
return
if (previousLocation == null) {
loadChargers()
}
val now = Instant.now()
@@ -307,17 +376,22 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
private fun loadChargers() {
val location = location ?: return
val referenceData = referenceData.value ?: return
val filters = filtersWithValue.value ?: return
val searchLocation =
prefs.placeSearchResultAndroidAuto ?: LatLng.fromLocation(location)
this.searchLocation = searchLocation
updateCoroutine = lifecycleScope.launch {
loadingError = false
try {
filterStatus = prefs.filterStatus
val filterValues =
db.filterValueDao().getFilterValuesAsync(filterStatus, prefs.dataSource)
val filters = repo.getFiltersAsync(carContext.stringProvider())
filtersWithValue = filtersWithValue(filters, filterValues)
// load chargers
if (filterStatus.value == FILTERS_FAVORITES) {
if (filterStatus == FILTERS_FAVORITES) {
chargers =
db.favoritesDao().getAllFavoritesAsync().map { it.charger }.sortedBy {
distanceBetween(
@@ -326,60 +400,102 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
)
}
} else {
val response = api.getChargepointsRadius(
referenceData,
searchLocation,
searchRadius,
zoom = 16f,
filters
)
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
chargers?.let {
if (it.size < maxRows) {
// try again with larger radius
val response = api.getChargepointsRadius(
referenceData,
searchLocation,
searchRadius * 10,
zoom = 16f,
filters
)
chargers =
response.data?.filterIsInstance(ChargeLocation::class.java)
// try multiple search radii until we have enough chargers
var chargers: List<ChargeLocation>? = null
for (radius in listOf(searchRadius, searchRadius * 10, searchRadius * 50)) {
val response = repo.getChargepointsRadius(
searchLocation,
radius,
zoom = 16f,
filtersWithValue
).awaitFinished()
if (response.status == Status.ERROR) {
loadingError = true
invalidate()
return@launch
}
chargers = headingFilter(
response.data?.filterIsInstance(ChargeLocation::class.java),
searchLocation
)
if (chargers == null || chargers.size >= maxRows) {
break
}
}
this@MapScreen.chargers = chargers
}
updateCoroutine = null
lastDistanceUpdateTime = Instant.now()
invalidate()
} catch (e: IOException) {
withContext(Dispatchers.Main) {
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
.show()
}
loadingError = true
invalidate()
}
}
}
/**
* Filters by heading if heading available and enabled
*/
private fun headingFilter(
chargers: List<ChargeLocation>?,
searchLocation: LatLng
): List<ChargeLocation>? =
heading?.orientations?.value?.get(0)?.let { heading ->
if (!prefs.showChargersAheadAndroidAuto) return@let chargers
chargers?.filter {
val bearing = bearingBetween(
searchLocation.latitude,
searchLocation.longitude,
it.coordinates.lat,
it.coordinates.lng
)
val diff = headingDiff(bearing, heading.toDouble())
abs(diff) < 30
}
} ?: chargers
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
val isUpdate = this.energyLevel == null
this.energyLevel = energyLevel
if (isUpdate) invalidate()
}
private fun onCompassUpdated(compass: Compass) {
this.heading = compass
}
override fun onStart(owner: LifecycleOwner) {
setupListeners()
session.requestLocationUpdates()
locationError = false
Handler(Looper.getMainLooper()).postDelayed({
if (location == null) {
locationError = true
invalidate()
}
}, 5000)
// Reloading chargers in onStart does not seem to count towards content limit.
// So let's do this so the user gets fresh chargers when re-entering the app.
invalidate()
filtersWithValue.observe(this@MapScreen) {
loadChargers()
if (prefs.dataSource != repo.api.value?.id) {
repo.api.value = createApi(prefs.dataSource, carContext)
}
invalidate()
loadChargers()
}
private fun setupListeners() {
val exec = ContextCompat.getMainExecutor(carContext)
if (supportsCarApiLevel3(carContext)) {
carSensors.addCompassListener(
CarSensors.UPDATE_RATE_NORMAL,
exec,
::onCompassUpdated
)
}
if (!permissions.all {
ContextCompat.checkSelfPermission(
carContext,
@@ -390,8 +506,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 +517,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)
}
}

View File

@@ -19,6 +19,8 @@ import kotlin.math.max
import kotlin.math.min
class SettingsScreen(ctx: CarContext) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
override fun onGetTemplate(): Template {
return ListTemplate.Builder().apply {
setTitle(carContext.getString(R.string.auto_settings))
@@ -73,6 +75,14 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
}
.build()
)
addItem(
Row.Builder()
.setTitle(carContext.getString(R.string.auto_chargers_ahead))
.setToggle(Toggle.Builder {
prefs.showChargersAheadAndroidAuto = it
}.setChecked(prefs.showChargersAheadAndroidAuto).build())
.build()
)
}
}.build())
}.build()
@@ -82,12 +92,9 @@ class SettingsScreen(ctx: CarContext) : Screen(ctx) {
class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
val prefs = PreferenceDataSource(ctx)
val db = AppDatabase.getInstance(ctx)
val dataSourceNames = carContext.resources.getStringArray(R.array.pref_data_source_names)
val dataSourceValues = carContext.resources.getStringArray(R.array.pref_data_source_values)
val dataSourceDescriptions = listOf(
carContext.getString(R.string.data_source_goingelectric_desc),
carContext.getString(R.string.data_source_openchargemap_desc)
)
val searchProviderNames =
carContext.resources.getStringArray(R.array.pref_search_provider_names)
val searchProviderValues =
@@ -108,14 +115,9 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
screenManager.push(
ChooseDataSourceScreen(
carContext,
R.string.pref_data_source,
dataSourceNames,
dataSourceValues,
prefs.dataSource,
dataSourceDescriptions
) {
prefs.dataSource = it
})
ChooseDataSourceScreen.Type.CHARGER_DATA_SOURCE
)
)
}
}.build())
addItem(Row.Builder().apply {
@@ -129,13 +131,9 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
screenManager.push(
ChooseDataSourceScreen(
carContext,
R.string.pref_search_provider,
searchProviderNames,
searchProviderValues,
prefs.searchProvider
) {
prefs.searchProvider = it
})
ChooseDataSourceScreen.Type.SEARCH_PROVIDER
)
)
}
}.build())
addItem(Row.Builder().apply {
@@ -158,34 +156,85 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
class ChooseDataSourceScreen(
ctx: CarContext,
@StringRes val title: Int,
val names: Array<String>,
val values: Array<String>,
val currentValue: String,
val descriptions: List<String>? = null,
val callback: (String) -> Unit
val type: Type,
val initialChoice: Boolean = false,
@StringRes val extraDesc: Int? = null
) : Screen(ctx) {
enum class Type {
CHARGER_DATA_SOURCE, SEARCH_PROVIDER
}
val prefs = PreferenceDataSource(carContext)
val title = when (type) {
Type.CHARGER_DATA_SOURCE -> R.string.pref_data_source
Type.SEARCH_PROVIDER -> R.string.pref_search_provider
}
val names = when (type) {
Type.CHARGER_DATA_SOURCE -> carContext.resources.getStringArray(R.array.pref_data_source_names)
Type.SEARCH_PROVIDER -> carContext.resources.getStringArray(R.array.pref_search_provider_names)
}
val values = when (type) {
Type.CHARGER_DATA_SOURCE -> carContext.resources.getStringArray(R.array.pref_data_source_values)
Type.SEARCH_PROVIDER -> carContext.resources.getStringArray(R.array.pref_search_provider_values)
}
val currentValue: String = when (type) {
Type.CHARGER_DATA_SOURCE -> prefs.dataSource
Type.SEARCH_PROVIDER -> prefs.searchProvider
}
val descriptions = when (type) {
Type.CHARGER_DATA_SOURCE -> listOf(
carContext.getString(R.string.data_source_goingelectric_desc),
carContext.getString(R.string.data_source_openchargemap_desc)
)
Type.SEARCH_PROVIDER -> null
}
val callback: (String) -> Unit = when (type) {
Type.CHARGER_DATA_SOURCE -> { it ->
prefs.dataSourceSet = true
prefs.dataSource = it
}
Type.SEARCH_PROVIDER -> { it ->
prefs.searchProvider = it
}
}
override fun onGetTemplate(): Template {
return ListTemplate.Builder().apply {
setTitle(carContext.getString(title))
setHeaderAction(Action.BACK)
setSingleList(ItemList.Builder().apply {
setHeaderAction(if (initialChoice) Action.APP_ICON else Action.BACK)
val list = ItemList.Builder().apply {
for (i in names.indices) {
addItem(Row.Builder().apply {
setTitle(names[i])
descriptions?.let { addText(it[i]) }
if (initialChoice) {
setBrowsable(true)
setOnClickListener {
itemSelected(i)
}
}
}.build())
}
setOnSelectedListener {
callback(values[it])
screenManager.pop()
if (!initialChoice) {
setOnSelectedListener {
itemSelected(it)
}
setSelectedIndex(values.indexOf(currentValue))
}
setSelectedIndex(values.indexOf(currentValue))
}.build())
}.build()
if (extraDesc != null) {
addSectionedList(SectionedItemList.create(list, carContext.getString(extraDesc)))
} else {
setSingleList(list)
}
}.build()
}
private fun itemSelected(i: Int) {
callback(values[i])
screenManager.pop()
}
}
class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {

View File

@@ -3,13 +3,13 @@ package net.vonforst.evmap.auto
import android.content.Context
import android.graphics.Bitmap
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.common.CarUnit
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.model.*
import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.graphics.drawable.IconCompat
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import java.util.*
import kotlin.math.roundToInt
@@ -35,11 +35,13 @@ val CarContext.constraintManager
fun Bitmap.asCarIcon(): CarIcon = CarIcon.Builder(IconCompat.createWithBitmap(this)).build()
val emptyCarIcon = Bitmap.createBitmap(
1,
1,
Bitmap.Config.ARGB_8888
).asCarIcon()
val emptyCarIcon: CarIcon by lazy {
Bitmap.createBitmap(
1,
1,
Bitmap.Config.ARGB_8888
).asCarIcon()
}
private const val kmPerMile = 1.609344
private const val ftPerMile = 5280
@@ -134,6 +136,40 @@ private fun roundToMultipleOf(num: Double, step: Double): Double {
return (num / step).roundToInt() * step
}
/**
* Paginates data based on specific limits for each page.
* If the data fits on a single page, this page can have a maximum size nSingle. Otherwise, the
* first page has maximum nFirst items, the last page nLast items, and all intermediate pages nOther
* items.
*/
fun <T> List<T>.paginate(nSingle: Int, nFirst: Int, nOther: Int, nLast: Int): List<List<T>> {
if (nOther > nLast) {
throw IllegalArgumentException("nLast has to be larger than or equal to nOther")
}
return if (size <= nSingle) {
listOf(this)
} else {
val result = mutableListOf<List<T>>()
var i = 0
var page = 0
while (true) {
val remaining = size - i
if (page == 0) {
result.add(subList(i, i + nFirst))
i += nFirst
} else if (remaining <= nLast) {
result.add(subList(i, size))
break
} else {
result.add(subList(i, i + nOther))
i += nOther
}
page++
}
result
}
}
fun getAndroidAutoVersion(ctx: Context): List<String> {
val info = ctx.packageManager.getPackageInfo("com.google.android.projection.gearhead", 0)
return info.versionName.split(".")
@@ -152,4 +188,17 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
}
}
return true
}
class DummyReturnScreen(ctx: CarContext) : Screen(ctx) {
/*
Dummy screen to get around template refresh limitations.
It immediately pops back to the previous screen.
*/
override fun onGetTemplate(): Template {
screenManager.pop()
return MessageTemplate.Builder(carContext.getString(R.string.loading)).setLoading(true)
.build()
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -34,4 +34,6 @@
<string name="data_sources_hint">I innstillingene kan du også bytte mellom Google Maps og OpenStreetMap (Mapbox) for kartdata.</string>
<string name="selecting_all">valgte alle elementene</string>
<string name="sounds_cool">den er grei</string>
<string name="auto_chargers_ahead">Kun ladere i kjøreretningen</string>
<string name="loading">Laster inn …</string>
</resources>

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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)

View File

@@ -2,15 +2,16 @@ package net.vonforst.evmap
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen
@@ -32,7 +33,6 @@ import net.vonforst.evmap.fragment.MapFragmentArgs
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.navigation.NavHostFragment
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.utils.LocaleContextWrapper
import net.vonforst.evmap.utils.getLocationFromIntent
@@ -53,14 +53,6 @@ class MapsActivity : AppCompatActivity(),
var fragmentCallback: FragmentCallback? = null
private lateinit var prefs: PreferenceDataSource
override fun attachBaseContext(newBase: Context) {
return super.attachBaseContext(
LocaleContextWrapper.wrap(
newBase, PreferenceDataSource(newBase).language
)
);
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val splashScreen = installSplashScreen()
@@ -141,6 +133,37 @@ class MapsActivity : AppCompatActivity(),
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
if (id != null) {
if (prefs.dataSource != "goingelectric") {
prefs.dataSource = "goingelectric"
Toast.makeText(
this,
getString(
R.string.data_source_switched_to,
getString(R.string.data_source_goingelectric)
),
Toast.LENGTH_LONG
).show()
}
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
}
} else if (intent?.scheme == "https" && intent?.data?.host == "openchargemap.org") {
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
if (id != null) {
if (prefs.dataSource != "openchargemap") {
prefs.dataSource = "openchargemap"
Toast.makeText(
this,
getString(
R.string.data_source_switched_to,
getString(R.string.data_source_openchargemap)
),
Toast.LENGTH_LONG
).show()
}
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
@@ -206,6 +229,7 @@ class MapsActivity : AppCompatActivity(),
}
fun openUrl(url: String) {
val pkg = CustomTabsClient.getPackageName(this, null)
val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
@@ -213,6 +237,11 @@ class MapsActivity : AppCompatActivity(),
.build()
)
.build()
pkg?.let {
// prefer to open URL in custom tab, even if native app
// available (such as EVMap itself)
intent.intent.setPackage(pkg)
}
try {
intent.launchUrl(this, Uri.parse(url))
} catch (e: ActivityNotFoundException) {

View File

@@ -6,11 +6,6 @@ import android.graphics.Typeface
import android.os.Bundle
import android.text.*
import android.text.style.StyleSpan
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.util.*
fun Bundle.optDouble(name: String): Double? {
@@ -85,25 +80,6 @@ fun max(a: Int?, b: Int?): Int? {
fun <T> List<T>.containsAny(vararg values: T) = values.any { this.contains(it) }
public suspend fun <T> LiveData<T>.await(): T {
return withContext(Dispatchers.Main.immediate) {
suspendCancellableCoroutine { continuation ->
val observer = object : Observer<T> {
override fun onChanged(value: T) {
removeObserver(this)
continuation.resume(value, null)
}
}
observeForever(observer)
continuation.invokeOnCancellation {
removeObserver(observer)
}
}
}
}
fun Context.isDarkMode() =
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES

View File

@@ -161,6 +161,7 @@ class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
val binding = holder.binding as ItemConnectorButtonBinding
binding.enabled = enabledConnectors?.let { item.type in it } ?: true
val root = binding.root as CheckableConstraintLayout
root.setOnCheckedChangeListener { _, _ -> }
root.isChecked = checkedItem == position
root.setOnClickListener {
root.isChecked = true

View File

@@ -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) {
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
)
}

View File

@@ -130,13 +130,14 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
}
val connectorStatus = details.flatMap { it.evses }.flatMap { evse ->
evse.connectors.map { connector ->
connector to evse.status
Triple(connector, evse.status, evse.evseId)
}
}
val nmConnectors = mutableMapOf<Long, Pair<Double, String>>()
val nmStatus = mutableMapOf<Long, ChargepointStatus>()
connectorStatus.forEach { (connector, statusStr) ->
val nmEvseId = mutableMapOf<Long, String>()
connectorStatus.forEach { (connector, statusStr, evseId) ->
val id = connector.uid
val power = connector.electricalProperties.getPower()
val type = when (connector.connectorType.toLowerCase(Locale.ROOT)) {
@@ -161,15 +162,20 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
}
nmConnectors.put(id, power to type)
nmStatus.put(id, status)
evseId?.let { nmEvseId[id] = it }
}
val match = matchChargepoints(nmConnectors, location.chargepointsMerged)
val chargepointStatus = match.mapValues { entry ->
entry.value.map { nmStatus[it]!! }
}
val evseIds = if (nmEvseId.size == nmStatus.size) match.mapValues { entry ->
entry.value.map { nmEvseId[it]!! }
} else null
return ChargeLocationStatus(
chargepointStatus,
"NewMotion"
"NewMotion",
evseIds
)
}

View File

@@ -112,18 +112,47 @@ interface ChargepriceApi {
else -> throw IllegalArgumentException()
}
/**
* Checks if a charger is supported by Chargeprice.
*
* This function just applies some heuristics on the charger's data without making API
* calls. If it returns true, that is not a guarantee that Chargeprice will have information
* on this charger. But if it is false, it is pretty unlikely that Chargeprice will have
* useful data, so we do not show the price comparison button in this case.
*/
@JvmStatic
fun isCountrySupported(country: String, dataSource: String): Boolean = when (dataSource) {
// list of countries updated 2021/08/24
"goingelectric" -> country in listOf(
"Deutschland",
"Österreich",
"Schweiz",
"Frankreich",
"Belgien",
"Niederlande",
"Luxemburg",
"Dänemark",
fun isChargerSupported(charger: ChargeLocation): Boolean {
val dataSourceSupported = charger.dataSource in listOf("goingelectric", "openchargemap")
val countrySupported =
charger.chargepriceData?.country?.let { isCountrySupported(it, charger.dataSource) }
?: false
val networkSupported = charger.chargepriceData?.network?.let {
if (charger.dataSource == "openchargemap") {
it !in listOf(
"1", // unknown operator
"44", // private residence/individual
"45" // business owner at location
)
} else {
true
}
} ?: false
val powerAvailable = charger.chargepoints.all { it.hasKnownPower() }
return dataSourceSupported && countrySupported && networkSupported && powerAvailable
}
private fun isCountrySupported(country: String, dataSource: String): Boolean =
when (dataSource) {
"goingelectric" -> country in listOf(
// list of countries according to Chargeprice.app, 2021/08/24
"Deutschland",
"Österreich",
"Schweiz",
"Frankreich",
"Belgien",
"Niederlande",
"Luxemburg",
"Dänemark",
"Norwegen",
"Schweden",
"Slowenien",
@@ -133,9 +162,28 @@ interface ChargepriceApi {
"Italien",
"Spanien",
"Großbritannien",
"Irland"
"Irland",
// additional countries found 2022/09/17, https://github.com/johan12345/EVMap/issues/234
"Finnland",
"Lettland",
"Litauen",
"Estland",
"Liechtenstein",
"Rumänien",
"Slowakei",
"Slowenien",
"Polen",
"Serbien",
"Bulgarien",
"Kosovo",
"Montenegro",
"Albanien",
"Griechenland",
"Portugal",
"Island"
)
"openchargemap" -> country in listOf(
// list of countries according to Chargeprice.app, 2021/08/24
"DE",
"AT",
"CH",
@@ -153,7 +201,25 @@ interface ChargepriceApi {
"IT",
"ES",
"GB",
"IE"
"IE",
// additional countries found 2022/09/17, https://github.com/johan12345/EVMap/issues/234
"FI",
"LV",
"LT",
"EE",
"LI",
"RO",
"SK",
"SI",
"PL",
"RS",
"BG",
"XK",
"ME",
"AL",
"GR",
"PT",
"IS"
)
else -> false
}

View File

@@ -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?

View File

@@ -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()
}

View File

@@ -0,0 +1,87 @@
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
interface FronyxApi {
@GET("predictions/evse-id/{evseId}")
suspend fun getPredictionsForEvseId(
@Path("evseId") evseId: String,
@Query("timeframe") timeframe: Int? = null
): 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
): FronyxApi {
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(FronyxApi::class.java)
}
/**
* 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
}
}
}

View File

@@ -0,0 +1,20 @@
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>
)
@JsonClass(generateAdapter = true)
data class FronyxPrediction(
val timestamp: ZonedDateTime,
val status: FronyxStatus
)
enum class FronyxStatus {
AVAILABLE, UNAVAILABLE
}

View File

@@ -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,

View File

@@ -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(",")

View File

@@ -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) {
chargepriceAdapter.submitList(it.data)
}
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

View File

@@ -27,7 +27,7 @@ class DataSourceSelectDialog : MaterialDialogFragment() {
}
}
override fun onCreateView(
override fun createView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?

View File

@@ -56,8 +56,7 @@ import com.google.android.material.transition.MaterialContainerTransform
import com.google.android.material.transition.MaterialFadeThrough
import com.google.android.material.transition.MaterialSharedAxis
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.*
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
import com.stfalcon.imageviewer.StfalconImageViewer
import io.michaelrocks.bimap.HashBiMap
@@ -72,7 +71,6 @@ import net.vonforst.evmap.adapter.ConnectorAdapter
import net.vonforst.evmap.adapter.DetailsAdapter
import net.vonforst.evmap.adapter.GalleryAdapter
import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.autocomplete.ApiUnavailableException
import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.bold
@@ -132,7 +130,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val state = bottomSheetBehavior.state
if (state != STATE_COLLAPSED && state != STATE_HIDDEN) {
bottomSheetBehavior.state = STATE_COLLAPSED
if (bottomSheetCollapsible) {
bottomSheetBehavior.state = STATE_COLLAPSED
} else {
vm.chargerSparse.value = null
}
} else if (state == STATE_COLLAPSED) {
vm.chargerSparse.value = null
} else if (state == STATE_HIDDEN) {
@@ -238,6 +240,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
return binding.root
}
val bottomSheetCollapsible
get() = resources.getBoolean(R.bool.bottom_sheet_collapsible)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
@@ -253,6 +258,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
binding.detailView.topPart.doOnNextLayout {
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom
}
bottomSheetBehavior.isCollapsible = bottomSheetCollapsible
setupObservers()
setupClickListeners()
@@ -370,12 +376,25 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
null, extras
)
}
binding.detailView.imgPredictionSource.setOnClickListener {
(activity as? MapsActivity)?.openUrl(getString(R.string.fronyx_url))
}
binding.detailView.btnPredictionHelp.setOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setMessage(getString(R.string.prediction_help))
.setPositiveButton(R.string.ok) { _, _ -> }
.show()
}
binding.detailView.topPart.setOnClickListener {
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
}
setupSearchAutocomplete()
binding.detailAppBar.toolbar.setNavigationOnClickListener {
bottomSheetBehavior.state = STATE_COLLAPSED
if (bottomSheetCollapsible) {
bottomSheetBehavior.state = STATE_COLLAPSED
} else {
vm.chargerSparse.value = null
}
}
binding.detailAppBar.toolbar.setOnMenuItemClickListener {
when (it.itemId) {
@@ -394,7 +413,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val charger = vm.charger.value?.data
if (charger?.editUrl != null) {
(activity as? MapsActivity)?.openUrl(charger.editUrl)
if (vm.apiType == GoingElectricApiWrapper::class.java) {
if (vm.apiId.value == "going_electric") {
// instructions specific to GoingElectric
Toast.makeText(
requireContext(),
@@ -554,7 +573,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
vm.chargerSparse.observe(viewLifecycleOwner, Observer {
if (it != null) {
if (vm.bottomSheetState.value != BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT) {
bottomSheetBehavior.state = STATE_COLLAPSED
bottomSheetBehavior.state =
if (bottomSheetCollapsible) STATE_COLLAPSED else STATE_ANCHOR_POINT
}
removeSearchFocus()
binding.fabDirections.show()
@@ -970,7 +990,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
lifecycleScope.launch {
val address = withContext(Dispatchers.IO) {
try {
Geocoder(requireContext()).getFromLocationName(locationName, 1).getOrNull(0)
Geocoder(requireContext()).getFromLocationName(locationName, 1)
?.getOrNull(0)
} catch (e: IOException) {
null
}

View File

@@ -39,7 +39,7 @@ class MultiSelectDialog : MaterialDialogFragment() {
private lateinit var items: List<MultiSelectItem>
private lateinit var binding: DialogMultiSelectBinding
override fun onCreateView(
override fun createView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?

View File

@@ -8,6 +8,7 @@ import androidx.navigation.ui.setupWithNavController
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialFadeThrough
import com.google.android.material.transition.MaterialSharedAxis
import com.mikepenz.aboutlibraries.LibsBuilder
@@ -44,6 +45,14 @@ class AboutFragment : PreferenceFragmentCompat() {
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
"contributors" -> {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.about_contributors)
.setMessage(getString(R.string.about_contributors_text) + "\n\n" + getString(R.string.about_contributors_list))
.setPositiveButton(R.string.ok) { _, _ -> }
.show()
true
}
"github_link" -> {
(activity as? MapsActivity)?.openUrl(getString(R.string.github_link))
true

View File

@@ -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)
}
}

View File

@@ -13,7 +13,7 @@ import net.vonforst.evmap.ui.MaterialDialogFragment
class OpensourceDonationsDialogFragment : MaterialDialogFragment() {
private lateinit var binding: DialogOpensourceDonationsBinding
override fun onCreateView(
override fun createView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?

View File

@@ -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()
}

View File

@@ -374,7 +374,21 @@ data class Address(
val street: String?
) : Parcelable {
override fun toString(): String {
return "${street ?: ""}, ${postcode ?: ""} ${city ?: ""}"
// TODO: the order here follows a German-style format (i.e. street, postcode city).
// in principle this should be country-dependent (e.g. UK has postcode after city)
return buildString {
street?.let {
append(it)
append(", ")
}
postcode?.let {
append(it)
append(" ")
}
city?.let {
append(it)
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -1,31 +1,28 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.liveData
import androidx.room.*
import net.vonforst.evmap.await
import net.vonforst.evmap.model.*
@Dao
abstract class FilterValueDao {
@Query("SELECT * FROM booleanfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract fun getBooleanFilterValues(
protected abstract suspend fun getBooleanFilterValues(
profile: Long,
dataSource: String
): LiveData<List<BooleanFilterValue>>
): List<BooleanFilterValue>
@Query("SELECT * FROM multiplechoicefiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract fun getMultipleChoiceFilterValues(
protected abstract suspend fun getMultipleChoiceFilterValues(
profile: Long,
dataSource: String
): LiveData<List<MultipleChoiceFilterValue>>
): List<MultipleChoiceFilterValue>
@Query("SELECT * FROM sliderfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
protected abstract fun getSliderFilterValues(
protected abstract suspend fun getSliderFilterValues(
profile: Long,
dataSource: String
): LiveData<List<SliderFilterValue>>
): List<SliderFilterValue>
@Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract suspend fun insert(vararg values: BooleanFilterValue)
@@ -54,27 +51,23 @@ abstract class FilterValueDao {
dataSource: String
)
open fun getFilterValues(filterStatus: Long, dataSource: String): LiveData<List<FilterValue>> =
open suspend fun getFilterValuesAsync(
filterStatus: Long,
dataSource: String
): List<FilterValue> =
if (filterStatus == FILTERS_DISABLED || filterStatus == FILTERS_FAVORITES) {
MutableLiveData(emptyList())
emptyList()
} else {
MediatorLiveData<List<FilterValue>>().apply {
val sources = listOf(
getBooleanFilterValues(filterStatus, dataSource),
getMultipleChoiceFilterValues(filterStatus, dataSource),
getBooleanFilterValues(filterStatus, dataSource) +
getMultipleChoiceFilterValues(filterStatus, dataSource) +
getSliderFilterValues(filterStatus, dataSource)
)
for (source in sources) {
addSource(source) {
val values = sources.map { it.value }
if (values.all { it != null }) {
value = values.filterNotNull().flatten()
}
}
}
}
}
open fun getFilterValues(filterStatus: Long, dataSource: String) = liveData {
emit(null)
emit(getFilterValuesAsync(filterStatus, dataSource))
}
@Transaction
open suspend fun insert(vararg values: FilterValue) {
values.forEach {
@@ -98,7 +91,7 @@ abstract class FilterValueDao {
if (filterStatus == FILTERS_CUSTOM) return
deleteFilterValuesForProfile(FILTERS_CUSTOM, dataSource)
val values = getFilterValues(filterStatus, dataSource).await().onEach {
val values = getFilterValuesAsync(filterStatus, dataSource).onEach {
it.profile = FILTERS_CUSTOM
}
insert(*values.toTypedArray())

View File

@@ -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

View File

@@ -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

View File

@@ -75,8 +75,15 @@ class PreferenceDataSource(val context: Context) {
}
val language: String
get() = sp.getString("language", "default")!!
/**
* Sets app language. Will be removed and set to null with the next update because storage is
* handled by AppCompat.
*/
var language: String?
get() = sp.getString("language", null)
set(lang) {
sp.edit().putString("language", lang).apply()
}
val darkmode: String
get() = sp.getString("darkmode", "default")!!
@@ -239,4 +246,13 @@ class PreferenceDataSource(val context: Context) {
set(value) {
sp.edit().putString("place_search_result_android_auto_name", value).apply()
}
var showChargersAheadAndroidAuto: Boolean
get() = sp.getBoolean("show_chargers_ahead_android_auto", false)
set(value) {
sp.edit().putBoolean("show_chargers_ahead_android_auto", value).apply()
}
val predictionEnabled: Boolean
get() = sp.getBoolean("prediction_enabled", true)
}

View File

@@ -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)

View File

@@ -0,0 +1,352 @@
package net.vonforst.evmap.ui
import android.content.Context
import android.graphics.*
import android.text.Layout
import android.text.SpannableString
import android.text.StaticLayout
import android.text.TextPaint
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import net.vonforst.evmap.R
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.*
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val dp = context.resources.displayMetrics.density
private val sp = context.resources.displayMetrics.scaledDensity
var zeroHeight = 4 * dp
var barWidth = (16 * dp).roundToInt()
var barMargin = (2 * dp).roundToInt()
var legendWidth = 12 * dp
var legendLineLength = 4 * dp
var legendLineWidth = 1 * dp
var dashLength = 4 * dp
var bubbleTextSize = (12 * sp).roundToInt()
var bubblePadding = (6 * dp).roundToInt()
var selectedBar: Int = 0
var bubbleStrokeWidth = 1 * dp
var barDrawable =
AppCompatResources.getDrawable(context, R.drawable.bar_graph)!!
var colorAvailable = ContextCompat.getColor(context, R.color.available)
var colorUnavailable = ContextCompat.getColor(context, R.color.unavailable)
var data: Map<ZonedDateTime, Int>? = null
set(value) {
field = value
invalidate()
}
var maxValue: Int? = null
set(value) {
field = value
invalidate()
}
var activeAlpha = 0.87f
var inactiveAlpha = 0.60f
private val legendPaint = Paint().apply {
val ta = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorControlNormal))
color = ta.getColor(0, 0)
strokeWidth = legendLineWidth
textSize = legendWidth - legendLineLength
}
private val legendDashedPaint = Paint().apply {
set(legendPaint)
alpha = (inactiveAlpha * 255).roundToInt()
style = Paint.Style.STROKE
pathEffect = DashPathEffect(floatArrayOf(dashLength, dashLength), 0f)
strokeWidth = 1f
}
private val bubblePaint = Paint().apply {
set(legendPaint)
alpha = (inactiveAlpha * 255).roundToInt()
style = Paint.Style.STROKE
strokeWidth = bubbleStrokeWidth
}
private val bubbleTextPaint = TextPaint().apply {
set(legendPaint)
textSize = bubbleTextSize.toFloat()
}
private var graphBounds: Rect? = null
private var bubbleBounds: Rect? = null
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
val bottom = (paddingBottom + legendWidth).roundToInt()
val left = (paddingLeft + legendWidth).roundToInt()
val right = (paddingRight + legendWidth).roundToInt()
val top = (paddingTop + bubbleStrokeWidth / 2).roundToInt()
val bubbleTextHeight = bubbleTextPaint.fontMetrics.run { descent - ascent }
val bubbleHeight = (bubbleTextHeight + 3 * bubblePadding).roundToInt()
val bubbleLeft = (paddingLeft + bubbleStrokeWidth / 2).roundToInt()
val bubbleRight = (paddingRight + bubbleStrokeWidth / 2).roundToInt()
graphBounds = Rect(left, top + bubbleHeight, w - right, h - bottom)
bubbleBounds = Rect(bubbleLeft, top, w - bubbleRight, top + bubbleHeight)
}
private val timeFormat = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
override fun onDraw(canvas: Canvas) {
if (isInEditMode && data == null) {
// show sample data
val now = ZonedDateTime.now().run {
val minutesRound = ((minute / 15) + 1) * 15
plusMinutes((minutesRound - minute).toLong())
}
data = (0..20).associate {
now.plusMinutes(15L * it) to (Math.random() * 8).roundToInt()
}
maxValue = 8
}
val data = data?.toSortedMap() ?: return
if (data.isEmpty()) return
val maxValue = maxValue ?: data.maxOf { it.value }
drawGraph(canvas, data, maxValue)
drawBubble(canvas, data, maxValue)
}
private fun drawGraph(
canvas: Canvas,
data: SortedMap<ZonedDateTime, Int>,
maxValue: Int
) {
val graphBounds = graphBounds ?: return
canvas.apply {
drawLine(
graphBounds.left.toFloat(),
graphBounds.top.toFloat(),
graphBounds.right.toFloat(),
graphBounds.top.toFloat(),
legendDashedPaint
)
legendPaint.textAlign = Paint.Align.CENTER
data.entries.forEachIndexed { i, (t, v) ->
val height =
zeroHeight + (graphBounds.height() - zeroHeight) * v.toFloat() / maxValue
val left = graphBounds.left + (barWidth + barMargin) * i
if (left + barWidth > graphBounds.right) return@forEachIndexed
barDrawable.setBounds(
left,
graphBounds.bottom - height.roundToInt(),
left + barWidth,
graphBounds.bottom
)
barDrawable.alpha =
((if (i == selectedBar) activeAlpha else inactiveAlpha) * 255).roundToInt()
barDrawable.setTint(getColor(v, maxValue))
barDrawable.draw(canvas)
val center = left.toFloat() + barWidth / 2
if (t.minute == 0) {
drawLine(
center, graphBounds.bottom.toFloat(),
center, graphBounds.bottom + legendLineLength, legendPaint
)
drawText(
t.withZoneSameInstant(ZoneId.systemDefault()).format(timeFormat),
center, graphBounds.bottom + legendWidth, legendPaint
)
}
if (i == selectedBar) {
drawLine(
center,
graphBounds.bottom - height,
center,
graphBounds.top.toFloat(),
legendDashedPaint
)
}
}
drawLine(
graphBounds.left.toFloat(),
graphBounds.bottom.toFloat(),
graphBounds.right.toFloat(),
graphBounds.bottom.toFloat(),
legendPaint
)
drawLine(
graphBounds.left.toFloat(),
graphBounds.bottom.toFloat(),
graphBounds.right.toFloat(),
graphBounds.bottom.toFloat(),
legendPaint
)
legendPaint.textAlign = Paint.Align.LEFT
drawText(
this@BarGraphView.maxValue.toString(),
graphBounds.right.toFloat() + legendLineLength,
graphBounds.top + (legendWidth - legendLineLength) / 3,
legendPaint
)
}
}
private fun getColor(v: Int, maxValue: Int) =
if (v < maxValue) colorAvailable else colorUnavailable
private fun drawBubble(canvas: Canvas, data: SortedMap<ZonedDateTime, Int>, maxValue: Int) {
val bubbleBounds = bubbleBounds ?: return
val graphBounds = graphBounds ?: return
val data = data.toList()
if (data.size <= selectedBar) return
canvas.apply {
val center = graphBounds.left + selectedBar * (barWidth + barMargin) + barWidth * 0.5f
val (t, v) = data[selectedBar]
val tformat = context.getString(
R.string.prediction_time_colon,
t.withZoneSameInstant(ZoneId.systemDefault()).format(timeFormat)
)
val availableformat = context.resources.getQuantityString(
R.plurals.prediction_number_available,
maxValue - v,
maxValue - v,
maxValue
)
val text = SpannableString("$tformat $availableformat").apply {
setSpan(
ForegroundColorSpan(getColor(v, maxValue)),
0,
tformat.length + 1,
SpannableString.SPAN_INCLUSIVE_INCLUSIVE
)
setSpan(
StyleSpan(Typeface.BOLD),
0,
tformat.length + 1,
SpannableString.SPAN_INCLUSIVE_INCLUSIVE
)
}
val bubbleTextWidth = StaticLayout.getDesiredWidth(text, bubbleTextPaint)
val bubbleWidth = bubbleTextWidth + 2 * bubblePadding
val bubbleLeft = max(
min(center - bubbleWidth / 2, bubbleBounds.right - bubbleWidth),
bubbleBounds.left.toFloat()
)
val bubblePath = generateBubblePath(
center,
bubbleBounds.bottom.toFloat(),
bubbleLeft,
bubbleBounds.top.toFloat(),
bubbleLeft + bubbleWidth,
(bubbleBounds.bottom - bubblePadding).toFloat(),
bubblePadding.toFloat()
)
drawPath(bubblePath, bubblePaint)
val layout = StaticLayout(
text,
bubbleTextPaint,
ceil(bubbleTextWidth).toInt(),
Layout.Alignment.ALIGN_NORMAL,
1f,
0f,
false
)
canvas.save()
canvas.translate(bubbleLeft + bubblePadding, bubbleBounds.top + bubblePadding.toFloat())
layout.draw(canvas)
canvas.restore()
//drawText(text, 0, text.length, bubbleLeft + bubblePadding, bubbleBounds.top + 2f * bubblePadding, bubbleTextPaint)
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
val graphBounds = graphBounds ?: return super.onTouchEvent(event)
val x = event.x.roundToInt()
val y = event.y.roundToInt()
if (graphBounds.contains(x, y) && event.action == MotionEvent.ACTION_DOWN) {
parent.requestDisallowInterceptTouchEvent(true)
updateSelectedBar(x)
return true
} else if (event.action == MotionEvent.ACTION_MOVE && x > graphBounds.left && y < graphBounds.right) {
updateSelectedBar(x)
return true
} else if (event.action == MotionEvent.ACTION_UP) {
parent.requestDisallowInterceptTouchEvent(false)
return true
}
return super.onTouchEvent(event)
}
private fun updateSelectedBar(x: Int) {
val graphBounds = graphBounds ?: return
val bar = (x - graphBounds.left) / (barWidth + barMargin)
if (bar != selectedBar) {
selectedBar = bar
invalidate()
}
}
/**
* Generates a path that represents a "speech bubble" with tip position at tipX, tipY,
* bubble bounds left, top, right bottom and corner radius cornerRadius.
*/
private fun generateBubblePath(
tipX: Float,
tipY: Float,
left: Float,
top: Float,
right: Float,
bottom: Float,
cornerRadius: Float
): Path {
val tipWidth = tipY - bottom
return Path().apply {
moveTo(tipX, tipY)
lineTo(min(tipX + tipWidth, right - cornerRadius), bottom)
lineTo(right - cornerRadius, bottom)
arcTo(
right - cornerRadius * 2,
bottom - cornerRadius * 2,
right,
bottom,
90f,
-90f,
false
)
lineTo(right, top + cornerRadius)
arcTo(right - cornerRadius * 2, top, right, top + cornerRadius * 2, 0f, -90f, false)
lineTo(left + cornerRadius, top)
arcTo(left, top, left + cornerRadius * 2, top + cornerRadius * 2, 270f, -90f, false)
lineTo(left, bottom - cornerRadius)
arcTo(
left,
bottom - cornerRadius * 2,
left + cornerRadius * 2,
bottom,
180f,
-90f,
false
)
lineTo(max(tipX - tipWidth, left + cornerRadius), bottom)
close()
}
}
}

View File

@@ -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()
}
}

View File

@@ -69,7 +69,7 @@ abstract class MaterialDialogFragment : AppCompatDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = MaterialAlertDialogBuilder(requireContext(), theme).apply {
dialogView =
onCreateView(LayoutInflater.from(requireContext()), null, savedInstanceState)!!
createView(LayoutInflater.from(requireContext()), null, savedInstanceState)
setView(dialogView)
}.create()
@@ -77,6 +77,12 @@ abstract class MaterialDialogFragment : AppCompatDialogFragment() {
return dialog
}
abstract fun createView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View
abstract fun initView(view: View, savedInstanceState: Bundle?)
override fun getView(): View {

View File

@@ -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
}
)
}

View File

@@ -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))
}
}
}

View File

@@ -47,6 +47,25 @@ fun distanceBetween(
}
fun bearingBetween(startLat: Double, startLng: Double, endLat: Double, endLng: Double): Double {
val dLon = Math.toRadians(-endLng) - Math.toRadians(-startLng)
val originLat = Math.toRadians(startLat)
val destinationLat = Math.toRadians(endLat)
return Math.toDegrees(
atan2(
sin(dLon) * cos(destinationLat),
cos(originLat) * sin(destinationLat) - sin(originLat) * cos(destinationLat) * cos(dLon)
)
)
}
fun headingDiff(h1: Double, h2: Double): Double {
return (h1 - h2 + 540) % 360 - 180
}
fun getLocationFromIntent(intent: Intent): List<Double>? {
val pos = intent.data?.schemeSpecificPart?.split("?")?.get(0)
var coords = stringToCoords(pos)

View File

@@ -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(

View File

@@ -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)
}

View File

@@ -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() {

View File

@@ -6,30 +6,36 @@ import androidx.lifecycle.*
import com.car2go.maps.AnyMap
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.AvailabilityDetectorException
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.api.fronyx.FronyxApi
import net.vonforst.evmap.api.fronyx.FronyxEvseIdResponse
import net.vonforst.evmap.api.fronyx.FronyxStatus
import net.vonforst.evmap.api.goingelectric.GEChargepoint
import net.vonforst.evmap.api.goingelectric.GEReferenceData
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.openchargemap.OCMConnection
import net.vonforst.evmap.api.openchargemap.OCMReferenceData
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.cluster
import net.vonforst.evmap.utils.distanceBetween
import retrofit2.HttpException
import java.io.IOException
import java.time.ZonedDateTime
@Parcelize
data class MapPosition(val bounds: LatLngBounds, val zoom: Float) : Parcelable
@@ -44,21 +50,43 @@ internal fun getClusterDistance(zoom: Float): Int? {
class MapViewModel(application: Application, private val state: SavedStateHandle) :
AndroidViewModel(application) {
val apiType: Class<ChargepointApi<ReferenceData>>
get() = api.value!!.javaClass
val apiName: String
get() = api.value!!.getName()
private val db = AppDatabase.getInstance(application)
private val prefs = PreferenceDataSource(application)
private val repo = ChargeLocationsRepository(
createApi(prefs.dataSource, application),
viewModelScope,
db,
prefs
)
private var db = AppDatabase.getInstance(application)
private var prefs = PreferenceDataSource(application)
private var api = MutableLiveData<ChargepointApi<ReferenceData>>().apply {
value = createApi(prefs.dataSource, application)
val apiId = repo.api.map { it.id }
init {
// necessary so that apiId is updated
apiId.observeForever { }
}
val apiName = repo.api.map { it.name }
val bottomSheetState: MutableLiveData<Int> by lazy {
state.getLiveData("bottomSheetState")
}
val bottomSheetExpanded = MediatorLiveData<Boolean>().apply {
addSource(bottomSheetState) {
when (it) {
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED,
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN -> {
value = false
}
BottomSheetBehaviorGoogleMapsLike.STATE_EXPANDED,
BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT -> {
value = true
}
}
}
}.distinctUntilChanged()
val mapPosition: MutableLiveData<MapPosition> by lazy {
state.getLiveData("mapPosition")
}
@@ -71,47 +99,28 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}
}
private val filterValues: LiveData<List<FilterValue>> =
private val filterValues: LiveData<List<FilterValue>?> = repo.api.switchMap {
db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
private val referenceData =
Transformations.switchMap(api) { it.getReferenceData(viewModelScope, application) }
private val filters = Transformations.map(referenceData) {
api.value!!.getFilters(
it,
application.stringProvider()
)
}
private val filters = repo.getFilters(application.stringProvider())
private val filtersWithValue: LiveData<FilterValues> by lazy {
private val filtersWithValue: LiveData<FilterValues?> by lazy {
filtersWithValue(filters, filterValues)
}
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
val filterProfiles: LiveData<List<FilterProfile>> = repo.api.switchMap {
db.filterProfileDao().getProfiles(prefs.dataSource)
}
val chargeCardMap: LiveData<Map<Long, ChargeCard>> by lazy {
MediatorLiveData<Map<Long, ChargeCard>>().apply {
value = null
addSource(referenceData) { data ->
value = if (data is GEReferenceData) {
data.chargecards.map {
it.id to it.convert()
}.toMap()
} else {
null
}
}
}
}
val chargeCardMap = repo.chargeCardMap
val filtersCount: LiveData<Int> by lazy {
MediatorLiveData<Int>().apply {
value = 0
addSource(filtersWithValue) { filtersWithValue ->
value = filtersWithValue.count {
value = filtersWithValue?.count {
!it.value.hasSameValueAs(it.filter.defaultValue())
}
} ?: 0
}
}
}
@@ -121,7 +130,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
value = Resource.loading(emptyList())
// this is not automatically updated with mapPosition, as we only want to update
// when map is idle.
listOf(filtersWithValue, referenceData).forEach {
listOf(filtersWithValue, repo.api).forEach {
addSource(it) {
reloadChargepoints()
}
@@ -141,28 +150,17 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
val chargerSparse: MutableLiveData<ChargeLocation> by lazy {
state.getLiveData("chargerSparse")
}
val chargerDetails: MediatorLiveData<Resource<ChargeLocation>> by lazy {
MediatorLiveData<Resource<ChargeLocation>>().apply {
value = state["chargerDetails"]
listOf(chargerSparse, referenceData).forEach {
addSource(it) { _ ->
val charger = chargerSparse.value
val refData = referenceData.value
if (charger != null && refData != null) {
if (charger.id != value?.data?.id) {
loadChargerDetails(charger, refData)
}
} else {
value = null
}
}
}
observeForever {
// persist data in case fragment gets recreated
state["chargerDetails"] = it
}
val chargerDetails: LiveData<Resource<ChargeLocation>> = chargerSparse.switchMap { charger ->
charger?.id?.let {
repo.getChargepointDetail(it)
}
}.apply {
observeForever { chargerDetail ->
// persist data in case fragment gets recreated
state["chargerDetails"] = chargerDetail
}
}
val charger: MediatorLiveData<Resource<ChargeLocation>> by lazy {
MediatorLiveData<Resource<ChargeLocation>>().apply {
addSource(chargerDetails) {
@@ -196,15 +194,15 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
val location: MutableLiveData<LatLng> by lazy {
MutableLiveData<LatLng>()
}
val availability: MediatorLiveData<Resource<ChargeLocationStatus>> by lazy {
MediatorLiveData<Resource<ChargeLocationStatus>>().apply {
addSource(chargerSparse) { charger ->
if (charger != null) {
viewModelScope.launch {
loadAvailability(charger)
private val triggerAvailabilityRefresh = MutableLiveData<Boolean>(true)
val availability: LiveData<Resource<ChargeLocationStatus>> by lazy {
chargerSparse.switchMap { charger ->
charger?.let {
triggerAvailabilityRefresh.switchMap {
liveData {
emit(Resource.loading(null))
emit(getAvailability(charger))
}
} else {
value = null
}
}
}
@@ -230,6 +228,124 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
addSource(filteredMinPower, callback)
}
}
val predictionApi = FronyxApi.create(application.getString(R.string.fronyx_key))
val prediction: LiveData<Resource<List<FronyxEvseIdResponse>>> by lazy {
availability.switchMap { av ->
if (!prefs.predictionEnabled) return@switchMap null
av.data?.evseIds?.let { evseIds ->
liveData {
emit(Resource.loading(null))
val charger = charger.value?.data ?: return@liveData
val allEvseIds =
evseIds.filterKeys {
FronyxApi.isChargepointSupported(charger, it) &&
filteredConnectors.value?.let { filtered ->
equivalentPlugTypes(
it.type
).any { filtered.contains(it) }
} ?: true
}.flatMap { it.value }
try {
val result = allEvseIds.map {
predictionApi.getPredictionsForEvseId(it)
}
emit(Resource.success(result))
println(result)
} catch (e: IOException) {
emit(Resource.error(e.message, null))
e.printStackTrace()
} catch (e: HttpException) {
emit(Resource.error(e.message, null))
e.printStackTrace()
} catch (e: AvailabilityDetectorException) {
emit(Resource.error(e.message, null))
e.printStackTrace()
}
}
} ?: liveData { emit(Resource.success(null)) }
}
}
val predictionGraph: LiveData<Map<ZonedDateTime, Int>?> by lazy {
prediction.map {
it.data?.let { responses ->
if (responses.isEmpty()) {
null
} else {
val evseIds = responses.map { it.evseId }
val groupByTimestamp = responses.flatMap { response ->
response.predictions.map {
Triple(
it.timestamp,
response.evseId,
it.status
)
}
}
.groupBy { it.first } // group by timestamp
.mapValues { it.value.map { it.second to it.third } } // only keep EVSEID and status
.filterValues { it.map { it.first } == evseIds } // remove values where status is not given for all EVSEs
.filterKeys { it > ZonedDateTime.now() } // only show predictions in the future
groupByTimestamp.mapValues {
it.value.count {
it.second == FronyxStatus.UNAVAILABLE
}
}.ifEmpty { null }
}
}
}
}
private val predictedChargepoints = charger.map {
it.data?.let { charger ->
charger.chargepoints.filter {
FronyxApi.isChargepointSupported(charger, it) &&
filteredConnectors.value?.let { filtered ->
equivalentPlugTypes(it.type).any {
filtered.contains(
it
)
}
} ?: true
}
}
}
val predictionMaxValue: LiveData<Int> by lazy {
predictedChargepoints.map {
it?.sumOf { it.count } ?: 0
}
}
val predictionDescription: LiveData<String?> by lazy {
predictedChargepoints.map { predictedChargepoints ->
if (predictedChargepoints == null) return@map null
val allChargepoints = charger.value?.data?.chargepoints ?: return@map null
val predictedChargepointTypes = predictedChargepoints.map { it.type }.distinct()
if (allChargepoints == predictedChargepoints) {
null
} else if (predictedChargepointTypes.size == 1) {
application.getString(
R.string.prediction_only,
nameForPlugType(application.stringProvider(), predictedChargepointTypes[0])
)
} else {
application.getString(
R.string.prediction_only,
application.getString(R.string.prediction_dc_plugs_only)
)
}
}
}
val myLocationEnabled: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>()
}
@@ -267,7 +383,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 +425,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 +452,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 +484,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
}
})
}
}
}

View File

@@ -4,10 +4,7 @@ import android.os.Parcelable
import androidx.annotation.MainThread
import androidx.annotation.Nullable
import androidx.lifecycle.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
import java.util.concurrent.atomic.AtomicBoolean
@@ -107,4 +104,41 @@ fun <T> throttleLatest(
waitingParam = param
}
}
}
public suspend fun <T> LiveData<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
val observer = object : Observer<T> {
override fun onChanged(value: T?) {
if (value == null) return
removeObserver(this)
continuation.resume(value, null)
}
}
observeForever(observer)
continuation.invokeOnCancellation {
removeObserver(observer)
}
}
}
public suspend fun <T> LiveData<Resource<T>>.awaitFinished(): Resource<T> {
return suspendCancellableCoroutine { continuation ->
val observer = object : Observer<Resource<T>> {
override fun onChanged(value: Resource<T>) {
if (value.status != Status.LOADING) {
removeObserver(this)
continuation.resume(value, null)
}
}
}
observeForever(observer)
continuation.invokeOnCancellation {
removeObserver(observer)
}
}
}

View 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>

View 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>

View File

@@ -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" />

View 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>

View 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>

View File

@@ -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&lt;ChargeLocationStatus&gt;" />
<variable
name="predictionGraph"
type="Map&lt;ZonedDateTime, Integer&gt;" />
<variable
name="predictionMaxValue"
type="Integer" />
<variable
name="predictionDescription"
type="String" />
<variable
name="filteredAvailability"
type="Resource&lt;ChargeLocationStatus&gt;" />
@@ -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 &amp;&amp; !expanded}"
app:invisibleUnless="@{filteredAvailability.data != null &amp;&amp; !expanded}"
app:invisibleUnlessAnimated="@{filteredAvailability.data != null &amp;&amp; !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 &amp;&amp; charger.data.chargepriceData != null &amp;&amp; charger.data.chargepriceData.country != null &amp;&amp; ChargepriceApi.isCountrySupported(charger.data.chargepriceData.country, charger.data.dataSource)}"
app:goneUnless="@{charger.data != null &amp;&amp; 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 &amp;&amp; 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>

View File

@@ -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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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>

View 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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>

View 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>

View File

@@ -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 &amp;&amp; vm.bottomSheetState != BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN}"
app:expanded="@{vm.bottomSheetExpanded}"
app:apiName="@{vm.apiName}" />
</androidx.core.widget.NestedScrollView>
@@ -211,6 +216,7 @@
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_directions"
android:translationX="@dimen/directions_fab_translationx"
app:layout_anchor="@id/bottom_sheet"
app:layout_anchorGravity="top|right|end"
app:layout_behavior="@string/ScrollAwareFABBehavior"
@@ -218,7 +224,7 @@
<com.mahc.custombottomsheetbehavior.MergedAppBarLayout
android:id="@+id/detail_app_bar"
android:layout_width="match_parent"
android:layout_width="@dimen/map_toolbar_width"
android:layout_height="wrap_content"
app:layout_behavior="@string/MergedAppBarLayoutBehavior"
android:theme="@style/ThemeOverlay.Material3.Dark.ActionBar" />
@@ -237,7 +243,7 @@
app:borderWidth="0dp"
app:fabSize="mini"
app:srcCompat="@drawable/ic_layers"
app:layout_behavior="net.vonforst.evmap.ui.HideOnExpandFabBehavior"
app:layout_behavior="@string/hide_on_scroll_fab_behavior"
android:theme="@style/NoElevationOverlay" />
<androidx.cardview.widget.CardView

View File

@@ -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"

View File

@@ -39,7 +39,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="38dp"
android:layout_marginTop="38dp"
android:text="@{String.format(&quot;× %d&quot;, item.chargepoint.count)}"
android:text="@{String.format(&quot;\u00D7 %d&quot;, item.chargepoint.count)}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintStart_toStartOf="@+id/imageView"
app:layout_constraintTop_toTopOf="@+id/imageView"

View File

@@ -3,8 +3,8 @@
<string name="app_name">EVMap</string>
<string name="title_activity_maps">EVMap</string>
<string name="connectors">Anschlüsse</string>
<string name="no_maps_app_found">Keine Navigations-App gefunden</string>
<string name="no_browser_app_found">Kein Webbrowser gefunden</string>
<string name="no_maps_app_found">Bitte installiere eine Navigations-App</string>
<string name="no_browser_app_found">Bitte installiere einen Webbrowser</string>
<string name="address">Adresse</string>
<string name="operator">Betreiber</string>
<string name="network">Verbund</string>
@@ -23,8 +23,8 @@
<string name="charging_paid">Kostenpflichtig</string>
<string name="parking_free">Kostenlos</string>
<string name="parking_paid">Kostenpflichtig</string>
<string name="amenities">Ladeweile</string>
<string name="general_info">Allgemeine Hinweise</string>
<string name="amenities">Ausstattung</string>
<string name="general_info">Allgemein</string>
<string name="realtime_data_unavailable">Echtzeitstatus nicht verfügbar</string>
<string name="realtime_data_loading">Prüfe Echtzeitstatus…</string>
<string name="realtime_data_source">Quelle Echtzeitdaten (beta): %s</string>
@@ -37,7 +37,7 @@
<string name="about">Über EVMap</string>
<string name="version">Version</string>
<string name="github_link_title">Quellcode</string>
<string name="oss_licenses">Open Source-Lizenzen</string>
<string name="oss_licenses">Lizenzen</string>
<string name="settings">Einstellungen</string>
<string name="settings_ui">Oberfläche</string>
<string name="settings_map">Karte</string>
@@ -45,10 +45,10 @@
<string name="copyright_summary">©20202022 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>
@@ -152,16 +150,16 @@
<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="filterprofiles_empty_state">Du hast keine Filterprofile gespeichert</string>
<string name="welcome_to_evmap">Willkommen bei EVMap</string>
<string name="welcome_1">Finde Ladestationen für Elektroautos in deiner Nähe.</string>
<string name="welcome_1">Finde Ladestationen für Elektroautos in deiner Nähe</string>
<string name="welcome_2_title">Auf die Leistung kommt es an</string>
<string name="welcome_2">Die Farbe einer Ladestation auf der Karte zeigt dir die maximale Ladeleistung.</string>
<string name="welcome_2_detail">Du kannst die Farben im Menü unter “Über EVMap → FAQ” erneut ansehen)</string>
<string name="welcome_2">Die Farbe einer Ladestation zeigt dir die maximale Ladeleistung</string>
<string name="welcome_2_detail">Die Farben kannst du unter “Über EVMap → Häufig gestellte Fragen” erneut ansehen</string>
<string name="donation_dialog_title">Danke, dass du EVMap nutzt!</string>
<string name="donation_dialog_detail">EVMap ist kostenlos und Open Source, ich entwickle es in meiner Freizeit. Über GitHub kann jeder zur Weiterentwicklung der App beitragen. Durch die steigende Beliebtheit der App müssen allerdings auch laufende Kosten, z.B. für den Zugriff auf die Datenquellen, gedeckt werden. Daher freue ich mich auch über Spenden in der App oder über GitHub Sponsors.</string>
<string name="donation_dialog_detail">EVMap ist kostenlos und Open Source. Über GitHub kann jeder zur Weiterentwicklung der App beitragen. Um die laufenden Kosten für den für die Datenquellen zu decken, freue ich mich auch über Spenden in der App oder über GitHub Sponsors.</string>
<string name="chargeprice_donation_dialog_title">Du bist ein richtiger Sparfuchs!</string>
<string name="chargeprice_donation_dialog_detail">Es sieht so aus, als wenn du den Preisvergleich sehr gern nutzt. Für den Zugang zu den Preisinformationen muss der Entwickler von EVMap eine monatliche Gebühr an die Datenquelle Chargeprice.app zahlen. Um diesen Dienst weiter anbieten zu können, würde ich mich sehr über Spenden freuen.</string>
<string name="chargeprice_donation_dialog_detail">Anscheinend nutzt du den Preisvergleich sehr gern. Mit einer Spende für EVMap kannst du helfen, die Kosten für den Datenzugriff zu decken.</string>
<string name="deleted_filterprofile">„%s” gelöscht</string>
<string name="undo">Rückgängig</string>
<string name="rename">Umbenennen</string>
@@ -172,7 +170,7 @@
</plurals>
<string name="navigate">Navigieren</string>
<string name="verified">Verifiziert</string>
<string name="verified_desc">Verifiziert von der %s Community nicht zwangsläufig auch aktuell verfügbar.</string>
<string name="verified_desc">Ladestation wurde mindestens einmal von einem Mitglied der %s Community getestet</string>
<string name="charge_price_format">%1$.2f %2$s</string>
<string name="charge_price_average_format">⌀ %1$.2f %2$s/kWh</string>
<string name="charge_price_kwh_format">%1$.2f %2$s/kWh</string>
@@ -182,27 +180,28 @@
<string name="chargeprice_session_fee">Startgebühr</string>
<string name="chargeprice_per_kwh">pro kWh</string>
<string name="chargeprice_per_minute">pro min</string>
<string name="chargeprice_blocking_fee">Blockiergeb. >%s</string>
<string name="chargeprice_no_tariffs_found">Keine geeigneten Tarife für diese Ladestation bei Chargeprice.app gefunden.</string>
<string name="chargeprice_blocking_fee">Blockiergeb. &gt;%s</string>
<string name="chargeprice_no_tariffs_found">Keine Tarife für diese Ladestation bei Chargeprice.app gefunden</string>
<string name="powered_by_chargeprice">powered by Chargeprice</string>
<string name="chargeprice_base_fee">Fixkosten: %1$.2f %2$s/Monat</string>
<string name="chargeprice_min_spend">Mindestumsatz: %1$.2f %2$s/Monat</string>
<string name="settings_chargeprice">Preisvergleich</string>
<string name="pref_my_vehicle">Meine Fahrzeuge</string>
<string name="pref_chargeprice_no_base_fee">Nur Tarife ohne monatliche Gebühren</string>
<string name="pref_chargeprice_no_base_fee">Tarife mit monatlichen Gebühren ausschließen</string>
<string name="pref_chargeprice_show_provider_customer_tariffs">Exklusive Energiekunden-Tarife anzeigen</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Einige Anbieter bieten für ihre Kunden (z.B. Haushaltsstrom, Gas) günstigere Tarife an</string>
<string name="chargeprice_select_car_first">Bitte wähle zuerst dein Auto in den Einstellungen aus.</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Einige Energieversorger bieten für ihre Kunden spezielle Tarife an</string>
<string name="chargeprice_select_car_first">Bitte wähle zuerst dein Auto in den Einstellungen aus</string>
<string name="chargeprice_battery_range">Laden von %1$.0f%% bis %2$.0f%%</string>
<string name="chargeprice_battery_range_from">Laden von</string>
<string name="chargeprice_battery_range_to">bis</string>
<string name="chargeprice_stats">(%1$.0f kWh, ca. %2$s, ⌀ %3$.0f kW)</string>
<string name="chargeprice_vehicle">Fahrzeug</string>
<string name="edit_on_goingelectric_info">Falls hier nur eine leere Seite erscheint, logge dich bitte zuerst bei GoingElectric.de ein.</string>
<string name="close">schließen</string>
<string name="chargeprice_price_not_available">Preis nicht verfügbar</string>
<string name="edit_on_goingelectric_info">Logge dich zuerst bei GoingElectric.de ein, falls hier nur eine leere Seite erscheint</string>
<string name="close">Schließen</string>
<string name="chargeprice_title">Preise</string>
<string name="chargeprice_connection_error">Preise konnten nicht geladen werden</string>
<string name="chargeprice_no_compatible_connectors">Keiner der Anschlüsse dieser Ladestation ist mit deinem Fahrzeug kompatibel.</string>
<string name="chargeprice_no_compatible_connectors">Kein kompatibler Anschluss an dieser Ladestation</string>
<string name="pref_chargeprice_currency">Währung</string>
<string name="pref_my_tariffs">Meine Tarife</string>
<string name="chargeprice_all_tariffs_selected">alle Tarife ausgewählt</string>
@@ -218,20 +217,20 @@
<item quantity="other">%d Tarife ausgewählt</item>
</plurals>
<string name="unknown_operator">Unbekannter Betreiber</string>
<string name="data_sources_description">EVMap unterstützt verschiedene Datenquellen für Ladestationen. Bitte wähle aus, welche du nutzen möchtest. Du kannst sie später in den Einstellungen der App ändern.</string>
<string name="data_sources_description">Bitte wähle eine Datenquelle für Ladestationen aus. Du kannst sie später in den Einstellungen der App ändern.</string>
<string name="data_source_goingelectric">GoingElectric.de</string>
<string name="data_source_openchargemap">Open Charge Map</string>
<string name="data_source_goingelectric_desc">Sehr gute Abdeckung in Deutschland, Österreich, Schweiz und vielen angrenzenden Ländern. Beschreibungen in Deutsch. Von der Community gepflegt.</string>
<string name="data_source_openchargemap_desc"><![CDATA[Weltweite Abdeckung mit variierender Qualität. Beschreibungen in Englisch oder Landessprache. Von der Community gepflegt & offizielle Verzeichnisse einiger Länder (z.B. Nordamerika, UK, Frankreich, Norwegen).]]></string>
<string name="data_source_goingelectric_desc">Sehr gute Abdeckung in den deutschsprachigen Ländern. Beschreibungen in Deutsch. Von der Community gepflegt.</string>
<string name="data_source_openchargemap_desc"><![CDATA[Weltweite Abdeckung mit variierender Qualität. Beschreibungen in Englisch oder Landessprache. Von der Community gepflegt und offizielle Verzeichnisse einiger Länder (z.B. Nordamerika, UK, Frankreich, Norwegen).]]></string>
<string name="next">weiter</string>
<string name="get_started">Los geht\'s</string>
<string name="got_it">Alles klar</string>
<string name="lets_go">Und los</string>
<string name="crash_report_text">Sorry, anscheinend ist EVMap abgestürzt. Bitte schicke einen Fehlerbericht an den Entwickler.</string>
<string name="crash_report_text">EVMap ist abgestürzt. Bitte schicke einen Fehlerbericht an den Entwickler.</string>
<string name="crash_report_comment_prompt">Du kannst unten noch einen Kommentar hinzufügen:</string>
<string name="powered_by_mapbox">powered by Mapbox</string>
<string name="pref_search_provider">Anbieter für Ortssuche</string>
<string name="pref_search_provider_info"><![CDATA[Die Daten für die Ortssuche, vor allem von Google Maps, sind relativ teuer. Wenn du diese Funktion häufig nutzt, würde ich mich über eine Spende unter \"Über EVMap -> Spenden\" sehr freuen.]]></string>
<string name="pref_search_provider_info">Die Daten für die Ortssuche, vor allem von Google Maps, sind relativ teuer. Über eine Spende unter \"Über EVMap -&gt; Spenden\" würde ich mich sehr freuen.</string>
<string name="github_sponsors">GitHub Sponsors</string>
<string name="donate_desc">Unterstütze die Weiterentwicklung von EVMap mit einer einmaligen Spende</string>
<string name="github_sponsors_desc">Unterstütze EVMap über GitHub Sponsors</string>
@@ -247,10 +246,10 @@
<string name="help">Hilfe</string>
<string name="settings_android_auto">Android Auto</string>
<string name="pref_chargeprice_allow_unbalanced_load">Schieflast erlauben</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary"><![CDATA[Erlaubt das Laden mit >4.5 kW an AC-Stationen für Autos mit 1-phasigem Lader]]></string>
<string name="pref_map_rotate_gestures_enabled">Kartenrotation erlauben</string>
<string name="pref_map_rotate_gestures_on">Karte kann mit Zweifingergeste rotiert werden</string>
<string name="pref_map_rotate_gestures_off">Karte bleibt fest nach Norden ausgerichtet</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Einphasiges Laden mit mehr als 4.5 kW erlauben</string>
<string name="pref_map_rotate_gestures_enabled">Kartenrotation</string>
<string name="pref_map_rotate_gestures_on">Karte mit zwei Fingern rotieren</string>
<string name="pref_map_rotate_gestures_off">Karte immer nach Norden ausrichten</string>
<string name="refresh_live_data">Echtzeitstatus aktualisieren</string>
<string name="autocomplete_connection_error">Vorschläge konnten nicht geladen werden</string>
<string name="pref_language_device_default">Gerätesprache verwenden</string>
@@ -271,4 +270,20 @@
<string name="pref_chargeprice_currency_usd">US-Dollar (USD)</string>
<string name="pref_provider_google_maps">Google Maps</string>
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
</resources>
<string name="about_contributors">Mitwirkende</string>
<string name="about_contributors_text">Dank an alle Mitwirkenden für ihre Beiträge von Code und Übersetzungen für EVMap:</string>
<string name="utilization_prediction">Auslastungsprognose</string>
<string name="prediction_help">Die Prognose basiert auf Faktoren wie Wochentag, Uhrzeit und Nutzung in der Vergangenheit. So kannst du stark ausgelastete Ladesäulen vermeiden. Keine Garantie.</string>
<string name="prediction_time_colon">%s Uhr:</string>
<plurals name="prediction_number_available">
<item quantity="one">%1$d/%2$d verfügbar</item>
<item quantity="other">%1$d/%2$d verfügbar</item>
</plurals>
<string name="pref_prediction_enabled">Auslastungsprognosen anzeigen</string>
<string name="pref_prediction_enabled_summary">für unterstützte Ladestationen\n(momentan nur Schnellader in Deutschland)</string>
<string name="prediction_only">(nur %s)</string>
<string name="prediction_dc_plugs_only">DC-Anschlüsse</string>
<string name="data_source_switched_to">Datenquelle zu %s umgeschaltet</string>
<string name="pref_applink_associate">Unterstützte Links öffnen</string>
<string name="pref_applink_associate_summary">von goingelectric.de und openchargemap.org</string>
</resources>

View File

@@ -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 &gt;%s</string>
<string name="chargeprice_no_tariffs_found">Chargeprice.app n\'a trouvé aucun tarif de recharge compatible avec ce chargeur.</string>
<string name="chargeprice_no_tariffs_found">Aucun tarif de recharge pour ce chargeur sur Chargeprice.app</string>
<string name="pref_chargeprice_show_provider_customer_tariffs">Afficher les tarifs exclusifs aux clients</string>
<string name="chargeprice_battery_range">Charge de %1$.0f%% à %2$.0f%%</string>
<string name="chargeprice_battery_range_from">Charge de</string>
<string name="chargeprice_stats">(%1$.0f kWh, approx. %2$s, ⌀ %3$.0f kW)</string>
<string name="chargeprice_vehicle">Véhicule</string>
<string name="close">fermer</string>
<string name="close">Fermer</string>
<string name="chargeprice_title">Prix</string>
<string name="pref_chargeprice_currency">Devise</string>
<string name="data_source_goingelectric">GoingElectric.de</string>
@@ -105,6 +104,7 @@
<string name="pref_data_source">Source des données</string>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d tarif sélectionné</item>
<item quantity="many">%d tarifs sélectionnés</item>
<item quantity="other">%d tarifs sélectionnés</item>
</plurals>
<string name="data_source_openchargemap">Open Charge Map</string>
@@ -121,8 +121,8 @@
<string name="pref_search_delete_recent">Supprimer les résultats de recherche récents</string>
<string name="settings_android_auto">Android Auto</string>
<string name="pref_chargeprice_allow_unbalanced_load">Permettre une charge déséquilibrée</string>
<string name="pref_map_rotate_gestures_enabled">Activer la rotation de la carte</string>
<string name="pref_map_rotate_gestures_off">La carte reste orientée vers le nord</string>
<string name="pref_map_rotate_gestures_enabled">Rotation de la carte</string>
<string name="pref_map_rotate_gestures_off">Rotation désactivée (nord toujours en haut)</string>
<string name="refresh_live_data">rafraîchir le statut en temps réel</string>
<string name="pref_language_device_default">Utiliser la langue de l\'appareil</string>
<string name="pref_darkmode_device_default">Utiliser le réglage de l\'appareil</string>
@@ -141,22 +141,22 @@
<string name="general_info">Informations générales</string>
<string name="realtime_data_loading">Vérification du statut en temps réel…</string>
<string name="plug_ccs">CCS</string>
<string name="donation_successful">Merci ! ❤️</string>
<string name="donation_failed">Quelque chose s\'est mal passé. 😕</string>
<string name="donation_successful">Merci ❤️</string>
<string name="donation_failed">Quelque chose s\'est mal passé 😕</string>
<string name="category_supermarket">Supermarché</string>
<string name="version">Version</string>
<string name="oss_licenses">Licences Open Source</string>
<string name="oss_licenses">Licences</string>
<string name="realtime_data_source">Source du statut en temps réel (bêta) : %s</string>
<string name="plug_type_2">Type 2</string>
<string name="plug_type_3">Type 3a</string>
<string name="plug_type_3">Type 3A</string>
<string name="plug_cee_rot">CEE Rouge</string>
<string name="all">tous</string>
<string name="fault_report_date">Rapport d\'anomalie (dernière mise à jour : %s)</string>
<string name="menu_report_new_charger">Signaler un nouveau chargeur</string>
<string name="menu_report_new_charger">Nouveau chargeur</string>
<string name="filter_connectors">Connecteurs</string>
<string name="copyright_summary">©2020-2022 Johan von Forstner</string>
<string name="other">Autre</string>
<string name="pref_navigate_use_maps_off">Le bouton de navigation lance lapplication de cartes avec lemplacement du chargeur</string>
<string name="pref_navigate_use_maps_off">Le bouton de navigation lance lapplication de cartes à lemplacement 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 dEVMap -&gt; 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\" -&gt; \"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 &gt;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>

View File

@@ -1,29 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">EVMap</string>
<string name="no_maps_app_found">Fant ingen navigeringsprogrammer</string>
<string name="no_maps_app_found">Installer et navigeringsprogram først</string>
<string name="closed"><b>Stengt</b></string>
<string name="open_closesat"><b>Åpen</b> · Stenger %s</string>
<string name="holiday">Ferie</string>
<string name="cost">Kostnad</string>
<string name="general_info">Generell info</string>
<string name="menu_filter">Filter</string>
<string name="about">Om EVMap</string>
<string name="about">Om</string>
<string name="version">Versjon</string>
<string name="settings">Innstillinger</string>
<string name="settings_map">Kart</string>
<string name="fav_add">Legg til som favoritt</string>
<string name="fav_add">Lagre som favoritt</string>
<string name="fav_remove">Fjern fra favoritter</string>
<string name="share">Del</string>
<string name="filter_free">Kun gratisladere</string>
<string name="faq">O-S-S</string>
<string name="faq_desc">Ofte stilte spørsmål</string>
<string name="faq">Ofte stilte spørsmål</string>
<string name="menu_edit_filters">Rediger filtre</string>
<string name="edit">rediger</string>
<string name="cancel">Avbryt</string>
<string name="ok">OK</string>
<string name="pref_language">Språk</string>
<string name="pref_language_summary">Endre programspråket</string>
<string name="pref_language">Programspråk</string>
<string name="and_n_others">og %d andre</string>
<string name="pref_map_provider">Karttilbyder</string>
<string name="twitter">Twitter</string>
@@ -35,7 +33,7 @@
<string name="filter_favorites">Favoritter</string>
<string name="delete">Slett</string>
<string name="save_as_profile">Lagre som profil</string>
<string name="donation_dialog_title">Takk for at du bruker EVMap!</string>
<string name="donation_dialog_title">Takk for at du bruker EVMap</string>
<string name="license">Lisens</string>
<string name="pref_data_source">Datakilder</string>
<string name="required">påkrevd</string>
@@ -43,9 +41,9 @@
<string name="help">Hjelp</string>
<string name="hours">Åpningstider</string>
<string name="open_247"><b>Døgnåpen</b></string>
<string name="settings_ui">Brukergrensesnitt</string>
<string name="settings_ui">Grensesnitt</string>
<string name="title_activity_maps">EVMap</string>
<string name="no_browser_app_found">Fant ingen nettlesere</string>
<string name="no_browser_app_found">Installer en nettleser først</string>
<string name="address">Adresse</string>
<string name="network">Nettverk</string>
<string name="closed_unfmt">Stengt</string>
@@ -61,12 +59,12 @@
<string name="search">Søk</string>
<string name="not_implemented">ikke implementert enda</string>
<string name="github_link_title">Kildekode</string>
<string name="oss_licenses">Frie lisenser</string>
<string name="oss_licenses">Lisenser</string>
<string name="copyright">Opphavsrett</string>
<string name="coordinates">Koordinater</string>
<string name="fault_report">Feilrapport</string>
<string name="privacy">Personvernsmerknad</string>
<string name="pref_navigate_use_maps">Start navigasjon umiddelbart</string>
<string name="privacy">Personvern</string>
<string name="pref_navigate_use_maps">Umiddelbar navigasjon</string>
<string name="charge_cards">Betalingsmetoder</string>
<string name="go_to_chargeprice">Sammenlign priser</string>
<string name="filter_networks">Nettverk</string>
@@ -76,7 +74,7 @@
<string name="pref_chargeprice_currency">Valuta</string>
<string name="next">neste</string>
<string name="github_sponsors">GitHub-sponsorer</string>
<string name="menu_report_new_charger">Rapporter ny lader</string>
<string name="menu_report_new_charger">Ny lader</string>
<string name="category_private_charger">Privat lader</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_museum">Museum</string>
@@ -96,9 +94,9 @@
<string name="other">Andre</string>
<string name="cost_detail"><b>Lading:</b> %1$s · <b>Parkering:</b> %2$s</string>
<string name="copyright_summary">©20202022 Johan von Forstner</string>
<string name="pref_navigate_use_maps_on">Navigasjonsnkappen starter Google Maps-navigasjon umiddelbart</string>
<string name="pref_navigate_use_maps_on">Navigasjonsnkappen starter ruteveiledning på Google Maps</string>
<string name="filter_free_parking">Kun ladere med gratis parkering</string>
<string name="filter_min_power">Minimumseffekt</string>
<string name="filter_min_power">Min. effekt</string>
<string name="plug_type_1">Type 1</string>
<string name="plug_chademo">CHAdeMO</string>
<string name="plug_schuko">Schuko</string>
@@ -113,7 +111,7 @@
<string name="map_type">Karttype</string>
<string name="map_details">Kartdetaljer</string>
<string name="map_traffic">Trafikk</string>
<string name="favorites_empty_state">Ladere lagret som favoritter vises her.</string>
<string name="favorites_empty_state">Lagrede ladere vises her</string>
<string name="plug_cee_rot">CEE rød</string>
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
<string name="menu_filters_active">Aktive filtre</string>
@@ -145,13 +143,13 @@
<string name="category_parking_underground">Parkeringsgarasje under bakken</string>
<string name="reorder">endre rekkefølge</string>
<string name="save_profile_enter_name">Skriv inn navnet på filterprofilen:</string>
<string name="filterprofiles_empty_state">Du har ikke lagret noen filterprofiler.</string>
<string name="filterprofiles_empty_state">Du har ikke noen lagrede filterprofiler</string>
<string name="chargeprice_donation_dialog_title">Du er en sann gjerrigknark.</string>
<string name="deleted_filterprofile">Slettet «%s»</string>
<string name="charging_barrierfree">Kan brukes uten registrering</string>
<string name="welcome_1">Finn kjøretøyladere der du er.</string>
<string name="welcome_2">Maksimal ladeeffekt er angitt ved forskjellige farger på respektive ladere i kartet.</string>
<string name="welcome_2_detail">(Du kan sjekke fargene igjen i «Om EVMap → O-S-S» i menyen)</string>
<string name="welcome_1">Finn kjøretøyladere der du er</string>
<string name="welcome_2">Hver laders farge samsvarer med dens høyeste ladeeffekt</string>
<string name="welcome_2_detail">Dette er også å finne i «Om»«O-S-S» i menyen</string>
<string name="verified_desc">Lader bekreftet av et medlem av %s-gemenskapen. Dette betyr ikke at den virker nå.</string>
<string name="charge_price_format">%2$s%1$.2f</string>
<string name="charge_price_average_format">⌀ %2$s%1$.2f/kWt</string>
@@ -164,11 +162,11 @@
<string name="pref_my_vehicle">Mine kjøretøy</string>
<string name="chargeprice_battery_range_to">til</string>
<string name="chargeprice_stats">(%1$.0f kWt, omtrentlig. %2$s, ⌀ %3$.0f kW)</string>
<string name="chargeprice_select_car_first">Velg bilen din i innstillingene først.</string>
<string name="chargeprice_select_car_first">Velg bilmodellen din i innstillingene først</string>
<string name="chargeprice_battery_range">Lad fra %1$.0f%% til %2$.0f%%</string>
<string name="chargeprice_battery_range_from">Lad fra</string>
<string name="chargeprice_vehicle">Kjøretøy</string>
<string name="close">lukk</string>
<string name="close">Lukk</string>
<string name="chargeprice_title">Priser</string>
<string name="chargeprice_connection_error">Kunne ikke laste inn priser</string>
<plurals name="pref_my_tariffs_summary">
@@ -182,7 +180,7 @@
<string name="github_sponsors_desc">Støtt EVMap med GitHub-sponsorer</string>
<string name="donate_desc">Støtt utviklingen av EVMap med en engangsdonasjon</string>
<string name="settings_android_auto">Android Auto</string>
<string name="pref_map_rotate_gestures_enabled">Skru på kartrotasjon</string>
<string name="pref_map_rotate_gestures_enabled">Kartrotasjon</string>
<string name="deleted_recent_search_results">Nylige søkeresultater slettet</string>
<string name="autocomplete_connection_error">Kunne ikke laste inn forslag</string>
<string name="pref_language_device_default">Enhetsforvalg</string>
@@ -190,20 +188,20 @@
<string name="pref_chargeprice_currency_czk">Tsjekkiske kroner (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Danske kroner (DKK)</string>
<string name="pref_chargeprice_currency_hrk">Kroatiske kroner (HRK)</string>
<string name="pref_map_rotate_gestures_on">Kartet kan roteres med to fingre</string>
<string name="pref_map_rotate_gestures_off">Kartet vil orienteres mot nord</string>
<string name="pref_map_rotate_gestures_on">Bruk to fingre for å rotere kartet</string>
<string name="pref_map_rotate_gestures_off">Rotasjon avslått (nord er alltid oppover)</string>
<string name="refresh_live_data">oppdater sanntidsstatus</string>
<string name="pref_chargeprice_currency_isk">Islandske kroner (ISK)</string>
<string name="pref_chargeprice_currency_pln">Polske zloty (PLN)</string>
<string name="pref_chargeprice_currency_usd">Amerikanske dollar (USD)</string>
<string name="filters_deactivated">Filtre deaktivert</string>
<string name="pref_navigate_use_maps_off">Navigasjonsknapp starter kartprogram med laderposisjon</string>
<string name="pref_navigate_use_maps_off">Navigasjonsknapp åpner kartprogrammet med laderposisjon</string>
<string name="show_more">flere …</string>
<string name="filters_activated">Filtre aktivert</string>
<string name="donate">Doner</string>
<string name="donation_successful">Takk. ❤️</string>
<string name="donation_successful">Takk ❤️</string>
<string name="map_type_normal">Forvalg</string>
<string name="donation_failed">Noe gikk galt. 😕</string>
<string name="donation_failed">Noe gikk galt 😕</string>
<string name="filter_custom">Endret filter</string>
<string name="welcome_to_evmap">Velkommen til EVMap</string>
<string name="rename">Gi nytt navn</string>
@@ -221,48 +219,47 @@
<string name="plug_cee_blau">CEE blå</string>
<string name="filter_open_247">Døgnåpent</string>
<string name="pref_chargeprice_currency_huf">Ungarske forint (HUF)</string>
<string name="donation_dialog_detail">Fri programvare utviklet på fritiden. Kodebidrag mottas med takk. Siden programmet er mer og mer populært må driftskostnader dekkes. Overvei å gi din støtte gjennom GitHub-sponsorer.</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Tillat lading over 4.5 kW på vekselstrømsstasjoner med enfaselader</string>
<string name="donation_dialog_detail">EVMap er fri programvare. Kodebidrag mottas med takk. Overvei å gi din støtte gjennom GitHub-sponsorer for å dekke løpende utgifter.</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Tillat enfaselading over 4.5 kW</string>
<string name="connectors">Tilkobling</string>
<string name="operator">Operatør</string>
<string name="amenities">Tilleggstjenester</string>
<string name="filter_min_connectors">Min. antall tilkoblinger</string>
<string name="filter_connectors">Tilkoblinger</string>
<string name="filter_operators">Operatører</string>
<string name="pref_darkmode_summary">Når mørk drakt er iført</string>
<string name="category_service_off_motorway">Rasteplass (ikke på motorvei)</string>
<string name="welcome_2_title">Effekten til veies tilgjengeliggjøres</string>
<string name="chargeprice_donation_dialog_detail">Du bruker prissammenligningen en del. Dette bekostes av utvikleren som månedlig avgift til Chargeprice.app-datatilbyderen.
\nOvervei å støtte EVMap med en donasjon.</string>
<string name="chargeprice_donation_dialog_detail">Du bruker prissammenligningen en del.
\nOvervei å dekke kostnadene ved å støtte EVMap med en donasjon.</string>
<string name="navigate">Navigasjon</string>
<string name="chargeprice_session_fee">startgebyr</string>
<string name="powered_by_chargeprice">tilbudt av Chargeprice</string>
<string name="chargeprice_base_fee">Grunnkostnad: %2$s%1$.2f/måned</string>
<string name="chargeprice_no_tariffs_found">Chargeprice.app fant ikke noen ladeabonnementer kompatible med denne laderen.</string>
<string name="pref_chargeprice_no_base_fee">Kun vis abonnementer uten månedlige gebyr</string>
<string name="chargeprice_no_tariffs_found">Ingen ladeabonnement for denne laderen på Chargeprice.app</string>
<string name="pref_chargeprice_no_base_fee">Utelat abonnementer med månedlige gebyr</string>
<string name="chargeprice_select_connector">Velg tilkobling</string>
<string name="chargeprice_provider_customer_tariff">Kun for tilbyderkunder</string>
<string name="chargeprice_provider_customer_tariff">Kun for kundekoblingssalg</string>
<string name="chargeprice_blocking_fee">Blokkeringsgebyr &gt;%s</string>
<string name="pref_chargeprice_show_provider_customer_tariffs">Vis abonnementer fra kundekoblingssalg</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Noen tilbydere gir billigere abonnementer til sine kunder (f.eks. husstandselektrisitet, gass, osv)</string>
<string name="pref_chargeprice_show_provider_customer_tariffs">Inkluder kundekoblingssalg</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Strømselskaper tilbyr noen ganger billigere abonnementer til sine kunder</string>
<string name="pref_my_tariffs">Mine ladeabonnementer</string>
<string name="chargeprice_no_compatible_connectors">Ingen av tilkoblingene på denne ladestasjonen er kompatible med ditt kjøretøy.</string>
<string name="data_sources_description">Flere datakilder støttes for innhenting av stasjoner. Velg den du ønsker å bruke og gjør endringer senere i innstillingene hvis nødvendig.</string>
<string name="chargeprice_no_compatible_connectors">Ingen kompatible ladere på denne ladestasjonen</string>
<string name="data_sources_description">Velg en datakilde for ladestasjoner. (Kan endres senere i programinnstillingene.)</string>
<string name="unknown_operator">Ukjent operatør</string>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d plan valgt</item>
<item quantity="other">%d planer valgt</item>
</plurals>
<string name="data_source_goingelectric_desc">Veldig god dekning i Tyskland, Østerrike, Sveits, og mange land i nærheten. Beskrivelser på tysk. Gemenskapsdrevet.</string>
<string name="data_source_goingelectric_desc">Storartet i tyskspråklige land. Beskrivelser på tysk. Gemenskapsdrevet.</string>
<string name="powered_by_mapbox">tilbudt av Mapbox</string>
<string name="pref_search_provider_info">Data for stedssøk. Spesielt for Google Maps er dette relativt kostbart. Hvis du bruker dette ofte bes du om å donere gjennom «Om EVMap → Doner».</string>
<string name="data_source_openchargemap_desc">Verdensomspennende dekning med varierende kvalitet. Beskrivelser på engelsk eller lokalt språk. Gemenskapsdrevet og åpen data fra myndigheter i noen land (f.eks. Nord-Amerika, Storbritannia, Frankrike, Norge.)</string>
<string name="pref_search_provider_info">Data for søk er dyre å hente, spesielt fra Google Maps. Overvei å donere gjennom «Om»«Doner».</string>
<string name="data_source_openchargemap_desc">Verdensomspennende, med varierende kvalitet. Beskrivelser på engelsk eller det lokale språket. Gemenskapsdrevet og åpen myndighetsdata i noen land (f.eks. Nord-Amerika, Storbritannia, Frankrike, og Norge.)</string>
<string name="lets_go">Begynn</string>
<string name="crash_report_text">EVMap har krasjet. Send en rapport til utvikleren.</string>
<string name="crash_report_text">EVMap krasjet. Send en rapport til utvikleren.</string>
<string name="chargeprice_all_tariffs_selected">alle planer valgt</string>
<string name="pref_search_provider">Stedssøkstilbyder</string>
<string name="pref_chargeprice_allow_unbalanced_load">Tillat skjev-belastning</string>
<string name="edit_on_goingelectric_info">Hvis en tom side vises her må du logge inn på GoingElectric.de først.</string>
<string name="pref_search_provider">Søketilbyder</string>
<string name="pref_chargeprice_allow_unbalanced_load">Tillat skjev last</string>
<string name="edit_on_goingelectric_info">Logg inn på GoingElectric.de hvis denne siden er tom</string>
<string name="pref_provider_google_maps">Google Maps</string>
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="charging_free">Gratis</string>
@@ -272,4 +269,19 @@
<string name="privacy_link">https://evmap.vonforst.net/en/privacy.html</string>
<string name="faq_link">https://evmap.vonforst.net/en/faq.html</string>
<string name="chargeprice_faq_link">https://evmap.vonforst.net/en/chargeprice_faq.html</string>
<string name="about_contributors">Bidragsytere</string>
<string name="about_contributors_text">Takk til alle som har kodet og oversatt EVMap:</string>
<plurals name="prediction_number_available">
<item quantity="one">%1$d/%2$d tilgjengelig</item>
<item quantity="other">%1$d/%2$d tilgjengelig</item>
</plurals>
<string name="prediction_help">Basert på hvilken dag det er, tid på dagen, og tidligere bruk, slik at du kan finne ledige ladere. Ingen garanti dog.</string>
<string name="prediction_time_colon">%s:</string>
<string name="prediction_dc_plugs_only">Likestrømmsstøpsel</string>
<string name="location_error">Klarte ikke å fastsette posisjon. Sjekk systeminnstillingene.</string>
<string name="utilization_prediction">Bruksprognose</string>
<string name="prediction_only">(kun %s)</string>
<string name="pref_prediction_enabled">Vis bruksprognoser</string>
<string name="pref_prediction_enabled_summary">for støttede ladere
\n(foreløpig kun for likestrøm i Tyskland)</string>
</resources>

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="bottom_sheet_collapsible">false</bool>
</resources>

View File

@@ -2,4 +2,5 @@
<resources>
<dimen name="map_toolbar_width">500dp</dimen>
<dimen name="layers_fab_top_padding">20dp</dimen>
<dimen name="directions_fab_translationx">-44dp</dimen>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hide_on_scroll_fab_behavior">@null</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="bottom_sheet_collapsible">true</bool>
</resources>

View File

@@ -28,4 +28,5 @@
<color name="my_tariff_background">#1F000000</color>
<color name="background">#FFFFFF</color>
<color name="pager_unselected">#1F000000</color>
<color name="logo_tint_night">@null</color>
</resources>

View File

@@ -11,4 +11,5 @@
<item name="match_parent" type="dimen">-1</item>
<dimen name="map_toolbar_width">@dimen/match_parent</dimen>
<dimen name="layers_fab_top_padding">100dp</dimen>
<dimen name="directions_fab_translationx">0dp</dimen>
</resources>

View File

@@ -8,8 +8,19 @@
<string name="goingelectric_forum_url"><![CDATA[https://www.goingelectric.de/forum/viewtopic.php?f=5&t=56342]]></string>
<string name="github_sponsors_link">https://github.com/sponsors/johan12345/</string>
<string name="chargeprice_api_url">https://api.chargeprice.app/v1/</string>
<string name="fronyx_url">https://fronyx.io/</string>
<string name="pref_language_en">English</string>
<string name="pref_language_de">Deutsch</string>
<string name="pref_language_fr">Français</string>
<string name="pref_language_nb_rNO">Norsk Bokmål</string>
<string name="about_contributors_list">
Danilo Bargen\n
Altonss\n
Allan Nordhøy\n
Maximilian Goldschmidt\n
Licaon_Kter\n
pt2121\n
nautilusx
</string>
<string name="hide_on_scroll_fab_behavior">net.vonforst.evmap.ui.HideOnScrollFabBehavior</string>
</resources>

View File

@@ -2,8 +2,8 @@
<string name="app_name">EVMap</string>
<string name="title_activity_maps">EVMap</string>
<string name="connectors">Connectors</string>
<string name="no_maps_app_found">No navigation app found</string>
<string name="no_browser_app_found">No web browser found</string>
<string name="no_maps_app_found">Install a navigation app first</string>
<string name="no_browser_app_found">Install a web browser first</string>
<string name="address">Address</string>
<string name="operator">Operator</string>
<string name="network">Network</string>
@@ -23,7 +23,7 @@
<string name="parking_free">Free</string>
<string name="parking_paid">Paid</string>
<string name="amenities">Amenities</string>
<string name="general_info">General information</string>
<string name="general_info">General info</string>
<string name="realtime_data_unavailable">Real-time status unavailable</string>
<string name="realtime_data_loading">Checking real-time status…</string>
<string name="realtime_data_source">Real-time status source (beta): %s</string>
@@ -33,32 +33,32 @@
<string name="menu_favs">Favorites</string>
<string name="menu_filter">Filter</string>
<string name="not_implemented">not implemented yet</string>
<string name="about">About EVMap</string>
<string name="about">About</string>
<string name="version">Version</string>
<string name="github_link_title">Source code</string>
<string name="oss_licenses">Open Source Licenses</string>
<string name="oss_licenses">Licenses</string>
<string name="settings">Settings</string>
<string name="settings_ui">User Interface</string>
<string name="settings_ui">Interface</string>
<string name="settings_map">Map</string>
<string name="copyright">Copyright</string>
<string name="copyright_summary">©20202022 Johan von Forstner</string>
<string name="other">Other</string>
<string name="privacy">Privacy Notice</string>
<string name="fav_add">Add to favorites</string>
<string name="privacy">Privacy</string>
<string name="fav_add">Save as favorite</string>
<string name="fav_remove">Remove from favorites</string>
<string name="pref_navigate_use_maps">Start navigation immediately</string>
<string name="pref_navigate_use_maps_on">Navigation button starts Google Maps navigation immediately</string>
<string name="pref_navigate_use_maps_off">Navigation button launches maps app with charger location</string>
<string name="pref_navigate_use_maps">Immediate navigation</string>
<string name="pref_navigate_use_maps_on">Navigation button starts route guidance with Google Maps</string>
<string name="pref_navigate_use_maps_off">Navigation button opens the maps app with charger location</string>
<string name="coordinates">Coordinates</string>
<string name="share">Share</string>
<string name="filter_free">Only free chargers</string>
<string name="filter_min_power">Minimum power</string>
<string name="filter_min_power">Min power</string>
<string name="filter_free_parking">Only chargers with free parking</string>
<string name="filter_min_connectors">Minimum number of connectors</string>
<string name="filter_min_connectors">Min number of connectors</string>
<string name="filter_connectors">Connectors</string>
<string name="plug_type_1">Type 1</string>
<string name="plug_type_2">Type 2</string>
<string name="plug_type_3">Type 3a</string>
<string name="plug_type_3">Type 3A</string>
<string name="plug_ccs">CCS</string>
<string name="plug_schuko">Schuko</string>
<string name="plug_chademo">CHAdeMO</string>
@@ -70,18 +70,17 @@
<string name="none">none</string>
<string name="show_more">more…</string>
<string name="show_less">less…</string>
<string name="favorites_empty_state">If you add chargers as favorites, they will show up here.</string>
<string name="favorites_empty_state">Saved chargers show up here</string>
<string name="donate">Donate</string>
<string name="donation_successful">Thank you! ❤️</string>
<string name="donation_failed">Something went wrong. 😕</string>
<string name="donation_successful">Thank you ❤️</string>
<string name="donation_failed">Something went wrong 😕</string>
<string name="map_type_normal">Default</string>
<string name="map_type_satellite">Satellite</string>
<string name="map_type_terrain">Terrain</string>
<string name="map_type">Map type</string>
<string name="map_details">Map details</string>
<string name="map_traffic">Traffic</string>
<string name="faq">FAQ</string>
<string name="faq_desc">Frequently asked questions</string>
<string name="faq">Frequently asked questions</string>
<string name="menu_filters_active">Filters active</string>
<string name="filters_activated">Filters activated</string>
<string name="filters_deactivated">Filters deactivated</string>
@@ -98,11 +97,10 @@
<string name="edit">edit</string>
<string name="cancel">Cancel</string>
<string name="ok">OK</string>
<string name="pref_language">Language</string>
<string name="pref_language_summary">Change the app language</string>
<string name="pref_language">App language</string>
<string name="pref_darkmode">Dark mode</string>
<string name="pref_darkmode_summary">Set when dark mode is activated</string>
<string name="connection_error">Could not load charging stations</string>
<string name="location_error">Failed to detect location. Please check system settings</string>
<string name="retry">Retry</string>
<string name="filter_open_247">Available 24/7</string>
<string name="filter_barrierfree">Usable without registration</string>
@@ -113,7 +111,7 @@
<string name="twitter">Twitter</string>
<string name="goingelectric_forum">Forum thread at GoingElectric.de</string>
<string name="contact">Contact</string>
<string name="menu_report_new_charger">Report new charger</string>
<string name="menu_report_new_charger">New charger</string>
<string name="edit_at_datasource">edit at %s</string>
<string name="categories">Categories</string>
<string name="category_car_dealership">Car Dealership</string>
@@ -131,7 +129,7 @@
<string name="category_church">Church</string>
<string name="category_hospital">Hospital</string>
<string name="category_museum">Museum</string>
<string name="category_parking_multi">Multi-storey car park</string>
<string name="category_parking_multi">Parking garage</string>
<string name="category_parking">Car park</string>
<string name="category_private_charger">Private charger</string>
<string name="category_rest_area">Rest area</string>
@@ -151,16 +149,16 @@
<string name="delete">Delete</string>
<string name="save_as_profile">Save as profile</string>
<string name="save_profile_enter_name">Enter the name of the filter profile:</string>
<string name="filterprofiles_empty_state">You have not yet saved any filter profiles.</string>
<string name="filterprofiles_empty_state">You have no filter profiles saved</string>
<string name="welcome_to_evmap">Welcome to EVMap</string>
<string name="welcome_1">Find electric vehicle chargers around you.</string>
<string name="welcome_1">Find electric vehicle chargers around you</string>
<string name="welcome_2_title">You\'ve got the power</string>
<string name="welcome_2">The color of a charger on the map shows you its maximum charging power.</string>
<string name="welcome_2_detail">(You can check the colors again under “About EVMap → FAQ” in the menu)</string>
<string name="donation_dialog_title">Thank you for using EVMap!</string>
<string name="donation_dialog_detail">EVMap is free and Open Source software that I develop in my spare time. Coding contributions on GitHub are very much appreciated. However, due to increasing popularity of the app, I also need to cover some running costs, e.g. for access to the data sources. Therefore, please consider supporting the app through a donation or via GitHub Sponsors.</string>
<string name="welcome_2">Each charger\'s color corresponds to its max charging power</string>
<string name="welcome_2_detail">This can also be seen in “About” → “Frequently Asked Questions”</string>
<string name="donation_dialog_title">Thank you for using EVMap</string>
<string name="donation_dialog_detail">EVMap is libre and free of charge. Code contributions on GitHub are much appreciated. To help cover the running costs for data access, please consider donating an amount of your choice to the developer.</string>
<string name="chargeprice_donation_dialog_title">You\'re a real bargain hunter!</string>
<string name="chargeprice_donation_dialog_detail">It seems like you like the price comparison feature a lot. To access the pricing data, the developer of EVMap needs to pay a monthly fee to the data provider Chargeprice.app. Therefore, please consider supporting EVMap through a donation.</string>
<string name="chargeprice_donation_dialog_detail">You make great use of the price comparison feature. Please help cover the costs for this data by supporting EVMap with a donation.</string>
<string name="deleted_filterprofile">Deleted “%s”</string>
<string name="undo">Undo</string>
<string name="rename">Rename</string>
@@ -171,42 +169,43 @@
</plurals>
<string name="navigate">Navigate</string>
<string name="verified">verified</string>
<string name="verified_desc">Charger verified by a member at the %s community — not necessarily working right now.</string>
<string name="verified_desc">Charger has once been confirmed to work by a member of the %s community</string>
<string name="charge_price_format">%2$s%1$.2f</string>
<string name="charge_price_average_format">⌀ %2$s%1$.2f/kWh</string>
<string name="charge_price_kwh_format">%2$s%1$.2f/kWh</string>
<string name="chargeprice_select_connector">Choose connector</string>
<string name="chargeprice_provider_customer_tariff">Only for provider customers</string>
<string name="edit_on_goingelectric_info">If only an empty page is showing here, please first log in to GoingElectric.de.</string>
<string name="chargeprice_provider_customer_tariff">Only for tie-in customers</string>
<string name="edit_on_goingelectric_info">Please log in at GoingElectric.de if this page is empty</string>
<string name="percent_format">%.0f%%</string>
<string name="chargeprice_session_fee">session fee</string>
<string name="chargeprice_per_kwh">per kWh</string>
<string name="chargeprice_per_minute">per min</string>
<string name="chargeprice_blocking_fee">Blocking fee >%s</string>
<string name="chargeprice_no_tariffs_found">Chargeprice.app found no charging plans compatible with this charger.</string>
<string name="chargeprice_no_tariffs_found">No charging plans for this charger on Chargeprice.app</string>
<string name="powered_by_chargeprice">powered by Chargeprice</string>
<string name="chargeprice_base_fee">Base fee: %2$s%1$.2f/month</string>
<string name="chargeprice_min_spend">Minimum spend: %2$s%1$.2f/month</string>
<string name="settings_chargeprice">Price comparison</string>
<string name="pref_my_vehicle">My vehicles</string>
<string name="pref_chargeprice_no_base_fee">Only show plans with no monthly fees</string>
<string name="pref_chargeprice_show_provider_customer_tariffs">Show customer-exclusive plans</string>
<string name="chargeprice_select_car_first">Please first select your car model in the settings.</string>
<string name="pref_chargeprice_no_base_fee">Exclude plans with monthly fees</string>
<string name="pref_chargeprice_show_provider_customer_tariffs">Include tie-in plans</string>
<string name="chargeprice_select_car_first">Please select your car model in the settings first</string>
<string name="chargeprice_battery_range">Charge from %1$.0f%% to %2$.0f%%</string>
<string name="chargeprice_battery_range_from">Charge from</string>
<string name="chargeprice_battery_range_to">to</string>
<string name="chargeprice_stats">(%1$.0f kWh, approx. %2$s, ⌀ %3$.0f kW)</string>
<string name="chargeprice_vehicle">Vehicle</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Some providers offer cheaper plans exclusively to their customers (e.g., household electricity, gas)</string>
<string name="close">close</string>
<string name="chargeprice_price_not_available">Price not available</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Utility companies sometimes offer special plans for their customers</string>
<string name="close">Close</string>
<string name="chargeprice_title">Prices</string>
<string name="chargeprice_connection_error">Could not load prices</string>
<string name="chargeprice_no_compatible_connectors">None of the connectors on this charging station is compatible with your vehicle.</string>
<string name="chargeprice_no_compatible_connectors">No compatible connectors at this charging station</string>
<string name="pref_chargeprice_currency">Currency</string>
<string name="pref_my_tariffs">My charging plans</string>
<plurals name="pref_my_tariffs_summary">
<item quantity="one">(will be highlighted in price comparison)</item>
<item quantity="other">(will be highlighted in price comparison)</item>
<item quantity="one">(will be highlighted in the price comparison)</item>
<item quantity="other">(will be highlighted in the price comparison)</item>
</plurals>
<string name="chargeprice_all_tariffs_selected">all plans selected</string>
<string name="license">License</string>
@@ -217,20 +216,20 @@
<item quantity="other">%d plans selected</item>
</plurals>
<string name="unknown_operator">Unknown operator</string>
<string name="data_sources_description">EVMap supports multiple data sources for charging stations. Please select the one you would like to use. You can always change it later in the app\'s settings.</string>
<string name="data_sources_description">Please pick a data source for charging stations. It can later be changed in the app settings.</string>
<string name="data_source_goingelectric">GoingElectric.de</string>
<string name="data_source_openchargemap">Open Charge Map</string>
<string name="data_source_goingelectric_desc">Very good coverage in Germany, Austria and Switzerland and many neighboring countries. Descriptions in German. Community-maintained.</string>
<string name="data_source_openchargemap_desc"><![CDATA[Worldwide coverage with varying quality. Descriptions in English or local language. Community-maintained & government open data in some countries (e.g. North America, UK, France, Norway).]]></string>
<string name="data_source_goingelectric_desc">Great in the German-speaking countries. Descriptions in German. Community-maintained.</string>
<string name="data_source_openchargemap_desc"><![CDATA[Worldwide, with varying quality. Descriptions in English or the local language. Community-maintained and open government data in some countries (e.g. North America, UK, France, Norway).]]></string>
<string name="next">next</string>
<string name="get_started">Get started</string>
<string name="got_it">Got it</string>
<string name="lets_go">Let\'s go</string>
<string name="crash_report_text">Sorry, it seems that EVMap has crashed. Please send a crash report to the developer.</string>
<string name="crash_report_text">EVMap crashed. Please send a crash report to the developer.</string>
<string name="crash_report_comment_prompt">You can add a comment below:</string>
<string name="powered_by_mapbox">powered by Mapbox</string>
<string name="pref_search_provider">Place search provider</string>
<string name="pref_search_provider_info"><![CDATA[Data for place search, especially from Google Maps, is relatively expensive. If you use this feature often, please consider making a donation through \"About EVMap -> Donate\".]]></string>
<string name="pref_search_provider">Search provider</string>
<string name="pref_search_provider_info"><![CDATA[Data for searches is expensive to fetch, especially from Google Maps. Please consider donating through About” → “Donate.]]></string>
<string name="github_sponsors">GitHub Sponsors</string>
<string name="donate_desc">Support EVMap\'s development with a one-time donation</string>
<string name="github_sponsors_desc">Support EVMap on GitHub Sponsors</string>
@@ -245,13 +244,13 @@
<string name="settings_data_sources">Data sources</string>
<string name="help">Help</string>
<string name="settings_android_auto">Android Auto</string>
<string name="pref_chargeprice_allow_unbalanced_load">Enable unbalanced load</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary"><![CDATA[Allow charging with >4.5 kW at AC stations for cars with single-phase charger]]></string>
<string name="pref_map_rotate_gestures_enabled">Enable map rotation</string>
<string name="pref_map_rotate_gestures_on">Map can be rotated with two-finger gesture</string>
<string name="pref_map_rotate_gestures_off">Map will be fixed to north-up</string>
<string name="pref_chargeprice_allow_unbalanced_load">Allow unbalanced load</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Allow single-phase AC charging with more than 4.5 kW</string>
<string name="pref_map_rotate_gestures_enabled">Map rotation</string>
<string name="pref_map_rotate_gestures_on">Use two fingers to rotate the map</string>
<string name="pref_map_rotate_gestures_off">Rotation off (north always up)</string>
<string name="refresh_live_data">refresh real-time status</string>
<string name="autocomplete_connection_error">Suggestions could not be loaded</string>
<string name="autocomplete_connection_error">Could not load suggestions</string>
<string name="pref_language_device_default">Device default</string>
<string name="pref_darkmode_device_default">Device default</string>
<string name="pref_darkmode_always_on">always on</string>
@@ -270,4 +269,20 @@
<string name="pref_chargeprice_currency_usd">US dollar (USD)</string>
<string name="pref_provider_google_maps">Google Maps</string>
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="about_contributors">Contributors</string>
<string name="about_contributors_text">Thanks to all contributors for their coding and translation contributions to EVMap:</string>
<string name="utilization_prediction">Utilization prediction</string>
<string name="prediction_help">The prediction is based on factors like day of the week, time of day and past usage, so that you can avoid overcrowded chargers. No guarantee.</string>
<string name="prediction_time_colon">%s:</string>
<plurals name="prediction_number_available">
<item quantity="one">%1$d/%2$d available</item>
<item quantity="other">%1$d/%2$d available</item>
</plurals>
<string name="pref_prediction_enabled">Show utilization predictions</string>
<string name="pref_prediction_enabled_summary">for supported chargers\n(currently only DC in Germany)</string>
<string name="prediction_only">(%s only)</string>
<string name="prediction_dc_plugs_only">DC plugs</string>
<string name="data_source_switched_to">Data source switched to %s</string>
<string name="pref_applink_associate">Open supported links</string>
<string name="pref_applink_associate_summary">from goingelectric.de and openchargemap.org</string>
</resources>

View File

@@ -14,10 +14,13 @@
android:title="@string/copyright"
android:summary="@string/copyright_summary" />
<Preference
android:key="contributors"
android:title="@string/about_contributors" />
<Preference
android:key="faq"
android:title="@string/faq"
android:summary="@string/faq_desc" />
android:title="@string/faq" />
<Preference
android:key="donate"

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en" />
<locale android:name="de" />
<locale android:name="fr" />
<locale android:name="nb-NO" />
</locale-config>

View File

@@ -9,6 +9,12 @@
android:entryValues="@array/pref_data_source_values"
android:defaultValue="goingelectric"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference
android:key="prediction_enabled"
android:title="@string/pref_prediction_enabled"
android:defaultValue="true"
android:summary="@string/pref_prediction_enabled_summary" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_map">

View File

@@ -2,11 +2,12 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
android:key="language"
android:persistent="false"
android:title="@string/pref_language"
android:entries="@array/pref_language_names"
android:entryValues="@array/pref_language_values"
android:defaultValue="default"
android:summary="@string/pref_language_summary" />
android:summary="%s" />
<ListPreference
android:key="darkmode"
@@ -14,7 +15,7 @@
android:entries="@array/pref_darkmode_names"
android:entryValues="@array/pref_darkmode_values"
android:defaultValue="default"
android:summary="@string/pref_darkmode_summary" />
android:summary="%s" />
<CheckBoxPreference
android:key="map_rotate_gestures_enabled"
android:title="@string/pref_map_rotate_gestures_enabled"
@@ -27,4 +28,8 @@
android:summaryOn="@string/pref_navigate_use_maps_on"
android:summaryOff="@string/pref_navigate_use_maps_off"
android:defaultValue="true" />
<Preference
android:key="applink_associate"
android:title="@string/pref_applink_associate"
android:summary="@string/pref_applink_associate_summary" />
</PreferenceScreen>

View File

@@ -0,0 +1,60 @@
package net.vonforst.evmap.api.fronyx
import kotlinx.coroutines.runBlocking
import net.vonforst.evmap.okResponse
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.junit.Assert.assertEquals
import org.junit.Test
import java.net.HttpURLConnection
import java.time.ZoneOffset
import java.time.ZonedDateTime
class FronyxApiTest {
val webServer = MockWebServer()
val fronyx: FronyxApi
init {
webServer.start()
val apikey = ""
fronyx = FronyxApi.create(
apikey,
webServer.url("/").toString()
)
webServer.dispatcher = object : Dispatcher() {
val notFoundResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
override fun dispatch(request: RecordedRequest): MockResponse {
val segments = request.requestUrl!!.pathSegments
val urlHead = segments.subList(0, 2).joinToString("/")
when (urlHead) {
"predictions/evse-id" -> {
val id = segments[2]
return okResponse("/fronyx/${id.replace("*", "_")}.json")
}
else -> return notFoundResponse
}
}
}
}
@Test
fun apiTest() {
val evseId = "DE*ION*E202102"
runBlocking {
val result = fronyx.getPredictionsForEvseId(evseId)
assertEquals(result.evseId, evseId)
assertEquals(25, result.predictions.size)
assertEquals(
ZonedDateTime.of(2022, 9, 18, 13, 45, 0, 0, ZoneOffset.UTC),
result.predictions[0].timestamp
)
assertEquals(FronyxStatus.AVAILABLE, result.predictions[0].status)
}
}
}

View File

@@ -0,0 +1,21 @@
package net.vonforst.evmap.model
import org.junit.Assert.assertEquals
import org.junit.Test
class ChargersModelTest {
@Test
fun testAddressToString() {
assertEquals("Berlin", Address("Berlin", null, null, null).toString())
assertEquals("12345 Berlin", Address("Berlin", null, "12345", null).toString())
assertEquals(
"Pariser Platz 1, Berlin",
Address("Berlin", null, null, "Pariser Platz 1").toString()
)
assertEquals(
"Pariser Platz 1, 12345 Berlin",
Address("Berlin", null, "12345", "Pariser Platz 1").toString()
)
}
}

View File

@@ -0,0 +1,105 @@
{
"evseId": "DE*ION*E202102",
"predictions": [
{
"timestamp": "2022-09-18T13:45:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T14:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T14:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T14:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T14:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T15:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T15:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T15:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T15:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T16:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T16:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T16:30:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T16:45:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T17:00:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T17:15:00.000Z",
"status": "UNAVAILABLE"
},
{
"timestamp": "2022-09-18T17:30:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T17:45:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T18:00:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T18:15:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T18:30:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T18:45:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T19:00:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T19:15:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T19:30:00.000Z",
"status": "AVAILABLE"
},
{
"timestamp": "2022-09-18T19:45:00.000Z",
"status": "AVAILABLE"
}
]
}

View File

@@ -0,0 +1,40 @@
package net.vonforst.evmap.auto
import org.junit.Assert.assertEquals
import org.junit.Test
class UtilsTest {
@Test
fun testPaginate() {
var (nSingle, nFirst, nOther, nLast) = listOf(6, 5, 4, 5)
for (i in 0..30) {
paginateTest(i, nSingle, nFirst, nOther, nLast)
}
nSingle = 4; nFirst = 4; nOther = 6; nLast = 6
for (i in 0..30) {
paginateTest(i, nSingle, nFirst, nOther, nLast)
}
}
private fun paginateTest(
i: Int,
nSingle: Int,
nFirst: Int,
nOther: Int,
nLast: Int
) {
val list = (0..i).toList()
val paginated = list.paginate(nSingle, nFirst, nOther, nLast)
assertEquals(list, paginated.flatten())
assert(paginated.all { it.isNotEmpty() })
if (paginated.size == 1) {
assert(paginated.first().size <= nSingle)
} else {
assert(paginated.first().size == nFirst)
for (j in 1 until paginated.size - 1) {
assert(paginated[j].size == nOther)
}
assert(paginated.last().size <= nLast)
}
}
}

View File

@@ -3,14 +3,14 @@
buildscript {
ext.kotlin_version = '1.7.10'
ext.about_libs_version = '8.9.4'
ext.nav_version = '2.5.1'
ext.nav_version = '2.5.3'
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"

View File

@@ -26,6 +26,9 @@ be put into the app in the form of a resource file called `apikeys.xml` under
<string name="openchargemap_key" translatable="false">
insert your OpenChargeMap key here
</string>
<string name="fronyx_key" translatable="false">
insert your Fronyx key here
</string>
</resources>
```
@@ -169,3 +172,18 @@ In case you want to pay for access to the full Chargeprice API, check out their
[sales@chargeprice.net](mailto:sales@chargeprice.net).
</details>
Availability data providers
---------------------------
### fronyx
[fronyx](https://fronyx.io/) provides us predictions of charging station availability.
<details>
<summary>How to obtain an API key</summary>
The API is not publically available, contact [fronyx](https://fronyx.io/contact-us/) to get an API
key and documentation.
If you don't want to test this functionality, simply leave the API key blank.
</details>

View File

@@ -0,0 +1,2 @@
Fehler behoben:
- Wechseln der Datenquelle nicht möglich

View File

@@ -0,0 +1,8 @@
Verbesserungen:
- Einige Texte vereinfacht
- Unterstützung für Sprachauswahl pro App von Android 13
Fehler behoben:
- Filtermenü ließ sich nicht öffnen
- Abstürze / Inkonsistenzen nach Wechsel der Datenquelle
- Abstürze unter Android Auto

View File

@@ -0,0 +1,8 @@
Verbesserungen:
- Weitere unterstützte Länder für Preisvergleich mit Chargeprice.app
- Android Auto: Suchbutton nun auf dem Hauptbildschirm
- Android Auto: Emoji durch Icons ersetzt
Fehler behoben:
- Abstürze / Inkonsistenzen nach Wechsel der Datenquelle
- Probleme beim Laden der Echtzeitdaten

View File

@@ -0,0 +1,2 @@
Fehler behoben:
- verschiedene filterabhängige Anzeigen waren seit 1.3.11 nicht mehr korrekt

Some files were not shown because too many files have changed in this diff Show More