Compare commits

...

68 Commits
0.3.3 ... 0.4.3

Author SHA1 Message Date
Johan von Forstner
f0f6c08610 Release 0.4.3 2021-01-17 14:15:46 +01:00
Johan von Forstner
a2fe9a06c5 fix another IllegalStateException 2021-01-17 14:09:37 +01:00
Johan von Forstner
cb79f17c23 catch IllegalArgumentException 2021-01-17 14:08:28 +01:00
Johan von Forstner
0009895537 fix IllegalStateException 2021-01-17 14:07:20 +01:00
Johan von Forstner
df705670b1 fix ClassCastException 2021-01-17 14:00:35 +01:00
Johan von Forstner
c616e9fdbd README.md: Describe map backends
see also #36
2021-01-06 19:30:45 +01:00
Johan von Forstner
c70a092d99 Release 0.4.2 2021-01-03 16:47:15 +01:00
Johan von Forstner
34fee47c08 Fix incorrect linking of text (fixes #29) 2021-01-03 16:23:07 +01:00
Johan von Forstner
bf97a14fe3 add station availability in map screen (fixes #52) 2021-01-03 15:28:58 +01:00
Johan von Forstner
60d4d56f80 Fix links to Google Maps
(maps app was not found due to https://developer.android.com/training/basics/intents/package-visibility)
2021-01-03 11:00:22 +01:00
Johan von Forstner
8bf33c7384 FilterProfilesFragment: Add rename and delete buttons + undo function 2021-01-03 10:45:56 +01:00
Johan von Forstner
595e6e9a8f Welcome dialog: replace > with ≥ 2021-01-03 09:52:07 +01:00
Johan von Forstner
9efbdfc046 Fix typo in welcome page 2021-01-02 22:38:54 +01:00
Johan von Forstner
e1d4b6bcc5 welcome dialog: fix height on small screens 2021-01-02 20:09:12 +01:00
Johan von Forstner
a6db74488e release 0.4.1 2020-12-31 20:29:48 +01:00
Johan von Forstner
821f5d61b5 add welcome dialog on first start (fixes #66) 2020-12-31 19:18:58 +01:00
Johan von Forstner
f83ac17c83 don't generate icons in background for Mapbox 2020-12-30 20:10:52 +01:00
Johan von Forstner
3519c7f699 decrease memory usage of charger icons
by allowing "fault" and "highlight" only with scale == 1f
refs #59
2020-12-30 20:01:47 +01:00
Licaon_Kter
78d9706cb7 Remove suffix for fdroid (#67)
...as you'd know it's not-google 👍 
Also, breaks AutoUpdate
2020-12-30 19:01:03 +01:00
Johan von Forstner
a593a8054b fix some gallery glitches (#61) 2020-12-30 18:58:55 +01:00
Johan von Forstner
9556be6b85 Gallery fixes 2020-12-29 20:24:22 +01:00
Johan von Forstner
e8669f8a3d Gallery: replace Picasso with Coil 2020-12-29 18:09:29 +01:00
Johan von Forstner
6a887ee1e4 NewMotionAvailabilityDetector: add some more plug types
(rarely occurring)
2020-12-29 18:08:37 +01:00
Johan von Forstner
6dbaaa3099 travis CI: use latest android SDK commandline tools 2020-12-28 11:14:24 +01:00
Johan von Forstner
7f9242da1e fix license file 2020-12-28 10:58:07 +01:00
Johan von Forstner
2c3151089f CI: accept android licenses 2020-12-28 10:53:42 +01:00
Johan von Forstner
1ee388126f travis: build-tools;30.0.3 2020-12-28 10:46:18 +01:00
Johan von Forstner
964cecdf66 travis CI: use Android 30 sdk 2020-12-28 10:38:15 +01:00
Johan von Forstner
7141eb5013 update to Android 11 SDK 2020-12-27 17:15:18 +01:00
Johan von Forstner
d7fcb35a4e Release 0.4.0 2020-12-26 19:15:07 +01:00
Johan von Forstner
56348905a6 fix DB migration 2020-12-26 16:59:28 +01:00
Johan von Forstner
3336faa953 fix crash on first start 2020-12-26 16:45:09 +01:00
Johan von Forstner
e22e1521a4 fix display of 24/7 opening hours
(regression introduced in 2cd9e9d6)
2020-12-26 16:41:53 +01:00
Johan von Forstner
e974acac4e Implement filter profiles (#37)
* start to work on filter profiles

* fix migration

* add "save as profile" button

* try to make profile a primary key

* start to create preliminary filter profile saving dialog

* implement saving and selecting filter profiles

* fix selection of filter profiles after creation, improve UX

* facilitate editing of existing filter profiles

* implement list of filter profiles with swipe-to-delete

* improve UX for deleting filter profiles

* add possibility to reorder filter profiles

* add empty state for filter profiles
2020-12-26 16:36:43 +01:00
Johan von Forstner
8a13bfcd9e fix compilation for foss variant 2020-12-24 15:46:11 +01:00
Johan von Forstner
1e04d6e98a implement hashCode for MultipleChoiceFilterValue 2020-12-24 15:38:58 +01:00
Johan von Forstner
a0045fc6bb add filter by categories (fixes #64) 2020-12-24 15:37:13 +01:00
Johan von Forstner
ec10b51387 fix crash caused by switching to view binding from android-ktx-extensions 2020-12-24 15:33:00 +01:00
Johan von Forstner
b054464280 fix some deprecations / warnings 2020-12-23 16:29:37 +01:00
Johan von Forstner
1a32159526 Kotlin version and various library upgrades 2020-12-23 16:12:49 +01:00
Johan von Forstner
c6cc7102e6 update Gradle and Android plugin 2020-12-23 14:58:39 +01:00
johan12345
6a5dc93fd8 show distance of charging stations to current location 2020-10-27 22:52:03 +01:00
johan12345
a85966bb1d Add button to edit a station on GoingElectric.de (fixes #62) 2020-10-27 22:28:23 +01:00
johan12345
bf3c401c37 add map scale (fixes #38) 2020-10-26 23:11:59 +01:00
johan12345
4da7e0b50d Don't highlight "Report new station" in drawer (fixes #60) 2020-10-26 22:51:45 +01:00
johan12345
d78f2f08cb update AnyMaps 2020-10-22 08:48:14 +02:00
johan12345
d2952766e4 update OkHttp mockwebserver 2020-09-20 23:05:28 +02:00
johan12345
40503b6bd2 handle rate limiting by NewMotion API 2020-09-20 23:02:23 +02:00
johan12345
e875e0ee42 fix tests 2020-09-20 22:48:52 +02:00
johan12345
6f9ea6c6e3 add cookieManager to HTTP client used by AvailabilityDetector 2020-09-20 22:37:23 +02:00
johan12345
a79d013179 upgrade Retrofit and OkHttp 2020-09-20 22:36:55 +02:00
johan12345
4b75389a31 favorites: sort by distance (fixes #57) 2020-09-20 22:20:57 +02:00
johan12345
1039251d63 normalize app name: EV Map -> EVMap 2020-09-20 22:11:02 +02:00
Johan von Forstner
2cd9e9d642 correct display of opening hours description if there are no opening hours 2020-09-12 17:14:21 +02:00
Johan von Forstner
7d495468ea Add better description for "Tesla HPC" connector 2020-09-12 17:12:35 +02:00
johan12345
e47a82a4bc add F-Droid badge to README.md 2020-09-08 21:53:51 +02:00
Johan von Forstner
87421e450a fix missing API key in Google variant, hotfix release 2020-08-28 15:17:05 +02:00
johan12345
479917fad1 fix wrong position of layers button in case of display cutout (fixes #51) 2020-08-24 22:57:02 +02:00
johan12345
dfaf841160 Release 0.3.4 2020-08-24 22:20:43 +02:00
johan12345
c18ea5b15d add link “report new station” to main menu (fixes #53) 2020-08-24 20:11:44 +02:00
johan12345
62116473c8 Navigation component: add settings as top-level destination 2020-08-24 19:46:32 +02:00
johan12345
bc8106bd81 add Google Maps API key only to Google variant 2020-08-23 23:35:46 +02:00
johan12345
7bd89b9ecb get rid of Mapbox telemetry dependency 2020-08-23 23:31:01 +02:00
Johan von Forstner
898b61945e Make links under "general information" and "amenities" clickable 2020-08-23 12:25:38 +02:00
Johan von Forstner
38e022b547 Add links to Twitter account and GoingElectric.de forum thread 2020-08-22 20:07:33 +02:00
Johan von Forstner
b8c438503c add new icon for "more than one connector" 2020-08-22 09:20:05 +02:00
Johan von Forstner
2ca6a8e3e8 fix vertical position of markers 2020-08-22 08:21:13 +02:00
Johan von Forstner
0ae201e363 MultiSelectDialog: case-insensitive sorting (fixes #44) 2020-08-22 08:11:50 +02:00
89 changed files with 2352 additions and 382 deletions

3
.gitignore vendored
View File

@@ -11,4 +11,5 @@ apikeys.xml
/app/**/*.aab
/app/**/*.apk
/_img/connectors/*.ai
api-7125266970515251116-798419-8e2dda660c80.json
api-7125266970515251116-798419-8e2dda660c80.json
output-metadata.json

View File

@@ -1,16 +1,23 @@
language: android
language: java
dist: trusty
android:
components:
- build-tools-29.0.3
- android-29
env:
global:
- secure: KYdFlMarsyXw+OHht1Atp+Kirbw9O09Ck14EjFuKb1eNtknurZ/tGEXuD+8xWh1W8W21kgHEG7s3rzru53t29buz+FW9f+ZmhEWXFP3OydyvXLw4BAVVOjm6xG2uHX/8MOGLJNM7cfaF25EPQ+kznHe84R29KaLH90mNRr2lPa4VnfbcnvDStiVaez/vJ72UoYSP5HICAzoF70yC3ZvvCK1hZv71UIysCbFE2IkxvMhG9OOGebdnRmFssaRCrvfRLjitobcLzkPWzZZIqdjNASf8/iAxX8VgGBYfVj8ID06AfMrtgXNJRCvcD0LICraQ+WPUbikMunRieGO8PNHSB5vKdPoC50aLUa0RoRb4G3QM1pR2A8xAFlIJFX2R7iY+2t24L9hRFqB98+QoQzutfkAI1T0rzem/wtpZpuan+bDawDJEHGCeYbE0aPDAl6lytgrEE9fRgV3c1jJLQzu0xIWG8YLl3iMg0hL+c0wCKXoeqrfCFS6kYmmG7W+rQp4tCZifvRbWfAXwIPQieffKxqdEuUwiUsYxdzCu9v9uU3nflEOLLuRgeMP3gV8mpur9b5GztpkfgfzcAqsF+NiY01kYgGtrgCYlMy0TxASE+UuALrtkQtU01wwhs9RH7Az0Ib3C+MT5DTjxHQCYETIViocmNEG2vfAbgHazCpGAhcY=
- secure: Hoko50vP+Mwm/O4CWvPvjMxd1gGhi+Bultjyy1WpludSmZFCfKz45Mj1EqzeYk6MeMLvGOEkSLB5wjdXdgJ9j5gEOF5K34k4vESJA7+DqDO3I7Xw9cnWgOXdFqB0qGHar0TVP3Dfg7ZcRgtmeX6t2uoELFLiS9GvTnbZXk4PCUybd979Xi8XHjEQV7+3EZbSjtsI4GFeIK1rrjd0I+UM88zYrWnz1KhdCjWvQb4iZjo+ib6NmGGEMqR7jJPRZz3KB01Y+n5h21qq9/Nv31zQIqt2B5nRxy3vBvvqKapgprIk+hVOpNnBU8w89uWUU6tYUeFk0t7z1TYWjgaBrMmGCM+aKkQ2q5F/ygNzDwB+KkpJx709Yhf0ZX8g2yvkdz+Ok7moYuvmrOPOf4E/U3BlfZxbGtRD2bGYbDgHLFYlTn6v5J2kJHDJAz31yvF5jvJejDaPp2IBVfoMRy7ZJFmGUNHGd9Se6bRwxS++AoobP5WDrBiUXNe2KKDMs3e7vbaO+hLbZ9XHpjeWIJGUfvtTee8EHZqF/8A3ju53V4/R0ehlOv2UZbpYNqcmwrsy9/R4pgMfDkG3Q054LmYrxD8DIC9b8excVMwWRP5aQ1TqZnxO2B1/vJU87RcnGl3jekeHTdHXbRq9BMV4dAdPfB9X3nGIi2GgV0iTBk+24xccOc0=
- secure: RsN/2nBVv0byMzwchuAhDti1AWKECg3Uzqk6Ahgaqg02zx8GHj60j0qNax7KuMUg70X3G4b3m8ZzndAU0wcJ98UyIku7ofUYgXHm0XYKTJwiyyhrKJ8pZ4qoeCoRkitIGIitlb84fSufVamcoLyLNbLUsO5OzL+Uscyhq/BwAhyOhj5FB9DM+GE9ntanQ4m3k4guMyMctR9CGS6Tk4LKJ1WCm0AnjTPalc+we+7yORY2J/9d6VHLKfaFXbpuWmrdnFfDZqxqcUODsxPVE0LuUtXyKQDTjjfQc8106Z/z19AJS+oLm/2UND84PD3MqsjX6EX0/9k3fY0OsoCuQAivDRmtevQ0bDQrTAyeBLcfnPCw/MYiJWyBcaYBAYK42EAfsFTDBRIduFB/Dpvg+8GuLZSdm4xVYpTlQ2pMtlGNWIGIRQsjk9LZ8swq8QBMiiF/bpMGKdfmnyQj8jkEOCWaAzkR9O+4E4Y7PuBENef9XuV0hLMryCrML2YXigxAKkEUOPhfnG+AbEY+g2kAMp+2EtXaG3tsGxoMhEYA+uRd3o2cacQMMwRpnePH5DYg6F05mrvdHPpt+P9UR1iHaHmjBPAYeksmrdP8bS088zcnePgiL6+6N0m3+l8Krihmxg5RyWjnH18IwX4RO+xg4x3cW+zaScCDbbotDMEYtChF+Hs=
- ANDROID_HOME=$HOME/android-sdk
before_install:
- openssl aes-256-cbc -K $encrypted_53968681344a_key -iv $encrypted_53968681344a_iv -in _ci/keystore.jks.enc -out _ci/keystore.jks -d
install:
# Download and unzip the Android command line tools (if not already there thanks to the cache mechanism)
# Latest version of this file available here: https://developer.android.com/studio/#command-tools
- if test ! -e $HOME/android-cmdline-tools/cmdline-tools.zip ; then curl https://dl.google.com/android/repository/commandlinetools-linux-6609375_latest.zip > $HOME/android-cmdline-tools/cmdline-tools.zip ; fi
- unzip -qq -n $HOME/android-cmdline-tools/cmdline-tools.zip -d $HOME/android-cmdline-tools
# Install or update Android SDK components (will not do anything if already up to date thanks to the cache mechanism)
- echo y | $HOME/android-cmdline-tools/tools/bin/sdkmanager --sdk_root=$HOME/android-sdk 'platform-tools' > /dev/null
# Latest version of build-tools available here: https://developer.android.com/studio/releases/build-tools.html
- echo y | $HOME/android-cmdline-tools/tools/bin/sdkmanager --sdk_root=$HOME/android-sdk 'build-tools;29.0.3' > /dev/null
- echo y | $HOME/android-cmdline-tools/tools/bin/sdkmanager --sdk_root=$HOME/android-sdk 'platforms;android-29' > /dev/null
script:
- "./gradlew lintFossDebug testFossDebugUnitTest lintGoogleDebug testGoogleDebugUnitTest"
- "./gradlew assembleRelease"
@@ -22,6 +29,8 @@ cache:
- "$HOME/.gradle/caches/"
- "$HOME/.gradle/wrapper/"
- "$HOME/.android/build-cache"
- "$HOME/android-cmdline-tools"
- "$HOME/android-sdk"
deploy:
provider: releases
api_key:

View File

@@ -7,6 +7,8 @@ Android app to access the goingelectric.de electric vehicle charging station dir
<a href="https://play.google.com/store/apps/details?id=net.vonforst.evmap" target="_blank">
<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="100"/></a>
<a href="https://f-droid.org/repository/browse/?fdid=net.vonforst.evmap" target="_blank">
<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="100"/></a>
Features
--------
@@ -18,6 +20,7 @@ Features
- Favorites list, also with availability information
- No ads, fully open source
- Compatible with Android 5.0 and above
- Can use Google Maps or Mapbox (OpenStreetMap) as map backends - the version available on F-Droid only uses Mapbox.
Screenshots
-----------

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 233.8 368.4" style="enable-background:new 0 0 233.8 368.4;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#B5B5B5;}
.st2{fill:#808080;}
</style>
<g>
<g>
<g>
<path class="st0" d="M109.8,0h13.6c33.9,1.9,67.1,18.5,87.7,45.8c13.5,17.2,21,38.6,22.7,60.3v8.1c-0.8,42.1-27.7,76.6-51,109.4
c-26.2,37-50.4,77.3-57.1,122.9c-1.8,7.7,0.4,18.5-8.9,22c-2.2-1.7-4.7-3.1-6.2-5.4c-2.7-25.5-9.1-50.7-20-73.9
c-12.3-27.1-29.5-51.6-47-75.6C33,199,23,184.2,14.7,168.3c-13-23.8-17.9-51.9-12.5-78.6C6.6,68.6,17.6,49.1,32.8,34
C53.3,14,81.1,1.8,109.8,0z" />
</g>
</g>
</g>
<g>
<polygon class="st1"
points="143.2,109.4 123.5,143.2 123.5,181.3 166.9,106.9 144.7,106.9 " />
<path class="st1"
d="M122.2,101.9h16.7h5.7l22.3-44.6c0,0-10.2,0-22.4,0l-1.1,2.2L122.2,101.9z" />
<path class="st2" d="M138.9,57.3c-9.7,0-19.8,0-26.4,0c-2.5,0-5.1,0-7.6,0c-8.2,0-16.1,0-21.4,0c-4.1,0-6.6,0-6.6,0v68.2h18.6v55.8
l43.4-74.4h-24.8L138.9,57.3z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,20 +1,20 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 29
versionCode 24
versionName "0.3.3"
targetSdkVersion 30
versionCode 37
versionName "0.4.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -48,7 +48,6 @@ android {
productFlavors {
foss {
dimension "dependencies"
versionNameSuffix "-foss"
}
google {
dimension "dependencies"
@@ -68,6 +67,7 @@ android {
buildFeatures {
dataBinding = true
viewBinding true
}
// add API keys from environment variable if not set in apikeys.xml
@@ -78,7 +78,7 @@ android {
variant.resValue "string", "goingelectric_key", goingelectricKey
}
def googleMapsKey = env.GOOGLE_MAPS_API_KEY ?: project.findProperty("GOOGLE_MAPS_API_KEY")
if (googleMapsKey != null) {
if (googleMapsKey != null && variant.flavorName == 'google') {
variant.resValue "string", "google_maps_key", googleMapsKey
}
def mapboxKey = env.MAPBOX_API_KEY ?: project.findProperty("MAPBOX_API_KEY")
@@ -92,31 +92,35 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.core:core-ktx:1.3.2'
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.core:core:1.3.1'
implementation 'androidx.core:core:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.android.material:material:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.browser:browser:1.3.0'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
implementation 'com.squareup.retrofit2:retrofit:2.7.2'
implementation 'com.squareup.retrofit2:converter-moshi:2.7.2'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.9.2'
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'com.github.MikeOrtiz:TouchImageView:2.3.3'
implementation 'io.coil-kt:coil:1.1.0'
implementation 'com.github.MikeOrtiz:TouchImageView:3.0.3'
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
implementation 'com.airbnb.android:lottie:3.4.0'
implementation 'io.michaelrocks:bimap:1.0.2'
implementation 'com.mapzen.android:lost:3.0.2'
implementation 'com.google.guava:guava:29.0-android'
implementation 'com.github.pengrad:mapscaleview:1.6.0'
// AnyMaps
def anyMapsVersion = 'e6e014dd11'
def anyMapsVersion = '7753eeb7b0'
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"
@@ -125,21 +129,24 @@ dependencies {
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.1.1'
googleImplementation 'com.google.android.gms:play-services-base:17.3.0'
googleImplementation 'com.google.android.gms:play-services-basement:17.3.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.0.0'
googleImplementation 'com.google.android.gms:play-services-tasks:17.1.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.3'
googleImplementation 'com.google.android.datatransport:transport-runtime:2.2.5'
googleImplementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
// Mapbox places (autocomplete)
implementation 'com.mapbox.mapboxsdk:mapbox-android-plugin-places-v9:0.12.0'
implementation('com.mapbox.mapboxsdk:mapbox-android-plugin-places-v9:0.12.0') {
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-accounts'
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-telemetry'
}
// navigation library
def nav_version = "2.3.0"
def nav_version = "2.3.2"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
@@ -149,13 +156,13 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// room library
def room_version = "2.2.5"
def room_version = "2.2.6"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
// billing library
def billing_version = "3.0.0"
def billing_version = "3.0.2"
googleImplementation "com.android.billingclient:billing:$billing_version"
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
@@ -163,13 +170,13 @@ dependencies {
implementation 'com.facebook.stetho:stetho:1.5.1'
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
testImplementation 'junit:junit:4.13'
testImplementation "com.squareup.okhttp3:mockwebserver:3.14.7"
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
//noinspection GradleDependency
testImplementation 'org.json:json:20080701'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.9.2"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
}
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
}

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">EV Map (debug)</string>
<string name="app_name">EVMap (debug)</string>
</resources>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">EV Map (debug)</string>
<string name="app_name">EVMap (debug)</string>
</resources>

View File

@@ -8,29 +8,32 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import kotlinx.android.synthetic.foss.fragment_donate.*
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.databinding.FragmentDonateBinding
class DonateFragment : Fragment() {
private lateinit var binding: FragmentDonateBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_donate, container, false)
binding = FragmentDonateBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
(requireActivity() as AppCompatActivity).setSupportActionBar(toolbar)
(requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar)
val navController = findNavController()
toolbar.setupWithNavController(
binding.toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
btnDonate.setOnClickListener {
binding.btnDonate.setOnClickListener {
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link))
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.vonforst.evmap">
<application>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key" />
</application>
</manifest>

View File

@@ -17,7 +17,7 @@ fun checkPlayServices(activity: Activity): Boolean {
val resultCode = apiAvailability.isGooglePlayServicesAvailable(activity)
if (resultCode != ConnectionResult.SUCCESS) {
if (apiAvailability.isUserResolvableError(resultCode)) {
apiAvailability.getErrorDialog(activity, resultCode, request).show()
apiAvailability.getErrorDialog(activity, resultCode, request)?.show()
} else {
Log.d("EVMap", "This device is not supported.")
}

View File

@@ -5,6 +5,17 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="geo" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="google.navigation" />
</intent>
</queries>
<application
android:name=".EvMapApplication"
android:allowBackup="true"
@@ -14,17 +25,6 @@
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!--
The API key for Google Maps-based APIs is defined as a string resource.
(See the file "res/values/apikeys.xml").
Note that the API key is linked to the encryption key used to sign the APK.
You need a different API key for each encryption key, including the release key that is used to
sign the APK for publishing.
You can define the keys for the debug and release targets in src/debug/ and src/release/.
-->
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key" />
<meta-data
android:name="com.mapbox.ACCESS_TOKEN"
android:value="@string/mapbox_key" />

View File

@@ -6,6 +6,7 @@ import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.drawerlayout.widget.DrawerLayout
@@ -53,7 +54,8 @@ class MapsActivity : AppCompatActivity() {
setOf(
R.id.map,
R.id.favs,
R.id.about
R.id.about,
R.id.settings
),
findViewById<DrawerLayout>(R.id.drawer_layout)
)
@@ -89,13 +91,17 @@ class MapsActivity : AppCompatActivity() {
cb.getRootView(),
R.string.no_maps_app_found,
Snackbar.LENGTH_SHORT
)
).show()
}
}
fun openUrl(url: String) {
val intent = CustomTabsIntent.Builder()
.setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary))
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary))
.build()
)
.build()
intent.launchUrl(this, Uri.parse(url))
}

View File

@@ -48,7 +48,8 @@ fun buildDetails(
R.drawable.ic_address,
R.string.address,
loc.address.toString(),
loc.locationDescription
loc.locationDescription,
clickable = true
),
if (loc.operator != null) DetailsAdapter.Detail(
R.drawable.ic_operator,
@@ -79,8 +80,11 @@ fun buildDetails(
if (loc.openinghours != null && !loc.openinghours.isEmpty) DetailsAdapter.Detail(
R.drawable.ic_hours,
R.string.hours,
loc.openinghours.getStatusText(ctx),
loc.openinghours.description,
if (loc.openinghours.days != null || loc.openinghours.twentyfourSeven)
loc.openinghours.getStatusText(ctx)
else
loc.openinghours.description ?: "",
if (loc.openinghours.days != null) loc.openinghours.description else null,
hoursDays = loc.openinghours.days
) else null,
if (loc.cost != null) DetailsAdapter.Detail(

View File

@@ -0,0 +1,55 @@
package net.vonforst.evmap.adapter
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.animation.AccelerateInterpolator
import androidx.recyclerview.widget.ItemTouchHelper
import net.vonforst.evmap.R
import net.vonforst.evmap.databinding.ItemFilterProfileBinding
import net.vonforst.evmap.storage.FilterProfile
class FilterProfilesAdapter(
val dragHelper: ItemTouchHelper,
val onDelete: (FilterProfile) -> Unit,
val onRename: (FilterProfile) -> Unit
) : DataBindingAdapter<FilterProfile>() {
init {
setHasStableIds(true)
}
@SuppressLint("ClickableViewAccessibility")
override fun bind(
holder: ViewHolder<FilterProfile>,
item: FilterProfile
) {
super.bind(holder, item)
val binding = holder.binding as ItemFilterProfileBinding
binding.handle.setOnTouchListener { v, event ->
if (event?.action == MotionEvent.ACTION_DOWN) {
dragHelper.startDrag(holder)
}
false
}
binding.foreground.translationX = 0f
binding.btnDelete.setOnClickListener {
binding.foreground.animate()
.translationX(binding.foreground.width.toFloat())
.setDuration(250)
.setInterpolator(AccelerateInterpolator())
.withEndAction {
onDelete(item)
}
.start()
}
binding.btnRename.setOnClickListener {
onRename(item)
}
}
override fun getItemId(position: Int): Long {
return getItem(position).id
}
override fun getItemViewType(position: Int): Int = R.layout.item_filter_profile
}

View File

@@ -7,9 +7,11 @@ import android.widget.ImageView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.memory.MemoryCache
import coil.size.OriginalSize
import coil.size.SizeResolver
import com.ortiz.touchview.TouchImageView
import com.squareup.picasso.Callback
import com.squareup.picasso.Picasso
import net.vonforst.evmap.R
import net.vonforst.evmap.api.goingelectric.ChargerPhoto
@@ -19,17 +21,19 @@ class GalleryAdapter(
val itemClickListener: ItemClickListener? = null,
val detailView: Boolean = false,
val pageToLoad: Int? = null,
val imageCacheKey: MemoryCache.Key? = null,
val loadedListener: (() -> Unit)? = null
) :
ListAdapter<ChargerPhoto, GalleryAdapter.ViewHolder>(ChargerPhotoDiffCallback()) {
class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view)
interface ItemClickListener {
fun onItemClick(view: View, position: Int)
fun onItemClick(view: View, position: Int, imageCacheKey: MemoryCache.Key?)
}
val apikey = context.getString(R.string.goingelectric_key)
var loaded = false
val memoryKeys = HashMap<String, MemoryCache.Key?>()
@SuppressLint("ClickableViewAccessibility")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@@ -37,11 +41,11 @@ class GalleryAdapter(
val view: ImageView
if (detailView) {
view = inflater.inflate(R.layout.gallery_item_fullscreen, parent, false) as ImageView
view.setOnTouchListener { view, event ->
view.setOnTouchListener { v, event ->
var result = true
//can scroll horizontally checks if there's still a part of the image
//that can be scrolled until you reach the edge
if (event.pointerCount >= 2 || view.canScrollHorizontally(1) && view.canScrollHorizontally(
if (event.pointerCount >= 2 || v.canScrollHorizontally(1) && v.canScrollHorizontally(
-1
)
) {
@@ -73,46 +77,63 @@ class GalleryAdapter(
if (detailView) {
(holder.view as TouchImageView).resetZoom()
}
Picasso.get()
.load(
"https://api.goingelectric.de/chargepoints/photo/?key=${apikey}" +
"&id=${getItem(position).id}" +
if (detailView) {
"&size=1000"
} else {
"&height=${holder.view.height}"
}
)
.into(holder.view, object : Callback {
override fun onSuccess() {
if (!loaded && loadedListener != null && pageToLoad == position) {
holder.view.viewTreeObserver.addOnPreDrawListener(object :
ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
holder.view.viewTreeObserver.removeOnPreDrawListener(this)
loadedListener.invoke()
return true
}
})
loaded = true
}
val id = getItem(position).id
val url = "https://api.goingelectric.de/chargepoints/photo/?key=${apikey}" +
"&id=$id" +
if (detailView) {
"&size=1000"
} else {
"&height=${holder.view.height}"
}
override fun onError(e: Exception?) {
holder.view.load(
url
) {
if (pageToLoad == position && imageCacheKey != null) {
placeholderMemoryCacheKey(imageCacheKey)
}
size(SizeResolver(OriginalSize))
allowHardware(false)
listener(
onSuccess = { _, metadata ->
memoryKeys[id] = metadata.memoryCacheKey
if (pageToLoad == position) invokeLoadedListener(holder.view)
},
onError = { _, _ ->
if (!loaded && loadedListener != null && pageToLoad == position) {
loadedListener.invoke()
loaded = true
}
}
})
)
}
if (pageToLoad == position && imageCacheKey != null) {
// start transition immediately
if (pageToLoad == position) invokeLoadedListener(holder.view)
}
holder.view.transitionName = galleryTransitionName(position)
if (itemClickListener != null) {
holder.view.setOnClickListener {
itemClickListener.onItemClick(holder.view, position)
itemClickListener.onItemClick(holder.view, position, memoryKeys[id])
}
}
}
private fun invokeLoadedListener(
view: ImageView
) {
if (!loaded && loadedListener != null) {
view.viewTreeObserver.addOnPreDrawListener(object :
ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
view.viewTreeObserver.removeOnPreDrawListener(this)
loadedListener.invoke()
return true
}
})
loaded = true
}
}
}
fun galleryTransitionName(position: Int) = "gallery_$position"

View File

@@ -0,0 +1,33 @@
package net.vonforst.evmap.api
import com.google.common.util.concurrent.RateLimiter
import okhttp3.Interceptor
import okhttp3.Response
class RateLimitInterceptor : Interceptor {
private val rateLimiter = RateLimiter.create(3.0)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.url.host == "my.newmotion.com") {
// limit requests sent to NewMotion to 3 per second
rateLimiter.acquire(1)
var response: Response = chain.proceed(request)
// 403 is how the NewMotion API indicates a rate limit error
if (!response.isSuccessful && response.code == 403) {
response.close()
// wait & retry
try {
Thread.sleep(1000)
} catch (e: InterruptedException) {
}
response = chain.proceed(request)
}
return response
} else {
return chain.proceed(request)
}
}
}

View File

@@ -10,7 +10,10 @@ import java.io.IOException
import kotlin.coroutines.resumeWithException
operator fun <T> JSONArray.iterator(): Iterator<T> =
(0 until length()).asSequence().map { get(it) as T }.iterator()
(0 until length()).asSequence().map {
@Suppress("UNCHECKED_CAST")
get(it) as T
}.iterator()
@ExperimentalCoroutinesApi
suspend fun Call.await(): Response {

View File

@@ -2,21 +2,27 @@ package net.vonforst.evmap.api.availability
import com.facebook.stetho.okhttp3.StethoInterceptor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.withContext
import net.vonforst.evmap.api.RateLimitInterceptor
import net.vonforst.evmap.api.await
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.Chargepoint
import net.vonforst.evmap.viewmodel.Resource
import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient
import okhttp3.Request
import retrofit2.HttpException
import java.io.IOException
import java.net.CookieManager
import java.net.CookiePolicy
import java.util.concurrent.TimeUnit
interface AvailabilityDetector {
suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus
}
@ExperimentalCoroutinesApi
abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : AvailabilityDetector {
protected val radius = 150 // max radius in meters
@@ -24,9 +30,9 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
val request = Request.Builder().url(url).build()
val response = client.newCall(request).await()
if (!response.isSuccessful) throw IOException(response.message())
if (!response.isSuccessful) throw IOException(response.message)
val str = response.body()!!.string()
val str = response.body!!.string()
return str
}
@@ -115,10 +121,16 @@ enum class ChargepointStatus {
class AvailabilityDetectorException(message: String) : Exception(message)
private val cookieManager = CookieManager().apply {
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
}
private val okhttp = OkHttpClient.Builder()
.addInterceptor(RateLimitInterceptor())
.addNetworkInterceptor(StethoInterceptor())
.readTimeout(10, TimeUnit.SECONDS)
.connectTimeout(10, TimeUnit.SECONDS)
.cookieJar(JavaNetCookieJar(cookieManager))
.build()
val availabilityDetectors = listOf(
NewMotionAvailabilityDetector(okhttp)

View File

@@ -8,6 +8,7 @@ import okhttp3.OkHttpClient
import org.json.JSONObject
import java.io.IOException
@ExperimentalCoroutinesApi
class ChargecloudAvailabilityDetector(
client: OkHttpClient,
private val operatorId: String

View File

@@ -9,6 +9,7 @@ import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
import java.util.*
private const val coordRange = 0.1 // range of latitude and longitude for loading the map
private const val maxDistance = 15 // max distance between reported positions in meters
@@ -95,7 +96,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
// find nearest station to this position
var markers =
api.getMarkers(lng - coordRange, lng + coordRange, lat - coordRange, lat + coordRange)
val nearest = markers.minBy { marker ->
val nearest = markers.minByOrNull { marker ->
distanceBetween(marker.coordinates.latitude, marker.coordinates.longitude, lat, lng)
} ?: throw AvailabilityDetectorException("no candidates found.")
@@ -138,14 +139,17 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
connectorStatus.forEach { (connector, statusStr) ->
val id = connector.uid
val power = connector.electricalProperties.getPower()
val type = when (connector.connectorType) {
"Type3" -> Chargepoint.TYPE_3
"Type2" -> Chargepoint.TYPE_2
"Type1" -> Chargepoint.TYPE_1
"Domestic" -> Chargepoint.SCHUKO
"Type2Combo" -> Chargepoint.CCS
"TepcoCHAdeMO" -> Chargepoint.CHADEMO
"Unspecified" -> "unspecified"
val type = when (connector.connectorType.toLowerCase(Locale.ROOT)) {
"type3" -> Chargepoint.TYPE_3
"type2" -> Chargepoint.TYPE_2
"type1" -> Chargepoint.TYPE_1
"domestic" -> Chargepoint.SCHUKO
"type1combo" -> Chargepoint.CCS // US CCS, aka type1_combo
"type2combo" -> Chargepoint.CCS // EU CCS, aka type2_combo
"tepcochademo" -> Chargepoint.CHADEMO
"unspecified" -> "unknown"
"unknown" -> "unknown"
"saej1772" -> "unknown"
else -> throw IllegalArgumentException("unrecognized type ${connector.connectorType}")
}
val status = when (statusStr) {

View File

@@ -27,6 +27,7 @@ interface GoingElectricApi {
@Query("plugs") plugs: String? = null,
@Query("chargecards") chargecards: String? = null,
@Query("networks") networks: String? = null,
@Query("categories") categories: String? = null,
@Query("startkey") startkey: Int? = null,
@Query("open_twentyfourseven") open247: Boolean = false,
@Query("barrierfree") barrierfree: Boolean = false,
@@ -46,7 +47,7 @@ interface GoingElectricApi {
suspend fun getChargeCards(): Response<ChargeCardList>
companion object {
private val cacheSize = 10L * 1024 * 1024; // 10MB
private val cacheSize = 10L * 1024 * 1024 // 10MB
fun create(
apikey: String,
@@ -57,7 +58,7 @@ interface GoingElectricApi {
addInterceptor { chain ->
// add API key to every request
var original = chain.request()
val url = original.url().newBuilder().addQueryParameter("key", apikey).build()
val url = original.url.newBuilder().addQueryParameter("key", apikey).build()
original = original.newBuilder().url(url).build()
chain.proceed(original)
}

View File

@@ -8,7 +8,7 @@ import androidx.room.Entity
import androidx.room.PrimaryKey
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.Equatable
import java.time.DayOfWeek
@@ -77,7 +77,7 @@ data class ChargeLocation(
*/
fun maxPower(connectors: Set<String>? = null): Double {
return chargepoints.filter { connectors?.contains(it.type) ?: true }
.map { it.power }.max() ?: 0.0
.map { it.power }.maxOrNull() ?: 0.0
}
/**
@@ -97,6 +97,9 @@ data class ChargeLocation(
}
}
val totalChargepoints: Int
get() = chargepoints.sumBy { it.count }
fun formatChargepoints(): String {
return chargepointsMerged.map {
"${it.count} × ${it.type} ${it.formatPower()}"
@@ -277,6 +280,7 @@ data class Chargepoint(val type: String, val power: Double, val count: Int) : Eq
const val SUPERCHARGER = "Tesla Supercharger"
const val CEE_BLAU = "CEE Blau"
const val CEE_ROT = "CEE Rot"
const val TESLA_ROADSTER_HPC = "Tesla HPC"
}
}

View File

@@ -59,6 +59,14 @@ class AboutFragment : PreferenceFragmentCompat() {
findNavController().navigate(R.id.action_about_to_donateFragment)
true
}
"twitter" -> {
(activity as? MapsActivity)?.openUrl(getString(R.string.twitter_url))
true
}
"goingelectric" -> {
(activity as? MapsActivity)?.openUrl(getString(R.string.goingelectric_forum_url))
true
}
else -> super.onPreferenceTreeClick(preference)
}
}

View File

@@ -86,8 +86,9 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
}
override fun onConnected() {
val context = this.context ?: return
if (ContextCompat.checkSelfPermission(
requireContext(),
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {

View File

@@ -17,9 +17,11 @@ import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.FiltersAdapter
import net.vonforst.evmap.databinding.FragmentFilterBinding
import net.vonforst.evmap.ui.showEditTextDialog
import net.vonforst.evmap.viewmodel.FilterViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class FilterFragment : Fragment() {
private lateinit var binding: FragmentFilterBinding
private val vm: FilterViewModel by viewModels(factoryProducer = {
@@ -35,13 +37,15 @@ class FilterFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_filter, container, false)
binding.lifecycleOwner = this
binding.vm = vm
setHasOptionsMenu(true)
vm.filterProfile.observe(viewLifecycleOwner) {}
return binding.root
}
@@ -85,6 +89,26 @@ class FilterFragment : Fragment() {
}
true
}
R.id.menu_save_profile -> {
showEditTextDialog(requireContext()) { dialog, input ->
vm.filterProfile.value?.let { profile ->
input.setText(profile.name)
}
dialog.setTitle(R.string.save_as_profile)
.setMessage(R.string.save_profile_enter_name)
.setPositiveButton(R.string.ok) { di, button ->
lifecycleScope.launch {
vm.saveAsProfile(input.text.toString())
findNavController().popBackStack()
}
}
.setNegativeButton(R.string.cancel) { di, button ->
}
}
true
}
else -> super.onOptionsItemSelected(item)
}
}

View File

@@ -0,0 +1,228 @@
package net.vonforst.evmap.fragment
import android.graphics.Canvas
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.DataBindingAdapter
import net.vonforst.evmap.adapter.FilterProfilesAdapter
import net.vonforst.evmap.databinding.FragmentFilterProfilesBinding
import net.vonforst.evmap.databinding.ItemFilterProfileBinding
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.ui.showEditTextDialog
import net.vonforst.evmap.viewmodel.FilterProfilesViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
class FilterProfilesFragment : Fragment() {
private lateinit var binding: FragmentFilterProfilesBinding
private val vm: FilterProfilesViewModel by viewModels(factoryProducer = {
viewModelFactory {
FilterProfilesViewModel(requireActivity().application)
}
})
private var deleteSnackbar: Snackbar? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentFilterProfilesBinding.inflate(inflater, container, false)
binding.lifecycleOwner = this
binding.vm = vm
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
(requireActivity() as AppCompatActivity).setSupportActionBar(toolbar)
val navController = findNavController()
toolbar.setupWithNavController(
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
val touchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPos = viewHolder.adapterPosition;
val toPos = target.adapterPosition;
val list = vm.filterProfiles.value?.toMutableList()
if (list != null) {
val item = list[fromPos]
list.removeAt(fromPos)
list.add(toPos, item)
list.forEachIndexed { index, filterProfile ->
filterProfile.order = index
}
vm.reorderProfiles(list)
}
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val fp = vm.filterProfiles.value?.find { it.id == viewHolder.itemId }
fp?.let { delete(it) }
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
if (viewHolder != null && actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
val binding =
(viewHolder as DataBindingAdapter.ViewHolder<*>).binding as ItemFilterProfileBinding
getDefaultUIUtil().onSelected(binding.foreground)
} else {
super.onSelectedChanged(viewHolder, actionState)
}
}
override fun onChildDrawOver(
c: Canvas, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float,
actionState: Int, isCurrentlyActive: Boolean
) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
val binding =
(viewHolder as DataBindingAdapter.ViewHolder<*>).binding as ItemFilterProfileBinding
getDefaultUIUtil().onDrawOver(
c, recyclerView, binding.foreground, dX, dY,
actionState, isCurrentlyActive
)
val lp = (binding.deleteIcon.layoutParams as FrameLayout.LayoutParams)
lp.gravity = Gravity.CENTER_VERTICAL or if (dX > 0) {
Gravity.START
} else {
Gravity.END
}
binding.deleteIcon.layoutParams = lp
} else {
super.onChildDrawOver(
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
}
}
override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
val binding =
(viewHolder as DataBindingAdapter.ViewHolder<*>).binding as ItemFilterProfileBinding
getDefaultUIUtil().clearView(binding.foreground)
}
override fun onChildDraw(
c: Canvas, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float,
actionState: Int, isCurrentlyActive: Boolean
) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
val binding =
(viewHolder as DataBindingAdapter.ViewHolder<*>).binding as ItemFilterProfileBinding
getDefaultUIUtil().onDraw(
c, recyclerView, binding.foreground, dX, dY,
actionState, isCurrentlyActive
)
} else {
super.onChildDraw(
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
}
}
})
val adapter = FilterProfilesAdapter(touchHelper, onDelete = { fp ->
delete(fp)
}, onRename = { fp ->
showEditTextDialog(requireContext()) { dialog, input ->
input.setText(fp.name)
dialog.setTitle(R.string.rename)
.setMessage(R.string.save_profile_enter_name)
.setPositiveButton(R.string.ok) { di, button ->
lifecycleScope.launch {
vm.insert(fp.copy(name = input.text.toString()))
}
}
.setNegativeButton(R.string.cancel) { di, button ->
}
}
})
binding.filterProfilesList.apply {
this.adapter = adapter
layoutManager =
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
addItemDecoration(
DividerItemDecoration(
context, LinearLayoutManager.VERTICAL
)
)
}
touchHelper.attachToRecyclerView(binding.filterProfilesList)
toolbar.setNavigationOnClickListener {
findNavController().popBackStack()
}
}
fun delete(fp: FilterProfile) {
vm.delete(fp.id)
deleteSnackbar?.dismiss()
view?.let {
val snackbar = Snackbar.make(
it,
getString(R.string.deleted_filterprofile, fp.name),
Snackbar.LENGTH_LONG
).setAction(R.string.undo) {
vm.insert(fp.copy(id = 0))
}
deleteSnackbar = snackbar
snackbar.show()
}
}
}

View File

@@ -12,6 +12,7 @@ import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.transition.TransitionInflater
import androidx.viewpager2.widget.ViewPager2
import coil.memory.MemoryCache
import com.ortiz.touchview.TouchImageView
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.GalleryAdapter
@@ -24,18 +25,23 @@ class GalleryFragment : Fragment() {
companion object {
private const val EXTRA_POSITION = "position"
private const val EXTRA_PHOTOS = "photos"
private const val EXTRA_IMAGE_CACHE_KEY = "image_cache_key"
private const val SAVED_CURRENT_PAGE_POSITION = "current_page_position"
fun buildArgs(photos: List<ChargerPhoto>, position: Int): Bundle {
fun buildArgs(
photos: List<ChargerPhoto>,
position: Int,
imageCacheKey: MemoryCache.Key?
): Bundle {
return Bundle().apply {
putParcelableArrayList(EXTRA_PHOTOS, ArrayList(photos))
putInt(EXTRA_POSITION, position)
putParcelable(EXTRA_IMAGE_CACHE_KEY, imageCacheKey)
}
}
}
private lateinit var binding: FragmentGalleryBinding
private var isReturning: Boolean = false
private var startingPosition: Int = 0
private var currentPosition: Int = 0
private lateinit var galleryAdapter: GalleryAdapter
@@ -49,7 +55,6 @@ class GalleryFragment : Fragment() {
if (image != null && image.currentZoom !in 0.95f..1.05f) {
image.setZoomAnimated(1f, 0.5f, 0.5f)
} else {
isReturning = true
galleryVm.galleryPosition.value = currentPosition
findNavController().popBackStack()
}
@@ -73,10 +78,13 @@ class GalleryFragment : Fragment() {
savedInstanceState?.getInt(SAVED_CURRENT_PAGE_POSITION) ?: startingPosition
galleryAdapter =
GalleryAdapter(requireContext(), detailView = true, pageToLoad = currentPosition) {
GalleryAdapter(
requireContext(), detailView = true, pageToLoad = currentPosition,
imageCacheKey = args.getParcelable(EXTRA_IMAGE_CACHE_KEY)
) {
startPostponedEnterTransition()
}
binding.gallery.setPageTransformer { page, position ->
binding.gallery.setPageTransformer { page, _ ->
val v = page as TouchImageView
currentPage = v
}
@@ -120,10 +128,8 @@ class GalleryFragment : Fragment() {
names: MutableList<String>,
sharedElements: MutableMap<String, View>
) {
if (isReturning) {
val currentPage = currentPage ?: return
sharedElements[names[0]] = currentPage
}
val currentPage = currentPage ?: return
sharedElements[names[0]] = currentPage
}
}

View File

@@ -18,6 +18,7 @@ import androidx.appcompat.widget.PopupMenu
import androidx.core.app.ActivityCompat
import androidx.core.app.SharedElementCallback
import androidx.core.content.ContextCompat
import androidx.core.view.MenuCompat
import androidx.core.view.updateLayoutParams
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
@@ -25,6 +26,7 @@ import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
@@ -33,6 +35,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import coil.memory.MemoryCache
import com.car2go.maps.AnyMap
import com.car2go.maps.MapFragment
import com.car2go.maps.OnMapReadyCallback
@@ -51,7 +54,9 @@ import com.mapzen.android.lost.api.LocationServices
import com.mapzen.android.lost.api.LostApiClient
import io.michaelrocks.bimap.HashBiMap
import io.michaelrocks.bimap.MutableBiMap
import kotlinx.android.synthetic.main.fragment_map.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.*
import net.vonforst.evmap.adapter.ConnectorAdapter
import net.vonforst.evmap.adapter.DetailsAdapter
@@ -86,7 +91,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
})
private val galleryVm: GalleryViewModel by activityViewModels()
private lateinit var mapFragment: MapFragment
private var mapFragment: MapFragment? = null
private var map: AnyMap? = null
private lateinit var locationClient: LostApiClient
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
@@ -121,19 +126,29 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
locationClient = LostApiClient.Builder(requireContext())
.addConnectionCallbacks(this)
.build()
locationClient.connect()
clusterIconGenerator = ClusterIconGenerator(requireContext())
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_map, container, false)
binding.lifecycleOwner = this
binding.vm = vm
mapFragment = MapFragment()
val provider = PreferenceDataSource(requireContext()).mapProvider
mapFragment.setPriority(
arrayOf(
if (mapFragment == null || mapFragment!!.priority[0] != provider) {
mapFragment = MapFragment()
mapFragment!!.priority = arrayOf(
when (provider) {
"mapbox" -> MapFragment.MAPBOX
"google" -> MapFragment.GOOGLE
@@ -142,37 +157,42 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
MapFragment.GOOGLE,
MapFragment.MAPBOX
)
)
requireActivity().supportFragmentManager
.beginTransaction()
.replace(R.id.map, mapFragment)
.commit()
requireActivity().supportFragmentManager
.beginTransaction()
.replace(R.id.map, mapFragment!!)
.commit()
// reset map-related stuff (map provider may have changed)
map = null
markers.clear()
clusterMarkers = emptyList()
searchResultMarker = null
searchResultIcon = null
locationClient = LostApiClient.Builder(requireContext())
.addConnectionCallbacks(this)
.build()
locationClient.connect()
clusterIconGenerator = ClusterIconGenerator(requireContext())
// reset map-related stuff (map provider may have changed)
map = null
markers.clear()
clusterMarkers = emptyList()
searchResultMarker = null
searchResultIcon = null
}
setHasOptionsMenu(true)
postponeEnterTransition()
binding.root.setOnApplyWindowInsetsListener { v, insets ->
binding.root.setOnApplyWindowInsetsListener { _, insets ->
binding.detailAppBar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.systemWindowInsetTop
}
// margin of layers button
val density = resources.displayMetrics.density
// status bar height + toolbar height + margin
val margin =
insets.systemWindowInsetTop + (48 * density).toInt() + (24 * density).toInt()
binding.fabLayers.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = margin
}
binding.layersSheet.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = margin
}
insets
}
setExitSharedElementCallback(exitElementCallback)
setExitSharedElementCallback(reenterSharedElementCallback)
exitTransition = TransitionInflater.from(requireContext())
.inflateTransition(R.transition.map_exit_transition)
@@ -185,7 +205,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mapFragment.getMapAsync(this)
mapFragment!!.getMapAsync(this)
bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet)
detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
@@ -202,12 +222,21 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
navController,
(requireActivity() as MapsActivity).appBarConfiguration
)
if (!PreferenceDataSource(requireContext()).welcomeDialogShown) {
try {
navController.navigate(R.id.action_map_to_welcome)
} catch (ignored: IllegalArgumentException) {
// when there is already another navigation going on
}
}
}
override fun onResume() {
super.onResume()
val hostActivity = activity as? MapsActivity ?: return
hostActivity.fragmentCallback = this
vm.reloadPrefs()
}
private fun setupClickListeners() {
@@ -269,6 +298,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
true
}
R.id.menu_edit -> {
val charger = vm.charger.value?.data
if (charger != null) {
(activity as? MapsActivity)?.openUrl("https:${charger.url}edit/")
}
true
}
else -> false
}
}
@@ -421,7 +457,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
markers.forEach { (m, c) ->
m.setIcon(
chargerIconGenerator.getBitmapDescriptor(
getMarkerTint(c, vm.filteredConnectors.value), fault = c.faultReport != null
getMarkerTint(c, vm.filteredConnectors.value),
highlight = false,
fault = c.faultReport != null,
multi = getMarkerMulti(c, vm.filteredConnectors.value)
)
)
}
@@ -434,7 +473,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
chargerIconGenerator.getBitmapDescriptor(
getMarkerTint(charger, vm.filteredConnectors.value),
highlight = true,
fault = charger.faultReport != null
fault = charger.faultReport != null,
multi = getMarkerMulti(charger, vm.filteredConnectors.value)
)
)
animator.animateMarkerBounce(marker)
@@ -444,13 +484,31 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
if (m != marker) {
m.setIcon(
chargerIconGenerator.getBitmapDescriptor(
getMarkerTint(c, vm.filteredConnectors.value), fault = c.faultReport != null
getMarkerTint(c, vm.filteredConnectors.value),
highlight = false,
fault = c.faultReport != null,
multi = getMarkerMulti(c, vm.filteredConnectors.value)
)
)
}
}
}
private fun getMarkerMulti(charger: ChargeLocation, filteredConnectors: Set<String>?): Boolean {
var chargepoints = charger.chargepointsMerged
.filter { filteredConnectors?.contains(it.type) ?: true }
if (charger.maxPower(filteredConnectors) >= 43) {
// fast charger -> only count fast chargers
chargepoints = chargepoints.filter { it.power >= 43 }
}
val connectors = chargepoints.map { it.type }.distinct().toSet()
// check if there is more than one plug for any connector type
val chargepointsPerConnector =
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumBy { it.count } }
return chargepointsPerConnector.any { it > 1 }
}
private fun updateFavoriteToggle() {
val favs = vm.favorites.value ?: return
val charger = vm.chargerSparse.value ?: return
@@ -463,12 +521,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private fun setupAdapters() {
val galleryClickListener = object : GalleryAdapter.ItemClickListener {
override fun onItemClick(view: View, position: Int) {
override fun onItemClick(view: View, position: Int, imageCacheKey: MemoryCache.Key?) {
val photos = vm.charger.value?.data?.photos ?: return
val extras = FragmentNavigatorExtras(view to view.transitionName)
view.findNavController().navigate(
R.id.action_map_to_galleryFragment,
GalleryFragment.buildArgs(photos, position),
GalleryFragment.buildArgs(photos, position, imageCacheKey),
null,
extras
)
@@ -487,17 +545,43 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
DividerItemDecoration(
context, LinearLayoutManager.HORIZONTAL
).apply {
setDrawable(context.getDrawable(R.drawable.gallery_divider)!!)
setDrawable(ContextCompat.getDrawable(context, R.drawable.gallery_divider)!!)
})
}
if (galleryPosition == null) {
startPostponedEnterTransition()
} else {
binding.gallery.scrollToPosition(galleryPosition)
binding.gallery.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
v.removeOnLayoutChangeListener(this)
val layoutManager = binding.gallery.layoutManager!!
val viewAtPosition = layoutManager.findViewByPosition(galleryPosition)
if (viewAtPosition == null || layoutManager.isViewPartiallyVisible(
viewAtPosition,
false,
true
)
) {
binding.gallery.post {
layoutManager.scrollToPosition(galleryPosition)
}
}
}
})
// make sure that the app does not freeze waiting for a picture to load
Handler().postDelayed({
startPostponedEnterTransition()
}, 500)
}, 100)
}
binding.detailView.connectors.apply {
@@ -513,7 +597,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val charger = vm.chargerDetails.value?.data
if (charger != null) {
when (it.icon) {
R.drawable.ic_location -> {
R.drawable.ic_location, R.drawable.ic_address -> {
(activity as? MapsActivity)?.showLocation(charger)
}
R.drawable.ic_fault_report -> {
@@ -565,6 +649,21 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
override fun onMapReady(map: AnyMap) {
this.map = map
chargerIconGenerator = ChargerIconGenerator(requireContext(), map.bitmapDescriptorFactory)
if (BuildConfig.FLAVOR == "google" && mapFragment!!.priority[0] == MapFragment.GOOGLE) {
// Google Maps: icons can be generated in background thread
lifecycleScope.launch {
withContext(Dispatchers.IO) {
chargerIconGenerator.preloadCache()
}
}
} else {
// Mapbox: needs to be run on main thread
chargerIconGenerator.preloadCache()
}
animator = MarkerAnimator(chargerIconGenerator)
map.uiSettings.setTiltGesturesEnabled(false)
map.setIndoorEnabled(false)
@@ -573,6 +672,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
vm.mapPosition.value = MapPosition(
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
)
binding.scaleView.update(map.cameraPosition.zoom, map.cameraPosition.target.latitude)
}
map.setOnCameraMoveListener {
binding.scaleView.update(map.cameraPosition.zoom, map.cameraPosition.target.latitude)
}
map.setOnMarkerClickListener { marker ->
when (marker) {
@@ -678,6 +781,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient)
if (location != null) {
val latLng = LatLng(location.latitude, location.longitude)
vm.location.value = latLng
val camUpdate = map.cameraUpdateFactory.newLatLngZoom(latLng, 13f)
if (animate) {
map.animateCamera(camUpdate)
@@ -703,7 +807,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
chargerIconGenerator.getBitmapDescriptor(
getMarkerTint(charger, vm.filteredConnectors.value),
highlight = charger == vm.chargerSparse.value,
fault = charger.faultReport != null
fault = charger.faultReport != null,
multi = getMarkerMulti(charger, vm.filteredConnectors.value)
)
)
}
@@ -720,7 +825,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
animator.animateMarkerDisappear(marker, tint, highlight, fault)
val multi = getMarkerMulti(charger, vm.filteredConnectors.value)
animator.animateMarkerDisappear(marker, tint, highlight, fault, multi)
} else {
animator.deleteMarker(marker)
}
@@ -734,21 +840,23 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
val multi = getMarkerMulti(charger, vm.filteredConnectors.value)
val marker = map.addMarker(
MarkerOptions()
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
.icon(
chargerIconGenerator.getBitmapDescriptor(
tint,
0,
0f,
255,
highlight,
fault
fault,
multi
)
)
.anchor(0.5f, 1f)
)
animator.animateMarkerAppear(marker, tint, highlight, fault)
animator.animateMarkerAppear(marker, tint, highlight, fault, multi)
markers[marker] = charger
}
}
@@ -801,34 +909,94 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
})
}
filterView?.setOnClickListener {
var profilesMap: MutableBiMap<Long, MenuItem> = HashBiMap()
val popup = PopupMenu(requireContext(), it, Gravity.END)
popup.menuInflater.inflate(R.menu.popup_filter, popup.menu)
MenuCompat.setGroupDividerEnabled(popup.menu, true)
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_edit_filters -> {
lifecycleScope.launch {
vm.copyFiltersToCustom()
requireView().findNavController().navigate(
R.id.action_map_to_filterFragment
)
}
true
}
R.id.menu_manage_filter_profiles -> {
requireView().findNavController().navigate(
R.id.action_map_to_filterFragment
R.id.action_map_to_filterProfilesFragment
)
true
}
R.id.menu_filters_active -> {
vm.filtersActive.value = !vm.filtersActive.value!!
else -> {
val profileId = profilesMap.inverse[it]
if (profileId != null) {
vm.filterStatus.value = profileId
}
true
}
else -> false
}
}
val checkItem = popup.menu.findItem(R.id.menu_filters_active)
vm.filtersActive.observe(viewLifecycleOwner, Observer {
checkItem.isChecked = it
vm.filterProfiles.observe(viewLifecycleOwner, { profiles ->
popup.menu.removeGroup(R.id.menu_group_filter_profiles)
val noFiltersItem = popup.menu.add(
R.id.menu_group_filter_profiles,
Menu.NONE, Menu.NONE, R.string.no_filters
)
profiles.forEach { profile ->
val item = popup.menu.add(
R.id.menu_group_filter_profiles,
Menu.NONE,
Menu.NONE,
profile.name
)
profilesMap[profile.id] = item
}
val customItem = popup.menu.add(
R.id.menu_group_filter_profiles,
Menu.NONE, Menu.NONE, R.string.filter_custom
)
profilesMap[FILTERS_DISABLED] = noFiltersItem
profilesMap[FILTERS_CUSTOM] = customItem
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()
vm.filterStatus.observe(viewLifecycleOwner, Observer { id ->
when (id) {
FILTERS_DISABLED -> {
customItem.isVisible = false
noFiltersItem.isChecked = true
}
FILTERS_CUSTOM -> {
customItem.isVisible = true
customItem.isChecked = true
}
else -> {
customItem.isVisible = false
val item = profilesMap[id]
if (item != null) {
item.isChecked = true
}
// else unknown ID -> wait for filterProfiles to update
}
}
})
})
popup.show()
}
filterView?.setOnLongClickListener {
// enable/disable filters
vm.filtersActive.value = !vm.filtersActive.value!!
vm.toggleFilters()
// haptic feedback
filterView.performHapticFeedback(
HapticFeedbackConstants.LONG_PRESS,
@@ -836,7 +1004,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
)
// show snackbar
Snackbar.make(
requireView(), if (vm.filtersActive.value!!) {
requireView(), if (vm.filterStatus.value != FILTERS_DISABLED) {
R.string.filters_activated
} else {
R.string.filters_deactivated
@@ -858,19 +1026,20 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
}
override fun getRootView(): View {
return root
return binding.root
}
private val exitElementCallback: SharedElementCallback = object : SharedElementCallback() {
override fun onMapSharedElements(
names: MutableList<String>,
sharedElements: MutableMap<String, View>
) {
// Locate the ViewHolder for the clicked position.
val position = galleryVm.galleryPosition.value ?: return
private val reenterSharedElementCallback: SharedElementCallback =
object : SharedElementCallback() {
override fun onMapSharedElements(
names: MutableList<String>,
sharedElements: MutableMap<String, View>
) {
// Locate the ViewHolder for the clicked position.
val position = galleryVm.galleryPosition.value ?: return
val vh = binding.gallery.findViewHolderForAdapterPosition(position)
if (vh?.itemView == null) return
val vh = binding.gallery.findViewHolderForAdapterPosition(position)
if (vh?.itemView == null) return
// Map the first shared element name to the child ImageView.
sharedElements[names[0]] = vh.itemView
@@ -889,9 +1058,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
override fun onConnected() {
val map = this.map ?: return
val context = this.context ?: return
if (vm.myLocationEnabled.value == true) {
if (ActivityCompat.checkSelfPermission(
requireContext(),
context,
ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {

View File

@@ -7,10 +7,10 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.core.widget.doAfterTextChanged
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.dialog_multi_select.*
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.DataBindingAdapter
import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.databinding.DialogMultiSelectBinding
import java.util.*
import kotlin.collections.HashMap
import kotlin.collections.HashSet
@@ -37,13 +37,15 @@ class MultiSelectDialog : AppCompatDialogFragment() {
var okListener: ((Set<String>) -> Unit)? = null
var cancelListener: (() -> Unit)? = null
private lateinit var items: List<MultiSelectItem>
private lateinit var binding: DialogMultiSelectBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.dialog_multi_select, container)
): View {
binding = DialogMultiSelectBinding.inflate(inflater, container, false)
return binding.root
}
override fun onStart() {
@@ -65,41 +67,41 @@ class MultiSelectDialog : AppCompatDialogFragment() {
args.getSerializable("commonChoices") as HashSet<String>
} else null
dialogTitle.text = title
binding.dialogTitle.text = title
val adapter = Adapter()
list.adapter = adapter
list.layoutManager = LinearLayoutManager(view.context)
binding.list.adapter = adapter
binding.list.layoutManager = LinearLayoutManager(view.context)
items = data.entries.toList()
.sortedBy { it.value }
.sortedBy { it.value.toLowerCase(Locale.getDefault()) }
.sortedByDescending { commonChoices?.contains(it.key) == true }
.map { MultiSelectItem(it.key, it.value, it.key in selected) }
adapter.submitList(items)
etSearch.doAfterTextChanged { text ->
binding.etSearch.doAfterTextChanged { text ->
adapter.submitList(search(items, text.toString()))
}
btnCancel.setOnClickListener {
binding.btnCancel.setOnClickListener {
cancelListener?.let { listener ->
listener()
}
dismiss()
}
btnOK.setOnClickListener {
binding.btnOK.setOnClickListener {
okListener?.let { listener ->
val result = items.filter { it.selected }.map { it.key }.toSet()
listener(result)
}
dismiss()
}
btnAll.setOnClickListener {
binding.btnAll.setOnClickListener {
items = items.map { MultiSelectItem(it.key, it.name, true) }
adapter.submitList(search(items, etSearch.text.toString()))
adapter.submitList(search(items, binding.etSearch.text.toString()))
}
btnNone.setOnClickListener {
binding.btnNone.setOnClickListener {
items = items.map { MultiSelectItem(it.key, it.name, false) }
adapter.submitList(search(items, etSearch.text.toString()))
adapter.submitList(search(items, binding.etSearch.text.toString()))
}
}
}

View File

@@ -0,0 +1,40 @@
package net.vonforst.evmap.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import net.vonforst.evmap.databinding.DialogWelcomeBinding
import net.vonforst.evmap.storage.PreferenceDataSource
class WelcomeDialogFragment : AppCompatDialogFragment() {
private lateinit var binding: DialogWelcomeBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DialogWelcomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.btnOk.setOnClickListener {
PreferenceDataSource(requireContext()).welcomeDialogShown = true
dismiss()
}
}
override fun onStart() {
super.onStart()
dialog?.window?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT
)
}
}

View File

@@ -0,0 +1,55 @@
package net.vonforst.evmap.navigation
import android.app.Activity
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.util.AttributeSet
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.core.content.withStyledAttributes
import androidx.navigation.NavDestination
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
import net.vonforst.evmap.R
@Navigator.Name("chrome")
class ChromeCustomTabsNavigator(
private val context: Context
) : Navigator<ChromeCustomTabsNavigator.Destination>() {
override fun createDestination() =
Destination(this)
override fun navigate(
destination: Destination,
args: Bundle?,
navOptions: NavOptions?,
navigatorExtras: Extras?
): NavDestination? {
val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(ContextCompat.getColor(context, R.color.colorPrimary))
.build()
)
.build()
intent.launchUrl(context, destination.url!!)
return null // Do not add to the back stack, managed by Chrome Custom Tabs
}
override fun popBackStack() = true // Managed by Chrome Custom Tabs
@NavDestination.ClassType(Activity::class)
class Destination(navigator: Navigator<out NavDestination>) : NavDestination(navigator) {
var url: Uri? = null
override fun onInflate(context: Context, attrs: AttributeSet) {
super.onInflate(context, attrs)
context.withStyledAttributes(attrs, R.styleable.ChromeCustomTabsNavigator, 0, 0) {
url = Uri.parse(getString(R.styleable.ChromeCustomTabsNavigator_url))
}
}
}
}

View File

@@ -0,0 +1,15 @@
package net.vonforst.evmap.navigation
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
class NavHostFragment : NavHostFragment() {
override fun onCreateNavController(navController: NavController) {
super.onCreateNavController(navController)
navController.navigatorProvider.addNavigator(
ChromeCustomTabsNavigator(
requireContext()
)
)
}
}

View File

@@ -10,6 +10,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
import net.vonforst.evmap.api.goingelectric.ChargeCard
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.viewmodel.BooleanFilterValue
import net.vonforst.evmap.viewmodel.FILTERS_CUSTOM
import net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue
import net.vonforst.evmap.viewmodel.SliderFilterValue
@@ -19,15 +20,17 @@ import net.vonforst.evmap.viewmodel.SliderFilterValue
BooleanFilterValue::class,
MultipleChoiceFilterValue::class,
SliderFilterValue::class,
FilterProfile::class,
Plug::class,
Network::class,
ChargeCard::class
], version = 8
], version = 10
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun chargeLocationsDao(): ChargeLocationsDao
abstract fun filterValueDao(): FilterValueDao
abstract fun filterProfileDao(): FilterProfileDao
abstract fun plugDao(): PlugDao
abstract fun networkDao(): NetworkDao
abstract fun chargeCardDao(): ChargeCardDao
@@ -38,8 +41,14 @@ abstract class AppDatabase : RoomDatabase() {
Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db")
.addMigrations(
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
MIGRATION_7, MIGRATION_8
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
// create default filter profile
db.execSQL("INSERT INTO `FilterProfile` (`name`, `id`, `order`) VALUES ('FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
}
})
.build()
}
@@ -120,5 +129,46 @@ abstract class AppDatabase : RoomDatabase() {
db.execSQL("ALTER TABLE `ChargeLocation` ADD `chargecards` TEXT")
}
}
private val MIGRATION_9 = object : Migration(8, 9) {
override fun migrate(db: SupportSQLiteDatabase) {
db.beginTransaction()
try {
// create filter profiles table
db.execSQL("CREATE TABLE IF NOT EXISTS `FilterProfile` (`name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)")
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_FilterProfile_name` ON `FilterProfile` (`name`)")
// create default filter profile
db.execSQL("INSERT INTO `FilterProfile` (`name`, `id`) VALUES ('FILTERS_CUSTOM', $FILTERS_CUSTOM)")
// add profile column to existing filtervalue tables
db.execSQL("CREATE TABLE `BooleanFilterValueNew` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`), FOREIGN KEY(`profile`) REFERENCES `FilterProfile`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE `MultipleChoiceFilterValueNew` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`), FOREIGN KEY(`profile`) REFERENCES `FilterProfile`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("CREATE TABLE IF NOT EXISTS `SliderFilterValueNew` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`), FOREIGN KEY(`profile`) REFERENCES `FilterProfile`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )");
for (table in listOf(
"BooleanFilterValue",
"MultipleChoiceFilterValue",
"SliderFilterValue"
)) {
db.execSQL("ALTER TABLE `$table` ADD COLUMN `profile` INTEGER NOT NULL DEFAULT $FILTERS_CUSTOM")
db.execSQL("INSERT INTO `${table}New` SELECT * FROM `$table`")
db.execSQL("DROP TABLE `$table`")
db.execSQL("ALTER TABLE `${table}New` RENAME TO `$table`")
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
}
private val MIGRATION_10 = object : Migration(9, 10) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `FilterProfile` ADD `order` INTEGER NOT NULL DEFAULT 0")
}
}
}
}

View File

@@ -0,0 +1,36 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData
import androidx.room.*
import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.viewmodel.FILTERS_CUSTOM
@Entity(
indices = [Index(value = ["name"], unique = true)]
)
data class FilterProfile(
val name: String,
@PrimaryKey(autoGenerate = true) val id: Long = 0,
var order: Int = 0
) : Equatable
@Dao
interface FilterProfileDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(profile: FilterProfile): Long
@Update
suspend fun update(vararg profiles: FilterProfile)
@Delete
suspend fun delete(vararg profiles: FilterProfile)
@Query("SELECT * FROM filterProfile WHERE id != $FILTERS_CUSTOM ORDER BY `order` ASC, `name` ASC")
fun getProfiles(): LiveData<List<FilterProfile>>
@Query("SELECT * FROM filterProfile WHERE name = :name")
suspend fun getProfileByName(name: String): FilterProfile?
@Query("SELECT * FROM filterProfile WHERE id = :id")
suspend fun getProfileById(id: Long): FilterProfile?
}

View File

@@ -2,22 +2,20 @@ package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.room.*
import net.vonforst.evmap.viewmodel.BooleanFilterValue
import net.vonforst.evmap.viewmodel.FilterValue
import net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue
import net.vonforst.evmap.viewmodel.SliderFilterValue
import net.vonforst.evmap.viewmodel.*
@Dao
abstract class FilterValueDao {
@Query("SELECT * FROM booleanfiltervalue")
protected abstract fun getBooleanFilterValues(): LiveData<List<BooleanFilterValue>>
@Query("SELECT * FROM booleanfiltervalue WHERE profile = :profile")
protected abstract fun getBooleanFilterValues(profile: Long): LiveData<List<BooleanFilterValue>>
@Query("SELECT * FROM multiplechoicefiltervalue")
protected abstract fun getMultipleChoiceFilterValues(): LiveData<List<MultipleChoiceFilterValue>>
@Query("SELECT * FROM multiplechoicefiltervalue WHERE profile = :profile")
protected abstract fun getMultipleChoiceFilterValues(profile: Long): LiveData<List<MultipleChoiceFilterValue>>
@Query("SELECT * FROM sliderfiltervalue")
protected abstract fun getSliderFilterValues(): LiveData<List<SliderFilterValue>>
@Query("SELECT * FROM sliderfiltervalue WHERE profile = :profile")
protected abstract fun getSliderFilterValues(profile: Long): LiveData<List<SliderFilterValue>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract suspend fun insert(vararg values: BooleanFilterValue)
@@ -28,16 +26,29 @@ abstract class FilterValueDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract suspend fun insert(vararg values: SliderFilterValue)
open fun getFilterValues(): LiveData<List<FilterValue>> =
MediatorLiveData<List<FilterValue>>().apply {
val sources = listOf(
getBooleanFilterValues(),
getMultipleChoiceFilterValues(),
getSliderFilterValues()
)
for (source in sources) {
addSource(source) {
value = sources.mapNotNull { it.value }.flatten()
@Query("DELETE FROM booleanfiltervalue WHERE profile = :profile")
protected abstract suspend fun deleteBooleanFilterValuesForProfile(profile: Long)
@Query("DELETE FROM multiplechoicefiltervalue WHERE profile = :profile")
protected abstract suspend fun deleteMultipleChoiceFilterValuesForProfile(profile: Long)
@Query("DELETE FROM sliderfiltervalue WHERE profile = :profile")
protected abstract suspend fun deleteSliderFilterValuesForProfile(profile: Long)
open fun getFilterValues(filterStatus: Long): LiveData<List<FilterValue>> =
if (filterStatus == FILTERS_DISABLED) {
MutableLiveData(emptyList())
} else {
MediatorLiveData<List<FilterValue>>().apply {
val sources = listOf(
getBooleanFilterValues(filterStatus),
getMultipleChoiceFilterValues(filterStatus),
getSliderFilterValues(filterStatus)
)
for (source in sources) {
addSource(source) {
value = sources.mapNotNull { it.value }.flatten()
}
}
}
}
@@ -52,4 +63,12 @@ abstract class FilterValueDao {
}
}
}
@Transaction
open suspend fun deleteFilterValuesForProfile(profile: Long) {
deleteBooleanFilterValuesForProfile(profile)
deleteMultipleChoiceFilterValuesForProfile(profile)
deleteSliderFilterValuesForProfile(profile)
}
}

View File

@@ -3,6 +3,8 @@ package net.vonforst.evmap.storage
import android.content.Context
import androidx.preference.PreferenceManager
import net.vonforst.evmap.R
import net.vonforst.evmap.viewmodel.FILTERS_CUSTOM
import net.vonforst.evmap.viewmodel.FILTERS_DISABLED
import java.time.Instant
class PreferenceDataSource(val context: Context) {
@@ -32,12 +34,33 @@ class PreferenceDataSource(val context: Context) {
sp.edit().putLong("last_chargecard_update", value.toEpochMilli()).apply()
}
var filtersActive: Boolean
get() = sp.getBoolean("filters_active", true)
/**
* Stores the current filtering status, which is either the ID of a filter profile or
* one of FILTERS_DISABLED, FILTERS_CUSTOM
*/
var filterStatus: Long
get() =
sp.getLong(
"filter_status",
// migration from versions before filter profiles were implemented
if (sp.getBoolean("filters_active", true))
FILTERS_CUSTOM else FILTERS_DISABLED
)
set(value) {
sp.edit().putBoolean("filters_active", value).apply()
sp.edit().putLong("filter_status", value).apply()
}
/**
* Stores the last filter profile which was selected
* (excluding FILTERS_DISABLED, but including FILTERS_CUSTOM)
*/
var lastFilterProfile: Long
get() = sp.getLong("last_filter_profile", FILTERS_CUSTOM)
set(value) {
sp.edit().putLong("last_filter_profile", value).apply()
}
val language: String
get() = sp.getString("language", "default")!!
@@ -49,4 +72,10 @@ class PreferenceDataSource(val context: Context) {
"map_provider",
context.getString(R.string.pref_map_provider_default)
)!!
var welcomeDialogShown: Boolean
get() = sp.getBoolean("welcome_dialog_shown", false)
set(value) {
sp.edit().putBoolean("welcome_dialog_shown", value).apply()
}
}

View File

@@ -2,6 +2,7 @@ package net.vonforst.evmap.ui
import android.content.Context
import android.content.res.ColorStateList
import android.text.SpannableString
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.widget.ImageView
@@ -25,6 +26,25 @@ fun goneUnless(view: View, visible: Boolean) {
view.visibility = if (visible) View.VISIBLE else View.GONE
}
@BindingAdapter("goneUnlessAnimated")
fun goneUnlessAnimated(view: View, oldValue: Boolean, newValue: Boolean) {
if (oldValue == newValue) return
view.animate().cancel()
if (newValue) {
view.visibility = View.VISIBLE
view.alpha = 0f
view.animate().alpha(1f).withEndAction {
view.alpha = 1f
}
} else {
view.animate().alpha(0f).withEndAction {
view.alpha = 1f
view.visibility = View.GONE
}
}
}
@BindingAdapter("invisibleUnless")
fun invisibleUnless(view: View, visible: Boolean) {
view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
@@ -131,6 +151,26 @@ fun setTopMargin(view: View, topMargin: Float) {
view.layoutParams = layoutParams
}
/**
* Linkify is already possible using the autoLink and linksClickable attributes, but this does not
* remove spans correctly. So we implement a new version that manually removes the spans.
*/
@BindingAdapter("linkify")
fun setLinkify(textView: TextView, oldValue: Int, newValue: Int) {
if (oldValue == newValue) return
textView.autoLinkMask = newValue
textView.linksClickable = newValue != 0
// remove spans
val text = textView.text
if (newValue == 0 && text != null && text is SpannableString) {
text.getSpans(0, text.length, Any::class.java).forEach {
text.removeSpan(it)
}
}
}
private fun availabilityColor(
status: List<ChargepointStatus>?,
context: Context
@@ -160,4 +200,8 @@ fun availabilityText(status: List<ChargepointStatus>?): String? {
return if (unknown > 0) {
if (unknown == total) "?" else "$available?"
} else available.toString()
}
fun flatten(it: Iterable<Iterable<ChargepointStatus>>?): List<ChargepointStatus>? {
return it?.flatten()
}

View File

@@ -0,0 +1,63 @@
package net.vonforst.evmap.ui
import android.content.Context
import android.content.DialogInterface
import android.view.Gravity
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.FrameLayout
import androidx.appcompat.app.AlertDialog
private fun dialogEditText(ctx: Context): Pair<View, EditText> {
val container = FrameLayout(ctx)
container.setPadding(
(16 * ctx.resources.displayMetrics.density).toInt(), 0,
(16 * ctx.resources.displayMetrics.density).toInt(), 0
)
val input = EditText(ctx)
input.isSingleLine = true
container.addView(input)
return container to input
}
fun showEditTextDialog(
ctx: Context,
customize: (AlertDialog.Builder, EditText) -> Unit
): AlertDialog {
val (container, input) = dialogEditText(ctx)
val dialogBuilder = AlertDialog.Builder(ctx)
.setView(container)
customize(dialogBuilder, input)
val dialog = dialogBuilder.show()
// move dialog to top
val attrs = dialog.window?.attributes?.apply {
gravity = Gravity.TOP
}
dialog.window?.attributes = attrs
// focus and show keyboard
input.requestFocus()
input.postDelayed({
val imm =
ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT)
}, 100)
input.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
val text = input.text
val button = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
if (text != null && button != null) {
button.performClick()
return@setOnEditorActionListener true
}
}
false
}
return dialog
}

View File

@@ -16,6 +16,7 @@ import com.car2go.maps.model.BitmapDescriptor
import com.google.maps.android.ui.IconGenerator
import com.google.maps.android.ui.SquareTextView
import net.vonforst.evmap.R
import kotlin.math.roundToInt
class ClusterIconGenerator(context: Context) : IconGenerator(context) {
init {
@@ -41,27 +42,30 @@ class ClusterIconGenerator(context: Context) : IconGenerator(context) {
}
class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFactory) {
data class BitmapData(
class ChargerIconGenerator(
val context: Context, val factory: BitmapDescriptorFactory,
val scaleResolution: Int = 20
) {
private data class BitmapData(
val tint: Int,
val scale: Int,
val alpha: Int,
val highlight: Boolean,
val fault: Boolean
val fault: Boolean,
val multi: Boolean
)
val cacheSize = 420; // 420 items: 21 sizes, 5 colors, highlight on/off, fault on/off
val cache = LruCache<BitmapData, BitmapDescriptor>(cacheSize)
val oversize = 1.4f // increase to add padding for fault icon or scale > 1
val icon = R.drawable.ic_map_marker_charging
val highlightIcon = R.drawable.ic_map_marker_highlight
val faultIcon = R.drawable.ic_map_marker_fault
// 230 items: (21 sizes, 5 colors, multi on/off) + highlight + fault (only with scale = 1)
private val cacheSize = (scaleResolution + 3) * 5 * 2;
private val cache = LruCache<BitmapData, BitmapDescriptor>(cacheSize)
private val oversize = 1.4f // increase to add padding for fault icon or scale > 1
private val icon = R.drawable.ic_map_marker_charging
private val multiIcon = R.drawable.ic_map_marker_charging_multiple
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
init {
preloadCache()
}
private fun preloadCache() {
fun preloadCache() {
// pre-generates images for scale from 0 to 255 for all possible tint colors
val tints = listOf(
R.color.charger_100kw,
@@ -72,9 +76,14 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
)
for (fault in listOf(false, true)) {
for (highlight in listOf(false, true)) {
for (tint in tints) {
for (scale in 0..20) {
getBitmapDescriptor(tint, scale, 255, highlight, fault)
for (multi in listOf(false, true)) {
for (tint in tints) {
for (scale in 0..scaleResolution) {
getBitmapDescriptor(
tint, scale.toFloat() / scaleResolution,
255, highlight, fault, multi
)
}
}
}
}
@@ -83,12 +92,19 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
fun getBitmapDescriptor(
@ColorRes tint: Int,
scale: Int = 20,
scale: Float = 1f,
alpha: Int = 255,
highlight: Boolean = false,
fault: Boolean = false
fault: Boolean = false,
multi: Boolean = false
): BitmapDescriptor? {
val data = BitmapData(tint, scale, alpha, highlight, fault)
val data = BitmapData(
tint, (scale * scaleResolution).roundToInt(),
alpha,
if (scale == 1f) highlight else false,
if (scale == 1f) fault else false,
multi
)
val cachedImg = cache[data]
return if (cachedImg != null) {
cachedImg
@@ -101,13 +117,14 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
}
private fun generateBitmap(data: BitmapData): Bitmap {
val vd: Drawable = context.getDrawable(icon)!!
val icon = if (data.multi) multiIcon else icon
val vd: Drawable = ContextCompat.getDrawable(context, icon)!!
DrawableCompat.setTint(vd, ContextCompat.getColor(context, data.tint));
DrawableCompat.setTintMode(vd, PorterDuff.Mode.MULTIPLY);
val leftPadding = vd.intrinsicWidth * (oversize - 1) / 2
val topPadding = vd.intrinsicWidth * (oversize - 1)
val topPadding = vd.intrinsicHeight * (oversize - 1)
vd.setBounds(
leftPadding.toInt(), topPadding.toInt(),
leftPadding.toInt() + vd.intrinsicWidth,
@@ -121,7 +138,7 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
)
val canvas = Canvas(bm)
val scale = data.scale / 20f
val scale = data.scale.toFloat() / scaleResolution
canvas.scale(
scale,
scale,
@@ -132,7 +149,8 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
vd.draw(canvas)
if (data.highlight) {
val highlightDrawable = context.getDrawable(highlightIcon)!!
val hIcon = if (data.multi) highlightIconMulti else highlightIcon
val highlightDrawable = ContextCompat.getDrawable(context, hIcon)!!
highlightDrawable.setBounds(
leftPadding.toInt(), topPadding.toInt(),
leftPadding.toInt() + vd.intrinsicWidth,
@@ -143,7 +161,7 @@ class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFa
}
if (data.fault) {
val faultDrawable = context.getDrawable(faultIcon)!!
val faultDrawable = ContextCompat.getDrawable(context, faultIcon)!!
val faultSize = 0.75
val faultShift = 0.25
val base = vd.intrinsicWidth

View File

@@ -28,24 +28,26 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
marker: Marker,
tint: Int,
highlight: Boolean,
fault: Boolean
fault: Boolean,
multi: Boolean
) {
animatingMarkers[marker]?.let {
it.cancel()
animatingMarkers.remove(marker)
}
val anim = ValueAnimator.ofInt(0, 20).apply {
val anim = ValueAnimator.ofFloat(0f, 1f).apply {
duration = 250
interpolator = LinearOutSlowInInterpolator()
addUpdateListener { animationState ->
val scale = animationState.animatedValue as Int
val scale = animationState.animatedValue as Float
marker.setIcon(
gen.getBitmapDescriptor(
tint,
scale = scale,
highlight = highlight,
fault = fault
fault = fault,
multi = multi
)
)
}
@@ -63,24 +65,26 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
marker: Marker,
tint: Int,
highlight: Boolean,
fault: Boolean
fault: Boolean,
multi: Boolean
) {
animatingMarkers[marker]?.let {
it.cancel()
animatingMarkers.remove(marker)
}
val anim = ValueAnimator.ofInt(20, 0).apply {
val anim = ValueAnimator.ofFloat(1f, 0f).apply {
duration = 200
interpolator = FastOutLinearInInterpolator()
addUpdateListener { animationState ->
val scale = animationState.animatedValue as Int
val scale = animationState.animatedValue as Float
marker.setIcon(
gen.getBitmapDescriptor(
tint,
scale = scale,
highlight = highlight,
fault = fault
fault = fault,
multi = multi
)
)
}

View File

@@ -13,8 +13,6 @@ class LocaleContextWrapper(base: Context?) : ContextWrapper(base) {
fun wrap(context: Context, language: String): ContextWrapper {
val sysConfig: Configuration = context.applicationContext.resources.configuration
val appConfig: Configuration = context.resources.configuration
var ctx = context
if (language == "" || language == "default") {
// set default locale
@@ -37,8 +35,7 @@ class LocaleContextWrapper(base: Context?) : ContextWrapper(base) {
}
}
ctx = context.createConfigurationContext(appConfig)
return LocaleContextWrapper(ctx)
return LocaleContextWrapper(context.createConfigurationContext(appConfig))
}
}
}

View File

@@ -71,7 +71,7 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
) / 1000
}
})
}
}?.sortedBy { it.distance }
}
addSource(favorites, callback)
addSource(location, callback)

View File

@@ -0,0 +1,41 @@
package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
class FilterProfilesViewModel(application: Application) : AndroidViewModel(application) {
private var db = AppDatabase.getInstance(application)
private var prefs = PreferenceDataSource(application)
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
db.filterProfileDao().getProfiles()
}
fun delete(itemId: Long) {
viewModelScope.launch {
val profile = db.filterProfileDao().getProfileById(itemId)
profile?.let { db.filterProfileDao().delete(it) }
if (prefs.filterStatus == profile?.id) {
prefs.filterStatus = FILTERS_DISABLED
}
}
}
fun insert(item: FilterProfile) {
viewModelScope.launch {
db.filterProfileDao().insert(item)
}
}
fun reorderProfiles(list: List<FilterProfile>) {
viewModelScope.launch {
db.filterProfileDao().update(*list.toTypedArray())
}
}
}

View File

@@ -2,12 +2,11 @@ package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.databinding.BaseObservable
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.*
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.ForeignKey
import androidx.room.ForeignKey.CASCADE
import kotlinx.coroutines.launch
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.api.goingelectric.ChargeCard
@@ -22,7 +21,7 @@ val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 75, 100, 150, 200, 250, 300,
internal fun mapPower(i: Int) = powerSteps[i]
internal fun mapPowerInverse(power: Int) = powerSteps
.mapIndexed { index, v -> abs(v - power) to index }
.minBy { it.first }?.second ?: 0
.minByOrNull { it.first }?.second ?: 0
internal fun getFilters(
application: Application,
@@ -40,7 +39,8 @@ internal fun getFilters(
Chargepoint.CHADEMO to application.getString(R.string.plug_chademo),
Chargepoint.SUPERCHARGER to application.getString(R.string.plug_supercharger),
Chargepoint.CEE_BLAU to application.getString(R.string.plug_cee_blau),
Chargepoint.CEE_ROT to application.getString(R.string.plug_cee_rot)
Chargepoint.CEE_ROT to application.getString(R.string.plug_cee_rot),
Chargepoint.TESLA_ROADSTER_HPC to application.getString(R.string.plug_roadster_hpc)
)
listOf(plugs, networks, chargeCards).forEach { source ->
addSource(source) { _ ->
@@ -62,6 +62,34 @@ private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
}?.toMap() ?: return
val networkMap = networks.value?.map { it.name to it.name }?.toMap() ?: return
val chargecardMap = chargeCards.value?.map { it.id.toString() to it.name }?.toMap() ?: return
val categoryMap = mapOf(
"Autohaus" to application.getString(R.string.category_car_dealership),
"Autobahnraststätte" to application.getString(R.string.category_service_on_motorway),
"Autohof" to application.getString(R.string.category_service_off_motorway),
"Bahnhof" to application.getString(R.string.category_railway_station),
"Behörde" to application.getString(R.string.category_public_authorities),
"Campingplatz" to application.getString(R.string.category_camping),
"Einkaufszentrum" to application.getString(R.string.category_shopping_mall),
"Ferienwohnung" to application.getString(R.string.category_holiday_home),
"Flughafen" to application.getString(R.string.category_airport),
"Freizeitpark" to application.getString(R.string.category_amusement_park),
"Hotel" to application.getString(R.string.category_hotel),
"Kino" to application.getString(R.string.category_cinema),
"Kirche" to application.getString(R.string.category_church),
"Krankenhaus" to application.getString(R.string.category_hospital),
"Museum" to application.getString(R.string.category_museum),
"Parkhaus" to application.getString(R.string.category_parking_multi),
"Parkplatz" to application.getString(R.string.category_parking),
"Privater Ladepunkt" to application.getString(R.string.category_private_charger),
"Rastplatz" to application.getString(R.string.category_rest_area),
"Restaurant" to application.getString(R.string.category_restaurant),
"Schwimmbad" to application.getString(R.string.category_swimming_pool),
"Supermarkt" to application.getString(R.string.category_supermarket),
"Tankstelle" to application.getString(R.string.category_petrol_station),
"Tiefgarage" to application.getString(R.string.category_parking_underground),
"Tierpark" to application.getString(R.string.category_zoo),
"Wohnmobilstellplatz" to application.getString(R.string.category_caravan_site)
)
value = listOf(
BooleanFilter(application.getString(R.string.filter_free), "freecharging"),
BooleanFilter(application.getString(R.string.filter_free_parking), "freeparking"),
@@ -89,6 +117,11 @@ private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
application.getString(R.string.filter_networks), "networks",
networkMap, manyChoices = true
),
MultipleChoiceFilter(
application.getString(R.string.categories), "categories",
categoryMap,
manyChoices = true
),
BooleanFilter(application.getString(R.string.filter_barrierfree), "barrierfree"),
MultipleChoiceFilter(
application.getString(R.string.filter_chargecards), "chargecards",
@@ -101,25 +134,17 @@ private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
internal fun filtersWithValue(
filters: LiveData<List<Filter<FilterValue>>>,
filterValues: LiveData<List<FilterValue>>,
active: LiveData<Boolean>? = null
filterValues: LiveData<List<FilterValue>>
): MediatorLiveData<List<FilterWithValue<out FilterValue>>> =
MediatorLiveData<List<FilterWithValue<out FilterValue>>>().apply {
listOf(filters, filterValues, active).forEach {
if (it == null) return@forEach
listOf(filters, filterValues).forEach {
addSource(it) {
val filters = filters.value ?: return@addSource
value = if (active != null && !active.value!!) {
filters.map { filter ->
FilterWithValue(filter, filter.defaultValue())
}
} else {
val values = filterValues.value ?: return@addSource
filters.map { filter ->
val value =
values.find { it.key == filter.key } ?: filter.defaultValue()
FilterWithValue(filter, filter.valueClass.cast(value))
}
val f = filters.value ?: return@addSource
val values = filterValues.value ?: return@addSource
value = f.map { filter ->
val value =
values.find { it.key == filter.key } ?: filter.defaultValue()
FilterWithValue(filter, filter.valueClass.cast(value))
}
}
}
@@ -145,17 +170,59 @@ class FilterViewModel(application: Application, geApiKey: String) :
}
private val filterValues: LiveData<List<FilterValue>> by lazy {
db.filterValueDao().getFilterValues()
db.filterValueDao().getFilterValues(FILTERS_CUSTOM)
}
val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
filtersWithValue(filters, filterValues)
}
private val filterStatus: LiveData<Long> by lazy {
MutableLiveData<Long>().apply {
value = prefs.filterStatus
}
}
val filterProfile: LiveData<FilterProfile> by lazy {
MediatorLiveData<FilterProfile>().apply {
addSource(filterStatus) { id ->
when (id) {
FILTERS_CUSTOM, FILTERS_DISABLED -> value = null
else -> viewModelScope.launch {
value = db.filterProfileDao().getProfileById(id)
}
}
}
}
}
suspend fun saveFilterValues() {
filtersWithValue.value?.forEach {
db.filterValueDao().insert(it.value)
val value = it.value
value.profile = FILTERS_CUSTOM
db.filterValueDao().insert(value)
}
// set selected profile
prefs.filterStatus = FILTERS_CUSTOM
}
suspend fun saveAsProfile(name: String) {
// get or create profile
var profileId = db.filterProfileDao().getProfileByName(name)?.id
if (profileId == null) {
profileId = db.filterProfileDao().insert(FilterProfile(name))
}
// save filter values
filtersWithValue.value?.forEach {
val value = it.value
value.profile = profileId
db.filterValueDao().insert(value)
}
// set selected profile
prefs.filterStatus = profileId
}
}
@@ -198,17 +265,34 @@ data class SliderFilter(
sealed class FilterValue : BaseObservable(), Equatable {
abstract val key: String
var profile: Long = FILTERS_CUSTOM
}
@Entity
@Entity(
foreignKeys = [ForeignKey(
entity = FilterProfile::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("profile"),
onDelete = CASCADE
)],
primaryKeys = ["key", "profile"]
)
data class BooleanFilterValue(
@PrimaryKey override val key: String,
override val key: String,
var value: Boolean
) : FilterValue()
@Entity
@Entity(
foreignKeys = [ForeignKey(
entity = FilterProfile::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("profile"),
onDelete = CASCADE
)],
primaryKeys = ["key", "profile"]
)
data class MultipleChoiceFilterValue(
@PrimaryKey override val key: String,
override val key: String,
var values: MutableSet<String>,
var all: Boolean
) : FilterValue() {
@@ -222,12 +306,30 @@ data class MultipleChoiceFilterValue(
!other.all && values == other.values
}
}
override fun hashCode(): Int {
var result = key.hashCode()
result = 31 * result + all.hashCode()
result = 31 * result + if (all) 0 else values.hashCode()
return result
}
}
@Entity
@Entity(
foreignKeys = [ForeignKey(
entity = FilterProfile::class,
parentColumns = arrayOf("id"),
childColumns = arrayOf("profile"),
onDelete = CASCADE
)],
primaryKeys = ["key", "profile"]
)
data class SliderFilterValue(
@PrimaryKey override val key: String,
override val key: String,
var value: Int
) : FilterValue()
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable
const val FILTERS_DISABLED = -2L
const val FILTERS_CUSTOM = -1L

View File

@@ -10,6 +10,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.distanceBetween
import net.vonforst.evmap.api.goingelectric.*
import net.vonforst.evmap.storage.*
import net.vonforst.evmap.ui.cluster
@@ -46,7 +47,16 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
MutableLiveData<MapPosition>()
}
private val filterValues: LiveData<List<FilterValue>> by lazy {
db.filterValueDao().getFilterValues()
MediatorLiveData<List<FilterValue>>().apply {
var source: LiveData<List<FilterValue>>? = null
addSource(filterStatus) { status ->
source?.let { removeSource(it) }
source = db.filterValueDao().getFilterValues(status)
addSource(source!!) { result ->
value = result
}
}
}
}
private val plugs: LiveData<List<Plug>> by lazy {
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
@@ -60,7 +70,11 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
private val filters = getFilters(application, plugs, networks, chargeCards)
private val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
filtersWithValue(filters, filterValues, filtersActive)
filtersWithValue(filters, filterValues)
}
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
db.filterProfileDao().getProfiles()
}
val chargeCardMap: LiveData<Map<Long, ChargeCard>> by lazy {
@@ -128,6 +142,28 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
}
}
val chargerDistance: MediatorLiveData<Double> by lazy {
MediatorLiveData<Double>().apply {
val callback = { _: Any? ->
val loc = location.value
val charger = chargerSparse.value
value = if (loc != null && charger != null && myLocationEnabled.value == true) {
distanceBetween(
loc.latitude,
loc.longitude,
charger.coordinates.lat,
charger.coordinates.lng
) / 1000
} else null
}
addSource(chargerSparse, callback)
addSource(location, callback)
addSource(myLocationEnabled, callback)
}
}
val location: MutableLiveData<LatLng> by lazy {
MutableLiveData<LatLng>()
}
val availability: MediatorLiveData<Resource<ChargeLocationStatus>> by lazy {
MediatorLiveData<Resource<ChargeLocationStatus>>().apply {
addSource(chargerSparse) { charger ->
@@ -170,15 +206,38 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
}
val filtersActive: MutableLiveData<Boolean> by lazy {
MutableLiveData<Boolean>().apply {
value = prefs.filtersActive
val filterStatus: MutableLiveData<Long> by lazy {
MutableLiveData<Long>().apply {
value = prefs.filterStatus
observeForever {
prefs.filtersActive = it
prefs.filterStatus = it
if (it != FILTERS_DISABLED) prefs.lastFilterProfile = it
}
}
}
fun reloadPrefs() {
filterStatus.value = prefs.filterStatus
}
fun toggleFilters() {
if (filterStatus.value == FILTERS_DISABLED) {
filterStatus.value = prefs.lastFilterProfile
} else {
filterStatus.value = FILTERS_DISABLED
}
}
suspend fun copyFiltersToCustom() {
if (filterStatus.value == FILTERS_CUSTOM) return
db.filterValueDao().deleteFilterValuesForProfile(FILTERS_CUSTOM)
filterValues.value?.forEach {
it.profile = FILTERS_CUSTOM
db.filterValueDao().insert(it)
}
}
fun setMapType(type: AnyMap.Type) {
mapType.value = type
}
@@ -257,6 +316,13 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
val networks = formatMultipleChoice(networksVal)
val categoriesVal = getMultipleChoiceValue(filters, "categories")
if (categoriesVal.values.isEmpty() && !categoriesVal.all) {
// no categories chosen
return Triple(Resource.success(emptyList()), filteredConnectors, filteredChargeCards)
}
val categories = formatMultipleChoice(categoriesVal)
// do not use clustering if filters need to be applied locally.
val useClustering = zoom < 13
val geClusteringAvailable = minConnectors <= 1
@@ -285,6 +351,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
plugs = connectors,
chargecards = chargeCards,
networks = networks,
categories = categories,
startkey = startkey
)
if (!response.isSuccessful || response.body()!!.status != "ok") {

View File

@@ -6,6 +6,7 @@ import androidx.lifecycle.*
import java.util.concurrent.atomic.AtomicBoolean
@Suppress("UNCHECKED_CAST")
inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(aClass: Class<T>): T = f() as T

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="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</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="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>

View File

@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="44.11976dp"
android:viewportWidth="233.8"
android:viewportHeight="368.4">
<path
android:fillColor="#FFFFFF"
android:fillAlpha="0.8"
android:pathData="M143.2,109.4l-19.7,33.8l0,38.1l43.4,-74.4l-22.2,0z" />
<path
android:fillColor="#FFFFFF"
android:fillAlpha="0.8"
android:pathData="M122.2,101.9h16.7h5.7l22.3,-44.6c0,0 -10.2,0 -22.4,0l-1.1,2.2L122.2,101.9z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M138.9,57.3c-9.7,0 -19.8,0 -26.4,0c-2.5,0 -5.1,0 -7.6,0c-8.2,0 -16.1,0 -21.4,0c-4.1,0 -6.6,0 -6.6,0v68.2h18.6v55.8l43.4,-74.4h-24.8L138.9,57.3z" />
</vector>

View File

@@ -0,0 +1,18 @@
<vector android:height="44.11976dp"
android:viewportHeight="368.4"
android:viewportWidth="233.8"
android:width="28dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#FFFFFF"
android:pathData="M109.8,0h13.6c33.9,1.9 67.1,18.5 87.7,45.8c13.5,17.2 21,38.6 22.7,60.3v8.1c-0.8,42.1 -27.7,76.6 -51,109.4c-26.2,37 -50.4,77.3 -57.1,122.9c-1.8,7.7 0.4,18.5 -8.9,22c-2.2,-1.7 -4.7,-3.1 -6.2,-5.4c-2.7,-25.5 -9.1,-50.7 -20,-73.9c-12.3,-27.1 -29.5,-51.6 -47,-75.6C33,199 23,184.2 14.7,168.3c-13,-23.8 -17.9,-51.9 -12.5,-78.6C6.6,68.6 17.6,49.1 32.8,34C53.3,14 81.1,1.8 109.8,0z" />
<path
android:fillColor="#B5B5B5"
android:pathData="M143.2,109.4l-19.7,33.8l0,38.1l43.4,-74.4l-22.2,0z" />
<path
android:fillColor="#B5B5B5"
android:pathData="M122.2,101.9h16.7h5.7l22.3,-44.6c0,0 -10.2,0 -22.4,0l-1.1,2.2L122.2,101.9z" />
<path
android:fillColor="#808080"
android:pathData="M138.9,57.3c-9.7,0 -19.8,0 -26.4,0c-2.5,0 -5.1,0 -7.6,0c-8.2,0 -16.1,0 -21.4,0c-4.1,0 -6.6,0 -6.6,0v68.2h18.6v55.8l43.4,-74.4h-24.8L138.9,57.3z" />
</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="M3,15h18v-2L3,13v2zM3,19h18v-2L3,17v2zM3,11h18L21,9L3,9v2zM3,5v2h18L21,5L3,5z" />
</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="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
</vector>

View File

@@ -7,7 +7,7 @@
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:name="net.vonforst.evmap.navigation.NavHostFragment"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:fitsSystemWindows="true"

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imageView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_launcher_foreground" />
<TextView
android:id="@+id/textView14"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline4"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/imageView2"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -21,10 +21,16 @@
<import type="net.vonforst.evmap.viewmodel.Status" />
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
<variable
name="charger"
type="Resource&lt;ChargeLocation&gt;" />
<variable
name="distance"
type="Double" />
<variable
name="availability"
type="Resource&lt;ChargeLocationStatus&gt;" />
@@ -37,6 +43,10 @@
name="filteredChargeCards"
type="java.util.Set&lt;Long&gt;" />
<variable
name="expanded"
type="Boolean" />
</data>
<androidx.cardview.widget.CardView
@@ -55,14 +65,15 @@
android:paddingBottom="8dp">
<TextView
android:id="@+id/textView"
android:id="@+id/txtName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@{charger.data.name}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintEnd_toStartOf="@+id/txtAvailability"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="Parkhaus" />
@@ -77,18 +88,55 @@
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView"
app:layout_constraintTop_toBottomOf="@+id/txtName"
tools:text="Beispielstraße 10, 12345 Berlin" />
<TextView
android:id="@+id/textView3"
android:id="@+id/txtDistance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:gravity="end"
android:maxLines="1"
android:minWidth="50dp"
android:text="@{@string/distance_format(distance)}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintBottom_toBottomOf="@+id/topPart"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
tools:text="10 km" />
<TextView
android:id="@+id/txtAvailability"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="72dp"
android:background="@drawable/rounded_rect"
android:ellipsize="end"
android:gravity="end"
android:maxLines="1"
android:padding="2dp"
android:text="@{String.format(&quot;%s/%d&quot;, BindingAdaptersKt.availabilityText(BindingAdaptersKt.flatten(availability.data.status.values())), charger.data.totalChargepoints)}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@android:color/white"
app:backgroundTintAvailability="@{BindingAdaptersKt.flatten(availability.data.status.values())}"
app:goneUnlessAnimated="@{availability.data != null &amp;&amp; !expanded}"
app:goneUnless="@{availability.data != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="@+id/txtName"
tools:backgroundTint="@color/available"
tools:text="2/2" />
<TextView
android:id="@+id/txtConnectors"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@{charger.data.formatChargepoints()}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintEnd_toStartOf="@+id/txtDistance"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView2"
tools:text="2x Typ 2 22 kW" />
@@ -116,7 +164,7 @@
android:textColor="?colorPrimary"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/textView3" />
app:layout_constraintTop_toBottomOf="@+id/txtConnectors" />
<TextView
android:id="@+id/textView12"
@@ -139,6 +187,8 @@
android:layout_marginTop="2dp"
android:text="@{charger.data.amenities}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:autoLink="web"
android:linksClickable="true"
app:goneUnless="@{charger.data.amenities != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
@@ -167,6 +217,8 @@
android:layout_marginTop="2dp"
android:text="@{charger.data.generalInformation}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
android:autoLink="web"
android:linksClickable="true"
app:goneUnless="@{charger.data.generalInformation != null}"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintHorizontal_bias="0.0"
@@ -239,10 +291,10 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="@+id/textView3"
app:layout_constraintBottom_toBottomOf="@+id/txtConnectors"
app:layout_constraintEnd_toStartOf="@+id/guideline2"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="@+id/textView" />
app:layout_constraintTop_toTopOf="@+id/txtName" />
<Button
android:id="@+id/btnChargeprice"

View File

@@ -0,0 +1,199 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/linearLayout4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="16dp"
android:paddingTop="16dp"
android:paddingRight="16dp"
android:paddingBottom="16dp">
<include
android:id="@+id/include"
layout="@layout/app_logo"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/welcomeTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/welcome_to_evmap"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/include" />
<TextView
android:id="@+id/welcomeText1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/welcome_1"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/welcomeTitle" />
<ImageView
android:id="@+id/icon1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="@+id/iconLabel1"
app:layout_constraintStart_toStartOf="@+id/iconLabel1"
app:layout_constraintTop_toBottomOf="@+id/welcomeText1"
app:srcCompat="@drawable/ic_map_marker_charging"
app:tint="@color/charger_low"
app:tintMode="multiply" />
<TextView
android:id="@+id/iconLabel1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="&lt;11 kW"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/iconLabel2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/icon1" />
<ImageView
android:id="@+id/icon2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="@+id/iconLabel2"
app:layout_constraintStart_toStartOf="@+id/iconLabel2"
app:layout_constraintTop_toBottomOf="@+id/welcomeText1"
app:srcCompat="@drawable/ic_map_marker_charging"
app:tint="@color/charger_11kw"
app:tintMode="multiply" />
<TextView
android:id="@+id/iconLabel2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="≥11 kW"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/iconLabel3"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/iconLabel1"
app:layout_constraintTop_toBottomOf="@+id/icon2" />
<ImageView
android:id="@+id/icon3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="@+id/iconLabel3"
app:layout_constraintStart_toStartOf="@+id/iconLabel3"
app:layout_constraintTop_toBottomOf="@+id/welcomeText1"
app:srcCompat="@drawable/ic_map_marker_charging"
app:tint="@color/charger_20kw"
app:tintMode="multiply" />
<TextView
android:id="@+id/iconLabel3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="≥20 kW"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/iconLabel4"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/iconLabel2"
app:layout_constraintTop_toBottomOf="@+id/icon3" />
<ImageView
android:id="@+id/icon4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="@+id/iconLabel4"
app:layout_constraintStart_toStartOf="@+id/iconLabel4"
app:layout_constraintTop_toBottomOf="@+id/welcomeText1"
app:srcCompat="@drawable/ic_map_marker_charging"
app:tint="@color/charger_43kw"
app:tintMode="multiply" />
<TextView
android:id="@+id/iconLabel4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="≥43 kW"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toStartOf="@+id/iconLabel5"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/iconLabel3"
app:layout_constraintTop_toBottomOf="@+id/icon4" />
<ImageView
android:id="@+id/icon5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="@+id/iconLabel5"
app:layout_constraintStart_toStartOf="@+id/iconLabel5"
app:layout_constraintTop_toBottomOf="@+id/welcomeText1"
app:srcCompat="@drawable/ic_map_marker_charging"
app:tint="@color/charger_100kw"
app:tintMode="multiply" />
<TextView
android:id="@+id/iconLabel5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="≥100 kW"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/iconLabel4"
app:layout_constraintTop_toBottomOf="@+id/icon5" />
<TextView
android:id="@+id/welcomeText2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/welcome_2"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iconLabel1" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<Button
android:id="@+id/btnOk"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ok"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:layout_gravity="end" />
</LinearLayout>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="net.vonforst.evmap.viewmodel.FilterProfilesViewModel" />
<variable
name="vm"
type="FilterProfilesViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/linearLayout3"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintBottom_toTopOf="@+id/filter_profiles_list"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/filter_profiles_list"
android:layout_width="0dp"
android:layout_height="0dp"
app:data="@{vm.filterProfiles}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar_container"
tools:itemCount="3"
tools:listitem="@layout/item_filter_boolean" />
<TextView
android:id="@+id/textView19"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:breakStrategy="balanced"
android:gravity="center_horizontal"
android:text="@string/filterprofiles_empty_state"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:textColorSecondary"
app:goneUnless="@{vm.filterProfiles.size() == 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View File

@@ -9,6 +9,8 @@
<import type="net.vonforst.evmap.viewmodel.Status" />
<import type="com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike" />
<variable
name="vm"
type="net.vonforst.evmap.viewmodel.MapViewModel" />
@@ -24,6 +26,20 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_anchor="@id/fab_locate"
app:layout_anchorGravity="start|center_vertical"
android:layout_gravity="start|center_vertical">
<com.github.pengrad.mapscaleview.MapScaleView
android:id="@+id/scaleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp" />
</FrameLayout>
<FrameLayout
android:id="@+id/toolbar_container"
android:layout_width="match_parent"
@@ -74,7 +90,6 @@
android:layout_width="match_parent"
android:layout_height="@dimen/gallery_height_with_margin"
android:background="?android:colorBackground"
android:fitsSystemWindows="true"
app:layout_behavior="@string/BackDropBottomSheetBehavior">
<androidx.recyclerview.widget.RecyclerView
@@ -110,7 +125,6 @@
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:fillViewport="true"
android:orientation="vertical"
app:bottomsheetbehavior_anchorPoint="@dimen/gallery_height"
@@ -118,7 +132,7 @@
app:behavior_peekHeight="@dimen/peek_height"
app:bottomsheetbehavior_defaultState="stateHidden"
app:layout_behavior="@string/BottomSheetBehaviorGoogleMapsLike"
tools:bottomsheetbehavior_defaultState="stateHidden">
tools:bottomsheetbehavior_defaultState="stateCollapsed">
<include
android:id="@+id/detail_view"
@@ -126,7 +140,9 @@
app:charger="@{vm.charger}"
app:availability="@{vm.availability}"
app:chargeCards="@{vm.chargeCardMap}"
app:filteredChargeCards="@{vm.filteredChargeCards}" />
app:filteredChargeCards="@{vm.filteredChargeCards}"
app:distance="@{vm.chargerDistance}"
app:expanded="@{vm.bottomSheetState != BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED &amp;&amp; vm.bottomSheetState != BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN}" />
</androidx.core.widget.NestedScrollView>

View File

@@ -18,7 +18,7 @@
app:selectableItemBackground="@{item.clickable}">
<TextView
android:id="@+id/textView9"
android:id="@+id/txtTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
@@ -48,7 +48,7 @@
tools:srcCompat="@drawable/ic_address" />
<TextView
android:id="@+id/textView8"
android:id="@+id/txtContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
@@ -56,13 +56,12 @@
android:layout_marginBottom="14dp"
android:text="@{item.detailText}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:autoLink="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
android:linksClickable="@{item.links}"
app:linkify="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
app:goneUnless="@{item.detailText != null}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintTop_toBottomOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintTop_toBottomOf="@+id/txtTitle"
tools:text="Lorem ipsum" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -24,7 +24,7 @@
app:selectableItemBackground="@{item.clickable}">
<TextView
android:id="@+id/textView9"
android:id="@+id/txtTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
@@ -54,21 +54,20 @@
tools:srcCompat="@drawable/ic_address" />
<TextView
android:id="@+id/textView8"
android:id="@+id/txtContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
android:autoLink="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
android:linksClickable="@{item.links}"
app:linkify="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
android:text="@{item.detailText}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:goneUnless="@{item.detailText != null}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/expandToggle"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintTop_toBottomOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintTop_toBottomOf="@+id/txtTitle"
app:layout_constraintVertical_bias="0.0"
tools:text="Lorem ipsum" />
@@ -83,8 +82,8 @@
app:goneUnless="@{expandToggle.checked}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintTop_toBottomOf="@+id/textView8" />
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintTop_toBottomOf="@+id/txtContent" />
<include
android:id="@+id/hours_tue"
@@ -97,7 +96,7 @@
app:dayOfWeek="@{DayOfWeek.TUESDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintTop_toBottomOf="@id/hours_mon" />
<include
@@ -111,7 +110,7 @@
app:dayOfWeek="@{DayOfWeek.WEDNESDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintTop_toBottomOf="@id/hours_tue" />
<include
@@ -125,7 +124,7 @@
app:dayOfWeek="@{DayOfWeek.THURSDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintTop_toBottomOf="@id/hours_wed" />
<include
@@ -139,7 +138,7 @@
app:dayOfWeek="@{DayOfWeek.FRIDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintTop_toBottomOf="@id/hours_thu" />
<include
@@ -153,7 +152,7 @@
app:dayOfWeek="@{DayOfWeek.SATURDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintTop_toBottomOf="@id/hours_fri" />
<include
@@ -167,7 +166,7 @@
app:dayOfWeek="@{DayOfWeek.SUNDAY}"
app:hours="@{item.hoursDays}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintTop_toBottomOf="@id/hours_sat" />
<include
@@ -183,7 +182,7 @@
app:hours="@{item.hoursDays}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textView9"
app:layout_constraintStart_toStartOf="@+id/txtTitle"
app:layout_constraintTop_toBottomOf="@id/hours_sun" />
<ToggleButton

View File

@@ -45,7 +45,7 @@
tools:text="Beispielstraße 10, 12345 Berlin" />
<TextView
android:id="@+id/textView3"
android:id="@+id/txtConnectors"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="item"
type="net.vonforst.evmap.storage.FilterProfile" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/delete_red"> <!--Add your background color here-->
<ImageView
android:id="@+id/delete_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical|end"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
app:tint="@android:color/white"
app:srcCompat="@drawable/ic_delete" />
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/foreground"
android:layout_width="match_parent"
android:layout_height="?listPreferredItemHeight"
android:background="?android:colorBackground">
<TextView
android:id="@+id/textView9"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:text="@{item.name}"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnRename"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.511"
tools:text="Lorem ipsum" />
<ImageButton
android:id="@+id/btnDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:tint="?colorControlNormal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/handle"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_delete"
android:contentDescription="@string/delete" />
<ImageButton
android:id="@+id/btnRename"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:tint="?colorControlNormal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btnDelete"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_edit"
android:contentDescription="@string/rename" />
<ImageView
android:id="@+id/handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:contentDescription="@string/reorder"
android:scaleType="center"
android:src="@drawable/ic_reorder"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</layout>

View File

@@ -1,28 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="24dp">
<ImageView
android:id="@+id/imageView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_launcher_foreground" />
<TextView
android:id="@+id/textView14"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline4"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/imageView2"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include layout="@layout/app_logo" />
</FrameLayout>

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_edit"
android:icon="@drawable/ic_edit"
android:title="@string/edit_on_goingelectric"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_share"
android:icon="@drawable/ic_share"

View File

@@ -13,6 +13,14 @@
android:icon="@drawable/ic_fav"
android:title="@string/menu_favs" />
</group>
<group
android:id="@+id/nav_group_links"
android:checkableBehavior="none">
<item
android:id="@+id/report_new_charger"
android:icon="@drawable/ic_add"
android:title="@string/menu_report_new_charger" />
</group>
<group
android:id="@+id/nav_group_settings"
android:checkableBehavior="single">

View File

@@ -1,9 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_save_profile"
android:title="@string/menu_save_profile"
android:icon="@drawable/ic_save"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_apply"
android:title="@string/menu_filter"
android:title="@string/menu_apply"
android:icon="@drawable/ic_check"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -1,11 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_filters_active"
android:title="@string/menu_filters_active"
android:checkable="true"
android:checked="true" />
<group
android:checkableBehavior="single"
android:id="@+id/menu_group_filter_profiles">
</group>
<item
android:id="@+id/menu_edit_filters"
android:title="@string/menu_edit_filters" />
android:title="@string/menu_edit_filters"
android:menuCategory="secondary" />
<item
android:id="@+id/menu_manage_filter_profiles"
android:title="@string/menu_manage_filter_profiles"
android:menuCategory="secondary" />
</menu>

View File

@@ -24,6 +24,16 @@
app:enterAnim="@anim/fragment_fade_enter"
app:popEnterAnim="@anim/fragment_fade_enter"
app:popExitAnim="@anim/fragment_fade_exit" />
<action
android:id="@+id/action_map_to_filterProfilesFragment"
app:destination="@id/filter_profiles"
app:exitAnim="@anim/fragment_fade_exit"
app:enterAnim="@anim/fragment_fade_enter"
app:popEnterAnim="@anim/fragment_fade_enter"
app:popExitAnim="@anim/fragment_fade_exit" />
<action
android:id="@+id/action_map_to_welcome"
app:destination="@id/welcome" />
</fragment>
<fragment
android:id="@+id/about"
@@ -58,9 +68,22 @@
android:name="net.vonforst.evmap.fragment.FilterFragment"
android:label="@string/menu_filter"
tools:layout="@layout/fragment_filter" />
<fragment
android:id="@+id/filter_profiles"
android:name="net.vonforst.evmap.fragment.FilterProfilesFragment"
android:label="@string/menu_manage_filter_profiles"
tools:layout="@layout/fragment_filter_profiles" />
<fragment
android:id="@+id/donate"
android:name="net.vonforst.evmap.fragment.DonateFragment"
android:label="@string/donate"
tools:layout="@layout/fragment_donate" />
<dialog
android:id="@+id/welcome"
android:name="net.vonforst.evmap.fragment.WelcomeDialogFragment"
android:label="@string/welcome_to_evmap"
tools:layout="@layout/dialog_welcome" />
<chrome
android:id="@+id/report_new_charger"
app:url="@string/report_new_charger_url" />
</navigation>

View File

@@ -3,7 +3,6 @@
android:duration="375"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:transitionOrdering="together">
<changeClipBounds />
<changeTransform />
<changeImageTransform />
<changeBounds />
</transitionSet>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">EV Map</string>
<string name="title_activity_maps">EV Map</string>
<string name="app_name">EVMap</string>
<string name="title_activity_maps">EVMap</string>
<string name="connectors">Anschlüsse</string>
<string name="no_maps_app_found">Keine Navigations-App gefunden</string>
<string name="address">Adresse</string>
@@ -62,6 +62,7 @@
<string name="plug_supercharger">Tesla Supercharger</string>
<string name="plug_cee_blau">CEE Blau</string>
<string name="plug_cee_rot">CEE Rot</string>
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
<string name="all">alle</string>
<string name="none">keine</string>
<string name="show_more">mehr…</string>
@@ -80,7 +81,8 @@
<string name="menu_filters_active">Filter aktiv</string>
<string name="filters_activated">Filter aktiviert</string>
<string name="filters_deactivated">Filter deaktiviert</string>
<string name="menu_edit_filters">Filter bearbeiten</string>
<string name="menu_edit_filters">Filter bearbeiten</string>
<string name="menu_manage_filter_profiles">Filterprofile verwalten</string>
<string name="go_to_chargeprice">Preisvergleich</string>
<string name="fault_report">Störungsmeldung</string>
<string name="fault_report_date">Störungsmeldung (Letztes Update: %s)</string>
@@ -103,8 +105,55 @@
<string name="charge_cards">Ladetarife</string>
<string name="and_n_others">und %d weitere</string>
<string name="pref_map_provider">Kartenanbieter</string>
<string name="twitter">Twitter</string>
<string name="goingelectric_forum">Forenthread bei GoingElectric.de</string>
<string name="contact">Kontakt</string>
<string name="menu_report_new_charger">Ladesäule melden</string>
<string name="edit_on_goingelectric">bei GoingElectric.de bearbeiten</string>
<string name="categories">Kategorien</string>
<string name="category_car_dealership">Autohaus</string>
<string name="category_service_on_motorway">Autobahnraststätte</string>
<string name="category_service_off_motorway">Autohof</string>
<string name="category_railway_station">Bahnhof</string>
<string name="category_public_authorities">Behörde</string>
<string name="category_camping">Campingplatz</string>
<string name="category_shopping_mall">Einkaufszentrum</string>
<string name="category_holiday_home">Ferienwohnung</string>
<string name="category_airport">Flughafen</string>
<string name="category_amusement_park">Freizeitpark</string>
<string name="category_hotel">Hotel</string>
<string name="category_cinema">Kino</string>
<string name="category_church">Kirche</string>
<string name="category_hospital">Krankenhaus</string>
<string name="category_museum">Museum</string>
<string name="category_parking_multi">Parkhaus</string>
<string name="category_parking">Parkplatz</string>
<string name="category_private_charger">Privater Ladepunkt</string>
<string name="category_rest_area">Rastplatz</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_swimming_pool">Schwimmbad</string>
<string name="category_supermarket">Supermarkt</string>
<string name="category_petrol_station">Tankstelle</string>
<string name="category_parking_underground">Tiefgarage</string>
<string name="category_zoo">Tierpark</string>
<string name="category_caravan_site">Wohnmobilstellplatz</string>
<string name="menu_apply">Filter anwenden</string>
<string name="menu_save_profile">Als Profil speichern</string>
<string name="no_filters">Keine Filter</string>
<string name="filter_custom">Verändertes Filterprofil</string>
<string name="reorder">Reihenfolge ändern</string>
<string name="delete">Löschen</string>
<string name="save_as_profile">Als Profil speichern</string>
<string name="save_profile_enter_name">Geben Sie den Namen des Filterprofils ein:</string>
<string name="filterprofiles_empty_state">Du hast noch keine Filterprofile gespeichert.</string>
<string name="welcome_to_evmap">Willkommen bei EVMap!</string>
<string name="welcome_1">Mit EVMap kannst du Ladestationen für Elektroautos in deiner Nähe finden. EVMap nutzt dafür die Community-gepflegte Datenbank von GoingElectric.de, die sich vor allem auf Europa und den deutschsprachigen Raum konzentriert. Über die Website GoingElectric.de kannst du selbst zum Verzeichnis beitragen.\n\nDie Ladestationen werden auf der Karte mit verschiedenen Farben angezeigt, die die maximale Ladeleistung angeben:</string>
<string name="welcome_2">EVMap ist kostenlos und Open Source. Du kannst bei GitHub zur Weiterentwicklung beitragen oder die Entwicklung mit Spenden unterstützen. Die entsprechenden Links findest du unter „Über EVMap” im Menü.</string>
<string name="deleted_filterprofile">„%s” gelöscht</string>
<string name="undo">Rückgängig</string>
<string name="rename">Umbenennen</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d kompatibler Ladetarif</item>
<item quantity="other">%d kompatible Ladetarife</item>
</plurals>
</resources>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ChromeCustomTabsNavigator">
<attr name="url" format="reference" />
</declare-styleable>
</resources>

View File

@@ -13,4 +13,5 @@
<color name="unavailable">#f44336</color>
<color name="unknown">#9e9e9e</color>
<color name="status_bar_scrim">#C3000000</color>
<color name="delete_red">#f44336</color>
</resources>

View File

@@ -4,4 +4,8 @@
<string name="github_link">https://github.com/johan12345/EVMap</string>
<string name="privacy_link">https://evmap.vonforst.net/privacy.html</string>
<string name="faq_link">https://evmap.vonforst.net/faq.html</string>
<string name="twitter_handle">\@ev_map</string>
<string name="twitter_url">https://twitter.com/ev_map</string>
<string name="goingelectric_forum_url"><![CDATA[https://www.goingelectric.de/forum/viewtopic.php?f=5&t=56342]]></string>
<string name="report_new_charger_url">https://www.goingelectric.de/stromtankstellen/new/</string>
</resources>

View File

@@ -1,6 +1,6 @@
<resources>
<string name="app_name">EV Map</string>
<string name="title_activity_maps">EV Map</string>
<string name="app_name">EVMap</string>
<string name="title_activity_maps">EVMap</string>
<string name="connectors">Connectors</string>
<string name="no_maps_app_found">No navigation app found</string>
<string name="address">Address</string>
@@ -61,6 +61,7 @@
<string name="plug_supercharger">Tesla Supercharger</string>
<string name="plug_cee_blau">CEE Blue</string>
<string name="plug_cee_rot">CEE Red</string>
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
<string name="all">all</string>
<string name="none">none</string>
<string name="show_more">more…</string>
@@ -79,7 +80,8 @@
<string name="menu_filters_active">Filters active</string>
<string name="filters_activated">Filters activated</string>
<string name="filters_deactivated">Filters deactivated</string>
<string name="menu_edit_filters">Edit filters</string>
<string name="menu_edit_filters">Edit filters</string>
<string name="menu_manage_filter_profiles">Manage filter profiles</string>
<string name="go_to_chargeprice">Compare prices</string>
<string name="fault_report">Fault report</string>
<string name="fault_report_date">Fault report (last update: %s)</string>
@@ -102,6 +104,53 @@
<string name="charge_cards">Payment methods</string>
<string name="and_n_others">and %d others</string>
<string name="pref_map_provider">Map provider</string>
<string name="twitter">Twitter</string>
<string name="goingelectric_forum">Forum thread at GoingElectric.de</string>
<string name="contact">Contact</string>
<string name="menu_report_new_charger">Report new charger</string>
<string name="edit_on_goingelectric">edit on GoingElectric.de</string>
<string name="categories">Categories</string>
<string name="category_car_dealership">Car Dealership</string>
<string name="category_service_on_motorway">Service area (on motorway)</string>
<string name="category_service_off_motorway">Service area (off motorway)</string>
<string name="category_railway_station">Railway station</string>
<string name="category_public_authorities">Public authorities</string>
<string name="category_camping">Camping site</string>
<string name="category_shopping_mall">Shopping mall</string>
<string name="category_holiday_home">Holiday home</string>
<string name="category_airport">Airport</string>
<string name="category_amusement_park">Amusement park</string>
<string name="category_hotel">Hotel</string>
<string name="category_cinema">Cinema</string>
<string name="category_church">Church</string>
<string name="category_hospital">Hospital</string>
<string name="category_museum">Museum</string>
<string name="category_parking_multi">Multi-storey car park</string>
<string name="category_parking">Car park</string>
<string name="category_private_charger">Private charger</string>
<string name="category_rest_area">Rest area</string>
<string name="category_restaurant">Restaurant</string>
<string name="category_swimming_pool">Swimming pool</string>
<string name="category_supermarket">Supermarket</string>
<string name="category_petrol_station">Petrol station</string>
<string name="category_parking_underground">Underground car park</string>
<string name="category_zoo">Zoo</string>
<string name="category_caravan_site">Caravan site</string>
<string name="menu_apply">Apply filters</string>
<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="reorder">reorder</string>
<string name="delete">Delete</string>
<string name="save_as_profile">Save as profile</string>
<string name="save_profile_enter_name">Enter the name of the filter profile:</string>
<string name="filterprofiles_empty_state">You have not yet saved any filter profiles.</string>
<string name="welcome_to_evmap">Welcome to EVMap!</string>
<string name="welcome_1">Using EVMap, you can find electric vehicle chargers around you. EVMap uses the community-maintained database from GoingElectric.de, which focuses on chargers in Europe and the German-speaking countries. You can contribute to this database on the GoingElectric.de website.\n\nChargers are shown on the map in different colors, which correspond to their maximum charging power:</string>
<string name="welcome_2">EVMap is free and Open Source software. You can contribute to the development on GitHub or support me through donations. The corresponding links can be found under “About EVMap” in the menu.</string>
<string name="deleted_filterprofile">Deleted “%s”</string>
<string name="undo">Undo</string>
<string name="rename">Rename</string>
<plurals name="charge_cards_compatible_num">
<item quantity="one">%d compatible payment method</item>
<item quantity="other">%d compatible payment methods</item>

View File

@@ -21,6 +21,18 @@
<Preference
android:key="donate"
android:title="@string/donate" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/contact">
<Preference
android:key="twitter"
android:title="@string/twitter"
android:summary="@string/twitter_handle" />
<Preference
android:key="goingelectric"
android:title="@string/goingelectric_forum" />
</PreferenceCategory>

View File

@@ -33,11 +33,11 @@ class NewMotionAvailabilityDetectorTest {
val notFoundResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
override fun dispatch(request: RecordedRequest): MockResponse {
val segments = request.requestUrl.pathSegments()
val segments = request.requestUrl!!.pathSegments
val urlHead = segments.subList(0, 2).joinToString("/")
when (urlHead) {
"ge/chargepoints" -> {
val id = request.requestUrl.queryParameter("ge_id")
val id = request.requestUrl!!.queryParameter("ge_id")
return okResponse("/chargers/$id.json")
}
"nm/markers" -> {

View File

@@ -24,18 +24,18 @@ class GoingElectricApiTest {
webServer.dispatcher = object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
val segments = request.requestUrl.pathSegments()
val segments = request.requestUrl!!.pathSegments
val urlHead = segments.subList(0, 2).joinToString("/")
when (urlHead) {
"ge/chargepoints" -> {
val id = request.requestUrl.queryParameter("ge_id")
val id = request.requestUrl!!.queryParameter("ge_id")
if (id != null) {
return okResponse("/chargers/$id.json")
} else {
val freeparking =
request.requestUrl.queryParameter("freeparking")!!.toBoolean()
request.requestUrl!!.queryParameter("freeparking")!!.toBoolean()
val freecharging =
request.requestUrl.queryParameter("freecharging")!!.toBoolean()
request.requestUrl!!.queryParameter("freecharging")!!.toBoolean()
return if (freeparking && freecharging) {
okResponse("/chargers/list-empty.json")
} else if (freecharging) {

View File

@@ -1,14 +1,14 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.72'
ext.kotlin_version = '1.4.21'
ext.about_libs_version = '8.1.1'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.1'
classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"

View File

@@ -0,0 +1,6 @@
Verbesserungen:
- Sortierung in Filterdialogen unabhängig von Groß- und Kleinschreibung
- Vertikale Position der Marker auf der Karte korrigiert
- Neues Icon für Ladestationen mit mehr als einem Anschluss (bei Schnelladern: mehr als ein schnelladefähiger Anschluss)
- "Über EVMap": Links zu Twitter-Account @ev_map und Thread im GoingElectric-Forum hinzugefügt
- Button "Ladesäule melden" im Hauptmenü hinzugefügt

View File

@@ -0,0 +1,14 @@
Frohes neues Jahr! 🎆
Neue Features:
- Sammlung von Filtern als Filterprofil speichern
- Filter nach Kategorien (Restaurant, Hotel, Parkhaus etc.)
- Maßstab wird auf der Karte angezeigt
- Knopf zum Bearbeiten eines Ladepunkts bei GoingElectric
- Willkommensdialog beim ersten Start
Korrekturen:
- Favoriten werden nach Abstand zur aktuellen Position sortiert
- Stabilitätsverbesserung beim Laden der Verfügbarkeit (v.a. in der Favoritenliste)
- App-Name "EV Map" -> "EVMap"
- Abstürze behoben

View File

@@ -0,0 +1,9 @@
Neue Funktionen:
- Filterprofile verwalten: Möglichkeit zum Löschen und Umbennenen der angelegten Filterprofile
- Verfügbarkeit der Ladestationen wird auch direkt in der Schnellansicht angezeigt
Verbesserungen:
- Fehlender OK-Button beim Willkommensdialog auf kleinen Bildschirmen behoben
- Nicht funktionierender Navigationsbutton auf Android 11 behoben
- Tippfehler behoben
- Fehlerhafte Links bei den Koordinaten behoben

View File

@@ -0,0 +1,2 @@
Verbesserungen:
- Verschiedene Abstürze behoben

View File

@@ -0,0 +1,6 @@
Improvements:
- Sorting of filter dialogs is case-insensitive
- Vertical positions of map markers fixed
- New icon for chargers with more than one connector (for fast chargers: more than one fast-charging connector)
- "About EVMap": Added links to Twitter account @ev_map and GoingElectric.de forum thread
- Added button "rerport new charger" in main menu

View File

@@ -0,0 +1,2 @@
Crash fixed
Fix wrong position of layers button on devices with camera cutout

View File

@@ -0,0 +1,14 @@
Happy new year! 🎆
New features:
- save collections of filter settings as a filter profile
- filter by categories (restaurant, hotel, car park etc.)
- show scale on map
- button to edit charger information on GoingElectric.de (only works when logged in)
- welcome dialog on first start
Fixes:
- favorites list sorted by distance
- improved stability of real-time availability info loading
- app name "EV Map" -> "EVMap"
- crashes fixed

View File

@@ -0,0 +1,9 @@
New features:
- Manage filter profiles: Buttons to delete and rename filter profiles
- Real-time availability also displayed in quick view
Improvements:
- Fixed missing OK button of welcome dialog on small screens
- Fixed not working navigation button on Android 11
- Fixed typos
- Removed incorrect links on charger coordinates

View File

@@ -0,0 +1,2 @@
Improvements:
- Fixed various crashes

View File

@@ -1,6 +1,6 @@
#Sun Mar 29 18:50:22 CEST 2020
#Wed Dec 23 14:54:49 CET 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip

View File

@@ -1,2 +1,2 @@
rootProject.name='EV Map'
rootProject.name='EVMap'
include ':app'