Compare commits

...

57 Commits
1.0.0 ... 1.1.2

Author SHA1 Message Date
johan12345
4f6f09dc83 Release 1.1.2 2021-11-14 18:17:39 +01:00
johan12345
7f6d0c1391 update dependencies 2021-11-14 17:59:41 +01:00
johan12345
96b60d0f49 GoingElectric API: do not show "paid parking" / "paid charging"
because that may also mean that no information is available
see #13
2021-11-14 17:33:36 +01:00
johan12345
2824f0b5c3 handle saved state for MapViewModel 2021-11-14 17:19:11 +01:00
johan12345
af0921ed20 implement a93bacd9b3 for Android Auto 2021-11-14 16:58:44 +01:00
johan12345
a5b55479cb Detail view: show opening hours description also if open 24/7 2021-11-14 16:40:06 +01:00
johan12345
a93bacd9b3 Chargeprice: show provider-exclusive plans when included in "my plans"
fixes #147
2021-11-14 16:26:43 +01:00
johan12345
9d7278e0e2 AvailabilityDetector: support missing household plugs in NewMotion data
fixes #146
2021-11-14 15:58:08 +01:00
johan12345
f6d9c615a0 AvailabilityDetectorTest: use proper socket type constants 2021-11-14 15:40:13 +01:00
johan12345
a8ee3f5b7d Change semantics of opening hours in model
to fix incompatibility with Room that caused a NullPointerException
2021-11-14 15:27:49 +01:00
johan12345
826b4f89f1 fix crash in light mode
introduced in 5d7d881729
2021-11-06 22:51:16 +01:00
johan12345
5675d065e3 update Car App Library to 1.1.0-rc01 2021-11-06 22:34:42 +01:00
johan12345
3e3531551d add link to Chargeprice FAQ page 2021-11-06 22:30:31 +01:00
johan12345
5d7d881729 Chargeprice: add branding 2021-11-06 21:29:17 +01:00
johan12345
23c73e3d7e Android Auto: add error message when Chargeprice data fails to load 2021-11-06 20:02:02 +01:00
johan12345
7835aa8d78 initialize Google map with correct locale 2021-11-04 21:47:33 +01:00
johan12345
f06b712090 disable NullSafeMutableLiveData lint error
seems to give false positives
2021-11-01 19:22:34 +01:00
johan12345
317695954d remove unneeded maven repositories 2021-11-01 15:27:56 +01:00
johan12345
24cfd1c10b upgrade dependencies 2021-11-01 15:16:21 +01:00
johan12345
775faa2f55 update AboutLibraries 2021-11-01 15:00:45 +01:00
johan12345
08bd2bdf5a update build tools 2021-11-01 15:00:30 +01:00
johan12345
90254915e3 Release 1.1.1 2021-11-01 13:10:37 +01:00
johan12345
b7f56ecff4 fix detection of imperial units when locale is "unspecified English" 2021-11-01 13:03:50 +01:00
johan12345
fa3910d3c8 avoid NPE when country == null 2021-11-01 12:47:05 +01:00
johan12345
4500c55560 Android Auto: throttle updates of distances
to mitigate bug in AA https://issuetracker.google.com/issues/204692002
2021-11-01 12:39:59 +01:00
johan12345
a493e1a548 Android Auto MapScreen: show distances in correct units 2021-11-01 12:03:27 +01:00
johan12345
ddaab42e45 update filteredConnectors before chargepoints
fixes incorrect marker colors
2021-11-01 10:37:46 +01:00
johan12345
9f50341ab7 use latest Google Maps renderer 2021-11-01 10:26:30 +01:00
johan12345
9966b44a76 Release 1.1.0 2021-10-17 21:29:53 +02:00
johan12345
d44b2206d2 update build status badge 2021-10-17 15:49:56 +02:00
johan12345
f61082f491 fix incorrect formatting in api_keys.md 2021-10-17 15:27:01 +02:00
johan12345
f58d96c939 fix links in README 2021-10-17 15:26:08 +02:00
Johan von Forstner
29aedfa3d9 Merge pull request #140 from johan12345/contributing-docs
Improve docs for first-time contributors
2021-10-17 15:25:15 +02:00
johan12345
8331f92f10 Improve docs for first-time contributors
adds two docs pages with info on API keys and Android Auto testing.
fixes #112
2021-10-17 15:24:50 +02:00
johan12345
123680d3e8 replace Mapbox android-plugin-places with just the Java SDK
(the UI has been replaced by our own one in #120)
2021-10-17 12:43:16 +02:00
johan12345
0f6b45d745 upgrade navigation component to 2.4.0-alpha10
(necessary for Android 12 compatibility of deep links)
2021-10-17 12:11:28 +02:00
Johan von Forstner
69faa94f18 Merge pull request #139 from pt2121/pt/safeArgs
refactor to use Android Navigation Component's SafeArgs
2021-10-17 12:10:16 +02:00
prat t
70805b7960 refactor to use Navigation Component's SafeArgs 2021-10-16 15:16:56 -07:00
johan12345
56453b0658 remove obsolete TODO 2021-10-16 15:51:36 +02:00
johan12345
975d95e37e refactor app settings into separate sub-screens
for better overview
2021-10-16 15:49:00 +02:00
johan12345
ba34cd016a fix static splashscreen icon size
after upgrade to core-splashscreen 1.0.0-alpha02 in e2bcf8d1
2021-10-16 12:36:29 +02:00
johan12345
590b16aa49 make debug build distinguishable from release build
with grayscale app icon + different label
fixes #113
2021-10-16 12:25:26 +02:00
johan12345
5fe8d0cab4 replace Google Maps v3 beta with Play Services version 17.0.1
fixes #124
fixes #30
2021-10-16 12:16:27 +02:00
johan12345
9d7b181410 remove focus from search when selecting a charger 2021-10-10 17:55:16 +02:00
johan12345
128532aac6 open autocomplete list more quickly 2021-10-10 17:51:54 +02:00
johan12345
486854f56c Android Auto: use slightly darker color for >100 kW chargers for better contrast 2021-10-09 19:16:21 +02:00
johan12345
1e30db5cd1 GoingElectricApi: map plug types correctly to names 2021-10-09 13:55:25 +02:00
johan12345
aad386ab04 MultiSelectDialog: put common choices on top even when selected 2021-10-09 13:44:17 +02:00
johan12345
e2bcf8d1cd upgrade dependencies 2021-10-08 22:02:04 +02:00
Johan von Forstner
f56fad1282 Merge pull request #134 from johan12345/filter-by-favorites
add possibility to show only the favorites on a map
2021-10-08 21:51:46 +02:00
johan12345
adb4d938cc add possibility to show only the favorites on a map (fixes #119) 2021-10-08 21:51:13 +02:00
Johan von Forstner
b773f65912 Update screenshot URLs 2021-10-06 08:39:13 +02:00
johan12345
de335b18d8 PlaceAutocompleteAdapter: do not modify resultList on background thread 2021-10-05 21:43:47 +02:00
johan12345
6c8380b8ce revisit Android Auto location service connection
to possibly fix IllegalArgumentException: Service not registered
based on https://stackoverflow.com/questions/22079909/android-java-lang-illegalargumentexception-service-not-registered
2021-10-05 21:12:03 +02:00
Johan von Forstner
81afdca19d Merge pull request #130 from johan12345/highlight_favorites
highlight favorites on map
2021-10-03 13:33:12 +02:00
johan12345
14e03ba6dd highlight favorites on map
fixes #118
2021-10-03 13:31:58 +02:00
Johan von Forstner
abe12b45c3 disable git LFS for screenshots
(not supported on F-Droid
2021-10-03 12:55:11 +02:00
116 changed files with 1593 additions and 590 deletions

View File

@@ -1,4 +1,4 @@
EVMap [![Build Status](https://travis-ci.org/johan12345/EVMap.svg?branch=master)](https://travis-ci.org/johan12345/EVMap)
EVMap [![Build Status](https://app.travis-ci.com/johan12345/EVMap.svg?branch=master)](https://app.travis-ci.com/johan12345/EVMap)
=====
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/>
@@ -28,38 +28,22 @@ Features
Screenshots
-----------
<img src="https://media.githubusercontent.com/media/johan12345/EVMap/master/_img/screenshots/phone/en/mapbox/01_map.png" width=250 alt="Screenshot 1"/><img src="https://media.githubusercontent.com/media/johan12345/EVMap/master/_img/screenshots/phone/en/mapbox/02_detail.png" width=250 alt="Screenshot 2"/>
<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"/>
Development setup
-----------------
The App is developed using Android Studio.
The App is developed using Android Studio and should pretty much work out-of-the-box when you clone
the Git repository and open the project with Android Studio.
For testing the app, you need to obtain free API Keys for the
[GoingElectric API](https://www.goingelectric.de/stromtankstellen/api/),
the [Chargeprice API](https://github.com/chargeprice/chargeprice-api-docs),
the [OpenChargeMap API](https://openchargemap.org/site/profile/appedit),
as well as for [Google APIs](https://console.developers.google.com/)
("Maps SDK for Android" and "Places API" need to be activated) and/or [Mapbox](https://www.mapbox.com/). These API keys need to be put into the
app in the form of a resource file called `apikeys.xml` under `app/src/main/res/values`, with the
following content:
The only exception is that you need to obtain some free API keys for the different data sources that
EVMap uses and put them into the app in the form of a resource file called `apikeys.xml` under
`app/src/main/res/values`. You can find more information on which API keys are necessary for which
features and how they can be obtained in our [documentation page](doc/api_keys.md).
```xml
<resources>
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">
insert your Google Maps key here
</string>
<string name="mapbox_key" translatable="false">
insert your Mapbox key here
</string>
<string name="goingelectric_key" translatable="false">
insert your GoingElectric key here
</string>
<string name="chargeprice_key" translatable="false">
insert your Chargeprice key here
</string>
<string name="openchargemap_key" translatable="false">
insert your OpenChargeMap key here
</string>
</resources>
```
There are two different build flavors, `google` and `foss`, where only the `google` variant uses
Google Maps data and provides the Android Auto integration. The `foss` variant only uses Mapbox data
and should run on devices without Google Play Services.
We also have a special [documentation page](doc/android_auto.md) on how to test the Android Auto
app.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 194 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 94 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 88 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 45 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 242 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 90 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 88 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 42 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 B

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 972 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 352 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 81 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 113 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 875 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 837 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 341 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 94 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 134 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 B

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 972 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 343 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 83 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 106 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 864 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 837 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 330 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 95 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 31
versionCode 63
versionName "1.0.0"
versionCode 66
versionName "1.1.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -103,22 +103,25 @@ android {
variant.resValue "string", "chargeprice_key", chargepriceKey
}
}
lintOptions {
disable 'NullSafeMutableLiveData'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.core:core-splashscreen:1.0.0-alpha01'
implementation "androidx.activity:activity-ktx:1.3.1"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.core:core-splashscreen:1.0.0-alpha02'
implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.3.6"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.browser:browser:1.3.0'
implementation 'androidx.browser:browser:1.4.0'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
@@ -140,43 +143,29 @@ dependencies {
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
// Android Auto
googleImplementation 'androidx.car.app:app:1.1.0-beta01'
googleImplementation 'androidx.car.app:app-projected:1.1.0-beta01'
googleImplementation 'androidx.car.app:app:1.1.0-rc01'
googleImplementation 'androidx.car.app:app-projected:1.1.0-rc01'
// AnyMaps
def anyMapsVersion = '95ddd6c083'
def anyMapsVersion = '751daec281'
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
googleImplementation 'com.google.android.gms:play-services-maps:18.0.0'
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"
// Google Maps v3 Beta
googleImplementation 'com.google.android.libraries.maps:maps:3.1.0-beta'
googleImplementation name:'places-maps-sdk-3.1.0-beta', ext:'aar'
googleImplementation 'com.android.volley:volley:1.2.0'
googleImplementation 'com.google.android.gms:play-services-base:17.5.0'
googleImplementation 'com.google.android.gms:play-services-basement:17.5.0'
googleImplementation 'com.google.android.gms:play-services-gcm:17.0.0'
googleImplementation 'com.google.android.gms:play-services-location:17.1.0'
googleImplementation 'com.google.android.gms:play-services-tasks:17.2.0'
googleImplementation 'com.google.auto.value:auto-value-annotations:1.6.3'
googleImplementation 'com.google.code.gson:gson:2.8.6'
googleImplementation 'com.google.android.datatransport:transport-runtime:2.2.5'
googleImplementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
// Google Places
implementation 'com.google.android.libraries.places:places:2.5.0'
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.4.1'
// Mapbox places (autocomplete)
// forked this library and included through JitPack to fix https://github.com/mapbox/mapbox-plugins-android/issues/1011
implementation('com.github.johan12345.mapbox-plugins-android:mapbox-android-plugin-places-v9:922bf877f6') {
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-accounts'
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-telemetry'
}
// Mapbox Geocoding
implementation 'com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0'
// navigation library
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// viewmodel library
def lifecycle_version = "2.3.1"
def lifecycle_version = "2.4.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
@@ -200,7 +189,7 @@ dependencies {
// debug tools
implementation 'com.facebook.stetho:stetho:1.5.1'
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
testImplementation 'junit:junit:4.13.1'
testImplementation 'junit:junit:4.13.2'
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
//noinspection GradleDependency
testImplementation 'org.json:json:20080701'

View File

Binary file not shown.

View File

@@ -0,0 +1,107 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="198.3471"
android:viewportHeight="198.3471">
<group
android:translateX="3.1735537"
android:translateY="3.1735537">
<group>
<clip-path android:pathData="M122.5,57.1c-5.8,-7.7 -15.2,-12.4 -24.7,-12.9H94c-8.1,0.5 -15.9,3.9 -21.7,9.6c-4.3,4.3 -7.4,9.7 -8.6,15.7c-1.5,7.5 -0.1,15.4 3.5,22.1c2.3,4.5 5.2,8.6 8.1,12.8c4.9,6.8 9.8,13.7 13.2,21.3c3.1,6.5 4.9,13.6 5.6,20.8c0.4,0.6 1.1,1 1.7,1.5c2.6,-1 2,-4 2.5,-6.2c1.9,-12.8 8.7,-24.2 16.1,-34.6c6.6,-9.2 14.1,-19 14.4,-30.8V74C128.5,67.9 126.3,61.9 122.5,57.1zM106.2,74.3l-12.2,21V79.5h-5.2V60.3h17.5l-7,14H106.2z" />
<path
android:pathData="M106.2,74.3h-7"
android:strokeAlpha="0.2"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:fillAlpha="0.2" />
</group>
<group>
<clip-path android:pathData="M122.5,57.1c-5.8,-7.7 -15.2,-12.4 -24.7,-12.9H94c-8.1,0.5 -15.9,3.9 -21.7,9.6c-4.3,4.3 -7.4,9.7 -8.6,15.7c-1.5,7.5 -0.1,15.4 3.5,22.1c2.3,4.5 5.2,8.6 8.1,12.8c4.9,6.8 9.8,13.7 13.2,21.3c3.1,6.5 4.9,13.6 5.6,20.8c0.4,0.6 1.1,1 1.7,1.5c2.6,-1 2,-4 2.5,-6.2c1.9,-12.8 8.7,-24.2 16.1,-34.6c6.6,-9.2 14.1,-19 14.4,-30.8V74C128.5,67.9 126.3,61.9 122.5,57.1zM106.2,74.3l-12.2,21V79.5h-5.2V60.3h17.5l-7,14H106.2z" />
<path
android:pathData="M106.2,60.3c0,0 -17.5,0 -17.5,0"
android:strokeAlpha="0.2"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:fillAlpha="0.2" />
</group>
<group>
<clip-path android:pathData="M122.5,57.1c-5.8,-7.7 -15.2,-12.4 -24.7,-12.9H94c-8.1,0.5 -15.9,3.9 -21.7,9.6c-4.3,4.3 -7.4,9.7 -8.6,15.7c-1.5,7.5 -0.1,15.4 3.5,22.1c2.3,4.5 5.2,8.6 8.1,12.8c4.9,6.8 9.8,13.7 13.2,21.3c3.1,6.5 4.9,13.6 5.6,20.8c0.4,0.6 1.1,1 1.7,1.5c2.6,-1 2,-4 2.5,-6.2c1.9,-12.8 8.7,-24.2 16.1,-34.6c6.6,-9.2 14.1,-19 14.4,-30.8V74C128.5,67.9 126.3,61.9 122.5,57.1zM106.2,74.3l-12.2,21V79.5h-5.2V60.3h17.5l-7,14H106.2z" />
<path
android:pathData="M93.9,79.5L88.7,79.5"
android:strokeAlpha="0.2"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:fillAlpha="0.2" />
</group>
<group>
<clip-path android:pathData="M122.5,57.1c-5.8,-7.7 -15.2,-12.4 -24.7,-12.9H94c-8.1,0.5 -15.9,3.9 -21.7,9.6c-4.3,4.3 -7.4,9.7 -8.6,15.7c-1.5,7.5 -0.1,15.4 3.5,22.1c2.3,4.5 5.2,8.6 8.1,12.8c4.9,6.8 9.8,13.7 13.2,21.3c3.1,6.5 4.9,13.6 5.6,20.8c0.4,0.6 1.1,1 1.7,1.5c2.6,-1 2,-4 2.5,-6.2c1.9,-12.8 8.7,-24.2 16.1,-34.6c6.6,-9.2 14.1,-19 14.4,-30.8V74C128.5,67.9 126.3,61.9 122.5,57.1zM106.2,74.3l-12.2,21V79.5h-5.2V60.3h17.5l-7,14H106.2z" />
<path
android:strokeWidth="1"
android:pathData="M94,79v16.2"
android:strokeAlpha="0.2"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:strokeColor="#212121"
android:fillAlpha="0.2"
android:strokeLineCap="round" />
</group>
<group>
<clip-path android:pathData="M122.5,57.1c-5.8,-7.7 -15.2,-12.4 -24.7,-12.9H94c-8.1,0.5 -15.9,3.9 -21.7,9.6c-4.3,4.3 -7.4,9.7 -8.6,15.7c-1.5,7.5 -0.1,15.4 3.5,22.1c2.3,4.5 5.2,8.6 8.1,12.8c4.9,6.8 9.8,13.7 13.2,21.3c3.1,6.5 4.9,13.6 5.6,20.8c0.4,0.6 1.1,1 1.7,1.5c2.6,-1 2,-4 2.5,-6.2c1.9,-12.8 8.7,-24.2 16.1,-34.6c6.6,-9.2 14.1,-19 14.4,-30.8V74C128.5,67.9 126.3,61.9 122.5,57.1zM106.2,74.3l-12.2,21V79.5h-5.2V60.3h17.5l-7,14H106.2z" />
<path
android:strokeWidth="1"
android:pathData="M106.2,60.3L99.2,74.3"
android:strokeAlpha="0.2"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:fillAlpha="0.2"
android:strokeLineCap="round" />
</group>
<group>
<clip-path android:pathData="M122.5,57.1c-5.8,-7.7 -15.2,-12.4 -24.7,-12.9H94c-8.1,0.5 -15.9,3.9 -21.7,9.6c-4.3,4.3 -7.4,9.7 -8.6,15.7c-1.5,7.5 -0.1,15.4 3.5,22.1c2.3,4.5 5.2,8.6 8.1,12.8c4.9,6.8 9.8,13.7 13.2,21.3c3.1,6.5 4.9,13.6 5.6,20.8c0.4,0.6 1.1,1 1.7,1.5c2.6,-1 2,-4 2.5,-6.2c1.9,-12.8 8.7,-24.2 16.1,-34.6c6.6,-9.2 14.1,-19 14.4,-30.8V74C128.5,67.9 126.3,61.9 122.5,57.1zM106.2,74.3l-12.2,21V79.5h-5.2V60.3h17.5l-7,14H106.2z" />
<path
android:strokeWidth="1"
android:pathData="M106.2,74.3L93.9,95.2"
android:strokeAlpha="0.2"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:fillAlpha="0.2"
android:strokeLineCap="round" />
</group>
<path
android:pathData="M67.6,120.6L65.7,104l-2.9,0.3l1.9,16.6L67.6,120.6zM77.9,119.4l-1.9,-16.6l-2.9,0.3l1.9,16.6L77.9,119.4z"
android:fillColor="#808080" />
<path
android:pathData="M83.3,142c-0.9,1.1 -1.6,1.8 -1.7,1.9c-2.6,2.1 -4.7,2.7 -6.4,1.9c-3,-1.5 -2.8,-7.1 -2.7,-7.7l2.1,0.1c-0.1,1.6 0.2,5 1.6,5.7c0.8,0.4 2.2,-0.1 4,-1.6l0,0c0,0 5.8,-5.8 4.6,-10.4c-1.4,-5.5 5,-13.4 7.1,-16.1l0.3,-0.3l1.7,1.3l-0.3,0.4c-6.5,8 -7.2,12.1 -6.7,14.2C87.9,135.4 85.2,139.7 83.3,142z"
android:fillColor="#9e9e9e" />
<path
android:pathData="M61.2,120.4l0.8,6.8l6.3,4.2l8.5,-0.9l5.2,-5.5l-0.8,-6.8L61.2,120.4z"
android:fillColor="#9e9e9e" />
<path
android:pathData="M76.7,130.5l-8.5,0.9l1.8,7.5l6.7,-0.8L76.7,130.5L76.7,130.5zM82.8,112.5l0.7,6.2l-24.4,2.8l-0.7,-6.2L82.8,112.5z"
android:fillColor="#666666" />
<path
android:pathData="M101.9,44.1c-17.5,0 -31.7,14.2 -31.7,31.7c0,23.9 26.7,36.4 29.9,70.5c0.1,1 0.9,1.7 1.9,1.7s1.8,-0.7 1.9,-1.7c3.2,-34.1 29.9,-46.6 29.9,-70.5C133.6,58.2 119.4,44.1 101.9,44.1z"
android:fillColor="#737373" />
<path
android:pathData="M101.9,44.8c17.4,0 31.5,14 31.7,31.3c0,-0.1 0,-0.2 0,-0.3c0,-17.5 -14.2,-31.7 -31.7,-31.7S70.2,58.2 70.2,75.8c0,0.1 0,0.2 0,0.3C70.4,58.8 84.5,44.8 101.9,44.8L101.9,44.8z"
android:fillColor="#FFFFFF"
android:fillAlpha="0.2" />
<path
android:pathData="M103.8,145.5c-0.1,1 -0.9,1.7 -1.9,1.7s-1.8,-0.7 -1.9,-1.7c-3.1,-34 -29.6,-46.5 -29.8,-70.1c0,0.2 0,0.3 0,0.5c0,23.9 26.7,36.4 29.9,70.5c0.1,1 0.9,1.7 1.9,1.7s1.8,-0.7 1.9,-1.7c3.2,-34.1 29.9,-46.6 29.9,-70.5c0,-0.2 0,-0.3 0,-0.5C133.4,99 106.9,111.5 103.8,145.5L103.8,145.5z"
android:fillColor="#303030"
android:fillAlpha="0.2" />
<path
android:fillColor="#FF000000"
android:pathData="M94.6,60.3v19.2h5.2v15.7l12.2,-21h-7l7,-14C112.1,60.3 94.6,60.3 94.6,60.3z"
android:strokeAlpha="0.45"
android:fillAlpha="0.45" />
</group>
</vector>

View File

@@ -5,10 +5,18 @@ import android.content.Context
import android.util.Log
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.maps.MapsInitializer
import com.google.android.libraries.places.api.Places
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.utils.LocaleContextWrapper
fun init(context: Context) {
Places.initialize(context, context.getString(R.string.google_maps_key));
Places.initialize(context, context.getString(R.string.google_maps_key))
val localeContext = LocaleContextWrapper.wrap(
context.applicationContext, PreferenceDataSource(context).language
)
MapsInitializer.initialize(localeContext, MapsInitializer.Renderer.LATEST, null)
}
fun checkPlayServices(activity: Activity): Boolean {

View File

@@ -62,6 +62,7 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
locationService = null
}
}
private var serviceBound = false
init {
lifecycle.addObserver(this)
@@ -91,13 +92,9 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
private fun onCarHardwareLocationReceived(loc: CarHardwareLocation) {
updateLocation(loc.location.value)
locationService?.let { service ->
// we successfully received a location from the car hardware,
// so we don't need the smartphone location anymore.
service.removeLocationUpdates()
cas.unbindService(serviceConnection)
locationService = null
}
// we successfully received a location from the car hardware,
// so we don't need the smartphone location anymore.
unbindLocationService()
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
@@ -111,7 +108,7 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
::onCarHardwareLocationReceived
)
}
cas.bindService(
serviceBound = cas.bindService(
Intent(cas, CarLocationService::class.java),
serviceConnection,
Context.BIND_AUTO_CREATE
@@ -119,13 +116,18 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
private fun unbindLocationService() {
private fun onStop() {
if (supportsCarApiLevel3(carContext)) {
hardwareMan.carSensors.removeCarHardwareLocationListener(::onCarHardwareLocationReceived)
}
locationService?.let { service ->
service.removeLocationUpdates()
unbindLocationService()
}
private fun unbindLocationService() {
locationService?.removeLocationUpdates()
if (serviceBound) {
cas.unbindService(serviceConnection)
serviceBound = false
}
}

View File

@@ -14,14 +14,20 @@ import androidx.car.app.model.*
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moe.banana.jsonapi2.HasMany
import moe.banana.jsonapi2.HasOne
import moe.banana.jsonapi2.JsonBuffer
import moe.banana.jsonapi2.ResourceIdentifier
import net.vonforst.evmap.*
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.currency
import java.io.IOException
class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(ctx) {
private val prefs = PreferenceDataSource(ctx)
@@ -164,94 +170,120 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
val manufacturer = model?.manufacturer?.value
val modelName = model?.name?.value
lifecycleScope.launch {
var vehicles = api.getVehicles().filter {
it.id in prefs.chargepriceMyVehicles
}
if (vehicles.isEmpty()) {
errorMessage = carContext.getString(R.string.chargeprice_select_car_first)
invalidate()
return@launch
} else if (vehicles.size > 1) {
if (manufacturer != null && modelName != null) {
vehicles = vehicles.filter {
it.brand == manufacturer && it.name.startsWith(modelName)
}
if (vehicles.isEmpty()) {
errorMessage = carContext.getString(
R.string.auto_chargeprice_vehicle_unknown,
manufacturer,
modelName
)
invalidate()
return@launch
} else if (vehicles.size > 1) {
errorMessage = carContext.getString(
R.string.auto_chargeprice_vehicle_ambiguous,
manufacturer,
modelName
)
try {
var vehicles = api.getVehicles().filter {
it.id in prefs.chargepriceMyVehicles
}
if (vehicles.isEmpty()) {
errorMessage = carContext.getString(R.string.chargeprice_select_car_first)
invalidate()
return@launch
} else if (vehicles.size > 1) {
if (manufacturer != null && modelName != null) {
vehicles = vehicles.filter {
it.brand == manufacturer && it.name.startsWith(modelName)
}
if (vehicles.isEmpty()) {
errorMessage = carContext.getString(
R.string.auto_chargeprice_vehicle_unknown,
manufacturer,
modelName
)
invalidate()
return@launch
} else if (vehicles.size > 1) {
errorMessage = carContext.getString(
R.string.auto_chargeprice_vehicle_ambiguous,
manufacturer,
modelName
)
invalidate()
return@launch
}
} else {
errorMessage =
carContext.getString(R.string.auto_chargeprice_vehicle_unavailable)
invalidate()
return@launch
}
} else {
}
val car = vehicles[0]
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
val result = api.getChargePrices(ChargepriceRequest().apply {
this.dataAdapter = dataAdapter
station = cpStation
vehicle = HasOne(car)
tariffs = if (!prefs.chargepriceMyTariffsAll) {
val myTariffs = prefs.chargepriceMyTariffs ?: emptySet()
HasMany<ChargepriceTariff>(*myTariffs.map {
ResourceIdentifier(
"tariff",
it
)
}.toTypedArray()).apply {
meta = JsonBuffer.create(
ChargepriceApi.moshi.adapter(ChargepriceRequestTariffMeta::class.java),
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS)
)
}
} else null
options = ChargepriceOptions(
batteryRange = batteryRange,
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency
)
}, ChargepriceApi.getChargepriceLanguage())
val myTariffs = prefs.chargepriceMyTariffs
// choose the highest power chargepoint compatible with the car
val chargepoint = cpStation.chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull { it.power }
if (chargepoint == null) {
errorMessage =
carContext.getString(R.string.auto_chargeprice_vehicle_unavailable)
carContext.getString(R.string.chargeprice_no_compatible_connectors)
invalidate()
return@launch
}
}
val car = vehicles[0]
meta =
(result.meta.get<ChargepriceMeta>(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta).chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull {
it.power
}
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
val result = api.getChargePrices(ChargepriceRequest().apply {
this.dataAdapter = dataAdapter
station = cpStation
vehicle = HasOne(car)
options = ChargepriceOptions(
batteryRange = batteryRange,
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
currency = prefs.chargepriceCurrency
)
}, ChargepriceApi.getChargepriceLanguage())
val myTariffs = prefs.chargepriceMyTariffs
// choose the highest power chargepoint compatible with the car
val chargepoint = cpStation.chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull { it.power }
if (chargepoint == null) {
errorMessage = carContext.getString(R.string.chargeprice_no_compatible_connectors)
prices = result.map { cp ->
val filteredPrices =
cp.chargepointPrices.filter {
it.plug == chargepoint.plug && it.power == chargepoint.power
}
if (filteredPrices.isEmpty()) {
null
} else {
cp.clone().apply {
chargepointPrices = filteredPrices
}
}
}.filterNotNull()
.sortedBy { it.chargepointPrices.first().price }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||
myTariffs != null && it.tariff?.get()?.id in myTariffs
}
invalidate()
return@launch
} catch (e: IOException) {
withContext(Dispatchers.Main) {
CarToast.makeText(
carContext,
R.string.chargeprice_connection_error,
CarToast.LENGTH_LONG
)
.show()
}
}
meta =
(result.meta.get<ChargepriceMeta>(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta).chargePoints.filterIndexed { i, cp ->
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
}.maxByOrNull {
it.power
}
prices = result.map { cp ->
val filteredPrices =
cp.chargepointPrices.filter {
it.plug == chargepoint.plug && it.power == chargepoint.power
}
if (filteredPrices.isEmpty()) {
null
} else {
cp.clone().apply {
chargepointPrices = filteredPrices
}
}
}.filterNotNull()
.sortedBy { it.chargepointPrices.first().price }
.sortedByDescending {
prefs.chargepriceMyTariffsAll ||
myTariffs != null && it.tariff?.get()?.id in myTariffs
}
invalidate()
}
}

View File

@@ -7,7 +7,9 @@ import androidx.car.app.model.*
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.LiveData
import net.vonforst.evmap.R
import net.vonforst.evmap.model.FILTERS_CUSTOM
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
@@ -40,9 +42,12 @@ class FilterScreen(ctx: CarContext) : Screen(ctx) {
}
override fun onGetTemplate(): Template {
val filterStatus =
prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
?: FILTERS_DISABLED
return ListTemplate.Builder().apply {
filterProfiles.value?.let {
setSingleList(buildFilterProfilesList(it.take(maxRows), prefs.filterStatus))
setSingleList(buildFilterProfilesList(it.take(maxRows), filterStatus))
} ?: setLoading(true)
setTitle(carContext.getString(R.string.menu_filter))
setHeaderAction(Action.BACK)

View File

@@ -1,5 +1,6 @@
package net.vonforst.evmap.auto
import android.content.pm.PackageManager
import android.location.Location
import android.text.SpannableStringBuilder
import android.text.Spanned
@@ -7,10 +8,14 @@ import androidx.car.app.CarContext
import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.model.*
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.lifecycleScope
import com.car2go.maps.model.LatLng
import kotlinx.coroutines.*
@@ -22,6 +27,7 @@ import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.FILTERS_CUSTOM
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.model.FILTERS_FAVORITES
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.availabilityText
@@ -33,6 +39,7 @@ import net.vonforst.evmap.viewmodel.getFilters
import net.vonforst.evmap.viewmodel.getReferenceData
import java.io.IOException
import java.time.Duration
import java.time.Instant
import java.time.ZonedDateTime
import kotlin.collections.set
import kotlin.math.roundToInt
@@ -51,7 +58,8 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
private val maxNumUpdates = 1
private var location: Location? = null
private var lastUpdateLocation: Location? = null
private var lastChargerUpdateLocation: Location? = null
private var lastDistanceUpdateTime: Instant? = null
private var chargers: List<ChargeLocation>? = null
private var prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
@@ -59,7 +67,8 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
createApi(prefs.dataSource, ctx)
}
private val searchRadius = 5 // kilometers
private val updateThreshold = 2000 // meters
private val chargerUpdateThreshold = 2000 // meters
private val distanceUpdateThreshold = Duration.ofSeconds(15)
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus>> =
HashMap()
@@ -69,12 +78,16 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
private val filterStatus = MutableLiveData<Long>().apply {
value = prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM } ?: FILTERS_DISABLED
value = prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
?: FILTERS_DISABLED
}
private val filterValues = db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
private val filters = api.getFilters(referenceData, carContext.stringProvider())
private val filtersWithValue = filtersWithValue(filters, filterValues)
private val hardwareMan = ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
private var energyLevel: EnergyLevel? = null
init {
filtersWithValue.observe(this) {
loadChargers()
@@ -139,7 +152,9 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
screenManager.pushForResult(FilterScreen(carContext)) {
chargers = null
numUpdates = 0
filterStatus.value = prefs.filterStatus
filterStatus.value =
prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM || it == FILTERS_FAVORITES }
?: FILTERS_DISABLED
}
session.mapScreen = null
}
@@ -151,7 +166,12 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
}
private fun formatCharger(charger: ChargeLocation, showCity: Boolean): Row {
val color = ContextCompat.getColor(carContext, getMarkerTint(charger))
val markerTint = if (charger.maxPower > 100) {
R.color.charger_100kw_dark // slightly darker color for better contrast
} else {
getMarkerTint(charger)
}
val color = ContextCompat.getColor(carContext, markerTint)
val place =
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
.setMarker(
@@ -177,13 +197,18 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
// distance
location?.let {
val distance = distanceBetween(
val distanceMeters = distanceBetween(
it.latitude, it.longitude,
charger.coordinates.lat, charger.coordinates.lng
) / 1000
)
text.append(
"distance",
DistanceSpan.create(Distance.create(distance, Distance.UNIT_KILOMETERS)),
DistanceSpan.create(
roundValueToDistance(
distanceMeters,
energyLevel?.distanceDisplayUnit?.value
)
),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
@@ -231,12 +256,19 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
return
}
invalidate()
if (lastUpdateLocation == null ||
location.distanceTo(lastUpdateLocation) > updateThreshold
val now = Instant.now()
if (lastDistanceUpdateTime == null ||
Duration.between(lastDistanceUpdateTime, now) > distanceUpdateThreshold
) {
lastUpdateLocation = location
lastDistanceUpdateTime = now
// update displayed distances
invalidate()
}
if (lastChargerUpdateLocation == null ||
location.distanceTo(lastChargerUpdateLocation) > chargerUpdateThreshold
) {
lastChargerUpdateLocation = location
// update displayed chargers
loadChargers()
}
@@ -312,6 +344,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
}?.awaitAll()
updateCoroutine = null
lastDistanceUpdateTime = Instant.now()
invalidate()
} catch (e: IOException) {
withContext(Dispatchers.Main) {
@@ -321,4 +354,30 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
}
}
}
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
this.energyLevel = energyLevel
invalidate()
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
private fun setupListeners() {
if (ContextCompat.checkSelfPermission(
carContext,
"com.google.android.gms.permission.CAR_FUEL"
) != PackageManager.PERMISSION_GRANTED
)
return
println("Setting up energy level listener")
val exec = ContextCompat.getMainExecutor(carContext)
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
private fun removeListeners() {
println("Removing energy level listener")
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
}
}

View File

@@ -7,10 +7,12 @@ import androidx.car.app.constraints.ConstraintManager
import androidx.car.app.hardware.common.CarUnit
import androidx.car.app.model.CarColor
import androidx.car.app.model.CarIcon
import androidx.car.app.model.Distance
import androidx.car.app.versioning.CarAppApiLevels
import androidx.core.graphics.drawable.IconCompat
import net.vonforst.evmap.api.availability.ChargepointStatus
import java.util.*
import kotlin.math.roundToInt
fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
val unknown = status.any { it == ChargepointStatus.UNKNOWN }
@@ -34,14 +36,22 @@ val CarContext.constraintManager
fun Bitmap.asCarIcon(): CarIcon = CarIcon.Builder(IconCompat.createWithBitmap(this)).build()
private const val kmPerMile = 1.609344
private const val ftPerMile = 5280
private const val ydPerMile = 1760
fun getDefaultDistanceUnit(): Int {
return when (Locale.getDefault().country) {
"US", "GB", "MM", "LR" -> CarUnit.MILE
else -> CarUnit.KILOMETER
return if (usesImperialUnits(Locale.getDefault())) {
CarUnit.MILE
} else {
CarUnit.KILOMETER
}
}
fun usesImperialUnits(locale: Locale): Boolean {
return locale.country in listOf("US", "GB", "MM", "LR")
|| locale.country == "" && locale.language == "en"
}
fun getDefaultSpeedUnit(): Int {
return when (Locale.getDefault().country) {
"US", "GB", "MM", "LR" -> CarUnit.MILES_PER_HOUR
@@ -72,6 +82,52 @@ fun formatCarUnitSpeed(value: Float?, unit: Int?): String {
}
}
fun roundValueToDistance(value: Double, unit: Int? = null): Distance {
// value is in meters
when (unit ?: getDefaultDistanceUnit()) {
CarUnit.MILE -> {
// imperial system
val miles = value / 1000 / kmPerMile
val yards = miles * ydPerMile
val feet = miles * ftPerMile
return when (miles) {
in 0.0..0.1 -> if (Locale.getDefault().country == "UK") {
Distance.create(roundToMultipleOf(yards, 10.0), Distance.UNIT_YARDS)
} else {
Distance.create(roundToMultipleOf(feet, 10.0), Distance.UNIT_FEET)
}
in 0.1..10.0 -> Distance.create(
roundToMultipleOf(miles, 0.1),
Distance.UNIT_MILES_P1
)
else -> Distance.create(roundToMultipleOf(miles, 1.0), Distance.UNIT_MILES)
}
}
else -> {
// metric system
return when (value) {
in 0.0..999.0 -> Distance.create(
roundToMultipleOf(value, 10.0),
Distance.UNIT_METERS
)
in 1000.0..10000.0 -> Distance.create(
roundToMultipleOf(value / 1000, 0.1),
Distance.UNIT_KILOMETERS_P1
)
else -> Distance.create(
roundToMultipleOf(value / 1000, 1.0),
Distance.UNIT_KILOMETERS
)
}
}
}
}
private fun roundToMultipleOf(num: Double, step: Double): Double {
return (num / step).roundToInt() * step
}
fun getAndroidAutoVersion(ctx: Context): List<String> {
val info = ctx.packageManager.getPackageInfo("com.google.android.projection.gearhead", 0)
return info.versionName.split(".")

View File

@@ -7,9 +7,9 @@ import android.text.style.StyleSpan
import com.car2go.maps.google.adapter.AnyMapAdapter
import com.car2go.maps.util.SphericalUtil
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.LatLngBounds
import com.google.android.gms.tasks.Tasks.await
import com.google.android.libraries.maps.model.LatLng
import com.google.android.libraries.maps.model.LatLngBounds
import com.google.android.libraries.places.api.Places
import com.google.android.libraries.places.api.model.AutocompleteSessionToken
import com.google.android.libraries.places.api.model.Place

View File

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

View File

@@ -32,7 +32,7 @@
<activity
android:name=".MapsActivity"
android:label="@string/title_activity_maps"
android:label="@string/app_name"
android:theme="@style/AppTheme.LaunchScreen"
android:exported="true">
<intent-filter>

View File

@@ -19,11 +19,15 @@ import androidx.core.view.WindowInsetsCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.fragment.FragmentNavigator
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.car2go.maps.model.LatLng
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import net.vonforst.evmap.fragment.MapFragment
import net.vonforst.evmap.fragment.MapFragmentArgs
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.utils.LocaleContextWrapper
@@ -35,7 +39,8 @@ const val EXTRA_CHARGER_ID = "chargerId"
const val EXTRA_LAT = "lat"
const val EXTRA_LON = "lon"
class MapsActivity : AppCompatActivity() {
class MapsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
interface FragmentCallback {
fun getRootView(): View
}
@@ -102,11 +107,11 @@ class MapsActivity : AppCompatActivity() {
}
})
}
navGraph.startDestination = R.id.onboarding
navGraph.setStartDestination(R.id.onboarding)
navController.graph = navGraph
return
} else {
navGraph.startDestination = R.id.map
navGraph.setStartDestination(R.id.map)
navController.graph = navGraph
}
@@ -121,14 +126,14 @@ class MapsActivity : AppCompatActivity() {
val deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragment.showLocation(lat, lon))
.setArguments(MapFragmentArgs(latLng = LatLng(lat, lon)).toBundle())
.createPendingIntent()
deepLink.send()
} else if (query != null && query.isNotEmpty()) {
val deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragment.showLocationByName(query))
.setArguments(MapFragmentArgs(locationName = query).toBundle())
.createPendingIntent()
deepLink.send()
}
@@ -138,7 +143,7 @@ class MapsActivity : AppCompatActivity() {
val deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragment.showChargerById(id))
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
deepLink.send()
}
@@ -146,11 +151,13 @@ class MapsActivity : AppCompatActivity() {
navController.createDeepLink()
.setDestination(R.id.map)
.setArguments(
MapFragment.showCharger(
intent.getLongExtra(EXTRA_CHARGER_ID, 0),
intent.getDoubleExtra(EXTRA_LAT, 0.0),
intent.getDoubleExtra(EXTRA_LON, 0.0)
)
MapFragmentArgs(
chargerId = intent.getLongExtra(EXTRA_CHARGER_ID, 0),
latLng = LatLng(
intent.getDoubleExtra(EXTRA_LAT, 0.0),
intent.getDoubleExtra(EXTRA_LON, 0.0)
)
).toBundle()
)
.createPendingIntent()
.send()
@@ -213,4 +220,15 @@ class MapsActivity : AppCompatActivity() {
}
startActivity(intent)
}
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
// Identify the Navigation Destination
val navDestination = navController.graph
.find { target -> target is FragmentNavigator.Destination && pref.fragment == target.className }
navDestination?.let { target -> navController.navigate(target.id) }
return true
}
}

View File

@@ -84,10 +84,10 @@ fun buildDetails(
loc.openinghours.getStatusText(ctx)
else
loc.openinghours.description ?: "",
if (loc.openinghours.days != null) loc.openinghours.description else null,
if (loc.openinghours.days != null || loc.openinghours.twentyfourSeven) loc.openinghours.description else null,
hoursDays = loc.openinghours.days
) else null,
if (loc.cost != null) DetailsAdapter.Detail(
if (loc.cost != null && !loc.cost.isEmpty) DetailsAdapter.Detail(
R.drawable.ic_cost,
R.string.cost,
loc.cost.getStatusText(ctx),

View File

@@ -98,11 +98,12 @@ class PlaceAutocompleteAdapter(val context: Context, val location: LiveData<LatL
init {
if (PreferenceDataSource(context).searchProvider == "mapbox") {
// set delay to 500 ms to reduce paid Mapbox API requests
this.setDelayer { 500L }
this.setDelayer { q -> if (isShortQuery(q)) 0L else 500L }
}
}
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
resultList = results?.values as? List<AutocompletePlace>?
if (results != null && results.count > 0) {
notifyDataSetChanged()
} else {
@@ -112,6 +113,7 @@ class PlaceAutocompleteAdapter(val context: Context, val location: LiveData<LatL
override fun performFiltering(constraint: CharSequence?): FilterResults {
val query = constraint.toString()
var resultList: List<AutocompletePlace>? = null
if (constraint != null) {
for (provider in providers) {
try {
@@ -132,14 +134,13 @@ class PlaceAutocompleteAdapter(val context: Context, val location: LiveData<LatL
}
// if we already have enough results or the query is short, stop here
if (query.length < 3 || recentResults.size >= maxItems) break
if (isShortQuery(query) || recentResults.size >= maxItems) break
// then search online
val recentIds = recentPlaces.map { it.id }
resultList =
(recentPlaces.map { it.asAutocompletePlace(location.value) } +
provider.autocomplete(query, location.value)
.filter { !recentIds.contains(it.id) }).take(maxItems)
(resultList!! + provider.autocomplete(query, location.value)
.filter { !recentIds.contains(it.id) }).take(maxItems)
break
} catch (e: ApiUnavailableException) {
e.printStackTrace()
@@ -150,7 +151,7 @@ class PlaceAutocompleteAdapter(val context: Context, val location: LiveData<LatL
if (currentProvider is MapboxAutocompleteProvider && !delaySet) {
// set delay to 500 ms to reduce paid Mapbox API requests
this.setDelayer { 500L }
this.setDelayer { q -> if (isShortQuery(q)) 0L else 500L }
}
return resultList.asFilterResults()
@@ -167,6 +168,8 @@ class PlaceAutocompleteAdapter(val context: Context, val location: LiveData<LatL
}
}
private fun isShortQuery(query: CharSequence) = query.length < 3
suspend fun getDetails(id: String): PlaceWithBounds {
val provider = currentProvider!!
val result = resultList!!.find { it.id == id }!!

View File

@@ -65,10 +65,20 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
connectors: Map<Long, Pair<Double, String>>,
chargepoints: List<Chargepoint>
): Map<Chargepoint, Set<Long>> {
var chargepoints = chargepoints
// iterate over each connector type
val types = connectors.map { it.value.second }.distinct().toSet()
val equivalentTypes = types.map { equivalentPlugTypes(it).plus(it) }.cartesianProduct()
val geTypes = chargepoints.map { it.type }.distinct().toSet()
var geTypes = chargepoints.map { it.type }.distinct().toSet()
if (!equivalentTypes.any { it == geTypes } && geTypes.size > 1 && geTypes.contains(
Chargepoint.SCHUKO
)) {
// If charger has household plugs and other plugs, try removing the household plugs
// (common e.g. in Hamburg -> 2x Type 2 + 2x Schuko, but NM only lists Type 2)
geTypes = geTypes.filter { it != Chargepoint.SCHUKO }.toSet()
chargepoints = chargepoints.filter { it.type != Chargepoint.SCHUKO }
}
if (!equivalentTypes.any { it == geTypes }) throw AvailabilityDetectorException("chargepoints do not match")
return types.flatMap { type ->
// find connectors of this type
@@ -92,7 +102,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
chargepoint to ids
}
} else if (powers.size == 1 && gePowers.size == 2
&& chargepoints.sumBy { it.count } == connsOfType.size
&& chargepoints.sumOf { it.count } == connsOfType.size
) {
// special case: dual charger(s) with load balancing
// GoingElectric shows 2 different powers, NewMotion just one

View File

@@ -205,6 +205,9 @@ class ChargePrice : Resource(), Equatable, Cloneable {
@field:Json(name = "charge_point_prices")
lateinit var chargepointPrices: List<ChargepointPrice>
@field:Json(name = "branding")
var branding: ChargepriceBranding? = null
var tariff: HasOne<ChargepriceTariff>? = null
@@ -238,6 +241,7 @@ class ChargePrice : Resource(), Equatable, Cloneable {
if (startTime != other.startTime) return false
if (tags != other.tags) return false
if (chargepointPrices != other.chargepointPrices) return false
if (branding != other.branding) return false
return true
}
@@ -256,6 +260,7 @@ class ChargePrice : Resource(), Equatable, Cloneable {
result = 31 * result + startTime
result = 31 * result + tags.hashCode()
result = 31 * result + chargepointPrices.hashCode()
result = 31 * result + branding.hashCode()
return result
}
@@ -274,6 +279,7 @@ class ChargePrice : Resource(), Equatable, Cloneable {
totalMonthlyFee = this@ChargePrice.totalMonthlyFee
url = this@ChargePrice.url
tariff = this@ChargePrice.tariff
branding = this@ChargePrice.branding
}
}
}
@@ -328,6 +334,12 @@ data class ChargepointPrice(
}
}
data class ChargepriceBranding(
@Json(name = "background_color") val backgroundColor: String,
@Json(name = "text_color") val textColor: String,
@Json(name = "logo_url") val logoUrl: String
)
data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) {
val isOnlyKwh =
kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0)
@@ -339,6 +351,19 @@ data class ChargepriceMeta(
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepointMeta>
)
enum class ChargepriceInclude {
@Json(name = "filter")
FILTER,
@Json(name = "always")
ALWAYS,
@Json(name = "exclusive")
EXCLUSIVE
}
data class ChargepriceRequestTariffMeta(
val include: ChargepriceInclude
)
data class ChargepriceChargepointMeta(
val power: Double,
val plug: String,

View File

@@ -402,7 +402,7 @@ class GoingElectricApiWrapper(
val chargeCards = referenceData.chargecards
val plugMap = plugs.map { plug ->
plug to nameForPlugType(sp, 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()
@@ -448,11 +448,11 @@ class GoingElectricApiWrapper(
MultipleChoiceFilter(
sp.getString(R.string.filter_connectors), "connectors",
plugMap,
commonChoices = setOf(
commonChoices = listOf(
Chargepoint.TYPE_2_UNKNOWN,
Chargepoint.CCS_UNKNOWN,
Chargepoint.CHADEMO
),
).map { GEChargepoint.convertTypeToGE(it)!! }.toSet(),
manyChoices = true
),
SliderFilter(

View File

@@ -87,7 +87,14 @@ data class GECost(
@JsonObjectOrFalse @Json(name = "description_short") val descriptionShort: String?,
@JsonObjectOrFalse @Json(name = "description_long") val descriptionLong: String?
) {
fun convert() = Cost(freecharging, freeparking, descriptionShort, descriptionLong)
fun convert() = Cost(
// In GE, freecharging = false can either mean "paid charging" or "no information
// available", only freecharging = true provides useful information. Therefore convert
// false to null. Same for freeparking.
if (freecharging) freecharging else null,
if (freeparking) freeparking else null,
descriptionShort, descriptionLong
)
}
@JsonClass(generateAdapter = true)
@@ -126,7 +133,7 @@ data class GEHours(
val start: LocalTime?,
val end: LocalTime?
) {
fun convert() = Hours(start, end)
fun convert() = if (start != null && end != null) Hours(start, end) else null
}
@JsonClass(generateAdapter = true)

View File

@@ -1,9 +1,11 @@
package net.vonforst.evmap.autocomplete
import android.os.Parcelable
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlinx.parcelize.Parcelize
interface AutocompleteProvider {
val id: String
@@ -183,4 +185,5 @@ enum class AutocompletePlaceType {
}
}
data class PlaceWithBounds(val latLng: LatLng, val viewport: LatLngBounds?)
@Parcelize
data class PlaceWithBounds(val latLng: LatLng, val viewport: LatLngBounds?) : Parcelable

View File

@@ -11,6 +11,7 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@@ -20,15 +21,10 @@ import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.ChargepriceAdapter
import net.vonforst.evmap.adapter.CheckableChargepriceCarAdapter
import net.vonforst.evmap.adapter.CheckableConnectorAdapter
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.model.ReferenceData
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.viewModelFactory
@@ -84,8 +80,9 @@ class ChargepriceFragment : DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val charger = requireArguments().getParcelable<ChargeLocation>(ARG_CHARGER)!!
val dataSource = requireArguments().getString(ARG_DATASOURCE)!!
val fragmentArgs: ChargepriceFragmentArgs by navArgs()
val charger = fragmentArgs.charger
val dataSource = fragmentArgs.dataSource
vm.charger.value = charger
vm.dataSource.value = dataSource
if (vm.chargepoint.value == null) {
@@ -178,6 +175,10 @@ class ChargepriceFragment : DialogFragment() {
dismiss()
true
}
R.id.menu_help -> {
(activity as? MapsActivity)?.openUrl(getString(R.string.chargeprice_faq_link))
true
}
else -> false
}
}
@@ -216,28 +217,4 @@ class ChargepriceFragment : DialogFragment() {
)
}
companion object {
const val ARG_CHARGER = "charger"
const val ARG_DATASOURCE = "datasource"
fun showCharger(
charger: ChargeLocation,
dataSource: Class<ChargepointApi<ReferenceData>>
): Bundle {
return Bundle().apply {
putParcelable(
ARG_CHARGER,
charger
)
putString(
ARG_DATASOURCE,
when (dataSource) {
GoingElectricApiWrapper::class.java -> "going_electric"
OpenChargeMapApiWrapper::class.java -> "open_charge_map"
else -> throw IllegalArgumentException("unsupported data source")
}
)
}
}
}
}

View File

@@ -77,7 +77,10 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
onClickListener = {
findNavController().navigate(
R.id.action_favs_to_map,
MapFragment.showCharger(it.charger)
MapFragmentArgs(
chargerId = it.charger.id,
latLng = LatLng(it.charger.coordinates.lat, it.charger.coordinates.lng)
).toBundle()
)
}
}

View File

@@ -33,6 +33,7 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
@@ -72,6 +73,7 @@ import net.vonforst.evmap.adapter.DetailsAdapter
import net.vonforst.evmap.adapter.GalleryAdapter
import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.autocomplete.ApiUnavailableException
import net.vonforst.evmap.autocomplete.PlaceWithBounds
import net.vonforst.evmap.databinding.FragmentMapBinding
@@ -89,11 +91,6 @@ import net.vonforst.evmap.viewmodel.*
import java.io.IOException
const val ARG_CHARGER_ID = "chargerId"
const val ARG_LAT = "lat"
const val ARG_LON = "lon"
const val ARG_LOCATION_NAME = "locationName"
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback,
LostApiClient.ConnectionCallbacks, LocationListener {
private lateinit var binding: FragmentMapBinding
@@ -309,9 +306,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
binding.detailView.btnChargeprice.setOnClickListener {
val charger = vm.charger.value?.data ?: return@setOnClickListener
val dataSource = when (vm.apiType) {
GoingElectricApiWrapper::class.java -> "going_electric"
OpenChargeMapApiWrapper::class.java -> "open_charge_map"
else -> throw IllegalArgumentException("unsupported data source")
}
findNavController().navigate(
R.id.action_map_to_chargepriceFragment,
ChargepriceFragment.showCharger(charger, vm.apiType)
ChargepriceFragmentArgs(charger, dataSource).toBundle()
)
}
binding.detailView.topPart.setOnClickListener {
@@ -442,11 +444,21 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private fun toggleFavorite() {
val favs = vm.favorites.value ?: return
val charger = vm.chargerSparse.value ?: return
if (favs.find { it.id == charger.id } != null) {
val isFav = favs.find { it.id == charger.id } != null
if (isFav) {
vm.deleteFavorite(charger)
} else {
vm.insertFavorite(charger)
}
markers.inverse[charger]?.setIcon(
chargerIconGenerator.getBitmapDescriptor(
getMarkerTint(charger, vm.filteredConnectors.value),
highlight = true,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value),
fav = !isFav
)
)
}
private fun setupObservers() {
@@ -475,6 +487,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
if (vm.bottomSheetState.value != BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT) {
bottomSheetBehavior.state = STATE_COLLAPSED
}
removeSearchFocus()
binding.fabDirections.show()
detailAppBarBehavior.setToolbarTitle(it.name)
updateFavoriteToggle()
@@ -573,7 +586,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
getMarkerTint(c, vm.filteredConnectors.value),
highlight = false,
fault = c.faultReport != null,
multi = c.isMulti(vm.filteredConnectors.value)
multi = c.isMulti(vm.filteredConnectors.value),
fav = c.id in vm.favorites.value?.map { it.id } ?: emptyList()
)
)
}
@@ -587,7 +601,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
getMarkerTint(charger, vm.filteredConnectors.value),
highlight = true,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value)
multi = charger.isMulti(vm.filteredConnectors.value),
fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
)
)
animator.animateMarkerBounce(marker)
@@ -600,7 +615,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
getMarkerTint(c, vm.filteredConnectors.value),
highlight = false,
fault = c.faultReport != null,
multi = c.isMulti(vm.filteredConnectors.value)
multi = c.isMulti(vm.filteredConnectors.value),
fav = c.id in vm.favorites.value?.map { it.id } ?: emptyList()
)
)
}
@@ -802,10 +818,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val position = vm.mapPosition.value
val lat = arguments?.optDouble(ARG_LAT)
val lon = arguments?.optDouble(ARG_LON)
val chargerId = arguments?.optLong(ARG_CHARGER_ID)
val locationName = arguments?.getString(ARG_LOCATION_NAME)
val fragmentArgs: MapFragmentArgs by navArgs()
val locationName = fragmentArgs.locationName
val chargerId = fragmentArgs.chargerId
val latLng = fragmentArgs.latLng
var positionSet = false
@@ -814,7 +830,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
map.cameraUpdateFactory.newLatLngZoom(position.bounds.center, position.zoom)
map.moveCamera(cameraUpdate)
positionSet = true
} else if (chargerId != null && (lat == null || lon == null)) {
} else if (chargerId != 0L && latLng == null) {
// show given charger ID
vm.loadChargerById(chargerId)
vm.chargerSparse.observe(
@@ -832,13 +848,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
})
positionSet = true
} else if (lat != null && lon != null) {
} else if (latLng != null) {
// show given position
val latLng = LatLng(lat, lon)
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(latLng, 16f)
map.moveCamera(cameraUpdate)
if (chargerId != null) {
if (chargerId != 0L) {
// show charger detail after chargers were loaded
vm.chargepoints.observe(
viewLifecycleOwner,
@@ -895,6 +910,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
vm.mapPosition.value = MapPosition(
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
)
if (vm.searchResult.value != null) {
// show search result (after configuration change)
vm.searchResult.postValue(vm.searchResult.value)
}
}
@RequiresPermission(anyOf = [ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION])
@@ -943,7 +963,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
getMarkerTint(charger, vm.filteredConnectors.value),
highlight = charger == vm.chargerSparse.value,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value)
multi = charger.isMulti(vm.filteredConnectors.value),
fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
)
)
}
@@ -961,7 +982,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
val multi = charger.isMulti(vm.filteredConnectors.value)
animator.animateMarkerDisappear(marker, tint, highlight, fault, multi)
val fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
animator.animateMarkerDisappear(marker, tint, highlight, fault, multi, fav)
} else {
animator.deleteMarker(marker)
}
@@ -976,6 +998,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
val multi = charger.isMulti(vm.filteredConnectors.value)
val fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
val marker = map.addMarker(
MarkerOptions()
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
@@ -986,12 +1009,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
255,
highlight,
fault,
multi
multi,
fav
)
)
.anchor(0.5f, 1f)
)
animator.animateMarkerAppear(marker, tint, highlight, fault, multi)
animator.animateMarkerAppear(marker, tint, highlight, fault, multi, fav)
markers[marker] = charger
}
}
@@ -1083,6 +1107,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
R.id.menu_group_filter_profiles,
Menu.NONE, Menu.NONE, R.string.no_filters
)
val favoritesItem = popup.menu.add(
R.id.menu_group_filter_profiles,
Menu.NONE,
Menu.NONE, R.string.filter_favorites
)
profiles.forEach { profile ->
val item = popup.menu.add(
R.id.menu_group_filter_profiles,
@@ -1099,11 +1128,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
profilesMap[FILTERS_DISABLED] = noFiltersItem
profilesMap[FILTERS_CUSTOM] = customItem
profilesMap[FILTERS_FAVORITES] = favoritesItem
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.isEmpty()
manageFiltersItem.isVisible = profiles.isNotEmpty()
vm.filterStatus.observe(viewLifecycleOwner, Observer { id ->
when (id) {
@@ -1115,6 +1145,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
customItem.isVisible = true
customItem.isChecked = true
}
FILTERS_FAVORITES -> {
customItem.isVisible = false
favoritesItem.isChecked = true
}
else -> {
customItem.isVisible = false
val item = profilesMap[id]
@@ -1153,43 +1187,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
return binding.root
}
companion object {
fun showCharger(charger: ChargeLocation): Bundle {
return Bundle().apply {
putLong(ARG_CHARGER_ID, charger.id)
putDouble(ARG_LAT, charger.coordinates.lat)
putDouble(ARG_LON, charger.coordinates.lng)
}
}
fun showLocation(lat: Double, lon: Double): Bundle {
return Bundle().apply {
putDouble(ARG_LAT, lat)
putDouble(ARG_LON, lon)
}
}
fun showChargerById(id: Long): Bundle {
return Bundle().apply {
putLong(ARG_CHARGER_ID, id)
}
}
fun showCharger(id: Long, lat: Double, lon: Double): Bundle {
return Bundle().apply {
putLong(ARG_CHARGER_ID, id)
putDouble(ARG_LAT, lat)
putDouble(ARG_LON, lon)
}
}
fun showLocationByName(query: String): Bundle {
return Bundle().apply {
putString(ARG_LOCATION_NAME, query)
}
}
}
override fun onConnected() {
val map = this.map ?: return
val context = this.context ?: return

View File

@@ -81,9 +81,10 @@ class MultiSelectDialog : AppCompatDialogFragment() {
.sortedBy { it.value.toLowerCase(Locale.getDefault()) }
.sortedBy {
when {
selected.contains(it.key) -> 0
commonChoices?.contains(it.key) == true -> 1
else -> 2
selected.contains(it.key) && commonChoices?.contains(it.key) == true -> 0
selected.contains(it.key) -> 1
commonChoices?.contains(it.key) == true -> 2
else -> 3
}
}
.map { MultiSelectItem(it.key, it.value, it.key in selected) }

View File

@@ -1,4 +1,4 @@
package net.vonforst.evmap.fragment
package net.vonforst.evmap.fragment.preference
import android.os.Bundle
import androidx.appcompat.widget.Toolbar

View File

@@ -0,0 +1,40 @@
package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceFragmentCompat
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.storage.PreferenceDataSource
abstract class BaseSettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
protected lateinit var prefs: PreferenceDataSource
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
prefs = PreferenceDataSource(requireContext())
}
override fun onResume() {
super.onResume()
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
val navController = findNavController()
val toolbar = requireView().findViewById(R.id.toolbar) as Toolbar
toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
}
override fun onPause() {
preferenceManager.sharedPreferences
.unregisterOnSharedPreferenceChangeListener(this)
super.onPause()
}
}

View File

@@ -1,28 +1,15 @@
package net.vonforst.evmap.fragment
package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.updateNightMode
import net.vonforst.evmap.viewmodel.SettingsViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class SettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
private lateinit var prefs: PreferenceDataSource
class ChargepriceSettingsFragment : BaseSettingsFragment() {
private val vm: SettingsViewModel by viewModels(factoryProducer = {
viewModelFactory {
SettingsViewModel(
@@ -37,7 +24,6 @@ class SettingsFragment : PreferenceFragmentCompat(),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
prefs = PreferenceDataSource(requireContext())
myVehiclePreference = findPreference("chargeprice_my_vehicle")!!
myVehiclePreference.isEnabled = false
@@ -92,68 +78,21 @@ class SettingsFragment : PreferenceFragmentCompat(),
"${it.brand} ${it.name}"
}.joinToString(", ")
myVehiclePreference.summary = summary
// TODO: prefs.chargepriceMyVehicleDcChargeports = it.dcChargePorts
}
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey)
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
return when (preference?.key) {
"search_delete_recent" -> {
Toast.makeText(context, R.string.deleted_recent_search_results, Toast.LENGTH_LONG)
.show()
vm.deleteRecentSearchResults()
true
}
else -> super.onPreferenceTreeClick(preference)
}
setPreferencesFromResource(R.xml.settings_chargeprice, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
"language" -> {
activity?.let {
it.finish();
it.startActivity(it.intent);
}
}
"darkmode" -> {
updateNightMode(prefs)
}
"chargeprice_my_vehicle" -> {
updateMyVehiclesSummary()
}
"chargeprice_my_tariffs" -> {
updateMyTariffsSummary()
}
"search_provider" -> {
if (prefs.searchProvider == "google") {
Toast.makeText(context, R.string.pref_search_provider_info, Toast.LENGTH_LONG)
.show()
}
}
}
}
override fun onResume() {
super.onResume()
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
val navController = findNavController()
val toolbar = requireView().findViewById(R.id.toolbar) as Toolbar
toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
}
override fun onPause() {
preferenceManager.sharedPreferences
.unregisterOnSharedPreferenceChangeListener(this)
super.onPause()
}
}

View File

@@ -0,0 +1,63 @@
package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import android.widget.TextView
import androidx.fragment.app.viewModels
import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar
import net.vonforst.evmap.R
import net.vonforst.evmap.viewmodel.SettingsViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class DataSettingsFragment : BaseSettingsFragment() {
private val vm: SettingsViewModel by viewModels(factoryProducer = {
viewModelFactory {
SettingsViewModel(
requireActivity().application,
getString(R.string.chargeprice_key)
)
}
})
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_data, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
"search_provider" -> {
if (prefs.searchProvider == "google") {
Snackbar.make(
requireView(),
R.string.pref_search_provider_info,
Snackbar.LENGTH_INDEFINITE
).apply {
setAction(R.string.ok) {}
this.view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text)
?.apply {
maxLines = 6
}
}
.show()
}
}
}
}
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
return when (preference?.key) {
"search_delete_recent" -> {
Snackbar.make(
requireView(),
R.string.deleted_recent_search_results,
Snackbar.LENGTH_LONG
)
.show()
vm.deleteRecentSearchResults()
true
}
else -> super.onPreferenceTreeClick(preference)
}
}
}

View File

@@ -0,0 +1,22 @@
package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import net.vonforst.evmap.R
class SettingsFragment : BaseSettingsFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
}
}

View File

@@ -0,0 +1,26 @@
package net.vonforst.evmap.fragment.preference
import android.content.SharedPreferences
import android.os.Bundle
import net.vonforst.evmap.R
import net.vonforst.evmap.ui.updateNightMode
class UiSettingsFragment : BaseSettingsFragment() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings_ui, rootKey)
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
"language" -> {
activity?.let {
it.finish();
it.startActivity(it.intent);
}
}
"darkmode" -> {
updateNightMode(prefs)
}
}
}
}

View File

@@ -126,6 +126,9 @@ data class Cost(
val descriptionShort: String? = null,
val descriptionLong: String? = null
) : Parcelable {
val isEmpty: Boolean
get() = descriptionLong == null && descriptionShort == null && freecharging == null && freeparking == null
fun getStatusText(ctx: Context, emoji: Boolean = false): CharSequence {
if (freecharging != null && freeparking != null) {
val charging =
@@ -137,6 +140,22 @@ data class Cost(
} else {
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail, charging, parking), 0)
}
} else if (freecharging != null) {
val charging =
if (freecharging) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
return if (emoji) {
"$charging"
} else {
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail_charging, charging), 0)
}
} else if (freeparking != null) {
val parking =
if (freeparking) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
return if (emoji) {
"$parking"
} else {
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail_parking, parking), 0)
}
} else if (descriptionShort != null) {
return descriptionShort
} else if (descriptionLong != null) {
@@ -162,9 +181,7 @@ data class OpeningHours(
return HtmlCompat.fromHtml(ctx.getString(R.string.open_247), 0)
} else if (days != null) {
val hours = days.getHoursForDate(LocalDate.now())
if (hours.start == null || hours.end == null) {
return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
}
?: return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
val now = LocalTime.now()
if (hours.start.isBefore(now) && hours.end.isAfter(now)) {
@@ -192,21 +209,21 @@ data class OpeningHours(
@Parcelize
data class OpeningHoursDays(
@Embedded(prefix = "mo") val monday: Hours,
@Embedded(prefix = "tu") val tuesday: Hours,
@Embedded(prefix = "we") val wednesday: Hours,
@Embedded(prefix = "th") val thursday: Hours,
@Embedded(prefix = "fr") val friday: Hours,
@Embedded(prefix = "sa") val saturday: Hours,
@Embedded(prefix = "su") val sunday: Hours,
@Embedded(prefix = "ho") val holiday: Hours
@Embedded(prefix = "mo") val monday: Hours?,
@Embedded(prefix = "tu") val tuesday: Hours?,
@Embedded(prefix = "we") val wednesday: Hours?,
@Embedded(prefix = "th") val thursday: Hours?,
@Embedded(prefix = "fr") val friday: Hours?,
@Embedded(prefix = "sa") val saturday: Hours?,
@Embedded(prefix = "su") val sunday: Hours?,
@Embedded(prefix = "ho") val holiday: Hours?
) : Parcelable {
fun getHoursForDate(date: LocalDate): Hours {
fun getHoursForDate(date: LocalDate): Hours? {
// TODO: check for holidays
return getHoursForDayOfWeek(date.dayOfWeek)
}
fun getHoursForDayOfWeek(dayOfWeek: DayOfWeek?): Hours {
fun getHoursForDayOfWeek(dayOfWeek: DayOfWeek?): Hours? {
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
return when (dayOfWeek) {
DayOfWeek.MONDAY -> monday
@@ -223,16 +240,12 @@ data class OpeningHoursDays(
@Parcelize
data class Hours(
val start: LocalTime?,
val end: LocalTime?
val start: LocalTime,
val end: LocalTime
) : Parcelable {
override fun toString(): String {
if (start != null && end != null) {
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
return "${start.format(fmt)} - ${end.format(fmt)}"
} else {
return "closed"
}
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
return "${start.format(fmt)} - ${end.format(fmt)}"
}
}

View File

@@ -129,4 +129,5 @@ fun FilterValues.getMultipleChoiceValue(key: String) =
this.find { it.value.key == key }?.value as MultipleChoiceFilterValue?
const val FILTERS_DISABLED = -2L
const val FILTERS_CUSTOM = -1L
const val FILTERS_CUSTOM = -1L
const val FILTERS_FAVORITES = -3L

View File

@@ -23,4 +23,12 @@ interface ChargeLocationsDao {
@Query("SELECT * FROM chargelocation")
fun getAllChargeLocationsBlocking(): List<ChargeLocation>
@Query("SELECT * FROM chargelocation WHERE lat >= :lat1 AND lat <= :lat2 AND lng >= :lng1 AND lng <= :lng2")
suspend fun getChargeLocationsInBoundsAsync(
lat1: Double,
lat2: Double,
lng1: Double,
lng2: Double
): List<ChargeLocation>
}

View File

@@ -54,7 +54,7 @@ abstract class FilterValueDao {
)
open fun getFilterValues(filterStatus: Long, dataSource: String): LiveData<List<FilterValue>> =
if (filterStatus == FILTERS_DISABLED) {
if (filterStatus == FILTERS_DISABLED || filterStatus == FILTERS_FAVORITES) {
MutableLiveData(emptyList())
} else {
MediatorLiveData<List<FilterValue>>().apply {

View File

@@ -2,6 +2,10 @@ package net.vonforst.evmap.ui
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.text.SpannableString
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
@@ -18,14 +22,12 @@ import androidx.databinding.InverseBindingListener
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import coil.load
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.slider.RangeSlider
import net.vonforst.evmap.R
import net.vonforst.evmap.*
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.iconForPlugType
import net.vonforst.evmap.kmPerMile
import net.vonforst.evmap.meterPerFt
import net.vonforst.evmap.shouldUseImperialUnits
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.roundToInt
@@ -344,18 +346,68 @@ fun setImageTintList(view: ImageView, @ColorInt color: Int) {
view.imageTintList = ColorStateList.valueOf(color)
}
@BindingAdapter("myTariffsBackground")
fun myTariffsBackground(view: View, myTariff: Boolean) {
if (myTariff) {
view.background = ContextCompat.getDrawable(view.context, R.drawable.my_tariff_background)
} else {
view.context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
view.background = it.getDrawable(0)
fun tariffBackground(context: Context, myTariff: Boolean, brandingColor: String?): Drawable? {
when {
myTariff -> {
return ContextCompat.getDrawable(context, R.drawable.my_tariff_background)
}
brandingColor != null -> {
val drawable = ContextCompat.getDrawable(context, R.drawable.branded_tariff_background)
val color = colorToTransparent(Color.parseColor(brandingColor))
(drawable as LayerDrawable).setDrawableByLayerId(
R.id.background, ColorDrawable(
color
)
)
return drawable
}
else -> {
context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
return it.getDrawable(0)
}
}
}
}
fun isDarkMode(context: Context) = context.isDarkMode()
/**
* Converts an opaque color to a transparent color, assuming it was on a white background
* with a certain opacity targetAlpha.
*/
private fun colorToTransparent(color: Int, targetAlpha: Float = 31f / 255): Int {
if (Color.alpha(color) != 255) return color
val red = Color.red(color)
val green = Color.green(color)
val blue = Color.blue(color)
val newRed = ((red - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
val newGreen = ((green - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
val newBlue = ((blue - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
return Color.argb((targetAlpha * 255).roundToInt(), newRed, newGreen, newBlue)
}
@BindingAdapter("imageUrl")
fun loadImage(view: ImageView, url: String?) {
if (url != null) {
view.load(url)
} else {
view.setImageDrawable(null)
}
}
@BindingAdapter("tooltipTextCompat")
fun setTooltipTextCompat(view: View, text: String) {
TooltipCompat.setTooltipText(view, text)
}
@BindingAdapter("tintNullable")
fun setImageTint(view: ImageView, @ColorInt tint: Int?) {
if (tint != null) {
view.imageTintList = ColorStateList.valueOf(tint)
} else {
view.imageTintList = null
}
}

View File

@@ -55,7 +55,8 @@ class ChargerIconGenerator(
val alpha: Int,
val highlight: Boolean,
val fault: Boolean,
val multi: Boolean
val multi: Boolean,
val fav: Boolean
)
// 230 items: (21 sizes, 5 colors, multi on/off) + highlight + fault (only with scale = 1)
@@ -66,6 +67,7 @@ class ChargerIconGenerator(
private val highlightIcon = R.drawable.ic_map_marker_highlight
private val highlightIconMulti = R.drawable.ic_map_marker_charging_highlight_multiple
private val faultIcon = R.drawable.ic_map_marker_fault
private val favIcon = R.drawable.ic_map_marker_fav
fun preloadCache() {
// pre-generates images for scale from 0 to 255 for all possible tint colors
@@ -79,12 +81,14 @@ class ChargerIconGenerator(
for (fault in listOf(false, true)) {
for (highlight in listOf(false, true)) {
for (multi in listOf(false, true)) {
for (tint in tints) {
for (scale in 0..scaleResolution) {
getBitmapDescriptor(
tint, scale.toFloat() / scaleResolution,
255, highlight, fault, multi
)
for (fav in listOf(false, true)) {
for (tint in tints) {
for (scale in 0..scaleResolution) {
getBitmapDescriptor(
tint, scale.toFloat() / scaleResolution,
255, highlight, fault, multi, fav
)
}
}
}
}
@@ -98,14 +102,16 @@ class ChargerIconGenerator(
alpha: Int = 255,
highlight: Boolean = false,
fault: Boolean = false,
multi: Boolean = false
multi: Boolean = false,
fav: Boolean = false
): BitmapDescriptor? {
val data = BitmapData(
tint, (scale * scaleResolution).roundToInt(),
alpha,
if (scale == 1f) highlight else false,
if (scale == 1f) fault else false,
multi
multi,
if (scale == 1f) fav else false
)
val cachedImg = cache[data]
return if (cachedImg != null) {
@@ -124,14 +130,16 @@ class ChargerIconGenerator(
alpha: Int = 255,
highlight: Boolean = false,
fault: Boolean = false,
multi: Boolean = false
multi: Boolean = false,
fav: Boolean = false
): Bitmap {
val data = BitmapData(
tint, (scale * scaleResolution).roundToInt(),
alpha,
if (scale == 1f) highlight else false,
if (scale == 1f) fault else false,
multi
multi,
if (scale == 1f) fav else false,
)
return generateBitmap(data)
}
@@ -200,6 +208,22 @@ class ChargerIconGenerator(
faultDrawable.draw(canvas)
}
if (data.fav) {
val favDrawable = ContextCompat.getDrawable(context, favIcon)!!
val favSize = 0.75
val favShiftY = 0.25
val favShiftX = if (data.fault) -0.5 else 0.25
val base = width
favDrawable.setBounds(
(leftPadding.toInt() + base * (1 - favSize + favShiftX)).toInt(),
(topPadding.toInt() - base * favShiftY).toInt(),
(leftPadding.toInt() + base * (1 + favShiftX)).toInt(),
(topPadding.toInt() + base * (favSize - favShiftY)).toInt()
)
favDrawable.alpha = data.alpha
favDrawable.draw(canvas)
}
return bm
}
}

View File

@@ -29,7 +29,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
tint: Int,
highlight: Boolean,
fault: Boolean,
multi: Boolean
multi: Boolean,
fav: Boolean
) {
animatingMarkers[marker]?.let {
it.cancel()
@@ -47,7 +48,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
scale = scale,
highlight = highlight,
fault = fault,
multi = multi
multi = multi,
fav = fav
)
)
}
@@ -66,7 +68,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
tint: Int,
highlight: Boolean,
fault: Boolean,
multi: Boolean
multi: Boolean,
fav: Boolean
) {
animatingMarkers[marker]?.let {
it.cancel()
@@ -84,7 +87,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
scale = scale,
highlight = highlight,
fault = fault,
multi = multi
multi = multi,
fav = fav
)
)
}

View File

@@ -4,7 +4,10 @@ import android.app.Application
import androidx.lifecycle.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import moe.banana.jsonapi2.HasMany
import moe.banana.jsonapi2.HasOne
import moe.banana.jsonapi2.JsonBuffer
import moe.banana.jsonapi2.ResourceIdentifier
import net.vonforst.evmap.api.chargeprice.*
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.model.ChargeLocation
@@ -100,7 +103,8 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
dataSource,
batteryRange,
batteryRangeSliderDragging,
vehicleCompatibleConnectors
vehicleCompatibleConnectors,
myTariffs, myTariffsAll
).forEach {
addSource(it) {
if (!batteryRangeSliderDragging.value!!) loadPrices()
@@ -208,7 +212,9 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
val car = vehicle.value
val compatibleConnectors = vehicleCompatibleConnectors.value
val dataSource = dataSource.value
if (charger == null || car == null || compatibleConnectors == null || dataSource == null) {
val myTariffs = myTariffs.value
val myTariffsAll = myTariffsAll.value
if (charger == null || car == null || compatibleConnectors == null || dataSource == null || myTariffs == null || myTariffsAll == null) {
chargePrices.value = Resource.error(null, null)
return
}
@@ -222,6 +228,19 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
dataAdapter = dataSource
station = cpStation
vehicle = HasOne(car)
tariffs = if (!myTariffsAll) {
HasMany<ChargepriceTariff>(*myTariffs.map {
ResourceIdentifier(
"tariff",
it
)
}.toTypedArray()).apply {
meta = JsonBuffer.create(
ChargepriceApi.moshi.adapter(ChargepriceRequestTariffMeta::class.java),
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS)
)
}
} else null
options = ChargepriceOptions(
batteryRange = batteryRange.value!!.map { it.toDouble() },
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,

View File

@@ -42,7 +42,7 @@ class FilterViewModel(application: Application) : AndroidViewModel(application)
MediatorLiveData<FilterProfile>().apply {
addSource(filterStatus) { id ->
when (id) {
FILTERS_CUSTOM, FILTERS_DISABLED -> value = null
FILTERS_CUSTOM, FILTERS_DISABLED, FILTERS_FAVORITES -> value = null
else -> viewModelScope.launch {
value = db.filterProfileDao().getProfileById(id, prefs.dataSource)
}

View File

@@ -1,12 +1,14 @@
package net.vonforst.evmap.viewmodel
import android.app.Application
import android.os.Parcelable
import androidx.lifecycle.*
import com.car2go.maps.AnyMap
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
@@ -23,10 +25,12 @@ import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.AppDatabase
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 java.io.IOException
data class MapPosition(val bounds: LatLngBounds, val zoom: Float)
@Parcelize
data class MapPosition(val bounds: LatLngBounds, val zoom: Float) : Parcelable
internal fun getClusterDistance(zoom: Float): Int? {
return when (zoom) {
@@ -38,7 +42,8 @@ internal fun getClusterDistance(zoom: Float): Int? {
}
}
class MapViewModel(application: Application) : AndroidViewModel(application) {
class MapViewModel(application: Application, private val state: SavedStateHandle) :
AndroidViewModel(application) {
val apiType: Class<ChargepointApi<ReferenceData>>
get() = api.javaClass
val apiName: String
@@ -49,11 +54,11 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
private var api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
val bottomSheetState: MutableLiveData<Int> by lazy {
MutableLiveData<Int>()
state.getLiveData("bottomSheetState")
}
val mapPosition: MutableLiveData<MapPosition> by lazy {
MutableLiveData<MapPosition>()
state.getLiveData("mapPosition")
}
val filterStatus: MutableLiveData<Long> by lazy {
MutableLiveData<Long>().apply {
@@ -124,7 +129,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
}
val chargerSparse: MutableLiveData<ChargeLocation> by lazy {
MutableLiveData<ChargeLocation>()
state.getLiveData("chargerSparse")
}
val chargerDetails: MediatorLiveData<Resource<ChargeLocation>> by lazy {
MediatorLiveData<Resource<ChargeLocation>>().apply {
@@ -222,7 +227,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
}
val searchResult: MutableLiveData<PlaceWithBounds> by lazy {
MutableLiveData<PlaceWithBounds>()
state.getLiveData("searchResult")
}
val mapType: MutableLiveData<AnyMap.Type> by lazy {
@@ -296,20 +301,32 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
viewModelScope
) { data: Triple<MapPosition, FilterValues, ReferenceData> ->
chargepoints.value = Resource.loading(chargepoints.value?.data)
filteredConnectors.value = null
filteredMinPower.value = null
filteredChargeCards.value = null
val mapPosition = data.first
val filters = data.second
val api = api
val refData = data.third
var result = api.getChargepoints(refData, mapPosition.bounds, mapPosition.zoom, filters)
if (result.status == Status.ERROR && result.data == null) {
// keep old results if new data could not be loaded
result = Resource.error(result.message, chargepoints.value?.data)
if (filterStatus.value == FILTERS_FAVORITES) {
// load favorites from local DB
val b = mapPosition.bounds
var chargers = db.chargeLocationsDao().getChargeLocationsInBoundsAsync(
b.southwest.latitude,
b.northeast.latitude,
b.southwest.longitude,
b.northeast.longitude
) as List<ChargepointListItem>
val clusterDistance = getClusterDistance(mapPosition.zoom)
clusterDistance?.let {
chargers = cluster(chargers, mapPosition.zoom, clusterDistance)
}
filteredConnectors.value = null
filteredMinPower.value = null
filteredChargeCards.value = null
chargepoints.value = Resource.success(chargers)
return@throttleLatest
}
chargepoints.value = result
if (api is GoingElectricApiWrapper) {
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
@@ -333,7 +350,19 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
)
}.toSet()
filteredMinPower.value = filters.getSliderValue("minPower")
} else {
filteredConnectors.value = null
filteredMinPower.value = null
filteredChargeCards.value = null
}
var result = api.getChargepoints(refData, mapPosition.bounds, mapPosition.zoom, filters)
if (result.status == Status.ERROR && result.data == null) {
// keep old results if new data could not be loaded
result = Resource.error(result.message, chargepoints.value?.data)
}
chargepoints.value = result
}
private suspend fun loadAvailability(charger: ChargeLocation) {

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/background"
android:drawable="@color/chip_background" />
<item android:drawable="?selectableItemBackground" />
</layer-list>

View File

@@ -1,4 +1,5 @@
<vector android:height="15.811624dp" android:viewportHeight="131.5"
android:tint="?attr/colorControlNormal"
android:viewportWidth="199.6" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M197.544,65.685l-9.2,-4.8l8,-4.2c2.7,-1.4 2.7,-3.8 0,-5.2l-8.6,-4.5l8.6,-4.5c2.7,-1.4 2.7,-3.8 0,-5.2l-68.9,-36.1c-2.7,-1.4 -7.2,-1.4 -9.9,0l-115.5,59.7c-2.7,1.4 -2.7,3.7 0,5.1l8.8,4.5l-8.8,4.6c-2.7,1.4 -2.7,3.7 0,5.1l9.4,4.8l-8.2,4.3c-2.7,1.4 -2.7,3.7 0,5.1l70.4,36.2c2.7,1.4 7.2,1.4 9.9,0l114,-59.6C200.344,69.385 200.344,67.085 197.544,65.685L197.544,65.685zM123.144,18.785L105.844,38.685c-0.9,1 -0.6,2.3 0.6,2.9l13.7,7.1c1.2,0.6 1.2,1.6 0,2.2l-43.1,22.3c-1.2,0.6 -1.4,0.3 -0.6,-0.7l17.3,-19.9c0.9,-1 0.6,-2.3 -0.6,-2.9l-13.7,-7.1c-1.2,-0.6 -1.2,-1.6 0,-2.2l43.1,-22.3C123.744,17.485 123.944,17.785 123.144,18.785L123.144,18.785z"/>
</vector>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF4081"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z" />
</vector>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#000"
android:pathData="M12,3C7.58,3 4,4.79 4,7C4,9.21 7.58,11 12,11C12.5,11 13,10.97 13.5,10.92V9.5H16.39L15.39,8.5L18.9,5C17.5,3.8 14.94,3 12,3M18.92,7.08L17.5,8.5L20,11H15V13H20L17.5,15.5L18.92,16.92L23.84,12M4,9V12C4,14.21 7.58,16 12,16C13.17,16 14.26,15.85 15.25,15.63L16.38,14.5H13.5V12.92C13,12.97 12.5,13 12,13C7.58,13 4,11.21 4,9M4,14V17C4,19.21 7.58,21 12,21C14.94,21 17.5,20.2 18.9,19L17,17.1C15.61,17.66 13.9,18 12,18C7.58,18 4,16.21 4,14Z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M7,14c-1.66,0 -3,1.34 -3,3 0,1.31 -1.16,2 -2,2 0.92,1.22 2.49,2 4,2 2.21,0 4,-1.79 4,-4 0,-1.66 -1.34,-3 -3,-3zM20.71,4.63l-1.34,-1.34c-0.39,-0.39 -1.02,-0.39 -1.41,0L9,12.25 11.75,15l8.96,-8.96c0.39,-0.39 0.39,-1.02 0,-1.41z" />
</vector>

View File

@@ -1,13 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="160dp"
android:height="160dp"
android:viewportWidth="120"
android:viewportHeight="120">
android:width="192dp"
android:height="192dp"
android:viewportWidth="192"
android:viewportHeight="192">
<group android:name="_R_G">
<group
android:name="_R_G_L_2_G_N_1_T_0"
android:translateX="58"
android:translateY="60">
android:translateX="94"
android:translateY="96">
<group
android:name="_R_G_L_2_G"
android:pivotX="53.625"
@@ -43,8 +43,8 @@
</group>
<group
android:name="_R_G_L_1_G_N_1_T_0"
android:translateX="58"
android:translateY="60">
android:translateX="94"
android:translateY="96">
<group
android:name="_R_G_L_1_G"
android:translateX="-26.049"
@@ -77,8 +77,8 @@
</group>
<group
android:name="_R_G_L_0_G_N_1_T_0"
android:translateX="58"
android:translateY="60">
android:translateX="94"
android:translateY="96">
<group
android:name="_R_G_L_0_G"
android:translateX="-1.3999999999999995"

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/chip_background" />
<item android:drawable="?selectableItemBackground" />
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/chip_background" />
<item android:drawable="?selectableItemBackground" />
</layer-list>

View File

@@ -317,7 +317,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/go_to_chargeprice"
app:goneUnless="@{charger.data != null &amp;&amp; ChargepriceApi.isCountrySupported(charger.data.chargepriceData.country, charger.data.dataSource)}"
app:goneUnless="@{charger.data != null &amp;&amp; charger.data.chargepriceData != null &amp;&amp; charger.data.chargepriceData.country != null &amp;&amp; ChargepriceApi.isCountrySupported(charger.data.chargepriceData.country, charger.data.dataSource)}"
app:icon="@drawable/ic_chargeprice"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"

View File

@@ -37,17 +37,16 @@
android:paddingTop="8dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
app:myTariffsBackground="@{!myTariffsAll &amp;&amp; myTariffs.contains(item.tariff.get().id)}">
android:background="@{BindingAdaptersKt.tariffBackground(context,!myTariffsAll &amp;&amp; myTariffs.contains(item.tariff.get().id), item.branding.backgroundColor)}">
<TextView
android:id="@+id/txtTariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@{item.tariffName}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintBottom_toTopOf="@+id/txtProvider"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
@@ -57,12 +56,11 @@
android:id="@+id/txtProvider"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@{item.provider}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{!item.tariffName.toLowerCase().startsWith(item.provider.toLowerCase())}"
app:layout_constraintBottom_toTopOf="@+id/rvTags"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtTariff"
tools:text="Cheap Charging Co." />
@@ -71,11 +69,10 @@
android:id="@+id/rvTags"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:nestedScrollingEnabled="false"
app:data="@{item.tags}"
app:layout_constraintBottom_toTopOf="@+id/txtProviderCustomerTariff"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtProvider"
tools:itemCount="1"
@@ -85,12 +82,12 @@
android:id="@+id/txtProviderCustomerTariff"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@string/chargeprice_provider_customer_tariff"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{item.providerCustomerTariff}"
app:layout_constraintBottom_toTopOf="@id/txtMonthlyFee"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/rvTags" />
@@ -98,12 +95,11 @@
android:id="@+id/txtMonthlyFee"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:text="@{item.formatMonthlyFees(context)}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{item.totalMonthlyFee > 0 || item.monthlyMinSales > 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
app:layout_constraintStart_toStartOf="@+id/txtTariff"
app:layout_constraintTop_toBottomOf="@+id/txtProviderCustomerTariff"
tools:text="Base fee 1 €/month" />
@@ -160,5 +156,19 @@
android:orientation="vertical"
app:layout_constraintGuide_percent="0.65" />
<ImageView
android:id="@+id/ivLogo"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="8dp"
android:scaleType="fitCenter"
app:goneUnless="@{item.branding.logoUrl != null}"
app:imageUrl="@{item.branding.logoUrl}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline5"
app:layout_constraintTop_toTopOf="parent"
app:tintNullable="@{BindingAdaptersKt.isDarkMode(context) ? @android:color/white : null}"
tools:srcCompat="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -37,7 +37,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="100dp"
android:text="@{hours.getHoursForDayOfWeek(dayOfWeek).toString().equals(&quot;closed&quot;) ? @string/closed_unfmt : hours.getHoursForDayOfWeek(dayOfWeek).toString()}"
android:text="@{hours.getHoursForDayOfWeek(dayOfWeek) == null ? @string/closed_unfmt : hours.getHoursForDayOfWeek(dayOfWeek).toString()}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -2,6 +2,12 @@
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_help"
android:icon="@drawable/ic_help"
android:title="@string/help"
app:showAsAction="always" />
<item
android:id="@+id/menu_close"
android:icon="@drawable/ic_close"

View File

@@ -33,10 +33,24 @@
<action
android:id="@+id/action_map_to_opensource_donations"
app:destination="@id/opensource_donations" />
<argument
android:name="locationName"
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
<argument
android:name="chargerId"
android:defaultValue="0L"
app:argType="long" />
<argument
android:name="latLng"
android:defaultValue="@null"
app:argType="com.car2go.maps.model.LatLng"
app:nullable="true" />
</fragment>
<fragment
android:id="@+id/about"
android:name="net.vonforst.evmap.fragment.AboutFragment"
android:name="net.vonforst.evmap.fragment.preference.AboutFragment"
android:label="@string/about"
tools:layout="@layout/fragment_preference">
<action
@@ -48,9 +62,24 @@
</fragment>
<fragment
android:id="@+id/settings"
android:name="net.vonforst.evmap.fragment.SettingsFragment"
android:name="net.vonforst.evmap.fragment.preference.SettingsFragment"
android:label="@string/settings"
tools:layout="@layout/fragment_preference" />
<fragment
android:id="@+id/settings_ui"
android:name="net.vonforst.evmap.fragment.preference.UiSettingsFragment"
android:label="@string/settings_ui"
tools:layout="@layout/fragment_preference" />
<fragment
android:id="@+id/settings_data"
android:name="net.vonforst.evmap.fragment.preference.DataSettingsFragment"
android:label="@string/settings_data_sources"
tools:layout="@layout/fragment_preference" />
<fragment
android:id="@+id/settings_chargeprice"
android:name="net.vonforst.evmap.fragment.preference.ChargepriceSettingsFragment"
android:label="@string/settings_chargeprice"
tools:layout="@layout/fragment_preference" />
<fragment
android:id="@+id/favs"
android:name="net.vonforst.evmap.fragment.FavoritesFragment"
@@ -82,6 +111,12 @@
app:enterAnim="@animator/nav_default_enter_anim"
app:popEnterAnim="@animator/nav_default_enter_anim"
app:popExitAnim="@animator/nav_default_exit_anim" />
<argument
android:name="charger"
app:argType="net.vonforst.evmap.model.ChargeLocation" />
<argument
android:name="dataSource"
app:argType="string" />
</dialog>
<fragment
android:id="@+id/donate"

View File

@@ -17,6 +17,8 @@
<string name="closed_opensat"><![CDATA[<b>Geschlossen</b> · Öffnet um %s]]></string>
<string name="cost">Kosten</string>
<string name="cost_detail"><![CDATA[<b>Laden:</b> %s · <b>Parken:</b> %s]]></string>
<string name="cost_detail_charging"><![CDATA[<b>%s laden</b>]]></string>
<string name="cost_detail_parking"><![CDATA[<b>%s parken</b>]]></string>
<string name="free">Kostenlos</string>
<string name="paid">Kostenpflichtig</string>
<string name="amenities">Ladeweile</string>
@@ -143,6 +145,7 @@
<string name="menu_save_profile">Als Profil speichern</string>
<string name="no_filters">Keine Filter</string>
<string name="filter_custom">Verändertes Filterprofil</string>
<string name="filter_favorites">Favoriten</string>
<string name="reorder">Reihenfolge ändern</string>
<string name="delete">Löschen</string>
<string name="save_as_profile">Als Profil speichern</string>
@@ -191,7 +194,7 @@
<string name="chargeprice_vehicle">Fahrzeug</string>
<string name="edit_on_goingelectric_info">Falls hier nur eine leere Seite erscheint, logge dich bitte zuerst bei GoingElectric.de ein.</string>
<string name="close">schließen</string>
<string name="chargeprice_title">Preisvergleich</string>
<string name="chargeprice_title">Preise</string>
<string name="chargeprice_connection_error">Preise konnten nicht geladen werden</string>
<string name="chargeprice_no_compatible_connectors">Keiner der Anschlüsse dieser Ladestation ist mit deinem Fahrzeug kompatibel.</string>
<string name="pref_chargeprice_currency">Währung</string>
@@ -240,8 +243,11 @@
<string name="unnamed_filter_profile">Unbenanntes Filterprofil</string>
<string name="privacy_link">https://evmap.vonforst.net/de/privacy.html</string>
<string name="faq_link">https://evmap.vonforst.net/de/faq.html</string>
<string name="chargeprice_faq_link">https://evmap.vonforst.net/de/chargeprice_faq.html</string>
<string name="required">erforderlich</string>
<string name="edit_filter_profile">„%s“ bearbeiten</string>
<string name="pref_search_delete_recent">Suchverlauf löschen</string>
<string name="deleted_recent_search_results">Suchverlauf wurde gelöscht</string>
<string name="settings_data_sources">Datenquellen</string>
<string name="help">Hilfe</string>
</resources>

View File

@@ -16,6 +16,8 @@
<string name="holiday">Holiday</string>
<string name="cost">Cost</string>
<string name="cost_detail"><![CDATA[<b>Charging:</b> %s · <b>Parking:</b> %s]]></string>
<string name="cost_detail_charging"><![CDATA[<b>%s charging</b>]]></string>
<string name="cost_detail_parking"><![CDATA[<b>%s parking</b>]]></string>
<string name="free">Free</string>
<string name="paid">Paid</string>
<string name="amenities">Amenities</string>
@@ -142,6 +144,7 @@
<string name="menu_save_profile">Save as profile</string>
<string name="no_filters">No filters</string>
<string name="filter_custom">Modified filter</string>
<string name="filter_favorites">Favorites</string>
<string name="reorder">reorder</string>
<string name="delete">Delete</string>
<string name="save_as_profile">Save as profile</string>
@@ -225,8 +228,11 @@
<string name="unnamed_filter_profile">Unnamed filter profile</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">required</string>
<string name="edit_filter_profile">Edit “%s”</string>
<string name="pref_search_delete_recent">Delete recent search results</string>
<string name="deleted_recent_search_results">Recent search results have been deleted</string>
<string name="settings_data_sources">Data sources</string>
<string name="help">Help</string>
</resources>

View File

@@ -1,91 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory android:title="@string/settings_ui">
<ListPreference
android:key="language"
android:title="@string/pref_language"
android:entries="@array/pref_language_names"
android:entryValues="@array/pref_language_values"
android:defaultValue="default"
android:summary="@string/pref_language_summary" />
<ListPreference
android:key="darkmode"
android:title="@string/pref_darkmode"
android:entries="@array/pref_darkmode_names"
android:entryValues="@array/pref_darkmode_values"
android:defaultValue="default"
android:summary="@string/pref_darkmode_summary" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_charger_data">
<net.vonforst.evmap.ui.DataSourceSelectDialogPreference
android:key="data_source"
android:title="@string/pref_data_source"
android:entries="@array/pref_data_source_names"
android:entryValues="@array/pref_data_source_values"
android:defaultValue="goingelectric"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_map">
<ListPreference
android:key="map_provider"
android:title="@string/pref_map_provider"
android:entries="@array/pref_map_provider_names"
android:entryValues="@array/pref_map_provider_values"
android:defaultValue="@string/pref_map_provider_default"
android:summary="%s" />
<ListPreference
android:key="search_provider"
android:title="@string/pref_search_provider"
android:entries="@array/pref_search_provider_names"
android:entryValues="@array/pref_search_provider_values"
android:defaultValue="@string/pref_search_provider_default"
android:summary="%s" />
<Preference
android:key="search_delete_recent"
android:title="@string/pref_search_delete_recent" />
<CheckBoxPreference
android:key="navigate_use_maps"
android:title="@string/pref_navigate_use_maps"
android:summaryOn="@string/pref_navigate_use_maps_on"
android:summaryOff="@string/pref_navigate_use_maps_off"
android:defaultValue="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_chargeprice">
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_vehicle"
android:title="@string/pref_my_vehicle"
app:showAllButton="false"
app:defaultToAll="false" />
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_tariffs"
android:title="@string/pref_my_tariffs"
android:summary="@string/pref_my_tariffs_summary" />
<ListPreference
android:key="chargeprice_currency"
android:title="@string/pref_chargeprice_currency"
android:entries="@array/pref_chargeprice_currency_names"
android:entryValues="@array/pref_chargeprice_currency_values"
android:defaultValue="EUR"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference
android:key="chargeprice_no_base_fee"
android:title="@string/pref_chargeprice_no_base_fee"
android:defaultValue="false"
app:singleLineTitle="false" />
<CheckBoxPreference
android:key="chargeprice_show_provider_customer_tariffs"
android:title="@string/pref_chargeprice_show_provider_customer_tariffs"
android:summary="@string/pref_chargeprice_show_provider_customer_tariffs_summary"
android:defaultValue="false"
app:singleLineTitle="false" />
</PreferenceCategory>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:fragment="net.vonforst.evmap.fragment.preference.UiSettingsFragment"
android:title="@string/settings_ui"
android:icon="@drawable/ic_settings_ui" />
<Preference
android:fragment="net.vonforst.evmap.fragment.preference.DataSettingsFragment"
android:title="@string/settings_data_sources"
android:icon="@drawable/ic_settings_data_source" />
<Preference
android:fragment="net.vonforst.evmap.fragment.preference.ChargepriceSettingsFragment"
android:title="@string/settings_chargeprice"
android:icon="@drawable/ic_chargeprice" />
</PreferenceScreen>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_vehicle"
android:title="@string/pref_my_vehicle"
app:showAllButton="false"
app:defaultToAll="false" />
<net.vonforst.evmap.ui.MultiSelectDialogPreference
android:key="chargeprice_my_tariffs"
android:title="@string/pref_my_tariffs"
android:summary="@string/pref_my_tariffs_summary" />
<ListPreference
android:key="chargeprice_currency"
android:title="@string/pref_chargeprice_currency"
android:entries="@array/pref_chargeprice_currency_names"
android:entryValues="@array/pref_chargeprice_currency_values"
android:defaultValue="EUR"
app:useSimpleSummaryProvider="true" />
<CheckBoxPreference
android:key="chargeprice_no_base_fee"
android:title="@string/pref_chargeprice_no_base_fee"
android:defaultValue="false"
app:singleLineTitle="false" />
<CheckBoxPreference
android:key="chargeprice_show_provider_customer_tariffs"
android:title="@string/pref_chargeprice_show_provider_customer_tariffs"
android:summary="@string/pref_chargeprice_show_provider_customer_tariffs_summary"
android:defaultValue="false"
app:singleLineTitle="false" />
</PreferenceScreen>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory android:title="@string/settings_charger_data">
<net.vonforst.evmap.ui.DataSourceSelectDialogPreference
android:key="data_source"
android:title="@string/pref_data_source"
android:entries="@array/pref_data_source_names"
android:entryValues="@array/pref_data_source_values"
android:defaultValue="goingelectric"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_map">
<ListPreference
android:key="map_provider"
android:title="@string/pref_map_provider"
android:entries="@array/pref_map_provider_names"
android:entryValues="@array/pref_map_provider_values"
android:defaultValue="@string/pref_map_provider_default"
android:summary="%s" />
<ListPreference
android:key="search_provider"
android:title="@string/pref_search_provider"
android:entries="@array/pref_search_provider_names"
android:entryValues="@array/pref_search_provider_values"
android:defaultValue="@string/pref_search_provider_default"
android:summary="%s" />
<Preference
android:key="search_delete_recent"
android:title="@string/pref_search_delete_recent" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
android:key="language"
android:title="@string/pref_language"
android:entries="@array/pref_language_names"
android:entryValues="@array/pref_language_values"
android:defaultValue="default"
android:summary="@string/pref_language_summary" />
<ListPreference
android:key="darkmode"
android:title="@string/pref_darkmode"
android:entries="@array/pref_darkmode_names"
android:entryValues="@array/pref_darkmode_values"
android:defaultValue="default"
android:summary="@string/pref_darkmode_summary" />
<CheckBoxPreference
android:key="navigate_use_maps"
android:title="@string/pref_navigate_use_maps"
android:summaryOn="@string/pref_navigate_use_maps_on"
android:summaryOff="@string/pref_navigate_use_maps_off"
android:defaultValue="true" />
</PreferenceScreen>

View File

@@ -8,13 +8,16 @@ class AvailabilityDetectorTest {
@Test
fun testMatchChargepointsSingleCorrect() {
// single charger with 2 22kW chargepoints
val chargepoints = listOf(Chargepoint("Typ2", 22.0, 2))
val chargepoints = listOf(Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2))
// correct data in NewMotion
assertEquals(
mapOf(chargepoints[0] to setOf(0L, 1L)),
BaseAvailabilityDetector.matchChargepoints(
mapOf(0L to (22.0 to "Typ2"), 1L to (22.0 to "Typ2")),
mapOf(
0L to (22.0 to Chargepoint.TYPE_2_UNKNOWN),
1L to (22.0 to Chargepoint.TYPE_2_UNKNOWN)
),
chargepoints
)
)
@@ -23,13 +26,16 @@ class AvailabilityDetectorTest {
@Test
fun testMatchChargepointsSingleWrongPower() {
// single charger with 2 22kW chargepoints
val chargepoints = listOf(Chargepoint("Typ2", 22.0, 2))
val chargepoints = listOf(Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2))
// wrong power in NewMotion
assertEquals(
mapOf(chargepoints[0] to setOf(0L, 1L)),
BaseAvailabilityDetector.matchChargepoints(
mapOf(0L to (27.0 to "Typ2"), 1L to (27.0 to "Typ2")),
mapOf(
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
),
chargepoints
)
)
@@ -38,11 +44,15 @@ class AvailabilityDetectorTest {
@Test(expected = AvailabilityDetectorException::class)
fun testMatchChargepointsSingleWrong() {
// single charger with 2 22kW chargepoints
val chargepoints = listOf(Chargepoint("Typ2", 22.0, 2))
val chargepoints = listOf(Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2))
// non-matching data in NewMotion
BaseAvailabilityDetector.matchChargepoints(
mapOf(0L to (27.0 to "Typ2"), 1L to (27.0 to "Typ2"), 2L to (50.0 to "CCS")),
mapOf(
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
2L to (50.0 to Chargepoint.CCS_UNKNOWN)
),
chargepoints
)
}
@@ -51,11 +61,11 @@ class AvailabilityDetectorTest {
fun testMatchChargepointsComplex() {
// charger with many different connectors
val chargepoints = listOf(
Chargepoint("Typ2", 43.0, 1),
Chargepoint("CCS", 50.0, 1),
Chargepoint("CHAdeMO", 50.0, 2),
Chargepoint("CCS", 160.0, 1),
Chargepoint("CCS", 320.0, 2)
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 43.0, 1),
Chargepoint(Chargepoint.CCS_UNKNOWN, 50.0, 1),
Chargepoint(Chargepoint.CHADEMO, 50.0, 2),
Chargepoint(Chargepoint.CCS_UNKNOWN, 160.0, 1),
Chargepoint(Chargepoint.CCS_UNKNOWN, 320.0, 2)
)
// partly wrong power in NewMotion
@@ -70,15 +80,15 @@ class AvailabilityDetectorTest {
BaseAvailabilityDetector.matchChargepoints(
mapOf(
// CHAdeMO + CCS HPC
0L to (50.0 to "CHAdeMO"),
1L to (200.0 to "CCS"),
0L to (50.0 to Chargepoint.CHADEMO),
1L to (200.0 to Chargepoint.CCS_UNKNOWN),
// dual CCS HPC
2L to (80.0 to "CCS"),
3L to (200.0 to "CCS"),
2L to (80.0 to Chargepoint.CCS_UNKNOWN),
3L to (200.0 to Chargepoint.CCS_UNKNOWN),
// 50kW triple charger
4L to (50.0 to "CCS"),
5L to (50.0 to "CHAdeMO"),
6L to (43.0 to "Typ2")
4L to (50.0 to Chargepoint.CCS_UNKNOWN),
5L to (50.0 to Chargepoint.CHADEMO),
6L to (43.0 to Chargepoint.TYPE_2_UNKNOWN)
),
chargepoints
)
@@ -89,15 +99,18 @@ class AvailabilityDetectorTest {
fun testMatchChargepointsDifferentPower() {
// single charger with 1 22kW and 1 11kW chargepoint (common when load balancing is applied)
val chargepoints = listOf(
Chargepoint("Typ2", 22.0, 1),
Chargepoint("Typ2", 11.0, 1)
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 1),
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 11.0, 1)
)
// both have 27 kW power in NewMotion
assertEquals(
mapOf(chargepoints[1] to setOf(0L), chargepoints[0] to setOf(1L)),
BaseAvailabilityDetector.matchChargepoints(
mapOf(0L to (27.0 to "Typ2"), 1L to (27.0 to "Typ2")),
mapOf(
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
),
chargepoints
)
)
@@ -107,8 +120,8 @@ class AvailabilityDetectorTest {
fun testMatchChargepointsDifferentPower2() {
// two chargers with 1 22kW and 1 11kW chargepoint (common when load balancing is applied)
val chargepoints = listOf(
Chargepoint("Typ2", 22.0, 2),
Chargepoint("Typ2", 11.0, 2)
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2),
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 11.0, 2)
)
// both have 27 kW power in NewMotion
@@ -116,10 +129,54 @@ class AvailabilityDetectorTest {
mapOf(chargepoints[1] to setOf(0L, 1L), chargepoints[0] to setOf(2L, 3L)),
BaseAvailabilityDetector.matchChargepoints(
mapOf(
0L to (27.0 to "Typ2"),
1L to (27.0 to "Typ2"),
2L to (27.0 to "Typ2"),
3L to (27.0 to "Typ2")
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
2L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
3L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
),
chargepoints
)
)
}
@Test
fun testMatchChargepointsMissingSchuko() {
// single charger with 2 22kw chargepoints and two Schuko sockets
val chargepoints = listOf(
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2),
Chargepoint(Chargepoint.SCHUKO, 2.3, 2)
)
// NewMotion only includes the Type 2 sockets
assertEquals(
mapOf(chargepoints[0] to setOf(0L, 1L)),
BaseAvailabilityDetector.matchChargepoints(
mapOf(
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
),
chargepoints
)
)
}
@Test
fun testMatchChargepointsMissingSchukoDifferentPower() {
// single charger with 2 22kw chargepoints with load balancing and two Schuko sockets
val chargepoints = listOf(
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 1),
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 11.0, 1),
Chargepoint(Chargepoint.SCHUKO, 2.3, 2)
)
// NewMotion only includes the Type 2 sockets
assertEquals(
mapOf(chargepoints[1] to setOf(0L), chargepoints[0] to setOf(1L)),
BaseAvailabilityDetector.matchChargepoints(
mapOf(
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
),
chargepoints
)

View File

@@ -1,16 +1,16 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.5.20'
ext.about_libs_version = '8.8.5'
ext.nav_version = '2.3.5'
ext.kotlin_version = '1.5.31'
ext.about_libs_version = '8.9.4'
ext.nav_version = '2.4.0-beta02'
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.2'
classpath 'com.android.tools.build:gradle:7.0.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
@@ -27,10 +27,6 @@ allprojects {
//noinspection JcenterRepositoryObsolete
jcenter() // still required for https://github.com/kamikat/moshi-jsonapi
maven { url 'https://jitpack.io' }
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
flatDir {
dirs 'libs'
}
}
}

75
doc/android_auto.md Normal file
View File

@@ -0,0 +1,75 @@
Testing EVMap on Android Auto
=============================
In addition to the Android app on the phone, EVMap is also available as an Android Auto app built
using the [Android for Cars App Library](https://developer.android.com/training/cars/apps). The
Android Auto app is only available in the `google` build flavor of the app, and thus its code is
located in the `app/src/google/java` directory under the `net.vonforst.evmap.auto` package.
This page contains instructions on how to test the Android Auto app using the Desktop Head Unit
(DHU).
Further information about testing Android Auto apps is also available on the
[Android Developers site](https://developer.android.com/training/cars/testing).
Install the Desktop Head Unit
-----------------------------
Refer to the instructions on the
[Android Developers site](https://developer.android.com/training/cars/testing#install)
to install the DHU 2.0 using the SDK manager.
Install Android Auto
--------------------
If you haven't already, install the
[Android Auto](https://play.google.com/store/apps/details?id=com.google.android.projection.gearhead)
and
[Android Auto for phone screens](https://play.google.com/store/apps/details?id=com.google.android.projection.gearhead.phonescreen)
apps on your test device from the Google Play Store.
If you are using the Android Emulator, the Play Store may show the Android Auto app as incompatible.
In that case, download the APK for the newest version from a site like
[APKMirror](https://www.apkmirror.com/apk/google-inc/android-auto/)
(choosing the correct architecture for your emulator - x86_64, x86 or ARM)
and drag it onto the running emulator window to install.
Starting the DHU
----------------
(see also the corresponding section on
the [Android Developers site](https://developer.android.com/training/cars/testing#running-dhu))
1. Start the Android Auto for phone screens app, tap the menu icon on the top left to go to settings
2. Scroll all the way down to the app version, tap it 10 times
3. Click *OK* in the dialog that appears to enable developer mode
4. In the menu on the top left, tap *Start head unit server*
5. On your computer, run the following command to set up the required port forwarding:
```shell
adb forward tcp:5277 tcp:5277
```
6. Start the DHU by running the command `desktop-head-unit.exe` (on Windows) or
`./desktop-head-unit` (on macOS or Linux) in a console window from the
`SDK_LOCATION/extras/google/auto/` directory.
The desktop head unit should appear and show the Android Auto interface. If this is the first time
the Android device is connected to the DHU, you may need to open the Android Auto app again on the
phone to accept some permissions before the connection can succeed.
Testing EVMap on the DHU
------------------------
Make sure that you have selected the `googleDebug` variant in the *Build Variants* tool window in
Android Studio (the `foss` variants do not contain the Android Auto app). Then, install the app on
your phone - if the DHU is connected, the app should also automatically appear in the apps menu on
Android Auto.
For testing features that require car sensors, you need to start the DHU with the option
`-c config/default_sensors.ini` to select a configuration file that enables these sensors. From the
console, you can then type certain commands to update the data of these sensors, such as:
```shell
location 54.0 9.0 # latitude, longitude
fuel 50 # percentage
range 100 # in kilometers
speed 28 # in m/s
```

165
doc/api_keys.md Normal file
View File

@@ -0,0 +1,165 @@
API keys required for testing EVMap
===================================
EVMap uses multiple different data sources, most of which require an API key. These API keys need to
be put into the app in the form of a resource file called `apikeys.xml` under
`app/src/main/res/values`, with the following content:
<details>
<summary>apikeys.xml content</summary>
```xml
<resources>
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">
insert your Google Maps key here
</string>
<string name="mapbox_key" translatable="false">
insert your Mapbox key here
</string>
<string name="goingelectric_key" translatable="false">
insert your GoingElectric key here
</string>
<string name="chargeprice_key" translatable="false">
insert your Chargeprice key here
</string>
<string name="openchargemap_key" translatable="false">
insert your OpenChargeMap key here
</string>
</resources>
```
</details>
Not all API keys are strictly required if you only want to work on certain parts of the app. For
example, you can choose only one of the map providers and one of the charging station databases. The
Chargeprice API key is also only required if you want to test the price comparison feature.
All API keys are available for free. Some APIs require payment above a certain limit, but the free
tier should be plenty for local testing and development.
Below you find a list of all the services and how to obtain the API keys.
Map providers
-------------
The different Map SDKs are wrapped by our [fork](https://github.com/johan12345/AnyMaps) of the
[AnyMaps](https://github.com/sharenowTech/AnyMaps) library to provide a common API. The `google`
build flavor of the app includes both Google Maps and Mapbox and allows the user to switch between
the two, while the `foss` flavor only includes the Mapbox SDK.
> ⚠️ When testing the app using the Android Emulator, we recommend using Google Maps and not Mapbox, as the latter has
[issues displaying the markers](https://github.com/mapbox/mapbox-gl-native/issues/10829). It works fine on real Android devices.
### Google Maps
[Maps SDK for Android](https://developers.google.com/maps/documentation/android-sdk/overview),
[Places API](https://developers.google.com/maps/documentation/places/android-sdk/overview)
<details>
<summary>How to obtain an API key</summary>
1. Log in to the [Google API console](https://console.developers.google.com/) with your Google
account
2. Create a new project, or select an existing one that you want to use
3. Under *APIs & Services → Library*, enable
the [Maps SDK for Android](https://console.cloud.google.com/apis/library/maps-android-backend.googleapis.com)
and [Places API](https://console.cloud.google.com/apis/library/places-backend.googleapis.com).
4. Under *APIs & Services → Credentials*, click on *Create credentials → API Key*
5. Copy the displayed key to your `apikeys.xml` file.
</details>
### Mapbox
[Maps SDK for Android](https://docs.mapbox.com/android/maps)
<details>
<summary>How to obtain an API key</summary>
1. [Sign up](https://account.mapbox.com/auth/signup) for a Mapbox account
2. Under [Access Tokens](https://account.mapbox.com/access-tokens/), create a new access token
3. Set a name for the scope and enable only the preselected public scopes. Do not restrict the token
to a specific URL (this setting is not compatible with Android apps)
</details>
Charging station databases
--------------------------
### **GoingElectric.de**
GoingElectric.de provides an [API](https://www.goingelectric.de/stromtankstellen/api/) for their
community-maintained directory of charging stations. The website and data are mostly only available
in German.
<details>
<summary>How to obtain an API key</summary>
1. [Sign up](https://www.goingelectric.de/forum/ucp.php?mode=register) for an account in the
GoingElectric.de forum. The registration page can be switched to English using the dropdown menu
under "Sprache". Then, agree to the registration terms.
2. Fill in your desired username, password and email address and submit the registration form. You
do not need to fill the information under *GoingElectric Usermap*.
3. Verify your account by clicking on the link in the email you received
4. [Log in](https://www.goingelectric.de/forum/ucp.php?mode=login) to the GoingElectric forum
5. Go to [this link](https://www.goingelectric.de/stromtankstellen/api/new/) to request access to
the API. This page is only available in German. You need to fill in the following data:
- name / company (*Name / Firma*)
- street address (*Straße, Nr.*)
- postal code, town (*Postleitzahl, Ort*)
- country (*Land*)
- email address (*E-Mail Adresse*)
- website (*Webseite*, optional)
- phone number (*Telefonnummer*, optional)
- name of the app (*Name der App*): EVMap
- app website (*Webseite der App*): https://github.com/johan12345/EVMap
- description (*kurze Beschreibung der App*): please explain that you would like to contribute to
the development of EVMap and therefore need access to the GoingElectric.de API.
- Referrer (*Herkunft*): leave this field blank!
6. When your access to the API is approved, you can access the
[API console](https://www.goingelectric.de/stromtankstellen/api/ucp/) to retrieve your API key.
</details>
### **OpenChargeMap**
[API documentation](https://openchargemap.org/site/develop/api)
<details>
<summary>How to obtain an API key</summary>
1. [Sign up](https://openchargemap.org/site/loginprovider/register) for an account at OpenChargeMap
2. Go to the [My Apps](https://openchargemap.org/site/profile/applications) page and click
*Register an application*
3. Enter the name of the app (EVMap) and website (https://github.com/johan12345/EVMap), and in the
description field describe that you would like to contribute to the development of EVMap and
therefore need access to the OpenChargeMap API. Do not tick the *List App in Public Showcase*
box. Then, click *save*.
4. Your API key will appear on the
[My Apps](https://openchargemap.org/site/profile/applications) page.
</details>
Pricing providers
-----------------
### Chargeprice.app
[API documentation](https://github.com/chargeprice/chargeprice-api-docs)
<details>
<summary>How to obtain an API key</summary>
1. Check the
[Pricing page](https://github.com/chargeprice/chargeprice-api-docs/blob/master/plans.md)
for information on the current plans at Chargeprice. There should be a free tier up to a certain
limit of API calls per month.
2. Contact [contact@chargeprice.net](mailto:contact@chargeprice.net), stating that you would like to
contribute to the development the open source EVMap app and therefore need access to the
Chargeprice API for testing.
3. When your access to the API is approved, you will receive an API key via email.
</details>

View File

@@ -1,3 +1,3 @@
Verbesserungen:
- Kleinere Verbesserungen für die Chargeprice.app-Integration
Verbesserungen:
- Kleinere Verbesserungen für die Chargeprice.app-Integration
- Verschiedene Abstürze behoben

View File

@@ -1,4 +1,4 @@
Verbesserungen für Chargeprice.app-Integration:
- Währung kann in den Einstellungen gewählt werden
- Ausgewählter Ladebereich wird gespeichert
Verbesserungen für Chargeprice.app-Integration:
- Währung kann in den Einstellungen gewählt werden
- Ausgewählter Ladebereich wird gespeichert
- Eigene Tarife können in den Einstellungen ausgewählt werden

View File

@@ -0,0 +1,11 @@
Neue Funktionen:
- Favoriten werden auf der Karte hervorgehoben
- Filtermöglichkeit um nur Favoriten auf der Karte anzuzeigen
Verbesserungen:
- Liste mit vorherigen Suchergebnissen wird schneller geöffnet
- Verbesserungen der Markerdarstellung in Google Maps-Karten
- Umstrukturierung der Einstellungen
- Android Auto: Gelbe Marker leicht verdunkelt für besseren Kontrast
- GoingElectric: Filter nach Anschlüssen besser sortiert
- Verschiedene Abstürze behoben

View File

@@ -0,0 +1,6 @@
Verbesserungen:
- Google Maps: neue Rendering-Engine für bessere Performance
- Gelegentlich falsche Markerfarben bei Filter nach Anschlüssen behoben
- Android Auto: Distanz wird automatisch in passenden Einheiten angezeigt (m, km, mi, ft, yd)
- Android Auto: Aktualisierungsfrequenz reduziert um störende Animation zu vermeiden
- Verschiedene Abstürze behoben

View File

@@ -0,0 +1,11 @@
Verbesserungen:
- Echtzeitdaten für weitere AC-Ladestationen
- FAQ-Seite zum Preisvergleich
- GoingElectric: irreführendes "Laden/Parken: kostenpflichtig" entfernt
Fehlerbehebungen:
- GoingElectric: Beschreibung zu Öffnungszeiten wurde nicht immer angezeigt
- Leere Detailansicht, nachdem die App längere Zeit im Hintergrund war
- Preisvergleich: Exklusive Energiekunden-Tarife unter "Meine Tarife" wurden nicht angezeigt
- Eingestellte Sprache wurde nicht für Google Maps genutzt
- Abstürze behoben

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 71 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 B

After

Width:  |  Height:  |  Size: 24 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 B

After

Width:  |  Height:  |  Size: 875 KiB

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