mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-25 08:07:46 -05:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14edb6f0cd | ||
|
|
7726088f91 | ||
|
|
cbc7c5a6d8 | ||
|
|
d510d81914 | ||
|
|
9f5abd6c91 | ||
|
|
966f62ac3d | ||
|
|
91caf40bdb | ||
|
|
72c0293365 | ||
|
|
ca9dc9629f | ||
|
|
438e529257 | ||
|
|
5f69123d89 | ||
|
|
cf421b52a8 | ||
|
|
1b049d35b8 | ||
|
|
f6690a3566 | ||
|
|
cc97020216 | ||
|
|
0e1e3ba46e | ||
|
|
657c209827 | ||
|
|
6ec44bb526 | ||
|
|
0943505d90 | ||
|
|
f155f7615f | ||
|
|
e8850575f2 | ||
|
|
d1c4d0a621 | ||
|
|
ecf27abdc5 | ||
|
|
5f5142baa6 | ||
|
|
fa53a9fc5a | ||
|
|
9a0a7b4e5f | ||
|
|
1a43703db5 | ||
|
|
459589c51f | ||
|
|
9393fe7380 | ||
|
|
f62bd1c3c4 | ||
|
|
27ff992d97 | ||
|
|
cb4b8a7d5f | ||
|
|
671424b202 | ||
|
|
ce1a7da1f5 | ||
|
|
236aefa34d | ||
|
|
d179490891 | ||
|
|
91e4cb3f14 | ||
|
|
37f02f52e9 | ||
|
|
01f1ffb646 | ||
|
|
131c93c86b | ||
|
|
4dd1a648ce | ||
|
|
91df749bc4 | ||
|
|
20b04e55fb | ||
|
|
a94ad9e8c2 | ||
|
|
807ff50612 | ||
|
|
d46ff39c2b | ||
|
|
199de04562 | ||
|
|
f2a18b7677 | ||
|
|
aab816db32 | ||
|
|
66ad6b9931 | ||
|
|
beeefb2be1 | ||
|
|
110c418d01 | ||
|
|
1296e66902 | ||
|
|
31d969e071 | ||
|
|
32681f6ea8 | ||
|
|
d77f67aa91 | ||
|
|
be071cfa3a | ||
|
|
e098c70684 | ||
|
|
91509f5846 | ||
|
|
454cc44793 | ||
|
|
1baf94d788 | ||
|
|
b0d9317f73 | ||
|
|
9b80f03993 | ||
|
|
af0fd8bf69 | ||
|
|
f76b19e818 | ||
|
|
02b717c612 | ||
|
|
e29d40bca2 | ||
|
|
7f8403cfb4 | ||
|
|
d5168f12c6 | ||
|
|
9b94bbf098 | ||
|
|
34c83c2253 | ||
|
|
16cfa3b37b |
@@ -6,6 +6,7 @@ env:
|
||||
- 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=
|
||||
- secure: LQHMdhaPUlCuJPFrCPpUphJSY6xzAFI/7RrcAVLtLcPhGdS+MeNifIkkAH7MeitTHroOC0dGkZ4bg/8/7bKfgwY4vPH9P50kZcnX5mI6zfBHgNYJzuthj+vJH9RAtkdQOW9Fe1uPIx8R9GUWUOVnkoJh0PQ1gDXdZW5fePqUtn1kYrcCCBE+Bhe3wz6QzTBqGS1nsVRTxQfSJNGi9uH1oi9kQGgQFuCCiJ/P0A6MIhSItkOfuggx/iorA+iASbhWkB4nXYQBbFe/ZhFJWbVfgYlOM0HtpKh8B2AqKw21Em32JoovCbUof4adkY7cH8/4Rt9SujC9YOw+a6oM+e//jJT0sie77V7zl670j+qODTuNvV4qVUwtoxShyc1Sfbd+Xb0xn/OC7DzBg97YuYCF/84yyuq12rl/cofynWE1L5YvGNSJk241XUw98Bvl0MK4VIfQvG9zJP0HnQZcWKt6kFOIEJSCRbmkd2tPPAZFBXBQf/bvpULOoKwneGJZBSapRoCyGwemM+EAzVB9UOXAqsXZ4FHkt1SSJVrTVwgxvXpCfmF6LZPhbz6nvouRWGsC/GdWjrHtdW5lEOvS27qKEL5rXwQ0o+71ZICGo8j4E0GOHXyi857qZhvO7cbOnts+iiawXiWzPXv2gGGabuqPwcU8JPEoWdaiIaeGUczfjBU=
|
||||
- secure: fvPVjj3l+TZ7HF5aGn/pmrkipGIrz+MkKNy3I7pnCJSuD/oVp9nQ5ePP/dAhaRThaW+fQbq7hOmCquPAtfoN9CUnHNV2f2l9RavDQIxdqvpXqY13A0BFffZho6A6H2kO7k6kQQPQEhl4SMJjObnX12/YDaTVx3b7aIroEJ8DyY62xGTsjExtaAksuFwUEekjh0MoWICvyBoDfrYhpiEVI2721rGMHu7FIXwmE38+jj7wwZd3Bp37yI9NY/b3ZQ/HUKyYDuoAL0xl5/GaQlRepD0v2xWQUQ40NArHLfMoscXi55UaENuswCg7rt9os8jCcZ8FkZf1cVsQ71JrE0uxgs00Jfjy2QKM5u1XUZefl1Nw5cfCDTWXIEGsz9OGiidFLehWUupX/6C6wr1BStdlRt+6Pt/FXsYHxO/qog++cKqHjOJRXi+raGAb99HhQ/hLnLUMKl5DIWlKF9DImXiOpfYxrgCJc3y91vNX6noJyWYs6PvErMukTsXFHen+fM0NtfTFoKW682oILvXjoeFvuzKpk49+rcpkJbRi5+Zdo/duSPp/flwvC4LOMi0RZOO9TNMhWKdkyWweDr1HEpvQn6RS87rpHzQwRDvm85F+PkZLMMqyWpuxBWbJf0jVbew21KvTJWamuizsIgCebFh0SSxgObzmMbAIFCkzL0PRsms=
|
||||
- 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
|
||||
|
||||
@@ -2,7 +2,7 @@ GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.2)
|
||||
addressable (2.7.0)
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.1.0)
|
||||
@@ -125,7 +125,7 @@ GEM
|
||||
naturally (2.2.0)
|
||||
os (1.1.1)
|
||||
plist (3.5.0)
|
||||
public_suffix (4.0.5)
|
||||
public_suffix (4.0.6)
|
||||
rake (13.0.1)
|
||||
representable (3.0.4)
|
||||
declarative (< 0.1.0)
|
||||
@@ -154,6 +154,7 @@ GEM
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unf_ext (0.0.7.7-x64-mingw32)
|
||||
unicode-display_width (1.7.0)
|
||||
word_wrap (1.0.0)
|
||||
@@ -170,6 +171,7 @@ GEM
|
||||
|
||||
PLATFORMS
|
||||
x64-mingw32
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane
|
||||
|
||||
10
README.md
10
README.md
@@ -3,7 +3,7 @@ EVMap [
|
||||
|
||||
<img src="https://raw.githubusercontent.com/johan12345/EVMap/master/_img/feature_graphic.svg" width=700 alt="Logo"/>
|
||||
|
||||
Android app to access the goingelectric.de electric vehicle charging station directory.
|
||||
Android app to find electric vehicle charging stations.
|
||||
|
||||
<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>
|
||||
@@ -14,7 +14,7 @@ Features
|
||||
--------
|
||||
|
||||
- [Material Design](https://material.io/)
|
||||
- Shows all charging stations from the community-maintained [GoingElectric.de](https://www.goingelectric.de/stromtankstellen/) directory
|
||||
- Shows all charging stations from the community-maintained [GoingElectric.de](https://www.goingelectric.de/stromtankstellen/) and [Open Charge Map](https://openchargemap.org) directories
|
||||
- Realtime availability information (beta)
|
||||
- Search places
|
||||
- Favorites list, also with availability information
|
||||
@@ -36,7 +36,8 @@ The App is developed using Android Studio.
|
||||
|
||||
For testing the app, you need to obtain free API Keys for the
|
||||
[GoingElectric API](https://www.goingelectric.de/stromtankstellen/api/),
|
||||
the [Chargeprice API](https://github.com/chargeprice/chargeprice-api-docs)
|
||||
the [Chargeprice API](https://github.com/chargeprice/chargeprice-api-docs),
|
||||
the [OpenChargeMap API](https://openchargemap.org/site/profile/appedit),
|
||||
as well as for [Google APIs](https://console.developers.google.com/)
|
||||
("Maps SDK for Android" and "Places API" need to be activated) and/or [Mapbox](https://www.mapbox.com/). These API keys need to be put into the
|
||||
app in the form of a resource file called `apikeys.xml` under `app/src/main/res/values`, with the
|
||||
@@ -56,5 +57,8 @@ following content:
|
||||
<string name="chargeprice_key" translatable="false">
|
||||
insert your Chargeprice key here
|
||||
</string>
|
||||
<string name="openchargemap_key" translatable="false">
|
||||
insert your OpenChargeMap key here
|
||||
</string>
|
||||
</resources>
|
||||
```
|
||||
|
||||
25
_img/connectors/connector_ccs_typ1.svg
Normal file
25
_img/connectors/connector_ccs_typ1.svg
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Ebene_5" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:none;stroke:#000000;stroke-width:2;stroke-miterlimit:10;}
|
||||
.st1{fill:none;stroke:#000000;stroke-width:1.7;stroke-miterlimit:10;}
|
||||
.st2{fill:none;stroke:#000000;stroke-width:0.5;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<circle cx="9" cy="18.7" r="1.4" />
|
||||
<circle cx="15" cy="18.7" r="1.4" />
|
||||
<path class="st0" d="M8.9,16.1h6.2c1.5,0,2.7,1.2,2.7,2.7l0,0c0,1.5-1.2,2.7-2.7,2.7H8.9c-1.5,0-2.7-1.2-2.7-2.7l0,0
|
||||
C6.2,17.3,7.4,16.1,8.9,16.1z" />
|
||||
<g>
|
||||
<circle cx="14.7" cy="6.4" r="1.3" />
|
||||
<circle cx="15.3" cy="10.5" r="0.8" />
|
||||
<circle cx="8.7" cy="10.5" r="0.8" />
|
||||
<circle cx="9.3" cy="6.4" r="1.3" />
|
||||
<circle cx="12" cy="13.1" r="1.3" />
|
||||
<circle class="st1" cx="12" cy="9.1" r="6.3" />
|
||||
<rect x="11" y="15.4" width="2" height="1.3" />
|
||||
<line class="st2" x1="10.9" y1="1.3" x2="13.1" y2="1.3" />
|
||||
<polygon points="13.1,0.9 13.1,2.4 14.5,3.1 13.8,1 " />
|
||||
<polygon points="10.9,0.9 10.9,2.4 9.5,3.1 10.2,1 " />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 47
|
||||
versionName "0.7.3"
|
||||
versionCode 51
|
||||
versionName "0.8.3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -77,6 +77,13 @@ android {
|
||||
if (goingelectricKey != null) {
|
||||
variant.resValue "string", "goingelectric_key", goingelectricKey
|
||||
}
|
||||
def openchargemapKey = env.OPENCHARGEMAP_API_KEY ?: project.findProperty("OPENCHARGEMAP_API_KEY")
|
||||
if (openchargemapKey == null && project.hasProperty("OPENCHARGEMAP_API_KEY_ENCRYPTED")) {
|
||||
openchargemapKey = decode(project.findProperty("OPENCHARGEMAP_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
|
||||
}
|
||||
if (openchargemapKey != null) {
|
||||
variant.resValue "string", "openchargemap_key", openchargemapKey
|
||||
}
|
||||
def googleMapsKey = env.GOOGLE_MAPS_API_KEY ?: project.findProperty("GOOGLE_MAPS_API_KEY")
|
||||
if (googleMapsKey != null && variant.flavorName == 'google') {
|
||||
variant.resValue "string", "google_maps_key", googleMapsKey
|
||||
@@ -113,11 +120,12 @@ dependencies {
|
||||
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.moshi:moshi-kotlin:1.12.0'
|
||||
implementation 'com.squareup.moshi:moshi-adapters:1.12.0'
|
||||
implementation 'moe.banana:moshi-jsonapi:3.5.0'
|
||||
implementation 'moe.banana:moshi-jsonapi-retrofit-converter:3.5.0'
|
||||
implementation 'io.coil-kt:coil:1.1.0'
|
||||
implementation 'com.github.MikeOrtiz:TouchImageView:3.0.3'
|
||||
implementation 'com.github.johan12345:StfalconImageViewer:5082ebd392'
|
||||
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
|
||||
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
|
||||
implementation 'com.airbnb.android:lottie:3.4.0'
|
||||
@@ -125,12 +133,13 @@ dependencies {
|
||||
implementation 'com.mapzen.android:lost:3.0.2'
|
||||
implementation 'com.google.guava:guava:29.0-android'
|
||||
implementation 'com.github.pengrad:mapscaleview:1.6.0'
|
||||
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
|
||||
|
||||
// Android Auto
|
||||
googleImplementation 'androidx.car.app:app:1.0.0'
|
||||
|
||||
// AnyMaps
|
||||
def anyMapsVersion = '1f050d860f'
|
||||
def anyMapsVersion = '95ddd6c083'
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
|
||||
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"
|
||||
@@ -186,7 +195,7 @@ dependencies {
|
||||
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"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.12.0"
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
}
|
||||
|
||||
@@ -22,26 +22,32 @@ import androidx.car.app.model.Distance.UNIT_KILOMETERS
|
||||
import androidx.car.app.validation.HostValidator
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.*
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.car2go.maps.model.LatLng
|
||||
import kotlinx.coroutines.*
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.ReferenceData
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.GEReferenceDataRepository
|
||||
import net.vonforst.evmap.storage.OCMReferenceDataRepository
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||
import net.vonforst.evmap.ui.availabilityText
|
||||
import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.ZoneId
|
||||
@@ -55,6 +61,7 @@ interface LocationAwareScreen {
|
||||
fun updateLocation(location: Location)
|
||||
}
|
||||
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class CarAppService : androidx.car.app.CarAppService() {
|
||||
override fun createHostValidator(): HostValidator {
|
||||
return if (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) {
|
||||
@@ -71,6 +78,7 @@ class CarAppService : androidx.car.app.CarAppService() {
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
|
||||
var mapScreen: LocationAwareScreen? = null
|
||||
set(value) {
|
||||
@@ -156,6 +164,7 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
|
||||
/**
|
||||
* Welcome screen with selection between favorites and nearby chargers
|
||||
*/
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class WelcomeScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), LocationAwareScreen {
|
||||
private var location: Location? = null
|
||||
|
||||
@@ -216,6 +225,7 @@ class WelcomeScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), L
|
||||
/**
|
||||
* Screen to grant location permission
|
||||
*/
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
override fun onGetTemplate(): Template {
|
||||
return MessageTemplate.Builder(carContext.getString(R.string.auto_location_permission_needed))
|
||||
@@ -270,6 +280,7 @@ class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx)
|
||||
/**
|
||||
* Main map screen showing either nearby chargers or favorites
|
||||
*/
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boolean = false) :
|
||||
Screen(ctx), LocationAwareScreen {
|
||||
private var updateCoroutine: Job? = null
|
||||
@@ -279,8 +290,9 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
private var location: Location? = null
|
||||
private var lastUpdateLocation: Location? = null
|
||||
private var chargers: List<ChargeLocation>? = null
|
||||
private var prefs = PreferenceDataSource(ctx)
|
||||
private val api by lazy {
|
||||
GoingElectricApi.create(ctx.getString(R.string.goingelectric_key), context = ctx)
|
||||
createApi(prefs.dataSource, ctx)
|
||||
}
|
||||
private val searchRadius = 5 // kilometers
|
||||
private val updateThreshold = 2000 // meters
|
||||
@@ -306,8 +318,10 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
} ?: setLoading(true)
|
||||
chargers?.take(maxRows)?.let { chargerList ->
|
||||
val builder = ItemList.Builder()
|
||||
// only show the city if not all chargers are in the same city
|
||||
val showCity = chargerList.map { it.address.city }.distinct().size > 1
|
||||
chargerList.forEach { charger ->
|
||||
builder.addItem(formatCharger(charger))
|
||||
builder.addItem(formatCharger(charger, showCity))
|
||||
}
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(
|
||||
@@ -326,7 +340,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun formatCharger(charger: ChargeLocation): Row {
|
||||
private fun formatCharger(charger: ChargeLocation, showCity: Boolean): Row {
|
||||
val color = ContextCompat.getColor(carContext, getMarkerTint(charger))
|
||||
val place =
|
||||
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
|
||||
@@ -338,7 +352,16 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
.build()
|
||||
|
||||
return Row.Builder().apply {
|
||||
setTitle(charger.name)
|
||||
// only show the city if not all chargers are in the same city (-> showCity == true)
|
||||
// and the city is not already contained in the charger name
|
||||
if (showCity && charger.address.city != null && charger.address.city !in charger.name) {
|
||||
setTitle(CarText.Builder("${charger.name} · ${charger.address.city}")
|
||||
.addVariant(charger.name)
|
||||
.build())
|
||||
} else {
|
||||
setTitle(charger.name)
|
||||
}
|
||||
|
||||
val text = SpannableStringBuilder()
|
||||
|
||||
// distance
|
||||
@@ -425,24 +448,25 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
}
|
||||
} else {
|
||||
val response = api.getChargepointsRadius(
|
||||
location.latitude,
|
||||
location.longitude,
|
||||
getReferenceData(),
|
||||
LatLng.fromLocation(location),
|
||||
searchRadius,
|
||||
zoom = 16f
|
||||
zoom = 16f,
|
||||
null
|
||||
)
|
||||
chargers =
|
||||
response.body()?.chargelocations?.filterIsInstance(ChargeLocation::class.java)
|
||||
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
chargers?.let {
|
||||
if (it.size < 6) {
|
||||
// try again with larger radius
|
||||
val response = api.getChargepointsRadius(
|
||||
location.latitude,
|
||||
location.longitude,
|
||||
getReferenceData(),
|
||||
LatLng.fromLocation(location),
|
||||
searchRadius * 5,
|
||||
zoom = 16f
|
||||
zoom = 16f,
|
||||
emptyList()
|
||||
)
|
||||
chargers =
|
||||
response.body()?.chargelocations?.filterIsInstance(ChargeLocation::class.java)
|
||||
response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -479,6 +503,31 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getReferenceData(): ReferenceData {
|
||||
val api = api
|
||||
return when (api) {
|
||||
is GoingElectricApiWrapper -> {
|
||||
GEReferenceDataRepository(
|
||||
api,
|
||||
lifecycleScope,
|
||||
db.geReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData().await()
|
||||
}
|
||||
is OpenChargeMapApiWrapper -> {
|
||||
OCMReferenceDataRepository(
|
||||
api,
|
||||
lifecycleScope,
|
||||
db.ocmReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData().await()
|
||||
}
|
||||
else -> {
|
||||
throw RuntimeException("no reference data implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : Screen(ctx) {
|
||||
@@ -486,9 +535,10 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
var photo: Bitmap? = null
|
||||
private var availability: ChargeLocationStatus? = null
|
||||
|
||||
val apikey = ctx.getString(R.string.goingelectric_key)
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val api by lazy {
|
||||
GoingElectricApi.create(apikey, context = ctx)
|
||||
createApi(prefs.dataSource, ctx)
|
||||
}
|
||||
|
||||
private val iconGen = ChargerIconGenerator(carContext, null, oversize = 1.4f, height = 64)
|
||||
@@ -518,7 +568,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
chargepointsText.append(
|
||||
"${cp.count}× ${
|
||||
nameForPlugType(
|
||||
carContext,
|
||||
carContext.stringProvider(),
|
||||
cp.type
|
||||
)
|
||||
} ${cp.formatPower()}"
|
||||
@@ -622,15 +672,14 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
|
||||
private fun loadCharger() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val response = api.getChargepointDetail(chargerSparse.id)
|
||||
charger = response.body()?.chargelocations?.get(0) as ChargeLocation
|
||||
val response = api.getChargepointDetail(getReferenceData(), chargerSparse.id)
|
||||
if (response.status == Status.SUCCESS) {
|
||||
charger = response.data!!
|
||||
|
||||
val photo = charger?.photos?.firstOrNull()
|
||||
photo?.let {
|
||||
val size = (carContext.resources.displayMetrics.density * 64).roundToInt()
|
||||
val url = "https://api.goingelectric.de/chargepoints/photo/?key=${apikey}" +
|
||||
"&id=${photo.id}&size=${size}"
|
||||
val url = photo.getUrl(size = size)
|
||||
val request = ImageRequest.Builder(carContext).data(url).build()
|
||||
this@ChargerDetailScreen.photo =
|
||||
(carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap
|
||||
@@ -639,7 +688,7 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
availability = charger?.let { getAvailability(it).data }
|
||||
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
@@ -647,6 +696,31 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getReferenceData(): ReferenceData {
|
||||
val api = api
|
||||
return when (api) {
|
||||
is GoingElectricApiWrapper -> {
|
||||
GEReferenceDataRepository(
|
||||
api,
|
||||
lifecycleScope,
|
||||
db.geReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData().await()
|
||||
}
|
||||
is OpenChargeMapApiWrapper -> {
|
||||
OCMReferenceDataRepository(
|
||||
api,
|
||||
lifecycleScope,
|
||||
db.ocmReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData().await()
|
||||
}
|
||||
else -> {
|
||||
throw RuntimeException("no reference data implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
|
||||
|
||||
@@ -19,8 +19,8 @@ import androidx.navigation.ui.AppBarConfiguration
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.fragment.MapFragment
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.utils.LocaleContextWrapper
|
||||
import net.vonforst.evmap.utils.getLocationFromIntent
|
||||
@@ -58,6 +58,7 @@ class MapsActivity : AppCompatActivity() {
|
||||
setContentView(R.layout.activity_maps)
|
||||
|
||||
navController = findNavController(R.id.nav_host_fragment)
|
||||
val navGraph = navController.navInflater.inflate(R.navigation.nav_graph)
|
||||
appBarConfiguration = AppBarConfiguration(
|
||||
setOf(
|
||||
R.id.map,
|
||||
@@ -69,7 +70,7 @@ class MapsActivity : AppCompatActivity() {
|
||||
)
|
||||
val navView = findViewById<NavigationView>(R.id.nav_view)
|
||||
navView.setupWithNavController(navController)
|
||||
|
||||
|
||||
val header = navView.getHeaderView(0)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(header) { v, insets ->
|
||||
v.setPadding(0, insets.getInsets(WindowInsetsCompat.Type.statusBars()).top, 0, 0)
|
||||
@@ -80,6 +81,17 @@ class MapsActivity : AppCompatActivity() {
|
||||
|
||||
checkPlayServices(this)
|
||||
|
||||
|
||||
if (!prefs.welcomeDialogShown || !prefs.dataSourceSet) {
|
||||
navGraph.startDestination = R.id.onboarding
|
||||
navController.graph = navGraph
|
||||
return
|
||||
} else {
|
||||
navGraph.startDestination = R.id.map
|
||||
navController.graph = navGraph
|
||||
}
|
||||
|
||||
|
||||
if (intent?.scheme == "geo") {
|
||||
val query = intent.data?.query?.split("=")?.get(1)
|
||||
val coords = getLocationFromIntent(intent)
|
||||
|
||||
@@ -4,6 +4,11 @@ import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.*
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
fun Bundle.optDouble(name: String): Double? {
|
||||
if (!this.containsKey(name)) return null
|
||||
@@ -51,4 +56,45 @@ fun String.bold(): CharSequence {
|
||||
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Collection<Iterable<T>>.cartesianProduct(): Set<Set<T>> =
|
||||
/**
|
||||
Returns all possible combinations of entries of a list
|
||||
*/
|
||||
if (isEmpty()) emptySet()
|
||||
else drop(1).fold(first().map(::setOf)) { acc, iterable ->
|
||||
acc.flatMap { list -> iterable.map(list::plus) }
|
||||
}.toSet()
|
||||
|
||||
|
||||
fun max(a: Int?, b: Int?): Int? {
|
||||
/**
|
||||
* Returns the maximum of two values of both are non-null,
|
||||
* otherwise the non-null value or null
|
||||
*/
|
||||
return if (a != null && b != null) {
|
||||
max(a, b)
|
||||
} else {
|
||||
a ?: b
|
||||
}
|
||||
}
|
||||
|
||||
public suspend fun <T> LiveData<T>.await(): T {
|
||||
return withContext(Dispatchers.Main.immediate) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val observer = object : Observer<T> {
|
||||
override fun onChanged(value: T) {
|
||||
removeObserver(this)
|
||||
continuation.resume(value, null)
|
||||
}
|
||||
}
|
||||
|
||||
observeForever(observer)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,20 +9,22 @@ import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.chip.Chip
|
||||
import net.vonforst.evmap.BR
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.chargeprice.ChargePrice
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceTag
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.databinding.ItemChargepriceBinding
|
||||
import net.vonforst.evmap.databinding.ItemChargepriceVehicleChipBinding
|
||||
import net.vonforst.evmap.databinding.ItemConnectorButtonBinding
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.ui.CheckableConstraintLayout
|
||||
import net.vonforst.evmap.viewmodel.FavoritesViewModel
|
||||
|
||||
interface Equatable {
|
||||
override fun equals(other: Any?): Boolean;
|
||||
override fun equals(other: Any?): Boolean
|
||||
}
|
||||
|
||||
abstract class DataBindingAdapter<T : Equatable>(getKey: ((T) -> Any)? = null) :
|
||||
@@ -86,18 +88,6 @@ class ConnectorAdapter : DataBindingAdapter<ConnectorAdapter.ChargepointWithAvai
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_connector
|
||||
}
|
||||
|
||||
|
||||
class FavoritesAdapter(val vm: FavoritesViewModel) :
|
||||
DataBindingAdapter<FavoritesViewModel.FavoritesListItem>() {
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_favorite
|
||||
|
||||
override fun getItemId(position: Int): Long = getItem(position).charger.id
|
||||
}
|
||||
|
||||
class ChargepriceAdapter() :
|
||||
DataBindingAdapter<ChargePrice>() {
|
||||
|
||||
@@ -179,7 +169,9 @@ class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
|
||||
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
|
||||
if (checked) {
|
||||
checkedItem = position
|
||||
notifyDataSetChanged()
|
||||
root.post {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
onCheckedItemChangedListener?.invoke(getCheckedItem()!!)
|
||||
}
|
||||
}
|
||||
@@ -188,7 +180,7 @@ class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
|
||||
fun getCheckedItem(): Chargepoint? = checkedItem?.let { getItem(it) }
|
||||
|
||||
fun setCheckedItem(item: Chargepoint?) {
|
||||
checkedItem = item?.let { currentList.indexOf(item) } ?: null
|
||||
checkedItem = item?.let { currentList.indexOf(item) }
|
||||
}
|
||||
|
||||
var onCheckedItemChangedListener: ((Chargepoint?) -> Unit)? = null
|
||||
@@ -197,4 +189,38 @@ class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
|
||||
class ChargepriceTagsAdapter() :
|
||||
DataBindingAdapter<ChargepriceTag>() {
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice_tag
|
||||
}
|
||||
|
||||
class CheckableChargepriceCarAdapter : DataBindingAdapter<ChargepriceCar>() {
|
||||
private var checkedItem: ChargepriceCar? = null
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice_vehicle_chip
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder<ChargepriceCar>, position: Int) {
|
||||
val item = getItem(position)
|
||||
super.bind(holder, item)
|
||||
val binding = holder.binding as ItemChargepriceVehicleChipBinding
|
||||
val root = binding.root as Chip
|
||||
root.isChecked = checkedItem == item
|
||||
root.setOnClickListener {
|
||||
root.isChecked = true
|
||||
}
|
||||
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
|
||||
if (checked && item != checkedItem) {
|
||||
checkedItem = item
|
||||
root.post {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
onCheckedItemChangedListener?.invoke(getCheckedItem()!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCheckedItem(): ChargepriceCar? = checkedItem
|
||||
|
||||
fun setCheckedItem(item: ChargepriceCar?) {
|
||||
checkedItem = item
|
||||
}
|
||||
|
||||
var onCheckedItemChangedListener: ((ChargepriceCar?) -> Unit)? = null
|
||||
}
|
||||
@@ -3,12 +3,12 @@ package net.vonforst.evmap.adapter
|
||||
import android.content.Context
|
||||
import androidx.core.text.HtmlCompat
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCardId
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.OpeningHoursDays
|
||||
import net.vonforst.evmap.bold
|
||||
import net.vonforst.evmap.joinToSpannedString
|
||||
import net.vonforst.evmap.model.ChargeCard
|
||||
import net.vonforst.evmap.model.ChargeCardId
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.OpeningHoursDays
|
||||
import net.vonforst.evmap.plus
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
@@ -18,7 +18,7 @@ class DetailsAdapter : DataBindingAdapter<DetailsAdapter.Detail>() {
|
||||
data class Detail(
|
||||
val icon: Int,
|
||||
val contentDescription: Int,
|
||||
val text: CharSequence,
|
||||
val text: CharSequence?,
|
||||
val detailText: CharSequence? = null,
|
||||
val links: Boolean = true,
|
||||
val clickable: Boolean = false,
|
||||
@@ -119,7 +119,7 @@ fun buildDetails(
|
||||
loc.coordinates.formatDecimal(),
|
||||
links = false,
|
||||
clickable = true
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.ItemFavoriteBinding
|
||||
import net.vonforst.evmap.viewmodel.FavoritesViewModel
|
||||
|
||||
class FavoritesAdapter(val onDelete: (FavoritesViewModel.FavoritesListItem) -> Unit) :
|
||||
DataBindingAdapter<FavoritesViewModel.FavoritesListItem>() {
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_favorite
|
||||
|
||||
override fun getItemId(position: Int): Long = getItem(position).charger.id
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun bind(
|
||||
holder: ViewHolder<FavoritesViewModel.FavoritesListItem>,
|
||||
item: FavoritesViewModel.FavoritesListItem
|
||||
) {
|
||||
super.bind(holder, item)
|
||||
|
||||
val binding = holder.binding as ItemFavoriteBinding
|
||||
binding.foreground.translationX = 0f
|
||||
binding.btnDelete.setOnClickListener {
|
||||
binding.foreground.animate()
|
||||
.translationX(binding.foreground.width.toFloat())
|
||||
.setDuration(250)
|
||||
.setInterpolator(AccelerateInterpolator())
|
||||
.withEndAction {
|
||||
onDelete(item)
|
||||
}
|
||||
.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceBinding
|
||||
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceLargeBinding
|
||||
import net.vonforst.evmap.databinding.ItemFilterSliderBinding
|
||||
import net.vonforst.evmap.fragment.MultiSelectDialog
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
import net.vonforst.evmap.model.*
|
||||
import kotlin.math.max
|
||||
|
||||
class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
|
||||
|
||||
@@ -2,7 +2,9 @@ package net.vonforst.evmap.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
@@ -11,19 +13,11 @@ import coil.load
|
||||
import coil.memory.MemoryCache
|
||||
import coil.size.OriginalSize
|
||||
import coil.size.SizeResolver
|
||||
import com.ortiz.touchview.TouchImageView
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.ChargerPhoto
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
|
||||
|
||||
class GalleryAdapter(
|
||||
context: Context,
|
||||
val itemClickListener: ItemClickListener? = null,
|
||||
val detailView: Boolean = false,
|
||||
val pageToLoad: Int? = null,
|
||||
val imageCacheKey: MemoryCache.Key? = null,
|
||||
val loadedListener: (() -> Unit)? = null
|
||||
) :
|
||||
class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener? = null) :
|
||||
ListAdapter<ChargerPhoto, GalleryAdapter.ViewHolder>(ChargerPhotoDiffCallback()) {
|
||||
class ViewHolder(val view: ImageView) : RecyclerView.ViewHolder(view)
|
||||
|
||||
@@ -38,106 +32,34 @@ class GalleryAdapter(
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view: ImageView
|
||||
if (detailView) {
|
||||
view = inflater.inflate(R.layout.gallery_item_fullscreen, parent, false) as ImageView
|
||||
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 || v.canScrollHorizontally(1) && v.canScrollHorizontally(
|
||||
-1
|
||||
)
|
||||
) {
|
||||
//multi-touch event
|
||||
result = when (event.action) {
|
||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
|
||||
// Disallow RecyclerView to intercept touch events.
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
// Disable touch on view
|
||||
false
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
// Allow RecyclerView to intercept touch events.
|
||||
parent.requestDisallowInterceptTouchEvent(false)
|
||||
true
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
} else {
|
||||
view = inflater.inflate(R.layout.gallery_item, parent, false) as ImageView
|
||||
}
|
||||
val view = inflater.inflate(R.layout.gallery_item, parent, false) as ImageView
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
if (detailView) {
|
||||
(holder.view as TouchImageView).resetZoom()
|
||||
}
|
||||
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}"
|
||||
}
|
||||
val url = getItem(position).getUrl(height = holder.view.height)
|
||||
|
||||
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, 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"
|
||||
|
||||
class ChargerPhotoDiffCallback : DiffUtil.ItemCallback<ChargerPhoto>() {
|
||||
override fun areItemsTheSame(oldItem: ChargerPhoto, newItem: ChargerPhoto): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
|
||||
68
app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt
Normal file
68
app/src/main/java/net/vonforst/evmap/api/ChargepointApi.kt
Normal file
@@ -0,0 +1,68 @@
|
||||
package net.vonforst.evmap.api
|
||||
|
||||
import android.content.Context
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
|
||||
interface ChargepointApi<out T : ReferenceData> {
|
||||
suspend fun getChargepoints(
|
||||
referenceData: ReferenceData,
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
filters: FilterValues?
|
||||
): Resource<List<ChargepointListItem>>
|
||||
|
||||
suspend fun getChargepointsRadius(
|
||||
referenceData: ReferenceData,
|
||||
location: LatLng,
|
||||
radius: Int,
|
||||
zoom: Float,
|
||||
filters: FilterValues?
|
||||
): Resource<List<ChargepointListItem>>
|
||||
|
||||
suspend fun getChargepointDetail(
|
||||
referenceData: ReferenceData,
|
||||
id: Long
|
||||
): Resource<ChargeLocation>
|
||||
|
||||
suspend fun getReferenceData(): Resource<T>
|
||||
|
||||
fun getFilters(referenceData: ReferenceData, sp: StringProvider): List<Filter<FilterValue>>
|
||||
|
||||
fun getName(): String
|
||||
}
|
||||
|
||||
interface StringProvider {
|
||||
fun getString(id: Int): String
|
||||
}
|
||||
|
||||
fun Context.stringProvider() = object : StringProvider {
|
||||
override fun getString(id: Int): String {
|
||||
return this@stringProvider.getString(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
|
||||
return when (type) {
|
||||
"openchargemap" -> {
|
||||
OpenChargeMapApiWrapper(
|
||||
ctx.getString(
|
||||
R.string.openchargemap_key
|
||||
)
|
||||
)
|
||||
}
|
||||
"goingelectric" -> {
|
||||
GoingElectricApiWrapper(
|
||||
ctx.getString(
|
||||
R.string.goingelectric_key
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
package net.vonforst.evmap.api
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.DrawableRes
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Response
|
||||
import org.json.JSONArray
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.math.abs
|
||||
|
||||
operator fun <T> JSONArray.iterator(): Iterator<T> =
|
||||
(0 until length()).asSequence().map {
|
||||
@@ -45,9 +45,13 @@ suspend fun Call.await(): Response {
|
||||
|
||||
private val plugNames = mapOf(
|
||||
Chargepoint.TYPE_1 to R.string.plug_type_1,
|
||||
Chargepoint.TYPE_2 to R.string.plug_type_2,
|
||||
Chargepoint.TYPE_2_UNKNOWN to R.string.plug_type_2,
|
||||
Chargepoint.TYPE_2_PLUG to R.string.plug_type_2,
|
||||
Chargepoint.TYPE_2_SOCKET to R.string.plug_type_2,
|
||||
Chargepoint.TYPE_3 to R.string.plug_type_3,
|
||||
Chargepoint.CCS to R.string.plug_ccs,
|
||||
Chargepoint.CCS_UNKNOWN to R.string.plug_ccs,
|
||||
Chargepoint.CCS_TYPE_1 to R.string.plug_ccs,
|
||||
Chargepoint.CCS_TYPE_2 to R.string.plug_ccs,
|
||||
Chargepoint.SCHUKO to R.string.plug_schuko,
|
||||
Chargepoint.CHADEMO to R.string.plug_chademo,
|
||||
Chargepoint.SUPERCHARGER to R.string.plug_supercharger,
|
||||
@@ -56,22 +60,52 @@ private val plugNames = mapOf(
|
||||
Chargepoint.TESLA_ROADSTER_HPC to R.string.plug_roadster_hpc
|
||||
)
|
||||
|
||||
fun nameForPlugType(ctx: Context, type: String): String =
|
||||
fun nameForPlugType(ctx: StringProvider, type: String): String =
|
||||
plugNames[type]?.let {
|
||||
ctx.getString(it)
|
||||
} ?: type
|
||||
|
||||
fun equivalentPlugTypes(type: String): Set<String> {
|
||||
return when (type) {
|
||||
Chargepoint.CCS_TYPE_1 -> setOf(Chargepoint.CCS_UNKNOWN, Chargepoint.CCS_TYPE_1)
|
||||
Chargepoint.CCS_TYPE_2 -> setOf(Chargepoint.CCS_UNKNOWN, Chargepoint.CCS_TYPE_2)
|
||||
Chargepoint.CCS_UNKNOWN -> setOf(
|
||||
Chargepoint.CCS_UNKNOWN,
|
||||
Chargepoint.CCS_TYPE_1,
|
||||
Chargepoint.CCS_TYPE_2
|
||||
)
|
||||
Chargepoint.TYPE_2_PLUG -> setOf(Chargepoint.TYPE_2_UNKNOWN, Chargepoint.TYPE_2_PLUG)
|
||||
Chargepoint.TYPE_2_SOCKET -> setOf(Chargepoint.TYPE_2_UNKNOWN, Chargepoint.TYPE_2_SOCKET)
|
||||
Chargepoint.TYPE_2_UNKNOWN -> setOf(
|
||||
Chargepoint.TYPE_2_UNKNOWN,
|
||||
Chargepoint.TYPE_2_PLUG,
|
||||
Chargepoint.TYPE_2_SOCKET
|
||||
)
|
||||
else -> setOf(type)
|
||||
}
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
fun iconForPlugType(type: String): Int =
|
||||
when (type) {
|
||||
Chargepoint.CCS -> R.drawable.ic_connector_ccs
|
||||
Chargepoint.CCS_TYPE_2 -> R.drawable.ic_connector_ccs_typ2
|
||||
Chargepoint.CCS_UNKNOWN -> R.drawable.ic_connector_ccs_typ2
|
||||
Chargepoint.CCS_TYPE_1 -> R.drawable.ic_connector_ccs_typ1
|
||||
Chargepoint.CHADEMO -> R.drawable.ic_connector_chademo
|
||||
Chargepoint.SCHUKO -> R.drawable.ic_connector_schuko
|
||||
Chargepoint.SUPERCHARGER -> R.drawable.ic_connector_supercharger
|
||||
Chargepoint.TYPE_2 -> R.drawable.ic_connector_typ2
|
||||
Chargepoint.TYPE_2_UNKNOWN -> R.drawable.ic_connector_typ2
|
||||
Chargepoint.TYPE_2_SOCKET -> R.drawable.ic_connector_typ2
|
||||
Chargepoint.TYPE_2_PLUG -> R.drawable.ic_connector_typ2
|
||||
Chargepoint.CEE_BLAU -> R.drawable.ic_connector_cee_blau
|
||||
Chargepoint.CEE_ROT -> R.drawable.ic_connector_cee_rot
|
||||
Chargepoint.TYPE_1 -> R.drawable.ic_connector_typ1
|
||||
// TODO: add other connectors
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 75, 100, 150, 200, 250, 300, 350)
|
||||
fun mapPower(i: Int) = powerSteps[i]
|
||||
fun mapPowerInverse(power: Int) = powerSteps
|
||||
.mapIndexed { index, v -> abs(v - power) to index }
|
||||
.minByOrNull { it.first }?.second ?: 0
|
||||
@@ -6,12 +6,11 @@ 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.FilterValues
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.cartesianProduct
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import net.vonforst.evmap.viewmodel.getMultipleChoiceValue
|
||||
import net.vonforst.evmap.viewmodel.getSliderValue
|
||||
import okhttp3.JavaNetCookieJar
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
@@ -68,8 +67,9 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
): Map<Chargepoint, Set<Long>> {
|
||||
// iterate over each connector type
|
||||
val types = connectors.map { it.value.second }.distinct().toSet()
|
||||
val equivalentTypes = types.map { equivalentPlugTypes(it).plus(it) }.cartesianProduct()
|
||||
val geTypes = chargepoints.map { it.type }.distinct().toSet()
|
||||
if (types != geTypes) throw AvailabilityDetectorException("chargepoints do not match")
|
||||
if (!equivalentTypes.any { it == geTypes }) throw AvailabilityDetectorException("chargepoints do not match")
|
||||
return types.flatMap { type ->
|
||||
// find connectors of this type
|
||||
val connsOfType = connectors.filter { it.value.second == type }
|
||||
@@ -77,13 +77,14 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
val powers = connsOfType.map { it.value.first }.distinct().sorted()
|
||||
// find corresponding powers in GE data
|
||||
val gePowers =
|
||||
chargepoints.filter { it.type == type }.map { it.power }.distinct().sorted()
|
||||
chargepoints.filter { equivalentPlugTypes(it.type).any { it == type } }
|
||||
.map { it.power }.distinct().sorted()
|
||||
|
||||
// if the distinct number of powers is the same, try to match.
|
||||
if (powers.size == gePowers.size) {
|
||||
gePowers.zip(powers).map { (gePower, power) ->
|
||||
val chargepoint =
|
||||
chargepoints.find { it.type == type && it.power == gePower }!!
|
||||
chargepoints.find { equivalentPlugTypes(it.type).any { it == type } && it.power == gePower }!!
|
||||
val ids = connsOfType.filter { it.value.first == power }.keys
|
||||
if (chargepoint.count != ids.size) {
|
||||
throw AvailabilityDetectorException("chargepoints do not match")
|
||||
@@ -117,14 +118,12 @@ data class ChargeLocationStatus(
|
||||
val status: Map<Chargepoint, List<ChargepointStatus>>,
|
||||
val source: String
|
||||
) {
|
||||
fun applyFilters(filters: FilterValues?): ChargeLocationStatus {
|
||||
if (filters == null) return this
|
||||
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")
|
||||
val minPower = filters.getSliderValue("min_power")
|
||||
|
||||
fun applyFilters(connectors: Set<String>?, minPower: Int?): ChargeLocationStatus {
|
||||
val statusFiltered = status.filterKeys {
|
||||
(connectorsVal.all || it.type in connectorsVal.values) && it.power > minPower
|
||||
(connectors == null || connectors.map {
|
||||
equivalentPlugTypes(it)
|
||||
}.any { equivalent -> it.type in equivalent })
|
||||
&& (minPower == null || it.power > minPower)
|
||||
}
|
||||
return this.copy(status = statusFiltered)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.api.iterator
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import okhttp3.OkHttpClient
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
@@ -85,9 +85,9 @@ class ChargecloudAvailabilityDetector(
|
||||
|
||||
private fun getType(string: String): String {
|
||||
return when (string) {
|
||||
"IEC_62196_T2" -> Chargepoint.TYPE_2
|
||||
"IEC_62196_T2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||
"DOMESTIC_F" -> Chargepoint.SCHUKO
|
||||
"IEC_62196_T2_COMBO" -> Chargepoint.CCS
|
||||
"IEC_62196_T2_COMBO" -> Chargepoint.CCS_TYPE_2
|
||||
"CHADEMO" -> Chargepoint.CHADEMO
|
||||
else -> throw IllegalArgumentException("unrecognized type $string")
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
@@ -141,11 +141,11 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
val power = connector.electricalProperties.getPower()
|
||||
val type = when (connector.connectorType.toLowerCase(Locale.ROOT)) {
|
||||
"type3" -> Chargepoint.TYPE_3
|
||||
"type2" -> Chargepoint.TYPE_2
|
||||
"type2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||
"type1" -> Chargepoint.TYPE_1
|
||||
"domestic" -> Chargepoint.SCHUKO
|
||||
"type1combo" -> Chargepoint.CCS // US CCS, aka type1_combo
|
||||
"type2combo" -> Chargepoint.CCS // EU CCS, aka type2_combo
|
||||
"type1combo" -> Chargepoint.CCS_TYPE_1 // US CCS, aka type1_combo
|
||||
"type2combo" -> Chargepoint.CCS_TYPE_2 // EU CCS, aka type2_combo
|
||||
"tepcochademo" -> Chargepoint.CHADEMO
|
||||
"unspecified" -> "unknown"
|
||||
"unknown" -> "unknown"
|
||||
|
||||
@@ -9,7 +9,8 @@ import moe.banana.jsonapi2.JsonApi
|
||||
import moe.banana.jsonapi2.Resource
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.ui.currency
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
@@ -33,19 +34,23 @@ data class ChargepriceStation(
|
||||
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepoint>
|
||||
) {
|
||||
companion object {
|
||||
fun fromGoingelectric(
|
||||
geCharger: ChargeLocation,
|
||||
compatibleConnectors: List<String>
|
||||
fun fromEvmap(
|
||||
charger: ChargeLocation,
|
||||
compatibleConnectors: List<String>,
|
||||
): ChargepriceStation {
|
||||
if (charger.chargepriceData == null) throw IllegalArgumentException()
|
||||
|
||||
val plugTypes =
|
||||
charger.chargepriceData.plugTypes ?: charger.chargepoints.map { it.type }
|
||||
return ChargepriceStation(
|
||||
geCharger.coordinates.lng,
|
||||
geCharger.coordinates.lat,
|
||||
geCharger.address.country,
|
||||
geCharger.network,
|
||||
geCharger.chargepoints.filter {
|
||||
it.type in compatibleConnectors
|
||||
charger.coordinates.lng,
|
||||
charger.coordinates.lat,
|
||||
charger.chargepriceData.country,
|
||||
charger.chargepriceData.network,
|
||||
charger.chargepoints.zip(plugTypes).filter {
|
||||
equivalentPlugTypes(it.first.type).any { it in compatibleConnectors }
|
||||
}.map {
|
||||
ChargepriceChargepoint(it.power, it.type)
|
||||
ChargepriceChargepoint(it.first.power, it.second)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -112,7 +117,7 @@ class ChargepriceTariff() : Resource() {
|
||||
}
|
||||
|
||||
@JsonApi(type = "car")
|
||||
class ChargepriceCar : Resource() {
|
||||
class ChargepriceCar : Resource(), Equatable {
|
||||
lateinit var name: String
|
||||
lateinit var brand: String
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ internal class ChargepointListItemJsonAdapterFactory : JsonAdapter.Factory {
|
||||
annotations: MutableSet<out Annotation>,
|
||||
moshi: Moshi
|
||||
): JsonAdapter<*>? {
|
||||
if (Types.getRawType(type) == ChargepointListItem::class.java) {
|
||||
if (Types.getRawType(type) == GEChargepointListItem::class.java) {
|
||||
return ChargepointListItemJsonAdapter(
|
||||
moshi
|
||||
)
|
||||
@@ -26,18 +26,18 @@ internal class ChargepointListItemJsonAdapterFactory : JsonAdapter.Factory {
|
||||
|
||||
|
||||
internal class ChargepointListItemJsonAdapter(val moshi: Moshi) :
|
||||
JsonAdapter<ChargepointListItem>() {
|
||||
JsonAdapter<GEChargepointListItem>() {
|
||||
private val clusterAdapter =
|
||||
moshi.adapter<ChargeLocationCluster>(
|
||||
ChargeLocationCluster::class.java
|
||||
moshi.adapter<GEChargeLocationCluster>(
|
||||
GEChargeLocationCluster::class.java
|
||||
)
|
||||
|
||||
private val locationAdapter = moshi.adapter<ChargeLocation>(
|
||||
ChargeLocation::class.java
|
||||
private val locationAdapter = moshi.adapter<GEChargeLocation>(
|
||||
GEChargeLocation::class.java
|
||||
)
|
||||
|
||||
@FromJson
|
||||
override fun fromJson(reader: JsonReader): ChargepointListItem {
|
||||
override fun fromJson(reader: JsonReader): GEChargepointListItem {
|
||||
var clustered = false
|
||||
reader.peekJson().use { peeked ->
|
||||
peeked.beginObject()
|
||||
@@ -61,7 +61,7 @@ internal class ChargepointListItemJsonAdapter(val moshi: Moshi) :
|
||||
val CLUSTERED: JsonReader.Options = JsonReader.Options.of("clustered")
|
||||
}
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: ChargepointListItem?) {
|
||||
override fun toJson(writer: JsonWriter, value: GEChargepointListItem?) {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
}
|
||||
@@ -94,8 +94,8 @@ internal class JsonObjectOrFalseAdapter<T> private constructor(
|
||||
JsonReader.Token.BOOLEAN -> when (reader.nextBoolean()) {
|
||||
false -> null // Response was false
|
||||
else -> {
|
||||
if (this.clazz == FaultReport::class.java) {
|
||||
FaultReport(null, null) as T
|
||||
if (this.clazz == GEFaultReport::class.java) {
|
||||
GEFaultReport(null, null) as T
|
||||
} else {
|
||||
throw IllegalStateException("Non-false boolean for @JsonObjectOrFalse field")
|
||||
}
|
||||
@@ -126,20 +126,20 @@ internal class HoursAdapter {
|
||||
private val regex = Regex("from (.*) till (.*)")
|
||||
|
||||
@FromJson
|
||||
fun fromJson(str: String): Hours? {
|
||||
fun fromJson(str: String): GEHours? {
|
||||
if (str == "closed") {
|
||||
return Hours(null, null)
|
||||
return GEHours(null, null)
|
||||
} else {
|
||||
val match = regex.find(str)
|
||||
if (match != null) {
|
||||
return Hours(
|
||||
return GEHours(
|
||||
LocalTime.parse(match.groupValues[1]),
|
||||
LocalTime.parse(match.groupValues[2])
|
||||
)
|
||||
} else {
|
||||
// I cannot reproduce this case, but it seems to occur once in a while
|
||||
Log.e("GoingElectricApi", "invalid hours value: " + str)
|
||||
return Hours(
|
||||
return GEHours(
|
||||
LocalTime.MIN, LocalTime.MIN
|
||||
)
|
||||
}
|
||||
@@ -147,7 +147,7 @@ internal class HoursAdapter {
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: Hours): String {
|
||||
fun toJson(value: GEHours): String {
|
||||
if (value.start == null || value.end == null) {
|
||||
return "closed"
|
||||
} else {
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
package net.vonforst.evmap.api.goingelectric
|
||||
|
||||
import android.content.Context
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.*
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.ui.cluster
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import net.vonforst.evmap.viewmodel.getClusterDistance
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Response
|
||||
@@ -11,6 +23,7 @@ import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
import java.io.IOException
|
||||
|
||||
interface GoingElectricApi {
|
||||
@GET("chargepoints/")
|
||||
@@ -31,7 +44,7 @@ interface GoingElectricApi {
|
||||
@Query("open_twentyfourseven") open247: Boolean = false,
|
||||
@Query("barrierfree") barrierfree: Boolean = false,
|
||||
@Query("exclude_faults") excludeFaults: Boolean = false
|
||||
): Response<ChargepointList>
|
||||
): Response<GEChargepointList>
|
||||
|
||||
@GET("chargepoints/")
|
||||
suspend fun getChargepointsRadius(
|
||||
@@ -52,24 +65,24 @@ interface GoingElectricApi {
|
||||
@Query("open_twentyfourseven") open247: Boolean = false,
|
||||
@Query("barrierfree") barrierfree: Boolean = false,
|
||||
@Query("exclude_faults") excludeFaults: Boolean = false
|
||||
): Response<ChargepointList>
|
||||
): Response<GEChargepointList>
|
||||
|
||||
@GET("chargepoints/")
|
||||
suspend fun getChargepointDetail(@Query("ge_id") id: Long): Response<ChargepointList>
|
||||
suspend fun getChargepointDetail(@Query("ge_id") id: Long): Response<GEChargepointList>
|
||||
|
||||
@GET("chargepoints/pluglist/")
|
||||
suspend fun getPlugs(): Response<StringList>
|
||||
suspend fun getPlugs(): Response<GEStringList>
|
||||
|
||||
@GET("chargepoints/networklist/")
|
||||
suspend fun getNetworks(): Response<StringList>
|
||||
suspend fun getNetworks(): Response<GEStringList>
|
||||
|
||||
@GET("chargepoints/chargecardlist/")
|
||||
suspend fun getChargeCards(): Response<ChargeCardList>
|
||||
suspend fun getChargeCards(): Response<GEChargeCardList>
|
||||
|
||||
companion object {
|
||||
private val cacheSize = 10L * 1024 * 1024 // 10MB
|
||||
|
||||
val moshi = Moshi.Builder()
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(ChargepointListItemJsonAdapterFactory())
|
||||
.add(JsonObjectOrFalseAdapter.Factory())
|
||||
.add(HoursAdapter())
|
||||
@@ -106,3 +119,364 @@ interface GoingElectricApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GoingElectricApiWrapper(
|
||||
val apikey: String,
|
||||
baseurl: String = "https://api.goingelectric.de",
|
||||
context: Context? = null
|
||||
) : ChargepointApi<GEReferenceData> {
|
||||
val api = GoingElectricApi.create(apikey, baseurl, context)
|
||||
|
||||
override fun getName() = "GoingElectric.de"
|
||||
|
||||
override suspend fun getChargepoints(
|
||||
referenceData: ReferenceData,
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
filters: FilterValues?
|
||||
): Resource<List<ChargepointListItem>> {
|
||||
val freecharging = filters?.getBooleanValue("freecharging")
|
||||
val freeparking = filters?.getBooleanValue("freeparking")
|
||||
val open247 = filters?.getBooleanValue("open_247")
|
||||
val barrierfree = filters?.getBooleanValue("barrierfree")
|
||||
val excludeFaults = filters?.getBooleanValue("exclude_faults")
|
||||
val minPower = filters?.getSliderValue("min_power")
|
||||
val minConnectors = filters?.getSliderValue("min_connectors")
|
||||
|
||||
var connectorsVal = filters?.getMultipleChoiceValue("connectors")
|
||||
if (connectorsVal != null && connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
|
||||
val chargeCardsVal = filters?.getMultipleChoiceValue("chargecards")
|
||||
if (chargeCardsVal != null && chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) {
|
||||
// no chargeCards chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val chargeCards = formatMultipleChoice(chargeCardsVal)
|
||||
|
||||
val networksVal = filters?.getMultipleChoiceValue("networks")
|
||||
if (networksVal != null && networksVal.values.isEmpty() && !networksVal.all) {
|
||||
// no networks chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val networks = formatMultipleChoice(networksVal)
|
||||
|
||||
val categoriesVal = filters?.getMultipleChoiceValue("categories")
|
||||
if (categoriesVal != null && categoriesVal.values.isEmpty() && !categoriesVal.all) {
|
||||
// no categories chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val categories = formatMultipleChoice(categoriesVal)
|
||||
|
||||
// do not use clustering if filters need to be applied locally.
|
||||
val useClustering = zoom < 13
|
||||
val geClusteringAvailable = minConnectors == null || minConnectors <= 1
|
||||
val useGeClustering = useClustering && geClusteringAvailable
|
||||
val clusterDistance = if (useClustering) getClusterDistance(zoom) else null
|
||||
|
||||
var startkey: Int? = null
|
||||
val data = mutableListOf<GEChargepointListItem>()
|
||||
do {
|
||||
// load all pages of the response
|
||||
try {
|
||||
val response = api.getChargepoints(
|
||||
bounds.southwest.latitude,
|
||||
bounds.southwest.longitude,
|
||||
bounds.northeast.latitude,
|
||||
bounds.northeast.longitude,
|
||||
clustering = useGeClustering,
|
||||
zoom = zoom,
|
||||
clusterDistance = clusterDistance,
|
||||
freecharging = freecharging ?: false,
|
||||
minPower = minPower ?: 0,
|
||||
freeparking = freeparking ?: false,
|
||||
open247 = open247 ?: false,
|
||||
barrierfree = barrierfree ?: false,
|
||||
excludeFaults = excludeFaults ?: false,
|
||||
plugs = connectors,
|
||||
chargecards = chargeCards,
|
||||
networks = networks,
|
||||
categories = categories,
|
||||
startkey = startkey
|
||||
)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
return Resource.error(response.message(), null)
|
||||
} else {
|
||||
val body = response.body()!!
|
||||
data.addAll(body.chargelocations)
|
||||
startkey = body.startkey
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
} while (startkey != null && startkey < 10000)
|
||||
|
||||
var result = postprocessResult(data, minPower, connectorsVal, minConnectors, zoom)
|
||||
|
||||
return Resource.success(result)
|
||||
}
|
||||
|
||||
private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) =
|
||||
if (value == null || value.all) null else value.values.joinToString(",")
|
||||
|
||||
override suspend fun getChargepointsRadius(
|
||||
referenceData: ReferenceData,
|
||||
location: LatLng,
|
||||
radius: Int,
|
||||
zoom: Float,
|
||||
filters: FilterValues?
|
||||
): Resource<List<ChargepointListItem>> {
|
||||
val freecharging = filters?.getBooleanValue("freecharging")
|
||||
val freeparking = filters?.getBooleanValue("freeparking")
|
||||
val open247 = filters?.getBooleanValue("open_247")
|
||||
val barrierfree = filters?.getBooleanValue("barrierfree")
|
||||
val excludeFaults = filters?.getBooleanValue("exclude_faults")
|
||||
val minPower = filters?.getSliderValue("min_power")
|
||||
val minConnectors = filters?.getSliderValue("min_connectors")
|
||||
|
||||
var connectorsVal = filters?.getMultipleChoiceValue("connectors")
|
||||
if (connectorsVal != null && connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
|
||||
val chargeCardsVal = filters?.getMultipleChoiceValue("chargecards")
|
||||
if (chargeCardsVal != null && chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) {
|
||||
// no chargeCards chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val chargeCards = formatMultipleChoice(chargeCardsVal)
|
||||
|
||||
val networksVal = filters?.getMultipleChoiceValue("networks")
|
||||
if (networksVal != null && networksVal.values.isEmpty() && !networksVal.all) {
|
||||
// no networks chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val networks = formatMultipleChoice(networksVal)
|
||||
|
||||
val categoriesVal = filters?.getMultipleChoiceValue("categories")
|
||||
if (categoriesVal != null && categoriesVal.values.isEmpty() && !categoriesVal.all) {
|
||||
// no categories chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val categories = formatMultipleChoice(categoriesVal)
|
||||
|
||||
// do not use clustering if filters need to be applied locally.
|
||||
val useClustering = zoom < 13
|
||||
val geClusteringAvailable = minConnectors == null || minConnectors <= 1
|
||||
val useGeClustering = useClustering && geClusteringAvailable
|
||||
val clusterDistance = if (useClustering) getClusterDistance(zoom) else null
|
||||
|
||||
var startkey: Int? = null
|
||||
val data = mutableListOf<GEChargepointListItem>()
|
||||
do {
|
||||
// load all pages of the response
|
||||
try {
|
||||
val response = api.getChargepointsRadius(
|
||||
location.latitude, location.longitude, radius,
|
||||
clustering = useGeClustering,
|
||||
zoom = zoom,
|
||||
clusterDistance = clusterDistance,
|
||||
freecharging = freecharging ?: false,
|
||||
minPower = minPower ?: 0,
|
||||
freeparking = freeparking ?: false,
|
||||
open247 = open247 ?: false,
|
||||
barrierfree = barrierfree ?: false,
|
||||
excludeFaults = excludeFaults ?: false,
|
||||
plugs = connectors,
|
||||
chargecards = chargeCards,
|
||||
networks = networks,
|
||||
categories = categories,
|
||||
startkey = startkey
|
||||
)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
return Resource.error(response.message(), null)
|
||||
} else {
|
||||
val body = response.body()!!
|
||||
data.addAll(body.chargelocations)
|
||||
startkey = body.startkey
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
} while (startkey != null && startkey < 10000)
|
||||
|
||||
val result = postprocessResult(data, minPower, connectorsVal, minConnectors, zoom)
|
||||
return Resource.success(result)
|
||||
}
|
||||
|
||||
private fun postprocessResult(
|
||||
chargers: List<GEChargepointListItem>,
|
||||
minPower: Int?,
|
||||
connectorsVal: MultipleChoiceFilterValue?,
|
||||
minConnectors: Int?,
|
||||
zoom: Float
|
||||
): List<ChargepointListItem> {
|
||||
// apply filters which GoingElectric does not support natively
|
||||
var result = chargers.filter { it ->
|
||||
if (it is GEChargeLocation) {
|
||||
it.chargepoints
|
||||
.filter { it.power >= (minPower ?: 0) }
|
||||
.filter { if (connectorsVal != null && !connectorsVal.all) it.type in connectorsVal.values else true }
|
||||
.sumOf { it.count } >= (minConnectors ?: 0)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}.map { it.convert(apikey) }
|
||||
|
||||
// apply clustering
|
||||
val useClustering = zoom < 13
|
||||
val geClusteringAvailable = minConnectors == null || minConnectors <= 1
|
||||
val clusterDistance = if (useClustering) getClusterDistance(zoom) else null
|
||||
if (!geClusteringAvailable && useClustering) {
|
||||
// apply local clustering if server side clustering is not available
|
||||
Dispatchers.IO.run {
|
||||
result = cluster(result, zoom, clusterDistance!!)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun getChargepointDetail(
|
||||
referenceData: ReferenceData,
|
||||
id: Long
|
||||
): Resource<ChargeLocation> {
|
||||
try {
|
||||
val response = api.getChargepointDetail(id)
|
||||
return if (response.isSuccessful && response.body()!!.status == "ok" && response.body()!!.chargelocations.size == 1) {
|
||||
Resource.success(
|
||||
(response.body()!!.chargelocations[0] as GEChargeLocation).convert(
|
||||
apikey
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Resource.error(response.message(), null)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getReferenceData(): Resource<GEReferenceData> =
|
||||
withContext(Dispatchers.IO) {
|
||||
supervisorScope {
|
||||
try {
|
||||
val plugs = async { api.getPlugs() }
|
||||
val chargeCards = async { api.getChargeCards() }
|
||||
val networks = async { api.getNetworks() }
|
||||
|
||||
val plugsResponse = plugs.await()
|
||||
val chargeCardsResponse = chargeCards.await()
|
||||
val networksResponse = networks.await()
|
||||
|
||||
val responses = listOf(plugsResponse, chargeCardsResponse, networksResponse)
|
||||
|
||||
if (responses.map { it.isSuccessful }.all { it }) {
|
||||
Resource.success(
|
||||
GEReferenceData(
|
||||
plugsResponse.body()!!.result,
|
||||
networksResponse.body()!!.result,
|
||||
chargeCardsResponse.body()!!.result
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Resource.error(responses.find { !it.isSuccessful }!!.message(), null)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilters(
|
||||
referenceData: ReferenceData,
|
||||
sp: StringProvider
|
||||
): List<Filter<FilterValue>> {
|
||||
val referenceData = referenceData as GEReferenceData
|
||||
val plugs = referenceData.plugs
|
||||
val networks = referenceData.networks
|
||||
val chargeCards = referenceData.chargecards
|
||||
|
||||
val plugMap = plugs.map { plug ->
|
||||
plug to nameForPlugType(sp, plug)
|
||||
}.toMap()
|
||||
val networkMap = networks.map { it to it }.toMap()
|
||||
val chargecardMap = chargeCards.map { it.id.toString() to it.name }.toMap()
|
||||
val categoryMap = mapOf(
|
||||
"Autohaus" to sp.getString(R.string.category_car_dealership),
|
||||
"Autobahnraststätte" to sp.getString(R.string.category_service_on_motorway),
|
||||
"Autohof" to sp.getString(R.string.category_service_off_motorway),
|
||||
"Bahnhof" to sp.getString(R.string.category_railway_station),
|
||||
"Behörde" to sp.getString(R.string.category_public_authorities),
|
||||
"Campingplatz" to sp.getString(R.string.category_camping),
|
||||
"Einkaufszentrum" to sp.getString(R.string.category_shopping_mall),
|
||||
"Ferienwohnung" to sp.getString(R.string.category_holiday_home),
|
||||
"Flughafen" to sp.getString(R.string.category_airport),
|
||||
"Freizeitpark" to sp.getString(R.string.category_amusement_park),
|
||||
"Hotel" to sp.getString(R.string.category_hotel),
|
||||
"Kino" to sp.getString(R.string.category_cinema),
|
||||
"Kirche" to sp.getString(R.string.category_church),
|
||||
"Krankenhaus" to sp.getString(R.string.category_hospital),
|
||||
"Museum" to sp.getString(R.string.category_museum),
|
||||
"Parkhaus" to sp.getString(R.string.category_parking_multi),
|
||||
"Parkplatz" to sp.getString(R.string.category_parking),
|
||||
"Privater Ladepunkt" to sp.getString(R.string.category_private_charger),
|
||||
"Rastplatz" to sp.getString(R.string.category_rest_area),
|
||||
"Restaurant" to sp.getString(R.string.category_restaurant),
|
||||
"Schwimmbad" to sp.getString(R.string.category_swimming_pool),
|
||||
"Supermarkt" to sp.getString(R.string.category_supermarket),
|
||||
"Tankstelle" to sp.getString(R.string.category_petrol_station),
|
||||
"Tiefgarage" to sp.getString(R.string.category_parking_underground),
|
||||
"Tierpark" to sp.getString(R.string.category_zoo),
|
||||
"Wohnmobilstellplatz" to sp.getString(R.string.category_caravan_site)
|
||||
)
|
||||
return listOf(
|
||||
BooleanFilter(sp.getString(R.string.filter_free), "freecharging"),
|
||||
BooleanFilter(sp.getString(R.string.filter_free_parking), "freeparking"),
|
||||
BooleanFilter(sp.getString(R.string.filter_open_247), "open_247"),
|
||||
SliderFilter(
|
||||
sp.getString(R.string.filter_min_power), "min_power",
|
||||
powerSteps.size - 1,
|
||||
mapping = ::mapPower,
|
||||
inverseMapping = ::mapPowerInverse,
|
||||
unit = "kW"
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.filter_connectors), "connectors",
|
||||
plugMap,
|
||||
commonChoices = setOf(
|
||||
Chargepoint.TYPE_2_UNKNOWN,
|
||||
Chargepoint.CCS_UNKNOWN,
|
||||
Chargepoint.CHADEMO
|
||||
),
|
||||
manyChoices = true
|
||||
),
|
||||
SliderFilter(
|
||||
sp.getString(R.string.filter_min_connectors),
|
||||
"min_connectors",
|
||||
10,
|
||||
min = 1
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.filter_networks), "networks",
|
||||
networkMap, manyChoices = true
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.categories), "categories",
|
||||
categoryMap,
|
||||
manyChoices = true
|
||||
),
|
||||
BooleanFilter(sp.getString(R.string.filter_barrierfree), "barrierfree"),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.filter_chargecards), "chargecards",
|
||||
chargecardMap, manyChoices = true
|
||||
),
|
||||
BooleanFilter(sp.getString(R.string.filter_exclude_faults), "exclude_faults")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,58 +1,47 @@
|
||||
package net.vonforst.evmap.api.goingelectric
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import java.time.DayOfWeek
|
||||
import net.vonforst.evmap.model.*
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepointList(
|
||||
data class GEChargepointList(
|
||||
val status: String,
|
||||
val chargelocations: List<ChargepointListItem>,
|
||||
val chargelocations: List<GEChargepointListItem>,
|
||||
@JsonObjectOrFalse val startkey: Int?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class StringList(
|
||||
data class GEStringList(
|
||||
val status: String,
|
||||
val result: List<String>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargeCardList(
|
||||
data class GEChargeCardList(
|
||||
val status: String,
|
||||
val result: List<ChargeCard>
|
||||
val result: List<GEChargeCard>
|
||||
)
|
||||
|
||||
sealed class ChargepointListItem
|
||||
sealed class GEChargepointListItem {
|
||||
abstract fun convert(apikey: String): ChargepointListItem
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Entity
|
||||
data class ChargeLocation(
|
||||
@Json(name = "ge_id") @PrimaryKey val id: Long,
|
||||
data class GEChargeLocation(
|
||||
@Json(name = "ge_id") val id: Long,
|
||||
val name: String,
|
||||
@Embedded val coordinates: Coordinate,
|
||||
@Embedded val address: Address,
|
||||
val chargepoints: List<Chargepoint>,
|
||||
val coordinates: GECoordinate,
|
||||
val address: GEAddress,
|
||||
val chargepoints: List<GEChargepoint>,
|
||||
@JsonObjectOrFalse val network: String?,
|
||||
val url: String,
|
||||
@Embedded(prefix = "fault_report_") @JsonObjectOrFalse @Json(name = "fault_report") val faultReport: FaultReport?,
|
||||
@JsonObjectOrFalse @Json(name = "fault_report") val faultReport: GEFaultReport?,
|
||||
val verified: Boolean,
|
||||
@Json(name = "barrierfree") val barrierFree: Boolean?,
|
||||
// only shown in details:
|
||||
@@ -60,260 +49,193 @@ data class ChargeLocation(
|
||||
@JsonObjectOrFalse @Json(name = "general_information") val generalInformation: String?,
|
||||
@JsonObjectOrFalse @Json(name = "ladeweile") val amenities: String?,
|
||||
@JsonObjectOrFalse @Json(name = "location_description") val locationDescription: String?,
|
||||
val photos: List<ChargerPhoto>?,
|
||||
@JsonObjectOrFalse val chargecards: List<ChargeCardId>?,
|
||||
@Embedded val openinghours: OpeningHours?,
|
||||
@Embedded val cost: Cost?
|
||||
) : ChargepointListItem(), Equatable {
|
||||
/**
|
||||
* maximum power available from this charger.
|
||||
*/
|
||||
val maxPower: Double
|
||||
get() {
|
||||
return maxPower()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum power available from certain connectors of this charger.
|
||||
*/
|
||||
fun maxPower(connectors: Set<String>? = null): Double {
|
||||
return chargepoints.filter { connectors?.contains(it.type) ?: true }
|
||||
.map { it.power }.maxOrNull() ?: 0.0
|
||||
}
|
||||
|
||||
fun isMulti(filteredConnectors: Set<String>? = null): Boolean {
|
||||
var chargepoints = chargepointsMerged
|
||||
.filter { filteredConnectors?.contains(it.type) ?: true }
|
||||
if (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 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges chargepoints if they have the same plug and power
|
||||
*
|
||||
* This occurs e.g. for Type2 sockets and plugs, which are distinct on the GE website, but not
|
||||
* separable in the API
|
||||
*/
|
||||
val chargepointsMerged: List<Chargepoint>
|
||||
get() {
|
||||
val variants = chargepoints.distinctBy { it.power to it.type }
|
||||
return variants.map { variant ->
|
||||
val count = chargepoints
|
||||
.filter { it.type == variant.type && it.power == variant.power }
|
||||
.sumBy { it.count }
|
||||
Chargepoint(variant.type, variant.power, count)
|
||||
}
|
||||
}
|
||||
|
||||
val totalChargepoints: Int
|
||||
get() = chargepoints.sumBy { it.count }
|
||||
|
||||
fun formatChargepoints(): String {
|
||||
return chargepointsMerged.map {
|
||||
"${it.count} × ${it.type} ${it.formatPower()}"
|
||||
}.joinToString(" · ")
|
||||
}
|
||||
val photos: List<GEChargerPhoto>?,
|
||||
@JsonObjectOrFalse val chargecards: List<GEChargeCardId>?,
|
||||
val openinghours: GEOpeningHours?,
|
||||
val cost: GECost?
|
||||
) : GEChargepointListItem() {
|
||||
override fun convert(apikey: String) = ChargeLocation(
|
||||
id,
|
||||
name,
|
||||
coordinates.convert(),
|
||||
address.convert(),
|
||||
chargepoints.map { it.convert() },
|
||||
network,
|
||||
"https:${url}",
|
||||
"https:${url}edit/",
|
||||
faultReport?.convert(),
|
||||
verified,
|
||||
barrierFree,
|
||||
operator,
|
||||
generalInformation,
|
||||
amenities,
|
||||
locationDescription,
|
||||
photos?.map { it.convert(apikey) },
|
||||
chargecards?.map { it.convert() },
|
||||
openinghours?.convert(),
|
||||
cost?.convert(),
|
||||
null,
|
||||
ChargepriceData(address.country, network, chargepoints.map { it.type })
|
||||
)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Cost(
|
||||
data class GECost(
|
||||
val freecharging: Boolean,
|
||||
val freeparking: Boolean,
|
||||
@JsonObjectOrFalse @Json(name = "description_short") val descriptionShort: String?,
|
||||
@JsonObjectOrFalse @Json(name = "description_long") val descriptionLong: String?
|
||||
) {
|
||||
fun getStatusText(ctx: Context, emoji: Boolean = false): CharSequence {
|
||||
val charging =
|
||||
if (freecharging) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
|
||||
val parking =
|
||||
if (freeparking) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
|
||||
return if (emoji) {
|
||||
"⚡ $charging · \uD83C\uDD7F️ $parking"
|
||||
} else {
|
||||
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail, charging, parking), 0)
|
||||
}
|
||||
}
|
||||
fun convert() = Cost(freecharging, freeparking, descriptionShort, descriptionLong)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OpeningHours(
|
||||
data class GEOpeningHours(
|
||||
@Json(name = "24/7") val twentyfourSeven: Boolean,
|
||||
@JsonObjectOrFalse val description: String?,
|
||||
@Embedded val days: OpeningHoursDays?
|
||||
val days: GEOpeningHoursDays?
|
||||
) {
|
||||
val isEmpty: Boolean
|
||||
get() = description == "Leider noch keine Informationen zu Öffnungszeiten vorhanden."
|
||||
&& days == null && !twentyfourSeven
|
||||
|
||||
fun getStatusText(ctx: Context): CharSequence {
|
||||
if (twentyfourSeven) {
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.open_247), 0)
|
||||
} else if (days != null) {
|
||||
val hours = days.getHoursForDate(LocalDate.now())
|
||||
if (hours.start == null || hours.end == null) {
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
|
||||
}
|
||||
|
||||
val now = LocalTime.now()
|
||||
if (hours.start.isBefore(now) && hours.end.isAfter(now)) {
|
||||
return HtmlCompat.fromHtml(
|
||||
ctx.getString(
|
||||
R.string.open_closesat,
|
||||
hours.end.toString()
|
||||
), 0
|
||||
)
|
||||
} else if (hours.end.isBefore(now)) {
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
|
||||
} else {
|
||||
return HtmlCompat.fromHtml(
|
||||
ctx.getString(
|
||||
R.string.closed_opensat,
|
||||
hours.start.toString()
|
||||
), 0
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
fun convert() = OpeningHours(twentyfourSeven, description, days?.convert())
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OpeningHoursDays(
|
||||
@Embedded(prefix = "mo") val monday: Hours,
|
||||
@Embedded(prefix = "tu") val tuesday: Hours,
|
||||
@Embedded(prefix = "we") val wednesday: Hours,
|
||||
@Embedded(prefix = "th") val thursday: Hours,
|
||||
@Embedded(prefix = "fr") val friday: Hours,
|
||||
@Embedded(prefix = "sa") val saturday: Hours,
|
||||
@Embedded(prefix = "su") val sunday: Hours,
|
||||
@Embedded(prefix = "ho") val holiday: Hours
|
||||
data class GEOpeningHoursDays(
|
||||
val monday: GEHours,
|
||||
val tuesday: GEHours,
|
||||
val wednesday: GEHours,
|
||||
val thursday: GEHours,
|
||||
val friday: GEHours,
|
||||
val saturday: GEHours,
|
||||
val sunday: GEHours,
|
||||
val holiday: GEHours
|
||||
) {
|
||||
fun getHoursForDate(date: LocalDate): Hours {
|
||||
// TODO: check for holidays
|
||||
return getHoursForDayOfWeek(date.dayOfWeek)
|
||||
}
|
||||
|
||||
fun getHoursForDayOfWeek(dayOfWeek: DayOfWeek?): Hours {
|
||||
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
|
||||
return when (dayOfWeek) {
|
||||
DayOfWeek.MONDAY -> monday
|
||||
DayOfWeek.TUESDAY -> tuesday
|
||||
DayOfWeek.WEDNESDAY -> wednesday
|
||||
DayOfWeek.THURSDAY -> thursday
|
||||
DayOfWeek.FRIDAY -> friday
|
||||
DayOfWeek.SATURDAY -> saturday
|
||||
DayOfWeek.SUNDAY -> sunday
|
||||
null -> holiday
|
||||
}
|
||||
}
|
||||
fun convert() = OpeningHoursDays(
|
||||
monday.convert(),
|
||||
tuesday.convert(),
|
||||
wednesday.convert(),
|
||||
thursday.convert(),
|
||||
friday.convert(),
|
||||
saturday.convert(),
|
||||
sunday.convert(),
|
||||
holiday.convert()
|
||||
)
|
||||
}
|
||||
|
||||
data class Hours(
|
||||
data class GEHours(
|
||||
val start: LocalTime?,
|
||||
val end: LocalTime?
|
||||
) {
|
||||
override fun toString(): String {
|
||||
if (start != null && end != null) {
|
||||
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
return "${start.format(fmt)} - ${end.format(fmt)}"
|
||||
} else {
|
||||
return "closed"
|
||||
}
|
||||
}
|
||||
fun convert() = Hours(start, end)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GEChargerPhoto(val id: String) {
|
||||
fun convert(apikey: String): ChargerPhoto = GEChargerPhotoAdapter(id, apikey)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class ChargerPhoto(val id: String) : Parcelable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargeLocationCluster(
|
||||
val clusterCount: Int,
|
||||
val coordinates: Coordinate
|
||||
) : ChargepointListItem()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Coordinate(val lat: Double, val lng: Double) {
|
||||
fun formatDMS(): String {
|
||||
return "${dms(lat, false)}, ${dms(lng, true)}"
|
||||
}
|
||||
|
||||
private fun dms(value: Double, lon: Boolean): String {
|
||||
val hemisphere = if (lon) {
|
||||
if (value >= 0) "E" else "W"
|
||||
} else {
|
||||
if (value >= 0) "N" else "S"
|
||||
}
|
||||
val d = abs(value)
|
||||
val degrees = floor(d).toInt()
|
||||
val minutes = floor((d - degrees) * 60).toInt()
|
||||
val seconds = ((d - degrees) * 60 - minutes) * 60
|
||||
return "%d°%02d'%02.1f\"%s".format(Locale.ENGLISH, degrees, minutes, seconds, hemisphere)
|
||||
}
|
||||
|
||||
fun formatDecimal(): String {
|
||||
return "%.6f, %.6f".format(Locale.ENGLISH, lat, lng)
|
||||
class GEChargerPhotoAdapter(override val id: String, val apikey: String) :
|
||||
ChargerPhoto(id) {
|
||||
override fun getUrl(height: Int?, width: Int?, size: Int?): String {
|
||||
return "https://api.goingelectric.de/chargepoints/photo/?key=${apikey}&id=$id" +
|
||||
when {
|
||||
size != null -> "&size=$size"
|
||||
height != null -> "&height=$height"
|
||||
width != null -> "&width=$width"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Address(
|
||||
data class GEChargeLocationCluster(
|
||||
val clusterCount: Int,
|
||||
val coordinates: GECoordinate
|
||||
) : GEChargepointListItem() {
|
||||
override fun convert(apikey: String) =
|
||||
ChargeLocationCluster(clusterCount, coordinates.convert())
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GECoordinate(val lat: Double, val lng: Double) {
|
||||
fun convert() = Coordinate(lat, lng)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GEAddress(
|
||||
@JsonObjectOrFalse val city: String?,
|
||||
@JsonObjectOrFalse val country: String?,
|
||||
@JsonObjectOrFalse val postcode: String?,
|
||||
@JsonObjectOrFalse val street: String?
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "${street ?: ""}, ${postcode ?: ""} ${city ?: ""}"
|
||||
}
|
||||
fun convert() = Address(city, country, postcode, street)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Chargepoint(val type: String, val power: Double, val count: Int) : Equatable {
|
||||
fun formatPower(): String {
|
||||
val powerFmt = if (power - power.toInt() == 0.0) {
|
||||
"%.0f".format(power)
|
||||
} else {
|
||||
"%.1f".format(power)
|
||||
}
|
||||
return "$powerFmt kW"
|
||||
}
|
||||
data class GEChargepoint(val type: String, val power: Double, val count: Int) {
|
||||
fun convert() = Chargepoint(convertTypeFromGE(type), power, count)
|
||||
|
||||
companion object {
|
||||
const val TYPE_1 = "Typ1"
|
||||
const val TYPE_2 = "Typ2"
|
||||
const val TYPE_3 = "Typ3"
|
||||
const val CCS = "CCS"
|
||||
const val SCHUKO = "Schuko"
|
||||
const val CHADEMO = "CHAdeMO"
|
||||
const val SUPERCHARGER = "Tesla Supercharger"
|
||||
const val CEE_BLAU = "CEE Blau"
|
||||
const val CEE_ROT = "CEE Rot"
|
||||
const val TESLA_ROADSTER_HPC = "Tesla HPC"
|
||||
fun convertTypeToGE(type: String): String? {
|
||||
return when (type) {
|
||||
Chargepoint.TYPE_1 -> "Typ1"
|
||||
Chargepoint.TYPE_2_UNKNOWN -> "Typ2"
|
||||
Chargepoint.TYPE_3 -> "Typ3"
|
||||
Chargepoint.CCS_UNKNOWN -> "CCS"
|
||||
Chargepoint.CCS_TYPE_2 -> "Typ2"
|
||||
Chargepoint.SCHUKO -> "Schuko"
|
||||
Chargepoint.CHADEMO -> "CHAdeMO"
|
||||
Chargepoint.SUPERCHARGER -> "Tesla Supercharger"
|
||||
Chargepoint.CEE_BLAU -> "CEE Blau"
|
||||
Chargepoint.CEE_ROT -> "CEE Rot"
|
||||
Chargepoint.TESLA_ROADSTER_HPC -> "Tesla HPC"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun convertTypeFromGE(type: String): String {
|
||||
return when (type) {
|
||||
"Typ1" -> Chargepoint.TYPE_1
|
||||
"Typ2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||
"Typ3" -> Chargepoint.TYPE_3
|
||||
"CCS" -> Chargepoint.CCS_UNKNOWN
|
||||
"Schuko" -> Chargepoint.SCHUKO
|
||||
"CHAdeMO" -> Chargepoint.CHADEMO
|
||||
"Tesla Supercharger" -> Chargepoint.SUPERCHARGER
|
||||
"CEE Blau" -> Chargepoint.CEE_BLAU
|
||||
"CEE Rot" -> Chargepoint.CEE_ROT
|
||||
"Tesla HPC" -> Chargepoint.TESLA_ROADSTER_HPC
|
||||
else -> type
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FaultReport(val created: Instant?, val description: String?)
|
||||
data class GEFaultReport(val created: Instant?, val description: String?) {
|
||||
fun convert() = FaultReport(created, description)
|
||||
}
|
||||
|
||||
@Entity
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargeCard(
|
||||
@Entity
|
||||
data class GEChargeCard(
|
||||
@Json(name = "card_id") @PrimaryKey val id: Long,
|
||||
val name: String,
|
||||
val url: String
|
||||
)
|
||||
) {
|
||||
fun convert() = ChargeCard(id, name, url)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargeCardId(
|
||||
data class GEChargeCardId(
|
||||
val id: Long
|
||||
)
|
||||
) {
|
||||
fun convert() = ChargeCardId(id)
|
||||
}
|
||||
|
||||
data class GEReferenceData(
|
||||
val plugs: List<String>,
|
||||
val networks: List<String>,
|
||||
val chargecards: List<GEChargeCard>
|
||||
) : ReferenceData()
|
||||
@@ -0,0 +1,15 @@
|
||||
package net.vonforst.evmap.api.openchargemap
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.ToJson
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
internal class ZonedDateTimeAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String?): ZonedDateTime? = value?.let {
|
||||
ZonedDateTime.parse(value)
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: ZonedDateTime?): String? = value?.toString()
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
package net.vonforst.evmap.api.openchargemap
|
||||
|
||||
import android.content.Context
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.*
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.ui.cluster
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import net.vonforst.evmap.viewmodel.getClusterDistance
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
import java.io.IOException
|
||||
|
||||
interface OpenChargeMapApi {
|
||||
@GET("poi/")
|
||||
suspend fun getChargepoints(
|
||||
@Query("boundingbox") boundingbox: OCMBoundingBox,
|
||||
@Query("connectiontypeid") plugs: String? = null,
|
||||
@Query("minpowerkw") minPower: Double? = null,
|
||||
@Query("operatorid") operators: String? = null,
|
||||
@Query("statustypeid") statusType: String? = null,
|
||||
@Query("maxresults") maxresults: Int = 500,
|
||||
@Query("compact") compact: Boolean = true,
|
||||
@Query("verbose") verbose: Boolean = false
|
||||
): Response<List<OCMChargepoint>>
|
||||
|
||||
@GET("poi/")
|
||||
suspend fun getChargepointsRadius(
|
||||
@Query("latitude") latitude: Double,
|
||||
@Query("longitude") longitude: Double,
|
||||
@Query("distance") distance: Double,
|
||||
@Query("distanceunit") distanceUnit: String = "KM",
|
||||
@Query("connectiontypeid") plugs: String? = null,
|
||||
@Query("minpowerkw") minPower: Double? = null,
|
||||
@Query("operatorid") operators: String? = null,
|
||||
@Query("statustypeid") statusType: String? = null,
|
||||
@Query("maxresults") maxresults: Int = 500,
|
||||
@Query("compact") compact: Boolean = true,
|
||||
@Query("verbose") verbose: Boolean = false
|
||||
): Response<List<OCMChargepoint>>
|
||||
|
||||
@GET("poi/")
|
||||
suspend fun getChargepointDetail(
|
||||
@Query("chargepointid") id: Long,
|
||||
@Query("includecomments") includeComments: Boolean = true,
|
||||
@Query("compact") compact: Boolean = false,
|
||||
@Query("verbose") verbose: Boolean = false
|
||||
): Response<List<OCMChargepoint>>
|
||||
|
||||
@GET("referencedata/")
|
||||
suspend fun getReferenceData(): Response<OCMReferenceData>
|
||||
|
||||
companion object {
|
||||
private val cacheSize = 10L * 1024 * 1024 // 10MB
|
||||
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(ZonedDateTimeAdapter())
|
||||
.build()
|
||||
|
||||
fun create(
|
||||
apikey: String,
|
||||
baseurl: String = "https://api.openchargemap.io/v3/",
|
||||
context: Context? = null
|
||||
): OpenChargeMapApi {
|
||||
val client = OkHttpClient.Builder().apply {
|
||||
addInterceptor { chain ->
|
||||
// add API key to every request
|
||||
val original = chain.request()
|
||||
val new = original.newBuilder()
|
||||
.header("X-API-Key", apikey)
|
||||
.build()
|
||||
chain.proceed(new)
|
||||
}
|
||||
if (BuildConfig.DEBUG) {
|
||||
addNetworkInterceptor(StethoInterceptor())
|
||||
}
|
||||
if (context != null) {
|
||||
cache(Cache(context.cacheDir, cacheSize))
|
||||
}
|
||||
}.build()
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseurl)
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.client(client)
|
||||
.build()
|
||||
return retrofit.create(OpenChargeMapApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OpenChargeMapApiWrapper(
|
||||
apikey: String,
|
||||
baseurl: String = "https://api.openchargemap.io/v3/",
|
||||
context: Context? = null
|
||||
) : ChargepointApi<OCMReferenceData> {
|
||||
val api = OpenChargeMapApi.create(apikey, baseurl, context)
|
||||
|
||||
override fun getName() = "OpenChargeMap.org"
|
||||
|
||||
private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) =
|
||||
if (value == null || value.all) null else value.values.joinToString(",")
|
||||
|
||||
override suspend fun getChargepoints(
|
||||
referenceData: ReferenceData,
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
filters: FilterValues?,
|
||||
): Resource<List<ChargepointListItem>> {
|
||||
val referenceData = referenceData as OCMReferenceData
|
||||
|
||||
val minPower = filters?.getSliderValue("min_power")?.toDouble()
|
||||
val minConnectors = filters?.getSliderValue("min_connectors")
|
||||
val excludeFaults = filters?.getBooleanValue("exclude_faults")
|
||||
|
||||
val connectorsVal = filters?.getMultipleChoiceValue("connectors")
|
||||
if (connectorsVal != null && connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
|
||||
val operatorsVal = filters?.getMultipleChoiceValue("operators")!!
|
||||
if (operatorsVal != null && operatorsVal.values.isEmpty() && !operatorsVal.all) {
|
||||
// no operators chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val operators = formatMultipleChoice(operatorsVal)
|
||||
|
||||
try {
|
||||
val response = api.getChargepoints(
|
||||
OCMBoundingBox(
|
||||
bounds.southwest.latitude, bounds.southwest.longitude,
|
||||
bounds.northeast.latitude, bounds.northeast.longitude
|
||||
),
|
||||
minPower = minPower,
|
||||
plugs = connectors,
|
||||
operators = operators,
|
||||
statusType = if (excludeFaults == true) noFaultStatuses.joinToString(",") else null
|
||||
)
|
||||
if (!response.isSuccessful) {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
|
||||
var result = postprocessResult(
|
||||
response.body()!!,
|
||||
minPower,
|
||||
connectorsVal,
|
||||
minConnectors,
|
||||
referenceData,
|
||||
zoom
|
||||
)
|
||||
return Resource.success(result)
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getChargepointsRadius(
|
||||
referenceData: ReferenceData,
|
||||
location: LatLng,
|
||||
radius: Int,
|
||||
zoom: Float,
|
||||
filters: FilterValues?
|
||||
): Resource<List<ChargepointListItem>> {
|
||||
val referenceData = referenceData as OCMReferenceData
|
||||
|
||||
val minPower = filters?.getSliderValue("min_power")?.toDouble()
|
||||
val minConnectors = filters?.getSliderValue("min_connectors")
|
||||
val excludeFaults = filters?.getBooleanValue("exclude_faults")
|
||||
|
||||
val connectorsVal = filters?.getMultipleChoiceValue("connectors")
|
||||
if (connectorsVal != null && connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
|
||||
val operatorsVal = filters?.getMultipleChoiceValue("operators")
|
||||
if (operatorsVal != null && operatorsVal.values.isEmpty() && !operatorsVal.all) {
|
||||
// no operators chosen
|
||||
return Resource.success(emptyList())
|
||||
}
|
||||
val operators = formatMultipleChoice(operatorsVal)
|
||||
|
||||
try {
|
||||
val response = api.getChargepointsRadius(
|
||||
location.latitude, location.longitude,
|
||||
radius.toDouble(),
|
||||
minPower = minPower,
|
||||
plugs = connectors,
|
||||
operators = operators,
|
||||
statusType = if (excludeFaults == true) noFaultStatuses.joinToString(",") else null
|
||||
)
|
||||
if (!response.isSuccessful) {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
|
||||
val result = postprocessResult(
|
||||
response.body()!!,
|
||||
minPower,
|
||||
connectorsVal,
|
||||
minConnectors,
|
||||
referenceData,
|
||||
zoom
|
||||
)
|
||||
return Resource.success(result)
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun postprocessResult(
|
||||
chargers: List<OCMChargepoint>,
|
||||
minPower: Double?,
|
||||
connectorsVal: MultipleChoiceFilterValue?,
|
||||
minConnectors: Int?,
|
||||
referenceData: OCMReferenceData,
|
||||
zoom: Float
|
||||
): List<ChargepointListItem> {
|
||||
// apply filters which OCM does not support natively
|
||||
var result = chargers.filter { it ->
|
||||
it.connections
|
||||
.filter { it.power == null || it.power >= (minPower ?: 0.0) }
|
||||
.filter { if (connectorsVal != null && !connectorsVal.all) it.connectionTypeId in connectorsVal.values.map { it.toLong() } else true }
|
||||
.sumOf { it.quantity ?: 1 } >= (minConnectors ?: 0)
|
||||
}.map { it.convert(referenceData) }.distinct() as List<ChargepointListItem>
|
||||
|
||||
// apply clustering
|
||||
val useClustering = zoom < 13
|
||||
if (useClustering) {
|
||||
val clusterDistance = getClusterDistance(zoom)
|
||||
Dispatchers.IO.run {
|
||||
result = cluster(result, zoom, clusterDistance!!)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun getChargepointDetail(
|
||||
referenceData: ReferenceData,
|
||||
id: Long
|
||||
): Resource<ChargeLocation> {
|
||||
val referenceData = referenceData as OCMReferenceData
|
||||
try {
|
||||
val response = api.getChargepointDetail(id)
|
||||
if (response.isSuccessful && response.body()?.size == 1) {
|
||||
return Resource.success(response.body()!![0].convert(referenceData))
|
||||
} else {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getReferenceData(): Resource<OCMReferenceData> {
|
||||
try {
|
||||
val response = api.getReferenceData()
|
||||
if (response.isSuccessful) {
|
||||
return Resource.success(response.body()!!)
|
||||
} else {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilters(
|
||||
referenceData: ReferenceData,
|
||||
sp: StringProvider
|
||||
): List<Filter<FilterValue>> {
|
||||
val referenceData = referenceData as OCMReferenceData
|
||||
|
||||
val operatorsMap = referenceData.operators.map { it.id.toString() to it.title }.toMap()
|
||||
val plugMap = referenceData.connectionTypes.map { it.id.toString() to it.title }.toMap()
|
||||
|
||||
return listOf(
|
||||
// supported by OCM API
|
||||
SliderFilter(
|
||||
sp.getString(R.string.filter_min_power), "min_power",
|
||||
powerSteps.size - 1,
|
||||
mapping = ::mapPower,
|
||||
inverseMapping = ::mapPowerInverse,
|
||||
unit = "kW"
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.filter_connectors), "connectors",
|
||||
plugMap,
|
||||
commonChoices = setOf(
|
||||
"1", // Type 1 (J1772)
|
||||
"25", // Type 2 (Socket only)
|
||||
"1036", // Type 2 (Tethered connector)
|
||||
"32", // CCS (Type 1)
|
||||
"33", // CCS (Type 2)
|
||||
"2" // CHAdeMO
|
||||
),
|
||||
manyChoices = true
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.filter_operators), "operators",
|
||||
operatorsMap, manyChoices = true
|
||||
),
|
||||
BooleanFilter(sp.getString(R.string.filter_exclude_faults), "exclude_faults"),
|
||||
|
||||
// local filters
|
||||
SliderFilter(
|
||||
sp.getString(R.string.filter_min_connectors),
|
||||
"min_connectors",
|
||||
10,
|
||||
min = 1
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package net.vonforst.evmap.api.openchargemap
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import net.vonforst.evmap.max
|
||||
import net.vonforst.evmap.model.*
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
// Unknown, Currently Available, Currently In Use, Operational
|
||||
val noFaultStatuses = listOf(0, 10, 20, 50)
|
||||
|
||||
// Temporarily Unavailable, Partly Operational, Not Operational, Planned For Future Date, Removed (Decommissioned)
|
||||
val faultStatuses = listOf(30L, 75L, 100L, 150L, 200L)
|
||||
val faultReportCommentType = 1000L
|
||||
|
||||
data class OCMBoundingBox(
|
||||
val sw_lat: Double, val sw_lng: Double,
|
||||
val ne_lat: Double, val ne_lng: Double
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return "($sw_lat,$sw_lng),($ne_lat,$ne_lng)"
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OCMChargepoint(
|
||||
@Json(name = "ID") val id: Long,
|
||||
@Json(name = "IsRecentlyVerified") val recentlyVerified: Boolean,
|
||||
@Json(name = "DateLastVerified") val dateLastVerified: ZonedDateTime?,
|
||||
@Json(name = "UsageCost") val cost: String?,
|
||||
@Json(name = "AddressInfo") val addressInfo: OCMAddressInfo,
|
||||
@Json(name = "Connections") val connections: List<OCMConnection>,
|
||||
@Json(name = "NumberOfPoints") val numPoints: Int?,
|
||||
@Json(name = "GeneralComments") val generalComments: String?,
|
||||
@Json(name = "OperatorInfo") val operatorInfo: OCMOperator?,
|
||||
@Json(name = "OperatorID") val operatorId: Long?,
|
||||
@Json(name = "DataProvider") val dataProvider: OCMDataProvider?,
|
||||
@Json(name = "MediaItems") val mediaItems: List<OCMMediaItem>?,
|
||||
@Json(name = "StatusTypeID") val statusTypeId: Long?,
|
||||
@Json(name = "StatusType") val statusType: OCMStatusType?,
|
||||
@Json(name = "UserComments") val userComments: List<OCMUserComment>?,
|
||||
@Json(name = "DateLastStatusUpdate") val lastStatusUpdateDate: ZonedDateTime?
|
||||
) {
|
||||
fun convert(refData: OCMReferenceData) = ChargeLocation(
|
||||
id,
|
||||
addressInfo.title,
|
||||
Coordinate(addressInfo.latitude, addressInfo.longitude),
|
||||
addressInfo.toAddress(refData),
|
||||
connections.map { it.convert(refData) },
|
||||
operatorInfo?.title,
|
||||
"https://openchargemap.org/site/poi/details/$id",
|
||||
"https://openchargemap.org/site/poi/edit/$id",
|
||||
convertFaultReport(),
|
||||
recentlyVerified,
|
||||
null,
|
||||
null,
|
||||
generalComments,
|
||||
null,
|
||||
addressInfo.accessComments,
|
||||
mediaItems?.mapNotNull { it.convert() },
|
||||
null,
|
||||
null,
|
||||
cost?.let { Cost(descriptionShort = it) },
|
||||
dataProvider?.let { "© ${it.title}" + if (it.license != null) ". ${it.license}" else "" },
|
||||
ChargepriceData(
|
||||
addressInfo.countryISOCode(refData),
|
||||
operatorId?.toString(),
|
||||
connections.map { "${it.connectionTypeId},${it.currentTypeId}" })
|
||||
)
|
||||
|
||||
private fun convertFaultReport(): FaultReport? {
|
||||
if (statusTypeId in faultStatuses || connections.any { it.statusTypeId in faultStatuses }) {
|
||||
if (userComments != null) {
|
||||
val comment = userComments.filter { it.commentTypeId == faultReportCommentType }
|
||||
.maxByOrNull { it.dateCreated }
|
||||
if (comment != null) {
|
||||
return FaultReport(comment.dateCreated.toInstant(), comment.comment)
|
||||
}
|
||||
}
|
||||
if (statusType != null && statusType.id in faultStatuses) {
|
||||
return FaultReport(lastStatusUpdateDate?.toInstant(), statusType.title)
|
||||
} else if (connections.any { it.statusType != null && it.statusTypeId in faultStatuses }) {
|
||||
return FaultReport(
|
||||
lastStatusUpdateDate?.toInstant(),
|
||||
connections.first { it.statusType != null && it.statusTypeId in faultStatuses }.statusType!!.title
|
||||
)
|
||||
}
|
||||
return FaultReport(null, null)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OCMAddressInfo(
|
||||
@Json(name = "Title") val title: String,
|
||||
@Json(name = "AddressLine1") val addressLine1: String?,
|
||||
@Json(name = "AddressLine2") val addressLine2: String?,
|
||||
@Json(name = "Town") val town: String?,
|
||||
@Json(name = "StateOrProvince") val stateOrProvince: String?,
|
||||
@Json(name = "Postcode") val postcode: String?,
|
||||
@Json(name = "CountryID") val countryId: Long,
|
||||
@Json(name = "Latitude") val latitude: Double,
|
||||
@Json(name = "Longitude") val longitude: Double,
|
||||
@Json(name = "ContactTelephone1") val contactTelephone1: String?,
|
||||
@Json(name = "ContactTelephone2") val contactTelephone2: String?,
|
||||
@Json(name = "ContactEmail") val contactEmail: String?,
|
||||
@Json(name = "AccessComments") val accessComments: String?,
|
||||
@Json(name = "RelatedURL") val relatedUrl: String?
|
||||
) {
|
||||
fun toAddress(refData: OCMReferenceData) = Address(
|
||||
town,
|
||||
refData.countries.find { it.id == countryId }?.title,
|
||||
postcode,
|
||||
listOfNotNull(addressLine1, addressLine2).joinToString(", ")
|
||||
)
|
||||
|
||||
fun countryISOCode(refData: OCMReferenceData) =
|
||||
refData.countries.find { it.id == countryId }?.isoCode
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OCMConnection(
|
||||
@Json(name = "ConnectionTypeID") val connectionTypeId: Long,
|
||||
@Json(name = "CurrentTypeID") val currentTypeId: Long?,
|
||||
@Json(name = "Amps") val amps: Int?,
|
||||
@Json(name = "Voltage") val voltage: Int?,
|
||||
@Json(name = "PowerKW") val power: Double?,
|
||||
@Json(name = "Quantity") val quantity: Int?,
|
||||
@Json(name = "Comments") val comments: String?,
|
||||
@Json(name = "StatusTypeID") val statusTypeId: Long?,
|
||||
@Json(name = "StatusType") val statusType: OCMStatusType?
|
||||
) {
|
||||
fun convert(refData: OCMReferenceData) = Chargepoint(
|
||||
convertConnectionTypeFromOCM(connectionTypeId, refData),
|
||||
power ?: 0.0,
|
||||
quantity ?: 1
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun convertConnectionTypeFromOCM(id: Long, refData: OCMReferenceData): String {
|
||||
val title = refData.connectionTypes.find { it.id == id }?.title
|
||||
return when (id) {
|
||||
32L -> Chargepoint.CCS_TYPE_1
|
||||
33L -> Chargepoint.CCS_TYPE_2
|
||||
2L -> Chargepoint.CHADEMO
|
||||
16L -> Chargepoint.CEE_BLAU
|
||||
17L -> Chargepoint.CEE_ROT
|
||||
28L -> Chargepoint.SCHUKO
|
||||
8L -> Chargepoint.TESLA_ROADSTER_HPC
|
||||
27L -> Chargepoint.SUPERCHARGER
|
||||
25L -> Chargepoint.TYPE_2_SOCKET
|
||||
1036L -> Chargepoint.TYPE_2_PLUG
|
||||
1L -> Chargepoint.TYPE_1
|
||||
36L -> Chargepoint.TYPE_3
|
||||
26L -> Chargepoint.TYPE_3
|
||||
else -> title ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OCMReferenceData(
|
||||
@Json(name = "ConnectionTypes") val connectionTypes: List<OCMConnectionType>,
|
||||
@Json(name = "Countries") val countries: List<OCMCountry>,
|
||||
@Json(name = "Operators") val operators: List<OCMOperator>
|
||||
) : ReferenceData()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Entity
|
||||
data class OCMConnectionType(
|
||||
@Json(name = "ID") @PrimaryKey val id: Long,
|
||||
@Json(name = "Title") val title: String,
|
||||
@Json(name = "FormalName") val formalName: String?,
|
||||
@Json(name = "IsDiscontinued") val discontinued: Boolean?,
|
||||
@Json(name = "IsObsolete") val obsolete: Boolean?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Entity
|
||||
data class OCMCountry(
|
||||
@Json(name = "ID") @PrimaryKey val id: Long,
|
||||
@Json(name = "ISOCode") val isoCode: String,
|
||||
@Json(name = "ContinentCode") val continentCode: String?,
|
||||
@Json(name = "Title") val title: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OCMDataProvider(
|
||||
@Json(name = "ID") val id: Long,
|
||||
@Json(name = "WebsiteURL") val websiteUrl: String?,
|
||||
@Json(name = "Title") val title: String,
|
||||
@Json(name = "License") val license: String?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Entity
|
||||
data class OCMOperator(
|
||||
@Json(name = "ID") @PrimaryKey val id: Long,
|
||||
@Json(name = "WebsiteURL") val websiteUrl: String?,
|
||||
@Json(name = "Title") val title: String,
|
||||
@Json(name = "ContactEmail") val contactEmail: String?,
|
||||
@Json(name = "PhonePrimaryContact") val contactTelephone1: String?,
|
||||
@Json(name = "PhoneSecondaryContact") val contactTelephone2: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OCMMediaItem(
|
||||
@Json(name = "ID") val id: Long,
|
||||
@Json(name = "ItemURL") val url: String,
|
||||
@Json(name = "ItemThumbnailURL") val thumbUrl: String,
|
||||
@Json(name = "IsVideo") val isVideo: Boolean,
|
||||
@Json(name = "IsExternalResource") val isExternalResource: Boolean,
|
||||
@Json(name = "Comment") val comment: String?
|
||||
) {
|
||||
fun convert(): ChargerPhoto? {
|
||||
if (isVideo or isExternalResource) return null
|
||||
|
||||
return OCMChargerPhotoAdapter(id.toString(), url, thumbUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OCMUserComment(
|
||||
@Json(name = "ID") val id: Long,
|
||||
@Json(name = "CommentTypeID") val commentTypeId: Long,
|
||||
@Json(name = "Comment") val comment: String,
|
||||
@Json(name = "UserName") val userName: String,
|
||||
@Json(name = "DateCreated") val dateCreated: ZonedDateTime
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OCMStatusType(
|
||||
@Json(name = "ID") val id: Long,
|
||||
@Json(name = "Title") val title: String
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
class OCMChargerPhotoAdapter(
|
||||
override val id: String,
|
||||
val largeUrl: String,
|
||||
val thumbUrl: String
|
||||
) : ChargerPhoto(id) {
|
||||
override fun getUrl(height: Int?, width: Int?, size: Int?): String {
|
||||
val maxSize = size ?: max(height, width)
|
||||
val mediumUrl = thumbUrl.replace(".thmb.", ".medi.")
|
||||
return when (maxSize) {
|
||||
0 -> mediumUrl
|
||||
in 1..100 -> thumbUrl
|
||||
in 101..400 -> mediumUrl
|
||||
else -> largeUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,17 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.ChargepriceAdapter
|
||||
import net.vonforst.evmap.adapter.CheckableChargepriceCarAdapter
|
||||
import net.vonforst.evmap.adapter.CheckableConnectorAdapter
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.databinding.FragmentChargepriceBinding
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.model.ReferenceData
|
||||
import net.vonforst.evmap.viewmodel.ChargepriceViewModel
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
@@ -51,7 +57,7 @@ class ChargepriceFragment : DialogFragment() {
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
): View {
|
||||
binding = DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.fragment_chargeprice, container, false
|
||||
@@ -87,13 +93,26 @@ class ChargepriceFragment : DialogFragment() {
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
val jsonAdapter = GoingElectricApi.moshi.adapter(ChargeLocation::class.java)
|
||||
val charger = jsonAdapter.fromJson(requireArguments().getString(ARG_CHARGER)!!)!!
|
||||
val charger = requireArguments().getParcelable<ChargeLocation>(ARG_CHARGER)!!
|
||||
val dataSource = requireArguments().getString(ARG_DATASOURCE)!!
|
||||
vm.charger.value = charger
|
||||
vm.dataSource.value = dataSource
|
||||
if (vm.chargepoint.value == null) {
|
||||
vm.chargepoint.value = charger.chargepointsMerged.get(0)
|
||||
}
|
||||
|
||||
val vehicleAdapter = CheckableChargepriceCarAdapter()
|
||||
binding.vehicleSelection.adapter = vehicleAdapter
|
||||
val vehicleObserver: Observer<ChargepriceCar> = Observer {
|
||||
vehicleAdapter.setCheckedItem(it)
|
||||
}
|
||||
vm.vehicle.observe(viewLifecycleOwner, vehicleObserver)
|
||||
vehicleAdapter.onCheckedItemChangedListener = {
|
||||
vm.vehicle.removeObserver(vehicleObserver)
|
||||
vm.vehicle.value = it
|
||||
vm.vehicle.observe(viewLifecycleOwner, vehicleObserver)
|
||||
}
|
||||
|
||||
val chargepriceAdapter = ChargepriceAdapter().apply {
|
||||
onClickListener = {
|
||||
(requireActivity() as MapsActivity).openUrl(it.url)
|
||||
@@ -131,8 +150,9 @@ class ChargepriceFragment : DialogFragment() {
|
||||
vm.chargepoint.observe(viewLifecycleOwner, observer)
|
||||
}
|
||||
|
||||
vm.vehicleCompatibleConnectors.observe(viewLifecycleOwner) {
|
||||
connectorsAdapter.enabledConnectors = it
|
||||
vm.vehicleCompatibleConnectors.observe(viewLifecycleOwner) { plugs ->
|
||||
connectorsAdapter.enabledConnectors =
|
||||
plugs?.flatMap { plug -> equivalentPlugTypes(plug) }
|
||||
}
|
||||
|
||||
binding.connectorsList.apply {
|
||||
@@ -198,13 +218,25 @@ class ChargepriceFragment : DialogFragment() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ARG_CHARGER = "charger"
|
||||
const val ARG_CHARGER = "charger"
|
||||
const val ARG_DATASOURCE = "datasource"
|
||||
|
||||
fun showCharger(charger: ChargeLocation): Bundle {
|
||||
fun showCharger(
|
||||
charger: ChargeLocation,
|
||||
dataSource: Class<ChargepointApi<ReferenceData>>
|
||||
): Bundle {
|
||||
return Bundle().apply {
|
||||
putString(
|
||||
putParcelable(
|
||||
ARG_CHARGER,
|
||||
GoingElectricApi.moshi.adapter(ChargeLocation::class.java).toJson(charger)
|
||||
charger
|
||||
)
|
||||
putString(
|
||||
ARG_DATASOURCE,
|
||||
when (dataSource) {
|
||||
GoingElectricApiWrapper::class.java -> "going_electric"
|
||||
OpenChargeMapApiWrapper::class.java -> "open_charge_map"
|
||||
else -> throw IllegalArgumentException("unsupported data source")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import net.vonforst.evmap.databinding.DialogDataSourceSelectBinding
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import java.util.*
|
||||
|
||||
class DataSourceSelectDialog : AppCompatDialogFragment() {
|
||||
private lateinit var binding: DialogDataSourceSelectBinding
|
||||
var okListener: ((String) -> Unit)? = null
|
||||
|
||||
companion object {
|
||||
fun getInstance(
|
||||
cancelEnabled: Boolean
|
||||
): DataSourceSelectDialog {
|
||||
val dialog = DataSourceSelectDialog()
|
||||
dialog.arguments = args(cancelEnabled)
|
||||
return dialog
|
||||
}
|
||||
|
||||
fun args(cancelEnabled: Boolean) = Bundle().apply {
|
||||
putBoolean("cancel_enabled", cancelEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = DialogDataSourceSelectBinding.inflate(inflater, container, false)
|
||||
prefs = PreferenceDataSource(requireContext())
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
dialog?.window?.setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val args = requireArguments()
|
||||
binding.btnCancel.visibility =
|
||||
if (args.getBoolean("cancel_enabled")) View.VISIBLE else View.GONE
|
||||
|
||||
if (prefs.dataSourceSet) {
|
||||
when (prefs.dataSource) {
|
||||
"goingelectric" -> binding.rgDataSource.rbGoingElectric.isChecked = true
|
||||
"openchargemap" -> binding.rgDataSource.rbOpenChargeMap.isChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
binding.btnCancel.setOnClickListener {
|
||||
dismiss()
|
||||
}
|
||||
binding.btnOK.setOnClickListener {
|
||||
val result = if (binding.rgDataSource.rbGoingElectric.isChecked) {
|
||||
"goingelectric"
|
||||
} else if (binding.rgDataSource.rbOpenChargeMap.isChecked) {
|
||||
"openchargemap"
|
||||
} else {
|
||||
return@setOnClickListener
|
||||
}
|
||||
prefs.dataSource = result
|
||||
prefs.filterStatus = FILTERS_DISABLED
|
||||
okListener?.let { listener ->
|
||||
listener(result)
|
||||
}
|
||||
prefs.dataSourceSet = true
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,13 @@ package net.vonforst.evmap.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
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.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
@@ -14,20 +17,29 @@ import androidx.fragment.app.viewModels
|
||||
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.car2go.maps.model.LatLng
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.mapzen.android.lost.api.LocationServices
|
||||
import com.mapzen.android.lost.api.LostApiClient
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.DataBindingAdapter
|
||||
import net.vonforst.evmap.adapter.FavoritesAdapter
|
||||
import net.vonforst.evmap.databinding.FragmentFavoritesBinding
|
||||
import net.vonforst.evmap.databinding.ItemFavoriteBinding
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.viewmodel.FavoritesViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
|
||||
class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
private lateinit var binding: FragmentFavoritesBinding
|
||||
private lateinit var locationClient: LostApiClient
|
||||
private var toDelete: ChargeLocation? = null
|
||||
private var deleteSnackbar: Snackbar? = null
|
||||
private lateinit var adapter: FavoritesAdapter
|
||||
|
||||
private val vm: FavoritesViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory {
|
||||
@@ -66,13 +78,15 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
val favAdapter = FavoritesAdapter(vm).apply {
|
||||
adapter = FavoritesAdapter(onDelete = {
|
||||
delete(it.charger)
|
||||
}).apply {
|
||||
onClickListener = {
|
||||
navController.navigate(R.id.action_favs_to_map, MapFragment.showCharger(it.charger))
|
||||
}
|
||||
}
|
||||
binding.favsList.apply {
|
||||
adapter = favAdapter
|
||||
adapter = this@FavoritesFragment.adapter
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
addItemDecoration(
|
||||
@@ -81,6 +95,7 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
)
|
||||
)
|
||||
}
|
||||
createTouchHelper().attachToRecyclerView(binding.favsList)
|
||||
|
||||
locationClient.connect()
|
||||
}
|
||||
@@ -102,4 +117,143 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
override fun onConnectionSuspended() {
|
||||
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (locationClient.isConnected) {
|
||||
locationClient.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(fav: ChargeLocation) {
|
||||
val position = vm.listData.value?.indexOfFirst { it.charger == fav } ?: return
|
||||
// if there is already a profile to delete, delete it now
|
||||
actuallyDelete()
|
||||
deleteSnackbar?.dismiss()
|
||||
|
||||
toDelete = fav
|
||||
|
||||
view?.let {
|
||||
val snackbar = Snackbar.make(
|
||||
it,
|
||||
getString(R.string.deleted_filterprofile, fav.name),
|
||||
Snackbar.LENGTH_LONG
|
||||
).setAction(R.string.undo) {
|
||||
toDelete = null
|
||||
adapter.notifyItemChanged(position)
|
||||
}.addCallback(object : Snackbar.Callback() {
|
||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
||||
// if undo was not clicked, actually delete
|
||||
if (event == DISMISS_EVENT_TIMEOUT || event == DISMISS_EVENT_SWIPE) {
|
||||
actuallyDelete()
|
||||
}
|
||||
}
|
||||
})
|
||||
deleteSnackbar = snackbar
|
||||
snackbar.show()
|
||||
} ?: run {
|
||||
actuallyDelete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun actuallyDelete() {
|
||||
toDelete?.let { vm.deleteFavorite(it) }
|
||||
toDelete = null
|
||||
}
|
||||
|
||||
private fun createTouchHelper(): ItemTouchHelper {
|
||||
return ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
|
||||
0,
|
||||
ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT
|
||||
) {
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
val fav = vm.favorites.value?.find { it.id == viewHolder.itemId }
|
||||
fav?.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 ItemFavoriteBinding
|
||||
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 ItemFavoriteBinding
|
||||
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 ItemFavoriteBinding
|
||||
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 ItemFavoriteBinding
|
||||
getDefaultUIUtil().onDraw(
|
||||
c, recyclerView, binding.foreground, dX, dY,
|
||||
actionState, isCurrentlyActive
|
||||
)
|
||||
} else {
|
||||
super.onChildDraw(
|
||||
c,
|
||||
recyclerView,
|
||||
viewHolder,
|
||||
dX,
|
||||
dY,
|
||||
actionState,
|
||||
isCurrentlyActive
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,19 +19,11 @@ 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 = {
|
||||
viewModelFactory {
|
||||
FilterViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.goingelectric_key)
|
||||
)
|
||||
}
|
||||
})
|
||||
private val vm: FilterViewModel by viewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.app.SharedElementCallback
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
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
|
||||
import net.vonforst.evmap.api.goingelectric.ChargerPhoto
|
||||
import net.vonforst.evmap.databinding.FragmentGalleryBinding
|
||||
import net.vonforst.evmap.viewmodel.GalleryViewModel
|
||||
|
||||
|
||||
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,
|
||||
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 startingPosition: Int = 0
|
||||
private var currentPosition: Int = 0
|
||||
private lateinit var galleryAdapter: GalleryAdapter
|
||||
private var currentPage: TouchImageView? = null
|
||||
private val galleryVm: GalleryViewModel by activityViewModels()
|
||||
|
||||
private val backPressedCallback = object :
|
||||
OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
val image = currentPage
|
||||
if (image != null && image.currentZoom !in 0.95f..1.05f) {
|
||||
image.setZoomAnimated(1f, 0.5f, 0.5f)
|
||||
} else {
|
||||
galleryVm.galleryPosition.value = currentPosition
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.fragment_gallery, container, false
|
||||
)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
val args = requireArguments()
|
||||
startingPosition = args.getInt(EXTRA_POSITION, 0)
|
||||
currentPosition =
|
||||
savedInstanceState?.getInt(SAVED_CURRENT_PAGE_POSITION) ?: startingPosition
|
||||
|
||||
galleryAdapter =
|
||||
GalleryAdapter(
|
||||
requireContext(), detailView = true, pageToLoad = currentPosition,
|
||||
imageCacheKey = args.getParcelable(EXTRA_IMAGE_CACHE_KEY)
|
||||
) {
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
binding.gallery.setPageTransformer { page, _ ->
|
||||
val v = page as TouchImageView
|
||||
currentPage = v
|
||||
}
|
||||
binding.gallery.adapter = galleryAdapter
|
||||
binding.photos = args.getParcelableArrayList(EXTRA_PHOTOS)
|
||||
|
||||
binding.gallery.post {
|
||||
binding.gallery.setCurrentItem(currentPosition, false)
|
||||
binding.gallery.registerOnPageChangeCallback(object :
|
||||
ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
currentPosition = position
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
sharedElementEnterTransition = TransitionInflater.from(context)
|
||||
.inflateTransition(R.transition.image_shared_element_transition)
|
||||
sharedElementReturnTransition = TransitionInflater.from(context)
|
||||
.inflateTransition(R.transition.image_shared_element_transition)
|
||||
setEnterSharedElementCallback(enterElementCallback)
|
||||
if (savedInstanceState == null) {
|
||||
postponeEnterTransition();
|
||||
}
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
backPressedCallback
|
||||
)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putInt(SAVED_CURRENT_PAGE_POSITION, currentPosition)
|
||||
}
|
||||
|
||||
private val enterElementCallback: SharedElementCallback = object : SharedElementCallback() {
|
||||
override fun onMapSharedElements(
|
||||
names: MutableList<String>,
|
||||
sharedElements: MutableMap<String, View>
|
||||
) {
|
||||
val currentPage = currentPage ?: return
|
||||
sharedElements[names[0]] = currentPage
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,8 +10,8 @@ import android.graphics.Color
|
||||
import android.location.Geocoder
|
||||
import android.location.Location
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.view.*
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
@@ -19,7 +19,6 @@ import androidx.annotation.RequiresPermission
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
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
|
||||
@@ -31,14 +30,16 @@ 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
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.transition.TransitionInflater
|
||||
import androidx.transition.TransitionManager
|
||||
import coil.load
|
||||
import coil.memory.MemoryCache
|
||||
import coil.size.OriginalSize
|
||||
import coil.size.SizeResolver
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.MapFragment
|
||||
import com.car2go.maps.OnMapReadyCallback
|
||||
@@ -57,6 +58,7 @@ import com.mapzen.android.lost.api.LocationListener
|
||||
import com.mapzen.android.lost.api.LocationRequest
|
||||
import com.mapzen.android.lost.api.LocationServices
|
||||
import com.mapzen.android.lost.api.LostApiClient
|
||||
import com.stfalcon.imageviewer.StfalconImageViewer
|
||||
import io.michaelrocks.bimap.HashBiMap
|
||||
import io.michaelrocks.bimap.MutableBiMap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -66,12 +68,11 @@ import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.adapter.ConnectorAdapter
|
||||
import net.vonforst.evmap.adapter.DetailsAdapter
|
||||
import net.vonforst.evmap.adapter.GalleryAdapter
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocationCluster
|
||||
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.autocomplete.handleAutocompleteResult
|
||||
import net.vonforst.evmap.autocomplete.launchAutocomplete
|
||||
import net.vonforst.evmap.databinding.FragmentMapBinding
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||
import net.vonforst.evmap.ui.ClusterIconGenerator
|
||||
@@ -91,14 +92,7 @@ const val ARG_LOCATION_NAME = "locationName"
|
||||
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback,
|
||||
LostApiClient.ConnectionCallbacks, LocationListener {
|
||||
private lateinit var binding: FragmentMapBinding
|
||||
private val vm: MapViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory {
|
||||
MapViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.goingelectric_key)
|
||||
)
|
||||
}
|
||||
})
|
||||
private val vm: MapViewModel by viewModels()
|
||||
private val galleryVm: GalleryViewModel by activityViewModels()
|
||||
private var mapFragment: MapFragment? = null
|
||||
private var map: AnyMap? = null
|
||||
@@ -181,7 +175,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
postponeEnterTransition()
|
||||
|
||||
binding.root.setOnApplyWindowInsetsListener { _, insets ->
|
||||
binding.detailAppBar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
@@ -202,7 +195,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
insets
|
||||
}
|
||||
|
||||
setExitSharedElementCallback(reenterSharedElementCallback)
|
||||
exitTransition = TransitionInflater.from(requireContext())
|
||||
.inflateTransition(R.transition.map_exit_transition)
|
||||
|
||||
@@ -221,27 +213,21 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
binding.detailAppBar.toolbar.inflateMenu(R.menu.detail)
|
||||
favToggle = binding.detailAppBar.toolbar.menu.findItem(R.id.menu_fav)
|
||||
binding.detailAppBar.toolbar.menu.findItem(R.id.menu_edit).title =
|
||||
getString(R.string.edit_at_datasource, vm.apiName)
|
||||
|
||||
setupObservers()
|
||||
setupClickListeners()
|
||||
setupAdapters()
|
||||
(activity as? MapsActivity)?.setSupportActionBar(binding.toolbar)
|
||||
|
||||
val prefs = PreferenceDataSource(requireContext())
|
||||
val navController = findNavController()
|
||||
if (!prefs.welcomeDialogShown) {
|
||||
try {
|
||||
navController.navigate(R.id.action_map_to_welcome)
|
||||
} catch (ignored: IllegalArgumentException) {
|
||||
// when there is already another navigation going on
|
||||
}
|
||||
} else if (!prefs.update060AndroidAutoDialogShown) {
|
||||
/*if (!prefs.update060AndroidAutoDialogShown) {
|
||||
try {
|
||||
navController.navigate(R.id.action_map_to_update_060_androidauto)
|
||||
} catch (ignored: IllegalArgumentException) {
|
||||
// when there is already another navigation going on
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@@ -294,17 +280,17 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.layers.btnClose.setOnClickListener {
|
||||
closeLayersMenu()
|
||||
}
|
||||
binding.detailView.goingelectricButton.setOnClickListener {
|
||||
binding.detailView.sourceButton.setOnClickListener {
|
||||
val charger = vm.charger.value?.data
|
||||
if (charger != null) {
|
||||
(activity as? MapsActivity)?.openUrl("https:${charger.url}")
|
||||
(activity as? MapsActivity)?.openUrl(charger.url)
|
||||
}
|
||||
}
|
||||
binding.detailView.btnChargeprice.setOnClickListener {
|
||||
val charger = vm.charger.value?.data ?: return@setOnClickListener
|
||||
findNavController().navigate(
|
||||
R.id.action_map_to_chargepriceFragment,
|
||||
ChargepriceFragment.showCharger(charger)
|
||||
ChargepriceFragment.showCharger(charger, vm.apiType)
|
||||
)
|
||||
}
|
||||
binding.detailView.topPart.setOnClickListener {
|
||||
@@ -325,19 +311,22 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
R.id.menu_share -> {
|
||||
val charger = vm.charger.value?.data
|
||||
if (charger != null) {
|
||||
(activity as? MapsActivity)?.shareUrl("https:${charger.url}")
|
||||
(activity as? MapsActivity)?.shareUrl(charger.url)
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.menu_edit -> {
|
||||
val charger = vm.charger.value?.data
|
||||
if (charger != null) {
|
||||
(activity as? MapsActivity)?.openUrl("https:${charger.url}edit/")
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.edit_on_goingelectric_info,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
if (charger?.editUrl != null) {
|
||||
(activity as? MapsActivity)?.openUrl(charger.editUrl)
|
||||
if (vm.apiType == GoingElectricApiWrapper::class.java) {
|
||||
// instructions specific to GoingElectric
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.edit_on_goingelectric_info,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -548,24 +537,35 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
|
||||
private fun setupAdapters() {
|
||||
var viewer: StfalconImageViewer<ChargerPhoto>? = null
|
||||
val galleryClickListener = object : GalleryAdapter.ItemClickListener {
|
||||
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, imageCacheKey),
|
||||
null,
|
||||
extras
|
||||
)
|
||||
|
||||
viewer = StfalconImageViewer.Builder(context, photos) { imageView, photo ->
|
||||
imageView.load(photo.getUrl(size = 1000)) {
|
||||
if (photo == photos[position] && imageCacheKey != null) {
|
||||
placeholderMemoryCacheKey(imageCacheKey)
|
||||
}
|
||||
size(SizeResolver(OriginalSize))
|
||||
allowHardware(false)
|
||||
}
|
||||
}
|
||||
.withTransitionFrom(view as ImageView)
|
||||
.withImageChangeListener {
|
||||
binding.gallery.layoutManager!!.scrollToPosition(it)
|
||||
binding.gallery.layoutManager!!.findViewByPosition(it)?.let {
|
||||
viewer?.updateTransitionImage(it as ImageView)
|
||||
}
|
||||
}
|
||||
.withStartPosition(position)
|
||||
.show()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
val galleryPosition = galleryVm.galleryPosition.value
|
||||
binding.gallery.apply {
|
||||
adapter = GalleryAdapter(context, galleryClickListener, pageToLoad = galleryPosition) {
|
||||
startPostponedEnterTransition()
|
||||
}
|
||||
adapter = GalleryAdapter(context, galleryClickListener)
|
||||
itemAnimator = null
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
|
||||
@@ -576,41 +576,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
setDrawable(ContextCompat.getDrawable(context, R.drawable.gallery_divider)!!)
|
||||
})
|
||||
}
|
||||
if (galleryPosition == null) {
|
||||
startPostponedEnterTransition()
|
||||
} else {
|
||||
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()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
binding.detailView.connectors.apply {
|
||||
adapter = ConnectorAdapter()
|
||||
@@ -629,7 +594,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
(activity as? MapsActivity)?.showLocation(charger)
|
||||
}
|
||||
R.drawable.ic_fault_report -> {
|
||||
(activity as? MapsActivity)?.openUrl("https:${charger.url}")
|
||||
(activity as? MapsActivity)?.openUrl(charger.url)
|
||||
}
|
||||
R.drawable.ic_payment -> {
|
||||
showPaymentMethodsDialog(charger)
|
||||
@@ -1122,23 +1087,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
return binding.root
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Map the first shared element name to the child ImageView.
|
||||
sharedElements[names[0]] = vh.itemView
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun showCharger(charger: ChargeLocation): Bundle {
|
||||
return Bundle().apply {
|
||||
@@ -1233,4 +1181,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
super.onPause()
|
||||
removeLocationUpdates()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (locationClient.isConnected) {
|
||||
locationClient.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,8 @@ class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
title: String,
|
||||
data: Map<String, String>,
|
||||
selected: Set<String>,
|
||||
commonChoices: Set<String>?
|
||||
commonChoices: Set<String>?,
|
||||
showAllButton: Boolean = true
|
||||
): MultiSelectDialog {
|
||||
val dialog = MultiSelectDialog()
|
||||
dialog.arguments = Bundle().apply {
|
||||
@@ -29,6 +30,7 @@ class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
putSerializable("data", HashMap(data))
|
||||
putSerializable("selected", HashSet(selected))
|
||||
if (commonChoices != null) putSerializable("commonChoices", HashSet(commonChoices))
|
||||
putBoolean("showAllButton", showAllButton)
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
@@ -66,12 +68,15 @@ class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
val commonChoices = if (args.containsKey("commonChoices")) {
|
||||
args.getSerializable("commonChoices") as HashSet<String>
|
||||
} else null
|
||||
val showAllButton = args.getBoolean("showAllButton")
|
||||
|
||||
binding.dialogTitle.text = title
|
||||
val adapter = Adapter()
|
||||
binding.list.adapter = adapter
|
||||
binding.list.layoutManager = LinearLayoutManager(view.context)
|
||||
|
||||
binding.btnAll.visibility = if (showAllButton) View.VISIBLE else View.INVISIBLE
|
||||
|
||||
items = data.entries.toList()
|
||||
.sortedBy { it.value.toLowerCase(Locale.getDefault()) }
|
||||
.sortedByDescending { commonChoices?.contains(it.key) == true }
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.*
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
class OnboardingFragment : Fragment() {
|
||||
private lateinit var binding: FragmentOnboardingBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentOnboardingBinding.inflate(inflater)
|
||||
|
||||
val adapter = OnboardingViewPagerAdapter(this)
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.pageIndicatorView.count = adapter.itemCount
|
||||
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
binding.pageIndicatorView.onPageScrollStateChanged(state)
|
||||
}
|
||||
|
||||
override fun onPageScrolled(
|
||||
position: Int,
|
||||
positionOffset: Float,
|
||||
positionOffsetPixels: Int
|
||||
) {
|
||||
binding.pageIndicatorView.onPageScrolled(
|
||||
position,
|
||||
positionOffset,
|
||||
positionOffsetPixels
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
binding.pageIndicatorView.selection = position
|
||||
}
|
||||
})
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
fun goToNext() {
|
||||
if (binding.viewPager.currentItem == 2) {
|
||||
findNavController().navigate(R.id.action_onboarding_to_map)
|
||||
} else {
|
||||
binding.viewPager.setCurrentItem(binding.viewPager.currentItem + 1, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OnboardingViewPagerAdapter(fragment: Fragment) :
|
||||
FragmentStateAdapter(fragment) {
|
||||
override fun getItemCount(): Int = 3
|
||||
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
0 -> WelcomeFragment()
|
||||
1 -> IconsFragment()
|
||||
2 -> DataSourceSelectFragment()
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
abstract class OnboardingPageFragment : Fragment() {
|
||||
lateinit var parent: OnboardingFragment
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
parent = parentFragment as OnboardingFragment
|
||||
}
|
||||
}
|
||||
|
||||
class WelcomeFragment : OnboardingPageFragment() {
|
||||
private lateinit var binding: FragmentOnboardingWelcomeBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentOnboardingWelcomeBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.btnGetStarted.setOnClickListener {
|
||||
parent.goToNext()
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.animationView.playAnimation()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
binding.animationView.progress = 0f
|
||||
}
|
||||
}
|
||||
|
||||
class IconsFragment : OnboardingPageFragment() {
|
||||
private lateinit var binding: FragmentOnboardingIconsBinding
|
||||
|
||||
val labels
|
||||
get() = listOf(
|
||||
binding.iconLabel1,
|
||||
binding.iconLabel2,
|
||||
binding.iconLabel3,
|
||||
binding.iconLabel4,
|
||||
binding.iconLabel5
|
||||
)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentOnboardingIconsBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.btnGetStarted.setOnClickListener {
|
||||
parent.goToNext()
|
||||
}
|
||||
labels.forEach { it.alpha = 0f }
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val animators = labels.flatMapIndexed { i, view ->
|
||||
listOf(
|
||||
ObjectAnimator.ofFloat(view, "translationY", -20f, 0f).apply {
|
||||
startDelay = 40L * i
|
||||
interpolator = DecelerateInterpolator()
|
||||
},
|
||||
ObjectAnimator.ofFloat(view, "alpha", 0f, 1f).apply {
|
||||
startDelay = 40L * i
|
||||
interpolator = DecelerateInterpolator()
|
||||
}
|
||||
)
|
||||
}
|
||||
AnimatorSet().apply {
|
||||
playTogether(animators)
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
labels.forEach { it.alpha = 0f }
|
||||
}
|
||||
}
|
||||
|
||||
class DataSourceSelectFragment : OnboardingPageFragment() {
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
private lateinit var binding: FragmentOnboardingDataSourceBinding
|
||||
|
||||
val animatedItems
|
||||
get() = listOf(
|
||||
binding.rgDataSource.rbGoingElectric,
|
||||
binding.rgDataSource.textView27,
|
||||
binding.rgDataSource.rbOpenChargeMap,
|
||||
binding.rgDataSource.textView28
|
||||
)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentOnboardingDataSourceBinding.inflate(inflater, container, false)
|
||||
prefs = PreferenceDataSource(requireContext())
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.btnGetStarted.visibility = View.INVISIBLE
|
||||
|
||||
for (rb in listOf(
|
||||
binding.rgDataSource.rbGoingElectric,
|
||||
binding.rgDataSource.rbOpenChargeMap
|
||||
)) {
|
||||
rb.setOnCheckedChangeListener { _, _ ->
|
||||
if (binding.btnGetStarted.visibility == View.INVISIBLE) {
|
||||
binding.btnGetStarted.visibility = View.VISIBLE
|
||||
ObjectAnimator.ofFloat(binding.btnGetStarted, "alpha", 0f, 1f).apply {
|
||||
interpolator = DecelerateInterpolator()
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.btnGetStarted.setOnClickListener {
|
||||
val result = if (binding.rgDataSource.rbGoingElectric.isChecked) {
|
||||
"goingelectric"
|
||||
} else if (binding.rgDataSource.rbOpenChargeMap.isChecked) {
|
||||
"openchargemap"
|
||||
} else {
|
||||
return@setOnClickListener
|
||||
}
|
||||
prefs.dataSource = result
|
||||
prefs.filterStatus = FILTERS_DISABLED
|
||||
prefs.dataSourceSet = true
|
||||
prefs.welcomeDialogShown = true
|
||||
parent.goToNext()
|
||||
}
|
||||
animatedItems.forEach { it.alpha = 0f }
|
||||
}
|
||||
|
||||
@SuppressLint("Recycle")
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val animators = animatedItems.flatMapIndexed { i, view ->
|
||||
listOf(
|
||||
ObjectAnimator.ofFloat(view, "translationY", 20f, 0f).apply {
|
||||
startDelay = 40L * i
|
||||
interpolator = DecelerateInterpolator()
|
||||
},
|
||||
ObjectAnimator.ofFloat(view, "alpha", 0f, 1f).apply {
|
||||
startDelay = 40L * i
|
||||
interpolator = DecelerateInterpolator()
|
||||
}
|
||||
)
|
||||
}
|
||||
AnimatorSet().apply {
|
||||
playTogether(animators)
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
animatedItems.forEach { it.alpha = 0f }
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
@@ -32,20 +31,13 @@ class SettingsFragment : PreferenceFragmentCompat(),
|
||||
}
|
||||
})
|
||||
|
||||
private lateinit var myVehiclePreference: ListPreference
|
||||
private lateinit var myVehiclePreference: MultiSelectListPreference
|
||||
private lateinit var myTariffsPreference: MultiSelectListPreference
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
prefs = PreferenceDataSource(requireContext())
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
myVehiclePreference = findPreference("chargeprice_my_vehicle")!!
|
||||
myVehiclePreference.isEnabled = false
|
||||
vm.vehicles.observe(viewLifecycleOwner) { res ->
|
||||
@@ -55,8 +47,7 @@ class SettingsFragment : PreferenceFragmentCompat(),
|
||||
myVehiclePreference.entries =
|
||||
sortedCars.map { "${it.brand} ${it.name}" }.toTypedArray()
|
||||
myVehiclePreference.isEnabled = true
|
||||
myVehiclePreference.summary = cars.find { it.id == prefs.chargepriceMyVehicle }
|
||||
?.let { "${it.brand} ${it.name}" }
|
||||
updateMyVehiclesSummary()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +79,17 @@ class SettingsFragment : PreferenceFragmentCompat(),
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMyVehiclesSummary() {
|
||||
vm.vehicles.value?.data?.let { cars ->
|
||||
val vehicles = cars.filter { it.id in prefs.chargepriceMyVehicles }
|
||||
val summary = vehicles.map {
|
||||
"${it.brand} ${it.name}"
|
||||
}.joinToString(", ")
|
||||
myVehiclePreference.summary = summary
|
||||
// TODO: prefs.chargepriceMyVehicleDcChargeports = it.dcChargePorts
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings, rootKey)
|
||||
}
|
||||
@@ -110,13 +112,7 @@ class SettingsFragment : PreferenceFragmentCompat(),
|
||||
updateNightMode(prefs)
|
||||
}
|
||||
"chargeprice_my_vehicle" -> {
|
||||
vm.vehicles.value?.data?.let { cars ->
|
||||
val vehicle = cars.find { it.id == prefs.chargepriceMyVehicle }
|
||||
vehicle?.let {
|
||||
myVehiclePreference.summary = "${it.brand} ${it.name}"
|
||||
prefs.chargepriceMyVehicleDcChargeports = it.dcChargePorts
|
||||
}
|
||||
}
|
||||
updateMyVehiclesSummary()
|
||||
}
|
||||
"chargeprice_my_tariffs" -> {
|
||||
updateMyTariffsSummary()
|
||||
@@ -127,6 +123,13 @@ class SettingsFragment : PreferenceFragmentCompat(),
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
|
||||
val navController = findNavController()
|
||||
val toolbar = requireView().findViewById(R.id.toolbar) as Toolbar
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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 {
|
||||
val prefs = PreferenceDataSource(requireContext())
|
||||
prefs.welcomeDialogShown = true
|
||||
prefs.update060AndroidAutoDialogShown = true
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
dialog?.window?.setLayout(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
}
|
||||
}
|
||||
328
app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt
Normal file
328
app/src/main/java/net/vonforst/evmap/model/ChargersModel.kt
Normal file
@@ -0,0 +1,328 @@
|
||||
package net.vonforst.evmap.model
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.StringProvider
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import java.time.DayOfWeek
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
|
||||
sealed class ChargepointListItem
|
||||
|
||||
@Entity
|
||||
@Parcelize
|
||||
data class ChargeLocation(
|
||||
@PrimaryKey val id: Long,
|
||||
val name: String,
|
||||
@Embedded val coordinates: Coordinate,
|
||||
@Embedded val address: Address,
|
||||
val chargepoints: List<Chargepoint>,
|
||||
val network: String?,
|
||||
val url: String,
|
||||
val editUrl: String?,
|
||||
@Embedded(prefix = "fault_report_") val faultReport: FaultReport?,
|
||||
val verified: Boolean,
|
||||
val barrierFree: Boolean?,
|
||||
// only shown in details:
|
||||
val operator: String?,
|
||||
val generalInformation: String?,
|
||||
val amenities: String?,
|
||||
val locationDescription: String?,
|
||||
val photos: List<ChargerPhoto>?,
|
||||
val chargecards: List<ChargeCardId>?,
|
||||
@Embedded val openinghours: OpeningHours?,
|
||||
@Embedded val cost: Cost?,
|
||||
val license: String?,
|
||||
@Embedded(prefix = "chargeprice") val chargepriceData: ChargepriceData?
|
||||
) : ChargepointListItem(), Equatable, Parcelable {
|
||||
/**
|
||||
* maximum power available from this charger.
|
||||
*/
|
||||
val maxPower: Double
|
||||
get() {
|
||||
return maxPower()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum power available from certain connectors of this charger.
|
||||
*/
|
||||
fun maxPower(connectors: Set<String>? = null): Double {
|
||||
return chargepoints.filter { connectors?.contains(it.type) ?: true }
|
||||
.map { it.power }.maxOrNull() ?: 0.0
|
||||
}
|
||||
|
||||
fun isMulti(filteredConnectors: Set<String>? = null): Boolean {
|
||||
var chargepoints = chargepointsMerged
|
||||
.filter { filteredConnectors?.contains(it.type) ?: true }
|
||||
if (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 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges chargepoints if they have the same plug and power
|
||||
*
|
||||
* This occurs e.g. for Type2 sockets and plugs, which are distinct on the GE website, but not
|
||||
* separable in the API
|
||||
*/
|
||||
val chargepointsMerged: List<Chargepoint>
|
||||
get() {
|
||||
val variants = chargepoints.distinctBy { it.power to it.type }
|
||||
return variants.map { variant ->
|
||||
val count = chargepoints
|
||||
.filter { it.type == variant.type && it.power == variant.power }
|
||||
.sumBy { it.count }
|
||||
Chargepoint(variant.type, variant.power, count)
|
||||
}
|
||||
}
|
||||
|
||||
val totalChargepoints: Int
|
||||
get() = chargepoints.sumBy { it.count }
|
||||
|
||||
fun formatChargepoints(sp: StringProvider): String {
|
||||
return chargepointsMerged.map {
|
||||
"${it.count} × ${nameForPlugType(sp, it.type)} ${it.formatPower()}"
|
||||
}.joinToString(" · ")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional data needed for the Chargeprice implementation
|
||||
*/
|
||||
@Parcelize
|
||||
data class ChargepriceData(
|
||||
val country: String?,
|
||||
val network: String?,
|
||||
val plugTypes: List<String>?
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class Cost(
|
||||
val freecharging: Boolean? = null,
|
||||
val freeparking: Boolean? = null,
|
||||
val descriptionShort: String? = null,
|
||||
val descriptionLong: String? = null
|
||||
) : Parcelable {
|
||||
fun getStatusText(ctx: Context, emoji: Boolean = false): CharSequence {
|
||||
if (freecharging != null && freeparking != null) {
|
||||
val charging =
|
||||
if (freecharging) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
|
||||
val parking =
|
||||
if (freeparking) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
|
||||
return if (emoji) {
|
||||
"⚡ $charging · \uD83C\uDD7F️ $parking"
|
||||
} else {
|
||||
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail, charging, parking), 0)
|
||||
}
|
||||
} else if (descriptionShort != null) {
|
||||
return descriptionShort
|
||||
} else if (descriptionLong != null) {
|
||||
return descriptionLong
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class OpeningHours(
|
||||
val twentyfourSeven: Boolean,
|
||||
val description: String?,
|
||||
@Embedded val days: OpeningHoursDays?
|
||||
) : Parcelable {
|
||||
val isEmpty: Boolean
|
||||
get() = description == "Leider noch keine Informationen zu Öffnungszeiten vorhanden."
|
||||
&& days == null && !twentyfourSeven
|
||||
|
||||
fun getStatusText(ctx: Context): CharSequence {
|
||||
if (twentyfourSeven) {
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.open_247), 0)
|
||||
} else if (days != null) {
|
||||
val hours = days.getHoursForDate(LocalDate.now())
|
||||
if (hours.start == null || hours.end == null) {
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
|
||||
}
|
||||
|
||||
val now = LocalTime.now()
|
||||
if (hours.start.isBefore(now) && hours.end.isAfter(now)) {
|
||||
return HtmlCompat.fromHtml(
|
||||
ctx.getString(
|
||||
R.string.open_closesat,
|
||||
hours.end.toString()
|
||||
), 0
|
||||
)
|
||||
} else if (hours.end.isBefore(now)) {
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
|
||||
} else {
|
||||
return HtmlCompat.fromHtml(
|
||||
ctx.getString(
|
||||
R.string.closed_opensat,
|
||||
hours.start.toString()
|
||||
), 0
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class OpeningHoursDays(
|
||||
@Embedded(prefix = "mo") val monday: Hours,
|
||||
@Embedded(prefix = "tu") val tuesday: Hours,
|
||||
@Embedded(prefix = "we") val wednesday: Hours,
|
||||
@Embedded(prefix = "th") val thursday: Hours,
|
||||
@Embedded(prefix = "fr") val friday: Hours,
|
||||
@Embedded(prefix = "sa") val saturday: Hours,
|
||||
@Embedded(prefix = "su") val sunday: Hours,
|
||||
@Embedded(prefix = "ho") val holiday: Hours
|
||||
) : Parcelable {
|
||||
fun getHoursForDate(date: LocalDate): Hours {
|
||||
// TODO: check for holidays
|
||||
return getHoursForDayOfWeek(date.dayOfWeek)
|
||||
}
|
||||
|
||||
fun getHoursForDayOfWeek(dayOfWeek: DayOfWeek?): Hours {
|
||||
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
|
||||
return when (dayOfWeek) {
|
||||
DayOfWeek.MONDAY -> monday
|
||||
DayOfWeek.TUESDAY -> tuesday
|
||||
DayOfWeek.WEDNESDAY -> wednesday
|
||||
DayOfWeek.THURSDAY -> thursday
|
||||
DayOfWeek.FRIDAY -> friday
|
||||
DayOfWeek.SATURDAY -> saturday
|
||||
DayOfWeek.SUNDAY -> sunday
|
||||
null -> holiday
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Hours(
|
||||
val start: LocalTime?,
|
||||
val end: LocalTime?
|
||||
) : Parcelable {
|
||||
override fun toString(): String {
|
||||
if (start != null && end != null) {
|
||||
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
return "${start.format(fmt)} - ${end.format(fmt)}"
|
||||
} else {
|
||||
return "closed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ChargerPhoto(open val id: String) : Parcelable {
|
||||
abstract fun getUrl(height: Int? = null, width: Int? = null, size: Int? = null): String
|
||||
}
|
||||
|
||||
data class ChargeLocationCluster(
|
||||
val clusterCount: Int,
|
||||
val coordinates: Coordinate
|
||||
) : ChargepointListItem()
|
||||
|
||||
@Parcelize
|
||||
data class Coordinate(val lat: Double, val lng: Double) : Parcelable {
|
||||
fun formatDMS(): String {
|
||||
return "${dms(lat, false)}, ${dms(lng, true)}"
|
||||
}
|
||||
|
||||
private fun dms(value: Double, lon: Boolean): String {
|
||||
val hemisphere = if (lon) {
|
||||
if (value >= 0) "E" else "W"
|
||||
} else {
|
||||
if (value >= 0) "N" else "S"
|
||||
}
|
||||
val d = abs(value)
|
||||
val degrees = floor(d).toInt()
|
||||
val minutes = floor((d - degrees) * 60).toInt()
|
||||
val seconds = ((d - degrees) * 60 - minutes) * 60
|
||||
return "%d°%02d'%02.1f\"%s".format(Locale.ENGLISH, degrees, minutes, seconds, hemisphere)
|
||||
}
|
||||
|
||||
fun formatDecimal(): String {
|
||||
return "%.6f, %.6f".format(Locale.ENGLISH, lat, lng)
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Address(
|
||||
val city: String?,
|
||||
val country: String?,
|
||||
val postcode: String?,
|
||||
val street: String?
|
||||
) : Parcelable {
|
||||
override fun toString(): String {
|
||||
return "${street ?: ""}, ${postcode ?: ""} ${city ?: ""}"
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Chargepoint(val type: String, val power: Double, val count: Int) : Equatable,
|
||||
Parcelable {
|
||||
fun formatPower(): String {
|
||||
val powerFmt = if (power - power.toInt() == 0.0) {
|
||||
"%.0f".format(power)
|
||||
} else {
|
||||
"%.1f".format(power)
|
||||
}
|
||||
return "$powerFmt kW"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE_1 = "Type 1"
|
||||
const val TYPE_2_UNKNOWN = "Type 2 (either plug or socket)"
|
||||
const val TYPE_2_SOCKET = "Type 2 socket"
|
||||
const val TYPE_2_PLUG = "Type 2 plug"
|
||||
const val TYPE_3 = "Type 3"
|
||||
const val CCS_TYPE_2 = "CCS Type 2"
|
||||
const val CCS_TYPE_1 = "CCS Type 1"
|
||||
const val CCS_UNKNOWN = "CCS (either Type 1 or Type 2)"
|
||||
const val SCHUKO = "Schuko"
|
||||
const val CHADEMO = "CHAdeMO"
|
||||
const val SUPERCHARGER = "Tesla Supercharger"
|
||||
const val CEE_BLAU = "CEE Blau"
|
||||
const val CEE_ROT = "CEE Rot"
|
||||
const val TESLA_ROADSTER_HPC = "Tesla HPC"
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class FaultReport(val created: Instant?, val description: String?) : Parcelable
|
||||
|
||||
@Entity
|
||||
data class ChargeCard(
|
||||
@PrimaryKey val id: Long,
|
||||
val name: String,
|
||||
val url: String
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargeCardId(
|
||||
val id: Long
|
||||
) : Parcelable
|
||||
132
app/src/main/java/net/vonforst/evmap/model/FiltersModel.kt
Normal file
132
app/src/main/java/net/vonforst/evmap/model/FiltersModel.kt
Normal file
@@ -0,0 +1,132 @@
|
||||
package net.vonforst.evmap.model
|
||||
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.storage.FilterProfile
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
sealed class Filter<out T : FilterValue> : Equatable {
|
||||
abstract val name: String
|
||||
abstract val key: String
|
||||
abstract val valueClass: KClass<out T>
|
||||
abstract fun defaultValue(): T
|
||||
}
|
||||
|
||||
data class BooleanFilter(override val name: String, override val key: String) :
|
||||
Filter<BooleanFilterValue>() {
|
||||
override val valueClass: KClass<BooleanFilterValue> = BooleanFilterValue::class
|
||||
override fun defaultValue() = BooleanFilterValue(key, false)
|
||||
}
|
||||
|
||||
data class MultipleChoiceFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val choices: Map<String, String>,
|
||||
val commonChoices: Set<String>? = null,
|
||||
val manyChoices: Boolean = false
|
||||
) : Filter<MultipleChoiceFilterValue>() {
|
||||
override val valueClass: KClass<MultipleChoiceFilterValue> = MultipleChoiceFilterValue::class
|
||||
override fun defaultValue() = MultipleChoiceFilterValue(key, mutableSetOf(), true)
|
||||
}
|
||||
|
||||
data class SliderFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val max: Int,
|
||||
val min: Int = 0,
|
||||
val mapping: ((Int) -> Int) = { it },
|
||||
val inverseMapping: ((Int) -> Int) = { it },
|
||||
val unit: String? = ""
|
||||
) : Filter<SliderFilterValue>() {
|
||||
override val valueClass: KClass<SliderFilterValue> = SliderFilterValue::class
|
||||
override fun defaultValue() = SliderFilterValue(key, min)
|
||||
}
|
||||
|
||||
sealed class FilterValue : BaseObservable(), Equatable {
|
||||
abstract val key: String
|
||||
var dataSource: String = ""
|
||||
var profile: Long = FILTERS_CUSTOM
|
||||
|
||||
abstract fun hasSameValueAs(other: FilterValue): Boolean
|
||||
}
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id", "dataSource"),
|
||||
childColumns = arrayOf("profile", "dataSource"),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile", "dataSource"]
|
||||
)
|
||||
data class BooleanFilterValue(
|
||||
override val key: String,
|
||||
var value: Boolean
|
||||
) : FilterValue() {
|
||||
override fun hasSameValueAs(other: FilterValue): Boolean {
|
||||
return other is BooleanFilterValue && other.value == this.value
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id", "dataSource"),
|
||||
childColumns = arrayOf("profile", "dataSource"),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile", "dataSource"]
|
||||
)
|
||||
data class MultipleChoiceFilterValue(
|
||||
override val key: String,
|
||||
var values: MutableSet<String>,
|
||||
var all: Boolean
|
||||
) : FilterValue() {
|
||||
|
||||
override fun hasSameValueAs(other: FilterValue): Boolean {
|
||||
return other is MultipleChoiceFilterValue && if (other.all) {
|
||||
this.all
|
||||
} else {
|
||||
!this.all && other.values == this.values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id", "dataSource"),
|
||||
childColumns = arrayOf("profile", "dataSource"),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile", "dataSource"]
|
||||
)
|
||||
data class SliderFilterValue(
|
||||
override val key: String,
|
||||
var value: Int
|
||||
) : FilterValue() {
|
||||
override fun hasSameValueAs(other: FilterValue): Boolean {
|
||||
return other is SliderFilterValue && other.value == this.value
|
||||
}
|
||||
}
|
||||
|
||||
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable
|
||||
|
||||
typealias FilterValues = List<FilterWithValue<out FilterValue>>
|
||||
|
||||
fun FilterValues.getBooleanValue(key: String) =
|
||||
(this.find { it.value.key == key }?.value as BooleanFilterValue?)?.value
|
||||
|
||||
fun FilterValues.getSliderValue(key: String) =
|
||||
(this.find { it.value.key == key }?.value as SliderFilterValue?)?.value
|
||||
|
||||
fun FilterValues.getMultipleChoiceFilter(key: String) =
|
||||
this.find { it.value.key == key }?.filter as MultipleChoiceFilter?
|
||||
|
||||
fun FilterValues.getMultipleChoiceValue(key: String) =
|
||||
this.find { it.value.key == key }?.value as MultipleChoiceFilterValue?
|
||||
|
||||
const val FILTERS_DISABLED = -2L
|
||||
const val FILTERS_CUSTOM = -1L
|
||||
@@ -0,0 +1,3 @@
|
||||
package net.vonforst.evmap.model
|
||||
|
||||
abstract class ReferenceData
|
||||
@@ -13,11 +13,12 @@ import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.Navigator
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
@Navigator.Name("chrome")
|
||||
class ChromeCustomTabsNavigator(
|
||||
@Navigator.Name("custom")
|
||||
class CustomNavigator(
|
||||
private val context: Context
|
||||
) : Navigator<ChromeCustomTabsNavigator.Destination>() {
|
||||
) : Navigator<CustomNavigator.Destination>() {
|
||||
|
||||
override fun createDestination() =
|
||||
Destination(this)
|
||||
@@ -28,6 +29,19 @@ class ChromeCustomTabsNavigator(
|
||||
navOptions: NavOptions?,
|
||||
navigatorExtras: Extras?
|
||||
): NavDestination? {
|
||||
if (destination.destination == "report_new_charger") {
|
||||
val prefs = PreferenceDataSource(context)
|
||||
val url = when (prefs.dataSource) {
|
||||
"goingelectric" -> "https://www.goingelectric.de/stromtankstellen/new/"
|
||||
"openchargemap" -> "https://openchargemap.org/site/poi/add"
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
launchCustomTab(url)
|
||||
}
|
||||
return null // Do not add to the back stack, managed by Chrome Custom Tabs
|
||||
}
|
||||
|
||||
fun launchCustomTab(url: String) {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
@@ -35,20 +49,19 @@ class ChromeCustomTabsNavigator(
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
intent.launchUrl(context, destination.url!!)
|
||||
return null // Do not add to the back stack, managed by Chrome Custom Tabs
|
||||
intent.launchUrl(context, Uri.parse(url))
|
||||
}
|
||||
|
||||
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
|
||||
lateinit var destination: String
|
||||
|
||||
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))
|
||||
context.withStyledAttributes(attrs, R.styleable.CustomNavigator, 0, 0) {
|
||||
destination = getString(R.styleable.CustomNavigator_customDestination)!!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ class NavHostFragment : NavHostFragment() {
|
||||
override fun onCreateNavController(navController: NavController) {
|
||||
super.onCreateNavController(navController)
|
||||
navController.navigatorProvider.addNavigator(
|
||||
ChromeCustomTabsNavigator(
|
||||
CustomNavigator(
|
||||
requireContext()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Dao
|
||||
interface ChargeCardDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg chargeCards: ChargeCard)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg chargeCards: ChargeCard)
|
||||
|
||||
@Query("SELECT * FROM chargeCard")
|
||||
fun getAllChargeCards(): LiveData<List<ChargeCard>>
|
||||
}
|
||||
|
||||
class ChargeCardRepository(
|
||||
private val api: GoingElectricApi, private val scope: CoroutineScope,
|
||||
private val dao: ChargeCardDao, private val prefs: PreferenceDataSource
|
||||
) {
|
||||
fun getChargeCards(): LiveData<List<ChargeCard>> {
|
||||
scope.launch {
|
||||
updateChargeCards()
|
||||
}
|
||||
return dao.getAllChargeCards()
|
||||
}
|
||||
|
||||
private suspend fun updateChargeCards() {
|
||||
if (Duration.between(prefs.lastChargeCardUpdate, Instant.now()) < Duration.ofDays(1)) return
|
||||
|
||||
try {
|
||||
val response = api.getChargeCards()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (card in response.body()!!.result) {
|
||||
dao.insert(card)
|
||||
}
|
||||
|
||||
prefs.lastChargeCardUpdate = Instant.now()
|
||||
} catch (e: IOException) {
|
||||
// ignore, and retry next time
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
|
||||
@Dao
|
||||
interface ChargeLocationsDao {
|
||||
|
||||
@@ -7,12 +7,11 @@ import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
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
|
||||
import net.vonforst.evmap.api.goingelectric.GEChargeCard
|
||||
import net.vonforst.evmap.api.openchargemap.OCMConnectionType
|
||||
import net.vonforst.evmap.api.openchargemap.OCMCountry
|
||||
import net.vonforst.evmap.api.openchargemap.OCMOperator
|
||||
import net.vonforst.evmap.model.*
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
@@ -21,19 +20,25 @@ import net.vonforst.evmap.viewmodel.SliderFilterValue
|
||||
MultipleChoiceFilterValue::class,
|
||||
SliderFilterValue::class,
|
||||
FilterProfile::class,
|
||||
Plug::class,
|
||||
Network::class,
|
||||
ChargeCard::class
|
||||
], version = 11
|
||||
GEPlug::class,
|
||||
GENetwork::class,
|
||||
GEChargeCard::class,
|
||||
OCMConnectionType::class,
|
||||
OCMCountry::class,
|
||||
OCMOperator::class
|
||||
], version = 12
|
||||
)
|
||||
@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
|
||||
|
||||
// GoingElectric API specific
|
||||
abstract fun geReferenceDataDao(): GEReferenceDataDao
|
||||
|
||||
// OpenChargeMap API specific
|
||||
abstract fun ocmReferenceDataDao(): OCMReferenceDataDao
|
||||
|
||||
companion object {
|
||||
private lateinit var context: Context
|
||||
@@ -41,12 +46,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_9, MIGRATION_10, MIGRATION_11
|
||||
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11,
|
||||
MIGRATION_12
|
||||
)
|
||||
.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)")
|
||||
// create default filter profile for each data source
|
||||
db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('goingelectric', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
||||
db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('openchargemap', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
||||
}
|
||||
})
|
||||
.build()
|
||||
@@ -176,5 +183,73 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `barrierFree` INTEGER")
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_12 = object : Migration(11, 12) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.beginTransaction()
|
||||
try {
|
||||
//////////////////////////////////////////
|
||||
// create OpenChargeMap-specific tables //
|
||||
//////////////////////////////////////////
|
||||
db.execSQL("CREATE TABLE `OCMConnectionType` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `formalName` TEXT, `discontinued` INTEGER, `obsolete` INTEGER, PRIMARY KEY(`id`))")
|
||||
db.execSQL("CREATE TABLE `OCMCountry` (`id` INTEGER NOT NULL, `isoCode` TEXT NOT NULL, `continentCode` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`id`))")
|
||||
db.execSQL("CREATE TABLE `OCMOperator` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))");
|
||||
|
||||
//////////////////////////////////////////
|
||||
// rename GoingElectric-specific tables //
|
||||
//////////////////////////////////////////
|
||||
db.execSQL("ALTER TABLE `ChargeCard` RENAME TO `GEChargeCard`")
|
||||
db.execSQL("ALTER TABLE `Network` RENAME TO `GENetwork`")
|
||||
db.execSQL("ALTER TABLE `Plug` RENAME TO `GEPlug`")
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// add new columns to ChargeLocation table //
|
||||
/////////////////////////////////////////////
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `editUrl` TEXT")
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `license` TEXT")
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `chargepricecountry` TEXT")
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `chargepricenetwork` TEXT")
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `chargepriceplugTypes` TEXT")
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Separate FilterValues and FilterProfiles by DataSource //
|
||||
////////////////////////////////////////////////////////////
|
||||
// recreate tables
|
||||
db.execSQL("CREATE TABLE `FilterProfileNew` (`name` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `id` INTEGER NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`dataSource`, `id`))")
|
||||
db.execSQL("CREATE UNIQUE INDEX `index_FilterProfile_dataSource_name` ON `FilterProfileNew` (`dataSource`, `name`)")
|
||||
|
||||
db.execSQL("CREATE TABLE `BooleanFilterValueNew` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("CREATE TABLE `MultipleChoiceFilterValueNew` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
db.execSQL("CREATE TABLE `SliderFilterValueNew` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )")
|
||||
|
||||
val tables = listOf(
|
||||
"FilterProfile",
|
||||
"BooleanFilterValue",
|
||||
"MultipleChoiceFilterValue",
|
||||
"SliderFilterValue",
|
||||
)
|
||||
// copy data
|
||||
for (table in tables) {
|
||||
val columnList = when (table) {
|
||||
"BooleanFilterValue", "SliderFilterValue" -> "`key`, `value`, `dataSource`, `profile`"
|
||||
"MultipleChoiceFilterValue" -> "`key`, `values`, `all`, `dataSource`, `profile`"
|
||||
"FilterProfile" -> "`name`, `dataSource`, `id`, `order`"
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
db.execSQL("ALTER TABLE `$table` ADD COLUMN `dataSource` STRING NOT NULL DEFAULT 'goingelectric'")
|
||||
db.execSQL("INSERT INTO `${table}New`($columnList) SELECT $columnList FROM `$table`")
|
||||
db.execSQL("DROP TABLE `$table`")
|
||||
db.execSQL("ALTER TABLE `${table}New` RENAME TO `$table`")
|
||||
}
|
||||
|
||||
// create default filter profile for openchargemap
|
||||
db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('openchargemap', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,16 @@ 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
|
||||
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
||||
|
||||
@Entity(
|
||||
indices = [Index(value = ["name"], unique = true)]
|
||||
indices = [Index(value = ["dataSource", "name"], unique = true)],
|
||||
primaryKeys = ["dataSource", "id"],
|
||||
)
|
||||
data class FilterProfile(
|
||||
val name: String,
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
val dataSource: String,
|
||||
val id: Long,
|
||||
var order: Int = 0
|
||||
) : Equatable
|
||||
|
||||
@@ -25,12 +27,15 @@ interface FilterProfileDao {
|
||||
@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 dataSource = :dataSource AND id != $FILTERS_CUSTOM ORDER BY `order` ASC, `name` ASC")
|
||||
fun getProfiles(dataSource: String): LiveData<List<FilterProfile>>
|
||||
|
||||
@Query("SELECT * FROM filterProfile WHERE name = :name")
|
||||
suspend fun getProfileByName(name: String): FilterProfile?
|
||||
@Query("SELECT * FROM filterProfile WHERE dataSource = :dataSource AND name = :name")
|
||||
suspend fun getProfileByName(name: String, dataSource: String): FilterProfile?
|
||||
|
||||
@Query("SELECT * FROM filterProfile WHERE id = :id")
|
||||
suspend fun getProfileById(id: Long): FilterProfile?
|
||||
@Query("SELECT * FROM filterProfile WHERE dataSource = :dataSource AND id = :id")
|
||||
suspend fun getProfileById(id: Long, dataSource: String): FilterProfile?
|
||||
|
||||
@Query("SELECT (MAX(id) + 1) FROM filterProfile WHERE dataSource = :dataSource")
|
||||
suspend fun getNewId(dataSource: String): Long
|
||||
}
|
||||
@@ -4,18 +4,27 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.room.*
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
import net.vonforst.evmap.model.*
|
||||
|
||||
@Dao
|
||||
abstract class FilterValueDao {
|
||||
@Query("SELECT * FROM booleanfiltervalue WHERE profile = :profile")
|
||||
protected abstract fun getBooleanFilterValues(profile: Long): LiveData<List<BooleanFilterValue>>
|
||||
@Query("SELECT * FROM booleanfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract fun getBooleanFilterValues(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): LiveData<List<BooleanFilterValue>>
|
||||
|
||||
@Query("SELECT * FROM multiplechoicefiltervalue WHERE profile = :profile")
|
||||
protected abstract fun getMultipleChoiceFilterValues(profile: Long): LiveData<List<MultipleChoiceFilterValue>>
|
||||
@Query("SELECT * FROM multiplechoicefiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract fun getMultipleChoiceFilterValues(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): LiveData<List<MultipleChoiceFilterValue>>
|
||||
|
||||
@Query("SELECT * FROM sliderfiltervalue WHERE profile = :profile")
|
||||
protected abstract fun getSliderFilterValues(profile: Long): LiveData<List<SliderFilterValue>>
|
||||
@Query("SELECT * FROM sliderfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract fun getSliderFilterValues(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
): LiveData<List<SliderFilterValue>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract suspend fun insert(vararg values: BooleanFilterValue)
|
||||
@@ -26,28 +35,40 @@ abstract class FilterValueDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract suspend fun insert(vararg values: SliderFilterValue)
|
||||
|
||||
@Query("DELETE FROM booleanfiltervalue WHERE profile = :profile")
|
||||
protected abstract suspend fun deleteBooleanFilterValuesForProfile(profile: Long)
|
||||
@Query("DELETE FROM booleanfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract suspend fun deleteBooleanFilterValuesForProfile(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
)
|
||||
|
||||
@Query("DELETE FROM multiplechoicefiltervalue WHERE profile = :profile")
|
||||
protected abstract suspend fun deleteMultipleChoiceFilterValuesForProfile(profile: Long)
|
||||
@Query("DELETE FROM multiplechoicefiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract suspend fun deleteMultipleChoiceFilterValuesForProfile(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
)
|
||||
|
||||
@Query("DELETE FROM sliderfiltervalue WHERE profile = :profile")
|
||||
protected abstract suspend fun deleteSliderFilterValuesForProfile(profile: Long)
|
||||
@Query("DELETE FROM sliderfiltervalue WHERE profile = :profile AND dataSource = :dataSource")
|
||||
protected abstract suspend fun deleteSliderFilterValuesForProfile(
|
||||
profile: Long,
|
||||
dataSource: String
|
||||
)
|
||||
|
||||
open fun getFilterValues(filterStatus: Long): LiveData<List<FilterValue>> =
|
||||
open fun getFilterValues(filterStatus: Long, dataSource: String): LiveData<List<FilterValue>> =
|
||||
if (filterStatus == FILTERS_DISABLED) {
|
||||
MutableLiveData(emptyList())
|
||||
} else {
|
||||
MediatorLiveData<List<FilterValue>>().apply {
|
||||
val sources = listOf(
|
||||
getBooleanFilterValues(filterStatus),
|
||||
getMultipleChoiceFilterValues(filterStatus),
|
||||
getSliderFilterValues(filterStatus)
|
||||
getBooleanFilterValues(filterStatus, dataSource),
|
||||
getMultipleChoiceFilterValues(filterStatus, dataSource),
|
||||
getSliderFilterValues(filterStatus, dataSource)
|
||||
)
|
||||
for (source in sources) {
|
||||
addSource(source) {
|
||||
value = sources.mapNotNull { it.value }.flatten()
|
||||
val values = sources.map { it.value }
|
||||
if (values.all { it != null }) {
|
||||
value = values.filterNotNull().flatten()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,10 +86,10 @@ abstract class FilterValueDao {
|
||||
}
|
||||
|
||||
@Transaction
|
||||
open suspend fun deleteFilterValuesForProfile(profile: Long) {
|
||||
deleteBooleanFilterValuesForProfile(profile)
|
||||
deleteMultipleChoiceFilterValuesForProfile(profile)
|
||||
deleteSliderFilterValuesForProfile(profile)
|
||||
open suspend fun deleteFilterValuesForProfile(profile: Long, dataSource: String) {
|
||||
deleteBooleanFilterValuesForProfile(profile, dataSource)
|
||||
deleteMultipleChoiceFilterValuesForProfile(profile, dataSource)
|
||||
deleteSliderFilterValuesForProfile(profile, dataSource)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.goingelectric.GEChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.GEReferenceData
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Entity
|
||||
data class GENetwork(@PrimaryKey val name: String)
|
||||
|
||||
@Entity
|
||||
data class GEPlug(@PrimaryKey val name: String)
|
||||
|
||||
@Dao
|
||||
abstract class GEReferenceDataDao {
|
||||
// NETWORKS
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun insert(vararg networks: GENetwork)
|
||||
|
||||
@Query("DELETE FROM genetwork")
|
||||
abstract fun deleteAllNetworks()
|
||||
|
||||
@Transaction
|
||||
open suspend fun updateNetworks(networks: List<GENetwork>) {
|
||||
deleteAllNetworks()
|
||||
for (network in networks) {
|
||||
insert(network)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM genetwork")
|
||||
abstract fun getAllNetworks(): LiveData<List<GENetwork>>
|
||||
|
||||
// PLUGS
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun insert(vararg plugs: GEPlug)
|
||||
|
||||
@Query("DELETE FROM geplug")
|
||||
abstract fun deleteAllPlugs()
|
||||
|
||||
@Transaction
|
||||
open suspend fun updatePlugs(plugs: List<GEPlug>) {
|
||||
deleteAllPlugs()
|
||||
for (plug in plugs) {
|
||||
insert(plug)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM geplug")
|
||||
abstract fun getAllPlugs(): LiveData<List<GEPlug>>
|
||||
|
||||
// CHARGE CARDS
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun insert(vararg chargeCards: GEChargeCard)
|
||||
|
||||
@Query("DELETE FROM gechargecard")
|
||||
abstract fun deleteAllChargeCards()
|
||||
|
||||
@Transaction
|
||||
open suspend fun updateChargeCards(chargeCards: List<GEChargeCard>) {
|
||||
deleteAllChargeCards()
|
||||
for (chargeCard in chargeCards) {
|
||||
insert(chargeCard)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM gechargecard")
|
||||
abstract fun getAllChargeCards(): LiveData<List<GEChargeCard>>
|
||||
}
|
||||
|
||||
class GEReferenceDataRepository(
|
||||
private val api: GoingElectricApiWrapper, private val scope: CoroutineScope,
|
||||
private val dao: GEReferenceDataDao, private val prefs: PreferenceDataSource
|
||||
) {
|
||||
fun getReferenceData(): LiveData<GEReferenceData> {
|
||||
scope.launch {
|
||||
updateData()
|
||||
}
|
||||
val plugs = dao.getAllPlugs()
|
||||
val networks = dao.getAllNetworks()
|
||||
val chargeCards = dao.getAllChargeCards()
|
||||
return MediatorLiveData<GEReferenceData>().apply {
|
||||
listOf(chargeCards, networks, plugs).map { source ->
|
||||
addSource(source) { _ ->
|
||||
val p = plugs.value ?: return@addSource
|
||||
val n = networks.value ?: return@addSource
|
||||
val cc = chargeCards.value ?: return@addSource
|
||||
value = GEReferenceData(p.map { it.name }, n.map { it.name }, cc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateData() {
|
||||
if (Duration.between(
|
||||
prefs.lastGeReferenceDataUpdate,
|
||||
Instant.now()
|
||||
) < Duration.ofDays(1)
|
||||
) return
|
||||
|
||||
val response = api.getReferenceData()
|
||||
if (response.status == Status.ERROR) return // ignore and retry next time
|
||||
|
||||
|
||||
val data = response.data!!
|
||||
dao.updateNetworks(data.networks.map { GENetwork(it) })
|
||||
dao.updatePlugs(data.plugs.map { GEPlug(it) })
|
||||
dao.updateChargeCards(data.chargecards)
|
||||
|
||||
prefs.lastGeReferenceDataUpdate = Instant.now()
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Entity
|
||||
data class Network(@PrimaryKey val name: String)
|
||||
|
||||
@Dao
|
||||
interface NetworkDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg networks: Network)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg networks: Network)
|
||||
|
||||
@Query("SELECT * FROM network")
|
||||
fun getAllNetworks(): LiveData<List<Network>>
|
||||
}
|
||||
|
||||
class NetworkRepository(
|
||||
private val api: GoingElectricApi, private val scope: CoroutineScope,
|
||||
private val dao: NetworkDao, private val prefs: PreferenceDataSource
|
||||
) {
|
||||
fun getNetworks(): LiveData<List<Network>> {
|
||||
scope.launch {
|
||||
updateNetworks()
|
||||
}
|
||||
return dao.getAllNetworks()
|
||||
}
|
||||
|
||||
private suspend fun updateNetworks() {
|
||||
if (Duration.between(prefs.lastNetworkUpdate, Instant.now()) < Duration.ofDays(1)) return
|
||||
|
||||
try {
|
||||
val response = api.getNetworks()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (name in response.body()!!.result) {
|
||||
dao.insert(Network(name))
|
||||
}
|
||||
|
||||
prefs.lastNetworkUpdate = Instant.now()
|
||||
} catch (e: IOException) {
|
||||
// ignore, and retry next time
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.openchargemap.*
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Dao
|
||||
abstract class OCMReferenceDataDao {
|
||||
// CONNECTION TYPES
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun insert(vararg connectionTypes: OCMConnectionType)
|
||||
|
||||
@Query("DELETE FROM ocmconnectiontype")
|
||||
abstract fun deleteAllConnectionTypes()
|
||||
|
||||
@Transaction
|
||||
open suspend fun updateConnectionTypes(connectionTypes: List<OCMConnectionType>) {
|
||||
deleteAllConnectionTypes()
|
||||
for (connectionType in connectionTypes) {
|
||||
insert(connectionType)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM ocmconnectiontype")
|
||||
abstract fun getAllConnectionTypes(): LiveData<List<OCMConnectionType>>
|
||||
|
||||
// COUNTRIES
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun insert(vararg countries: OCMCountry)
|
||||
|
||||
@Query("DELETE FROM ocmcountry")
|
||||
abstract fun deleteAllCountries()
|
||||
|
||||
@Transaction
|
||||
open suspend fun updateCountries(countries: List<OCMCountry>) {
|
||||
deleteAllCountries()
|
||||
for (country in countries) {
|
||||
insert(country)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM ocmcountry")
|
||||
abstract fun getAllCountries(): LiveData<List<OCMCountry>>
|
||||
|
||||
// OPERATORS
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract suspend fun insert(vararg operators: OCMOperator)
|
||||
|
||||
@Query("DELETE FROM ocmoperator")
|
||||
abstract fun deleteAllOperators()
|
||||
|
||||
@Transaction
|
||||
open suspend fun updateOperators(operators: List<OCMOperator>) {
|
||||
deleteAllOperators()
|
||||
for (operator in operators) {
|
||||
insert(operator)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM ocmoperator")
|
||||
abstract fun getAllOperators(): LiveData<List<OCMOperator>>
|
||||
}
|
||||
|
||||
class OCMReferenceDataRepository(
|
||||
private val api: OpenChargeMapApiWrapper, private val scope: CoroutineScope,
|
||||
private val dao: OCMReferenceDataDao, private val prefs: PreferenceDataSource
|
||||
) {
|
||||
fun getReferenceData(): LiveData<OCMReferenceData> {
|
||||
scope.launch {
|
||||
updateData()
|
||||
}
|
||||
val connectionTypes = dao.getAllConnectionTypes()
|
||||
val countries = dao.getAllCountries()
|
||||
val operators = dao.getAllOperators()
|
||||
return MediatorLiveData<OCMReferenceData>().apply {
|
||||
listOf(countries, connectionTypes, operators).map { source ->
|
||||
addSource(source) { _ ->
|
||||
val ct = connectionTypes.value
|
||||
val c = countries.value
|
||||
val o = operators.value
|
||||
if (ct.isNullOrEmpty() || c.isNullOrEmpty() || o.isNullOrEmpty()) return@addSource
|
||||
value = OCMReferenceData(ct, c, o)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateData() {
|
||||
if (Duration.between(
|
||||
prefs.lastOcmReferenceDataUpdate,
|
||||
Instant.now()
|
||||
) < Duration.ofDays(1)
|
||||
) return
|
||||
|
||||
val response = api.getReferenceData()
|
||||
if (response.status == Status.ERROR) return // ignore and retry next time
|
||||
|
||||
|
||||
val data = response.data!!
|
||||
dao.updateConnectionTypes(data.connectionTypes)
|
||||
dao.updateCountries(data.countries)
|
||||
dao.updateOperators(data.operators)
|
||||
|
||||
prefs.lastOcmReferenceDataUpdate = Instant.now()
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Entity
|
||||
data class Plug(@PrimaryKey val name: String)
|
||||
|
||||
@Dao
|
||||
interface PlugDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg plugs: Plug)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg plugs: Plug)
|
||||
|
||||
@Query("SELECT * FROM plug")
|
||||
fun getAllPlugs(): LiveData<List<Plug>>
|
||||
}
|
||||
|
||||
class PlugRepository(
|
||||
private val api: GoingElectricApi, private val scope: CoroutineScope,
|
||||
private val dao: PlugDao, private val prefs: PreferenceDataSource
|
||||
) {
|
||||
fun getPlugs(): LiveData<List<Plug>> {
|
||||
scope.launch {
|
||||
updatePlugs()
|
||||
}
|
||||
return dao.getAllPlugs()
|
||||
}
|
||||
|
||||
private suspend fun updatePlugs() {
|
||||
if (Duration.between(prefs.lastPlugUpdate, Instant.now()) < Duration.ofDays(1)) return
|
||||
|
||||
try {
|
||||
val response = api.getPlugs()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (name in response.body()!!.result) {
|
||||
dao.insert(Plug(name))
|
||||
}
|
||||
|
||||
prefs.lastPlugUpdate = Instant.now()
|
||||
} catch (e: IOException) {
|
||||
// ignore, and retry next time
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,35 +4,41 @@ import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.car2go.maps.AnyMap
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.viewmodel.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.viewmodel.FILTERS_DISABLED
|
||||
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import java.time.Instant
|
||||
|
||||
class PreferenceDataSource(val context: Context) {
|
||||
val sp = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
var dataSource: String
|
||||
get() = sp.getString("data_source", "goingelectric")!!
|
||||
set(value) {
|
||||
sp.edit().putString("data_source", value).apply()
|
||||
}
|
||||
|
||||
var dataSourceSet: Boolean
|
||||
get() = sp.getBoolean("data_source_set", false)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("data_source_set", value).apply()
|
||||
}
|
||||
|
||||
var navigateUseMaps: Boolean
|
||||
get() = sp.getBoolean("navigate_use_maps", true)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("navigate_use_maps", value).apply()
|
||||
}
|
||||
|
||||
var lastPlugUpdate: Instant
|
||||
get() = Instant.ofEpochMilli(sp.getLong("last_plug_update", 0L))
|
||||
var lastGeReferenceDataUpdate: Instant
|
||||
get() = Instant.ofEpochMilli(sp.getLong("last_ge_reference_data_update", 0L))
|
||||
set(value) {
|
||||
sp.edit().putLong("last_plug_update", value.toEpochMilli()).apply()
|
||||
sp.edit().putLong("last_ge_reference_data_update", value.toEpochMilli()).apply()
|
||||
}
|
||||
|
||||
var lastNetworkUpdate: Instant
|
||||
get() = Instant.ofEpochMilli(sp.getLong("last_network_update", 0L))
|
||||
var lastOcmReferenceDataUpdate: Instant
|
||||
get() = Instant.ofEpochMilli(sp.getLong("last_ocm_reference_data_update", 0L))
|
||||
set(value) {
|
||||
sp.edit().putLong("last_network_update", value.toEpochMilli()).apply()
|
||||
}
|
||||
|
||||
var lastChargeCardUpdate: Instant
|
||||
get() = Instant.ofEpochMilli(sp.getLong("last_chargecard_update", 0L))
|
||||
set(value) {
|
||||
sp.edit().putLong("last_chargecard_update", value.toEpochMilli()).apply()
|
||||
sp.edit().putLong("last_ocm_reference_data_update", value.toEpochMilli()).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,17 +104,21 @@ class PreferenceDataSource(val context: Context) {
|
||||
sp.edit().putBoolean("update_0.6.0_androidauto_dialog_shown", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceMyVehicle: String?
|
||||
get() = sp.getString("chargeprice_my_vehicle", null)
|
||||
var chargepriceMyVehicles: Set<String>
|
||||
get() = try {
|
||||
sp.getStringSet("chargeprice_my_vehicle", emptySet())!!
|
||||
} catch (e: ClassCastException) {
|
||||
// backwards compatibility
|
||||
sp.getString("chargeprice_my_vehicle", null)?.let { setOf(it) } ?: emptySet()
|
||||
}
|
||||
set(value) {
|
||||
sp.edit().putString("chargeprice_my_vehicle", value).apply()
|
||||
sp.edit().putStringSet("chargeprice_my_vehicle", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceMyVehicleDcChargeports: List<String>?
|
||||
get() = sp.getString("chargeprice_my_vehicle_dc_chargeports", null)?.split(",")
|
||||
var chargepriceLastSelectedVehicle: String?
|
||||
get() = sp.getString("chargeprice_last_vehicle", null)
|
||||
set(value) {
|
||||
sp.edit().putString("chargeprice_my_vehicle_dc_chargeports", value?.joinToString(","))
|
||||
.apply()
|
||||
sp.edit().putString("chargeprice_last_vehicle", value).apply()
|
||||
}
|
||||
|
||||
var chargepriceMyTariffs: Set<String>?
|
||||
|
||||
@@ -3,14 +3,24 @@ package net.vonforst.evmap.storage
|
||||
import androidx.room.TypeConverter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCardId
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.api.goingelectric.ChargerPhoto
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import net.vonforst.evmap.api.goingelectric.GEChargerPhotoAdapter
|
||||
import net.vonforst.evmap.api.openchargemap.OCMChargerPhotoAdapter
|
||||
import net.vonforst.evmap.model.ChargeCardId
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
import java.time.Instant
|
||||
import java.time.LocalTime
|
||||
|
||||
class Converters {
|
||||
val moshi = Moshi.Builder().build()
|
||||
val moshi = Moshi.Builder()
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(ChargerPhoto::class.java, "type")
|
||||
.withSubtype(GEChargerPhotoAdapter::class.java, "goingelectric")
|
||||
.withSubtype(OCMChargerPhotoAdapter::class.java, "openchargemap")
|
||||
.withDefaultValue(null)
|
||||
)
|
||||
.build()
|
||||
private val chargepointListAdapter by lazy {
|
||||
val type = Types.newParameterizedType(List::class.java, Chargepoint::class.java)
|
||||
moshi.adapter<List<Chargepoint>>(type)
|
||||
@@ -27,6 +37,10 @@ class Converters {
|
||||
val type = Types.newParameterizedType(Set::class.java, String::class.java)
|
||||
moshi.adapter<Set<String>>(type)
|
||||
}
|
||||
private val stringListAdapter by lazy {
|
||||
val type = Types.newParameterizedType(List::class.java, String::class.java)
|
||||
moshi.adapter<List<String>>(type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromChargepointList(value: List<Chargepoint>?): String {
|
||||
@@ -45,7 +59,7 @@ class Converters {
|
||||
|
||||
@TypeConverter
|
||||
fun toChargerPhotoList(value: String): List<ChargerPhoto>? {
|
||||
return chargerPhotoListAdapter.fromJson(value)
|
||||
return chargerPhotoListAdapter.fromJson(value)?.filterNotNull()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
@@ -91,4 +105,14 @@ class Converters {
|
||||
fun toStringSet(value: String): Set<String>? {
|
||||
return stringSetAdapter.fromJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromStringList(value: List<String>?): String {
|
||||
return stringListAdapter.toJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toStringList(value: String): List<String>? {
|
||||
return stringListAdapter.fromJson(value)
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import com.google.android.material.slider.RangeSlider
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.iconForPlugType
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
@@ -265,6 +267,13 @@ fun currency(currency: String): String {
|
||||
}
|
||||
}
|
||||
|
||||
fun time(value: Int): String {
|
||||
val h = floor(value.toDouble() / 60).toInt();
|
||||
val min = ceil(value.toDouble() % 60).toInt();
|
||||
return if (h == 0 && min > 0) "$min min";
|
||||
else "%d:%02d h".format(h, min);
|
||||
}
|
||||
|
||||
@InverseBindingAdapter(attribute = "app:values")
|
||||
fun getRangeSliderValue(slider: RangeSlider) = slider.values
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ package net.vonforst.evmap.ui;
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.google.maps.android.clustering.ClusterItem
|
||||
import com.google.maps.android.clustering.algo.NonHierarchicalDistanceBasedAlgorithm
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocationCluster
|
||||
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
|
||||
import net.vonforst.evmap.api.goingelectric.Coordinate
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.ChargeLocationCluster
|
||||
import net.vonforst.evmap.model.ChargepointListItem
|
||||
import net.vonforst.evmap.model.Coordinate
|
||||
|
||||
|
||||
fun cluster(
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.ListPreference
|
||||
import net.vonforst.evmap.fragment.DataSourceSelectDialog
|
||||
|
||||
class DataSourceSelectDialogPreference(ctx: Context, attrs: AttributeSet) :
|
||||
ListPreference(ctx, attrs) {
|
||||
override fun onClick() {
|
||||
val dialog = DataSourceSelectDialog.getInstance(true)
|
||||
dialog.okListener = { selected ->
|
||||
value = selected
|
||||
}
|
||||
dialog.show((context as AppCompatActivity).supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import androidx.interpolator.view.animation.FastOutLinearInInterpolator
|
||||
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
||||
import com.car2go.maps.model.Marker
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import kotlin.math.max
|
||||
|
||||
fun getMarkerTint(
|
||||
|
||||
@@ -4,17 +4,44 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.fragment.MultiSelectDialog
|
||||
|
||||
class MultiSelectDialogPreference(ctx: Context, attrs: AttributeSet) :
|
||||
MultiSelectListPreference(ctx, attrs) {
|
||||
val showAllButton: Boolean
|
||||
val defaultToAll: Boolean
|
||||
|
||||
init {
|
||||
val a = ctx.obtainStyledAttributes(attrs, R.styleable.MultiSelectDialogPreference)
|
||||
showAllButton = a.getBoolean(R.styleable.MultiSelectDialogPreference_showAllButton, true)
|
||||
defaultToAll = a.getBoolean(R.styleable.MultiSelectDialogPreference_defaultToAll, true)
|
||||
a.recycle()
|
||||
}
|
||||
|
||||
override fun onSetInitialValue(defaultValue: Any?) {
|
||||
try {
|
||||
super.onSetInitialValue(defaultValue)
|
||||
} catch (e: ClassCastException) {
|
||||
// backwards compatibility when changing a ListPreference into a MultiSelectListPreference
|
||||
val value =
|
||||
getPersistedString(null)?.let { setOf(it) } ?: (defaultValue as Set<String>?)
|
||||
sharedPreferences.edit()
|
||||
.remove(key)
|
||||
.putStringSet(key, value)
|
||||
.apply()
|
||||
super.onSetInitialValue(defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
val dialog =
|
||||
MultiSelectDialog.getInstance(
|
||||
title.toString(),
|
||||
entryValues.map { it.toString() }.zip(entries.map { it.toString() }).toMap(),
|
||||
if (all) entryValues.map { it.toString() }.toSet() else values,
|
||||
emptySet()
|
||||
emptySet(),
|
||||
showAllButton
|
||||
)
|
||||
dialog.okListener = { selected ->
|
||||
all = selected == entryValues.toSet()
|
||||
@@ -24,7 +51,7 @@ class MultiSelectDialogPreference(ctx: Context, attrs: AttributeSet) :
|
||||
}
|
||||
|
||||
var all: Boolean
|
||||
get() = sharedPreferences.getBoolean(key + "_all", true)
|
||||
get() = sharedPreferences.getBoolean(key + "_all", defaultToAll)
|
||||
set(value) {
|
||||
sharedPreferences.edit().putBoolean(key + "_all", value).apply()
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.banana.jsonapi2.HasOne
|
||||
import net.vonforst.evmap.api.chargeprice.*
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
@@ -21,34 +23,54 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
MutableLiveData<ChargeLocation>()
|
||||
}
|
||||
|
||||
val dataSource: MutableLiveData<String> by lazy {
|
||||
MutableLiveData<String>()
|
||||
}
|
||||
|
||||
val chargepoint: MutableLiveData<Chargepoint> by lazy {
|
||||
MutableLiveData<Chargepoint>()
|
||||
}
|
||||
|
||||
val vehicle: LiveData<ChargepriceCar> by lazy {
|
||||
MutableLiveData<ChargepriceCar>().apply {
|
||||
value = prefs.chargepriceMyVehicle?.let { ChargepriceCar().apply { id = it } }
|
||||
val vehicles: MutableLiveData<Resource<List<ChargepriceCar>>> by lazy {
|
||||
MutableLiveData<Resource<List<ChargepriceCar>>>().apply {
|
||||
if (prefs.chargepriceMyVehicles.isEmpty()) {
|
||||
value = Resource.success(emptyList())
|
||||
} else {
|
||||
value = Resource.loading(null)
|
||||
loadVehicles()
|
||||
}
|
||||
observeForever {
|
||||
vehicle.value = it.data?.firstOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val vehicle: MutableLiveData<ChargepriceCar> by lazy {
|
||||
MutableLiveData<ChargepriceCar>()
|
||||
}
|
||||
|
||||
private val acConnectors = listOf(
|
||||
Chargepoint.CEE_BLAU,
|
||||
Chargepoint.CEE_ROT,
|
||||
Chargepoint.SCHUKO,
|
||||
Chargepoint.TYPE_1,
|
||||
Chargepoint.TYPE_2
|
||||
Chargepoint.TYPE_2_UNKNOWN,
|
||||
Chargepoint.TYPE_2_SOCKET,
|
||||
Chargepoint.TYPE_2_PLUG
|
||||
)
|
||||
private val plugMapping = mapOf(
|
||||
"ccs" to Chargepoint.CCS,
|
||||
"ccs" to Chargepoint.CCS_UNKNOWN,
|
||||
"tesla_suc" to Chargepoint.SUPERCHARGER,
|
||||
"tesla_ccs" to Chargepoint.CCS,
|
||||
"tesla_ccs" to Chargepoint.CCS_UNKNOWN,
|
||||
"chademo" to Chargepoint.CHADEMO
|
||||
)
|
||||
val vehicleCompatibleConnectors: LiveData<List<String>> by lazy {
|
||||
MutableLiveData<List<String>>().apply {
|
||||
value = prefs.chargepriceMyVehicleDcChargeports?.map {
|
||||
plugMapping.get(it)
|
||||
}?.filterNotNull()?.plus(acConnectors)
|
||||
MediatorLiveData<List<String>>().apply {
|
||||
addSource(vehicle) {
|
||||
value = it?.dcChargePorts?.map {
|
||||
plugMapping[it]
|
||||
}?.filterNotNull()?.plus(acConnectors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +81,8 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
addSource(it) {
|
||||
val charger = charger.value ?: return@addSource
|
||||
val connectors = vehicleCompatibleConnectors.value ?: return@addSource
|
||||
value = !charger.chargepoints.map { it.type }.any { it in connectors }
|
||||
value = !charger.chargepoints.flatMap { equivalentPlugTypes(it.type) }
|
||||
.any { it in connectors }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +107,7 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
value = Resource.loading(null)
|
||||
listOf(
|
||||
charger,
|
||||
vehicle,
|
||||
dataSource,
|
||||
batteryRange,
|
||||
batteryRangeSliderDragging,
|
||||
vehicleCompatibleConnectors
|
||||
@@ -118,7 +141,9 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
val myTariffs = prefs.chargepriceMyTariffs
|
||||
value = Resource.success(cps.data!!.map { cp ->
|
||||
val filteredPrices =
|
||||
cp.chargepointPrices.filter { it.plug == chargepoint.type && it.power == chargepoint.power }
|
||||
cp.chargepointPrices.filter {
|
||||
it.plug == getChargepricePlugType(chargepoint) && it.power == chargepoint.power
|
||||
}
|
||||
if (filteredPrices.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
@@ -139,6 +164,12 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChargepricePlugType(chargepoint: Chargepoint): String {
|
||||
val index = charger.value!!.chargepointsMerged.indexOf(chargepoint)
|
||||
val type = charger.value!!.chargepriceData!!.plugTypes?.get(index) ?: chargepoint.type
|
||||
return type
|
||||
}
|
||||
|
||||
val myTariffs: LiveData<Set<String>> by lazy {
|
||||
MutableLiveData<Set<String>>().apply {
|
||||
value = prefs.chargepriceMyTariffs
|
||||
@@ -164,7 +195,11 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
value = Resource.loading(null)
|
||||
} else {
|
||||
value =
|
||||
Resource.success(cpMeta.data!!.chargePoints.filter { it.plug == chargepoint.type && it.power == chargepoint.power }[0])
|
||||
Resource.success(cpMeta.data!!.chargePoints.filter {
|
||||
it.plug == getChargepricePlugType(
|
||||
chargepoint
|
||||
) && it.power == chargepoint.power
|
||||
}[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,21 +209,23 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
private var loadPricesJob: Job? = null
|
||||
fun loadPrices() {
|
||||
chargePrices.value = Resource.loading(null)
|
||||
val geCharger = charger.value
|
||||
chargePriceMeta.value = Resource.loading(null)
|
||||
val charger = charger.value
|
||||
val car = vehicle.value
|
||||
val compatibleConnectors = vehicleCompatibleConnectors.value
|
||||
if (geCharger == null || car == null || compatibleConnectors == null) {
|
||||
val dataSource = dataSource.value
|
||||
if (charger == null || car == null || compatibleConnectors == null || dataSource == null) {
|
||||
chargePrices.value = Resource.error(null, null)
|
||||
return
|
||||
}
|
||||
|
||||
val cpStation = ChargepriceStation.fromGoingelectric(geCharger, compatibleConnectors)
|
||||
val cpStation = ChargepriceStation.fromEvmap(charger, compatibleConnectors)
|
||||
|
||||
loadPricesJob?.cancel()
|
||||
loadPricesJob = viewModelScope.launch {
|
||||
try {
|
||||
val result = api.getChargePrices(ChargepriceRequest().apply {
|
||||
dataAdapter = "going_electric"
|
||||
dataAdapter = dataSource
|
||||
station = cpStation
|
||||
vehicle = HasOne(car)
|
||||
options = ChargepriceOptions(
|
||||
@@ -205,6 +242,22 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
} catch (e: IOException) {
|
||||
chargePrices.value = Resource.error(e.message, null)
|
||||
chargePriceMeta.value = Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
chargePrices.value = Resource.error(e.message, null)
|
||||
chargePriceMeta.value = Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadVehicles() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val result = api.getVehicles()
|
||||
vehicles.value = Resource.success(result.filter {
|
||||
it.id in prefs.chargepriceMyVehicles
|
||||
})
|
||||
} catch (e: IOException) {
|
||||
vehicles.value = Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,12 @@ import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
|
||||
class FavoritesViewModel(application: Application, geApiKey: String) :
|
||||
AndroidViewModel(application) {
|
||||
private var api = GoingElectricApi.create(geApiKey, context = application)
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
|
||||
val favorites: LiveData<List<ChargeLocation>> by lazy {
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.model.FILTERS_DISABLED
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.FilterProfile
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
@@ -14,12 +15,12 @@ class FilterProfilesViewModel(application: Application) : AndroidViewModel(appli
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
|
||||
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
|
||||
db.filterProfileDao().getProfiles()
|
||||
db.filterProfileDao().getProfiles(prefs.dataSource)
|
||||
}
|
||||
|
||||
fun delete(itemId: Long) {
|
||||
viewModelScope.launch {
|
||||
val profile = db.filterProfileDao().getProfileById(itemId)
|
||||
val profile = db.filterProfileDao().getProfileById(itemId, prefs.dataSource)
|
||||
profile?.let { db.filterProfileDao().delete(it) }
|
||||
if (prefs.filterStatus == profile?.id) {
|
||||
prefs.filterStatus = FILTERS_DISABLED
|
||||
|
||||
@@ -1,125 +1,17 @@
|
||||
package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.databinding.BaseObservable
|
||||
import androidx.lifecycle.*
|
||||
import androidx.room.Entity
|
||||
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
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.api.nameForPlugType
|
||||
import net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.cast
|
||||
|
||||
val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 75, 100, 150, 200, 250, 300, 350)
|
||||
internal fun mapPower(i: Int) = powerSteps[i]
|
||||
internal fun mapPowerInverse(power: Int) = powerSteps
|
||||
.mapIndexed { index, v -> abs(v - power) to index }
|
||||
.minByOrNull { it.first }?.second ?: 0
|
||||
|
||||
internal fun getFilters(
|
||||
application: Application,
|
||||
plugs: LiveData<List<Plug>>,
|
||||
networks: LiveData<List<Network>>,
|
||||
chargeCards: LiveData<List<ChargeCard>>
|
||||
): LiveData<List<Filter<FilterValue>>> {
|
||||
return MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||
listOf(plugs, networks, chargeCards).forEach { source ->
|
||||
addSource(source) { _ ->
|
||||
buildFilters(plugs, networks, chargeCards, application)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
|
||||
plugs: LiveData<List<Plug>>,
|
||||
networks: LiveData<List<Network>>,
|
||||
chargeCards: LiveData<List<ChargeCard>>,
|
||||
application: Application
|
||||
) {
|
||||
val plugMap = plugs.value?.map { plug ->
|
||||
plug.name to nameForPlugType(application, plug.name)
|
||||
}?.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"),
|
||||
BooleanFilter(application.getString(R.string.filter_open_247), "open_247"),
|
||||
SliderFilter(
|
||||
application.getString(R.string.filter_min_power), "min_power",
|
||||
powerSteps.size - 1,
|
||||
mapping = ::mapPower,
|
||||
inverseMapping = ::mapPowerInverse,
|
||||
unit = "kW"
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_connectors), "connectors",
|
||||
plugMap,
|
||||
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO),
|
||||
manyChoices = true
|
||||
),
|
||||
SliderFilter(
|
||||
application.getString(R.string.filter_min_connectors),
|
||||
"min_connectors",
|
||||
10,
|
||||
min = 1
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
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",
|
||||
chargecardMap, manyChoices = true
|
||||
),
|
||||
BooleanFilter(application.getString(R.string.filter_exclude_faults), "exclude_faults")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
internal fun filtersWithValue(
|
||||
filters: LiveData<List<Filter<FilterValue>>>,
|
||||
filterValues: LiveData<List<FilterValue>>
|
||||
@@ -138,27 +30,43 @@ internal fun filtersWithValue(
|
||||
}
|
||||
}
|
||||
|
||||
class FilterViewModel(application: Application, geApiKey: String) :
|
||||
AndroidViewModel(application) {
|
||||
private var api = GoingElectricApi.create(geApiKey, context = application)
|
||||
class FilterViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
private var api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
|
||||
|
||||
private val plugs: LiveData<List<Plug>> by lazy {
|
||||
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
|
||||
private val referenceData: LiveData<out ReferenceData> by lazy {
|
||||
val api = api
|
||||
when (api) {
|
||||
is GoingElectricApiWrapper -> {
|
||||
GEReferenceDataRepository(
|
||||
api,
|
||||
viewModelScope,
|
||||
db.geReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
is OpenChargeMapApiWrapper -> {
|
||||
OCMReferenceDataRepository(
|
||||
api,
|
||||
viewModelScope,
|
||||
db.ocmReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
else -> {
|
||||
throw RuntimeException("no reference data implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
private val networks: LiveData<List<Network>> by lazy {
|
||||
NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks()
|
||||
}
|
||||
private val chargeCards: LiveData<List<ChargeCard>> by lazy {
|
||||
ChargeCardRepository(api, viewModelScope, db.chargeCardDao(), prefs).getChargeCards()
|
||||
}
|
||||
private val filters: LiveData<List<Filter<FilterValue>>> by lazy {
|
||||
getFilters(application, plugs, networks, chargeCards)
|
||||
private val filters = MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||
addSource(referenceData) { data ->
|
||||
value = api.getFilters(data, application.stringProvider())
|
||||
}
|
||||
}
|
||||
|
||||
private val filterValues: LiveData<List<FilterValue>> by lazy {
|
||||
db.filterValueDao().getFilterValues(FILTERS_CUSTOM)
|
||||
db.filterValueDao().getFilterValues(FILTERS_CUSTOM, prefs.dataSource)
|
||||
}
|
||||
|
||||
val filtersWithValue: LiveData<FilterValues> by lazy {
|
||||
@@ -177,7 +85,7 @@ class FilterViewModel(application: Application, geApiKey: String) :
|
||||
when (id) {
|
||||
FILTERS_CUSTOM, FILTERS_DISABLED -> value = null
|
||||
else -> viewModelScope.launch {
|
||||
value = db.filterProfileDao().getProfileById(id)
|
||||
value = db.filterProfileDao().getProfileById(id, prefs.dataSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,10 +93,13 @@ class FilterViewModel(application: Application, geApiKey: String) :
|
||||
}
|
||||
|
||||
suspend fun saveFilterValues() {
|
||||
filtersWithValue.value?.forEach {
|
||||
filtersWithValue.value?.map {
|
||||
val value = it.value
|
||||
value.profile = FILTERS_CUSTOM
|
||||
db.filterValueDao().insert(value)
|
||||
value.dataSource = prefs.dataSource
|
||||
value
|
||||
}?.let {
|
||||
db.filterValueDao().insert(*it.toTypedArray())
|
||||
}
|
||||
|
||||
// set selected profile
|
||||
@@ -197,141 +108,23 @@ class FilterViewModel(application: Application, geApiKey: String) :
|
||||
|
||||
suspend fun saveAsProfile(name: String) {
|
||||
// get or create profile
|
||||
var profileId = db.filterProfileDao().getProfileByName(name)?.id
|
||||
var profileId = db.filterProfileDao().getProfileByName(name, prefs.dataSource)?.id
|
||||
if (profileId == null) {
|
||||
profileId = db.filterProfileDao().insert(FilterProfile(name))
|
||||
profileId = db.filterProfileDao().getNewId(prefs.dataSource)
|
||||
db.filterProfileDao().insert(FilterProfile(name, prefs.dataSource, profileId))
|
||||
}
|
||||
|
||||
// save filter values
|
||||
filtersWithValue.value?.forEach {
|
||||
filtersWithValue.value?.map {
|
||||
val value = it.value
|
||||
value.profile = profileId
|
||||
db.filterValueDao().insert(value)
|
||||
value.dataSource = prefs.dataSource
|
||||
value
|
||||
}?.let {
|
||||
db.filterValueDao().insert(*it.toTypedArray())
|
||||
}
|
||||
|
||||
// set selected profile
|
||||
prefs.filterStatus = profileId
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Filter<out T : FilterValue> : Equatable {
|
||||
abstract val name: String
|
||||
abstract val key: String
|
||||
abstract val valueClass: KClass<out T>
|
||||
abstract fun defaultValue(): T
|
||||
}
|
||||
|
||||
data class BooleanFilter(override val name: String, override val key: String) :
|
||||
Filter<BooleanFilterValue>() {
|
||||
override val valueClass: KClass<BooleanFilterValue> = BooleanFilterValue::class
|
||||
override fun defaultValue() = BooleanFilterValue(key, false)
|
||||
}
|
||||
|
||||
data class MultipleChoiceFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val choices: Map<String, String>,
|
||||
val commonChoices: Set<String>? = null,
|
||||
val manyChoices: Boolean = false
|
||||
) : Filter<MultipleChoiceFilterValue>() {
|
||||
override val valueClass: KClass<MultipleChoiceFilterValue> = MultipleChoiceFilterValue::class
|
||||
override fun defaultValue() = MultipleChoiceFilterValue(key, mutableSetOf(), true)
|
||||
}
|
||||
|
||||
data class SliderFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val max: Int,
|
||||
val min: Int = 0,
|
||||
val mapping: ((Int) -> Int) = { it },
|
||||
val inverseMapping: ((Int) -> Int) = { it },
|
||||
val unit: String? = ""
|
||||
) : Filter<SliderFilterValue>() {
|
||||
override val valueClass: KClass<SliderFilterValue> = SliderFilterValue::class
|
||||
override fun defaultValue() = SliderFilterValue(key, min)
|
||||
}
|
||||
|
||||
sealed class FilterValue : BaseObservable(), Equatable {
|
||||
abstract val key: String
|
||||
var profile: Long = FILTERS_CUSTOM
|
||||
}
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id"),
|
||||
childColumns = arrayOf("profile"),
|
||||
onDelete = CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile"]
|
||||
)
|
||||
data class BooleanFilterValue(
|
||||
override val key: String,
|
||||
var value: Boolean
|
||||
) : FilterValue()
|
||||
|
||||
@Entity(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id"),
|
||||
childColumns = arrayOf("profile"),
|
||||
onDelete = CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile"]
|
||||
)
|
||||
data class MultipleChoiceFilterValue(
|
||||
override val key: String,
|
||||
var values: MutableSet<String>,
|
||||
var all: Boolean
|
||||
) : FilterValue() {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other == null || other !is MultipleChoiceFilterValue) return false
|
||||
if (key != other.key) return false
|
||||
|
||||
return if (all) {
|
||||
other.all
|
||||
} else {
|
||||
!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(
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = FilterProfile::class,
|
||||
parentColumns = arrayOf("id"),
|
||||
childColumns = arrayOf("profile"),
|
||||
onDelete = CASCADE
|
||||
)],
|
||||
primaryKeys = ["key", "profile"]
|
||||
)
|
||||
data class SliderFilterValue(
|
||||
override val key: String,
|
||||
var value: Int
|
||||
) : FilterValue()
|
||||
|
||||
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable
|
||||
|
||||
typealias FilterValues = List<FilterWithValue<out FilterValue>>
|
||||
|
||||
fun FilterValues.getBooleanValue(key: String) =
|
||||
(this.find { it.value.key == key }!!.value as BooleanFilterValue).value
|
||||
|
||||
fun FilterValues.getSliderValue(key: String) =
|
||||
(this.find { it.value.key == key }!!.value as SliderFilterValue).value
|
||||
|
||||
fun FilterValues.getMultipleChoiceFilter(key: String) =
|
||||
this.find { it.value.key == key }!!.filter as MultipleChoiceFilter
|
||||
|
||||
fun FilterValues.getMultipleChoiceValue(key: String) =
|
||||
this.find { it.value.key == key }!!.value as MultipleChoiceFilterValue
|
||||
|
||||
const val FILTERS_DISABLED = -2L
|
||||
const val FILTERS_CUSTOM = -1L
|
||||
}
|
||||
@@ -5,16 +5,21 @@ import androidx.lifecycle.*
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.api.createApi
|
||||
import net.vonforst.evmap.api.goingelectric.GEChargepoint
|
||||
import net.vonforst.evmap.api.goingelectric.GEReferenceData
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.openchargemap.OCMConnection
|
||||
import net.vonforst.evmap.api.openchargemap.OCMReferenceData
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.*
|
||||
import net.vonforst.evmap.storage.*
|
||||
import net.vonforst.evmap.ui.cluster
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import java.io.IOException
|
||||
|
||||
@@ -32,10 +37,15 @@ internal fun getClusterDistance(zoom: Float): Int? {
|
||||
}
|
||||
}
|
||||
|
||||
class MapViewModel(application: Application, geApiKey: String) : AndroidViewModel(application) {
|
||||
private var api = GoingElectricApi.create(geApiKey, context = application)
|
||||
class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val apiType: Class<ChargepointApi<ReferenceData>>
|
||||
get() = api.javaClass
|
||||
val apiName: String
|
||||
get() = api.getName()
|
||||
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
private var api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
|
||||
|
||||
val bottomSheetState: MutableLiveData<Int> by lazy {
|
||||
MutableLiveData<Int>()
|
||||
@@ -49,39 +59,63 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
var source: LiveData<List<FilterValue>>? = null
|
||||
addSource(filterStatus) { status ->
|
||||
source?.let { removeSource(it) }
|
||||
source = db.filterValueDao().getFilterValues(status)
|
||||
source = db.filterValueDao().getFilterValues(status, prefs.dataSource)
|
||||
addSource(source!!) { result ->
|
||||
value = result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private val plugs: LiveData<List<Plug>> by lazy {
|
||||
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
|
||||
private val referenceData: LiveData<out ReferenceData> by lazy {
|
||||
val api = api
|
||||
when (api) {
|
||||
is GoingElectricApiWrapper -> {
|
||||
GEReferenceDataRepository(
|
||||
api,
|
||||
viewModelScope,
|
||||
db.geReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
is OpenChargeMapApiWrapper -> {
|
||||
OCMReferenceDataRepository(
|
||||
api,
|
||||
viewModelScope,
|
||||
db.ocmReferenceDataDao(),
|
||||
prefs
|
||||
).getReferenceData()
|
||||
}
|
||||
else -> {
|
||||
throw RuntimeException("no reference data implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
private val networks: LiveData<List<Network>> by lazy {
|
||||
NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks()
|
||||
private val filters = MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||
addSource(referenceData) { data ->
|
||||
val api = api
|
||||
value = api.getFilters(data, application.stringProvider())
|
||||
}
|
||||
}
|
||||
private val chargeCards: LiveData<List<ChargeCard>> by lazy {
|
||||
ChargeCardRepository(api, viewModelScope, db.chargeCardDao(), prefs).getChargeCards()
|
||||
}
|
||||
private val filters = getFilters(application, plugs, networks, chargeCards)
|
||||
|
||||
private val filtersWithValue: LiveData<FilterValues> by lazy {
|
||||
filtersWithValue(filters, filterValues)
|
||||
}
|
||||
|
||||
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
|
||||
db.filterProfileDao().getProfiles()
|
||||
db.filterProfileDao().getProfiles(prefs.dataSource)
|
||||
}
|
||||
|
||||
val chargeCardMap: LiveData<Map<Long, ChargeCard>> by lazy {
|
||||
MediatorLiveData<Map<Long, ChargeCard>>().apply {
|
||||
value = null
|
||||
addSource(chargeCards) {
|
||||
value = chargeCards.value?.map {
|
||||
it.id to it
|
||||
}?.toMap()
|
||||
addSource(referenceData) { data ->
|
||||
value = if (data is GEReferenceData) {
|
||||
data.chargecards.map {
|
||||
it.id to it.convert()
|
||||
}.toMap()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +125,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
value = 0
|
||||
addSource(filtersWithValue) { filtersWithValue ->
|
||||
value = filtersWithValue.count {
|
||||
it.filter.defaultValue() != it.value
|
||||
!it.value.hasSameValueAs(it.filter.defaultValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,7 +134,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
MediatorLiveData<Resource<List<ChargepointListItem>>>()
|
||||
.apply {
|
||||
value = Resource.loading(emptyList())
|
||||
listOf(mapPosition, filtersWithValue).forEach {
|
||||
listOf(mapPosition, filtersWithValue, referenceData).forEach {
|
||||
addSource(it) {
|
||||
reloadChargepoints()
|
||||
}
|
||||
@@ -110,6 +144,9 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
val filteredConnectors: MutableLiveData<Set<String>> by lazy {
|
||||
MutableLiveData<Set<String>>()
|
||||
}
|
||||
val filteredMinPower: MutableLiveData<Int> by lazy {
|
||||
MutableLiveData<Int>()
|
||||
}
|
||||
val filteredChargeCards: MutableLiveData<Set<Long>> by lazy {
|
||||
MutableLiveData<Set<Long>>()
|
||||
}
|
||||
@@ -119,11 +156,15 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
}
|
||||
val chargerDetails: MediatorLiveData<Resource<ChargeLocation>> by lazy {
|
||||
MediatorLiveData<Resource<ChargeLocation>>().apply {
|
||||
addSource(chargerSparse) { charger ->
|
||||
if (charger != null) {
|
||||
loadChargerDetails(charger)
|
||||
} else {
|
||||
value = null
|
||||
listOf(chargerSparse, referenceData).forEach {
|
||||
addSource(it) { _ ->
|
||||
val charger = chargerSparse.value
|
||||
val refData = referenceData.value
|
||||
if (charger != null && refData != null) {
|
||||
loadChargerDetails(charger, refData)
|
||||
} else {
|
||||
value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,13 +221,19 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
val av = availability.value
|
||||
val filters = filtersWithValue.value
|
||||
if (av?.status == Status.SUCCESS && filters != null) {
|
||||
value = Resource.success(av.data!!.applyFilters(filters))
|
||||
value = Resource.success(
|
||||
av.data!!.applyFilters(
|
||||
filteredConnectors.value,
|
||||
filteredMinPower.value
|
||||
)
|
||||
)
|
||||
} else {
|
||||
value = av
|
||||
}
|
||||
}
|
||||
addSource(availability, callback)
|
||||
addSource(filtersWithValue, callback)
|
||||
addSource(filteredConnectors, callback)
|
||||
addSource(filteredMinPower, callback)
|
||||
}
|
||||
}
|
||||
val myLocationEnabled: MutableLiveData<Boolean> by lazy {
|
||||
@@ -249,10 +296,12 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
suspend fun copyFiltersToCustom() {
|
||||
if (filterStatus.value == FILTERS_CUSTOM) return
|
||||
|
||||
db.filterValueDao().deleteFilterValuesForProfile(FILTERS_CUSTOM)
|
||||
filterValues.value?.forEach {
|
||||
db.filterValueDao().deleteFilterValuesForProfile(FILTERS_CUSTOM, prefs.dataSource)
|
||||
filterValues.value?.map {
|
||||
it.profile = FILTERS_CUSTOM
|
||||
db.filterValueDao().insert(it)
|
||||
it
|
||||
}?.let {
|
||||
db.filterValueDao().insert(*it.toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,165 +324,69 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
fun reloadChargepoints() {
|
||||
val pos = mapPosition.value ?: return
|
||||
val filters = filtersWithValue.value ?: return
|
||||
loadChargepoints(pos, filters)
|
||||
val referenceData = referenceData.value ?: return
|
||||
chargepointLoader(Triple(pos, filters, referenceData))
|
||||
}
|
||||
|
||||
private var chargepointLoader =
|
||||
throttleLatest(500L, viewModelScope) { data: Pair<MapPosition, FilterValues> ->
|
||||
throttleLatest(
|
||||
500L,
|
||||
viewModelScope
|
||||
) { data: Triple<MapPosition, FilterValues, ReferenceData> ->
|
||||
chargepoints.value = Resource.loading(chargepoints.value?.data)
|
||||
filteredConnectors.value = null
|
||||
filteredMinPower.value = null
|
||||
filteredChargeCards.value = null
|
||||
|
||||
val mapPosition = data.first
|
||||
val filters = data.second
|
||||
val result = getChargepointsWithFilters(mapPosition.bounds, mapPosition.zoom, filters)
|
||||
filteredConnectors.value = result.second
|
||||
filteredChargeCards.value = result.third
|
||||
chargepoints.value = result.first
|
||||
}
|
||||
|
||||
private fun loadChargepoints(
|
||||
mapPosition: MapPosition,
|
||||
filters: FilterValues
|
||||
) {
|
||||
chargepointLoader(Pair(mapPosition, filters))
|
||||
}
|
||||
|
||||
private suspend fun getChargepointsWithFilters(
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
filters: FilterValues
|
||||
): Triple<Resource<List<ChargepointListItem>>, Set<String>?, Set<Long>?> {
|
||||
val freecharging = filters.getBooleanValue("freecharging")
|
||||
val freeparking = filters.getBooleanValue("freeparking")
|
||||
val open247 = filters.getBooleanValue("open_247")
|
||||
val barrierfree = filters.getBooleanValue("barrierfree")
|
||||
val excludeFaults = filters.getBooleanValue("exclude_faults")
|
||||
val minPower = filters.getSliderValue("min_power")
|
||||
val minConnectors = filters.getSliderValue("min_connectors")
|
||||
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")
|
||||
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Triple(Resource.success(emptyList()), null, null)
|
||||
}
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
val filteredConnectors = if (connectorsVal.all) null else connectorsVal.values
|
||||
|
||||
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")
|
||||
if (chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) {
|
||||
// no chargeCards chosen
|
||||
return Triple(Resource.success(emptyList()), filteredConnectors, null)
|
||||
}
|
||||
val chargeCards = formatMultipleChoice(chargeCardsVal)
|
||||
val filteredChargeCards =
|
||||
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }.toSet()
|
||||
|
||||
val networksVal = filters.getMultipleChoiceValue("networks")
|
||||
if (networksVal.values.isEmpty() && !networksVal.all) {
|
||||
// no networks chosen
|
||||
return Triple(Resource.success(emptyList()), filteredConnectors, filteredChargeCards)
|
||||
}
|
||||
val networks = formatMultipleChoice(networksVal)
|
||||
|
||||
val categoriesVal = filters.getMultipleChoiceValue("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
|
||||
val useGeClustering = useClustering && geClusteringAvailable
|
||||
val clusterDistance = if (useClustering) getClusterDistance(zoom) else null
|
||||
|
||||
var startkey: Int? = null
|
||||
val data = mutableListOf<ChargepointListItem>()
|
||||
do {
|
||||
// load all pages of the response
|
||||
try {
|
||||
val response = api.getChargepoints(
|
||||
bounds.southwest.latitude,
|
||||
bounds.southwest.longitude,
|
||||
bounds.northeast.latitude,
|
||||
bounds.northeast.longitude,
|
||||
clustering = useGeClustering,
|
||||
zoom = zoom,
|
||||
clusterDistance = clusterDistance,
|
||||
freecharging = freecharging,
|
||||
minPower = minPower,
|
||||
freeparking = freeparking,
|
||||
open247 = open247,
|
||||
barrierfree = barrierfree,
|
||||
excludeFaults = excludeFaults,
|
||||
plugs = connectors,
|
||||
chargecards = chargeCards,
|
||||
networks = networks,
|
||||
categories = categories,
|
||||
startkey = startkey
|
||||
)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
return Triple(
|
||||
Resource.error(response.message(), chargepoints.value?.data),
|
||||
null,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
val body = response.body()!!
|
||||
data.addAll(body.chargelocations)
|
||||
startkey = body.startkey
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Triple(
|
||||
Resource.error(e.message, chargepoints.value?.data),
|
||||
filteredConnectors,
|
||||
filteredChargeCards
|
||||
)
|
||||
val api = api
|
||||
val refData = data.third
|
||||
var result = api.getChargepoints(refData, mapPosition.bounds, mapPosition.zoom, filters)
|
||||
if (result.status == Status.ERROR && result.data == null) {
|
||||
// keep old results if new data could not be loaded
|
||||
result = Resource.error(result.message, chargepoints.value?.data)
|
||||
}
|
||||
} while (startkey != null && startkey < 10000)
|
||||
chargepoints.value = result
|
||||
|
||||
var result = data.filter { it ->
|
||||
// apply filters which GoingElectric does not support natively
|
||||
if (it is ChargeLocation) {
|
||||
it.chargepoints
|
||||
.filter { it.power >= minPower }
|
||||
.filter { if (!connectorsVal.all) it.type in connectorsVal.values else true }
|
||||
.sumBy { it.count } >= minConnectors
|
||||
} else {
|
||||
true
|
||||
if (api is GoingElectricApiWrapper) {
|
||||
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
|
||||
filteredChargeCards.value =
|
||||
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }
|
||||
.toSet()
|
||||
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
|
||||
filteredConnectors.value =
|
||||
if (connectorsVal.all) null else connectorsVal.values.map {
|
||||
GEChargepoint.convertTypeFromGE(it)
|
||||
}.toSet()
|
||||
filteredMinPower.value = filters.getSliderValue("minPower")
|
||||
} else if (api is OpenChargeMapApiWrapper) {
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
|
||||
filteredConnectors.value =
|
||||
if (connectorsVal.all) null else connectorsVal.values.map {
|
||||
OCMConnection.convertConnectionTypeFromOCM(
|
||||
it.toLong(),
|
||||
refData as OCMReferenceData
|
||||
)
|
||||
}.toSet()
|
||||
filteredMinPower.value = filters.getSliderValue("minPower")
|
||||
}
|
||||
}
|
||||
if (!geClusteringAvailable && useClustering) {
|
||||
// apply local clustering if server side clustering is not available
|
||||
Dispatchers.IO.run {
|
||||
result = cluster(result, zoom, clusterDistance!!)
|
||||
}
|
||||
}
|
||||
|
||||
return Triple(Resource.success(result), filteredConnectors, filteredChargeCards)
|
||||
}
|
||||
|
||||
private fun formatMultipleChoice(connectorsVal: MultipleChoiceFilterValue) =
|
||||
if (connectorsVal.all) null else connectorsVal.values.joinToString(",")
|
||||
|
||||
private suspend fun loadAvailability(charger: ChargeLocation) {
|
||||
availability.value = Resource.loading(null)
|
||||
availability.value = getAvailability(charger)
|
||||
}
|
||||
|
||||
private fun loadChargerDetails(charger: ChargeLocation) {
|
||||
private var chargerLoadingTask: Job? = null
|
||||
|
||||
private fun loadChargerDetails(charger: ChargeLocation, referenceData: ReferenceData) {
|
||||
chargerDetails.value = Resource.loading(null)
|
||||
viewModelScope.launch {
|
||||
chargerLoadingTask?.cancel()
|
||||
chargerLoadingTask = viewModelScope.launch {
|
||||
try {
|
||||
val response = api.getChargepointDetail(charger.id)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
chargerDetails.value = Resource.error(response.message(), null)
|
||||
} else {
|
||||
chargerDetails.value =
|
||||
Resource.success(response.body()!!.chargelocations[0] as ChargeLocation)
|
||||
}
|
||||
chargerDetails.value = api.getChargepointDetail(referenceData, charger.id)
|
||||
} catch (e: IOException) {
|
||||
chargerDetails.value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
@@ -444,22 +397,19 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
fun loadChargerById(chargerId: Long) {
|
||||
chargerDetails.value = Resource.loading(null)
|
||||
chargerSparse.value = null
|
||||
viewModelScope.launch {
|
||||
val response = api.getChargepointDetail(chargerId)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
chargerSparse.value = null
|
||||
chargerDetails.value = Resource.error(response.message(), null)
|
||||
} else {
|
||||
val chargers = response.body()!!.chargelocations
|
||||
if (chargers.isNotEmpty()) {
|
||||
val charger = chargers[0] as ChargeLocation
|
||||
chargerDetails.value =
|
||||
Resource.success(charger)
|
||||
chargerSparse.value = charger} else {
|
||||
chargerDetails.value = Resource.error("not found", null)
|
||||
referenceData.observeForever(object : Observer<ReferenceData> {
|
||||
override fun onChanged(refData: ReferenceData) {
|
||||
referenceData.removeObserver(this)
|
||||
viewModelScope.launch {
|
||||
val response = api.getChargepointDetail(refData, chargerId)
|
||||
chargerDetails.value = response
|
||||
if (response.status == Status.SUCCESS) {
|
||||
chargerSparse.value = response.data
|
||||
} else {
|
||||
chargerSparse.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
51
app/src/main/res/drawable/ic_connector_ccs_typ1.xml
Normal file
51
app/src/main/res/drawable/ic_connector_ccs_typ1.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M9,18.7m-1.4,0a1.4,1.4 0,1 1,2.8 0a1.4,1.4 0,1 1,-2.8 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15,18.7m-1.4,0a1.4,1.4 0,1 1,2.8 0a1.4,1.4 0,1 1,-2.8 0" />
|
||||
<path
|
||||
android:pathData="M8.9,16.1h6.2c1.5,0 2.7,1.2 2.7,2.7l0,0c0,1.5 -1.2,2.7 -2.7,2.7H8.9c-1.5,0 -2.7,-1.2 -2.7,-2.7l0,0C6.2,17.3 7.4,16.1 8.9,16.1z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M14.7,6.4m-1.3,0a1.3,1.3 0,1 1,2.6 0a1.3,1.3 0,1 1,-2.6 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15.3,10.5m-0.8,0a0.8,0.8 0,1 1,1.6 0a0.8,0.8 0,1 1,-1.6 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8.7,10.5m-0.8,0a0.8,0.8 0,1 1,1.6 0a0.8,0.8 0,1 1,-1.6 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M9.3,6.4m-1.3,0a1.3,1.3 0,1 1,2.6 0a1.3,1.3 0,1 1,-2.6 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,13.1m-1.3,0a1.3,1.3 0,1 1,2.6 0a1.3,1.3 0,1 1,-2.6 0" />
|
||||
<path
|
||||
android:pathData="M12,9.1m-6.3,0a6.3,6.3 0,1 1,12.6 0a6.3,6.3 0,1 1,-12.6 0"
|
||||
android:strokeWidth="1.7"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M11,15.4h2v1.3h-2z" />
|
||||
<path
|
||||
android:pathData="M10.9,1.3L13.1,1.3"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M13.1,0.9l0,1.5l1.4,0.7l-0.7,-2.1z" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10.9,0.9l0,1.5l-1.4,0.7l0.7,-2.1z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_license.xml
Normal file
10
app/src/main/res/drawable/ic_license.xml
Normal 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="M11.88,9.14c1.28,0.06 1.61,1.15 1.63,1.66h1.79c-0.08,-1.98 -1.49,-3.19 -3.45,-3.19C9.64,7.61 8,9 8,12.14c0,1.94 0.93,4.24 3.84,4.24c2.22,0 3.41,-1.65 3.44,-2.95h-1.79c-0.03,0.59 -0.45,1.38 -1.63,1.44C10.55,14.83 10,13.81 10,12.14C10,9.25 11.28,9.16 11.88,9.14zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8S16.41,20 12,20z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
<item android:drawable="?selectableItemBackground" />
|
||||
</layer-list>
|
||||
@@ -0,0 +1,66 @@
|
||||
<?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="match_parent">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scroll"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/welcomeTitle"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<include
|
||||
android:id="@+id/rg_data_source"
|
||||
layout="@layout/data_source_select" />
|
||||
</ScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/pref_data_source"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeText2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/scroll" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeText2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="28dp"
|
||||
android:breakStrategy="balanced"
|
||||
android:text="@string/data_sources_description"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnGetStarted"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/scroll" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnGetStarted"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@string/lets_go"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/scroll" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
175
app/src/main/res/layout-land/fragment_onboarding_icons.xml
Normal file
175
app/src/main/res/layout-land/fragment_onboarding_icons.xml
Normal file
@@ -0,0 +1,175 @@
|
||||
<?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="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toTopOf="@+id/iconLabel1"
|
||||
app:layout_constraintEnd_toEndOf="@+id/iconLabel1"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/iconLabel1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.65"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
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_marginStart="72dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="< 11 kW"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintBottom_toTopOf="@id/icon4"
|
||||
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"
|
||||
app:layout_constraintEnd_toEndOf="@+id/iconLabel2"
|
||||
app:layout_constraintStart_toStartOf="@+id/iconLabel2"
|
||||
app:layout_constraintTop_toTopOf="@id/icon1"
|
||||
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_marginStart="16dp"
|
||||
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"
|
||||
app:layout_constraintEnd_toEndOf="@+id/iconLabel3"
|
||||
app:layout_constraintStart_toStartOf="@+id/iconLabel3"
|
||||
app:layout_constraintTop_toTopOf="@id/icon1"
|
||||
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_marginStart="16dp"
|
||||
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="24dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/iconLabel4"
|
||||
app:layout_constraintEnd_toEndOf="@+id/iconLabel4"
|
||||
app:layout_constraintStart_toStartOf="@+id/iconLabel4"
|
||||
app:layout_constraintTop_toBottomOf="@+id/iconLabel1"
|
||||
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_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/iconLabel5"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="@id/iconLabel1"
|
||||
app:layout_constraintTop_toBottomOf="@+id/icon4" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
app:layout_constraintEnd_toEndOf="@+id/iconLabel5"
|
||||
app:layout_constraintStart_toStartOf="@+id/iconLabel5"
|
||||
app:layout_constraintTop_toBottomOf="@+id/iconLabel1"
|
||||
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_marginStart="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="≥ 100 kW"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintEnd_toEndOf="@id/iconLabel3"
|
||||
app:layout_constraintStart_toEndOf="@+id/iconLabel4"
|
||||
app:layout_constraintTop_toBottomOf="@+id/icon5" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="72dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/welcome_2_title"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeText2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/iconLabel3"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeText2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="72dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="42dp"
|
||||
android:breakStrategy="balanced"
|
||||
android:text="@string/welcome_2"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnGetStarted"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/iconLabel3" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnGetStarted"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="72dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@string/got_it"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/iconLabel3" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
61
app/src/main/res/layout-land/fragment_onboarding_welcome.xml
Normal file
61
app/src/main/res/layout-land/fragment_onboarding_welcome.xml
Normal file
@@ -0,0 +1,61 @@
|
||||
<?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="match_parent">
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/animation_view"
|
||||
android:layout_width="256dp"
|
||||
android:layout_height="256dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginBottom="48dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:lottie_autoPlay="true"
|
||||
app:lottie_rawRes="@raw/logo_anim"
|
||||
app:lottie_speed="0.75" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/welcome_to_evmap"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeText1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/animation_view" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeText1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="56dp"
|
||||
android:breakStrategy="balanced"
|
||||
android:text="@string/welcome_1"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnGetStarted"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/animation_view" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnGetStarted"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@string/get_started"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/animation_view" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -10,9 +10,7 @@
|
||||
android:name="net.vonforst.evmap.navigation.NavHostFragment"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/nav_graph" />
|
||||
android:fitsSystemWindows="true" />
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/nav_view"
|
||||
|
||||
43
app/src/main/res/layout/data_source_select.xml
Normal file
43
app/src/main/res/layout/data_source_select.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rbGoingElectric"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/data_source_goingelectric"
|
||||
android:textColor="#098ac7"
|
||||
android:buttonTint="#098ac7"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView27"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:text="@string/data_source_goingelectric_desc" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rbOpenChargeMap"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/data_source_openchargemap"
|
||||
android:textColor="#587e25"
|
||||
android:buttonTint="#587e25"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView28"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-8dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:text="@string/data_source_openchargemap_desc" />
|
||||
|
||||
</RadioGroup>
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.goingelectric.ChargeLocation" />
|
||||
<import type="net.vonforst.evmap.model.ChargeLocation" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.goingelectric.Chargepoint" />
|
||||
<import type="net.vonforst.evmap.model.Chargepoint" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.goingelectric.ChargeCard" />
|
||||
<import type="net.vonforst.evmap.model.ChargeCard" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.availability.ChargeLocationStatus" />
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.ChargepointApiKt" />
|
||||
|
||||
<variable
|
||||
name="charger"
|
||||
type="Resource<ChargeLocation>" />
|
||||
@@ -51,6 +53,10 @@
|
||||
name="expanded"
|
||||
type="Boolean" />
|
||||
|
||||
<variable
|
||||
name="apiName"
|
||||
type="String" />
|
||||
|
||||
</data>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
@@ -140,7 +146,7 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@{charger.data.formatChargepoints()}"
|
||||
android:text="@{charger.data.formatChargepoints(ChargepointApiKt.stringProvider(context))}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintEnd_toStartOf="@+id/txtDistance"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
@@ -268,14 +274,13 @@
|
||||
app:layout_constraintGuide_begin="16dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/goingelectricButton"
|
||||
android:id="@+id/sourceButton"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/go_to_goingelectric"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:text="@{@string/source(apiName)}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView4" />
|
||||
@@ -322,7 +327,7 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:contentDescription="@string/verified"
|
||||
android:tooltipText="@string/verified_desc"
|
||||
android:tooltipText="@{@string/verified_desc(apiName)}"
|
||||
app:goneUnless="@{ charger.data.verified }"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/txtName"
|
||||
app:layout_constraintStart_toEndOf="@+id/imgFaultReport"
|
||||
@@ -347,6 +352,23 @@
|
||||
app:srcCompat="@drawable/ic_map_marker_fault"
|
||||
tools:targetApi="o" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtLicense"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:textStyle="italic"
|
||||
android:text="@{charger.data.license}"
|
||||
android:breakStrategy="balanced"
|
||||
app:goneUnless="@{charger.data.license != null}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/sourceButton"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:text="The data is provided under the National Oman Open Data LicensE (NOODLE), Version 3.14, and may be used for any purpose whatsoever." />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
71
app/src/main/res/layout/dialog_data_source_select.xml
Normal file
71
app/src/main/res/layout/dialog_data_source_select.xml
Normal file
@@ -0,0 +1,71 @@
|
||||
<?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">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialogTitle"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/pref_data_source"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dataSourceDescription"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/data_sources_description"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/dialogTitle" />
|
||||
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnOK"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/ok"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/rg_data_source" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnCancel"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/cancel"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnOK"
|
||||
app:layout_constraintTop_toBottomOf="@+id/rg_data_source" />
|
||||
|
||||
<include
|
||||
android:id="@+id/rg_data_source"
|
||||
layout="@layout/data_source_select"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/dataSourceDescription" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,199 +0,0 @@
|
||||
<?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="<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>
|
||||
@@ -7,13 +7,14 @@
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.ChargepriceViewModel" />
|
||||
<import type="net.vonforst.evmap.viewmodel.Status" />
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<variable
|
||||
name="vm"
|
||||
type="ChargepriceViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -21,7 +22,7 @@
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@@ -51,137 +52,194 @@
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/chargeprice_select_connector"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar_container" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/connectors_list"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:data="@{vm.charger.chargepointsMerged}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
tools:itemCount="3"
|
||||
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_connector_button"
|
||||
tools:orientation="horizontal" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@{String.format(@string/chargeprice_battery_range, vm.batteryRange[0], vm.batteryRange[1])}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/connectors_list"
|
||||
tools:text="Charge from 20% to 80%" />
|
||||
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/battery_range"
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:valueFrom="0.0"
|
||||
android:valueTo="100.0"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView2"
|
||||
app:values="@={vm.batteryRange}" />
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/charge_prices_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:data="@{vm.chargePricesForChargepoint.data}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/battery_range"
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/item_chargeprice" />
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView8"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_no_tariffs_found"
|
||||
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS && vm.chargePricesForChargepoint.data.size() == 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/chargeprice_select_connector"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/vehicle_selection" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView9"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_no_compatible_connectors"
|
||||
app:goneUnless="@{vm.noCompatibleConnectors}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/connectors_list"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:data="@{vm.charger.chargepointsMerged}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView"
|
||||
tools:itemCount="3"
|
||||
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_connector_button"
|
||||
tools:orientation="horizontal" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_select_car_first"
|
||||
app:goneUnless="@{vm.vehicle == null}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnSettings"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
<TextView
|
||||
android:id="@+id/textView2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@{String.format(@string/chargeprice_battery_range, vm.batteryRange[0], vm.batteryRange[1])}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
|
||||
android:textColor="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/connectors_list"
|
||||
tools:text="Charge from 20% to 80%" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar5"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.LOADING}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/charge_prices_list"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
<TextView
|
||||
android:id="@+id/textView4"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@{@string/chargeprice_duration(BindingAdaptersKt.time((int) Math.round(vm.chargepriceMetaForChargepoint.data.duration)))}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
|
||||
android:textColor="?colorPrimary"
|
||||
app:invisibleUnlessAnimated="@{!vm.batteryRangeSliderDragging && vm.chargepriceMetaForChargepoint.status == Status.SUCCESS}"
|
||||
app:layout_constraintStart_toEndOf="@+id/textView2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/connectors_list"
|
||||
tools:text="(25 min)" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSettings"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/settings"
|
||||
app:goneUnless="@{vm.vehicle == null}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView3" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<TextView
|
||||
android:id="@+id/tvVehicleHeader"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/chargeprice_vehicle"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
|
||||
android:textColor="?colorPrimary"
|
||||
app:goneUnless="@{vm.vehicles.data != null && vm.vehicles.data.size() > 1}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/vehicle_selection"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvVehicleHeader"
|
||||
app:data="@{vm.vehicles.data}"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:goneUnless="@{vm.vehicles.data != null && vm.vehicles.data.size() > 1}"
|
||||
android:orientation="horizontal"
|
||||
tools:listitem="@layout/item_chargeprice_vehicle_chip" />
|
||||
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/battery_range"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:valueFrom="0.0"
|
||||
android:valueTo="100.0"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView2"
|
||||
app:values="@={vm.batteryRange}" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/charge_prices_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:data="@{vm.chargePricesForChargepoint.data}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/battery_range"
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/item_chargeprice" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView8"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_no_tariffs_found"
|
||||
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.SUCCESS && vm.chargePricesForChargepoint.data.size() == 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView9"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_no_compatible_connectors"
|
||||
app:goneUnless="@{vm.noCompatibleConnectors}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/chargeprice_select_car_first"
|
||||
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS && vm.vehicles.data.size() == 0}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnSettings"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar5"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:goneUnless="@{vm.chargePricesForChargepoint.status == Status.LOADING || vm.vehicles.status == Status.LOADING}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/charge_prices_list"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/charge_prices_list"
|
||||
app:layout_constraintTop_toTopOf="@+id/charge_prices_list" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnSettings"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/settings"
|
||||
app:goneUnless="@{vm.vehicles.status == Status.SUCCESS && vm.vehicles.data.size() == 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView3" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
@@ -1,22 +0,0 @@
|
||||
<?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">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.goingelectric.ChargerPhoto" />
|
||||
|
||||
<import type="java.util.List" />
|
||||
|
||||
<variable
|
||||
name="photos"
|
||||
type="List<ChargerPhoto>" />
|
||||
</data>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/gallery"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/black"
|
||||
app:data="@{photos}" />
|
||||
</layout>
|
||||
@@ -144,7 +144,8 @@
|
||||
app:chargeCards="@{vm.chargeCardMap}"
|
||||
app:filteredChargeCards="@{vm.filteredChargeCards}"
|
||||
app:distance="@{vm.chargerDistance}"
|
||||
app:expanded="@{vm.bottomSheetState != BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED && vm.bottomSheetState != BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN}" />
|
||||
app:expanded="@{vm.bottomSheetState != BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED && vm.bottomSheetState != BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN}"
|
||||
app:apiName="@{vm.apiName}" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
|
||||
33
app/src/main/res/layout/fragment_onboarding.xml
Normal file
33
app/src/main/res/layout/fragment_onboarding.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?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:id="@+id/rl_create_account"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToPadding="false"
|
||||
android:overScrollMode="never" />
|
||||
|
||||
|
||||
<com.rd.PageIndicatorView
|
||||
android:id="@+id/pageIndicatorView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="24dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
app:piv_animationType="worm"
|
||||
app:piv_dynamicCount="true"
|
||||
app:piv_interactiveAnimation="true"
|
||||
app:piv_selectedColor="@color/colorPrimary"
|
||||
app:piv_unselectedColor="@color/colorPrimaryTransparent"
|
||||
app:piv_viewPager="@id/viewPager"
|
||||
app:piv_padding="8dp"
|
||||
app:piv_radius="6dp" />
|
||||
|
||||
</LinearLayout>
|
||||
63
app/src/main/res/layout/fragment_onboarding_data_source.xml
Normal file
63
app/src/main/res/layout/fragment_onboarding_data_source.xml
Normal file
@@ -0,0 +1,63 @@
|
||||
<?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="match_parent">
|
||||
|
||||
<include
|
||||
android:id="@+id/rg_data_source"
|
||||
layout="@layout/data_source_select"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="56dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnGetStarted"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/pref_data_source"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeText2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeText2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/data_sources_description"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:breakStrategy="balanced"
|
||||
app:layout_constraintBottom_toTopOf="@+id/rg_data_source"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnGetStarted"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@string/lets_go"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
170
app/src/main/res/layout/fragment_onboarding_icons.xml
Normal file
170
app/src/main/res/layout/fragment_onboarding_icons.xml
Normal file
@@ -0,0 +1,170 @@
|
||||
<?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="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toTopOf="@+id/iconLabel1"
|
||||
app:layout_constraintEnd_toEndOf="@+id/iconLabel1"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/iconLabel1"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
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:layout_marginBottom="28dp"
|
||||
android:text="< 11 kW"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
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"
|
||||
app:layout_constraintEnd_toEndOf="@+id/iconLabel2"
|
||||
app:layout_constraintStart_toStartOf="@+id/iconLabel2"
|
||||
app:layout_constraintTop_toTopOf="@id/icon1"
|
||||
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"
|
||||
app:layout_constraintEnd_toEndOf="@+id/iconLabel3"
|
||||
app:layout_constraintStart_toStartOf="@+id/iconLabel3"
|
||||
app:layout_constraintTop_toTopOf="@id/icon1"
|
||||
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"
|
||||
app:layout_constraintEnd_toEndOf="@+id/iconLabel4"
|
||||
app:layout_constraintStart_toStartOf="@+id/iconLabel4"
|
||||
app:layout_constraintTop_toTopOf="@id/icon1"
|
||||
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"
|
||||
app:layout_constraintEnd_toEndOf="@+id/iconLabel5"
|
||||
app:layout_constraintStart_toStartOf="@+id/iconLabel5"
|
||||
app:layout_constraintTop_toTopOf="@id/icon1"
|
||||
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/welcomeTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/welcome_2_title"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeText2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeText2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="56dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/welcome_2"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:breakStrategy="balanced"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnGetStarted"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnGetStarted"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@string/got_it"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
65
app/src/main/res/layout/fragment_onboarding_welcome.xml
Normal file
65
app/src/main/res/layout/fragment_onboarding_welcome.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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="match_parent">
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/animation_view"
|
||||
android:layout_width="256dp"
|
||||
android:layout_height="256dp"
|
||||
android:layout_marginBottom="28dp"
|
||||
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"
|
||||
app:lottie_autoPlay="true"
|
||||
app:lottie_rawRes="@raw/logo_anim"
|
||||
app:lottie_speed="0.75" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/welcome_to_evmap"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
|
||||
app:layout_constraintBottom_toTopOf="@+id/welcomeText1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/welcomeText1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="56dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/welcome_1"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:breakStrategy="balanced"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnGetStarted"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnGetStarted"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@string/get_started"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.ortiz.touchview.TouchImageView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="fitCenter"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:src="@tools:sample/backgrounds/scenic" />
|
||||
27
app/src/main/res/layout/item_chargeprice_vehicle_chip.xml
Normal file
27
app/src/main/res/layout/item_chargeprice_vehicle_chip.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.chargeprice.ChargepriceCar" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="ChargepriceCar" />
|
||||
|
||||
<variable
|
||||
name="selectedItem"
|
||||
type="ChargepriceCar" />
|
||||
</data>
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
style="@style/Widget.MaterialComponents.Chip.Choice"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_marginRight="4dp"
|
||||
android:text="@{item.brand + ' ' + item.name}"
|
||||
android:checked="@{item == selectedItem}"
|
||||
tools:text="Tesla Model 2" />
|
||||
</layout>
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.goingelectric.Chargepoint" />
|
||||
<import type="net.vonforst.evmap.model.Chargepoint" />
|
||||
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<variable
|
||||
name="hours"
|
||||
type="net.vonforst.evmap.api.goingelectric.OpeningHoursDays" />
|
||||
type="net.vonforst.evmap.model.OpeningHoursDays" />
|
||||
|
||||
<variable
|
||||
name="dayOfWeek"
|
||||
|
||||
@@ -6,29 +6,57 @@
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.UtilsKt" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.Status" />
|
||||
|
||||
<import type="net.vonforst.evmap.ui.BindingAdaptersKt" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.ChargepointApiKt" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="net.vonforst.evmap.viewmodel.FavoritesViewModel.FavoritesListItem" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView15"
|
||||
android:layout_width="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="wrap_content"
|
||||
android:text="@{item.charger.name}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Parkhaus" />
|
||||
android:padding="16dp"
|
||||
android:background="@drawable/selectable_opaque_background">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView15"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.charger.name}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Parkhaus" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView2"
|
||||
@@ -51,46 +79,64 @@
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@{item.charger.formatChargepoints()}"
|
||||
android:text="@{item.charger.formatChargepoints(ChargepointApiKt.stringProvider(context))}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintEnd_toStartOf="@+id/textView7"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView2"
|
||||
tools:text="2x Typ 2 22 kW" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView16"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{@string/distance_format(item.distance)}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="9999,9 km" />
|
||||
<TextView
|
||||
android:id="@+id/textView16"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@{@string/distance_format(item.distance)}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:goneUnless="@{item.distance != null}"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnDelete"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="9999,9 km" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView7"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/rounded_rect"
|
||||
android:padding="2dp"
|
||||
android:text="@{String.format("%s/%d", BindingAdaptersKt.availabilityText(item.available.data), item.total)}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:textColor="@android:color/white"
|
||||
app:backgroundTintAvailability="@{item.available.data}"
|
||||
app:goneUnless="@{item.available.status == Status.SUCCESS}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:backgroundTint="@color/available"
|
||||
tools:text="80/99" />
|
||||
<TextView
|
||||
android:id="@+id/textView7"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/rounded_rect"
|
||||
android:padding="2dp"
|
||||
android:text="@{String.format("%s/%d", BindingAdaptersKt.availabilityText(item.available.data), item.total)}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:textColor="@android:color/white"
|
||||
app:backgroundTintAvailability="@{item.available.data}"
|
||||
app:goneUnless="@{item.available.status == Status.SUCCESS}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnDelete"
|
||||
tools:backgroundTint="@color/available"
|
||||
tools:text="80/99" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar4"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
app:goneUnless="@{item.available.status == Status.LOADING}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar4"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:goneUnless="@{item.available.status == Status.LOADING}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnDelete" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btnDelete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
app:tint="?colorControlNormal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_delete"
|
||||
android:contentDescription="@string/delete" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
||||
</layout>
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.BooleanFilter" />
|
||||
<import type="net.vonforst.evmap.model.BooleanFilter" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.BooleanFilterValue" />
|
||||
<import type="net.vonforst.evmap.model.BooleanFilterValue" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.FilterWithValue" />
|
||||
<import type="net.vonforst.evmap.model.FilterWithValue" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.MultipleChoiceFilter" />
|
||||
<import type="net.vonforst.evmap.model.MultipleChoiceFilter" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue" />
|
||||
<import type="net.vonforst.evmap.model.MultipleChoiceFilterValue" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.FilterWithValue" />
|
||||
<import type="net.vonforst.evmap.model.FilterWithValue" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.MultipleChoiceFilter" />
|
||||
<import type="net.vonforst.evmap.model.MultipleChoiceFilter" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue" />
|
||||
<import type="net.vonforst.evmap.model.MultipleChoiceFilterValue" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.FilterWithValue" />
|
||||
<import type="net.vonforst.evmap.model.FilterWithValue" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.SliderFilter" />
|
||||
<import type="net.vonforst.evmap.model.SliderFilter" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.SliderFilterValue" />
|
||||
<import type="net.vonforst.evmap.model.SliderFilterValue" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.FilterWithValue" />
|
||||
<import type="net.vonforst.evmap.model.FilterWithValue" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<item
|
||||
android:id="@+id/menu_edit"
|
||||
android:icon="@drawable/ic_edit"
|
||||
android:title="@string/edit_on_goingelectric"
|
||||
android:title="@string/edit_at_datasource"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
|
||||
@@ -2,21 +2,13 @@
|
||||
<navigation 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"
|
||||
android:id="@+id/nav_graph"
|
||||
app:startDestination="@id/map">
|
||||
android:id="@+id/nav_graph">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/map"
|
||||
android:name="net.vonforst.evmap.fragment.MapFragment"
|
||||
android:label="MapFragment"
|
||||
tools:layout="@layout/fragment_map">
|
||||
<action
|
||||
android:id="@+id/action_map_to_galleryFragment"
|
||||
app:destination="@id/gallery"
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:exitAnim="@animator/nav_default_exit_anim"
|
||||
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_filterFragment"
|
||||
app:destination="@id/filter"
|
||||
@@ -38,9 +30,6 @@
|
||||
app:enterAnim="@animator/nav_default_enter_anim"
|
||||
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_welcome"
|
||||
app:destination="@id/welcome" />
|
||||
<action
|
||||
android:id="@+id/action_map_to_update_060_androidauto"
|
||||
app:destination="@id/update_060_androidauto" />
|
||||
@@ -59,11 +48,6 @@
|
||||
android:name="net.vonforst.evmap.fragment.SettingsFragment"
|
||||
android:label="@string/settings"
|
||||
tools:layout="@layout/fragment_preference" />
|
||||
<fragment
|
||||
android:id="@+id/gallery"
|
||||
android:name="net.vonforst.evmap.fragment.GalleryFragment"
|
||||
android:label="GalleryFragment"
|
||||
tools:layout="@layout/fragment_gallery" />
|
||||
<fragment
|
||||
android:id="@+id/favs"
|
||||
android:name="net.vonforst.evmap.fragment.FavoritesFragment"
|
||||
@@ -101,17 +85,20 @@
|
||||
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" />
|
||||
<dialog
|
||||
android:id="@+id/update_060_androidauto"
|
||||
android:name="net.vonforst.evmap.fragment.updatedialogs.Update060AndroidAutoDialogFramgent"
|
||||
android:label="@string/welcome_to_evmap"
|
||||
tools:layout="@layout/dialog_update_060_androidauto" />
|
||||
<chrome
|
||||
<custom
|
||||
android:id="@+id/report_new_charger"
|
||||
app:url="@string/report_new_charger_url" />
|
||||
app:customDestination="report_new_charger" />
|
||||
<fragment
|
||||
android:id="@+id/onboarding"
|
||||
android:name="net.vonforst.evmap.fragment.OnboardingFragment"
|
||||
android:label="OnboardingFragment">
|
||||
<action
|
||||
android:id="@+id/action_onboarding_to_map"
|
||||
app:destination="@id/map" />
|
||||
</fragment>
|
||||
</navigation>
|
||||
3366
app/src/main/res/raw/logo_anim.json
Normal file
3366
app/src/main/res/raw/logo_anim.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,4 +10,8 @@
|
||||
<item>immer an</item>
|
||||
<item>immer aus</item>
|
||||
</string-array>
|
||||
<string-array name="pref_data_source_names">
|
||||
<item>GoingElectric.de</item>
|
||||
<item>Open Charge Map</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -24,7 +24,7 @@
|
||||
<string name="realtime_data_unavailable">Echtzeitstatus nicht verfügbar</string>
|
||||
<string name="realtime_data_loading">Prüfe Echtzeitstatus…</string>
|
||||
<string name="realtime_data_source">Quelle Echtzeitdaten (beta): %s</string>
|
||||
<string name="go_to_goingelectric">Quelle: goingelectric.de</string>
|
||||
<string name="source">Quelle: %s</string>
|
||||
<string name="search">Suche</string>
|
||||
<string name="menu_map">Karte</string>
|
||||
<string name="menu_favs">Favoriten</string>
|
||||
@@ -88,6 +88,7 @@
|
||||
<string name="fault_report">Störungsmeldung</string>
|
||||
<string name="fault_report_date">Störungsmeldung (Letztes Update: %s)</string>
|
||||
<string name="filter_networks">Verbünde</string>
|
||||
<string name="filter_operators">Betreiber</string>
|
||||
<string name="filter_chargecards">Ladetarife</string>
|
||||
<string name="all_selected">Alle ausgewählt</string>
|
||||
<string name="number_selected">%d ausgewählt</string>
|
||||
@@ -110,7 +111,7 @@
|
||||
<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="edit_at_datasource">bei %s bearbeiten</string>
|
||||
<string name="categories">Kategorien</string>
|
||||
<string name="category_car_dealership">Autohaus</string>
|
||||
<string name="category_service_on_motorway">Autobahnraststätte</string>
|
||||
@@ -147,9 +148,11 @@
|
||||
<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="welcome_to_evmap">Willkommen bei EVMap</string>
|
||||
<string name="welcome_1">Finde Ladestationen für Elektroautos in deiner Nähe.</string>
|
||||
<string name="welcome_2_title">Auf die Leistung kommt es an</string>
|
||||
<string name="welcome_2">Die Farbe einer Ladestatione auf der Karte zeigt dir die maximale Ladeleistung.</string>
|
||||
<string name="welcome_3">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>
|
||||
@@ -160,7 +163,7 @@
|
||||
</plurals>
|
||||
<string name="navigate">Navigieren</string>
|
||||
<string name="verified">Verifiziert</string>
|
||||
<string name="verified_desc">Verifiziert von der GoingElectric.de Community – nicht zwangsläufig auch aktuell verfügbar.</string>
|
||||
<string name="verified_desc">Verifiziert von der %s Community – nicht zwangsläufig auch aktuell verfügbar.</string>
|
||||
<string name="update_060_androidauto_title">Neues Update: Android Auto</string>
|
||||
<string name="update_060_androidauto_text">Mit diesem neuen Update kannst du EVMap nutzen, um Ladestationen in der Nähe auf unterstützen Autos direkt aus Android Auto zu finden. Öffne einfach die EVMap-App aus dem Menü von Android Auto.</string>
|
||||
<string name="charge_price_format">%1$.2f %2$s</string>
|
||||
@@ -178,12 +181,14 @@
|
||||
<string name="chargeprice_base_fee">Fixkosten: %1$.2f %2$s/Monat</string>
|
||||
<string name="chargeprice_min_spend">Mindestumsatz: %1$.2f %2$s/Monat</string>
|
||||
<string name="settings_chargeprice">Preisvergleich</string>
|
||||
<string name="pref_my_vehicle">Mein Fahrzeug</string>
|
||||
<string name="pref_my_vehicle">Meine Fahrzeuge</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Nur Tarife ohne monatliche Gebühren</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs">Exklusive Energiekunden-Tarife anzeigen</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Einige Anbieter bieten für ihre Kunden (z.B. Haushaltsstrom, Gas) günstigere Tarife an</string>
|
||||
<string name="chargeprice_select_car_first">Bitte wähle zuerst dein Auto in den Einstellungen aus.</string>
|
||||
<string name="chargeprice_battery_range">Laden von %1$.0f%% bis %2$.0f%%</string>
|
||||
<string name="chargeprice_duration">(ca. %s)</string>
|
||||
<string name="chargeprice_vehicle">Fahrzeug</string>
|
||||
<string name="edit_on_goingelectric_info">Falls hier nur eine leere Seite erscheint, logge dich bitte zuerst bei GoingElectric.de ein.</string>
|
||||
<string name="close">schließen</string>
|
||||
<string name="chargeprice_title">Preisvergleich</string>
|
||||
@@ -192,6 +197,9 @@
|
||||
<string name="pref_chargeprice_currency">Währung</string>
|
||||
<string name="pref_my_tariffs">Meine Tarife</string>
|
||||
<string name="chargeprice_all_tariffs_selected">alle Tarife ausgewählt</string>
|
||||
<string name="license">Lizenz</string>
|
||||
<string name="settings_charger_data">Ladesäulen</string>
|
||||
<string name="pref_data_source">Datenquelle</string>
|
||||
<string-array name="pref_chargeprice_currency_names">
|
||||
<item>Schweizer Franken (CHF)</item>
|
||||
<item>Tschechische Krone (CZK)</item>
|
||||
@@ -211,4 +219,13 @@
|
||||
<item quantity="other">%d Tarife ausgewählt</item>
|
||||
</plurals>
|
||||
<string name="unknown_operator">Unbekannter Betreiber</string>
|
||||
<string name="data_sources_description">EVMap unterstützt verschiedene Datenquellen. Bitte wähle aus, welche du nutzen möchtest. Du kannst sie später in den Einstellungen der App ändern.</string>
|
||||
<string name="data_source_goingelectric">GoingElectric.de</string>
|
||||
<string name="data_source_openchargemap">Open Charge Map</string>
|
||||
<string name="data_source_goingelectric_desc">Sehr gute Abdeckung in Deutschland, Österreich, Schweiz und vielen angrenzenden Ländern. Beschreibungen in Deutsch. Von der Community gepflegt.</string>
|
||||
<string name="data_source_openchargemap_desc"><![CDATA[Weltweite Abdeckung mit variierender Qualität. Beschreibungen in Englisch oder Landessprache. Von der Community gepflegt & offizielle Verzeichnisse einiger Länder (z.B. Nordamerika, UK, Frankreich, Norwegen).]]></string>
|
||||
<string name="next">weiter</string>
|
||||
<string name="get_started">Los geht\'s</string>
|
||||
<string name="got_it">Alles klar</string>
|
||||
<string name="lets_go">Und los</string>
|
||||
</resources>
|
||||
|
||||
@@ -48,4 +48,12 @@
|
||||
<item>SEK</item>
|
||||
<item>USD</item>
|
||||
</string-array>
|
||||
<string-array name="pref_data_source_names">
|
||||
<item>GoingElectric.de</item>
|
||||
<item>Open Charge Map</item>
|
||||
</string-array>
|
||||
<string-array name="pref_data_source_values" tranlatable="false">
|
||||
<item>goingelectric</item>
|
||||
<item>openchargemap</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -1,6 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="ChromeCustomTabsNavigator">
|
||||
<attr name="url" format="reference" />
|
||||
<declare-styleable name="CustomNavigator">
|
||||
<attr name="customDestination" format="string" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="MultiSelectDialogPreference">
|
||||
<attr name="showAllButton" format="boolean" />
|
||||
<attr name="defaultToAll" format="boolean" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#4caf50</color>
|
||||
<color name="colorPrimaryTransparent">#444caf50</color>
|
||||
<color name="colorPrimaryVariant">#087f23</color>
|
||||
<color name="colorSecondary">#00e676</color>
|
||||
<color name="colorSecondaryVariant">#00b248</color>
|
||||
|
||||
@@ -7,5 +7,4 @@
|
||||
<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>
|
||||
@@ -23,7 +23,7 @@
|
||||
<string name="realtime_data_unavailable">Real-time status unavailable</string>
|
||||
<string name="realtime_data_loading">Checking real-time status…</string>
|
||||
<string name="realtime_data_source">Real-time status source (beta): %s</string>
|
||||
<string name="go_to_goingelectric">Source: goingelectric.de</string>
|
||||
<string name="source">Source: %s</string>
|
||||
<string name="search">Search</string>
|
||||
<string name="menu_map">Map</string>
|
||||
<string name="menu_favs">Favorites</string>
|
||||
@@ -87,6 +87,7 @@
|
||||
<string name="fault_report">Fault report</string>
|
||||
<string name="fault_report_date">Fault report (last update: %s)</string>
|
||||
<string name="filter_networks">Networks</string>
|
||||
<string name="filter_operators">Operators</string>
|
||||
<string name="filter_chargecards">Payment methods</string>
|
||||
<string name="all_selected">All selected</string>
|
||||
<string name="number_selected">%d selected</string>
|
||||
@@ -109,7 +110,7 @@
|
||||
<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="edit_at_datasource">edit at %s</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>
|
||||
@@ -146,9 +147,11 @@
|
||||
<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="welcome_to_evmap">Welcome to EVMap</string>
|
||||
<string name="welcome_1">Find electric vehicle chargers around you.</string>
|
||||
<string name="welcome_2_title">You\'ve got the power</string>
|
||||
<string name="welcome_2">The color of a charger on the map shows you its maximum charging power.</string>
|
||||
<string name="welcome_3">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>
|
||||
@@ -159,7 +162,7 @@
|
||||
</plurals>
|
||||
<string name="navigate">Navigate</string>
|
||||
<string name="verified">verified</string>
|
||||
<string name="verified_desc">Charger verified by a member at the GoingElectric.de community — not necessarily working right now.</string>
|
||||
<string name="verified_desc">Charger verified by a member at the %s community — not necessarily working right now.</string>
|
||||
<string name="update_060_androidauto_title">New update: Android Auto</string>
|
||||
<string name="update_060_androidauto_text">With this new update, you can also use EVMap to find nearby chargers from within Android Auto on supported cars. Simply select the EVMap app in the Android Auto menu.</string>
|
||||
<string name="charge_price_format">%2$s%1$.2f</string>
|
||||
@@ -178,11 +181,13 @@
|
||||
<string name="chargeprice_base_fee">Base fee: %2$s%1$.2f/month</string>
|
||||
<string name="chargeprice_min_spend">Minimum spend: %2$s%1$.2f/month</string>
|
||||
<string name="settings_chargeprice">Price comparison</string>
|
||||
<string name="pref_my_vehicle">My vehicle</string>
|
||||
<string name="pref_my_vehicle">My vehicles</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Only show plans with no monthly fees</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs">Show customer-exclusive plans</string>
|
||||
<string name="chargeprice_select_car_first">Please first select your car model in the settings.</string>
|
||||
<string name="chargeprice_battery_range">Charge from %1$.0f%% to %2$.0f%%</string>
|
||||
<string name="chargeprice_duration">(approx. %s)</string>
|
||||
<string name="chargeprice_vehicle">Vehicle</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Some providers offer cheaper plans exclusively to their customers (e.g., household electricity, gas)</string>
|
||||
<string name="close">close</string>
|
||||
<string name="chargeprice_title">Prices</string>
|
||||
@@ -191,9 +196,21 @@
|
||||
<string name="pref_chargeprice_currency">Currency</string>
|
||||
<string name="pref_my_tariffs">My charging plans</string>
|
||||
<string name="chargeprice_all_tariffs_selected">all plans selected</string>
|
||||
<string name="license">License</string>
|
||||
<string name="settings_charger_data">Charging stations</string>
|
||||
<string name="pref_data_source">Data source</string>
|
||||
<plurals name="chargeprice_some_tariffs_selected">
|
||||
<item quantity="one">%d plan selected</item>
|
||||
<item quantity="other">%d plans selected</item>
|
||||
</plurals>
|
||||
<string name="unknown_operator">Unknown operator</string>
|
||||
<string name="data_sources_description">EVMap supports multiple data sources. Please select the one you would like to use. You can always change it later in the app\'s settings.</string>
|
||||
<string name="data_source_goingelectric">GoingElectric.de</string>
|
||||
<string name="data_source_openchargemap">Open Charge Map</string>
|
||||
<string name="data_source_goingelectric_desc">Very good coverage in Germany, Austria and Switzerland and many neighboring countries. Descriptions in German. Community-maintained.</string>
|
||||
<string name="data_source_openchargemap_desc"><![CDATA[Worldwide coverage with varying quality. Descriptions in English or local language. Community-maintained & government open data in some countries (e.g. North America, UK, France, Norway).]]></string>
|
||||
<string name="next">next</string>
|
||||
<string name="get_started">Get started</string>
|
||||
<string name="got_it">Got it</string>
|
||||
<string name="lets_go">Let\'s go</string>
|
||||
</resources>
|
||||
|
||||
@@ -20,6 +20,15 @@
|
||||
android:summary="@string/pref_darkmode_summary" />
|
||||
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/settings_charger_data">
|
||||
<net.vonforst.evmap.ui.DataSourceSelectDialogPreference
|
||||
android:key="data_source"
|
||||
android:title="@string/pref_data_source"
|
||||
android:entries="@array/pref_data_source_names"
|
||||
android:entryValues="@array/pref_data_source_values"
|
||||
android:defaultValue="goingelectric"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/settings_map">
|
||||
|
||||
<ListPreference
|
||||
@@ -39,9 +48,11 @@
|
||||
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/settings_chargeprice">
|
||||
<ListPreference
|
||||
<net.vonforst.evmap.ui.MultiSelectDialogPreference
|
||||
android:key="chargeprice_my_vehicle"
|
||||
android:title="@string/pref_my_vehicle" />
|
||||
android:title="@string/pref_my_vehicle"
|
||||
app:showAllButton="false"
|
||||
app:defaultToAll="false" />
|
||||
<net.vonforst.evmap.ui.MultiSelectDialogPreference
|
||||
android:key="chargeprice_my_tariffs"
|
||||
android:title="@string/pref_my_tariffs" />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user