Compare commits

...

65 Commits
1.3.6 ... 1.3.9

Author SHA1 Message Date
johan12345
1b374cda1c Release 1.3.9 2022-08-24 22:29:10 +02:00
johan12345
b82f6f68fb Enhance marker color contrast
fix #220
2022-08-24 22:12:29 +02:00
johan12345
8e19399aaa Make border of mini markers more prominent (#220) 2022-08-24 21:41:35 +02:00
johan12345
e315da926e Fix selection of Norwegian locale in language chooser 2022-08-24 21:21:57 +02:00
johan12345
9450230856 minor fixes to translations
- make language names untranslatable
- missing plurals and strings
- add Norwegian to language selection
2022-08-24 21:14:28 +02:00
Johan von Forstner
81d62860e2 Merge pull request #211 from weblate/weblate-evmap-android
Translations update from Hosted Weblate
2022-08-24 20:45:58 +02:00
johan12345
e825654b9c suppress lint MissingQuantity for French
temporary until Weblate 4.14 with new plural requirements data is released
2022-08-24 20:45:41 +02:00
Hosted Weblate
0a4878a129 Translated using Weblate (Norwegian Bokmål)
Currently translated at 77.3% (205 of 265 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 75.7% (200 of 264 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Norwegian Bokmål)

Currently translated at 77.6% (205 of 264 strings)

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Norwegian Bokmål)

Currently translated at 73.4% (194 of 264 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 81.2% (26 of 32 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (39 of 39 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 34.4% (91 of 264 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 34.4% (91 of 264 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 71.8% (23 of 32 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 32.9% (87 of 264 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (3 of 3 strings)

Added translation using Weblate (Norwegian Bokmål)

Added translation using Weblate (Norwegian Bokmål)

Added translation using Weblate (Norwegian Bokmål)

Added translation using Weblate (Norwegian Bokmål)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Johan von Forstner <johan.forstner@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-automotive/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/evmap/app-store-metadata/nb_NO/
Translation: EVMap/Android
Translation: EVMap/Android (strings specific to F-Droid variant)
Translation: EVMap/Android (strings specific to Google Play variant)
Translation: EVMap/Android (strings specific to the Android Automotive OS app)
Translation: EVMap/App Store metadata
2022-08-24 20:41:14 +02:00
johan12345
af50a95abd update AnyMaps
might reduce "white flash" in dark mode (at least for Google Maps) #216
2022-08-24 20:40:50 +02:00
johan12345
fe58551de9 fix highlighting of my charging plans
caused by problem with markomilos.jsonapi library (155aca0041)
fixes #221
2022-08-24 09:27:19 +02:00
johan12345
abc85c136b add French to manual language selection 2022-08-23 00:19:19 +02:00
Hosted Weblate
0ad4691d30 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/
Translation: EVMap/Android
2022-08-23 00:13:30 +02:00
Hosted Weblate
d85a64ec77 Translated using Weblate (French)
Currently translated at 100.0% (264 of 264 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (French)

Currently translated at 100.0% (264 of 264 strings)

Translated using Weblate (French)

Currently translated at 99.6% (263 of 264 strings)

Translated using Weblate (French)

Currently translated at 100.0% (39 of 39 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (French)

Currently translated at 100.0% (32 of 32 strings)

Translated using Weblate (French)

Currently translated at 96.5% (255 of 264 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3 of 3 strings)

Translated using Weblate (French)

Currently translated at 5.6% (15 of 264 strings)

Translated using Weblate (French)

Currently translated at 0.3% (1 of 264 strings)

Added translation using Weblate (French)

Added translation using Weblate (French)

Added translation using Weblate (French)

Added translation using Weblate (French)

Co-authored-by: Altons <marsupilami450@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Johan von Forstner <johan.forstner@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-automotive/fr/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/fr/
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/fr/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/fr/
Translate-URL: https://hosted.weblate.org/projects/evmap/app-store-metadata/fr/
Translation: EVMap/Android
Translation: EVMap/Android (strings specific to F-Droid variant)
Translation: EVMap/Android (strings specific to Google Play variant)
Translation: EVMap/Android (strings specific to the Android Automotive OS app)
Translation: EVMap/App Store metadata
2022-08-23 00:11:58 +02:00
johan12345
52fefb564a eliminate duplicate strings for Google Maps and Mapbox 2022-08-22 23:25:34 +02:00
johan12345
d18b2e26b8 Separate "free" and "paid" strings for charging and parking
because... French :D
2022-08-22 23:22:16 +02:00
johan12345
d5f55366a9 Android Auto: Avoid possible extra template refresh due to race condition 2022-08-22 23:03:08 +02:00
johan12345
2f93e92b57 Android Auto: Use more appropriate title for place search data source selection screen 2022-08-22 22:47:49 +02:00
johan12345
24e5d072d6 GoingElectric: swap order of "categories" and "exclude faults" filters
Categories is not very useful as many chargers have no category assigned. Also, Polestar/Volvo AAOS only shows maximum 8 list items.
2022-08-22 22:37:15 +02:00
johan12345
a9e9055671 Release 1.3.8 2022-08-21 20:36:39 +02:00
johan12345
53ab8dc4e8 Add singular and plural variants for prefs_my_tariffs_summary 2022-08-21 20:18:21 +02:00
johan12345
c0d7d59817 work around double call of onViewCreated in MaterialDialogFragment 2022-08-21 18:50:23 +02:00
johan12345
42cfdfee1d add permission annotations 2022-08-20 21:54:23 +02:00
johan12345
41cb6cf6b0 Implement our own new fused location provider for Android API < 31
should address #214
regression since b445be99bb
2022-08-20 21:46:28 +02:00
johan12345
64f50cc5e6 Fix typo in fragment class name 2022-08-20 20:03:19 +02:00
johan12345
c24c03bb32 Fix crash in OpensourceDonationsDialogFramgent
caused by 17bd7f024e
2022-08-20 20:02:53 +02:00
johan12345
bec25dd4d2 update CustomBottomSheetBehavior
fixes detailAppBar disappearing after recreation (as mentioned by @PulsarFX in #213)
2022-08-20 19:44:05 +02:00
johan12345
4e4c5a0e9a keep map position after recreation even if there is a current search result
fixes #213
2022-08-20 19:27:50 +02:00
johan12345
17bd7f024e use Material styling for all dialogs 2022-08-20 18:40:48 +02:00
johan12345
fc5e77b01a Rework EditTextDialog
- use MaterialAlertDialog for modern styling
- use proper way to move dialog above keyboard
- fixes #212 (buttons were not clickable)
2022-08-20 17:33:49 +02:00
johan12345
6a114fc2ea Metadata: update title to fit within 30 characters
(as already done in Play Store)
2022-08-19 22:37:29 +02:00
johan12345
6e32c6644c docs/api_keys.md: Update documentation regarding Chargeprice API pricing 2022-08-18 22:32:57 +02:00
johan12345
8fb34ae66f Add information about Weblate translation to README
#208
2022-08-18 22:19:26 +02:00
johan12345
ae3489621e fix untranslated string 2022-08-18 21:56:06 +02:00
johan12345
ff0a110f51 restructure strings for compatibility with Weblate
#208
2022-08-18 21:45:03 +02:00
johan12345
24ef4888a8 restructure strings for compatibility with Weblate
#208
2022-08-18 21:07:09 +02:00
johan12345
13916b0c8d Allow moving map when filter menu is open
workaround using reflection
fixes #155
2022-08-18 20:33:50 +02:00
johan12345
efdd0d6bc5 Don't hide status bar in gallery fullscreen view
easy workaround to fix #193
2022-08-18 19:54:44 +02:00
johan12345
155aca0041 ChargepriceApi: replace moe.banana:moshi-jsonapi with com.markomilos.jsonapi 2022-08-17 17:33:44 +02:00
johan12345
6393eadc81 increase version code
(previous Play Store release was not accepted)
2022-08-16 22:02:24 +02:00
johan12345
4319ece4f3 ChargepriceFragment: avoid reloading prices on orientation change
using SavedStateHandle
2022-08-16 21:20:03 +02:00
johan12345
62f2002e5c Reload Chargeprice data when vehicles have changed
fixes #209
2022-08-16 21:04:44 +02:00
johan12345
4a82250a3d Release 1.3.7 2022-08-15 21:37:43 +02:00
johan12345
a8f23e9fb6 Android Auto/Automotive: Refresh data after relaunching app from background
fixes #207
2022-08-15 21:25:20 +02:00
johan12345
7da64fd566 after first start, remove onboarding from back stack
fixes #202
2022-08-15 20:56:28 +02:00
johan12345
09b5d536cb keep ChargerDetails in saved state
fixes #205
2022-08-15 20:53:22 +02:00
johan12345
5e01200d96 Chargeprice: remove errorneous tint from provider logo in dark mode
fixes #206
2022-08-15 18:54:05 +02:00
johan12345
c8d2e73218 BindingAdapters.colorToTransparent(): fix when values become negative
fixes #206
2022-08-15 18:52:13 +02:00
johan12345
d7b377ea56 disable mini markers completely when filtered by HPCs 2022-08-15 18:38:18 +02:00
johan12345
edd35fba1b suppress lint again 2022-08-13 16:29:18 +02:00
johan12345
1f23080141 suppress lint 2022-08-13 16:17:29 +02:00
johan12345
a3d9ecf49e fix another lint warning 2022-08-13 15:01:39 +02:00
johan12345
6681d3cc17 Android Auto: don't exit app when canceling vehicle data permission 2022-08-13 14:59:21 +02:00
johan12345
a184b817bc Android Auto: fix alignment of +/- buttons for Chargeprice range selection 2022-08-13 14:43:21 +02:00
johan12345
b658e0183c fix lint error 2022-08-13 14:38:11 +02:00
johan12345
6a0234ac2f use proper app icon for persistent Android Auto notification
fixes #201
2022-08-13 14:15:19 +02:00
johan12345
d5ac35100b use new Activity Result API for requesting permissions 2022-08-13 13:08:55 +02:00
johan12345
d3b4cb6a90 update AnyMaps 2022-08-13 13:08:55 +02:00
johan12345
5d70d8c09a replace deprecated override in NavHostFragment 2022-08-13 13:08:55 +02:00
johan12345
9642a58206 implement new MenuProvider API
to avoid deprecated functions
2022-08-13 13:08:55 +02:00
johan12345
0e3280a119 upgrade Kotlin to 1.7.10 2022-08-13 13:08:55 +02:00
johan12345
c60043f925 disable AndroidX Jetifier
which is now not needed anymore
2022-08-13 13:08:55 +02:00
johan12345
b445be99bb remove dependency on unmaintained Lost library
use Android's own Location APIs instead
2022-08-13 13:08:55 +02:00
johan12345
02395dda7f upgrade libraries 2022-08-13 13:08:55 +02:00
johan12345
c33c69db0b update Android Gradle Plugin 2022-08-13 13:08:55 +02:00
Johan von Forstner
77fdfc7ccb Update comparison of product flavors in README 2022-08-10 18:20:45 +02:00
96 changed files with 2161 additions and 842 deletions

View File

@@ -20,7 +20,7 @@ Features
- Advanced filtering options, including saved filter profiles
- Favorites list, also with availability information
- Integrated price comparison using [Chargeprice.app](https://chargeprice.app) (only in Europe)
- Android Auto integration
- Android Auto & Android Automotive OS integration
- No ads, fully open source
- Compatible with Android 5.0 and above
- Can use Google Maps or Mapbox (OpenStreetMap) as map backends - the version available on F-Droid only uses Mapbox.
@@ -41,9 +41,24 @@ EVMap uses and put them into the app in the form of a resource file called `apik
`app/src/main/res/values`. You can find more information on which API keys are necessary for which
features and how they can be obtained in our [documentation page](doc/api_keys.md).
There are two different build flavors, `google` and `foss`, where only the `google` variant uses
Google Maps data and provides the Android Auto integration. The `foss` variant only uses Mapbox data
and should run on devices without Google Play Services.
There are three different build flavors, `googleNormal`, `fossNormal` and `googleAutomotive`.
- The `foss` variant only uses Mapbox data and should run on most Android devices, even without
Google Play Services.
- The `google` variants also include access to Google Maps data.
- `googleNormal` is intended to run on smartphones and tablets, and also includes the Android
Auto app for use on the car display.
- `googleAutomotive` variant is intended to be installed directly on car infotainment systems
using the Google-flavored Android Automotive OS. It does not provide the usual smartphone UI.
We also have a special [documentation page](doc/android_auto.md) on how to test the Android Auto
app.
Translations
------------
You can use our [Weblate page](https://hosted.weblate.org/projects/evmap/) to help translate EVMap
into new languages.
<a href="https://hosted.weblate.org/engage/evmap/">
<img src="https://hosted.weblate.org/widgets/evmap/-/open-graph.png" width="500" alt="Translation status" />
</a>

View File

@@ -0,0 +1,23 @@
<?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 120 120" style="enable-background:new 0 0 120 120;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g>
<g>
<path class="st0"
d="M27.1,88.3l-2.2-19.2l-3.3,0.3l2.2,19.2L27.1,88.3z M39,86.9l-2.2-19.2l-3.3,0.3l2.2,19.2L39,86.9z" />
<path class="st0" d="M45.2,113c-1,1.3-1.8,2.1-2,2.2c-3,2.4-5.4,3.1-7.4,2.2c-3.5-1.7-3.2-8.2-3.1-8.9l2.4,0.1
c-0.1,1.8,0.2,5.8,1.8,6.6c0.9,0.5,2.5-0.1,4.6-1.8l0,0c0,0,6.7-6.7,5.3-12c-1.6-6.4,5.8-15.5,8.2-18.6l0.3-0.3l2,1.5l-0.3,0.5
c-7.5,9.2-8.3,14-7.7,16.4C50.5,105.4,47.4,110.4,45.2,113z" />
<path class="st0" d="M19.7,88.1l0.9,7.9l7.3,4.9l9.8-1l6-6.4l-0.9-7.9L19.7,88.1z" />
<g>
<path class="st0"
d="M37.6,99.7l-9.8,1l2.1,8.7l7.7-0.9V99.7L37.6,99.7z M44.6,79l0.8,7.2l-28.2,3.2l-0.8-7.2L44.6,79z" />
</g>
</g>
<path class="st0" d="M66.7,0C46.5,0,30.1,16.4,30.1,36.6c0,27.6,30.8,42,34.5,81.4c0.1,1.2,1,2,2.2,2c1.2,0,2.1-0.8,2.2-2
c3.7-39.4,34.5-53.8,34.5-81.4C103.3,16.2,86.9,0,66.7,0z M78.4,34.7L64.3,59V40.8h-6V18.7c0,0,20.2,0,20.1-0.1l-8.1,16.2H78.4z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,31 +1,25 @@
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<?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 233.8 368.4" style="enable-background:new 0 0 233.8 368.4;" xml:space="preserve">
viewBox="0 0 233.8 368.4" style="enable-background:new 0 0 233.8 368.4;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{display:none;}
.st2{display:inline;fill:#802C27;}
.st3{fill:#808080;}
.st4{display:none;fill:#802C27;}
.st1{fill:#808080;}
</style>
<g>
<g>
<g>
<path class="st0" d="M109.8,0h13.6c33.9,1.9,67.1,18.5,87.7,45.8c13.5,17.2,21,38.6,22.7,60.3v8.1c-0.8,42.1-27.7,76.6-51,109.4
c-26.2,37-50.4,77.3-57.1,122.9c-1.8,7.7,0.4,18.5-8.9,22c-2.2-1.7-4.7-3.1-6.2-5.4c-2.7-25.5-9.1-50.7-20-73.9
c-12.3-27.1-29.5-51.6-47-75.6C33,199,23,184.2,14.7,168.3c-13-23.8-17.9-51.9-12.5-78.6c4.4-21.1,15.4-40.6,30.6-55.7
C53.3,14,81.1,1.8,109.8,0z" />
</g>
</g>
<g class="st1">
<path class="st2" d="M107.2,74.1c18.9-4.8,40.4,5.5,47.7,23.7c6.1,14.5,1.9,32.5-9.9,42.9c-12.6,11.5-32.4,14-47.5,6
c-13.9-6.8-23-22.6-21.3-38.1C77.6,92,91.1,77.7,107.2,74.1z" />
</g>
<path class="st0" d="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-74
c-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.2
C53.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,8
c-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.5
C123.3,359,122.8,364.9,117,367.4z" />
<path class="st1" d="M123.3,2c34.1,1.9,66.3,18.8,86.2,45c12.6,16.1,20.5,37.1,22.3,59.1l0,8c-0.7,38.5-23.6,70.5-45.7,101.3
c-1.7,2.3-3.3,4.6-5,6.9c-24.5,34.6-50.5,76.3-57.4,123.7c-0.5,2.1-0.7,4.4-0.9,6.7c-0.5,5.9-1,11-5.8,13.4
c-0.2-0.2-0.5-0.4-0.7-0.5c-1.5-1.1-2.9-2-3.8-3.3c-2.9-26.9-9.7-51.8-20.1-74C80,261,62.3,235.8,45.2,212.4
c-8.7-11.9-19.8-27.8-28.8-45.1C3.3,143.3-1,115.9,4.2,90.1c4.2-20.2,14.9-39.6,30-54.7C54.2,16,81.7,3.8,109.9,2H123.3 M123.4,0
h-13.6c-28.7,1.8-56.5,14-77,34C17.6,49.1,6.6,68.6,2.2,89.7c-5.4,26.7-0.5,54.8,12.5,78.6C23,184.2,33,199,43.6,213.6
c17.5,24,34.7,48.5,47,75.6c10.9,23.2,17.3,48.4,20,73.9c1.5,2.3,4,3.7,6.2,5.4c9.3-3.5,7.1-14.3,8.9-22
c6.7-45.6,30.9-85.9,57.1-122.9c23.3-32.8,50.2-67.3,51-109.4v-8.1c-1.7-21.7-9.2-43.1-22.7-60.3C190.5,18.5,157.3,1.9,123.4,0
L123.4,0z" />
</g>
<path class="st3" d="M90.9,57.3v68.2h18.6v55.8l43.4-74.4h-24.8l24.8-49.6H90.9z" />
<path class="st4" d="M159,85.3L159,85.3l-20.8-20.9l-5.9,5.9l11.8,11.8c-5.3,2-9,7.1-9,13.1c0,7.7,6.3,14,14,14c2,0,3.9-0.4,5.6-1.2
v40.4c0,3.1-2.5,5.6-5.6,5.6s-5.6-2.5-5.6-5.6v-25.2c0-6.2-5-11.2-11.2-11.2h-5.6V72.8c0-6.2-5-11.2-11.2-11.2H81.8
c-6.2,0-11.2,5-11.2,11.2v89.7h56.1v-42.1h8.4v28c0,7.7,6.3,14,14,14s14-6.3,14-14V95.2C163.1,91.3,161.6,87.8,159,85.3
M149.1,100.8c-3.1,0-5.6-2.5-5.6-5.6c0-3.1,2.5-5.6,5.6-5.6s5.6,2.5,5.6,5.6C154.7,98.3,152.2,100.8,149.1,100.8 M93.1,145.6v-25.2
H81.8l22.4-42.1v28h11.2L93.1,145.6z" />
<path class="st1"
d="M90.9,57.3v68.2h18.6v55.8l43.4-74.4h-24.8l24.8-49.6C152.9,57.3,90.9,57.3,90.9,57.3z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,27 +1,28 @@
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<?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 233.8 368.4" style="enable-background:new 0 0 233.8 368.4;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#B5B5B5;}
.st2{fill:#808080;}
.st1{fill:#808080;}
.st2{fill:#B5B5B5;}
</style>
<g>
<g>
<g>
<path class="st0" d="M109.8,0h13.6c33.9,1.9,67.1,18.5,87.7,45.8c13.5,17.2,21,38.6,22.7,60.3v8.1c-0.8,42.1-27.7,76.6-51,109.4
c-26.2,37-50.4,77.3-57.1,122.9c-1.8,7.7,0.4,18.5-8.9,22c-2.2-1.7-4.7-3.1-6.2-5.4c-2.7-25.5-9.1-50.7-20-73.9
c-12.3-27.1-29.5-51.6-47-75.6C33,199,23,184.2,14.7,168.3c-13-23.8-17.9-51.9-12.5-78.6C6.6,68.6,17.6,49.1,32.8,34
C53.3,14,81.1,1.8,109.8,0z" />
</g>
</g>
</g>
<g>
<polygon class="st1"
points="143.2,109.4 123.5,143.2 123.5,181.3 166.9,106.9 144.7,106.9 " />
<path class="st1"
d="M122.2,101.9h16.7h5.7l22.3-44.6c0,0-10.2,0-22.4,0l-1.1,2.2L122.2,101.9z" />
<path class="st2" d="M138.9,57.3c-9.7,0-19.8,0-26.4,0c-2.5,0-5.1,0-7.6,0c-8.2,0-16.1,0-21.4,0c-4.1,0-6.6,0-6.6,0v68.2h18.6v55.8
l43.4-74.4h-24.8L138.9,57.3z" />
<path class="st0" d="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-74
c-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.2
C53.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,8
c-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.5
C123.3,359,122.8,364.9,117,367.4z" />
<path class="st1" d="M123.3,2c34.1,1.9,66.3,18.8,86.2,45c12.6,16.1,20.5,37.1,22.3,59.1l0,8c-0.7,38.5-23.6,70.5-45.7,101.3
c-1.7,2.3-3.3,4.6-5,6.9c-24.5,34.6-50.5,76.3-57.4,123.7c-0.5,2.1-0.7,4.4-0.9,6.7c-0.5,5.9-1,11-5.8,13.4
c-0.2-0.2-0.5-0.4-0.7-0.5c-1.5-1.1-2.9-2-3.8-3.3c-2.9-26.9-9.7-51.8-20.1-74C80,261,62.3,235.8,45.2,212.4
c-8.7-11.9-19.8-27.8-28.8-45.1C3.3,143.3-1,115.9,4.2,90.1c4.2-20.2,14.9-39.6,30-54.7C54.2,16,81.7,3.8,109.9,2H123.3 M123.4,0
h-13.6c-28.7,1.8-56.5,14-77,34C17.6,49.1,6.6,68.6,2.2,89.7c-5.4,26.7-0.5,54.8,12.5,78.6C23,184.2,33,199,43.6,213.6
c17.5,24,34.7,48.5,47,75.6c10.9,23.2,17.3,48.4,20,73.9c1.5,2.3,4,3.7,6.2,5.4c9.3-3.5,7.1-14.3,8.9-22
c6.7-45.6,30.9-85.9,57.1-122.9c23.3-32.8,50.2-67.3,51-109.4v-8.1c-1.7-21.7-9.2-43.1-22.7-60.3C190.5,18.5,157.3,1.9,123.4,0
L123.4,0z" />
</g>
<polygon class="st2" points="143.2,109.4 123.5,143.2 123.5,181.3 166.9,106.9 144.7,106.9 " />
<path class="st2" d="M122.2,101.9h16.7h5.7l22.3-44.6c0,0-10.2,0-22.4,0l-1.1,2.2L122.2,101.9z" />
<path class="st1" d="M138.9,57.3c-9.7,0-19.8,0-26.4,0c-2.5,0-5.1,0-7.6,0c-8.2,0-16.1,0-21.4,0c-4.1,0-6.6,0-6.6,0v68.2h18.6v55.8
l43.4-74.4h-24.8L138.9,57.3z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -19,8 +19,8 @@ android {
minSdkVersion 21
targetSdkVersion 31
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode 96
versionName "1.3.6"
versionCode 104
versionName "1.3.9"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -158,22 +158,23 @@ dependencies {
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:f69f532660'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:dd0167dbff'
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 'moe.banana:moshi-jsonapi:3.5.0'
implementation 'moe.banana:moshi-jsonapi-retrofit-converter:3.5.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 'io.coil-kt:coil:1.1.0'
implementation 'com.github.johan12345:StfalconImageViewer:5082ebd392'
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
implementation 'com.airbnb.android:lottie:4.1.0'
implementation 'io.michaelrocks.bimap:bimap:1.1.0'
implementation 'com.mapzen.android:lost:3.0.2'
implementation 'com.google.guava:guava:29.0-android'
implementation 'com.github.pengrad:mapscaleview:1.6.0'
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
@@ -185,7 +186,7 @@ dependencies {
googleAutomotiveImplementation "androidx.car.app:app-automotive:$carAppVersion"
// AnyMaps
def anyMapsVersion = 'c401a4256a'
def anyMapsVersion = 'a9b3dd7d99'
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
googleImplementation 'com.google.android.gms:play-services-maps:18.1.0'
@@ -200,7 +201,7 @@ dependencies {
// Google Places
googleImplementation 'com.google.android.libraries.places:places:2.6.0'
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.1'
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.1'
// Mapbox Geocoding
implementation 'com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0'
@@ -232,8 +233,8 @@ dependencies {
implementation("ch.acra:acra-limiter:$acraVersion")
// debug tools
implementation 'com.facebook.stetho:stetho:1.5.1'
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
implementation 'com.facebook.stetho:stetho:1.6.0'
implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0'
// testing
testImplementation 'junit:junit:4.13.2'
@@ -243,7 +244,7 @@ dependencies {
// testing for car app
testGoogleImplementation "androidx.car.app:app-testing:$carAppVersion"
testGoogleImplementation 'org.robolectric:robolectric:4.7.3'
testGoogleImplementation 'org.robolectric:robolectric:4.8.1'
testGoogleImplementation 'androidx.test:core:1.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="chargeprice_api_url">https://staging-api.chargeprice.app/v1/</string>
<string name="chargeprice_key">20c0d68918c9dc96c564784b711a6570</string>
</resources>

View File

@@ -1,14 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_map_provider_names">
<item>OpenStreetMap (Mapbox)</item>
</string-array>
<string-array name="pref_search_provider_names">
<item>OpenStreetMap (Mapbox)</item>
</string-array>
<string-array name="pref_search_provider_values" tranlatable="false">
<item>mapbox</item>
</string-array>
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.</string>
<string name="donate_paypal">Mit PayPal spenden</string>
<string name="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap (Mapbox).</string>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile \? Soutenez son développement en envoyant un don au développeur.</string>
<string name="data_sources_hint">Les données cartographiques de l\'application sont fournies par OpenStreetMap (Mapbox).</string>
<string name="donate_paypal">Faire un don avec PayPal</string>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donate_paypal">Doner med PayPal</string>
<string name="data_sources_hint">Kartdata i programmet tilbys av OpenStreetMap (Mapbox).</string>
<string name="donations_info" formatted="false">Synes du EVMap er nyttig\? Støtt utviklingen ved å sende en slant til utvikleren.</string>
</resources>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_map_provider_names">
<item>@string/pref_provider_osm_mapbox</item>
</string-array>
<string-array name="pref_map_provider_values" translatable="false">
<item>mapbox</item>
</string-array>
<string-array name="pref_search_provider_names">
<item>@string/pref_provider_osm_mapbox</item>
</string-array>
<string-array name="pref_search_provider_values" translatable="false">
<item>mapbox</item>
</string-array>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pref_search_provider_default" translatable="false">mapbox</string>
<string name="pref_map_provider_default" translatable="false">mapbox</string>
<string name="paypal_link" translatable="false">https://paypal.me/johan98</string>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.</string>
<string name="donate_paypal">Donate with PayPal</string>
<string name="data_sources_hint">Map data in the app is provided by OpenStreetMap (Mapbox).</string>
</resources>

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_map_provider_names">
<item>OpenStreetMap (Mapbox)</item>
</string-array>
<string-array name="pref_map_provider_values" tranlatable="false">
<item>mapbox</item>
</string-array>
<string-array name="pref_search_provider_names">
<item>OpenStreetMap (Mapbox)</item>
</string-array>
<string-array name="pref_search_provider_values" tranlatable="false">
<item>mapbox</item>
</string-array>
<string name="pref_search_provider_default" translatable="false">mapbox</string>
<string name="pref_map_provider_default" translatable="false">mapbox</string>
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.</string>
<string name="donate_paypal">Donate with PayPal</string>
<string name="paypal_link" translatable="false">https://paypal.me/johan98</string>
<string name="data_sources_hint">Map data in the app is provided by OpenStreetMap (Mapbox).</string>
</resources>

View File

@@ -5,12 +5,9 @@ import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.location.Criteria
import android.location.Location
import android.location.LocationManager
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresPermission
@@ -31,6 +28,9 @@ import androidx.core.location.LocationListenerCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
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.utils.checkFineLocationPermission
@@ -71,7 +71,8 @@ class CarAppService : androidx.car.app.CarAppService() {
.setContentTitle(getString(R.string.app_name))
.setOngoing(true)
.setPriority(NotificationManagerCompat.IMPORTANCE_HIGH)
.setSmallIcon(R.mipmap.ic_launcher)
.setSmallIcon(R.drawable.ic_appicon_notification)
.setColor(ContextCompat.getColor(this, R.color.colorPrimary))
.setTicker(getString(R.string.auto_location_service))
.setWhen(System.currentTimeMillis())
@@ -102,8 +103,8 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
location?.let { value?.updateLocation(it) }
}
private var location: Location? = null
private val locationManager: LocationManager by lazy {
carContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private val locationEngine: LocationEngine by lazy {
FusionEngine(carContext)
}
private val hardwareMan: CarHardwareManager by lazy {
@@ -178,16 +179,11 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
private fun requestPhoneLocationUpdates() {
val provider = locationManager.getBestProvider(Criteria().apply {
accuracy = Criteria.ACCURACY_FINE
}, true) ?: return
val location = locationManager.getLastKnownLocation(provider)
val location = locationEngine.getLastKnownLocation()
updateLocation(location)
locationManager.requestLocationUpdates(
provider,
locationEngine.requestLocationUpdates(
Priority.HIGH_ACCURACY,
1000,
1f,
phoneLocationListener
)
}
@@ -208,7 +204,7 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
private fun removePhoneLocationUpdates() {
locationManager.removeUpdates(phoneLocationListener)
locationEngine.removeUpdates(phoneLocationListener)
}
@SuppressLint("MissingPermission")

View File

@@ -15,13 +15,13 @@ import androidx.car.app.model.*
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import jsonapi.Meta
import jsonapi.Relationship
import jsonapi.Relationships
import jsonapi.ResourceIdentifier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moe.banana.jsonapi2.HasMany
import moe.banana.jsonapi2.HasOne
import moe.banana.jsonapi2.JsonBuffer
import moe.banana.jsonapi2.ResourceIdentifier
import net.vonforst.evmap.R
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.model.ChargeLocation
@@ -34,7 +34,10 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
private val prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
private val api by lazy {
ChargepriceApi.create(carContext.getString(R.string.chargeprice_key))
ChargepriceApi.create(
carContext.getString(R.string.chargeprice_key),
carContext.getString(R.string.chargeprice_api_url)
)
}
private var prices: List<ChargePrice>? = null
private var meta: ChargepriceChargepointMeta? = null
@@ -94,7 +97,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
)
.build().intent
intent.data =
Uri.parse("https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=${getDataAdapter()}")
Uri.parse(ChargepriceApi.getPoiUrl(charger))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
carContext.startActivity(intent)
@@ -169,39 +172,44 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
}
private fun loadPrices(model: Model?) {
val dataAdapter = getDataAdapter() ?: return
val dataAdapter = ChargepriceApi.getDataAdapter(charger) ?: return
val manufacturer = model?.manufacturer?.value
val modelName = getVehicleModel(model?.manufacturer?.value, model?.name?.value)
lifecycleScope.launch {
try {
val car = determineVehicle(manufacturer, modelName)
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
val result = api.getChargePrices(ChargepriceRequest().apply {
this.dataAdapter = dataAdapter
station = cpStation
vehicle = HasOne(car)
tariffs = if (!prefs.chargepriceMyTariffsAll) {
val myTariffs = prefs.chargepriceMyTariffs ?: emptySet()
HasMany<ChargepriceTariff>(*myTariffs.map {
ResourceIdentifier(
"tariff",
it
val result = api.getChargePrices(
ChargepriceRequest(
dataAdapter = dataAdapter,
station = cpStation,
vehicle = car,
options = ChargepriceOptions(
batteryRange = batteryRange.map { it.toDouble() },
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency,
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad
),
relationships = if (!prefs.chargepriceMyTariffsAll) {
val myTariffs = prefs.chargepriceMyTariffs ?: emptySet()
Relationships(
"tariffs" to Relationship.ToMany(
myTariffs.map {
ResourceIdentifier(
"tariff",
id = it
)
},
meta = Meta.from(
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS),
ChargepriceApi.moshi
)
)
)
}.toTypedArray()).apply {
meta = JsonBuffer.create(
ChargepriceApi.moshi.adapter(ChargepriceRequestTariffMeta::class.java),
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS)
)
}
} else null
options = ChargepriceOptions(
batteryRange = batteryRange.map { it.toDouble() },
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency,
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad
)
}, ChargepriceApi.getChargepriceLanguage())
} else null
), ChargepriceApi.getChargepriceLanguage()
)
val myTariffs = prefs.chargepriceMyTariffs
@@ -215,14 +223,16 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
invalidate()
return@launch
}
meta =
(result.meta.get<ChargepriceMeta>(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta).chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull {
it.power
}
prices = result.map { cp ->
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
}
prices = result.data!!.map { cp ->
val filteredPrices =
cp.chargepointPrices.filter {
it.plug == chargepoint.plug && it.power == chargepoint.power
@@ -230,15 +240,15 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
if (filteredPrices.isEmpty()) {
null
} else {
cp.clone().apply {
cp.copy(
chargepointPrices = filteredPrices
}
)
}
}.filterNotNull()
.sortedBy { it.chargepointPrices.first().price }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||
myTariffs != null && it.tariff?.get()?.id in myTariffs
myTariffs != null && it.tariffId in myTariffs
}
invalidate()
} catch (e: IOException) {
@@ -316,10 +326,4 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
}
return vehicles[0]
}
private fun getDataAdapter(): String? = when (charger.dataSource) {
"goingelectric" -> ChargepriceApi.DATA_SOURCE_GOINGELECTRIC
"openchargemap" -> ChargepriceApi.DATA_SOURCE_OPENCHARGEMAP
else -> null
}
}

View File

@@ -102,9 +102,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
private var searchLocation: LatLng? = null
init {
filtersWithValue.observe(this) {
loadChargers()
}
lifecycle.addObserver(this)
marker = MARKER
}
@@ -192,7 +190,6 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
)
.setOnClickListener {
screenManager.pushForResult(FilterScreen(carContext, session)) {
chargers = null
filterStatus.value = prefs.filterStatus
}
session.mapScreen = null
@@ -280,13 +277,8 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
)
setOnClickListener {
screenManager.pushForResult(ChargerDetailScreen(carContext, charger)) {
if (filterStatus.value == FILTERS_FAVORITES) {
// favorites list may have been updated
chargers = null
loadChargers()
}
}
screenManager.push(ChargerDetailScreen(carContext, charger))
session.mapScreen = null
}
}.build()
}
@@ -376,8 +368,15 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
if (isUpdate) invalidate()
}
override fun onResume(owner: LifecycleOwner) {
override fun onStart(owner: LifecycleOwner) {
setupListeners()
// 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()
}
}
private fun setupListeners() {
@@ -396,7 +395,13 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
}
}
override fun onPause(owner: LifecycleOwner) {
override fun onStop(owner: LifecycleOwner) {
// 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.
// Deleting the data already in onStop makes sure that we show a loading screen directly
// (i.e. onGetTemplate is not called while the old data is still there)
chargers = null
availabilities.clear()
removeListeners()
}

View File

@@ -12,7 +12,8 @@ import net.vonforst.evmap.R
class PermissionScreen(
ctx: CarContext,
@StringRes val message: Int,
val permissions: List<String>
val permissions: List<String>,
val finishApp: Boolean = true
) : Screen(ctx) {
override fun onGetTemplate(): Template {
return MessageTemplate.Builder(carContext.getString(message))
@@ -31,7 +32,13 @@ class PermissionScreen(
Action.Builder()
.setTitle(carContext.getString(R.string.cancel))
.setOnClickListener {
carContext.finishCarApp()
if (finishApp) {
carContext.finishCarApp()
} else {
// pop twice to get away from the screen that requires the permission
screenManager.pop()
screenManager.pop()
}
}
.build(),
)

View File

@@ -1,5 +1,6 @@
package net.vonforst.evmap.auto
import androidx.annotation.StringRes
import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
@@ -107,6 +108,7 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
screenManager.push(
ChooseDataSourceScreen(
carContext,
R.string.pref_data_source,
dataSourceNames,
dataSourceValues,
prefs.dataSource,
@@ -127,6 +129,7 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) {
screenManager.push(
ChooseDataSourceScreen(
carContext,
R.string.pref_search_provider,
searchProviderNames,
searchProviderValues,
prefs.searchProvider
@@ -155,6 +158,7 @@ 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,
@@ -165,7 +169,7 @@ class ChooseDataSourceScreen(
override fun onGetTemplate(): Template {
return ListTemplate.Builder().apply {
setTitle(carContext.getString(R.string.pref_data_source))
setTitle(carContext.getString(title))
setHeaderAction(Action.BACK)
setSingleList(ItemList.Builder().apply {
for (i in names.indices) {
@@ -218,7 +222,10 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
R.plurals.chargeprice_some_tariffs_selected,
n,
n
) + "\n" + carContext.getString(R.string.pref_my_tariffs_summary)
) + "\n" + carContext.resources.getQuantityString(
R.plurals.pref_my_tariffs_summary,
n
)
}
)
}.build())
@@ -283,7 +290,10 @@ class ChargepriceSettingsScreen(ctx: CarContext) : Screen(ctx) {
class SelectVehiclesScreen(ctx: CarContext) : MultiSelectSearchScreen<ChargepriceCar>(ctx) {
private val prefs = PreferenceDataSource(carContext)
private var api = ChargepriceApi.create(carContext.getString(R.string.chargeprice_key))
private var api = ChargepriceApi.create(
carContext.getString(R.string.chargeprice_key),
carContext.getString(R.string.chargeprice_api_url)
)
override val isMultiSelect = true
override val shouldShowSelectAll = false
@@ -308,7 +318,10 @@ class SelectVehiclesScreen(ctx: CarContext) : MultiSelectSearchScreen<Chargepric
class SelectTariffsScreen(ctx: CarContext) : MultiSelectSearchScreen<ChargepriceTariff>(ctx) {
private val prefs = PreferenceDataSource(carContext)
private var api = ChargepriceApi.create(carContext.getString(R.string.chargeprice_key))
private var api = ChargepriceApi.create(
carContext.getString(R.string.chargeprice_key),
carContext.getString(R.string.chargeprice_api_url)
)
override val isMultiSelect = true
override val shouldShowSelectAll = true
@@ -442,6 +455,7 @@ class SelectChargingRangeScreen(ctx: CarContext) : Screen(ctx) {
val nSpacers = when {
maxItems % 3 == 0 -> 1
maxItems == 100 -> 0 // AA has increased the limit to 100 and changed the way items are laid out
maxItems % 4 == 0 -> 2
else -> 0
}

View File

@@ -58,7 +58,8 @@ class VehicleDataScreen(ctx: CarContext) : Screen(ctx), DefaultLifecycleObserver
PermissionScreen(
carContext,
R.string.auto_vehicle_data_permission_needed,
permissions
permissions,
finishApp = false
)
) {
setupListeners()

View File

@@ -1,13 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_map_provider_names">
<item>Google Maps</item>
<item>OpenStreetMap (Mapbox)</item>
</string-array>
<string-array name="pref_search_provider_names">
<item>Google Maps</item>
<item>OpenStreetMap (Mapbox)</item>
</string-array>
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 15% Gebühren ab.</string>
<string name="auto_location_service">EVMap läuft unter Android Auto und nutzt dafür deinen Standort.</string>
<string name="auto_no_chargers_found">Keine Ladestationen in der Nähe gefunden</string>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile \? Soutenez son développement en envoyant un don au développeur.
\n
\nGoogle prend 15% sur chaque don.</string>
<string name="auto_location_service">EVMap fonctionne sur Android Auto et utilise votre position.</string>
<string name="open_in_app">Ouvrir dans l\'application</string>
<string name="opened_on_phone">Ouvert sur le téléphone</string>
<string name="auto_location_permission_needed">Pour exécuter EVMap sur Android Auto, vous devez autoriser l\'accès à votre emplacement.</string>
<string name="grant_on_phone">Grant au téléphone</string>
<string name="auto_prices">Prix</string>
<string name="auto_vehicle_data">Données sur le véhicule</string>
<string name="auto_range">Autonomie</string>
<string name="auto_speed">Vitesse</string>
<string name="welcome_android_auto">Prise en charge dAndroid Auto</string>
<string name="sounds_cool">ça a l\'air cool</string>
<string name="auto_chargeprice_vehicle_unknown">Aucun des véhicules sélectionnés dans l\'application ne correspond à ce véhicule (%1$s %2$s).</string>
<string name="auto_chargeprice_vehicle_ambiguous">Plusieurs véhicules sélectionnés dans l\'application correspondent à ce véhicule (%1$s %2$s).</string>
<string name="selecting_all">tous les éléments sélectionnés</string>
<string name="data_sources_hint">Dans les paramètres, vous pouvez également choisir entre Google Maps et OpenStreetMap (Mapbox) pour les données cartographiques.</string>
<string name="auto_chargeprice_vehicle_unavailable">EVMap n\'a pas pu déterminer le modèle de votre véhicule.</string>
<string name="auto_no_chargers_found">Aucun chargeur à proximité n\'a été trouvé</string>
<string name="auto_no_favorites_found">Pas de favoris trouvés</string>
<string name="auto_charging_level">Niveau de charge</string>
<string name="auto_chargers_closeby">Chargeurs à proximité</string>
<string name="auto_chargers_near_location">Près de %s</string>
<string name="auto_fault_report_date">⚠️ Rapport d\'anomalie (%s)</string>
<string name="auto_no_data">Indisponible</string>
<string name="auto_settings">Paramètres</string>
<string name="selecting_none">désélectionner tous les éléments</string>
<string name="auto_vehicle_data_permission_needed">Pour cette fonction, EVMap doit avoir accès aux données de votre véhicule.</string>
<string name="auto_heading">Direction</string>
<string name="auto_favorites">Favoris</string>
<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>
</resources>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Synes du EVMap er nyttig\? Støtt utviklingen ved å sende penger til utvikleren.
\n
\nGoogle tar 15% av alle donasjoner.</string>
<string name="auto_favorites">Favoritter</string>
<string name="auto_charging_level">Ladingsnivå</string>
<string name="auto_chargeprice_vehicle_unavailable">EVMap kunne ikke fastsette kjøretøymodellen.</string>
<string name="selecting_none">fravalgte alle elementer</string>
<string name="grant_on_phone">Innvilg på mobilenheten</string>
<string name="auto_chargers_closeby">Ladere i nærheten</string>
<string name="auto_prices">Pris</string>
<string name="auto_no_chargers_found">Ingen ladere i nærheten</string>
<string name="auto_no_favorites_found">Fant ikke noen favoritter</string>
<string name="open_in_app">Åpne i programmet</string>
<string name="auto_location_service">EVMap kjører på Android Auto og bruker posisjonen din.</string>
<string name="auto_heading">Fartsretning</string>
<string name="opened_on_phone">Åpnet på mobilenheten</string>
<string name="auto_location_permission_needed">Innvilg posisjonstilgang for å bruke EVMap på Android Auto.</string>
<string name="auto_chargers_near_location">Nær %s</string>
<string name="auto_fault_report_date">⚠️ Feilrapport (%s)</string>
<string name="auto_vehicle_data">Kjøretøydata</string>
<string name="auto_no_data">Utilgjengelig</string>
<string name="auto_speed">Hastighet</string>
<string name="auto_settings">Innstillinger</string>
<string name="auto_chargeprice_vehicle_unknown">Ingen av kjøretøyene valgt i programmet samsvarer med dette kjøretøyet (%1$s %2$s).</string>
<string name="welcome_android_auto">Android Auto-støtte</string>
<string name="auto_chargeprice_vehicle_ambiguous">Flere kjøretøy valgt i programmet samsvarer med dette kjøretøyet (%1$s %2$s).</string>
<string name="auto_vehicle_data_permission_needed">EvMap trenger tilgang til kjøretøydata for å bruke denne funksjonen.</string>
<string name="auto_no_refresh_possible">Videre oppdateringer er ikke mulig. Gå tilbake og start på ny.</string>
<string name="auto_range">Rekkevidde</string>
<string name="welcome_android_auto_detail">Du kan også bruke EVMap inne i Android Auto på bilder som støtter dette ved å velge det i Android Auto-menyen.</string>
<string name="settings_android_auto_chargeprice_range">Prissammenligning for laderekkevidde fordelt på pris</string>
<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>
</resources>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_map_provider_names">
<item>@string/pref_provider_google_maps</item>
<item>@string/pref_provider_osm_mapbox</item>
</string-array>
<string-array name="pref_map_provider_values" translatable="false">
<item>google</item>
<item>mapbox</item>
</string-array>
<string-array name="pref_search_provider_names">
<item>@string/pref_provider_google_maps</item>
<item>@string/pref_provider_osm_mapbox</item>
</string-array>
<string-array name="pref_search_provider_values" translatable="false">
<item>google</item>
<item>mapbox</item>
</string-array>
</resources>

View File

@@ -3,5 +3,5 @@
<color name="gauge_active">#00e676</color>
<color name="gauge_middle">#087f23</color>
<color name="gauge_inactive">#9e9e9e</color>
<color name="charger_100kw_dark">#fdd835</color>
<color name="charger_100kw_dark">#FBC02D</color>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pref_map_provider_default" translatable="false">google</string>
<string name="pref_search_provider_default" translatable="false">mapbox</string>
</resources>

View File

@@ -1,23 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_map_provider_names">
<item>Google Maps</item>
<item>OpenStreetMap (Mapbox)</item>
</string-array>
<string-array name="pref_map_provider_values" tranlatable="false">
<item>google</item>
<item>mapbox</item>
</string-array>
<string-array name="pref_search_provider_names">
<item>Google Maps</item>
<item>OpenStreetMap (Mapbox)</item>
</string-array>
<string-array name="pref_search_provider_values" tranlatable="false">
<item>google</item>
<item>mapbox</item>
</string-array>
<string name="pref_map_provider_default" translatable="false">google</string>
<string name="pref_search_provider_default" translatable="false">mapbox</string>
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 15% off every donation.</string>
<string name="auto_location_service">EVMap is running on Android Auto and using your location.</string>
<string name="auto_no_chargers_found">No nearby chargers found</string>
@@ -45,7 +27,7 @@
<string name="sounds_cool">sounds cool</string>
<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">Mehrere der in der App ausgewählten Fahrzeuge passen zu diesem Fahrzeug (%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="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>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="grant_on_phone">Autoriser</string>
<string name="auto_location_permission_needed">Pour exécuter EVMap sur Android Auto, vous devez autoriser l\'accès à votre emplacement.</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="auto_location_permission_needed">Du må du innvilge posisjonstilgang for å kjøre EVMap i bilen din.</string>
<string name="grant_on_phone">Tillat</string>
</resources>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="auto_location_permission_needed">To run EVMap on your car, you need to grant access to your location.</string>
<string name="grant_on_phone">Allow</string>
<string name="auto_location_permission_needed">To run EVMap on your car, you need to grant access to your location.</string>
</resources>

View File

@@ -261,17 +261,6 @@
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<!-- Override services of the com.mapzen.android.lost library with exported:false
until https://github.com/lostzen/lost/pull/270 is merged -->
<service
android:name="com.mapzen.android.lost.internal.GeofencingIntentService"
android:exported="false">
<intent-filter>
<action android:name="com.mapzen.lost.action.ACTION_GEOFENCING_SERVICE" />
</intent-filter>
</service>
</application>
</manifest>

View File

@@ -36,7 +36,6 @@ import net.vonforst.evmap.utils.LocaleContextWrapper
import net.vonforst.evmap.utils.getLocationFromIntent
const val REQUEST_LOCATION_PERMISSION = 1
const val EXTRA_CHARGER_ID = "chargerId"
const val EXTRA_LAT = "lat"
const val EXTRA_LON = "lon"
@@ -87,7 +86,7 @@ class MapsActivity : AppCompatActivity(),
ViewCompat.setOnApplyWindowInsetsListener(navView) { v, insets ->
val header = navView.getHeaderView(0)
header.setPadding(0, insets.getInsets(WindowInsetsCompat.Type.statusBars()).top, 0, 0)
WindowInsetsCompat.CONSUMED
insets
}
prefs = PreferenceDataSource(this)

View File

@@ -62,6 +62,8 @@ class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
)
}
}
is BooleanFilterValue -> {
}
}
}

View File

@@ -3,14 +3,16 @@ package net.vonforst.evmap.api.chargeprice
import android.content.Context
import com.facebook.stetho.okhttp3.StethoInterceptor
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import moe.banana.jsonapi2.ArrayDocument
import moe.banana.jsonapi2.JsonApiConverterFactory
import moe.banana.jsonapi2.ResourceAdapterFactory
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import jsonapi.Document
import jsonapi.JsonApiFactory
import jsonapi.retrofit.DocumentConverterFactory
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.model.ChargeLocation
import okhttp3.Cache
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
@@ -20,34 +22,45 @@ import java.util.*
interface ChargepriceApi {
@POST("charge_prices")
suspend fun getChargePrices(
@Body request: ChargepriceRequest,
@Body @jsonapi.retrofit.Document request: ChargepriceRequest,
@Header("Accept-Language") language: String
): ArrayDocument<ChargePrice>
): Document<List<ChargePrice>>
@GET("vehicles")
suspend fun getVehicles(): ArrayDocument<ChargepriceCar>
@jsonapi.retrofit.Document
suspend fun getVehicles(): List<ChargepriceCar>
@GET("tariffs")
suspend fun getTariffs(): ArrayDocument<ChargepriceTariff>
@jsonapi.retrofit.Document
suspend fun getTariffs(): List<ChargepriceTariff>
@POST("user_feedback")
suspend fun userFeedback(@Body @jsonapi.retrofit.Document feedback: ChargepriceUserFeedback)
companion object {
private val cacheSize = 1L * 1024 * 1024 // 1MB
val supportedLanguages = setOf("de", "en", "fr", "nl")
val DATA_SOURCE_GOINGELECTRIC = "going_electric"
val DATA_SOURCE_OPENCHARGEMAP = "open_charge_map"
private val DATA_SOURCE_GOINGELECTRIC = "going_electric"
private val DATA_SOURCE_OPENCHARGEMAP = "open_charge_map"
private val jsonApiAdapterFactory = ResourceAdapterFactory.builder()
.add(ChargepriceRequest::class.java)
.add(ChargepriceTariff::class.java)
.add(ChargepriceBrand::class.java)
.add(ChargePrice::class.java)
.add(ChargepriceCar::class.java)
private val jsonApiAdapterFactory = JsonApiFactory.Builder()
.addType(ChargepriceRequest::class.java)
.addType(ChargepriceTariff::class.java)
.addType(ChargepriceBrand::class.java)
.addType(ChargePrice::class.java)
.addType(ChargepriceCar::class.java)
.build()
val moshi = Moshi.Builder()
.add(jsonApiAdapterFactory)
.add(KotlinJsonAdapterFactory())
.add(
PolymorphicJsonAdapterFactory.of(ChargepriceUserFeedback::class.java, "type")
.withSubtype(ChargepriceMissingPriceFeedback::class.java, "missing_price")
.withSubtype(ChargepriceWrongPriceFeedback::class.java, "wrong_price")
.withSubtype(ChargepriceMissingVehicleFeedback::class.java, "missing_vehicle")
)
.build()
fun create(
apikey: String,
baseurl: String = "https://api.chargeprice.app/v1/",
@@ -73,7 +86,8 @@ interface ChargepriceApi {
val retrofit = Retrofit.Builder()
.baseUrl(baseurl)
.addConverterFactory(JsonApiConverterFactory.create(moshi))
.addConverterFactory(DocumentConverterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(client)
.build()
return retrofit.create(ChargepriceApi::class.java)
@@ -89,6 +103,15 @@ interface ChargepriceApi {
}
}
fun getPoiUrl(charger: ChargeLocation) =
"https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=${getDataAdapter(charger)}"
fun getDataAdapter(charger: ChargeLocation) = when (charger.dataSource) {
"goingelectric" -> DATA_SOURCE_GOINGELECTRIC
"openchargemap" -> DATA_SOURCE_OPENCHARGEMAP
else -> throw IllegalArgumentException()
}
@JvmStatic
fun isCountrySupported(country: String, dataSource: String): Boolean = when (dataSource) {
// list of countries updated 2021/08/24

View File

@@ -1,12 +1,12 @@
package net.vonforst.evmap.api.chargeprice
import android.content.Context
import android.os.Parcelable
import android.util.Patterns
import com.squareup.moshi.Json
import moe.banana.jsonapi2.HasMany
import moe.banana.jsonapi2.HasOne
import moe.banana.jsonapi2.JsonApi
import moe.banana.jsonapi2.Resource
import com.squareup.moshi.JsonClass
import jsonapi.*
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.api.equivalentPlugTypes
@@ -17,16 +17,21 @@ import kotlin.math.ceil
import kotlin.math.floor
@JsonApi(type = "charge_price_request")
class ChargepriceRequest : Resource() {
@field:Json(name = "data_adapter")
lateinit var dataAdapter: String
lateinit var station: ChargepriceStation
lateinit var options: ChargepriceOptions
var tariffs: HasMany<ChargepriceTariff>? = null
var vehicle: HasOne<ChargepriceCar>? = null
}
@Resource("charge_price_request")
@JsonClass(generateAdapter = true)
data class ChargepriceRequest(
@Json(name = "data_adapter")
val dataAdapter: String,
val station: ChargepriceStation,
val options: ChargepriceOptions,
@ToMany("tariffs")
val tariffs: List<ChargepriceTariff>? = null,
@ToOne("vehicle")
val vehicle: ChargepriceCar? = null,
@RelationshipsObject var relationships: Relationships? = null
)
@JsonClass(generateAdapter = true)
data class ChargepriceStation(
val longitude: Double,
val latitude: Double,
@@ -56,11 +61,13 @@ data class ChargepriceStation(
}
}
@JsonClass(generateAdapter = true)
data class ChargepriceChargepoint(
val power: Double,
val plug: String
)
@JsonClass(generateAdapter = true)
data class ChargepriceOptions(
@Json(name = "max_monthly_fees") val maxMonthlyFees: Double? = null,
val energy: Double? = null,
@@ -73,142 +80,107 @@ data class ChargepriceOptions(
@Json(name = "provider_customer_tariffs") val providerCustomerTariffs: Boolean? = null
)
@JsonApi(type = "tariff")
class ChargepriceTariff() : Resource() {
lateinit var provider: String
lateinit var name: String
@field:Json(name = "direct_payment")
var directPayment: Boolean = false
@field:Json(name = "provider_customer_tariff")
var providerCustomerTariff: Boolean = false
@field:Json(name = "supported_cuntries")
lateinit var supportedCountries: Set<String>
@field:Json(name = "charge_card_id")
lateinit var chargeCardId: String // GE charge card ID
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as ChargepriceTariff
if (provider != other.provider) return false
if (name != other.name) return false
if (directPayment != other.directPayment) return false
if (providerCustomerTariff != other.providerCustomerTariff) return false
if (supportedCountries != other.supportedCountries) return false
if (chargeCardId != other.chargeCardId) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + provider.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + directPayment.hashCode()
result = 31 * result + providerCustomerTariff.hashCode()
result = 31 * result + supportedCountries.hashCode()
result = 31 * result + chargeCardId.hashCode()
return result
}
@Resource("tariff")
@Parcelize
@JsonClass(generateAdapter = true)
data class ChargepriceTariff(
@Id val id_: String?,
val provider: String,
val name: String,
@Json(name = "direct_payment")
val directPayment: Boolean = false,
@Json(name = "provider_customer_tariff")
val providerCustomerTariff: Boolean = false,
@Json(name = "supported_countries")
val supportedCountries: Set<String>,
@Json(name = "charge_card_id")
val chargeCardId: String?, // GE charge card ID
) : Parcelable {
val id: String
get() = id_!!
}
@JsonApi(type = "car")
class ChargepriceCar : Resource(), Equatable {
lateinit var name: String
lateinit var brand: String
@JsonClass(generateAdapter = true)
@Resource("car")
@Parcelize
data class ChargepriceCar(
@Id val id_: String?,
val name: String,
val brand: String,
@field:Json(name = "dc_charge_ports")
lateinit var dcChargePorts: List<String>
lateinit var manufacturer: HasOne<ChargepriceBrand>
@Json(name = "dc_charge_ports")
val dcChargePorts: List<String>
) : Equatable, Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as ChargepriceCar
if (name != other.name) return false
if (brand != other.brand) return false
if (dcChargePorts != other.dcChargePorts) return false
if (manufacturer != other.manufacturer) return false
return true
companion object {
private val acConnectors = listOf(
Chargepoint.CEE_BLAU,
Chargepoint.CEE_ROT,
Chargepoint.SCHUKO,
Chargepoint.TYPE_1,
Chargepoint.TYPE_2_UNKNOWN,
Chargepoint.TYPE_2_SOCKET,
Chargepoint.TYPE_2_PLUG
)
private val plugMapping = mapOf(
"ccs" to Chargepoint.CCS_UNKNOWN,
"tesla_suc" to Chargepoint.SUPERCHARGER,
"tesla_ccs" to Chargepoint.CCS_UNKNOWN,
"chademo" to Chargepoint.CHADEMO
)
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + brand.hashCode()
result = 31 * result + dcChargePorts.hashCode()
result = 31 * result + manufacturer.hashCode()
return result
}
val id: String
get() = id_!!
private val acConnectors = listOf(
Chargepoint.CEE_BLAU,
Chargepoint.CEE_ROT,
Chargepoint.SCHUKO,
Chargepoint.TYPE_1,
Chargepoint.TYPE_2_UNKNOWN,
Chargepoint.TYPE_2_SOCKET,
Chargepoint.TYPE_2_PLUG
)
private val plugMapping = mapOf(
"ccs" to Chargepoint.CCS_UNKNOWN,
"tesla_suc" to Chargepoint.SUPERCHARGER,
"tesla_ccs" to Chargepoint.CCS_UNKNOWN,
"chademo" to Chargepoint.CHADEMO
)
val compatibleEvmapConnectors: List<String>
get() = dcChargePorts.map {
plugMapping[it]
}.filterNotNull().plus(acConnectors)
}
@JsonApi(type = "brand")
class ChargepriceBrand : Resource()
@JsonClass(generateAdapter = true)
@Resource("brand")
@Parcelize
data class ChargepriceBrand(
@Id val id: String?
) : Parcelable
@JsonApi(type = "charge_price")
class ChargePrice : Resource(), Equatable, Cloneable {
lateinit var provider: String
@JsonClass(generateAdapter = true)
@Resource("charge_price")
@Parcelize
data class ChargePrice(
val provider: String,
@Json(name = "tariff_name")
val tariffName: String,
val url: String,
@Json(name = "monthly_min_sales")
val monthlyMinSales: Double = 0.0,
@Json(name = "total_monthly_fee")
val totalMonthlyFee: Double = 0.0,
@Json(name = "flat_rate")
val flatRate: Boolean = false,
@field:Json(name = "tariff_name")
lateinit var tariffName: String
lateinit var url: String
@Json(name = "direct_payment")
val directPayment: Boolean = false,
@field:Json(name = "monthly_min_sales")
var monthlyMinSales: Double = 0.0
@Json(name = "provider_customer_tariff")
val providerCustomerTariff: Boolean = false,
val currency: String,
@field:Json(name = "total_monthly_fee")
var totalMonthlyFee: Double = 0.0
@Json(name = "start_time")
val startTime: Int = 0,
val tags: List<ChargepriceTag>,
@field:Json(name = "flat_rate")
var flatRate: Boolean = false
@field:Json(name = "direct_payment")
var directPayment: Boolean = false
@field:Json(name = "provider_customer_tariff")
var providerCustomerTariff: Boolean = false
lateinit var currency: String
@field:Json(name = "start_time")
var startTime: Int = 0
lateinit var tags: List<ChargepriceTag>
@field:Json(name = "charge_point_prices")
lateinit var chargepointPrices: List<ChargepointPrice>
@field:Json(name = "branding")
var branding: ChargepriceBranding? = null
var tariff: HasOne<ChargepriceTariff>? = null
@Json(name = "charge_point_prices")
val chargepointPrices: List<ChargepointPrice>,
@Json(name = "branding")
val branding: ChargepriceBranding? = null,
@ToOne("tariff")
val tariffId: String?
) : Equatable, Cloneable, Parcelable {
fun formatMonthlyFees(ctx: Context): String {
return listOfNotNull(
if (totalMonthlyFee > 0) {
@@ -219,69 +191,10 @@ class ChargePrice : Resource(), Equatable, Cloneable {
} else null
).joinToString(", ")
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
other as ChargePrice
if (provider != other.provider) return false
if (tariffName != other.tariffName) return false
if (url != other.url) return false
if (monthlyMinSales != other.monthlyMinSales) return false
if (totalMonthlyFee != other.totalMonthlyFee) return false
if (flatRate != other.flatRate) return false
if (directPayment != other.directPayment) return false
if (providerCustomerTariff != other.providerCustomerTariff) return false
if (currency != other.currency) return false
if (startTime != other.startTime) return false
if (tags != other.tags) return false
if (chargepointPrices != other.chargepointPrices) return false
if (branding != other.branding) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + provider.hashCode()
result = 31 * result + tariffName.hashCode()
result = 31 * result + url.hashCode()
result = 31 * result + monthlyMinSales.hashCode()
result = 31 * result + totalMonthlyFee.hashCode()
result = 31 * result + flatRate.hashCode()
result = 31 * result + directPayment.hashCode()
result = 31 * result + providerCustomerTariff.hashCode()
result = 31 * result + currency.hashCode()
result = 31 * result + startTime
result = 31 * result + tags.hashCode()
result = 31 * result + chargepointPrices.hashCode()
result = 31 * result + branding.hashCode()
return result
}
public override fun clone(): ChargePrice {
return ChargePrice().apply {
chargepointPrices = this@ChargePrice.chargepointPrices
currency = this@ChargePrice.currency
directPayment = this@ChargePrice.directPayment
flatRate = this@ChargePrice.flatRate
monthlyMinSales = this@ChargePrice.monthlyMinSales
provider = this@ChargePrice.provider
providerCustomerTariff = this@ChargePrice.providerCustomerTariff
startTime = this@ChargePrice.startTime
tags = this@ChargePrice.tags
tariffName = this@ChargePrice.tariffName
totalMonthlyFee = this@ChargePrice.totalMonthlyFee
url = this@ChargePrice.url
tariff = this@ChargePrice.tariff
branding = this@ChargePrice.branding
}
}
}
@JsonClass(generateAdapter = true)
@Parcelize
data class ChargepointPrice(
val power: Double,
val plug: String,
@@ -289,7 +202,7 @@ data class ChargepointPrice(
@Json(name = "price_distribution") val priceDistribution: PriceDistribution,
@Json(name = "blocking_fee_start") val blockingFeeStart: Int?,
@Json(name = "no_price_reason") var noPriceReason: String?
) {
) : Parcelable {
fun formatDistribution(ctx: Context): String {
fun percent(value: Double): String {
return ctx.getString(R.string.percent_format, value * 100) + "\u00a0"
@@ -332,19 +245,28 @@ data class ChargepointPrice(
}
}
@JsonClass(generateAdapter = true)
@Parcelize
data class ChargepriceBranding(
@Json(name = "background_color") val backgroundColor: String,
@Json(name = "text_color") val textColor: String,
@Json(name = "logo_url") val logoUrl: String
)
) : Parcelable
data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) {
val isOnlyKwh =
kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0)
@JsonClass(generateAdapter = true)
@Parcelize
data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) :
Parcelable {
val isOnlyKwh
get() = kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0)
}
data class ChargepriceTag(val kind: String, val text: String, val url: String?) : Equatable
@JsonClass(generateAdapter = true)
@Parcelize
data class ChargepriceTag(val kind: String, val text: String, val url: String?) : Equatable,
Parcelable
@JsonClass(generateAdapter = true)
data class ChargepriceMeta(
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepointMeta>
)
@@ -358,13 +280,97 @@ enum class ChargepriceInclude {
EXCLUSIVE
}
@JsonClass(generateAdapter = true)
@Parcelize
data class ChargepriceRequestTariffMeta(
val include: ChargepriceInclude
)
) : Parcelable
@JsonClass(generateAdapter = true)
data class ChargepriceChargepointMeta(
val power: Double,
val plug: String,
val energy: Double,
val duration: Double
)
)
@Resource("user_feedback")
sealed class ChargepriceUserFeedback(
val notes: String,
val email: String,
val context: String,
val language: String
) {
init {
if (email.isBlank() || email.length > 100 || !Patterns.EMAIL_ADDRESS.matcher(email)
.matches()
) {
throw IllegalArgumentException("invalid email")
}
if (!ChargepriceApi.supportedLanguages.contains(language)) {
throw IllegalArgumentException("invalid language")
}
if (context.length > 500) throw IllegalArgumentException("invalid context")
if (notes.length > 1000) throw IllegalArgumentException("invalid notes")
}
}
@JsonClass(generateAdapter = true)
@Resource(type = "missing_price")
class ChargepriceMissingPriceFeedback(
val tariff: String,
val cpo: String,
val price: String,
@Json(name = "poi_link") val poiLink: String,
notes: String,
email: String,
context: String,
language: String
) : ChargepriceUserFeedback(notes, email, context, language) {
init {
if (tariff.isBlank() || tariff.length > 100) throw IllegalArgumentException("invalid tariff")
if (cpo.length > 200) throw IllegalArgumentException("invalid cpo")
if (price.isBlank() || price.length > 100) throw IllegalArgumentException("invalid price")
if (poiLink.isBlank() || poiLink.length > 200) throw IllegalArgumentException("invalid poiLink")
}
}
@JsonClass(generateAdapter = true)
@Resource(type = "wrong_price")
class ChargepriceWrongPriceFeedback(
val tariff: String,
val cpo: String,
@Json(name = "displayed_price") val displayedPrice: String,
@Json(name = "actual_price") val actualPrice: String,
@Json(name = "poi_link") val poiLink: String,
notes: String,
email: String,
context: String,
language: String,
) : ChargepriceUserFeedback(notes, email, context, language) {
init {
if (tariff.length > 100) throw IllegalArgumentException("invalid tariff")
if (cpo.length > 200) throw IllegalArgumentException("invalid cpo")
if (displayedPrice.length > 100) throw IllegalArgumentException("invalid displayedPrice")
if (actualPrice.length > 100) throw IllegalArgumentException("invalid actualPrice")
if (poiLink.length > 200) throw IllegalArgumentException("invalid poiLink")
}
}
@JsonClass(generateAdapter = true)
@Resource(type = "missing_vehicle")
class ChargepriceMissingVehicleFeedback(
val brand: String,
val model: String,
notes: String,
email: String,
context: String,
language: String,
) : ChargepriceUserFeedback(notes, email, context, language) {
init {
if (brand.length > 100) throw IllegalArgumentException("invalid brand")
if (model.length > 100) throw IllegalArgumentException("invalid model")
}
}

View File

@@ -467,17 +467,17 @@ class GoingElectricApiWrapper(
sp.getString(R.string.filter_networks), "networks",
networkMap, manyChoices = true
),
MultipleChoiceFilter(
sp.getString(R.string.categories), "categories",
categoryMap,
manyChoices = true
),
BooleanFilter(sp.getString(R.string.filter_exclude_faults), "exclude_faults"),
BooleanFilter(sp.getString(R.string.filter_barrierfree), "barrierfree"),
MultipleChoiceFilter(
sp.getString(R.string.filter_chargecards), "chargecards",
chargecardMap, manyChoices = true
),
BooleanFilter(sp.getString(R.string.filter_exclude_faults), "exclude_faults")
MultipleChoiceFilter(
sp.getString(R.string.categories), "categories",
categoryMap,
manyChoices = true
)
)
}
}

View File

@@ -6,7 +6,6 @@ import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
@@ -17,6 +16,7 @@ import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.MaterialContainerTransform
import net.vonforst.evmap.MapsActivity
@@ -24,6 +24,7 @@ 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.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
@@ -31,7 +32,7 @@ import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.viewModelFactory
import net.vonforst.evmap.viewmodel.savedStateViewModelFactory
import java.text.NumberFormat
class ChargepriceFragment : Fragment() {
@@ -39,10 +40,12 @@ class ChargepriceFragment : Fragment() {
private var connectionErrorSnackbar: Snackbar? = null
private val vm: ChargepriceViewModel by viewModels(factoryProducer = {
viewModelFactory {
savedStateViewModelFactory { state ->
ChargepriceViewModel(
requireActivity().application,
getString(R.string.chargeprice_key)
getString(R.string.chargeprice_key),
getString(R.string.chargeprice_api_url),
state
)
}
})
@@ -60,8 +63,13 @@ class ChargepriceFragment : Fragment() {
}
}
override fun onResume() {
super.onResume()
vm.reloadPrefs()
}
private fun showDonationDialog() {
AlertDialog.Builder(requireContext())
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.chargeprice_donation_dialog_title)
.setMessage(R.string.chargeprice_donation_dialog_detail)
.setNegativeButton(R.string.ok) { di, _ ->
@@ -103,9 +111,7 @@ class ChargepriceFragment : Fragment() {
val fragmentArgs: ChargepriceFragmentArgs by navArgs()
val charger = fragmentArgs.charger
val dataSource = fragmentArgs.dataSource
vm.charger.value = charger
vm.dataSource.value = dataSource
if (vm.chargepoint.value == null) {
vm.chargepoint.value = charger.chargepointsMerged.get(0)
}
@@ -170,7 +176,7 @@ class ChargepriceFragment : Fragment() {
}
binding.imgChargepriceLogo.setOnClickListener {
(requireActivity() as MapsActivity).openUrl("https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=${dataSource}")
(requireActivity() as MapsActivity).openUrl(ChargepriceApi.getPoiUrl(charger))
}
binding.btnSettings.setOnClickListener {

View File

@@ -4,13 +4,12 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialogFragment
import net.vonforst.evmap.databinding.DialogDataSourceSelectBinding
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.storage.PreferenceDataSource
import java.util.*
import net.vonforst.evmap.ui.MaterialDialogFragment
class DataSourceSelectDialog : AppCompatDialogFragment() {
class DataSourceSelectDialog : MaterialDialogFragment() {
private lateinit var binding: DialogDataSourceSelectBinding
var okListener: ((String) -> Unit)? = null
@@ -41,16 +40,12 @@ class DataSourceSelectDialog : AppCompatDialogFragment() {
override fun onStart() {
super.onStart()
// dialog with 95% screen height
dialog?.window?.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
(resources.displayMetrics.heightPixels * 0.95).toInt()
)
setFullSize()
}
private lateinit var prefs: PreferenceDataSource
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
override fun initView(view: View, savedInstanceState: Bundle?) {
val args = requireArguments()
binding.btnCancel.visibility =
if (args.getBoolean("cancel_enabled")) View.VISIBLE else View.GONE

View File

@@ -20,23 +20,23 @@ import com.car2go.maps.model.LatLng
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.MaterialFadeThrough
import com.mapzen.android.lost.api.LocationServices
import com.mapzen.android.lost.api.LostApiClient
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.DataBindingAdapter
import net.vonforst.evmap.adapter.FavoritesAdapter
import net.vonforst.evmap.databinding.FragmentFavoritesBinding
import net.vonforst.evmap.databinding.ItemFavoriteBinding
import net.vonforst.evmap.location.FusionEngine
import net.vonforst.evmap.location.LocationEngine
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.model.FavoriteWithDetail
import net.vonforst.evmap.utils.checkAnyLocationPermission
import net.vonforst.evmap.viewmodel.FavoritesViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
class FavoritesFragment : Fragment() {
private lateinit var binding: FragmentFavoritesBinding
private var locationClient: LostApiClient? = null
private lateinit var locationEngine: LocationEngine
private var toDelete: Favorite? = null
private var deleteSnackbar: Snackbar? = null
private lateinit var adapter: FavoritesAdapter
@@ -52,8 +52,8 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
locationClient = LostApiClient.Builder(requireContext())
.addConnectionCallbacks(this).build()
locationEngine = FusionEngine(requireContext())
enterTransition = MaterialFadeThrough()
exitTransition = MaterialFadeThrough()
@@ -109,8 +109,6 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
}
createTouchHelper().attachToRecyclerView(binding.favsList)
locationClient!!.connect()
binding.swipeRefresh.setOnRefreshListener {
vm.reloadAvailability() {
binding.swipeRefresh.isRefreshing = false
@@ -118,27 +116,17 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
}
}
override fun onConnected() {
val context = this.context ?: return
if (context.checkAnyLocationPermission()) {
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient!!)
if (location != null) {
vm.location.value = LatLng(location.latitude, location.longitude)
override fun onStart() {
super.onStart()
if (requireContext().checkAnyLocationPermission()) {
val location = locationEngine.getLastKnownLocation()
location?.let {
vm.location.value = LatLng(it.latitude, it.longitude)
}
}
}
override fun onConnectionSuspended() {
}
override fun onDestroy() {
super.onDestroy()
locationClient?.let {
if (it.isConnected) it.disconnect()
}
}
fun delete(fav: FavoriteWithDetail) {
val position =
vm.listData.value?.indexOfFirst { it.fav.favorite.favoriteId == fav.favorite.favoriteId }

View File

@@ -3,9 +3,11 @@ package net.vonforst.evmap.fragment
import android.os.Bundle
import android.view.*
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuProvider
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
@@ -22,7 +24,7 @@ import net.vonforst.evmap.ui.showEditTextDialog
import net.vonforst.evmap.viewmodel.FilterViewModel
class FilterFragment : Fragment() {
class FilterFragment : Fragment(), MenuProvider {
private lateinit var binding: FragmentFilterBinding
private val vm: FilterViewModel by viewModels()
@@ -40,9 +42,6 @@ class FilterFragment : Fragment() {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_filter, container, false)
binding.lifecycleOwner = this
binding.vm = vm
setHasOptionsMenu(true)
vm.filterProfile.observe(viewLifecycleOwner) {}
return binding.root
@@ -50,6 +49,7 @@ class FilterFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
(requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar)
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
binding.toolbar.setupWithNavController(
findNavController(),
@@ -81,12 +81,11 @@ class FilterFragment : Fragment() {
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.filter, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
override fun onMenuItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_apply -> {
lifecycleScope.launch {
@@ -99,7 +98,7 @@ class FilterFragment : Fragment() {
saveProfile()
true
}
else -> super.onOptionsItemSelected(item)
else -> false
}
}

View File

@@ -4,11 +4,9 @@ import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Color
import android.location.Geocoder
import android.location.Location
import android.os.Bundle
import android.text.method.KeyListener
import android.view.*
@@ -18,11 +16,11 @@ import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresPermission
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.PopupMenu
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.location.LocationListenerCompat
import androidx.core.view.*
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
@@ -51,6 +49,7 @@ import com.car2go.maps.model.BitmapDescriptor
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.Marker
import com.car2go.maps.model.MarkerOptions
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.MaterialArcMotion
import com.google.android.material.transition.MaterialContainerTransform
@@ -60,26 +59,27 @@ import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
import com.mapzen.android.lost.api.LocationListener
import com.mapzen.android.lost.api.LocationRequest
import com.mapzen.android.lost.api.LocationServices
import com.mapzen.android.lost.api.LostApiClient
import com.stfalcon.imageviewer.StfalconImageViewer
import io.michaelrocks.bimap.HashBiMap
import io.michaelrocks.bimap.MutableBiMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.*
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
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.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.autocomplete.ApiUnavailableException
import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.bold
import net.vonforst.evmap.databinding.FragmentMapBinding
import net.vonforst.evmap.location.FusionEngine
import net.vonforst.evmap.location.LocationEngine
import net.vonforst.evmap.location.Priority
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.*
@@ -95,14 +95,13 @@ import kotlin.collections.contains
import kotlin.collections.set
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback,
LostApiClient.ConnectionCallbacks, LocationListener {
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback, MenuProvider {
private lateinit var binding: FragmentMapBinding
private val vm: MapViewModel by viewModels()
private val galleryVm: GalleryViewModel by activityViewModels()
private var mapFragment: MapFragment? = null
private var map: AnyMap? = null
private lateinit var locationClient: LostApiClient
private lateinit var locationEngine: LocationEngine
private var requestingLocationUpdates = false
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior
@@ -147,10 +146,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
prefs = PreferenceDataSource(requireContext())
locationClient = LostApiClient.Builder(requireContext())
.addConnectionCallbacks(this)
.build()
locationClient.connect()
locationEngine = FusionEngine(requireContext())
clusterIconGenerator = ClusterIconGenerator(requireContext())
enterTransition = MaterialFadeThrough()
@@ -197,8 +193,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
searchResultIcon = null
}
setHasOptionsMenu(true)
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { v, insets ->
@@ -245,6 +239,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
mapFragment!!.getMapAsync(this)
bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet)
detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
@@ -319,19 +315,25 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
vm.reloadPrefs()
if (requestingLocationUpdates && requireContext().checkAnyLocationPermission()
&& locationClient.isConnected
) {
requestLocationUpdates()
}
}
@SuppressLint("MissingPermission")
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
val context = context ?: return@registerForActivityResult
if (context.checkAnyLocationPermission()) {
enableLocation(moveTo = true, animate = true)
}
}
private fun setupClickListeners() {
binding.fabLocate.setOnClickListener {
if (!requireContext().checkFineLocationPermission()) {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION),
REQUEST_LOCATION_PERMISSION
requestPermissionLauncher.launch(
arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)
)
}
if (requireContext().checkAnyLocationPermission()) {
@@ -360,16 +362,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
binding.detailView.btnChargeprice.setOnClickListener {
val charger = vm.charger.value?.data ?: return@setOnClickListener
val dataSource = when (vm.apiType) {
GoingElectricApiWrapper::class.java -> "going_electric"
OpenChargeMapApiWrapper::class.java -> "open_charge_map"
else -> throw IllegalArgumentException("unsupported data source")
}
val extras =
FragmentNavigatorExtras(binding.detailView.btnChargeprice to getString(R.string.shared_element_chargeprice))
findNavController().navigate(
R.id.action_map_to_chargepriceFragment,
ChargepriceFragmentArgs(charger, dataSource).toBundle(),
ChargepriceFragmentArgs(charger).toBundle(),
null, extras
)
}
@@ -602,35 +599,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
updateFavoriteToggle()
})
vm.searchResult.observe(viewLifecycleOwner, Observer { place ->
val map = this.map ?: return@Observer
searchResultMarker?.remove()
searchResultMarker = null
if (place != null) {
// disable location following when search result is shown
vm.myLocationEnabled.value = false
if (place.viewport != null) {
map.animateCamera(map.cameraUpdateFactory.newLatLngBounds(place.viewport, 0))
} else {
map.animateCamera(map.cameraUpdateFactory.newLatLngZoom(place.latLng, 12f))
}
if (searchResultIcon == null) {
searchResultIcon =
map.bitmapDescriptorFactory.fromResource(R.drawable.ic_map_marker)
}
searchResultMarker = map.addMarker(
MarkerOptions()
.z(placeSearchZ)
.position(place.latLng)
.icon(searchResultIcon)
.anchor(0.5f, 1f)
)
} else {
binding.search.setText("")
}
updateBackPressedCallback()
displaySearchResult(place, moveCamera = true)
})
vm.layersMenuOpen.observe(viewLifecycleOwner, Observer { open ->
binding.fabLayers.visibility = if (open) View.INVISIBLE else View.VISIBLE
@@ -647,6 +616,40 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
updateBackPressedCallback()
}
private fun displaySearchResult(place: PlaceWithBounds?, moveCamera: Boolean) {
val map = this.map ?: return
searchResultMarker?.remove()
searchResultMarker = null
if (place != null) {
// disable location following when search result is shown
if (moveCamera) {
vm.myLocationEnabled.value = false
if (place.viewport != null) {
map.animateCamera(map.cameraUpdateFactory.newLatLngBounds(place.viewport, 0))
} else {
map.animateCamera(map.cameraUpdateFactory.newLatLngZoom(place.latLng, 12f))
}
}
if (searchResultIcon == null) {
searchResultIcon =
map.bitmapDescriptorFactory.fromResource(R.drawable.ic_map_marker)
}
searchResultMarker = map.addMarker(
MarkerOptions()
.z(placeSearchZ)
.position(place.latLng)
.icon(searchResultIcon)
.anchor(0.5f, 1f)
)
} else {
binding.search.setText("")
}
updateBackPressedCallback()
}
private fun updateBackPressedCallback() {
backPressedCallback.isEnabled =
vm.bottomSheetState.value != null && vm.bottomSheetState.value != STATE_HIDDEN
@@ -735,6 +738,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
}
.withStartPosition(position)
.withHiddenStatusBar(false)
.show()
}
@@ -807,7 +811,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
it.name
}
}
AlertDialog.Builder(activity)
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.charge_cards)
.setItems(names.toTypedArray()) { _, i ->
val card = data[i]
@@ -1005,7 +1009,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
if (vm.searchResult.value != null) {
// show search result (after configuration change)
vm.searchResult.postValue(vm.searchResult.value)
displaySearchResult(vm.searchResult.value, moveCamera = !positionSet)
}
}
@@ -1016,16 +1020,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
map.uiSettings.setMyLocationButtonEnabled(false)
if (moveTo) {
vm.myLocationEnabled.value = true
if (locationClient.isConnected) {
moveToLastLocation(map, animate)
requestLocationUpdates()
}
moveToLastLocation(map, animate)
requestLocationUpdates()
}
}
@RequiresPermission(anyOf = [ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION])
private fun moveToLastLocation(map: AnyMap, animate: Boolean) {
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient)
val location = locationEngine.getLastKnownLocation()
if (location != null) {
val latLng = LatLng(location.latitude, location.longitude)
vm.location.value = latLng
@@ -1142,23 +1144,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
}
@SuppressLint("MissingPermission")
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
when (requestCode) {
REQUEST_LOCATION_PERMISSION -> {
if ((grantResults.isNotEmpty() && grantResults.any { it == PackageManager.PERMISSION_GRANTED })) {
enableLocation(moveTo = true, animate = true)
}
}
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.map, menu)
val filterItem = menu.findItem(R.id.menu_filter)
@@ -1273,6 +1259,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
})
})
popup.setTouchModal(false)
popup.show()
}
@@ -1296,42 +1283,35 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return false
}
override fun getRootView(): View {
return binding.root
}
override fun onConnected() {
val map = this.map ?: return
val context = this.context ?: return
if (vm.myLocationEnabled.value == true) {
if (context.checkAnyLocationPermission()) {
moveToLastLocation(map, false)
requestLocationUpdates()
}
}
}
@RequiresPermission(ACCESS_FINE_LOCATION)
@RequiresPermission(anyOf = [ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION])
private fun requestLocationUpdates() {
val request: LocationRequest = LocationRequest.create()
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
.setInterval(5000)
LocationServices.FusedLocationApi.requestLocationUpdates(locationClient, request, this)
locationEngine.requestLocationUpdates(
Priority.HIGH_ACCURACY,
5000,
locationListener
)
requestingLocationUpdates = true
}
@SuppressLint("MissingPermission")
private fun removeLocationUpdates() {
if (locationClient.isConnected) {
LocationServices.FusedLocationApi.removeLocationUpdates(locationClient, this)
if (context?.checkAnyLocationPermission() == true) {
locationEngine.removeUpdates(locationListener)
}
}
override fun onConnectionSuspended() {
}
override fun onLocationChanged(location: Location?) {
val map = this.map ?: return
if (location == null || vm.myLocationEnabled.value == false) return
private val locationListener = LocationListenerCompat { location ->
val map = this.map ?: return@LocationListenerCompat
if (vm.myLocationEnabled.value == false) return@LocationListenerCompat
val latLng = LatLng(location.latitude, location.longitude)
val oldLoc = vm.location.value
@@ -1356,8 +1336,5 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
override fun onDestroy() {
super.onDestroy()
if (locationClient.isConnected) {
locationClient.disconnect()
}
}
}

View File

@@ -4,20 +4,16 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.LinearLayoutManager
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.DataBindingAdapter
import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.databinding.DialogMultiSelectBinding
import net.vonforst.evmap.ui.MaterialDialogFragment
import java.util.*
import kotlin.collections.HashMap
import kotlin.collections.HashSet
import kotlin.math.roundToInt
class MultiSelectDialog : AppCompatDialogFragment() {
class MultiSelectDialog : MaterialDialogFragment() {
companion object {
fun getInstance(
title: String,
@@ -54,19 +50,10 @@ class MultiSelectDialog : AppCompatDialogFragment() {
override fun onStart() {
super.onStart()
val density = resources.displayMetrics.density
val width = resources.displayMetrics.widthPixels
val maxWidth = (500 * density).roundToInt()
// dialog with 95% screen height
dialog?.window?.setLayout(
if (width < maxWidth) WindowManager.LayoutParams.MATCH_PARENT else maxWidth,
(resources.displayMetrics.heightPixels * 0.95).toInt()
)
setFullSize(maxWidthDp = 500)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
override fun initView(view: View, savedInstanceState: Bundle?) {
val args = requireArguments()
val data = args.getSerializable("data") as HashMap<String, String>
val selected = args.getSerializable("selected") as HashSet<String>

View File

@@ -16,7 +16,8 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
viewModelFactory {
SettingsViewModel(
requireActivity().application,
getString(R.string.chargeprice_key)
getString(R.string.chargeprice_key),
getString(R.string.chargeprice_api_url)
)
}
})
@@ -69,7 +70,8 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
R.plurals.chargeprice_some_tariffs_selected,
n,
n
) + "\n" + getString(R.string.pref_my_tariffs_summary)
) + "\n" + requireContext().resources
.getQuantityString(R.plurals.pref_my_tariffs_summary, n)
}
}

View File

@@ -17,7 +17,8 @@ class DataSettingsFragment : BaseSettingsFragment() {
viewModelFactory {
SettingsViewModel(
requireActivity().application,
getString(R.string.chargeprice_key)
getString(R.string.chargeprice_key),
getString(R.string.chargeprice_api_url)
)
}
})

View File

@@ -4,15 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.navigation.fragment.findNavController
import net.vonforst.evmap.R
import net.vonforst.evmap.databinding.DialogOpensourceDonationsBinding
import net.vonforst.evmap.storage.PreferenceDataSource
import kotlin.math.roundToInt
import net.vonforst.evmap.ui.MaterialDialogFragment
class OpensourceDonationsDialogFramgent : AppCompatDialogFragment() {
class OpensourceDonationsDialogFragment : MaterialDialogFragment() {
private lateinit var binding: DialogOpensourceDonationsBinding
override fun onCreateView(
@@ -24,9 +22,7 @@ class OpensourceDonationsDialogFramgent : AppCompatDialogFragment() {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
override fun initView(view: View, savedInstanceState: Bundle?) {
val prefs = PreferenceDataSource(requireContext())
binding.btnOk.setOnClickListener {
prefs.opensourceDonationsDialogShown = true
@@ -44,14 +40,5 @@ class OpensourceDonationsDialogFramgent : AppCompatDialogFragment() {
override fun onStart() {
super.onStart()
val density = resources.displayMetrics.density
val width = resources.displayMetrics.widthPixels
val maxWidth = (500 * density).roundToInt()
dialog?.window?.setLayout(
if (width < maxWidth) WindowManager.LayoutParams.MATCH_PARENT else maxWidth,
WindowManager.LayoutParams.WRAP_CONTENT
)
}
}

View File

@@ -0,0 +1,271 @@
package net.vonforst.evmap.location
import android.Manifest
import android.content.Context
import android.location.Location
import android.location.LocationManager
import android.os.Build
import android.os.SystemClock
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.RequiresPermission
import androidx.core.location.LocationListenerCompat
/**
* Location engine that fuses GPS and network locations.
*
* Simplified version of
* https://github.com/lostzen/lost/blob/master/lost/src/main/java/com/mapzen/android/lost/internal/FusionEngine.java
*/
class FusionEngine(context: Context) : LocationEngine(context),
LocationListenerCompat {
/**
* Location updates more than 60 seconds old are considered stale.
*/
private val RECENT_UPDATE_THRESHOLD_IN_MILLIS = (60 * 1000).toLong()
private val RECENT_UPDATE_THRESHOLD_IN_NANOS = RECENT_UPDATE_THRESHOLD_IN_MILLIS * 1000000
private val TAG = FusionEngine::class.java.simpleName
private val locationManager =
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private var gpsLocation: Location? = null
private var networkLocation: Location? = null
private val supportsSystemFusedProvider: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && locationManager.allProviders.contains(
LocationManager.FUSED_PROVIDER
)
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun getLastKnownLocation(): Location? {
if (supportsSystemFusedProvider) {
try {
return locationManager.getLastKnownLocation(LocationManager.FUSED_PROVIDER)
} catch (e: SecurityException) {
Log.e(TAG, "Permissions not granted for fused provider", e)
}
}
val minTime = SystemClock.elapsedRealtimeNanos() - RECENT_UPDATE_THRESHOLD_IN_NANOS
var bestLocation: Location? = null
var bestAccuracy = Float.MAX_VALUE
var bestTime = Long.MIN_VALUE
for (provider in locationManager.allProviders) {
try {
val location = locationManager.getLastKnownLocation(provider)
if (location != null) {
val accuracy = location.accuracy
val time = location.elapsedRealtimeNanos
if (time > minTime && accuracy < bestAccuracy) {
bestLocation = location
bestAccuracy = accuracy
bestTime = time
} else if (time < minTime && bestAccuracy == Float.MAX_VALUE && time > bestTime) {
bestLocation = location
bestTime = time
}
}
} catch (e: SecurityException) {
Log.e(TAG, "Permissions not granted for provider: $provider", e)
}
}
return bestLocation
}
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun enable() {
var networkInterval = Long.MAX_VALUE
var gpsInterval = Long.MAX_VALUE
var passiveInterval = Long.MAX_VALUE
for ((priority, interval) in requests) {
when (priority) {
Priority.HIGH_ACCURACY -> {
if (interval < gpsInterval) {
gpsInterval = interval
}
if (interval < networkInterval) {
networkInterval = interval
}
}
Priority.BALANCED_POWER_ACCURACY, Priority.LOW_POWER -> if (interval < networkInterval) {
networkInterval = interval
}
Priority.NO_POWER -> if (interval < passiveInterval) {
passiveInterval = interval
}
}
}
if (supportsSystemFusedProvider && gpsInterval < Long.MAX_VALUE) {
try {
enableFused(gpsInterval)
checkLastKnownFused()
return
} catch (e: SecurityException) {
Log.e(TAG, "Permissions not granted for fused provider", e)
}
}
var checkGps = false
if (gpsInterval < Long.MAX_VALUE) {
enableGps(gpsInterval)
checkGps = true
}
if (networkInterval < Long.MAX_VALUE) {
enableNetwork(networkInterval)
if (checkGps) {
val lastGps = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
val lastNetwork =
locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
if (lastGps != null && lastNetwork != null) {
val useGps = lastGps.isBetterThan(lastNetwork)
if (useGps) {
checkLastKnownGps()
} else {
checkLastKnownNetwork()
}
} else if (lastGps != null) {
checkLastKnownGps()
} else {
checkLastKnownNetwork()
}
} else {
checkLastKnownNetwork()
}
}
if (passiveInterval < Long.MAX_VALUE) {
enablePassive(passiveInterval)
checkLastKnownPassive()
}
}
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun disable() {
locationManager.removeUpdates(this)
}
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
private fun enableGps(interval: Long) {
try {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
interval,
0f,
this,
looper
)
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Unable to register for GPS updates.", e)
}
}
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
private fun enableNetwork(interval: Long) {
try {
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
interval,
0f,
this,
looper
)
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Unable to register for network updates.", e)
}
}
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
private fun enablePassive(interval: Long) {
try {
locationManager.requestLocationUpdates(
LocationManager.PASSIVE_PROVIDER,
interval,
0f,
this,
looper
)
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Unable to register for passive updates.", e)
}
}
@RequiresApi(Build.VERSION_CODES.S)
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
private fun enableFused(interval: Long) {
try {
locationManager.requestLocationUpdates(
LocationManager.FUSED_PROVIDER,
interval,
0f,
this,
looper
)
} catch (e: IllegalArgumentException) {
Log.e(TAG, "Unable to register for passive updates.", e)
}
}
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
private fun checkLastKnownGps() {
checkLastKnownAndNotify(LocationManager.GPS_PROVIDER)
}
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
private fun checkLastKnownNetwork() {
checkLastKnownAndNotify(LocationManager.NETWORK_PROVIDER)
}
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
private fun checkLastKnownPassive() {
checkLastKnownAndNotify(LocationManager.PASSIVE_PROVIDER)
}
@RequiresApi(Build.VERSION_CODES.S)
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
private fun checkLastKnownFused() {
checkLastKnownAndNotify(LocationManager.FUSED_PROVIDER)
}
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
private fun checkLastKnownAndNotify(provider: String) {
val location = locationManager.getLastKnownLocation(provider)
location?.let { onLocationChanged(it) }
}
override fun onLocationChanged(location: Location) {
if (LocationManager.FUSED_PROVIDER == location.provider) {
requests.forEach { it.listener.onLocationChanged(location) }
} else if (LocationManager.GPS_PROVIDER == location.provider) {
gpsLocation = location
if (gpsLocation.isBetterThan(networkLocation)) {
requests.forEach { it.listener.onLocationChanged(location) }
}
} else if (LocationManager.NETWORK_PROVIDER == location.provider) {
networkLocation = location
if (networkLocation.isBetterThan(gpsLocation)) {
requests.forEach { it.listener.onLocationChanged(location) }
}
}
}
private fun Location?.isBetterThan(other: Location?): Boolean {
if (this == null) {
return false
}
if (other == null) {
return true
}
if (this.elapsedRealtimeNanos
> other.elapsedRealtimeNanos + RECENT_UPDATE_THRESHOLD_IN_NANOS
) {
return true
}
if (!this.hasAccuracy()) {
return false
}
return if (!other.hasAccuracy()) {
true
} else this.accuracy < other.accuracy
}
}

View File

@@ -0,0 +1,83 @@
package net.vonforst.evmap.location
import android.Manifest
import android.content.Context
import android.location.Location
import android.location.LocationListener
import android.os.Looper
import androidx.annotation.RequiresPermission
/**
* Base class for [com.mapzen.android.lost.internal.FusionEngine].
*/
abstract class LocationEngine(protected val context: Context) {
protected val requests: MutableList<LocationRequest> = mutableListOf()
/**
* Return most best recent location available.
*/
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
abstract fun getLastKnownLocation(): Location?
/**
* Enables the engine on receiving a valid location request.
*
* @param request Valid location request to enable.
*/
@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))
enable()
}
/**
* Disables the engine when no requests remain, otherwise updates the engine's configuration.
*
* @param requests Valid location request to enable.
*/
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
fun removeUpdates(listener: LocationListener) {
this.requests.removeIf { it.listener == listener }
disable()
if (this.requests.isNotEmpty()) enable()
}
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
fun removeAllRequests() {
requests.clear()
disable()
}
/**
* Subclass should perform all operations required to enable the engine. (ex. Register for
* location updates.)
*/
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
protected abstract fun enable()
/**
* Subclass should perform all operations required to disable the engine. (ex. Remove location
* updates.)
*/
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
protected abstract fun disable()
protected val looper: Looper
get() = context.mainLooper
interface Callback {
fun reportLocation(location: Location)
}
}
data class LocationRequest(
val priority: Priority,
val intervalMs: Long,
val listener: LocationListener
)
enum class Priority {
HIGH_ACCURACY,
BALANCED_POWER_ACCURACY,
LOW_POWER,
NO_POWER
}

View File

@@ -167,9 +167,9 @@ data class Cost(
fun getStatusText(ctx: Context, emoji: Boolean = false): CharSequence {
if (freecharging != null && freeparking != null) {
val charging =
if (freecharging) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
if (freecharging) ctx.getString(R.string.charging_free) else ctx.getString(R.string.charging_paid)
val parking =
if (freeparking) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
if (freeparking) ctx.getString(R.string.parking_free) else ctx.getString(R.string.parking_paid)
return if (emoji) {
"$charging · \uD83C\uDD7F $parking"
} else {
@@ -177,7 +177,7 @@ data class Cost(
}
} else if (freecharging != null) {
val charging =
if (freecharging) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
if (freecharging) ctx.getString(R.string.charging_free) else ctx.getString(R.string.charging_paid)
return if (emoji) {
"$charging"
} else {
@@ -185,7 +185,7 @@ data class Cost(
}
} else if (freeparking != null) {
val parking =
if (freeparking) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
if (freeparking) ctx.getString(R.string.parking_free) else ctx.getString(R.string.parking_paid)
return if (emoji) {
"\uD83C\uDD7F $parking"
} else {

View File

@@ -1,12 +1,12 @@
package net.vonforst.evmap.navigation
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import androidx.navigation.fragment.NavHostFragment
class NavHostFragment : NavHostFragment() {
override fun onCreateNavController(navController: NavController) {
super.onCreateNavController(navController)
navController.navigatorProvider.addNavigator(
override fun onCreateNavHostController(navHostController: NavHostController) {
super.onCreateNavHostController(navHostController)
navHostController.navigatorProvider.addNavigator(
CustomNavigator(
requireContext()
)

View File

@@ -389,9 +389,10 @@ private fun colorToTransparent(color: Int, targetAlpha: Float = 31f / 255): Int
val green = Color.green(color)
val blue = Color.blue(color)
val newRed = ((red - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
val newGreen = ((green - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
val newBlue = ((blue - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
val newRed = kotlin.math.max(((red - (1 - targetAlpha) * 255) / targetAlpha).roundToInt(), 0)
val newGreen =
kotlin.math.max(((green - (1 - targetAlpha) * 255) / targetAlpha).roundToInt(), 0)
val newBlue = kotlin.math.max(((blue - (1 - targetAlpha) * 255) / targetAlpha).roundToInt(), 0)
return Color.argb((targetAlpha * 255).roundToInt(), newRed, newGreen, newBlue)
}

View File

@@ -1,14 +1,20 @@
package net.vonforst.evmap.ui
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.view.Gravity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.FrameLayout
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlin.math.roundToInt
private fun dialogEditText(ctx: Context): Pair<View, EditText> {
val container = FrameLayout(ctx)
@@ -24,30 +30,19 @@ private fun dialogEditText(ctx: Context): Pair<View, EditText> {
fun showEditTextDialog(
ctx: Context,
customize: (AlertDialog.Builder, EditText) -> Unit
customize: (MaterialAlertDialogBuilder, EditText) -> Unit
): AlertDialog {
val (container, input) = dialogEditText(ctx)
val dialogBuilder = AlertDialog.Builder(ctx)
val dialogBuilder = MaterialAlertDialogBuilder(ctx)
.setView(container)
customize(dialogBuilder, input)
val dialog = dialogBuilder.show()
// move dialog to top
val attrs = dialog.window?.attributes?.apply {
gravity = Gravity.TOP
}
dialog.window?.attributes = attrs
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
// focus and show keyboard
input.requestFocus()
input.postDelayed({
val imm =
ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT)
}, 100)
input.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
val text = input.text
@@ -60,4 +55,57 @@ fun showEditTextDialog(
false
}
return dialog
}
/**
* DialogFragment that uses Material styling.
* This needs a bit of a workaround, see also
* https://github.com/material-components/material-components-android/issues/540 and
* https://dev.to/bhullnatik/how-to-use-material-dialogs-with-dialogfragment-28i1
*/
abstract class MaterialDialogFragment : AppCompatDialogFragment() {
private lateinit var dialogView: View
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = MaterialAlertDialogBuilder(requireContext(), theme).apply {
dialogView =
onCreateView(LayoutInflater.from(requireContext()), null, savedInstanceState)!!
setView(dialogView)
}.create()
initView(dialogView, savedInstanceState)
return dialog
}
abstract fun initView(view: View, savedInstanceState: Bundle?)
override fun getView(): View {
return dialogView
}
override fun onStart() {
super.onStart()
// make sure that custom view fills whole dialog height
(view.parent as View).layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
(view.parent.parent as View).layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
(view.parent.parent.parent as View).layoutParams.height =
ViewGroup.LayoutParams.MATCH_PARENT
}
/**
* Makes the dialog fill the whole width & height of the screen, with an optional maximum
* width in dp. Call this during onStart.
*/
fun setFullSize(maxWidthDp: Int? = null) {
val width = resources.displayMetrics.widthPixels
val maxWidth = if (maxWidthDp != null) {
val density = resources.displayMetrics.density
(maxWidthDp * density).roundToInt()
} else null
dialog?.window?.setLayout(
if (maxWidth == null || width < maxWidth) WindowManager.LayoutParams.MATCH_PARENT else maxWidth,
WindowManager.LayoutParams.MATCH_PARENT
)
}
}

View File

@@ -0,0 +1,32 @@
package net.vonforst.evmap.ui
import android.annotation.SuppressLint
import androidx.appcompat.view.menu.MenuPopupHelper
import androidx.appcompat.widget.MenuPopupWindow
import androidx.appcompat.widget.PopupMenu
/**
* Reflection workaround to make setTouchModal accessible for
*/
@SuppressLint("RestrictedApi")
fun PopupMenu.setTouchModal(modal: Boolean) {
try {
val mPopup = javaClass.getDeclaredField("mPopup").let { field ->
field.isAccessible = true
field.get(this)
} as MenuPopupHelper
val mPopup2 = mPopup.javaClass.getDeclaredMethod("getPopup").let { method ->
method.isAccessible = true
method.invoke(mPopup)
}
val mPopup3 = mPopup2.javaClass.getDeclaredField("mPopup").let { field ->
field.isAccessible = true
field.get(mPopup2)
} as MenuPopupWindow
mPopup3.setTouchModal(modal)
} catch (e: NoSuchFieldException) {
e.printStackTrace()
} catch (e: NoSuchMethodException) {
e.printStackTrace()
}
}

View File

@@ -25,7 +25,12 @@ class LocaleContextWrapper(base: Context?) : ContextWrapper(base) {
}
} else {
// set selected locale
val locale = Locale(language)
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)

View File

@@ -2,12 +2,12 @@ package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.*
import jsonapi.Meta
import jsonapi.Relationship
import jsonapi.Relationships
import jsonapi.ResourceIdentifier
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import moe.banana.jsonapi2.HasMany
import moe.banana.jsonapi2.HasOne
import moe.banana.jsonapi2.JsonBuffer
import moe.banana.jsonapi2.ResourceIdentifier
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.model.ChargeLocation
@@ -16,30 +16,48 @@ import net.vonforst.evmap.storage.PreferenceDataSource
import retrofit2.HttpException
import java.io.IOException
class ChargepriceViewModel(application: Application, chargepriceApiKey: String) :
class ChargepriceViewModel(
application: Application,
chargepriceApiKey: String,
chargepriceApiUrl: String,
private val state: SavedStateHandle
) :
AndroidViewModel(application) {
private var api = ChargepriceApi.create(chargepriceApiKey)
private var api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl)
private var prefs = PreferenceDataSource(application)
val charger: MutableLiveData<ChargeLocation> by lazy {
MutableLiveData<ChargeLocation>()
}
val dataSource: MutableLiveData<String> by lazy {
MutableLiveData<String>()
state.getLiveData("charger")
}
val chargepoint: MutableLiveData<Chargepoint> by lazy {
MutableLiveData<Chargepoint>()
state.getLiveData("chargepoint")
}
val vehicles: MutableLiveData<Resource<List<ChargepriceCar>>> by lazy {
MutableLiveData<Resource<List<ChargepriceCar>>>().apply {
if (prefs.chargepriceMyVehicles.isEmpty()) {
value = Resource.success(emptyList())
} else {
value = Resource.loading(null)
loadVehicles()
private val vehicleIds: MutableLiveData<Set<String>> by lazy {
MutableLiveData<Set<String>>().apply {
value = prefs.chargepriceMyVehicles
}
}
val vehicles: LiveData<Resource<List<ChargepriceCar>>> by lazy {
MediatorLiveData<Resource<List<ChargepriceCar>>>().apply {
addSource(vehicleIds.distinctUntilChanged()) { vehicleIds ->
if (vehicleIds.isEmpty()) {
value = Resource.success(emptyList())
} else {
value = Resource.loading(null)
viewModelScope.launch {
value = try {
val result = api.getVehicles()
Resource.success(result.filter {
it.id in vehicleIds
})
} catch (e: IOException) {
Resource.error(e.message, null)
}
}
}
}
observeForever {
vehicle.value = it.data?.firstOrNull()
@@ -48,7 +66,7 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
}
val vehicle: MutableLiveData<ChargepriceCar> by lazy {
MutableLiveData<ChargepriceCar>()
state.getLiveData("vehicle")
}
val vehicleCompatibleConnectors: LiveData<List<String>> by lazy {
@@ -94,21 +112,24 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
}
}
val chargePrices: MediatorLiveData<Resource<List<ChargePrice>>> by lazy {
val chargePrices: MutableLiveData<Resource<List<ChargePrice>>> by lazy {
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
value = Resource.loading(null)
value = state["chargePrices"] ?: Resource.loading(null)
listOf(
charger,
dataSource,
batteryRange,
batteryRangeSliderDragging,
vehicleCompatibleConnectors,
myTariffs, myTariffsAll
).forEach {
addSource(it) {
addSource(it.distinctUntilChanged()) {
if (!batteryRangeSliderDragging.value!!) loadPrices()
}
}
observeForever {
// persist data in case fragment gets recreated
state["chargePrices"] = it
}
}
}
@@ -140,15 +161,15 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
if (filteredPrices.isEmpty()) {
null
} else {
cp.clone().apply {
cp.copy(
chargepointPrices = filteredPrices
}
)
}
}.filterNotNull()
.sortedBy { it.chargepointPrices.first().price }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||
myTariffs != null && it.tariff?.get()?.id in myTariffs
myTariffs != null && it.tariffId in myTariffs
}
)
}
@@ -157,6 +178,10 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
}
}
fun reloadPrefs() {
vehicleIds.value = prefs.chargepriceMyVehicles
}
private fun getChargepricePlugType(chargepoint: Chargepoint): String {
val index = charger.value!!.chargepointsMerged.indexOf(chargepoint)
val type = charger.value!!.chargepriceData!!.plugTypes?.get(index) ?: chargepoint.type
@@ -210,10 +235,9 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
val charger = charger.value
val car = vehicle.value
val compatibleConnectors = vehicleCompatibleConnectors.value
val dataSource = dataSource.value
val myTariffs = myTariffs.value
val myTariffsAll = myTariffsAll.value
if (charger == null || car == null || compatibleConnectors == null || dataSource == null || myTariffsAll == null || myTariffsAll == false && myTariffs == null) {
if (charger == null || car == null || compatibleConnectors == null || myTariffsAll == null || myTariffsAll == false && myTariffs == null) {
chargePrices.value = Resource.error(null, null)
return
}
@@ -223,34 +247,39 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
loadPricesJob?.cancel()
loadPricesJob = viewModelScope.launch {
try {
val result = api.getChargePrices(ChargepriceRequest().apply {
dataAdapter = dataSource
station = cpStation
vehicle = HasOne(car)
tariffs = if (!myTariffsAll) {
HasMany<ChargepriceTariff>(*myTariffs!!.map {
ResourceIdentifier(
"tariff",
it
val result = api.getChargePrices(
ChargepriceRequest(
dataAdapter = ChargepriceApi.getDataAdapter(charger),
station = cpStation,
vehicle = car,
options = ChargepriceOptions(
batteryRange = batteryRange.value!!.map { it.toDouble() },
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency,
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad
),
relationships = if (!myTariffsAll) {
Relationships(
"tariffs" to Relationship.ToMany(
(myTariffs ?: emptySet()).map {
ResourceIdentifier(
"tariff",
id = it
)
},
meta = Meta.from(
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS),
ChargepriceApi.moshi
)
)
)
}.toTypedArray()).apply {
meta = JsonBuffer.create(
ChargepriceApi.moshi.adapter(ChargepriceRequestTariffMeta::class.java),
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS)
)
}
} else null
options = ChargepriceOptions(
batteryRange = batteryRange.value!!.map { it.toDouble() },
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency,
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad
)
}, ChargepriceApi.getChargepriceLanguage())
val meta =
result.meta.get<ChargepriceMeta>(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta
chargePrices.value = Resource.success(result)
} else null
), ChargepriceApi.getChargepriceLanguage()
)
val meta = result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!!
chargePrices.value = Resource.success(result.data)
chargePriceMeta.value = Resource.success(meta)
} catch (e: IOException) {
chargePrices.value = Resource.error(e.message, null)
@@ -261,17 +290,4 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
}
}
}
private fun loadVehicles() {
viewModelScope.launch {
try {
val result = api.getVehicles()
vehicles.value = Resource.success(result.filter {
it.id in prefs.chargepriceMyVehicles
})
} catch (e: IOException) {
vehicles.value = Resource.error(e.message, null)
}
}
}
}

View File

@@ -6,8 +6,10 @@ import androidx.lifecycle.*
import com.car2go.maps.AnyMap
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
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.api.availability.ChargeLocationStatus
@@ -141,17 +143,24 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
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) {
loadChargerDetails(charger, refData)
if (charger.id != value?.data?.id) {
loadChargerDetails(charger, refData)
}
} else {
value = null
}
}
}
observeForever {
// persist data in case fragment gets recreated
state["chargerDetails"] = it
}
}
}
val charger: MediatorLiveData<Resource<ChargeLocation>> by lazy {
@@ -270,7 +279,11 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
suspend fun copyFiltersToCustom() {
filterStatus.value?.let { db.filterValueDao().copyFiltersToCustom(it, prefs.dataSource) }
filterStatus.value?.let {
withContext(Dispatchers.IO) {
db.filterValueDao().copyFiltersToCustom(it, prefs.dataSource)
}
}
}
fun setMapType(type: AnyMap.Type) {
@@ -311,7 +324,8 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
minPower >= 100 -> {
// when only showing high-power chargers we can use large markers
zoom < clusterThreshold
// because the density is much lower
false
}
else -> {
zoom < miniMarkerThreshold

View File

@@ -11,9 +11,13 @@ import net.vonforst.evmap.api.chargeprice.ChargepriceTariff
import net.vonforst.evmap.storage.AppDatabase
import java.io.IOException
class SettingsViewModel(application: Application, chargepriceApiKey: String) :
class SettingsViewModel(
application: Application,
chargepriceApiKey: String,
chargepriceApiUrl: String
) :
AndroidViewModel(application) {
private var api = ChargepriceApi.create(chargepriceApiKey)
private var api = ChargepriceApi.create(chargepriceApiKey, chargepriceApiUrl)
private var db = AppDatabase.getInstance(application)
val vehicles: MutableLiveData<Resource<List<ChargepriceCar>>> by lazy {

View File

@@ -1,5 +1,6 @@
package net.vonforst.evmap.viewmodel
import android.os.Parcelable
import androidx.annotation.MainThread
import androidx.annotation.Nullable
import androidx.lifecycle.*
@@ -7,6 +8,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
import java.util.concurrent.atomic.AtomicBoolean
@@ -16,6 +19,16 @@ inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
override fun <T : ViewModel> create(aClass: Class<T>): T = f() as T
}
@Suppress("UNCHECKED_CAST")
inline fun <VM : ViewModel> savedStateViewModelFactory(crossinline f: (SavedStateHandle) -> VM) =
object : AbstractSavedStateViewModelFactory() {
override fun <T : ViewModel> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
) = f(handle) as T
}
enum class Status {
SUCCESS,
ERROR,
@@ -24,9 +37,13 @@ enum class Status {
/**
* A generic class that holds a value with its loading status.
* @param <T>
</T> */
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
*
* Note that this class implements Parcelable for convenience, but will give a runtime error when
* trying to write it to a Parcel if the type parameter does not implement Parcelable.
*/
@Parcelize
data class Resource<out T>(val status: Status, val data: @RawValue T?, val message: String?) :
Parcelable {
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)

View File

@@ -0,0 +1,28 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF">
<group
android:scaleX="0.184"
android:scaleY="0.184"
android:translateX="0.96"
android:translateY="0.96">
<path
android:pathData="M27.1,88.3l-2.2,-19.2l-3.3,0.3l2.2,19.2L27.1,88.3zM39,86.9l-2.2,-19.2l-3.3,0.3l2.2,19.2L39,86.9z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M45.2,113c-1,1.3 -1.8,2.1 -2,2.2c-3,2.4 -5.4,3.1 -7.4,2.2c-3.5,-1.7 -3.2,-8.2 -3.1,-8.9l2.4,0.1c-0.1,1.8 0.2,5.8 1.8,6.6c0.9,0.5 2.5,-0.1 4.6,-1.8l0,0c0,0 6.7,-6.7 5.3,-12c-1.6,-6.4 5.8,-15.5 8.2,-18.6l0.3,-0.3l2,1.5l-0.3,0.5c-7.5,9.2 -8.3,14 -7.7,16.4C50.5,105.4 47.4,110.4 45.2,113z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M19.7,88.1l0.9,7.9l7.3,4.9l9.8,-1l6,-6.4l-0.9,-7.9L19.7,88.1z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M37.6,99.7l-9.8,1l2.1,8.7l7.7,-0.9V99.7L37.6,99.7zM44.6,79l0.8,7.2l-28.2,3.2l-0.8,-7.2L44.6,79z"
android:fillColor="#FFFFFF" />
<path
android:pathData="M66.7,0C46.5,0 30.1,16.4 30.1,36.6c0,27.6 30.8,42 34.5,81.4c0.1,1.2 1,2 2.2,2c1.2,0 2.1,-0.8 2.2,-2c3.7,-39.4 34.5,-53.8 34.5,-81.4C103.3,16.2 86.9,0 66.7,0zM78.4,34.7L64.3,59V40.8h-6V18.7c0,0 20.2,0 20.1,-0.1l-8.1,16.2H78.4z"
android:fillColor="#FFFFFF" />
</group>
</vector>

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,12 +1,15 @@
<vector android:height="44.11976dp"
android:viewportHeight="368.4"
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="233.8dp"
android:height="368.4dp"
android:viewportWidth="233.8"
android:width="28dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#FFFFFF"
android:pathData="M109.8,0h13.6c33.9,1.9 67.1,18.5 87.7,45.8c13.5,17.2 21,38.6 22.7,60.3v8.1c-0.8,42.1 -27.7,76.6 -51,109.4c-26.2,37 -50.4,77.3 -57.1,122.9c-1.8,7.7 0.4,18.5 -8.9,22c-2.2,-1.7 -4.7,-3.1 -6.2,-5.4c-2.7,-25.5 -9.1,-50.7 -20,-73.9c-12.3,-27.1 -29.5,-51.6 -47,-75.6C33,199 23,184.2 14.7,168.3c-13,-23.8 -17.9,-51.9 -12.5,-78.6c4.4,-21.1 15.4,-40.6 30.6,-55.7C53.3,14 81.1,1.8 109.8,0z" />
<path
android:fillColor="#808080"
android:pathData="M90.9,57.3v68.2h18.6v55.8l43.4,-74.4h-24.8l24.8,-49.6H90.9z" />
android:viewportHeight="368.4">
<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" />
<path
android:pathData="M123.3,2c34.1,1.9 66.3,18.8 86.2,45c12.6,16.1 20.5,37.1 22.3,59.1l0,8c-0.7,38.5 -23.6,70.5 -45.7,101.3c-1.7,2.3 -3.3,4.6 -5,6.9c-24.5,34.6 -50.5,76.3 -57.4,123.7c-0.5,2.1 -0.7,4.4 -0.9,6.7c-0.5,5.9 -1,11 -5.8,13.4c-0.2,-0.2 -0.5,-0.4 -0.7,-0.5c-1.5,-1.1 -2.9,-2 -3.8,-3.3c-2.9,-26.9 -9.7,-51.8 -20.1,-74C80,261 62.3,235.8 45.2,212.4c-8.7,-11.9 -19.8,-27.8 -28.8,-45.1C3.3,143.3 -1,115.9 4.2,90.1c4.2,-20.2 14.9,-39.6 30,-54.7C54.2,16 81.7,3.8 109.9,2H123.3M123.4,0h-13.6c-28.7,1.8 -56.5,14 -77,34C17.6,49.1 6.6,68.6 2.2,89.7c-5.4,26.7 -0.5,54.8 12.5,78.6C23,184.2 33,199 43.6,213.6c17.5,24 34.7,48.5 47,75.6c10.9,23.2 17.3,48.4 20,73.9c1.5,2.3 4,3.7 6.2,5.4c9.3,-3.5 7.1,-14.3 8.9,-22c6.7,-45.6 30.9,-85.9 57.1,-122.9c23.3,-32.8 50.2,-67.3 51,-109.4v-8.1c-1.7,-21.7 -9.2,-43.1 -22.7,-60.3C190.5,18.5 157.3,1.9 123.4,0L123.4,0z"
android:fillColor="#808080" />
<path
android:pathData="M90.9,57.3v68.2h18.6v55.8l43.4,-74.4h-24.8l24.8,-49.6C152.9,57.3 90.9,57.3 90.9,57.3z"
android:fillColor="#808080" />
</vector>

View File

@@ -4,8 +4,8 @@
android:height="16dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#dddddd"
android:pathData="M12,12m-8.5,0a8.5,8.5 0,1 1,17 0a8.5,8.5 0,1 1,-17 0" />
android:fillColor="#808080"
android:pathData="M12,12m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0" />
<path
android:fillColor="@android:color/white"
android:pathData="M12,12m-7.5,0a7.5,7.5 0,1 1,15 0a7.5,7.5 0,1 1,-15 0" />

View File

@@ -12,6 +12,9 @@
<path
android:fillColor="#B5B5B5"
android:pathData="M122.2,101.9h16.7h5.7l22.3,-44.6c0,0 -10.2,0 -22.4,0l-1.1,2.2L122.2,101.9z" />
<path
android:pathData="M123.3,2c34.1,1.9 66.3,18.8 86.2,45c12.6,16.1 20.5,37.1 22.3,59.1l0,8c-0.7,38.5 -23.6,70.5 -45.7,101.3c-1.7,2.3 -3.3,4.6 -5,6.9c-24.5,34.6 -50.5,76.3 -57.4,123.7c-0.5,2.1 -0.7,4.4 -0.9,6.7c-0.5,5.9 -1,11 -5.8,13.4c-0.2,-0.2 -0.5,-0.4 -0.7,-0.5c-1.5,-1.1 -2.9,-2 -3.8,-3.3c-2.9,-26.9 -9.7,-51.8 -20.1,-74C80,261 62.3,235.8 45.2,212.4c-8.7,-11.9 -19.8,-27.8 -28.8,-45.1C3.3,143.3 -1,115.9 4.2,90.1c4.2,-20.2 14.9,-39.6 30,-54.7C54.2,16 81.7,3.8 109.9,2H123.3M123.4,0h-13.6c-28.7,1.8 -56.5,14 -77,34C17.6,49.1 6.6,68.6 2.2,89.7c-5.4,26.7 -0.5,54.8 12.5,78.6C23,184.2 33,199 43.6,213.6c17.5,24 34.7,48.5 47,75.6c10.9,23.2 17.3,48.4 20,73.9c1.5,2.3 4,3.7 6.2,5.4c9.3,-3.5 7.1,-14.3 8.9,-22c6.7,-45.6 30.9,-85.9 57.1,-122.9c23.3,-32.8 50.2,-67.3 51,-109.4v-8.1c-1.7,-21.7 -9.2,-43.1 -22.7,-60.3C190.5,18.5 157.3,1.9 123.4,0L123.4,0z"
android:fillColor="#808080" />
<path
android:fillColor="#808080"
android:pathData="M138.9,57.3c-9.7,0 -19.8,0 -26.4,0c-2.5,0 -5.1,0 -7.6,0c-8.2,0 -16.1,0 -21.4,0c-4.1,0 -6.6,0 -6.6,0v68.2h18.6v55.8l43.4,-74.4h-24.8L138.9,57.3z" />

View File

@@ -17,7 +17,7 @@
android:orientation="vertical">
<FrameLayout
android:id="@+id/topPanel"
android:id="@+id/topPane"
android:layout_width="0dp"
android:layout_height="88dp"
android:background="@color/colorPrimary"
@@ -46,7 +46,7 @@
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/topPanel" />
app:layout_constraintTop_toBottomOf="@+id/topPane" />
<TextView
android:id="@+id/welcomeText1"

View File

@@ -37,7 +37,7 @@
android:paddingTop="8dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
android:background="@{BindingAdaptersKt.tariffBackground(context,!myTariffsAll &amp;&amp; myTariffs.contains(item.tariff.get().id), item.branding.backgroundColor)}">
android:background="@{BindingAdaptersKt.tariffBackground(context,!myTariffsAll &amp;&amp; myTariffs.contains(item.tariffId), item.branding.backgroundColor)}">
<TextView
android:id="@+id/txtTariff"
@@ -167,7 +167,6 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintTop_toTopOf="parent"
app:tintNullable="@{BindingAdaptersKt.isDarkMode(context) ? @android:color/white : null}"
tools:srcCompat="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -130,9 +130,6 @@
<argument
android:name="charger"
app:argType="net.vonforst.evmap.model.ChargeLocation" />
<argument
android:name="dataSource"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/donate"
@@ -141,7 +138,7 @@
tools:layout="@layout/fragment_donate" />
<dialog
android:id="@+id/opensource_donations"
android:name="net.vonforst.evmap.fragment.updatedialogs.OpensourceDonationsDialogFramgent"
android:name="net.vonforst.evmap.fragment.updatedialogs.OpensourceDonationsDialogFragment"
android:label="@string/donation_dialog_title"
tools:layout="@layout/dialog_opensource_donations">
<action
@@ -163,6 +160,8 @@
android:label="OnboardingFragment">
<action
android:id="@+id/action_onboarding_to_map"
app:destination="@id/map" />
app:destination="@id/map"
app:popUpTo="@id/onboarding"
app:popUpToInclusive="true" />
</fragment>
</navigation>

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_language_names">
<item>Gerätesprache verwenden</item>
<item>Englisch</item>
<item>Deutsch</item>
</string-array>
<string-array name="pref_darkmode_names">
<item>Geräteeinstellung verwenden</item>
<item>immer an</item>
<item>immer aus</item>
</string-array>
<string-array name="pref_data_source_names">
<item>GoingElectric.de</item>
<item>Open Charge Map</item>
</string-array>
</resources>

View File

@@ -19,8 +19,10 @@
<string name="cost_detail"><![CDATA[<b>Laden:</b> %1$s · <b>Parken:</b> %2$s]]></string>
<string name="cost_detail_charging"><![CDATA[<b>%s laden</b>]]></string>
<string name="cost_detail_parking"><![CDATA[<b>%s parken</b>]]></string>
<string name="free">Kostenlos</string>
<string name="paid">Kostenpflichtig</string>
<string name="charging_free">Kostenlos</string>
<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="realtime_data_unavailable">Echtzeitstatus nicht verfügbar</string>
@@ -204,24 +206,13 @@
<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>
<string name="pref_my_tariffs_summary">(werden im Preisvergleich hervorgehoben)</string>
<plurals name="pref_my_tariffs_summary">
<item quantity="one">(wird im Preisvergleich hervorgehoben)</item>
<item quantity="other">(werden im Preisvergleich hervorgehoben)</item>
</plurals>
<string name="license">Lizenz</string>
<string name="settings_charger_data">Ladesäulen</string>
<string name="pref_data_source">Datenquelle</string>
<string-array name="pref_chargeprice_currency_names">
<item>Schweizer Franken (CHF)</item>
<item>Tschechische Krone (CZK)</item>
<item>Dänische Krone (DKK)</item>
<item>Euro (EUR)</item>
<item>Britisches Pfund (GBP)</item>
<item>Kroatische Kuna (HRK)</item>
<item>Ungarischer Forint (HUF)</item>
<item>Isländische Krone (ISK)</item>
<item>Norwegische Krone (NOK)</item>
<item>Polnischer Złoty (PLN)</item>
<item>Schwedische Krone (SEK)</item>
<item>US-Dollar (USD)</item>
</string-array>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d Tarif ausgewählt</item>
<item quantity="other">%d Tarife ausgewählt</item>
@@ -262,4 +253,22 @@
<string name="pref_map_rotate_gestures_off">Karte bleibt fest nach Norden ausgerichtet</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>
<string name="pref_darkmode_device_default">Geräteeinstellung verwenden</string>
<string name="pref_darkmode_always_on">immer an</string>
<string name="pref_darkmode_always_off">immer aus</string>
<string name="pref_chargeprice_currency_chf">Schweizer Franken (CHF)</string>
<string name="pref_chargeprice_currency_czk">Tschechische Krone (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Dänische Krone (DKK)</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="pref_chargeprice_currency_gbp">Britisches Pfund (GBP)</string>
<string name="pref_chargeprice_currency_hrk">Kroatische Kuna (HRK)</string>
<string name="pref_chargeprice_currency_huf">Ungarischer Forint (HUF)</string>
<string name="pref_chargeprice_currency_isk">Isländische Krone (ISK)</string>
<string name="pref_chargeprice_currency_nok">Norwegische Krone (NOK)</string>
<string name="pref_chargeprice_currency_pln">Polnischer Złoty (PLN)</string>
<string name="pref_chargeprice_currency_sek">Schwedische Krone (SEK)</string>
<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>

View File

@@ -0,0 +1,274 @@
<?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">
<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="address">Adresse</string>
<string name="operator">Opérateur</string>
<string name="network">Réseau</string>
<string name="hours">Heures d\'ouverture</string>
<string name="open_247"><b>Ouvert 24h/24 et 7j/7</b></string>
<string name="open_closesat"><b>Ouvert</b> · Ferme à %s</string>
<string name="closed_unfmt">Fermé</string>
<string name="cost">Coût</string>
<string name="closed"><b>Fermé</b></string>
<string name="closed_opensat"><b>Fermé</b> · Ouvre à %s</string>
<string name="holiday">Jour férié</string>
<string name="cost_detail"><b>Recharge :</b> %1$s · <b>Stationnement :</b> %2$s</string>
<string name="realtime_data_unavailable">Statut en temps réel non disponible</string>
<string name="source">Source : %s</string>
<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="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="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="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="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="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="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>
<string name="filter_barrierfree">Utilisable sans enregistrement</string>
<string name="filter_exclude_faults">Exclure les chargeurs avec des défauts signalés</string>
<string name="charge_cards">Méthodes de paiement</string>
<string name="goingelectric_forum">Fil de discussion du forum sur GoingElectric.de</string>
<string name="edit_at_datasource">modifier à %s</string>
<string name="categories">Catégories</string>
<string name="category_car_dealership">Concessionnaire automobile</string>
<string name="category_public_authorities">Pouvoirs publics</string>
<string name="category_church">Église</string>
<string name="category_hospital">Hôpital</string>
<string name="category_museum">Musée</string>
<string name="category_parking_multi">Parking à étages</string>
<string name="category_parking">Parking</string>
<string name="category_private_charger">Chargeur privé</string>
<string name="category_rest_area">Aire de repos</string>
<string name="category_parking_underground">Parking souterrain</string>
<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="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>
<string name="rename">Renommer</string>
<string name="verified">vérifié</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d mode de paiement compatible</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="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="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="chargeprice_title">Prix</string>
<string name="pref_chargeprice_currency">Devise</string>
<string name="data_source_goingelectric">GoingElectric.de</string>
<string name="chargeprice_all_tariffs_selected">tous les tarifs sélectionnés</string>
<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="other">%d tarifs sélectionnés</item>
</plurals>
<string name="data_source_openchargemap">Open Charge Map</string>
<string name="next">suivant</string>
<string name="get_started">Commencez</string>
<string name="crash_report_comment_prompt">Vous pouvez ajouter un commentaire ci-dessous :</string>
<string name="github_sponsors">GitHub Sponsors</string>
<string name="donate_desc">Soutenir le développement d\'EVMap par un don unique</string>
<string name="github_sponsors_desc">Soutenir EVMap sur GitHub Sponsors</string>
<string name="unnamed_filter_profile">Profil de filtrage sans nom</string>
<string name="privacy_link">https://evmap.vonforst.net/en/privacy.html</string>
<string name="required">requis</string>
<string name="edit_filter_profile">Modifier \"%s\"</string>
<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="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>
<string name="pref_darkmode_always_on">toujours allumé</string>
<string name="pref_darkmode_always_off">toujours éteint</string>
<string name="pref_chargeprice_currency_czk">Couronne tchèque (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Couronne danoise (DKK)</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="show_more">plus…</string>
<string name="fav_remove">Retirer des favoris</string>
<string name="amenities">Commodités</string>
<string name="search">Recherche</string>
<string name="menu_map">Carte</string>
<string name="settings">Paramètres</string>
<string name="copyright">Copyright</string>
<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="category_supermarket">Supermarché</string>
<string name="version">Version</string>
<string name="oss_licenses">Licences Open Source</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_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="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="settings_map">Carte</string>
<string name="fault_report">Rapport d\'anomalie</string>
<string name="filter_free">Uniquement des chargeurs gratuits</string>
<string name="filter_min_power">Puissance minimale</string>
<string name="filter_free_parking">Uniquement les chargeurs avec un parking gratuit</string>
<string name="filter_min_connectors">Nombre minimal de connecteurs</string>
<string name="plug_type_1">Type 1</string>
<string name="plug_schuko">Schuko</string>
<string name="plug_cee_blau">CEE Bleu</string>
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
<string name="none">aucun</string>
<string name="map_type_normal">Défaut</string>
<string name="filter_networks">Réseaux</string>
<string name="ok">OK</string>
<string name="pref_darkmode">Mode sombre</string>
<string name="menu_edit_filters">Modifier les filtres</string>
<string name="go_to_chargeprice">Comparer les prix</string>
<string name="filter_chargecards">Méthodes de paiement</string>
<string name="all_selected">Tous sélectionnés</string>
<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="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="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>
<string name="category_camping">Camping</string>
<string name="category_airport">Aéroport</string>
<string name="category_amusement_park">Parc d\'attractions</string>
<string name="category_hotel">Hôtel</string>
<string name="category_restaurant">Restaurant</string>
<string name="filter_favorites">Favoris</string>
<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="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>
<string name="category_shopping_mall">Centre commercial</string>
<string name="category_cinema">Cinéma</string>
<string name="category_swimming_pool">Piscine</string>
<string name="menu_save_profile">Enregistrer en tant que profil</string>
<string name="no_filters">Aucun filtre</string>
<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="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="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_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="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>
<string name="pref_chargeprice_currency_gbp">Livre sterling (GBP)</string>
<string name="pref_chargeprice_currency_isk">Couronne islandaise (ISK)</string>
<string name="pref_chargeprice_currency_nok">Couronne norvégienne (NOK)</string>
<string name="settings_charger_data">Stations de recharge</string>
<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="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_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="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_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_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="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>
<string name="charge_price_kwh_format">%1$.2f %2$s/kWh</string>
<string name="chargeprice_base_fee">Frais fixes : %1$.2f %2$s/mois</string>
<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="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="charging_paid">Payante</string>
<string name="charging_free">Gratuite</string>
</resources>

View File

@@ -0,0 +1,275 @@
<?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="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="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_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="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="and_n_others">og %d andre</string>
<string name="pref_map_provider">Karttilbyder</string>
<string name="twitter">Twitter</string>
<string name="contact">Kontakt</string>
<string name="categories">Kategorier</string>
<string name="category_airport">Flyplass</string>
<string name="category_hotel">Hotell</string>
<string name="category_church">Kirke</string>
<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="license">Lisens</string>
<string name="pref_data_source">Datakilder</string>
<string name="required">påkrevd</string>
<string name="edit_filter_profile">Rediger «%s»</string>
<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="title_activity_maps">EVMap</string>
<string name="no_browser_app_found">Fant ingen nettlesere</string>
<string name="address">Adresse</string>
<string name="network">Nettverk</string>
<string name="closed_unfmt">Stengt</string>
<string name="cost_detail_charging"><b>%s-lading</b></string>
<string name="cost_detail_parking"><b>%s-parkering</b></string>
<string name="menu_map">Kart</string>
<string name="category_petrol_station">Bensinstasjon</string>
<string name="closed_opensat"><b>Stengt</b> · Åpner %s</string>
<string name="retry">Prøv igjen</string>
<string name="source">Kilde: %s</string>
<string name="menu_favs">Favoritter</string>
<string name="menu_manage_filter_profiles">Håndter filterprofiler</string>
<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="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="charge_cards">Betalingsmetoder</string>
<string name="go_to_chargeprice">Sammenlign priser</string>
<string name="filter_networks">Nettverk</string>
<string name="filter_chargecards">Betalingsmetoder</string>
<string name="category_hospital">Sykehus</string>
<string name="menu_save_profile">Lagre som profil</string>
<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="category_private_charger">Privat lader</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_museum">Museum</string>
<string name="category_swimming_pool">Svømmebasseng</string>
<string name="unnamed_filter_profile">Filterprofil uten navn</string>
<string name="get_started">Begynn</string>
<string name="got_it">Skjønner</string>
<string name="settings_data_sources">Datakilder</string>
<string name="pref_search_delete_recent">Slett nylige søkeresultater</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="pref_chargeprice_currency_nok">Norske kroner (NOK)</string>
<string name="pref_chargeprice_currency_gbp">Britiske pund (GBP)</string>
<string name="pref_chargeprice_currency_sek">Svenske kroner (SEK)</string>
<string name="realtime_data_loading">Sjekker sanntidsstatus …</string>
<string name="realtime_data_source">Kilde for sanntidsstatus (beta): %s</string>
<string name="realtime_data_unavailable">Sanntidsstatus utilgjengelig</string>
<string name="other">Andre</string>
<string name="cost_detail"><b>Lading:</b> %1$s · <b>Parkering:</b> %2$s</string>
<string name="copyright_summary">©20202022 Johan von Forstner</string>
<string name="pref_navigate_use_maps_on">Navigasjonsnkappen starter Google Maps-navigasjon umiddelbart</string>
<string name="filter_free_parking">Kun ladere med gratis parkering</string>
<string name="filter_min_power">Minimumseffekt</string>
<string name="plug_type_1">Type 1</string>
<string name="plug_chademo">CHAdeMO</string>
<string name="plug_schuko">Schuko</string>
<string name="plug_supercharger">Tesla Supercharger</string>
<string name="plug_type_2">Type 2</string>
<string name="plug_type_3">Type 3A</string>
<string name="all">alle</string>
<string name="none">ingen</string>
<string name="show_less">færre …</string>
<string name="map_type_satellite">Satellitt</string>
<string name="map_type_terrain">Terreng</string>
<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="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>
<string name="fault_report_date">Feilrapport (siste oppdatering: %s)</string>
<string name="all_selected">Alle valgt</string>
<string name="number_selected">%d valgt</string>
<string name="pref_darkmode">Mørk drakt</string>
<string name="connection_error">Kunne ikke laste inn ladestasjoner</string>
<string name="filter_barrierfree">Kan brukes uten registrering</string>
<string name="goingelectric_forum">Forumtråd på GoingElectric.de</string>
<string name="category_car_dealership">Bilforhandlere</string>
<string name="category_railway_station">Togstasjon</string>
<string name="category_public_authorities">Offentlige myndigheter</string>
<string name="category_amusement_park">Fornøyelsespark</string>
<string name="category_cinema">Kino</string>
<string name="category_parking_multi">Parkeringshus</string>
<string name="edit_at_datasource">rediger på %s</string>
<string name="category_camping">Campingplass</string>
<string name="category_service_on_motorway">Rasteplass (på motorvei)</string>
<string name="category_shopping_mall">Kjøpesenter</string>
<string name="category_holiday_home">Feriehjem</string>
<string name="category_parking">Parkeringsplass</string>
<string name="category_rest_area">Rasteplass</string>
<string name="category_supermarket">Supermarked</string>
<string name="menu_apply">Bruk filtre</string>
<string name="no_filters">Ingen filtre</string>
<string name="category_zoo">Dyrehage</string>
<string name="category_caravan_site">Campingplass</string>
<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="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="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>
<string name="charge_price_kwh_format">%2$s%1$.2f/kWt</string>
<string name="chargeprice_per_kwh">per kWt</string>
<string name="percent_format">%.0f%%</string>
<string name="chargeprice_per_minute">per min</string>
<string name="chargeprice_min_spend">Minimumskostnad: %2$s%1$.2f/måned</string>
<string name="settings_chargeprice">Prissammenligning</string>
<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_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="chargeprice_title">Priser</string>
<string name="chargeprice_connection_error">Kunne ikke laste inn priser</string>
<plurals name="pref_my_tariffs_summary">
<item quantity="one">(vil bli framhevet i prissammenligningen)</item>
<item quantity="other">(vil bli framhevet i prissammenligningen)</item>
</plurals>
<string name="settings_charger_data">Ladestasjoner</string>
<string name="data_source_goingelectric">GoingElectric.de</string>
<string name="data_source_openchargemap">Open Charge Map</string>
<string name="crash_report_comment_prompt">Du kan legge til en kommentar nedenfor:</string>
<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="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>
<string name="pref_chargeprice_currency_chf">Sveitserfranc (CHF)</string>
<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="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="show_more">flere …</string>
<string name="filters_activated">Filtre aktivert</string>
<string name="donate">Doner</string>
<string name="donation_successful">Takk. ❤️</string>
<string name="map_type_normal">Forvalg</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>
<string name="undo">Angre</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d kompatibel betalingsmetode</item>
<item quantity="other">%d kompatible betalingsmetoder</item>
</plurals>
<string name="plug_ccs">CCS</string>
<string name="verified">bekreftet</string>
<string name="pref_darkmode_always_on">alltid på</string>
<string name="pref_darkmode_device_default">Enhetsforvalg</string>
<string name="pref_darkmode_always_off">alltid av</string>
<string name="filter_exclude_faults">Utelat ladere med rapporterte feil</string>
<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="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="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_select_connector">Velg tilkobling</string>
<string name="chargeprice_provider_customer_tariff">Kun for tilbyderkunder</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_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="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="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="lets_go">Begynn</string>
<string name="crash_report_text">EVMap har 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_provider_google_maps">Google Maps</string>
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="charging_free">Gratis</string>
<string name="parking_free">Gratis</string>
<string name="charging_paid">Betalt</string>
<string name="parking_paid">Betalt</string>
<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>
</resources>

View File

@@ -1,40 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_language_names">
<item>Device default</item>
<item>English</item>
<item>German</item>
<item>@string/pref_language_device_default</item>
<item>@string/pref_language_en</item>
<item>@string/pref_language_de</item>
<item>@string/pref_language_fr</item>
<item>@string/pref_language_nb_rNO</item>
</string-array>
<string-array name="pref_language_values" tranlatable="false">
<string-array name="pref_language_values" translatable="false">
<item>default</item>
<item>en</item>
<item>de</item>
<item>fr</item>
<item>nb-NO</item>
</string-array>
<string-array name="pref_darkmode_names">
<item>Device default</item>
<item>always on</item>
<item>always off</item>
<item>@string/pref_darkmode_device_default</item>
<item>@string/pref_darkmode_always_on</item>
<item>@string/pref_darkmode_always_off</item>
</string-array>
<string-array name="pref_darkmode_values" tranlatable="false">
<string-array name="pref_darkmode_values" translatable="false">
<item>default</item>
<item>on</item>
<item>off</item>
</string-array>
<string-array name="pref_chargeprice_currency_names">
<item>Swiss franc (CHF)</item>
<item>Czech koruna (CZK)</item>
<item>Danish krone (DKK)</item>
<item>Euro (EUR)</item>
<item>Pound sterling (GBP)</item>
<item>Croatian kuna (HRK)</item>
<item>Hungarian forint (HUF)</item>
<item>Icelandic króna (ISK)</item>
<item>Norwegian krone (NOK)</item>
<item>Polish złoty (PLN)</item>
<item>Swedish krona (SEK)</item>
<item>US dollar (USD)</item>
<item>@string/pref_chargeprice_currency_chf</item>
<item>@string/pref_chargeprice_currency_czk</item>
<item>@string/pref_chargeprice_currency_dkk</item>
<item>@string/pref_chargeprice_currency_eur</item>
<item>@string/pref_chargeprice_currency_gbp</item>
<item>@string/pref_chargeprice_currency_hrk</item>
<item>@string/pref_chargeprice_currency_huf</item>
<item>@string/pref_chargeprice_currency_isk</item>
<item>@string/pref_chargeprice_currency_nok</item>
<item>@string/pref_chargeprice_currency_pln</item>
<item>@string/pref_chargeprice_currency_sek</item>
<item>@string/pref_chargeprice_currency_usd</item>
</string-array>
<string-array name="pref_chargeprice_currency_values" donottranslate="true">
<string-array name="pref_chargeprice_currency_values" translatable="false">
<item>CHF</item>
<item>CZK</item>
<item>DKK</item>
@@ -49,8 +53,8 @@
<item>USD</item>
</string-array>
<string-array name="pref_data_source_names">
<item>GoingElectric.de</item>
<item>Open Charge Map</item>
<item>@string/data_source_goingelectric</item>
<item>@string/data_source_openchargemap</item>
</string-array>
<string-array name="pref_data_source_values" tranlatable="false">
<item>goingelectric</item>

View File

@@ -8,7 +8,7 @@
<color name="colorSecondaryDark">#00b249</color>
<color name="colorSecondaryContainer">#b5f4c7</color>
<color name="colorOnSecondaryContainer">#007c00</color>
<color name="charger_100kw">#ffeb3b</color>
<color name="charger_100kw">#FDD835</color>
<color name="charger_43kw">#ff9800</color>
<color name="charger_20kw">#03a9f4</color>
<color name="charger_11kw">#9e9e9e</color>

View File

@@ -7,4 +7,9 @@
<string name="twitter_url">https://twitter.com/ev_map</string>
<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="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>
</resources>

View File

@@ -18,8 +18,10 @@
<string name="cost_detail"><![CDATA[<b>Charging:</b> %1$s · <b>Parking:</b> %2$s]]></string>
<string name="cost_detail_charging"><![CDATA[<b>%s charging</b>]]></string>
<string name="cost_detail_parking"><![CDATA[<b>%s parking</b>]]></string>
<string name="free">Free</string>
<string name="paid">Paid</string>
<string name="charging_free">Free</string>
<string name="charging_paid">Paid</string>
<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="realtime_data_unavailable">Real-time status unavailable</string>
@@ -202,7 +204,10 @@
<string name="chargeprice_no_compatible_connectors">None of the connectors on this charging station is compatible with your vehicle.</string>
<string name="pref_chargeprice_currency">Currency</string>
<string name="pref_my_tariffs">My charging plans</string>
<string name="pref_my_tariffs_summary">(will be highlighted in price comparison)</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>
</plurals>
<string name="chargeprice_all_tariffs_selected">all plans selected</string>
<string name="license">License</string>
<string name="settings_charger_data">Charging stations</string>
@@ -247,4 +252,22 @@
<string name="pref_map_rotate_gestures_off">Map will be fixed to north-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="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>
<string name="pref_darkmode_always_off">always off</string>
<string name="pref_chargeprice_currency_chf">Swiss franc (CHF)</string>
<string name="pref_chargeprice_currency_czk">Czech koruna (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Danish krone (DKK)</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="pref_chargeprice_currency_gbp">Pound sterling (GBP)</string>
<string name="pref_chargeprice_currency_hrk">Croatian kuna (HRK)</string>
<string name="pref_chargeprice_currency_huf">Hungarian forint (HUF)</string>
<string name="pref_chargeprice_currency_isk">Icelandic króna (ISK)</string>
<string name="pref_chargeprice_currency_nok">Norwegian krone (NOK)</string>
<string name="pref_chargeprice_currency_pln">Polish złoty (PLN)</string>
<string name="pref_chargeprice_currency_sek">Swedish krona (SEK)</string>
<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>

View File

@@ -13,6 +13,8 @@
<item name="colorOnSecondaryContainer">@color/colorSecondaryDark</item>
<item name="android:windowTranslucentStatus">true</item>
<item name="preferenceTheme">@style/AppTheme.Preference</item>
<item name="alertDialogTheme">@style/AppTheme.AlertDialog</item>
<item name="materialAlertDialogTheme">@style/AppTheme.AlertDialog</item>
</style>
<style name="AppTheme.Preference" parent="@style/PreferenceThemeOverlay">
@@ -67,4 +69,10 @@
<item name="iconTint">?android:textColorSecondary</item>
</style>
<style name="AppTheme.AlertDialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
<!-- this is necessary to make sure the dialog gets "pushed up" when the keyboard appears -->
<item name="android:windowTranslucentStatus">false</item>
<item name="dialogCornerRadius">28dp</item>
</style>
</resources>

View File

@@ -8,8 +8,7 @@
app:defaultToAll="false" />
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_tariffs"
android:title="@string/pref_my_tariffs"
android:summary="@string/pref_my_tariffs_summary" />
android:title="@string/pref_my_tariffs" />
<ListPreference
android:key="chargeprice_currency"
android:title="@string/pref_chargeprice_currency"

View File

@@ -63,14 +63,14 @@ class ChargepriceApiTest {
runBlocking {
val result = chargeprice.getChargePrices(
ChargepriceRequest().apply {
dataAdapter = "going_electric"
ChargepriceRequest(
dataAdapter = "going_electric",
station =
ChargepriceStation.fromEvmap(charger, listOf("Typ2", "Schuko"))
ChargepriceStation.fromEvmap(charger, listOf("Typ2", "Schuko")),
options = ChargepriceOptions(energy = 22.0, duration = 60)
}, "en"
), "en"
)
assertEquals(25, result.size)
assertEquals(25, result.data!!.size)
}
}
}

View File

@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.6.21'
ext.kotlin_version = '1.7.10'
ext.about_libs_version = '8.9.4'
ext.nav_version = '2.5.1'
repositories {
@@ -10,7 +10,7 @@ buildscript {
gradlePluginPortal()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.1'
classpath 'com.android.tools.build:gradle:7.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
@@ -26,7 +26,6 @@ allprojects {
google()
mavenCentral()
//noinspection JcenterRepositoryObsolete
jcenter() // still required for https://github.com/kamikat/moshi-jsonapi
maven { url 'https://jitpack.io' }
}
}

View File

@@ -35,8 +35,9 @@ Not all API keys are strictly required if you only want to work on certain parts
example, you can choose only one of the map providers and one of the charging station databases. The
Chargeprice API key is also only required if you want to test the price comparison feature.
All API keys are available for free. Some APIs require payment above a certain limit, but the free
tier should be plenty for local testing and development.
All APIs can be used for free, at least for testing. Some APIs require payment above a certain usage
limit or to get access to the full dataset, but the free tiers should be plenty for local testing
and development.
Below you find a list of all the services and how to obtain the API keys.
@@ -152,14 +153,19 @@ Pricing providers
<details>
<summary>How to obtain an API key</summary>
1. Check the
[Pricing page](https://github.com/chargeprice/chargeprice-api-docs/blob/master/plans.md)
for information on the current plans at Chargeprice. There should be a free tier up to a certain
limit of API calls per month.
2. Contact [contact@chargeprice.net](mailto:contact@chargeprice.net), stating that you would like to
contribute to the development the open source EVMap app and therefore need access to the
Chargeprice API for testing.
3. When your access to the API is approved, you will receive an API key via email.
Since February 2022, the Chargeprice API is no longer available for free to new customers. However,
you can use their
[staging API](https://github.com/chargeprice/chargeprice-api-docs/blob/master/test_the_api.md)
for free to test the Chargeprice features. This is already
[configured](https://github.com/johan12345/EVMap/blob/master/app/src/debug/res/values/donottranslate.xml)
by default for the debug version of the app, so you can leave the `chargeprice_key` field in your
new `app/src/main/res/values/apikeys.xml` file blank. Note that the staging API contains only a
limited dataset, so it only outputs prices for certain charge point operators and payment plans (see
[here](https://docs.google.com/document/d/14zlFr5IEhhR3uGXO5QePKjNUQANVwA-Ba-cZbOCiOBk/edit) for
details).
In case you want to pay for access to the full Chargeprice API, check out their
[API docs](https://github.com/chargeprice/chargeprice-api-docs) on GitHub and contact them at
[sales@chargeprice.net](mailto:sales@chargeprice.net).
</details>

View File

@@ -0,0 +1,12 @@
Unter https://hosted.weblate.org/engage/evmap/ können Sie jetzt dazu beitragen,
EVMap in andere Sprachen zu übersetzen!
Verbesserungen:
- Neues Design für Dialoge
- Karte kann bei geöffnetem Filtermenü verschoben werden
- Vorbereitungen für Übersetzung der App in andere Sprachen
- Android 11 und niedriger: Ortung verbessert, wenn GPS aktiviert aber nicht verfügbar (z.B. in Gebäuden)
Fehler behoben:
- Absturz im Preisvergleich behoben
- Verbesserungen für weitere kleine Darstellungsfehler

View File

@@ -0,0 +1,9 @@
Verbesserungen:
- Übersetzung auf Norwegisch und Französisch (Danke an die Beitragenden!)
- Kontrast der Marker auf der Karte (v.a. gelb) erhöht
- Android Automotive OS: Filter "Störung ausschließen" verfügbar
Fehler behoben:
- "Meine Tarife" wurden nicht mehr oben angeordnet und hervorgehoben
- Dark Mode: Weißes Aufblitzen bei Wechsel zur Karte teilweise reduziert
- Android Auto: Abstürze behoben

View File

@@ -0,0 +1,6 @@
Fehler behoben:
- Darstellungsprobleme mit einigen hervorgehobenen Tarifen im Preisvergleich behoben
- Verbesserungen für weitere kleine Darstellungsfehler
- Android Auto: Richtiges Icon für dauerhafte Benachrichtigung zum Standortzugriff verwenden
- Android Auto: Ausrichtung von +/- Buttons korrigiert
- Android Auto: Liste der Ladestationen nach Neustart der App aktualisieren

View File

@@ -1 +1 @@
EVMap - Elektroauto-Ladestationen
EVMap - Elektroauto laden

View File

@@ -0,0 +1,11 @@
At https://hosted.weblate.org/engage/evmap/ you can now help translating EVMap into other languages!
Improvements:
- New design for dialogs
- Map can be moved when filter menu is open
- Preparations for translating the app into other languages
- Android 11 and lower: Improved localization if GPS is enabled but not available (e.g. in buildings)
Bugfixes:
- Fixed crash in price comparison
- Improvements for some more minor visual issues

View File

@@ -0,0 +1,9 @@
Improvements:
- Added Norwegian and French translations (thanks to the contributors!)
- Improved contrast of markers on map (especially yellow)
- Android Automotive OS: Filter "exclude chargers with reported faults" available
Bugfixes:
- "My charging plans" were not sorted and highlighted
- Dark Mode: Partly fixed white flashing when switching back to map
- Android Auto: fixed crashes

View File

@@ -0,0 +1,6 @@
Bugfixes:
- Fixed visual problems with some highlighted providers in price comparison
- Improvements for some other minor visual issues
- Android Auto: Use proper icon for persistent notification about location access
- Android Auto: Fix alignment of +/- buttons
- Android Auto: Refresh chargers after going back to app

View File

@@ -1 +1 @@
EVMap - Electric vehicle chargers
EVMap - EV chargers

View File

@@ -0,0 +1,18 @@
Grâce à EVMap, vous pouvez trouver les chargeurs de véhicules électriques confortablement à l'aide de votre téléphone Android. Il fournit un accès mobile aux bases de données communautaires de GoingElectric.de et Open Charge Map, qui contiennent des informations sur les points de charge dans le monde entier. Pour de nombreux points de charge en Europe, vous pouvez voir des informations sur le statut en temps réel.
Caractéristiques :
- Conception matérielle ("Material Design")
- Affiche toutes les stations de recharge des répertoires GoingElectric.de et Open Charge Map gérés par la communauté.
- Informations sur la disponibilité en temps réel (uniquement en Europe)
- Comparaison des prix intégrée grâce à Chargeprice.app (uniquement en Europe)
- Données cartographiques provenant d'OpenStreetMap (Mapbox)
- Recherche de lieux
- Options de filtrage avancées, y compris les profils de filtrage enregistrés
- Liste de favoris, avec également des informations sur la disponibilité
- Pas de publicité, entièrement open source
EVMap est un projet open source et peut être trouvé sur https://github.com/johan12345/EVMap.
Cette application n'est pas un produit officiel de GoingElectric.de ou Open Charge Map, elle utilise uniquement leurs API publiques.
Une liste des permissions nécessaires avec des explications est disponible ici : https://evmap.vonforst.net/en/permissions.html

View File

@@ -0,0 +1 @@
Trouver des stations de recharge pour véhicules électriques

View File

@@ -0,0 +1 @@
EVMap - Charger son VE

View File

@@ -0,0 +1,19 @@
Finn steder å lade fra den gemenskapsdrevne databasen til GoingElectric.de og Open Charge Map med din Android-enhet.
Du finner info om ladestasjoner i hele verden og sanntidsinfo for mange av dem som er å finne i Europa.
- Fri programvare
- Materiell design
- Sanntidsinfo (kun i Europa)
- Integrert sammenligningsinfo ved bruk av Chargeprice.app (kun i Europa)
- Kartdata fra OpenStreetMap (Mapbox)
- Søk etter steder
- Avanserte filtreringsvalg, inkludert lagrede filterprofiler
- Favorittliste, som også har tilgjengelighetsinfo
- Ingen reklame
Du finner kildekoden på https://github.com/johan12345/EVMap.
Dette kartet er ikke et offisielt program fra hverken GoingElectric.de eller Open Charge Map.
Kun de offentlige API-ene derfra benyttes.
Nødvendige tilganger er forklart på https://evmap.vonforst.net/en/permissions.html

View File

@@ -0,0 +1 @@
Finn ladestasjoner for elektriske kjøretøy

View File

@@ -0,0 +1 @@
EVMap — Elbil-ladere

View File

@@ -14,4 +14,3 @@
kotlin.code.style=official
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
android.useAndroidX=true
android.enableJetifier=true