Compare commits

..

35 Commits
flows ... 2.0.0

Author SHA1 Message Date
johan12345
a20f25af17 add Nobil API key on CI 2025-09-21 17:10:26 +02:00
johan12345
b2a2114c88 Release 2.0.0 (first beta) 2025-09-21 16:52:30 +02:00
johan12345
c2896ade45 export licenses for Appning on CI 2025-09-21 16:52:30 +02:00
johan12345
45983bce7f add changelogs from 1.9.x branch 2025-09-21 16:36:00 +02:00
Hosted Weblate
d0fffb1a97 Translated using Weblate (Swedish)
Currently translated at 99.7% (364 of 365 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Robert Högberg <robert.hogberg@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/sv/
Translation: EVMap/Android
2025-09-21 16:28:35 +02:00
johan12345
4819a10d03 add OSM taginfo.json
#97
2025-09-21 16:27:09 +02:00
johan12345
8a0f7e79f0 add .kotlin to .gitignore 2025-09-21 16:27:09 +02:00
johan12345
c727d9f1b8 delete removed chargers from DB during fullDownload
addresses #364 for data sources that use fullDownload (OSM, Nobil)
2025-09-21 15:52:13 +02:00
johan12345
e5d0ebbbb5 add German translations for Nobil strings 2025-09-21 15:52:13 +02:00
johan12345
ee4b5e7319 when using spatial index, explicitly specify the column 2025-09-21 15:52:13 +02:00
johan12345
fecde441f1 fix migration 26 2025-09-21 15:52:13 +02:00
Robert Högberg
cb1543cb4a Adapt ChargeLocationsDaoTest to nobil changes 2025-09-21 15:52:13 +02:00
johan12345
276daac607 fix CI build 2025-09-21 15:52:13 +02:00
johan12345
f7d39a1ba5 fix DB migrations 2025-09-21 15:52:13 +02:00
Robert Högberg
fa09b9188e Add support for full nobil data download
This improves the speed of the nobil implementation since all data
is cached on the device and it also reduces the load on the nobil server(s)
since fewer nobil requests are needed.
2025-09-21 15:52:13 +02:00
Robert Högberg
b31e55f130 Add Tesla realtime for nobil
Copy-n-paste implementation that needs to be cleaned up.
2025-09-21 15:52:13 +02:00
Robert Högberg
c494b0d5e2 Use EnBw realtime data for nobil 2025-09-21 15:52:13 +02:00
Robert Högberg
272b86ff88 Parse payment methods into Cost object description 2025-09-21 15:52:13 +02:00
Robert Högberg
32de28bc1c Add connector type filter 2025-09-21 15:52:13 +02:00
Robert Högberg
4cd6c44ba1 Add charge location accessibility to ChargeLocation and as filter 2025-09-21 15:52:13 +02:00
Robert Högberg
3265694c51 Add nobil api key handling in build.gradle.kts 2025-09-21 15:52:13 +02:00
Robert Högberg
529be2cc34 Hide share-charge-location-button if there's no URL for the location 2025-09-21 15:52:13 +02:00
Robert Högberg
00862b66a1 Add Chargelocation.dataSourceUrl and make ChargeLocation.url optional
Nobil has no suitable sites to individual charging stations so url needs
to be optional and then we use dataSourceUrl instead in "data source button".
2025-09-21 15:52:13 +02:00
Robert Högberg
cabaa42772 Add evseId to class Chargepoint
.. and populate it in nobil data source
2025-09-21 15:52:13 +02:00
Robert Högberg
1663607171 Add URLs to edit nobil chargers
There's a web page for Swedish chargers, but we need to send email
for the other countries.
2025-09-21 15:52:13 +02:00
Robert Högberg
126c47bbc1 Add basic filters 2025-09-21 15:52:13 +02:00
Robert Högberg
b93d01f96d Basic NOBIL implementation 2025-09-21 15:52:13 +02:00
Hosted Weblate
7fb5df29e4 Translated using Weblate (German)
Currently translated at 100.0% (365 of 365 strings)

Translated using Weblate (German)

Currently translated at 100.0% (365 of 365 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Johan von Forstner <johan.forstner@gmail.com>
Co-authored-by: mcliquid <info@mcliquid.de>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/de/
Translation: EVMap/Android
2025-09-20 17:08:58 +02:00
Hosted Weblate
b878d37982 Translated using Weblate (Czech)
Currently translated at 100.0% (365 of 365 strings)

Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/cs/
Translation: EVMap/Android
2025-09-14 19:25:14 +02:00
Hosted Weblate
0f7aa44d8e Translated using Weblate (Estonian)
Currently translated at 100.0% (365 of 365 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/et/
Translation: EVMap/Android
2025-09-14 19:25:13 +02:00
johan12345
d8e1c36993 MapSurfaceCallback: Implement double-tap zoom 2025-09-09 23:47:04 +02:00
johan12345
03f613fa4b MapSurfaceCallback: Apply status bar offset on all AAOS systems 2025-09-09 23:47:04 +02:00
Robert Högberg
aba533e553 Add Swedish translation 2025-09-09 22:20:45 +02:00
Robert Högberg
307af88f01 Separate connectors "type 2 socket" and "type 2 plug"
This avoids duplicate "Type 2" entries when using filters to select
connectors and when showing charger details.
2025-09-09 20:39:56 +02:00
johan12345
8478948d5f upgrade LocaleConfigX
possible fix for #386
2025-09-08 23:21:21 +02:00
67 changed files with 3135 additions and 379 deletions

View File

@@ -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 & export libraries
- name: Build app release & export licenses
env:
GOINGELECTRIC_API_KEY: ${{ secrets.GOINGELECTRIC_API_KEY }}
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
@@ -35,11 +35,15 @@ 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 exportLibraryDefinitions assembleRelease --no-daemon
- name: Export licenses in Appning format
run: python3 _ci/export_licenses_appning.py
- name: release
uses: actions/create-release@v1
id: create_release
@@ -97,3 +101,21 @@ jobs:
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

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
*.iml
.gradle
.kotlin
/local.properties
/.idea/*
.DS_Store

View File

@@ -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>

View File

@@ -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
View 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"
}
]
}

View File

@@ -21,8 +21,8 @@ android {
minSdk = 21
targetSdk = 36
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode = 230
versionName = "1.9.6"
versionCode = 262
versionName = "2.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -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()
@@ -317,7 +328,7 @@ dependencies {
implementation("io.michaelrocks.bimap:bimap:1.1.0")
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"

View File

@@ -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')"

View 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')"
]
}
}

View File

@@ -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 {

View 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>

View 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>

View 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>

View File

@@ -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(

View File

@@ -8,7 +8,7 @@ import kotlin.math.abs
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,

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View 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
}
}

View 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")
}

View 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
}
}
}

View File

@@ -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(

View File

@@ -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",

View File

@@ -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

View File

@@ -448,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)
)

View File

@@ -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) {

View File

@@ -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
@@ -440,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 {
@@ -501,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
@@ -509,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(
@@ -698,6 +717,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MenuProvider {
removeSearchFocus()
binding.fabDirections.show()
detailAppBarBehavior.setToolbarTitle(it.name)
updateShareItemVisibility()
updateFavoriteToggle()
markerManager?.highlighedCharger = it
markerManager?.animateBounce(it)
@@ -808,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 {
@@ -871,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 -> {

View File

@@ -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) {

View File

@@ -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

View File

@@ -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()
}

View File

@@ -3,11 +3,6 @@ package net.vonforst.evmap.storage
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.viewmodel.Resource
import net.vonforst.evmap.viewmodel.Status
@@ -146,44 +141,4 @@ class PreferCacheLiveData(
}
}
}
}
/**
* Flow-based implementation that allows loading data both from a cache and an API.
*
* It first tries loading from cache, and if the result is newer than `cacheSoftLimit` it does not
* reload from the API.
*/
fun preferCacheFlow(
cache: Flow<ChargeLocation?>,
api: Flow<Resource<ChargeLocation>>,
cacheSoftLimit: Duration
): Flow<Resource<ChargeLocation>> = flow {
emit(Resource.loading(null)) // initial state
val cacheRes = cache.firstOrNull() // read cache once
if (cacheRes != null) {
if (cacheRes.isDetailed && cacheRes.timeRetrieved > Instant.now() - cacheSoftLimit) {
emit(Resource.success(cacheRes))
return@flow
} else {
emit(Resource.loading(cacheRes))
emitAll(api.map { apiRes ->
when (apiRes.status) {
Status.SUCCESS -> apiRes
Status.ERROR -> Resource.error(apiRes.message, cacheRes)
Status.LOADING -> Resource.loading(cacheRes)
}
})
}
} else {
// No cache → straight to API
emitAll(api.map { apiRes ->
when (apiRes.status) {
Status.SUCCESS -> apiRes
Status.ERROR -> Resource.error(apiRes.message, null)
Status.LOADING -> Resource.loading(null)
}
})
}
}

View File

@@ -14,14 +14,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.vonforst.evmap.api.ChargepointApi
@@ -30,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
@@ -41,9 +35,9 @@ import net.vonforst.evmap.utils.splitAtAntimeridian
import net.vonforst.evmap.viewmodel.Resource
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.await
import net.vonforst.evmap.viewmodel.singleSwitchMap
import java.time.Duration
import java.time.Instant
import kotlin.time.TimeSource
const val CLUSTER_MAX_ZOOM_LEVEL = 11f
@@ -75,6 +69,9 @@ abstract class ChargeLocationsDao {
@Query("DELETE FROM chargelocation WHERE NOT EXISTS (SELECT 1 FROM favorite WHERE favorite.chargerId = chargelocation.id)")
abstract suspend fun deleteAllIfNotFavorite()
@Query("DELETE FROM chargelocation WHERE dataSource == :dataSource AND id NOT IN (:chargerIds)")
abstract suspend fun deleteIdNotIn(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,
@@ -90,7 +87,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,
@@ -101,7 +98,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,
@@ -177,9 +174,10 @@ private const val TAG = "ChargeLocationsDao"
* and clustering functionality.
*/
class ChargeLocationsRepository(
private val api: ChargepointApi<ReferenceData>, private val scope: CoroutineScope,
api: ChargepointApi<ReferenceData>, private val scope: CoroutineScope,
private val db: AppDatabase, private val prefs: PreferenceDataSource
) {
val api = MutableLiveData<ChargepointApi<ReferenceData>>().apply { value = api }
// if zoom level is below this value, server-side clustering will be used (if the API provides it)
private val serverSideClusteringThreshold = 9f
@@ -188,33 +186,39 @@ class ChargeLocationsRepository(
// if cached data is available and more recent than this duration, API will not be queried
private val cacheSoftLimit = Duration.ofDays(1)
val referenceData = when (api) {
is GoingElectricApiWrapper -> {
GEReferenceDataRepository(
api,
scope,
db.geReferenceDataDao(),
prefs
).getReferenceData()
}
val referenceData = this.api.switchMap { api ->
when (api) {
is GoingElectricApiWrapper -> {
GEReferenceDataRepository(
api,
scope,
db.geReferenceDataDao(),
prefs
).getReferenceData()
}
is OpenChargeMapApiWrapper -> {
OCMReferenceDataRepository(
api,
scope,
db.ocmReferenceDataDao(),
prefs
).getReferenceData()
}
is NobilApiWrapper -> {
NobilReferenceDataRepository(scope, prefs).getReferenceData()
}
is OpenStreetMapApiWrapper -> {
OSMReferenceDataRepository(db.osmReferenceDataDao()).getReferenceData()
}
is OpenChargeMapApiWrapper -> {
OCMReferenceDataRepository(
api,
scope,
db.ocmReferenceDataDao(),
prefs
).getReferenceData()
}
else -> {
throw RuntimeException("no reference data implemented")
is OpenStreetMapApiWrapper -> {
OSMReferenceDataRepository(db.osmReferenceDataDao()).getReferenceData()
}
else -> {
throw RuntimeException("no reference data implemented")
}
}
}.shareIn(scope, SharingStarted.Lazily, 1)
}
private val chargeLocationsDao = db.chargeLocationsDao()
private val savedRegionDao = db.savedRegionDao()
@@ -225,36 +229,41 @@ class ChargeLocationsRepository(
bounds: LatLngBounds,
zoom: Float,
filters: FilterValues?,
overrideCache: Boolean = false,
): Flow<List<ChargepointListItem>> {
overrideCache: Boolean = false
): LiveData<Resource<List<ChargepointListItem>>> {
if (bounds.crossesAntimeridian()) {
val (a, b) = bounds.splitAtAntimeridian()
val flowA = getChargepoints(a, zoom, filters, overrideCache)
val flowB = getChargepoints(b, zoom, filters, overrideCache)
return flowA.combine(flowB) { a, b -> a + b }
val liveDataA = getChargepoints(a, zoom, filters, overrideCache)
val liveDataB = getChargepoints(b, zoom, filters, overrideCache)
return combineLiveData(liveDataA, liveDataB)
}
val dbResult = flow {
val t1 = TimeSource.Monotonic.markNow()
val result = if (filters.isNullOrEmpty()) {
chargeLocationsDao.getChargeLocationsClustered(
bounds.southwest.latitude,
bounds.northeast.latitude,
bounds.southwest.longitude,
bounds.northeast.longitude,
api.id,
cacheLimitDate(api),
zoom
val api = api.value!!
val t1 = System.currentTimeMillis()
val dbResult = if (filters.isNullOrEmpty()) {
liveData {
emit(
Resource.success(
chargeLocationsDao.getChargeLocationsClustered(
bounds.southwest.latitude,
bounds.northeast.latitude,
bounds.southwest.longitude,
bounds.northeast.longitude,
api.id,
cacheLimitDate(api),
zoom
)
)
)
} else {
queryWithFiltersClustered(api, filters, bounds, zoom)
}
val t2 = TimeSource.Monotonic.markNow()
} else {
queryWithFiltersClustered(api, filters, bounds, zoom)
}.map {
val t2 = System.currentTimeMillis()
Log.d(TAG, "DB loading time: ${t2 - t1}")
Log.d(TAG, "number of chargers: ${result.size}")
emit(result)
Log.d(TAG, "number of chargers: ${it.data?.size}")
it
}
val filtersSerialized =
filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() }
?.serialize()
@@ -271,8 +280,8 @@ class ChargeLocationsRepository(
)
val useClustering = shouldUseServerSideClustering(zoom)
if (api.supportsOnlineQueries) {
val apiResult = flow {
val refData = referenceData.first()
val apiResult = liveData {
val refData = referenceData.await()
val time = Instant.now()
val result = api.getChargepoints(refData, bounds, zoom, useClustering, filters)
emit(applyLocalClustering(result, zoom))
@@ -327,11 +336,39 @@ class ChargeLocationsRepository(
}
}
private fun combineLiveData(
liveDataA: LiveData<Resource<List<ChargepointListItem>>>,
liveDataB: LiveData<Resource<List<ChargepointListItem>>>
) = MediatorLiveData<Resource<List<ChargepointListItem>>>().apply {
listOf(liveDataA, liveDataB).forEach {
addSource(it) {
val valA = liveDataA.value
val valB = liveDataB.value
val combinedList = if (valA?.data != null && valB?.data != null) {
valA.data + valB.data
} else if (valA?.data != null) {
valA.data
} else if (valB?.data != null) {
valB.data
} else null
if (valA?.status == Status.SUCCESS && valB?.status == Status.SUCCESS) {
Resource.success(combinedList)
} else if (valA?.status == Status.ERROR || valB?.status == Status.ERROR) {
Resource.error(valA?.message ?: valB?.message, combinedList)
} else {
Resource.loading(combinedList)
}
}
}
}
fun getChargepointsRadius(
location: LatLng,
radius: Int,
filters: FilterValues?
): LiveData<Resource<List<ChargeLocation>>> {
val api = api.value!!
val radiusMeters = radius.toDouble() * 1000
val dbResult = if (filters.isNullOrEmpty()) {
liveData {
@@ -365,7 +402,7 @@ class ChargeLocationsRepository(
)
if (api.supportsOnlineQueries) {
val apiResult = liveData {
val refData = referenceData.first()
val refData = referenceData.await()
val time = Instant.now()
val result =
api.getChargepointsRadius(
@@ -426,7 +463,7 @@ class ChargeLocationsRepository(
}
}
private suspend fun applyLocalClustering(
private fun applyLocalClustering(
result: Resource<ChargepointList>,
zoom: Float
): Resource<List<ChargepointListItem>> {
@@ -443,7 +480,7 @@ class ChargeLocationsRepository(
return Resource(result.status, clustered, result.message)
}
private suspend fun applyLocalClustering(
private fun applyLocalClustering(
chargers: List<ChargeLocation>,
zoom: Float
): List<ChargepointListItem> {
@@ -453,7 +490,7 @@ class ChargeLocationsRepository(
val useClustering = chargers.size > 500 || zoom <= CLUSTER_MAX_ZOOM_LEVEL
val chargersClustered = if (useClustering) {
withContext(Dispatchers.Default) {
Dispatchers.Default.run {
cluster(chargers, zoom)
}
} else chargers
@@ -463,8 +500,9 @@ class ChargeLocationsRepository(
fun getChargepointDetail(
id: Long,
overrideCache: Boolean = false
): Flow<Resource<ChargeLocation>> {
val dbResult = flow {
): LiveData<Resource<ChargeLocation>> {
val api = api.value!!
val dbResult = liveData {
emit(
chargeLocationsDao.getChargeLocationById(
id,
@@ -474,8 +512,9 @@ class ChargeLocationsRepository(
)
}
if (api.supportsOnlineQueries) {
val apiResult = flow {
val refData = referenceData.first()
val apiResult = liveData {
emit(Resource.loading(null))
val refData = referenceData.await()
val result = api.getChargepointDetail(refData, id)
emit(result)
if (result.status == Status.SUCCESS) {
@@ -485,15 +524,22 @@ class ChargeLocationsRepository(
return if (overrideCache) {
apiResult
} else {
preferCacheFlow(dbResult, apiResult, cacheSoftLimit)
PreferCacheLiveData(dbResult, apiResult, cacheSoftLimit)
}
} else {
return dbResult.map { Resource.success(it) }
}
}
fun getFilters(sp: StringProvider) = referenceData.map {
api.getFilters(it, sp)
fun getFilters(sp: StringProvider) = MediatorLiveData<List<Filter<FilterValue>>>().apply {
addSource(referenceData) { refData: ReferenceData? ->
refData?.let { value = api.value!!.getFilters(refData, sp) }
}
}
suspend fun getFiltersAsync(sp: StringProvider): List<Filter<FilterValue>> {
val refData = referenceData.await()
return api.value!!.getFilters(refData, sp)
}
val chargeCardMap by lazy {
@@ -508,29 +554,29 @@ class ChargeLocationsRepository(
}
}
private suspend fun queryWithFilters(
private fun queryWithFilters(
api: ChargepointApi<ReferenceData>,
filters: FilterValues,
bounds: LatLngBounds
): List<ChargeLocation> {
): LiveData<Resource<List<ChargeLocation>>> {
return queryWithFilters(api, filters, boundsSpatialIndexQuery(bounds))
}
private suspend fun queryWithFiltersClustered(
private fun queryWithFiltersClustered(
api: ChargepointApi<ReferenceData>,
filters: FilterValues,
bounds: LatLngBounds,
zoom: Float
): List<ChargepointListItem> {
): LiveData<Resource<List<ChargepointListItem>>> {
return queryWithFiltersClustered(api, filters, boundsSpatialIndexQuery(bounds), zoom)
}
private suspend fun queryWithFilters(
private fun queryWithFilters(
api: ChargepointApi<ReferenceData>,
filters: FilterValues,
location: LatLng,
radius: Double
): List<ChargeLocation> {
): LiveData<Resource<List<ChargeLocation>>> {
val region =
radiusSpatialIndexQuery(location, radius)
val order =
@@ -539,55 +585,81 @@ 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 suspend fun queryWithFilters(
private fun queryWithFilters(
api: ChargepointApi<ReferenceData>,
filters: FilterValues,
regionSql: String,
orderSql: String? = null
): List<ChargeLocation> {
val query = api.convertFiltersToSQL(filters, referenceData.first())
val after = cacheLimitDate(api)
val sql = buildFilteredQuery(query, regionSql, after, orderSql)
): LiveData<Resource<List<ChargeLocation>>> = referenceData.singleSwitchMap { refData ->
try {
val query = api.convertFiltersToSQL(filters, refData)
val after = cacheLimitDate(api)
val sql = buildFilteredQuery(query, regionSql, after, orderSql)
return chargeLocationsDao.getChargeLocationsCustom(
SimpleSQLiteQuery(
sql,
null
)
)
liveData {
emit(
Resource.success(
chargeLocationsDao.getChargeLocationsCustom(
SimpleSQLiteQuery(
sql,
null
)
)
)
)
}
} catch (e: NotImplementedError) {
MutableLiveData() // in this case we cannot get a DB result
}
}
private suspend fun queryWithFiltersClustered(
private fun queryWithFiltersClustered(
api: ChargepointApi<ReferenceData>,
filters: FilterValues,
regionSql: String,
zoom: Float,
orderSql: String? = null
): List<ChargepointListItem> = if (zoom > CLUSTER_MAX_ZOOM_LEVEL) {
queryWithFilters(api, filters, regionSql, orderSql).map { it }
} else {
val query = api.convertFiltersToSQL(filters, referenceData.first())
val after = cacheLimitDate(api)
val clusterPrecision = getClusterPrecision(zoom)
val sql = buildFilteredQuery(query, regionSql, after, orderSql, clusterPrecision)
): LiveData<Resource<List<ChargepointListItem>>> = referenceData.singleSwitchMap { refData ->
try {
if (zoom > CLUSTER_MAX_ZOOM_LEVEL) {
queryWithFilters(api, filters, regionSql, orderSql).map { it }
} else {
val query = api.convertFiltersToSQL(filters, refData)
val after = cacheLimitDate(api)
val clusterPrecision = getClusterPrecision(zoom)
val sql = buildFilteredQuery(query, regionSql, after, orderSql, clusterPrecision)
val clusters = chargeLocationsDao.getChargeLocationClustersCustom(
SimpleSQLiteQuery(
sql,
null
)
)
val singleChargers =
chargeLocationsDao.getChargeLocationsById(clusters.filter { it.clusterCount == 1 }
.map { it.ids }
.flatten(), prefs.dataSource, after)
clusters.filter { it.clusterCount > 1 }
.map { it.convert() } + singleChargers
liveData {
val clusters = chargeLocationsDao.getChargeLocationClustersCustom(
SimpleSQLiteQuery(
sql,
null
)
)
val singleChargers =
chargeLocationsDao.getChargeLocationsById(clusters.filter { it.clusterCount == 1 }
.map { it.ids }
.flatten(), prefs.dataSource, after)
emit(
Resource.success(
clusters.filter { it.clusterCount > 1 }
.map { it.convert() } + singleChargers
))
}
}
} catch (e: NotImplementedError) {
MutableLiveData(
Resource.error(
e.message,
null
)
) // in this case we cannot get a DB result
}
}
private fun buildFilteredQuery(
@@ -627,12 +699,14 @@ class ChargeLocationsRepository(
}.toString()
private suspend fun fullDownload() {
val api = api.value!!
if (!api.supportsFullDownload) return
val time = Instant.now()
val result = api.fullDownload()
try {
var insertJob: Job? = null
val chargerIds = mutableListOf<Long>()
result.chargers.chunked(1024).forEach {
insertJob?.join()
insertJob = withContext(Dispatchers.IO) {
@@ -640,8 +714,12 @@ class ChargeLocationsRepository(
chargeLocationsDao.insert(*it.toTypedArray())
}
}
chargerIds.addAll(it.map { it.id })
fullDownloadProgress.value = result.progress
}
// delete chargers that have been removed
chargeLocationsDao.deleteIdNotIn(api.id, chargerIds)
val region = Mbr(
-180.0,
-90.0,

View File

@@ -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")
}
}
}
/**

View File

@@ -1,15 +1,9 @@
package net.vonforst.evmap.storage
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.Transaction
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.room.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.goingelectric.GEChargeCard
import net.vonforst.evmap.api.goingelectric.GEReferenceData
@@ -42,7 +36,7 @@ abstract class GEReferenceDataDao {
}
@Query("SELECT * FROM genetwork")
abstract fun getAllNetworks(): Flow<List<GENetwork>>
abstract fun getAllNetworks(): LiveData<List<GENetwork>>
// PLUGS
@Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -60,7 +54,7 @@ abstract class GEReferenceDataDao {
}
@Query("SELECT * FROM geplug")
abstract fun getAllPlugs(): Flow<List<GEPlug>>
abstract fun getAllPlugs(): LiveData<List<GEPlug>>
// CHARGE CARDS
@Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -78,21 +72,31 @@ abstract class GEReferenceDataDao {
}
@Query("SELECT * FROM gechargecard")
abstract fun getAllChargeCards(): Flow<List<GEChargeCard>>
abstract fun getAllChargeCards(): LiveData<List<GEChargeCard>>
}
class GEReferenceDataRepository(
private val api: GoingElectricApiWrapper, private val scope: CoroutineScope,
private val dao: GEReferenceDataDao, private val prefs: PreferenceDataSource
) {
fun getReferenceData(): Flow<GEReferenceData> {
fun getReferenceData(): LiveData<GEReferenceData> {
scope.launch {
updateData()
}
val plugs = dao.getAllPlugs()
val networks = dao.getAllNetworks()
val chargeCards = dao.getAllChargeCards()
return combine(plugs, networks, chargeCards) { p, n, c -> GEReferenceData(p.map { it.name }, n.map { it.name }, c) }
return MediatorLiveData<GEReferenceData>().apply {
value = null
listOf(chargeCards, networks, plugs).map { source ->
addSource(source) { _ ->
val p = plugs.value ?: return@addSource
val n = networks.value ?: return@addSource
val cc = chargeCards.value ?: return@addSource
value = GEReferenceData(p.map { it.name }, n.map { it.name }, cc)
}
}
}
}
private suspend fun updateData() {

View File

@@ -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)
}
}
}

View File

@@ -1,19 +1,11 @@
package net.vonforst.evmap.storage
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.room.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.openchargemap.OCMConnectionType
import net.vonforst.evmap.api.openchargemap.OCMCountry
import net.vonforst.evmap.api.openchargemap.OCMOperator
import net.vonforst.evmap.api.openchargemap.OCMReferenceData
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.api.openchargemap.*
import net.vonforst.evmap.viewmodel.Status
import java.time.Duration
import java.time.Instant
@@ -36,7 +28,7 @@ abstract class OCMReferenceDataDao {
}
@Query("SELECT * FROM ocmconnectiontype")
abstract fun getAllConnectionTypes(): Flow<List<OCMConnectionType>>
abstract fun getAllConnectionTypes(): LiveData<List<OCMConnectionType>>
// COUNTRIES
@Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -54,7 +46,7 @@ abstract class OCMReferenceDataDao {
}
@Query("SELECT * FROM ocmcountry")
abstract fun getAllCountries(): Flow<List<OCMCountry>>
abstract fun getAllCountries(): LiveData<List<OCMCountry>>
// OPERATORS
@Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -72,21 +64,32 @@ abstract class OCMReferenceDataDao {
}
@Query("SELECT * FROM ocmoperator")
abstract fun getAllOperators(): Flow<List<OCMOperator>>
abstract fun getAllOperators(): LiveData<List<OCMOperator>>
}
class OCMReferenceDataRepository(
private val api: OpenChargeMapApiWrapper, private val scope: CoroutineScope,
private val dao: OCMReferenceDataDao, private val prefs: PreferenceDataSource
) {
fun getReferenceData(): Flow<OCMReferenceData> {
fun getReferenceData(): LiveData<OCMReferenceData> {
scope.launch {
updateData()
}
val connectionTypes = dao.getAllConnectionTypes()
val countries = dao.getAllCountries()
val operators = dao.getAllOperators()
return combine(connectionTypes, countries, operators) { ct, c, o -> OCMReferenceData(ct, c, o) }
return MediatorLiveData<OCMReferenceData>().apply {
value = null
listOf(countries, connectionTypes, operators).map { source ->
addSource(source) { _ ->
val ct = connectionTypes.value
val c = countries.value
val o = operators.value
if (ct.isNullOrEmpty() || c.isNullOrEmpty() || o.isNullOrEmpty()) return@addSource
value = OCMReferenceData(ct, c, o)
}
}
}
}
private suspend fun updateData() {

View File

@@ -4,8 +4,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.room.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.openchargemap.*
import net.vonforst.evmap.api.openstreetmap.OSMReferenceData
@@ -34,13 +32,19 @@ abstract class OSMReferenceDataDao {
}
@Query("SELECT * FROM osmnetwork")
abstract fun getAllNetworks(): Flow<List<OSMNetwork>>
abstract fun getAllNetworks(): LiveData<List<OSMNetwork>>
}
class OSMReferenceDataRepository(private val dao: OSMReferenceDataDao) {
fun getReferenceData(): Flow<OSMReferenceData> {
fun getReferenceData(): LiveData<OSMReferenceData> {
val networks = dao.getAllNetworks()
return networks.map { OSMReferenceData(it.map { it.name }) }
return MediatorLiveData<OSMReferenceData>().apply {
value = null
addSource(networks) { _ ->
val n = networks.value ?: return@addSource
value = OSMReferenceData(n.map { it.name })
}
}
}
suspend fun updateReferenceData(refData: OSMReferenceData) {

View File

@@ -1,12 +1,8 @@
package net.vonforst.evmap.storage
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Index
import androidx.room.Insert
import androidx.room.PrimaryKey
import androidx.room.Query
import androidx.room.SkipQueryVerification
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
import androidx.room.*
import co.anbora.labs.spatia.geometry.Geometry
import co.anbora.labs.spatia.geometry.LineString
import co.anbora.labs.spatia.geometry.Polygon
@@ -39,31 +35,31 @@ abstract class SavedRegionDao {
@SkipQueryVerification
@Query("SELECT Covers(GUnion(region), BuildMbr(:lng1, :lat1, :lng2, :lat2, 4326)) FROM savedregion WHERE dataSource == :dataSource AND timeRetrieved > :after AND Intersects(region, BuildMbr(:lng1, :lat1, :lng2, :lat2, 4326)) AND (filters == :filters OR filters IS NULL) AND (isDetailed OR NOT :isDetailed)")
protected abstract suspend fun savedRegionCoversInt(
protected abstract fun savedRegionCoversInt(
lat1: Double,
lat2: Double,
lng1: Double,
lng2: Double,
dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false
): Int
): LiveData<Int>
@SkipQueryVerification
@Query("SELECT Covers(GUnion(region), MakeEllipse(:lng, :lat, :radiusLng, :radiusLat, 4326)) FROM savedregion WHERE dataSource == :dataSource AND timeRetrieved > :after AND Intersects(region, MakeEllipse(:lng, :lat, :radiusLng, :radiusLat, 4326)) AND (filters == :filters OR filters IS NULL) AND (isDetailed OR NOT :isDetailed)")
protected abstract suspend fun savedRegionCoversRadiusInt(
protected abstract fun savedRegionCoversRadiusInt(
lat: Double,
lng: Double,
radiusLat: Double,
radiusLng: Double,
dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false
): Int
): LiveData<Int>
suspend fun savedRegionCovers(
fun savedRegionCovers(
lat1: Double,
lat2: Double,
lng1: Double,
lng2: Double,
dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false
): Boolean {
): LiveData<Boolean> {
return savedRegionCoversInt(
lat1,
lat2,
@@ -73,15 +69,15 @@ abstract class SavedRegionDao {
after,
filters,
isDetailed
) == 1
).map { it == 1 }
}
suspend fun savedRegionCoversRadius(
fun savedRegionCoversRadius(
lat: Double,
lng: Double,
radius: Double,
dataSource: String, after: Long, filters: String? = null, isDetailed: Boolean = false
): Boolean {
): LiveData<Boolean> {
val (radiusLat, radiusLng) = circleAsEllipse(lat, lng, radius)
return savedRegionCoversRadiusInt(
lat,
@@ -92,7 +88,7 @@ abstract class SavedRegionDao {
after,
filters,
isDetailed
) == 1
).map { it == 1 }
}
@Insert

View File

@@ -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)

View File

@@ -23,6 +23,7 @@ class UpdateFullDownloadWorker(appContext: Context, workerParams: WorkerParamete
var insertJob: Job? = null
val result = api.fullDownload()
val chargerIds = mutableListOf<Long>()
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())
}
}
chargerIds.addAll(it.map { it.id })
}
// delete chargers that have been removed
chargeLocations.deleteIdNotIn(api.id, chargerIds)
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()
}
}

View File

@@ -148,4 +148,20 @@ suspend fun <T> LiveData<Resource<T>>.awaitFinished(): Resource<T> {
removeObserver(observer)
}
}
}
inline fun <X, Y> LiveData<X>.singleSwitchMap(crossinline transform: (X) -> LiveData<Y>?): MediatorLiveData<Y> {
val result = MediatorLiveData<Y>()
result.addSource(this@singleSwitchMap, object : Observer<X> {
override fun onChanged(t: X) {
if (t == null) return
result.removeSource(this@singleSwitchMap)
transform(t)?.let { transformed ->
result.addSource(transformed) {
result.value = it
}
}
}
})
return result
}

View File

@@ -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>

View File

@@ -380,4 +380,5 @@
<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>

View File

@@ -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>

View File

@@ -376,4 +376,5 @@
<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>

View 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 &gt;%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 &amp; 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)

View File

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

View File

@@ -0,0 +1,2 @@
Fehler behoben:
- Anzeigefehler behoben

View File

@@ -0,0 +1,5 @@
Verbesserungen:
- Preisvergleich: Zurücksetzen der Ladebereichsauswahl durch Tippen auf den Titel darüber
Fehler behoben:
- Anzeigefehler behoben

View File

@@ -0,0 +1,2 @@
Fehler behoben:
- Anzeigefehler behoben

View File

@@ -0,0 +1,3 @@
Fehler behoben:
- Anzeigefehler behoben
- Abstürze behoben

View File

@@ -0,0 +1,2 @@
Fehler behoben:
- Echtzeitdaten für Tesla Supercharger repariert

View File

@@ -0,0 +1,3 @@
Fehler behoben:
- Abstürze behoben
- Aktueller Standort wurde nicht immer angezeigt, obwohl verfügbar

View File

@@ -0,0 +1 @@
Kompatibilität mit Android 15

View 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

View File

@@ -0,0 +1,2 @@
Bugfixes:
- Fixed crashes

View File

@@ -0,0 +1,2 @@
Bugfixes:
- Fixed display errors

View File

@@ -0,0 +1,5 @@
Improvements:
- Price comparison: Reset charging range by tapping on title above it
Bugfixes:
- Fixed display errors

View File

@@ -0,0 +1,2 @@
Bugfixes:
- Fixed display errors

View File

@@ -0,0 +1,3 @@
Bugfixes:
- Fixed display errors
- Fixed crashes

View File

@@ -0,0 +1,2 @@
Bugfixes:
- Fixed realtime data for Tesla Superchargers

View File

@@ -0,0 +1,3 @@
Bugfixes:
- Fixed crashes
- Fixed current location not being shown despite it being available

View File

@@ -0,0 +1 @@
Android 15 compatibility

View 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