Compare commits

..

83 Commits
1.4.6 ... 1.5.1

Author SHA1 Message Date
johan12345
ea906ec969 Release 1.5.1 2023-05-29 09:46:44 +02:00
johan12345
ec2b6d4f28 Chargeprice: fix crash with StackOverflowError 2023-05-29 09:40:54 +02:00
johan12345
e7c2683ee2 Tesla: profile_image_url is nullable 2023-05-29 09:40:54 +02:00
johan12345
d76051ec3a add backup_rules 2023-05-28 23:20:03 +02:00
johan12345
975ba2bcce Release 1.5.0 2023-05-28 22:43:29 +02:00
johan12345
dc067fd86b build speedup: enable non-transitive R classes and non-final resource IDs 2023-05-28 21:59:55 +02:00
johan12345
226ca3a60e update dependencies 2023-05-28 21:53:42 +02:00
johan12345
af63ee350b update Android Gradle plugin 2023-05-28 21:49:23 +02:00
johan12345
21d4060ac9 minor improvements for Tesla login 2023-05-28 00:11:43 +02:00
johan12345
3b9efa0302 install splashscreen before super.onCreate
fixes crashes after rotation on Android 7
reason: Splash screen does not use an AppCompat theme, therefore view state is not restored correctly
2023-05-27 23:23:41 +02:00
johan12345
95d93af0d6 some Kotlin code refactoring 2023-05-18 23:37:02 +02:00
johan12345
17a6a253d4 other dependency upgrades 2023-05-18 23:25:28 +02:00
johan12345
f73545c01e upgrade to Kotlin 1.8.20 2023-05-18 23:13:17 +02:00
johan12345
e4fa1f2c78 add Supercharger utilization graph
#272
2023-05-18 01:10:55 +02:00
johan12345
b2b5cc63e8 update Supercharger icon
so that Tesla logo is not overlapped by realtime data
2023-05-18 01:10:47 +02:00
johan12345
84ba62f755 add pricing information from Tesla
#272
2023-05-18 01:10:47 +02:00
johan12345
b29653049a fix typo 2023-05-17 23:16:23 +02:00
johan12345
4159491589 layout fix if neither Chargeprice nor charger website buttons are shown 2023-05-16 23:31:58 +02:00
johan12345
4e67f434cd disable Chargeprice for Tesla 2023-05-16 23:30:03 +02:00
johan12345
5e58d52a0d Tesla AvailabilityDetector: add notice to sign in 2023-05-16 21:33:59 +02:00
johan12345
eddc1f9b61 add link 2023-05-14 23:23:17 +02:00
johan12345
b5054b4dc9 fix tests 2023-05-14 23:11:31 +02:00
johan12345
926799bb1d Implement Tesla Supercharger AvailabilityDetector
#272
2023-05-14 22:31:07 +02:00
johan12345
f038138620 remove unnecessary line break 2023-05-14 18:14:03 +02:00
johan12345
1c44e5ae3d FusionEngine: reset when disabled 2023-05-14 18:08:48 +02:00
johan12345
c58543fe3f Add developer options fragment with location provider debug info
refs #276
2023-05-14 18:04:57 +02:00
johan12345
a5db42322f Fix issue if fused location provider is available but does not deliver any location updates
might resolve #276
2023-05-14 17:13:28 +02:00
johan12345
bb0d2e35d4 fix CarAppTest with Flipper 2023-05-06 23:31:06 +02:00
johan12345
38c8c5510f pt strings: fix lint hints 2023-05-06 17:36:12 +02:00
johan12345
8d1d15ad68 update copyright 2023-05-06 17:35:27 +02:00
johan12345
954203bf18 CI: update Java 2023-05-06 17:29:04 +02:00
johan12345
524e9fcfc0 Update Gradle plugin 2023-05-04 18:10:13 +02:00
johan12345
ae2041d26b fr, pt: add "many" plurals 2023-05-04 18:09:33 +02:00
johan12345
698c832518 update okhttp 2023-05-02 20:36:32 +02:00
johan12345
17c1a11675 Flipper: fix unit tests 2023-05-01 18:28:16 +02:00
johan12345
d04661e925 Replace deprecated Stetho with Flipper
https://github.com/facebookarchive/stetho -> archived
https://github.com/facebook/flipper
2023-05-01 17:47:54 +02:00
johan12345
02316fceb9 fix NewMotionAvailabilityDetectorTest 2023-05-01 17:11:52 +02:00
johan12345
9bf7a90302 fix broken NewMotion availability API
- domain changed to ui-map.shellrecharge.com
- zoom parameter is now required

fixes #278
2023-05-01 17:05:43 +02:00
johan12345
2697389b49 fix portuguese plurals 2023-05-01 16:19:07 +02:00
Hosted Weblate
cd0e381707 Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.8% (238 of 284 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nb_NO/
Translation: EVMap/Android
2023-05-01 13:06:23 +02:00
Hosted Weblate
e5ed5eeafe Translated using Weblate (Portuguese)
Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (283 of 283 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2023-05-01 13:06:23 +02:00
johan12345
b25c61fbea update Android Gradle plugin 2023-05-01 13:03:01 +02:00
johan12345
d472be1676 Release 1.4.10 2023-04-22 15:00:15 +02:00
Hosted Weblate
24fa85929e Added translation using Weblate (Romanian)
Added translation using Weblate (Romanian)

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>
2023-04-22 14:48:46 +02:00
johan12345
4a67ffd956 OpenChargeMap: do not show addressInfo.relatedUrl if it is identical to operatorInfo.websiteUrl 2023-04-20 20:22:44 +02:00
johan12345
fab66d1f84 OpenChargeMap: fix "exclude faults" filter and hide removed chargers
"exclude faults" has to be implemented as a local filter because otherwise chargers with unknown status (StatusTypeId == null) will also be filtered out
2023-04-20 20:12:54 +02:00
johan12345
0783c6c272 move ic_link to correct folder 2023-04-20 19:54:35 +02:00
johan12345
c5714c8592 OpenChargeMap: add networkUrl and chargerUrl
#273
2023-04-20 19:51:30 +02:00
iboboc
cb4b571721 Adding Romanian language support (#274)
* Adding Romanian language support

* Adding Romanian language support (fix for strings - plurals and add to supported list)

* Revert unused declaration.

---------

Co-authored-by: iboboc <Raluca2018>
2023-04-20 18:38:55 +02:00
Johan von Forstner
0bfa80bbe0 EnBW: expand list of countries 2023-04-14 21:04:30 +02:00
Johan von Forstner
d77f13682d enable portuguese locale 2023-04-07 23:01:15 +02:00
Johan von Forstner
0c19eb5833 minor fixes in Portuguese 2023-04-07 23:00:57 +02:00
Johan von Forstner
a5abedae55 Update translation files 2023-04-07 22:52:20 +02:00
johan12345
8405f4f4fa Release 1.4.9 2023-03-24 20:14:43 +01:00
johan12345
f435180c03 Chargeprice vehicle selection: add battery size and charging power to distinguish vehicle models
same as ee354d2cd1, but now also for Android Auto
2023-03-24 20:10:14 +01:00
Hosted Weblate
c2c3e96e97 Update translation files
Updated by "Squash Git commits" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/
Translation: EVMap/Android (strings specific to Google Play variant)
2023-03-21 21:49:07 +01:00
Hosted Weblate
9100a6f442 Translated using Weblate (French)
Currently translated at 99.6% (282 of 283 strings)

Translated using Weblate (French)

Currently translated at 92.2% (261 of 283 strings)

Translated using Weblate (French)

Currently translated at 97.2% (35 of 36 strings)

Co-authored-by: Altons <marsupilami450@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-google/fr/
Translate-URL: https://hosted.weblate.org/projects/evmap/android/fr/
Translation: EVMap/Android
Translation: EVMap/Android (strings specific to Google Play variant)
2023-03-21 21:49:06 +01:00
johan12345
5403549e0a gallery image loading improvements
refs #271
2023-03-21 21:48:08 +01:00
johan12345
c95f1e7c24 Coil: Allow hardware renderer
this didn't work previously as we used shared element transitions, but should be fine now that we switched to StfalconImageViewer in 0e1e3ba
2023-03-21 21:48:08 +01:00
johan12345
f8d5b78112 Remove SizeResolver(OriginalSize) for Coil
(determining the size automatically based on the actual ImageView size seems to work well now)
fixes #271
2023-03-21 21:48:08 +01:00
Hosted Weblate
246d456851 Translated using Weblate (German)
Currently translated at 100.0% (283 of 283 strings)

Co-authored-by: nautilusx <translate@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/de/
Translation: EVMap/Android
2023-03-05 21:19:01 +01:00
Johan von Forstner
3d303b6535 GoingElectric HoursAdapter: catch parsing exception 2023-03-05 13:31:54 +01:00
johan12345
135fce43c3 CarModels: add Renault BCB -> Megane E-Tech
see #269
2023-02-18 21:09:56 +01:00
johan12345
ee354d2cd1 Chargeprice vehicle selection: Add battery size and charging power to distinguish vehicle models
fixes #268
2023-02-18 21:05:45 +01:00
johan12345
350f18df8e Release 1.4.8 2023-02-14 19:34:46 +01:00
johan12345
dda151abf5 add @DoubleYouEl to contributors list 2023-02-14 19:30:41 +01:00
johan12345
a86f1397f4 fix unnecessary empty requests to fronyx API 2023-02-14 19:25:25 +01:00
johan12345
086cc51dd3 Release 1.4.7 2023-02-12 18:20:36 +01:00
johan12345
0de91bc107 update CustomBottomSheetBehavior 2023-02-12 18:17:38 +01:00
johan12345
3436bcd870 update CustomBottomSheetBehavior 2023-02-12 18:04:40 +01:00
johan12345
22c150d557 upgrade dependencies 2023-02-12 17:53:21 +01:00
johan12345
675abb5011 DonateViewModel: fix possible NPE when loading products
see also https://github.com/EventFahrplan/EventFahrplan/issues/71
2023-02-12 17:40:06 +01:00
johan12345
af2a2cfcae enable Dutch locale
fixes #267
2023-02-08 21:26:44 +01:00
Hosted Weblate
f74526fdd6 Update translation files
Updated by "Squash Git commits" hook in Weblate.

Update translation files

Updated by "Squash Git commits" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/
Translate-URL: https://hosted.weblate.org/projects/evmap/app-store-metadata/
Translation: EVMap/Android (strings specific to F-Droid variant)
Translation: EVMap/App Store metadata
2023-02-08 21:19:47 +01:00
Hosted Weblate
c5bbca0428 Update translation files
Updated by "Squash Git commits" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android-fdroid/
Translation: EVMap/Android (strings specific to F-Droid variant)
2023-02-08 21:19:26 +01:00
johan12345
6167079c0e update dependencies 2023-02-05 15:36:08 +01:00
Hosted Weblate
c3836a92ad Translated using Weblate (German)
Currently translated at 100.0% (283 of 283 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/de/
Translation: EVMap/Android
2023-02-05 15:31:48 +01:00
Hosted Weblate
dccce1a0a0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.7% (237 of 283 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/nb_NO/
Translation: EVMap/Android
2023-02-05 15:31:47 +01:00
Hosted Weblate
74d79640a8 Translated using Weblate (English)
Currently translated at 100.0% (283 of 283 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/en/
Translation: EVMap/Android
2023-02-05 15:31:47 +01:00
Hosted Weblate
0eb6ece780 Translated using Weblate (French)
Currently translated at 92.2% (261 of 283 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/fr/
Translation: EVMap/Android
2023-02-05 15:31:47 +01:00
johan12345
ae15b13591 use patched mapbox-events-android version only for foss variant
to allow Google location services in google variant, even when Mapbox is selected
2023-02-04 19:40:16 +01:00
johan12345
4962eb7268 transfer project to ev-map GitHub org 2023-02-04 19:23:29 +01:00
johan12345
abe360d7c2 transfer JitPack dependencies to ev-map GitHub org 2023-02-04 19:18:42 +01:00
133 changed files with 3468 additions and 479 deletions

View File

@@ -16,7 +16,7 @@ jobs:
- name: Set up Java environment
uses: actions/setup-java@v2
with:
java-version: 11
java-version: 17
distribution: 'zulu'
cache: 'gradle'
- name: Decrypt keystore

View File

@@ -21,7 +21,7 @@ jobs:
- name: Set up Java environment
uses: actions/setup-java@v2
with:
java-version: 11
java-version: 17
distribution: 'zulu'
cache: 'gradle'

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2022 Johan von Forstner
Copyright (c) 2020-2023 Johan von Forstner and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,7 +1,7 @@
EVMap [![Build Status](https://github.com/johan12345/EVMap/actions/workflows/tests.yml/badge.svg)](https://github.com/johan12345/EVMap/actions)
EVMap [![Build Status](https://github.com/ev-map/EVMap/actions/workflows/tests.yml/badge.svg)](https://github.com/ev-map/EVMap/actions)
=====
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/>
<img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/>
Android app to find electric vehicle charging stations.
@@ -28,7 +28,7 @@ Features
Screenshots
-----------
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/screenshots/phone/en/mapbox/01_map.png" width=250 alt="Screenshot 1"/><img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/screenshots/phone/en/mapbox/02_detail.png" width=250 alt="Screenshot 2"/>
<img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/screenshots/phone/en/mapbox/01_map.png" width=250 alt="Screenshot 1"/><img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/screenshots/phone/en/mapbox/02_detail.png" width=250 alt="Screenshot 2"/>
Development setup
-----------------

View File

@@ -1,23 +1,25 @@
<svg id="Ebene_5" data-name="Ebene 5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<defs>
<style>.cls-1,.cls-2{fill:none;}.cls-2{stroke:#000;stroke-miterlimit:10;stroke-width:2px;}
</style>
</defs>
<title>connector_supercharger</title>
<path class="cls-1" d="M12,12H36V36H12Z" />
<path class="cls-2"
d="M13.45,17.08a8.24,8.24,0,0,1-3.11.6,8.34,8.34,0,0,1-6-14.18H16.3a8.35,8.35,0,0,1,1.07,10.33" />
<circle cx="10.34" cy="9.34" r="1.67" />
<circle cx="15.35" cy="9.34" r="1.67" />
<circle cx="12.84" cy="13.51" r="1.67" />
<circle cx="7.84" cy="13.51" r="1.67" />
<circle cx="5.34" cy="9.34" r="1.67" />
<circle cx="7.84" cy="5.59" r="1" />
<circle cx="12.84" cy="5.59" r="1.04" />
<?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_5" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 24 24"
style="enable-background:new 0 0 24 24;" xml:space="preserve">
<style type="text/css">
.st0{display:none;fill:none;}
.st1{fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:10;}
</style>
<path class="st0" d="M12,12h24v24H12V12z" />
<path class="st1"
d="M6.2,13.8C4.1,10.6,4.6,6.3,7.3,3.5h12c1.5,1.6,2.4,3.7,2.4,5.9c0,4.6-3.8,8.3-8.4,8.3c-1.1,0-2.1-0.2-3.1-0.6" />
<circle cx="13.3" cy="9.3" r="1.7" />
<circle cx="8.3" cy="9.3" r="1.7" />
<circle cx="10.8" cy="13.5" r="1.7" />
<circle cx="15.8" cy="13.5" r="1.7" />
<circle cx="18.3" cy="9.3" r="1.7" />
<circle cx="15.8" cy="5.6" r="1" />
<circle cx="10.8" cy="5.6" r="1" />
<g id="T">
<path id="path35"
d="M18.18,22.23l1-5.48c.93,0,1.22.1,1.27.52a2.15,2.15,0,0,0,.93-.7,6.91,6.91,0,0,0-2.46-.6l-.71.88h0L17.46,16a7,7,0,0,0-2.46.6,2.22,2.22,0,0,0,.94.7c0-.42.33-.52,1.26-.52l1,5.48" />
<path id="path37"
d="M18.18,15.72a7.9,7.9,0,0,1,3.28.66,2.65,2.65,0,0,0,.2-.4,9.24,9.24,0,0,0-7,0,2.61,2.61,0,0,0,.19.4,7.94,7.94,0,0,1,3.29-.66h0" />
</g>
</svg>
<path id="path35" d="M5.4,22.3l1-5.5c0.9,0,1.3,0.1,1.3,0.5c0.4-0.1,0.7-0.4,0.9-0.7C7.8,16.3,7,16,6.1,16l-0.8,0.8l0,0L4.7,16
c-0.8,0-1.7,0.3-2.5,0.6c0.2,0.3,0.6,0.6,0.9,0.7c0.1-0.4,0.3-0.5,1.3-0.5L5.4,22.3" />
<path id="path37" d="M5.5,15.7L5.5,15.7c1.1,0,2.3,0.2,3.3,0.7c0.1-0.1,0.1-0.3,0.2-0.4c-2.2-0.9-4.8-0.9-7,0
c0.1,0.1,0.1,0.3,0.2,0.4C3.2,15.9,4.3,15.7,5.5,15.7" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -9,7 +9,7 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
def supportedLocales = "en,de,fr,nb-rNO"
def supportedLocales = "en,de,fr,nb-rNO,nl,pt,ro"
android {
compileSdkVersion 33
@@ -20,8 +20,8 @@ android {
minSdkVersion 21
targetSdkVersion 33
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode 160
versionName "1.4.6"
versionCode 172
versionName "1.5.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs supportedLocales.split(',')
@@ -90,6 +90,12 @@ android {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs).configureEach {
kotlinOptions {
jvmTarget = "1.8"
}
}
buildFeatures {
dataBinding = true
viewBinding true
@@ -155,28 +161,29 @@ configurations {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "androidx.activity:activity-ktx:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.5.5"
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.activity:activity-ktx:1.7.2"
implementation "androidx.fragment:fragment-ktx:1.5.7"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'com.google.android.material:material:1.8.0'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.browser:browser:1.4.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'androidx.browser:browser:1.5.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f4f641aab5'
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
implementation 'com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
implementation 'com.squareup.moshi:moshi-adapters:1.13.0'
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.11.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
implementation 'com.squareup.moshi:moshi-adapters:1.15.0'
implementation 'com.markomilos.jsonapi:jsonapi-retrofit:1.1.0'
implementation 'io.coil-kt:coil:1.1.0'
implementation 'com.github.johan12345:StfalconImageViewer:5082ebd392'
implementation 'com.github.ev-map:StfalconImageViewer:5082ebd392'
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
implementation 'com.airbnb.android:lottie:4.1.0'
@@ -193,20 +200,22 @@ dependencies {
// AnyMaps
def anyMapsVersion = '7fdcf50fc4'
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
implementation "com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion"
googleImplementation "com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion"
googleImplementation 'com.google.android.gms:play-services-maps:18.1.0'
implementation("com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion") {
implementation("com.github.ev-map.AnyMaps:anymaps-mapbox:$anyMapsVersion") {
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-accounts'
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-telemetry'
exclude group: 'com.google.android.gms', module: 'play-services-location'
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-core'
}
// patched version of mapbox-android-core that removes build-time dependency on GMS
implementation 'com.github.johan12345:mapbox-events-android:a21c324501'
// original version of mapbox-android-core
googleImplementation 'com.mapbox.mapboxsdk:mapbox-android-core:2.0.1'
// patched version that removes build-time dependency on GMS (-> no Google location services)
fossImplementation 'com.github.ev-map:mapbox-events-android:a21c324501'
// Google Places
googleImplementation 'com.google.android.libraries.places:places:3.0.0'
googleImplementation 'com.google.android.libraries.places:places:3.1.0'
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4'
// Mapbox Geocoding
@@ -217,18 +226,18 @@ dependencies {
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// viewmodel library
def lifecycle_version = "2.5.1"
def lifecycle_version = "2.6.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// room library
def room_version = "2.5.0"
def room_version = "2.5.1"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
// billing library
def billing_version = "5.1.0"
def billing_version = "6.0.0"
googleImplementation "com.android.billingclient:billing:$billing_version"
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
@@ -239,26 +248,27 @@ dependencies {
implementation("ch.acra:acra-limiter:$acraVersion")
// debug tools
implementation 'com.facebook.stetho:stetho:1.6.0'
implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0'
debugImplementation 'com.facebook.flipper:flipper:0.190.0'
debugImplementation 'com.facebook.soloader:soloader:0.10.5'
debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.190.0'
// testing
testImplementation 'junit:junit:4.13.2'
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
testImplementation "com.squareup.okhttp3:mockwebserver:4.11.0"
//noinspection GradleDependency
testImplementation 'org.json:json:20080701'
// testing for car app
testGoogleImplementation "androidx.car.app:app-testing:$carAppVersion"
testGoogleImplementation 'org.robolectric:robolectric:4.9'
testGoogleImplementation 'org.robolectric:robolectric:4.9.2'
testGoogleImplementation 'androidx.test:core:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.13.0"
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.15.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
}
private static String decode(String s, String key) {

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
android:exported="true" />
</application>
</manifest>

View File

@@ -0,0 +1,42 @@
package net.vonforst.evmap
import android.content.Context
import android.os.Build
import com.facebook.flipper.android.AndroidFlipperClient
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
import com.facebook.flipper.plugins.inspector.DescriptorMapping
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
import com.facebook.soloader.SoLoader
import okhttp3.OkHttpClient
private val networkFlipperPlugin = NetworkFlipperPlugin()
fun addDebugInterceptors(context: Context) {
if (Build.FINGERPRINT == "robolectric") return
SoLoader.init(context, false)
val client = AndroidFlipperClient.getInstance(context)
client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()))
client.addPlugin(networkFlipperPlugin)
client.addPlugin(DatabasesFlipperPlugin(context))
client.addPlugin(SharedPreferencesFlipperPlugin(context))
client.start()
}
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder {
// Flipper does not work during unit tests - so check whether we are running tests first
var isRunningTest = true
try {
Class.forName("org.junit.Test")
} catch (e: ClassNotFoundException) {
isRunningTest = false
}
if (!isRunningTest) {
this.addNetworkInterceptor(FlipperOkhttpInterceptor(networkFlipperPlugin))
}
return this
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Vond je EVMap nuttig\? Je kan de ontwikkeling ondersteunen door een donatie te sturen naar de ontwikkelaar.</string>
<string name="donate_paypal">Doneer via PayPal</string>
<string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap (Mapbox).</string>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="data_sources_hint">Os dados do mapa são fornecidos pelo OpenStreetMap (Mapbox).</string>
<string name="donate_paypal">Doar com o PayPal</string>
<string name="donations_info" formatted="false">Acha que o EVMap é útil\? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.</string>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Crezi ca EVMap este util? Sprijina dezvoltarea printr-o donatie pentru dezvoltator.</string>
<string name="donate_paypal">Doneaza cu PayPal</string>
<string name="data_sources_hint">Hartile din aplicatie sunt furnizate de OpenStreetMap (Mapbox).</string>
</resources>

View File

@@ -8,6 +8,9 @@ package net.vonforst.evmap.auto
private val models = mapOf(
"Audi" to mapOf(
"516 (G4x)" to "e-tron"
),
"Renault" to mapOf(
"BCB" to "Megane E-Tech"
)
)

View File

@@ -275,7 +275,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!!
meta = metaMapped.chargePoints.maxByOrNull { it.power }
prices = result.data!!.map { cp ->
prices = result.data!!.mapNotNull { cp ->
val filteredPrices =
cp.chargepointPrices.filter {
it.plug == chargepoint.plug && it.power == chargepoint.power
@@ -287,7 +287,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
chargepointPrices = filteredPrices
)
}
}.filterNotNull()
}
.sortedBy { it.chargepointPrices.first().price ?: Double.MAX_VALUE }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||
@@ -352,7 +352,7 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
} else if (vehicles.size > 1) {
if (modelName != null) {
vehicles = vehicles.filter {
it.name.startsWith(modelName)
it.name.lowercase().startsWith(modelName.lowercase())
}
if (vehicles.isEmpty()) {
throw VehicleUnknownException()

View File

@@ -23,8 +23,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.*
import net.vonforst.evmap.api.availability.AvailabilityRepository
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.iconForPlugType
@@ -57,6 +57,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
private val db = AppDatabase.getInstance(carContext)
private val repo =
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
private val availabilityRepo = AvailabilityRepository(ctx)
private val imageSize = 128 // images should be 128dp according to docs
private val imageSizeLarge = 480 // images should be 480 x 480 dp according to docs
@@ -465,7 +466,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
invalidate()
availability = getAvailability(charger).data
availability = availabilityRepo.getAvailability(charger).data
invalidate()
} else {

View File

@@ -24,8 +24,8 @@ import com.car2go.maps.model.LatLng
import kotlinx.coroutines.*
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.AvailabilityRepository
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
@@ -79,6 +79,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
private val db = AppDatabase.getInstance(carContext)
private val repo =
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
private val availabilityRepo = AvailabilityRepository(ctx)
private val searchRadius = 5 // kilometers
private val distanceUpdateThreshold = Duration.ofSeconds(15)
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
@@ -325,7 +326,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
}
// power
val power = charger.maxPower;
val power = charger.maxPower
if (power != null) {
if (text.isNotEmpty()) text.append(" · ")
text.append("${power.roundToInt()} kW")
@@ -577,7 +578,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
override fun onItemVisibilityChanged(startIndex: Int, endIndex: Int) {
// when the list is scrolled, load corresponding availabilities
if (startIndex == visibleStart && endIndex == visibleEnd && !availabilities.isEmpty()) return
if (startIndex == visibleStart && endIndex == visibleEnd && availabilities.isNotEmpty()) return
if (startIndex == -1 || endIndex == -1) return
if (availabilityUpdateCoroutine != null) return
@@ -606,7 +607,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession) :
// update only if not yet stored
if (!availabilities.containsKey(it.id)) {
lifecycleScope.async {
val availability = getAvailability(it).data
val availability = availabilityRepo.getAvailability(it).data
val date = ZonedDateTime.now()
availabilities[it.id] = date to availability
}

View File

@@ -86,6 +86,7 @@ abstract class MultiSelectSearchScreen<T>(ctx: CarContext) : Screen(ctx),
addItem(
Row.Builder()
.setTitle(getLabel(item))
.apply { getDetails(item)?.let { addText(it) } }
.setImage(if (isSelected(item)) checkedIcon else uncheckedIcon)
.setOnClickListener {
toggleSelected(item)
@@ -130,5 +131,7 @@ abstract class MultiSelectSearchScreen<T>(ctx: CarContext) : Screen(ctx),
abstract fun getLabel(it: T): String
open fun getDetails(it: T): String? = null
abstract suspend fun loadData(): List<T>
}

View File

@@ -390,6 +390,8 @@ class SelectVehiclesScreen(ctx: CarContext) : MultiSelectSearchScreen<Chargepric
override fun getLabel(it: ChargepriceCar) = "${it.brand} ${it.name}"
override fun getDetails(it: ChargepriceCar) = it.formatSpecs()
override suspend fun loadData(): List<ChargepriceCar> {
return api.getVehicles()
}

View File

@@ -38,10 +38,10 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
): List<AutocompletePlace> {
val request = FindAutocompletePredictionsRequest.builder().apply {
if (location != null) {
setLocationBias(calcLocationBias(location))
setOrigin(LatLng(location.latitude, location.longitude))
locationBias = calcLocationBias(location)
origin = LatLng(location.latitude, location.longitude)
}
setSessionToken(token)
sessionToken = token
setQuery(query)
}.build()
try {
@@ -92,10 +92,11 @@ class GooglePlacesAutocompleteProvider(val context: Context) : AutocompleteProvi
}
}
override fun getAttributionString(): Int = R.string.places_powered_by_google
override fun getAttributionString(): Int =
com.google.android.libraries.places.R.string.places_powered_by_google
override fun getAttributionImage(dark: Boolean): Int =
if (dark) R.drawable.places_powered_by_google_dark else R.drawable.places_powered_by_google_light
if (dark) com.google.android.libraries.places.R.drawable.places_powered_by_google_dark else com.google.android.libraries.places.R.drawable.places_powered_by_google_light
private fun calcLocationBias(location: com.car2go.maps.model.LatLng): RectangularBounds {
val radius = 100e3 // meters

View File

@@ -3,12 +3,8 @@ package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.preference.MultiSelectListPreference
import net.vonforst.evmap.R
import net.vonforst.evmap.ui.RangeSliderPreference
import net.vonforst.evmap.viewmodel.SettingsViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
import java.text.NumberFormat
class AndroidAutoSettingsFragment : BaseSettingsFragment() {

View File

@@ -15,6 +15,12 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
.setListener(this)
.enablePendingPurchases()
.build()
val products: MutableLiveData<Resource<List<DonationItem>>> by lazy {
MutableLiveData<Resource<List<DonationItem>>>().apply {
value = Resource.loading(null)
}
}
init {
billingClient.startConnection(object : BillingClientStateListener {
@@ -70,12 +76,6 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
}
}
val products: MutableLiveData<Resource<List<DonationItem>>> by lazy {
MutableLiveData<Resource<List<DonationItem>>>().apply {
value = Resource.loading(null)
}
}
val purchaseSuccessful = SingleLiveEvent<Nothing>()
val purchaseFailed = SingleLiveEvent<Nothing>()

View File

@@ -35,4 +35,6 @@
<string name="settings_android_auto_chargeprice_range">Plage de charge pour la comparaison des prix</string>
<string name="welcome_android_auto_detail">Vous pouvez également utiliser EVMap à partir d\'Android Auto sur les voitures prises en charge. Il suffit de sélectionner l\'application EVMap dans le menu Android Auto.</string>
<string name="loading">Chargement…</string>
<string name="auto_multipage_goto">Page %d</string>
<string name="auto_multipage">(%d/%d)</string>
</resources>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="auto_chargeprice_vehicle_ambiguous">Meerdere voertuigen geselecteerd in de app komen overeen met dit voertuig (%1$s %2$s).</string>
<string name="donations_info" formatted="false">Vind je EVMap nuttig\? Je kan de ontwikkeling steunen via een donatie aan de ontwikkelaar.
\n
\nGoogle houdt 15% in van elke donatie.</string>
<string name="auto_location_service">EVMap draait op Android Auto en gebruikt jouw locatie.</string>
<string name="auto_no_chargers_found">Geen laadpunten gevonden in de omgeving</string>
<string name="auto_no_favorites_found">Geen favorieten gevonden</string>
<string name="open_in_app">Open in de app</string>
<string name="opened_on_phone">Geopend op de telefoon</string>
<string name="auto_location_permission_needed">Om EVMap op Android Auto te gebruiken, moet je toegang geven tot je locatie.</string>
<string name="auto_vehicle_data_permission_needed">Voor deze functie heeft EVMap toegang nodig tot de gegevens van je voertuig.</string>
<string name="grant_on_phone">Geef toestemming op telefoon</string>
<string name="auto_chargers_closeby">Oplaadpunten in de buurt</string>
<string name="auto_favorites">Favorieten</string>
<string name="auto_chargers_near_location">Nabij %s</string>
<string name="auto_fault_report_date">⚠️ Foutrapport (%s)</string>
<string name="auto_no_refresh_possible">Verdere updates zijn niet mogelijk. Ga terug en herbegin.</string>
<string name="auto_prices">Prijzen</string>
<string name="auto_vehicle_data">Voertuiggegevens</string>
<string name="auto_charging_level">Laadniveau (SoC)</string>
<string name="auto_no_data">Niet beschikbaar</string>
<string name="auto_range">Reikwijdte</string>
<string name="auto_speed">Snelheid</string>
<string name="auto_heading">Richting</string>
<string name="auto_settings">Instellingen</string>
<string name="welcome_android_auto">Android Auto support</string>
<string name="welcome_android_auto_detail">Je kan EVMap ook gebruiken in Android Auto op ondersteunde voertuigen. Selecteer gewoon de EVMap app in het Android Auto menu.</string>
<string name="sounds_cool">klinkt cool</string>
<string name="auto_chargeprice_vehicle_unavailable">EVMap kon je voertuigtype niet bepalen.</string>
<string name="auto_chargers_ahead">Alleen laadpunten in rijrichting</string>
<string name="settings_android_auto_chargeprice_range">Laadbereik voor prijsvergelijking</string>
<string name="data_sources_hint">In de instellingen kan je ook wisselen tussen Google Maps en OpenStreetMap (Mapbox) voor de kaartgegevens.</string>
<string name="selecting_all">alle items geselecteerd</string>
<string name="selecting_none">alle items gedeselecteerd</string>
<string name="loading">Laden…</string>
<string name="auto_multipage_goto">Pagina %d</string>
<string name="auto_multipage">(%d/%d)</string>
<string name="auto_chargeprice_vehicle_unknown">Geen enkel voertuig geselecteerd in de app komt overeen met dit voertuig (%1$s %2$s).</string>
</resources>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="auto_no_chargers_found">Não foram encontrados carregadores próximo de si</string>
<string name="auto_no_favorites_found">Nenhum favorito encontrado</string>
<string name="opened_on_phone">Aberto no telefone</string>
<string name="auto_location_permission_needed">Para usar o EVMap no Android Auto, permita o acesso à sua localização.</string>
<string name="open_in_app">Abrir na app</string>
<string name="auto_vehicle_data_permission_needed">Para esta funcionalidade, o EVMap precisa de aceder aos dados do seu veículo.</string>
<string name="auto_chargers_closeby">Carregadores próximos</string>
<string name="grant_on_phone">Conceda permissões no telefone</string>
<string name="auto_chargers_near_location">Perto de %s</string>
<string name="auto_favorites">Favoritos</string>
<string name="auto_chargeprice_vehicle_ambiguous">Vários veículos selecionados na app correspondem a este veículo (%1$s %2$s).</string>
<string name="selecting_none">todos os items desmarcados</string>
<string name="data_sources_hint">Também pode mudar entre o Google Maps e OpenStreetMap (Mapbox) nas definições da app.</string>
<string name="selecting_all">todos os items selecionados</string>
<string name="loading">Carregando…</string>
<string name="auto_multipage_goto">Página %d</string>
<string name="auto_multipage">(%d/%d)</string>
<string name="settings_android_auto_chargeprice_range">Escala de carregamento para comparação de preços</string>
<string name="donations_info" formatted="false">Acha que o EVMap é útil\? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.
\n
\nA Google cobra 15% de cada doação.</string>
<string name="auto_location_service">O EVMap está a funcionar no Android Auto e usando a sua localização.</string>
<string name="auto_fault_report_date">⚠️ Problemas (%s)</string>
<string name="auto_no_refresh_possible">Não é possível atualizar. Por favor volte atrás e reinicie.</string>
<string name="auto_prices">Preços</string>
<string name="auto_vehicle_data">Dados do veículo</string>
<string name="auto_charging_level">Nível de carregamento</string>
<string name="auto_no_data">Não disponível</string>
<string name="auto_speed">Velocidade</string>
<string name="auto_heading">Direção</string>
<string name="auto_settings">Definições</string>
<string name="welcome_android_auto">Suporte para Android Auto</string>
<string name="auto_range">Alcance</string>
<string name="welcome_android_auto_detail">Também pode usar o EVMap no Android Auto em carros compatíveis. Basta selecionar a app EVMap no menu do Android Auto.</string>
<string name="auto_chargeprice_vehicle_unavailable">O EVMap não pôde determinar o modelo do seu veículo.</string>
<string name="auto_chargeprice_vehicle_unknown">Nenhum dos veículos selecionados na app corresponde a este veículo (%1$s %2$s).</string>
<string name="auto_chargers_ahead">Apenas carregadores na direção do destino</string>
<string name="sounds_cool">Continuar</string>
</resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="grant_on_phone">Toestaan</string>
<string name="auto_location_permission_needed">Om EVmap te gebruiken in je wagen, moet je toegang geven tot je locatie.</string>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="grant_on_phone">Permitir</string>
<string name="auto_location_permission_needed">Para usar o EVMap no seu carro, permita o acesso à sua localização.</string>
</resources>

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@@ -23,6 +23,8 @@
<application
android:name=".EvMapApplication"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/backup_rules_api31"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"

View File

@@ -1,7 +1,6 @@
package net.vonforst.evmap
import android.app.Application
import com.facebook.stetho.Stetho
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.updateAppLocale
import net.vonforst.evmap.ui.updateNightMode
@@ -24,8 +23,8 @@ class EvMapApplication : Application() {
prefs.language = null
}
Stetho.initializeWithDefaults(this);
init(applicationContext)
addDebugInterceptors(applicationContext)
if (!BuildConfig.DEBUG) {
initAcra {

View File

@@ -55,8 +55,8 @@ class MapsActivity : AppCompatActivity(),
private lateinit var prefs: PreferenceDataSource
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_maps)
@@ -207,7 +207,7 @@ class MapsActivity : AppCompatActivity(),
intent.data = Uri.parse("google.navigation:q=${coord.lat},${coord.lng}")
intent.`package` = "com.google.android.apps.maps"
if (prefs.navigateUseMaps && intent.resolveActivity(packageManager) != null) {
startActivity(intent);
startActivity(intent)
} else {
// fallback: generic geo intent
showLocation(charger)
@@ -223,7 +223,7 @@ class MapsActivity : AppCompatActivity(),
})"
)
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent);
startActivity(intent)
} else {
val cb = fragmentCallback ?: return
Snackbar.make(
@@ -262,7 +262,7 @@ class MapsActivity : AppCompatActivity(),
fun shareUrl(url: String) {
val intent = Intent(Intent.ACTION_SEND).apply {
setType("text/plain")
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, url)
}
startActivity(intent)

View File

@@ -90,7 +90,7 @@ class ConnectorAdapter : DataBindingAdapter<ConnectorAdapter.ChargepointWithAvai
class ChargepriceAdapter() :
DataBindingAdapter<ChargePrice>() {
val viewPool = RecyclerView.RecycledViewPool();
val viewPool = RecyclerView.RecycledViewPool()
var meta: ChargepriceChargepointMeta? = null
set(value) {
field = value

View File

@@ -1,8 +1,13 @@
package net.vonforst.evmap.adapter
import android.content.Context
import android.graphics.Typeface
import android.text.Spannable
import android.text.style.StyleSpan
import androidx.core.text.HtmlCompat
import androidx.core.text.buildSpannedString
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.TeslaGraphQlApi
import net.vonforst.evmap.bold
import net.vonforst.evmap.joinToSpannedString
import net.vonforst.evmap.model.ChargeCard
@@ -10,6 +15,7 @@ import net.vonforst.evmap.model.ChargeCardId
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.OpeningHoursDays
import net.vonforst.evmap.plus
import net.vonforst.evmap.ui.currency
import net.vonforst.evmap.utils.formatDMS
import net.vonforst.evmap.utils.formatDecimal
import java.time.ZoneId
@@ -41,11 +47,18 @@ fun buildDetails(
loc: ChargeLocation?,
chargeCards: Map<Long, ChargeCard>?,
filteredChargeCards: Set<Long>?,
teslaPricing: TeslaGraphQlApi.Pricing?,
ctx: Context
): List<DetailsAdapter.Detail> {
if (loc == null) return emptyList()
return listOfNotNull(
if (teslaPricing != null) DetailsAdapter.Detail(
R.drawable.ic_tesla,
R.string.cost,
formatTeslaPricing(teslaPricing, ctx),
formatTeslaParkingFee(teslaPricing, ctx)
) else null,
if (loc.address != null) DetailsAdapter.Detail(
R.drawable.ic_address,
R.string.address,
@@ -61,7 +74,8 @@ fun buildDetails(
if (loc.network != null) DetailsAdapter.Detail(
R.drawable.ic_network,
R.string.network,
loc.network
loc.network,
clickable = loc.networkUrl != null
) else null,
if (loc.faultReport != null) DetailsAdapter.Detail(
R.drawable.ic_fault_report,
@@ -125,6 +139,128 @@ fun buildDetails(
)
}
private fun formatTeslaParkingFee(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
teslaPricing.memberRates?.activePricebook?.parking?.let { parkingFee ->
ctx.getString(
R.string.tesla_pricing_blocking_fee,
formatTeslaPricingRate(parkingFee.rates, parkingFee.currencyCode, parkingFee.uom, ctx)
)
}
private fun formatTeslaPricing(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
buildSpannedString {
teslaPricing.memberRates?.let { memberRates ->
append(
ctx.getString(if (teslaPricing.userRates != null) R.string.tesla_pricing_members else R.string.tesla_pricing_owners),
StyleSpan(Typeface.BOLD),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
append(formatTeslaPricingRates(memberRates, ctx))
}
teslaPricing.userRates?.let { userRates ->
append("\n\n")
append(
ctx.getString(R.string.tesla_pricing_others),
StyleSpan(Typeface.BOLD),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
append(formatTeslaPricingRates(userRates, ctx))
}
}
private fun formatTeslaPricingRates(rates: TeslaGraphQlApi.Rates, ctx: Context) =
buildSpannedString {
val timeFmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
if (rates.activePricebook.charging.touRates.enabled) {
// time-of-day-based rates
val ratesByTime = rates.activePricebook.charging.touRates.activeRatesByTime
val distinctRates =
ratesByTime.map { it.rates }.distinct().sortedByDescending { it.max() }
if (distinctRates.size == 2) {
// special case: only list periods with higher price
val highPriceTimes = ratesByTime.filter { it.rates == distinctRates[0] }
append("\n")
append(highPriceTimes.joinToString(", ") {
timeFmt.format(it.startTime) + " - " + timeFmt.format(it.endTime)
} + ": ", StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
append(
formatTeslaPricingRate(
distinctRates[0],
rates.activePricebook.charging.currencyCode,
rates.activePricebook.charging.uom,
ctx
)
)
append("\n")
append(
ctx.getString(R.string.tesla_pricing_other_times),
StyleSpan(Typeface.ITALIC),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
append(" ")
append(
formatTeslaPricingRate(
distinctRates[1],
rates.activePricebook.charging.currencyCode,
rates.activePricebook.charging.uom,
ctx
)
)
} else {
// general case
ratesByTime.forEach { rate ->
append("\n")
append(
timeFmt.format(rate.startTime) + " - " + timeFmt.format(rate.endTime) + ": ",
StyleSpan(Typeface.ITALIC),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
append(
formatTeslaPricingRate(
rate.rates,
rates.activePricebook.charging.currencyCode,
rates.activePricebook.charging.uom,
ctx
)
)
}
}
} else {
// fixed rates
append(" ")
append(
formatTeslaPricingRate(
rates.activePricebook.charging.rates,
rates.activePricebook.charging.currencyCode,
rates.activePricebook.charging.uom,
ctx
)
)
}
}
private fun formatTeslaPricingRate(
rates: List<Double>,
currencyCode: String,
uom: String,
ctx: Context
): String {
if (rates.isEmpty()) return ""
val rate = rates.max()
val value = ctx.getString(
when (uom) {
"kwh" -> R.string.charge_price_kwh_format
"min" -> R.string.charge_price_minute_format
else -> return ""
}, rate, currency(currencyCode)
)
return if (rates.size > 1) {
ctx.getString(R.string.pricing_up_to, value)
} else {
value
}
}
fun formatChargeCards(
chargecards: List<ChargeCardId>,
chargecardData: Map<Long, ChargeCard>?,

View File

@@ -5,14 +5,14 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.ImageView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.memory.MemoryCache
import coil.size.OriginalSize
import coil.size.SizeResolver
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.model.ChargerPhoto
@@ -37,27 +37,43 @@ class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener?
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val id = getItem(position).id
val url = getItem(position).getUrl(height = holder.view.height)
val item = getItem(position)
holder.view.load(
url
) {
size(SizeResolver(OriginalSize))
allowHardware(false)
listener(
onSuccess = { _, metadata ->
memoryKeys[id] = metadata.memoryCacheKey
if (holder.view.height == 0) {
holder.view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
holder.view.viewTreeObserver.removeOnGlobalLayoutListener(this)
loadImage(item, holder)
}
)
})
} else {
loadImage(item, holder)
}
if (itemClickListener != null) {
holder.view.setOnClickListener {
itemClickListener.onItemClick(holder.view, position, memoryKeys[id])
itemClickListener.onItemClick(holder.view, position, memoryKeys[item.id])
}
}
}
private fun loadImage(
item: ChargerPhoto,
holder: ViewHolder
) {
val url = item.getUrl(height = holder.view.height)
holder.view.load(
url
) {
listener(
onSuccess = { _, metadata ->
memoryKeys[item.id] = metadata.memoryCacheKey
}
)
allowHardware(!BuildConfig.DEBUG)
}
}
}
class ChargerPhotoDiffCallback : DiffUtil.ItemCallback<ChargerPhoto>() {

View File

@@ -10,7 +10,7 @@ class RateLimitInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.url.host == "my.newmotion.com") {
if (request.url.host == "ui-map.shellrecharge.com") {
// limit requests sent to NewMotion to 3 per second
rateLimiter.acquire(1)

View File

@@ -1,14 +1,16 @@
package net.vonforst.evmap.api.availability
import com.facebook.stetho.okhttp3.StethoInterceptor
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.RateLimitInterceptor
import net.vonforst.evmap.api.await
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.cartesianProduct
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
import net.vonforst.evmap.viewmodel.Resource
import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient
@@ -133,7 +135,9 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
data class ChargeLocationStatus(
val status: Map<Chargepoint, List<ChargepointStatus>>,
val source: String,
val evseIds: Map<Chargepoint, List<String>>? = null
val evseIds: Map<Chargepoint, List<String>>? = null,
val congestionHistogram: List<Double>? = null,
val extraData: Any? = null // API-specific data
) {
fun applyFilters(connectors: Set<String>?, minPower: Int?): ChargeLocationStatus {
val statusFiltered = status.filterKeys {
@@ -158,38 +162,41 @@ private val cookieManager = CookieManager().apply {
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
}
private val okhttp = OkHttpClient.Builder()
.addInterceptor(RateLimitInterceptor())
.addNetworkInterceptor(StethoInterceptor())
.readTimeout(10, TimeUnit.SECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
.cookieJar(JavaNetCookieJar(cookieManager))
.build()
val availabilityDetectors = listOf(
RheinenergieAvailabilityDetector(okhttp),
EnBwAvailabilityDetector(okhttp),
NewMotionAvailabilityDetector(okhttp)
)
class AvailabilityRepository(context: Context) {
private val okhttp = OkHttpClient.Builder()
.addInterceptor(RateLimitInterceptor())
.addDebugInterceptors()
.readTimeout(10, TimeUnit.SECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
.cookieJar(JavaNetCookieJar(cookieManager))
.build()
private val availabilityDetectors = listOf(
RheinenergieAvailabilityDetector(okhttp),
TeslaAvailabilityDetector(okhttp, EncryptedPreferenceDataStore(context)),
EnBwAvailabilityDetector(okhttp),
NewMotionAvailabilityDetector(okhttp)
)
suspend fun getAvailability(charger: ChargeLocation): Resource<ChargeLocationStatus> {
var value: Resource<ChargeLocationStatus>? = null
withContext(Dispatchers.IO) {
for (ad in availabilityDetectors) {
if (!ad.isChargerSupported(charger)) continue
try {
value = Resource.success(ad.getAvailability(charger))
break
} catch (e: IOException) {
value = Resource.error(e.message, null)
e.printStackTrace()
} catch (e: HttpException) {
value = Resource.error(e.message, null)
e.printStackTrace()
} catch (e: AvailabilityDetectorException) {
value = Resource.error(e.message, null)
e.printStackTrace()
suspend fun getAvailability(charger: ChargeLocation): Resource<ChargeLocationStatus> {
var value: Resource<ChargeLocationStatus>? = null
withContext(Dispatchers.IO) {
for (ad in availabilityDetectors) {
if (!ad.isChargerSupported(charger)) continue
try {
value = Resource.success(ad.getAvailability(charger))
break
} catch (e: IOException) {
value = Resource.error(e.message, null)
e.printStackTrace()
} catch (e: HttpException) {
value = Resource.error(e.message, null)
e.printStackTrace()
} catch (e: AvailabilityDetectorException) {
value = Resource.error(e.message, null)
e.printStackTrace()
}
}
}
return value ?: Resource.error(null, null)
}
return value ?: Resource.error(null, null)
}

View File

@@ -142,7 +142,7 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
) < maxDistance
}
var details = markers.filter {
val details = markers.filter {
// only include stations from same operator
it.operator == nearest.operator && it.stationId != null
}.map {
@@ -203,30 +203,46 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
val country = charger.chargepriceData?.country
?: charger.address?.country ?: return false
return when (charger.dataSource) {
// list of countries as of 2021/06/30, according to
// https://www.electrive.net/2021/06/30/enbw-expandiert-mit-ladenetz-in-drei-weitere-laender/
// list of countries as of 2023/04/14, according to
// https://www.enbw.com/elektromobilitaet/produkte/ladetarife
"goingelectric" -> country in listOf(
"Deutschland",
"Österreich",
"Schweiz",
"Frankreich",
"Belgien",
"Niederlande",
"Luxemburg",
"Liechtenstein",
"Dänemark",
"Frankreich",
"Italien",
)
"Kroatien",
"Liechtenstein",
"Luxemburg",
"Niederlande",
"Polen",
"Schweden",
"Slowakei",
"Slowenien",
"Spanien",
"Tschechien"
) && charger.network != "Tesla Supercharger"
"openchargemap" -> country in listOf(
"DE",
"AT",
"CH",
"FR",
"BE",
"NE",
"LU",
"DK",
"FR",
"IT",
"HR",
"LI",
"IT"
)
"LU",
"NE",
"PL",
"SE",
"SK",
"SI",
"ES",
"CZ"
) && charger.chargepriceData?.network !in listOf("23", "3534")
else -> false
}
}

View File

@@ -15,12 +15,13 @@ private const val coordRange = 0.005 // range of latitude and longitude for loa
private const val maxDistance = 40 // max distance between reported positions in meters
interface NewMotionApi {
@GET("markers/{lngMin}/{lngMax}/{latMin}/{latMax}")
@GET("markers/{lngMin}/{lngMax}/{latMin}/{latMax}/{zoom}")
suspend fun getMarkers(
@Path("lngMin") lngMin: Double,
@Path("lngMax") lngMax: Double,
@Path("latMin") latMin: Double,
@Path("latMax") latMax: Double
@Path("latMax") latMax: Double,
@Path("zoom") zoom: Int = 22
): List<NMMarker>
@GET("locations/{id}")
@@ -76,7 +77,7 @@ interface NewMotionApi {
companion object {
fun create(client: OkHttpClient, baseUrl: String? = null): NewMotionApi {
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl ?: "https://my.newmotion.com/api/map/v2/")
.baseUrl(baseUrl ?: "https://ui-map.shellrecharge.com/api/map/v2/")
.addConverterFactory(MoshiConverterFactory.create())
.client(client)
.build()
@@ -181,7 +182,11 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
override fun isChargerSupported(charger: ChargeLocation): Boolean {
// NewMotion is our fallback
return true
return when (charger.dataSource) {
"goingelectric" -> charger.network != "Tesla Supercharger"
"openchargemap" -> charger.chargepriceData?.network !in listOf("23", "3534")
else -> false
}
}
}

View File

@@ -0,0 +1,628 @@
package net.vonforst.evmap.api.availability
import android.util.Base64
import com.squareup.moshi.FromJson
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import kotlinx.coroutines.runBlocking
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.model.Coordinate
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import java.io.IOException
import java.security.MessageDigest
import java.security.SecureRandom
import java.time.Instant
import java.time.LocalTime
import java.util.Collections
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
interface TeslaAuthenticationApi {
@POST("oauth2/v3/token")
suspend fun getToken(@Body request: OAuth2Request): OAuth2Response
@JsonClass(generateAdapter = true)
class AuthCodeRequest(
val code: String,
@Json(name = "code_verifier") val codeVerifier: String,
@Json(name = "redirect_uri") val redirectUri: String = "https://auth.tesla.com/void/callback",
scope: String = "openid email offline_access",
@Json(name = "client_id") clientId: String = "ownerapi"
) : OAuth2Request(scope, clientId)
@JsonClass(generateAdapter = true)
class RefreshTokenRequest(
@Json(name = "refresh_token") val refreshToken: String,
scope: String = "openid email offline_access",
@Json(name = "client_id") clientId: String = "ownerapi"
) : OAuth2Request(scope, clientId)
sealed class OAuth2Request(
val scope: String,
val clientId: String
)
@JsonClass(generateAdapter = true)
data class OAuth2Response(
@Json(name = "access_token") val accessToken: String,
@Json(name = "token_type") val tokenType: String,
@Json(name = "expires_in") val expiresIn: Long,
@Json(name = "refresh_token") val refreshToken: String,
)
companion object {
fun create(client: OkHttpClient, baseUrl: String? = null): TeslaAuthenticationApi {
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl ?: "https://auth.tesla.com")
.addConverterFactory(
MoshiConverterFactory.create(
Moshi.Builder()
.add(
PolymorphicJsonAdapterFactory.of(
OAuth2Request::class.java,
"grant_type"
)
.withSubtype(AuthCodeRequest::class.java, "authorization_code")
.withSubtype(RefreshTokenRequest::class.java, "refresh_token")
.withDefaultValue(null)
)
.build()
)
)
.client(client)
.build()
return retrofit.create(TeslaAuthenticationApi::class.java)
}
fun generateCodeVerifier(): String {
val code = ByteArray(64)
SecureRandom().nextBytes(code)
return Base64.encodeToString(
code,
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
)
}
fun generateCodeChallenge(codeVerifier: String): String {
val bytes = codeVerifier.toByteArray()
val messageDigest = MessageDigest.getInstance("SHA-256")
messageDigest.update(bytes, 0, bytes.size)
return Base64.encodeToString(
messageDigest.digest(),
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
)
}
}
}
interface TeslaOwnerApi {
@GET("/api/1/users/me")
suspend fun getUserInfo(): UserInfoResponse
@JsonClass(generateAdapter = true)
data class UserInfoResponse(
val response: UserInfo
)
@JsonClass(generateAdapter = true)
data class UserInfo(
val email: String,
@Json(name = "full_name") val fullName: String,
@Json(name = "profile_image_url") val profileImageUrl: String?
)
companion object {
fun create(client: OkHttpClient, token: String, baseUrl: String? = null): TeslaOwnerApi {
val clientWithInterceptor = client.newBuilder()
.addInterceptor { chain ->
// add API key to every request
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $token")
.header("User-Agent", "okhttp/4.9.2")
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27")
.header("Accept", "*/*")
.build()
chain.proceed(request)
}.build()
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl ?: "https://owner-api.teslamotors.com")
.addConverterFactory(MoshiConverterFactory.create())
.client(clientWithInterceptor)
.build()
return retrofit.create(TeslaOwnerApi::class.java)
}
}
}
interface TeslaGraphQlApi {
@POST("/graphql")
suspend fun getNearbyChargingSites(
@Body request: GetNearbyChargingSitesRequest,
@Query("operationName") operationName: String = "GetNearbyChargingSites",
@Query("deviceLanguage") deviceLanguage: String = "en",
@Query("deviceCountry") deviceCountry: String = "US",
@Query("ttpLocale") ttpLocale: String = "en_US",
@Query("vin") vin: String = "",
): GetNearbyChargingSitesResponse
@POST("/graphql")
suspend fun getChargingSiteInformation(
@Body request: GetChargingSiteInformationRequest,
@Query("operationName") operationName: String = "getChargingSiteInformation",
@Query("deviceLanguage") deviceLanguage: String = "en",
@Query("deviceCountry") deviceCountry: String = "US",
@Query("ttpLocale") ttpLocale: String = "en_US",
@Query("vin") vin: String = "",
): GetChargingSiteInformationResponse
@JsonClass(generateAdapter = true)
data class GetNearbyChargingSitesRequest(
override val variables: GetNearbyChargingSitesVariables,
override val operationName: String = "GetNearbyChargingSites",
override val query: String =
"\n query GetNearbyChargingSites(\$args: GetNearbyChargingSitesRequestType!) {\n charging {\n nearbySites(args: \$args) {\n sitesAndDistances {\n ...ChargingNearbySitesFragment\n }\n }\n }\n}\n \n fragment ChargingNearbySitesFragment on ChargerSiteAndDistanceType {\n activeOutages {\n message\n nonTeslasAffectedOnly {\n value\n }\n }\n availableStalls {\n value\n }\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n drivingDistanceMiles {\n value\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n haversineDistanceMiles {\n value\n }\n id {\n text\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n trtId {\n value\n }\n totalStalls {\n value\n }\n siteType\n accessType\n waitEstimateBucket\n hasHighCongestion\n}\n \n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n "
) : GraphQlRequest()
@JsonClass(generateAdapter = true)
data class GetNearbyChargingSitesVariables(val args: GetNearbyChargingSitesArgs)
@JsonClass(generateAdapter = true)
data class GetNearbyChargingSitesArgs(
val userLocation: Coordinate,
val northwestCorner: Coordinate,
val southeastCorner: Coordinate,
val openToNonTeslasFilter: OpenToNonTeslasFilterValue,
val languageCode: String = "en",
val countryCode: String = "US",
//val vin: String = "",
//val maxCount: Int = 100
)
@JsonClass(generateAdapter = true)
data class OpenToNonTeslasFilterValue(val value: Boolean)
@JsonClass(generateAdapter = true)
data class Coordinate(val latitude: Double, val longitude: Double)
@JsonClass(generateAdapter = true)
data class GetChargingSiteInformationRequest(
override val variables: GetChargingSiteInformationVariables,
override val operationName: String = "getChargingSiteInformation",
override val query: String =
"\n query getChargingSiteInformation(\$id: ChargingSiteIdentifierInputType!, \$vehicleMakeType: ChargingVehicleMakeTypeEnum, \$deviceCountry: String!, \$deviceLanguage: String!) {\n charging {\n site(\n id: \$id\n deviceCountry: \$deviceCountry\n deviceLanguage: \$deviceLanguage\n vehicleMakeType: \$vehicleMakeType\n ) {\n siteStatic {\n ...SiteStaticFragmentNoHoldAmt\n }\n siteDynamic {\n ...SiteDynamicFragment\n }\n pricing(vehicleMakeType: \$vehicleMakeType) {\n userRates {\n ...ChargingActiveRateFragment\n }\n memberRates {\n ...ChargingActiveRateFragment\n }\n hasMembershipPricing\n hasMSPPricing\n canDisplayCombinedComparison\n }\n holdAmount(vehicleMakeType: \$vehicleMakeType) {\n currencyCode\n holdAmount\n }\n congestionPriceHistogram(vehicleMakeType: \$vehicleMakeType) {\n ...CongestionPriceHistogramFragment\n }\n }\n }\n}\n \n fragment SiteStaticFragmentNoHoldAmt on ChargingSiteStaticType {\n address {\n ...AddressFragment\n }\n amenities\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n id {\n text\n }\n accessCode {\n value\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n name\n openToPublic\n chargers {\n id {\n text\n }\n label {\n value\n }\n }\n publicStallCount\n timeZone {\n id\n version\n }\n fastchargeSiteId {\n value\n }\n siteType\n accessType\n isMagicDockSupportedSite\n trtId {\n value\n }\n}\n \n fragment AddressFragment on EnergySvcAddressType {\n streetNumber {\n value\n }\n street {\n value\n }\n district {\n value\n }\n city {\n value\n }\n state {\n value\n }\n postalCode {\n value\n }\n country\n}\n \n\n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n \n\n fragment SiteDynamicFragment on ChargingSiteDynamicType {\n id {\n text\n }\n activeOutages {\n message\n nonTeslasAffectedOnly {\n value\n }\n }\n chargersAvailable {\n value\n }\n chargerDetails {\n charger {\n id {\n text\n }\n label {\n value\n }\n name\n }\n availability\n }\n waitEstimateBucket\n currentCongestion\n}\n \n\n fragment ChargingActiveRateFragment on ChargingActiveRateType {\n activePricebook {\n charging {\n ...ChargingUserRateFragment\n }\n parking {\n ...ChargingUserRateFragment\n }\n priceBookID\n }\n}\n \n fragment ChargingUserRateFragment on ChargingUserRateType {\n currencyCode\n programType\n rates\n buckets {\n start\n end\n }\n bucketUom\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n uom\n vehicleMakeType\n}\n \n\n fragment CongestionPriceHistogramFragment on HistogramData {\n axisLabels {\n index\n value\n }\n regionLabels {\n index\n value {\n ...ChargingPriceFragment\n ... on HistogramRegionLabelValueString {\n value\n }\n }\n }\n chargingUom\n parkingUom\n parkingRate {\n ...ChargingPriceFragment\n }\n data\n activeBar\n maxRateIndex\n whenRateChanges\n dataAttributes {\n congestionThreshold\n label\n }\n}\n \n fragment ChargingPriceFragment on ChargingPrice {\n currencyCode\n price\n}\n"
) : GraphQlRequest()
@JsonClass(generateAdapter = true)
data class GetChargingSiteInformationVariables(
val id: ChargingSiteIdentifier,
val vehicleMakeType: VehicleMakeType,
val deviceLanguage: String = "en",
val deviceCountry: String = "US",
val ttpLocale: String = "en_US"
)
@JsonClass(generateAdapter = true)
data class ChargingSiteIdentifier(
val id: String,
val type: ChargingSiteIdentifierType = ChargingSiteIdentifierType.SITE_ID
)
enum class ChargingSiteIdentifierType {
SITE_ID
}
enum class VehicleMakeType {
TESLA, NON_TESLA
}
sealed class GraphQlRequest {
abstract val operationName: String
abstract val query: String
abstract val variables: Any?
}
@JsonClass(generateAdapter = true)
data class GetNearbyChargingSitesResponse(val data: GetNearbyChargingSitesResponseData)
@JsonClass(generateAdapter = true)
data class GetNearbyChargingSitesResponseData(val charging: GetNearbyChargingSitesResponseDataCharging)
@JsonClass(generateAdapter = true)
data class GetNearbyChargingSitesResponseDataCharging(val nearbySites: GetNearbyChargingSitesResponseDataChargingNearbySites)
@JsonClass(generateAdapter = true)
data class GetNearbyChargingSitesResponseDataChargingNearbySites(val sitesAndDistances: List<ChargingSite>)
@JsonClass(generateAdapter = true)
data class ChargingSite(
val activeOutages: List<Outage>,
val availableStalls: Value<Int>?,
val centroid: Coordinate,
val drivingDistanceMiles: Value<Double>?,
val entryPoint: Coordinate,
val haversineDistanceMiles: Value<Double>,
val id: Text,
val localizedSiteName: Value<String>,
val maxPowerKw: Value<Int>,
val totalStalls: Value<Int>
// TODO: siteType, accessType
)
@JsonClass(generateAdapter = true)
data class Outage(val message: String /* TODO: */)
@JsonClass(generateAdapter = true)
data class Value<T : Any>(val value: T)
@JsonClass(generateAdapter = true)
data class Text(val text: String)
@JsonClass(generateAdapter = true)
data class GetChargingSiteInformationResponse(val data: GetChargingSiteInformationResponseData)
@JsonClass(generateAdapter = true)
data class GetChargingSiteInformationResponseData(val charging: GetChargingSiteInformationResponseDataCharging)
@JsonClass(generateAdapter = true)
data class GetChargingSiteInformationResponseDataCharging(val site: ChargingSiteInformation)
@JsonClass(generateAdapter = true)
data class ChargingSiteInformation(
val siteDynamic: SiteDynamic,
val siteStatic: SiteStatic,
val pricing: Pricing,
val congestionPriceHistogram: CongestionPriceHistogram,
)
@JsonClass(generateAdapter = true)
data class SiteDynamic(
val activeOutages: List<Outage>,
val chargerDetails: List<ChargerDetail>,
val chargersAvailable: Value<Int>?,
val currentCongestion: Double,
val id: Text,
val waitEstimateBucket: WaitEstimateBucket
)
@JsonClass(generateAdapter = true)
data class ChargerDetail(
val availability: ChargerAvailability,
val charger: ChargerId
)
@JsonClass(generateAdapter = true)
data class ChargerId(
val id: Text,
val label: Value<String>,
val name: String?
) {
val labelNumber
get() = label.value.replace(Regex("""\D"""), "").toInt()
val labelLetter
get() = label.value.replace(Regex("""\d"""), "")
}
@JsonClass(generateAdapter = true)
data class SiteStatic(
val accessCode: Value<String>?,
val centroid: Coordinate,
val chargers: List<ChargerId>,
val entryPoint: Coordinate,
val fastchargeSiteId: Value<Long>,
val id: Text,
val isMagicDockSupportedSite: Boolean,
val localizedSiteName: Value<String>,
val maxPowerKw: Value<Int>,
val name: String,
val openToPublic: Boolean,
val publicStallCount: Int
// TODO: siteType, accessType, address, amenities, timeZone
)
@JsonClass(generateAdapter = true)
data class Pricing(
val canDisplayCombinedComparison: Boolean,
val hasMSPPricing: Boolean,
val hasMembershipPricing: Boolean,
val memberRates: Rates?, // rates for Tesla drivers & non-Tesla drivers with subscription
val userRates: Rates? // rates without subscription
)
@JsonClass(generateAdapter = true)
data class Rates(
val activePricebook: Pricebook
)
@JsonClass(generateAdapter = true)
data class Pricebook(
val charging: PricebookDetails,
val parking: PricebookDetails,
val priceBookID: Long
)
@JsonClass(generateAdapter = true)
data class PricebookDetails(
val bucketUom: String, // unit of measurement for buckets (typically "kw")
val buckets: List<Bucket>, // buckets of charging power (used for minute-based pricing)
val currencyCode: String,
val programType: String,
val rates: List<Double>,
val touRates: TouRates,
val uom: String, // unit of measurement ("kwh" or "min")
val vehicleMakeType: String
)
@JsonClass(generateAdapter = true)
data class Bucket(
val start: Int,
val end: Int
)
@JsonClass(generateAdapter = true)
data class TouRates(
val activeRatesByTime: List<ActiveRatesByTime>,
val enabled: Boolean
)
@JsonClass(generateAdapter = true)
data class ActiveRatesByTime(
val startTime: LocalTime,
val endTime: LocalTime,
val rates: List<Double>
)
@JsonClass(generateAdapter = true)
data class CongestionPriceHistogram(
val data: List<Double>,
val dataAttributes: List<CongestionHistogramDataAttributes>
)
@JsonClass(generateAdapter = true)
data class CongestionHistogramDataAttributes(
val congestionThreshold: String, // "LEVEL_1"
val label: String // "1AM", "2AM", etc.
)
enum class ChargerAvailability {
@Json(name = "CHARGER_AVAILABILITY_AVAILABLE")
AVAILABLE,
@Json(name = "CHARGER_AVAILABILITY_OCCUPIED")
OCCUPIED,
@Json(name = "CHARGER_AVAILABILITY_DOWN")
DOWN,
@Json(name = "CHARGER_AVAILABILITY_UNKNOWN")
UNKNOWN;
fun toStatus() = when (this) {
AVAILABLE -> ChargepointStatus.AVAILABLE
OCCUPIED -> ChargepointStatus.OCCUPIED
DOWN -> ChargepointStatus.FAULTED
UNKNOWN -> ChargepointStatus.UNKNOWN
}
}
enum class WaitEstimateBucket {
@Json(name = "WAIT_ESTIMATE_BUCKET_NO_WAIT")
NO_WAIT,
@Json(name = "WAIT_ESTIMATE_BUCKET_LESS_THAN_5_MINUTES")
LESS_THAN_5_MINUTES,
@Json(name = "WAIT_ESTIMATE_BUCKET_APPROXIMATELY_5_MINUTES")
APPROXIMATELY_5_MINUTES,
@Json(name = "WAIT_ESTIMATE_BUCKET_APPROXIMATELY_10_MINUTES")
APPROXIMATELY_10_MINUTES,
@Json(name = "WAIT_ESTIMATE_BUCKET_APPROXIMATELY_15_MINUTES")
APPROXIMATELY_15_MINUTES,
@Json(name = "WAIT_ESTIMATE_BUCKET_APPROXIMATELY_20_MINUTES")
APPROXIMATELY_20_MINUTES,
@Json(name = "WAIT_ESTIMATE_BUCKET_UNKNOWN")
UNKNOWN
}
companion object {
fun create(
client: OkHttpClient,
baseUrl: String? = null,
token: suspend () -> String
): TeslaGraphQlApi {
val clientWithInterceptor = client.newBuilder()
.addInterceptor { chain ->
val t = runBlocking { token() }
// add API key to every request
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $t")
.header("User-Agent", "okhttp/4.9.2")
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27")
.header("Accept", "*/*")
.build()
chain.proceed(request)
}.build()
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl ?: "https://akamai-apigateway-charging-ownership.tesla.com")
.addConverterFactory(
MoshiConverterFactory.create(
Moshi.Builder().add(LocalTimeAdapter()).build()
)
)
.client(clientWithInterceptor)
.build()
return retrofit.create(TeslaGraphQlApi::class.java)
}
}
}
internal class LocalTimeAdapter {
@FromJson
fun fromJson(value: String?): LocalTime? = value?.let {
if (it == "24:00") LocalTime.MAX else LocalTime.parse(it)
}
@ToJson
fun toJson(value: LocalTime?): String? = value?.toString()
}
fun Coordinate.asTeslaCoord() =
TeslaGraphQlApi.Coordinate(this.lat, this.lng)
class TeslaAvailabilityDetector(
private val client: OkHttpClient,
private val tokenStore: TokenStore,
private val baseUrl: String? = null
) :
BaseAvailabilityDetector(client) {
private val authApi = TeslaAuthenticationApi.create(client, null)
private var api: TeslaGraphQlApi? = null
interface TokenStore {
var teslaRefreshToken: String?
var teslaAccessToken: String?
var teslaAccessTokenExpiry: Long
}
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
val api = initApi()
val req = TeslaGraphQlApi.GetNearbyChargingSitesRequest(
TeslaGraphQlApi.GetNearbyChargingSitesVariables(
TeslaGraphQlApi.GetNearbyChargingSitesArgs(
location.coordinates.asTeslaCoord(),
TeslaGraphQlApi.Coordinate(
location.coordinates.lat + coordRange,
location.coordinates.lng - coordRange
),
TeslaGraphQlApi.Coordinate(
location.coordinates.lat - coordRange,
location.coordinates.lng + coordRange
),
TeslaGraphQlApi.OpenToNonTeslasFilterValue(false)
)
)
)
val results = api.getNearbyChargingSites(
req,
req.operationName
).data.charging.nearbySites.sitesAndDistances
val result =
results.minByOrNull { it.haversineDistanceMiles.value }
?: throw AvailabilityDetectorException("no candidates found.")
val details = api.getChargingSiteInformation(
TeslaGraphQlApi.GetChargingSiteInformationRequest(
TeslaGraphQlApi.GetChargingSiteInformationVariables(
TeslaGraphQlApi.ChargingSiteIdentifier(result.id.text),
TeslaGraphQlApi.VehicleMakeType.NON_TESLA
)
)
).data.charging.site
val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
val scV2CCSConnectors = location.chargepoints.filter {
it.type in listOf(
Chargepoint.CCS_TYPE_2,
Chargepoint.CCS_UNKNOWN
) && it.power != null && it.power <= 150
}
val scV3Connectors = location.chargepoints.filter {
it.type in listOf(
Chargepoint.CCS_TYPE_2,
Chargepoint.CCS_UNKNOWN
) && it.power != null && it.power > 150
}
if (location.totalChargepoints != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } + scV2CCSConnectors.sumOf { it.count }) throw AvailabilityDetectorException(
"charger has unknown connectors"
)
val statusSorted = details.siteDynamic.chargerDetails.sortedBy { it.charger.labelLetter }
.sortedBy { it.charger.labelNumber }
val statusMap = emptyMap<Chargepoint, List<ChargepointStatus>>().toMutableMap()
var i = 0
for (connector in scV2Connectors) {
statusMap[connector] =
statusSorted.subList(i, i + connector.count).map { it.availability.toStatus() }
i += connector.count
}
if (scV2CCSConnectors.isNotEmpty()) {
i = 0
for (connector in scV2CCSConnectors) {
statusMap[connector] =
statusSorted.subList(i, i + connector.count).map { it.availability.toStatus() }
i += connector.count
}
}
for (connector in scV3Connectors) {
statusMap[connector] =
statusSorted.subList(i, i + connector.count).map { it.availability.toStatus() }
i += connector.count
}
val indexOfMidnight =
details.congestionPriceHistogram.dataAttributes.indexOfFirst { it.label == "12AM" }
val congestionHistogram = indexOfMidnight.takeIf { it >= 0 }?.let { index ->
val data = details.congestionPriceHistogram.data.toMutableList()
Collections.rotate(data, -index)
data
}
return ChargeLocationStatus(
statusMap,
"Tesla",
congestionHistogram = congestionHistogram,
extraData = details.pricing
)
}
override fun isChargerSupported(charger: ChargeLocation): Boolean {
return when (charger.dataSource) {
"goingelectric" -> charger.network == "Tesla Supercharger"
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
else -> false
}
}
private suspend fun initApi(): TeslaGraphQlApi {
return api ?: run {
val newApi = TeslaGraphQlApi.create(client, baseUrl) {
val now = Instant.now().epochSecond
val token =
tokenStore.teslaAccessToken.takeIf { tokenStore.teslaAccessTokenExpiry > now }
?: run {
val refreshToken = tokenStore.teslaRefreshToken
?: throw IOException("not signed in")
val response =
authApi.getToken(
TeslaAuthenticationApi.RefreshTokenRequest(
refreshToken
)
)
tokenStore.teslaAccessToken = response.accessToken
tokenStore.teslaAccessTokenExpiry = now + response.expiresIn
response.accessToken
}
token
}
api = newApi
newApi
}
}
}

View File

@@ -1,13 +1,13 @@
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.adapters.PolymorphicJsonAdapterFactory
import jsonapi.Document
import jsonapi.JsonApiFactory
import jsonapi.retrofit.DocumentConverterFactory
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.model.ChargeLocation
import okhttp3.Cache
import okhttp3.OkHttpClient
@@ -77,10 +77,10 @@ interface ChargepriceApi {
chain.proceed(new)
}
if (BuildConfig.DEBUG) {
addNetworkInterceptor(StethoInterceptor())
addDebugInterceptors()
}
if (context != null) {
cache(Cache(context.getCacheDir(), cacheSize))
cache(Cache(context.cacheDir, cacheSize))
}
}.build()
@@ -127,14 +127,16 @@ interface ChargepriceApi {
charger.chargepriceData?.country?.let { isCountrySupported(it, charger.dataSource) }
?: false
val networkSupported = charger.chargepriceData?.network?.let {
if (charger.dataSource == "openchargemap") {
it !in listOf(
when (charger.dataSource) {
"openchargemap" -> it !in listOf(
"1", // unknown operator
"44", // private residence/individual
"45" // business owner at location
"45", // business owner at location
"23", "3534" // Tesla
)
} else {
true
"goingelectric" -> it != "Tesla Supercharger"
else -> true
}
} ?: false
val powerAvailable = charger.chargepoints.all { it.hasKnownPower() }
@@ -163,7 +165,7 @@ interface ChargepriceApi {
"Spanien",
"Großbritannien",
"Irland",
// additional countries found 2022/09/17, https://github.com/johan12345/EVMap/issues/234
// additional countries found 2022/09/17, https://github.com/ev-map/EVMap/issues/234
"Finnland",
"Lettland",
"Litauen",
@@ -202,7 +204,7 @@ interface ChargepriceApi {
"ES",
"GB",
"IE",
// additional countries found 2022/09/17, https://github.com/johan12345/EVMap/issues/234
// additional countries found 2022/09/17, https://github.com/ev-map/EVMap/issues/234
"FI",
"LV",
"LT",

View File

@@ -114,8 +114,26 @@ data class ChargepriceCar(
val brand: String,
@Json(name = "dc_charge_ports")
val dcChargePorts: List<String>
val dcChargePorts: List<String>,
@Json(name = "usable_battery_size")
val usableBatterySize: Float,
@Json(name = "ac_max_power")
val acMaxPower: Float,
@Json(name = "dc_max_power")
val dcMaxPower: Float?
) : Equatable, Parcelable {
fun formatSpecs(): String = buildString {
append("%.0f kWh".format(usableBatterySize))
append(" | ")
append("AC %.0f kW".format(acMaxPower))
dcMaxPower?.let {
append(" | ")
append("DC %.0f kW".format(it))
}
}
companion object {
private val acConnectors = listOf(
@@ -139,9 +157,9 @@ data class ChargepriceCar(
get() = id_!!
val compatibleEvmapConnectors: List<String>
get() = dcChargePorts.map {
get() = dcChargePorts.mapNotNull {
plugMapping[it]
}.filterNotNull().plus(acConnectors)
}.plus(acConnectors)
}
@JsonClass(generateAdapter = true)
@@ -210,7 +228,7 @@ internal object RelationshipsParceler : Parceler<Relationships?> {
if (parcel.readInt() == 0) return null
val nMembers = parcel.readInt()
val members = (0 until nMembers).map { _ ->
val members = (0 until nMembers).associate { _ ->
val key = parcel.readString()!!
val value = if (parcel.readInt() == 0) {
val type = parcel.readString()
@@ -229,7 +247,7 @@ internal object RelationshipsParceler : Parceler<Relationships?> {
Relationship.ToMany(ris)
}
key to value
}.toMap()
}
return Relationships(members)
}
@@ -281,12 +299,12 @@ data class ChargepointPrice(
}
fun time(value: Int): String {
val h = floor(value.toDouble() / 60).toInt();
val min = ceil(value.toDouble() % 60).toInt();
if (h == 0 && min > 0) return "${min}min";
val h = floor(value.toDouble() / 60).toInt()
val min = ceil(value.toDouble() % 60).toInt()
return if (h == 0 && min > 0) "${min}min";
// be slightly sloppy (3:01 is shown as 3h) to save space
else if (h > 0 && (min == 0 || min == 1)) return "${h}h";
else return "%d:%02dh".format(h, min);
else if (h > 0 && (min == 0 || min == 1)) "${h}h";
else "%d:%02dh".format(h, min)
}
// based on https://github.com/chargeprice/chargeprice-client/blob/d420bb2f216d9ad91a210a36dd0859a368a8229a/src/views/priceList.js

View File

@@ -1,9 +1,9 @@
package net.vonforst.evmap.api.fronyx
import android.content.Context
import com.facebook.stetho.okhttp3.StethoInterceptor
import com.squareup.moshi.Moshi
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import okhttp3.Cache
@@ -49,10 +49,10 @@ private interface FronyxApiRetrofit {
chain.proceed(new)
}
if (BuildConfig.DEBUG) {
addNetworkInterceptor(StethoInterceptor())
addDebugInterceptors()
}
if (context != null) {
cache(Cache(context.getCacheDir(), cacheSize))
cache(Cache(context.cacheDir, cacheSize))
}
}.build()

View File

@@ -5,6 +5,7 @@ import com.squareup.moshi.*
import java.lang.reflect.Type
import java.time.Instant
import java.time.LocalTime
import java.time.format.DateTimeParseException
internal class ChargepointListItemJsonAdapterFactory : JsonAdapter.Factory {
@@ -13,12 +14,12 @@ internal class ChargepointListItemJsonAdapterFactory : JsonAdapter.Factory {
annotations: MutableSet<out Annotation>,
moshi: Moshi
): JsonAdapter<*>? {
if (Types.getRawType(type) == GEChargepointListItem::class.java) {
return ChargepointListItemJsonAdapter(
return if (Types.getRawType(type) == GEChargepointListItem::class.java) {
ChargepointListItemJsonAdapter(
moshi
)
} else {
return null
null
}
}
@@ -138,7 +139,12 @@ internal class HoursAdapter {
val end = if (match.groupValues[2] == "24:00") {
LocalTime.MAX
} else {
LocalTime.parse(match.groupValues[2])
try {
LocalTime.parse(match.groupValues[2])
} catch (e: DateTimeParseException) {
// got a rare bug report where the value is 24:0000
LocalTime.MIN
}
}
return GEHours(start, end)
} else {

View File

@@ -3,7 +3,6 @@ package net.vonforst.evmap.api.goingelectric
import android.content.Context
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import com.facebook.stetho.okhttp3.StethoInterceptor
import com.squareup.moshi.Moshi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@@ -11,6 +10,7 @@ import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.*
import net.vonforst.evmap.model.*
import net.vonforst.evmap.ui.cluster
@@ -104,10 +104,10 @@ interface GoingElectricApi {
chain.proceed(original)
}
if (BuildConfig.DEBUG) {
addNetworkInterceptor(StethoInterceptor())
addDebugInterceptors()
}
if (context != null) {
cache(Cache(context.getCacheDir(), cacheSize))
cache(Cache(context.cacheDir, cacheSize))
}
}.build()
@@ -146,7 +146,7 @@ class GoingElectricApiWrapper(
val minPower = filters?.getSliderValue("min_power")
val minConnectors = filters?.getSliderValue("min_connectors")
var connectorsVal = filters?.getMultipleChoiceValue("connectors")
val connectorsVal = filters?.getMultipleChoiceValue("connectors")
if (connectorsVal != null && connectorsVal.values.isEmpty() && !connectorsVal.all) {
// no connectors chosen
return Resource.success(emptyList())
@@ -217,7 +217,7 @@ class GoingElectricApiWrapper(
}
} while (startkey != null && startkey < 10000)
var result = postprocessResult(data, minPower, connectorsVal, minConnectors, zoom)
val result = postprocessResult(data, minPower, connectorsVal, minConnectors, zoom)
return Resource.success(result)
}
@@ -240,7 +240,7 @@ class GoingElectricApiWrapper(
val minPower = filters?.getSliderValue("min_power")
val minConnectors = filters?.getSliderValue("min_connectors")
var connectorsVal = filters?.getMultipleChoiceValue("connectors")
val connectorsVal = filters?.getMultipleChoiceValue("connectors")
if (connectorsVal != null && connectorsVal.values.isEmpty() && !connectorsVal.all) {
// no connectors chosen
return Resource.success(emptyList())
@@ -404,11 +404,11 @@ class GoingElectricApiWrapper(
val networks = refData.networks
val chargeCards = refData.chargecards
val plugMap = plugs.map { plug ->
plug to nameForPlugType(sp, GEChargepoint.convertTypeFromGE(plug))
}.toMap()
val networkMap = networks.map { it to it }.toMap()
val chargecardMap = chargeCards.map { it.id.toString() to it.name }.toMap()
val plugMap = plugs.associateWith { plug ->
nameForPlugType(sp, GEChargepoint.convertTypeFromGE(plug))
}
val networkMap = networks.associateWith { it }
val chargecardMap = chargeCards.associate { it.id.toString() to it.name }
val categoryMap = mapOf(
"Autohaus" to sp.getString(R.string.category_car_dealership),
"Autobahnraststätte" to sp.getString(R.string.category_service_on_motorway),

View File

@@ -77,6 +77,8 @@ data class GEChargeLocation(
cost?.convert(),
null,
ChargepriceData(address.country, network, chargepoints.map { it.type }),
null,
null,
Instant.now(),
isDetailed
)
@@ -147,7 +149,7 @@ data class GEChargerPhoto(val id: String) {
@JsonClass(generateAdapter = true)
class GEChargerPhotoAdapter(override val id: String, val apikey: String) :
ChargerPhoto(id) {
override fun getUrl(height: Int?, width: Int?, size: Int?): String {
override fun getUrl(height: Int?, width: Int?, size: Int?, allowOriginal: Boolean): String {
return "https://api.goingelectric.de/chargepoints/photo/?key=${apikey}&id=$id" +
when {
size != null -> "&size=$size"
@@ -209,6 +211,7 @@ data class GEChargepoint(val type: String, val power: Double, val count: Int) {
"Typ1" -> Chargepoint.TYPE_1
"Typ2" -> Chargepoint.TYPE_2_UNKNOWN
"Typ3" -> Chargepoint.TYPE_3
"Tesla Supercharger CCS" -> Chargepoint.CCS_UNKNOWN
"CCS" -> Chargepoint.CCS_UNKNOWN
"Schuko" -> Chargepoint.SCHUKO
"CHAdeMO" -> Chargepoint.CHADEMO

View File

@@ -3,11 +3,11 @@ package net.vonforst.evmap.api.openchargemap
import android.content.Context
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import com.facebook.stetho.okhttp3.StethoInterceptor
import com.squareup.moshi.Moshi
import kotlinx.coroutines.Dispatchers
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.*
import net.vonforst.evmap.model.*
import net.vonforst.evmap.ui.cluster
@@ -83,7 +83,7 @@ interface OpenChargeMapApi {
chain.proceed(new)
}
if (BuildConfig.DEBUG) {
addNetworkInterceptor(StethoInterceptor())
addDebugInterceptors()
}
if (context != null) {
cache(Cache(context.cacheDir, cacheSize))
@@ -148,18 +148,18 @@ class OpenChargeMapApiWrapper(
),
minPower = minPower,
plugs = connectors,
operators = operators,
statusType = if (excludeFaults == true) noFaultStatuses.joinToString(",") else null
operators = operators
)
if (!response.isSuccessful) {
return Resource.error(response.message(), null)
}
var result = postprocessResult(
val result = postprocessResult(
response.body()!!,
minPower,
connectorsVal,
minConnectors,
excludeFaults,
refData,
zoom
)
@@ -202,8 +202,7 @@ class OpenChargeMapApiWrapper(
radius.toDouble(),
minPower = minPower,
plugs = connectors,
operators = operators,
statusType = if (excludeFaults == true) noFaultStatuses.joinToString(",") else null
operators = operators
)
if (!response.isSuccessful) {
return Resource.error(response.message(), null)
@@ -214,6 +213,7 @@ class OpenChargeMapApiWrapper(
minPower,
connectorsVal,
minConnectors,
excludeFaults,
refData,
zoom
)
@@ -228,6 +228,7 @@ class OpenChargeMapApiWrapper(
minPower: Double?,
connectorsVal: MultipleChoiceFilterValue?,
minConnectors: Int?,
excludeFaults: Boolean?,
referenceData: OCMReferenceData,
zoom: Float
): List<ChargepointListItem> {
@@ -237,6 +238,8 @@ class OpenChargeMapApiWrapper(
.filter { it.power == null || it.power >= (minPower ?: 0.0) }
.filter { if (connectorsVal != null && !connectorsVal.all) it.connectionTypeId in connectorsVal.values.map { it.toLong() } else true }
.sumOf { it.quantity ?: 1 } >= (minConnectors ?: 0)
}.filter {
it.statusTypeId == null || (it.statusTypeId !in removedStatuses && if (excludeFaults == true) it.statusTypeId !in faultStatuses else true)
}.map { it.convert(referenceData, false) }.distinct() as List<ChargepointListItem>
// apply clustering
@@ -286,8 +289,8 @@ class OpenChargeMapApiWrapper(
): List<Filter<FilterValue>> {
val refData = referenceData as OCMReferenceData
val operatorsMap = refData.operators.map { it.id.toString() to it.title }.toMap()
val plugMap = refData.connectionTypes.map { it.id.toString() to it.title }.toMap()
val operatorsMap = refData.operators.associate { it.id.toString() to it.title }
val plugMap = refData.connectionTypes.associate { it.id.toString() to it.title }
return listOf(
// supported by OCM API

View File

@@ -11,12 +11,15 @@ import java.time.Instant
import java.time.ZonedDateTime
// Unknown, Currently Available, Currently In Use, Operational
val noFaultStatuses = listOf(0, 10, 20, 50)
val noFaultStatuses = listOf(0L, 10L, 20L, 50L)
// Temporarily Unavailable, Partly Operational, Not Operational, Planned For Future Date, Removed (Decommissioned)
val faultStatuses = listOf(30L, 75L, 100L, 150L, 200L)
// Temporarily Unavailable, Partly Operational, Not Operational, Planned For Future Date
val faultStatuses = listOf(30L, 75L, 100L, 150L)
val faultReportCommentType = 1000L
// Removed (Decommissioned), Removed (Duplicate Listing)
val removedStatuses = listOf(200L, 210L)
data class OCMBoundingBox(
val sw_lat: Double, val sw_lng: Double,
val ne_lat: Double, val ne_lng: Double
@@ -71,10 +74,16 @@ data class OCMChargepoint(
addressInfo.countryISOCode(refData),
operatorId?.toString(),
connections.map { "${it.connectionTypeId},${it.currentTypeId}" }),
operatorInfo?.websiteUrl,
if (operatorInfo?.websiteUrl?.withoutTrailingSlash() != addressInfo.relatedUrl?.withoutTrailingSlash()) addressInfo.relatedUrl else null,
Instant.now(),
isDetailed
)
private fun String.withoutTrailingSlash(): String {
return this.replace(Regex("/$"), "")
}
private fun convertFaultReport(): FaultReport? {
if (statusTypeId in faultStatuses || connections.any { it.statusTypeId in faultStatuses }) {
if (userComments != null) {
@@ -251,14 +260,13 @@ class OCMChargerPhotoAdapter(
val largeUrl: String,
val thumbUrl: String
) : ChargerPhoto(id) {
override fun getUrl(height: Int?, width: Int?, size: Int?): String {
override fun getUrl(height: Int?, width: Int?, size: Int?, allowOriginal: Boolean): String {
val maxSize = size ?: max(height, width)
val mediumUrl = thumbUrl.replace(".thmb.", ".medi.")
return when (maxSize) {
0 -> mediumUrl
in 1..100 -> thumbUrl
in 0..100 -> thumbUrl
in 101..400 -> mediumUrl
else -> largeUrl
else -> if (allowOriginal) largeUrl else mediumUrl
}
}
}

View File

@@ -105,6 +105,8 @@ data class OSMChargingStation(
getCost(),
"© OpenStreetMap contributors",
null,
null,
null,
dataFetchTimestamp,
true,
)
@@ -118,7 +120,7 @@ data class OSMChargingStation(
// If that is missing as well, use a generic "Charging Station" string.
return tags["name"]
?: tags["operator"]
?: "Charging Station";
?: "Charging Station"
}
/**
@@ -191,7 +193,7 @@ data class OSMChargingStation(
*/
fun parseOutputPower(rawOutput: String?): Double? {
if (rawOutput == null) {
return null;
return null
}
val pattern = Regex("([0-9.,]+)\\s*(kW|kVA)", setOf(RegexOption.IGNORE_CASE))
val matchResult = pattern.matchEntire(rawOutput) ?: return null

View File

@@ -176,10 +176,10 @@ enum class AutocompletePlaceType {
companion object {
fun valueOfOrNull(value: String): AutocompletePlaceType? {
try {
return valueOf(value)
return try {
valueOf(value)
} catch (e: IllegalArgumentException) {
return null
null
}
}
}

View File

@@ -114,7 +114,7 @@ class MapboxAutocompleteProvider(val context: Context) : AutocompleteProvider {
override fun getAttributionString(): Int = R.string.powered_by_mapbox
override fun getAttributionImage(dark: Boolean): Int =
if (dark) R.drawable.mapbox_logo_icon else R.drawable.mapbox_logo
if (dark) com.mapbox.mapboxsdk.R.drawable.mapbox_logo_icon else R.drawable.mapbox_logo
}
private fun BoundingBox.toLatLngBounds(): LatLngBounds {

View File

@@ -123,12 +123,12 @@ class ChargepriceFragment : Fragment() {
val charger = fragmentArgs.charger
vm.charger.value = charger
if (vm.chargepoint.value == null) {
vm.chargepoint.value = charger.chargepointsMerged.get(0)
vm.chargepoint.value = charger.chargepointsMerged[0]
}
val vehicleAdapter = CheckableChargepriceCarAdapter()
headerBinding.vehicleSelection.adapter = vehicleAdapter
val vehicleObserver: Observer<ChargepriceCar> = Observer {
val vehicleObserver: Observer<ChargepriceCar?> = Observer {
vehicleAdapter.setCheckedItem(it)
}
vm.vehicle.observe(viewLifecycleOwner, vehicleObserver)

View File

@@ -80,8 +80,8 @@ class FilterProfilesFragment : Fragment() {
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPos = viewHolder.bindingAdapterPosition;
val toPos = target.bindingAdapterPosition;
val fromPos = viewHolder.bindingAdapterPosition
val toPos = target.bindingAdapterPosition
val list = vm.filterProfiles.value?.toMutableList()
if (list != null) {

View File

@@ -40,8 +40,6 @@ import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import coil.load
import coil.memory.MemoryCache
import coil.size.OriginalSize
import coil.size.SizeResolver
import com.car2go.maps.AnyMap
import com.car2go.maps.MapFragment
import com.car2go.maps.OnMapReadyCallback
@@ -75,6 +73,7 @@ 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.fragment.preference.DataSettingsFragmentArgs
import net.vonforst.evmap.location.FusionEngine
import net.vonforst.evmap.location.LocationEngine
import net.vonforst.evmap.location.Priority
@@ -130,16 +129,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
val state = bottomSheetBehavior.state
if (state != STATE_COLLAPSED && state != STATE_HIDDEN) {
if (bottomSheetCollapsible) {
when (state) {
STATE_COLLAPSED -> vm.chargerSparse.value = null
STATE_HIDDEN -> vm.searchResult.value = null
else -> if (bottomSheetCollapsible) {
bottomSheetBehavior.state = STATE_COLLAPSED
} else {
vm.chargerSparse.value = null
}
} else if (state == STATE_COLLAPSED) {
vm.chargerSparse.value = null
} else if (state == STATE_HIDDEN) {
vm.searchResult.value = null
}
}
}
@@ -164,6 +161,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
savedInstanceState: Bundle?
): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_map, container, false)
println(binding.detailView.sourceButton)
binding.lifecycleOwner = this
binding.vm = vm
@@ -248,7 +246,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
mapFragment!!.getMapAsync(this)
bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet)
bottomSheetBehavior = from(binding.bottomSheet)
detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
binding.detailAppBar.toolbar.inflateMenu(R.menu.detail)
@@ -377,6 +375,16 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
null, extras
)
}
binding.detailView.btnChargerWebsite.setOnClickListener {
val charger = vm.charger.value?.data ?: return@setOnClickListener
charger.chargerUrl?.let { (activity as? MapsActivity)?.openUrl(it) }
}
binding.detailView.btnLogin.setOnClickListener {
findNavController().navigate(
R.id.settings_data,
DataSettingsFragmentArgs(true).toBundle()
)
}
binding.detailView.imgPredictionSource.setOnClickListener {
(activity as? MapsActivity)?.openUrl(getString(R.string.fronyx_url))
}
@@ -387,7 +395,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
.show()
}
binding.detailView.topPart.setOnClickListener {
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
bottomSheetBehavior.state = STATE_ANCHOR_POINT
}
setupSearchAutocomplete()
binding.detailAppBar.toolbar.setNavigationOnClickListener {
@@ -552,7 +560,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private fun setupObservers() {
bottomSheetBehavior.addBottomSheetCallback(object :
BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
BottomSheetCallback() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {
if (bottomSheetBehavior.state == STATE_HIDDEN) {
map?.setPadding(0, mapTopPadding, 0, 0)
@@ -581,9 +589,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
}
})
vm.chargerSparse.observe(viewLifecycleOwner, Observer {
vm.chargerSparse.observe(viewLifecycleOwner) {
if (it != null) {
if (vm.bottomSheetState.value != BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT) {
if (vm.bottomSheetState.value != STATE_ANCHOR_POINT) {
bottomSheetBehavior.state =
if (bottomSheetCollapsible) STATE_COLLAPSED else STATE_ANCHOR_POINT
}
@@ -596,7 +604,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
bottomSheetBehavior.state = STATE_HIDDEN
unhighlightAllMarkers()
}
})
}
vm.chargepoints.observe(viewLifecycleOwner, Observer { res ->
when (res.status) {
Status.ERROR -> {
@@ -626,23 +634,23 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
vm.useMiniMarkers.observe(viewLifecycleOwner) {
vm.chargepoints.value?.data?.let { updateMap(it) }
}
vm.favorites.observe(viewLifecycleOwner, Observer {
vm.favorites.observe(viewLifecycleOwner) {
updateFavoriteToggle()
})
vm.searchResult.observe(viewLifecycleOwner, Observer { place ->
}
vm.searchResult.observe(viewLifecycleOwner) { place ->
displaySearchResult(place, moveCamera = true)
})
vm.layersMenuOpen.observe(viewLifecycleOwner, Observer { open ->
}
vm.layersMenuOpen.observe(viewLifecycleOwner) { open ->
binding.fabLayers.visibility = if (open) View.INVISIBLE else View.VISIBLE
binding.layersSheet.visibility = if (open) View.VISIBLE else View.INVISIBLE
updateBackPressedCallback()
})
vm.mapType.observe(viewLifecycleOwner, Observer {
}
vm.mapType.observe(viewLifecycleOwner) {
map?.setMapType(it)
})
vm.mapTrafficEnabled.observe(viewLifecycleOwner, Observer {
}
vm.mapTrafficEnabled.observe(viewLifecycleOwner) {
map?.setTrafficEnabled(it)
})
}
updateBackPressedCallback()
}
@@ -697,7 +705,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = false,
fault = c.faultReport != null,
multi = c.isMulti(vm.filteredConnectors.value),
fav = c.id in vm.favorites.value?.map { it.charger.id } ?: emptyList(),
fav = c.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
mini = vm.useMiniMarkers.value == true
)
)
@@ -713,7 +721,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = true,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value),
fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList(),
fav = charger.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
mini = vm.useMiniMarkers.value == true
)
)
@@ -728,7 +736,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = false,
fault = c.faultReport != null,
multi = c.isMulti(vm.filteredConnectors.value),
fav = c.id in vm.favorites.value?.map { it.charger.id } ?: emptyList(),
fav = c.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
mini = vm.useMiniMarkers.value == true
)
)
@@ -753,12 +761,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val photos = vm.charger.value?.data?.photos ?: return
viewer = StfalconImageViewer.Builder(context, photos) { imageView, photo ->
imageView.load(photo.getUrl(size = 1000)) {
imageView.load(photo.getUrl(size = 1000, allowOriginal = true)) {
if (photo == photos[position] && imageCacheKey != null) {
placeholderMemoryCacheKey(imageCacheKey)
}
size(SizeResolver(OriginalSize))
allowHardware(false)
}
}
.withTransitionFrom(view as ImageView)
@@ -810,6 +816,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
R.drawable.ic_payment -> {
showPaymentMethodsDialog(charger)
}
R.drawable.ic_network -> {
charger.networkUrl?.let { (activity as? MapsActivity)?.openUrl(it) }
}
}
}
}
@@ -959,10 +968,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
vm.chargerSparse.observe(
viewLifecycleOwner,
object : Observer<ChargeLocation> {
override fun onChanged(item: ChargeLocation?) {
if (item?.id == chargerId) {
override fun onChanged(value: ChargeLocation) {
if (value.id == chargerId) {
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(
LatLng(item.coordinates.lat, item.coordinates.lng), 16f
LatLng(value.coordinates.lat, value.coordinates.lng), 16f
)
map.moveCamera(cameraUpdate)
vm.chargerSparse.removeObserver(this)
@@ -981,9 +990,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
vm.chargepoints.observe(
viewLifecycleOwner,
object : Observer<Resource<List<ChargepointListItem>>> {
override fun onChanged(res: Resource<List<ChargepointListItem>>) {
if (res.data == null) return
for (item in res.data) {
override fun onChanged(value: Resource<List<ChargepointListItem>>) {
if (value.data == null) return
for (item in value.data) {
if (item is ChargeLocation && item.id == chargerId) {
vm.chargerSparse.value = item
vm.chargepoints.removeObserver(this)
@@ -1091,7 +1100,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = highlight,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value),
fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList(),
fav = charger.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
mini = vm.useMiniMarkers.value == true
)
)
@@ -1112,7 +1121,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val fault = charger.faultReport != null
val multi = charger.isMulti(vm.filteredConnectors.value)
val fav =
charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
charger.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList())
animator.animateMarkerDisappear(
marker, tint, highlight, fault, multi, fav,
vm.useMiniMarkers.value == true
@@ -1131,7 +1140,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val highlight = charger.id == vm.chargerSparse.value?.id
val fault = charger.faultReport != null
val multi = charger.isMulti(vm.filteredConnectors.value)
val fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
val fav =
charger.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList())
val marker = map.addMarker(
MarkerOptions()
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
@@ -1185,13 +1195,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val filterBadge = filterView?.findViewById<TextView>(R.id.filter_badge)
if (filterBadge != null) {
// set up badge showing number of active filters
vm.filtersCount.observe(viewLifecycleOwner, Observer {
vm.filtersCount.observe(viewLifecycleOwner) {
filterBadge.visibility = if (it > 0) View.VISIBLE else View.GONE
filterBadge.text = it.toString()
})
}
}
filterView?.setOnClickListener {
var profilesMap: MutableBiMap<Long, MenuItem> = HashBiMap()
val profilesMap: MutableBiMap<Long, MenuItem> = HashBiMap()
val popup = PopupMenu(
ContextThemeWrapper(requireContext(), R.style.RoundedPopup),
@@ -1232,7 +1242,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
}
vm.filterProfiles.observe(viewLifecycleOwner, { profiles ->
vm.filterProfiles.observe(viewLifecycleOwner) { profiles ->
popup.menu.removeGroup(R.id.menu_group_filter_profiles)
val noFiltersItem = popup.menu.add(
@@ -1262,25 +1272,28 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
profilesMap[FILTERS_CUSTOM] = customItem
profilesMap[FILTERS_FAVORITES] = favoritesItem
popup.menu.setGroupCheckable(R.id.menu_group_filter_profiles, true, true);
popup.menu.setGroupCheckable(R.id.menu_group_filter_profiles, true, true)
val manageFiltersItem = popup.menu.findItem(R.id.menu_manage_filter_profiles)
manageFiltersItem.isVisible = profiles.isNotEmpty()
vm.filterStatus.observe(viewLifecycleOwner, Observer { id ->
vm.filterStatus.observe(viewLifecycleOwner) { id ->
when (id) {
FILTERS_DISABLED -> {
customItem.isVisible = false
noFiltersItem.isChecked = true
}
FILTERS_CUSTOM -> {
customItem.isVisible = true
customItem.isChecked = true
}
FILTERS_FAVORITES -> {
customItem.isVisible = false
favoritesItem.isChecked = true
}
else -> {
customItem.isVisible = false
val item = profilesMap[id]
@@ -1290,8 +1303,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
// else unknown ID -> wait for filterProfiles to update
}
}
})
})
}
}
popup.setTouchModal(false)
popup.show()
}

View File

@@ -17,7 +17,7 @@ class MultiSelectDialog : MaterialDialogFragment() {
companion object {
fun getInstance(
title: String,
data: Map<String, String>,
data: Map<String, CharSequence>,
selected: Set<String>,
commonChoices: Set<String>?,
showAllButton: Boolean = true
@@ -55,7 +55,7 @@ class MultiSelectDialog : MaterialDialogFragment() {
override fun initView(view: View, savedInstanceState: Bundle?) {
val args = requireArguments()
val data = args.getSerializable("data") as HashMap<String, String>
val data = args.getSerializable("data") as HashMap<String, CharSequence>
val selected = args.getSerializable("selected") as HashSet<String>
val title = args.getString("title")
val commonChoices = if (args.containsKey("commonChoices")) {
@@ -71,7 +71,7 @@ class MultiSelectDialog : MaterialDialogFragment() {
binding.btnAll.visibility = if (showAllButton) View.VISIBLE else View.INVISIBLE
items = data.entries.toList()
.sortedBy { it.value.lowercase(Locale.getDefault()) }
.sortedBy { it.value.toString().lowercase(Locale.getDefault()) }
.sortedBy {
when {
selected.contains(it.key) && commonChoices?.contains(it.key) == true -> 0
@@ -117,7 +117,7 @@ private fun search(
): List<MultiSelectItem> {
return items.filter { item ->
// search for string within name
text.lowercase(Locale.getDefault()) in item.name.lowercase(Locale.getDefault())
text.lowercase(Locale.getDefault()) in item.name.toString().lowercase(Locale.getDefault())
}
}
@@ -125,4 +125,5 @@ class Adapter() : DataBindingAdapter<MultiSelectItem>({ it.key }) {
override fun getItemViewType(position: Int) = R.layout.dialog_multi_select_item
}
data class MultiSelectItem(val key: String, val name: String, var selected: Boolean) : Equatable
data class MultiSelectItem(val key: String, val name: CharSequence, var selected: Boolean) :
Equatable

View File

@@ -0,0 +1,91 @@
package net.vonforst.evmap.fragment.oauth
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.util.Base64
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.transition.MaterialSharedAxis
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
class OAuthLoginFragment : Fragment() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.fragment_oauth_login, container, false)
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toolbar = view.findViewById<Toolbar>(R.id.toolbar)
toolbar.setupWithNavController(
findNavController(),
(requireActivity() as MapsActivity).appBarConfiguration
)
val args = OAuthLoginFragmentArgs.fromBundle(requireArguments())
val uri = Uri.parse(args.url)
webView = view.findViewById(R.id.webView)
args.color?.let { webView.setBackgroundColor(Color.parseColor(it)) }
val progress = view.findViewById<LinearProgressIndicator>(R.id.progress_indicator)
CookieManager.getInstance().removeAllCookies(null)
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
val url = request.url
if (url.toString().startsWith(args.resultUrlPrefix)) {
val result = Bundle()
result.putString("url", url.toString())
setFragmentResult(args.url, result)
findNavController().popBackStack()
}
return url.host != uri.host
}
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
progress.show()
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
progress.hide()
webView.background = null
}
}
webView.settings.javaScriptEnabled = true
webView.loadUrl(args.url)
}
}

View File

@@ -2,6 +2,7 @@ package net.vonforst.evmap.fragment.preference
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
@@ -15,9 +16,13 @@ import com.mikepenz.aboutlibraries.LibsBuilder
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.storage.PreferenceDataSource
class AboutFragment : PreferenceFragmentCompat() {
private lateinit var prefs: PreferenceDataSource
private var developerOptionsCounter = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialFadeThrough()
@@ -33,6 +38,8 @@ class AboutFragment : PreferenceFragmentCompat() {
(requireActivity() as MapsActivity).appBarConfiguration
)
prefs = PreferenceDataSource(requireContext())
// Workaround for AndroidX bug: https://github.com/material-components/material-components-android/issues/1984
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))
}
@@ -45,6 +52,21 @@ class AboutFragment : PreferenceFragmentCompat() {
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
"version" -> {
if (!prefs.developerModeEnabled) {
developerOptionsCounter += 1
if (developerOptionsCounter >= 7) {
prefs.developerModeEnabled = true
Toast.makeText(
requireContext(),
getString(R.string.developer_mode_enabled),
Toast.LENGTH_SHORT
).show()
}
}
true
}
"contributors" -> {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.about_contributors)
@@ -53,6 +75,7 @@ class AboutFragment : PreferenceFragmentCompat() {
.show()
true
}
"github_link" -> {
(activity as? MapsActivity)?.openUrl(getString(R.string.github_link))
true

View File

@@ -12,14 +12,18 @@ import com.google.android.material.transition.MaterialFadeThrough
import com.google.android.material.transition.MaterialSharedAxis
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
import net.vonforst.evmap.storage.PreferenceDataSource
abstract class BaseSettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
protected lateinit var prefs: PreferenceDataSource
protected lateinit var encryptedPrefs: EncryptedPreferenceDataStore
protected abstract val isTopLevel: Boolean
override fun onCreate(savedInstanceState: Bundle?) {
prefs = PreferenceDataSource(requireContext())
encryptedPrefs = EncryptedPreferenceDataStore(requireContext())
super.onCreate(savedInstanceState)
if (isTopLevel) {
@@ -40,8 +44,6 @@ abstract class BaseSettingsFragment : PreferenceFragmentCompat(),
(requireActivity() as MapsActivity).appBarConfiguration
)
prefs = PreferenceDataSource(requireContext())
// Workaround for AndroidX bug: https://github.com/material-components/material-components-android/issues/1984
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))
}

View File

@@ -2,13 +2,17 @@ package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.RelativeSizeSpan
import android.view.View
import androidx.fragment.app.viewModels
import androidx.preference.MultiSelectListPreference
import net.vonforst.evmap.R
import net.vonforst.evmap.ui.MultiSelectDialogPreference
import net.vonforst.evmap.viewmodel.SettingsViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class ChargepriceSettingsFragment : BaseSettingsFragment() {
override val isTopLevel = false
@@ -22,8 +26,8 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
}
})
private lateinit var myVehiclePreference: MultiSelectListPreference
private lateinit var myTariffsPreference: MultiSelectListPreference
private lateinit var myVehiclePreference: MultiSelectDialogPreference
private lateinit var myTariffsPreference: MultiSelectDialogPreference
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -34,8 +38,16 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
res.data?.let { cars ->
val sortedCars = cars.sortedBy { it.brand }
myVehiclePreference.entryValues = sortedCars.map { it.id }.toTypedArray()
myVehiclePreference.entries =
sortedCars.map { "${it.brand} ${it.name}" }.toTypedArray()
myVehiclePreference.entries = sortedCars.map {
SpannableStringBuilder().apply {
appendLine("${it.brand} ${it.name}")
append(
it.formatSpecs(),
RelativeSizeSpan(0.86f),
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
}
}.toTypedArray()
myVehiclePreference.isEnabled = true
updateMyVehiclesSummary()
}
@@ -78,9 +90,9 @@ class ChargepriceSettingsFragment : BaseSettingsFragment() {
private fun updateMyVehiclesSummary() {
vm.vehicles.value?.data?.let { cars ->
val vehicles = cars.filter { it.id in prefs.chargepriceMyVehicles }
val summary = vehicles.map {
val summary = vehicles.joinToString(", ") {
"${it.brand} ${it.name}"
}.joinToString(", ")
}
myVehiclePreference.summary = summary
}
}

View File

@@ -1,14 +1,28 @@
package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import net.vonforst.evmap.R
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.availability.TeslaAuthenticationApi
import net.vonforst.evmap.api.availability.TeslaOwnerApi
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs
import net.vonforst.evmap.viewmodel.SettingsViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
import okhttp3.OkHttpClient
import okio.IOException
import java.time.Instant
class DataSettingsFragment : BaseSettingsFragment() {
override val isTopLevel = false
@@ -23,8 +37,38 @@ class DataSettingsFragment : BaseSettingsFragment() {
}
})
private lateinit var teslaAccountPreference: Preference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_data, rootKey)
teslaAccountPreference = findPreference<Preference>("tesla_account")!!
refreshTeslaAccountStatus()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.let {
val args = DataSettingsFragmentArgs.fromBundle(it)
if (args.startTeslaLogin) {
teslaLogin()
arguments = null
}
}
}
override fun onResume() {
super.onResume()
refreshTeslaAccountStatus()
}
private fun refreshTeslaAccountStatus() {
teslaAccountPreference.summary =
if (encryptedPrefs.teslaRefreshToken != null) {
getString(R.string.pref_tesla_account_enabled, encryptedPrefs.teslaEmail)
} else {
getString(R.string.pref_tesla_account_disabled)
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
@@ -60,7 +104,86 @@ class DataSettingsFragment : BaseSettingsFragment() {
vm.deleteRecentSearchResults()
true
}
"tesla_account" -> {
if (encryptedPrefs.teslaRefreshToken != null) {
teslaLogout()
} else {
teslaLogin()
}
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
private fun teslaLogin() {
val codeVerifier = TeslaAuthenticationApi.generateCodeVerifier()
val codeChallenge = TeslaAuthenticationApi.generateCodeChallenge(codeVerifier)
val uri = Uri.parse("https://auth.tesla.com/oauth2/v3/authorize").buildUpon()
.appendQueryParameter("client_id", "ownerapi")
.appendQueryParameter("code_challenge", codeChallenge)
.appendQueryParameter("code_challenge_method", "S256")
.appendQueryParameter("redirect_uri", "https://auth.tesla.com/void/callback")
.appendQueryParameter("response_type", "code")
.appendQueryParameter("scope", "openid email offline_access")
.appendQueryParameter("state", "123").build()
val args = OAuthLoginFragmentArgs(
uri.toString(),
"https://auth.tesla.com/void/callback",
"#000000"
).toBundle()
setFragmentResultListener(uri.toString()) { _, result ->
teslaGetAccessToken(result, codeVerifier)
}
findNavController().navigate(R.id.oauth_login, args)
}
private fun teslaGetAccessToken(result: Bundle, codeVerifier: String) {
teslaAccountPreference.summary = getString(R.string.logging_in)
val url = Uri.parse(result.getString("url"))
val code = url.getQueryParameter("code")!!
val okhttp = OkHttpClient.Builder().addDebugInterceptors().build()
val request = TeslaAuthenticationApi.AuthCodeRequest(code, codeVerifier)
lifecycleScope.launch {
try {
val time = Instant.now().epochSecond
val response =
TeslaAuthenticationApi.create(okhttp).getToken(request)
val userResponse =
TeslaOwnerApi.create(okhttp, response.accessToken).getUserInfo()
encryptedPrefs.teslaEmail = userResponse.response.email
encryptedPrefs.teslaAccessToken = response.accessToken
encryptedPrefs.teslaAccessTokenExpiry = time + response.expiresIn
encryptedPrefs.teslaRefreshToken = response.refreshToken
} catch (e: IOException) {
view?.let {
Snackbar.make(it, R.string.connection_error, Snackbar.LENGTH_SHORT).show()
}
}
refreshTeslaAccountStatus()
}
}
private fun teslaLogout() {
MaterialAlertDialogBuilder(requireContext())
.setMessage(getString(R.string.pref_tesla_account_enabled, encryptedPrefs.teslaEmail))
.setPositiveButton(R.string.ok) { _, _ -> }
.setNegativeButton(R.string.log_out) { _, _ ->
// sign out
encryptedPrefs.teslaRefreshToken = null
encryptedPrefs.teslaAccessToken = null
encryptedPrefs.teslaAccessTokenExpiry = -1
encryptedPrefs.teslaEmail = null
view?.let { Snackbar.make(it, R.string.logged_out, Snackbar.LENGTH_SHORT).show() }
refreshTeslaAccountStatus()
}
.show()
}
}

View File

@@ -0,0 +1,102 @@
package net.vonforst.evmap.fragment.preference
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationManager
import android.os.Build
import android.os.Bundle
import android.text.format.DateUtils
import android.view.View
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import net.vonforst.evmap.R
class DeveloperSettingsFragment : BaseSettingsFragment() {
override val isTopLevel = false
private val locationManager: LocationManager by lazy {
requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_developer, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val locationPref = findPreference<Preference>("location_status")!!
val coarseGranted = ContextCompat.checkSelfPermission(
requireContext(),
android.Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
val fineGranted = ContextCompat.checkSelfPermission(
requireContext(),
android.Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
locationPref.summary = buildString {
append("Coarse location permission: ")
appendLine(if (coarseGranted) "granted" else "not granted")
append("Fine location permission: ")
appendLine(if (fineGranted) "granted" else "not granted")
appendLine()
if (coarseGranted) {
append("Last network location: ")
appendLine(printLocation(locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)))
}
if (fineGranted) {
append("Last GPS location: ")
appendLine(printLocation(locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && locationManager.allProviders.contains(
LocationManager.FUSED_PROVIDER
)
) {
append("Last fused location: ")
append(printLocation(locationManager.getLastKnownLocation(LocationManager.FUSED_PROVIDER)))
} else {
append("System's fused location provider not available")
}
}
}
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
"disable_developer_mode" -> {
prefs.developerModeEnabled = false
Toast.makeText(
requireContext(),
getString(R.string.developer_mode_disabled),
Toast.LENGTH_SHORT
).show()
findNavController().popBackStack()
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
fun printLocation(location: Location?): String {
if (location == null) return "not available"
return buildString {
append("%.4f".format(location.latitude))
append(",")
append("%.4f".format(location.longitude))
append(" (")
append(DateUtils.getRelativeTimeSpanString(location.time))
append(")")
}
}
}

View File

@@ -2,20 +2,22 @@ package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.preference.Preference
import net.vonforst.evmap.R
class SettingsFragment : BaseSettingsFragment() {
override val isTopLevel = true
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.settings)
addPreferencesFromResource(R.xml.settings_variantspecific)
findPreference<Preference>("developer_options")?.isVisible = prefs.developerModeEnabled
}
override fun onResume() {
super.onResume()
findPreference<Preference>("developer_options")?.isVisible = prefs.developerModeEnabled
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {

View File

@@ -31,6 +31,7 @@ class FusionEngine(context: Context) : LocationEngine(context),
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private var gpsLocation: Location? = null
private var networkLocation: Location? = null
private var fusedLocation: Location? = null
private val supportsSystemFusedProvider: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && locationManager.allProviders.contains(
@@ -101,7 +102,6 @@ class FusionEngine(context: Context) : LocationEngine(context),
try {
enableFused(gpsInterval)
checkLastKnownFused()
return
} catch (e: SecurityException) {
Log.e(TAG, "Permissions not granted for fused provider", e)
}
@@ -143,6 +143,9 @@ class FusionEngine(context: Context) : LocationEngine(context),
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
override fun disable() {
locationManager.removeUpdates(this)
gpsLocation = null
networkLocation = null
fusedLocation = null
}
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION])
@@ -235,15 +238,16 @@ class FusionEngine(context: Context) : LocationEngine(context),
override fun onLocationChanged(location: Location) {
if (LocationManager.FUSED_PROVIDER == location.provider) {
fusedLocation = location
requests.forEach { it.listener.onLocationChanged(location) }
} else if (LocationManager.GPS_PROVIDER == location.provider) {
gpsLocation = location
if (gpsLocation.isBetterThan(networkLocation)) {
if (gpsLocation.isBetterThan(networkLocation) && fusedLocation == null) {
requests.forEach { it.listener.onLocationChanged(location) }
}
} else if (LocationManager.NETWORK_PROVIDER == location.provider) {
networkLocation = location
if (networkLocation.isBetterThan(gpsLocation)) {
if (networkLocation.isBetterThan(gpsLocation) && fusedLocation == null) {
requests.forEach { it.listener.onLocationChanged(location) }
}
}

View File

@@ -62,8 +62,8 @@ data class ChargeLocation(
@Embedded val address: Address?,
val chargepoints: List<Chargepoint>,
val network: String?,
val url: String,
val editUrl: String?,
val url: String, // URL of this charger at the data source
val editUrl: String?, // URL to edit this charger at the data source
@Embedded(prefix = "fault_report_") val faultReport: FaultReport?,
val verified: Boolean,
val barrierFree: Boolean?,
@@ -78,6 +78,8 @@ data class ChargeLocation(
@Embedded val cost: Cost?,
val license: String?,
@Embedded(prefix = "chargeprice") val chargepriceData: ChargepriceData?,
val networkUrl: String?, // Website of the network
val chargerUrl: String?, // Website for this specific charging site. Might be an ad-hoc payment page.
val timeRetrieved: Instant,
val isDetailed: Boolean
) : ChargepointListItem(), Equatable, Parcelable {
@@ -136,9 +138,9 @@ data class ChargeLocation(
get() = chargepoints.sumOf { it.count }
fun formatChargepoints(sp: StringProvider): String {
return chargepointsMerged.map {
return chargepointsMerged.joinToString(" · ") {
"${it.count} × ${nameForPlugType(sp, it.type)} ${it.formatPower()}"
}.joinToString(" · ")
}
}
}
@@ -332,7 +334,19 @@ data class Hours(
}
abstract class ChargerPhoto(open val id: String) : Parcelable {
abstract fun getUrl(height: Int? = null, width: Int? = null, size: Int? = null): String
/**
* Gets a URL of the image corresponding to a given size.
*
* If the data source supports accessing the image in its original (potentially unlimited) size,
* this size will only be returned if allowOriginal is set to true. Otherwise, only scaled
* versions of the images will be returned.
*/
abstract fun getUrl(
height: Int? = null,
width: Int? = null,
size: Int? = null,
allowOriginal: Boolean = false
): String
}
data class ChargeLocationCluster(

View File

@@ -23,6 +23,6 @@ data class Favorite(
)
data class FavoriteWithDetail(
@Embedded() val favorite: Favorite,
@Embedded val favorite: Favorite,
@Embedded val charger: ChargeLocation
)

View File

@@ -32,7 +32,7 @@ import net.vonforst.evmap.model.*
OCMConnectionType::class,
OCMCountry::class,
OCMOperator::class
], version = 18
], version = 19
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
@@ -56,7 +56,7 @@ abstract class AppDatabase : RoomDatabase() {
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11,
MIGRATION_12, MIGRATION_13, MIGRATION_14, MIGRATION_15, MIGRATION_16,
MIGRATION_17, MIGRATION_18
MIGRATION_17, MIGRATION_18, MIGRATION_19
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
@@ -78,7 +78,7 @@ abstract class AppDatabase : RoomDatabase() {
// SQL for creating tables copied from build/generated/source/kapt/debug/net/vonforst/evmap/storage/AppDatbase_Impl
db.execSQL("CREATE TABLE IF NOT EXISTS `BooleanFilterValue` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))")
db.execSQL("CREATE TABLE IF NOT EXISTS `MultipleChoiceFilterValue` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, PRIMARY KEY(`key`))")
db.execSQL("CREATE TABLE IF NOT EXISTS `SliderFilterValue` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))");
db.execSQL("CREATE TABLE IF NOT EXISTS `SliderFilterValue` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))")
}
}
@@ -87,8 +87,8 @@ abstract class AppDatabase : RoomDatabase() {
// recreate ChargeLocation table to make postcode nullable
db.beginTransaction()
try {
db.execSQL("CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `verified` INTEGER NOT NULL, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT NOT NULL, `country` TEXT NOT NULL, `postcode` TEXT, `street` TEXT NOT NULL, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, PRIMARY KEY(`id`))");
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT * FROM `ChargeLocation`");
db.execSQL("CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `verified` INTEGER NOT NULL, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT NOT NULL, `country` TEXT NOT NULL, `postcode` TEXT, `street` TEXT NOT NULL, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, PRIMARY KEY(`id`))")
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT * FROM `ChargeLocation`")
db.execSQL("DROP TABLE `ChargeLocation`")
db.execSQL("ALTER TABLE `ChargeLocationNew` RENAME TO `ChargeLocation`")
db.setTransactionSuccessful()
@@ -109,8 +109,8 @@ abstract class AppDatabase : RoomDatabase() {
// recreate ChargeLocation table to make other address fields nullable
db.beginTransaction()
try {
db.execSQL("CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `verified` INTEGER NOT NULL, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, PRIMARY KEY(`id`))");
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT * FROM `ChargeLocation`");
db.execSQL("CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `verified` INTEGER NOT NULL, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, PRIMARY KEY(`id`))")
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT * FROM `ChargeLocation`")
db.execSQL("DROP TABLE `ChargeLocation`")
db.execSQL("ALTER TABLE `ChargeLocationNew` RENAME TO `ChargeLocation`")
db.setTransactionSuccessful()
@@ -160,7 +160,7 @@ abstract class AppDatabase : RoomDatabase() {
// add profile column to existing filtervalue tables
db.execSQL("CREATE TABLE `BooleanFilterValueNew` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`), FOREIGN KEY(`profile`) REFERENCES `FilterProfile`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE `MultipleChoiceFilterValueNew` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`), FOREIGN KEY(`profile`) REFERENCES `FilterProfile`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `SliderFilterValueNew` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`), FOREIGN KEY(`profile`) REFERENCES `FilterProfile`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )");
db.execSQL("CREATE TABLE IF NOT EXISTS `SliderFilterValueNew` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`), FOREIGN KEY(`profile`) REFERENCES `FilterProfile`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
for (table in listOf(
"BooleanFilterValue",
@@ -202,7 +202,7 @@ abstract class AppDatabase : RoomDatabase() {
//////////////////////////////////////////
db.execSQL("CREATE TABLE `OCMConnectionType` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `formalName` TEXT, `discontinued` INTEGER, `obsolete` INTEGER, PRIMARY KEY(`id`))")
db.execSQL("CREATE TABLE `OCMCountry` (`id` INTEGER NOT NULL, `isoCode` TEXT NOT NULL, `continentCode` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`id`))")
db.execSQL("CREATE TABLE `OCMOperator` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))");
db.execSQL("CREATE TABLE `OCMOperator` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))")
//////////////////////////////////////////
// rename GoingElectric-specific tables //
@@ -295,7 +295,7 @@ abstract class AppDatabase : RoomDatabase() {
// update ChargeLocation table to change primary key
db.execSQL(
"CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `license` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, PRIMARY KEY(`id`, `dataSource`))"
);
)
val columnList =
"`id`,`dataSource`,`name`,`chargepoints`,`network`,`url`,`editUrl`,`verified`,`barrierFree`,`operator`,`generalInformation`,`amenities`,`locationDescription`,`photos`,`chargecards`,`license`,`lat`,`lng`,`city`,`country`,`postcode`,`street`,`fault_report_created`,`fault_report_description`,`twentyfourSeven`,`description`,`mostart`,`moend`,`tustart`,`tuend`,`westart`,`weend`,`thstart`,`thend`,`frstart`,`frend`,`sastart`,`saend`,`sustart`,`suend`,`hostart`,`hoend`,`freecharging`,`freeparking`,`descriptionShort`,`descriptionLong`,`chargepricecountry`,`chargepricenetwork`,`chargepriceplugTypes`"
db.execSQL("INSERT INTO `ChargeLocationNew`($columnList) SELECT $columnList FROM `ChargeLocation`")
@@ -311,7 +311,7 @@ abstract class AppDatabase : RoomDatabase() {
private val MIGRATION_14 = object : Migration(13, 14) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS `RecentAutocompletePlace` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))");
db.execSQL("CREATE TABLE IF NOT EXISTS `RecentAutocompletePlace` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))")
}
}
@@ -321,7 +321,7 @@ abstract class AppDatabase : RoomDatabase() {
override fun migrate(db: SupportSQLiteDatabase) {
try {
db.beginTransaction()
db.execSQL("CREATE TABLE IF NOT EXISTS `Favorite` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE RESTRICT )");
db.execSQL("CREATE TABLE IF NOT EXISTS `Favorite` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE RESTRICT )")
val cursor = db.query("SELECT * FROM `ChargeLocation`")
while (cursor.moveToNext()) {
@@ -361,7 +361,7 @@ abstract class AppDatabase : RoomDatabase() {
override fun migrate(db: SupportSQLiteDatabase) {
try {
db.beginTransaction()
db.execSQL("CREATE TABLE IF NOT EXISTS `FavoriteNew` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE NO ACTION )");
db.execSQL("CREATE TABLE IF NOT EXISTS `FavoriteNew` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE NO ACTION )")
val columnList =
"`favoriteId`,`chargerId`,`chargerDataSource`"
db.execSQL("INSERT INTO `FavoriteNew`($columnList) SELECT $columnList FROM `Favorite`")
@@ -376,5 +376,12 @@ abstract class AppDatabase : RoomDatabase() {
}
}
private val MIGRATION_19 = object : Migration(18, 19) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `ChargeLocation` ADD `networkUrl` TEXT")
db.execSQL("ALTER TABLE `ChargeLocation` ADD `chargerUrl` TEXT")
}
}
}
}

View File

@@ -0,0 +1,45 @@
package net.vonforst.evmap.storage
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import net.vonforst.evmap.api.availability.TeslaAvailabilityDetector
/**
* Encrypted data storage for sensitive data such as API access tokens.
* This will not be included in backups.
*/
class EncryptedPreferenceDataStore(context: Context) : TeslaAvailabilityDetector.TokenStore {
val sp = EncryptedSharedPreferences.create(
context,
"encrypted_prefs",
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
override var teslaRefreshToken: String?
get() = sp.getString(
"tesla_refresh_token", null
)
set(value) {
sp.edit().putString("tesla_refresh_token", value).apply()
}
override var teslaAccessToken: String?
get() = sp.getString("tesla_access_token", null)
set(value) {
sp.edit().putString("tesla_access_token", value).apply()
}
override var teslaAccessTokenExpiry: Long
get() = sp.getLong("tesla_access_token_expiry", -1)
set(value) {
sp.edit().putLong("tesla_access_token_expiry", value).apply()
}
var teslaEmail: String?
get() = sp.getString("tesla_email", null)
set(value) {
sp.edit().putString("tesla_email", value).apply()
}
}

View File

@@ -68,7 +68,7 @@ class Converters {
@TypeConverter
fun toChargerPhotoList(value: String): List<ChargerPhoto>? {
return chargerPhotoListAdapter.fromJson(value)?.filterNotNull()
return chargerPhotoListAdapter.fromJson(value)
}
@TypeConverter

View File

@@ -14,6 +14,7 @@ import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import net.vonforst.evmap.R
import java.time.Duration
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
@@ -28,8 +29,8 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
private val dp = context.resources.displayMetrics.density
private val sp = context.resources.displayMetrics.scaledDensity
var zeroHeight = 4 * dp
var barWidth = (16 * dp).roundToInt()
var barMargin = (2 * dp).roundToInt()
var barWidth = 16 * dp
var barMargin = 2 * dp
var legendWidth = 12 * dp
var legendLineLength = 4 * dp
var legendLineWidth = 1 * dp
@@ -42,24 +43,27 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
var barDrawable =
AppCompatResources.getDrawable(context, R.drawable.bar_graph)!!
var colorAvailable = ContextCompat.getColor(context, R.color.available)
var colorSomeAvailable = ContextCompat.getColor(context, R.color.some_available)
var colorUnavailable = ContextCompat.getColor(context, R.color.unavailable)
var data: Map<ZonedDateTime, Int>? = null
var data: Map<ZonedDateTime, Double>? = null
set(value) {
field = value
invalidate()
}
var maxValue: Int? = null
var maxValue: Double? = null
set(value) {
field = value
invalidate()
}
var isPercentage: Boolean = false
var activeAlpha = 0.87f
var inactiveAlpha = 0.60f
private val legendPaint = Paint().apply {
val ta = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorControlNormal))
val ta =
context.theme.obtainStyledAttributes(intArrayOf(androidx.appcompat.R.attr.colorControlNormal))
color = ta.getColor(0, 0)
strokeWidth = legendLineWidth
textSize = legendWidth - legendLineLength
@@ -110,22 +114,28 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
plusMinutes((minutesRound - minute).toLong())
}
data = (0..20).associate {
now.plusMinutes(15L * it) to (Math.random() * 8).roundToInt()
now.plusMinutes(15L * it) to (Math.random() * 8)
}
maxValue = 8
maxValue = 8.0
}
val data = data?.toSortedMap() ?: return
if (data.isEmpty()) return
val maxValue = maxValue ?: data.maxOf { it.value }
val graphWidth = graphBounds?.width() ?: return
val n = data.size
val barMarginFactor = 0.1f
barWidth = graphWidth / (n + barMarginFactor * (n - 1))
barMargin = barWidth * barMarginFactor
drawGraph(canvas, data, maxValue)
drawBubble(canvas, data, maxValue)
}
private fun drawGraph(
canvas: Canvas,
data: SortedMap<ZonedDateTime, Int>,
maxValue: Int
data: SortedMap<ZonedDateTime, Double>,
maxValue: Double
) {
val graphBounds = graphBounds ?: return
@@ -139,26 +149,30 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
)
legendPaint.textAlign = Paint.Align.CENTER
data.entries.forEachIndexed { i, (t, v) ->
val height =
zeroHeight + (graphBounds.height() - zeroHeight) * v.toFloat() / maxValue
zeroHeight + (graphBounds.height() - zeroHeight) * v.toFloat() / maxValue.toFloat()
val left = graphBounds.left + (barWidth + barMargin) * i
if (left + barWidth > graphBounds.right) return@forEachIndexed
barDrawable.setBounds(
left,
0,
graphBounds.bottom - height.roundToInt(),
left + barWidth,
barWidth.roundToInt(),
graphBounds.bottom
)
canvas.translate(left, 0f)
barDrawable.alpha =
((if (i == selectedBar) activeAlpha else inactiveAlpha) * 255).roundToInt()
barDrawable.setTint(getColor(v, maxValue))
barDrawable.draw(canvas)
canvas.translate(-left, 0f)
val center = left.toFloat() + barWidth / 2
if (t.minute == 0) {
val center = left + barWidth / 2
if (shouldDrawLabel(t, data)) {
drawLine(
center, graphBounds.bottom.toFloat(),
center, graphBounds.bottom + legendLineLength, legendPaint
@@ -196,19 +210,44 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
)
legendPaint.textAlign = Paint.Align.LEFT
drawText(
this@BarGraphView.maxValue.toString(),
graphBounds.right.toFloat() + legendLineLength,
graphBounds.top + (legendWidth - legendLineLength) / 3,
legendPaint
)
if (!isPercentage) {
drawText(
maxValue.roundToInt().toString(),
graphBounds.right.toFloat() + legendLineLength,
graphBounds.top + (legendWidth - legendLineLength) / 3,
legendPaint
)
}
}
}
private fun getColor(v: Int, maxValue: Int) =
if (v < maxValue) colorAvailable else colorUnavailable
private fun shouldDrawLabel(t: ZonedDateTime, data: SortedMap<ZonedDateTime, Double>): Boolean {
val ts = data.keys.toList()
return if (Duration.between(ts[0], ts[1]) > Duration.ofMinutes(31)) {
// label every 6 hours
t.hour % 6 == 0
} else {
// label every 15 minutes
t.minute == 0
}
}
private fun drawBubble(canvas: Canvas, data: SortedMap<ZonedDateTime, Int>, maxValue: Int) {
private fun getColor(v: Double, maxValue: Double) =
if (isPercentage) {
when (v) {
in 0.0..0.5 -> colorAvailable
in 0.5..0.8 -> colorSomeAvailable
else -> colorUnavailable
}
} else {
if (v < maxValue) colorAvailable else colorUnavailable
}
private fun drawBubble(
canvas: Canvas,
data: SortedMap<ZonedDateTime, Double>,
maxValue: Double
) {
val bubbleBounds = bubbleBounds ?: return
val graphBounds = graphBounds ?: return
val d = data.toList()
@@ -221,12 +260,16 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
R.string.prediction_time_colon,
t.withZoneSameInstant(ZoneId.systemDefault()).format(timeFormat)
)
val availableformat = context.resources.getQuantityString(
R.plurals.prediction_number_available,
maxValue - v,
maxValue - v,
maxValue
)
val availableformat = if (isPercentage) {
"%.0f %%".format(v * 100)
} else {
context.resources.getQuantityString(
R.plurals.prediction_number_available,
(maxValue - v).roundToInt(),
(maxValue - v).roundToInt(),
maxValue.roundToInt()
)
}
val text = SpannableString("$tformat $availableformat").apply {
setSpan(
ForegroundColorSpan(getColor(v, maxValue)),
@@ -297,7 +340,7 @@ class BarGraphView(context: Context, attrs: AttributeSet) : View(context, attrs)
private fun updateSelectedBar(x: Int) {
val graphBounds = graphBounds ?: return
val bar = (x - graphBounds.left) / (barWidth + barMargin)
val bar = ((x - graphBounds.left) / (barWidth + barMargin)).roundToInt()
if (bar != selectedBar) {
selectedBar = bar
invalidate()

View File

@@ -110,9 +110,9 @@ private fun activeTint(
val color = context.theme.obtainStyledAttributes(
intArrayOf(
if (isColored) {
R.attr.colorPrimary
androidx.appcompat.R.attr.colorPrimary
} else {
R.attr.colorControlNormal
androidx.appcompat.R.attr.colorControlNormal
}
)
)
@@ -169,9 +169,10 @@ fun setBackgroundTintAvailability(view: View, available: List<ChargepointStatus>
@BindingAdapter("selectableItemBackground")
fun applySelectableItemBackground(view: View, apply: Boolean) {
if (apply) {
view.context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
view.background = it.getDrawable(0)
}
view.context.obtainStyledAttributes(intArrayOf(androidx.appcompat.R.attr.selectableItemBackground))
.use {
view.background = it.getDrawable(0)
}
} else {
view.background = null
}
@@ -263,7 +264,8 @@ private fun availabilityColor(
ContextCompat.getColor(context, R.color.charging)
}
} else {
val ta = context.theme.obtainStyledAttributes(intArrayOf(R.attr.colorControlNormal))
val ta =
context.theme.obtainStyledAttributes(intArrayOf(androidx.appcompat.R.attr.colorControlNormal))
ta.getColor(0, 0)
}
@@ -295,16 +297,16 @@ fun currency(currency: String): String {
"GBP" -> "£"
"HRK" -> "kn"
"HUF" -> "Ft"
"ISK" -> "Kr"
"ISK" -> "kr"
else -> currency
}
}
fun time(value: Int): String {
val h = floor(value.toDouble() / 60).toInt();
val min = ceil(value.toDouble() % 60).toInt();
return if (h == 0 && min > 0) "$min min";
else "%d:%02d h".format(h, min);
val h = floor(value.toDouble() / 60).toInt()
val min = ceil(value.toDouble() % 60).toInt()
return if (h == 0 && min > 0) "$min min"
else "%d:%02d h".format(h, min)
}
fun distance(meters: Number?): String? {
@@ -371,9 +373,10 @@ fun tariffBackground(context: Context, myTariff: Boolean, brandingColor: String?
return drawable
}
else -> {
context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
return it.getDrawable(0)
}
context.obtainStyledAttributes(intArrayOf(androidx.appcompat.R.attr.selectableItemBackground))
.use {
return it.getDrawable(0)
}
}
}
}
@@ -420,4 +423,9 @@ fun setImageTint(view: ImageView, @ColorInt tint: Int?) {
} else {
view.imageTintList = null
}
}
@BindingAdapter("isPercentage")
fun setIsPercentage(view: BarGraphView, value: Boolean) {
view.isPercentage = value
}

View File

@@ -15,9 +15,9 @@ class CheckableConstraintLayout(ctx: Context, attrs: AttributeSet) : ConstraintL
override fun setChecked(b: Boolean) {
if (b != checked) {
checked = b;
refreshDrawableState();
onCheckedChangeListener?.invoke(this, checked);
checked = b
refreshDrawableState()
onCheckedChangeListener?.invoke(this, checked)
}
}

View File

@@ -1,4 +1,4 @@
package net.vonforst.evmap.ui;
package net.vonforst.evmap.ui
import com.car2go.maps.model.LatLng
import com.google.maps.android.clustering.ClusterItem

View File

@@ -101,12 +101,12 @@ class HideOnExpandFabBehavior(context: Context, attrs: AttributeSet) :
type,
consumed
)
if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
if (dyConsumed > 0 && child.visibility == View.VISIBLE) {
// User scrolled down and the FAB is currently visible -> hide the FAB
child.hide();
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
child.hide()
} else if (dyConsumed < 0 && child.visibility != View.VISIBLE) {
// User scrolled up and the FAB is currently not visible -> show the FAB
child.show();
child.show()
}
}
}

View File

@@ -98,12 +98,12 @@ class HideOnScrollFabBehavior(context: Context, attrs: AttributeSet) :
type,
consumed
)
if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
if (dyConsumed > 0 && child.visibility == View.VISIBLE) {
// User scrolled down and the FAB is currently visible -> hide the FAB
child.hide();
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
child.hide()
} else if (dyConsumed < 0 && child.visibility != View.VISIBLE) {
// User scrolled up and the FAB is currently not visible -> show the FAB
child.show();
child.show()
}
}
}

View File

@@ -35,7 +35,10 @@ class ClusterIconGenerator(context: Context) : IconGenerator(context) {
)
id = R.id.amu_text
setPadding(twelveDpi, twelveDpi, twelveDpi, twelveDpi)
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_AppCompat)
TextViewCompat.setTextAppearance(
this,
androidx.appcompat.R.style.TextAppearance_AppCompat
)
setTextColor(ContextCompat.getColor(context, android.R.color.white))
}
}
@@ -64,7 +67,7 @@ class ChargerIconGenerator(
// 340 items:
// large: (21 sizes, 5 colors, multi on/off) + highlight + fault + fav (only with scale = 1)
// mini: (11 sizes, 5 colors) + highlight (only with scale = 1)
private val cacheSize = (scaleResolution + 8) * 5 * 2 + (scaleResolutionMini + 2) * 5;
private val cacheSize = (scaleResolution + 8) * 5 * 2 + (scaleResolutionMini + 2) * 5
private val cache = LruCache<BitmapData, BitmapDescriptor>(cacheSize)
private val icon = R.drawable.ic_map_marker_charging
private val multiIcon = R.drawable.ic_map_marker_charging_multiple

View File

@@ -38,7 +38,7 @@ class MultiSelectDialogPreference(ctx: Context, attrs: AttributeSet) :
val dialog =
MultiSelectDialog.getInstance(
title.toString(),
entryValues.map { it.toString() }.zip(entries.map { it.toString() }).toMap(),
entryValues.map { it.toString() }.zip(entries).toMap(),
if (all) entryValues.map { it.toString() }.toSet() else values,
emptySet(),
showAllButton

View File

@@ -116,20 +116,18 @@ class ChargepriceViewModel(
MediatorLiveData<Resource<List<ChargePrice>>>().apply {
value = state["chargePrices"] ?: Resource.loading(null)
listOf(
charger,
batteryRange,
batteryRangeSliderDragging,
vehicleCompatibleConnectors,
myTariffs, myTariffsAll
myTariffs, myTariffsAll, charger
).forEach {
addSource(it.distinctUntilChanged()) {
if (!batteryRangeSliderDragging.value!!) loadPrices()
if (!batteryRangeSliderDragging.value!!) {
loadPrices()
state["chargePrices"] = this.value
}
}
}
observeForever {
// persist data in case fragment gets recreated
state["chargePrices"] = it
}
}
}
@@ -153,7 +151,7 @@ class ChargepriceViewModel(
value = Resource.loading(null)
} else {
val myTariffs = prefs.chargepriceMyTariffs
value = Resource.success(cps.data!!.map { cp ->
value = Resource.success(cps.data!!.mapNotNull { cp ->
val filteredPrices =
cp.chargepointPrices.filter {
it.plug == getChargepricePlugType(chargepoint) && it.power == chargepoint.power
@@ -165,7 +163,7 @@ class ChargepriceViewModel(
chargepointPrices = filteredPrices
)
}
}.filterNotNull()
}
.sortedBy { it.chargepointPrices.first().price ?: Double.MAX_VALUE }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||

View File

@@ -7,9 +7,9 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.api.availability.AvailabilityRepository
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.model.FavoriteWithDetail
@@ -19,6 +19,7 @@ import net.vonforst.evmap.utils.distanceBetween
class FavoritesViewModel(application: Application) :
AndroidViewModel(application) {
private var db = AppDatabase.getInstance(application)
private val availabilityRepo = AvailabilityRepository(application)
val favorites: LiveData<List<FavoriteWithDetail>> by lazy {
db.favoritesDao().getAllFavorites()
@@ -53,7 +54,7 @@ class FavoritesViewModel(application: Application) :
chargers.map { charger ->
async {
data[charger.id] = getAvailability(charger)
data[charger.id] = availabilityRepo.getAvailability(charger)
availability.value = data
}
}.awaitAll()

View File

@@ -14,8 +14,9 @@ import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.AvailabilityDetectorException
import net.vonforst.evmap.api.availability.AvailabilityRepository
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.availability.TeslaGraphQlApi
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.api.fronyx.FronyxApi
@@ -30,12 +31,16 @@ import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.ChargeLocationsRepository
import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.cluster
import net.vonforst.evmap.utils.distanceBetween
import retrofit2.HttpException
import java.io.IOException
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
import java.time.ZonedDateTime
@Parcelize
@@ -53,12 +58,14 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
AndroidViewModel(application) {
private val db = AppDatabase.getInstance(application)
private val prefs = PreferenceDataSource(application)
private val encryptedPrefs = EncryptedPreferenceDataStore(application)
private val repo = ChargeLocationsRepository(
createApi(prefs.dataSource, application),
viewModelScope,
db,
prefs
)
private val availabilityRepo = AvailabilityRepository(application)
val apiId = repo.api.map { it.id }
@@ -202,7 +209,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
triggerAvailabilityRefresh.switchMap {
liveData {
emit(Resource.loading(null))
emit(getAvailability(charger))
emit(availabilityRepo.getAvailability(charger))
}
}
}
@@ -230,6 +237,10 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}
val teslaPricing = availability.map {
it.data?.extraData as? TeslaGraphQlApi.Pricing
}
val predictionApi = FronyxApi(application.getString(R.string.fronyx_key))
val prediction: LiveData<Resource<List<FronyxEvseIdResponse>>> by lazy {
@@ -250,6 +261,10 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
).any { filtered.contains(it) }
} ?: true
}.flatMap { it.value }
if (allEvseIds.isEmpty()) {
emit(Resource.success(emptyList()))
return@liveData
}
try {
val result = predictionApi.getPredictionsForEvseIds(allEvseIds)
if (result.size == allEvseIds.size) {
@@ -276,33 +291,45 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}
val predictionGraph: LiveData<Map<ZonedDateTime, Int>?> by lazy {
prediction.map {
it.data?.let { responses ->
if (responses.isEmpty()) {
null
} else {
val evseIds = responses.map { it.evseId }
val groupByTimestamp = responses.flatMap { response ->
response.predictions.map {
Triple(
it.timestamp,
response.evseId,
it.status
)
val predictionGraph: LiveData<Map<ZonedDateTime, Double>?> =
MediatorLiveData<Map<ZonedDateTime, Double>?>().apply {
listOf(prediction, availability).forEach {
addSource(it) {
val congestionHistogram = availability.value?.data?.congestionHistogram
val prediction = prediction.value?.data
value = if (congestionHistogram != null && prediction == null) {
congestionHistogram.mapIndexed { i, value ->
LocalTime.of(i, 0).atDate(LocalDate.now())
.atZone(ZoneId.systemDefault()) to value
}.toMap()
} else {
prediction?.let { responses ->
if (responses.isEmpty()) {
null
} else {
val evseIds = responses.map { it.evseId }
val groupByTimestamp = responses.flatMap { response ->
response.predictions.map {
Triple(
it.timestamp,
response.evseId,
it.status
)
}
}
.groupBy { it.first } // group by timestamp
.mapValues { it.value.map { it.second to it.third } } // only keep EVSEID and status
.filterValues { it.map { it.first } == evseIds } // remove values where status is not given for all EVSEs
.filterKeys { it > ZonedDateTime.now() } // only show predictions in the future
groupByTimestamp.mapValues {
it.value.count {
it.second == FronyxStatus.UNAVAILABLE
}.toDouble()
}.ifEmpty { null }
}
}
}
.groupBy { it.first } // group by timestamp
.mapValues { it.value.map { it.second to it.third } } // only keep EVSEID and status
.filterValues { it.map { it.first } == evseIds } // remove values where status is not given for all EVSEs
.filterKeys { it > ZonedDateTime.now() } // only show predictions in the future
groupByTimestamp.mapValues {
it.value.count {
it.second == FronyxStatus.UNAVAILABLE
}
}.ifEmpty { null }
}
}
}
}
@@ -322,9 +349,25 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}
val predictionMaxValue: LiveData<Int> by lazy {
predictedChargepoints.map {
it?.sumOf { it.count } ?: 0
val predictionMaxValue: LiveData<Double> = MediatorLiveData<Double>().apply {
listOf(prediction, availability).forEach {
addSource(it) {
value =
if (availability.value?.data?.congestionHistogram != null && prediction.value?.data == null) {
1.0
} else {
(predictedChargepoints.value?.sumOf { it.count } ?: 0).toDouble()
}
}
}
}
val predictionIsPercentage: LiveData<Boolean> = MediatorLiveData<Boolean>().apply {
listOf(prediction, availability).forEach {
addSource(it) {
value =
availability.value?.data?.congestionHistogram != null && prediction.value?.data == null
}
}
}
@@ -385,11 +428,17 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}
private var hasTeslaLogin: MutableLiveData<Boolean> = state.getLiveData("hasTeslaLogin")
fun reloadPrefs() {
filterStatus.value = prefs.filterStatus
if (prefs.dataSource != apiId.value) {
repo.api.value = createApi(prefs.dataSource, getApplication())
}
if (hasTeslaLogin.value != (encryptedPrefs.teslaAccessToken != null)) {
hasTeslaLogin.value = encryptedPrefs.teslaAccessToken != null
reloadAvailability()
}
}
fun toggleFilters() {

View File

@@ -110,7 +110,7 @@ fun <T> throttleLatest(
suspend fun <T> LiveData<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
val observer = object : Observer<T> {
override fun onChanged(value: T?) {
override fun onChanged(value: T) {
if (value == null) return
removeObserver(this)
continuation.resume(value, null)

View File

@@ -4,35 +4,35 @@
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13.45,17.08a8.24,8.24 0,0 1,-3.11 0.6,8.34 8.34,0 0,1 -6,-14.18H16.3a8.35,8.35 0,0 1,1.07 10.33"
android:pathData="M6.2,13.8C4.1,10.6 4.6,6.3 7.3,3.5h12c1.5,1.6 2.4,3.7 2.4,5.9c0,4.6 -3.8,8.3 -8.4,8.3c-1.1,0 -2.1,-0.2 -3.1,-0.6"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000" />
android:strokeColor="#000000" />
<path
android:fillColor="#FF000000"
android:pathData="M10.34,9.34m-1.67,0a1.67,1.67 0,1 1,3.34 0a1.67,1.67 0,1 1,-3.34 0" />
android:pathData="M13.3,9.3m-1.7,0a1.7,1.7 0,1 1,3.4 0a1.7,1.7 0,1 1,-3.4 0" />
<path
android:fillColor="#FF000000"
android:pathData="M15.35,9.34m-1.67,0a1.67,1.67 0,1 1,3.34 0a1.67,1.67 0,1 1,-3.34 0" />
android:pathData="M8.3,9.3m-1.7,0a1.7,1.7 0,1 1,3.4 0a1.7,1.7 0,1 1,-3.4 0" />
<path
android:fillColor="#FF000000"
android:pathData="M12.84,13.51m-1.67,0a1.67,1.67 0,1 1,3.34 0a1.67,1.67 0,1 1,-3.34 0" />
android:pathData="M10.8,13.5m-1.7,0a1.7,1.7 0,1 1,3.4 0a1.7,1.7 0,1 1,-3.4 0" />
<path
android:fillColor="#FF000000"
android:pathData="M7.84,13.51m-1.67,0a1.67,1.67 0,1 1,3.34 0a1.67,1.67 0,1 1,-3.34 0" />
android:pathData="M15.8,13.5m-1.7,0a1.7,1.7 0,1 1,3.4 0a1.7,1.7 0,1 1,-3.4 0" />
<path
android:fillColor="#FF000000"
android:pathData="M5.34,9.34m-1.67,0a1.67,1.67 0,1 1,3.34 0a1.67,1.67 0,1 1,-3.34 0" />
android:pathData="M18.3,9.3m-1.7,0a1.7,1.7 0,1 1,3.4 0a1.7,1.7 0,1 1,-3.4 0" />
<path
android:fillColor="#FF000000"
android:pathData="M7.84,5.59m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
android:pathData="M15.8,5.6m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
<path
android:fillColor="#FF000000"
android:pathData="M12.84,5.59m-1.04,0a1.04,1.04 0,1 1,2.08 0a1.04,1.04 0,1 1,-2.08 0" />
android:pathData="M10.8,5.6m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
<path
android:fillColor="#FF000000"
android:pathData="M18.18,22.23l1,-5.48c0.93,0 1.22,0.1 1.27,0.52a2.15,2.15 0,0 0,0.93 -0.7,6.91 6.91,0 0,0 -2.46,-0.6l-0.71,0.88h0L17.46,16a7,7 0,0 0,-2.46 0.6,2.22 2.22,0 0,0 0.94,0.7c0,-0.42 0.33,-0.52 1.26,-0.52l1,5.48" />
android:pathData="M5.4,22.3l1,-5.5c0.9,0 1.3,0.1 1.3,0.5c0.4,-0.1 0.7,-0.4 0.9,-0.7C7.8,16.3 7,16 6.1,16l-0.8,0.8l0,0L4.7,16c-0.8,0 -1.7,0.3 -2.5,0.6c0.2,0.3 0.6,0.6 0.9,0.7c0.1,-0.4 0.3,-0.5 1.3,-0.5L5.4,22.3" />
<path
android:fillColor="#FF000000"
android:pathData="M18.18,15.72a7.9,7.9 0,0 1,3.28 0.66,2.65 2.65,0 0,0 0.2,-0.4 9.24,9.24 0,0 0,-7 0,2.61 2.61,0 0,0 0.19,0.4 7.94,7.94 0,0 1,3.29 -0.66h0" />
android:pathData="M5.5,15.7L5.5,15.7c1.1,0 2.3,0.2 3.3,0.7c0.1,-0.1 0.1,-0.3 0.2,-0.4c-2.2,-0.9 -4.8,-0.9 -7,0c0.1,0.1 0.1,0.3 0.2,0.4C3.2,15.9 4.3,15.7 5.5,15.7" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z" />
</vector>

View File

@@ -0,0 +1,15 @@
<vector android:height="24dp"
android:viewportHeight="253.5"
android:viewportWidth="254.58"
android:width="24.10225dp"
xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#e82127"
android:pathData="M127.31,253 L162.78,53.48c33.81,0 44.48,3.71 46.02,18.84 0,0 22.68,-8.46 34.13,-25.64C198.28,26 153.42,25.07 153.42,25.07l-26.18,31.88 0.06,-0 -26.18,-31.88c0,0 -44.86,0.93 -89.5,21.62 11.43,17.18 34.12,25.64 34.12,25.64 1.55,-15.14 12.2,-18.84 45.79,-18.87l35.76,199.55"
android:strokeColor="#00000000" />
<path
android:fillColor="#e82127"
android:pathData="m127.29,15.86c36.09,-0.28 77.4,5.58 119.69,24.01 5.65,-10.17 7.11,-14.67 7.11,-14.67C207.86,6.92 164.57,0.66 127.29,0.5 90.01,0.66 46.72,6.92 0.5,25.21c0,0 2.06,5.54 7.1,14.67 42.28,-18.43 83.6,-24.29 119.69,-24.01h0"
android:strokeColor="#00000000" />
</vector>

View File

@@ -45,11 +45,15 @@
<variable
name="predictionGraph"
type="Map&lt;ZonedDateTime, Integer&gt;" />
type="Map&lt;ZonedDateTime, Double&gt;" />
<variable
name="predictionMaxValue"
type="Integer" />
type="Double" />
<variable
name="predictionIsPercentage"
type="Boolean" />
<variable
name="predictionDescription"
@@ -59,6 +63,10 @@
name="filteredAvailability"
type="Resource&lt;ChargeLocationStatus&gt;" />
<variable
name="teslaPricing"
type="net.vonforst.evmap.api.availability.TeslaGraphQlApi.Pricing" />
<variable
name="chargeCards"
type="java.util.Map&lt;Long, ChargeCard&gt;" />
@@ -279,7 +287,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
app:data="@{DetailsAdapterKt.buildDetails(charger.data, chargeCards, filteredChargeCards, context)}"
app:data="@{DetailsAdapterKt.buildDetails(charger.data, chargeCards, filteredChargeCards, teslaPricing, context)}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="parent"
@@ -311,10 +319,11 @@
android:id="@+id/textView13"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:gravity="right|end"
android:text="@{availability.status == Status.SUCCESS ? @string/realtime_data_source(availability.data.source) : availability.status == Status.LOADING ? @string/realtime_data_loading : @string/realtime_data_unavailable}"
android:text="@{availability.status == Status.SUCCESS ? @string/realtime_data_source(availability.data.source) : availability.status == Status.LOADING ? @string/realtime_data_loading : availability.message == &quot;not signed in&quot; ? @string/realtime_data_login_needed : @string/realtime_data_unavailable}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintEnd_toStartOf="@+id/btnLogin"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/connectors"
tools:text="Echtzeitdaten nicht verfügbar" />
@@ -330,20 +339,6 @@
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="@+id/txtName" />
<Button
android:id="@+id/btnChargeprice"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/go_to_chargeprice"
android:transitionName="@string/shared_element_chargeprice"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isChargerSupported(charger.data)}"
app:icon="@drawable/ic_chargeprice"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/divider1" />
<View
android:id="@+id/divider2"
android:layout_width="match_parent"
@@ -359,14 +354,15 @@
android:layout_marginTop="8dp"
android:background="?android:attr/listDivider"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isChargerSupported(charger.data)}"
app:layout_constraintTop_toBottomOf="@+id/btnChargeprice" />
app:layout_constraintTop_toBottomOf="@+id/buttonsScroller" />
<TextView
android:id="@+id/textView8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/utilization_prediction"
android:text="@{predictionIsPercentage ? @string/average_utilization : @string/utilization_prediction}"
tools:text="@string/utilization_prediction"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?colorPrimary"
app:goneUnless="@{predictionGraph != null}"
@@ -381,7 +377,7 @@
android:layout_marginEnd="8dp"
android:text="@{predictionDescription}"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
app:goneUnless="@{predictionGraph != null}"
app:goneUnless="@{predictionGraph != null &amp;&amp; !predictionIsPercentage}"
app:layout_constraintBaseline_toBaselineOf="@+id/textView8"
app:layout_constraintEnd_toStartOf="@+id/btnPredictionHelp"
app:layout_constraintStart_toEndOf="@+id/textView8"
@@ -393,7 +389,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/help"
app:goneUnless="@{predictionGraph != null}"
app:goneUnless="@{predictionGraph != null &amp;&amp; !predictionIsPercentage}"
app:icon="@drawable/ic_help"
app:iconTint="?android:textColorSecondary"
app:layout_constraintBottom_toBottomOf="@+id/textView8"
@@ -411,6 +407,7 @@
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView8"
app:maxValue="@{predictionMaxValue}"
app:isPercentage="@{predictionIsPercentage}"
tools:itemCount="3"
tools:layoutManager="LinearLayoutManager"
tools:listitem="@layout/item_connector"
@@ -424,7 +421,7 @@
android:adjustViewBounds="true"
android:background="?selectableItemBackgroundBorderless"
android:scaleType="fitCenter"
app:goneUnless="@{predictionGraph != null}"
app:goneUnless="@{predictionGraph != null &amp;&amp; !predictionIsPercentage}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toBottomOf="@+id/prediction"
app:srcCompat="@drawable/ic_powered_by_fronyx"
@@ -501,6 +498,57 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/textView7" />
<HorizontalScrollView
android:id="@+id/buttonsScroller"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/divider1"
app:layout_constrainedWidth="true"
android:fillViewport="true"
app:goneUnless="@{charger.data != null &amp;&amp; (ChargepriceApi.isChargerSupported(charger.data) || charger.data.chargerUrl != null)}">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btnChargeprice"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/go_to_chargeprice"
android:transitionName="@string/shared_element_chargeprice"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isChargerSupported(charger.data)}"
app:icon="@drawable/ic_chargeprice" />
<Button
android:id="@+id/btnChargerWebsite"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/charger_website"
app:goneUnless="@{charger.data != null &amp;&amp; charger.data.chargerUrl != null}"
app:icon="@drawable/ic_link" />
</LinearLayout>
</HorizontalScrollView>
<Button
android:id="@+id/btnLogin"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/login"
app:goneUnless="@{availability.status == Status.ERROR &amp;&amp; availability.message == &quot;not signed in&quot;}"
app:layout_constraintBottom_toBottomOf="@+id/textView13"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/textView13" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -199,12 +199,14 @@
app:filteredAvailability="@{vm.filteredAvailability}"
app:predictionGraph="@{vm.predictionGraph}"
app:predictionMaxValue="@{vm.predictionMaxValue}"
app:predictionIsPercentage="@{vm.predictionIsPercentage}"
app:predictionDescription="@{vm.predictionDescription}"
app:chargeCards="@{vm.chargeCardMap}"
app:filteredChargeCards="@{vm.filteredChargeCards}"
app:distance="@{vm.chargerDistance}"
app:expanded="@{vm.bottomSheetExpanded}"
app:apiName="@{vm.apiName}" />
app:apiName="@{vm.apiName}"
app:teslaPricing="@{vm.teslaPricing}" />
</androidx.core.widget.NestedScrollView>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:actionBarSize" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:indeterminate="true"
app:hideAnimationBehavior="inward"
app:indicatorColor="@color/colorSecondary"
app:showAnimationBehavior="outward" />
</com.google.android.material.appbar.AppBarLayout>
<WebView
android:id="@+id/webView"
android:layout_height="match_parent"
android:layout_width="match_parent" />
</LinearLayout>

View File

@@ -78,7 +78,12 @@
android:id="@+id/settings_data"
android:name="net.vonforst.evmap.fragment.preference.DataSettingsFragment"
android:label="@string/settings_data_sources"
tools:layout="@layout/fragment_preference" />
tools:layout="@layout/fragment_preference">
<argument
android:name="startTeslaLogin"
app:argType="boolean"
android:defaultValue="false" />
</fragment>
<fragment
android:id="@+id/settings_chargeprice"
android:name="net.vonforst.evmap.fragment.preference.ChargepriceSettingsFragment"
@@ -89,6 +94,11 @@
android:name="net.vonforst.evmap.fragment.preference.AndroidAutoSettingsFragment"
android:label="@string/settings_android_auto"
tools:layout="@layout/fragment_preference" />
<fragment
android:id="@+id/settings_developer"
android:name="net.vonforst.evmap.fragment.preference.DeveloperSettingsFragment"
android:label="@string/developer_options"
tools:layout="@layout/fragment_preference" />
<navigation
android:id="@+id/favs"
app:startDestination="@id/favs_frag">
@@ -164,4 +174,19 @@
app:popUpTo="@id/onboarding"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/oauth_login"
android:name="net.vonforst.evmap.fragment.oauth.OAuthLoginFragment"
android:label="@string/login">
<argument
android:name="url"
app:argType="string" />
<argument
android:name="resultUrlPrefix"
app:argType="string" />
<argument
android:name="color"
app:argType="string"
app:nullable="true" />
</fragment>
</navigation>

View File

@@ -26,6 +26,7 @@
<string name="amenities">Ausstattung</string>
<string name="general_info">Allgemein</string>
<string name="realtime_data_unavailable">Echtzeitstatus nicht verfügbar</string>
<string name="realtime_data_login_needed">Tesla-Account für Echtzeitdaten benötigt</string>
<string name="realtime_data_loading">Prüfe Echtzeitstatus…</string>
<string name="realtime_data_source">Quelle Echtzeitdaten (beta): %s</string>
<string name="source">Quelle: %s</string>
@@ -42,7 +43,6 @@
<string name="settings_ui">Oberfläche</string>
<string name="settings_map">Karte</string>
<string name="copyright">Copyright</string>
<string name="copyright_summary">©20202022 Johan von Forstner</string>
<string name="other">Sonstiges</string>
<string name="privacy">Datenschutzerklärung</string>
<string name="fav_add">Als Favorit speichern</string>
@@ -158,7 +158,7 @@
<string name="welcome_2">Die Farbe einer Ladestation zeigt dir die maximale Ladeleistung</string>
<string name="welcome_2_detail">Die Farben kannst du unter “Über EVMap → Häufig gestellte Fragen” erneut ansehen</string>
<string name="donation_dialog_title">Danke, dass du EVMap nutzt!</string>
<string name="donation_dialog_detail">EVMap ist kostenlos und Open Source. Über GitHub kann jeder zur Weiterentwicklung der App beitragen. Um die laufenden Kosten für den für die Datenquellen zu decken, freue ich mich auch über Spenden in der App oder über GitHub Sponsors.</string>
<string name="donation_dialog_detail">EVMap ist kostenlos und Open Source. Über GitHub kann jeder zur Weiterentwicklung der App beitragen. Um die laufenden Kosten für die Datenquellen zu decken, freut sich der Entwickler über Spenden mit einem Betrag deiner Wahl.</string>
<string name="chargeprice_donation_dialog_title">Du bist ein richtiger Sparfuchs!</string>
<string name="chargeprice_donation_dialog_detail">Anscheinend nutzt du den Preisvergleich sehr gern. Mit einer Spende für EVMap kannst du helfen, die Kosten für den Datenzugriff zu decken.</string>
<string name="deleted_filterprofile">„%s” gelöscht</string>
@@ -175,6 +175,7 @@
<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="charge_price_minute_format">%1$.2f %2$s/min</string>
<string name="chargeprice_select_connector">Anschluss auswählen</string>
<string name="chargeprice_provider_customer_tariff">Nur für Energiekunden</string>
<string name="percent_format">%.0f%%</string>
@@ -295,4 +296,21 @@
<string name="developer_mode_disabled">Entwicklermodus deaktiviert</string>
<string name="gps">GPS</string>
<string name="compass">Kompass</string>
<string name="charger_website">Website</string>
<string name="location_status">Standortdienste-Status</string>
<string name="pref_tesla_account">Tesla-Account</string>
<string name="pref_tesla_account_enabled">Angemeldet als %s</string>
<string name="pref_tesla_account_disabled">Anmelden, um Echtzeitdaten für Tesla Supercharger zu sehen. Kein Tesla-Fahrzeug notwendig</string>
<string name="logging_in">Anmelden…</string>
<string name="log_out">Abmelden</string>
<string name="logged_out">Abgemeldet</string>
<string name="login">Login</string>
<string name="login_error">Login fehlgeschlagen</string>
<string name="tesla_pricing_owners">Nur Tesla-Fahrzeuge:</string>
<string name="tesla_pricing_members">Tesla-Fahrzeuge &amp; Mitglieder:</string>
<string name="tesla_pricing_others">Andere Kunden:</string>
<string name="pricing_up_to">bis zu %s</string>
<string name="tesla_pricing_other_times">Andere Zeiten:</string>
<string name="tesla_pricing_blocking_fee">Blockiergebühr: %s</string>
<string name="average_utilization">Durchschnittliche Auslastung</string>
</resources>

View File

@@ -81,7 +81,7 @@
<string name="verified">vérifié</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d mode de paiement compatible</item>
<item quantity="many">%d modes de paiement compatibles</item>
<item quantity="many">%d de modes de paiement compatibles</item>
<item quantity="other">%d modes de paiement compatibles</item>
</plurals>
<string name="verified_desc">Le fonctionnement du chargeur a été confirmé au moins une fois par un membre de la communauté %s</string>
@@ -104,7 +104,7 @@
<string name="pref_data_source">Source des données</string>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d tarif sélectionné</item>
<item quantity="many">%d tarifs sélectionnés</item>
<item quantity="many">%d de tarifs sélectionnés</item>
<item quantity="other">%d tarifs sélectionnés</item>
</plurals>
<string name="data_source_openchargemap">Open Charge Map</string>
@@ -154,7 +154,6 @@
<string name="fault_report_date">Rapport d\'anomalie (dernière mise à jour : %s)</string>
<string name="menu_report_new_charger">Nouveau chargeur</string>
<string name="filter_connectors">Connecteurs</string>
<string name="copyright_summary">©2020-2022 Johan von Forstner</string>
<string name="other">Autre</string>
<string name="pref_navigate_use_maps_off">Le bouton de navigation lance lapplication de cartes à lemplacement du chargeur</string>
<string name="settings_map">Carte</string>
@@ -273,4 +272,30 @@
<string name="charging_free">gratuite</string>
<string name="about_contributors">Contributeurs</string>
<string name="about_contributors_text">Merci à tous les contributeurs pour leur contribution au codage et à la traduction d\'EVMap :</string>
<string name="location_error">Localisation non détectée. Veuillez vérifier les paramètres du système</string>
<string name="developer_mode_enabled">Mode développeur activé</string>
<string name="pref_prediction_enabled">Afficher les prévisions d\'utilisation</string>
<string name="pref_prediction_enabled_summary">pour les chargeurs pris en charge
\n(actuellement seulement chargeurs rapides en Allemagne)</string>
<string name="pref_applink_associate">Ouvrir les liens pris en charge</string>
<string name="pref_applink_associate_summary">de goingelectric.de et openchargemap.org</string>
<string name="chargeprice_header_my_tariffs">Mes tarifs</string>
<string name="chargeprice_header_other_tariffs">Autres tarifs</string>
<string name="disable_developer_mode">Désactiver le mode développeur</string>
<string name="developer_mode_disabled">Mode développeur désactivé</string>
<string name="gps">GPS</string>
<string name="compass">Boussole</string>
<string name="prediction_dc_plugs_only">Prises DC</string>
<string name="data_source_switched_to">Source de données basculée vers %s</string>
<string name="menu_reset">Réinitialiser le filtre</string>
<string name="chargeprice_price_not_available">Prix non disponible</string>
<string name="utilization_prediction">Prévision d\'utilisation</string>
<string name="prediction_help">La prédiction est basée sur des facteurs tels que le jour de la semaine, l\'heure de la journée et l\'utilisation passée, ce qui vous permet d\'éviter les chargeurs surchargés. Pas de garantie.</string>
<plurals name="prediction_number_available">
<item quantity="one">%1$d/%2$d disponible</item>
<item quantity="many">%1$d/%2$d disponibles</item>
<item quantity="other">%1$d/%2$d disponibles</item>
</plurals>
<string name="developer_options">Paramètres développeur</string>
<string name="prediction_time_colon">%s :</string>
</resources>

View File

@@ -93,7 +93,6 @@
<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 ruteveiledning på Google Maps</string>
<string name="filter_free_parking">Kun ladere med gratis parkering</string>
<string name="filter_min_power">Min. effekt</string>
@@ -296,4 +295,6 @@
<string name="developer_options">Utvikleralternativer</string>
<string name="data_source_switched_to">Datakilde byttet til %s</string>
<string name="developer_mode_enabled">Utviklermodus påslått</string>
<string name="menu_reset">Tilbakestill filterinnstillinger</string>
<string name="charger_website">Nettside</string>
</resources>

View File

@@ -0,0 +1,298 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="data_source_goingelectric_desc">Ideaal in Duitstalige landen. Beschrijvingen in het Duits. Onderhouden door de gebruikersgemeenschap.</string>
<string name="crash_report_text">EVMap is afgebroken. Stuur een crash rapport naar de ontwikkelaar.</string>
<string name="pref_search_provider_info">Gegevens opzoeken is duur, vooral via Google Maps. Overweeg aub om een donatie te doen via “Over” -&gt; “Doneer”.</string>
<string name="data_source_openchargemap_desc">Werelddekkend, met variabele kwaliteit. Beschrijving in Engels of lokale taal. Onderhouden door de gebruikers. Ook open overheidswege eens in sommige landen (bv. Noord-Amerika, UK, Frankrijk, Noorwegen).</string>
<string name="pref_darkmode_always_off">altijd uit</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="chargeprice_select_car_first">Kiest eerst je voertuig model in de instellingen</string>
<string name="chargeprice_no_compatible_connectors">Geen compatibele connectoren aan dit laadstation</string>
<string name="license">Licentie</string>
<string name="data_sources_description">Kies een gegevensbron voor laadstations. Dit kan later worden aangepast in de app-instellingen.</string>
<string name="category_church">Kerk</string>
<string name="welcome_2">Elk laadpunt heeft een kleur die het maximale laadvermogen weergeeft</string>
<string name="donation_dialog_detail">EVMap is open source en gratis. Via GitHub kan iedereen bijdragen aan de app. Om de vaste kosten te helpen dragen, kan je overwegen een donatie te schenken aan de ontwikkelaar.</string>
<string name="charging_barrierfree">Te gebruiken zonder registratie</string>
<string name="verified_desc">Laadpunt is minstens 1x bevestigd als werkend door een lid van de %s gemeenschap</string>
<string name="chargeprice_no_tariffs_found">Geen tarieven voor dit laadpunt op Chargeprice.app</string>
<string name="category_hospital">Ziekenhuis</string>
<string name="title_activity_maps">EVMap</string>
<string name="connectors">Connectoren</string>
<string name="no_browser_app_found">Installeer eerst een web browser</string>
<string name="address">Adres</string>
<string name="operator">Operator</string>
<string name="network">Netwerk</string>
<string name="open_247"><b>24/7 open</b></string>
<string name="closed"><b>Gesloten</b></string>
<string name="open_closesat"><b>Open</b> · Sluit om %s</string>
<string name="closed_opensat"><b>Gesloten</b> · Opent om %s</string>
<string name="closed_unfmt">Gesloten</string>
<string name="holiday">Feestdag</string>
<string name="cost">Kostprijs</string>
<string name="cost_detail"><b>Laden:</b> %1$s · <b>Parkeren:</b> %2$s</string>
<string name="cost_detail_charging"><b>%s laden</b></string>
<string name="cost_detail_parking"><b>%s parkeren</b></string>
<string name="charging_free">Gratis</string>
<string name="parking_free">Gratis</string>
<string name="amenities">Voorzieningen</string>
<string name="general_info">Algemene informatie</string>
<string name="realtime_data_unavailable">Real-time status niet beschikbaar</string>
<string name="realtime_data_loading">Real-time status opvragen…</string>
<string name="source">Bron: %s</string>
<string name="search">Zoek</string>
<string name="menu_map">Kaart</string>
<string name="menu_favs">Favorieten</string>
<string name="menu_filter">Filter</string>
<string name="not_implemented">nog niet geïmplementeerd</string>
<string name="about">Over</string>
<string name="version">Versie</string>
<string name="github_link_title">Broncode</string>
<string name="oss_licenses">Licenties</string>
<string name="settings">Instellingen</string>
<string name="settings_ui">Interface</string>
<string name="settings_map">Kaart</string>
<string name="copyright">Copyright</string>
<string name="other">Andere</string>
<string name="privacy">Privacy</string>
<string name="pref_navigate_use_maps_off">Navigatieknop opent de kaart app met de locatie van het laadstation</string>
<string name="coordinates">Coördinaten</string>
<string name="share">Deel</string>
<string name="filter_free">Allen gratis laadpunten</string>
<string name="filter_min_power">Minimaal vermogen</string>
<string name="filter_free_parking">Alleen laadpunten met gratis parking</string>
<string name="filter_min_connectors">Minimaal aantal connecteren</string>
<string name="filter_connectors">Connectoren</string>
<string name="plug_type_3">Type 3A</string>
<string name="plug_schuko">Schuko</string>
<string name="plug_chademo">CHAdeMO</string>
<string name="plug_cee_rot">CEE Red</string>
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
<string name="all">allemaal</string>
<string name="none">geen</string>
<string name="show_more">meer…</string>
<string name="favorites_empty_state">Opgeslagen laadpunten verschijnen hier</string>
<string name="donate">Doneer</string>
<string name="donation_successful">Dank u ❤️</string>
<string name="donation_failed">Er ging iets mis 😕</string>
<string name="map_type_normal">Default</string>
<string name="map_type_satellite">Satelliet</string>
<string name="map_type_terrain">Terrein</string>
<string name="map_traffic">Verkeer</string>
<string name="faq">Veelgestelde vragen</string>
<string name="menu_filters_active">Actieve filters</string>
<string name="filters_activated">Filters geactiveerd</string>
<string name="filters_deactivated">Filters gedeactiveerd</string>
<string name="menu_edit_filters">Pas filters aan</string>
<string name="menu_manage_filter_profiles">Beheer filterprofielen</string>
<string name="go_to_chargeprice">Vergelijk prijzen</string>
<string name="fault_report">Foutenrapport</string>
<string name="fault_report_date">Foutenrapport (laatste update: %s)</string>
<string name="filter_networks">Netwerken</string>
<string name="filter_operators">Operatoren</string>
<string name="filter_chargecards">Betaalmethoden</string>
<string name="all_selected">Alle geselecteerd</string>
<string name="number_selected">%d geselecteerd</string>
<string name="edit">aanpassen</string>
<string name="cancel">Afbreken</string>
<string name="ok">OK</string>
<string name="pref_language">App-taal</string>
<string name="pref_darkmode">Donkere modus</string>
<string name="connection_error">Laadstations konden niet worden geladen</string>
<string name="location_error">Kon locatie niet bepalen. Controleer de instellingen</string>
<string name="retry">Opnieuw</string>
<string name="filter_open_247">24/7 beschikbaar</string>
<string name="filter_barrierfree">Te gebruiken zonder registratie</string>
<string name="filter_exclude_faults">Sluit laadstations uit met gerapporteerde fouten</string>
<string name="categories">Categorieën</string>
<string name="category_car_dealership">Autoverdeler</string>
<string name="category_service_on_motorway">Herstelzone (op snelweg)</string>
<string name="category_service_off_motorway">Herstelzone (niet langs de snelweg)</string>
<string name="category_railway_station">Treinstation</string>
<string name="category_shopping_mall">Winkelcentrum</string>
<string name="category_holiday_home">Vakantiewoning</string>
<string name="category_airport">Luchthaven</string>
<string name="category_amusement_park">Attractiepark</string>
<string name="category_hotel">Hotel</string>
<string name="category_cinema">Bioscoop</string>
<string name="category_museum">Museum</string>
<string name="category_parking_multi">Parkeergarage</string>
<string name="category_parking">Parking</string>
<string name="category_private_charger">Privé-laadpunt</string>
<string name="category_rest_area">Rustplaats</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_swimming_pool">Zwembad</string>
<string name="category_supermarket">Supermarkt</string>
<string name="category_petrol_station">Benzinestation</string>
<string name="category_parking_underground">Ondergrondse parking</string>
<string name="category_zoo">Zoo</string>
<string name="category_caravan_site">Staanplaats voor caravans</string>
<string name="menu_apply">Pas filters toe</string>
<string name="menu_save_profile">Bewaar als profiel</string>
<string name="menu_reset">Reset filters</string>
<string name="no_filters">Geen filters</string>
<string name="filter_custom">Aangepaste filter</string>
<string name="filter_favorites">Favorieten</string>
<string name="reorder">herorden</string>
<string name="delete">Verwijder</string>
<string name="save_as_profile">Bewaar als profiel</string>
<string name="save_profile_enter_name">Geef de naam van het filterprofiel:</string>
<string name="filterprofiles_empty_state">Je hebt geen bewaarde filterprofielen</string>
<string name="welcome_to_evmap">Welkom bij EVMap</string>
<string name="welcome_1">Zoek EV laadpunten in je omgeving</string>
<string name="welcome_2_title">Een kwestie van power</string>
<string name="welcome_2_detail">Dit vind je ook in “Over” → “Veelgestelde vragen”</string>
<string name="donation_dialog_title">Bedankt om EVMap te gebruiken</string>
<string name="chargeprice_donation_dialog_title">Jij bent een echte koopjeszoeker!</string>
<string name="chargeprice_donation_dialog_detail">Blijkbaar maak je dankbaar gebruik van de prijsvergelijkingen. Met een donatie kan je de kosten voor deze data helpen dragen.</string>
<string name="deleted_filterprofile">“%s” verwijderd</string>
<string name="undo">Ongedaan maken</string>
<string name="rename">Hernoem</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d compatibele betaalmethode</item>
<item quantity="other">%d compatibele betaalmethodes</item>
</plurals>
<string name="navigate">Navigeer naar hier</string>
<string name="verified">geverifieerd</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_select_connector">Kies connector</string>
<string name="chargeprice_provider_customer_tariff">Alleen voor eigen klanten</string>
<string name="edit_on_goingelectric_info">Log aub in op GoingElectric.de als deze pagina leeg is</string>
<string name="percent_format">%.0f%%</string>
<string name="chargeprice_session_fee">kostprijs  sessie</string>
<string name="chargeprice_per_kwh">per kWh</string>
<string name="chargeprice_per_minute">per min</string>
<string name="chargeprice_blocking_fee">Kostprijs blokkeren &gt;%s</string>
<string name="powered_by_chargeprice">powered by Chargeprice</string>
<string name="settings_chargeprice">Prijsvergelijking</string>
<string name="pref_my_vehicle">Mijn voertuigen</string>
<string name="pref_chargeprice_no_base_fee">Sluit plannen uit met maandelijkse kost</string>
<string name="pref_chargeprice_show_provider_customer_tariffs">Neem plannen op die enkel voor klanten gelden</string>
<string name="chargeprice_battery_range_from">Laden vanaf</string>
<string name="chargeprice_battery_range_to">tot</string>
<string name="chargeprice_vehicle">Voertuig</string>
<string name="chargeprice_price_not_available">Prijs niet beschikbaar</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Sommige energieleveranciers bieden speciale plannen voor hun klanten</string>
<string name="close">Sluiten</string>
<string name="chargeprice_title">Prijzen</string>
<string name="chargeprice_connection_error">Kon prijzen niet laden</string>
<string name="pref_chargeprice_currency">Valuta</string>
<string name="pref_my_tariffs">Mijn laadplannen</string>
<plurals name="pref_my_tariffs_summary">
<item quantity="one">(wordt aangeduid in de prijsvergelijking)</item>
<item quantity="other">(worden aangeduid in de prijsvergelijking)</item>
</plurals>
<string name="chargeprice_all_tariffs_selected">alle plannen geselecteerd</string>
<string name="settings_charger_data">Laadstations</string>
<string name="pref_data_source">Databron</string>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d plan geselecteerd</item>
<item quantity="other">%d plannen geselecteerd</item>
</plurals>
<string name="unknown_operator">Onbekende operator</string>
<string name="data_source_goingelectric">GoingElectric.de</string>
<string name="data_source_openchargemap">OpenChargeMap</string>
<string name="chargeprice_base_fee">Abonnementskost: %1$.2f %2$s/maand</string>
<string name="chargeprice_min_spend">Minimale kost: %1$.2f %2$s/maand</string>
<string name="chargeprice_battery_range">Laden van %1$.0f%% tot %2$.0f%%</string>
<string name="chargeprice_stats">(%1$.0f kWh, ca. %2$s, ⌀ %3$.0f kW)</string>
<string name="next">volgende</string>
<string name="get_started">Starten</string>
<string name="got_it">Begrepen</string>
<string name="lets_go">Laten we beginnen</string>
<string name="crash_report_comment_prompt">Je kan hieronder commentaar geven:</string>
<string name="powered_by_mapbox">powered by Mapbox</string>
<string name="pref_search_provider">Zoekprovider</string>
<string name="donate_desc">Ondersteun de EVMap ontwikkeling via een eenmalige donatie</string>
<string name="github_sponsors_desc">Ondersteun EVMap op GitHub Spinsors</string>
<string name="github_sponsors">GitHub Sponsors</string>
<string name="unnamed_filter_profile">Naamloos filterprofiel</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>
<string name="required">verplicht</string>
<string name="pref_search_delete_recent">Verwijder recente zoekresultaten</string>
<string name="deleted_recent_search_results">Recente zoekresultaten zijn verwijderd</string>
<string name="settings_data_sources">Gegevensbronnen</string>
<string name="help">Help</string>
<string name="settings_android_auto">Android Auto</string>
<string name="pref_chargeprice_allow_unbalanced_load">Ongebalanceerd laden toelaten</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Eenfasig AC laden toelaten met meer dan 4.5 kW</string>
<string name="pref_map_rotate_gestures_enabled">Kaartrotatie</string>
<string name="pref_map_rotate_gestures_on">Gebruik twee vingers om de kaart te draaien</string>
<string name="pref_map_rotate_gestures_off">Rotatie afzetten (noorden naar boven)</string>
<string name="refresh_live_data">vernieuw de real-time status</string>
<string name="autocomplete_connection_error">Suggesties konden niet worden geladen</string>
<string name="pref_language_device_default">Standaardtaal van toestel</string>
<string name="pref_darkmode_device_default">Standaardinstelling van toestel</string>
<string name="pref_darkmode_always_on">altijd aan</string>
<string name="pref_chargeprice_currency_chf">Zwitserse Frank (CHF)</string>
<string name="pref_chargeprice_currency_czk">Tsjechische koruna (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Deense kroon (DKK)</string>
<string name="pref_chargeprice_currency_gbp">Britse Pond (GBP)</string>
<string name="pref_chargeprice_currency_hrk">Kroatische Kuna (HRK)</string>
<string name="pref_chargeprice_currency_huf">Hongaarse Forint (HUF)</string>
<string name="pref_chargeprice_currency_isk">IJslandse Kroon (ISK)</string>
<string name="pref_chargeprice_currency_nok">Noorse Kroon (NOK)</string>
<string name="pref_chargeprice_currency_pln">Poolse Złoty (PLN)</string>
<string name="pref_chargeprice_currency_sek">Zweedse Kroon (SEK)</string>
<string name="pref_chargeprice_currency_usd">Amerikaanse Dollar (USD)</string>
<string name="pref_provider_google_maps">Google Maps</string>
<string name="edit_filter_profile">“%s” editeren</string>
<string name="compass">Kompas</string>
<string name="gps">GPS</string>
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="about_contributors">Bijdragers</string>
<string name="about_contributors_text">Dank aan iedereen die heeft bijgedragen aan de code en vertaling van EVMap:</string>
<string name="utilization_prediction">Voorspeld verbruik</string>
<string name="prediction_help">De voorspelling is gebaseerd op factoren zoals weekdag, tijdstip en gebruik in het verleden, zodat je zwaar bezette laders kan vermijden. Geen garantie, uiteraard.</string>
<string name="prediction_time_colon">%s:</string>
<string name="pref_prediction_enabled">Toon voorspeld gebruik</string>
<string name="pref_prediction_enabled_summary">voor ondersteunde laders
\n(momenteel enkel DC in Duitsland)</string>
<string name="prediction_only">(enkel %s)</string>
<string name="prediction_dc_plugs_only">DC aansluitingen</string>
<string name="data_source_switched_to">Gegevensbron gewijzigd naar %s</string>
<string name="pref_applink_associate">Open ondersteunde links</string>
<string name="pref_applink_associate_summary">van going electric.de en openchargemap.org</string>
<string name="chargeprice_header_my_tariffs">Mijn plannen</string>
<string name="chargeprice_header_other_tariffs">Andere plannen</string>
<string name="developer_mode_enabled">Ontwillekaarsmodus geactiveerd</string>
<string name="developer_options">Ontwikkelaarsopties</string>
<string name="disable_developer_mode">Ontwikkelaarsmodus uitzetten</string>
<string name="developer_mode_disabled">Ontwikkelaarsmodus uitgezet</string>
<plurals name="prediction_number_available">
<item quantity="one">%1$d/%2$d beschkbaar</item>
<item quantity="other">%1$d/%2$d beschikbaar</item>
</plurals>
<string name="app_name">EVMap</string>
<string name="no_maps_app_found">Installeer eerst een navigatie-app</string>
<string name="hours">Openingsuren</string>
<string name="charging_paid">Betalend</string>
<string name="parking_paid">Betalend</string>
<string name="realtime_data_source">Real-time status bron (beta): %s</string>
<string name="pref_navigate_use_maps">Onmiddellijke navigatie</string>
<string name="fav_remove">Verwijder uit favorieten</string>
<string name="pref_navigate_use_maps_on">Navigatieknop start routebegeleiding met Google Maps</string>
<string name="fav_add">Bewaar als favoriet</string>
<string name="goingelectric_forum">Forumthread op GoingElectric.de</string>
<string name="plug_type_2">Type 2</string>
<string name="plug_supercharger">Tesla Supercharger</string>
<string name="plug_cee_blau">CEE Blue</string>
<string name="plug_ccs">CCS</string>
<string name="plug_type_1">Type 1</string>
<string name="menu_report_new_charger">Nieuw laadpunt</string>
<string name="show_less">minder…</string>
<string name="map_type">Kaarttype</string>
<string name="map_details">Kaartdetails</string>
<string name="edit_at_datasource">aanpassen op %s</string>
<string name="charge_cards">Betaalmethoden</string>
<string name="pref_map_provider">Kaartaanbieder</string>
<string name="twitter">Twitter</string>
<string name="contact">Contact</string>
<string name="and_n_others">en %d andere</string>
<string name="category_camping">Kampeerplaats</string>
<string name="category_public_authorities">Publieke instanties</string>
</resources>

View File

@@ -0,0 +1,303 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="menu_reset">Repor filtros</string>
<string name="filter_custom">Filtro modificado</string>
<string name="filter_favorites">Favoritos</string>
<string name="reorder">reordenar</string>
<string name="delete">Apagar</string>
<string name="welcome_2_detail">Também pode encontrar esta informação em \"Sobre\" → \"Perguntas frequentes\"</string>
<string name="chargeprice_donation_dialog_title">Você é um verdadeiro caçador de pechinchas!</string>
<string name="donation_dialog_title">Obrigado por usar o EVMap</string>
<string name="deleted_filterprofile">“%s” removido</string>
<string name="undo">Refazer</string>
<string name="rename">Renomear</string>
<string name="chargeprice_donation_dialog_detail">Você faz grande uso da comparação de preços. Ajude a cobrir os custos de acesso à informação apoiando o EVMap com uma doação.</string>
<string name="verified">verificado</string>
<string name="chargeprice_select_connector">Escolhe o conector</string>
<string name="verified_desc">O carregador foi marcado como funcional por um membro da comunidade %s</string>
<string name="charge_price_format">%2$s%1$.2f</string>
<string name="charge_price_average_format">⌀ %2$s%1$.2f/kWh</string>
<string name="charge_price_kwh_format">%2$s%1$.2f/kWh</string>
<string name="percent_format">%.0f%%</string>
<string name="pref_my_vehicle">Os meus veículos</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">As empresas de serviços públicos às vezes oferecem planos especiais para os seus clientes</string>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d plano selecionado</item>
<item quantity="many">%d planos selecionados</item>
<item quantity="other">%d de planos selecionados</item>
</plurals>
<string name="data_sources_description">Escolha uma fonte para as estações de carregamento. Pode alterar mais tarde nas definições da app.</string>
<string name="about_contributors_text">Obrigado a todos os contribuidores de código e traduções para o EVMap:</string>
<string name="utilization_prediction">Previsão de utilização</string>
<string name="prediction_time_colon">%s:</string>
<string name="pref_applink_associate_summary">de goingelectric.de e openchargemap.org</string>
<string name="none">nenhum</string>
<string name="donate">Doar</string>
<string name="show_less">menos…</string>
<string name="all">todos</string>
<string name="show_more">mais…</string>
<string name="filters_deactivated">Filtros desativados</string>
<string name="favorites_empty_state">Carregadores guardados aparecem aqui</string>
<string name="donation_successful">Obrigado ❤️</string>
<string name="category_car_dealership">Stand de carros</string>
<string name="map_traffic">Trânsito</string>
<string name="faq">Perguntas frequentes</string>
<string name="menu_filters_active">Filtros ativos</string>
<string name="menu_edit_filters">Editar filtros</string>
<string name="filters_activated">Filtros ativados</string>
<string name="menu_manage_filter_profiles">Gerir perfis de filtros</string>
<string name="go_to_chargeprice">Comparar preços</string>
<string name="filter_operators">Operadores</string>
<string name="location_error">Localização não encontrada. Verifique se a app tem permissão para usar aceder à sua localização</string>
<string name="filter_networks">Redes</string>
<string name="fault_report">Com problemas</string>
<string name="number_selected">%d selecionados</string>
<string name="cancel">Cancelar</string>
<string name="ok">OK</string>
<string name="filter_barrierfree">Não necessita de registo</string>
<string name="fault_report_date">Com problemas (atualizado: %s)</string>
<string name="filter_chargecards">Formas de pagamento</string>
<string name="pref_language">Língua da app</string>
<string name="all_selected">Todos selecionados</string>
<string name="edit">editar</string>
<string name="pref_darkmode">Modo escuro</string>
<string name="connection_error">Não foi possível carregar a lista de carregadores</string>
<string name="retry">Tentar novamente</string>
<string name="filter_open_247">Disponível 24/7</string>
<string name="filter_exclude_faults">Excluir carregadores com relatos de falhas</string>
<string name="charge_cards">Formas de pagamento</string>
<string name="and_n_others">e %d outros</string>
<string name="goingelectric_forum">Tópico no fórum GoingElectric.de</string>
<string name="contact">Contato</string>
<string name="menu_report_new_charger">Novo carregador</string>
<string name="category_holiday_home">Casa de férias</string>
<string name="pref_map_provider">Provedor do mapa</string>
<string name="twitter">Twitter</string>
<string name="category_public_authorities">Autoridades públicas</string>
<string name="category_private_charger">Carregador privado</string>
<string name="category_rest_area">Área de descanso</string>
<string name="edit_at_datasource">editado em %s</string>
<string name="categories">Categorias</string>
<string name="category_service_on_motorway">Área de serviço (autoestrada)</string>
<string name="category_service_off_motorway">Área de serviço (fora da autoestrada)</string>
<string name="category_railway_station">Estação de comboio</string>
<string name="category_shopping_mall">Centro comercial</string>
<string name="category_amusement_park">Parque de diversões</string>
<string name="category_airport">Aeroporto</string>
<string name="category_parking_multi">Garagem de estacionamento</string>
<string name="category_camping">Parque de campismo</string>
<string name="category_cinema">Cinema</string>
<string name="category_hotel">Hotel</string>
<string name="category_church">Igreja</string>
<string name="category_hospital">Hospital</string>
<string name="category_museum">Museu</string>
<string name="category_parking">Parque de estacionamento</string>
<string name="category_restaurant">Restaurante</string>
<string name="save_profile_enter_name">Insira o nome do perfil com este filtro:</string>
<string name="save_as_profile">Guardar como perfil</string>
<string name="filterprofiles_empty_state">Não existem filtros guardados</string>
<string name="welcome_2">Cada cor corresponde a potência máxima do carregador</string>
<string name="welcome_to_evmap">Bem-vindo ao EVMap</string>
<string name="pref_darkmode_always_off">Sempre desligado</string>
<string name="welcome_2_title">Escolha a potência</string>
<string name="navigate">Navegar</string>
<string name="donation_dialog_detail">O EVMap é gratuito e de código aberto. Contribuições de código no GitHub são bem-vindas. Para ajudar a cobrir os custos de operação, por favor considere fazer uma doação ao criador da app.</string>
<string name="charging_barrierfree">Não necessita de registo</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d forma de pagamento compatível</item>
<item quantity="other">%d formas de pagamento compatíveis</item>
<item quantity="many">%d de formas de pagamento compatíveis</item>
</plurals>
<string name="chargeprice_session_fee">custo da sessão</string>
<string name="edit_on_goingelectric_info">Por favor faça o login em GoingElectric.de se esta página estiver vazia</string>
<string name="chargeprice_blocking_fee">Taxa de bloqueio %s</string>
<string name="chargeprice_per_kwh">por kWh</string>
<string name="chargeprice_per_minute">por minuto</string>
<string name="pref_chargeprice_no_base_fee">Excluir planos com taxas mensais</string>
<string name="chargeprice_no_tariffs_found">O Chargeprice.app não encontrou planos de carregamento para este carregador</string>
<string name="chargeprice_min_spend">Gasto mínimo: %2$s%1$.2f/mês</string>
<string name="powered_by_chargeprice">informação da Chargeprice</string>
<string name="chargeprice_base_fee">Taxa base: %2$s%1$.2f/mês</string>
<string name="settings_chargeprice">Comparação de preços</string>
<string name="chargeprice_provider_customer_tariff">Apenas para clientes com subscrição</string>
<string name="chargeprice_battery_range">Carregar de %1$.0f%% até %2$.0f%%</string>
<string name="chargeprice_battery_range_from">Carregar de</string>
<string name="pref_chargeprice_show_provider_customer_tariffs">Incluir planos de subscrição</string>
<string name="chargeprice_select_car_first">Por favor escolha o modelo do seu carro nas definições primeiro</string>
<string name="chargeprice_battery_range_to">até</string>
<string name="chargeprice_stats">(%1$.0f kWh, %2$s aprox., ⌀ %3$.0f kW)</string>
<string name="chargeprice_connection_error">Não foi possível carregar os preços</string>
<string name="chargeprice_vehicle">Veículo</string>
<string name="chargeprice_title">Preços</string>
<string name="chargeprice_no_compatible_connectors">Não existem conectores compatíveis nesta estação de carregamento</string>
<string name="pref_chargeprice_currency">Moeda</string>
<string name="got_it">Continuar</string>
<string name="chargeprice_price_not_available">Preço não disponível</string>
<string name="pref_my_tariffs">Os meus planos de carregamento</string>
<plurals name="pref_my_tariffs_summary">
<item quantity="one" tools:ignore="ImpliedQuantity">(será destacado na comparação de preços)</item>
<item quantity="other">(serão destacados na comparação de preços)</item>
<item quantity="many">(serão destacados na comparação de preços)</item>
</plurals>
<string name="data_source_goingelectric">GoingElectric.de</string>
<string name="chargeprice_all_tariffs_selected">Todos os planos selecionados</string>
<string name="license">Licença</string>
<string name="unknown_operator">Operador desconhecido</string>
<string name="settings_charger_data">Estações de carregamento</string>
<string name="data_source_goingelectric_desc">Boa escolha para países de língua alemã. Descrições em alemão. Mantido pela comunidade.</string>
<string name="pref_data_source">Fonte da informação</string>
<string name="data_source_openchargemap">Open Charge Map</string>
<string name="next">próximo</string>
<string name="data_source_openchargemap_desc">Mundial, com vários níveis de qualidade. Descrições em inglês ou língua local. Mantido pela comunidade e usa informação governamental publica em alguns países (ex: América do Norte, Reino Unido, França, Noruega, etc).</string>
<string name="get_started">Começar</string>
<string name="lets_go">Vamos lá</string>
<string name="crash_report_text">O EVMap encontrou um problema. Por favor envie um relatório do erro para o criador da app.</string>
<string name="crash_report_comment_prompt">Pode adicionar um comentário abaixo:</string>
<string name="pref_search_provider">Fornecedor da pesquisa</string>
<string name="powered_by_mapbox">via Mapbox</string>
<string name="github_sponsors">GitHub Sponsors</string>
<string name="donate_desc">Apoie o desenvolvimento do EVMap com uma única doação</string>
<string name="pref_map_rotate_gestures_on">Use dois dedos para girar o mapa</string>
<string name="pref_map_rotate_gestures_off">Rotação desligada (norte sempre para cima)</string>
<string name="refresh_live_data">atualizar estado em tempo real</string>
<string name="pref_search_provider_info">As pesquisas são caras, especialmente quando o Google Maps é usado. Por favor considere doar através de \"Sobre\" → \"Doar\".</string>
<string name="github_sponsors_desc">Apoie o EVMap através do GitHub</string>
<string name="unnamed_filter_profile">Filtro sem nome</string>
<string name="deleted_recent_search_results">As pesquisas recentes foram eliminadas</string>
<string name="help">Ajuda</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>
<string name="pref_search_delete_recent">Apagar pesquisas recentes</string>
<string name="required">obrigatório</string>
<string name="settings_data_sources">Fontes de informação</string>
<string name="settings_android_auto">Android Auto</string>
<string name="pref_map_rotate_gestures_enabled">Rotação do mapa</string>
<string name="autocomplete_connection_error">Não foi possível carregar as sugestões</string>
<string name="pref_language_device_default">Língua do dispositivo</string>
<string name="pref_darkmode_device_default">Padrão do dispositivo</string>
<string name="pref_darkmode_always_on">Sempre ligado</string>
<string name="pref_chargeprice_currency_chf">Franco suíço (CHF)</string>
<string name="pref_chargeprice_currency_czk">Coroa checa (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Coroa dinamarquesa (DKK)</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="pref_chargeprice_currency_gbp">Libra esterlina (GBP)</string>
<string name="chargeprice_header_my_tariffs">Os meus planos</string>
<string name="chargeprice_header_other_tariffs">Outros planos</string>
<string name="developer_options">Opções de desenvolvedor</string>
<string name="prediction_help">A previsão é baseada em fatores como dia da semana, hora do dia e uso anterior para que você evite carregadores superlotados. Sem garantias de estar correta.</string>
<string name="disable_developer_mode">Desativar o modo de desenvolvedor</string>
<string name="compass">Compasso</string>
<plurals name="prediction_number_available">
<item quantity="one">%1$d/%2$d disponível</item>
<item quantity="many">%1$d/%2$d disponíveis</item>
<item quantity="other">%1$d/%2$d disponíveis</item>
</plurals>
<string name="pref_prediction_enabled">Mostrar previsões de utilização</string>
<string name="developer_mode_enabled">Modo de desenvolvedor ativado</string>
<string name="pref_prediction_enabled_summary">para carregadores suportados
\n(atualmente apenas CC/DC na Alemanha)</string>
<string name="prediction_only">(apenas %s)</string>
<string name="prediction_dc_plugs_only">Conectores CC/DC</string>
<string name="pref_applink_associate">Abrir links suportados</string>
<string name="data_source_switched_to">Fonte de dados alterada para %s</string>
<string name="developer_mode_disabled">Modo de desenvolvedor desativado</string>
<string name="gps">GPS</string>
<string name="no_maps_app_found">Instale a app de navegação primeiro</string>
<string name="no_browser_app_found">Instale um navegador web primeiro</string>
<string name="connectors">Conectores</string>
<string name="address">Endereço</string>
<string name="operator">Operador</string>
<string name="network">Rede</string>
<string name="hours">Horário de abertura</string>
<string name="open_247"><b>Aberto 24/7</b></string>
<string name="closed"><b>Fechado</b></string>
<string name="open_closesat"><b>Aberto</b> · Fecha às %s</string>
<string name="closed_opensat"><b>Fechado</b> · Abre às %s</string>
<string name="app_name">EVMap</string>
<string name="title_activity_maps">EVMap</string>
<string name="closed_unfmt">Fechado</string>
<string name="holiday">Feriado</string>
<string name="cost">Custo</string>
<string name="cost_detail"><b>Carregamento:</b> %1$s · <b>Parque:</b> %2$s</string>
<string name="cost_detail_charging"><b>Carregamento %s</b></string>
<string name="cost_detail_parking"><b>Parque %s</b></string>
<string name="charging_free">Gratuito</string>
<string name="charging_paid">Pago</string>
<string name="parking_free">Gratuito</string>
<string name="parking_paid">Pago</string>
<string name="amenities">Facilidades</string>
<string name="general_info">Informação geral</string>
<string name="realtime_data_unavailable">Estado em tempo real não disponível</string>
<string name="realtime_data_loading">Verificado estado em tempo real…</string>
<string name="realtime_data_source">Fonte do estado em tempo real (beta): %s</string>
<string name="source">Fonte: %s</string>
<string name="search">Pesquisa</string>
<string name="menu_map">Mapa</string>
<string name="menu_favs">Favoritos</string>
<string name="menu_filter">Filtro</string>
<string name="not_implemented">ainda não implementado</string>
<string name="about">Sobre</string>
<string name="version">Versão</string>
<string name="github_link_title">Código-fonte</string>
<string name="oss_licenses">Licenças</string>
<string name="settings_ui">Interface</string>
<string name="settings_map">Mapa</string>
<string name="copyright">Direitos de autor</string>
<string name="other">Outro</string>
<string name="privacy">Privacidade</string>
<string name="settings">Definições</string>
<string name="fav_add">Guardar como favorito</string>
<string name="fav_remove">Remover dos favoritos</string>
<string name="pref_navigate_use_maps">Navegar agora</string>
<string name="pref_navigate_use_maps_on">O botão de navegação inicia a navegação com o Google Maps</string>
<string name="pref_navigate_use_maps_off">O botão de navegação abre a app dos mapas com a localização do carregador</string>
<string name="coordinates">Coordenadas</string>
<string name="share">Partilhar</string>
<string name="filter_free">Apenas carregadores gratuitos</string>
<string name="filter_min_power">Potência minima</string>
<string name="filter_free_parking">Apenas carregadores com parque gratuito</string>
<string name="filter_min_connectors">Número mínimo de conectores</string>
<string name="filter_connectors">Conectores</string>
<string name="plug_type_1">Tipo 1</string>
<string name="plug_type_2">Tipo 2</string>
<string name="plug_type_3">Tipo 3A</string>
<string name="plug_ccs">CCS</string>
<string name="plug_schuko">Schuko</string>
<string name="plug_chademo">CHAdeMO</string>
<string name="plug_cee_blau">CEE Azul</string>
<string name="plug_cee_rot">CEE Vermelho</string>
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
<string name="plug_supercharger">Supercarregador Tesla</string>
<string name="donation_failed">Algo correu mal 😕</string>
<string name="map_type_satellite">Satélite</string>
<string name="map_type_terrain">Terreno</string>
<string name="map_type">Tipo de mapa</string>
<string name="map_details">Detalhes do mapa</string>
<string name="map_type_normal">Padrão</string>
<string name="category_swimming_pool">Piscina</string>
<string name="category_supermarket">Supermercado</string>
<string name="category_petrol_station">Posto de combustível</string>
<string name="category_parking_underground">Parque de estacionamento subterrâneo</string>
<string name="category_zoo">Zoo</string>
<string name="category_caravan_site">Caravanas</string>
<string name="menu_apply">Aplicar filtros</string>
<string name="menu_save_profile">Guardar como perfil</string>
<string name="no_filters">Sem filtros</string>
<string name="welcome_1">Encontre carregadores elétricos perto de si</string>
<string name="close">Fechar</string>
<string name="edit_filter_profile">Editar “%s”</string>
<string name="pref_chargeprice_currency_hrk">Cuna croata (HRK)</string>
<string name="pref_chargeprice_currency_huf">Florim húngaro (HUF)</string>
<string name="pref_chargeprice_currency_isk">Coroa islandesa (ISK)</string>
<string name="pref_chargeprice_currency_nok">Coroa norueguesa (NOK)</string>
<string name="pref_chargeprice_currency_pln">Złoty polaco (PLN)</string>
<string name="pref_chargeprice_currency_sek">Coroa sueca (SEK)</string>
<string name="pref_chargeprice_currency_usd">Dólar americano (USD)</string>
<string name="pref_provider_google_maps">Google Maps</string>
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="about_contributors">Contribuidores</string>
<string name="pref_chargeprice_allow_unbalanced_load">Permitir carga não balanceada</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Permitir carregamento CA/AC monofásico (1 fase) com mais de 4.5 kW</string>
<string name="charger_website">Website</string>
</resources>

View File

@@ -0,0 +1,301 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">EVMap</string>
<string name="title_activity_maps">EVMap</string>
<string name="connectors">Conectori</string>
<string name="no_maps_app_found">Instalati o aplicatie de navigatie</string>
<string name="no_browser_app_found">Instalati un browser web</string>
<string name="address">Adresa</string>
<string name="operator">Operator</string>
<string name="network">Retea</string>
<string name="hours">Program</string>
<string name="open_247"><![CDATA[<b>Deschis nonstop</b>]]></string>
<string name="closed"><![CDATA[<b>Inchis</b>]]></string>
<string name="open_closesat"><![CDATA[<b>Deschis</b> · Inchide la %s]]></string>
<string name="closed_opensat"><![CDATA[<b>Inchis</b> · Deschide la %s]]></string>
<string name="closed_unfmt">Inchis</string>
<string name="holiday">Sarbatoare</string>
<string name="cost">Cost</string>
<string name="cost_detail"><![CDATA[<b>Incarcare:</b> %1$s · <b>Parcare:</b> %2$s]]></string>
<string name="cost_detail_charging"><![CDATA[<b>%s incarcare</b>]]></string>
<string name="cost_detail_parking"><![CDATA[<b>%s parcare</b>]]></string>
<string name="charging_free">Gratuit</string>
<string name="charging_paid">Cu plata</string>
<string name="parking_free">Gratuit</string>
<string name="parking_paid">Cu plata</string>
<string name="amenities">Facilitati</string>
<string name="general_info">Informatii generale</string>
<string name="realtime_data_unavailable">Stare in tip real indisponibila</string>
<string name="realtime_data_loading">Verificare stare in timp real…</string>
<string name="realtime_data_source">Sursa verificare in timp real (beta): %s</string>
<string name="source">Sursa: %s</string>
<string name="search">Cautare</string>
<string name="menu_map">Harta</string>
<string name="menu_favs">Favorite</string>
<string name="menu_filter">Fitru</string>
<string name="not_implemented">indisponibil momentan</string>
<string name="about">Despre </string>
<string name="version">Versiune</string>
<string name="github_link_title">Cod sursa</string>
<string name="oss_licenses">Licente</string>
<string name="settings">Setari</string>
<string name="settings_ui">Interfata</string>
<string name="settings_map">Harta</string>
<string name="copyright">Copyright</string>
<string name="other">Altele</string>
<string name="privacy">Confidentialitate</string>
<string name="fav_add">Salveaza ca favorit</string>
<string name="fav_remove">Sterge din favorite</string>
<string name="pref_navigate_use_maps">Indicatii navigare</string>
<string name="pref_navigate_use_maps_on">Butonul de navigare porneste cu Google Maps</string>
<string name="pref_navigate_use_maps_off">Butonul de navigare deschide aplicatia de harti cu locatia statiei</string>
<string name="coordinates">Coordonate</string>
<string name="share">Distribuie</string>
<string name="filter_free">Doar statii gratuite</string>
<string name="filter_min_power">Putere minima</string>
<string name="filter_free_parking">Doar statii cu parcare gratuita</string>
<string name="filter_min_connectors">Numar minim de conectori</string>
<string name="filter_connectors">Conectori</string>
<string name="plug_type_1">Type 1</string>
<string name="plug_type_2">Type 2</string>
<string name="plug_type_3">Type 3A</string>
<string name="plug_ccs">CCS</string>
<string name="plug_schuko">Schuko</string>
<string name="plug_chademo">CHAdeMO</string>
<string name="plug_supercharger">Tesla Supercharger</string>
<string name="plug_cee_blau">CEE Blue</string>
<string name="plug_cee_rot">CEE Red</string>
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
<string name="all">toate</string>
<string name="none">niciunul</string>
<string name="show_more">mai mult…</string>
<string name="show_less">mai putin…</string>
<string name="favorites_empty_state">Statiile salvate apar aici</string>
<string name="donate">Doneaza</string>
<string name="donation_successful">Multumesc ❤️</string>
<string name="donation_failed">A aparut o eroare 😕</string>
<string name="map_type_normal">Implicit</string>
<string name="map_type_satellite">Satelit</string>
<string name="map_type_terrain">Teren</string>
<string name="map_type">Tip harta</string>
<string name="map_details">Detalii harta</string>
<string name="map_traffic">Trafic</string>
<string name="faq">Intrebari frecvente</string>
<string name="menu_filters_active">Filtre active</string>
<string name="filters_activated">Filtre activate</string>
<string name="filters_deactivated">Filtre dezactivate</string>
<string name="menu_edit_filters">Modificare filtre</string>
<string name="menu_manage_filter_profiles">Modifica profile filtre</string>
<string name="go_to_chargeprice">Compara preturi</string>
<string name="fault_report">Raport defectiune</string>
<string name="fault_report_date">raport defectiune (ultima actualizare: %s)</string>
<string name="filter_networks">Retele</string>
<string name="filter_operators">Operatori</string>
<string name="filter_chargecards">Metode de plata</string>
<string name="all_selected">Selectate toate</string>
<string name="number_selected">%d selectate</string>
<string name="edit">modifica</string>
<string name="cancel">Anulare</string>
<string name="ok">OK</string>
<string name="pref_language">Limba aplicatie</string>
<string name="pref_darkmode">Mod intunecat</string>
<string name="connection_error">Eroare conexiune</string>
<string name="location_error">Locatia nu a putut fi detectata. Verificati setarile</string>
<string name="retry">Reincearca</string>
<string name="filter_open_247">Disponibile nonstop</string>
<string name="filter_barrierfree">Disponibile fara inregistrare</string>
<string name="filter_exclude_faults">Exclude statiile raportate defecte</string>
<string name="charge_cards">Metode de plata</string>
<string name="and_n_others">si %d altele</string>
<string name="pref_map_provider">Furnizor harta</string>
<string name="twitter">Twitter</string>
<string name="goingelectric_forum">Forum conversatii pe GoingElectric.de</string>
<string name="contact">Contact</string>
<string name="menu_report_new_charger">Statie noua</string>
<string name="edit_at_datasource">modificat la %s</string>
<string name="categories">Categorii</string>
<string name="category_car_dealership">Reprezentanta auto</string>
<string name="category_service_on_motorway">Zona servicii (autostrada)</string>
<string name="category_service_off_motorway">Zona servicii (in afara autostrazii)</string>
<string name="category_railway_station">Statie tren</string>
<string name="category_public_authorities">Autoritati locale</string>
<string name="category_camping">Camping</string>
<string name="category_shopping_mall">Mall</string>
<string name="category_holiday_home">Casa de vacanta</string>
<string name="category_airport">Aerport</string>
<string name="category_amusement_park">Parc de distractii</string>
<string name="category_hotel">Hotel</string>
<string name="category_cinema">Cinema</string>
<string name="category_church">Biserica</string>
<string name="category_hospital">Spital</string>
<string name="category_museum">Museu</string>
<string name="category_parking_multi">Parcare etajata</string>
<string name="category_parking">Parcare</string>
<string name="category_private_charger">Statie de incarcare privata</string>
<string name="category_rest_area">Zona de odihna</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_swimming_pool">Piscina</string>
<string name="category_supermarket">Supermarket</string>
<string name="category_petrol_station">Benzinarie</string>
<string name="category_parking_underground">Parcare subterana</string>
<string name="category_zoo">Gradina Zoo</string>
<string name="category_caravan_site">Camping rulote</string>
<string name="menu_apply">Aplica filtre</string>
<string name="menu_save_profile">Salveaza profil</string>
<string name="menu_reset">Sterge setari filtre</string>
<string name="no_filters">Fara filtre</string>
<string name="filter_custom">Filtre personalizate</string>
<string name="filter_favorites">Favorite</string>
<string name="reorder">reordonare</string>
<string name="delete">Sterge</string>
<string name="save_as_profile">Salveaza ca profil</string>
<string name="save_profile_enter_name">Completati nume profil:</string>
<string name="filterprofiles_empty_state">Nu sunt profile salvate</string>
<string name="welcome_to_evmap">Bine ati venit la EVMap</string>
<string name="welcome_1">Cauta statii de incarcare in apropiere</string>
<string name="welcome_2_title">Esti mereu la curent</string>
<string name="welcome_2">Fiecare culoare a statiei corespunde puterii maxime de incarcare</string>
<string name="welcome_2_detail">Puteti gasi si in sectiunea “Despre” → “Intrebari frecvente”</string>
<string name="donation_dialog_title">Multumim ca utilizati EVMap</string>
<string name="donation_dialog_detail">EVMap este libera si gratuita. Contributiile pe GitHub sunt apreciate. Pentru a acoperi costurile pentru acces la date, va rugam sa donati orice suma pentru dezvoltator.</string>
<string name="chargeprice_donation_dialog_title">Stii sa cauti ofertele cele mai bune!</string>
<string name="chargeprice_donation_dialog_detail">Stii sa folosesti optiunea de comparare preturi. Puteti ajuta pentru a acoperi costurile pentru accesul la aceste date donand pentru EVMap.</string>
<string name="deleted_filterprofile">“%s” a fost sters</string>
<string name="undo">Anuleaza</string>
<string name="rename">Redenumeste</string>
<string name="charging_barrierfree">Utilizabile fara inregistrare</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d metoda de plata compatibila</item>
<item quantity="other">%d metode de plata compatibile</item>
<item quantity="few">%d metode de plata compatibile</item>
</plurals>
<string name="navigate">Navigare</string>
<string name="verified">verificat</string>
<string name="verified_desc">Statia de incarcare a fost confirmata functionala de un mebru din comunitatea %s</string>
<string name="charge_price_format">%2$s%1$.2f</string>
<string name="charge_price_average_format">⌀ %2$s%1$.2f/kWh</string>
<string name="charge_price_kwh_format">%2$s%1$.2f/kWh</string>
<string name="chargeprice_select_connector">Alege conector</string>
<string name="chargeprice_provider_customer_tariff">Doar pentru clienti</string>
<string name="edit_on_goingelectric_info">Va rugam autentificati-va la GoingElectric.de daca pagina e goala</string>
<string name="percent_format">%.0f%%</string>
<string name="chargeprice_session_fee">taxa sesiune</string>
<string name="chargeprice_per_kwh">pe kWh</string>
<string name="chargeprice_per_minute">pe min</string>
<string name="chargeprice_blocking_fee">Taxa blocare &gt;%s</string>
<string name="chargeprice_no_tariffs_found">Fara pret pentru aceasta statie in Chargeprice.app</string>
<string name="powered_by_chargeprice">oferit de Chargeprice</string>
<string name="chargeprice_base_fee">Pret de baza: %2$s%1$.2f/luna</string>
<string name="chargeprice_min_spend">Suma minima: %2$s%1$.2f/luna</string>
<string name="settings_chargeprice">Comparatie preturi</string>
<string name="pref_my_vehicle">Masinile mele</string>
<string name="pref_chargeprice_no_base_fee">Exclude abonamente cu plata lunara</string>
<string name="pref_chargeprice_show_provider_customer_tariffs">Include abonament clienti</string>
<string name="chargeprice_select_car_first">Configurati modelul masinii in setari</string>
<string name="chargeprice_battery_range">Incarcare de la %1$.0f%% la %2$.0f%%</string>
<string name="chargeprice_battery_range_from">Incarcare de la</string>
<string name="chargeprice_battery_range_to">la</string>
<string name="chargeprice_stats">(%1$.0f kWh, aprox. %2$s, ⌀ %3$.0f kW)</string>
<string name="chargeprice_vehicle">Masina</string>
<string name="chargeprice_price_not_available">Pret indisponibil</string>
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Furnizorii de utilitati ofera uneori abonamente speciale pentru clientii lor</string>
<string name="close">Inchidere</string>
<string name="chargeprice_title">Preturi</string>
<string name="chargeprice_connection_error">Preturi nu au putut fi incarcate</string>
<string name="chargeprice_no_compatible_connectors">Nu sunt conectori compatibili la aceasta statie</string>
<string name="pref_chargeprice_currency">Moneda</string>
<string name="pref_my_tariffs">Abonamentele mele</string>
<plurals name="pref_my_tariffs_summary">
<item quantity="one">(va fi evidentiat in comparatia preturilor)</item>
<item quantity="other">(vor fi evidentiate in comparatia preturilor)</item>
<item quantity="few">(vor fi evidentiate in comparatia preturilor)</item>
</plurals>
<string name="chargeprice_all_tariffs_selected">toate abonamentele selectate</string>
<string name="license">Licenta</string>
<string name="settings_charger_data">Statii de incarcare</string>
<string name="pref_data_source">Sursa date</string>
<plurals name="chargeprice_some_tariffs_selected">
<item quantity="one">%d abonament selectat</item>
<item quantity="other">%d abonamente selectate</item>
<item quantity="few">%d abonamente selectate</item>
</plurals>
<string name="unknown_operator">Operator necunoscut</string>
<string name="data_sources_description">Alegeti o sursa pentru statiile de incarcare. Puteti modifica ulterior in setarile aplicatiei.</string>
<string name="data_source_goingelectric">GoingElectric.de</string>
<string name="data_source_openchargemap">Open Charge Map</string>
<string name="data_source_goingelectric_desc">Recomandat in tarile vorbitaore de limba germana. Descrieri in limba germana. Actualizat de comunitate.</string>
<string name="data_source_openchargemap_desc"><![CDATA[International, calitate variata. Descrieri in engleza sau in limba locala. Actualizat de comunitate si de autoritati in unele tari (ex. America de Nord, UK, Franta, Norvegia).]]></string>
<string name="next">urmatorul</string>
<string name="get_started">Incepe</string>
<string name="got_it">Am inteles</string>
<string name="lets_go">Sa incepem</string>
<string name="crash_report_text">Eroare EVMap. Trimiteti raportul de eroare la dezvoltator.</string>
<string name="crash_report_comment_prompt">Puteti adauga un comentariu aici:</string>
<string name="powered_by_mapbox">furnizat de Mapbox</string>
<string name="pref_search_provider">Furnizor cautare</string>
<string name="pref_search_provider_info"><![CDATA[Datele de cautare sunt costisitoare, in special de la Google Maps. Va rugam sa luati in considerare o donatie in sectiunea “Despre” → “Doneaza”.]]></string>
<string name="github_sponsors">Sponsori GitHub</string>
<string name="donate_desc">Sprijina dezvoltarea EVMap\'s cu o donatie</string>
<string name="github_sponsors_desc">Sprijina EVMap pe GitHub</string>
<string name="unnamed_filter_profile">Profile filtre fara nume</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>
<string name="required">obligatoriu</string>
<string name="edit_filter_profile">Modifica “%s”</string>
<string name="pref_search_delete_recent">Sterge rezultate cautare recenta</string>
<string name="deleted_recent_search_results">Rezultate cautare recenta au fost sterse</string>
<string name="settings_data_sources">Surse date</string>
<string name="help">Ajutor</string>
<string name="settings_android_auto">Android Auto</string>
<string name="pref_chargeprice_allow_unbalanced_load">Include incarcare nebalansata</string>
<string name="pref_chargeprice_allow_unbalanced_load_summary">Include incarcare monofazata AC cu mai mult de 4.5 kW</string>
<string name="pref_map_rotate_gestures_enabled">Rotire harta</string>
<string name="pref_map_rotate_gestures_on">Foloseste doua degete pentru a roti harta</string>
<string name="pref_map_rotate_gestures_off">Rotire dezactivata (nordul mereu sus)</string>
<string name="refresh_live_data">actualizare stare in timp real</string>
<string name="autocomplete_connection_error">Sugestiile nu au putut fi incarcate</string>
<string name="pref_language_device_default">Implicit dispozitiv</string>
<string name="pref_darkmode_device_default">Implicit dispozitiv</string>
<string name="pref_darkmode_always_on">permanent</string>
<string name="pref_darkmode_always_off">dezactivat</string>
<string name="pref_chargeprice_currency_chf">Franci elvetieni (CHF)</string>
<string name="pref_chargeprice_currency_czk">Coroane cehe (CZK)</string>
<string name="pref_chargeprice_currency_dkk">Coroane daneze (DKK)</string>
<string name="pref_chargeprice_currency_eur">Euro (EUR)</string>
<string name="pref_chargeprice_currency_gbp">Lire sterline (GBP)</string>
<string name="pref_chargeprice_currency_hrk">Croatian kuna (HRK)</string>
<string name="pref_chargeprice_currency_huf">Forinti maghiari (HUF)</string>
<string name="pref_chargeprice_currency_isk">Coroane islandeze (ISK)</string>
<string name="pref_chargeprice_currency_nok">Coroane norvegiene (NOK)</string>
<string name="pref_chargeprice_currency_pln">Zloti polonezi (PLN)</string>
<string name="pref_chargeprice_currency_sek">Coroane suedeze (SEK)</string>
<string name="pref_chargeprice_currency_usd">Dolari americani (USD)</string>
<string name="pref_provider_google_maps">Google Maps</string>
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
<string name="about_contributors">Contribuitori</string>
<string name="about_contributors_text">Multumiri contribuitorilor pentru cod si traduceri pentru EVMap:</string>
<string name="utilization_prediction">Estimare utilizare</string>
<string name="prediction_help">Estimarea se bazeaza pe diversi factori cum ar fi ziua saptamanii, ora, utilizarile anterioare, astfel incat sa se evite supra aglomerarea. Fara garantie.</string>
<string name="prediction_time_colon">%s:</string>
<plurals name="prediction_number_available">
<item quantity="one">%1$d/%2$d disponibil</item>
<item quantity="other">%1$d/%2$d disponibile</item>
<item quantity="few">%1$d/%2$d disponibile</item>
</plurals>
<string name="pref_prediction_enabled">Arata estimare utilizare</string>
<string name="pref_prediction_enabled_summary">pentru statiile de incarcare suportate\n(momentan doar DC in Germania)</string>
<string name="prediction_only">(doar %s)</string>
<string name="prediction_dc_plugs_only">prize DC</string>
<string name="data_source_switched_to">Sursa date schimbat la %s</string>
<string name="pref_applink_associate">Linkuri suportate</string>
<string name="pref_applink_associate_summary">de la goingelectric.de si openchargemap.org</string>
<string name="chargeprice_header_my_tariffs">Abonamentele mele</string>
<string name="chargeprice_header_other_tariffs">Alte abonamente</string>
<string name="developer_mode_enabled">Activat mod dezvoltator</string>
<string name="developer_options">Optiuni dezvoltator</string>
<string name="disable_developer_mode">Dezactivare mod dezvoltator</string>
<string name="developer_mode_disabled">Mod dezvoltator dezactivat</string>
<string name="gps">GPS</string>
<string name="compass">Busola</string>
</resources>

View File

@@ -6,6 +6,9 @@
<item>@string/pref_language_de</item>
<item>@string/pref_language_fr</item>
<item>@string/pref_language_nb_rNO</item>
<item>@string/pref_language_nl</item>
<item>@string/pref_language_pt</item>
<item>@string/pref_language_ro</item>
</string-array>
<string-array name="pref_language_values" translatable="false">
<item>default</item>
@@ -13,6 +16,9 @@
<item>de</item>
<item>fr</item>
<item>nb-NO</item>
<item>nl</item>
<item>pt</item>
<item>ro</item>
</string-array>
<string-array name="pref_darkmode_names">
<item>@string/pref_darkmode_device_default</item>

View File

@@ -15,6 +15,7 @@
<color name="charger_low">#607d8b</color>
<color name="available">#4caf50</color>
<color name="charging">#00bcd4</color>
<color name="some_available">#ffc107</color>
<color name="unavailable">#f44336</color>
<color name="unknown">#9e9e9e</color>
<color name="status_bar_scrim">#C3000000</color>

View File

@@ -2,7 +2,7 @@
<resources>
<string name="shared_element_picture">picture</string>
<string name="shared_element_chargeprice">chargeprice</string>
<string name="github_link">https://github.com/johan12345/EVMap</string>
<string name="github_link">https://github.com/ev-map/EVMap</string>
<string name="twitter_handle">\@ev_map</string>
<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>
@@ -13,15 +13,22 @@
<string name="pref_language_de">Deutsch</string>
<string name="pref_language_fr">Français</string>
<string name="pref_language_nb_rNO">Norsk Bokmål</string>
<string name="pref_language_nl">Nederlands</string>
<string name="pref_language_pt">Português</string>
<string name="pref_language_ro">Romana</string>
<string name="about_contributors_list">
Danilo Bargen\n
Altonss\n
Allan Nordhøy\n
Maximilian Goldschmidt\n
Wim Lamotte\n
Licaon_Kter\n
Celso Azevedo\n
pt2121\n
nautilusx
nautilusx\n
Bobby Galati
</string>
<string name="hide_on_scroll_fab_behavior">net.vonforst.evmap.ui.HideOnScrollFabBehavior</string>
<string name="paypal_link" translatable="false">https://paypal.me/johan98</string>
<string name="copyright_summary">©20202023 Johan von Forstner and contributors</string>
</resources>

View File

@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">EVMap</string>
<string name="title_activity_maps">EVMap</string>
@@ -25,6 +26,7 @@
<string name="amenities">Amenities</string>
<string name="general_info">General info</string>
<string name="realtime_data_unavailable">Real-time status unavailable</string>
<string name="realtime_data_login_needed">Tesla account needed for real-time data</string>
<string name="realtime_data_loading">Checking real-time status…</string>
<string name="realtime_data_source">Real-time status source (beta): %s</string>
<string name="source">Source: %s</string>
@@ -41,7 +43,6 @@
<string name="settings_ui">Interface</string>
<string name="settings_map">Map</string>
<string name="copyright">Copyright</string>
<string name="copyright_summary">©20202022 Johan von Forstner</string>
<string name="other">Other</string>
<string name="privacy">Privacy</string>
<string name="fav_add">Save as favorite</string>
@@ -157,7 +158,7 @@
<string name="welcome_2">Each charger\'s color corresponds to its max charging power</string>
<string name="welcome_2_detail">This can also be seen in “About” → “Frequently Asked Questions”</string>
<string name="donation_dialog_title">Thank you for using EVMap</string>
<string name="donation_dialog_detail">EVMap is libre and free of charge. Code contributions on GitHub are much appreciated. To help cover the running costs for data access, please consider donating an amount of your choice to the developer.</string>
<string name="donation_dialog_detail">EVMap is open source and free of charge. Code contributions on GitHub are much appreciated. To help cover the running costs for data access, please consider donating an amount of your choice to the developer.</string>
<string name="chargeprice_donation_dialog_title">You\'re a real bargain hunter!</string>
<string name="chargeprice_donation_dialog_detail">You make great use of the price comparison feature. Please help cover the costs for this data by supporting EVMap with a donation.</string>
<string name="deleted_filterprofile">Deleted “%s”</string>
@@ -174,6 +175,7 @@
<string name="charge_price_format">%2$s%1$.2f</string>
<string name="charge_price_average_format">⌀ %2$s%1$.2f/kWh</string>
<string name="charge_price_kwh_format">%2$s%1$.2f/kWh</string>
<string name="charge_price_minute_format">%2$s%1$.2f/min</string>
<string name="chargeprice_select_connector">Choose connector</string>
<string name="chargeprice_provider_customer_tariff">Only for tie-in customers</string>
<string name="edit_on_goingelectric_info">Please log in at GoingElectric.de if this page is empty</string>
@@ -181,7 +183,7 @@
<string name="chargeprice_session_fee">session fee</string>
<string name="chargeprice_per_kwh">per kWh</string>
<string name="chargeprice_per_minute">per min</string>
<string name="chargeprice_blocking_fee">Blocking fee >%s</string>
<string name="chargeprice_blocking_fee">Blocking fee &gt;%s</string>
<string name="chargeprice_no_tariffs_found">No charging plans for this charger on Chargeprice.app</string>
<string name="powered_by_chargeprice">powered by Chargeprice</string>
<string name="chargeprice_base_fee">Base fee: %2$s%1$.2f/month</string>
@@ -294,4 +296,21 @@
<string name="developer_mode_disabled">Developer mode disabled</string>
<string name="gps">GPS</string>
<string name="compass">Compass</string>
</resources>
<string name="charger_website">Website</string>
<string name="location_status">Location provider status</string>
<string name="pref_tesla_account">Tesla account</string>
<string name="pref_tesla_account_enabled">Logged in as %s</string>
<string name="pref_tesla_account_disabled">Log in to see real-time data for Tesla Superchargers. No Tesla vehicle necessary</string>
<string name="logging_in">Logging in…</string>
<string name="log_out">Log out</string>
<string name="logged_out">Logged out</string>
<string name="login">Login</string>
<string name="login_error">Login failed</string>
<string name="tesla_pricing_owners">Tesla vehicles only:</string>
<string name="tesla_pricing_members">Tesla vehicles &amp; members:</string>
<string name="tesla_pricing_others">Other customers:</string>
<string name="pricing_up_to">up to %s</string>
<string name="tesla_pricing_other_times">Other times:</string>
<string name="tesla_pricing_blocking_fee">Blocking fee: %s</string>
<string name="average_utilization">Average Utilization</string>
</resources>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<include
domain="sharedpref"
path="." />
<exclude
domain="sharedpref"
path="encrypted_prefs.xml" />
<include
domain="database"
path="evmap.db" />
</full-backup-content>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<include
domain="sharedpref"
path="." />
<exclude
domain="sharedpref"
path="encrypted_prefs.xml" />
<include
domain="database"
path="evmap.db" />
</cloud-backup>
<device-transfer>
<include
domain="sharedpref"
path="." />
<exclude
domain="sharedpref"
path="encrypted_prefs.xml" />
<include
domain="database"
path="evmap.db" />
</device-transfer>
</data-extraction-rules>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
android:fragment="net.vonforst.evmap.fragment.preference.UiSettingsFragment"
android:title="@string/settings_ui"
@@ -12,4 +13,9 @@
android:fragment="net.vonforst.evmap.fragment.preference.ChargepriceSettingsFragment"
android:title="@string/settings_chargeprice"
android:icon="@drawable/ic_chargeprice" />
<Preference
android:key="developer_options"
android:fragment="net.vonforst.evmap.fragment.preference.DeveloperSettingsFragment"
android:title="@string/developer_options"
android:icon="@drawable/ic_developer" />
</PreferenceScreen>

View File

@@ -15,6 +15,10 @@
android:title="@string/pref_prediction_enabled"
android:defaultValue="true"
android:summary="@string/pref_prediction_enabled_summary" />
<Preference
android:key="tesla_account"
android:title="@string/pref_tesla_account" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_map">

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