mirror of
https://github.com/ev-map/EVMap.git
synced 2026-01-13 01:17:48 -05:00
Compare commits
62 Commits
flows
...
replace-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b99e2ea2c8 | ||
|
|
d2ae3733d1 | ||
|
|
72845da4b5 | ||
|
|
51b57433a8 | ||
|
|
3202f821d1 | ||
|
|
b7e1ff09db | ||
|
|
feabf49b8d | ||
|
|
dcbe4c6325 | ||
|
|
dcff74c125 | ||
|
|
d8f7d77a36 | ||
|
|
d03cf70499 | ||
|
|
7a6bebd143 | ||
|
|
66d68ca68e | ||
|
|
772885a8eb | ||
|
|
6b07ce012a | ||
|
|
29dbc202d8 | ||
|
|
cf8371d095 | ||
|
|
01cb551cbc | ||
|
|
45fe297616 | ||
|
|
32cabefe7d | ||
|
|
9ff8329171 | ||
|
|
e9b70a2f00 | ||
|
|
c4c3aba7c7 | ||
|
|
890af2ddef | ||
|
|
ba0b36b3ec | ||
|
|
161b48789f | ||
|
|
042b983aa3 | ||
|
|
1c21da7be0 | ||
|
|
405baed0f7 | ||
|
|
19c0d57f2b | ||
|
|
42c2a2f72a | ||
|
|
36ee3ff231 | ||
|
|
883735ef05 | ||
|
|
4c68356ae9 | ||
|
|
7fde5b50aa | ||
|
|
7c4136c66d | ||
|
|
6e56f5c3ff | ||
|
|
017be6f31a | ||
|
|
b398a5dc81 | ||
|
|
3fb0dec868 | ||
|
|
8c4de115ec | ||
|
|
334b68cf5e | ||
|
|
788c68c9dd | ||
|
|
7842a15529 | ||
|
|
e7c9432191 | ||
|
|
76b6abd3ca | ||
|
|
752c184146 | ||
|
|
5471ac5073 | ||
|
|
69ae13a199 | ||
|
|
8a2e2d9a25 | ||
|
|
fe69a78b94 | ||
|
|
2663bd7964 | ||
|
|
3b54b2799f | ||
|
|
3a24711626 | ||
|
|
c158744bc2 | ||
|
|
c01033a036 | ||
|
|
16474c3864 | ||
|
|
7ce2f8d452 | ||
|
|
28df158d94 | ||
|
|
90b3645a0b | ||
|
|
de901aa825 | ||
|
|
2ce61f2f6b |
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Set up Java environment
|
- name: Set up Java environment
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 21
|
java-version: 17
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
- name: Decrypt keystore
|
- name: Decrypt keystore
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Extract version code
|
- name: Extract version code
|
||||||
run: echo "VERSION_CODE=$(grep -o "^\s*versionCode\s*=\s*[0-9]\+" app/build.gradle.kts | awk '{ print $3 }' | tr -d \''"\\')" >> $GITHUB_ENV
|
run: echo "VERSION_CODE=$(grep -o "^\s*versionCode\s*=\s*[0-9]\+" app/build.gradle.kts | awk '{ print $3 }' | tr -d \''"\\')" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build app release & export libraries
|
- name: Build app release
|
||||||
env:
|
env:
|
||||||
GOINGELECTRIC_API_KEY: ${{ secrets.GOINGELECTRIC_API_KEY }}
|
GOINGELECTRIC_API_KEY: ${{ secrets.GOINGELECTRIC_API_KEY }}
|
||||||
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
|
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
|
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
|
||||||
KEYSTORE_ALIAS_PASSWORD: ${{ secrets.KEYSTORE_ALIAS_PASSWORD }}
|
KEYSTORE_ALIAS_PASSWORD: ${{ secrets.KEYSTORE_ALIAS_PASSWORD }}
|
||||||
run: ./gradlew exportLibraryDefinitions assembleRelease --no-daemon
|
run: ./gradlew assembleRelease --no-daemon
|
||||||
|
|
||||||
- name: release
|
- name: release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
@@ -88,12 +88,3 @@ jobs:
|
|||||||
asset_path: app/build/outputs/apk/fossAutomotive/release/app-foss-automotive-release.apk
|
asset_path: app/build/outputs/apk/fossAutomotive/release/app-foss-automotive-release.apk
|
||||||
asset_name: app-foss-automotive-release.apk
|
asset_name: app-foss-automotive-release.apk
|
||||||
asset_content_type: application/vnd.android.package-archive
|
asset_content_type: application/vnd.android.package-archive
|
||||||
- name: upload Licenses
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: app/build/generated/aboutLibraries/aboutlibraries.json
|
|
||||||
asset_name: aboutlibraries.json
|
|
||||||
asset_content_type: application/json
|
|
||||||
|
|||||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
- name: Set up Java environment
|
- name: Set up Java environment
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 21
|
java-version: 17
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
|
|||||||
@@ -86,13 +86,10 @@ Sponsors
|
|||||||
Many users currently support the development EVMap with their donations. You can find more
|
Many users currently support the development EVMap with their donations. You can find more
|
||||||
information on the [Donate page](https://ev-map.app/donate/) on the EVMap website.
|
information on the [Donate page](https://ev-map.app/donate/) on the EVMap website.
|
||||||
|
|
||||||
<a href="https://www.jawg.io"><img src="https://www.jawg.io/static/Blue@10x-9cdc4596e4e59acbd9ead55e9c28613e.png" alt="JawgMaps" height="38"/></a><br>
|
<a href="https://www.jawg.io"><img src="https://www.jawg.io/static/Blue@10x-9cdc4596e4e59acbd9ead55e9c28613e.png" alt="JawgMaps" height="58"/></a><br>
|
||||||
Since May 2024, **JawgMaps** provides their OpenStreetMap vector map tiles service to EVMap for
|
Since May 2024, **JawgMaps** provides their OpenStreetMap vector map tiles service to EVMap for
|
||||||
free, i.e. the background map displayed in the app if OpenStreetMap is selected as the data source.
|
free, i.e. the background map displayed in the app if OpenStreetMap is selected as the data source.
|
||||||
|
|
||||||
<a href="https://chargeprice.app"><img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/powered_by_chargeprice.svg" alt="Powered by Chargeprice" height="38"/></a><br>
|
<a href="https://chargeprice.app"><img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/powered_by_chargeprice.svg" alt="Powered by Chargeprice" height="58"/></a><br>
|
||||||
Since April 2021, **Chargeprice.app** provide their price comparison API at a greatly reduced
|
Since April 2021, **Chargeprice.app** provide their price comparison API at a greatly reduced
|
||||||
price for EVMap. This data is used in EVMap's price comparison feature.
|
price for EVMap. This data is used in EVMap's price comparison feature.
|
||||||
|
|
||||||
<a href="https://www.jetbrains.com/community/opensource/"><img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg" alt="JetBrains logo" height="38"/></a><br>
|
|
||||||
As part of its support program for Open-source projects, **JetBrains** supports the development of EVMap since December 2023 with a license of their software suite.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" version="1.1"
|
|
||||||
viewBox="0 0 108 108">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #000;
|
|
||||||
stroke-width: 0px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<path class="cls-1"
|
|
||||||
d="M53.9,28c-8.8,0-15.9,7.1-15.9,15.9s13.4,18.2,15,35.3c0,.5.5.9,1,.9s.9-.4,1-.9c1.6-17.1,15-23.3,15-35.3-.1-8.8-7.2-15.9-16-15.9ZM59,43.1l-6.1,10.5v-7.9h-2.6v-9.6s8.8,0,8.7,0l-3.5,7h3.5Z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 529 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
d="M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 395 B |
@@ -1,7 +1,7 @@
|
|||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.adarshr.test-logger") version "4.0.0"
|
id("com.adarshr.test-logger") version "3.1.0"
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
@@ -17,18 +17,18 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "net.vonforst.evmap"
|
applicationId = "net.vonforst.evmap"
|
||||||
compileSdk = 36
|
compileSdk = 35
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 36
|
targetSdk = 35
|
||||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||||
versionCode = 230
|
versionCode = 256
|
||||||
versionName = "1.9.6"
|
versionName = "1.9.18"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
|
||||||
|
|
||||||
ksp {
|
ksp {
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isRunningOnCI = System.getenv("CI") == "true"
|
val isRunningOnCI = System.getenv("CI") == "true"
|
||||||
@@ -78,7 +78,6 @@ android {
|
|||||||
productFlavors {
|
productFlavors {
|
||||||
create("foss") {
|
create("foss") {
|
||||||
dimension = "dependencies"
|
dimension = "dependencies"
|
||||||
isDefault = true
|
|
||||||
}
|
}
|
||||||
create("google") {
|
create("google") {
|
||||||
dimension = "dependencies"
|
dimension = "dependencies"
|
||||||
@@ -86,7 +85,6 @@ android {
|
|||||||
}
|
}
|
||||||
create("normal") {
|
create("normal") {
|
||||||
dimension = "automotive"
|
dimension = "automotive"
|
||||||
isDefault = true
|
|
||||||
}
|
}
|
||||||
create("automotive") {
|
create("automotive") {
|
||||||
dimension = "automotive"
|
dimension = "automotive"
|
||||||
@@ -258,21 +256,18 @@ configurations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
aboutLibraries {
|
aboutLibraries {
|
||||||
license {
|
allowedLicenses = arrayOf(
|
||||||
allowedLicenses = setOf(
|
"Apache-2.0", "mit", "BSD-2-Clause", "BSD-3-Clause", "EPL-1.0",
|
||||||
"Apache-2.0", "mit", "BSD-2-Clause", "BSD-3-Clause", "EPL-1.0",
|
"asdkl", // Android SDK
|
||||||
"asdkl", // Android SDK
|
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
|
||||||
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
|
"Google Maps Platform Terms of Service", // Google Maps SDK
|
||||||
"Google Maps Platform Terms of Service", // Google Maps SDK
|
"provided without support or warranty", // org.json
|
||||||
"Unicode/ICU License", "Unicode-3.0", // icu4j
|
"Unicode/ICU License", "Unicode-3.0", // icu4j
|
||||||
"Bouncy Castle Licence", // bcprov
|
"Bouncy Castle Licence", // bcprov
|
||||||
"CDDL + GPLv2 with classpath exception", // javax.annotation-api
|
"CDDL + GPLv2 with classpath exception", // javax.annotation-api
|
||||||
)
|
)
|
||||||
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
excludeFields = arrayOf("generated")
|
||||||
}
|
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
||||||
export {
|
|
||||||
excludeFields = setOf("generated")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -286,99 +281,101 @@ dependencies {
|
|||||||
val testGoogleImplementation by configurations
|
val testGoogleImplementation by configurations
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
|
||||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||||
implementation("androidx.core:core-ktx:1.17.0")
|
implementation("androidx.core:core-ktx:1.13.1")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||||
implementation("androidx.activity:activity-ktx:1.10.1")
|
implementation("androidx.activity:activity-ktx:1.9.0")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.8.9")
|
implementation("androidx.fragment:fragment-ktx:1.7.1")
|
||||||
implementation("androidx.cardview:cardview:1.0.0")
|
implementation("androidx.cardview:cardview:1.0.0")
|
||||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
implementation("com.google.android.material:material:1.13.0-rc01")
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.4.0")
|
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||||
implementation("androidx.browser:browser:1.9.0")
|
implementation("androidx.browser:browser:1.8.0")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
||||||
implementation("androidx.security:security-crypto:1.1.0")
|
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||||
implementation("androidx.work:work-runtime-ktx:2.10.3")
|
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||||
implementation("com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b")
|
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||||
implementation("com.squareup.retrofit2:retrofit:3.0.0")
|
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
|
||||||
implementation("com.squareup.retrofit2:converter-moshi:3.0.0")
|
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
implementation("com.squareup.okhttp3:okhttp-urlconnection:4.12.0")
|
implementation("com.squareup.okhttp3:okhttp-urlconnection:4.12.0")
|
||||||
implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
|
implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
|
||||||
implementation("com.squareup.moshi:moshi-adapters:1.15.2")
|
implementation("com.squareup.moshi:moshi-adapters:1.15.2")
|
||||||
implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
|
implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
|
||||||
implementation("io.coil-kt:coil:2.7.0")
|
implementation("io.coil-kt:coil:2.6.0")
|
||||||
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
|
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
|
||||||
implementation("com.mikepenz:aboutlibraries-core:$aboutLibsVersion")
|
implementation("com.mikepenz:aboutlibraries-core:$aboutLibsVersion")
|
||||||
implementation("com.mikepenz:aboutlibraries:$aboutLibsVersion")
|
implementation("com.mikepenz:aboutlibraries:$aboutLibsVersion")
|
||||||
implementation("com.airbnb.android:lottie:6.6.7")
|
implementation("com.airbnb.android:lottie:4.1.0")
|
||||||
implementation("io.michaelrocks.bimap:bimap:1.1.0")
|
implementation("io.michaelrocks.bimap:bimap:1.1.0")
|
||||||
|
implementation("com.google.guava:guava:29.0-android")
|
||||||
implementation("com.github.pengrad:mapscaleview:1.6.0")
|
implementation("com.github.pengrad:mapscaleview:1.6.0")
|
||||||
implementation("com.github.romandanylyk:PageIndicatorView:b1bad589b5")
|
implementation("com.github.romandanylyk:PageIndicatorView:b1bad589b5")
|
||||||
implementation("com.github.erfansn:locale-config-x:1.0.1")
|
implementation("com.github.erfansn:locale-config-x:1.0.1")
|
||||||
|
|
||||||
// Android Auto
|
// Android Auto
|
||||||
val carAppVersion = "1.7.0"
|
val carAppVersion = "1.7.0-rc01"
|
||||||
implementation("androidx.car.app:app:$carAppVersion")
|
implementation("androidx.car.app:app:$carAppVersion")
|
||||||
normalImplementation("androidx.car.app:app-projected:$carAppVersion")
|
normalImplementation("androidx.car.app:app-projected:$carAppVersion")
|
||||||
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
|
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
|
||||||
|
|
||||||
// AnyMaps
|
// AnyMaps
|
||||||
val anyMapsVersion = "1174ef9375"
|
val anyMapsVersion = "a3290b148d"
|
||||||
implementation("com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion")
|
implementation("com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion")
|
||||||
googleImplementation("com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion")
|
googleImplementation("com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion")
|
||||||
googleImplementation("com.google.android.gms:play-services-maps:19.2.0")
|
googleImplementation("com.google.android.gms:play-services-maps:19.0.0")
|
||||||
implementation("com.github.ev-map.AnyMaps:anymaps-maplibre:$anyMapsVersion") {
|
implementation("com.github.ev-map.AnyMaps:anymaps-maplibre:$anyMapsVersion") {
|
||||||
// duplicates classes from mapbox-sdk-services
|
// duplicates classes from mapbox-sdk-services
|
||||||
exclude("org.maplibre.gl", "android-sdk-geojson")
|
exclude("org.maplibre.gl", "android-sdk-geojson")
|
||||||
}
|
}
|
||||||
implementation("org.maplibre.gl:android-sdk:10.3.5") {
|
implementation("org.maplibre.gl:android-sdk:10.3.4") {
|
||||||
exclude("org.maplibre.gl", "android-sdk-geojson")
|
exclude("org.maplibre.gl", "android-sdk-geojson")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google Places
|
// Google Places
|
||||||
googleImplementation("com.google.android.libraries.places:places:3.5.0")
|
googleImplementation("com.google.android.libraries.places:places:3.5.0")
|
||||||
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2")
|
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
|
||||||
|
|
||||||
// Mapbox Geocoding
|
// Mapbox Geocoding
|
||||||
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.8.0")
|
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0")
|
||||||
|
|
||||||
// navigation library
|
// navigation library
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
|
implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
|
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
|
||||||
|
|
||||||
// viewmodel library
|
// viewmodel library
|
||||||
val lifecycleVersion = "2.9.2"
|
val lifecycle_version = "2.8.1"
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
|
||||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
|
||||||
|
|
||||||
// room library
|
// room library
|
||||||
val roomVersion = "2.7.2"
|
val room_version = "2.7.1"
|
||||||
implementation("androidx.room:room-runtime:$roomVersion")
|
implementation("androidx.room:room-runtime:$room_version")
|
||||||
ksp("androidx.room:room-compiler:$roomVersion")
|
ksp("androidx.room:room-compiler:$room_version")
|
||||||
implementation("androidx.room:room-ktx:$roomVersion")
|
implementation("androidx.room:room-ktx:$room_version")
|
||||||
implementation("com.github.anboralabs:spatia-room:0.3.0") {
|
implementation("com.github.anboralabs:spatia-room:0.3.0") {
|
||||||
exclude("com.github.dalgarins", "android-spatialite")
|
exclude("com.github.dalgarins", "android-spatialite")
|
||||||
}
|
}
|
||||||
// forked version with upgraded sqlite & libxml & 16 KB page size support
|
// forked version with upgraded sqlite & libxml
|
||||||
// https://github.com/dalgarins/android-spatialite/pull/11
|
// https://github.com/dalgarins/android-spatialite/pull/10
|
||||||
// https://github.com/dalgarins/android-spatialite/pull/12
|
implementation("com.github.ev-map:android-spatialite:31495dcd81")
|
||||||
implementation("io.github.ev-map:android-spatialite:2.2.1-alpha")
|
|
||||||
|
|
||||||
// billing library
|
// billing library
|
||||||
val billingVersion = "7.0.0"
|
val billing_version = "7.0.0"
|
||||||
googleImplementation("com.android.billingclient:billing:$billingVersion")
|
googleImplementation("com.android.billingclient:billing:$billing_version")
|
||||||
googleImplementation("com.android.billingclient:billing-ktx:$billingVersion")
|
googleImplementation("com.android.billingclient:billing-ktx:$billing_version")
|
||||||
|
|
||||||
// ACRA (crash reporting)
|
// ACRA (crash reporting)
|
||||||
val acraVersion = "5.12.0"
|
val acraVersion = "5.11.1"
|
||||||
implementation("ch.acra:acra-http:$acraVersion")
|
implementation("ch.acra:acra-http:$acraVersion")
|
||||||
implementation("ch.acra:acra-dialog:$acraVersion")
|
implementation("ch.acra:acra-dialog:$acraVersion")
|
||||||
implementation("ch.acra:acra-limiter:$acraVersion")
|
implementation("ch.acra:acra-limiter:$acraVersion")
|
||||||
|
|
||||||
// debug tools
|
// debug tools
|
||||||
|
debugImplementation("com.facebook.flipper:flipper:0.238.0")
|
||||||
|
debugImplementation("com.facebook.soloader:soloader:0.10.5")
|
||||||
|
debugImplementation("com.facebook.flipper:flipper-network-plugin:0.238.0")
|
||||||
debugImplementation("com.jakewharton.timber:timber:5.0.1")
|
debugImplementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
|
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
|
||||||
|
|
||||||
@@ -386,18 +383,20 @@ dependencies {
|
|||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
|
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
testImplementation("org.robolectric:robolectric:4.16-beta-1")
|
testImplementation("org.json:json:20080701")
|
||||||
testImplementation("androidx.test:core:1.7.0")
|
testImplementation("org.robolectric:robolectric:4.11.1")
|
||||||
|
testImplementation("androidx.test:core:1.5.0")
|
||||||
testImplementation("androidx.arch.core:core-testing:2.2.0")
|
testImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||||
testImplementation("androidx.car.app:app-testing:$carAppVersion")
|
testImplementation("androidx.car.app:app-testing:$carAppVersion")
|
||||||
|
testImplementation("androidx.test:core:1.5.0")
|
||||||
|
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.3.0")
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
|
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||||
|
|
||||||
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
|
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
|
||||||
|
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decode(s: String, key: String): String {
|
fun decode(s: String, key: String): String {
|
||||||
|
|||||||
@@ -1,997 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 23,
|
|
||||||
"identityHash": "e9e169ba4257824c82e4acb030730e97",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "ChargeLocation",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `coordinates` BLOB NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `license` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, PRIMARY KEY(`id`, `dataSource`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dataSource",
|
|
||||||
"columnName": "dataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "coordinates",
|
|
||||||
"columnName": "coordinates",
|
|
||||||
"affinity": "BLOB",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargepoints",
|
|
||||||
"columnName": "chargepoints",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "network",
|
|
||||||
"columnName": "network",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "url",
|
|
||||||
"columnName": "url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "editUrl",
|
|
||||||
"columnName": "editUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "verified",
|
|
||||||
"columnName": "verified",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "barrierFree",
|
|
||||||
"columnName": "barrierFree",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "operator",
|
|
||||||
"columnName": "operator",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "generalInformation",
|
|
||||||
"columnName": "generalInformation",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "amenities",
|
|
||||||
"columnName": "amenities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "locationDescription",
|
|
||||||
"columnName": "locationDescription",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photos",
|
|
||||||
"columnName": "photos",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargecards",
|
|
||||||
"columnName": "chargecards",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "license",
|
|
||||||
"columnName": "license",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "networkUrl",
|
|
||||||
"columnName": "networkUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargerUrl",
|
|
||||||
"columnName": "chargerUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "timeRetrieved",
|
|
||||||
"columnName": "timeRetrieved",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isDetailed",
|
|
||||||
"columnName": "isDetailed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "address.city",
|
|
||||||
"columnName": "city",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "address.country",
|
|
||||||
"columnName": "country",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "address.postcode",
|
|
||||||
"columnName": "postcode",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "address.street",
|
|
||||||
"columnName": "street",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "faultReport.created",
|
|
||||||
"columnName": "fault_report_created",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "faultReport.description",
|
|
||||||
"columnName": "fault_report_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.twentyfourSeven",
|
|
||||||
"columnName": "twentyfourSeven",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.description",
|
|
||||||
"columnName": "description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.monday.start",
|
|
||||||
"columnName": "mostart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.monday.end",
|
|
||||||
"columnName": "moend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.tuesday.start",
|
|
||||||
"columnName": "tustart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.tuesday.end",
|
|
||||||
"columnName": "tuend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.wednesday.start",
|
|
||||||
"columnName": "westart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.wednesday.end",
|
|
||||||
"columnName": "weend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.thursday.start",
|
|
||||||
"columnName": "thstart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.thursday.end",
|
|
||||||
"columnName": "thend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.friday.start",
|
|
||||||
"columnName": "frstart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.friday.end",
|
|
||||||
"columnName": "frend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.saturday.start",
|
|
||||||
"columnName": "sastart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.saturday.end",
|
|
||||||
"columnName": "saend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.sunday.start",
|
|
||||||
"columnName": "sustart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.sunday.end",
|
|
||||||
"columnName": "suend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.holiday.start",
|
|
||||||
"columnName": "hostart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.holiday.end",
|
|
||||||
"columnName": "hoend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "cost.freecharging",
|
|
||||||
"columnName": "freecharging",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "cost.freeparking",
|
|
||||||
"columnName": "freeparking",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "cost.descriptionShort",
|
|
||||||
"columnName": "descriptionShort",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "cost.descriptionLong",
|
|
||||||
"columnName": "descriptionLong",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargepriceData.country",
|
|
||||||
"columnName": "chargepricecountry",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargepriceData.network",
|
|
||||||
"columnName": "chargepricenetwork",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargepriceData.plugTypes",
|
|
||||||
"columnName": "chargepriceplugTypes",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "Favorite",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "favoriteId",
|
|
||||||
"columnName": "favoriteId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargerId",
|
|
||||||
"columnName": "chargerId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargerDataSource",
|
|
||||||
"columnName": "chargerDataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"favoriteId"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_Favorite_chargerId_chargerDataSource",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"chargerId",
|
|
||||||
"chargerDataSource"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_Favorite_chargerId_chargerDataSource` ON `${TABLE_NAME}` (`chargerId`, `chargerDataSource`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "ChargeLocation",
|
|
||||||
"onDelete": "NO ACTION",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"chargerId",
|
|
||||||
"chargerDataSource"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "BooleanFilterValue",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "key",
|
|
||||||
"columnName": "key",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "value",
|
|
||||||
"columnName": "value",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dataSource",
|
|
||||||
"columnName": "dataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "profile",
|
|
||||||
"columnName": "profile",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"key",
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_BooleanFilterValue_profile_dataSource",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_BooleanFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "FilterProfile",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "MultipleChoiceFilterValue",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "key",
|
|
||||||
"columnName": "key",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "values",
|
|
||||||
"columnName": "values",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "all",
|
|
||||||
"columnName": "all",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dataSource",
|
|
||||||
"columnName": "dataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "profile",
|
|
||||||
"columnName": "profile",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"key",
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_MultipleChoiceFilterValue_profile_dataSource",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_MultipleChoiceFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "FilterProfile",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "SliderFilterValue",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "key",
|
|
||||||
"columnName": "key",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "value",
|
|
||||||
"columnName": "value",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dataSource",
|
|
||||||
"columnName": "dataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "profile",
|
|
||||||
"columnName": "profile",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"key",
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_SliderFilterValue_profile_dataSource",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SliderFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "FilterProfile",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "FilterProfile",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `id` INTEGER NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`dataSource`, `id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dataSource",
|
|
||||||
"columnName": "dataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "order",
|
|
||||||
"columnName": "order",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"dataSource",
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_FilterProfile_dataSource_name",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"dataSource",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_FilterProfile_dataSource_name` ON `${TABLE_NAME}` (`dataSource`, `name`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "RecentAutocompletePlace",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dataSource",
|
|
||||||
"columnName": "dataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "timestamp",
|
|
||||||
"columnName": "timestamp",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "primaryText",
|
|
||||||
"columnName": "primaryText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "secondaryText",
|
|
||||||
"columnName": "secondaryText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "latLng",
|
|
||||||
"columnName": "latLng",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "viewport",
|
|
||||||
"columnName": "viewport",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "types",
|
|
||||||
"columnName": "types",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "GEPlug",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"name"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "GENetwork",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"name"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "GEChargeCard",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "url",
|
|
||||||
"columnName": "url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "OCMConnectionType",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `formalName` TEXT, `discontinued` INTEGER, `obsolete` INTEGER, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "formalName",
|
|
||||||
"columnName": "formalName",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "discontinued",
|
|
||||||
"columnName": "discontinued",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "obsolete",
|
|
||||||
"columnName": "obsolete",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "OCMCountry",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isoCode` TEXT NOT NULL, `continentCode` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isoCode",
|
|
||||||
"columnName": "isoCode",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "continentCode",
|
|
||||||
"columnName": "continentCode",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "OCMOperator",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "websiteUrl",
|
|
||||||
"columnName": "websiteUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "contactEmail",
|
|
||||||
"columnName": "contactEmail",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "contactTelephone1",
|
|
||||||
"columnName": "contactTelephone1",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "contactTelephone2",
|
|
||||||
"columnName": "contactTelephone2",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "OSMNetwork",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"name"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "SavedRegion",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`region` BLOB NOT NULL, `dataSource` TEXT NOT NULL, `timeRetrieved` INTEGER NOT NULL, `filters` TEXT, `isDetailed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "region",
|
|
||||||
"columnName": "region",
|
|
||||||
"affinity": "BLOB",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dataSource",
|
|
||||||
"columnName": "dataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "timeRetrieved",
|
|
||||||
"columnName": "timeRetrieved",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "filters",
|
|
||||||
"columnName": "filters",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isDetailed",
|
|
||||||
"columnName": "isDetailed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_SavedRegion_filters_dataSource",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"filters",
|
|
||||||
"dataSource"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SavedRegion_filters_dataSource` ON `${TABLE_NAME}` (`filters`, `dataSource`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e9e169ba4257824c82e4acb030730e97')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
package net.vonforst.evmap.storage
|
package com.johan.evmap.storage
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
package net.vonforst.evmap.storage
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
|
||||||
import androidx.test.core.app.ApplicationProvider
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
|
||||||
import net.vonforst.evmap.model.ChargeLocationCluster
|
|
||||||
import net.vonforst.evmap.model.ChargepointListItem
|
|
||||||
import net.vonforst.evmap.model.Coordinate
|
|
||||||
import net.vonforst.evmap.ui.cluster
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import java.time.Instant
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ChargeLocationsDaoTest {
|
|
||||||
private lateinit var database: AppDatabase
|
|
||||||
private lateinit var dao: ChargeLocationsDao
|
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
var instantExecutorRule = InstantTaskExecutorRule()
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
|
||||||
database = AppDatabase.createInMemory(context)
|
|
||||||
dao = database.chargeLocationsDao()
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
database.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testClustering() {
|
|
||||||
val lat1 = 53.0
|
|
||||||
val lng1 = 9.0
|
|
||||||
val lat2 = 54.0
|
|
||||||
val lng2 = 10.0
|
|
||||||
|
|
||||||
val chargeLocations = (0..100).map { i ->
|
|
||||||
val lat = Random.nextDouble(lat1, lat2)
|
|
||||||
val lng = Random.nextDouble(lng1, lng2)
|
|
||||||
ChargeLocation(
|
|
||||||
i.toLong(),
|
|
||||||
"test",
|
|
||||||
"test",
|
|
||||||
Coordinate(lat, lng),
|
|
||||||
null,
|
|
||||||
emptyList(),
|
|
||||||
null,
|
|
||||||
"https://google.com",
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null, null, null, null, null, null, null, Instant.now(), true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
runBlocking {
|
|
||||||
dao.insert(*chargeLocations.toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
val zoom = 10f
|
|
||||||
|
|
||||||
val clusteredInMemory = cluster(chargeLocations, zoom).sorted()
|
|
||||||
val clusteredInDB = runBlocking {
|
|
||||||
dao.getChargeLocationsClustered(lat1, lat2, lng1, lng2, "test", 0L, zoom)
|
|
||||||
}.sorted()
|
|
||||||
assertEquals(clusteredInMemory.size, clusteredInDB.size)
|
|
||||||
clusteredInDB.zip(clusteredInMemory).forEach { (a, b) ->
|
|
||||||
when (a) {
|
|
||||||
is ChargeLocation -> {
|
|
||||||
assertTrue(b is ChargeLocation)
|
|
||||||
assertEquals(a, b)
|
|
||||||
}
|
|
||||||
is ChargeLocationCluster -> {
|
|
||||||
assertTrue(b is ChargeLocationCluster)
|
|
||||||
assertEquals(a.clusterCount, (b as ChargeLocationCluster).clusterCount)
|
|
||||||
assertEquals(a.coordinates.lat, b.coordinates.lat, 1e-5)
|
|
||||||
assertEquals(a.coordinates.lng, b.coordinates.lng, 1e-5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<ChargepointListItem>.sorted() = sortedBy {
|
|
||||||
when (it) {
|
|
||||||
is ChargeLocationCluster -> it.coordinates.lat
|
|
||||||
is ChargeLocation -> it.coordinates.lat
|
|
||||||
else -> 0.0
|
|
||||||
}
|
|
||||||
}.sortedBy {
|
|
||||||
when (it) {
|
|
||||||
is ChargeLocationCluster -> it.coordinates.lng
|
|
||||||
is ChargeLocation -> it.coordinates.lng
|
|
||||||
else -> 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="grant_on_phone">Povolit</string>
|
<string name="grant_on_phone">Povolit</string>
|
||||||
<string name="auto_location_permission_needed">Pro spuštění aplikace EVMap ve vašem autě musíte povolit přístup k vaší poloze.</string>
|
<string name="auto_location_permission_needed">Pro spuštění aplikace EVMap ve vašem autě musíte povolit přístup k vaší poloze.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="grant_on_phone">Zulassen</string>
|
<string name="grant_on_phone">Zulassen</string>
|
||||||
<string name="auto_location_permission_needed">Um EVMap auf deinem Fahrzeug zu nutzen, braucht die App Zugriff auf deinen Standort.</string>
|
<string name="auto_location_permission_needed">Um EVMap auf deinem Fahrzeug zu nutzen, braucht die App Zugriff auf deinen Standort.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="grant_on_phone">Luba</string>
|
|
||||||
<string name="auto_location_permission_needed">Et EVMap toimiks sinu autos, palun luba tal asukohta tuvastada.</string>
|
|
||||||
</resources>
|
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="grant_on_phone">Autoriser</string>
|
<string name="grant_on_phone">Autoriser</string>
|
||||||
<string name="auto_location_permission_needed">Pour exécuter EVMap sur Android Auto, vous devez autoriser l\'accès à votre emplacement.</string>
|
<string name="auto_location_permission_needed">Pour exécuter EVMap sur Android Auto, vous devez autoriser l\'accès à votre emplacement.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="grant_on_phone">Consenti</string>
|
|
||||||
<string name="auto_location_permission_needed">Per eseguire EVMap sulla propria auto, è necessario concedere l\'accesso alla propria posizione.</string>
|
|
||||||
</resources>
|
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="auto_location_permission_needed">Du må du innvilge posisjonstilgang for å kjøre EVMap i bilen din.</string>
|
<string name="auto_location_permission_needed">Du må du innvilge posisjonstilgang for å kjøre EVMap i bilen din.</string>
|
||||||
<string name="grant_on_phone">Tillat</string>
|
<string name="grant_on_phone">Tillat</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="grant_on_phone">Toestaan</string>
|
<string name="grant_on_phone">Toestaan</string>
|
||||||
<string name="auto_location_permission_needed">Om EVmap te gebruiken in je wagen, moet je toegang geven tot je locatie.</string>
|
<string name="auto_location_permission_needed">Om EVmap te gebruiken in je wagen, moet je toegang geven tot je locatie.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="grant_on_phone">Permitir</string>
|
<string name="grant_on_phone">Permitir</string>
|
||||||
<string name="auto_location_permission_needed">Para usar o EVMap no seu carro, permita o acesso à sua localização.</string>
|
<string name="auto_location_permission_needed">Para usar o EVMap no seu carro, permita o acesso à sua localização.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources></resources>
|
||||||
</resources>
|
|
||||||
9
app/src/debug/AndroidManifest.xml
Normal file
9
app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
|
||||||
|
android:exported="true" />
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -2,15 +2,44 @@ package net.vonforst.evmap
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import com.facebook.flipper.android.AndroidFlipperClient
|
||||||
|
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
|
||||||
|
import com.facebook.flipper.plugins.inspector.DescriptorMapping
|
||||||
|
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
|
||||||
|
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
|
||||||
|
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
|
||||||
|
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
|
||||||
|
import com.facebook.soloader.SoLoader
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
private val networkFlipperPlugin = NetworkFlipperPlugin()
|
||||||
|
|
||||||
fun addDebugInterceptors(context: Context) {
|
fun addDebugInterceptors(context: Context) {
|
||||||
if (Build.FINGERPRINT == "robolectric") return
|
if (Build.FINGERPRINT == "robolectric") return
|
||||||
|
|
||||||
|
SoLoader.init(context, false)
|
||||||
|
val client = AndroidFlipperClient.getInstance(context)
|
||||||
|
client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()))
|
||||||
|
client.addPlugin(networkFlipperPlugin)
|
||||||
|
client.addPlugin(DatabasesFlipperPlugin(context))
|
||||||
|
client.addPlugin(SharedPreferencesFlipperPlugin(context))
|
||||||
|
client.start()
|
||||||
|
|
||||||
Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder {
|
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder {
|
||||||
|
// Flipper does not work during unit tests - so check whether we are running tests first
|
||||||
|
var isRunningTest = true
|
||||||
|
try {
|
||||||
|
Class.forName("org.junit.Test")
|
||||||
|
} catch (e: ClassNotFoundException) {
|
||||||
|
isRunningTest = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRunningTest) {
|
||||||
|
this.addNetworkInterceptor(FlipperOkhttpInterceptor(networkFlipperPlugin))
|
||||||
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@@ -3,4 +3,4 @@
|
|||||||
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj zasláním finančního daru vývojáři.</string>
|
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj zasláním finančního daru vývojáři.</string>
|
||||||
<string name="donate_paypal">Přispět pomocí PayPalu</string>
|
<string name="donate_paypal">Přispět pomocí PayPalu</string>
|
||||||
<string name="data_sources_hint">Mapová data v aplikaci poskytuje služba OpenStreetMap.</string>
|
<string name="data_sources_hint">Mapová data v aplikaci poskytuje služba OpenStreetMap.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -3,4 +3,4 @@
|
|||||||
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.</string>
|
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.</string>
|
||||||
<string name="donate_paypal">Mit PayPal spenden</string>
|
<string name="donate_paypal">Mit PayPal spenden</string>
|
||||||
<string name="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap.</string>
|
<string name="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="donations_info" formatted="false">Kas EVMap on sulle kasulik? Oma arendajale saadetava rahalise toetusega edendad ka arendustegevust.</string>
|
|
||||||
<string name="donate_paypal">Toeta PayPali abil</string>
|
|
||||||
<string name="data_sources_hint">Selles rakenduses näidatavad kaardiandmed on pärit OpenStreetMapist.</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile ? Soutenez son développement en envoyant un don au développeur.</string>
|
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile \? Soutenez son développement en envoyant un don au développeur.</string>
|
||||||
<string name="data_sources_hint">Les données cartographiques de l\'application sont fournies par OpenStreetMap.</string>
|
<string name="data_sources_hint">Les données cartographiques de l\'application sont fournies par OpenStreetMap.</string>
|
||||||
<string name="donate_paypal">Faire un don avec PayPal</string>
|
<string name="donate_paypal">Faire un don avec PayPal</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="donations_info" formatted="false">Trovi utile EVMap? Sostieni il suo sviluppo inviando una donazione allo sviluppatore.</string>
|
|
||||||
<string name="donate_paypal">Dona attraverso PayPal</string>
|
|
||||||
<string name="data_sources_hint">I dati cartografici dell\'applicazione sono forniti da OpenStreetMap.</string>
|
|
||||||
</resources>
|
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="donate_paypal">Doner med PayPal</string>
|
<string name="donate_paypal">Doner med PayPal</string>
|
||||||
<string name="data_sources_hint">Kartdata i programmet tilbys av OpenStreetMap.</string>
|
<string name="data_sources_hint">Kartdata i programmet tilbys av OpenStreetMap.</string>
|
||||||
<string name="donations_info" formatted="false">Synes du EVMap er nyttig? Støtt utviklingen ved å sende en slant til utvikleren.</string>
|
<string name="donations_info" formatted="false">Synes du EVMap er nyttig\? Støtt utviklingen ved å sende en slant til utvikleren.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Vond je EVMap nuttig? Je kan de ontwikkeling ondersteunen door een donatie te sturen naar de ontwikkelaar.</string>
|
<string name="donations_info" formatted="false">Vond je EVMap nuttig\? Je kan de ontwikkeling ondersteunen door een donatie te sturen naar de ontwikkelaar.</string>
|
||||||
<string name="donate_paypal">Doneer via PayPal</string>
|
<string name="donate_paypal">Doneer via PayPal</string>
|
||||||
<string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap.</string>
|
<string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="data_sources_hint">Os dados do mapa são fornecidos pelo OpenStreetMap.</string>
|
<string name="data_sources_hint">Os dados do mapa são fornecidos pelo OpenStreetMap.</string>
|
||||||
<string name="donate_paypal">Doar com o PayPal</string>
|
<string name="donate_paypal">Doar com o PayPal</string>
|
||||||
<string name="donations_info" formatted="false">Acha que o EVMap é útil? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.</string>
|
<string name="donations_info" formatted="false">Acha que o EVMap é útil\? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string-array name="pref_map_provider_names">
|
<string-array name="pref_map_provider_names">
|
||||||
<item>@string/pref_provider_osm</item>
|
<item>@string/pref_provider_osm_mapbox</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="pref_map_provider_values" translatable="false">
|
<string-array name="pref_map_provider_values" translatable="false">
|
||||||
<item>mapbox</item>
|
<item>mapbox</item>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj posláním finančního daru vývojáři. \n \nGoogle si z každého daru strhne 15 %.</string>
|
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj posláním finančního daru vývojáři.
|
||||||
|
\n
|
||||||
|
\nGoogle si z každého daru strhne 15 %.</string>
|
||||||
<string name="data_sources_hint">V nastavení můžete také pro mapová data přepínat mezi službami Mapy Google a OpenStreetMap.</string>
|
<string name="data_sources_hint">V nastavení můžete také pro mapová data přepínat mezi službami Mapy Google a OpenStreetMap.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 15% Gebühren ab.</string>
|
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 15% Gebühren ab.</string>
|
||||||
<string name="data_sources_hint">In den Einstellungen kannst du auch zwischen Google Maps und OpenStreetMap für die Kartendaten wechseln.</string>
|
<string name="data_sources_hint">In den Einstellungen kannst du auch zwischen Google Maps und OpenStreetMap für die Kartendaten wechseln.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="data_sources_hint">Seadistustes saad valida kahe kaardiandmete allika vahel: Google Maps ja OpenStreetMap.</string>
|
|
||||||
<string name="donations_info" formatted="false">EVMap on sinu jaoks kasulik? Toeta edasist arendust oma rahalise panusega.\n\nGoogle võtab igast toestussummast teenustasuna 15%.</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile ? Soutenez son développement en envoyant un don au développeur. \n \nGoogle prend 15% sur chaque don.</string>
|
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile \? Soutenez son développement en envoyant un don au développeur.
|
||||||
|
\n
|
||||||
|
\nGoogle prend 15% sur chaque don.</string>
|
||||||
<string name="data_sources_hint">Dans les paramètres, vous pouvez également choisir entre Google Maps et OpenStreetMap pour les données cartographiques.</string>
|
<string name="data_sources_hint">Dans les paramètres, vous pouvez également choisir entre Google Maps et OpenStreetMap pour les données cartographiques.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="donations_info" formatted="false">Trovi utile EVMap? Sostieni il suo sviluppo inviando una donazione allo sviluppatore.\n\nGoogle si prende il 15% su ogni donazione.</string>
|
|
||||||
<string name="data_sources_hint">Nelle impostazioni si può anche scegliere tra Google Maps e OpenStreetMap per i dati cartografici.</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Synes du EVMap er nyttig? Støtt utviklingen ved å sende penger til utvikleren. \n \nGoogle tar 15% av alle donasjoner.</string>
|
<string name="donations_info" formatted="false">Synes du EVMap er nyttig\? Støtt utviklingen ved å sende penger til utvikleren.
|
||||||
|
\n
|
||||||
|
\nGoogle tar 15% av alle donasjoner.</string>
|
||||||
<string name="data_sources_hint">I innstillingene kan du også bytte mellom Google Maps og OpenStreetMap for kartdata.</string>
|
<string name="data_sources_hint">I innstillingene kan du også bytte mellom Google Maps og OpenStreetMap for kartdata.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Vind je EVMap nuttig? Je kan de ontwikkeling steunen via een donatie aan de ontwikkelaar. \n \nGoogle houdt 15% in van elke donatie.</string>
|
<string name="donations_info" formatted="false">Vind je EVMap nuttig\? Je kan de ontwikkeling steunen via een donatie aan de ontwikkelaar.
|
||||||
|
\n
|
||||||
|
\nGoogle houdt 15% in van elke donatie.</string>
|
||||||
<string name="data_sources_hint">In de instellingen kan je ook wisselen tussen Google Maps en OpenStreetMap voor de kaartgegevens.</string>
|
<string name="data_sources_hint">In de instellingen kan je ook wisselen tussen Google Maps en OpenStreetMap voor de kaartgegevens.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Acha que o EVMap é útil? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app. \n \nA Google cobra 15% de cada doação.</string>
|
<string name="donations_info" formatted="false">Acha que o EVMap é útil\? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.
|
||||||
|
\n
|
||||||
|
\nA Google cobra 15% de cada doação.</string>
|
||||||
<string name="data_sources_hint">Também pode mudar entre o Google Maps e OpenStreetMap nas definições da app.</string>
|
<string name="data_sources_hint">Também pode mudar entre o Google Maps e OpenStreetMap nas definições da app.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources></resources>
|
||||||
</resources>
|
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
|
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
|
||||||
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
|
|
||||||
<uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
|
<uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
|
||||||
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED" />
|
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED" />
|
||||||
|
|
||||||
@@ -46,7 +45,8 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme"
|
||||||
|
android:enableOnBackInvokedCallback="true">
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.mapbox.ACCESS_TOKEN"
|
android:name="com.mapbox.ACCESS_TOKEN"
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.clustering;
|
||||||
|
|
||||||
|
import com.car2go.maps.model.LatLng;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of ClusterItems that are nearby each other.
|
||||||
|
*/
|
||||||
|
public interface Cluster<T extends ClusterItem> {
|
||||||
|
LatLng getPosition();
|
||||||
|
|
||||||
|
Collection<T> getItems();
|
||||||
|
|
||||||
|
int getSize();
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.clustering;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.car2go.maps.model.LatLng;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClusterItem represents a marker on the map.
|
||||||
|
*/
|
||||||
|
public interface ClusterItem {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The position of this marker. This must always return the same value.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
LatLng getPosition();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The title of this marker.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
String getTitle();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The description of this marker.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
String getSnippet();
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.maps.android.clustering.algo;
|
||||||
|
|
||||||
|
import com.google.maps.android.clustering.ClusterItem;
|
||||||
|
|
||||||
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base Algorithm class that implements lock/unlock functionality.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractAlgorithm<T extends ClusterItem> implements Algorithm<T> {
|
||||||
|
|
||||||
|
private final ReadWriteLock mLock = new ReentrantReadWriteLock();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void lock() {
|
||||||
|
mLock.writeLock().lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unlock() {
|
||||||
|
mLock.writeLock().unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.clustering.algo;
|
||||||
|
|
||||||
|
import com.google.maps.android.clustering.Cluster;
|
||||||
|
import com.google.maps.android.clustering.ClusterItem;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logic for computing clusters
|
||||||
|
*/
|
||||||
|
public interface Algorithm<T extends ClusterItem> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an item to the algorithm
|
||||||
|
*
|
||||||
|
* @param item the item to be added
|
||||||
|
* @return true if the algorithm contents changed as a result of the call
|
||||||
|
*/
|
||||||
|
boolean addItem(T item);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a collection of items to the algorithm
|
||||||
|
*
|
||||||
|
* @param items the items to be added
|
||||||
|
* @return true if the algorithm contents changed as a result of the call
|
||||||
|
*/
|
||||||
|
boolean addItems(Collection<T> items);
|
||||||
|
|
||||||
|
void clearItems();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an item from the algorithm
|
||||||
|
*
|
||||||
|
* @param item the item to be removed
|
||||||
|
* @return true if this algorithm contained the specified element (or equivalently, if this
|
||||||
|
* algorithm changed as a result of the call).
|
||||||
|
*/
|
||||||
|
boolean removeItem(T item);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the provided item in the algorithm
|
||||||
|
*
|
||||||
|
* @param item the item to be updated
|
||||||
|
* @return true if the item existed in the algorithm and was updated, or false if the item did
|
||||||
|
* not exist in the algorithm and the algorithm contents remain unchanged.
|
||||||
|
*/
|
||||||
|
boolean updateItem(T item);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a collection of items from the algorithm
|
||||||
|
*
|
||||||
|
* @param items the items to be removed
|
||||||
|
* @return true if this algorithm contents changed as a result of the call
|
||||||
|
*/
|
||||||
|
boolean removeItems(Collection<T> items);
|
||||||
|
|
||||||
|
Set<? extends Cluster<T>> getClusters(float zoom);
|
||||||
|
|
||||||
|
Collection<T> getItems();
|
||||||
|
|
||||||
|
void setMaxDistanceBetweenClusteredItems(int maxDistance);
|
||||||
|
|
||||||
|
int getMaxDistanceBetweenClusteredItems();
|
||||||
|
|
||||||
|
void lock();
|
||||||
|
|
||||||
|
void unlock();
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.clustering.algo;
|
||||||
|
|
||||||
|
import com.car2go.maps.model.LatLng;
|
||||||
|
import com.google.maps.android.clustering.Cluster;
|
||||||
|
import com.google.maps.android.clustering.ClusterItem;
|
||||||
|
import com.google.maps.android.geometry.Bounds;
|
||||||
|
import com.google.maps.android.geometry.Point;
|
||||||
|
import com.google.maps.android.projection.SphericalMercatorProjection;
|
||||||
|
import com.google.maps.android.quadtree.PointQuadTree;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple clustering algorithm with O(nlog n) performance. Resulting clusters are not
|
||||||
|
* hierarchical.
|
||||||
|
* <p/>
|
||||||
|
* High level algorithm:<br>
|
||||||
|
* 1. Iterate over items in the order they were added (candidate clusters).<br>
|
||||||
|
* 2. Create a cluster with the center of the item. <br>
|
||||||
|
* 3. Add all items that are within a certain distance to the cluster. <br>
|
||||||
|
* 4. Move any items out of an existing cluster if they are closer to another cluster. <br>
|
||||||
|
* 5. Remove those items from the list of candidate clusters.
|
||||||
|
* <p/>
|
||||||
|
* Clusters have the center of the first element (not the centroid of the items within it).
|
||||||
|
*/
|
||||||
|
public class NonHierarchicalDistanceBasedAlgorithm<T extends ClusterItem> extends AbstractAlgorithm<T> {
|
||||||
|
private static final int DEFAULT_MAX_DISTANCE_AT_ZOOM = 100; // essentially 100 dp.
|
||||||
|
|
||||||
|
private int mMaxDistance = DEFAULT_MAX_DISTANCE_AT_ZOOM;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any modifications should be synchronized on mQuadTree.
|
||||||
|
*/
|
||||||
|
private final Collection<QuadItem<T>> mItems = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any modifications should be synchronized on mQuadTree.
|
||||||
|
*/
|
||||||
|
private final PointQuadTree<QuadItem<T>> mQuadTree = new PointQuadTree<>(0, 1, 0, 1);
|
||||||
|
|
||||||
|
private static final SphericalMercatorProjection PROJECTION = new SphericalMercatorProjection(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an item to the algorithm
|
||||||
|
*
|
||||||
|
* @param item the item to be added
|
||||||
|
* @return true if the algorithm contents changed as a result of the call
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean addItem(T item) {
|
||||||
|
boolean result;
|
||||||
|
final QuadItem<T> quadItem = new QuadItem<>(item);
|
||||||
|
synchronized (mQuadTree) {
|
||||||
|
result = mItems.add(quadItem);
|
||||||
|
if (result) {
|
||||||
|
mQuadTree.add(quadItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a collection of items to the algorithm
|
||||||
|
*
|
||||||
|
* @param items the items to be added
|
||||||
|
* @return true if the algorithm contents changed as a result of the call
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean addItems(Collection<T> items) {
|
||||||
|
boolean result = false;
|
||||||
|
for (T item : items) {
|
||||||
|
boolean individualResult = addItem(item);
|
||||||
|
if (individualResult) {
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearItems() {
|
||||||
|
synchronized (mQuadTree) {
|
||||||
|
mItems.clear();
|
||||||
|
mQuadTree.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an item from the algorithm
|
||||||
|
*
|
||||||
|
* @param item the item to be removed
|
||||||
|
* @return true if this algorithm contained the specified element (or equivalently, if this
|
||||||
|
* algorithm changed as a result of the call).
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean removeItem(T item) {
|
||||||
|
boolean result;
|
||||||
|
// QuadItem delegates hashcode() and equals() to its item so,
|
||||||
|
// removing any QuadItem to that item will remove the item
|
||||||
|
final QuadItem<T> quadItem = new QuadItem<>(item);
|
||||||
|
synchronized (mQuadTree) {
|
||||||
|
result = mItems.remove(quadItem);
|
||||||
|
if (result) {
|
||||||
|
mQuadTree.remove(quadItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a collection of items from the algorithm
|
||||||
|
*
|
||||||
|
* @param items the items to be removed
|
||||||
|
* @return true if this algorithm contents changed as a result of the call
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean removeItems(Collection<T> items) {
|
||||||
|
boolean result = false;
|
||||||
|
synchronized (mQuadTree) {
|
||||||
|
for (T item : items) {
|
||||||
|
// QuadItem delegates hashcode() and equals() to its item so,
|
||||||
|
// removing any QuadItem to that item will remove the item
|
||||||
|
final QuadItem<T> quadItem = new QuadItem<>(item);
|
||||||
|
boolean individualResult = mItems.remove(quadItem);
|
||||||
|
if (individualResult) {
|
||||||
|
mQuadTree.remove(quadItem);
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the provided item in the algorithm
|
||||||
|
*
|
||||||
|
* @param item the item to be updated
|
||||||
|
* @return true if the item existed in the algorithm and was updated, or false if the item did
|
||||||
|
* not exist in the algorithm and the algorithm contents remain unchanged.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean updateItem(T item) {
|
||||||
|
// TODO - Can this be optimized to update the item in-place if the location hasn't changed?
|
||||||
|
boolean result;
|
||||||
|
synchronized (mQuadTree) {
|
||||||
|
result = removeItem(item);
|
||||||
|
if (result) {
|
||||||
|
// Only add the item if it was removed (to help prevent accidental duplicates on map)
|
||||||
|
result = addItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<? extends Cluster<T>> getClusters(float zoom) {
|
||||||
|
final int discreteZoom = (int) zoom;
|
||||||
|
|
||||||
|
final double zoomSpecificSpan = mMaxDistance / Math.pow(2, discreteZoom) / 256;
|
||||||
|
|
||||||
|
final Set<QuadItem<T>> visitedCandidates = new HashSet<>();
|
||||||
|
final Set<Cluster<T>> results = new HashSet<>();
|
||||||
|
final Map<QuadItem<T>, Double> distanceToCluster = new HashMap<>();
|
||||||
|
final Map<QuadItem<T>, StaticCluster<T>> itemToCluster = new HashMap<>();
|
||||||
|
|
||||||
|
synchronized (mQuadTree) {
|
||||||
|
for (QuadItem<T> candidate : getClusteringItems(mQuadTree, zoom)) {
|
||||||
|
if (visitedCandidates.contains(candidate)) {
|
||||||
|
// Candidate is already part of another cluster.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bounds searchBounds = createBoundsFromSpan(candidate.getPoint(), zoomSpecificSpan);
|
||||||
|
Collection<QuadItem<T>> clusterItems;
|
||||||
|
clusterItems = mQuadTree.search(searchBounds);
|
||||||
|
if (clusterItems.size() == 1) {
|
||||||
|
// Only the current marker is in range. Just add the single item to the results.
|
||||||
|
results.add(candidate);
|
||||||
|
visitedCandidates.add(candidate);
|
||||||
|
distanceToCluster.put(candidate, 0d);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
StaticCluster<T> cluster = new StaticCluster<>(candidate.mClusterItem.getPosition());
|
||||||
|
results.add(cluster);
|
||||||
|
|
||||||
|
for (QuadItem<T> clusterItem : clusterItems) {
|
||||||
|
Double existingDistance = distanceToCluster.get(clusterItem);
|
||||||
|
double distance = distanceSquared(clusterItem.getPoint(), candidate.getPoint());
|
||||||
|
if (existingDistance != null) {
|
||||||
|
// Item already belongs to another cluster. Check if it's closer to this cluster.
|
||||||
|
if (existingDistance < distance) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Move item to the closer cluster.
|
||||||
|
itemToCluster.get(clusterItem).remove(clusterItem.mClusterItem);
|
||||||
|
}
|
||||||
|
distanceToCluster.put(clusterItem, distance);
|
||||||
|
cluster.add(clusterItem.mClusterItem);
|
||||||
|
itemToCluster.put(clusterItem, cluster);
|
||||||
|
}
|
||||||
|
visitedCandidates.addAll(clusterItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Collection<QuadItem<T>> getClusteringItems(PointQuadTree<QuadItem<T>> quadTree, float zoom) {
|
||||||
|
return mItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<T> getItems() {
|
||||||
|
final Set<T> items = new LinkedHashSet<>();
|
||||||
|
synchronized (mQuadTree) {
|
||||||
|
for (QuadItem<T> quadItem : mItems) {
|
||||||
|
items.add(quadItem.mClusterItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMaxDistanceBetweenClusteredItems(int maxDistance) {
|
||||||
|
mMaxDistance = maxDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMaxDistanceBetweenClusteredItems() {
|
||||||
|
return mMaxDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double distanceSquared(Point a, Point b) {
|
||||||
|
return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bounds createBoundsFromSpan(Point p, double span) {
|
||||||
|
// TODO: Use a span that takes into account the visual size of the marker, not just its
|
||||||
|
// LatLng.
|
||||||
|
double halfSpan = span / 2;
|
||||||
|
return new Bounds(
|
||||||
|
p.x - halfSpan, p.x + halfSpan,
|
||||||
|
p.y - halfSpan, p.y + halfSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class QuadItem<T extends ClusterItem> implements PointQuadTree.Item, Cluster<T> {
|
||||||
|
private final T mClusterItem;
|
||||||
|
private final Point mPoint;
|
||||||
|
private final LatLng mPosition;
|
||||||
|
private Set<T> singletonSet;
|
||||||
|
|
||||||
|
private QuadItem(T item) {
|
||||||
|
mClusterItem = item;
|
||||||
|
mPosition = item.getPosition();
|
||||||
|
mPoint = PROJECTION.toPoint(mPosition);
|
||||||
|
singletonSet = Collections.singleton(mClusterItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Point getPoint() {
|
||||||
|
return mPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LatLng getPosition() {
|
||||||
|
return mPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<T> getItems() {
|
||||||
|
return singletonSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSize() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return mClusterItem.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (!(other instanceof QuadItem<?>)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((QuadItem<?>) other).mClusterItem.equals(mClusterItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.clustering.algo;
|
||||||
|
|
||||||
|
import com.car2go.maps.model.LatLng;
|
||||||
|
import com.google.maps.android.clustering.Cluster;
|
||||||
|
import com.google.maps.android.clustering.ClusterItem;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cluster whose center is determined upon creation.
|
||||||
|
*/
|
||||||
|
public class StaticCluster<T extends ClusterItem> implements Cluster<T> {
|
||||||
|
private final LatLng mCenter;
|
||||||
|
private final List<T> mItems = new ArrayList<T>();
|
||||||
|
|
||||||
|
public StaticCluster(LatLng center) {
|
||||||
|
mCenter = center;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean add(T t) {
|
||||||
|
return mItems.add(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LatLng getPosition() {
|
||||||
|
return mCenter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean remove(T t) {
|
||||||
|
return mItems.remove(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<T> getItems() {
|
||||||
|
return mItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSize() {
|
||||||
|
return mItems.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "StaticCluster{" +
|
||||||
|
"mCenter=" + mCenter +
|
||||||
|
", mItems.size=" + mItems.size() +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return mCenter.hashCode() + mItems.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (!(other instanceof StaticCluster<?>)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((StaticCluster<?>) other).mCenter.equals(mCenter)
|
||||||
|
&& ((StaticCluster<?>) other).mItems.equals(mItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.geometry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an area in the cartesian plane.
|
||||||
|
*/
|
||||||
|
public class Bounds {
|
||||||
|
public final double minX;
|
||||||
|
public final double minY;
|
||||||
|
|
||||||
|
public final double maxX;
|
||||||
|
public final double maxY;
|
||||||
|
|
||||||
|
public final double midX;
|
||||||
|
public final double midY;
|
||||||
|
|
||||||
|
public Bounds(double minX, double maxX, double minY, double maxY) {
|
||||||
|
this.minX = minX;
|
||||||
|
this.minY = minY;
|
||||||
|
this.maxX = maxX;
|
||||||
|
this.maxY = maxY;
|
||||||
|
|
||||||
|
midX = (minX + maxX) / 2;
|
||||||
|
midY = (minY + maxY) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(double x, double y) {
|
||||||
|
return minX <= x && x <= maxX && minY <= y && y <= maxY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(Point point) {
|
||||||
|
return contains(point.x, point.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean intersects(double minX, double maxX, double minY, double maxY) {
|
||||||
|
return minX < this.maxX && this.minX < maxX && minY < this.maxY && this.minY < maxY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean intersects(Bounds bounds) {
|
||||||
|
return intersects(bounds.minX, bounds.maxX, bounds.minY, bounds.maxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(Bounds bounds) {
|
||||||
|
return bounds.minX >= minX && bounds.maxX <= maxX && bounds.minY >= minY && bounds.maxY <= maxY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.geometry;
|
||||||
|
|
||||||
|
public class Point {
|
||||||
|
public final double x;
|
||||||
|
public final double y;
|
||||||
|
|
||||||
|
public Point(double x, double y) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Point{" +
|
||||||
|
"x=" + x +
|
||||||
|
", y=" + y +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.projection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated since 0.2. Use {@link com.google.maps.android.geometry.Point} instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public class Point extends com.google.maps.android.geometry.Point {
|
||||||
|
public Point(double x, double y) {
|
||||||
|
super(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.projection;
|
||||||
|
|
||||||
|
import com.car2go.maps.model.LatLng;
|
||||||
|
|
||||||
|
public class SphericalMercatorProjection {
|
||||||
|
final double mWorldWidth;
|
||||||
|
|
||||||
|
public SphericalMercatorProjection(final double worldWidth) {
|
||||||
|
mWorldWidth = worldWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public Point toPoint(final LatLng latLng) {
|
||||||
|
final double x = latLng.longitude / 360 + .5;
|
||||||
|
final double siny = Math.sin(Math.toRadians(latLng.latitude));
|
||||||
|
final double y = 0.5 * Math.log((1 + siny) / (1 - siny)) / -(2 * Math.PI) + .5;
|
||||||
|
|
||||||
|
return new Point(x * mWorldWidth, y * mWorldWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LatLng toLatLng(com.google.maps.android.geometry.Point point) {
|
||||||
|
final double x = point.x / mWorldWidth - 0.5;
|
||||||
|
final double lng = x * 360;
|
||||||
|
|
||||||
|
double y = .5 - (point.y / mWorldWidth);
|
||||||
|
final double lat = 90 - Math.toDegrees(Math.atan(Math.exp(-y * 2 * Math.PI)) * 2);
|
||||||
|
|
||||||
|
return new LatLng(lat, lng);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.quadtree;
|
||||||
|
|
||||||
|
import com.google.maps.android.geometry.Bounds;
|
||||||
|
import com.google.maps.android.geometry.Point;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A quad tree which tracks items with a Point geometry.
|
||||||
|
* See http://en.wikipedia.org/wiki/Quadtree for details on the data structure.
|
||||||
|
* This class is not thread safe.
|
||||||
|
*/
|
||||||
|
public class PointQuadTree<T extends PointQuadTree.Item> {
|
||||||
|
public interface Item {
|
||||||
|
Point getPoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bounds of this quad.
|
||||||
|
*/
|
||||||
|
private final Bounds mBounds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The depth of this quad in the tree.
|
||||||
|
*/
|
||||||
|
private final int mDepth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of elements to store in a quad before splitting.
|
||||||
|
*/
|
||||||
|
private final static int MAX_ELEMENTS = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The elements inside this quad, if any.
|
||||||
|
*/
|
||||||
|
private Set<T> mItems;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum depth.
|
||||||
|
*/
|
||||||
|
private final static int MAX_DEPTH = 40;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Child quads.
|
||||||
|
*/
|
||||||
|
private List<PointQuadTree<T>> mChildren = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new quad tree with specified bounds.
|
||||||
|
*
|
||||||
|
* @param minX
|
||||||
|
* @param maxX
|
||||||
|
* @param minY
|
||||||
|
* @param maxY
|
||||||
|
*/
|
||||||
|
public PointQuadTree(double minX, double maxX, double minY, double maxY) {
|
||||||
|
this(new Bounds(minX, maxX, minY, maxY));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PointQuadTree(Bounds bounds) {
|
||||||
|
this(bounds, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PointQuadTree(double minX, double maxX, double minY, double maxY, int depth) {
|
||||||
|
this(new Bounds(minX, maxX, minY, maxY), depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PointQuadTree(Bounds bounds, int depth) {
|
||||||
|
mBounds = bounds;
|
||||||
|
mDepth = depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert an item.
|
||||||
|
*/
|
||||||
|
public void add(T item) {
|
||||||
|
Point point = item.getPoint();
|
||||||
|
if (this.mBounds.contains(point.x, point.y)) {
|
||||||
|
insert(point.x, point.y, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insert(double x, double y, T item) {
|
||||||
|
if (this.mChildren != null) {
|
||||||
|
if (y < mBounds.midY) {
|
||||||
|
if (x < mBounds.midX) { // top left
|
||||||
|
mChildren.get(0).insert(x, y, item);
|
||||||
|
} else { // top right
|
||||||
|
mChildren.get(1).insert(x, y, item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (x < mBounds.midX) { // bottom left
|
||||||
|
mChildren.get(2).insert(x, y, item);
|
||||||
|
} else {
|
||||||
|
mChildren.get(3).insert(x, y, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mItems == null) {
|
||||||
|
mItems = new LinkedHashSet<>();
|
||||||
|
}
|
||||||
|
mItems.add(item);
|
||||||
|
if (mItems.size() > MAX_ELEMENTS && mDepth < MAX_DEPTH) {
|
||||||
|
split();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split this quad.
|
||||||
|
*/
|
||||||
|
private void split() {
|
||||||
|
mChildren = new ArrayList<PointQuadTree<T>>(4);
|
||||||
|
mChildren.add(new PointQuadTree<T>(mBounds.minX, mBounds.midX, mBounds.minY, mBounds.midY, mDepth + 1));
|
||||||
|
mChildren.add(new PointQuadTree<T>(mBounds.midX, mBounds.maxX, mBounds.minY, mBounds.midY, mDepth + 1));
|
||||||
|
mChildren.add(new PointQuadTree<T>(mBounds.minX, mBounds.midX, mBounds.midY, mBounds.maxY, mDepth + 1));
|
||||||
|
mChildren.add(new PointQuadTree<T>(mBounds.midX, mBounds.maxX, mBounds.midY, mBounds.maxY, mDepth + 1));
|
||||||
|
|
||||||
|
Set<T> items = mItems;
|
||||||
|
mItems = null;
|
||||||
|
|
||||||
|
for (T item : items) {
|
||||||
|
// re-insert items into child quads.
|
||||||
|
insert(item.getPoint().x, item.getPoint().y, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the given item from the set.
|
||||||
|
*
|
||||||
|
* @return whether the item was removed.
|
||||||
|
*/
|
||||||
|
public boolean remove(T item) {
|
||||||
|
Point point = item.getPoint();
|
||||||
|
if (this.mBounds.contains(point.x, point.y)) {
|
||||||
|
return remove(point.x, point.y, item);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean remove(double x, double y, T item) {
|
||||||
|
if (this.mChildren != null) {
|
||||||
|
if (y < mBounds.midY) {
|
||||||
|
if (x < mBounds.midX) { // top left
|
||||||
|
return mChildren.get(0).remove(x, y, item);
|
||||||
|
} else { // top right
|
||||||
|
return mChildren.get(1).remove(x, y, item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (x < mBounds.midX) { // bottom left
|
||||||
|
return mChildren.get(2).remove(x, y, item);
|
||||||
|
} else {
|
||||||
|
return mChildren.get(3).remove(x, y, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mItems == null) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return mItems.remove(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all points from the quadTree
|
||||||
|
*/
|
||||||
|
public void clear() {
|
||||||
|
mChildren = null;
|
||||||
|
if (mItems != null) {
|
||||||
|
mItems.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for all items within a given bounds.
|
||||||
|
*/
|
||||||
|
public Collection<T> search(Bounds searchBounds) {
|
||||||
|
final List<T> results = new ArrayList<T>();
|
||||||
|
search(searchBounds, results);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void search(Bounds searchBounds, Collection<T> results) {
|
||||||
|
if (!mBounds.intersects(searchBounds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mChildren != null) {
|
||||||
|
for (PointQuadTree<T> quad : mChildren) {
|
||||||
|
quad.search(searchBounds, results);
|
||||||
|
}
|
||||||
|
} else if (mItems != null) {
|
||||||
|
if (searchBounds.contains(mBounds)) {
|
||||||
|
results.addAll(mItems);
|
||||||
|
} else {
|
||||||
|
for (T item : mItems) {
|
||||||
|
if (searchBounds.contains(item.getPoint())) {
|
||||||
|
results.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,10 @@ import android.os.Build
|
|||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.NetworkType
|
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import net.vonforst.evmap.storage.CleanupCacheWorker
|
import net.vonforst.evmap.storage.CleanupCacheWorker
|
||||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||||
import net.vonforst.evmap.storage.UpdateFullDownloadWorker
|
|
||||||
import net.vonforst.evmap.ui.updateAppLocale
|
import net.vonforst.evmap.ui.updateAppLocale
|
||||||
import net.vonforst.evmap.ui.updateNightMode
|
import net.vonforst.evmap.ui.updateNightMode
|
||||||
import org.acra.config.dialog
|
import org.acra.config.dialog
|
||||||
@@ -70,7 +68,6 @@ class EvMapApplication : Application(), Configuration.Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val workManager = WorkManager.getInstance(this)
|
|
||||||
val cleanupCacheRequest = PeriodicWorkRequestBuilder<CleanupCacheWorker>(Duration.ofDays(1))
|
val cleanupCacheRequest = PeriodicWorkRequestBuilder<CleanupCacheWorker>(Duration.ofDays(1))
|
||||||
.setConstraints(Constraints.Builder().apply {
|
.setConstraints(Constraints.Builder().apply {
|
||||||
setRequiresBatteryNotLow(true)
|
setRequiresBatteryNotLow(true)
|
||||||
@@ -78,24 +75,9 @@ class EvMapApplication : Application(), Configuration.Provider {
|
|||||||
setRequiresDeviceIdle(true)
|
setRequiresDeviceIdle(true)
|
||||||
}
|
}
|
||||||
}.build()).build()
|
}.build()).build()
|
||||||
workManager.enqueueUniquePeriodicWork(
|
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
|
||||||
"CleanupCacheWorker", ExistingPeriodicWorkPolicy.UPDATE, cleanupCacheRequest
|
"CleanupCacheWorker", ExistingPeriodicWorkPolicy.UPDATE, cleanupCacheRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
val updateFullDownloadRequest =
|
|
||||||
PeriodicWorkRequestBuilder<UpdateFullDownloadWorker>(Duration.ofDays(7))
|
|
||||||
.setConstraints(Constraints.Builder().apply {
|
|
||||||
setRequiresBatteryNotLow(true)
|
|
||||||
setRequiredNetworkType(NetworkType.UNMETERED)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
setRequiresDeviceIdle(true)
|
|
||||||
}
|
|
||||||
}.build()).build()
|
|
||||||
workManager.enqueueUniquePeriodicWork(
|
|
||||||
"UpdateOsmWorker",
|
|
||||||
ExistingPeriodicWorkPolicy.UPDATE,
|
|
||||||
updateFullDownloadRequest
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val workManagerConfiguration = Configuration.Builder().build()
|
override val workManagerConfiguration = Configuration.Builder().build()
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.splashscreen.SplashScreen
|
import androidx.core.splashscreen.SplashScreen
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@@ -56,7 +55,6 @@ class MapsActivity : AppCompatActivity(),
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val splashScreen = installSplashScreen()
|
val splashScreen = installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
WindowCompat.enableEdgeToEdge(window)
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_maps)
|
setContentView(R.layout.activity_maps)
|
||||||
|
|
||||||
@@ -285,7 +283,7 @@ class MapsActivity : AppCompatActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openUrl(url: String, rootView: View, preferBrowser: Boolean = false) {
|
fun openUrl(url: String, rootView: View, preferBrowser: Boolean = true) {
|
||||||
val intent = CustomTabsIntent.Builder()
|
val intent = CustomTabsIntent.Builder()
|
||||||
.setDefaultColorSchemeParams(
|
.setDefaultColorSchemeParams(
|
||||||
CustomTabColorSchemeParams.Builder()
|
CustomTabColorSchemeParams.Builder()
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import net.vonforst.evmap.joinToSpannedString
|
|||||||
import net.vonforst.evmap.model.ChargeCard
|
import net.vonforst.evmap.model.ChargeCard
|
||||||
import net.vonforst.evmap.model.ChargeCardId
|
import net.vonforst.evmap.model.ChargeCardId
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
import net.vonforst.evmap.model.ChargeLocation
|
||||||
import net.vonforst.evmap.model.Coordinate
|
|
||||||
import net.vonforst.evmap.model.OpeningHoursDays
|
import net.vonforst.evmap.model.OpeningHoursDays
|
||||||
import net.vonforst.evmap.plus
|
import net.vonforst.evmap.plus
|
||||||
import net.vonforst.evmap.ui.currency
|
import net.vonforst.evmap.ui.currency
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import com.car2go.maps.model.LatLngBounds
|
|||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||||
import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper
|
|
||||||
import net.vonforst.evmap.model.*
|
import net.vonforst.evmap.model.*
|
||||||
import net.vonforst.evmap.viewmodel.Resource
|
import net.vonforst.evmap.viewmodel.Resource
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -59,27 +58,6 @@ interface ChargepointApi<out T : ReferenceData> {
|
|||||||
* Duration we are limited to if there is a required API local cache time limit.
|
* Duration we are limited to if there is a required API local cache time limit.
|
||||||
*/
|
*/
|
||||||
val cacheLimit: Duration
|
val cacheLimit: Duration
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this API supports querying for chargers at the backend
|
|
||||||
*
|
|
||||||
* This determines whether the getChargepoints, getChargepointsRadius and getChargepointDetail functions are supported.
|
|
||||||
*/
|
|
||||||
val supportsOnlineQueries: Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this API supports downloading the whole dataset into local storage
|
|
||||||
*
|
|
||||||
* This determines whether the getAllChargepoints function is supported.
|
|
||||||
*/
|
|
||||||
val supportsFullDownload: Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches all available chargers from this API.
|
|
||||||
*
|
|
||||||
* This may take a long time and should only be used when the user explicitly wants to download all chargers.
|
|
||||||
*/
|
|
||||||
suspend fun fullDownload(): FullDownloadResult<T>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StringProvider {
|
interface StringProvider {
|
||||||
@@ -101,7 +79,6 @@ fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"goingelectric" -> {
|
"goingelectric" -> {
|
||||||
GoingElectricApiWrapper(
|
GoingElectricApiWrapper(
|
||||||
ctx.getString(
|
ctx.getString(
|
||||||
@@ -109,11 +86,6 @@ fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"openstreetmap" -> {
|
|
||||||
OpenStreetMapApiWrapper()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,20 +100,4 @@ data class ChargepointList(val items: List<ChargepointListItem>, val isComplete:
|
|||||||
companion object {
|
companion object {
|
||||||
fun empty() = ChargepointList(emptyList(), true)
|
fun empty() = ChargepointList(emptyList(), true)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result returned from fullDownload() function.
|
|
||||||
*
|
|
||||||
* Note that [chargers] is implemented as a [Sequence] so that downloaded chargers can be saved
|
|
||||||
* while they are being parsed instead of having to keep all of them in RAM at once.
|
|
||||||
*
|
|
||||||
* [progress] is updated regularly to indicate the current download progress.
|
|
||||||
* [referenceData] will typically only be available once the download is completed, i.e. you have
|
|
||||||
* iterated over the whole sequence of [chargers].
|
|
||||||
*/
|
|
||||||
interface FullDownloadResult<out T : ReferenceData> {
|
|
||||||
val chargers: Sequence<ChargeLocation>
|
|
||||||
val progress: Float
|
|
||||||
val referenceData: T
|
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
package net.vonforst.evmap.api
|
package net.vonforst.evmap.api
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.RateLimiter
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
import kotlin.time.TimeSource
|
|
||||||
|
|
||||||
|
|
||||||
class RateLimitInterceptor : Interceptor {
|
class RateLimitInterceptor : Interceptor {
|
||||||
private val rateLimiter = SimpleRateLimiter(3.0)
|
private val rateLimiter = RateLimiter.create(3.0)
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
if (request.url.host == "ui-map.shellrecharge.com") {
|
if (request.url.host == "ui-map.shellrecharge.com") {
|
||||||
// limit requests sent to NewMotion to 3 per second
|
// limit requests sent to NewMotion to 3 per second
|
||||||
rateLimiter.acquire()
|
rateLimiter.acquire(1)
|
||||||
|
|
||||||
var response: Response = chain.proceed(request)
|
var response: Response = chain.proceed(request)
|
||||||
// 403 is how the NewMotion API indicates a rate limit error
|
// 403 is how the NewMotion API indicates a rate limit error
|
||||||
@@ -32,27 +30,4 @@ class RateLimitInterceptor : Interceptor {
|
|||||||
return chain.proceed(request)
|
return chain.proceed(request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
internal class SimpleRateLimiter(private val permitsPerSecond: Double) {
|
|
||||||
private val interval: Duration = (1.0 / permitsPerSecond).seconds
|
|
||||||
private var nextAvailable = TimeSource.Monotonic.markNow()
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun acquire() {
|
|
||||||
val now = TimeSource.Monotonic.markNow()
|
|
||||||
if (now < nextAvailable) {
|
|
||||||
val waitTime = nextAvailable - now
|
|
||||||
waitTime.sleep()
|
|
||||||
nextAvailable += interval
|
|
||||||
} else {
|
|
||||||
nextAvailable = now + interval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Duration.sleep() {
|
|
||||||
if (this.isPositive()) {
|
|
||||||
Thread.sleep(this.inWholeMilliseconds, (this.inWholeNanoseconds % 1_000_000).toInt())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,54 @@
|
|||||||
package net.vonforst.evmap.api
|
package net.vonforst.evmap.api
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
import net.vonforst.evmap.model.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
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
operator fun <T> JSONArray.iterator(): Iterator<T> =
|
||||||
|
(0 until length()).asSequence().map {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
get(it) as T
|
||||||
|
}.iterator()
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
suspend fun Call.await(): Response {
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
enqueue(object : Callback {
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
continuation.resume(response) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
if (continuation.isCancelled) return
|
||||||
|
continuation.resumeWithException(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
try {
|
||||||
|
cancel()
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
//Ignore cancel exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val plugNames = mapOf(
|
private val plugNames = mapOf(
|
||||||
Chargepoint.TYPE_1 to R.string.plug_type_1,
|
Chargepoint.TYPE_1 to R.string.plug_type_1,
|
||||||
Chargepoint.TYPE_2_UNKNOWN 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_PLUG to R.string.plug_type_2,
|
||||||
Chargepoint.TYPE_2_SOCKET to R.string.plug_type_2,
|
Chargepoint.TYPE_2_SOCKET to R.string.plug_type_2,
|
||||||
Chargepoint.TYPE_3A to R.string.plug_type_3a,
|
Chargepoint.TYPE_3 to R.string.plug_type_3,
|
||||||
Chargepoint.TYPE_3C to R.string.plug_type_3c,
|
|
||||||
Chargepoint.CCS_UNKNOWN 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_1 to R.string.plug_ccs,
|
||||||
Chargepoint.CCS_TYPE_2 to R.string.plug_ccs,
|
Chargepoint.CCS_TYPE_2 to R.string.plug_ccs,
|
||||||
@@ -64,7 +101,7 @@ fun iconForPlugType(type: String): Int =
|
|||||||
Chargepoint.CEE_ROT -> R.drawable.ic_connector_cee_rot
|
Chargepoint.CEE_ROT -> R.drawable.ic_connector_cee_rot
|
||||||
Chargepoint.TYPE_1 -> R.drawable.ic_connector_typ1
|
Chargepoint.TYPE_1 -> R.drawable.ic_connector_typ1
|
||||||
// TODO: add other connectors
|
// TODO: add other connectors
|
||||||
else -> R.drawable.ic_connector_unknown
|
else -> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 75, 100, 150, 200, 250, 300, 350)
|
val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 75, 100, 150, 200, 250, 300, 350)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.squareup.moshi.FromJson
|
|||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.ToJson
|
import com.squareup.moshi.ToJson
|
||||||
|
import net.vonforst.evmap.api.availability.tesla.LocalTimeAdapter
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
import net.vonforst.evmap.model.ChargeLocation
|
||||||
import net.vonforst.evmap.model.Chargepoint
|
import net.vonforst.evmap.model.Chargepoint
|
||||||
import net.vonforst.evmap.utils.distanceBetween
|
import net.vonforst.evmap.utils.distanceBetween
|
||||||
@@ -14,6 +15,7 @@ import retrofit2.http.GET
|
|||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
||||||
private const val maxDistance = 60 // max distance between reported positions in meters
|
private const val maxDistance = 60 // max distance between reported positions in meters
|
||||||
@@ -200,8 +202,7 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
|||||||
val id = index.toLong()
|
val id = index.toLong()
|
||||||
val power = connector.maxPowerInKw ?: 0.0
|
val power = connector.maxPowerInKw ?: 0.0
|
||||||
val type = when (connector.plugTypeName) {
|
val type = when (connector.plugTypeName) {
|
||||||
"Typ 3A" -> Chargepoint.TYPE_3A
|
"Typ 3A" -> Chargepoint.TYPE_3
|
||||||
"Typ 3C \"Scame\"" -> Chargepoint.TYPE_3C
|
|
||||||
"Typ 2" -> Chargepoint.TYPE_2_UNKNOWN
|
"Typ 2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||||
"Typ 1" -> Chargepoint.TYPE_1
|
"Typ 1" -> Chargepoint.TYPE_1
|
||||||
"Steckdose(D)" -> Chargepoint.SCHUKO
|
"Steckdose(D)" -> Chargepoint.SCHUKO
|
||||||
@@ -242,8 +243,8 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||||
val country = charger.chargepriceData?.country ?: charger.address?.country
|
val country = charger.chargepriceData?.country
|
||||||
|
?: charger.address?.country ?: return false
|
||||||
return when (charger.dataSource) {
|
return when (charger.dataSource) {
|
||||||
// list of countries as of 2023/04/14, according to
|
// list of countries as of 2023/04/14, according to
|
||||||
// https://www.enbw.com/elektromobilitaet/produkte/ladetarife
|
// https://www.enbw.com/elektromobilitaet/produkte/ladetarife
|
||||||
@@ -285,12 +286,6 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
|||||||
"ES",
|
"ES",
|
||||||
"CZ"
|
"CZ"
|
||||||
) && charger.chargepriceData?.network !in listOf("23", "3534")
|
) && charger.chargepriceData?.network !in listOf("23", "3534")
|
||||||
/* TODO: OSM usually does not have the country tagged. Therefore we currently just use
|
|
||||||
a bounding box to determine whether the charger is roughly in Europe */
|
|
||||||
"openstreetmap" -> charger.coordinates.lat in 35.0..72.0
|
|
||||||
&& charger.coordinates.lng in 25.0..65.0
|
|
||||||
&& charger.operator !in listOf("Tesla, Inc.", "Tesla")
|
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package net.vonforst.evmap.api.availability
|
package net.vonforst.evmap.api.availability
|
||||||
|
|
||||||
|
import androidx.car.app.model.DateTimeWithZone
|
||||||
import com.squareup.moshi.FromJson
|
import com.squareup.moshi.FromJson
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
@@ -12,8 +13,12 @@ import retrofit2.Retrofit
|
|||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.util.Locale
|
import java.time.format.DateTimeParseException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
||||||
private const val maxDistance = 60 // max distance between reported positions in meters
|
private const val maxDistance = 60 // max distance between reported positions in meters
|
||||||
@@ -175,7 +180,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
|||||||
val id = connector.uid
|
val id = connector.uid
|
||||||
val power = connector.electricalProperties.getPower()
|
val power = connector.electricalProperties.getPower()
|
||||||
val type = when (connector.connectorType.lowercase(Locale.ROOT)) {
|
val type = when (connector.connectorType.lowercase(Locale.ROOT)) {
|
||||||
"type3" -> Chargepoint.TYPE_3C
|
"type3" -> Chargepoint.TYPE_3
|
||||||
"type2" -> Chargepoint.TYPE_2_UNKNOWN
|
"type2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||||
"type1" -> Chargepoint.TYPE_1
|
"type1" -> Chargepoint.TYPE_1
|
||||||
"domestic" -> Chargepoint.SCHUKO
|
"domestic" -> Chargepoint.SCHUKO
|
||||||
@@ -221,7 +226,6 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
|||||||
return when (charger.dataSource) {
|
return when (charger.dataSource) {
|
||||||
"goingelectric" -> charger.network != "Tesla Supercharger"
|
"goingelectric" -> charger.network != "Tesla Supercharger"
|
||||||
"openchargemap" -> charger.chargepriceData?.network !in listOf("23", "3534")
|
"openchargemap" -> charger.chargepriceData?.network !in listOf("23", "3534")
|
||||||
"openstreetmap" -> charger.operator !in listOf("Tesla, Inc.", "Tesla")
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,6 @@ class TeslaGuestAvailabilityDetector(
|
|||||||
private var api = TeslaChargingGuestGraphQlApi.create(client, baseUrl)
|
private var api = TeslaChargingGuestGraphQlApi.create(client, baseUrl)
|
||||||
|
|
||||||
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
|
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
|
||||||
if (location.chargepoints.isEmpty() || location.chargepoints.any { !it.hasKnownPower() }) {
|
|
||||||
throw AvailabilityDetectorException("no candidates found.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val results = cuaApi.getTeslaLocations()
|
val results = cuaApi.getTeslaLocations()
|
||||||
|
|
||||||
val result =
|
val result =
|
||||||
@@ -153,7 +149,7 @@ class TeslaGuestAvailabilityDetector(
|
|||||||
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
|
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
|
||||||
val labelsMap = detailsMap.mapValues { it.value.map { it.label } }
|
val labelsMap = detailsMap.mapValues { it.value.map { it.label } }
|
||||||
|
|
||||||
val pricing = details.pricing?.copy(memberRates = guestPricing.await()?.userRates)
|
val pricing = details.pricing.copy(memberRates = guestPricing.await()?.userRates)
|
||||||
|
|
||||||
return ChargeLocationStatus(
|
return ChargeLocationStatus(
|
||||||
statusMap,
|
statusMap,
|
||||||
@@ -167,7 +163,6 @@ class TeslaGuestAvailabilityDetector(
|
|||||||
return when (charger.dataSource) {
|
return when (charger.dataSource) {
|
||||||
"goingelectric" -> charger.network == "Tesla Supercharger"
|
"goingelectric" -> charger.network == "Tesla Supercharger"
|
||||||
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
||||||
"openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla")
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,6 @@ class TeslaOwnerAvailabilityDetector(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
|
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
|
||||||
if (location.chargepoints.isEmpty() || location.chargepoints.any { !it.hasKnownPower() }) {
|
|
||||||
throw AvailabilityDetectorException("no candidates found.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val api = initApi()
|
val api = initApi()
|
||||||
val req = TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesRequest(
|
val req = TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesRequest(
|
||||||
TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesVariables(
|
TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesVariables(
|
||||||
@@ -45,7 +41,8 @@ class TeslaOwnerAvailabilityDetector(
|
|||||||
TeslaChargingOwnershipGraphQlApi.Coordinate(
|
TeslaChargingOwnershipGraphQlApi.Coordinate(
|
||||||
location.coordinates.lat - coordRange,
|
location.coordinates.lat - coordRange,
|
||||||
location.coordinates.lng + coordRange
|
location.coordinates.lng + coordRange
|
||||||
)
|
),
|
||||||
|
TeslaChargingOwnershipGraphQlApi.OpenToNonTeslasFilterValue(false)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -166,7 +163,6 @@ class TeslaOwnerAvailabilityDetector(
|
|||||||
return when (charger.dataSource) {
|
return when (charger.dataSource) {
|
||||||
"goingelectric" -> charger.network == "Tesla Supercharger"
|
"goingelectric" -> charger.network == "Tesla Supercharger"
|
||||||
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
||||||
"openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla")
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ interface TeslaChargingGuestGraphQlApi {
|
|||||||
val trtId: Long,
|
val trtId: Long,
|
||||||
val maxPowerKw: Int,
|
val maxPowerKw: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val pricing: Pricing?,
|
val pricing: Pricing,
|
||||||
val publicStallCount: Int
|
val publicStallCount: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -100,8 +100,7 @@ interface TeslaAuthenticationApi {
|
|||||||
.appendQueryParameter("code_challenge_method", "S256")
|
.appendQueryParameter("code_challenge_method", "S256")
|
||||||
.appendQueryParameter("redirect_uri", "https://auth.tesla.com/void/callback")
|
.appendQueryParameter("redirect_uri", "https://auth.tesla.com/void/callback")
|
||||||
.appendQueryParameter("response_type", "code")
|
.appendQueryParameter("response_type", "code")
|
||||||
.appendQueryParameter("scope", "openid email offline_access phone")
|
.appendQueryParameter("scope", "openid email offline_access")
|
||||||
.appendQueryParameter("is_in_app", "true")
|
|
||||||
.appendQueryParameter("state", "123").build()
|
.appendQueryParameter("state", "123").build()
|
||||||
|
|
||||||
val resultUrlPrefix = "https://auth.tesla.com/void/callback"
|
val resultUrlPrefix = "https://auth.tesla.com/void/callback"
|
||||||
@@ -184,7 +183,7 @@ interface TeslaChargingOwnershipGraphQlApi {
|
|||||||
val userLocation: Coordinate,
|
val userLocation: Coordinate,
|
||||||
val northwestCorner: Coordinate,
|
val northwestCorner: Coordinate,
|
||||||
val southeastCorner: Coordinate,
|
val southeastCorner: Coordinate,
|
||||||
val filters: List<String> = emptyList(),
|
val openToNonTeslasFilter: OpenToNonTeslasFilterValue,
|
||||||
val languageCode: String = "en",
|
val languageCode: String = "en",
|
||||||
val countryCode: String = "US",
|
val countryCode: String = "US",
|
||||||
//val vin: String = "",
|
//val vin: String = "",
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package net.vonforst.evmap.api.fronyx
|
package net.vonforst.evmap.api.fronyx
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.squareup.moshi.JsonDataException
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
|
import net.vonforst.evmap.api.availability.AvailabilityDetectorException
|
||||||
|
import net.vonforst.evmap.api.availability.AvailabilityRepository
|
||||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||||
import net.vonforst.evmap.api.nameForPlugType
|
import net.vonforst.evmap.api.nameForPlugType
|
||||||
@@ -10,6 +13,8 @@ import net.vonforst.evmap.model.ChargeLocation
|
|||||||
import net.vonforst.evmap.model.Chargepoint
|
import net.vonforst.evmap.model.Chargepoint
|
||||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||||
import net.vonforst.evmap.viewmodel.Resource
|
import net.vonforst.evmap.viewmodel.Resource
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import java.io.IOException
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@@ -50,9 +55,9 @@ class PredictionRepository(private val context: Context) {
|
|||||||
evseIds: Map<Chargepoint, List<String>>,
|
evseIds: Map<Chargepoint, List<String>>,
|
||||||
filteredConnectors: Set<String>?
|
filteredConnectors: Set<String>?
|
||||||
): Resource<List<FronyxEvseIdResponse>> {
|
): Resource<List<FronyxEvseIdResponse>> {
|
||||||
return Resource.success(null)
|
if (!prefs.predictionEnabled) return Resource.success(null)
|
||||||
|
|
||||||
/*val allEvseIds =
|
val allEvseIds =
|
||||||
evseIds.filterKeys {
|
evseIds.filterKeys {
|
||||||
FronyxApi.isChargepointSupported(charger, it) &&
|
FronyxApi.isChargepointSupported(charger, it) &&
|
||||||
filteredConnectors?.let { filtered ->
|
filteredConnectors?.let { filtered ->
|
||||||
@@ -84,7 +89,7 @@ class PredictionRepository(private val context: Context) {
|
|||||||
// malformed JSON response from fronyx API
|
// malformed JSON response from fronyx API
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return Resource.error(e.message, null)
|
return Resource.error(e.message, null)
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildPredictionGraph(
|
private fun buildPredictionGraph(
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import net.vonforst.evmap.addDebugInterceptors
|
|||||||
import net.vonforst.evmap.api.ChargepointApi
|
import net.vonforst.evmap.api.ChargepointApi
|
||||||
import net.vonforst.evmap.api.ChargepointList
|
import net.vonforst.evmap.api.ChargepointList
|
||||||
import net.vonforst.evmap.api.FiltersSQLQuery
|
import net.vonforst.evmap.api.FiltersSQLQuery
|
||||||
import net.vonforst.evmap.api.FullDownloadResult
|
|
||||||
import net.vonforst.evmap.api.StringProvider
|
import net.vonforst.evmap.api.StringProvider
|
||||||
import net.vonforst.evmap.api.mapPower
|
import net.vonforst.evmap.api.mapPower
|
||||||
import net.vonforst.evmap.api.mapPowerInverse
|
import net.vonforst.evmap.api.mapPowerInverse
|
||||||
@@ -160,12 +159,6 @@ class GoingElectricApiWrapper(
|
|||||||
override val name = "GoingElectric.de"
|
override val name = "GoingElectric.de"
|
||||||
override val id = "goingelectric"
|
override val id = "goingelectric"
|
||||||
override val cacheLimit = Duration.ofDays(1)
|
override val cacheLimit = Duration.ofDays(1)
|
||||||
override val supportsOnlineQueries = true
|
|
||||||
override val supportsFullDownload = false
|
|
||||||
|
|
||||||
override suspend fun fullDownload(): FullDownloadResult<GEReferenceData> {
|
|
||||||
throw NotImplementedError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getChargepoints(
|
override suspend fun getChargepoints(
|
||||||
referenceData: ReferenceData,
|
referenceData: ReferenceData,
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ data class GEChargepoint(val type: String, val power: Double, val count: Int) {
|
|||||||
return when (type) {
|
return when (type) {
|
||||||
Chargepoint.TYPE_1 -> "Typ1"
|
Chargepoint.TYPE_1 -> "Typ1"
|
||||||
Chargepoint.TYPE_2_UNKNOWN -> "Typ2"
|
Chargepoint.TYPE_2_UNKNOWN -> "Typ2"
|
||||||
Chargepoint.TYPE_3C -> "Typ3"
|
Chargepoint.TYPE_3 -> "Typ3"
|
||||||
Chargepoint.CCS_UNKNOWN -> "CCS"
|
Chargepoint.CCS_UNKNOWN -> "CCS"
|
||||||
Chargepoint.CCS_TYPE_2 -> "Typ2"
|
Chargepoint.CCS_TYPE_2 -> "Typ2"
|
||||||
Chargepoint.SCHUKO -> "Schuko"
|
Chargepoint.SCHUKO -> "Schuko"
|
||||||
@@ -225,7 +225,7 @@ data class GEChargepoint(val type: String, val power: Double, val count: Int) {
|
|||||||
return when (type) {
|
return when (type) {
|
||||||
"Typ1" -> Chargepoint.TYPE_1
|
"Typ1" -> Chargepoint.TYPE_1
|
||||||
"Typ2" -> Chargepoint.TYPE_2_UNKNOWN
|
"Typ2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||||
"Typ3" -> Chargepoint.TYPE_3C
|
"Typ3" -> Chargepoint.TYPE_3
|
||||||
"Tesla Supercharger CCS" -> Chargepoint.CCS_UNKNOWN
|
"Tesla Supercharger CCS" -> Chargepoint.CCS_UNKNOWN
|
||||||
"CCS" -> Chargepoint.CCS_UNKNOWN
|
"CCS" -> Chargepoint.CCS_UNKNOWN
|
||||||
"Schuko" -> Chargepoint.SCHUKO
|
"Schuko" -> Chargepoint.SCHUKO
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import net.vonforst.evmap.addDebugInterceptors
|
|||||||
import net.vonforst.evmap.api.ChargepointApi
|
import net.vonforst.evmap.api.ChargepointApi
|
||||||
import net.vonforst.evmap.api.ChargepointList
|
import net.vonforst.evmap.api.ChargepointList
|
||||||
import net.vonforst.evmap.api.FiltersSQLQuery
|
import net.vonforst.evmap.api.FiltersSQLQuery
|
||||||
import net.vonforst.evmap.api.FullDownloadResult
|
|
||||||
import net.vonforst.evmap.api.StringProvider
|
import net.vonforst.evmap.api.StringProvider
|
||||||
import net.vonforst.evmap.api.mapPower
|
import net.vonforst.evmap.api.mapPower
|
||||||
import net.vonforst.evmap.api.mapPowerInverse
|
import net.vonforst.evmap.api.mapPowerInverse
|
||||||
@@ -131,12 +130,6 @@ class OpenChargeMapApiWrapper(
|
|||||||
|
|
||||||
override val name = "OpenChargeMap.org"
|
override val name = "OpenChargeMap.org"
|
||||||
override val id = "openchargemap"
|
override val id = "openchargemap"
|
||||||
override val supportsOnlineQueries = true
|
|
||||||
override val supportsFullDownload = false
|
|
||||||
|
|
||||||
override suspend fun fullDownload(): FullDownloadResult<OCMReferenceData> {
|
|
||||||
throw NotImplementedError()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) =
|
private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) =
|
||||||
if (value == null || value.all) null else value.values.joinToString(",")
|
if (value == null || value.all) null else value.values.joinToString(",")
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ data class OCMChargepoint(
|
|||||||
mediaItems?.mapNotNull { it.convert() },
|
mediaItems?.mapNotNull { it.convert() },
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
cost?.takeIf { it.isNotBlank() }.let { Cost(descriptionShort = it) },
|
cost?.let { Cost(descriptionShort = it) },
|
||||||
dataProvider?.let { "© ${it.title}" + if (it.license != null) ". ${it.license}" else "" },
|
dataProvider?.let { "© ${it.title}" + if (it.license != null) ". ${it.license}" else "" },
|
||||||
ChargepriceData(
|
ChargepriceData(
|
||||||
addressInfo.countryISOCode(refData),
|
addressInfo.countryISOCode(refData),
|
||||||
@@ -180,8 +180,8 @@ data class OCMConnection(
|
|||||||
25L -> Chargepoint.TYPE_2_SOCKET
|
25L -> Chargepoint.TYPE_2_SOCKET
|
||||||
1036L -> Chargepoint.TYPE_2_PLUG
|
1036L -> Chargepoint.TYPE_2_PLUG
|
||||||
1L -> Chargepoint.TYPE_1
|
1L -> Chargepoint.TYPE_1
|
||||||
36L -> Chargepoint.TYPE_3A
|
36L -> Chargepoint.TYPE_3
|
||||||
26L -> Chargepoint.TYPE_3C
|
26L -> Chargepoint.TYPE_3
|
||||||
else -> title ?: ""
|
else -> title ?: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
package net.vonforst.evmap.api.openstreetmap
|
|
||||||
|
|
||||||
import com.squareup.moshi.FromJson
|
|
||||||
import com.squareup.moshi.JsonReader
|
|
||||||
import com.squareup.moshi.Moshi
|
|
||||||
import com.squareup.moshi.ToJson
|
|
||||||
import com.squareup.moshi.rawType
|
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import retrofit2.Converter
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
import java.time.Instant
|
|
||||||
import kotlin.math.floor
|
|
||||||
|
|
||||||
internal class InstantAdapter {
|
|
||||||
@FromJson
|
|
||||||
fun fromJson(value: Double?): Instant? = value?.let {
|
|
||||||
val seconds = floor(it).toLong()
|
|
||||||
val nanos = ((value - seconds) * 1e9).toLong()
|
|
||||||
Instant.ofEpochSecond(seconds, nanos)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ToJson
|
|
||||||
fun toJson(value: Instant?): Double? = value?.let {
|
|
||||||
it.epochSecond.toDouble() + it.nano / 1e9
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class OSMConverterFactory(val moshi: Moshi) : Converter.Factory() {
|
|
||||||
override fun responseBodyConverter(
|
|
||||||
type: Type,
|
|
||||||
annotations: Array<out Annotation>,
|
|
||||||
retrofit: Retrofit
|
|
||||||
): Converter<ResponseBody, *>? {
|
|
||||||
if (type.rawType != OSMDocument::class.java) return null
|
|
||||||
|
|
||||||
val instantAdapter = moshi.adapter(Instant::class.java)
|
|
||||||
val osmChargingStationAdapter = moshi.adapter(OSMChargingStation::class.java)
|
|
||||||
val longAdapter = moshi.adapter(Long::class.java)
|
|
||||||
return Converter<ResponseBody, OSMDocument> { body ->
|
|
||||||
val reader = JsonReader.of(body.source())
|
|
||||||
reader.beginObject()
|
|
||||||
|
|
||||||
var timestamp: Instant? = null
|
|
||||||
var doc: Sequence<OSMChargingStation>? = null
|
|
||||||
var count: Long? = null
|
|
||||||
while (reader.hasNext()) {
|
|
||||||
when (reader.nextName()) {
|
|
||||||
"timestamp" -> timestamp = instantAdapter.fromJson(reader)!!
|
|
||||||
"count" -> count = longAdapter.fromJson(reader)!!
|
|
||||||
"elements" -> {
|
|
||||||
doc = sequence {
|
|
||||||
reader.beginArray()
|
|
||||||
while (reader.hasNext()) {
|
|
||||||
yield(osmChargingStationAdapter.fromJson(reader)!!)
|
|
||||||
}
|
|
||||||
reader.endArray()
|
|
||||||
reader.close()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OSMDocument(timestamp!!, count!!, doc!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
package net.vonforst.evmap.api.openstreetmap
|
|
||||||
|
|
||||||
import android.database.DatabaseUtils
|
|
||||||
import com.car2go.maps.model.LatLng
|
|
||||||
import com.car2go.maps.model.LatLngBounds
|
|
||||||
import com.squareup.moshi.Moshi
|
|
||||||
import net.vonforst.evmap.BuildConfig
|
|
||||||
import net.vonforst.evmap.R
|
|
||||||
import net.vonforst.evmap.addDebugInterceptors
|
|
||||||
import net.vonforst.evmap.api.ChargepointApi
|
|
||||||
import net.vonforst.evmap.api.ChargepointList
|
|
||||||
import net.vonforst.evmap.api.FiltersSQLQuery
|
|
||||||
import net.vonforst.evmap.api.FullDownloadResult
|
|
||||||
import net.vonforst.evmap.api.StringProvider
|
|
||||||
import net.vonforst.evmap.api.mapPower
|
|
||||||
import net.vonforst.evmap.api.mapPowerInverse
|
|
||||||
import net.vonforst.evmap.api.nameForPlugType
|
|
||||||
import net.vonforst.evmap.api.openchargemap.ZonedDateTimeAdapter
|
|
||||||
import net.vonforst.evmap.api.powerSteps
|
|
||||||
import net.vonforst.evmap.model.BooleanFilter
|
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
|
||||||
import net.vonforst.evmap.model.Chargepoint
|
|
||||||
import net.vonforst.evmap.model.Filter
|
|
||||||
import net.vonforst.evmap.model.FilterValue
|
|
||||||
import net.vonforst.evmap.model.FilterValues
|
|
||||||
import net.vonforst.evmap.model.MultipleChoiceFilter
|
|
||||||
import net.vonforst.evmap.model.ReferenceData
|
|
||||||
import net.vonforst.evmap.model.SliderFilter
|
|
||||||
import net.vonforst.evmap.model.getBooleanValue
|
|
||||||
import net.vonforst.evmap.model.getMultipleChoiceValue
|
|
||||||
import net.vonforst.evmap.model.getSliderValue
|
|
||||||
import net.vonforst.evmap.viewmodel.Resource
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import retrofit2.Response
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import java.io.IOException
|
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
interface OpenStreetMapApi {
|
|
||||||
@GET("charging-stations-osm.json")
|
|
||||||
suspend fun getAllChargingStations(): Response<OSMDocument>
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val moshi = Moshi.Builder()
|
|
||||||
.add(ZonedDateTimeAdapter())
|
|
||||||
.add(InstantAdapter())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun create(
|
|
||||||
baseurl: String = "https://osm.ev-map.app/"
|
|
||||||
): OpenStreetMapApi {
|
|
||||||
val client = OkHttpClient.Builder().apply {
|
|
||||||
if (BuildConfig.DEBUG) addDebugInterceptors()
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val retrofit = Retrofit.Builder()
|
|
||||||
.baseUrl(baseurl)
|
|
||||||
.addConverterFactory(OSMConverterFactory(moshi))
|
|
||||||
.client(client)
|
|
||||||
.build()
|
|
||||||
return retrofit.create(OpenStreetMapApi::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class OpenStreetMapApiWrapper(baseurl: String = "https://osm.ev-map.app/") :
|
|
||||||
ChargepointApi<OSMReferenceData> {
|
|
||||||
override val name = "OpenStreetMap"
|
|
||||||
override val id = "openstreetmap"
|
|
||||||
override val cacheLimit = Duration.ofDays(300L)
|
|
||||||
override val supportsOnlineQueries = false
|
|
||||||
override val supportsFullDownload = true
|
|
||||||
|
|
||||||
val api = OpenStreetMapApi.create(baseurl)
|
|
||||||
|
|
||||||
override suspend fun getChargepoints(
|
|
||||||
referenceData: ReferenceData,
|
|
||||||
bounds: LatLngBounds,
|
|
||||||
zoom: Float,
|
|
||||||
useClustering: Boolean,
|
|
||||||
filters: FilterValues?
|
|
||||||
): Resource<ChargepointList> {
|
|
||||||
throw NotImplementedError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getChargepointsRadius(
|
|
||||||
referenceData: ReferenceData,
|
|
||||||
location: LatLng,
|
|
||||||
radius: Int,
|
|
||||||
zoom: Float,
|
|
||||||
useClustering: Boolean,
|
|
||||||
filters: FilterValues?
|
|
||||||
): Resource<ChargepointList> {
|
|
||||||
throw NotImplementedError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getChargepointDetail(
|
|
||||||
referenceData: ReferenceData,
|
|
||||||
id: Long
|
|
||||||
): Resource<ChargeLocation> {
|
|
||||||
throw NotImplementedError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getReferenceData(): Resource<OSMReferenceData> {
|
|
||||||
throw NotImplementedError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilters(
|
|
||||||
referenceData: ReferenceData,
|
|
||||||
sp: StringProvider
|
|
||||||
): List<Filter<FilterValue>> {
|
|
||||||
|
|
||||||
val plugs = listOf(
|
|
||||||
Chargepoint.TYPE_1,
|
|
||||||
Chargepoint.CCS_TYPE_1,
|
|
||||||
Chargepoint.TYPE_2_SOCKET,
|
|
||||||
Chargepoint.TYPE_2_PLUG,
|
|
||||||
Chargepoint.CCS_TYPE_2,
|
|
||||||
Chargepoint.CHADEMO,
|
|
||||||
Chargepoint.SUPERCHARGER,
|
|
||||||
Chargepoint.CEE_BLAU,
|
|
||||||
Chargepoint.CEE_ROT,
|
|
||||||
Chargepoint.SCHUKO
|
|
||||||
)
|
|
||||||
val plugMap = plugs.associateWith { plug ->
|
|
||||||
nameForPlugType(sp, plug)
|
|
||||||
}
|
|
||||||
|
|
||||||
val refData = referenceData as OSMReferenceData
|
|
||||||
val networkMap = refData.networks.associateWith { it }
|
|
||||||
|
|
||||||
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_1,
|
|
||||||
Chargepoint.TYPE_2_SOCKET,
|
|
||||||
Chargepoint.TYPE_2_PLUG,
|
|
||||||
Chargepoint.CCS_TYPE_1,
|
|
||||||
Chargepoint.CCS_TYPE_2,
|
|
||||||
Chargepoint.CHADEMO
|
|
||||||
),
|
|
||||||
manyChoices = true
|
|
||||||
),
|
|
||||||
MultipleChoiceFilter(
|
|
||||||
sp.getString(R.string.filter_networks), "networks",
|
|
||||||
networkMap, manyChoices = true
|
|
||||||
),
|
|
||||||
SliderFilter(
|
|
||||||
sp.getString(R.string.filter_min_connectors),
|
|
||||||
"min_connectors",
|
|
||||||
10,
|
|
||||||
min = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convertFiltersToSQL(
|
|
||||||
filters: FilterValues,
|
|
||||||
referenceData: ReferenceData
|
|
||||||
): FiltersSQLQuery {
|
|
||||||
if (filters.isEmpty()) return FiltersSQLQuery("", false, false)
|
|
||||||
var requiresChargepointQuery = false
|
|
||||||
|
|
||||||
val result = StringBuilder()
|
|
||||||
if (filters.getBooleanValue("freecharging") == true) {
|
|
||||||
result.append(" AND freecharging IS 1")
|
|
||||||
}
|
|
||||||
if (filters.getBooleanValue("freeparking") == true) {
|
|
||||||
result.append(" AND freeparking IS 1")
|
|
||||||
}
|
|
||||||
if (filters.getBooleanValue("open_247") == true) {
|
|
||||||
result.append(" AND twentyfourSeven IS 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
val minPower = filters.getSliderValue("min_power")
|
|
||||||
if (minPower != null && minPower > 0) {
|
|
||||||
result.append(" AND json_extract(cp.value, '$.power') >= ${minPower}")
|
|
||||||
requiresChargepointQuery = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val connectors = filters.getMultipleChoiceValue("connectors")
|
|
||||||
if (connectors != null && !connectors.all) {
|
|
||||||
val connectorsList = if (connectors.values.size == 0) {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
connectors.values.joinToString(",") {
|
|
||||||
DatabaseUtils.sqlEscapeString(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.append(" AND json_extract(cp.value, '$.type') IN (${connectorsList})")
|
|
||||||
requiresChargepointQuery = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val minConnectors = filters.getSliderValue("min_connectors")
|
|
||||||
if (minConnectors != null && minConnectors > 1) {
|
|
||||||
result.append(" GROUP BY ChargeLocation.id HAVING SUM(json_extract(cp.value, '$.count')) >= $minConnectors")
|
|
||||||
requiresChargepointQuery = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val networks = filters.getMultipleChoiceValue("networks")
|
|
||||||
if (networks != null && !networks.all) {
|
|
||||||
val networksList = if (networks.values.size == 0) {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
networks.values.joinToString(",") { DatabaseUtils.sqlEscapeString(it) }
|
|
||||||
}
|
|
||||||
result.append(" AND network IN (${networksList})")
|
|
||||||
}
|
|
||||||
|
|
||||||
return FiltersSQLQuery(result.toString(), requiresChargepointQuery, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun fullDownload(): FullDownloadResult<OSMReferenceData> {
|
|
||||||
val response = api.getAllChargingStations()
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
throw IOException(response.message())
|
|
||||||
} else {
|
|
||||||
val body = response.body()!!
|
|
||||||
return OSMFullDownloadResult(body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class OSMReferenceData(val networks: List<String>) : ReferenceData()
|
|
||||||
|
|
||||||
class OSMFullDownloadResult(private val body: OSMDocument) : FullDownloadResult<OSMReferenceData> {
|
|
||||||
private var downloadProgress = 0f
|
|
||||||
private var refData: OSMReferenceData? = null
|
|
||||||
|
|
||||||
override val chargers: Sequence<ChargeLocation>
|
|
||||||
get() {
|
|
||||||
val time = body.timestamp
|
|
||||||
val networks = mutableListOf<String>()
|
|
||||||
|
|
||||||
return sequence {
|
|
||||||
body.elements.forEachIndexed { i, it ->
|
|
||||||
val charger = it.convert(time)
|
|
||||||
yield(charger)
|
|
||||||
downloadProgress = i.toFloat() / body.count
|
|
||||||
charger.network?.let { networks.add(it) }
|
|
||||||
}
|
|
||||||
refData = OSMReferenceData(networks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override val progress: Float
|
|
||||||
get() = downloadProgress
|
|
||||||
override val referenceData: OSMReferenceData
|
|
||||||
get() = refData
|
|
||||||
?: throw UnsupportedOperationException("referenceData is only available once download is complete")
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package net.vonforst.evmap.api.openstreetmap
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import net.vonforst.evmap.model.*
|
import net.vonforst.evmap.model.*
|
||||||
import okhttp3.internal.immutableListOf
|
import okhttp3.internal.immutableListOf
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -41,7 +40,6 @@ private val SOCKET_TYPES = immutableListOf(
|
|||||||
// Tesla
|
// Tesla
|
||||||
OsmSocket("tesla_standard", null),
|
OsmSocket("tesla_standard", null),
|
||||||
OsmSocket("tesla_supercharger", Chargepoint.SUPERCHARGER),
|
OsmSocket("tesla_supercharger", Chargepoint.SUPERCHARGER),
|
||||||
OsmSocket("tesla_supercharger_ccs", Chargepoint.CCS_UNKNOWN),
|
|
||||||
|
|
||||||
// CEE
|
// CEE
|
||||||
OsmSocket("cee_blue", Chargepoint.CEE_BLAU), // Also known as "caravan socket"
|
OsmSocket("cee_blue", Chargepoint.CEE_BLAU), // Also known as "caravan socket"
|
||||||
@@ -60,12 +58,6 @@ private val SOCKET_TYPES = immutableListOf(
|
|||||||
OsmSocket("sev1011_t25", null),
|
OsmSocket("sev1011_t25", null),
|
||||||
)
|
)
|
||||||
|
|
||||||
data class OSMDocument(
|
|
||||||
val timestamp: Instant,
|
|
||||||
val count: Long,
|
|
||||||
val elements: Sequence<OSMChargingStation>
|
|
||||||
)
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class OSMChargingStation(
|
data class OSMChargingStation(
|
||||||
// Unique numeric ID
|
// Unique numeric ID
|
||||||
@@ -95,7 +87,7 @@ data class OSMChargingStation(
|
|||||||
"openstreetmap",
|
"openstreetmap",
|
||||||
getName(),
|
getName(),
|
||||||
Coordinate(lat, lon),
|
Coordinate(lat, lon),
|
||||||
getAddress(),
|
null, // TODO: Can we determine this with overpass?
|
||||||
getChargepoints(),
|
getChargepoints(),
|
||||||
tags["network"],
|
tags["network"],
|
||||||
"https://www.openstreetmap.org/node/$id",
|
"https://www.openstreetmap.org/node/$id",
|
||||||
@@ -107,31 +99,18 @@ data class OSMChargingStation(
|
|||||||
tags["description"],
|
tags["description"],
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
getPhotos(),
|
null,
|
||||||
null,
|
null,
|
||||||
getOpeningHours(),
|
getOpeningHours(),
|
||||||
getCost(),
|
getCost(),
|
||||||
"© OpenStreetMap contributors",
|
"© OpenStreetMap contributors",
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
tags["website"],
|
null,
|
||||||
dataFetchTimestamp,
|
dataFetchTimestamp,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getAddress(): Address? {
|
|
||||||
val city = tags["addr:city"]
|
|
||||||
val country = tags["addr:country"]
|
|
||||||
val postcode = tags["addr:postcode"]
|
|
||||||
val street = tags["addr:street"]
|
|
||||||
val housenumber = tags["addr:housenumber"] ?: tags["addr:housename"]
|
|
||||||
return if (listOf(city, country, postcode, street, housenumber).any { it != null }) {
|
|
||||||
Address(city, country, postcode, "$street $housenumber")
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the name for this charging station.
|
* Return the name for this charging station.
|
||||||
*/
|
*/
|
||||||
@@ -186,7 +165,7 @@ data class OSMChargingStation(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCost(): Cost {
|
private fun getCost(): Cost? {
|
||||||
val freecharging = when (tags["fee"]?.lowercase()) {
|
val freecharging = when (tags["fee"]?.lowercase()) {
|
||||||
"yes", "y" -> false
|
"yes", "y" -> false
|
||||||
"no", "n" -> true
|
"no", "n" -> true
|
||||||
@@ -197,28 +176,7 @@ data class OSMChargingStation(
|
|||||||
"yes", "y", "interval" -> false
|
"yes", "y", "interval" -> false
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
val description = listOfNotNull(tags["charge"], tags["charge:conditional"]).ifEmpty { null }
|
return Cost(freecharging, freeparking)
|
||||||
?.joinToString("\n")
|
|
||||||
return Cost(freecharging, freeparking, null, description)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPhotos(): List<ChargerPhoto> {
|
|
||||||
val photos = mutableListOf<ChargerPhoto>()
|
|
||||||
for (i in -1..9) {
|
|
||||||
val url = tags["image" + if (i >= 0) ":$i" else ""]
|
|
||||||
if (url != null) {
|
|
||||||
if (url.startsWith("https://i.imgur.com")) {
|
|
||||||
ImgurChargerPhoto.create(url)?.let { photos.add(it) }
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
TODO: Imgur seems to be by far the most common image hoster (650 images),
|
|
||||||
followed by Mapillary (450, requires an API key to retrieve images)
|
|
||||||
Other than that, we have Google Photos, Wikimedia Commons (100-150 images each).
|
|
||||||
And there are some other links to various sites, but not all are valid links pointing directly to a JPEG file...
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return photos
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -243,26 +201,4 @@ data class OSMChargingStation(
|
|||||||
return numberString.toDoubleOrNull()
|
return numberString.toDoubleOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
class ImgurChargerPhoto(override val id: String) : ChargerPhoto(id) {
|
|
||||||
override fun getUrl(height: Int?, width: Int?, size: Int?, allowOriginal: Boolean): String {
|
|
||||||
return if (allowOriginal) {
|
|
||||||
"https://i.imgur.com/$id.jpg"
|
|
||||||
} else {
|
|
||||||
val value = width ?: size ?: height
|
|
||||||
"https://i.imgur.com/${id}_d.jpg?maxwidth=$value"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val regex = Regex("https?://i.imgur.com/([\\w\\d]+)(?:_d)?.(?:webp|jpg)")
|
|
||||||
|
|
||||||
fun create(url: String): ImgurChargerPhoto? {
|
|
||||||
val id = regex.find(url)?.groups?.get(1)?.value
|
|
||||||
return id?.let { ImgurChargerPhoto(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,6 @@ import androidx.lifecycle.DefaultLifecycleObserver
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.car2go.maps.model.LatLng
|
import com.car2go.maps.model.LatLng
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
|
||||||
import net.vonforst.evmap.location.FusionEngine
|
import net.vonforst.evmap.location.FusionEngine
|
||||||
import net.vonforst.evmap.location.LocationEngine
|
import net.vonforst.evmap.location.LocationEngine
|
||||||
import net.vonforst.evmap.location.Priority
|
import net.vonforst.evmap.location.Priority
|
||||||
@@ -132,11 +131,8 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateScreen(intent: Intent): Screen {
|
override fun onCreateScreen(intent: Intent): Screen {
|
||||||
val mapScreen = if (supportsNewMapScreen(carContext)) {
|
|
||||||
MapScreen(carContext, this)
|
val mapScreen = MapScreen(carContext, this)
|
||||||
} else {
|
|
||||||
LegacyMapScreen(carContext, this)
|
|
||||||
}
|
|
||||||
val screens = mutableListOf<Screen>(mapScreen)
|
val screens = mutableListOf<Screen>(mapScreen)
|
||||||
|
|
||||||
handleActionsIntent(intent)?.let {
|
handleActionsIntent(intent)?.let {
|
||||||
@@ -196,7 +192,7 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
|
|||||||
val lon = it.getQueryParameter("longitude")?.toDouble()
|
val lon = it.getQueryParameter("longitude")?.toDouble()
|
||||||
val name = it.getQueryParameter("name")
|
val name = it.getQueryParameter("name")
|
||||||
if (lat != null && lon != null) {
|
if (lat != null && lon != null) {
|
||||||
prefs.placeSearchResultAndroidAuto = PlaceWithBounds(LatLng(lat, lon), null)
|
prefs.placeSearchResultAndroidAuto = LatLng(lat, lon)
|
||||||
prefs.placeSearchResultAndroidAutoName = name ?: "%.4f,%.4f".format(lat, lon)
|
prefs.placeSearchResultAndroidAutoName = name ?: "%.4f,%.4f".format(lat, lon)
|
||||||
return null
|
return null
|
||||||
} else if (name != null) {
|
} else if (name != null) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package net.vonforst.evmap.auto
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
@@ -10,8 +11,10 @@ import android.net.Uri
|
|||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
|
import android.util.Log
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.CarToast
|
import androidx.car.app.CarToast
|
||||||
|
import androidx.car.app.HostException
|
||||||
import androidx.car.app.Screen
|
import androidx.car.app.Screen
|
||||||
import androidx.car.app.annotations.ExperimentalCarApi
|
import androidx.car.app.annotations.ExperimentalCarApi
|
||||||
import androidx.car.app.constraints.ConstraintManager
|
import androidx.car.app.constraints.ConstraintManager
|
||||||
@@ -51,7 +54,9 @@ import net.vonforst.evmap.api.availability.tesla.Pricing
|
|||||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||||
import net.vonforst.evmap.api.createApi
|
import net.vonforst.evmap.api.createApi
|
||||||
import net.vonforst.evmap.api.fronyx.PredictionData
|
import net.vonforst.evmap.api.fronyx.PredictionData
|
||||||
import net.vonforst.evmap.api.fronyx.PredictionRepository
|
import net.vonforst.evmap.api.iconForPlugType
|
||||||
|
import net.vonforst.evmap.api.nameForPlugType
|
||||||
|
import net.vonforst.evmap.api.stringProvider
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
import net.vonforst.evmap.model.ChargeLocation
|
||||||
import net.vonforst.evmap.model.Cost
|
import net.vonforst.evmap.model.Cost
|
||||||
import net.vonforst.evmap.model.FaultReport
|
import net.vonforst.evmap.model.FaultReport
|
||||||
@@ -61,8 +66,8 @@ import net.vonforst.evmap.storage.AppDatabase
|
|||||||
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
||||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||||
|
import net.vonforst.evmap.ui.availabilityText
|
||||||
import net.vonforst.evmap.ui.getMarkerTint
|
import net.vonforst.evmap.ui.getMarkerTint
|
||||||
import net.vonforst.evmap.utils.formatDMS
|
|
||||||
import net.vonforst.evmap.viewmodel.Status
|
import net.vonforst.evmap.viewmodel.Status
|
||||||
import net.vonforst.evmap.viewmodel.awaitFinished
|
import net.vonforst.evmap.viewmodel.awaitFinished
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@@ -71,6 +76,8 @@ import java.time.format.FormatStyle
|
|||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
private const val TAG = "ChargerDetailScreen"
|
||||||
|
|
||||||
@ExperimentalCarApi
|
@ExperimentalCarApi
|
||||||
class ChargerDetailScreen(
|
class ChargerDetailScreen(
|
||||||
ctx: CarContext,
|
ctx: CarContext,
|
||||||
@@ -90,7 +97,7 @@ class ChargerDetailScreen(
|
|||||||
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
||||||
private val availabilityRepo = AvailabilityRepository(ctx)
|
private val availabilityRepo = AvailabilityRepository(ctx)
|
||||||
|
|
||||||
private val predictionRepo = PredictionRepository(ctx)
|
//private val predictionRepo = PredictionRepository(ctx)
|
||||||
private val timeFormat = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
private val timeFormat = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||||
|
|
||||||
private val imageSize = 128 // images should be 128dp according to docs
|
private val imageSize = 128 // images should be 128dp according to docs
|
||||||
@@ -136,7 +143,7 @@ class ChargerDetailScreen(
|
|||||||
.setFlags(Action.FLAG_PRIMARY)
|
.setFlags(Action.FLAG_PRIMARY)
|
||||||
.setBackgroundColor(CarColor.PRIMARY)
|
.setBackgroundColor(CarColor.PRIMARY)
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
navigateToCharger(carContext, session.cas, charger)
|
navigateToCharger(charger)
|
||||||
}
|
}
|
||||||
.build())
|
.build())
|
||||||
if (ChargepriceApi.isChargerSupported(charger)) {
|
if (ChargepriceApi.isChargerSupported(charger)) {
|
||||||
@@ -259,7 +266,7 @@ class ChargerDetailScreen(
|
|||||||
|
|
||||||
// Row 1: address + chargepoints
|
// Row 1: address + chargepoints
|
||||||
rows.add(Row.Builder().apply {
|
rows.add(Row.Builder().apply {
|
||||||
setTitle(charger.address?.toString() ?: charger.coordinates.formatDMS())
|
setTitle(charger.address.toString())
|
||||||
|
|
||||||
if (photo == null) {
|
if (photo == null) {
|
||||||
// show just the icon
|
// show just the icon
|
||||||
@@ -279,7 +286,7 @@ class ChargerDetailScreen(
|
|||||||
Row.IMAGE_TYPE_LARGE
|
Row.IMAGE_TYPE_LARGE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
addText(generateChargepointsText(charger, availability, carContext))
|
addText(generateChargepointsText(charger))
|
||||||
}.build())
|
}.build())
|
||||||
if (maxRows <= 3) {
|
if (maxRows <= 3) {
|
||||||
// row 2: operator + cost + fault report
|
// row 2: operator + cost + fault report
|
||||||
@@ -492,6 +499,47 @@ class ChargerDetailScreen(
|
|||||||
return string
|
return string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun generateChargepointsText(charger: ChargeLocation): SpannableStringBuilder {
|
||||||
|
val chargepointsText = SpannableStringBuilder()
|
||||||
|
charger.chargepointsMerged.forEachIndexed { i, cp ->
|
||||||
|
chargepointsText.apply {
|
||||||
|
if (i > 0) append(" · ")
|
||||||
|
append("${cp.count}× ")
|
||||||
|
val plugIcon = iconForPlugType(cp.type)
|
||||||
|
if (plugIcon != 0) {
|
||||||
|
append(
|
||||||
|
nameForPlugType(carContext.stringProvider(), cp.type),
|
||||||
|
CarIconSpan.create(
|
||||||
|
CarIcon.Builder(
|
||||||
|
IconCompat.createWithResource(
|
||||||
|
carContext,
|
||||||
|
plugIcon
|
||||||
|
)
|
||||||
|
).setTint(
|
||||||
|
CarColor.createCustom(Color.WHITE, Color.BLACK)
|
||||||
|
).build()
|
||||||
|
),
|
||||||
|
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
append(nameForPlugType(carContext.stringProvider(), cp.type))
|
||||||
|
}
|
||||||
|
cp.formatPower(carContext.currentOrDefaultLocale)?.let {
|
||||||
|
append(" ")
|
||||||
|
append(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
availability?.status?.get(cp)?.let { status ->
|
||||||
|
chargepointsText.append(
|
||||||
|
" (${availabilityText(status)}/${cp.count})",
|
||||||
|
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chargepointsText
|
||||||
|
}
|
||||||
|
|
||||||
private fun generateOperatorText(charger: ChargeLocation) =
|
private fun generateOperatorText(charger: ChargeLocation) =
|
||||||
if (charger.operator != null && charger.network != null) {
|
if (charger.operator != null && charger.network != null) {
|
||||||
if (charger.operator.contains(charger.network)) {
|
if (charger.operator.contains(charger.network)) {
|
||||||
@@ -509,6 +557,58 @@ class ChargerDetailScreen(
|
|||||||
carContext.getString(R.string.unknown_operator)
|
carContext.getString(R.string.unknown_operator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun navigateToCharger(charger: ChargeLocation) {
|
||||||
|
var success = navigateCarApp(charger)
|
||||||
|
if (!success && BuildConfig.FLAVOR_automotive == "automotive") {
|
||||||
|
// on AAOS, some OEMs' navigation apps might not support
|
||||||
|
success = navigateRegularApp(charger)
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
CarToast.makeText(carContext, R.string.no_maps_app_found, CarToast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateCarApp(charger: ChargeLocation): Boolean {
|
||||||
|
val coord = charger.coordinates
|
||||||
|
val intent =
|
||||||
|
Intent(
|
||||||
|
CarContext.ACTION_NAVIGATE,
|
||||||
|
Uri.parse("geo:${coord.lat},${coord.lng}")
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
carContext.startCarApp(intent)
|
||||||
|
return true
|
||||||
|
} catch (e: HostException) {
|
||||||
|
Log.w(TAG, "Could not start navigation using car app intent")
|
||||||
|
Log.w(TAG, intent.toString())
|
||||||
|
e.printStackTrace()
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.w(TAG, "Could not start navigation using car app intent")
|
||||||
|
Log.w(TAG, intent.toString())
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateRegularApp(charger: ChargeLocation): Boolean {
|
||||||
|
val coord = charger.coordinates
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
intent.data = Uri.parse(
|
||||||
|
"geo:${coord.lat},${coord.lng}?q=${coord.lat},${coord.lng}(${
|
||||||
|
Uri.encode(charger.name)
|
||||||
|
})"
|
||||||
|
)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
if (intent.resolveActivity(carContext.packageManager) != null) {
|
||||||
|
carContext.startActivity(intent)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Could not start navigation using regular intent")
|
||||||
|
Log.w(TAG, intent.toString())
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadCharger() {
|
private fun loadCharger() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
favorite = db.favoritesDao().findFavorite(chargerSparse.id, chargerSparse.dataSource)
|
favorite = db.favoritesDao().findFavorite(chargerSparse.id, chargerSparse.dataSource)
|
||||||
@@ -574,7 +674,7 @@ class ChargerDetailScreen(
|
|||||||
|
|
||||||
invalidate()
|
invalidate()
|
||||||
|
|
||||||
prediction = predictionRepo.getPredictionData(charger, availability)
|
//prediction = predictionRepo.getPredictionData(charger, availability)
|
||||||
|
|
||||||
invalidate()
|
invalidate()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,249 +0,0 @@
|
|||||||
package net.vonforst.evmap.auto
|
|
||||||
|
|
||||||
import android.location.Location
|
|
||||||
import android.text.SpannableString
|
|
||||||
import android.text.SpannableStringBuilder
|
|
||||||
import android.text.Spanned
|
|
||||||
import androidx.car.app.CarContext
|
|
||||||
import androidx.car.app.annotations.ExperimentalCarApi
|
|
||||||
import androidx.car.app.hardware.info.EnergyLevel
|
|
||||||
import androidx.car.app.model.Action
|
|
||||||
import androidx.car.app.model.CarColor
|
|
||||||
import androidx.car.app.model.CarIcon
|
|
||||||
import androidx.car.app.model.CarIconSpan
|
|
||||||
import androidx.car.app.model.CarLocation
|
|
||||||
import androidx.car.app.model.CarText
|
|
||||||
import androidx.car.app.model.DistanceSpan
|
|
||||||
import androidx.car.app.model.ForegroundCarColorSpan
|
|
||||||
import androidx.car.app.model.ItemList
|
|
||||||
import androidx.car.app.model.Metadata
|
|
||||||
import androidx.car.app.model.Pane
|
|
||||||
import androidx.car.app.model.Place
|
|
||||||
import androidx.car.app.model.PlaceMarker
|
|
||||||
import androidx.car.app.model.Row
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import net.vonforst.evmap.R
|
|
||||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
|
||||||
import net.vonforst.evmap.model.FILTERS_FAVORITES
|
|
||||||
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.utils.formatDecimal
|
|
||||||
import java.time.ZonedDateTime
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
interface ChargerListDelegate : ItemList.OnItemVisibilityChangedListener {
|
|
||||||
val locationError: Boolean
|
|
||||||
val loadingError: Boolean
|
|
||||||
val maxRows: Int
|
|
||||||
val filterStatus: Long
|
|
||||||
val location: Location?
|
|
||||||
val energyLevel: EnergyLevel?
|
|
||||||
fun onChargerClick(charger: ChargeLocation)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalCarApi
|
|
||||||
class ChargerListFormatter(
|
|
||||||
val carContext: CarContext,
|
|
||||||
val screen: ChargerListDelegate,
|
|
||||||
val cas: CarAppService
|
|
||||||
) {
|
|
||||||
private val iconGen = ChargerIconGenerator(carContext, null, height = 96)
|
|
||||||
var favorites: Set<Long> = emptySet()
|
|
||||||
|
|
||||||
fun buildChargerList(
|
|
||||||
chargers: List<ChargeLocation>?,
|
|
||||||
availabilities: Map<Long, Pair<ZonedDateTime, ChargeLocationStatus?>>
|
|
||||||
): ItemList? {
|
|
||||||
return if (chargers != null) {
|
|
||||||
val chargerList = chargers.take(screen.maxRows)
|
|
||||||
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,
|
|
||||||
availabilities,
|
|
||||||
showCity,
|
|
||||||
charger.id in favorites
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
builder.setNoItemsMessage(
|
|
||||||
carContext.getString(
|
|
||||||
if (screen.filterStatus == FILTERS_FAVORITES) {
|
|
||||||
R.string.auto_no_favorites_found
|
|
||||||
} else {
|
|
||||||
R.string.auto_no_chargers_found
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
builder.setOnItemsVisibilityChangedListener(screen)
|
|
||||||
builder.build()
|
|
||||||
} else {
|
|
||||||
if (screen.loadingError) {
|
|
||||||
val builder = ItemList.Builder()
|
|
||||||
builder.setNoItemsMessage(
|
|
||||||
carContext.getString(R.string.connection_error)
|
|
||||||
)
|
|
||||||
builder.build()
|
|
||||||
} else if (screen.locationError) {
|
|
||||||
val builder = ItemList.Builder()
|
|
||||||
builder.setNoItemsMessage(
|
|
||||||
carContext.getString(R.string.location_error)
|
|
||||||
)
|
|
||||||
builder.build()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun formatCharger(
|
|
||||||
charger: ChargeLocation,
|
|
||||||
availabilities: Map<Long, Pair<ZonedDateTime, ChargeLocationStatus?>>,
|
|
||||||
showCity: Boolean,
|
|
||||||
isFavorite: Boolean
|
|
||||||
): Row {
|
|
||||||
val markerTint = getMarkerTint(charger)
|
|
||||||
val backgroundTint = if ((charger.maxPower ?: 0.0) > 100) {
|
|
||||||
R.color.charger_100kw_dark // slightly darker color for better contrast
|
|
||||||
} else {
|
|
||||||
markerTint
|
|
||||||
}
|
|
||||||
val color = ContextCompat.getColor(carContext, backgroundTint)
|
|
||||||
val place =
|
|
||||||
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
|
|
||||||
.setMarker(
|
|
||||||
PlaceMarker.Builder()
|
|
||||||
.setColor(CarColor.createCustom(color, color))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val icon = iconGen.getBitmap(
|
|
||||||
markerTint,
|
|
||||||
fault = charger.faultReport != null,
|
|
||||||
multi = charger.isMulti(),
|
|
||||||
fav = isFavorite
|
|
||||||
)
|
|
||||||
val iconSpan =
|
|
||||||
CarIconSpan.create(CarIcon.Builder(IconCompat.createWithBitmap(icon)).build())
|
|
||||||
|
|
||||||
return Row.Builder().apply {
|
|
||||||
// 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
|
|
||||||
val title = SpannableStringBuilder().apply {
|
|
||||||
append(" ", iconSpan, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)
|
|
||||||
append(" ")
|
|
||||||
append(charger.name)
|
|
||||||
}
|
|
||||||
if (showCity && charger.address?.city != null && charger.address.city !in charger.name) {
|
|
||||||
val titleWithCity = SpannableStringBuilder().apply {
|
|
||||||
append("", iconSpan, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)
|
|
||||||
append(" ")
|
|
||||||
append("${charger.name} · ${charger.address.city}")
|
|
||||||
}
|
|
||||||
setTitle(CarText.Builder(titleWithCity).addVariant(title).build())
|
|
||||||
} else {
|
|
||||||
setTitle(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
val text = SpannableStringBuilder()
|
|
||||||
|
|
||||||
// distance
|
|
||||||
screen.location?.let {
|
|
||||||
val distanceMeters = distanceBetween(
|
|
||||||
it.latitude, it.longitude,
|
|
||||||
charger.coordinates.lat, charger.coordinates.lng
|
|
||||||
)
|
|
||||||
text.append(
|
|
||||||
"distance",
|
|
||||||
DistanceSpan.create(
|
|
||||||
roundValueToDistance(
|
|
||||||
distanceMeters,
|
|
||||||
screen.energyLevel?.distanceDisplayUnit?.value,
|
|
||||||
carContext
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// power
|
|
||||||
val power = charger.maxPower
|
|
||||||
if (power != null) {
|
|
||||||
if (text.isNotEmpty()) text.append(" · ")
|
|
||||||
text.append("${power.roundToInt()} kW")
|
|
||||||
}
|
|
||||||
|
|
||||||
// availability
|
|
||||||
availabilities[charger.id]?.second?.let { av ->
|
|
||||||
val status = av.status.values.flatten()
|
|
||||||
val available = availabilityText(status)
|
|
||||||
val total = charger.chargepoints.sumOf { it.count }
|
|
||||||
|
|
||||||
if (text.isNotEmpty()) text.append(" · ")
|
|
||||||
text.append(
|
|
||||||
"$available/$total",
|
|
||||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
addText(text)
|
|
||||||
setMetadata(
|
|
||||||
Metadata.Builder()
|
|
||||||
.setPlace(place)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
setOnClickListener {
|
|
||||||
screen.onChargerClick(charger)
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buildSingleCharger(
|
|
||||||
charger: ChargeLocation,
|
|
||||||
availability: ChargeLocationStatus?,
|
|
||||||
onClick: () -> Unit
|
|
||||||
) = Pane.Builder().apply {
|
|
||||||
val icon = iconGen.getBitmap(
|
|
||||||
getMarkerTint(charger),
|
|
||||||
fault = charger.faultReport != null,
|
|
||||||
multi = charger.isMulti(),
|
|
||||||
fav = charger.id in favorites
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
addRow(Row.Builder().apply {
|
|
||||||
setImage(CarIcon.Builder(IconCompat.createWithBitmap(icon)).build())
|
|
||||||
setTitle(charger.address?.toString() ?: charger.coordinates.formatDecimal())
|
|
||||||
addText(generateChargepointsText(charger, availability, carContext))
|
|
||||||
}.build())
|
|
||||||
addAction(Action.Builder().apply {
|
|
||||||
setTitle(carContext.getString(R.string.show_more))
|
|
||||||
setOnClickListener(onClick)
|
|
||||||
}.build())
|
|
||||||
addAction(Action.Builder().apply {
|
|
||||||
setIcon(
|
|
||||||
CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
carContext,
|
|
||||||
R.drawable.ic_navigation
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
)
|
|
||||||
setTitle(carContext.getString(R.string.navigate))
|
|
||||||
setBackgroundColor(CarColor.PRIMARY)
|
|
||||||
setOnClickListener {
|
|
||||||
navigateToCharger(carContext, cas, charger)
|
|
||||||
}
|
|
||||||
}.build())
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
@@ -1,532 +0,0 @@
|
|||||||
package net.vonforst.evmap.auto
|
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.location.Location
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import androidx.car.app.CarContext
|
|
||||||
import androidx.car.app.Screen
|
|
||||||
import androidx.car.app.constraints.ConstraintManager
|
|
||||||
import androidx.car.app.hardware.CarHardwareManager
|
|
||||||
import androidx.car.app.hardware.info.CarInfo
|
|
||||||
import androidx.car.app.hardware.info.CarSensors
|
|
||||||
import androidx.car.app.hardware.info.Compass
|
|
||||||
import androidx.car.app.hardware.info.EnergyLevel
|
|
||||||
import androidx.car.app.model.Action
|
|
||||||
import androidx.car.app.model.ActionStrip
|
|
||||||
import androidx.car.app.model.CarColor
|
|
||||||
import androidx.car.app.model.CarIcon
|
|
||||||
import androidx.car.app.model.CarLocation
|
|
||||||
import androidx.car.app.model.OnContentRefreshListener
|
|
||||||
import androidx.car.app.model.Place
|
|
||||||
import androidx.car.app.model.PlaceListMapTemplate
|
|
||||||
import androidx.car.app.model.PlaceMarker
|
|
||||||
import androidx.car.app.model.Template
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.car2go.maps.model.LatLng
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import net.vonforst.evmap.BuildConfig
|
|
||||||
import net.vonforst.evmap.R
|
|
||||||
import net.vonforst.evmap.api.availability.AvailabilityRepository
|
|
||||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
|
||||||
import net.vonforst.evmap.api.createApi
|
|
||||||
import net.vonforst.evmap.api.stringProvider
|
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
|
||||||
import net.vonforst.evmap.model.FILTERS_FAVORITES
|
|
||||||
import net.vonforst.evmap.model.FilterValue
|
|
||||||
import net.vonforst.evmap.model.FilterWithValue
|
|
||||||
import net.vonforst.evmap.storage.AppDatabase
|
|
||||||
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
|
||||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
|
||||||
import net.vonforst.evmap.utils.bearingBetween
|
|
||||||
import net.vonforst.evmap.utils.distanceBetween
|
|
||||||
import net.vonforst.evmap.utils.headingDiff
|
|
||||||
import net.vonforst.evmap.viewmodel.Status
|
|
||||||
import net.vonforst.evmap.viewmodel.await
|
|
||||||
import net.vonforst.evmap.viewmodel.awaitFinished
|
|
||||||
import net.vonforst.evmap.viewmodel.filtersWithValue
|
|
||||||
import retrofit2.HttpException
|
|
||||||
import java.io.IOException
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZonedDateTime
|
|
||||||
import kotlin.collections.set
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main map screen showing either nearby chargers or favorites
|
|
||||||
*
|
|
||||||
* Legacy implementation for Car App API level < 7
|
|
||||||
*/
|
|
||||||
@androidx.car.app.annotations.ExperimentalCarApi
|
|
||||||
class LegacyMapScreen(ctx: CarContext, val session: EVMapSession) :
|
|
||||||
Screen(ctx), LocationAwareScreen, OnContentRefreshListener,
|
|
||||||
ChargerListDelegate, DefaultLifecycleObserver {
|
|
||||||
|
|
||||||
private val db = AppDatabase.getInstance(carContext)
|
|
||||||
private var prefs = PreferenceDataSource(ctx)
|
|
||||||
private val repo =
|
|
||||||
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
|
||||||
private val availabilityRepo = AvailabilityRepository(ctx)
|
|
||||||
|
|
||||||
private var updateCoroutine: Job? = null
|
|
||||||
private var availabilityUpdateCoroutine: Job? = null
|
|
||||||
|
|
||||||
private var visibleStart: Int? = null
|
|
||||||
private var visibleEnd: Int? = null
|
|
||||||
|
|
||||||
override var location: Location? = null
|
|
||||||
private var lastDistanceUpdateTime: Instant? = null
|
|
||||||
private var lastChargersUpdateTime: Instant? = null
|
|
||||||
private var chargers: List<ChargeLocation>? = null
|
|
||||||
private val favorites = db.favoritesDao().getAllFavorites()
|
|
||||||
override var loadingError = false
|
|
||||||
override var locationError = false
|
|
||||||
private val searchRadius = 5 // kilometers
|
|
||||||
private val distanceUpdateThreshold = Duration.ofSeconds(15)
|
|
||||||
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
|
|
||||||
private val chargersUpdateThresholdDistance = 500 // meters
|
|
||||||
private val chargersUpdateThresholdTime = Duration.ofSeconds(30)
|
|
||||||
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus?>> =
|
|
||||||
HashMap()
|
|
||||||
override val maxRows =
|
|
||||||
min(ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST), 25)
|
|
||||||
private val supportsRefresh = ctx.isAppDrivenRefreshSupported
|
|
||||||
|
|
||||||
override var filterStatus = prefs.filterStatus
|
|
||||||
|
|
||||||
private var filtersWithValue: List<FilterWithValue<FilterValue>>? = null
|
|
||||||
|
|
||||||
private val carInfo: CarInfo by lazy {
|
|
||||||
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
|
|
||||||
}
|
|
||||||
private val carSensors: CarSensors by lazy { carContext.patchedCarSensors }
|
|
||||||
override var energyLevel: EnergyLevel? = null
|
|
||||||
private var heading: Compass? = null
|
|
||||||
private val permissions = if (BuildConfig.FLAVOR_automotive == "automotive") {
|
|
||||||
listOf(
|
|
||||||
"android.car.permission.CAR_ENERGY",
|
|
||||||
"android.car.permission.CAR_ENERGY_PORTS",
|
|
||||||
"android.car.permission.READ_CAR_DISPLAY_UNITS",
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
listOf(
|
|
||||||
"com.google.android.gms.permission.CAR_FUEL"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var searchLocation: LatLng? = null
|
|
||||||
|
|
||||||
private val formatter = ChargerListFormatter(ctx, this, session.cas)
|
|
||||||
|
|
||||||
init {
|
|
||||||
lifecycle.addObserver(this)
|
|
||||||
marker = MapScreen.MARKER
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
|
||||||
session.mapScreen = this
|
|
||||||
return PlaceListMapTemplate.Builder().apply {
|
|
||||||
setTitle(
|
|
||||||
prefs.placeSearchResultAndroidAutoName?.let {
|
|
||||||
carContext.getString(R.string.auto_chargers_near_location, it)
|
|
||||||
} ?: carContext.getString(
|
|
||||||
if (filterStatus == FILTERS_FAVORITES) {
|
|
||||||
R.string.auto_favorites
|
|
||||||
} else {
|
|
||||||
R.string.auto_chargers_closeby
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (prefs.placeSearchResultAndroidAutoName != null) {
|
|
||||||
searchLocation?.let {
|
|
||||||
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).apply {
|
|
||||||
if (prefs.placeSearchResultAndroidAutoName != null) {
|
|
||||||
setMarker(
|
|
||||||
PlaceMarker.Builder()
|
|
||||||
.setColor(CarColor.PRIMARY)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.build())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
location?.let {
|
|
||||||
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
formatter.buildChargerList(chargers, availabilities)?.let {
|
|
||||||
setItemList(it)
|
|
||||||
} ?: setLoading(true)
|
|
||||||
setCurrentLocationEnabled(true)
|
|
||||||
setHeaderAction(Action.APP_ICON)
|
|
||||||
val filtersCount = if (filterStatus == FILTERS_FAVORITES) 1 else {
|
|
||||||
filtersWithValue?.count {
|
|
||||||
!it.value.hasSameValueAs(it.filter.defaultValue())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setActionStrip(
|
|
||||||
ActionStrip.Builder()
|
|
||||||
.addAction(
|
|
||||||
Action.Builder()
|
|
||||||
.setIcon(
|
|
||||||
CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
carContext,
|
|
||||||
R.drawable.ic_settings
|
|
||||||
)
|
|
||||||
).setTint(CarColor.DEFAULT).build()
|
|
||||||
)
|
|
||||||
.setOnClickListener {
|
|
||||||
screenManager.push(SettingsScreen(carContext, session))
|
|
||||||
session.mapScreen = null
|
|
||||||
}
|
|
||||||
.build())
|
|
||||||
.addAction(Action.Builder().apply {
|
|
||||||
setIcon(
|
|
||||||
CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
carContext,
|
|
||||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
|
||||||
R.drawable.ic_search_off
|
|
||||||
} else {
|
|
||||||
R.drawable.ic_search
|
|
||||||
}
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
|
|
||||||
)
|
|
||||||
setOnClickListener {
|
|
||||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
|
||||||
prefs.placeSearchResultAndroidAutoName = null
|
|
||||||
prefs.placeSearchResultAndroidAuto = null
|
|
||||||
if (!supportsRefresh) {
|
|
||||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
|
||||||
chargers = null
|
|
||||||
loadChargers()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
chargers = null
|
|
||||||
loadChargers()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
screenManager.pushForResult(
|
|
||||||
PlaceSearchScreen(
|
|
||||||
carContext,
|
|
||||||
session
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
chargers = null
|
|
||||||
loadChargers()
|
|
||||||
}
|
|
||||||
session.mapScreen = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.build())
|
|
||||||
.addAction(
|
|
||||||
Action.Builder()
|
|
||||||
.setIcon(
|
|
||||||
CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
carContext,
|
|
||||||
R.drawable.ic_filter
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.setTint(if (filtersCount != null && filtersCount > 0) CarColor.SECONDARY else CarColor.DEFAULT)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.setOnClickListener {
|
|
||||||
screenManager.push(FilterScreen(carContext, session))
|
|
||||||
session.mapScreen = null
|
|
||||||
}
|
|
||||||
.build())
|
|
||||||
.build())
|
|
||||||
if (carContext.carAppApiLevel >= 5 ||
|
|
||||||
(BuildConfig.FLAVOR_automotive == "automotive" && carContext.carAppApiLevel >= 4)
|
|
||||||
) {
|
|
||||||
setOnContentRefreshListener(this@LegacyMapScreen)
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChargerClick(charger: ChargeLocation) {
|
|
||||||
screenManager.push(ChargerDetailScreen(carContext, charger, session))
|
|
||||||
session.mapScreen = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateLocation(location: Location) {
|
|
||||||
if (location.latitude == this.location?.latitude
|
|
||||||
&& location.longitude == this.location?.longitude
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val previousLocation = this.location
|
|
||||||
this.location = location
|
|
||||||
if (previousLocation == null) {
|
|
||||||
loadChargers()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val now = Instant.now()
|
|
||||||
if (lastDistanceUpdateTime == null ||
|
|
||||||
Duration.between(lastDistanceUpdateTime, now) > distanceUpdateThreshold
|
|
||||||
) {
|
|
||||||
lastDistanceUpdateTime = now
|
|
||||||
// update displayed distances
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// if chargers are searched around current location, consider app-driven refresh
|
|
||||||
val searchLocation =
|
|
||||||
if (prefs.placeSearchResultAndroidAuto == null) searchLocation else null
|
|
||||||
val distance = searchLocation?.let {
|
|
||||||
distanceBetween(
|
|
||||||
it.latitude, it.longitude, location.latitude, location.longitude
|
|
||||||
)
|
|
||||||
} ?: 0.0
|
|
||||||
if (supportsRefresh && (lastChargersUpdateTime == null ||
|
|
||||||
Duration.between(
|
|
||||||
lastChargersUpdateTime,
|
|
||||||
now
|
|
||||||
) > chargersUpdateThresholdTime) && (distance > chargersUpdateThresholdDistance)
|
|
||||||
) {
|
|
||||||
onContentRefreshRequested()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadChargers() {
|
|
||||||
val location = location ?: return
|
|
||||||
|
|
||||||
val searchLocation =
|
|
||||||
prefs.placeSearchResultAndroidAuto?.latLng ?: LatLng.fromLocation(location)
|
|
||||||
this.searchLocation = searchLocation
|
|
||||||
|
|
||||||
updateCoroutine = lifecycleScope.launch {
|
|
||||||
loadingError = false
|
|
||||||
try {
|
|
||||||
filterStatus = prefs.filterStatus
|
|
||||||
val filterValues =
|
|
||||||
db.filterValueDao().getFilterValuesAsync(filterStatus, prefs.dataSource)
|
|
||||||
val filters = repo.getFiltersAsync(carContext.stringProvider())
|
|
||||||
filtersWithValue = filtersWithValue(filters, filterValues)
|
|
||||||
|
|
||||||
val apiId = repo.api.value!!.id
|
|
||||||
|
|
||||||
// load chargers
|
|
||||||
if (filterStatus == FILTERS_FAVORITES) {
|
|
||||||
val chargers = favorites.await().map { it.charger }.sortedBy {
|
|
||||||
distanceBetween(
|
|
||||||
location.latitude, location.longitude,
|
|
||||||
it.coordinates.lat, it.coordinates.lng
|
|
||||||
)
|
|
||||||
}
|
|
||||||
this@LegacyMapScreen.chargers = chargers
|
|
||||||
} else {
|
|
||||||
// try multiple search radii until we have enough chargers
|
|
||||||
var chargers: List<ChargeLocation>? = null
|
|
||||||
val radiusValues = listOf(searchRadius, searchRadius * 10, searchRadius * 50)
|
|
||||||
for (radius in radiusValues) {
|
|
||||||
val response = repo.getChargepointsRadius(
|
|
||||||
searchLocation,
|
|
||||||
radius,
|
|
||||||
filtersWithValue
|
|
||||||
).awaitFinished()
|
|
||||||
if (response.status == Status.ERROR && if (radius == radiusValues.last()) response.data.isNullOrEmpty() else response.data == null) {
|
|
||||||
loadingError = true
|
|
||||||
this@LegacyMapScreen.chargers = null
|
|
||||||
invalidate()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
chargers = response.data
|
|
||||||
if (prefs.placeSearchResultAndroidAutoName == null) {
|
|
||||||
chargers = headingFilter(
|
|
||||||
chargers,
|
|
||||||
searchLocation
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (chargers == null || chargers.size >= maxRows) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this@LegacyMapScreen.chargers = chargers
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCoroutine = null
|
|
||||||
lastChargersUpdateTime = Instant.now()
|
|
||||||
lastDistanceUpdateTime = Instant.now()
|
|
||||||
invalidate()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
loadingError = true
|
|
||||||
invalidate()
|
|
||||||
} catch (e: HttpException) {
|
|
||||||
loadingError = true
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters by heading if heading available and enabled
|
|
||||||
*/
|
|
||||||
private fun headingFilter(
|
|
||||||
chargers: List<ChargeLocation>?,
|
|
||||||
searchLocation: LatLng
|
|
||||||
): List<ChargeLocation>? {
|
|
||||||
// use compass heading if available, otherwise fall back to GPS
|
|
||||||
val location = location
|
|
||||||
val heading = heading?.orientations?.value?.get(0)
|
|
||||||
?: if (location?.hasBearing() == true) location.bearing else null
|
|
||||||
return heading?.let {
|
|
||||||
if (!prefs.showChargersAheadAndroidAuto) return@let chargers
|
|
||||||
|
|
||||||
chargers?.filter {
|
|
||||||
val bearing = bearingBetween(
|
|
||||||
searchLocation.latitude,
|
|
||||||
searchLocation.longitude,
|
|
||||||
it.coordinates.lat,
|
|
||||||
it.coordinates.lng
|
|
||||||
)
|
|
||||||
val diff = headingDiff(bearing, heading.toDouble())
|
|
||||||
abs(diff) < 30
|
|
||||||
}
|
|
||||||
} ?: chargers
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
|
|
||||||
val isUpdate = this.energyLevel == null
|
|
||||||
this.energyLevel = energyLevel
|
|
||||||
if (isUpdate) invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCompassUpdated(compass: Compass) {
|
|
||||||
this.heading = compass
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
|
||||||
setupListeners()
|
|
||||||
session.requestLocationUpdates()
|
|
||||||
locationError = false
|
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
|
||||||
if (location == null) {
|
|
||||||
locationError = true
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
}, 5000)
|
|
||||||
|
|
||||||
// Reloading chargers in onStart does not seem to count towards content limit.
|
|
||||||
// So let's do this so the user gets fresh chargers when re-entering the app.
|
|
||||||
if (prefs.dataSource != repo.api.value?.id) {
|
|
||||||
repo.api.value = createApi(prefs.dataSource, carContext)
|
|
||||||
}
|
|
||||||
invalidate()
|
|
||||||
loadChargers()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupListeners() {
|
|
||||||
val exec = ContextCompat.getMainExecutor(carContext)
|
|
||||||
if (supportsCarApiLevel3(carContext)) {
|
|
||||||
carSensors.addCompassListener(
|
|
||||||
CarSensors.UPDATE_RATE_NORMAL,
|
|
||||||
exec,
|
|
||||||
::onCompassUpdated
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!permissions.all {
|
|
||||||
ContextCompat.checkSelfPermission(
|
|
||||||
carContext,
|
|
||||||
it
|
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
|
||||||
})
|
|
||||||
return
|
|
||||||
|
|
||||||
if (supportsCarApiLevel3(carContext)) {
|
|
||||||
println("Setting up energy level listener")
|
|
||||||
carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
|
||||||
// Reloading chargers in onStart does not seem to count towards content limit.
|
|
||||||
// So let's do this so the user gets fresh chargers when re-entering the app.
|
|
||||||
// Deleting the data already in onStop makes sure that we show a loading screen directly
|
|
||||||
// (i.e. onGetTemplate is not called while the old data is still there)
|
|
||||||
chargers = null
|
|
||||||
availabilities.clear()
|
|
||||||
location = null
|
|
||||||
removeListeners()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeListeners() {
|
|
||||||
if (supportsCarApiLevel3(carContext)) {
|
|
||||||
println("Removing energy level listener")
|
|
||||||
carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
|
||||||
carSensors.removeCompassListener(::onCompassUpdated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onContentRefreshRequested() {
|
|
||||||
loadChargers()
|
|
||||||
availabilities.clear()
|
|
||||||
|
|
||||||
val start = visibleStart
|
|
||||||
val end = visibleEnd
|
|
||||||
if (start != null && end != null) {
|
|
||||||
onItemVisibilityChanged(start, end)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemVisibilityChanged(startIndex: Int, endIndex: Int) {
|
|
||||||
// when the list is scrolled, load corresponding availabilities
|
|
||||||
if (startIndex == visibleStart && endIndex == visibleEnd && availabilities.isNotEmpty()) return
|
|
||||||
if (startIndex == -1 || endIndex == -1) return
|
|
||||||
if (availabilityUpdateCoroutine != null) return
|
|
||||||
|
|
||||||
visibleEnd = endIndex
|
|
||||||
visibleStart = startIndex
|
|
||||||
|
|
||||||
// remove outdated availabilities
|
|
||||||
availabilities = availabilities.filter {
|
|
||||||
Duration.between(
|
|
||||||
it.value.first,
|
|
||||||
ZonedDateTime.now()
|
|
||||||
) <= availabilityUpdateThreshold
|
|
||||||
}.toMutableMap()
|
|
||||||
|
|
||||||
// update availabilities
|
|
||||||
availabilityUpdateCoroutine = lifecycleScope.launch {
|
|
||||||
delay(300L)
|
|
||||||
|
|
||||||
val chargers = chargers ?: return@launch
|
|
||||||
if (chargers.isEmpty()) return@launch
|
|
||||||
|
|
||||||
val tasks = chargers.subList(
|
|
||||||
min(startIndex, chargers.size - 1),
|
|
||||||
min(endIndex, chargers.size - 1)
|
|
||||||
).mapNotNull {
|
|
||||||
// update only if not yet stored
|
|
||||||
if (!availabilities.containsKey(it.id)) {
|
|
||||||
lifecycleScope.async {
|
|
||||||
val availability = availabilityRepo.getAvailability(it).data
|
|
||||||
val date = ZonedDateTime.now()
|
|
||||||
availabilities[it.id] = date to availability
|
|
||||||
}
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
if (tasks.isNotEmpty()) {
|
|
||||||
tasks.awaitAll()
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
availabilityUpdateCoroutine = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package net.vonforst.evmap.auto
|
|
||||||
|
|
||||||
import androidx.car.app.CarContext
|
|
||||||
import androidx.car.app.Screen
|
|
||||||
import androidx.car.app.annotations.ExperimentalCarApi
|
|
||||||
import androidx.car.app.model.Action
|
|
||||||
import androidx.car.app.model.Header
|
|
||||||
import androidx.car.app.model.ItemList
|
|
||||||
import androidx.car.app.model.ListTemplate
|
|
||||||
import androidx.car.app.model.ParkedOnlyOnClickListener
|
|
||||||
import androidx.car.app.model.Row
|
|
||||||
import androidx.car.app.model.Template
|
|
||||||
import com.car2go.maps.AttributionClickListener
|
|
||||||
import net.vonforst.evmap.R
|
|
||||||
|
|
||||||
@ExperimentalCarApi
|
|
||||||
class MapAttributionScreen(
|
|
||||||
ctx: CarContext,
|
|
||||||
val session: EVMapSession,
|
|
||||||
val attributions: List<AttributionClickListener.Attribution>
|
|
||||||
) : Screen(ctx) {
|
|
||||||
override fun onGetTemplate(): Template {
|
|
||||||
return ListTemplate.Builder()
|
|
||||||
.setHeader(
|
|
||||||
Header.Builder()
|
|
||||||
.setStartHeaderAction(Action.BACK)
|
|
||||||
.setTitle(carContext.getString(R.string.maplibre_attributionsDialogTitle))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.setSingleList(ItemList.Builder().apply {
|
|
||||||
attributions.forEach { attr ->
|
|
||||||
addItem(
|
|
||||||
Row.Builder()
|
|
||||||
.setTitle(attr.title)
|
|
||||||
.setBrowsable(true)
|
|
||||||
.setOnClickListener(
|
|
||||||
ParkedOnlyOnClickListener.create {
|
|
||||||
openUrl(carContext, session.cas, attr.url)
|
|
||||||
}).build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.build())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,284 +0,0 @@
|
|||||||
package net.vonforst.evmap.auto
|
|
||||||
|
|
||||||
import android.animation.ValueAnimator
|
|
||||||
import android.app.Presentation
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Rect
|
|
||||||
import android.hardware.display.DisplayManager
|
|
||||||
import android.hardware.display.VirtualDisplay
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.SystemClock
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import androidx.car.app.CarContext
|
|
||||||
import androidx.car.app.SurfaceCallback
|
|
||||||
import androidx.car.app.SurfaceContainer
|
|
||||||
import androidx.car.app.annotations.RequiresCarApi
|
|
||||||
import androidx.core.animation.doOnEnd
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
|
||||||
import com.car2go.maps.AnyMap
|
|
||||||
import com.car2go.maps.AnyMap.CancelableCallback
|
|
||||||
import com.car2go.maps.CameraUpdate
|
|
||||||
import com.car2go.maps.MapContainerView
|
|
||||||
import com.car2go.maps.MapFactory
|
|
||||||
import com.car2go.maps.OnMapReadyCallback
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import net.vonforst.evmap.BuildConfig
|
|
||||||
import net.vonforst.evmap.R
|
|
||||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
|
||||||
import kotlin.math.hypot
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import kotlin.math.roundToLong
|
|
||||||
|
|
||||||
|
|
||||||
class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCoroutineScope) :
|
|
||||||
SurfaceCallback, OnMapReadyCallback {
|
|
||||||
private val VIRTUAL_DISPLAY_NAME = "evmap_map"
|
|
||||||
private val VELOCITY_THRESHOLD_IGNORE_FLING = 1000
|
|
||||||
private val STATUSBAR_OFFSET_SYSTEMS = listOf(
|
|
||||||
"VolvoCars/ihu_emulator_volvo_car/ihu_emulator:11",
|
|
||||||
"Google/sdk_gcar_x86_64/generic_64bitonly_x86_64:11"
|
|
||||||
)
|
|
||||||
|
|
||||||
private val prefs = PreferenceDataSource(ctx)
|
|
||||||
|
|
||||||
private lateinit var virtualDisplay: VirtualDisplay
|
|
||||||
lateinit var presentation: Presentation
|
|
||||||
private lateinit var mapView: MapContainerView
|
|
||||||
private var width: Int = 0
|
|
||||||
private var height: Int = 0
|
|
||||||
private var visibleArea: Rect? = null
|
|
||||||
private var map: AnyMap? = null
|
|
||||||
private val mapCallbacks = mutableListOf<OnMapReadyCallback>()
|
|
||||||
|
|
||||||
private var flingAnimator: ValueAnimator? = null
|
|
||||||
private var idle = true
|
|
||||||
private var idleDelay: Job? = null
|
|
||||||
var cameraMoveStartedListener: (() -> Unit)? = null
|
|
||||||
var cameraIdleListener: (() -> Unit)? = null
|
|
||||||
|
|
||||||
|
|
||||||
override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
|
|
||||||
if (surfaceContainer.surface == null || surfaceContainer.dpi == 0 || surfaceContainer.height == 0 || surfaceContainer.width == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.FINGERPRINT.contains("emulator") || Build.FINGERPRINT.contains("sdk_gcar")) {
|
|
||||||
// fix for MapLibre in Android Automotive Emulators
|
|
||||||
System.setProperty("ro.kernel.qemu", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
width = surfaceContainer.width
|
|
||||||
height = surfaceContainer.height
|
|
||||||
virtualDisplay = ContextCompat
|
|
||||||
.getSystemService(ctx, DisplayManager::class.java)!!
|
|
||||||
.createVirtualDisplay(
|
|
||||||
VIRTUAL_DISPLAY_NAME,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
(surfaceContainer.dpi * when (getMapProvider()) {
|
|
||||||
"mapbox" -> 1.6
|
|
||||||
"google" -> 1.0
|
|
||||||
else -> 1.0
|
|
||||||
}).roundToInt(),
|
|
||||||
surfaceContainer.surface,
|
|
||||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
|
|
||||||
)
|
|
||||||
presentation = Presentation(ctx, virtualDisplay.display, R.style.AppTheme)
|
|
||||||
|
|
||||||
mapView = createMap(presentation.context)
|
|
||||||
mapView.onCreate(null)
|
|
||||||
mapView.onResume()
|
|
||||||
|
|
||||||
presentation.setContentView(mapView)
|
|
||||||
presentation.show()
|
|
||||||
|
|
||||||
mapView.getMapAsync(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMapProvider(): String = if (BuildConfig.FLAVOR_automotive == "automotive") {
|
|
||||||
// Google Maps SDK is not available on AAOS (not even AAOS with GAS, so far)
|
|
||||||
"mapbox"
|
|
||||||
} else prefs.mapProvider
|
|
||||||
|
|
||||||
override fun onVisibleAreaChanged(visibleArea: Rect) {
|
|
||||||
Log.d("MapSurfaceCallback", "visible area: $visibleArea")
|
|
||||||
this.visibleArea = visibleArea
|
|
||||||
updateVisibleArea()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStableAreaChanged(stableArea: Rect) {
|
|
||||||
Log.d("MapSurfaceCallback", "stable area: $stableArea")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
|
|
||||||
mapView.onPause()
|
|
||||||
mapView.onStop()
|
|
||||||
mapView.onDestroy()
|
|
||||||
map = null
|
|
||||||
|
|
||||||
presentation.dismiss()
|
|
||||||
virtualDisplay.release()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresCarApi(2)
|
|
||||||
override fun onScroll(distanceX: Float, distanceY: Float) {
|
|
||||||
flingAnimator?.cancel()
|
|
||||||
val map = map ?: return
|
|
||||||
map.moveCamera(map.cameraUpdateFactory.scrollBy(distanceX, distanceY))
|
|
||||||
dispatchCameraMoveStarted()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresCarApi(2)
|
|
||||||
override fun onFling(velocityX: Float, velocityY: Float) {
|
|
||||||
val map = map ?: return
|
|
||||||
val screenDensity: Float = presentation.resources.displayMetrics.density
|
|
||||||
|
|
||||||
// calculate velocity vector for xy dimensions, independent from screen size
|
|
||||||
val velocityXY =
|
|
||||||
hypot((velocityX / screenDensity).toDouble(), (velocityY / screenDensity).toDouble())
|
|
||||||
if (velocityXY < VELOCITY_THRESHOLD_IGNORE_FLING) {
|
|
||||||
// ignore short flings, these can occur when other gestures just have finished executing
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
idleDelay?.cancel()
|
|
||||||
|
|
||||||
val offsetX = velocityX / 10
|
|
||||||
val offsetY = velocityY / 10
|
|
||||||
val animationTime = (velocityXY / 10).roundToLong()
|
|
||||||
|
|
||||||
flingAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
|
||||||
duration = animationTime
|
|
||||||
interpolator = LinearOutSlowInInterpolator()
|
|
||||||
|
|
||||||
var last = 0f
|
|
||||||
addUpdateListener {
|
|
||||||
val current = it.animatedFraction
|
|
||||||
val diff = last - current
|
|
||||||
map.moveCamera(map.cameraUpdateFactory.scrollBy(diff * offsetX, diff * offsetY))
|
|
||||||
last = current
|
|
||||||
}
|
|
||||||
start()
|
|
||||||
|
|
||||||
doOnEnd { dispatchCameraIdle() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresCarApi(2)
|
|
||||||
override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
|
|
||||||
flingAnimator?.cancel()
|
|
||||||
val map = map ?: return
|
|
||||||
if (scaleFactor == 2f) return
|
|
||||||
|
|
||||||
val offsetX = (focusX - mapView.width / 2) * (scaleFactor - 1f)
|
|
||||||
val offsetY = (offsetY(focusY) - mapView.height / 2) * (scaleFactor - 1f)
|
|
||||||
|
|
||||||
Log.i("MapSurfaceCallback", "focus: $focusX, $focusY, scaleFactor: $scaleFactor")
|
|
||||||
map.moveCamera(map.cameraUpdateFactory.zoomBy(scaleFactor - 1))
|
|
||||||
map.moveCamera(map.cameraUpdateFactory.scrollBy(offsetX, offsetY))
|
|
||||||
dispatchCameraMoveStarted()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun animateCamera(update: CameraUpdate) {
|
|
||||||
val map = map ?: return
|
|
||||||
map.animateCamera(update, object : CancelableCallback {
|
|
||||||
override fun onFinish() {
|
|
||||||
dispatchCameraIdle()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancel() {
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dispatchCameraMoveStarted() {
|
|
||||||
if (idle) {
|
|
||||||
idle = false
|
|
||||||
cameraMoveStartedListener?.invoke()
|
|
||||||
}
|
|
||||||
idleDelay?.cancel()
|
|
||||||
idleDelay = lifecycleScope.launch {
|
|
||||||
delay(500)
|
|
||||||
dispatchCameraIdle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dispatchCameraIdle() {
|
|
||||||
idle = true
|
|
||||||
cameraIdleListener?.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresCarApi(5)
|
|
||||||
override fun onClick(x: Float, y: Float) {
|
|
||||||
flingAnimator?.cancel()
|
|
||||||
val downTime: Long = SystemClock.uptimeMillis()
|
|
||||||
val eventTime: Long = downTime + 100
|
|
||||||
val yOffset = offsetY(y)
|
|
||||||
|
|
||||||
val downEvent = MotionEvent.obtain(
|
|
||||||
downTime,
|
|
||||||
downTime,
|
|
||||||
MotionEvent.ACTION_DOWN,
|
|
||||||
x,
|
|
||||||
yOffset,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
mapView.dispatchTouchEvent(downEvent)
|
|
||||||
downEvent.recycle()
|
|
||||||
val upEvent = MotionEvent.obtain(
|
|
||||||
downTime,
|
|
||||||
eventTime,
|
|
||||||
MotionEvent.ACTION_UP,
|
|
||||||
x,
|
|
||||||
yOffset,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
mapView.dispatchTouchEvent(upEvent)
|
|
||||||
upEvent.recycle()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun offsetY(y: Float): Float {
|
|
||||||
if (!STATUSBAR_OFFSET_SYSTEMS.any { Build.FINGERPRINT.startsWith(it) }) return y
|
|
||||||
|
|
||||||
// In some emulators, touch locations are offset by the status bar height
|
|
||||||
// related: https://issuetracker.google.com/issues/256905247
|
|
||||||
val resId = ctx.resources.getIdentifier("status_bar_height", "dimen", "android")
|
|
||||||
val offset = resId.takeIf { it > 0 }?.let { ctx.resources.getDimensionPixelSize(it) } ?: 0
|
|
||||||
return y + offset
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createMap(ctx: Context): MapContainerView {
|
|
||||||
val priority = arrayOf(
|
|
||||||
when (getMapProvider()) {
|
|
||||||
"mapbox" -> MapFactory.MAPLIBRE
|
|
||||||
"google" -> MapFactory.GOOGLE
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
MapFactory.GOOGLE,
|
|
||||||
MapFactory.MAPLIBRE
|
|
||||||
)
|
|
||||||
return MapFactory.createMap(ctx, priority).view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMapReady(anyMap: AnyMap) {
|
|
||||||
this.map = anyMap
|
|
||||||
updateVisibleArea()
|
|
||||||
mapCallbacks.forEach { it.onMapReady(anyMap) }
|
|
||||||
mapCallbacks.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateVisibleArea() {
|
|
||||||
visibleArea?.let {
|
|
||||||
map?.setPadding(it.left, it.top, width - it.right, height - it.bottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMapAsync(callback: OnMapReadyCallback) {
|
|
||||||
mapCallbacks.add(callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,18 @@
|
|||||||
package net.vonforst.evmap.auto
|
package net.vonforst.evmap.auto
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.add
|
import androidx.fragment.app.add
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import kotlinx.coroutines.flow.Flow
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
|
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
|
||||||
|
|
||||||
class OAuthLoginActivity : AppCompatActivity(R.layout.activity_oauth_login) {
|
class OAuthLoginActivity : AppCompatActivity(R.layout.activity_oauth_login) {
|
||||||
companion object {
|
|
||||||
private val resultRegistry: MutableMap<String, MutableSharedFlow<String>> = mutableMapOf()
|
|
||||||
|
|
||||||
fun registerForResult(url: String): Flow<String> {
|
|
||||||
val flow = MutableSharedFlow<String>(replay = 1)
|
|
||||||
resultRegistry[url] = flow
|
|
||||||
return flow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
@@ -29,14 +22,10 @@ class OAuthLoginActivity : AppCompatActivity(R.layout.activity_oauth_login) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val url = intent.getStringExtra(OAuthLoginFragment.EXTRA_URL)!!
|
LocalBroadcastManager.getInstance(this).registerReceiver(object : BroadcastReceiver() {
|
||||||
supportFragmentManager.setFragmentResultListener(url, this) { _, result ->
|
override fun onReceive(ctx: Context, intent: Intent) {
|
||||||
val resultUrl = result.getString(OAuthLoginFragment.EXTRA_URL) ?: return@setFragmentResultListener
|
finish()
|
||||||
resultRegistry[url]?.let { flow ->
|
|
||||||
flow.tryEmit(resultUrl)
|
|
||||||
resultRegistry.remove(url)
|
|
||||||
}
|
}
|
||||||
finish()
|
}, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,34 +11,19 @@ import androidx.car.app.annotations.ExperimentalCarApi
|
|||||||
import androidx.car.app.constraints.ConstraintManager
|
import androidx.car.app.constraints.ConstraintManager
|
||||||
import androidx.car.app.hardware.CarHardwareManager
|
import androidx.car.app.hardware.CarHardwareManager
|
||||||
import androidx.car.app.hardware.info.EnergyLevel
|
import androidx.car.app.hardware.info.EnergyLevel
|
||||||
import androidx.car.app.model.Action
|
import androidx.car.app.model.*
|
||||||
import androidx.car.app.model.CarColor
|
|
||||||
import androidx.car.app.model.CarIcon
|
|
||||||
import androidx.car.app.model.DistanceSpan
|
|
||||||
import androidx.car.app.model.ItemList
|
|
||||||
import androidx.car.app.model.Row
|
|
||||||
import androidx.car.app.model.SearchTemplate
|
|
||||||
import androidx.car.app.model.Template
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.car2go.maps.model.LatLng
|
import com.car2go.maps.model.LatLng
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import net.vonforst.evmap.BuildConfig
|
import net.vonforst.evmap.BuildConfig
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
import net.vonforst.evmap.adapter.iconForPlaceType
|
import net.vonforst.evmap.adapter.iconForPlaceType
|
||||||
import net.vonforst.evmap.adapter.isSpecialPlace
|
import net.vonforst.evmap.adapter.isSpecialPlace
|
||||||
import net.vonforst.evmap.autocomplete.ApiUnavailableException
|
import net.vonforst.evmap.autocomplete.*
|
||||||
import net.vonforst.evmap.autocomplete.AutocompletePlace
|
|
||||||
import net.vonforst.evmap.autocomplete.AutocompleteProvider
|
|
||||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
|
||||||
import net.vonforst.evmap.autocomplete.getAutocompleteProviders
|
|
||||||
import net.vonforst.evmap.storage.AppDatabase
|
import net.vonforst.evmap.storage.AppDatabase
|
||||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||||
import net.vonforst.evmap.storage.RecentAutocompletePlace
|
import net.vonforst.evmap.storage.RecentAutocompletePlace
|
||||||
@@ -132,7 +117,7 @@ class PlaceSearchScreen(
|
|||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val placeDetails = getDetails(place.id) ?: return@launch
|
val placeDetails = getDetails(place.id) ?: return@launch
|
||||||
prefs.placeSearchResultAndroidAuto = placeDetails
|
prefs.placeSearchResultAndroidAuto = placeDetails.latLng
|
||||||
prefs.placeSearchResultAndroidAutoName =
|
prefs.placeSearchResultAndroidAutoName =
|
||||||
place.primaryText.toString()
|
place.primaryText.toString()
|
||||||
screenManager.popTo(MapScreen.MARKER)
|
screenManager.popTo(MapScreen.MARKER)
|
||||||
|
|||||||
@@ -29,13 +29,9 @@ import androidx.car.app.model.Template
|
|||||||
import androidx.car.app.model.Toggle
|
import androidx.car.app.model.Toggle
|
||||||
import androidx.core.content.IntentCompat
|
import androidx.core.content.IntentCompat
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import kotlinx.coroutines.flow.single
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.vonforst.evmap.BuildConfig
|
import net.vonforst.evmap.BuildConfig
|
||||||
import net.vonforst.evmap.EXTRA_DONATE
|
import net.vonforst.evmap.EXTRA_DONATE
|
||||||
@@ -118,25 +114,22 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
|||||||
}
|
}
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
if (carContext.carAppApiLevel < 7 || !carContext.isAppDrivenRefreshSupported) {
|
addItem(
|
||||||
// this option is only supported in LegacyMapScreen
|
Row.Builder()
|
||||||
addItem(
|
.setTitle(carContext.getString(R.string.auto_chargers_ahead))
|
||||||
Row.Builder()
|
.setToggle(Toggle.Builder {
|
||||||
.setTitle(carContext.getString(R.string.auto_chargers_ahead))
|
prefs.showChargersAheadAndroidAuto = it
|
||||||
.setToggle(Toggle.Builder {
|
}.setChecked(prefs.showChargersAheadAndroidAuto).build())
|
||||||
prefs.showChargersAheadAndroidAuto = it
|
.setImage(
|
||||||
}.setChecked(prefs.showChargersAheadAndroidAuto).build())
|
CarIcon.Builder(
|
||||||
.setImage(
|
IconCompat.createWithResource(
|
||||||
CarIcon.Builder(
|
carContext,
|
||||||
IconCompat.createWithResource(
|
R.drawable.ic_navigation
|
||||||
carContext,
|
)
|
||||||
R.drawable.ic_navigation
|
).setTint(CarColor.DEFAULT).build()
|
||||||
)
|
)
|
||||||
).setTint(CarColor.DEFAULT).build()
|
.build()
|
||||||
)
|
)
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
addItem(
|
addItem(
|
||||||
Row.Builder()
|
Row.Builder()
|
||||||
@@ -161,7 +154,7 @@ class SettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalCarApi
|
@ExperimentalCarApi
|
||||||
class DataSettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), DefaultLifecycleObserver {
|
class DataSettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||||
val prefs = PreferenceDataSource(ctx)
|
val prefs = PreferenceDataSource(ctx)
|
||||||
val encryptedPrefs = EncryptedPreferenceDataStore(ctx)
|
val encryptedPrefs = EncryptedPreferenceDataStore(ctx)
|
||||||
val db = AppDatabase.getInstance(ctx)
|
val db = AppDatabase.getInstance(ctx)
|
||||||
@@ -172,22 +165,14 @@ class DataSettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ct
|
|||||||
carContext.resources.getStringArray(R.array.pref_search_provider_names)
|
carContext.resources.getStringArray(R.array.pref_search_provider_names)
|
||||||
val searchProviderValues =
|
val searchProviderValues =
|
||||||
carContext.resources.getStringArray(R.array.pref_search_provider_values)
|
carContext.resources.getStringArray(R.array.pref_search_provider_values)
|
||||||
val mapProviderNames =
|
|
||||||
carContext.resources.getStringArray(R.array.pref_map_provider_names)
|
|
||||||
val mapProviderValues =
|
|
||||||
carContext.resources.getStringArray(R.array.pref_map_provider_values)
|
|
||||||
|
|
||||||
var teslaLoggingIn = false
|
var teslaLoggingIn = false
|
||||||
|
|
||||||
init {
|
|
||||||
lifecycle.addObserver(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
return ListTemplate.Builder().apply {
|
return ListTemplate.Builder().apply {
|
||||||
setTitle(carContext.getString(R.string.settings_data_sources))
|
setTitle(carContext.getString(R.string.settings_data_sources))
|
||||||
setHeaderAction(Action.BACK)
|
setHeaderAction(Action.BACK)
|
||||||
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
|
setSingleList(ItemList.Builder().apply {
|
||||||
addItem(Row.Builder().apply {
|
addItem(Row.Builder().apply {
|
||||||
setTitle(carContext.getString(R.string.pref_data_source))
|
setTitle(carContext.getString(R.string.pref_data_source))
|
||||||
setBrowsable(true)
|
setBrowsable(true)
|
||||||
@@ -203,6 +188,35 @@ class DataSettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ct
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}.build())
|
}.build())
|
||||||
|
addItem(Row.Builder().apply {
|
||||||
|
setTitle(carContext.getString(R.string.pref_search_provider))
|
||||||
|
setBrowsable(true)
|
||||||
|
val searchProviderId = prefs.searchProvider
|
||||||
|
val searchProviderDesc =
|
||||||
|
searchProviderNames[searchProviderValues.indexOf(searchProviderId)]
|
||||||
|
addText(searchProviderDesc)
|
||||||
|
setOnClickListener {
|
||||||
|
screenManager.push(
|
||||||
|
ChooseDataSourceScreen(
|
||||||
|
carContext,
|
||||||
|
ChooseDataSourceScreen.Type.SEARCH_PROVIDER
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.build())
|
||||||
|
addItem(Row.Builder().apply {
|
||||||
|
setTitle(carContext.getString(R.string.pref_search_delete_recent))
|
||||||
|
setOnClickListener {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
db.recentAutocompletePlaceDao().deleteAll()
|
||||||
|
CarToast.makeText(
|
||||||
|
carContext,
|
||||||
|
R.string.deleted_recent_search_results,
|
||||||
|
CarToast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.build())
|
||||||
/*addItem(
|
/*addItem(
|
||||||
Row.Builder()
|
Row.Builder()
|
||||||
.setTitle(carContext.getString(R.string.pref_prediction_enabled))
|
.setTitle(carContext.getString(R.string.pref_prediction_enabled))
|
||||||
@@ -236,104 +250,10 @@ class DataSettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ct
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}.build())
|
}.build())
|
||||||
}.build(), carContext.getString(R.string.settings_charger_data)))
|
}.build())
|
||||||
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
|
|
||||||
addItem(Row.Builder().apply {
|
|
||||||
setTitle(carContext.getString(R.string.pref_search_provider))
|
|
||||||
setBrowsable(true)
|
|
||||||
val searchProviderId = prefs.searchProvider
|
|
||||||
val searchProviderDesc =
|
|
||||||
searchProviderNames[searchProviderValues.indexOf(searchProviderId)]
|
|
||||||
addText(searchProviderDesc)
|
|
||||||
setOnClickListener {
|
|
||||||
screenManager.push(
|
|
||||||
ChooseDataSourceScreen(
|
|
||||||
carContext,
|
|
||||||
ChooseDataSourceScreen.Type.SEARCH_PROVIDER
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.build())
|
|
||||||
if (supportsNewMapScreen(carContext) && BuildConfig.FLAVOR_automotive != "automotive") {
|
|
||||||
// Google Maps SDK is not available on AAOS (not even AAOS with GAS, so far)
|
|
||||||
addItem(Row.Builder().apply {
|
|
||||||
setTitle(carContext.getString(R.string.pref_map_provider))
|
|
||||||
setBrowsable(true)
|
|
||||||
val mapProviderId = prefs.mapProvider
|
|
||||||
val mapProviderDesc =
|
|
||||||
mapProviderNames[mapProviderValues.indexOf(mapProviderId)]
|
|
||||||
addText(mapProviderDesc)
|
|
||||||
setOnClickListener {
|
|
||||||
screenManager.push(
|
|
||||||
ChooseDataSourceScreen(
|
|
||||||
carContext,
|
|
||||||
ChooseDataSourceScreen.Type.MAP_PROVIDER
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.build())
|
|
||||||
}
|
|
||||||
addItem(Row.Builder().apply {
|
|
||||||
setTitle(carContext.getString(R.string.pref_search_delete_recent))
|
|
||||||
setOnClickListener {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
db.recentAutocompletePlaceDao().deleteAll()
|
|
||||||
CarToast.makeText(
|
|
||||||
carContext,
|
|
||||||
R.string.deleted_recent_search_results,
|
|
||||||
CarToast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.build())
|
|
||||||
}.build(), carContext.getString(R.string.settings_map)))
|
|
||||||
addSectionedList(SectionedItemList.create(ItemList.Builder().apply {
|
|
||||||
addItem(Row.Builder().apply {
|
|
||||||
setTitle(carContext.getString(R.string.settings_cache_count))
|
|
||||||
cacheCount?.let { count ->
|
|
||||||
cacheSize?.let { size ->
|
|
||||||
val sizeMb = size.toFloat() / 1024 / 1024
|
|
||||||
addText(
|
|
||||||
carContext.getString(
|
|
||||||
R.string.settings_cache_count_summary,
|
|
||||||
count,
|
|
||||||
sizeMb
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.build())
|
|
||||||
addItem(Row.Builder().apply {
|
|
||||||
setTitle(carContext.getString(R.string.settings_cache_clear))
|
|
||||||
addText(carContext.getString(R.string.settings_cache_clear_summary))
|
|
||||||
setOnClickListener {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
db.savedRegionDao().deleteAll()
|
|
||||||
db.chargeLocationsDao().deleteAllIfNotFavorite()
|
|
||||||
loadCacheSize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.build())
|
|
||||||
}.build(), carContext.getString(R.string.settings_caching)))
|
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
var cacheCount: Long? = null
|
|
||||||
var cacheSize: Long? = null
|
|
||||||
|
|
||||||
private suspend fun loadCacheSize() {
|
|
||||||
cacheCount = db.chargeLocationsDao().getCountAsync()
|
|
||||||
cacheSize = db.chargeLocationsDao().getSize()
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
|
||||||
super.onStart(owner)
|
|
||||||
lifecycleScope.launch {
|
|
||||||
loadCacheSize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun teslaLogin() {
|
private fun teslaLogin() {
|
||||||
val codeVerifier = TeslaAuthenticationApi.generateCodeVerifier()
|
val codeVerifier = TeslaAuthenticationApi.generateCodeVerifier()
|
||||||
val codeChallenge = TeslaAuthenticationApi.generateCodeChallenge(codeVerifier)
|
val codeChallenge = TeslaAuthenticationApi.generateCodeChallenge(codeVerifier)
|
||||||
@@ -342,18 +262,23 @@ class DataSettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ct
|
|||||||
val args = OAuthLoginFragmentArgs(
|
val args = OAuthLoginFragmentArgs(
|
||||||
uri.toString(),
|
uri.toString(),
|
||||||
TeslaAuthenticationApi.resultUrlPrefix,
|
TeslaAuthenticationApi.resultUrlPrefix,
|
||||||
"#FFFFFF"
|
"#000000"
|
||||||
).toBundle()
|
).toBundle()
|
||||||
val intent = Intent(carContext, OAuthLoginActivity::class.java)
|
val intent = Intent(carContext, OAuthLoginActivity::class.java)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
.putExtras(args)
|
.putExtras(args)
|
||||||
|
|
||||||
val resultFlow = OAuthLoginActivity.registerForResult(uri.toString())
|
LocalBroadcastManager.getInstance(carContext)
|
||||||
lifecycleScope.launch {
|
.registerReceiver(object : BroadcastReceiver() {
|
||||||
resultFlow.collect { resultUrl ->
|
override fun onReceive(ctx: Context, intent: Intent) {
|
||||||
teslaGetAccessToken(resultUrl.toUri(), codeVerifier)
|
val url = IntentCompat.getParcelableExtra(
|
||||||
}
|
intent,
|
||||||
}
|
OAuthLoginFragment.EXTRA_URL,
|
||||||
|
Uri::class.java
|
||||||
|
)
|
||||||
|
teslaGetAccessToken(url!!, codeVerifier)
|
||||||
|
}
|
||||||
|
}, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))
|
||||||
|
|
||||||
session.cas.startActivity(intent)
|
session.cas.startActivity(intent)
|
||||||
|
|
||||||
@@ -417,42 +342,32 @@ class ChooseDataSourceScreen(
|
|||||||
@StringRes val extraDesc: Int? = null
|
@StringRes val extraDesc: Int? = null
|
||||||
) : Screen(ctx) {
|
) : Screen(ctx) {
|
||||||
enum class Type {
|
enum class Type {
|
||||||
CHARGER_DATA_SOURCE, SEARCH_PROVIDER, MAP_PROVIDER
|
CHARGER_DATA_SOURCE, SEARCH_PROVIDER
|
||||||
}
|
}
|
||||||
|
|
||||||
val prefs = PreferenceDataSource(carContext)
|
val prefs = PreferenceDataSource(carContext)
|
||||||
val title = when (type) {
|
val title = when (type) {
|
||||||
Type.CHARGER_DATA_SOURCE -> R.string.pref_data_source
|
Type.CHARGER_DATA_SOURCE -> R.string.pref_data_source
|
||||||
Type.SEARCH_PROVIDER -> R.string.pref_search_provider
|
Type.SEARCH_PROVIDER -> R.string.pref_search_provider
|
||||||
Type.MAP_PROVIDER -> R.string.pref_map_provider
|
|
||||||
}
|
}
|
||||||
val names = carContext.resources.getStringArray(
|
val names = when (type) {
|
||||||
when (type) {
|
Type.CHARGER_DATA_SOURCE -> carContext.resources.getStringArray(R.array.pref_data_source_names)
|
||||||
Type.CHARGER_DATA_SOURCE -> R.array.pref_data_source_names
|
Type.SEARCH_PROVIDER -> carContext.resources.getStringArray(R.array.pref_search_provider_names)
|
||||||
Type.SEARCH_PROVIDER -> R.array.pref_search_provider_names
|
}
|
||||||
Type.MAP_PROVIDER -> R.array.pref_map_provider_names
|
val values = when (type) {
|
||||||
}
|
Type.CHARGER_DATA_SOURCE -> carContext.resources.getStringArray(R.array.pref_data_source_values)
|
||||||
)
|
Type.SEARCH_PROVIDER -> carContext.resources.getStringArray(R.array.pref_search_provider_values)
|
||||||
val values = carContext.resources.getStringArray(
|
}
|
||||||
when (type) {
|
|
||||||
Type.CHARGER_DATA_SOURCE -> R.array.pref_data_source_values
|
|
||||||
Type.SEARCH_PROVIDER -> R.array.pref_search_provider_values
|
|
||||||
Type.MAP_PROVIDER -> R.array.pref_map_provider_values
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val currentValue: String = when (type) {
|
val currentValue: String = when (type) {
|
||||||
Type.CHARGER_DATA_SOURCE -> prefs.dataSource
|
Type.CHARGER_DATA_SOURCE -> prefs.dataSource
|
||||||
Type.SEARCH_PROVIDER -> prefs.searchProvider
|
Type.SEARCH_PROVIDER -> prefs.searchProvider
|
||||||
Type.MAP_PROVIDER -> prefs.mapProvider
|
|
||||||
}
|
}
|
||||||
val descriptions = when (type) {
|
val descriptions = when (type) {
|
||||||
Type.CHARGER_DATA_SOURCE -> listOf(
|
Type.CHARGER_DATA_SOURCE -> listOf(
|
||||||
carContext.getString(R.string.data_source_goingelectric_desc),
|
carContext.getString(R.string.data_source_goingelectric_desc),
|
||||||
carContext.getString(R.string.data_source_openchargemap_desc),
|
carContext.getString(R.string.data_source_openchargemap_desc)
|
||||||
carContext.getString(R.string.data_source_openstreetmap_desc)
|
|
||||||
)
|
)
|
||||||
Type.SEARCH_PROVIDER -> null
|
Type.SEARCH_PROVIDER -> null
|
||||||
Type.MAP_PROVIDER -> null
|
|
||||||
}
|
}
|
||||||
val callback: (String) -> Unit = when (type) {
|
val callback: (String) -> Unit = when (type) {
|
||||||
Type.CHARGER_DATA_SOURCE -> { it ->
|
Type.CHARGER_DATA_SOURCE -> { it ->
|
||||||
@@ -462,9 +377,6 @@ class ChooseDataSourceScreen(
|
|||||||
Type.SEARCH_PROVIDER -> { it ->
|
Type.SEARCH_PROVIDER -> { it ->
|
||||||
prefs.searchProvider = it
|
prefs.searchProvider = it
|
||||||
}
|
}
|
||||||
Type.MAP_PROVIDER -> { it ->
|
|
||||||
prefs.mapProvider = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
override fun onGetTemplate(): Template {
|
||||||
|
|||||||
@@ -4,46 +4,32 @@ import android.content.ActivityNotFoundException
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.SpannableStringBuilder
|
|
||||||
import android.text.Spanned
|
|
||||||
import android.text.TextPaint
|
import android.text.TextPaint
|
||||||
import android.util.Log
|
|
||||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.CarToast
|
import androidx.car.app.CarToast
|
||||||
import androidx.car.app.HostException
|
|
||||||
import androidx.car.app.Screen
|
import androidx.car.app.Screen
|
||||||
import androidx.car.app.annotations.ExperimentalCarApi
|
import androidx.car.app.annotations.ExperimentalCarApi
|
||||||
import androidx.car.app.constraints.ConstraintManager
|
import androidx.car.app.constraints.ConstraintManager
|
||||||
import androidx.car.app.hardware.common.CarUnit
|
import androidx.car.app.hardware.common.CarUnit
|
||||||
import androidx.car.app.model.CarColor
|
import androidx.car.app.model.CarColor
|
||||||
import androidx.car.app.model.CarIcon
|
import androidx.car.app.model.CarIcon
|
||||||
import androidx.car.app.model.CarIconSpan
|
|
||||||
import androidx.car.app.model.Distance
|
import androidx.car.app.model.Distance
|
||||||
import androidx.car.app.model.ForegroundCarColorSpan
|
|
||||||
import androidx.car.app.model.MessageTemplate
|
import androidx.car.app.model.MessageTemplate
|
||||||
import androidx.car.app.model.Template
|
import androidx.car.app.model.Template
|
||||||
import androidx.car.app.versioning.CarAppApiLevels
|
import androidx.car.app.versioning.CarAppApiLevels
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import com.github.erfansn.localeconfigx.currentOrDefaultLocale
|
|
||||||
import net.vonforst.evmap.BuildConfig
|
import net.vonforst.evmap.BuildConfig
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
|
||||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||||
import net.vonforst.evmap.api.iconForPlugType
|
|
||||||
import net.vonforst.evmap.api.nameForPlugType
|
|
||||||
import net.vonforst.evmap.api.stringProvider
|
|
||||||
import net.vonforst.evmap.ftPerMile
|
import net.vonforst.evmap.ftPerMile
|
||||||
import net.vonforst.evmap.getPackageInfoCompat
|
import net.vonforst.evmap.getPackageInfoCompat
|
||||||
import net.vonforst.evmap.kmPerMile
|
import net.vonforst.evmap.kmPerMile
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
|
||||||
import net.vonforst.evmap.shouldUseImperialUnits
|
import net.vonforst.evmap.shouldUseImperialUnits
|
||||||
import net.vonforst.evmap.ui.availabilityText
|
|
||||||
import net.vonforst.evmap.ydPerMile
|
import net.vonforst.evmap.ydPerMile
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -236,9 +222,6 @@ fun supportsCarApiLevel3(ctx: CarContext): Boolean {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun supportsNewMapScreen(ctx: CarContext) =
|
|
||||||
ctx.carAppApiLevel >= 7 && ctx.isAppDrivenRefreshSupported
|
|
||||||
|
|
||||||
@ExperimentalCarApi
|
@ExperimentalCarApi
|
||||||
fun openUrl(carContext: CarContext, cas: CarAppService, url: String) {
|
fun openUrl(carContext: CarContext, cas: CarAppService, url: String) {
|
||||||
val intent = CustomTabsIntent.Builder()
|
val intent = CustomTabsIntent.Builder()
|
||||||
@@ -274,64 +257,6 @@ fun openUrl(carContext: CarContext, cas: CarAppService, url: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalCarApi
|
|
||||||
fun navigateToCharger(ctx: CarContext, cas: CarAppService, charger: ChargeLocation) {
|
|
||||||
var success = navigateCarApp(ctx, charger)
|
|
||||||
if (!success && BuildConfig.FLAVOR_automotive == "automotive") {
|
|
||||||
// on AAOS, some OEMs' navigation apps might not support
|
|
||||||
success = navigateRegularApp(ctx, cas, charger)
|
|
||||||
}
|
|
||||||
if (!success) {
|
|
||||||
CarToast.makeText(ctx, R.string.no_maps_app_found, CarToast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun navigateCarApp(ctx: CarContext, charger: ChargeLocation): Boolean {
|
|
||||||
val coord = charger.coordinates
|
|
||||||
val intent =
|
|
||||||
Intent(
|
|
||||||
CarContext.ACTION_NAVIGATE,
|
|
||||||
Uri.parse("geo:${coord.lat},${coord.lng}")
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
ctx.startCarApp(intent)
|
|
||||||
return true
|
|
||||||
} catch (e: HostException) {
|
|
||||||
Log.w("navigateToCharger", "Could not start navigation using car app intent")
|
|
||||||
Log.w("navigateToCharger", intent.toString())
|
|
||||||
e.printStackTrace()
|
|
||||||
} catch (e: SecurityException) {
|
|
||||||
Log.w("navigateToCharger", "Could not start navigation using car app intent")
|
|
||||||
Log.w("navigateToCharger", intent.toString())
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalCarApi
|
|
||||||
private fun navigateRegularApp(
|
|
||||||
ctx: CarContext,
|
|
||||||
cas: CarAppService,
|
|
||||||
charger: ChargeLocation
|
|
||||||
): Boolean {
|
|
||||||
val coord = charger.coordinates
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
|
||||||
intent.data = Uri.parse(
|
|
||||||
"geo:${coord.lat},${coord.lng}?q=${coord.lat},${coord.lng}(${
|
|
||||||
Uri.encode(charger.name)
|
|
||||||
})"
|
|
||||||
)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
if (intent.resolveActivity(ctx.packageManager) != null) {
|
|
||||||
cas.startActivity(intent)
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
Log.w("navigateToCharger", "Could not start navigation using regular intent")
|
|
||||||
Log.w("navigateToCharger", intent.toString())
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
class DummyReturnScreen(ctx: CarContext) : Screen(ctx) {
|
class DummyReturnScreen(ctx: CarContext) : Screen(ctx) {
|
||||||
/*
|
/*
|
||||||
Dummy screen to get around template refresh limitations.
|
Dummy screen to get around template refresh limitations.
|
||||||
@@ -356,49 +281,4 @@ class TextMeasurer(ctx: CarContext) {
|
|||||||
fun measureText(text: CharSequence): Float {
|
fun measureText(text: CharSequence): Float {
|
||||||
return textPaint.measureText(text, 0, text.length)
|
return textPaint.measureText(text, 0, text.length)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun generateChargepointsText(
|
|
||||||
charger: ChargeLocation,
|
|
||||||
availability: ChargeLocationStatus?,
|
|
||||||
ctx: Context
|
|
||||||
): SpannableStringBuilder {
|
|
||||||
val chargepointsText = SpannableStringBuilder()
|
|
||||||
charger.chargepointsMerged.forEachIndexed { i, cp ->
|
|
||||||
chargepointsText.apply {
|
|
||||||
if (i > 0) append(" · ")
|
|
||||||
append("${cp.count}× ")
|
|
||||||
val plugIcon = iconForPlugType(cp.type)
|
|
||||||
if (plugIcon != 0) {
|
|
||||||
append(
|
|
||||||
nameForPlugType(ctx.stringProvider(), cp.type),
|
|
||||||
CarIconSpan.create(
|
|
||||||
CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
ctx,
|
|
||||||
plugIcon
|
|
||||||
)
|
|
||||||
).setTint(
|
|
||||||
CarColor.createCustom(Color.WHITE, Color.BLACK)
|
|
||||||
).build()
|
|
||||||
),
|
|
||||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
append(nameForPlugType(ctx.stringProvider(), cp.type))
|
|
||||||
}
|
|
||||||
cp.formatPower(ctx.currentOrDefaultLocale)?.let {
|
|
||||||
append(" ")
|
|
||||||
append(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
availability?.status?.get(cp)?.let { status ->
|
|
||||||
chargepointsText.append(
|
|
||||||
" (${availabilityText(status)}/${cp.count})",
|
|
||||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return chargepointsText
|
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,6 @@ import android.view.LayoutInflater
|
|||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
@@ -111,11 +108,6 @@ class ChargepriceFragment : Fragment() {
|
|||||||
binding.toolbar.inflateMenu(R.menu.chargeprice)
|
binding.toolbar.inflateMenu(R.menu.chargeprice)
|
||||||
binding.toolbar.setTitle(R.string.chargeprice_title)
|
binding.toolbar.setTitle(R.string.chargeprice_title)
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.chargePricesList) { v, insets ->
|
|
||||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
|
||||||
WindowInsetsCompat.CONSUMED
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ class DataSourceSelectDialog : MaterialDialogFragment() {
|
|||||||
when (prefs.dataSource) {
|
when (prefs.dataSource) {
|
||||||
"goingelectric" -> binding.rgDataSource.rbGoingElectric.isChecked = true
|
"goingelectric" -> binding.rgDataSource.rbGoingElectric.isChecked = true
|
||||||
"openchargemap" -> binding.rgDataSource.rbOpenChargeMap.isChecked = true
|
"openchargemap" -> binding.rgDataSource.rbOpenChargeMap.isChecked = true
|
||||||
"openstreetmap" -> binding.rgDataSource.rbOpenStreetMap.isChecked = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,8 +65,6 @@ class DataSourceSelectDialog : MaterialDialogFragment() {
|
|||||||
"goingelectric"
|
"goingelectric"
|
||||||
} else if (binding.rgDataSource.rbOpenChargeMap.isChecked) {
|
} else if (binding.rgDataSource.rbOpenChargeMap.isChecked) {
|
||||||
"openchargemap"
|
"openchargemap"
|
||||||
} else if (binding.rgDataSource.rbOpenStreetMap.isChecked) {
|
|
||||||
"openstreetmap"
|
|
||||||
} else {
|
} else {
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import android.content.Intent
|
|||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
import net.vonforst.evmap.databinding.FragmentDonateReferralBinding
|
import net.vonforst.evmap.databinding.FragmentDonateReferralBinding
|
||||||
@@ -25,10 +22,5 @@ abstract class DonateFragmentBase : Fragment() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(referrals.root) { v, insets ->
|
|
||||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
|
||||||
WindowInsetsCompat.CONSUMED
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,6 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
@@ -71,13 +68,6 @@ class FavoritesFragment : Fragment() {
|
|||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
binding.vm = vm
|
binding.vm = vm
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
|
||||||
binding.favsList
|
|
||||||
) { v, insets ->
|
|
||||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
|
||||||
WindowInsetsCompat.CONSUMED
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
@@ -52,13 +49,6 @@ class FilterFragment : Fragment(), MenuProvider {
|
|||||||
binding.vm = vm
|
binding.vm = vm
|
||||||
vm.filterProfile.observe(viewLifecycleOwner) {}
|
vm.filterProfile.observe(viewLifecycleOwner) {}
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
|
||||||
binding.filtersList
|
|
||||||
) { v, insets ->
|
|
||||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
|
||||||
WindowInsetsCompat.CONSUMED
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -63,13 +60,6 @@ class FilterProfilesFragment : Fragment() {
|
|||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
binding.vm = vm
|
binding.vm = vm
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
|
||||||
binding.filterProfilesList
|
|
||||||
) { v, insets ->
|
|
||||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
|
||||||
WindowInsetsCompat.CONSUMED
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import android.widget.AdapterView
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.BackEventCompat
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
@@ -35,7 +36,6 @@ import androidx.core.view.MenuProvider
|
|||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.doOnLayout
|
import androidx.core.view.doOnLayout
|
||||||
import androidx.core.view.doOnNextLayout
|
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
@@ -58,7 +58,15 @@ import com.car2go.maps.AnyMap
|
|||||||
import com.car2go.maps.MapFactory
|
import com.car2go.maps.MapFactory
|
||||||
import com.car2go.maps.MapFragment
|
import com.car2go.maps.MapFragment
|
||||||
import com.car2go.maps.OnMapReadyCallback
|
import com.car2go.maps.OnMapReadyCallback
|
||||||
|
import com.car2go.maps.model.BitmapDescriptor
|
||||||
import com.car2go.maps.model.LatLng
|
import com.car2go.maps.model.LatLng
|
||||||
|
import com.car2go.maps.model.Marker
|
||||||
|
import com.car2go.maps.model.MarkerOptions
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_SETTLING
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.transition.MaterialArcMotion
|
import com.google.android.material.transition.MaterialArcMotion
|
||||||
@@ -66,24 +74,20 @@ import com.google.android.material.transition.MaterialContainerTransform
|
|||||||
import com.google.android.material.transition.MaterialContainerTransform.FADE_MODE_CROSS
|
import com.google.android.material.transition.MaterialContainerTransform.FADE_MODE_CROSS
|
||||||
import com.google.android.material.transition.MaterialFadeThrough
|
import com.google.android.material.transition.MaterialFadeThrough
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
|
|
||||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback
|
|
||||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
|
|
||||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
|
|
||||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
|
|
||||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING
|
|
||||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.from
|
|
||||||
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
|
|
||||||
import com.stfalcon.imageviewer.StfalconImageViewer
|
import com.stfalcon.imageviewer.StfalconImageViewer
|
||||||
import io.michaelrocks.bimap.HashBiMap
|
import io.michaelrocks.bimap.HashBiMap
|
||||||
import io.michaelrocks.bimap.MutableBiMap
|
import io.michaelrocks.bimap.MutableBiMap
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import net.vonforst.evmap.BuildConfig
|
||||||
import net.vonforst.evmap.MapsActivity
|
import net.vonforst.evmap.MapsActivity
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
import net.vonforst.evmap.adapter.ConnectorAdapter
|
import net.vonforst.evmap.adapter.ConnectorAdapter
|
||||||
import net.vonforst.evmap.adapter.DetailsAdapter
|
import net.vonforst.evmap.adapter.DetailsAdapter
|
||||||
import net.vonforst.evmap.adapter.GalleryAdapter
|
import net.vonforst.evmap.adapter.GalleryAdapter
|
||||||
import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter
|
import net.vonforst.evmap.adapter.PlaceAutocompleteAdapter
|
||||||
|
import net.vonforst.evmap.api.ChargepointList
|
||||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||||
import net.vonforst.evmap.autocomplete.ApiUnavailableException
|
import net.vonforst.evmap.autocomplete.ApiUnavailableException
|
||||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
||||||
@@ -93,6 +97,7 @@ import net.vonforst.evmap.location.FusionEngine
|
|||||||
import net.vonforst.evmap.location.LocationEngine
|
import net.vonforst.evmap.location.LocationEngine
|
||||||
import net.vonforst.evmap.location.Priority
|
import net.vonforst.evmap.location.Priority
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
import net.vonforst.evmap.model.ChargeLocation
|
||||||
|
import net.vonforst.evmap.model.ChargeLocationCluster
|
||||||
import net.vonforst.evmap.model.ChargepointListItem
|
import net.vonforst.evmap.model.ChargepointListItem
|
||||||
import net.vonforst.evmap.model.ChargerPhoto
|
import net.vonforst.evmap.model.ChargerPhoto
|
||||||
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
||||||
@@ -101,8 +106,14 @@ import net.vonforst.evmap.model.FILTERS_FAVORITES
|
|||||||
import net.vonforst.evmap.navigation.safeNavigate
|
import net.vonforst.evmap.navigation.safeNavigate
|
||||||
import net.vonforst.evmap.shouldUseImperialUnits
|
import net.vonforst.evmap.shouldUseImperialUnits
|
||||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||||
|
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||||
|
import net.vonforst.evmap.ui.ClusterIconGenerator
|
||||||
import net.vonforst.evmap.ui.HideOnScrollFabBehavior
|
import net.vonforst.evmap.ui.HideOnScrollFabBehavior
|
||||||
import net.vonforst.evmap.ui.MarkerManager
|
import net.vonforst.evmap.ui.MarkerAnimator
|
||||||
|
import net.vonforst.evmap.ui.chargerZ
|
||||||
|
import net.vonforst.evmap.ui.clusterZ
|
||||||
|
import net.vonforst.evmap.ui.getMarkerTint
|
||||||
|
import net.vonforst.evmap.ui.placeSearchZ
|
||||||
import net.vonforst.evmap.ui.setTouchModal
|
import net.vonforst.evmap.ui.setTouchModal
|
||||||
import net.vonforst.evmap.utils.boundingBox
|
import net.vonforst.evmap.utils.boundingBox
|
||||||
import net.vonforst.evmap.utils.checkAnyLocationPermission
|
import net.vonforst.evmap.utils.checkAnyLocationPermission
|
||||||
@@ -117,6 +128,10 @@ import net.vonforst.evmap.viewmodel.Status
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import kotlin.collections.component1
|
||||||
|
import kotlin.collections.component2
|
||||||
|
import kotlin.collections.contains
|
||||||
|
import kotlin.collections.set
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
@@ -127,19 +142,54 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
private val galleryVm: GalleryViewModel by activityViewModels()
|
private val galleryVm: GalleryViewModel by activityViewModels()
|
||||||
private var mapFragment: MapFragment? = null
|
private var mapFragment: MapFragment? = null
|
||||||
private var map: AnyMap? = null
|
private var map: AnyMap? = null
|
||||||
private var markerManager: MarkerManager? = null
|
|
||||||
private lateinit var locationEngine: LocationEngine
|
private lateinit var locationEngine: LocationEngine
|
||||||
private var requestingLocationUpdates = false
|
private var requestingLocationUpdates = false
|
||||||
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
|
private lateinit var bottomSheetBehavior: BottomSheetBehavior<View>
|
||||||
private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior
|
|
||||||
private lateinit var detailsDialog: ConnectorDetailsDialog
|
private lateinit var detailsDialog: ConnectorDetailsDialog
|
||||||
private lateinit var prefs: PreferenceDataSource
|
private lateinit var prefs: PreferenceDataSource
|
||||||
|
private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap()
|
||||||
|
private var clusterMarkers: List<Marker> = emptyList()
|
||||||
|
private var searchResultMarker: Marker? = null
|
||||||
|
private var searchResultIcon: BitmapDescriptor? = null
|
||||||
private var connectionErrorSnackbar: Snackbar? = null
|
private var connectionErrorSnackbar: Snackbar? = null
|
||||||
|
private var zoomInSnackbar: Snackbar? = null
|
||||||
|
private var previousChargepointIds: Set<Long>? = null
|
||||||
private var mapTopPadding: Int = 0
|
private var mapTopPadding: Int = 0
|
||||||
private var mapBottomPadding: Int = 0
|
|
||||||
private var popupMenu: PopupMenu? = null
|
private var popupMenu: PopupMenu? = null
|
||||||
private var insetBottom: Int = 0
|
|
||||||
|
private lateinit var clusterIconGenerator: ClusterIconGenerator
|
||||||
|
private lateinit var chargerIconGenerator: ChargerIconGenerator
|
||||||
|
private lateinit var animator: MarkerAnimator
|
||||||
private lateinit var favToggle: MenuItem
|
private lateinit var favToggle: MenuItem
|
||||||
|
|
||||||
|
private val bottomSheetBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
val state = bottomSheetBehavior.state
|
||||||
|
when (state) {
|
||||||
|
STATE_COLLAPSED -> vm.chargerSparse.value = null
|
||||||
|
STATE_HIDDEN -> return
|
||||||
|
else -> if (bottomSheetCollapsible) {
|
||||||
|
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||||
|
} else {
|
||||||
|
vm.chargerSparse.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bottomSheetBehavior.cancelBackProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleOnBackStarted(backEvent: BackEventCompat) {
|
||||||
|
bottomSheetBehavior.startBackProgress(backEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleOnBackProgressed(backEvent: BackEventCompat) {
|
||||||
|
bottomSheetBehavior.updateBackProgress(backEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleOnBackCancelled() {
|
||||||
|
bottomSheetBehavior.cancelBackProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
val value = vm.layersMenuOpen.value
|
val value = vm.layersMenuOpen.value
|
||||||
@@ -156,18 +206,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
|
|
||||||
if (binding.search.hasFocus()) {
|
if (binding.search.hasFocus()) {
|
||||||
removeSearchFocus()
|
removeSearchFocus()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val state = bottomSheetBehavior.state
|
vm.searchResult.value = null
|
||||||
when (state) {
|
|
||||||
STATE_COLLAPSED -> vm.chargerSparse.value = null
|
|
||||||
STATE_HIDDEN -> vm.searchResult.value = null
|
|
||||||
else -> if (bottomSheetCollapsible) {
|
|
||||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
|
||||||
} else {
|
|
||||||
vm.chargerSparse.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,6 +219,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
prefs = PreferenceDataSource(requireContext())
|
prefs = PreferenceDataSource(requireContext())
|
||||||
|
|
||||||
locationEngine = FusionEngine(requireContext())
|
locationEngine = FusionEngine(requireContext())
|
||||||
|
clusterIconGenerator = ClusterIconGenerator(requireContext())
|
||||||
|
|
||||||
enterTransition = MaterialFadeThrough()
|
enterTransition = MaterialFadeThrough()
|
||||||
exitTransition = MaterialFadeThrough()
|
exitTransition = MaterialFadeThrough()
|
||||||
@@ -211,32 +254,39 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
.replace(R.id.map, mapFragment!!, mapFragmentTag)
|
.replace(R.id.map, mapFragment!!, mapFragmentTag)
|
||||||
.commit()
|
.commit()
|
||||||
|
|
||||||
|
// reset map-related stuff (map provider may have changed)
|
||||||
map = null
|
map = null
|
||||||
markerManager = null
|
markers.clear()
|
||||||
|
clusterMarkers = emptyList()
|
||||||
|
searchResultMarker = null
|
||||||
|
searchResultIcon = null
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailAppBar.toolbar.popupTheme =
|
binding.detailView.toolbar.popupTheme =
|
||||||
com.google.android.material.R.style.Theme_Material3_DayNight
|
com.google.android.material.R.style.ThemeOverlay_AppCompat_DayNight
|
||||||
|
|
||||||
val density = resources.displayMetrics.density
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.detailAppBar.toolbar) { v, insets ->
|
binding.root
|
||||||
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top
|
) { _, insets ->
|
||||||
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
ViewCompat.onApplyWindowInsets(binding.root, insets)
|
||||||
|
|
||||||
|
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
|
||||||
|
/*binding.detailView.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
topMargin = systemWindowInsetTop
|
topMargin = systemWindowInsetTop
|
||||||
}
|
}*/
|
||||||
WindowInsetsCompat.CONSUMED
|
|
||||||
}
|
val insetsBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
|
||||||
|
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom + insetsBottom
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.fabLayers) { v, insets ->
|
|
||||||
// margin of layers button: status bar height + toolbar height + margin
|
// margin of layers button: status bar height + toolbar height + margin
|
||||||
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top
|
val density = resources.displayMetrics.density
|
||||||
val margin =
|
val margin =
|
||||||
if (binding.toolbarContainer.layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT) {
|
if (binding.toolbarContainer.layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||||
systemWindowInsetTop + (48 * density).toInt() + (28 * density).toInt()
|
systemWindowInsetTop + (48 * density).toInt() + (28 * density).toInt()
|
||||||
} else {
|
} else {
|
||||||
systemWindowInsetTop + (12 * density).toInt()
|
systemWindowInsetTop + (12 * density).toInt()
|
||||||
}
|
}
|
||||||
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.fabLayers.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
topMargin = margin
|
topMargin = margin
|
||||||
}
|
}
|
||||||
binding.layersSheet.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
binding.layersSheet.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||||
@@ -245,37 +295,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
|
|
||||||
// set map padding so that compass is not obstructed by toolbar
|
// set map padding so that compass is not obstructed by toolbar
|
||||||
mapTopPadding = systemWindowInsetTop + (48 * density).toInt() + (16 * density).toInt()
|
mapTopPadding = systemWindowInsetTop + (48 * density).toInt() + (16 * density).toInt()
|
||||||
mapBottomPadding = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
|
||||||
// if we actually use map.setPadding here, MapLibre will re-trigger onApplyWindowInsets
|
// if we actually use map.setPadding here, MapLibre will re-trigger onApplyWindowInsets
|
||||||
// and cause an infinite loop. So we rely on onMapReady being called later than
|
// and cause an infinite loop. So we rely on onMapReady being called later than
|
||||||
// onApplyWindowInsets.
|
// onApplyWindowInsets.
|
||||||
|
|
||||||
WindowInsetsCompat.CONSUMED
|
insets
|
||||||
}
|
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.fabLocate) { v, insets ->
|
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
|
||||||
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin =
|
|
||||||
systemBars + resources.getDimensionPixelSize(com.mahc.custombottomsheetbehavior.R.dimen.fab_margin)
|
|
||||||
}
|
|
||||||
WindowInsetsCompat.CONSUMED
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.navBarScrim) { v, insets ->
|
|
||||||
insetBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
|
|
||||||
v.layoutParams.height = insetBottom
|
|
||||||
updatePeekHeight()
|
|
||||||
WindowInsetsCompat.CONSUMED
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.galleryContainer) { v, insets ->
|
|
||||||
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top
|
|
||||||
val newHeight =
|
|
||||||
resources.getDimensionPixelSize(R.dimen.gallery_height_with_margin) + systemWindowInsetTop
|
|
||||||
v.layoutParams.height = newHeight
|
|
||||||
bottomSheetBehavior.anchorPoint = newHeight
|
|
||||||
WindowInsetsCompat.CONSUMED
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exitTransition = TransitionInflater.from(requireContext())
|
exitTransition = TransitionInflater.from(requireContext())
|
||||||
@@ -285,14 +309,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
viewLifecycleOwner,
|
viewLifecycleOwner,
|
||||||
backPressedCallback
|
backPressedCallback
|
||||||
)
|
)
|
||||||
|
requireActivity().onBackPressedDispatcher.addCallback(
|
||||||
|
viewLifecycleOwner,
|
||||||
|
bottomSheetBackPressedCallback
|
||||||
|
)
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePeekHeight() {
|
|
||||||
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom + insetBottom
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMapProvider(provider: String) = when (provider) {
|
private fun getMapProvider(provider: String) = when (provider) {
|
||||||
"mapbox" -> MapFactory.MAPLIBRE
|
"mapbox" -> MapFactory.MAPLIBRE
|
||||||
"google" -> MapFactory.GOOGLE
|
"google" -> MapFactory.GOOGLE
|
||||||
@@ -310,22 +334,20 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
|
||||||
|
|
||||||
mapFragment!!.getMapAsync(this)
|
mapFragment!!.getMapAsync(this)
|
||||||
bottomSheetBehavior = from(binding.bottomSheet)
|
bottomSheetBehavior = BottomSheetBehavior.from(binding.detailView.root)
|
||||||
detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
|
//detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
|
||||||
|
|
||||||
binding.detailAppBar.toolbar.inflateMenu(R.menu.detail)
|
binding.detailView.toolbar.inflateMenu(R.menu.detail)
|
||||||
favToggle = binding.detailAppBar.toolbar.menu.findItem(R.id.menu_fav)
|
favToggle = binding.detailView.toolbar.menu.findItem(R.id.menu_fav)
|
||||||
|
|
||||||
vm.apiName.observe(viewLifecycleOwner) {
|
vm.apiName.observe(viewLifecycleOwner) {
|
||||||
binding.detailAppBar.toolbar.menu.findItem(R.id.menu_edit).title =
|
binding.detailView.toolbar.menu.findItem(R.id.menu_edit).title =
|
||||||
getString(R.string.edit_at_datasource, it)
|
getString(R.string.edit_at_datasource, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.detailView.topPart.doOnNextLayout {
|
vm.bottomSheetState.value?.let { bottomSheetBehavior.state = it }
|
||||||
updatePeekHeight()
|
bottomSheetBehavior.skipCollapsed = !bottomSheetCollapsible
|
||||||
vm.bottomSheetState.value?.let { bottomSheetBehavior.state = it }
|
bottomSheetBehavior.state = STATE_HIDDEN
|
||||||
}
|
|
||||||
bottomSheetBehavior.isCollapsible = bottomSheetCollapsible
|
|
||||||
binding.detailView.connectorDetails
|
binding.detailView.connectorDetails
|
||||||
|
|
||||||
setupObservers()
|
setupObservers()
|
||||||
@@ -478,7 +500,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
binding.detailView.topPart.setOnClickListener {
|
binding.detailView.topPart.setOnClickListener {
|
||||||
bottomSheetBehavior.state = STATE_ANCHOR_POINT
|
bottomSheetBehavior.state = STATE_HALF_EXPANDED
|
||||||
}
|
}
|
||||||
binding.detailView.topPart.setOnLongClickListener {
|
binding.detailView.topPart.setOnLongClickListener {
|
||||||
val charger = vm.charger.value?.data ?: return@setOnLongClickListener false
|
val charger = vm.charger.value?.data ?: return@setOnLongClickListener false
|
||||||
@@ -486,14 +508,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
return@setOnLongClickListener true
|
return@setOnLongClickListener true
|
||||||
}
|
}
|
||||||
setupSearchAutocomplete()
|
setupSearchAutocomplete()
|
||||||
binding.detailAppBar.toolbar.setNavigationOnClickListener {
|
binding.detailView.toolbar.setNavigationOnClickListener {
|
||||||
if (bottomSheetCollapsible) {
|
if (bottomSheetCollapsible) {
|
||||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||||
} else {
|
} else {
|
||||||
vm.chargerSparse.value = null
|
vm.chargerSparse.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.detailAppBar.toolbar.setOnMenuItemClickListener {
|
binding.detailView.toolbar.setOnMenuItemClickListener {
|
||||||
when (it.itemId) {
|
when (it.itemId) {
|
||||||
R.id.menu_fav -> {
|
R.id.menu_fav -> {
|
||||||
toggleFavorite()
|
toggleFavorite()
|
||||||
@@ -640,36 +662,38 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
} else {
|
} else {
|
||||||
vm.insertFavorite(charger)
|
vm.insertFavorite(charger)
|
||||||
}
|
}
|
||||||
|
markers.inverse[charger]?.setIcon(
|
||||||
|
chargerIconGenerator.getBitmapDescriptor(
|
||||||
|
getMarkerTint(charger, vm.filteredConnectors.value),
|
||||||
|
highlight = true,
|
||||||
|
fault = charger.faultReport != null,
|
||||||
|
multi = charger.isMulti(vm.filteredConnectors.value),
|
||||||
|
fav = fav == null,
|
||||||
|
mini = vm.useMiniMarkers.value == true
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupObservers() {
|
private fun setupObservers() {
|
||||||
bottomSheetBehavior.addBottomSheetCallback(object :
|
bottomSheetBehavior.addBottomSheetCallback(object :
|
||||||
BottomSheetCallback() {
|
BottomSheetBehavior.BottomSheetCallback() {
|
||||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||||
if (bottomSheetBehavior.state == STATE_HIDDEN) {
|
if (bottomSheetBehavior.state == STATE_HIDDEN) {
|
||||||
map?.setPadding(0, mapTopPadding, 0, mapBottomPadding)
|
map?.setPadding(0, mapTopPadding, 0, 0)
|
||||||
} else {
|
} else {
|
||||||
val height = binding.root.height - bottomSheet.top
|
val height = binding.root.height - bottomSheet.top
|
||||||
map?.setPadding(
|
map?.setPadding(
|
||||||
0,
|
0,
|
||||||
mapTopPadding,
|
mapTopPadding,
|
||||||
0,
|
0,
|
||||||
mapBottomPadding + min(bottomSheetBehavior.peekHeight, height)
|
min(bottomSheetBehavior.peekHeight, height)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
println(slideOffset)
|
|
||||||
if (bottomSheetBehavior.state != STATE_HIDDEN) {
|
|
||||||
binding.navBarScrim.visibility = View.VISIBLE
|
|
||||||
binding.navBarScrim.translationY =
|
|
||||||
(if (slideOffset < 0f) -slideOffset else 2 * slideOffset) * binding.navBarScrim.height
|
|
||||||
} else {
|
|
||||||
binding.navBarScrim.visibility = View.INVISIBLE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||||
vm.bottomSheetState.value = newState
|
vm.bottomSheetState.value = newState
|
||||||
updateBackPressedCallback()
|
bottomSheetBackPressedCallback.isEnabled = newState != STATE_HIDDEN
|
||||||
|
|
||||||
if (vm.layersMenuOpen.value!! && newState !in listOf(
|
if (vm.layersMenuOpen.value!! && newState !in listOf(
|
||||||
STATE_SETTLING,
|
STATE_SETTLING,
|
||||||
@@ -681,7 +705,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (vm.selectedChargepoint.value != null && newState in listOf(
|
if (vm.selectedChargepoint.value != null && newState in listOf(
|
||||||
STATE_ANCHOR_POINT, STATE_COLLAPSED
|
STATE_HALF_EXPANDED, STATE_COLLAPSED
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
closeConnectorDetailsDialog()
|
closeConnectorDetailsDialog()
|
||||||
@@ -691,30 +715,29 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
})
|
})
|
||||||
vm.chargerSparse.observe(viewLifecycleOwner) {
|
vm.chargerSparse.observe(viewLifecycleOwner) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
if (vm.bottomSheetState.value != STATE_ANCHOR_POINT) {
|
if (vm.bottomSheetState.value != STATE_HALF_EXPANDED) {
|
||||||
bottomSheetBehavior.state =
|
bottomSheetBehavior.state =
|
||||||
if (bottomSheetCollapsible) STATE_COLLAPSED else STATE_ANCHOR_POINT
|
if (bottomSheetCollapsible) STATE_COLLAPSED else STATE_HALF_EXPANDED
|
||||||
}
|
}
|
||||||
removeSearchFocus()
|
removeSearchFocus()
|
||||||
binding.fabDirections.show()
|
binding.fabDirections.show()
|
||||||
detailAppBarBehavior.setToolbarTitle(it.name)
|
//detailAppBarBehavior.setToolbarTitle(it.name)
|
||||||
updateFavoriteToggle()
|
updateFavoriteToggle()
|
||||||
markerManager?.highlighedCharger = it
|
highlightMarker(it)
|
||||||
markerManager?.animateBounce(it)
|
|
||||||
} else {
|
} else {
|
||||||
bottomSheetBehavior.state = STATE_HIDDEN
|
bottomSheetBehavior.state = STATE_HIDDEN
|
||||||
markerManager?.highlighedCharger = null
|
unhighlightAllMarkers()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vm.chargepoints.observe(viewLifecycleOwner, Observer { res ->
|
vm.chargepoints.observe(viewLifecycleOwner, Observer { res ->
|
||||||
val chargepoints = res.data
|
val chargepoints = res.data
|
||||||
if (chargepoints != null) {
|
if (chargepoints != null) {
|
||||||
markerManager?.chargepoints = chargepoints
|
updateMap(chargepoints.items)
|
||||||
}
|
}
|
||||||
|
val view = view ?: return@Observer
|
||||||
when (res.status) {
|
when (res.status) {
|
||||||
Status.ERROR -> {
|
Status.ERROR -> {
|
||||||
val view = view ?: return@Observer
|
zoomInSnackbar?.dismiss()
|
||||||
|
|
||||||
connectionErrorSnackbar?.dismiss()
|
connectionErrorSnackbar?.dismiss()
|
||||||
connectionErrorSnackbar = Snackbar
|
connectionErrorSnackbar = Snackbar
|
||||||
.make(view, R.string.connection_error, Snackbar.LENGTH_INDEFINITE)
|
.make(view, R.string.connection_error, Snackbar.LENGTH_INDEFINITE)
|
||||||
@@ -726,20 +749,23 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
}
|
}
|
||||||
Status.SUCCESS -> {
|
Status.SUCCESS -> {
|
||||||
connectionErrorSnackbar?.dismiss()
|
connectionErrorSnackbar?.dismiss()
|
||||||
|
if (res.data != null && !res.data.isComplete) {
|
||||||
|
zoomInSnackbar?.dismiss()
|
||||||
|
zoomInSnackbar = Snackbar
|
||||||
|
.make(view, R.string.zoom_in_to_see_more, Snackbar.LENGTH_INDEFINITE)
|
||||||
|
zoomInSnackbar!!.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Status.LOADING -> {
|
Status.LOADING -> {
|
||||||
|
zoomInSnackbar?.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
vm.useMiniMarkers.observe(viewLifecycleOwner) {
|
vm.useMiniMarkers.observe(viewLifecycleOwner) {
|
||||||
markerManager?.mini = it
|
vm.chargepoints.value?.data?.let { updateMap(it.items) }
|
||||||
}
|
|
||||||
vm.filteredConnectors.observe(viewLifecycleOwner) {
|
|
||||||
markerManager?.filteredConnectors = it
|
|
||||||
}
|
}
|
||||||
vm.favorites.observe(viewLifecycleOwner) {
|
vm.favorites.observe(viewLifecycleOwner) {
|
||||||
updateFavoriteToggle()
|
updateFavoriteToggle()
|
||||||
markerManager?.favorites = it.map { it.favorite.chargerId }.toSet()
|
|
||||||
}
|
}
|
||||||
vm.searchResult.observe(viewLifecycleOwner) { place ->
|
vm.searchResult.observe(viewLifecycleOwner) { place ->
|
||||||
displaySearchResult(place, moveCamera = true)
|
displaySearchResult(place, moveCamera = true)
|
||||||
@@ -762,7 +788,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
if (it != null) {
|
if (it != null) {
|
||||||
detailsDialog.setData(it, vm.availability.value?.data)
|
detailsDialog.setData(it, vm.availability.value?.data)
|
||||||
}
|
}
|
||||||
updateBackPressedCallback()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBackPressedCallback()
|
updateBackPressedCallback()
|
||||||
@@ -770,7 +795,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
|
|
||||||
private fun displaySearchResult(place: PlaceWithBounds?, moveCamera: Boolean) {
|
private fun displaySearchResult(place: PlaceWithBounds?, moveCamera: Boolean) {
|
||||||
val map = this.map ?: return
|
val map = this.map ?: return
|
||||||
markerManager?.searchResult = place
|
searchResultMarker?.remove()
|
||||||
|
searchResultMarker = null
|
||||||
|
|
||||||
if (place != null) {
|
if (place != null) {
|
||||||
// disable location following when search result is shown
|
// disable location following when search result is shown
|
||||||
@@ -782,6 +808,18 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
map.animateCamera(map.cameraUpdateFactory.newLatLngZoom(place.latLng, 12f))
|
map.animateCamera(map.cameraUpdateFactory.newLatLngZoom(place.latLng, 12f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (searchResultIcon == null) {
|
||||||
|
searchResultIcon =
|
||||||
|
map.bitmapDescriptorFactory.fromResource(R.drawable.ic_map_marker)
|
||||||
|
}
|
||||||
|
searchResultMarker = map.addMarker(
|
||||||
|
MarkerOptions()
|
||||||
|
.z(placeSearchZ)
|
||||||
|
.position(place.latLng)
|
||||||
|
.icon(searchResultIcon)
|
||||||
|
.anchor(0.5f, 1f)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
binding.search.setText("")
|
binding.search.setText("")
|
||||||
}
|
}
|
||||||
@@ -790,12 +828,56 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateBackPressedCallback() {
|
private fun updateBackPressedCallback() {
|
||||||
backPressedCallback.isEnabled =
|
backPressedCallback.isEnabled = vm.searchResult.value != null
|
||||||
vm.bottomSheetState.value != null && vm.bottomSheetState.value != STATE_HIDDEN
|
|
||||||
|| vm.searchResult.value != null
|
|
||||||
|| (vm.layersMenuOpen.value ?: false)
|
|| (vm.layersMenuOpen.value ?: false)
|
||||||
|| binding.search.hasFocus()
|
|| binding.search.hasFocus()
|
||||||
|| vm.selectedChargepoint.value != null
|
}
|
||||||
|
|
||||||
|
private fun unhighlightAllMarkers() {
|
||||||
|
markers.forEach { (m, c) ->
|
||||||
|
m.setIcon(
|
||||||
|
chargerIconGenerator.getBitmapDescriptor(
|
||||||
|
getMarkerTint(c, vm.filteredConnectors.value),
|
||||||
|
highlight = false,
|
||||||
|
fault = c.faultReport != null,
|
||||||
|
multi = c.isMulti(vm.filteredConnectors.value),
|
||||||
|
fav = c.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
|
||||||
|
mini = vm.useMiniMarkers.value == true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun highlightMarker(charger: ChargeLocation) {
|
||||||
|
val marker = markers.inverse[charger] ?: return
|
||||||
|
// highlight this marker
|
||||||
|
marker.setIcon(
|
||||||
|
chargerIconGenerator.getBitmapDescriptor(
|
||||||
|
getMarkerTint(charger, vm.filteredConnectors.value),
|
||||||
|
highlight = true,
|
||||||
|
fault = charger.faultReport != null,
|
||||||
|
multi = charger.isMulti(vm.filteredConnectors.value),
|
||||||
|
fav = charger.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
|
||||||
|
mini = vm.useMiniMarkers.value == true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
animator.animateMarkerBounce(marker, vm.useMiniMarkers.value == true)
|
||||||
|
|
||||||
|
// un-highlight all other markers
|
||||||
|
markers.forEach { (m, c) ->
|
||||||
|
if (m != marker) {
|
||||||
|
m.setIcon(
|
||||||
|
chargerIconGenerator.getBitmapDescriptor(
|
||||||
|
getMarkerTint(c, vm.filteredConnectors.value),
|
||||||
|
highlight = false,
|
||||||
|
fault = c.faultReport != null,
|
||||||
|
multi = c.isMulti(vm.filteredConnectors.value),
|
||||||
|
fav = c.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
|
||||||
|
mini = vm.useMiniMarkers.value == true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFavoriteToggle() {
|
private fun updateFavoriteToggle() {
|
||||||
@@ -1015,30 +1097,31 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
this.map = map
|
this.map = map
|
||||||
val context = this.context ?: return
|
val context = this.context ?: return
|
||||||
view ?: return
|
view ?: return
|
||||||
markerManager = MarkerManager(context, map, this).apply {
|
|
||||||
onChargerClick = {
|
chargerIconGenerator = ChargerIconGenerator(context, map.bitmapDescriptorFactory)
|
||||||
vm.chargerSparse.value = it
|
|
||||||
|
vm.mapTrafficSupported.value =
|
||||||
|
mapFragment?.let { AnyMap.Feature.TRAFFIC_LAYER in it.supportedFeatures } ?: false
|
||||||
|
|
||||||
|
if (BuildConfig.FLAVOR.contains("google") && mapFragment!!.priority[0] == MapFactory.GOOGLE) {
|
||||||
|
// Google Maps: icons can be generated in background thread
|
||||||
|
lifecycleScope.launch {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
chargerIconGenerator.preloadCache()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onClusterClick = {
|
} else {
|
||||||
val newZoom = map.cameraPosition.zoom + 2
|
// MapLibre: needs to be run on main thread
|
||||||
map.animateCamera(
|
chargerIconGenerator.preloadCache()
|
||||||
map.cameraUpdateFactory.newLatLngZoom(
|
|
||||||
LatLng(it.coordinates.lat, it.coordinates.lng),
|
|
||||||
newZoom
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
chargepoints = vm.chargepoints.value?.data ?: emptyList()
|
|
||||||
highlighedCharger = vm.chargerSparse.value
|
|
||||||
searchResult = vm.searchResult.value
|
|
||||||
favorites = vm.favorites.value?.map { it.favorite.chargerId }?.toSet() ?: emptySet()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
animator = MarkerAnimator(chargerIconGenerator)
|
||||||
map.uiSettings.setTiltGesturesEnabled(false)
|
map.uiSettings.setTiltGesturesEnabled(false)
|
||||||
map.uiSettings.setRotateGesturesEnabled(prefs.mapRotateGesturesEnabled)
|
map.uiSettings.setRotateGesturesEnabled(prefs.mapRotateGesturesEnabled)
|
||||||
map.setIndoorEnabled(false)
|
map.setIndoorEnabled(false)
|
||||||
map.uiSettings.setIndoorLevelPickerEnabled(false)
|
map.uiSettings.setIndoorLevelPickerEnabled(false)
|
||||||
map.uiSettings.setMapToolbarEnabled(false)
|
|
||||||
|
|
||||||
map.setOnCameraIdleListener {
|
map.setOnCameraIdleListener {
|
||||||
vm.mapPosition.value = MapPosition(
|
vm.mapPosition.value = MapPosition(
|
||||||
@@ -1086,16 +1169,40 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
map.setOnMarkerClickListener { marker ->
|
||||||
|
val map = this@MapFragment.map ?: return@setOnMarkerClickListener false
|
||||||
|
when (marker) {
|
||||||
|
in markers -> {
|
||||||
|
vm.chargerSparse.value = markers[marker]
|
||||||
|
true
|
||||||
|
}
|
||||||
|
in clusterMarkers -> {
|
||||||
|
val newZoom = map.cameraPosition.zoom + 2
|
||||||
|
map.animateCamera(
|
||||||
|
map.cameraUpdateFactory.newLatLngZoom(
|
||||||
|
marker.position,
|
||||||
|
newZoom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
searchResultMarker -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
map.setOnMapClickListener {
|
map.setOnMapClickListener {
|
||||||
if (backPressedCallback.isEnabled) {
|
if (backPressedCallback.isEnabled) {
|
||||||
backPressedCallback.handleOnBackPressed()
|
backPressedCallback.handleOnBackPressed()
|
||||||
|
} else if (bottomSheetBackPressedCallback.isEnabled) {
|
||||||
|
bottomSheetBackPressedCallback.handleOnBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
map.setMapType(vm.mapType.value)
|
map.setMapType(vm.mapType.value)
|
||||||
map.setTrafficEnabled(vm.mapTrafficEnabled.value ?: false)
|
map.setTrafficEnabled(vm.mapTrafficEnabled.value ?: false)
|
||||||
|
|
||||||
// set padding so that compass is not obstructed by toolbar
|
// set padding so that compass is not obstructed by toolbar
|
||||||
map.setPadding(0, mapTopPadding, 0, mapBottomPadding)
|
map.setPadding(0, mapTopPadding, 0, 0)
|
||||||
|
|
||||||
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||||
map.setMapStyle(
|
map.setMapStyle(
|
||||||
@@ -1143,10 +1250,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
// show charger detail after chargers were loaded
|
// show charger detail after chargers were loaded
|
||||||
vm.chargepoints.observe(
|
vm.chargepoints.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner,
|
||||||
object : Observer<Resource<List<ChargepointListItem>>> {
|
object : Observer<Resource<ChargepointList>> {
|
||||||
override fun onChanged(value: Resource<List<ChargepointListItem>>) {
|
override fun onChanged(value: Resource<ChargepointList>) {
|
||||||
if (value.data == null) return
|
if (value.data == null) return
|
||||||
for (item in value.data) {
|
for (item in value.data.items) {
|
||||||
if (item is ChargeLocation && item.id == chargerId) {
|
if (item is ChargeLocation && item.id == chargerId) {
|
||||||
vm.chargerSparse.value = item
|
vm.chargerSparse.value = item
|
||||||
vm.chargepoints.removeObserver(this)
|
vm.chargepoints.removeObserver(this)
|
||||||
@@ -1221,6 +1328,111 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun updateMap(chargepoints: List<ChargepointListItem>) {
|
||||||
|
val map = this.map ?: return
|
||||||
|
clusterMarkers.forEach { it.remove() }
|
||||||
|
|
||||||
|
val clusters = chargepoints.filterIsInstance<ChargeLocationCluster>()
|
||||||
|
val chargers = chargepoints.filterIsInstance<ChargeLocation>()
|
||||||
|
|
||||||
|
val chargepointIds = chargers.map { it.id }.toSet()
|
||||||
|
|
||||||
|
// update icons of existing markers (connector filter may have changed)
|
||||||
|
for ((marker, charger) in markers) {
|
||||||
|
val highlight = charger.id == vm.chargerSparse.value?.id
|
||||||
|
marker.setIcon(
|
||||||
|
chargerIconGenerator.getBitmapDescriptor(
|
||||||
|
getMarkerTint(charger, vm.filteredConnectors.value),
|
||||||
|
highlight = highlight,
|
||||||
|
fault = charger.faultReport != null,
|
||||||
|
multi = charger.isMulti(vm.filteredConnectors.value),
|
||||||
|
fav = charger.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList()),
|
||||||
|
mini = vm.useMiniMarkers.value == true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
marker.setAnchor(0.5f, if (vm.useMiniMarkers.value == true) 0.5f else 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chargers.toSet() != markers.values) {
|
||||||
|
// remove markers that disappeared
|
||||||
|
val bounds = map.projection.visibleRegion.latLngBounds
|
||||||
|
markers.entries.toList().forEach {
|
||||||
|
val marker = it.key
|
||||||
|
val charger = it.value
|
||||||
|
if (!chargepointIds.contains(charger.id)) {
|
||||||
|
// animate marker if it is visible, otherwise remove immediately
|
||||||
|
if (bounds.contains(marker.position)) {
|
||||||
|
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
|
||||||
|
val highlight = charger.id == vm.chargerSparse.value?.id
|
||||||
|
val fault = charger.faultReport != null
|
||||||
|
val multi = charger.isMulti(vm.filteredConnectors.value)
|
||||||
|
val fav =
|
||||||
|
charger.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList())
|
||||||
|
animator.animateMarkerDisappear(
|
||||||
|
marker, tint, highlight, fault, multi, fav,
|
||||||
|
vm.useMiniMarkers.value == true
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
animator.deleteMarker(marker)
|
||||||
|
}
|
||||||
|
markers.remove(marker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add new markers
|
||||||
|
val map1 = markers.values.map { it.id }
|
||||||
|
for (charger in chargers) {
|
||||||
|
if (!map1.contains(charger.id)) {
|
||||||
|
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
|
||||||
|
val highlight = charger.id == vm.chargerSparse.value?.id
|
||||||
|
val fault = charger.faultReport != null
|
||||||
|
val multi = charger.isMulti(vm.filteredConnectors.value)
|
||||||
|
val fav =
|
||||||
|
charger.id in (vm.favorites.value?.map { it.charger.id } ?: emptyList())
|
||||||
|
val marker = map.addMarker(
|
||||||
|
MarkerOptions()
|
||||||
|
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
|
||||||
|
.z(chargerZ)
|
||||||
|
.icon(
|
||||||
|
chargerIconGenerator.getBitmapDescriptor(
|
||||||
|
tint,
|
||||||
|
0f,
|
||||||
|
255,
|
||||||
|
highlight,
|
||||||
|
fault,
|
||||||
|
multi,
|
||||||
|
fav,
|
||||||
|
vm.useMiniMarkers.value == true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.anchor(0.5f, if (vm.useMiniMarkers.value == true) 0.5f else 1f)
|
||||||
|
)
|
||||||
|
animator.animateMarkerAppear(
|
||||||
|
marker, tint, highlight, fault, multi, fav,
|
||||||
|
vm.useMiniMarkers.value == true
|
||||||
|
)
|
||||||
|
markers[marker] = charger
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previousChargepointIds = chargepointIds
|
||||||
|
}
|
||||||
|
clusterMarkers = clusters.map { cluster ->
|
||||||
|
map.addMarker(
|
||||||
|
MarkerOptions()
|
||||||
|
.position(LatLng(cluster.coordinates.lat, cluster.coordinates.lng))
|
||||||
|
.z(clusterZ)
|
||||||
|
.icon(
|
||||||
|
map.bitmapDescriptorFactory.fromBitmap(
|
||||||
|
clusterIconGenerator.makeIcon(
|
||||||
|
cluster.clusterCount.toString()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.anchor(0.5f, 0.5f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.map, menu)
|
inflater.inflate(R.menu.map, menu)
|
||||||
|
|
||||||
@@ -1428,7 +1640,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
|||||||
map = null
|
map = null
|
||||||
mapFragment = null
|
mapFragment = null
|
||||||
_binding = null
|
_binding = null
|
||||||
markerManager = null
|
markers.clear()
|
||||||
|
clusterMarkers = emptyList()
|
||||||
|
searchResultMarker = null
|
||||||
|
searchResultIcon = null
|
||||||
/* if we don't dismiss the popup menu, it will be recreated in some cases
|
/* if we don't dismiss the popup menu, it will be recreated in some cases
|
||||||
(split-screen mode) and then have references to a destroyed fragment. */
|
(split-screen mode) and then have references to a destroyed fragment. */
|
||||||
popupMenu?.dismiss()
|
popupMenu?.dismiss()
|
||||||
|
|||||||
@@ -218,8 +218,6 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
|
|||||||
binding.rgDataSource.textView27,
|
binding.rgDataSource.textView27,
|
||||||
binding.rgDataSource.rbOpenChargeMap,
|
binding.rgDataSource.rbOpenChargeMap,
|
||||||
binding.rgDataSource.textView28,
|
binding.rgDataSource.textView28,
|
||||||
binding.rgDataSource.rbOpenStreetMap,
|
|
||||||
binding.rgDataSource.textView29,
|
|
||||||
binding.dataSourceHint,
|
binding.dataSourceHint,
|
||||||
binding.cbAcceptPrivacy
|
binding.cbAcceptPrivacy
|
||||||
)
|
)
|
||||||
@@ -248,8 +246,7 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
|
|||||||
|
|
||||||
for (rb in listOf(
|
for (rb in listOf(
|
||||||
binding.rgDataSource.rbGoingElectric,
|
binding.rgDataSource.rbGoingElectric,
|
||||||
binding.rgDataSource.rbOpenChargeMap,
|
binding.rgDataSource.rbOpenChargeMap
|
||||||
binding.rgDataSource.rbOpenStreetMap
|
|
||||||
)) {
|
)) {
|
||||||
rb.setOnCheckedChangeListener { _, _ ->
|
rb.setOnCheckedChangeListener { _, _ ->
|
||||||
if (binding.btnGetStarted.visibility == View.INVISIBLE) {
|
if (binding.btnGetStarted.visibility == View.INVISIBLE) {
|
||||||
@@ -264,7 +261,6 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
|
|||||||
when (prefs.dataSource) {
|
when (prefs.dataSource) {
|
||||||
"goingelectric" -> binding.rgDataSource.rbGoingElectric.isChecked = true
|
"goingelectric" -> binding.rgDataSource.rbGoingElectric.isChecked = true
|
||||||
"openchargemap" -> binding.rgDataSource.rbOpenChargeMap.isChecked = true
|
"openchargemap" -> binding.rgDataSource.rbOpenChargeMap.isChecked = true
|
||||||
"openstreetmap" -> binding.rgDataSource.rbOpenStreetMap.isChecked = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,8 +279,6 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
|
|||||||
"goingelectric"
|
"goingelectric"
|
||||||
} else if (binding.rgDataSource.rbOpenChargeMap.isChecked) {
|
} else if (binding.rgDataSource.rbOpenChargeMap.isChecked) {
|
||||||
"openchargemap"
|
"openchargemap"
|
||||||
} else if (binding.rgDataSource.rbOpenStreetMap.isChecked) {
|
|
||||||
"openstreetmap"
|
|
||||||
} else {
|
} else {
|
||||||
return@setOnClickListener
|
return@setOnClickListener
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,35 @@
|
|||||||
package net.vonforst.evmap.fragment.oauth
|
package net.vonforst.evmap.fragment.oauth
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Base64
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.webkit.CookieManager
|
import android.webkit.CookieManager
|
||||||
import android.webkit.WebResourceError
|
|
||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebResourceResponse
|
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.graphics.toColorInt
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.setFragmentResult
|
import androidx.fragment.app.setFragmentResult
|
||||||
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.ui.setupWithNavController
|
import androidx.navigation.ui.setupWithNavController
|
||||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import net.vonforst.evmap.BuildConfig
|
|
||||||
import net.vonforst.evmap.MapsActivity
|
import net.vonforst.evmap.MapsActivity
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
|
import java.lang.IllegalStateException
|
||||||
|
|
||||||
class OAuthLoginFragment : Fragment() {
|
class OAuthLoginFragment : Fragment() {
|
||||||
companion object {
|
companion object {
|
||||||
|
val ACTION_OAUTH_RESULT = "oauth_result"
|
||||||
val EXTRA_URL = "url"
|
val EXTRA_URL = "url"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,11 +72,11 @@ class OAuthLoginFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val args = OAuthLoginFragmentArgs.fromBundle(requireArguments())
|
val args = OAuthLoginFragmentArgs.fromBundle(requireArguments())
|
||||||
val uri = args.url.toUri()
|
val uri = Uri.parse(args.url)
|
||||||
|
|
||||||
webView = view.findViewById(R.id.webView)
|
webView = view.findViewById(R.id.webView)
|
||||||
|
|
||||||
args.color?.let { webView.setBackgroundColor(it.toColorInt()) }
|
args.color?.let { webView.setBackgroundColor(Color.parseColor(it)) }
|
||||||
val progress = view.findViewById<LinearProgressIndicator>(R.id.progress_indicator)
|
val progress = view.findViewById<LinearProgressIndicator>(R.id.progress_indicator)
|
||||||
|
|
||||||
CookieManager.getInstance().removeAllCookies(null)
|
CookieManager.getInstance().removeAllCookies(null)
|
||||||
@@ -88,8 +89,13 @@ class OAuthLoginFragment : Fragment() {
|
|||||||
|
|
||||||
if (url.toString().startsWith(args.resultUrlPrefix)) {
|
if (url.toString().startsWith(args.resultUrlPrefix)) {
|
||||||
val result = Bundle()
|
val result = Bundle()
|
||||||
result.putString(EXTRA_URL, url.toString())
|
result.putString("url", url.toString())
|
||||||
setFragmentResult(args.url, result)
|
setFragmentResult(args.url, result)
|
||||||
|
context?.let {
|
||||||
|
LocalBroadcastManager.getInstance(it).sendBroadcast(
|
||||||
|
Intent(ACTION_OAUTH_RESULT).putExtra(EXTRA_URL, url)
|
||||||
|
)
|
||||||
|
}
|
||||||
navController?.popBackStack()
|
navController?.popBackStack()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,9 +104,6 @@ class OAuthLoginFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
||||||
super.onPageStarted(view, url, favicon)
|
super.onPageStarted(view, url, favicon)
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
Log.w("WebViewClient", url)
|
|
||||||
}
|
|
||||||
progress.show()
|
progress.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,24 +112,6 @@ class OAuthLoginFragment : Fragment() {
|
|||||||
progress.hide()
|
progress.hide()
|
||||||
webView.background = null
|
webView.background = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceivedError(
|
|
||||||
view: WebView,
|
|
||||||
request: WebResourceRequest,
|
|
||||||
error: WebResourceError
|
|
||||||
) {
|
|
||||||
super.onReceivedError(view, request, error)
|
|
||||||
Log.w("WebViewClient", error.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReceivedHttpError(
|
|
||||||
view: WebView,
|
|
||||||
request: WebResourceRequest,
|
|
||||||
errorResponse: WebResourceResponse
|
|
||||||
) {
|
|
||||||
super.onReceivedHttpError(view, request, errorResponse)
|
|
||||||
Log.w("WebViewClient", "HTTP Error ${errorResponse.statusCode}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
webView.settings.javaScriptEnabled = true
|
webView.settings.javaScriptEnabled = true
|
||||||
webView.loadUrl(args.url)
|
webView.loadUrl(args.url)
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
package net.vonforst.evmap.fragment.preference
|
package net.vonforst.evmap.fragment.preference
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.ui.setupWithNavController
|
import androidx.navigation.ui.setupWithNavController
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
@@ -35,19 +30,6 @@ class AboutFragment : PreferenceFragmentCompat() {
|
|||||||
exitTransition = MaterialFadeThrough()
|
exitTransition = MaterialFadeThrough()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(listView) { v, insets ->
|
|
||||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
|
||||||
WindowInsetsCompat.CONSUMED
|
|
||||||
}
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,8 @@ package net.vonforst.evmap.fragment.preference
|
|||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.ui.setupWithNavController
|
import androidx.navigation.ui.setupWithNavController
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
@@ -40,19 +35,6 @@ abstract class BaseSettingsFragment : PreferenceFragmentCompat(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val view = super.onCreateView(inflater, container, savedInstanceState)
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(listView) { v, insets ->
|
|
||||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
|
||||||
WindowInsetsCompat.CONSUMED
|
|
||||||
}
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,12 @@ import net.vonforst.evmap.R
|
|||||||
import net.vonforst.evmap.addDebugInterceptors
|
import net.vonforst.evmap.addDebugInterceptors
|
||||||
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi
|
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi
|
||||||
import net.vonforst.evmap.api.availability.tesla.TeslaOwnerApi
|
import net.vonforst.evmap.api.availability.tesla.TeslaOwnerApi
|
||||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
|
|
||||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs
|
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragmentArgs
|
||||||
import net.vonforst.evmap.viewmodel.SettingsViewModel
|
import net.vonforst.evmap.viewmodel.SettingsViewModel
|
||||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import androidx.core.net.toUri
|
|
||||||
|
|
||||||
class DataSettingsFragment : BaseSettingsFragment() {
|
class DataSettingsFragment : BaseSettingsFragment() {
|
||||||
override val isTopLevel = false
|
override val isTopLevel = false
|
||||||
@@ -148,7 +146,7 @@ class DataSettingsFragment : BaseSettingsFragment() {
|
|||||||
val args = OAuthLoginFragmentArgs(
|
val args = OAuthLoginFragmentArgs(
|
||||||
uri.toString(),
|
uri.toString(),
|
||||||
TeslaAuthenticationApi.resultUrlPrefix,
|
TeslaAuthenticationApi.resultUrlPrefix,
|
||||||
"#FFFFFF"
|
"#000000"
|
||||||
).toBundle()
|
).toBundle()
|
||||||
|
|
||||||
setFragmentResultListener(uri.toString()) { _, result ->
|
setFragmentResultListener(uri.toString()) { _, result ->
|
||||||
@@ -161,7 +159,7 @@ class DataSettingsFragment : BaseSettingsFragment() {
|
|||||||
private fun teslaGetAccessToken(result: Bundle, codeVerifier: String) {
|
private fun teslaGetAccessToken(result: Bundle, codeVerifier: String) {
|
||||||
teslaAccountPreference.summary = getString(R.string.logging_in)
|
teslaAccountPreference.summary = getString(R.string.logging_in)
|
||||||
|
|
||||||
val url = result.getString(OAuthLoginFragment.EXTRA_URL)!!.toUri()
|
val url = Uri.parse(result.getString("url"))
|
||||||
val code = url.getQueryParameter("code") ?: return
|
val code = url.getQueryParameter("code") ?: return
|
||||||
val okhttp = OkHttpClient.Builder().addDebugInterceptors().build()
|
val okhttp = OkHttpClient.Builder().addDebugInterceptors().build()
|
||||||
val request = TeslaAuthenticationApi.AuthCodeRequest(code, codeVerifier)
|
val request = TeslaAuthenticationApi.AuthCodeRequest(code, codeVerifier)
|
||||||
|
|||||||
@@ -3,19 +3,15 @@ package net.vonforst.evmap.model
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.room.ColumnInfo
|
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import co.anbora.labs.spatia.geometry.Point
|
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
import net.vonforst.evmap.adapter.Equatable
|
import net.vonforst.evmap.adapter.Equatable
|
||||||
import net.vonforst.evmap.api.StringProvider
|
import net.vonforst.evmap.api.StringProvider
|
||||||
import net.vonforst.evmap.api.nameForPlugType
|
import net.vonforst.evmap.api.nameForPlugType
|
||||||
import net.vonforst.evmap.utils.SphericalMercatorProjection
|
|
||||||
import java.time.DayOfWeek
|
import java.time.DayOfWeek
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
@@ -136,8 +132,8 @@ data class ChargeLocation(
|
|||||||
.filter { it.type == variant.type && it.power == variant.power }
|
.filter { it.type == variant.type && it.power == variant.power }
|
||||||
val count = filtered.sumOf { it.count }
|
val count = filtered.sumOf { it.count }
|
||||||
Chargepoint(variant.type, variant.power, count,
|
Chargepoint(variant.type, variant.power, count,
|
||||||
filtered.map { it.current }.distinct().singleOrNull(),
|
filtered.map { it.voltage }.distinct().singleOrNull(),
|
||||||
filtered.map { it.voltage }.distinct().singleOrNull()
|
filtered.map { it.current }.distinct().singleOrNull()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,16 +143,9 @@ data class ChargeLocation(
|
|||||||
|
|
||||||
fun formatChargepoints(sp: StringProvider, locale: Locale): String {
|
fun formatChargepoints(sp: StringProvider, locale: Locale): String {
|
||||||
return chargepointsMerged.joinToString(" · ") {
|
return chargepointsMerged.joinToString(" · ") {
|
||||||
"${it.count} × ${nameForPlugType(sp, it.type)}${
|
"${it.count} × ${nameForPlugType(sp, it.type)} ${it.formatPower(locale)}"
|
||||||
it.formatPower(locale)?.let { " $it" } ?: ""
|
|
||||||
}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// used to store projected coordinates in DB
|
|
||||||
@IgnoredOnParcel
|
|
||||||
var coordinatesProjected: Point =
|
|
||||||
SphericalMercatorProjection.project(Point(coordinates.lng, coordinates.lat))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -365,8 +354,8 @@ abstract class ChargerPhoto(open val id: String) : Parcelable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ChargeLocationCluster(
|
data class ChargeLocationCluster(
|
||||||
@ColumnInfo("clusterCount") val clusterCount: Int,
|
val clusterCount: Int,
|
||||||
@ColumnInfo("coordinates") val coordinates: Coordinate,
|
val coordinates: Coordinate,
|
||||||
val items: List<ChargeLocation>? = null
|
val items: List<ChargeLocation>? = null
|
||||||
) : ChargepointListItem()
|
) : ChargepointListItem()
|
||||||
|
|
||||||
@@ -383,15 +372,14 @@ data class Address(
|
|||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
// TODO: the order here follows a German-style format (i.e. street, postcode city).
|
// TODO: the order here follows a German-style format (i.e. street, postcode city).
|
||||||
// in principle this should be country-dependent (e.g. UK has postcode after city)
|
// in principle this should be country-dependent (e.g. UK has postcode after city)
|
||||||
// maybe use https://github.com/bettermile/address-formatter-kotlin
|
|
||||||
return buildString {
|
return buildString {
|
||||||
street?.let {
|
street?.let {
|
||||||
append(it)
|
append(it)
|
||||||
if (postcode != null || city != null) append(", ")
|
append(", ")
|
||||||
}
|
}
|
||||||
postcode?.let {
|
postcode?.let {
|
||||||
append(it)
|
append(it)
|
||||||
if (city != null) append(" ")
|
append(" ")
|
||||||
}
|
}
|
||||||
city?.let {
|
city?.let {
|
||||||
append(it)
|
append(it)
|
||||||
@@ -447,8 +435,7 @@ data class Chargepoint(
|
|||||||
const val TYPE_2_UNKNOWN = "Type 2 (either plug or socket)"
|
const val TYPE_2_UNKNOWN = "Type 2 (either plug or socket)"
|
||||||
const val TYPE_2_SOCKET = "Type 2 socket"
|
const val TYPE_2_SOCKET = "Type 2 socket"
|
||||||
const val TYPE_2_PLUG = "Type 2 plug"
|
const val TYPE_2_PLUG = "Type 2 plug"
|
||||||
const val TYPE_3A = "Type 3A"
|
const val TYPE_3 = "Type 3"
|
||||||
const val TYPE_3C = "Type 3C"
|
|
||||||
const val CCS_TYPE_2 = "CCS Type 2"
|
const val CCS_TYPE_2 = "CCS Type 2"
|
||||||
const val CCS_TYPE_1 = "CCS Type 1"
|
const val CCS_TYPE_1 = "CCS Type 1"
|
||||||
const val CCS_UNKNOWN = "CCS (either Type 1 or Type 2)"
|
const val CCS_UNKNOWN = "CCS (either Type 1 or Type 2)"
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ package net.vonforst.evmap.storage
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MediatorLiveData
|
import androidx.lifecycle.MediatorLiveData
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.emitAll
|
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
import net.vonforst.evmap.model.ChargeLocation
|
||||||
import net.vonforst.evmap.viewmodel.Resource
|
import net.vonforst.evmap.viewmodel.Resource
|
||||||
import net.vonforst.evmap.viewmodel.Status
|
import net.vonforst.evmap.viewmodel.Status
|
||||||
@@ -21,15 +16,14 @@ import java.time.Instant
|
|||||||
* successful.
|
* successful.
|
||||||
*/
|
*/
|
||||||
class CacheLiveData<T>(
|
class CacheLiveData<T>(
|
||||||
cache: LiveData<Resource<T>>,
|
cache: LiveData<T>,
|
||||||
api: LiveData<Resource<T>>,
|
api: LiveData<Resource<T>>,
|
||||||
skipApi: LiveData<Boolean>? = null
|
skipApi: LiveData<Boolean>? = null
|
||||||
) :
|
) :
|
||||||
MediatorLiveData<Resource<T>>() {
|
MediatorLiveData<Resource<T>>() {
|
||||||
private var cacheResult: Resource<T>? = null
|
private var cacheResult: T? = null
|
||||||
private var apiResult: Resource<T>? = null
|
private var apiResult: Resource<T>? = null
|
||||||
private var skipApiResult: Boolean = false
|
private var skipApiResult: Boolean = false
|
||||||
private val apiLiveData = api
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
updateValue()
|
updateValue()
|
||||||
@@ -70,21 +64,9 @@ class CacheLiveData<T>(
|
|||||||
Log.d("CacheLiveData", "cache has finished loading before API")
|
Log.d("CacheLiveData", "cache has finished loading before API")
|
||||||
// cache has finished loading before API
|
// cache has finished loading before API
|
||||||
if (skipApiResult) {
|
if (skipApiResult) {
|
||||||
value = when (cache.status) {
|
value = Resource.success(cache)
|
||||||
Status.SUCCESS -> cache
|
|
||||||
Status.ERROR -> {
|
|
||||||
Log.d("CacheLiveData", "Cache returned an error, querying API")
|
|
||||||
addSource(apiLiveData) {
|
|
||||||
apiResult = it
|
|
||||||
updateValue()
|
|
||||||
}
|
|
||||||
Resource.loading(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
Status.LOADING -> cache
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
value = Resource.loading(cache.data)
|
value = Resource.loading(cache)
|
||||||
}
|
}
|
||||||
} else if (cache == null && api != null) {
|
} else if (cache == null && api != null) {
|
||||||
Log.d("CacheLiveData", "API has finished loading before cache")
|
Log.d("CacheLiveData", "API has finished loading before cache")
|
||||||
@@ -99,7 +81,7 @@ class CacheLiveData<T>(
|
|||||||
// Both cache and API have finished loading
|
// Both cache and API have finished loading
|
||||||
value = when (api.status) {
|
value = when (api.status) {
|
||||||
Status.SUCCESS -> api
|
Status.SUCCESS -> api
|
||||||
Status.ERROR -> Resource.error(api.message, cache.data)
|
Status.ERROR -> Resource.error(api.message, cache)
|
||||||
Status.LOADING -> api // should not occur
|
Status.LOADING -> api // should not occur
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,7 +95,7 @@ class CacheLiveData<T>(
|
|||||||
* reload from the API.
|
* reload from the API.
|
||||||
*/
|
*/
|
||||||
class PreferCacheLiveData(
|
class PreferCacheLiveData(
|
||||||
cache: LiveData<ChargeLocation?>,
|
cache: LiveData<ChargeLocation>,
|
||||||
val api: LiveData<Resource<ChargeLocation>>,
|
val api: LiveData<Resource<ChargeLocation>>,
|
||||||
cacheSoftLimit: Duration
|
cacheSoftLimit: Duration
|
||||||
) :
|
) :
|
||||||
@@ -146,44 +128,4 @@ class PreferCacheLiveData(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flow-based implementation that allows loading data both from a cache and an API.
|
|
||||||
*
|
|
||||||
* It first tries loading from cache, and if the result is newer than `cacheSoftLimit` it does not
|
|
||||||
* reload from the API.
|
|
||||||
*/
|
|
||||||
fun preferCacheFlow(
|
|
||||||
cache: Flow<ChargeLocation?>,
|
|
||||||
api: Flow<Resource<ChargeLocation>>,
|
|
||||||
cacheSoftLimit: Duration
|
|
||||||
): Flow<Resource<ChargeLocation>> = flow {
|
|
||||||
emit(Resource.loading(null)) // initial state
|
|
||||||
|
|
||||||
val cacheRes = cache.firstOrNull() // read cache once
|
|
||||||
if (cacheRes != null) {
|
|
||||||
if (cacheRes.isDetailed && cacheRes.timeRetrieved > Instant.now() - cacheSoftLimit) {
|
|
||||||
emit(Resource.success(cacheRes))
|
|
||||||
return@flow
|
|
||||||
} else {
|
|
||||||
emit(Resource.loading(cacheRes))
|
|
||||||
emitAll(api.map { apiRes ->
|
|
||||||
when (apiRes.status) {
|
|
||||||
Status.SUCCESS -> apiRes
|
|
||||||
Status.ERROR -> Resource.error(apiRes.message, cacheRes)
|
|
||||||
Status.LOADING -> Resource.loading(cacheRes)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No cache → straight to API
|
|
||||||
emitAll(api.map { apiRes ->
|
|
||||||
when (apiRes.status) {
|
|
||||||
Status.SUCCESS -> apiRes
|
|
||||||
Status.ERROR -> Resource.error(apiRes.message, null)
|
|
||||||
Status.LOADING -> Resource.loading(null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,51 +1,34 @@
|
|||||||
package net.vonforst.evmap.storage
|
package net.vonforst.evmap.storage
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||||
import androidx.sqlite.db.SupportSQLiteQuery
|
import androidx.sqlite.db.SupportSQLiteQuery
|
||||||
import co.anbora.labs.spatia.geometry.Mbr
|
import co.anbora.labs.spatia.geometry.Mbr
|
||||||
import co.anbora.labs.spatia.geometry.Point
|
|
||||||
import co.anbora.labs.spatia.geometry.Polygon
|
import co.anbora.labs.spatia.geometry.Polygon
|
||||||
import com.car2go.maps.model.LatLng
|
import com.car2go.maps.model.LatLng
|
||||||
import com.car2go.maps.model.LatLngBounds
|
import com.car2go.maps.model.LatLngBounds
|
||||||
|
import com.car2go.maps.util.SphericalUtil
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.shareIn
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import net.vonforst.evmap.api.ChargepointApi
|
import net.vonforst.evmap.api.ChargepointApi
|
||||||
import net.vonforst.evmap.api.ChargepointList
|
import net.vonforst.evmap.api.ChargepointList
|
||||||
import net.vonforst.evmap.api.FiltersSQLQuery
|
|
||||||
import net.vonforst.evmap.api.StringProvider
|
import net.vonforst.evmap.api.StringProvider
|
||||||
import net.vonforst.evmap.api.goingelectric.GEReferenceData
|
import net.vonforst.evmap.api.goingelectric.GEReferenceData
|
||||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||||
import net.vonforst.evmap.api.openstreetmap.OSMReferenceData
|
|
||||||
import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper
|
|
||||||
import net.vonforst.evmap.model.*
|
import net.vonforst.evmap.model.*
|
||||||
import net.vonforst.evmap.ui.cluster
|
import net.vonforst.evmap.ui.cluster
|
||||||
import net.vonforst.evmap.ui.getClusterPrecision
|
|
||||||
import net.vonforst.evmap.utils.crossesAntimeridian
|
import net.vonforst.evmap.utils.crossesAntimeridian
|
||||||
import net.vonforst.evmap.utils.splitAtAntimeridian
|
import net.vonforst.evmap.utils.splitAtAntimeridian
|
||||||
import net.vonforst.evmap.viewmodel.Resource
|
import net.vonforst.evmap.viewmodel.Resource
|
||||||
import net.vonforst.evmap.viewmodel.Status
|
import net.vonforst.evmap.viewmodel.Status
|
||||||
import net.vonforst.evmap.viewmodel.await
|
import net.vonforst.evmap.viewmodel.await
|
||||||
|
import net.vonforst.evmap.viewmodel.getClusterDistance
|
||||||
|
import net.vonforst.evmap.viewmodel.singleSwitchMap
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import kotlin.time.TimeSource
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
const val CLUSTER_MAX_ZOOM_LEVEL = 11f
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class ChargeLocationsDao {
|
abstract class ChargeLocationsDao {
|
||||||
@@ -76,110 +59,53 @@ abstract class ChargeLocationsDao {
|
|||||||
abstract suspend fun deleteAllIfNotFavorite()
|
abstract suspend fun deleteAllIfNotFavorite()
|
||||||
|
|
||||||
@Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND id == :id AND isDetailed == 1 AND timeRetrieved > :after")
|
@Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND id == :id AND isDetailed == 1 AND timeRetrieved > :after")
|
||||||
abstract suspend fun getChargeLocationById(
|
abstract fun getChargeLocationById(
|
||||||
id: Long,
|
id: Long,
|
||||||
dataSource: String,
|
dataSource: String,
|
||||||
after: Long
|
after: Long
|
||||||
): ChargeLocation?
|
): LiveData<ChargeLocation>
|
||||||
|
|
||||||
@Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND id IN (:ids) AND timeRetrieved > :after")
|
|
||||||
abstract suspend fun getChargeLocationsById(
|
|
||||||
ids: List<Long>,
|
|
||||||
dataSource: String,
|
|
||||||
after: Long
|
|
||||||
): List<ChargeLocation>
|
|
||||||
|
|
||||||
@SkipQueryVerification
|
@SkipQueryVerification
|
||||||
@Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND timeRetrieved > :after AND ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND search_frame = BuildMbr(:lng1, :lat1, :lng2, :lat2))")
|
@Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND Within(coordinates, BuildMbr(:lng1, :lat1, :lng2, :lat2)) AND timeRetrieved > :after")
|
||||||
abstract suspend fun getChargeLocationsInBounds(
|
abstract fun getChargeLocationsInBounds(
|
||||||
lat1: Double,
|
lat1: Double,
|
||||||
lat2: Double,
|
lat2: Double,
|
||||||
lng1: Double,
|
lng1: Double,
|
||||||
lng2: Double,
|
lng2: Double,
|
||||||
dataSource: String,
|
dataSource: String,
|
||||||
after: Long
|
after: Long
|
||||||
): List<ChargeLocation>
|
): LiveData<List<ChargeLocation>>
|
||||||
|
|
||||||
@SkipQueryVerification
|
@SkipQueryVerification
|
||||||
@Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND PtDistWithin(coordinates, MakePoint(:lng, :lat, 4326), :radius) AND timeRetrieved > :after AND ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND search_frame = BuildCircleMbr(:lng, :lat, :radius)) ORDER BY Distance(coordinates, MakePoint(:lng, :lat, 4326))")
|
@Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND PtDistWithin(coordinates, MakePoint(:lng, :lat, 4326), :radius) AND timeRetrieved > :after ORDER BY Distance(coordinates, MakePoint(:lng, :lat, 4326))")
|
||||||
abstract suspend fun getChargeLocationsRadius(
|
abstract fun getChargeLocationsRadius(
|
||||||
lat: Double,
|
lat: Double,
|
||||||
lng: Double,
|
lng: Double,
|
||||||
radius: Double,
|
radius: Double,
|
||||||
dataSource: String,
|
dataSource: String,
|
||||||
after: Long
|
after: Long
|
||||||
): List<ChargeLocation>
|
): LiveData<List<ChargeLocation>>
|
||||||
|
|
||||||
@RawQuery(observedEntities = [ChargeLocation::class])
|
@RawQuery(observedEntities = [ChargeLocation::class])
|
||||||
abstract suspend fun getChargeLocationsCustom(query: SupportSQLiteQuery): List<ChargeLocation>
|
abstract fun getChargeLocationsCustom(query: SupportSQLiteQuery): LiveData<List<ChargeLocation>>
|
||||||
|
|
||||||
@SkipQueryVerification
|
|
||||||
@Query("SELECT SUM(1) AS clusterCount, Transform(MakePoint(AVG(X(coordinatesProjected)), AVG(Y(coordinatesProjected)), 3857), 4326) as center, SnapToGrid(coordinatesProjected, :precision) AS snapped, GROUP_CONCAT(id, ',') as ids FROM chargelocation WHERE dataSource == :dataSource AND timeRetrieved > :after AND ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND f_geometry_column = 'coordinates' AND search_frame = BuildMbr(:lng1, :lat1, :lng2, :lat2)) GROUP BY snapped")
|
|
||||||
abstract suspend fun getChargeLocationClusters(
|
|
||||||
lat1: Double,
|
|
||||||
lat2: Double,
|
|
||||||
lng1: Double,
|
|
||||||
lng2: Double,
|
|
||||||
dataSource: String,
|
|
||||||
after: Long,
|
|
||||||
precision: Double
|
|
||||||
): List<DBChargeLocationCluster>
|
|
||||||
|
|
||||||
@RawQuery(observedEntities = [ChargeLocation::class])
|
|
||||||
abstract suspend fun getChargeLocationClustersCustom(query: SupportSQLiteQuery): List<DBChargeLocationCluster>
|
|
||||||
|
|
||||||
suspend fun getChargeLocationsClustered(
|
|
||||||
lat1: Double,
|
|
||||||
lat2: Double,
|
|
||||||
lng1: Double,
|
|
||||||
lng2: Double,
|
|
||||||
dataSource: String,
|
|
||||||
after: Long,
|
|
||||||
zoom: Float
|
|
||||||
): List<ChargepointListItem> {
|
|
||||||
if (zoom > CLUSTER_MAX_ZOOM_LEVEL) {
|
|
||||||
return getChargeLocationsInBounds(lat1, lat2, lng1, lng2, dataSource, after)
|
|
||||||
}
|
|
||||||
|
|
||||||
val precision = getClusterPrecision(zoom)
|
|
||||||
val clusters =
|
|
||||||
getChargeLocationClusters(lat1, lat2, lng1, lng2, dataSource, after, precision)
|
|
||||||
val singleChargers =
|
|
||||||
getChargeLocationsById(clusters.filter { it.clusterCount == 1 }.map { it.ids }
|
|
||||||
.flatten(), dataSource, after)
|
|
||||||
return clusters.filter { it.clusterCount > 1 }.map { it.convert() } + singleChargers
|
|
||||||
}
|
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM chargelocation")
|
@Query("SELECT COUNT(*) FROM chargelocation")
|
||||||
abstract fun getCount(): LiveData<Long>
|
abstract fun getCount(): LiveData<Long>
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM chargelocation")
|
|
||||||
abstract suspend fun getCountAsync(): Long
|
|
||||||
|
|
||||||
@SkipQueryVerification
|
@SkipQueryVerification
|
||||||
@Query("SELECT SUM(pgsize) FROM dbstat WHERE name == \"ChargeLocation\"")
|
@Query("SELECT SUM(pgsize) FROM dbstat WHERE name == \"ChargeLocation\"")
|
||||||
abstract suspend fun getSize(): Long
|
abstract suspend fun getSize(): Long
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DBChargeLocationCluster(
|
|
||||||
@ColumnInfo("clusterCount") val clusterCount: Int,
|
|
||||||
@ColumnInfo("center") val center: Coordinate,
|
|
||||||
@ColumnInfo("snapped") val snapped: Point,
|
|
||||||
@ColumnInfo("ids") val ids: List<Long>
|
|
||||||
) {
|
|
||||||
fun convert() = ChargeLocationCluster(clusterCount, center, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val TAG = "ChargeLocationsDao"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ChargeLocationsRepository wraps the ChargepointApi and the DB to provide caching
|
* The ChargeLocationsRepository wraps the ChargepointApi and the DB to provide caching
|
||||||
* and clustering functionality.
|
* and clustering functionality.
|
||||||
*/
|
*/
|
||||||
class ChargeLocationsRepository(
|
class ChargeLocationsRepository(
|
||||||
private val api: ChargepointApi<ReferenceData>, private val scope: CoroutineScope,
|
api: ChargepointApi<ReferenceData>, private val scope: CoroutineScope,
|
||||||
private val db: AppDatabase, private val prefs: PreferenceDataSource
|
private val db: AppDatabase, private val prefs: PreferenceDataSource
|
||||||
) {
|
) {
|
||||||
|
val api = MutableLiveData<ChargepointApi<ReferenceData>>().apply { value = api }
|
||||||
|
|
||||||
// if zoom level is below this value, server-side clustering will be used (if the API provides it)
|
// if zoom level is below this value, server-side clustering will be used (if the API provides it)
|
||||||
private val serverSideClusteringThreshold = 9f
|
private val serverSideClusteringThreshold = 9f
|
||||||
@@ -188,73 +114,61 @@ class ChargeLocationsRepository(
|
|||||||
// if cached data is available and more recent than this duration, API will not be queried
|
// if cached data is available and more recent than this duration, API will not be queried
|
||||||
private val cacheSoftLimit = Duration.ofDays(1)
|
private val cacheSoftLimit = Duration.ofDays(1)
|
||||||
|
|
||||||
val referenceData = when (api) {
|
val referenceData = this.api.switchMap { api ->
|
||||||
is GoingElectricApiWrapper -> {
|
when (api) {
|
||||||
GEReferenceDataRepository(
|
is GoingElectricApiWrapper -> {
|
||||||
api,
|
GEReferenceDataRepository(
|
||||||
scope,
|
api,
|
||||||
db.geReferenceDataDao(),
|
scope,
|
||||||
prefs
|
db.geReferenceDataDao(),
|
||||||
).getReferenceData()
|
prefs
|
||||||
|
).getReferenceData()
|
||||||
|
}
|
||||||
|
is OpenChargeMapApiWrapper -> {
|
||||||
|
OCMReferenceDataRepository(
|
||||||
|
api,
|
||||||
|
scope,
|
||||||
|
db.ocmReferenceDataDao(),
|
||||||
|
prefs
|
||||||
|
).getReferenceData()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw RuntimeException("no reference data implemented")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
is OpenChargeMapApiWrapper -> {
|
|
||||||
OCMReferenceDataRepository(
|
|
||||||
api,
|
|
||||||
scope,
|
|
||||||
db.ocmReferenceDataDao(),
|
|
||||||
prefs
|
|
||||||
).getReferenceData()
|
|
||||||
}
|
|
||||||
|
|
||||||
is OpenStreetMapApiWrapper -> {
|
|
||||||
OSMReferenceDataRepository(db.osmReferenceDataDao()).getReferenceData()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
throw RuntimeException("no reference data implemented")
|
|
||||||
}
|
|
||||||
}.shareIn(scope, SharingStarted.Lazily, 1)
|
|
||||||
|
|
||||||
private val chargeLocationsDao = db.chargeLocationsDao()
|
private val chargeLocationsDao = db.chargeLocationsDao()
|
||||||
private val savedRegionDao = db.savedRegionDao()
|
private val savedRegionDao = db.savedRegionDao()
|
||||||
private var fullDownloadJob: Job? = null
|
|
||||||
private var fullDownloadProgress: MutableStateFlow<Float?> = MutableStateFlow(null)
|
|
||||||
|
|
||||||
fun getChargepoints(
|
fun getChargepoints(
|
||||||
bounds: LatLngBounds,
|
bounds: LatLngBounds,
|
||||||
zoom: Float,
|
zoom: Float,
|
||||||
filters: FilterValues?,
|
filters: FilterValues?,
|
||||||
overrideCache: Boolean = false,
|
overrideCache: Boolean = false
|
||||||
): Flow<List<ChargepointListItem>> {
|
): LiveData<Resource<ChargepointList>> {
|
||||||
if (bounds.crossesAntimeridian()) {
|
if (bounds.crossesAntimeridian()) {
|
||||||
val (a, b) = bounds.splitAtAntimeridian()
|
val (a, b) = bounds.splitAtAntimeridian()
|
||||||
val flowA = getChargepoints(a, zoom, filters, overrideCache)
|
val liveDataA = getChargepoints(a, zoom, filters, overrideCache)
|
||||||
val flowB = getChargepoints(b, zoom, filters, overrideCache)
|
val liveDataB = getChargepoints(b, zoom, filters, overrideCache)
|
||||||
return flowA.combine(flowB) { a, b -> a + b }
|
return combineLiveData(liveDataA, liveDataB)
|
||||||
}
|
|
||||||
|
|
||||||
val dbResult = flow {
|
|
||||||
val t1 = TimeSource.Monotonic.markNow()
|
|
||||||
val result = if (filters.isNullOrEmpty()) {
|
|
||||||
chargeLocationsDao.getChargeLocationsClustered(
|
|
||||||
bounds.southwest.latitude,
|
|
||||||
bounds.northeast.latitude,
|
|
||||||
bounds.southwest.longitude,
|
|
||||||
bounds.northeast.longitude,
|
|
||||||
api.id,
|
|
||||||
cacheLimitDate(api),
|
|
||||||
zoom
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
queryWithFiltersClustered(api, filters, bounds, zoom)
|
|
||||||
}
|
|
||||||
val t2 = TimeSource.Monotonic.markNow()
|
|
||||||
Log.d(TAG, "DB loading time: ${t2 - t1}")
|
|
||||||
Log.d(TAG, "number of chargers: ${result.size}")
|
|
||||||
emit(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val api = api.value!!
|
||||||
|
|
||||||
|
val dbResult = if (filters == null) {
|
||||||
|
chargeLocationsDao.getChargeLocationsInBounds(
|
||||||
|
bounds.southwest.latitude,
|
||||||
|
bounds.northeast.latitude,
|
||||||
|
bounds.southwest.longitude,
|
||||||
|
bounds.northeast.longitude,
|
||||||
|
api.id,
|
||||||
|
cacheLimitDate(api)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
queryWithFilters(api, filters, bounds)
|
||||||
|
}.map { ChargepointList(applyLocalClustering(it, zoom), true) }
|
||||||
val filtersSerialized =
|
val filtersSerialized =
|
||||||
filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() }
|
filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() }
|
||||||
?.serialize()
|
?.serialize()
|
||||||
@@ -270,59 +184,65 @@ class ChargeLocationsRepository(
|
|||||||
requiresDetail
|
requiresDetail
|
||||||
)
|
)
|
||||||
val useClustering = shouldUseServerSideClustering(zoom)
|
val useClustering = shouldUseServerSideClustering(zoom)
|
||||||
if (api.supportsOnlineQueries) {
|
val apiResult = liveData {
|
||||||
val apiResult = flow {
|
val refData = referenceData.await()
|
||||||
val refData = referenceData.first()
|
val time = Instant.now()
|
||||||
val time = Instant.now()
|
val result = api.getChargepoints(refData, bounds, zoom, useClustering, filters)
|
||||||
val result = api.getChargepoints(refData, bounds, zoom, useClustering, filters)
|
emit(applyLocalClustering(result, zoom))
|
||||||
emit(applyLocalClustering(result, zoom))
|
if (result.status == Status.SUCCESS) {
|
||||||
if (result.status == Status.SUCCESS) {
|
val chargers = result.data!!.items.filterIsInstance<ChargeLocation>()
|
||||||
val chargers = result.data!!.items.filterIsInstance<ChargeLocation>()
|
chargeLocationsDao.insertOrReplaceIfNoDetailedExists(
|
||||||
chargeLocationsDao.insertOrReplaceIfNoDetailedExists(
|
cacheLimitDate(api), *chargers.toTypedArray()
|
||||||
cacheLimitDate(api), *chargers.toTypedArray()
|
)
|
||||||
)
|
if (chargers.size == result.data.items.size && result.data.isComplete) {
|
||||||
if (chargers.size == result.data.items.size && result.data.isComplete) {
|
val region = Mbr(
|
||||||
val region = Mbr(
|
bounds.southwest.longitude,
|
||||||
bounds.southwest.longitude,
|
bounds.southwest.latitude,
|
||||||
bounds.southwest.latitude,
|
bounds.northeast.longitude,
|
||||||
bounds.northeast.longitude,
|
bounds.northeast.latitude, 4326
|
||||||
bounds.northeast.latitude, 4326
|
).asPolygon()
|
||||||
).asPolygon()
|
savedRegionDao.insert(
|
||||||
savedRegionDao.insert(
|
SavedRegion(
|
||||||
SavedRegion(
|
region, api.id, time,
|
||||||
region, api.id, time,
|
filtersSerialized,
|
||||||
filtersSerialized,
|
false
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if (overrideCache) {
|
}
|
||||||
apiResult
|
return if (overrideCache) {
|
||||||
} else {
|
apiResult
|
||||||
CacheLiveData(dbResult, apiResult, savedRegionResult).distinctUntilChanged()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return liveData {
|
CacheLiveData(dbResult, apiResult, savedRegionResult).distinctUntilChanged()
|
||||||
if (fullDownloadJob != null) {
|
}
|
||||||
fullDownloadProgress.value?.let { emit(Resource.loading(null, it)) }
|
}
|
||||||
|
|
||||||
|
private fun combineLiveData(
|
||||||
|
liveDataA: LiveData<Resource<ChargepointList>>,
|
||||||
|
liveDataB: LiveData<Resource<ChargepointList>>
|
||||||
|
) = MediatorLiveData<Resource<ChargepointList>>().apply {
|
||||||
|
listOf(liveDataA, liveDataB).forEach {
|
||||||
|
addSource(it) {
|
||||||
|
val valA = liveDataA.value
|
||||||
|
val valB = liveDataB.value
|
||||||
|
val combinedList = if (valA?.data != null && valB?.data != null) {
|
||||||
|
ChargepointList(
|
||||||
|
valA.data.items + valB.data.items,
|
||||||
|
valA.data.isComplete && valB.data.isComplete
|
||||||
|
)
|
||||||
|
} else if (valA?.data != null) {
|
||||||
|
ChargepointList(valA.data.items, false)
|
||||||
|
} else if (valB?.data != null) {
|
||||||
|
ChargepointList(valB.data.items, false)
|
||||||
|
} else null
|
||||||
|
if (valA?.status == Status.SUCCESS && valB?.status == Status.SUCCESS) {
|
||||||
|
Resource.success(combinedList)
|
||||||
|
} else if (valA?.status == Status.ERROR || valB?.status == Status.ERROR) {
|
||||||
|
Resource.error(valA?.message ?: valB?.message, combinedList)
|
||||||
|
} else {
|
||||||
|
Resource.loading(combinedList)
|
||||||
}
|
}
|
||||||
if (!savedRegionResult.await()) {
|
|
||||||
val job = fullDownloadJob ?: scope.launch {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
fullDownload()
|
|
||||||
}
|
|
||||||
}.also { fullDownloadJob = it }
|
|
||||||
val progressJob = scope.launch {
|
|
||||||
fullDownloadProgress.collect {
|
|
||||||
emit(Resource.loading(null, it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
job.join()
|
|
||||||
progressJob.cancelAndJoin()
|
|
||||||
}
|
|
||||||
emit(dbResult.await())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,26 +250,23 @@ class ChargeLocationsRepository(
|
|||||||
fun getChargepointsRadius(
|
fun getChargepointsRadius(
|
||||||
location: LatLng,
|
location: LatLng,
|
||||||
radius: Int,
|
radius: Int,
|
||||||
|
zoom: Float,
|
||||||
filters: FilterValues?
|
filters: FilterValues?
|
||||||
): LiveData<Resource<List<ChargeLocation>>> {
|
): LiveData<Resource<ChargepointList>> {
|
||||||
|
val api = api.value!!
|
||||||
|
|
||||||
val radiusMeters = radius.toDouble() * 1000
|
val radiusMeters = radius.toDouble() * 1000
|
||||||
val dbResult = if (filters.isNullOrEmpty()) {
|
val dbResult = if (filters == null) {
|
||||||
liveData {
|
chargeLocationsDao.getChargeLocationsRadius(
|
||||||
emit(
|
location.latitude,
|
||||||
Resource.success(
|
location.longitude,
|
||||||
chargeLocationsDao.getChargeLocationsRadius(
|
radiusMeters,
|
||||||
location.latitude,
|
api.id,
|
||||||
location.longitude,
|
cacheLimitDate(api)
|
||||||
radiusMeters,
|
)
|
||||||
api.id,
|
|
||||||
cacheLimitDate(api)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
queryWithFilters(api, filters, location, radiusMeters)
|
queryWithFilters(api, filters, location, radiusMeters)
|
||||||
}
|
}.map { ChargepointList(applyLocalClustering(it, zoom), true) }
|
||||||
val filtersSerialized =
|
val filtersSerialized =
|
||||||
filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() }
|
filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() }
|
||||||
?.serialize()
|
?.serialize()
|
||||||
@@ -363,98 +280,69 @@ class ChargeLocationsRepository(
|
|||||||
filtersSerialized,
|
filtersSerialized,
|
||||||
requiresDetail
|
requiresDetail
|
||||||
)
|
)
|
||||||
if (api.supportsOnlineQueries) {
|
val useClustering = shouldUseServerSideClustering(zoom)
|
||||||
val apiResult = liveData {
|
val apiResult = liveData {
|
||||||
val refData = referenceData.first()
|
val refData = referenceData.await()
|
||||||
val time = Instant.now()
|
val time = Instant.now()
|
||||||
val result =
|
val result =
|
||||||
api.getChargepointsRadius(
|
api.getChargepointsRadius(refData, location, radius, zoom, useClustering, filters)
|
||||||
refData,
|
emit(applyLocalClustering(result, zoom))
|
||||||
location,
|
if (result.status == Status.SUCCESS) {
|
||||||
radius,
|
val chargers = result.data!!.items.filterIsInstance<ChargeLocation>()
|
||||||
16f,
|
chargeLocationsDao.insertOrReplaceIfNoDetailedExists(
|
||||||
false,
|
cacheLimitDate(api), *chargers.toTypedArray()
|
||||||
filters
|
|
||||||
)
|
|
||||||
emit(
|
|
||||||
Resource(
|
|
||||||
result.status,
|
|
||||||
result.data?.items?.filterIsInstance<ChargeLocation>(),
|
|
||||||
result.message
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if (result.status == Status.SUCCESS) {
|
if (chargers.size == result.data.items.size && result.data.isComplete) {
|
||||||
val chargers = result.data!!.items.filterIsInstance<ChargeLocation>()
|
val region = Polygon(
|
||||||
chargeLocationsDao.insertOrReplaceIfNoDetailedExists(
|
savedRegionDao.makeCircle(
|
||||||
cacheLimitDate(api), *chargers.toTypedArray()
|
location.latitude,
|
||||||
|
location.longitude,
|
||||||
|
radiusMeters
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if (chargers.size == result.data.items.size && result.data.isComplete) {
|
savedRegionDao.insert(
|
||||||
val region = Polygon(
|
SavedRegion(
|
||||||
savedRegionDao.makeCircle(
|
region, api.id, time,
|
||||||
location.latitude,
|
filtersSerialized,
|
||||||
location.longitude,
|
false
|
||||||
radiusMeters
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
savedRegionDao.insert(
|
)
|
||||||
SavedRegion(
|
|
||||||
region, api.id, time,
|
|
||||||
filtersSerialized,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return CacheLiveData(dbResult, apiResult, savedRegionResult).distinctUntilChanged()
|
|
||||||
} else {
|
|
||||||
return liveData {
|
|
||||||
if (!savedRegionResult.await()) {
|
|
||||||
val job = fullDownloadJob ?: scope.launch {
|
|
||||||
fullDownload()
|
|
||||||
}.also { fullDownloadJob = it }
|
|
||||||
val progressJob = scope.launch {
|
|
||||||
fullDownloadProgress.collect {
|
|
||||||
emit(Resource.loading(null, it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
job.join()
|
|
||||||
progressJob.cancelAndJoin()
|
|
||||||
}
|
|
||||||
emit(dbResult.await())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return CacheLiveData(dbResult, apiResult, savedRegionResult).distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun applyLocalClustering(
|
private fun applyLocalClustering(
|
||||||
result: Resource<ChargepointList>,
|
result: Resource<ChargepointList>,
|
||||||
zoom: Float
|
zoom: Float
|
||||||
): Resource<List<ChargepointListItem>> {
|
): Resource<ChargepointList> {
|
||||||
val list = result.data ?: return Resource(result.status, null, result.message)
|
val list = result.data ?: return Resource(result.status, null, result.message)
|
||||||
val chargers = list.items.filterIsInstance<ChargeLocation>()
|
val chargers = list.items.filterIsInstance<ChargeLocation>()
|
||||||
|
|
||||||
if (chargers.size != list.items.size) return Resource(
|
if (chargers.size != list.items.size) return Resource(
|
||||||
result.status,
|
result.status,
|
||||||
list.items,
|
list,
|
||||||
result.message
|
result.message
|
||||||
) // list already contains clusters
|
) // list already contains clusters
|
||||||
|
|
||||||
val clustered = applyLocalClustering(chargers, zoom)
|
val clustered = applyLocalClustering(chargers, zoom)
|
||||||
return Resource(result.status, clustered, result.message)
|
return Resource(result.status, ChargepointList(clustered, list.isComplete), result.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun applyLocalClustering(
|
private fun applyLocalClustering(
|
||||||
chargers: List<ChargeLocation>,
|
chargers: List<ChargeLocation>,
|
||||||
zoom: Float
|
zoom: Float
|
||||||
): List<ChargepointListItem> {
|
): List<ChargepointListItem> {
|
||||||
/* in very crowded places (good example: central London on OpenChargeMap without filters)
|
/* in very crowded places (good example: central London on OpenChargeMap without filters)
|
||||||
we have to cluster even at pretty high zoom levels to make sure the map does not get
|
we have to cluster even at pretty high zoom levels to make sure the map does not get
|
||||||
laggy. Otherwise, only cluster at zoom levels <= 11. */
|
laggy. Otherwise, only cluster at zoom levels <= 11. */
|
||||||
val useClustering = chargers.size > 500 || zoom <= CLUSTER_MAX_ZOOM_LEVEL
|
val useClustering = chargers.size > 500 || zoom <= 11f
|
||||||
|
val clusterDistance = getClusterDistance(zoom)
|
||||||
|
|
||||||
val chargersClustered = if (useClustering) {
|
val chargersClustered = if (useClustering && clusterDistance != null) {
|
||||||
withContext(Dispatchers.Default) {
|
Dispatchers.Default.run {
|
||||||
cluster(chargers, zoom)
|
cluster(chargers, zoom, clusterDistance)
|
||||||
}
|
}
|
||||||
} else chargers
|
} else chargers
|
||||||
return chargersClustered
|
return chargersClustered
|
||||||
@@ -463,37 +351,37 @@ class ChargeLocationsRepository(
|
|||||||
fun getChargepointDetail(
|
fun getChargepointDetail(
|
||||||
id: Long,
|
id: Long,
|
||||||
overrideCache: Boolean = false
|
overrideCache: Boolean = false
|
||||||
): Flow<Resource<ChargeLocation>> {
|
): LiveData<Resource<ChargeLocation>> {
|
||||||
val dbResult = flow {
|
val dbResult = chargeLocationsDao.getChargeLocationById(
|
||||||
emit(
|
id,
|
||||||
chargeLocationsDao.getChargeLocationById(
|
prefs.dataSource,
|
||||||
id,
|
cacheLimitDate(api.value!!)
|
||||||
prefs.dataSource,
|
)
|
||||||
cacheLimitDate(api)
|
val apiResult = liveData {
|
||||||
)
|
emit(Resource.loading(null))
|
||||||
)
|
val refData = referenceData.await()
|
||||||
|
val result = api.value!!.getChargepointDetail(refData, id)
|
||||||
|
emit(result)
|
||||||
|
if (result.status == Status.SUCCESS) {
|
||||||
|
chargeLocationsDao.insert(result.data!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (api.supportsOnlineQueries) {
|
return if (overrideCache) {
|
||||||
val apiResult = flow {
|
apiResult
|
||||||
val refData = referenceData.first()
|
|
||||||
val result = api.getChargepointDetail(refData, id)
|
|
||||||
emit(result)
|
|
||||||
if (result.status == Status.SUCCESS) {
|
|
||||||
chargeLocationsDao.insert(result.data!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return if (overrideCache) {
|
|
||||||
apiResult
|
|
||||||
} else {
|
|
||||||
preferCacheFlow(dbResult, apiResult, cacheSoftLimit)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return dbResult.map { Resource.success(it) }
|
PreferCacheLiveData(dbResult, apiResult, cacheSoftLimit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFilters(sp: StringProvider) = referenceData.map {
|
fun getFilters(sp: StringProvider) = MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||||
api.getFilters(it, sp)
|
addSource(referenceData) { refData: ReferenceData? ->
|
||||||
|
refData?.let { value = api.value!!.getFilters(refData, sp) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getFiltersAsync(sp: StringProvider): List<Filter<FilterValue>> {
|
||||||
|
val refData = referenceData.await()
|
||||||
|
return api.value!!.getFilters(refData, sp)
|
||||||
}
|
}
|
||||||
|
|
||||||
val chargeCardMap by lazy {
|
val chargeCardMap by lazy {
|
||||||
@@ -508,164 +396,67 @@ class ChargeLocationsRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun queryWithFilters(
|
private fun queryWithFilters(
|
||||||
api: ChargepointApi<ReferenceData>,
|
api: ChargepointApi<ReferenceData>,
|
||||||
filters: FilterValues,
|
filters: FilterValues,
|
||||||
bounds: LatLngBounds
|
bounds: LatLngBounds
|
||||||
): List<ChargeLocation> {
|
): LiveData<List<ChargeLocation>> {
|
||||||
return queryWithFilters(api, filters, boundsSpatialIndexQuery(bounds))
|
val region =
|
||||||
|
"Within(coordinates, BuildMbr(${bounds.southwest.longitude}, ${bounds.southwest.latitude}, ${bounds.northeast.longitude}, ${bounds.northeast.latitude}))"
|
||||||
|
return queryWithFilters(api, filters, region)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun queryWithFiltersClustered(
|
private fun queryWithFilters(
|
||||||
api: ChargepointApi<ReferenceData>,
|
|
||||||
filters: FilterValues,
|
|
||||||
bounds: LatLngBounds,
|
|
||||||
zoom: Float
|
|
||||||
): List<ChargepointListItem> {
|
|
||||||
return queryWithFiltersClustered(api, filters, boundsSpatialIndexQuery(bounds), zoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun queryWithFilters(
|
|
||||||
api: ChargepointApi<ReferenceData>,
|
api: ChargepointApi<ReferenceData>,
|
||||||
filters: FilterValues,
|
filters: FilterValues,
|
||||||
location: LatLng,
|
location: LatLng,
|
||||||
radius: Double
|
radius: Double
|
||||||
): List<ChargeLocation> {
|
): LiveData<List<ChargeLocation>> {
|
||||||
val region =
|
val region =
|
||||||
radiusSpatialIndexQuery(location, radius)
|
"PtDistWithin(coordinates, MakePoint(${location.longitude}, ${location.latitude}, 4326), ${radius})"
|
||||||
val order =
|
val order =
|
||||||
"ORDER BY Distance(coordinates, MakePoint(${location.longitude}, ${location.latitude}, 4326))"
|
"ORDER BY Distance(coordinates, MakePoint(${location.longitude}, ${location.latitude}, 4326))"
|
||||||
return queryWithFilters(api, filters, region, order)
|
return queryWithFilters(api, filters, region, order)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun boundsSpatialIndexQuery(bounds: LatLngBounds) =
|
private fun queryWithFilters(
|
||||||
"ChargeLocation.ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND search_frame = BuildMbr(${bounds.southwest.longitude}, ${bounds.southwest.latitude}, ${bounds.northeast.longitude}, ${bounds.northeast.latitude}))"
|
|
||||||
|
|
||||||
private fun radiusSpatialIndexQuery(location: LatLng, radius: Double) =
|
|
||||||
"PtDistWithin(coordinates, MakePoint(${location.longitude}, ${location.latitude}, 4326), ${radius}) AND ChargeLocation.ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND search_frame = BuildCircleMbr(${location.longitude}, ${location.latitude}, $radius))"
|
|
||||||
|
|
||||||
private suspend fun queryWithFilters(
|
|
||||||
api: ChargepointApi<ReferenceData>,
|
api: ChargepointApi<ReferenceData>,
|
||||||
filters: FilterValues,
|
filters: FilterValues,
|
||||||
regionSql: String,
|
regionSql: String,
|
||||||
orderSql: String? = null
|
orderSql: String? = null
|
||||||
): List<ChargeLocation> {
|
): LiveData<List<ChargeLocation>> = referenceData.singleSwitchMap { refData ->
|
||||||
val query = api.convertFiltersToSQL(filters, referenceData.first())
|
|
||||||
val after = cacheLimitDate(api)
|
|
||||||
val sql = buildFilteredQuery(query, regionSql, after, orderSql)
|
|
||||||
|
|
||||||
return chargeLocationsDao.getChargeLocationsCustom(
|
|
||||||
SimpleSQLiteQuery(
|
|
||||||
sql,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun queryWithFiltersClustered(
|
|
||||||
api: ChargepointApi<ReferenceData>,
|
|
||||||
filters: FilterValues,
|
|
||||||
regionSql: String,
|
|
||||||
zoom: Float,
|
|
||||||
orderSql: String? = null
|
|
||||||
): List<ChargepointListItem> = if (zoom > CLUSTER_MAX_ZOOM_LEVEL) {
|
|
||||||
queryWithFilters(api, filters, regionSql, orderSql).map { it }
|
|
||||||
} else {
|
|
||||||
val query = api.convertFiltersToSQL(filters, referenceData.first())
|
|
||||||
val after = cacheLimitDate(api)
|
|
||||||
val clusterPrecision = getClusterPrecision(zoom)
|
|
||||||
val sql = buildFilteredQuery(query, regionSql, after, orderSql, clusterPrecision)
|
|
||||||
|
|
||||||
val clusters = chargeLocationsDao.getChargeLocationClustersCustom(
|
|
||||||
SimpleSQLiteQuery(
|
|
||||||
sql,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val singleChargers =
|
|
||||||
chargeLocationsDao.getChargeLocationsById(clusters.filter { it.clusterCount == 1 }
|
|
||||||
.map { it.ids }
|
|
||||||
.flatten(), prefs.dataSource, after)
|
|
||||||
clusters.filter { it.clusterCount > 1 }
|
|
||||||
.map { it.convert() } + singleChargers
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildFilteredQuery(
|
|
||||||
query: FiltersSQLQuery,
|
|
||||||
regionSql: String,
|
|
||||||
after: Long,
|
|
||||||
orderSql: String? = null,
|
|
||||||
clusterPrecision: Double? = null
|
|
||||||
) = StringBuilder().apply {
|
|
||||||
append("SELECT")
|
|
||||||
|
|
||||||
if (clusterPrecision != null) {
|
|
||||||
append(" SUM(1) AS clusterCount, Transform(MakePoint(AVG(X(query.coordinatesProjected)), AVG(Y(query.coordinatesProjected)), 3857), 4326) as center, SnapToGrid(query.coordinatesProjected, $clusterPrecision) AS snapped, GROUP_CONCAT(query.id, ',') as ids FROM (SELECT")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.requiresChargeCardQuery or query.requiresChargepointQuery) {
|
|
||||||
append(" DISTINCT chargelocation.*")
|
|
||||||
} else {
|
|
||||||
append(" *")
|
|
||||||
}
|
|
||||||
append(" FROM chargelocation")
|
|
||||||
if (query.requiresChargepointQuery) {
|
|
||||||
append(" JOIN json_each(chargelocation.chargepoints) AS cp")
|
|
||||||
}
|
|
||||||
if (query.requiresChargeCardQuery) {
|
|
||||||
append(" JOIN json_each(chargelocation.chargecards) AS cc")
|
|
||||||
}
|
|
||||||
append(" WHERE dataSource == '${prefs.dataSource}'")
|
|
||||||
append(" AND $regionSql")
|
|
||||||
append(" AND timeRetrieved > $after")
|
|
||||||
append(query.query)
|
|
||||||
orderSql?.let { append(" " + orderSql) }
|
|
||||||
|
|
||||||
if (clusterPrecision != null) {
|
|
||||||
append(") AS query GROUP BY snapped")
|
|
||||||
}
|
|
||||||
}.toString()
|
|
||||||
|
|
||||||
private suspend fun fullDownload() {
|
|
||||||
if (!api.supportsFullDownload) return
|
|
||||||
|
|
||||||
val time = Instant.now()
|
|
||||||
val result = api.fullDownload()
|
|
||||||
try {
|
try {
|
||||||
var insertJob: Job? = null
|
val query = api.convertFiltersToSQL(filters, refData)
|
||||||
result.chargers.chunked(1024).forEach {
|
val after = cacheLimitDate(api)
|
||||||
insertJob?.join()
|
val sql = StringBuilder().apply {
|
||||||
insertJob = withContext(Dispatchers.IO) {
|
append("SELECT")
|
||||||
scope.launch {
|
if (query.requiresChargeCardQuery or query.requiresChargepointQuery) {
|
||||||
chargeLocationsDao.insert(*it.toTypedArray())
|
append(" DISTINCT chargelocation.*")
|
||||||
}
|
} else {
|
||||||
|
append(" *")
|
||||||
}
|
}
|
||||||
fullDownloadProgress.value = result.progress
|
append(" FROM chargelocation")
|
||||||
}
|
if (query.requiresChargepointQuery) {
|
||||||
val region = Mbr(
|
append(" JOIN json_each(chargelocation.chargepoints) AS cp")
|
||||||
-180.0,
|
}
|
||||||
-90.0,
|
if (query.requiresChargeCardQuery) {
|
||||||
180.0,
|
append(" JOIN json_each(chargelocation.chargecards) AS cc")
|
||||||
90.0, 4326
|
}
|
||||||
).asPolygon()
|
append(" WHERE dataSource == '${prefs.dataSource}'")
|
||||||
savedRegionDao.insert(
|
append(" AND $regionSql")
|
||||||
SavedRegion(
|
append(" AND timeRetrieved > $after")
|
||||||
region, api.id, time,
|
append(query.query)
|
||||||
null,
|
orderSql?.let { append(" " + orderSql) }
|
||||||
true
|
}.toString()
|
||||||
|
|
||||||
|
chargeLocationsDao.getChargeLocationsCustom(
|
||||||
|
SimpleSQLiteQuery(
|
||||||
|
sql,
|
||||||
|
null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
} catch (e: NotImplementedError) {
|
||||||
when (api) {
|
MutableLiveData() // in this case we cannot get a DB result
|
||||||
is OpenStreetMapApiWrapper -> {
|
|
||||||
val refData = result.referenceData
|
|
||||||
OSMReferenceDataRepository(db.osmReferenceDataDao()).updateReferenceData(refData as OSMReferenceData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
fullDownloadProgress.value = null
|
|
||||||
fullDownloadJob = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class CleanupCacheWorker(appContext: Context, workerParams: WorkerParameters) :
|
|||||||
val savedRegionDao = db.savedRegionDao()
|
val savedRegionDao = db.savedRegionDao()
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
|
|
||||||
val dataSources = listOf("openchargemap", "openstreetmap", "goingelectric")
|
val dataSources = listOf("openchargemap", "goingelectric")
|
||||||
for (dataSource in dataSources) {
|
for (dataSource in dataSources) {
|
||||||
val api = createApi(dataSource, applicationContext)
|
val api = createApi(dataSource, applicationContext)
|
||||||
val limit = now.minus(api.cacheLimit).toEpochMilli()
|
val limit = now.minus(api.cacheLimit).toEpochMilli()
|
||||||
|
|||||||
@@ -16,12 +16,7 @@ import net.vonforst.evmap.api.goingelectric.GEChargepoint
|
|||||||
import net.vonforst.evmap.api.openchargemap.OCMConnectionType
|
import net.vonforst.evmap.api.openchargemap.OCMConnectionType
|
||||||
import net.vonforst.evmap.api.openchargemap.OCMCountry
|
import net.vonforst.evmap.api.openchargemap.OCMCountry
|
||||||
import net.vonforst.evmap.api.openchargemap.OCMOperator
|
import net.vonforst.evmap.api.openchargemap.OCMOperator
|
||||||
import net.vonforst.evmap.model.BooleanFilterValue
|
import net.vonforst.evmap.model.*
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
|
||||||
import net.vonforst.evmap.model.FILTERS_CUSTOM
|
|
||||||
import net.vonforst.evmap.model.Favorite
|
|
||||||
import net.vonforst.evmap.model.MultipleChoiceFilterValue
|
|
||||||
import net.vonforst.evmap.model.SliderFilterValue
|
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
@@ -38,9 +33,8 @@ import net.vonforst.evmap.model.SliderFilterValue
|
|||||||
OCMConnectionType::class,
|
OCMConnectionType::class,
|
||||||
OCMCountry::class,
|
OCMCountry::class,
|
||||||
OCMOperator::class,
|
OCMOperator::class,
|
||||||
OSMNetwork::class,
|
|
||||||
SavedRegion::class
|
SavedRegion::class
|
||||||
], version = 24
|
], version = 22
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class, GeometryConverters::class)
|
@TypeConverters(Converters::class, GeometryConverters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
@@ -57,9 +51,6 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
// OpenChargeMap API specific
|
// OpenChargeMap API specific
|
||||||
abstract fun ocmReferenceDataDao(): OCMReferenceDataDao
|
abstract fun ocmReferenceDataDao(): OCMReferenceDataDao
|
||||||
|
|
||||||
// OpenStreetMap API specific
|
|
||||||
abstract fun osmReferenceDataDao(): OSMReferenceDataDao
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private lateinit var context: Context
|
private lateinit var context: Context
|
||||||
private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||||
@@ -84,29 +75,22 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11,
|
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11,
|
||||||
MIGRATION_12, MIGRATION_13, MIGRATION_14, MIGRATION_15, MIGRATION_16,
|
MIGRATION_12, MIGRATION_13, MIGRATION_14, MIGRATION_15, MIGRATION_16,
|
||||||
MIGRATION_17, MIGRATION_18, MIGRATION_19, MIGRATION_20, MIGRATION_21,
|
MIGRATION_17, MIGRATION_18, MIGRATION_19, MIGRATION_20, MIGRATION_21,
|
||||||
MIGRATION_22, MIGRATION_23, MIGRATION_24
|
MIGRATION_22
|
||||||
)
|
)
|
||||||
.addCallback(object : Callback() {
|
.addCallback(object : Callback() {
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||||
// create default filter profile for each data source
|
// 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 ('goingelectric', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
||||||
db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('openchargemap', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('openchargemap', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
||||||
db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('openstreetmap', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
|
||||||
// initialize spatialite columns
|
// initialize spatialite columns
|
||||||
db.query("SELECT RecoverGeometryColumn('ChargeLocation', 'coordinates', 4326, 'POINT', 'XY');")
|
db.query("SELECT RecoverGeometryColumn('ChargeLocation', 'coordinates', 4326, 'POINT', 'XY');")
|
||||||
.moveToNext()
|
.moveToNext()
|
||||||
db.query("SELECT CreateSpatialIndex('ChargeLocation', 'coordinates');")
|
db.query("SELECT CreateSpatialIndex('ChargeLocation', 'coordinates');")
|
||||||
.moveToNext()
|
.moveToNext()
|
||||||
db.query("SELECT RecoverGeometryColumn('ChargeLocation', 'coordinatesProjected', 3857, 'POINT', 'XY');")
|
|
||||||
.moveToNext()
|
|
||||||
db.query("SELECT CreateSpatialIndex('ChargeLocation', 'coordinatesProjected');")
|
|
||||||
.moveToNext()
|
|
||||||
db.query("SELECT RecoverGeometryColumn('SavedRegion', 'region', 4326, 'POLYGON', 'XY');")
|
db.query("SELECT RecoverGeometryColumn('SavedRegion', 'region', 4326, 'POLYGON', 'XY');")
|
||||||
.moveToNext()
|
.moveToNext()
|
||||||
db.query("SELECT CreateSpatialIndex('SavedRegion', 'region');")
|
db.query("SELECT CreateSpatialIndex('SavedRegion', 'region');")
|
||||||
.moveToNext()
|
.moveToNext()
|
||||||
db.query("SELECT RecoverGeometryColumn('ChargeLocation', 'coordinatesProjected', 3857, 'POINT', 'XY');")
|
|
||||||
.moveToNext()
|
|
||||||
}
|
}
|
||||||
}).build()
|
}).build()
|
||||||
}
|
}
|
||||||
@@ -475,32 +459,6 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
db.execSQL("DELETE FROM savedregion")
|
db.execSQL("DELETE FROM savedregion")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val MIGRATION_23 = object : Migration(22, 23) {
|
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
|
||||||
// API openstreetmap added
|
|
||||||
db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('openstreetmap', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
|
||||||
db.execSQL("CREATE TABLE IF NOT EXISTS `OSMNetwork` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val MIGRATION_24 = object : Migration(23, 24) {
|
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
|
||||||
try {
|
|
||||||
db.beginTransaction()
|
|
||||||
// add column with projected location for fast clustering in DB
|
|
||||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `coordinatesProjected` BLOB NOT NULL DEFAULT x'00'")
|
|
||||||
db.query("SELECT RecoverGeometryColumn('ChargeLocation', 'coordinatesProjected', 3857, 'POINT', 'XY');")
|
|
||||||
.moveToNext()
|
|
||||||
db.query("SELECT CreateSpatialIndex('ChargeLocation', 'coordinatesProjected');")
|
|
||||||
.moveToNext()
|
|
||||||
db.execSQL("UPDATE `ChargeLocation` SET coordinatesProjected = Transform(coordinates, 3857);")
|
|
||||||
db.setTransactionSuccessful()
|
|
||||||
} finally {
|
|
||||||
db.endTransaction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
package net.vonforst.evmap.storage
|
package net.vonforst.evmap.storage
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.room.Entity
|
import androidx.lifecycle.MediatorLiveData
|
||||||
import androidx.room.Insert
|
import androidx.room.*
|
||||||
import androidx.room.OnConflictStrategy
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import androidx.room.Query
|
|
||||||
import androidx.room.Transaction
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import net.vonforst.evmap.api.goingelectric.GEChargeCard
|
import net.vonforst.evmap.api.goingelectric.GEChargeCard
|
||||||
import net.vonforst.evmap.api.goingelectric.GEReferenceData
|
import net.vonforst.evmap.api.goingelectric.GEReferenceData
|
||||||
@@ -42,7 +36,7 @@ abstract class GEReferenceDataDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Query("SELECT * FROM genetwork")
|
@Query("SELECT * FROM genetwork")
|
||||||
abstract fun getAllNetworks(): Flow<List<GENetwork>>
|
abstract fun getAllNetworks(): LiveData<List<GENetwork>>
|
||||||
|
|
||||||
// PLUGS
|
// PLUGS
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
@@ -60,7 +54,7 @@ abstract class GEReferenceDataDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Query("SELECT * FROM geplug")
|
@Query("SELECT * FROM geplug")
|
||||||
abstract fun getAllPlugs(): Flow<List<GEPlug>>
|
abstract fun getAllPlugs(): LiveData<List<GEPlug>>
|
||||||
|
|
||||||
// CHARGE CARDS
|
// CHARGE CARDS
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
@@ -78,21 +72,31 @@ abstract class GEReferenceDataDao {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Query("SELECT * FROM gechargecard")
|
@Query("SELECT * FROM gechargecard")
|
||||||
abstract fun getAllChargeCards(): Flow<List<GEChargeCard>>
|
abstract fun getAllChargeCards(): LiveData<List<GEChargeCard>>
|
||||||
}
|
}
|
||||||
|
|
||||||
class GEReferenceDataRepository(
|
class GEReferenceDataRepository(
|
||||||
private val api: GoingElectricApiWrapper, private val scope: CoroutineScope,
|
private val api: GoingElectricApiWrapper, private val scope: CoroutineScope,
|
||||||
private val dao: GEReferenceDataDao, private val prefs: PreferenceDataSource
|
private val dao: GEReferenceDataDao, private val prefs: PreferenceDataSource
|
||||||
) {
|
) {
|
||||||
fun getReferenceData(): Flow<GEReferenceData> {
|
fun getReferenceData(): LiveData<GEReferenceData> {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
updateData()
|
updateData()
|
||||||
}
|
}
|
||||||
val plugs = dao.getAllPlugs()
|
val plugs = dao.getAllPlugs()
|
||||||
val networks = dao.getAllNetworks()
|
val networks = dao.getAllNetworks()
|
||||||
val chargeCards = dao.getAllChargeCards()
|
val chargeCards = dao.getAllChargeCards()
|
||||||
return combine(plugs, networks, chargeCards) { p, n, c -> GEReferenceData(p.map { it.name }, n.map { it.name }, c) }
|
return MediatorLiveData<GEReferenceData>().apply {
|
||||||
|
value = null
|
||||||
|
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() {
|
private suspend fun updateData() {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user