mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-24 23:57:45 -05:00
Compare commits
68 Commits
openstreet
...
2.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3386092bf8 | ||
|
|
1318126780 | ||
|
|
abf9165602 | ||
|
|
2c35df6360 | ||
|
|
4ed046df7a | ||
|
|
a20f25af17 | ||
|
|
b2a2114c88 | ||
|
|
c2896ade45 | ||
|
|
45983bce7f | ||
|
|
d0fffb1a97 | ||
|
|
4819a10d03 | ||
|
|
8a0f7e79f0 | ||
|
|
c727d9f1b8 | ||
|
|
e5d0ebbbb5 | ||
|
|
ee4b5e7319 | ||
|
|
fecde441f1 | ||
|
|
cb1543cb4a | ||
|
|
276daac607 | ||
|
|
f7d39a1ba5 | ||
|
|
fa09b9188e | ||
|
|
b31e55f130 | ||
|
|
c494b0d5e2 | ||
|
|
272b86ff88 | ||
|
|
32de28bc1c | ||
|
|
4cd6c44ba1 | ||
|
|
3265694c51 | ||
|
|
529be2cc34 | ||
|
|
00862b66a1 | ||
|
|
cabaa42772 | ||
|
|
1663607171 | ||
|
|
126c47bbc1 | ||
|
|
b93d01f96d | ||
|
|
7fb5df29e4 | ||
|
|
b878d37982 | ||
|
|
0f7aa44d8e | ||
|
|
d8e1c36993 | ||
|
|
03f613fa4b | ||
|
|
aba533e553 | ||
|
|
307af88f01 | ||
|
|
8478948d5f | ||
|
|
7e96c9e5a7 | ||
|
|
44bd2c6159 | ||
|
|
7d2a19b0a3 | ||
|
|
3414a7581c | ||
|
|
df47f7b4c1 | ||
|
|
a08e2ab7e9 | ||
|
|
c1351ce935 | ||
|
|
b4a1a8b546 | ||
|
|
3865e6c33d | ||
|
|
091b0f5ac3 | ||
|
|
1148200f37 | ||
|
|
1847e8b771 | ||
|
|
bbfe8e2bb2 | ||
|
|
983d368a78 | ||
|
|
4a6a34db3a | ||
|
|
35ddece698 | ||
|
|
36c6a4053d | ||
|
|
104913b3c4 | ||
|
|
5cc510fe22 | ||
|
|
4250eb2ba8 | ||
|
|
1db82db066 | ||
|
|
d6a8fbee7d | ||
|
|
23e2f0baad | ||
|
|
ea4fb37f30 | ||
|
|
094f38ac87 | ||
|
|
b84d13d42b | ||
|
|
845bd2e5ca | ||
|
|
0b68ddb939 |
47
.github/workflows/release.yml
vendored
47
.github/workflows/release.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Java environment
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: 'zulu'
|
||||
cache: 'gradle'
|
||||
- name: Decrypt keystore
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- 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
|
||||
|
||||
- name: Build app release
|
||||
- name: Build app release & export licenses
|
||||
env:
|
||||
GOINGELECTRIC_API_KEY: ${{ secrets.GOINGELECTRIC_API_KEY }}
|
||||
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
|
||||
@@ -35,10 +35,14 @@ jobs:
|
||||
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }}
|
||||
ACRA_CRASHREPORT_CREDENTIALS: ${{ secrets.ACRA_CRASHREPORT_CREDENTIALS }}
|
||||
NOBIL_API_KEY: ${{ secrets.NOBIL_API_KEY }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
|
||||
KEYSTORE_ALIAS_PASSWORD: ${{ secrets.KEYSTORE_ALIAS_PASSWORD }}
|
||||
run: ./gradlew assembleRelease --no-daemon
|
||||
run: ./gradlew exportLibraryDefinitions assembleRelease --no-daemon
|
||||
|
||||
- name: Export licenses in Appning format
|
||||
run: python3 _ci/export_licenses_appning.py
|
||||
|
||||
- name: release
|
||||
uses: actions/create-release@v1
|
||||
@@ -88,3 +92,40 @@ jobs:
|
||||
asset_path: app/build/outputs/apk/fossAutomotive/release/app-foss-automotive-release.apk
|
||||
asset_name: app-foss-automotive-release.apk
|
||||
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
|
||||
- name: upload Licenses Appning
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: licenses_fossAutomotiveRelease_appning.csv
|
||||
asset_name: licenses_fossAutomotiveRelease_appning.csv
|
||||
asset_content_type: text/csv
|
||||
- name: upload Licenses Appning
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: licenses_fossNormalRelease_appning.csv
|
||||
asset_name: licenses_fossNormalRelease_appning.csv
|
||||
asset_content_type: text/csv
|
||||
|
||||
- name: Trigger Website update
|
||||
run: |
|
||||
curl -L \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ github.token }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/ev-map/ev-map.github.io/dispatches \
|
||||
-d "{\"event_type\": \"trigger-workflow\"}"
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Set up Java environment
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: 'zulu'
|
||||
cache: 'gradle'
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
*.iml
|
||||
.gradle
|
||||
.kotlin
|
||||
/local.properties
|
||||
/.idea/*
|
||||
.DS_Store
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<string name="goingelectric_key" translatable="false">ci</string>
|
||||
<string name="chargeprice_key" translatable="false">ci</string>
|
||||
<string name="openchargemap_key" translatable="false">ci</string>
|
||||
<string name="nobil_key" translatable="false">ci</string>
|
||||
<string name="fronyx_key" translatable="false">ci</string>
|
||||
<string name="acra_credentials" translatable="false">ci:ci</string>
|
||||
</resources>
|
||||
|
||||
@@ -4,13 +4,10 @@ import json
|
||||
build_types = ["fossNormalRelease", "fossAutomotiveRelease"]
|
||||
|
||||
for build_type in build_types:
|
||||
result = subprocess.run(["gradlew.bat", f"generateLibraryDefinitions{build_type.capitalize()}"],
|
||||
capture_output=True)
|
||||
|
||||
data = json.load(
|
||||
open(f"app/build/generated/aboutLibraries/{build_type}/res/raw/aboutlibraries.json"))
|
||||
|
||||
with open(f"licenses_{build_type}.csv", "w") as f:
|
||||
with open(f"licenses_{build_type}_appning.csv", "w") as f:
|
||||
f.write("component_name;license_title;license_url;public_repository;copyrights\n")
|
||||
for lib in data["libraries"]:
|
||||
license = data["licenses"][lib["licenses"][0]] if len(lib["licenses"]) > 0 else None
|
||||
231
_misc/taginfo.json
Normal file
231
_misc/taginfo.json
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"data_format": 1,
|
||||
"data_url": "https://raw.githubusercontent.com/ev-map/evmap/master/_misc/taginfo.json",
|
||||
"data_updated": "20250921T140000Z",
|
||||
"project": {
|
||||
"name": "EVMap",
|
||||
"description": "Find electric vehicle chargers comfortably using your Android phone.",
|
||||
"project_url": "https://ev-map.app/",
|
||||
"doc_url": "https://github.com/ev-map/evmap-osm",
|
||||
"icon_url": "https://avatars.githubusercontent.com/u/115927597?s=32",
|
||||
"contact_name": "Johan von Forstner",
|
||||
"contact_email": "evmap@vonforst.net"
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"key": "amenity",
|
||||
"value": "charging_station",
|
||||
"description": "Used to display charging stations."
|
||||
},
|
||||
{
|
||||
"key": "name"
|
||||
},
|
||||
{
|
||||
"key": "network"
|
||||
},
|
||||
{
|
||||
"key": "authentication:none",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "operator"
|
||||
},
|
||||
{
|
||||
"key": "description"
|
||||
},
|
||||
{
|
||||
"key": "website"
|
||||
},
|
||||
{
|
||||
"key": "addr:city"
|
||||
},
|
||||
{
|
||||
"key": "addr:country"
|
||||
},
|
||||
{
|
||||
"key": "addr:postcode"
|
||||
},
|
||||
{
|
||||
"key": "addr:street"
|
||||
},
|
||||
{
|
||||
"key": "addr:housenumber"
|
||||
},
|
||||
{
|
||||
"key": "addr:housename"
|
||||
},
|
||||
{
|
||||
"key": "socket:type1"
|
||||
},
|
||||
{
|
||||
"key": "socket:type1:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:type1_combo"
|
||||
},
|
||||
{
|
||||
"key": "socket:type1_combo:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2_cable"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2_cable:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2_combo"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2_combo:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:chademo"
|
||||
},
|
||||
{
|
||||
"key": "socket:chademo:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_standard"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_standard:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_supercharger"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_supercharger:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_supercharger_ccs"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_supercharger_ccs:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_blue"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_blue:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_16a"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_16a:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_32a"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_32a:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_63a"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_63a:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_125a"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_125a:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:schuko"
|
||||
},
|
||||
{
|
||||
"key": "socket:schuko:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t13"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t13:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t15"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t15:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t23"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t23:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t25"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t25:output"
|
||||
},
|
||||
{
|
||||
"key": "opening_hours",
|
||||
"value": "24/7"
|
||||
},
|
||||
{
|
||||
"key": "fee",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "fee",
|
||||
"value": "no"
|
||||
},
|
||||
{
|
||||
"key": "parking:fee",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "parking:fee",
|
||||
"value": "no"
|
||||
},
|
||||
{
|
||||
"key": "charge"
|
||||
},
|
||||
{
|
||||
"key": "charge:conditional"
|
||||
},
|
||||
{
|
||||
"key": "image"
|
||||
},
|
||||
{
|
||||
"key": "image:0"
|
||||
},
|
||||
{
|
||||
"key": "image:1"
|
||||
},
|
||||
{
|
||||
"key": "image:2"
|
||||
},
|
||||
{
|
||||
"key": "image:3"
|
||||
},
|
||||
{
|
||||
"key": "image:4"
|
||||
},
|
||||
{
|
||||
"key": "image:5"
|
||||
},
|
||||
{
|
||||
"key": "image:6"
|
||||
},
|
||||
{
|
||||
"key": "image:7"
|
||||
},
|
||||
{
|
||||
"key": "image:8"
|
||||
},
|
||||
{
|
||||
"key": "image:9"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import java.util.Base64
|
||||
|
||||
plugins {
|
||||
id("com.adarshr.test-logger") version "3.1.0"
|
||||
id("com.adarshr.test-logger") version "4.0.0"
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("kotlin-parcelize")
|
||||
@@ -17,18 +17,18 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "net.vonforst.evmap"
|
||||
compileSdk = 35
|
||||
compileSdk = 36
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
targetSdk = 36
|
||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||
versionCode = 230
|
||||
versionName = "1.9.6"
|
||||
versionCode = 264
|
||||
versionName = "2.0.1"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
}
|
||||
|
||||
val isRunningOnCI = System.getenv("CI") == "true"
|
||||
@@ -135,6 +135,17 @@ android {
|
||||
if (goingelectricKey != null) {
|
||||
resValue("string", "goingelectric_key", goingelectricKey)
|
||||
}
|
||||
var nobilKey =
|
||||
System.getenv("NOBIL_API_KEY") ?: project.findProperty("NOBIL_API_KEY")?.toString()
|
||||
if (nobilKey == null && project.hasProperty("NOBIL_API_KEY_ENCRYPTED")) {
|
||||
nobilKey = decode(
|
||||
project.findProperty("NOBIL_API_KEY_ENCRYPTED").toString(),
|
||||
"FmK.d,-f*p+rD+WK!eds"
|
||||
)
|
||||
}
|
||||
if (nobilKey != null) {
|
||||
resValue("string", "nobil_key", nobilKey)
|
||||
}
|
||||
var openchargemapKey =
|
||||
System.getenv("OPENCHARGEMAP_API_KEY") ?: project.findProperty("OPENCHARGEMAP_API_KEY")
|
||||
?.toString()
|
||||
@@ -258,18 +269,21 @@ configurations {
|
||||
}
|
||||
|
||||
aboutLibraries {
|
||||
allowedLicenses = arrayOf(
|
||||
"Apache-2.0", "mit", "BSD-2-Clause", "BSD-3-Clause", "EPL-1.0",
|
||||
"asdkl", // Android SDK
|
||||
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
|
||||
"Google Maps Platform Terms of Service", // Google Maps SDK
|
||||
"provided without support or warranty", // org.json
|
||||
"Unicode/ICU License", "Unicode-3.0", // icu4j
|
||||
"Bouncy Castle Licence", // bcprov
|
||||
"CDDL + GPLv2 with classpath exception", // javax.annotation-api
|
||||
)
|
||||
excludeFields = arrayOf("generated")
|
||||
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
||||
license {
|
||||
allowedLicenses = setOf(
|
||||
"Apache-2.0", "mit", "BSD-2-Clause", "BSD-3-Clause", "EPL-1.0",
|
||||
"asdkl", // Android SDK
|
||||
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
|
||||
"Google Maps Platform Terms of Service", // Google Maps SDK
|
||||
"Unicode/ICU License", "Unicode-3.0", // icu4j
|
||||
"Bouncy Castle Licence", // bcprov
|
||||
"CDDL + GPLv2 with classpath exception", // javax.annotation-api
|
||||
)
|
||||
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
||||
}
|
||||
export {
|
||||
excludeFields = setOf("generated")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -283,42 +297,41 @@ dependencies {
|
||||
val testGoogleImplementation by configurations
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
|
||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.core:core-ktx:1.17.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("androidx.activity:activity-ktx:1.9.0")
|
||||
implementation("androidx.fragment:fragment-ktx:1.7.1")
|
||||
implementation("androidx.activity:activity-ktx:1.10.1")
|
||||
implementation("androidx.fragment:fragment-ktx:1.8.9")
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("com.google.android.material:material:1.13.0-rc01")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||
implementation("androidx.browser:browser:1.8.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.4.0")
|
||||
implementation("androidx.browser:browser:1.9.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
||||
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||
implementation("androidx.security:security-crypto:1.1.0")
|
||||
implementation("androidx.work:work-runtime-ktx:2.10.3")
|
||||
implementation("com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b")
|
||||
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
|
||||
implementation("com.squareup.retrofit2:retrofit:3.0.0")
|
||||
implementation("com.squareup.retrofit2:converter-moshi:3.0.0")
|
||||
implementation("com.squareup.okhttp3:okhttp: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-adapters:1.15.2")
|
||||
implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
|
||||
implementation("io.coil-kt:coil:2.6.0")
|
||||
implementation("io.coil-kt:coil:2.7.0")
|
||||
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
|
||||
implementation("com.mikepenz:aboutlibraries-core:$aboutLibsVersion")
|
||||
implementation("com.mikepenz:aboutlibraries:$aboutLibsVersion")
|
||||
implementation("com.airbnb.android:lottie:4.1.0")
|
||||
implementation("com.airbnb.android:lottie:6.6.7")
|
||||
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.romandanylyk:PageIndicatorView:b1bad589b5")
|
||||
implementation("com.github.erfansn:locale-config-x:1.0.1")
|
||||
implementation("com.github.ev-map:locale-config-x:c97ce250b9")
|
||||
|
||||
// Android Auto
|
||||
val carAppVersion = "1.7.0-rc01"
|
||||
val carAppVersion = "1.7.0"
|
||||
implementation("androidx.car.app:app:$carAppVersion")
|
||||
normalImplementation("androidx.car.app:app-projected:$carAppVersion")
|
||||
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
|
||||
@@ -327,58 +340,56 @@ dependencies {
|
||||
val anyMapsVersion = "1174ef9375"
|
||||
implementation("com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion")
|
||||
googleImplementation("com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion")
|
||||
googleImplementation("com.google.android.gms:play-services-maps:19.0.0")
|
||||
googleImplementation("com.google.android.gms:play-services-maps:19.2.0")
|
||||
implementation("com.github.ev-map.AnyMaps:anymaps-maplibre:$anyMapsVersion") {
|
||||
// duplicates classes from mapbox-sdk-services
|
||||
exclude("org.maplibre.gl", "android-sdk-geojson")
|
||||
}
|
||||
implementation("org.maplibre.gl:android-sdk:10.3.4") {
|
||||
implementation("org.maplibre.gl:android-sdk:10.3.5") {
|
||||
exclude("org.maplibre.gl", "android-sdk-geojson")
|
||||
}
|
||||
|
||||
// Google Places
|
||||
googleImplementation("com.google.android.libraries.places:places:3.5.0")
|
||||
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
|
||||
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2")
|
||||
|
||||
// Mapbox Geocoding
|
||||
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0")
|
||||
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.8.0")
|
||||
|
||||
// navigation library
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
|
||||
|
||||
// viewmodel library
|
||||
val lifecycle_version = "2.8.1"
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
|
||||
val lifecycleVersion = "2.9.2"
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
|
||||
|
||||
// room library
|
||||
val room_version = "2.7.1"
|
||||
implementation("androidx.room:room-runtime:$room_version")
|
||||
ksp("androidx.room:room-compiler:$room_version")
|
||||
implementation("androidx.room:room-ktx:$room_version")
|
||||
val roomVersion = "2.7.2"
|
||||
implementation("androidx.room:room-runtime:$roomVersion")
|
||||
ksp("androidx.room:room-compiler:$roomVersion")
|
||||
implementation("androidx.room:room-ktx:$roomVersion")
|
||||
implementation("com.github.anboralabs:spatia-room:0.3.0") {
|
||||
exclude("com.github.dalgarins", "android-spatialite")
|
||||
}
|
||||
// forked version with upgraded sqlite & libxml
|
||||
// https://github.com/dalgarins/android-spatialite/pull/10
|
||||
implementation("com.github.ev-map:android-spatialite:31495dcd81")
|
||||
// forked version with upgraded sqlite & libxml & 16 KB page size support
|
||||
// https://github.com/dalgarins/android-spatialite/pull/11
|
||||
// https://github.com/dalgarins/android-spatialite/pull/12
|
||||
implementation("io.github.ev-map:android-spatialite:2.2.1-alpha")
|
||||
|
||||
// billing library
|
||||
val billing_version = "7.0.0"
|
||||
googleImplementation("com.android.billingclient:billing:$billing_version")
|
||||
googleImplementation("com.android.billingclient:billing-ktx:$billing_version")
|
||||
val billingVersion = "7.0.0"
|
||||
googleImplementation("com.android.billingclient:billing:$billingVersion")
|
||||
googleImplementation("com.android.billingclient:billing-ktx:$billingVersion")
|
||||
|
||||
// ACRA (crash reporting)
|
||||
val acraVersion = "5.11.1"
|
||||
val acraVersion = "5.12.0"
|
||||
implementation("ch.acra:acra-http:$acraVersion")
|
||||
implementation("ch.acra:acra-dialog:$acraVersion")
|
||||
implementation("ch.acra:acra-limiter:$acraVersion")
|
||||
|
||||
// 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.squareup.leakcanary:leakcanary-android:2.14")
|
||||
|
||||
@@ -386,20 +397,18 @@ dependencies {
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
|
||||
//noinspection GradleDependency
|
||||
testImplementation("org.json:json:20080701")
|
||||
testImplementation("org.robolectric:robolectric:4.11.1")
|
||||
testImplementation("androidx.test:core:1.5.0")
|
||||
testImplementation("org.robolectric:robolectric:4.16-beta-1")
|
||||
testImplementation("androidx.test:core:1.7.0")
|
||||
testImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||
testImplementation("androidx.car.app:app-testing:$carAppVersion")
|
||||
testImplementation("androidx.test:core:1.5.0")
|
||||
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.3.0")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
|
||||
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||
|
||||
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
|
||||
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||
}
|
||||
|
||||
fun decode(s: String, key: String): String {
|
||||
|
||||
@@ -41,8 +41,7 @@
|
||||
{
|
||||
"fieldPath": "network",
|
||||
"columnName": "network",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
@@ -53,8 +52,7 @@
|
||||
{
|
||||
"fieldPath": "editUrl",
|
||||
"columnName": "editUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "verified",
|
||||
@@ -65,62 +63,52 @@
|
||||
{
|
||||
"fieldPath": "barrierFree",
|
||||
"columnName": "barrierFree",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "operator",
|
||||
"columnName": "operator",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "generalInformation",
|
||||
"columnName": "generalInformation",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amenities",
|
||||
"columnName": "amenities",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "locationDescription",
|
||||
"columnName": "locationDescription",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photos",
|
||||
"columnName": "photos",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargecards",
|
||||
"columnName": "chargecards",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "license",
|
||||
"columnName": "license",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "networkUrl",
|
||||
"columnName": "networkUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargerUrl",
|
||||
"columnName": "chargerUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeRetrieved",
|
||||
@@ -143,188 +131,157 @@
|
||||
{
|
||||
"fieldPath": "address.city",
|
||||
"columnName": "city",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.country",
|
||||
"columnName": "country",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.postcode",
|
||||
"columnName": "postcode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.street",
|
||||
"columnName": "street",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "faultReport.created",
|
||||
"columnName": "fault_report_created",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "faultReport.description",
|
||||
"columnName": "fault_report_description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.twentyfourSeven",
|
||||
"columnName": "twentyfourSeven",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.monday.start",
|
||||
"columnName": "mostart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.monday.end",
|
||||
"columnName": "moend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.tuesday.start",
|
||||
"columnName": "tustart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.tuesday.end",
|
||||
"columnName": "tuend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.wednesday.start",
|
||||
"columnName": "westart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.wednesday.end",
|
||||
"columnName": "weend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.thursday.start",
|
||||
"columnName": "thstart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.thursday.end",
|
||||
"columnName": "thend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.friday.start",
|
||||
"columnName": "frstart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.friday.end",
|
||||
"columnName": "frend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.saturday.start",
|
||||
"columnName": "sastart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.saturday.end",
|
||||
"columnName": "saend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.sunday.start",
|
||||
"columnName": "sustart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.sunday.end",
|
||||
"columnName": "suend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.holiday.start",
|
||||
"columnName": "hostart",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.holiday.end",
|
||||
"columnName": "hoend",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.freecharging",
|
||||
"columnName": "freecharging",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.freeparking",
|
||||
"columnName": "freeparking",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.descriptionShort",
|
||||
"columnName": "descriptionShort",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.descriptionLong",
|
||||
"columnName": "descriptionLong",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.country",
|
||||
"columnName": "chargepricecountry",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.network",
|
||||
"columnName": "chargepricenetwork",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.plugTypes",
|
||||
"columnName": "chargepriceplugTypes",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@@ -333,9 +290,7 @@
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "Favorite",
|
||||
@@ -642,8 +597,7 @@
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_FilterProfile_dataSource_name` ON `${TABLE_NAME}` (`dataSource`, `name`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": []
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "RecentAutocompletePlace",
|
||||
@@ -688,8 +642,7 @@
|
||||
{
|
||||
"fieldPath": "viewport",
|
||||
"columnName": "viewport",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "types",
|
||||
@@ -704,9 +657,7 @@
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GEPlug",
|
||||
@@ -724,9 +675,7 @@
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GENetwork",
|
||||
@@ -744,9 +693,7 @@
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GEChargeCard",
|
||||
@@ -776,9 +723,7 @@
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMConnectionType",
|
||||
@@ -799,20 +744,17 @@
|
||||
{
|
||||
"fieldPath": "formalName",
|
||||
"columnName": "formalName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "discontinued",
|
||||
"columnName": "discontinued",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "obsolete",
|
||||
"columnName": "obsolete",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@@ -820,9 +762,7 @@
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMCountry",
|
||||
@@ -843,8 +783,7 @@
|
||||
{
|
||||
"fieldPath": "continentCode",
|
||||
"columnName": "continentCode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
@@ -858,9 +797,7 @@
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMOperator",
|
||||
@@ -875,8 +812,7 @@
|
||||
{
|
||||
"fieldPath": "websiteUrl",
|
||||
"columnName": "websiteUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
@@ -887,20 +823,17 @@
|
||||
{
|
||||
"fieldPath": "contactEmail",
|
||||
"columnName": "contactEmail",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactTelephone1",
|
||||
"columnName": "contactTelephone1",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactTelephone2",
|
||||
"columnName": "contactTelephone2",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@@ -908,9 +841,7 @@
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OSMNetwork",
|
||||
@@ -928,9 +859,7 @@
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "SavedRegion",
|
||||
@@ -957,8 +886,7 @@
|
||||
{
|
||||
"fieldPath": "filters",
|
||||
"columnName": "filters",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDetailed",
|
||||
@@ -969,8 +897,7 @@
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@@ -990,11 +917,9 @@
|
||||
"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, 'b2b3f39d450f4f7c8280ca850161bbb3')"
|
||||
|
||||
938
app/schemas/net.vonforst.evmap.storage.AppDatabase/27.json
Normal file
938
app/schemas/net.vonforst.evmap.storage.AppDatabase/27.json
Normal file
@@ -0,0 +1,938 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 27,
|
||||
"identityHash": "84f71cce385c444726ba336834ddf6b4",
|
||||
"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, `dataSourceUrl` TEXT NOT NULL, `url` TEXT, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `accessibility` TEXT, `license` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER NOT NULL, `coordinatesProjected` BLOB 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"
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSourceUrl",
|
||||
"columnName": "dataSourceUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "editUrl",
|
||||
"columnName": "editUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "verified",
|
||||
"columnName": "verified",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "barrierFree",
|
||||
"columnName": "barrierFree",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "operator",
|
||||
"columnName": "operator",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "generalInformation",
|
||||
"columnName": "generalInformation",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amenities",
|
||||
"columnName": "amenities",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "locationDescription",
|
||||
"columnName": "locationDescription",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photos",
|
||||
"columnName": "photos",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargecards",
|
||||
"columnName": "chargecards",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessibility",
|
||||
"columnName": "accessibility",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "license",
|
||||
"columnName": "license",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "networkUrl",
|
||||
"columnName": "networkUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargerUrl",
|
||||
"columnName": "chargerUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeRetrieved",
|
||||
"columnName": "timeRetrieved",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDetailed",
|
||||
"columnName": "isDetailed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "coordinatesProjected",
|
||||
"columnName": "coordinatesProjected",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.city",
|
||||
"columnName": "city",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.country",
|
||||
"columnName": "country",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.postcode",
|
||||
"columnName": "postcode",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.street",
|
||||
"columnName": "street",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "faultReport.created",
|
||||
"columnName": "fault_report_created",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "faultReport.description",
|
||||
"columnName": "fault_report_description",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.twentyfourSeven",
|
||||
"columnName": "twentyfourSeven",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.monday.start",
|
||||
"columnName": "mostart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.monday.end",
|
||||
"columnName": "moend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.tuesday.start",
|
||||
"columnName": "tustart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.tuesday.end",
|
||||
"columnName": "tuend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.wednesday.start",
|
||||
"columnName": "westart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.wednesday.end",
|
||||
"columnName": "weend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.thursday.start",
|
||||
"columnName": "thstart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.thursday.end",
|
||||
"columnName": "thend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.friday.start",
|
||||
"columnName": "frstart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.friday.end",
|
||||
"columnName": "frend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.saturday.start",
|
||||
"columnName": "sastart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.saturday.end",
|
||||
"columnName": "saend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.sunday.start",
|
||||
"columnName": "sustart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.sunday.end",
|
||||
"columnName": "suend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.holiday.start",
|
||||
"columnName": "hostart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.holiday.end",
|
||||
"columnName": "hoend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.freecharging",
|
||||
"columnName": "freecharging",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.freeparking",
|
||||
"columnName": "freeparking",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.descriptionShort",
|
||||
"columnName": "descriptionShort",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.descriptionLong",
|
||||
"columnName": "descriptionLong",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.country",
|
||||
"columnName": "chargepricecountry",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.network",
|
||||
"columnName": "chargepricenetwork",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.plugTypes",
|
||||
"columnName": "chargepriceplugTypes",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"fieldPath": "types",
|
||||
"columnName": "types",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"fieldPath": "discontinued",
|
||||
"columnName": "discontinued",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "obsolete",
|
||||
"columnName": "obsolete",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactEmail",
|
||||
"columnName": "contactEmail",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactTelephone1",
|
||||
"columnName": "contactTelephone1",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactTelephone2",
|
||||
"columnName": "contactTelephone2",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDetailed",
|
||||
"columnName": "isDetailed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"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`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, '84f71cce385c444726ba336834ddf6b4')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ class ChargeLocationsDaoTest {
|
||||
"https://google.com",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
@@ -68,7 +69,7 @@ class ChargeLocationsDaoTest {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null, null, null, null, null, null, null, Instant.now(), true
|
||||
null, null, null, null, null, null, null, null, Instant.now(), true
|
||||
)
|
||||
}
|
||||
runBlocking {
|
||||
|
||||
5
app/src/automotive/res/values-sv/strings.xml
Normal file
5
app/src/automotive/res/values-sv/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="grant_on_phone">Tillåt</string>
|
||||
<string name="auto_location_permission_needed">Du måste tillåta platsåtkomst för att använda EVMap i din bil.</string>
|
||||
</resources>
|
||||
@@ -1,9 +0,0 @@
|
||||
<?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,44 +2,15 @@ package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.facebook.flipper.android.AndroidFlipperClient
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
|
||||
import com.facebook.soloader.SoLoader
|
||||
import okhttp3.OkHttpClient
|
||||
import timber.log.Timber
|
||||
|
||||
private val networkFlipperPlugin = NetworkFlipperPlugin()
|
||||
|
||||
fun addDebugInterceptors(context: Context) {
|
||||
if (Build.FINGERPRINT == "robolectric") return
|
||||
|
||||
SoLoader.init(context, false)
|
||||
val client = AndroidFlipperClient.getInstance(context)
|
||||
client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()))
|
||||
client.addPlugin(networkFlipperPlugin)
|
||||
client.addPlugin(DatabasesFlipperPlugin(context))
|
||||
client.addPlugin(SharedPreferencesFlipperPlugin(context))
|
||||
client.start()
|
||||
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
6
app/src/foss/res/values-sv/strings.xml
Normal file
6
app/src/foss/res/values-sv/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Tycker du att EVMap är praktisk? Stöd utvecklingen genom att skicka en donation till utvecklaren.</string>
|
||||
<string name="donate_paypal">Donera med PayPal</string>
|
||||
<string name="data_sources_hint">Kartdata i appen tillhandahålls av OpenStreetMap.</string>
|
||||
</resources>
|
||||
5
app/src/google/res/values-sv/strings.xml
Normal file
5
app/src/google/res/values-sv/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Tycker du att EVMap är praktisk? Stöd utvecklingen genom att skicka en donation till utvecklaren.\n\nGoogle tar 15% av alla donationer.</string>
|
||||
<string name="data_sources_hint">I inställningarna kan du välja mellan Google Maps och OpenStreetMap som kartleverantör.</string>
|
||||
</resources>
|
||||
@@ -18,6 +18,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.splashscreen.SplashScreen
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.navigation.NavController
|
||||
@@ -55,6 +56,7 @@ class MapsActivity : AppCompatActivity(),
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val splashScreen = installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.enableEdgeToEdge(window)
|
||||
|
||||
setContentView(R.layout.activity_maps)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.nobil.NobilApiWrapper
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper
|
||||
import net.vonforst.evmap.model.*
|
||||
@@ -94,6 +95,13 @@ fun Context.stringProvider() = object : StringProvider {
|
||||
|
||||
fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
|
||||
return when (type) {
|
||||
"nobil" -> {
|
||||
NobilApiWrapper(
|
||||
ctx.getString(
|
||||
R.string.nobil_key
|
||||
)
|
||||
)
|
||||
}
|
||||
"openchargemap" -> {
|
||||
OpenChargeMapApiWrapper(
|
||||
ctx.getString(
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
package net.vonforst.evmap.api
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.TimeSource
|
||||
|
||||
|
||||
class RateLimitInterceptor : Interceptor {
|
||||
private val rateLimiter = RateLimiter.create(3.0)
|
||||
private val rateLimiter = SimpleRateLimiter(3.0)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
if (request.url.host == "ui-map.shellrecharge.com") {
|
||||
// limit requests sent to NewMotion to 3 per second
|
||||
rateLimiter.acquire(1)
|
||||
rateLimiter.acquire()
|
||||
|
||||
var response: Response = chain.proceed(request)
|
||||
// 403 is how the NewMotion API indicates a rate limit error
|
||||
@@ -30,4 +32,27 @@ class RateLimitInterceptor : Interceptor {
|
||||
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,53 +1,14 @@
|
||||
package net.vonforst.evmap.api
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import net.vonforst.evmap.R
|
||||
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.experimental.ExperimentalTypeInference
|
||||
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(
|
||||
Chargepoint.TYPE_1 to R.string.plug_type_1,
|
||||
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_tethered,
|
||||
Chargepoint.TYPE_2_SOCKET to R.string.plug_type_2,
|
||||
Chargepoint.TYPE_3A to R.string.plug_type_3a,
|
||||
Chargepoint.TYPE_3C to R.string.plug_type_3c,
|
||||
|
||||
@@ -266,6 +266,7 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
"Spanien",
|
||||
"Tschechien"
|
||||
) && charger.network != "Tesla Supercharger"
|
||||
"nobil" -> charger.network != "Tesla"
|
||||
"openchargemap" -> country in listOf(
|
||||
"DE",
|
||||
"AT",
|
||||
|
||||
@@ -86,6 +86,40 @@ class TeslaGuestAvailabilityDetector(
|
||||
}
|
||||
val details = detailsA.await()
|
||||
|
||||
if (location.dataSource == "nobil") {
|
||||
// TODO: Lots of copy & paste here. The main difference for nobil data
|
||||
// is that V2 chargers don't have duplicated connectors.
|
||||
var detailsSorted = details.chargerList
|
||||
.sortedBy { c -> c.labelLetter }
|
||||
.sortedBy { c -> c.labelNumber }
|
||||
|
||||
if (detailsSorted.size != location.chargepoints.size) {
|
||||
// TODO: Tesla data could also be missing for connectors
|
||||
throw AvailabilityDetectorException("charger has unknown connectors")
|
||||
}
|
||||
|
||||
val detailsMap =
|
||||
mutableMapOf<Chargepoint, List<TeslaChargingGuestGraphQlApi.ChargerDetail>>()
|
||||
var i = 0
|
||||
for (connector in location.chargepointsMerged) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
|
||||
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
|
||||
val labelsMap = detailsMap.mapValues { it.value.map { it.label } }
|
||||
|
||||
val pricing = details.pricing?.copy(memberRates = guestPricing.await()?.userRates)
|
||||
|
||||
return ChargeLocationStatus(
|
||||
statusMap,
|
||||
"Tesla",
|
||||
labels = labelsMap,
|
||||
extraData = pricing
|
||||
)
|
||||
}
|
||||
|
||||
val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
|
||||
val scV2CCSConnectors = location.chargepoints.filter {
|
||||
it.type in listOf(
|
||||
@@ -166,6 +200,7 @@ class TeslaGuestAvailabilityDetector(
|
||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||
return when (charger.dataSource) {
|
||||
"goingelectric" -> charger.network == "Tesla Supercharger"
|
||||
"nobil" -> charger.network == "Tesla"
|
||||
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
||||
"openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla")
|
||||
else -> false
|
||||
|
||||
@@ -67,7 +67,54 @@ class TeslaOwnerAvailabilityDetector(
|
||||
)
|
||||
).data.charging.site ?: throw AvailabilityDetectorException("no candidates found.")
|
||||
|
||||
if (location.dataSource == "nobil") {
|
||||
// TODO: Lots of copy & paste here. The main difference for nobil data
|
||||
// is that V2 chargers don't have duplicated connectors.s
|
||||
val chargerDetails = details.siteDynamic.chargerDetails
|
||||
val chargers = details.siteStatic.chargers.associateBy { it.id }
|
||||
var detailsSorted = chargerDetails
|
||||
.sortedBy { c -> c.charger.labelLetter ?: chargers[c.charger.id]?.labelLetter }
|
||||
.sortedBy { c -> c.charger.labelNumber ?: chargers[c.charger.id]?.labelNumber }
|
||||
if (detailsSorted.size != location.chargepoints.size) {
|
||||
// TODO: Code below suggests tesla data could also be missing for
|
||||
// connectors
|
||||
throw AvailabilityDetectorException("Tesla API chargepoints do not match data source")
|
||||
}
|
||||
|
||||
val congestionHistogram = details.congestionPriceHistogram?.let { cph ->
|
||||
val indexOfMidnight = cph.dataAttributes.indexOfFirst { it.label == "12AM" }
|
||||
indexOfMidnight.takeIf { it >= 0 }?.let { index ->
|
||||
val data = cph.data.toMutableList()
|
||||
Collections.rotate(data, -index)
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
val detailsMap =
|
||||
emptyMap<Chargepoint, List<TeslaChargingOwnershipGraphQlApi.ChargerDetail>>().toMutableMap()
|
||||
var i = 0
|
||||
for (connector in location.chargepointsMerged) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
|
||||
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
|
||||
|
||||
val labelsMap = detailsMap.mapValues {
|
||||
it.value.map {
|
||||
it.charger.label?.value ?: chargers[it.charger.id]?.label?.value
|
||||
}
|
||||
}
|
||||
|
||||
return ChargeLocationStatus(
|
||||
statusMap,
|
||||
"Tesla",
|
||||
labels = labelsMap,
|
||||
congestionHistogram = congestionHistogram,
|
||||
extraData = details.pricing
|
||||
)
|
||||
}
|
||||
val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
|
||||
val scV2CCSConnectors = location.chargepoints.filter {
|
||||
it.type in listOf(
|
||||
@@ -165,6 +212,7 @@ class TeslaOwnerAvailabilityDetector(
|
||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||
return when (charger.dataSource) {
|
||||
"goingelectric" -> charger.network == "Tesla Supercharger"
|
||||
"nobil" -> charger.network == "Tesla"
|
||||
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
||||
"openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla")
|
||||
else -> false
|
||||
|
||||
@@ -100,7 +100,8 @@ interface TeslaAuthenticationApi {
|
||||
.appendQueryParameter("code_challenge_method", "S256")
|
||||
.appendQueryParameter("redirect_uri", "https://auth.tesla.com/void/callback")
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.appendQueryParameter("scope", "openid email offline_access")
|
||||
.appendQueryParameter("scope", "openid email offline_access phone")
|
||||
.appendQueryParameter("is_in_app", "true")
|
||||
.appendQueryParameter("state", "123").build()
|
||||
|
||||
val resultUrlPrefix = "https://auth.tesla.com/void/callback"
|
||||
|
||||
@@ -77,6 +77,7 @@ data class GEChargeLocation(
|
||||
address.convert(),
|
||||
chargepoints.map { it.convert() },
|
||||
network,
|
||||
"https://www.goingelectric.de/",
|
||||
"https:${url}",
|
||||
"https:${url}edit/",
|
||||
faultReport?.convert(),
|
||||
@@ -88,6 +89,7 @@ data class GEChargeLocation(
|
||||
locationDescription,
|
||||
photos?.map { it.convert(apikey) },
|
||||
chargecards?.map { it.convert() },
|
||||
null,
|
||||
openinghours?.convert(),
|
||||
cost?.convert(),
|
||||
null,
|
||||
|
||||
128
app/src/main/java/net/vonforst/evmap/api/nobil/NobilAdapters.kt
Normal file
128
app/src/main/java/net/vonforst/evmap/api/nobil/NobilAdapters.kt
Normal file
@@ -0,0 +1,128 @@
|
||||
package net.vonforst.evmap.api.nobil
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.squareup.moshi.rawType
|
||||
import net.vonforst.evmap.model.Coordinate
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import java.lang.reflect.Type
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
internal class CoordinateAdapter {
|
||||
@FromJson
|
||||
fun fromJson(position: String): Coordinate {
|
||||
val pattern = """\((\d+(\.\d+)?), *(-?\d+(\.\d+)?)\)"""
|
||||
val match = Regex(pattern).matchEntire(position)
|
||||
?: throw JsonDataException("Unexpected coordinate format: '$position'")
|
||||
|
||||
val latitude : String = match.groups[1]?.value ?: "0.0"
|
||||
val longitude : String = match.groups[3]?.value ?: "0.0"
|
||||
return Coordinate(latitude.toDouble(), longitude.toDouble())
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: Coordinate): String = "(" + value.lat + ", " + value.lng + ")"
|
||||
}
|
||||
|
||||
internal class LocalDateTimeAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String?): LocalDateTime? = value?.let {
|
||||
LocalDateTime.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: LocalDateTime?): String? = value?.toString()
|
||||
}
|
||||
|
||||
internal class NobilConverterFactory(val moshi: Moshi) : Converter.Factory() {
|
||||
override fun responseBodyConverter(
|
||||
type: Type,
|
||||
annotations: Array<out Annotation>,
|
||||
retrofit: Retrofit
|
||||
): Converter<ResponseBody, *>? {
|
||||
val stringAdapter = moshi.adapter(String::class.java)
|
||||
|
||||
if (type.rawType == NobilNumChargepointsResponseData::class.java) {
|
||||
// {"Provider":"NOBIL.no",
|
||||
// "Rights":"Creative Commons Attribution 4.0 International License",
|
||||
// "apiver":"3",
|
||||
// "chargerstations": [{"count":8748}]
|
||||
// }
|
||||
return Converter<ResponseBody, NobilNumChargepointsResponseData> { body ->
|
||||
val reader = JsonReader.of(body.source())
|
||||
reader.beginObject()
|
||||
|
||||
var error: String? = null
|
||||
var provider: String? = null
|
||||
var rights: String? = null
|
||||
var apiver: String? = null
|
||||
var count: Int? = null
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"error" -> error = stringAdapter.fromJson(reader)!!
|
||||
"Provider" -> provider = stringAdapter.fromJson(reader)!!
|
||||
"Rights" -> rights = stringAdapter.fromJson(reader)!!
|
||||
"apiver" -> apiver = stringAdapter.fromJson(reader)!!
|
||||
"chargerstations" -> {
|
||||
reader.beginArray()
|
||||
val intAdapter = moshi.adapter(Int::class.java)
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"count" -> count = intAdapter.fromJson(reader)!!
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
reader.endArray()
|
||||
reader.close()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
NobilNumChargepointsResponseData(error, provider, rights, apiver, count)
|
||||
}
|
||||
}
|
||||
|
||||
if (type.rawType == NobilDynamicResponseData::class.java) {
|
||||
val nobilChargerStationAdapter = moshi.adapter(NobilChargerStation::class.java)
|
||||
return Converter<ResponseBody, NobilDynamicResponseData> { body ->
|
||||
val reader = JsonReader.of(body.source())
|
||||
reader.beginObject()
|
||||
|
||||
var error: String? = null
|
||||
var provider: String? = null
|
||||
var rights: String? = null
|
||||
var apiver: String? = null
|
||||
var doc: Sequence<NobilChargerStation>? = null
|
||||
while (reader.hasNext()) {
|
||||
when (reader.nextName()) {
|
||||
"error" -> error = stringAdapter.fromJson(reader)!!
|
||||
"Provider" -> provider = stringAdapter.fromJson(reader)!!
|
||||
"Rights" -> rights = stringAdapter.fromJson(reader)!!
|
||||
"apiver" -> apiver = stringAdapter.fromJson(reader)!!
|
||||
"chargerstations" -> {
|
||||
doc = sequence {
|
||||
reader.beginArray()
|
||||
while (reader.hasNext()) {
|
||||
yield(nobilChargerStationAdapter.fromJson(reader)!!)
|
||||
}
|
||||
reader.endArray()
|
||||
reader.close()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
NobilDynamicResponseData(error, provider, rights, apiver, doc)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
354
app/src/main/java/net/vonforst/evmap/api/nobil/NobilApi.kt
Normal file
354
app/src/main/java/net/vonforst/evmap/api/nobil/NobilApi.kt
Normal file
@@ -0,0 +1,354 @@
|
||||
package net.vonforst.evmap.api.nobil
|
||||
|
||||
import android.content.Context
|
||||
import android.database.DatabaseUtils
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import com.squareup.moshi.JsonDataException
|
||||
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.powerSteps
|
||||
import net.vonforst.evmap.model.BooleanFilter
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.model.ChargepointListItem
|
||||
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.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
|
||||
private const val maxResults = 2000
|
||||
|
||||
interface NobilApi {
|
||||
@GET("datadump.php")
|
||||
suspend fun getAllChargingStations(
|
||||
@Query("apikey") apikey: String,
|
||||
@Query("format") dataFormat: String = "json"
|
||||
): Response<NobilDynamicResponseData>
|
||||
|
||||
@POST("search.php")
|
||||
suspend fun getNumChargepoints(
|
||||
@Body request: NobilNumChargepointsRequest
|
||||
): Response<NobilNumChargepointsResponseData>
|
||||
|
||||
@POST("search.php")
|
||||
suspend fun getChargepoints(
|
||||
@Body request: NobilRectangleSearchRequest
|
||||
): Response<NobilResponseData>
|
||||
|
||||
@POST("search.php")
|
||||
suspend fun getChargepointsRadius(
|
||||
@Body request: NobilRadiusSearchRequest
|
||||
): Response<NobilResponseData>
|
||||
|
||||
@POST("search.php")
|
||||
suspend fun getChargepointDetail(
|
||||
@Body request: NobilDetailSearchRequest
|
||||
): Response<NobilResponseData>
|
||||
|
||||
companion object {
|
||||
private val cacheSize = 10L * 1024 * 1024 // 10MB
|
||||
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(LocalDateTimeAdapter())
|
||||
.add(CoordinateAdapter())
|
||||
.build()
|
||||
|
||||
fun create(
|
||||
baseurl: String,
|
||||
context: Context?
|
||||
): NobilApi {
|
||||
val client = OkHttpClient.Builder().apply {
|
||||
if (BuildConfig.DEBUG) {
|
||||
addDebugInterceptors()
|
||||
}
|
||||
if (context != null) {
|
||||
cache(Cache(context.cacheDir, cacheSize))
|
||||
}
|
||||
}.build()
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseurl)
|
||||
.addConverterFactory(NobilConverterFactory(moshi))
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.client(client)
|
||||
.build()
|
||||
return retrofit.create(NobilApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NobilApiWrapper(
|
||||
val apikey: String,
|
||||
baseurl: String = "https://nobil.no/api/server/",
|
||||
context: Context? = null
|
||||
) : ChargepointApi<NobilReferenceData> {
|
||||
override val name = "Nobil"
|
||||
override val id = "nobil"
|
||||
override val supportsOnlineQueries = false // Online queries are supported, but can't be used together with full downloads
|
||||
override val supportsFullDownload = true
|
||||
override val cacheLimit = Duration.ofDays(300L)
|
||||
val api = NobilApi.create(baseurl, context)
|
||||
|
||||
override suspend fun fullDownload(): FullDownloadResult<NobilReferenceData> {
|
||||
var numTotalChargepoints = 0
|
||||
arrayOf("DAN", "FIN", "ISL", "NOR", "SWE").forEach { countryCode ->
|
||||
val request = NobilNumChargepointsRequest(apikey, countryCode)
|
||||
val response = api.getNumChargepoints(request)
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException(response.message())
|
||||
}
|
||||
val numChargepoints = response.body()!!.count
|
||||
?: throw JsonDataException("Failed to get chargepoint count for '$countryCode'")
|
||||
numTotalChargepoints += numChargepoints
|
||||
}
|
||||
|
||||
val response = api.getAllChargingStations(apikey)
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException(response.message())
|
||||
} else {
|
||||
val data = response.body()!!
|
||||
return NobilFullDownloadResult(data, numTotalChargepoints)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getChargepoints(
|
||||
referenceData: ReferenceData,
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
useClustering: Boolean,
|
||||
filters: FilterValues?,
|
||||
): Resource<ChargepointList> {
|
||||
try {
|
||||
val northeast = "(" + bounds.northeast.latitude + ", " + bounds.northeast.longitude + ")"
|
||||
val southwest = "(" + bounds.southwest.latitude + ", " + bounds.southwest.longitude + ")"
|
||||
val request = NobilRectangleSearchRequest(apikey, northeast, southwest, maxResults)
|
||||
val response = api.getChargepoints(request)
|
||||
if (!response.isSuccessful) {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
|
||||
val data = response.body()!!
|
||||
if (data.chargerStations == null) {
|
||||
return Resource.success(ChargepointList.empty())
|
||||
}
|
||||
val result = postprocessResult(
|
||||
data,
|
||||
filters
|
||||
)
|
||||
return Resource.success(ChargepointList(result, data.chargerStations.size < maxResults))
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getChargepointsRadius(
|
||||
referenceData: ReferenceData,
|
||||
location: LatLng,
|
||||
radius: Int,
|
||||
zoom: Float,
|
||||
useClustering: Boolean,
|
||||
filters: FilterValues?
|
||||
): Resource<ChargepointList> {
|
||||
try {
|
||||
val request = NobilRadiusSearchRequest(apikey, location.latitude, location.longitude, radius * 1000.0, maxResults)
|
||||
val response = api.getChargepointsRadius(request)
|
||||
if (!response.isSuccessful) {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
|
||||
val data = response.body()!!
|
||||
if (data.chargerStations == null) {
|
||||
return Resource.error(response.message(), null)
|
||||
}
|
||||
val result = postprocessResult(
|
||||
data,
|
||||
filters
|
||||
)
|
||||
return Resource.success(ChargepointList(result, data.chargerStations.size < maxResults))
|
||||
} catch (e: IOException) {
|
||||
return Resource.error(e.message, null)
|
||||
} catch (e: HttpException) {
|
||||
return Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun postprocessResult(
|
||||
data: NobilResponseData,
|
||||
filters: FilterValues?
|
||||
): List<ChargepointListItem> {
|
||||
if (data.rights == null ) throw JsonDataException("Rights field is missing in received data")
|
||||
|
||||
return data.chargerStations!!.mapNotNull { it.convert(data.rights, filters) }.distinct()
|
||||
}
|
||||
|
||||
override suspend fun getChargepointDetail(
|
||||
referenceData: ReferenceData,
|
||||
id: Long
|
||||
): Resource<ChargeLocation> {
|
||||
// TODO: Nobil ids are "SWE_1234", not Long
|
||||
return Resource.error("getChargepointDetail is not implemented", null)
|
||||
}
|
||||
|
||||
override suspend fun getReferenceData(): Resource<NobilReferenceData> {
|
||||
return Resource.success(NobilReferenceData(0))
|
||||
}
|
||||
|
||||
override fun getFilters(
|
||||
referenceData: ReferenceData,
|
||||
sp: StringProvider
|
||||
): List<Filter<FilterValue>> {
|
||||
val connectors = listOf(
|
||||
Chargepoint.TYPE_1,
|
||||
Chargepoint.TYPE_2_SOCKET,
|
||||
Chargepoint.TYPE_2_PLUG,
|
||||
Chargepoint.CCS_UNKNOWN,
|
||||
Chargepoint.CHADEMO,
|
||||
Chargepoint.SUPERCHARGER
|
||||
)
|
||||
val connectorsMap = connectors.associateWith { connector ->
|
||||
nameForPlugType(sp, connector)
|
||||
}
|
||||
val accessibilityMap = mapOf(
|
||||
"Public" to sp.getString(R.string.accessibility_public),
|
||||
"Visitors" to sp.getString(R.string.accessibility_visitors),
|
||||
"Employees" to sp.getString(R.string.accessibility_employees),
|
||||
"By appointment" to sp.getString(R.string.accessibility_by_appointment),
|
||||
"Residents" to sp.getString(R.string.accessibility_residents)
|
||||
)
|
||||
return listOf(
|
||||
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",
|
||||
connectorsMap, manyChoices = true
|
||||
),
|
||||
SliderFilter(
|
||||
sp.getString(R.string.filter_min_connectors),
|
||||
"min_connectors",
|
||||
10,
|
||||
min = 1
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
sp.getString(R.string.filter_accessibility), "accessibilities",
|
||||
accessibilityMap, manyChoices = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun convertFiltersToSQL(
|
||||
filters: FilterValues,
|
||||
referenceData: ReferenceData
|
||||
): FiltersSQLQuery {
|
||||
if (filters.isEmpty()) return FiltersSQLQuery("",
|
||||
requiresChargepointQuery = false,
|
||||
requiresChargeCardQuery = false
|
||||
)
|
||||
|
||||
var requiresChargepointQuery = false
|
||||
val result = StringBuilder()
|
||||
|
||||
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 = 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 accessibilities = filters.getMultipleChoiceValue("accessibilities")
|
||||
if (accessibilities != null && !accessibilities.all) {
|
||||
val accessibilitiesList = accessibilities.values.joinToString(",") {
|
||||
DatabaseUtils.sqlEscapeString(it)
|
||||
}
|
||||
result.append(" AND accessibility IN (${accessibilitiesList})")
|
||||
}
|
||||
|
||||
return FiltersSQLQuery(result.toString(), requiresChargepointQuery, false)
|
||||
}
|
||||
|
||||
override fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class NobilFullDownloadResult(private val data: NobilDynamicResponseData,
|
||||
private val numTotalChargepoints: Int) : FullDownloadResult<NobilReferenceData> {
|
||||
private var downloadProgress = 0f
|
||||
private var refData: NobilReferenceData? = null
|
||||
|
||||
override val chargers: Sequence<ChargeLocation>
|
||||
get() {
|
||||
if (data.rights == null) throw JsonDataException("Rights field is missing in received data")
|
||||
return sequence {
|
||||
data.chargerStations?.forEachIndexed { i, it ->
|
||||
downloadProgress = i.toFloat() / numTotalChargepoints
|
||||
val charger = it.convert(data.rights, null)
|
||||
charger?.let { yield(charger) }
|
||||
}
|
||||
refData = NobilReferenceData(0)
|
||||
}
|
||||
}
|
||||
override val progress: Float
|
||||
get() = downloadProgress
|
||||
override val referenceData: NobilReferenceData
|
||||
get() = refData ?: throw UnsupportedOperationException("referenceData is only available once download is complete")
|
||||
}
|
||||
348
app/src/main/java/net/vonforst/evmap/api/nobil/NobilModel.kt
Normal file
348
app/src/main/java/net/vonforst/evmap/api/nobil/NobilModel.kt
Normal file
@@ -0,0 +1,348 @@
|
||||
package net.vonforst.evmap.api.nobil
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.text.HtmlCompat
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import net.vonforst.evmap.max
|
||||
import net.vonforst.evmap.model.Address
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.model.ChargerPhoto
|
||||
import net.vonforst.evmap.model.Coordinate
|
||||
import net.vonforst.evmap.model.Cost
|
||||
import net.vonforst.evmap.model.FilterValues
|
||||
import net.vonforst.evmap.model.OpeningHours
|
||||
import net.vonforst.evmap.model.ReferenceData
|
||||
import net.vonforst.evmap.model.getBooleanValue
|
||||
import net.vonforst.evmap.model.getMultipleChoiceValue
|
||||
import net.vonforst.evmap.model.getSliderValue
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class NobilReferenceData(
|
||||
val dummy: Int
|
||||
) : ReferenceData()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilNumChargepointsRequest(
|
||||
val apikey: String,
|
||||
val countrycode: String,
|
||||
val action: String = "search",
|
||||
val type: String = "stats_GetSumChargerstations",
|
||||
val format: String = "json",
|
||||
val apiversion: String = "3"
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilRectangleSearchRequest(
|
||||
val apikey: String,
|
||||
val northeast: String,
|
||||
val southwest: String,
|
||||
val limit: Int,
|
||||
val action: String = "search",
|
||||
val type: String = "rectangle",
|
||||
val format: String = "json",
|
||||
val apiversion: String = "3",
|
||||
// val existingids: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilRadiusSearchRequest(
|
||||
val apikey: String,
|
||||
val lat: Double,
|
||||
val long: Double,
|
||||
val distance: Double, // meters
|
||||
val limit: Int,
|
||||
val action: String = "search",
|
||||
val type: String = "near",
|
||||
val format: String = "json",
|
||||
val apiversion: String = "3",
|
||||
// val existingids: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilDetailSearchRequest(
|
||||
val apikey: String,
|
||||
val id: String,
|
||||
val action: String = "search",
|
||||
val type: String = "id",
|
||||
val format: String = "json",
|
||||
val apiversion: String = "3",
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilResponseData(
|
||||
@Json(name = "error") val error: String?,
|
||||
@Json(name = "Provider") val provider: String?,
|
||||
@Json(name = "Rights") val rights: String?,
|
||||
@Json(name = "apiver") val apiver: String?,
|
||||
@Json(name = "chargerstations") val chargerStations: List<NobilChargerStation>?
|
||||
)
|
||||
|
||||
data class NobilNumChargepointsResponseData(
|
||||
val error: String?,
|
||||
val provider: String?,
|
||||
val rights: String?,
|
||||
val apiver: String?,
|
||||
val count: Int?
|
||||
)
|
||||
|
||||
data class NobilDynamicResponseData(
|
||||
val error: String?,
|
||||
val provider: String?,
|
||||
val rights: String?,
|
||||
val apiver: String?,
|
||||
val chargerStations: Sequence<NobilChargerStation>?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilChargerStation(
|
||||
@Json(name = "csmd") val chargerStationData: NobilChargerStationData,
|
||||
@Json(name = "attr") val chargerStationAttributes: NobilChargerStationAttributes
|
||||
) {
|
||||
fun convert(dataLicense: String,
|
||||
filters: FilterValues?) : ChargeLocation? {
|
||||
val chargepoints = chargerStationAttributes.conn
|
||||
.mapNotNull { createChargepointFromNobilConnection(it.value) }
|
||||
if (chargepoints.isEmpty()) return null
|
||||
|
||||
val minPower = filters?.getSliderValue("min_power")
|
||||
val connectors = filters?.getMultipleChoiceValue("connectors")
|
||||
val minConnectors = filters?.getSliderValue("min_connectors")
|
||||
if (chargepoints
|
||||
.filter { it.power != null && it.power >= (minPower ?: 0) }
|
||||
.filter { if (connectors != null && !connectors.all) it.type in connectors.values else true }
|
||||
.size < (minConnectors ?: 0)) return null
|
||||
|
||||
val chargeLocation = ChargeLocation(
|
||||
chargerStationData.id,
|
||||
"nobil",
|
||||
HtmlCompat.fromHtml(chargerStationData.name, HtmlCompat.FROM_HTML_MODE_COMPACT)
|
||||
.toString(),
|
||||
chargerStationData.position,
|
||||
Address(
|
||||
chargerStationData.city,
|
||||
when (chargerStationData.landCode) {
|
||||
"DAN" -> "Denmark"
|
||||
"FIN" -> "Finland"
|
||||
"ISL" -> "Iceland"
|
||||
"NOR" -> "Norway"
|
||||
"SWE" -> "Sweden"
|
||||
else -> ""
|
||||
},
|
||||
chargerStationData.zipCode,
|
||||
listOfNotNull(
|
||||
chargerStationData.street,
|
||||
chargerStationData.houseNumber
|
||||
).joinToString(" ")
|
||||
),
|
||||
chargepoints,
|
||||
if (chargerStationData.operator != null) HtmlCompat.fromHtml(
|
||||
chargerStationData.operator,
|
||||
HtmlCompat.FROM_HTML_MODE_COMPACT
|
||||
).toString() else null,
|
||||
"https://nobil.no/",
|
||||
null,
|
||||
when (chargerStationData.landCode) {
|
||||
"SWE" -> "https://www.energimyndigheten.se/klimat/transporter/laddinfrastruktur/registrera-din-laddstation/elbilsagare/"
|
||||
else -> "mailto:post@nobil.no?subject=" + Uri.encode("Regarding charging station " + chargerStationData.internationalId)
|
||||
},
|
||||
null,
|
||||
chargerStationData.ocpiId != null ||
|
||||
chargerStationData.updated.isAfter(LocalDateTime.now().minusMonths(6)),
|
||||
null,
|
||||
if (chargerStationData.ownedBy != null) HtmlCompat.fromHtml(
|
||||
chargerStationData.ownedBy,
|
||||
HtmlCompat.FROM_HTML_MODE_COMPACT
|
||||
).toString() else null,
|
||||
if (chargerStationData.userComment != null) HtmlCompat.fromHtml(
|
||||
chargerStationData.userComment,
|
||||
HtmlCompat.FROM_HTML_MODE_COMPACT
|
||||
).toString() else null,
|
||||
null,
|
||||
if (chargerStationData.description != null) HtmlCompat.fromHtml(
|
||||
chargerStationData.description,
|
||||
HtmlCompat.FROM_HTML_MODE_COMPACT
|
||||
).toString() else null,
|
||||
if (Regex("""\d+\.\w+""").matchEntire(chargerStationData.image) != null) listOf(
|
||||
NobilChargerPhotoAdapter(chargerStationData.image)
|
||||
) else null,
|
||||
null,
|
||||
// 2: Availability
|
||||
chargerStationAttributes.st["2"]?.attrTrans,
|
||||
// 24: Open 24h
|
||||
if (chargerStationAttributes.st["24"]?.attrTrans == "Yes") OpeningHours(
|
||||
twentyfourSeven = true,
|
||||
null,
|
||||
null
|
||||
) else null,
|
||||
Cost(
|
||||
// 7: Parking fee
|
||||
freeparking = when (chargerStationAttributes.st["7"]?.attrTrans) {
|
||||
"Yes" -> false
|
||||
"No" -> true
|
||||
else -> null
|
||||
},
|
||||
descriptionLong = chargerStationAttributes.conn.mapNotNull {
|
||||
// 19: Payment method
|
||||
when (it.value["19"]?.attrValId) {
|
||||
"1" -> listOf("Mobile phone") // TODO: Translate
|
||||
"2" -> listOf("Bank card")
|
||||
"10" -> listOf("Other")
|
||||
"20" -> listOf("Mobile phone", "Charging card")
|
||||
"21" -> listOf("Bank card", "Charging card")
|
||||
"25" -> listOf("Bank card", "Charging card", "Mobile phone")
|
||||
else -> null
|
||||
}
|
||||
}.flatten().sorted().toSet().ifEmpty { null }
|
||||
?.joinToString(prefix = "Accepted payment methods: ")
|
||||
),
|
||||
dataLicense,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Instant.now(),
|
||||
true
|
||||
)
|
||||
|
||||
val accessibilities = filters?.getMultipleChoiceValue("accessibilities")
|
||||
if (accessibilities != null && !accessibilities.all) {
|
||||
if (!accessibilities.values.contains(chargeLocation.accessibility)) return null
|
||||
}
|
||||
|
||||
val freeparking = filters?.getBooleanValue("freeparking")
|
||||
if (freeparking == true && chargeLocation.cost?.freeparking != true) return null
|
||||
|
||||
val open247 = filters?.getBooleanValue("open_247")
|
||||
if (open247 == true && chargeLocation.openinghours?.twentyfourSeven != true) return null
|
||||
|
||||
return chargeLocation
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createChargepointFromNobilConnection(attribs: Map<String, NobilChargerStationGenericAttribute>): Chargepoint? {
|
||||
// https://nobil.no/admin/attributes.php
|
||||
|
||||
val isFixedCable = attribs["25"]?.attrTrans == "Yes"
|
||||
val connectionType = when (attribs["4"]?.attrValId) {
|
||||
"0" -> "" // Unspecified
|
||||
"30" -> Chargepoint.CHADEMO // CHAdeMO
|
||||
"31" -> Chargepoint.TYPE_1 // Type 1
|
||||
"32" -> if (isFixedCable) Chargepoint.TYPE_2_PLUG else Chargepoint.TYPE_2_SOCKET // Type 2
|
||||
"39" -> Chargepoint.CCS_UNKNOWN // CCS/Combo
|
||||
"40" -> Chargepoint.SUPERCHARGER // Tesla Connector Model
|
||||
"70" -> return null // Hydrogen
|
||||
"82" -> return null // Biogas
|
||||
"87" -> "" // MCS
|
||||
|
||||
// These are deprecated and not used
|
||||
"50" -> "" // Type 2 + Schuko
|
||||
"60" -> "" // Type1/Type2
|
||||
|
||||
else -> ""
|
||||
}
|
||||
|
||||
val connectionPower = when (attribs["5"]?.attrValId) {
|
||||
"7" -> 3.6 // 3,6 kW - 230V 1-phase max 16A
|
||||
"8" -> 7.4 // 7,4 kW - 230V 1-phase max 32A
|
||||
"10" -> 11.0 // 11 kW - 400V 3-phase max 16A
|
||||
"11" -> 22.0 // 22 kW - 400V 3-phase max 32A
|
||||
"12" -> 43.0 // 43 kW - 400V 3-phase max 63A
|
||||
"13" -> 50.0 // 50 kW - 500VDC max 100A
|
||||
"16" -> 11.0 // 230V 3-phase max 16A'
|
||||
"17" -> 22.0 // 230V 3-phase max 32A
|
||||
"18" -> 43.0 // 230V 3-phase max 63A
|
||||
"19" -> 20.0 // 20 kW - 500VDC max 50A
|
||||
"22" -> 135.0 // 135 kW - 480VDC max 270A
|
||||
"23" -> 100.0 // 100 kW - 500VDC max 200A
|
||||
"24" -> 150.0 // 150 kW DC
|
||||
"25" -> 350.0 // 350 kW DC
|
||||
"26" -> null // 350 bar
|
||||
"27" -> null // 700 bar
|
||||
"29" -> 75.0 // 75 kW DC
|
||||
"30" -> 225.0 // 225 kW DC
|
||||
"31" -> 250.0 // 250 kW DC
|
||||
"32" -> 200.0 // 200 kW DC
|
||||
"33" -> 300.0 // 300 kW DC
|
||||
"34" -> null // CBG
|
||||
"35" -> null // LBG
|
||||
"36" -> 400.0 // 400 kW DC
|
||||
"37" -> 30.0 // 30 kW DC
|
||||
"38" -> 62.5 // 62,5 kW DC
|
||||
"39" -> 500.0 // 500 kW DC
|
||||
"41" -> 175.0 // 175 kW DC
|
||||
"42" -> 180.0 // 180 kW DC
|
||||
"43" -> 600.0 // 600 kW DC
|
||||
"44" -> 700.0 // 700 kW DC
|
||||
"45" -> 800.0 // 800 kW DC
|
||||
else -> null
|
||||
}
|
||||
|
||||
val connectionVoltage = if (attribs["12"]?.attrVal is String) attribs["12"]?.attrVal.toString().toDoubleOrNull() else null
|
||||
val connectionCurrent = if (attribs["31"]?.attrVal is String) attribs["31"]?.attrVal.toString().toDoubleOrNull() else null
|
||||
val evseId = if (attribs["28"]?.attrVal is String) listOf(attribs["28"]?.attrVal.toString()) else null
|
||||
|
||||
return Chargepoint(connectionType, connectionPower, 1, connectionCurrent, connectionVoltage, evseId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilChargerStationData(
|
||||
@Json(name = "id") val id: Long,
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "ocpidb_mapping_stasjon_id") val ocpiId: String?,
|
||||
@Json(name = "Street") val street: String?,
|
||||
@Json(name = "House_number") val houseNumber: String,
|
||||
@Json(name = "Zipcode") val zipCode: String?,
|
||||
@Json(name = "City") val city: String?,
|
||||
@Json(name = "Municipality_ID") val municipalityId: String,
|
||||
@Json(name = "Municipality") val municipality: String,
|
||||
@Json(name = "County_ID") val countyId: String,
|
||||
@Json(name = "County") val county: String,
|
||||
@Json(name = "Description_of_location") val description: String?,
|
||||
@Json(name = "Owned_by") val ownedBy: String?,
|
||||
@Json(name = "Operator") val operator: String?,
|
||||
@Json(name = "Number_charging_points") val numChargePoints: Int,
|
||||
@Json(name = "Position") val position: Coordinate,
|
||||
@Json(name = "Image") val image: String,
|
||||
@Json(name = "Available_charging_points") val availableChargePoints: Int,
|
||||
@Json(name = "User_comment") val userComment: String?,
|
||||
@Json(name = "Contact_info") val contactInfo: String?,
|
||||
@Json(name = "Created") val created: LocalDateTime,
|
||||
@Json(name = "Updated") val updated: LocalDateTime,
|
||||
@Json(name = "Station_status") val stationStatus: Int,
|
||||
@Json(name = "Land_code") val landCode: String,
|
||||
@Json(name = "International_id") val internationalId: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilChargerStationAttributes(
|
||||
@Json(name = "st") val st: Map<String, NobilChargerStationGenericAttribute>,
|
||||
@Json(name = "conn") val conn: Map<String, Map<String, NobilChargerStationGenericAttribute>>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NobilChargerStationGenericAttribute(
|
||||
@Json(name = "attrtypeid") val attrTypeId: String,
|
||||
@Json(name = "attrname") val attrName: String,
|
||||
@Json(name = "attrvalid") val attrValId: String,
|
||||
@Json(name = "trans") val attrTrans: String,
|
||||
@Json(name = "attrval") val attrVal: Any
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@JsonClass(generateAdapter = true)
|
||||
class NobilChargerPhotoAdapter(override val id: String) :
|
||||
ChargerPhoto(id) {
|
||||
override fun getUrl(height: Int?, width: Int?, size: Int?, allowOriginal: Boolean): String {
|
||||
val maxSize = size ?: max(height, width)
|
||||
return "https://www.nobil.no/img/ladestasjonbilder/" +
|
||||
when (maxSize) {
|
||||
in 0..50 -> "tn_$id"
|
||||
else -> id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ data class OCMChargepoint(
|
||||
addressInfo.toAddress(refData),
|
||||
connections.map { it.convert(refData) },
|
||||
operatorInfo?.title ?: refData.operators.find { it.id == operatorId }?.title,
|
||||
"https://openchargemap.org/",
|
||||
"https://map.openchargemap.io/?id=$id",
|
||||
"https://map.openchargemap.io/?id=$id",
|
||||
convertFaultReport(),
|
||||
@@ -76,6 +77,7 @@ data class OCMChargepoint(
|
||||
mediaItems?.mapNotNull { it.convert() },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
cost?.takeIf { it.isNotBlank() }.let { Cost(descriptionShort = it) },
|
||||
dataProvider?.let { "© ${it.title}" + if (it.license != null) ". ${it.license}" else "" },
|
||||
ChargepriceData(
|
||||
|
||||
@@ -98,6 +98,7 @@ data class OSMChargingStation(
|
||||
getAddress(),
|
||||
getChargepoints(),
|
||||
tags["network"],
|
||||
"https://www.openstreetmap.org/",
|
||||
"https://www.openstreetmap.org/node/$id",
|
||||
"https://www.openstreetmap.org/edit?node=$id",
|
||||
null,
|
||||
@@ -109,6 +110,7 @@ data class OSMChargingStation(
|
||||
null,
|
||||
getPhotos(),
|
||||
null,
|
||||
null,
|
||||
getOpeningHours(),
|
||||
getCost(),
|
||||
"© OpenStreetMap contributors",
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.vonforst.evmap.auto
|
||||
import android.animation.ValueAnimator
|
||||
import android.app.Presentation
|
||||
import android.content.Context
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.hardware.display.VirtualDisplay
|
||||
@@ -39,10 +40,6 @@ class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCorou
|
||||
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)
|
||||
|
||||
@@ -173,14 +170,22 @@ class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCorou
|
||||
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))
|
||||
if (scaleFactor == 2f) {
|
||||
map.animateCamera(
|
||||
map.cameraUpdateFactory.zoomBy(
|
||||
scaleFactor - 1,
|
||||
Point(focusX.roundToInt(), focusY.roundToInt())
|
||||
)
|
||||
)
|
||||
} else {
|
||||
map.moveCamera(map.cameraUpdateFactory.zoomBy(scaleFactor - 1))
|
||||
map.moveCamera(map.cameraUpdateFactory.scrollBy(offsetX, offsetY))
|
||||
}
|
||||
dispatchCameraMoveStarted()
|
||||
}
|
||||
|
||||
@@ -243,9 +248,11 @@ class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCorou
|
||||
}
|
||||
|
||||
private fun offsetY(y: Float): Float {
|
||||
if (!STATUSBAR_OFFSET_SYSTEMS.any { Build.FINGERPRINT.startsWith(it) }) return y
|
||||
if (BuildConfig.FLAVOR_automotive != "automotive") {
|
||||
return y
|
||||
}
|
||||
|
||||
// In some emulators, touch locations are offset by the status bar height
|
||||
// On AAOS, touch locations seem to be 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
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
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 androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.add
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.fragment.oauth.OAuthLoginFragment
|
||||
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (savedInstanceState == null) {
|
||||
@@ -22,10 +29,14 @@ class OAuthLoginActivity : AppCompatActivity(R.layout.activity_oauth_login) {
|
||||
}
|
||||
}
|
||||
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(object : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context, intent: Intent) {
|
||||
finish()
|
||||
val url = intent.getStringExtra(OAuthLoginFragment.EXTRA_URL)!!
|
||||
supportFragmentManager.setFragmentResultListener(url, this) { _, result ->
|
||||
val resultUrl = result.getString(OAuthLoginFragment.EXTRA_URL) ?: return@setFragmentResultListener
|
||||
resultRegistry[url]?.let { flow ->
|
||||
flow.tryEmit(resultUrl)
|
||||
resultRegistry.remove(url)
|
||||
}
|
||||
}, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,11 +29,13 @@ import androidx.car.app.model.Template
|
||||
import androidx.car.app.model.Toggle
|
||||
import androidx.core.content.IntentCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.single
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.EXTRA_DONATE
|
||||
@@ -340,23 +342,18 @@ class DataSettingsScreen(ctx: CarContext, val session: EVMapSession) : Screen(ct
|
||||
val args = OAuthLoginFragmentArgs(
|
||||
uri.toString(),
|
||||
TeslaAuthenticationApi.resultUrlPrefix,
|
||||
"#000000"
|
||||
"#FFFFFF"
|
||||
).toBundle()
|
||||
val intent = Intent(carContext, OAuthLoginActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtras(args)
|
||||
|
||||
LocalBroadcastManager.getInstance(carContext)
|
||||
.registerReceiver(object : BroadcastReceiver() {
|
||||
override fun onReceive(ctx: Context, intent: Intent) {
|
||||
val url = IntentCompat.getParcelableExtra(
|
||||
intent,
|
||||
OAuthLoginFragment.EXTRA_URL,
|
||||
Uri::class.java
|
||||
)
|
||||
teslaGetAccessToken(url!!, codeVerifier)
|
||||
}
|
||||
}, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT))
|
||||
val resultFlow = OAuthLoginActivity.registerForResult(uri.toString())
|
||||
lifecycleScope.launch {
|
||||
resultFlow.collect { resultUrl ->
|
||||
teslaGetAccessToken(resultUrl.toUri(), codeVerifier)
|
||||
}
|
||||
}
|
||||
|
||||
session.cas.startActivity(intent)
|
||||
|
||||
@@ -451,6 +448,7 @@ class ChooseDataSourceScreen(
|
||||
val descriptions = when (type) {
|
||||
Type.CHARGER_DATA_SOURCE -> listOf(
|
||||
carContext.getString(R.string.data_source_goingelectric_desc),
|
||||
carContext.getString(R.string.data_source_nobil_desc),
|
||||
carContext.getString(R.string.data_source_openchargemap_desc),
|
||||
carContext.getString(R.string.data_source_openstreetmap_desc)
|
||||
)
|
||||
|
||||
@@ -6,6 +6,9 @@ import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
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.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -108,6 +111,11 @@ class ChargepriceFragment : Fragment() {
|
||||
binding.toolbar.inflateMenu(R.menu.chargeprice)
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ class DataSourceSelectDialog : MaterialDialogFragment() {
|
||||
if (prefs.dataSourceSet) {
|
||||
when (prefs.dataSource) {
|
||||
"goingelectric" -> binding.rgDataSource.rbGoingElectric.isChecked = true
|
||||
"nobil" -> binding.rgDataSource.rbNobil.isChecked = true
|
||||
"openchargemap" -> binding.rgDataSource.rbOpenChargeMap.isChecked = true
|
||||
"openstreetmap" -> binding.rgDataSource.rbOpenStreetMap.isChecked = true
|
||||
}
|
||||
@@ -64,6 +65,8 @@ class DataSourceSelectDialog : MaterialDialogFragment() {
|
||||
binding.btnOK.setOnClickListener {
|
||||
val result = if (binding.rgDataSource.rbGoingElectric.isChecked) {
|
||||
"goingelectric"
|
||||
} else if (binding.rgDataSource.rbNobil.isChecked) {
|
||||
"nobil"
|
||||
} else if (binding.rgDataSource.rbOpenChargeMap.isChecked) {
|
||||
"openchargemap"
|
||||
} else if (binding.rgDataSource.rbOpenStreetMap.isChecked) {
|
||||
|
||||
@@ -4,6 +4,9 @@ import android.content.Intent
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
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 net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.FragmentDonateReferralBinding
|
||||
@@ -22,5 +25,10 @@ abstract class DonateFragmentBase : Fragment() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(referrals.root) { v, insets ->
|
||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -68,6 +71,13 @@ class FavoritesFragment : Fragment() {
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.vm = vm
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.favsList
|
||||
) { v, insets ->
|
||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
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.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -49,6 +52,13 @@ class FilterFragment : Fragment(), MenuProvider {
|
||||
binding.vm = vm
|
||||
vm.filterProfile.observe(viewLifecycleOwner) {}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.filtersList
|
||||
) { v, insets ->
|
||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
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.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@@ -60,6 +63,13 @@ class FilterProfilesFragment : Fragment() {
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.vm = vm
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.filterProfilesList
|
||||
) { v, insets ->
|
||||
v.updatePadding(bottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,14 @@ package net.vonforst.evmap.fragment
|
||||
import android.Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.method.KeyListener
|
||||
@@ -117,7 +120,6 @@ import net.vonforst.evmap.viewmodel.Status
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.collections.set
|
||||
import kotlin.math.min
|
||||
|
||||
|
||||
@@ -137,7 +139,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
private var connectionErrorSnackbar: Snackbar? = null
|
||||
private var mapTopPadding: Int = 0
|
||||
private var mapBottomPadding: Int = 0
|
||||
private var popupMenu: PopupMenu? = null
|
||||
private var insetBottom: Int = 0
|
||||
private lateinit var favToggle: MenuItem
|
||||
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
@@ -215,27 +219,27 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
}
|
||||
|
||||
binding.detailAppBar.toolbar.popupTheme =
|
||||
com.google.android.material.R.style.ThemeOverlay_AppCompat_DayNight
|
||||
com.google.android.material.R.style.Theme_Material3_DayNight
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(
|
||||
binding.root
|
||||
) { _, insets ->
|
||||
ViewCompat.onApplyWindowInsets(binding.root, insets)
|
||||
|
||||
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
|
||||
binding.detailAppBar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
val density = resources.displayMetrics.density
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.detailAppBar.toolbar) { v, insets ->
|
||||
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top
|
||||
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = systemWindowInsetTop
|
||||
}
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.fabLayers) { v, insets ->
|
||||
// margin of layers button: status bar height + toolbar height + margin
|
||||
val density = resources.displayMetrics.density
|
||||
val systemWindowInsetTop = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top
|
||||
val margin =
|
||||
if (binding.toolbarContainer.layoutParams.width == ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||
systemWindowInsetTop + (48 * density).toInt() + (28 * density).toInt()
|
||||
} else {
|
||||
systemWindowInsetTop + (12 * density).toInt()
|
||||
}
|
||||
binding.fabLayers.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = margin
|
||||
}
|
||||
binding.layersSheet.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
@@ -244,11 +248,37 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
|
||||
// set map padding so that compass is not obstructed by toolbar
|
||||
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
|
||||
// and cause an infinite loop. So we rely on onMapReady being called later than
|
||||
// onApplyWindowInsets.
|
||||
|
||||
insets
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
||||
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())
|
||||
@@ -262,6 +292,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun updatePeekHeight() {
|
||||
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom + insetBottom
|
||||
}
|
||||
|
||||
private fun getMapProvider(provider: String) = when (provider) {
|
||||
"mapbox" -> MapFactory.MAPLIBRE
|
||||
"google" -> MapFactory.GOOGLE
|
||||
@@ -291,7 +325,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
}
|
||||
|
||||
binding.detailView.topPart.doOnNextLayout {
|
||||
bottomSheetBehavior.peekHeight = binding.detailView.topPart.bottom
|
||||
updatePeekHeight()
|
||||
vm.bottomSheetState.value?.let { bottomSheetBehavior.state = it }
|
||||
}
|
||||
bottomSheetBehavior.isCollapsible = bottomSheetCollapsible
|
||||
@@ -409,7 +443,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
binding.detailView.sourceButton.setOnClickListener {
|
||||
val charger = vm.charger.value?.data
|
||||
if (charger != null) {
|
||||
(activity as? MapsActivity)?.openUrl(charger.url, binding.root, true)
|
||||
(activity as? MapsActivity)?.openUrl(charger.url ?: charger.dataSourceUrl, binding.root, true)
|
||||
}
|
||||
}
|
||||
binding.detailView.btnChargeprice.setOnClickListener {
|
||||
@@ -470,7 +504,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
}
|
||||
R.id.menu_share -> {
|
||||
val charger = vm.charger.value?.data
|
||||
if (charger != null) {
|
||||
if (charger != null && charger.url != null) {
|
||||
(activity as? MapsActivity)?.shareUrl(charger.url)
|
||||
}
|
||||
true
|
||||
@@ -478,7 +512,23 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
R.id.menu_edit -> {
|
||||
val charger = vm.charger.value?.data
|
||||
if (charger?.editUrl != null) {
|
||||
(activity as? MapsActivity)?.openUrl(charger.editUrl, binding.root, true)
|
||||
val uri = Uri.parse(charger.editUrl)
|
||||
if (uri.getScheme() == "mailto") {
|
||||
val intent = Intent(Intent.ACTION_SENDTO, uri)
|
||||
try {
|
||||
startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.no_email_app_found,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
else {
|
||||
(activity as? MapsActivity)?.openUrl(charger.editUrl, binding.root, true)
|
||||
}
|
||||
|
||||
if (vm.apiId.value == "goingelectric") {
|
||||
// instructions specific to GoingElectric
|
||||
Toast.makeText(
|
||||
@@ -616,16 +666,24 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
if (bottomSheetBehavior.state == STATE_HIDDEN) {
|
||||
map?.setPadding(0, mapTopPadding, 0, 0)
|
||||
map?.setPadding(0, mapTopPadding, 0, mapBottomPadding)
|
||||
} else {
|
||||
val height = binding.root.height - bottomSheet.top
|
||||
map?.setPadding(
|
||||
0,
|
||||
mapTopPadding,
|
||||
0,
|
||||
min(bottomSheetBehavior.peekHeight, height)
|
||||
mapBottomPadding + 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) {
|
||||
@@ -659,6 +717,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
removeSearchFocus()
|
||||
binding.fabDirections.show()
|
||||
detailAppBarBehavior.setToolbarTitle(it.name)
|
||||
updateShareItemVisibility()
|
||||
updateFavoriteToggle()
|
||||
markerManager?.highlighedCharger = it
|
||||
markerManager?.animateBounce(it)
|
||||
@@ -769,6 +828,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateShareItemVisibility() {
|
||||
val charger = vm.chargerSparse.value ?: return
|
||||
val shareItem = binding.detailAppBar.toolbar.menu.findItem(R.id.menu_share)
|
||||
shareItem.isVisible = charger.url != null
|
||||
}
|
||||
|
||||
private fun setupAdapters() {
|
||||
var viewer: StfalconImageViewer<ChargerPhoto>? = null
|
||||
val galleryClickListener = object : GalleryAdapter.ItemClickListener {
|
||||
@@ -832,11 +897,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
(activity as? MapsActivity)?.showLocation(charger, binding.root)
|
||||
}
|
||||
R.drawable.ic_fault_report -> {
|
||||
(activity as? MapsActivity)?.openUrl(
|
||||
charger.url,
|
||||
binding.root,
|
||||
true
|
||||
)
|
||||
if (charger.url != null) {
|
||||
(activity as? MapsActivity)?.openUrl(
|
||||
charger.url,
|
||||
binding.root,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
R.drawable.ic_payment -> {
|
||||
@@ -1056,7 +1123,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
|
||||
map.setTrafficEnabled(vm.mapTrafficEnabled.value ?: false)
|
||||
|
||||
// set padding so that compass is not obstructed by toolbar
|
||||
map.setPadding(0, mapTopPadding, 0, 0)
|
||||
map.setPadding(0, mapTopPadding, 0, mapBottomPadding)
|
||||
|
||||
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
map.setMapStyle(
|
||||
|
||||
@@ -220,6 +220,8 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
|
||||
binding.rgDataSource.textView28,
|
||||
binding.rgDataSource.rbOpenStreetMap,
|
||||
binding.rgDataSource.textView29,
|
||||
binding.rgDataSource.rbNobil,
|
||||
binding.rgDataSource.textView30,
|
||||
binding.dataSourceHint,
|
||||
binding.cbAcceptPrivacy
|
||||
)
|
||||
@@ -248,6 +250,7 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
|
||||
|
||||
for (rb in listOf(
|
||||
binding.rgDataSource.rbGoingElectric,
|
||||
binding.rgDataSource.rbNobil,
|
||||
binding.rgDataSource.rbOpenChargeMap,
|
||||
binding.rgDataSource.rbOpenStreetMap
|
||||
)) {
|
||||
@@ -263,6 +266,7 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
|
||||
if (prefs.dataSourceSet) {
|
||||
when (prefs.dataSource) {
|
||||
"goingelectric" -> binding.rgDataSource.rbGoingElectric.isChecked = true
|
||||
"nobil" -> binding.rgDataSource.rbNobil.isChecked = true
|
||||
"openchargemap" -> binding.rgDataSource.rbOpenChargeMap.isChecked = true
|
||||
"openstreetmap" -> binding.rgDataSource.rbOpenStreetMap.isChecked = true
|
||||
}
|
||||
@@ -281,6 +285,8 @@ class DataSourceSelectFragment : OnboardingPageFragment() {
|
||||
|
||||
val result = if (binding.rgDataSource.rbGoingElectric.isChecked) {
|
||||
"goingelectric"
|
||||
} else if (binding.rgDataSource.rbNobil.isChecked) {
|
||||
"nobil"
|
||||
} else if (binding.rgDataSource.rbOpenChargeMap.isChecked) {
|
||||
"openchargemap"
|
||||
} else if (binding.rgDataSource.rbOpenStreetMap.isChecked) {
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
package net.vonforst.evmap.fragment.oauth
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.core.net.toUri
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
class OAuthLoginFragment : Fragment() {
|
||||
companion object {
|
||||
val ACTION_OAUTH_RESULT = "oauth_result"
|
||||
val EXTRA_URL = "url"
|
||||
}
|
||||
|
||||
@@ -72,11 +71,11 @@ class OAuthLoginFragment : Fragment() {
|
||||
}
|
||||
|
||||
val args = OAuthLoginFragmentArgs.fromBundle(requireArguments())
|
||||
val uri = Uri.parse(args.url)
|
||||
val uri = args.url.toUri()
|
||||
|
||||
webView = view.findViewById(R.id.webView)
|
||||
|
||||
args.color?.let { webView.setBackgroundColor(Color.parseColor(it)) }
|
||||
args.color?.let { webView.setBackgroundColor(it.toColorInt()) }
|
||||
val progress = view.findViewById<LinearProgressIndicator>(R.id.progress_indicator)
|
||||
|
||||
CookieManager.getInstance().removeAllCookies(null)
|
||||
@@ -89,13 +88,8 @@ class OAuthLoginFragment : Fragment() {
|
||||
|
||||
if (url.toString().startsWith(args.resultUrlPrefix)) {
|
||||
val result = Bundle()
|
||||
result.putString("url", url.toString())
|
||||
result.putString(EXTRA_URL, url.toString())
|
||||
setFragmentResult(args.url, result)
|
||||
context?.let {
|
||||
LocalBroadcastManager.getInstance(it).sendBroadcast(
|
||||
Intent(ACTION_OAUTH_RESULT).putExtra(EXTRA_URL, url)
|
||||
)
|
||||
}
|
||||
navController?.popBackStack()
|
||||
}
|
||||
|
||||
@@ -104,6 +98,9 @@ class OAuthLoginFragment : Fragment() {
|
||||
|
||||
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.w("WebViewClient", url)
|
||||
}
|
||||
progress.show()
|
||||
}
|
||||
|
||||
@@ -112,6 +109,24 @@ class OAuthLoginFragment : Fragment() {
|
||||
progress.hide()
|
||||
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.loadUrl(args.url)
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
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.ui.setupWithNavController
|
||||
import androidx.preference.Preference
|
||||
@@ -30,6 +35,19 @@ class AboutFragment : PreferenceFragmentCompat() {
|
||||
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?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
||||
@@ -2,8 +2,13 @@ package net.vonforst.evmap.fragment.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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.ui.setupWithNavController
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
@@ -35,6 +40,19 @@ 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?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
||||
@@ -17,12 +17,14 @@ import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.addDebugInterceptors
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi
|
||||
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.viewmodel.SettingsViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
import okhttp3.OkHttpClient
|
||||
import okio.IOException
|
||||
import java.time.Instant
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class DataSettingsFragment : BaseSettingsFragment() {
|
||||
override val isTopLevel = false
|
||||
@@ -146,7 +148,7 @@ class DataSettingsFragment : BaseSettingsFragment() {
|
||||
val args = OAuthLoginFragmentArgs(
|
||||
uri.toString(),
|
||||
TeslaAuthenticationApi.resultUrlPrefix,
|
||||
"#000000"
|
||||
"#FFFFFF"
|
||||
).toBundle()
|
||||
|
||||
setFragmentResultListener(uri.toString()) { _, result ->
|
||||
@@ -159,7 +161,7 @@ class DataSettingsFragment : BaseSettingsFragment() {
|
||||
private fun teslaGetAccessToken(result: Bundle, codeVerifier: String) {
|
||||
teslaAccountPreference.summary = getString(R.string.logging_in)
|
||||
|
||||
val url = Uri.parse(result.getString("url"))
|
||||
val url = result.getString(OAuthLoginFragment.EXTRA_URL)!!.toUri()
|
||||
val code = url.getQueryParameter("code") ?: return
|
||||
val okhttp = OkHttpClient.Builder().addDebugInterceptors().build()
|
||||
val request = TeslaAuthenticationApi.AuthCodeRequest(code, codeVerifier)
|
||||
|
||||
@@ -37,6 +37,7 @@ sealed class ChargepointListItem
|
||||
* @param address The charge location address
|
||||
* @param chargepoints List of chargepoints at this location
|
||||
* @param network The charging network (Mobility Service Provider, MSP)
|
||||
* @param dataSourceUrl A link to the data source website
|
||||
* @param url A link to this charging site
|
||||
* @param editUrl A link to a website where this charging site can be edited
|
||||
* @param faultReport Set this if the charging site is reported to be out of service
|
||||
@@ -49,6 +50,7 @@ sealed class ChargepointListItem
|
||||
* @param locationDescription Directions on how to find the charger (e.g. "In the parking garage on level 5")
|
||||
* @param photos List of photos of this charging site
|
||||
* @param chargecards List of charge cards accepted here
|
||||
* @param accessibility Specifies who may use this charge location
|
||||
* @param openinghours List of times when this charging site can be accessed / used
|
||||
* @param cost The cost for charging and/or parking
|
||||
* @param license How the data about this chargepoint is licensed
|
||||
@@ -67,7 +69,8 @@ data class ChargeLocation(
|
||||
@Embedded val address: Address?,
|
||||
val chargepoints: List<Chargepoint>,
|
||||
val network: String?,
|
||||
val url: String, // URL of this charger at the data source
|
||||
val dataSourceUrl: String, // URL to the data source
|
||||
val url: String?, // URL of this charger at the data source
|
||||
val editUrl: String?, // URL to edit this charger at the data source
|
||||
@Embedded(prefix = "fault_report_") val faultReport: FaultReport?,
|
||||
val verified: Boolean,
|
||||
@@ -79,6 +82,7 @@ data class ChargeLocation(
|
||||
val locationDescription: String?,
|
||||
val photos: List<ChargerPhoto>?,
|
||||
val chargecards: List<ChargeCardId>?,
|
||||
val accessibility: String?,
|
||||
@Embedded val openinghours: OpeningHours?,
|
||||
@Embedded val cost: Cost?,
|
||||
val license: String?,
|
||||
@@ -135,9 +139,11 @@ data class ChargeLocation(
|
||||
val filtered = chargepoints
|
||||
.filter { it.type == variant.type && it.power == variant.power }
|
||||
val count = filtered.sumOf { it.count }
|
||||
val mergedEvseIds = filtered.map { if (it.evseIds == null) List(it.count) {null} else it.evseIds }.flatten()
|
||||
Chargepoint(variant.type, variant.power, count,
|
||||
filtered.map { it.current }.distinct().singleOrNull(),
|
||||
filtered.map { it.voltage }.distinct().singleOrNull()
|
||||
filtered.map { it.voltage }.distinct().singleOrNull(),
|
||||
if (mergedEvseIds.all { it == null }) null else mergedEvseIds
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -417,7 +423,9 @@ data class Chargepoint(
|
||||
// Max voltage in V (or null if unknown).
|
||||
// note that for DC chargers: current * voltage may be larger than power
|
||||
// (each of the three can be separately limited)
|
||||
val voltage: Double? = null
|
||||
val voltage: Double? = null,
|
||||
// Electric Vehicle Supply Equipment Ids for this Chargepoint's plugs/sockets
|
||||
val evseIds: List<String?>? = null
|
||||
) : Equatable, Parcelable {
|
||||
fun hasKnownPower(): Boolean = power != null
|
||||
fun hasKnownVoltageAndCurrent(): Boolean = voltage != null && current != null
|
||||
|
||||
@@ -35,6 +35,7 @@ class CustomNavigator(
|
||||
val prefs = PreferenceDataSource(context)
|
||||
val url = when (prefs.dataSource) {
|
||||
"goingelectric" -> "https://www.goingelectric.de/stromtankstellen/new/"
|
||||
"nobil" -> "http://nobil.no/api/chargerregistration/chargerregistration.php?action=register"
|
||||
"openchargemap" -> "https://openchargemap.org/site/poi/add"
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
@@ -16,14 +16,15 @@ import java.time.Instant
|
||||
* successful.
|
||||
*/
|
||||
class CacheLiveData<T>(
|
||||
cache: LiveData<T>,
|
||||
cache: LiveData<Resource<T>>,
|
||||
api: LiveData<Resource<T>>,
|
||||
skipApi: LiveData<Boolean>? = null
|
||||
) :
|
||||
MediatorLiveData<Resource<T>>() {
|
||||
private var cacheResult: T? = null
|
||||
private var cacheResult: Resource<T>? = null
|
||||
private var apiResult: Resource<T>? = null
|
||||
private var skipApiResult: Boolean = false
|
||||
private val apiLiveData = api
|
||||
|
||||
init {
|
||||
updateValue()
|
||||
@@ -64,9 +65,21 @@ class CacheLiveData<T>(
|
||||
Log.d("CacheLiveData", "cache has finished loading before API")
|
||||
// cache has finished loading before API
|
||||
if (skipApiResult) {
|
||||
value = Resource.success(cache)
|
||||
value = when (cache.status) {
|
||||
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 {
|
||||
value = Resource.loading(cache)
|
||||
value = Resource.loading(cache.data)
|
||||
}
|
||||
} else if (cache == null && api != null) {
|
||||
Log.d("CacheLiveData", "API has finished loading before cache")
|
||||
@@ -81,7 +94,7 @@ class CacheLiveData<T>(
|
||||
// Both cache and API have finished loading
|
||||
value = when (api.status) {
|
||||
Status.SUCCESS -> api
|
||||
Status.ERROR -> Resource.error(api.message, cache)
|
||||
Status.ERROR -> Resource.error(api.message, cache.data)
|
||||
Status.LOADING -> api // should not occur
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import net.vonforst.evmap.api.FiltersSQLQuery
|
||||
import net.vonforst.evmap.api.StringProvider
|
||||
import net.vonforst.evmap.api.goingelectric.GEReferenceData
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||
import net.vonforst.evmap.api.nobil.NobilApiWrapper
|
||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||
import net.vonforst.evmap.api.openstreetmap.OSMReferenceData
|
||||
import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper
|
||||
@@ -68,6 +69,12 @@ abstract class ChargeLocationsDao {
|
||||
@Query("DELETE FROM chargelocation WHERE NOT EXISTS (SELECT 1 FROM favorite WHERE favorite.chargerId = chargelocation.id)")
|
||||
abstract suspend fun deleteAllIfNotFavorite()
|
||||
|
||||
@Query("SELECT id FROM chargelocation WHERE dataSource == :dataSource")
|
||||
abstract suspend fun getAllIds(dataSource: String): List<Long>
|
||||
|
||||
@Query("DELETE FROM chargelocation WHERE dataSource == :dataSource AND id IN (:chargerIds)")
|
||||
abstract suspend fun deleteById(dataSource: String, chargerIds: List<Long>)
|
||||
|
||||
@Query("SELECT * FROM chargelocation WHERE dataSource == :dataSource AND id == :id AND isDetailed == 1 AND timeRetrieved > :after")
|
||||
abstract suspend fun getChargeLocationById(
|
||||
id: Long,
|
||||
@@ -83,7 +90,7 @@ abstract class ChargeLocationsDao {
|
||||
): List<ChargeLocation>
|
||||
|
||||
@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 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))")
|
||||
abstract suspend fun getChargeLocationsInBounds(
|
||||
lat1: Double,
|
||||
lat2: Double,
|
||||
@@ -94,7 +101,7 @@ abstract class ChargeLocationsDao {
|
||||
): List<ChargeLocation>
|
||||
|
||||
@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 AND ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND f_geometry_column = 'coordinates' AND search_frame = BuildCircleMbr(:lng, :lat, :radius)) ORDER BY Distance(coordinates, MakePoint(:lng, :lat, 4326))")
|
||||
abstract suspend fun getChargeLocationsRadius(
|
||||
lat: Double,
|
||||
lng: Double,
|
||||
@@ -193,6 +200,10 @@ class ChargeLocationsRepository(
|
||||
).getReferenceData()
|
||||
}
|
||||
|
||||
is NobilApiWrapper -> {
|
||||
NobilReferenceDataRepository(scope, prefs).getReferenceData()
|
||||
}
|
||||
|
||||
is OpenChargeMapApiWrapper -> {
|
||||
OCMReferenceDataRepository(
|
||||
api,
|
||||
@@ -235,6 +246,7 @@ class ChargeLocationsRepository(
|
||||
val dbResult = if (filters.isNullOrEmpty()) {
|
||||
liveData {
|
||||
emit(
|
||||
Resource.success(
|
||||
chargeLocationsDao.getChargeLocationsClustered(
|
||||
bounds.southwest.latitude,
|
||||
bounds.northeast.latitude,
|
||||
@@ -244,6 +256,7 @@ class ChargeLocationsRepository(
|
||||
cacheLimitDate(api),
|
||||
zoom
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -251,7 +264,7 @@ class ChargeLocationsRepository(
|
||||
}.map {
|
||||
val t2 = System.currentTimeMillis()
|
||||
Log.d(TAG, "DB loading time: ${t2 - t1}")
|
||||
Log.d(TAG, "number of chargers: ${it.size}")
|
||||
Log.d(TAG, "number of chargers: ${it.data?.size}")
|
||||
it
|
||||
}
|
||||
val filtersSerialized =
|
||||
@@ -321,7 +334,7 @@ class ChargeLocationsRepository(
|
||||
job.join()
|
||||
progressJob.cancelAndJoin()
|
||||
}
|
||||
emit(Resource.success(dbResult.await()))
|
||||
emit(dbResult.await())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,6 +376,7 @@ class ChargeLocationsRepository(
|
||||
val dbResult = if (filters.isNullOrEmpty()) {
|
||||
liveData {
|
||||
emit(
|
||||
Resource.success(
|
||||
chargeLocationsDao.getChargeLocationsRadius(
|
||||
location.latitude,
|
||||
location.longitude,
|
||||
@@ -370,6 +384,7 @@ class ChargeLocationsRepository(
|
||||
api.id,
|
||||
cacheLimitDate(api)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -446,7 +461,7 @@ class ChargeLocationsRepository(
|
||||
job.join()
|
||||
progressJob.cancelAndJoin()
|
||||
}
|
||||
emit(Resource.success(dbResult.await()))
|
||||
emit(dbResult.await())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -546,7 +561,7 @@ class ChargeLocationsRepository(
|
||||
api: ChargepointApi<ReferenceData>,
|
||||
filters: FilterValues,
|
||||
bounds: LatLngBounds
|
||||
): LiveData<List<ChargeLocation>> {
|
||||
): LiveData<Resource<List<ChargeLocation>>> {
|
||||
return queryWithFilters(api, filters, boundsSpatialIndexQuery(bounds))
|
||||
}
|
||||
|
||||
@@ -555,7 +570,7 @@ class ChargeLocationsRepository(
|
||||
filters: FilterValues,
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float
|
||||
): LiveData<List<ChargepointListItem>> {
|
||||
): LiveData<Resource<List<ChargepointListItem>>> {
|
||||
return queryWithFiltersClustered(api, filters, boundsSpatialIndexQuery(bounds), zoom)
|
||||
}
|
||||
|
||||
@@ -564,7 +579,7 @@ class ChargeLocationsRepository(
|
||||
filters: FilterValues,
|
||||
location: LatLng,
|
||||
radius: Double
|
||||
): LiveData<List<ChargeLocation>> {
|
||||
): LiveData<Resource<List<ChargeLocation>>> {
|
||||
val region =
|
||||
radiusSpatialIndexQuery(location, radius)
|
||||
val order =
|
||||
@@ -573,17 +588,17 @@ class ChargeLocationsRepository(
|
||||
}
|
||||
|
||||
private fun boundsSpatialIndexQuery(bounds: LatLngBounds) =
|
||||
"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}))"
|
||||
"ChargeLocation.ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND f_geometry_column = 'coordinates' 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))"
|
||||
"PtDistWithin(coordinates, MakePoint(${location.longitude}, ${location.latitude}, 4326), ${radius}) AND ChargeLocation.ROWID IN (SELECT ROWID FROM SpatialIndex WHERE f_table_name = 'ChargeLocation' AND f_geometry_column = 'coordinates' AND search_frame = BuildCircleMbr(${location.longitude}, ${location.latitude}, $radius))"
|
||||
|
||||
private fun queryWithFilters(
|
||||
api: ChargepointApi<ReferenceData>,
|
||||
filters: FilterValues,
|
||||
regionSql: String,
|
||||
orderSql: String? = null
|
||||
): LiveData<List<ChargeLocation>> = referenceData.singleSwitchMap { refData ->
|
||||
): LiveData<Resource<List<ChargeLocation>>> = referenceData.singleSwitchMap { refData ->
|
||||
try {
|
||||
val query = api.convertFiltersToSQL(filters, refData)
|
||||
val after = cacheLimitDate(api)
|
||||
@@ -591,12 +606,14 @@ class ChargeLocationsRepository(
|
||||
|
||||
liveData {
|
||||
emit(
|
||||
Resource.success(
|
||||
chargeLocationsDao.getChargeLocationsCustom(
|
||||
SimpleSQLiteQuery(
|
||||
sql,
|
||||
null
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: NotImplementedError) {
|
||||
@@ -610,7 +627,7 @@ class ChargeLocationsRepository(
|
||||
regionSql: String,
|
||||
zoom: Float,
|
||||
orderSql: String? = null
|
||||
): LiveData<List<ChargepointListItem>> = referenceData.singleSwitchMap { refData ->
|
||||
): LiveData<Resource<List<ChargepointListItem>>> = referenceData.singleSwitchMap { refData ->
|
||||
try {
|
||||
if (zoom > CLUSTER_MAX_ZOOM_LEVEL) {
|
||||
queryWithFilters(api, filters, regionSql, orderSql).map { it }
|
||||
@@ -632,13 +649,19 @@ class ChargeLocationsRepository(
|
||||
.map { it.ids }
|
||||
.flatten(), prefs.dataSource, after)
|
||||
emit(
|
||||
Resource.success(
|
||||
clusters.filter { it.clusterCount > 1 }
|
||||
.map { it.convert() } + singleChargers
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
} catch (e: NotImplementedError) {
|
||||
MutableLiveData() // in this case we cannot get a DB result
|
||||
MutableLiveData(
|
||||
Resource.error(
|
||||
e.message,
|
||||
null
|
||||
)
|
||||
) // in this case we cannot get a DB result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,6 +709,7 @@ class ChargeLocationsRepository(
|
||||
val result = api.fullDownload()
|
||||
try {
|
||||
var insertJob: Job? = null
|
||||
val idsToDelete = chargeLocationsDao.getAllIds(api.id).toMutableSet()
|
||||
result.chargers.chunked(1024).forEach {
|
||||
insertJob?.join()
|
||||
insertJob = withContext(Dispatchers.IO) {
|
||||
@@ -693,8 +717,12 @@ class ChargeLocationsRepository(
|
||||
chargeLocationsDao.insert(*it.toTypedArray())
|
||||
}
|
||||
}
|
||||
idsToDelete.removeAll(it.map { it.id })
|
||||
fullDownloadProgress.value = result.progress
|
||||
}
|
||||
// delete chargers that have been removed
|
||||
chargeLocationsDao.deleteById(api.id, idsToDelete.toList())
|
||||
|
||||
val region = Mbr(
|
||||
-180.0,
|
||||
-90.0,
|
||||
|
||||
@@ -40,7 +40,7 @@ import net.vonforst.evmap.model.SliderFilterValue
|
||||
OCMOperator::class,
|
||||
OSMNetwork::class,
|
||||
SavedRegion::class
|
||||
], version = 24
|
||||
], version = 27
|
||||
)
|
||||
@TypeConverters(Converters::class, GeometryConverters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
@@ -84,12 +84,14 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11,
|
||||
MIGRATION_12, MIGRATION_13, MIGRATION_14, MIGRATION_15, MIGRATION_16,
|
||||
MIGRATION_17, MIGRATION_18, MIGRATION_19, MIGRATION_20, MIGRATION_21,
|
||||
MIGRATION_22, MIGRATION_23, MIGRATION_24
|
||||
MIGRATION_22, MIGRATION_23, MIGRATION_24, MIGRATION_25, MIGRATION_26,
|
||||
MIGRATION_27
|
||||
)
|
||||
.addCallback(object : Callback() {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
// 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 ('nobil', '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
|
||||
@@ -501,6 +503,50 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_25 = object : Migration(24, 25) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// API nobil added
|
||||
db.execSQL("INSERT INTO `FilterProfile` (`dataSource`, `name`, `id`, `order`) VALUES ('nobil', 'FILTERS_CUSTOM', $FILTERS_CUSTOM, 0)")
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_26 = object : Migration(25, 26) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// adding dataSourceUrl and making url optional
|
||||
try {
|
||||
db.beginTransaction()
|
||||
db.execSQL(
|
||||
"CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `coordinates` BLOB NOT NULL, `coordinatesProjected` BLOB NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `dataSourceUrl` TEXT NOT NULL, `url` TEXT, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `license` 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, `networkUrl` TEXT, `chargerUrl` TEXT, PRIMARY KEY(`id`, `dataSource`))"
|
||||
)
|
||||
|
||||
db.execSQL("INSERT INTO `ChargeLocationNew` SELECT `id`, `dataSource`, `name`, `coordinates`, `coordinatesProjected`, `chargepoints`, `network`, '', `url`, `editUrl`, `verified`, `barrierFree`, `operator`, `generalInformation`, `amenities`, `locationDescription`, `photos`, `chargecards`, `license`, `timeRetrieved`, `isDetailed`, `city`, `country`, `postcode`, `street`, `fault_report_created`, `fault_report_description`, `twentyfourSeven`, `description`, `mostart`, `moend`, `tustart`, `tuend`, `westart`, `weend`, `thstart`, `thend`, `frstart`, `frend`, `sastart`, `saend`, `sustart`, `suend`, `hostart`, `hoend`, `freecharging`, `freeparking`, `descriptionShort`, `descriptionLong`, `chargepricecountry`, `chargepricenetwork`, `chargepriceplugTypes`, `networkUrl`, `chargerUrl` FROM `ChargeLocation`")
|
||||
db.execSQL("UPDATE ChargeLocationNew SET `dataSourceUrl` = 'https://www.goingelectric.de/' WHERE `dataSource` = 'goingelectric'")
|
||||
db.execSQL("UPDATE ChargeLocationNew SET `dataSourceUrl` = 'https://openchargemap.org/' WHERE `dataSource` = 'openchargemap'")
|
||||
db.execSQL("UPDATE ChargeLocationNew SET `dataSourceUrl` = 'https://www.openstreetmap.org/' WHERE `dataSource` = 'openstreetmap'")
|
||||
db.query("SELECT DropGeoTable('ChargeLocation', FALSE)").moveToNext()
|
||||
db.execSQL("ALTER TABLE `ChargeLocationNew` RENAME TO `ChargeLocation`")
|
||||
db.query("SELECT RecoverGeometryColumn('ChargeLocation', 'coordinates', 4326, 'POINT', 'XY');")
|
||||
.moveToNext()
|
||||
db.query("SELECT CreateSpatialIndex('ChargeLocation', 'coordinates');")
|
||||
.moveToNext()
|
||||
db.query("SELECT RecoverGeometryColumn('ChargeLocation', 'coordinatesProjected', 3857, 'POINT', 'XY');")
|
||||
.moveToNext()
|
||||
db.query("SELECT CreateSpatialIndex('ChargeLocation', 'coordinatesProjected');")
|
||||
.moveToNext()
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_27 = object : Migration(26, 27) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// adding accessibility to ChargeLocation
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `accessibility` TEXT")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.nobil.*
|
||||
import net.vonforst.evmap.viewmodel.Status
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Dao
|
||||
abstract class NobilReferenceDataDao {
|
||||
}
|
||||
|
||||
class NobilReferenceDataRepository(
|
||||
private val scope: CoroutineScope,
|
||||
private val prefs: PreferenceDataSource
|
||||
) {
|
||||
fun getReferenceData(): LiveData<NobilReferenceData> {
|
||||
return MediatorLiveData<NobilReferenceData>().apply {
|
||||
value = NobilReferenceData(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import net.vonforst.evmap.api.goingelectric.GEChargerPhotoAdapter
|
||||
import net.vonforst.evmap.api.nobil.NobilChargerPhotoAdapter
|
||||
import net.vonforst.evmap.api.openchargemap.OCMChargerPhotoAdapter
|
||||
import net.vonforst.evmap.api.openstreetmap.ImgurChargerPhoto
|
||||
import net.vonforst.evmap.autocomplete.AutocompletePlaceType
|
||||
@@ -23,6 +24,7 @@ class Converters {
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(ChargerPhoto::class.java, "type")
|
||||
.withSubtype(GEChargerPhotoAdapter::class.java, "goingelectric")
|
||||
.withSubtype(NobilChargerPhotoAdapter::class.java, "nobil")
|
||||
.withSubtype(OCMChargerPhotoAdapter::class.java, "openchargemap")
|
||||
.withSubtype(ImgurChargerPhoto::class.java, "imgur")
|
||||
.withDefaultValue(null)
|
||||
|
||||
@@ -23,6 +23,7 @@ class UpdateFullDownloadWorker(appContext: Context, workerParams: WorkerParamete
|
||||
|
||||
var insertJob: Job? = null
|
||||
val result = api.fullDownload()
|
||||
val idsToDelete = chargeLocations.getAllIds(api.id).toMutableSet()
|
||||
result.chargers.chunked(1024).forEach {
|
||||
insertJob?.join()
|
||||
insertJob = withContext(Dispatchers.IO) {
|
||||
@@ -30,8 +31,12 @@ class UpdateFullDownloadWorker(appContext: Context, workerParams: WorkerParamete
|
||||
chargeLocations.insert(*it.toTypedArray())
|
||||
}
|
||||
}
|
||||
idsToDelete.removeAll(it.map { it.id })
|
||||
}
|
||||
|
||||
// delete chargers that have been removed
|
||||
chargeLocations.deleteById(api.id, idsToDelete.toList())
|
||||
|
||||
when (api) {
|
||||
is OpenStreetMapApiWrapper -> {
|
||||
val refData = result.referenceData
|
||||
@@ -40,7 +45,6 @@ class UpdateFullDownloadWorker(appContext: Context, workerParams: WorkerParamete
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove deleted chargers
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
@@ -61,4 +61,22 @@
|
||||
android:layout_marginStart="32dp"
|
||||
android:text="@string/data_source_openstreetmap_desc" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rbNobil"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/data_source_nobil"
|
||||
android:textColor="#69bf9c"
|
||||
app:buttonTint="#69bf9c"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView30"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-8dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:text="@string/data_source_nobil_desc" />
|
||||
|
||||
</RadioGroup>
|
||||
@@ -58,6 +58,7 @@
|
||||
android:id="@+id/charge_prices_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
android:id="@+id/favs_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
app:data="@{vm.listData}" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
android:id="@+id/filters_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
app:data="@{vm.filtersWithValue}"
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/item_filter_boolean" />
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
android:id="@+id/filter_profiles_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
app:data="@{vm.filterProfiles}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
||||
@@ -247,6 +247,15 @@
|
||||
app:layout_behavior="@string/hide_on_scroll_fab_behavior"
|
||||
android:theme="@style/NoElevationOverlay" />
|
||||
|
||||
<View
|
||||
android:id="@+id/navBarScrim"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="16dp"
|
||||
android:background="?android:colorBackground"
|
||||
android:layout_gravity="bottom"
|
||||
app:invisibleUnless="@{vm.bottomSheetState == BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED}"
|
||||
tools:visibility="invisible" />
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/layers_sheet"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -377,4 +377,8 @@
|
||||
<string name="referral_link">https://ev-map.app/referrals/</string>
|
||||
<string name="mastodon">Mastodon</string>
|
||||
<string name="tff_forum">Vlákno na fóru TFF-Forum.de</string>
|
||||
<string name="data_source_openstreetmap">OpenStreetMap</string>
|
||||
<string name="data_source_openstreetmap_desc">Experimentální podpora v EVMap, nejsou dostupné všechny funkce.</string>
|
||||
<string name="downloading_chargers_percent">Stahování… %.0f%%</string>
|
||||
<string name="plug_type_2_tethered">Provázaný kabel typ 2</string>
|
||||
</resources>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<string name="connectors">Anschlüsse</string>
|
||||
<string name="no_maps_app_found">Bitte installiere eine Navigations-App</string>
|
||||
<string name="no_browser_app_found">Bitte installiere einen Webbrowser</string>
|
||||
<string name="no_email_app_found">Bitte installiere eine E-Mail-App</string>
|
||||
<string name="address">Adresse</string>
|
||||
<string name="operator">Betreiber</string>
|
||||
<string name="network">Verbund</string>
|
||||
@@ -107,6 +108,7 @@
|
||||
<string name="filter_open_247">24 Stunden geöffnet</string>
|
||||
<string name="filter_barrierfree">Ohne Vertrag / Registrierung nutzbar</string>
|
||||
<string name="filter_exclude_faults">Ladesäulen mit Störung ausschließen</string>
|
||||
<string name="filter_accessibility">Zugänglichkeit der Ladestation</string>
|
||||
<string name="charge_cards">Ladetarife</string>
|
||||
<string name="and_n_others">und %d weitere</string>
|
||||
<string name="pref_map_provider">Kartenanbieter</string>
|
||||
@@ -225,9 +227,11 @@
|
||||
<string name="unknown_operator">Unbekannter Betreiber</string>
|
||||
<string name="data_sources_description">Bitte wähle eine Datenquelle für Ladestationen aus. Du kannst sie später in den Einstellungen der App ändern.</string>
|
||||
<string name="data_source_goingelectric">GoingElectric.de</string>
|
||||
<string name="data_source_nobil">NOBIL</string>
|
||||
<string name="data_source_openchargemap">Open Charge Map</string>
|
||||
<string name="data_source_openstreetmap">OpenStreetMap</string>
|
||||
<string name="data_source_goingelectric_desc">Sehr gute Abdeckung in den deutschsprachigen Ländern. Beschreibungen in Deutsch. Von der Community gepflegt.</string>
|
||||
<string name="data_source_nobil_desc"><![CDATA[Offizielles Verzeichnis der nordischen Länder]]></string>
|
||||
<string name="data_source_openchargemap_desc"><![CDATA[Weltweite Abdeckung mit variierender Qualität. Beschreibungen in Englisch oder Landessprache. Von der Community gepflegt und offizielle Verzeichnisse einiger Länder (z.B. Nordamerika, UK, Frankreich, Norwegen).]]></string>
|
||||
<string name="data_source_openstreetmap_desc">Experimentelle Unterstützung in EVMap, nicht alle Funktionen nutzbar.</string>
|
||||
<string name="next">weiter</string>
|
||||
@@ -376,4 +380,10 @@
|
||||
<string name="pref_chargeprice_native_integration_on">Preise werden direkt in EVMap angezeigt</string>
|
||||
<string name="pref_chargeprice_native_integration_off">Preisvergleich verlinkt auf die App oder Website von Chargeprice</string>
|
||||
<string name="auto_zoom_for_details">Für Details hineinzoomen</string>
|
||||
<string name="plug_type_2_tethered">Typ 2 Kabel mit Stecker</string>
|
||||
<string name="accessibility_public">Öffentlich</string>
|
||||
<string name="accessibility_visitors">Besucher</string>
|
||||
<string name="accessibility_employees">Mitarbeiter</string>
|
||||
<string name="accessibility_by_appointment">Nach Vereinbarung</string>
|
||||
<string name="accessibility_residents">Bewohner</string>
|
||||
</resources>
|
||||
|
||||
@@ -373,4 +373,8 @@
|
||||
<string name="referrals_info">Kui peale mõnele järgnevatest linkidest klikkamist ostad kaupu või teenused, siis toetad sellega EVMapi arendajat.</string>
|
||||
<string name="tff_forum">Jutulõng TFF-Forum.de kasutajate foorumis</string>
|
||||
<string name="mastodon">Mastodon</string>
|
||||
<string name="data_source_openstreetmap">OpenStreetMap</string>
|
||||
<string name="data_source_openstreetmap_desc">Katseline tugi EVMapis - kõik funktsionaalsused pole saadaval.</string>
|
||||
<string name="downloading_chargers_percent">Laadin alla… %.0f%%</string>
|
||||
<string name="plug_type_2_tethered">Tüüp 2 lõimitud kaabel</string>
|
||||
</resources>
|
||||
|
||||
@@ -345,7 +345,7 @@
|
||||
</plurals>
|
||||
<string name="data_source_goingelectric_desc">Ottimo nei paesi di lingua tedesca. Descrizioni in tedesco. Mantenuto dalla comunità.</string>
|
||||
<string name="auto_no_data">Non disponibile</string>
|
||||
<string name="auto_location_permission_needed">Per eseguire EVMap su Android Auto, devi concedere l\'accesso alla propria posizione.</string>
|
||||
<string name="auto_location_permission_needed">Per eseguire EVMap sulla propria auto, è necessario concedere l\'accesso alla propria posizione.</string>
|
||||
<string name="prediction_help">La previsione si basa su fattori quali il giorno della settimana, l\'ora del giorno e l\'utilizzo passato, in modo da evitare le colonnine di ricarica sovraffollate. Nessuna garanzia a riguardo.</string>
|
||||
<string name="charger_website">Sito web</string>
|
||||
<string name="pref_map_rotate_gestures_on">Usa due dita per ruotare la mappa</string>
|
||||
@@ -377,4 +377,7 @@
|
||||
<string name="pref_chargeprice_native_integration_on">I dati sui prezzi saranno visualizzati direttamente in EVMap</string>
|
||||
<string name="accept_privacy"><![CDATA[Ho letto e accettato l\'<a href=\"%s\">informativa sulla privacy</a> di EVMap.]]></string>
|
||||
<string name="auto_multipage_goto">Pagina %d</string>
|
||||
<string name="data_source_openstreetmap">OpenStreetMap</string>
|
||||
<string name="data_source_openstreetmap_desc">Supporto sperimentale in EVMap, non tutte le funzionalità sono disponibili.</string>
|
||||
<string name="downloading_chargers_percent">Scaricamento… %.0f%%</string>
|
||||
</resources>
|
||||
|
||||
@@ -377,4 +377,7 @@
|
||||
<string name="referral_link">https://ev-map.app/referrals/</string>
|
||||
<string name="tff_forum">Tópico no fórum TFF-Forum.de</string>
|
||||
<string name="mastodon">Mastodon</string>
|
||||
<string name="data_source_openstreetmap">OpenStreetMap</string>
|
||||
<string name="data_source_openstreetmap_desc">Suporte experimental, algumas funcionalidades não estão disponíveis.</string>
|
||||
<string name="downloading_chargers_percent">A descarregar… %.0f%%</string>
|
||||
</resources>
|
||||
|
||||
388
app/src/main/res/values-sv/strings.xml
Normal file
388
app/src/main/res/values-sv/strings.xml
Normal file
@@ -0,0 +1,388 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">EVMap</string>
|
||||
<string name="title_activity_maps">EVMap</string>
|
||||
<string name="connectors">Laddkontakter</string>
|
||||
<string name="no_maps_app_found">Installera en kartapp först</string>
|
||||
<string name="no_browser_app_found">Installera en webbläsare först</string>
|
||||
<string name="no_email_app_found">Installera en e-postapp först</string>
|
||||
<string name="address">Adress</string>
|
||||
<string name="operator">Operatör</string>
|
||||
<string name="network">Nätverk</string>
|
||||
<string name="hours">Öppettider</string>
|
||||
<string name="open_247"><![CDATA[<b>Öppen 24/7</b>]]></string>
|
||||
<string name="closed"><![CDATA[<b>Stängd</b>]]></string>
|
||||
<string name="open_closesat"><![CDATA[<b>Öppen</b> · Stänger %s]]></string>
|
||||
<string name="closed_opensat"><![CDATA[<b>Stängd</b> · Öppnar %s]]></string>
|
||||
<string name="closed_unfmt">Stängd</string>
|
||||
<string name="holiday">helgdag</string>
|
||||
<string name="cost">Kostnad</string>
|
||||
<string name="cost_detail"><![CDATA[<b>Laddning:</b> %1$s · <b>Parkering:</b> %2$s]]></string>
|
||||
<string name="cost_detail_charging"><![CDATA[<b>%s laddning</b>]]></string>
|
||||
<string name="cost_detail_parking"><![CDATA[<b>%s parkering</b>]]></string>
|
||||
<string name="charging_free">Gratis</string>
|
||||
<string name="charging_paid">Avgiftsbelagd</string>
|
||||
<string name="parking_free">Gratis</string>
|
||||
<string name="parking_paid">Avgiftsbelagd</string>
|
||||
<string name="amenities">Bekvämligheter</string>
|
||||
<string name="general_info">Allmän info</string>
|
||||
<string name="realtime_data_unavailable">Realtidsstatus saknas</string>
|
||||
<string name="realtime_data_login_needed">Teslakonto krävs för realtidsstatus</string>
|
||||
<string name="realtime_data_loading">Hämtar realtidsstatus…</string>
|
||||
<string name="realtime_data_source">Realtidsstatuskälla (beta): %s</string>
|
||||
<string name="source">Källa: %s</string>
|
||||
<string name="search">Sök</string>
|
||||
<string name="menu_map">Karta</string>
|
||||
<string name="menu_favs">Favoriter</string>
|
||||
<string name="menu_filter">Filter</string>
|
||||
<string name="not_implemented">inte implementerat ännu</string>
|
||||
<string name="about">Om</string>
|
||||
<string name="version">Version</string>
|
||||
<string name="github_link_title">Källkod</string>
|
||||
<string name="oss_licenses">Licenser</string>
|
||||
<string name="settings">Inställningar</string>
|
||||
<string name="settings_ui">Utseende</string>
|
||||
<string name="settings_map">Karta</string>
|
||||
<string name="copyright">Copyright</string>
|
||||
<string name="other">Övrigt</string>
|
||||
<string name="privacy">Integritet</string>
|
||||
<string name="fav_add">Spara som favorit</string>
|
||||
<string name="fav_remove">Ta bort från favoriter</string>
|
||||
<string name="pref_navigate_use_maps">Omedelbar navigering</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigeraknappen startar vägbeskrivning i Google Maps</string>
|
||||
<string name="pref_navigate_use_maps_off">Navigeraknappen öppnar kartappen med laddstationen</string>
|
||||
<string name="coordinates">Koordinater</string>
|
||||
<string name="share">Dela</string>
|
||||
<string name="filter_free">Endast gratis laddare</string>
|
||||
<string name="filter_min_power">Lägst effekt</string>
|
||||
<string name="filter_free_parking">Endast laddare med gratis parkering</string>
|
||||
<string name="filter_min_connectors">Lägst antal laddkontakter</string>
|
||||
<string name="filter_connectors">Laddkontakter</string>
|
||||
<string name="plug_type_1">Typ 1</string>
|
||||
<string name="plug_type_2">Typ 2</string>
|
||||
<string name="plug_type_2_tethered">Typ 2 fast kabel</string>
|
||||
<string name="plug_type_3a">Typ 3A</string>
|
||||
<string name="plug_type_3c">Typ 3C</string>
|
||||
<string name="plug_ccs">CCS</string>
|
||||
<string name="plug_schuko">Schuko</string>
|
||||
<string name="plug_chademo">CHAdeMO</string>
|
||||
<string name="plug_supercharger">Tesla Supercharger</string>
|
||||
<string name="plug_cee_blau">CEE blå</string>
|
||||
<string name="plug_cee_rot">CEE röd</string>
|
||||
<string name="plug_roadster_hpc">Tesla Roadster (2008) HPC</string>
|
||||
<string name="all">alla</string>
|
||||
<string name="none">inga</string>
|
||||
<string name="show_more">mer…</string>
|
||||
<string name="show_less">mindre…</string>
|
||||
<string name="favorites_empty_state">Här visas sparade laddare</string>
|
||||
<string name="donate">Donera</string>
|
||||
<string name="donation_successful">Tack ❤️</string>
|
||||
<string name="donation_failed">Något gick fel 😕</string>
|
||||
<string name="map_type_normal">Standard</string>
|
||||
<string name="map_type_satellite">Satellit</string>
|
||||
<string name="map_type_terrain">Terräng</string>
|
||||
<string name="map_type">Karttyp</string>
|
||||
<string name="map_details">Kartdetailjer</string>
|
||||
<string name="map_traffic">Trafik</string>
|
||||
<string name="faq">Vanliga frågor</string>
|
||||
<string name="menu_filters_active">Aktiva filter</string>
|
||||
<string name="filters_activated">Filter aktiverade</string>
|
||||
<string name="filters_deactivated">Filter inaktiverade</string>
|
||||
<string name="menu_edit_filters">Ändra filter</string>
|
||||
<string name="menu_manage_filter_profiles">Hantera filterprofiler</string>
|
||||
<string name="go_to_chargeprice">Jämför priser</string>
|
||||
<string name="fault_report">Felrapport</string>
|
||||
<string name="fault_report_date">Felrapport (senast uppdaterad: %s)</string>
|
||||
<string name="filter_networks">Nätverk</string>
|
||||
<string name="filter_operators">Operatörer</string>
|
||||
<string name="filter_chargecards">Betalningsalternativ</string>
|
||||
<string name="all_selected">Alla valda</string>
|
||||
<string name="number_selected">%d valda</string>
|
||||
<string name="edit">ändra</string>
|
||||
<string name="cancel">Avbryt</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="pref_language">Språk i appen</string>
|
||||
<string name="pref_darkmode">Mörkt läge</string>
|
||||
<string name="connection_error">Kunde inte hämta laddstationer</string>
|
||||
<string name="location_error">Kunde inte hämta plats. Kontrollera systeminställningarna</string>
|
||||
<string name="retry">Försök igen</string>
|
||||
<string name="filter_open_247">Tillgänglig 24/7</string>
|
||||
<string name="filter_barrierfree">Kan användas utan registrering</string>
|
||||
<string name="filter_exclude_faults">Utelämna laddare med felrapporter</string>
|
||||
<string name="filter_accessibility">Laddarens tillgänglighet</string>
|
||||
<string name="charge_cards">Betalningsalternativ</string>
|
||||
<string name="and_n_others">och %d andra</string>
|
||||
<string name="pref_map_provider">Kartleverantör</string>
|
||||
<string name="twitter">Twitter</string>
|
||||
<string name="mastodon">Mastodon</string>
|
||||
<string name="goingelectric_forum">Forumtråd hos GoingElectric.de</string>
|
||||
<string name="tff_forum">Forumtråd hos TFF-Forum.de</string>
|
||||
<string name="contact">Kontakt</string>
|
||||
<string name="menu_report_new_charger">Ny laddare</string>
|
||||
<string name="edit_at_datasource">Ändra hos %s</string>
|
||||
<string name="categories">Kategorier</string>
|
||||
<string name="category_car_dealership">Bilförsäljare</string>
|
||||
<string name="category_service_on_motorway">Rastplats (vid motorväg)</string>
|
||||
<string name="category_service_off_motorway">Rastplats (utanför motorväg)</string>
|
||||
<string name="category_railway_station">Tågstation</string>
|
||||
<string name="category_public_authorities">Offentliga myndigheter</string>
|
||||
<string name="category_camping">Campingplats</string>
|
||||
<string name="category_shopping_mall">Köpcenter</string>
|
||||
<string name="category_holiday_home">Semesterboende</string>
|
||||
<string name="category_airport">Flygplats</string>
|
||||
<string name="category_amusement_park">Nöjespark</string>
|
||||
<string name="category_hotel">Hotell</string>
|
||||
<string name="category_cinema">Biograf</string>
|
||||
<string name="category_church">Kyrka</string>
|
||||
<string name="category_hospital">Sjukhus</string>
|
||||
<string name="category_museum">Museum</string>
|
||||
<string name="category_parking_multi">Parkeringshus</string>
|
||||
<string name="category_parking">Parkeringsplats</string>
|
||||
<string name="category_private_charger">Privat laddare</string>
|
||||
<string name="category_rest_area">Rastplats</string>
|
||||
<string name="category_restaurant">Restaurang</string>
|
||||
<string name="category_swimming_pool">Simhall</string>
|
||||
<string name="category_supermarket">Supermarket</string>
|
||||
<string name="category_petrol_station">Bensinstation</string>
|
||||
<string name="category_parking_underground">Underjordiskt parkeringsgarage</string>
|
||||
<string name="category_zoo">Zoo</string>
|
||||
<string name="category_caravan_site">Campingplats</string>
|
||||
<string name="menu_apply">Använd filter</string>
|
||||
<string name="menu_save_profile">Spara som profil</string>
|
||||
<string name="menu_reset">Återställ filterinställningar</string>
|
||||
<string name="no_filters">Inga filter</string>
|
||||
<string name="filter_custom">Ändrat filter</string>
|
||||
<string name="filter_favorites">Favoriter</string>
|
||||
<string name="reorder">ändra ordning</string>
|
||||
<string name="delete">Ta bort</string>
|
||||
<string name="save_as_profile">Spara som profil</string>
|
||||
<string name="save_profile_enter_name">Ange namn för filterprofilen:</string>
|
||||
<string name="filterprofile_name_not_unique">Det finns redan en filterprofil med det namnet</string>
|
||||
<string name="filterprofiles_empty_state">Du har inga sparade filterprofiler</string>
|
||||
<string name="welcome_to_evmap">Välkommen till EVMap</string>
|
||||
<string name="welcome_1">Hitta laddare för elfordon i din närhet</string>
|
||||
<string name="welcome_2">Varje laddares färg motsvarar dess maximala laddeffekt</string>
|
||||
<string name="welcome_2_detail">Det här visas också i “Om” → “Vanliga frågor”</string>
|
||||
<string name="donation_dialog_title">Tack för att du använder EVMap</string>
|
||||
<string name="donation_dialog_detail">EVMap är fri programvara. Kodbidrag via GitHub tas tacksamt emot. Överväg gärna att donera en valfri summa till utvecklaren för att hjälpa till att täcka driftskostnader.</string>
|
||||
<string name="chargeprice_donation_dialog_title">Du är en ambitiös prisjämförare!</string>
|
||||
<string name="chargeprice_donation_dialog_detail">Du använder prisjämförelsefunktionen flitigt. Hjälp gärna till att täcka kostnaderna för denna funktion genom att stödja EVMap med en donation.</string>
|
||||
<string name="deleted_item">Tog bort “%s”</string>
|
||||
<string name="undo">Ångra</string>
|
||||
<string name="rename">Döp om</string>
|
||||
<string name="charging_barrierfree">Kan användas utan registrering</string>
|
||||
<plurals name="charge_cards_compatible_num">
|
||||
<item quantity="one">%d kompatibel betalningsalternativ</item>
|
||||
<item quantity="other">%d kompatibla betalningsalternativ</item>
|
||||
</plurals>
|
||||
<string name="navigate">Navigera</string>
|
||||
<string name="verified">verifierad</string>
|
||||
<string name="verified_desc">Laddaren har någon gång bekräftats fungera av en medlem i %s-gemenskapen</string>
|
||||
<string name="charge_price_format">%2$s%1$.2f</string>
|
||||
<string name="charge_price_average_format">⌀ %2$s%1$.2f/kWh</string>
|
||||
<string name="charge_price_kwh_format">%2$s%1$.2f/kWh</string>
|
||||
<string name="charge_price_minute_format">%2$s%1$.2f/min</string>
|
||||
<string name="chargeprice_select_connector">Välj laddkontakt</string>
|
||||
<string name="chargeprice_provider_customer_tariff">Endast för befintliga kunder</string>
|
||||
<string name="edit_on_goingelectric_info">Logga in hos GoingElectric.de om den här sidan är tom</string>
|
||||
<string name="percent_format">%.0f%%</string>
|
||||
<string name="chargeprice_session_fee">sessionsavgift</string>
|
||||
<string name="chargeprice_per_kwh">per kWh</string>
|
||||
<string name="chargeprice_per_minute">per min</string>
|
||||
<string name="chargeprice_blocking_fee">Trängselavgift >%s</string>
|
||||
<string name="chargeprice_no_tariffs_found">Inga prisplaner för den här laddstationen hos Chargeprice.app</string>
|
||||
<string name="powered_by_chargeprice">tillhandahålls av Chargeprice</string>
|
||||
<string name="chargeprice_base_fee">Grundavgift: %2$s%1$.2f/month</string>
|
||||
<string name="chargeprice_min_spend">Minimiavgift: %2$s%1$.2f/month</string>
|
||||
<string name="settings_chargeprice">Prisjämförelse</string>
|
||||
<string name="pref_my_vehicle">Mina fordon</string>
|
||||
<string name="pref_chargeprice_no_base_fee">Utelämna prisplaner med månadsavgift</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs">Inkludera prisplaner med kundrabatter</string>
|
||||
<string name="chargeprice_select_car_first">Vänligen välj först din bilmodell bland inställningarna</string>
|
||||
<string name="chargeprice_battery_range">Ladda från %1$.0f%% till %2$.0f%%</string>
|
||||
<string name="chargeprice_battery_range_from">Ladda från</string>
|
||||
<string name="chargeprice_battery_range_to">till</string>
|
||||
<string name="chargeprice_stats">(%1$.0f kWh, ca. %2$s, ⌀ %3$.0f kW)</string>
|
||||
<string name="chargeprice_vehicle">Fordon</string>
|
||||
<string name="chargeprice_price_not_available">Pris saknas</string>
|
||||
<string name="pref_chargeprice_show_provider_customer_tariffs_summary">Elbolag erbjuder ibland förmånliga priser till sina kunder</string>
|
||||
<string name="close">Stäng</string>
|
||||
<string name="chargeprice_title">Priser</string>
|
||||
<string name="chargeprice_connection_error">Kunde inte hämta priser</string>
|
||||
<string name="chargeprice_no_compatible_connectors">Inga kompatibla laddkontakter vid den här laddstationen</string>
|
||||
<string name="pref_chargeprice_currency">Valuta</string>
|
||||
<string name="pref_my_tariffs">Mina prisplaner</string>
|
||||
<plurals name="pref_my_tariffs_summary">
|
||||
<item quantity="one">(kommer framhävas i prisjämförelsen)</item>
|
||||
<item quantity="other">(kommer framhävas i prisjämförelsen)</item>
|
||||
</plurals>
|
||||
<string name="chargeprice_all_tariffs_selected">alla prisplaner valda</string>
|
||||
<string name="license">Licens</string>
|
||||
<string name="settings_charger_data">Laddstationer</string>
|
||||
<string name="pref_data_source">Datakälla</string>
|
||||
<plurals name="chargeprice_some_tariffs_selected">
|
||||
<item quantity="one">%d prisplan vald</item>
|
||||
<item quantity="other">%d prisplaner valda</item>
|
||||
</plurals>
|
||||
<string name="unknown_operator">Okänd operatör</string>
|
||||
<string name="data_sources_description">Vänligen välj en datakälla för laddstationer. Det kan ändras senare i inställningarna.</string>
|
||||
<string name="data_source_goingelectric">GoingElectric.de</string>
|
||||
<string name="data_source_nobil">NOBIL</string>
|
||||
<string name="data_source_openstreetmap">OpenStreetMap</string>
|
||||
<string name="data_source_openchargemap">Open Charge Map</string>
|
||||
<string name="data_source_goingelectric_desc">Mycket bra i tysktalande länder. Beskrivningar på tyska. Underhålls av frivilliga.</string>
|
||||
<string name="data_source_nobil_desc"><![CDATA[Öppen data från myndigheter och allmänheten för de nordiska länderna.]]></string>
|
||||
<string name="data_source_openchargemap_desc"><![CDATA[Världsomspännande med varierande kvalitet. Beskrivningar på engelska eller på det lokala språket. Underhålls av frivilliga och har öppen data från myndigheter i några länder (t.ex. Nordamerika, Storbritannien, Frankrike och Norge).]]></string>
|
||||
<string name="data_source_openstreetmap_desc">Experimentellt stöd i EVMap, inte alla funktioner är tillgängliga.</string>
|
||||
<string name="next">nästa</string>
|
||||
<string name="get_started">Kom igång</string>
|
||||
<string name="got_it">Jag fattar</string>
|
||||
<string name="lets_go">Nu kör vi</string>
|
||||
<string name="crash_report_text">EVMap kraschade. Vänligen skicka en kraschrapport till utvecklaren.</string>
|
||||
<string name="crash_report_comment_prompt">Du kan skriva en kommentar nedan:</string>
|
||||
<string name="powered_by_mapbox">tillhandahålls av Mapbox</string>
|
||||
<string name="pref_search_provider">Sökleverantör</string>
|
||||
<string name="pref_search_provider_info"><![CDATA[Data för sökningar är dyr att hämta, särskilt från Google Maps. Överväg gärna att donera via “Om” → “Donera”.]]></string>
|
||||
<string name="github_sponsors">GitHub Sponsors</string>
|
||||
<string name="donate_desc">Stöd EVMaps utveckling med en engångsdonation</string>
|
||||
<string name="github_sponsors_desc">Stöd EVMap via GitHub Sponsors</string>
|
||||
<string name="unnamed_filter_profile">Namnlös filterprofil</string>
|
||||
<string name="privacy_link">https://ev-map.app/privacypolicy/</string>
|
||||
<string name="faq_link">https://ev-map.app/faq/</string>
|
||||
<string name="chargeprice_faq_link">https://ev-map.app/faq/#price-comparison-feature</string>
|
||||
<string name="referral_link">https://ev-map.app/referrals/</string>
|
||||
<string name="required">obligatorisk</string>
|
||||
<string name="edit_filter_profile">Ändra “%s”</string>
|
||||
<string name="pref_search_delete_recent">Ta bort senaste sökresultaten</string>
|
||||
<string name="deleted_recent_search_results">Senaste sökresultaten har tagits bort</string>
|
||||
<string name="settings_data_sources">Datakällor</string>
|
||||
<string name="help">Hjälp</string>
|
||||
<string name="settings_android_auto">Android Auto</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load">Tillåt obalanserad last</string>
|
||||
<string name="pref_chargeprice_allow_unbalanced_load_summary">Tillåt enfasig AC-laddning över 4,5 kW</string>
|
||||
<string name="pref_map_rotate_gestures_enabled">Kartrotation</string>
|
||||
<string name="pref_map_rotate_gestures_on">Använd två fingrar för att rotera kartan</string>
|
||||
<string name="pref_map_rotate_gestures_off">Rotation av (norr alltid uppåt)</string>
|
||||
<string name="refresh_live_data">uppdatera realtidsstatus</string>
|
||||
<string name="autocomplete_connection_error">Kunde inte hämta förslag</string>
|
||||
<string name="pref_language_device_default">Denna enhets förval</string>
|
||||
<string name="pref_darkmode_device_default">Denna enhets förval</string>
|
||||
<string name="pref_darkmode_always_on">alltid på</string>
|
||||
<string name="pref_darkmode_always_off">alltid av</string>
|
||||
<string name="pref_provider_google_maps">Google Maps</string>
|
||||
<string name="pref_provider_osm">OpenStreetMap</string>
|
||||
<string name="pref_provider_osm_mapbox">OpenStreetMap (Mapbox)</string>
|
||||
<string name="about_contributors">Bidragsgivare</string>
|
||||
<string name="about_contributors_text">Tack till alla som bidragit med kod och översättningar till EVMap:</string>
|
||||
<string name="utilization_prediction">Utnyttjandeuppskattning</string>
|
||||
<string name="powered_by_fronyx">tillhandahålls av fronyx</string>
|
||||
<string name="prediction_help">Uppskattningen baseras på faktorer som veckodag, tid på dygnet och tidigare användning så att du kan undvika överfulla laddare. Ingen garanti.</string>
|
||||
<string name="prediction_time_colon">%s:</string>
|
||||
<plurals name="prediction_number_available">
|
||||
<item quantity="one">%1$d/%2$d tillgänglig</item>
|
||||
<item quantity="other">%1$d/%2$d tillgängliga</item>
|
||||
</plurals>
|
||||
<string name="pref_prediction_enabled">Visa uppskattad utnyttjandegrad</string>
|
||||
<string name="pref_prediction_enabled_summary">för laddare med stöd\n(just nu endast DC i Tyskland)</string>
|
||||
<string name="prediction_only">(%s endast)</string>
|
||||
<string name="prediction_dc_plugs_only">DC-laddkontakter</string>
|
||||
<string name="data_source_switched_to">Bytte datakälla till %s</string>
|
||||
<string name="pref_applink_associate">Öppna igenkända länkar</string>
|
||||
<string name="pref_applink_associate_summary">från goingelectric.de och openchargemap.org</string>
|
||||
<string name="chargeprice_header_my_tariffs">Mina prisplaner</string>
|
||||
<string name="chargeprice_header_other_tariffs">Övriga prisplaner</string>
|
||||
<string name="developer_mode_enabled">Utvecklarläge aktiverat</string>
|
||||
<string name="developer_options">Utvecklaralternativ</string>
|
||||
<string name="disable_developer_mode">Inaktivera utvecklarläge</string>
|
||||
<string name="developer_mode_disabled">Utvecklarläge inaktiverat</string>
|
||||
<string name="gps">GPS</string>
|
||||
<string name="compass">Kompass</string>
|
||||
<string name="charger_website">Webbsida</string>
|
||||
<string name="location_status">Status för platsleverantör</string>
|
||||
<string name="pref_tesla_account">Teslakonto</string>
|
||||
<string name="pref_tesla_account_enabled">Inloggad som %s</string>
|
||||
<string name="pref_tesla_account_disabled">Logga in för att se realtidsstatus för Tesla Supercharger. Inget Teslafordon krävs</string>
|
||||
<string name="logging_in">Loggar in…</string>
|
||||
<string name="log_out">Logga ut</string>
|
||||
<string name="logged_out">Utloggad</string>
|
||||
<string name="login">Logga in</string>
|
||||
<string name="login_error">Inloggning misslyckades</string>
|
||||
<string name="tesla_pricing_owners">Endast Teslafordon:</string>
|
||||
<string name="tesla_pricing_members">Teslafordon & medlemmar:</string>
|
||||
<string name="tesla_pricing_others">Övriga kunder:</string>
|
||||
<string name="pricing_up_to">upp till %s</string>
|
||||
<string name="tesla_pricing_other_times">Övriga tider:</string>
|
||||
<string name="tesla_pricing_blocking_fee">Trängselavgift: %s</string>
|
||||
<string name="average_utilization">Genomsnittligt utnyttjande</string>
|
||||
<string name="website">Webbsida</string>
|
||||
<string name="pref_map_scale">Visa skalstreck på kartan</string>
|
||||
<string name="pref_map_scale_meters_and_miles">Både miles och meter på kartskalstrecket</string>
|
||||
<string name="pref_units">Enheter</string>
|
||||
<string name="pref_units_default">Denna enhets förval</string>
|
||||
<string name="pref_units_metric">Metriska</string>
|
||||
<string name="pref_units_imperial">Brittiska</string>
|
||||
<string name="data_retrieved_at">Data hämtat %s</string>
|
||||
<string name="settings_caching">Datacache</string>
|
||||
<string name="settings_cache_count">Cachestorlek</string>
|
||||
<string name="settings_cache_clear">Töm cache</string>
|
||||
<string name="settings_cache_clear_summary">Tar bort alla cachade laddare, förutom favoriter</string>
|
||||
<string name="settings_cache_count_summary">%1$d laddare cachade, %2$.1f MB</string>
|
||||
<string name="auto_location_service">EVMap körs i Android Auto och använder din plats.</string>
|
||||
<string name="auto_no_chargers_found">Inga laddare i närheten hittades</string>
|
||||
<string name="auto_no_favorites_found">Inga favoriter hittades</string>
|
||||
<string name="open_in_app">Öppna i app</string>
|
||||
<string name="opened_on_phone">Öppnat på telefon</string>
|
||||
<string name="auto_location_permission_needed">För att använda EVMap i Android Auto måste du tillåta platsåtkomst.</string>
|
||||
<string name="auto_vehicle_data_permission_needed">För den här funktionen behöver EVMap åtkomst till ditt fordons data.</string>
|
||||
<string name="grant_on_phone">Godkänn på telefon</string>
|
||||
<string name="auto_chargers_closeby">Laddare i närheten</string>
|
||||
<string name="auto_favorites">Favoriter</string>
|
||||
<string name="auto_chargers_near_location">Nära %s</string>
|
||||
<string name="auto_fault_report_date">⚠️ Felrapport (%s)</string>
|
||||
<string name="auto_no_refresh_possible">Ytterligare uppdateringar är ej möjliga. Vänligen gå tillbaka och börja om.</string>
|
||||
<string name="auto_prices">Priser</string>
|
||||
<string name="auto_vehicle_data">Fordonsdata</string>
|
||||
<string name="auto_charging_level">Laddnivå</string>
|
||||
<string name="auto_no_data">Otillgänglig</string>
|
||||
<string name="auto_range">Räckvidd</string>
|
||||
<string name="auto_speed">Hastighet</string>
|
||||
<string name="auto_heading">Riktning</string>
|
||||
<string name="auto_settings">Inställningar</string>
|
||||
<string name="welcome_android_auto">Android Auto-stöd</string>
|
||||
<string name="welcome_android_auto_detail">Du kan också använda EVMap i Android Auto om bilen stöder det. Välj då bara EVMap i Android Auto-menyn.</string>
|
||||
<string name="sounds_cool">Låter schysst</string>
|
||||
<string name="auto_chargeprice_vehicle_unavailable">EVMap kunde inte avgöra ditt fordons modell.</string>
|
||||
<string name="auto_chargeprice_vehicle_unknown">Inga valda fordon i appen matchar detta fordon (%1$s %2$s).</string>
|
||||
<string name="auto_chargeprice_vehicle_ambiguous">Flera valda fordon i appen matchar detta fordon (%1$s %2$s).</string>
|
||||
<string name="auto_chargers_ahead">Endast laddare längs körriktningen</string>
|
||||
<string name="settings_android_auto_chargeprice_range">Laddintervall för prisjämförelse</string>
|
||||
<string name="selecting_all">Markerade alla poster</string>
|
||||
<string name="selecting_none">Avmarkerade alla poster</string>
|
||||
<string name="loading">Hämtar…</string>
|
||||
<string name="auto_multipage_goto">Sida %d</string>
|
||||
<string name="auto_multipage">(%1$d/%2$d)</string>
|
||||
<string name="reload">Uppdatera</string>
|
||||
<string name="accept_privacy"><![CDATA[Jag har läst och accepterar EVMaps <a href=\"%s\">integritetspolicy</a>.]]></string>
|
||||
<string name="referrals">Rekommendationslänkar</string>
|
||||
<string name="referrals_info">Du kan också använda en av rekommendationslänkarna nedan för att stödja utvecklaren genom ett köp.</string>
|
||||
<string name="referral_tesla">Tesla</string>
|
||||
<string name="generic_connection_error">Kunde inte hämta data</string>
|
||||
<string name="copied">Kopierat till urklipp</string>
|
||||
<string name="downloading_chargers_percent">Laddar ner… %.0f%%</string>
|
||||
<string name="status_available">Tillgänglig</string>
|
||||
<string name="status_occupied">Upptagen</string>
|
||||
<string name="status_charging">Laddar</string>
|
||||
<string name="status_faulted">Ur funktion</string>
|
||||
<string name="status_unknown">Okänd status</string>
|
||||
<string name="status_since">%1$s sedan %2$s</string>
|
||||
<string name="charger_name">Laddstationsnamn</string>
|
||||
<string name="pref_chargeprice_native_integration">Prisjämförelse i EVMap</string>
|
||||
<string name="pref_chargeprice_native_integration_on">Priser kommer visas direkt i EVMap</string>
|
||||
<string name="pref_chargeprice_native_integration_off">Prisjämförelseknappen kommer länka till Chargeprice-app eller webbsida</string>
|
||||
<string name="auto_zoom_for_details">Zooma in för att se detaljer</string>
|
||||
<string name="accessibility_public">Offentlig</string>
|
||||
<string name="accessibility_visitors">Besökare</string>
|
||||
<string name="accessibility_employees">Anställda</string>
|
||||
<string name="accessibility_by_appointment">Efter överenskommelse</string>
|
||||
<string name="accessibility_residents">Boende</string>
|
||||
</resources>
|
||||
@@ -26,11 +26,13 @@
|
||||
</string-array>
|
||||
<string-array name="pref_data_source_names">
|
||||
<item>@string/data_source_goingelectric</item>
|
||||
<item>@string/data_source_nobil</item>
|
||||
<item>@string/data_source_openchargemap</item>
|
||||
<item>@string/data_source_openstreetmap</item>
|
||||
</string-array>
|
||||
<string-array name="pref_data_source_values" tranlatable="false">
|
||||
<item>goingelectric</item>
|
||||
<item>nobil</item>
|
||||
<item>openchargemap</item>
|
||||
<item>openstreetmap</item>
|
||||
</string-array>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<string name="connectors">Connectors</string>
|
||||
<string name="no_maps_app_found">Install a navigation app first</string>
|
||||
<string name="no_browser_app_found">Install a web browser first</string>
|
||||
<string name="no_email_app_found">Install an email app first</string>
|
||||
<string name="address">Address</string>
|
||||
<string name="operator">Operator</string>
|
||||
<string name="network">Network</string>
|
||||
@@ -59,6 +60,7 @@
|
||||
<string name="filter_connectors">Connectors</string>
|
||||
<string name="plug_type_1">Type 1</string>
|
||||
<string name="plug_type_2">Type 2</string>
|
||||
<string name="plug_type_2_tethered">Type 2 tethered cable</string>
|
||||
<string name="plug_type_3a">Type 3A</string>
|
||||
<string name="plug_type_3c">Type 3C</string>
|
||||
<string name="plug_ccs">CCS</string>
|
||||
@@ -107,6 +109,7 @@
|
||||
<string name="filter_open_247">Available 24/7</string>
|
||||
<string name="filter_barrierfree">Usable without registration</string>
|
||||
<string name="filter_exclude_faults">Exclude chargers with reported faults</string>
|
||||
<string name="filter_accessibility">Charger accessibility</string>
|
||||
<string name="charge_cards">Payment methods</string>
|
||||
<string name="and_n_others">and %d others</string>
|
||||
<string name="pref_map_provider">Map provider</string>
|
||||
@@ -225,9 +228,11 @@
|
||||
<string name="unknown_operator">Unknown operator</string>
|
||||
<string name="data_sources_description">Please pick a data source for charging stations. It can later be changed in the app settings.</string>
|
||||
<string name="data_source_goingelectric">GoingElectric.de</string>
|
||||
<string name="data_source_nobil">NOBIL</string>
|
||||
<string name="data_source_openstreetmap">OpenStreetMap</string>
|
||||
<string name="data_source_openchargemap">Open Charge Map</string>
|
||||
<string name="data_source_goingelectric_desc">Great in the German-speaking countries. Descriptions in German. Community-maintained.</string>
|
||||
<string name="data_source_nobil_desc"><![CDATA[Open government and community provided data in the Nordic countries.]]></string>
|
||||
<string name="data_source_openchargemap_desc"><![CDATA[Worldwide, with varying quality. Descriptions in English or the local language. Community-maintained and open government data in some countries (e.g. North America, UK, France, Norway).]]></string>
|
||||
<string name="data_source_openstreetmap_desc">Experimental support in EVMap, not all features available.</string>
|
||||
<string name="next">next</string>
|
||||
@@ -376,4 +381,9 @@
|
||||
<string name="pref_chargeprice_native_integration_on">Pricing data will be shown directly in EVMap</string>
|
||||
<string name="pref_chargeprice_native_integration_off">Price comparison button will refer to the Chargeprice app or website</string>
|
||||
<string name="auto_zoom_for_details">Zoom in to see details</string>
|
||||
<string name="accessibility_public">Public</string>
|
||||
<string name="accessibility_visitors">Visitors</string>
|
||||
<string name="accessibility_employees">Employees</string>
|
||||
<string name="accessibility_by_appointment">By appointment</string>
|
||||
<string name="accessibility_residents">Residents</string>
|
||||
</resources>
|
||||
@@ -10,12 +10,10 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import net.vonforst.evmap.FakeAndroidKeyStore
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Robolectric
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.internal.DoNotInstrument
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
buildscript {
|
||||
val kotlinVersion by extra("2.0.21")
|
||||
val aboutLibsVersion by extra("10.9.1")
|
||||
val navVersion by extra("2.7.7")
|
||||
val aboutLibsVersion by extra("12.2.4")
|
||||
val navVersion by extra("2.9.3")
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.9.3")
|
||||
classpath("com.android.tools.build:gradle:8.12.0")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
classpath("com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$aboutLibsVersion")
|
||||
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion")
|
||||
|
||||
@@ -38,6 +38,9 @@ be put into the app in the form of a resource file called `apikeys.xml` under
|
||||
<string name="acra_credentials" translatable="false">
|
||||
insert your ACRA crash reporting credentials here
|
||||
</string>
|
||||
<string name="nobil_key" translatable="false">
|
||||
insert your nobil key here
|
||||
</string>
|
||||
</resources>
|
||||
```
|
||||
|
||||
@@ -167,6 +170,14 @@ in German.
|
||||
|
||||
</details>
|
||||
|
||||
### **NOBIL**
|
||||
|
||||
NOBIL lists charging stations in the Nordic countries (Denmark, Finland, Iceland, Norway, Sweden)
|
||||
and provides an open [API](https://info.nobil.no/api) to access the data.
|
||||
|
||||
To get a NOBIL API key, fill in and submit the form on [this page](https://info.nobil.no/api).
|
||||
Then, wait for an an e-mail with your API key.
|
||||
|
||||
### **OpenChargeMap**
|
||||
|
||||
[API documentation](https://openchargemap.org/site/develop/api)
|
||||
|
||||
2
fastlane/metadata/android/de-DE/changelogs/244.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/244.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
2
fastlane/metadata/android/de-DE/changelogs/246.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/246.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Anzeigefehler behoben
|
||||
5
fastlane/metadata/android/de-DE/changelogs/248.txt
Normal file
5
fastlane/metadata/android/de-DE/changelogs/248.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Verbesserungen:
|
||||
- Preisvergleich: Zurücksetzen der Ladebereichsauswahl durch Tippen auf den Titel darüber
|
||||
|
||||
Fehler behoben:
|
||||
- Anzeigefehler behoben
|
||||
2
fastlane/metadata/android/de-DE/changelogs/250.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/250.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Anzeigefehler behoben
|
||||
3
fastlane/metadata/android/de-DE/changelogs/252.txt
Normal file
3
fastlane/metadata/android/de-DE/changelogs/252.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Fehler behoben:
|
||||
- Anzeigefehler behoben
|
||||
- Abstürze behoben
|
||||
2
fastlane/metadata/android/de-DE/changelogs/254.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/254.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Echtzeitdaten für Tesla Supercharger repariert
|
||||
3
fastlane/metadata/android/de-DE/changelogs/256.txt
Normal file
3
fastlane/metadata/android/de-DE/changelogs/256.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Fehler behoben:
|
||||
- Abstürze behoben
|
||||
- Aktueller Standort wurde nicht immer angezeigt, obwohl verfügbar
|
||||
1
fastlane/metadata/android/de-DE/changelogs/258.txt
Normal file
1
fastlane/metadata/android/de-DE/changelogs/258.txt
Normal file
@@ -0,0 +1 @@
|
||||
Kompatibilität mit Android 15
|
||||
8
fastlane/metadata/android/de-DE/changelogs/262.txt
Normal file
8
fastlane/metadata/android/de-DE/changelogs/262.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Neue Funktionen:
|
||||
- Neue Datenquellen: OpenStreetMap, NOBIL
|
||||
- Android Auto, Android Automotive: Neue Karte inkl. Zoomen und Verschieben (wenn vom Auto unterstützt)
|
||||
- Neue Sprache: Schwedisch
|
||||
|
||||
Fehler behoben:
|
||||
- Anzeigefehler behoben
|
||||
- Abstürze behoben
|
||||
2
fastlane/metadata/android/de-DE/changelogs/264.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/264.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Fehler behoben:
|
||||
- Absturz behoben
|
||||
2
fastlane/metadata/android/en-US/changelogs/244.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/244.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bugfixes:
|
||||
- Fixed crashes
|
||||
2
fastlane/metadata/android/en-US/changelogs/246.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/246.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bugfixes:
|
||||
- Fixed display errors
|
||||
5
fastlane/metadata/android/en-US/changelogs/248.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/248.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Improvements:
|
||||
- Price comparison: Reset charging range by tapping on title above it
|
||||
|
||||
Bugfixes:
|
||||
- Fixed display errors
|
||||
2
fastlane/metadata/android/en-US/changelogs/250.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/250.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bugfixes:
|
||||
- Fixed display errors
|
||||
3
fastlane/metadata/android/en-US/changelogs/252.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/252.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Bugfixes:
|
||||
- Fixed display errors
|
||||
- Fixed crashes
|
||||
2
fastlane/metadata/android/en-US/changelogs/254.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/254.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bugfixes:
|
||||
- Fixed realtime data for Tesla Superchargers
|
||||
3
fastlane/metadata/android/en-US/changelogs/256.txt
Normal file
3
fastlane/metadata/android/en-US/changelogs/256.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Bugfixes:
|
||||
- Fixed crashes
|
||||
- Fixed current location not being shown despite it being available
|
||||
1
fastlane/metadata/android/en-US/changelogs/258.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/258.txt
Normal file
@@ -0,0 +1 @@
|
||||
Android 15 compatibility
|
||||
8
fastlane/metadata/android/en-US/changelogs/262.txt
Normal file
8
fastlane/metadata/android/en-US/changelogs/262.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
New Features:
|
||||
- New data sources: OpenStreetMap, NOBIL
|
||||
- Android Auto, Android Automotive OS: New map with pan & zoom (if supported by car)
|
||||
- New language: Swedish
|
||||
|
||||
Bugfixes:
|
||||
- Fixed display errors
|
||||
- Fixed crashes
|
||||
2
fastlane/metadata/android/en-US/changelogs/264.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/264.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Bugfixes:
|
||||
- Fixed crash
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Sat Aug 06 15:33:46 CEST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
Reference in New Issue
Block a user