mirror of
https://github.com/ev-map/EVMap.git
synced 2026-01-05 05:27:45 -05:00
Compare commits
62 Commits
master
...
replace-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b99e2ea2c8 | ||
|
|
d2ae3733d1 | ||
|
|
72845da4b5 | ||
|
|
51b57433a8 | ||
|
|
3202f821d1 | ||
|
|
b7e1ff09db | ||
|
|
feabf49b8d | ||
|
|
dcbe4c6325 | ||
|
|
dcff74c125 | ||
|
|
d8f7d77a36 | ||
|
|
d03cf70499 | ||
|
|
7a6bebd143 | ||
|
|
66d68ca68e | ||
|
|
772885a8eb | ||
|
|
6b07ce012a | ||
|
|
29dbc202d8 | ||
|
|
cf8371d095 | ||
|
|
01cb551cbc | ||
|
|
45fe297616 | ||
|
|
32cabefe7d | ||
|
|
9ff8329171 | ||
|
|
e9b70a2f00 | ||
|
|
c4c3aba7c7 | ||
|
|
890af2ddef | ||
|
|
ba0b36b3ec | ||
|
|
161b48789f | ||
|
|
042b983aa3 | ||
|
|
1c21da7be0 | ||
|
|
405baed0f7 | ||
|
|
19c0d57f2b | ||
|
|
42c2a2f72a | ||
|
|
36ee3ff231 | ||
|
|
883735ef05 | ||
|
|
4c68356ae9 | ||
|
|
7fde5b50aa | ||
|
|
7c4136c66d | ||
|
|
6e56f5c3ff | ||
|
|
017be6f31a | ||
|
|
b398a5dc81 | ||
|
|
3fb0dec868 | ||
|
|
8c4de115ec | ||
|
|
334b68cf5e | ||
|
|
788c68c9dd | ||
|
|
7842a15529 | ||
|
|
e7c9432191 | ||
|
|
76b6abd3ca | ||
|
|
752c184146 | ||
|
|
5471ac5073 | ||
|
|
69ae13a199 | ||
|
|
8a2e2d9a25 | ||
|
|
fe69a78b94 | ||
|
|
2663bd7964 | ||
|
|
3b54b2799f | ||
|
|
3a24711626 | ||
|
|
c158744bc2 | ||
|
|
c01033a036 | ||
|
|
16474c3864 | ||
|
|
7ce2f8d452 | ||
|
|
28df158d94 | ||
|
|
90b3645a0b | ||
|
|
de901aa825 | ||
|
|
2ce61f2f6b |
48
.github/workflows/release.yml
vendored
48
.github/workflows/release.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Set up Java environment
|
- name: Set up Java environment
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 21
|
java-version: 17
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
- name: Decrypt keystore
|
- name: Decrypt keystore
|
||||||
@@ -24,9 +24,8 @@ jobs:
|
|||||||
- name: Extract version code
|
- name: Extract version code
|
||||||
run: echo "VERSION_CODE=$(grep -o "^\s*versionCode\s*=\s*[0-9]\+" app/build.gradle.kts | awk '{ print $3 }' | tr -d \''"\\')" >> $GITHUB_ENV
|
run: echo "VERSION_CODE=$(grep -o "^\s*versionCode\s*=\s*[0-9]\+" app/build.gradle.kts | awk '{ print $3 }' | tr -d \''"\\')" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Build app release & export licenses
|
- name: Build app release
|
||||||
env:
|
env:
|
||||||
EVMAP_API_KEY: ${{ secrets.EVMAP_API_KEY }}
|
|
||||||
GOINGELECTRIC_API_KEY: ${{ secrets.GOINGELECTRIC_API_KEY }}
|
GOINGELECTRIC_API_KEY: ${{ secrets.GOINGELECTRIC_API_KEY }}
|
||||||
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
|
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
|
||||||
CHARGEPRICE_API_KEY: ${{ secrets.CHARGEPRICE_API_KEY }}
|
CHARGEPRICE_API_KEY: ${{ secrets.CHARGEPRICE_API_KEY }}
|
||||||
@@ -36,14 +35,10 @@ jobs:
|
|||||||
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||||
FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }}
|
FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }}
|
||||||
ACRA_CRASHREPORT_CREDENTIALS: ${{ secrets.ACRA_CRASHREPORT_CREDENTIALS }}
|
ACRA_CRASHREPORT_CREDENTIALS: ${{ secrets.ACRA_CRASHREPORT_CREDENTIALS }}
|
||||||
NOBIL_API_KEY: ${{ secrets.NOBIL_API_KEY }}
|
|
||||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
|
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
|
||||||
KEYSTORE_ALIAS_PASSWORD: ${{ secrets.KEYSTORE_ALIAS_PASSWORD }}
|
KEYSTORE_ALIAS_PASSWORD: ${{ secrets.KEYSTORE_ALIAS_PASSWORD }}
|
||||||
run: ./gradlew exportLibraryDefinitions assembleRelease --no-daemon
|
run: ./gradlew assembleRelease --no-daemon
|
||||||
|
|
||||||
- name: Export licenses in Appning format
|
|
||||||
run: python3 _ci/export_licenses_appning.py
|
|
||||||
|
|
||||||
- name: release
|
- name: release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
@@ -93,40 +88,3 @@ jobs:
|
|||||||
asset_path: app/build/outputs/apk/fossAutomotive/release/app-foss-automotive-release.apk
|
asset_path: app/build/outputs/apk/fossAutomotive/release/app-foss-automotive-release.apk
|
||||||
asset_name: app-foss-automotive-release.apk
|
asset_name: app-foss-automotive-release.apk
|
||||||
asset_content_type: application/vnd.android.package-archive
|
asset_content_type: application/vnd.android.package-archive
|
||||||
- name: upload Licenses
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: app/build/generated/aboutLibraries/aboutlibraries.json
|
|
||||||
asset_name: aboutlibraries.json
|
|
||||||
asset_content_type: application/json
|
|
||||||
- name: upload Licenses Appning
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: licenses_fossAutomotiveRelease_appning.csv
|
|
||||||
asset_name: licenses_fossAutomotiveRelease_appning.csv
|
|
||||||
asset_content_type: text/csv
|
|
||||||
- name: upload Licenses Appning
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: licenses_fossNormalRelease_appning.csv
|
|
||||||
asset_name: licenses_fossNormalRelease_appning.csv
|
|
||||||
asset_content_type: text/csv
|
|
||||||
|
|
||||||
- name: Trigger Website update
|
|
||||||
run: |
|
|
||||||
curl -L \
|
|
||||||
-X POST \
|
|
||||||
-H "Accept: application/vnd.github+json" \
|
|
||||||
-H "Authorization: Bearer ${{ github.token }}" \
|
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
||||||
https://api.github.com/repos/ev-map/ev-map.github.io/dispatches \
|
|
||||||
-d "{\"event_type\": \"trigger-workflow\"}"
|
|
||||||
|
|||||||
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
- name: Set up Java environment
|
- name: Set up Java environment
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 21
|
java-version: 17
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
@@ -75,10 +75,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
checksec --output=json --dir=lib > checksec_output.json
|
checksec --output=json --dir=lib > checksec_output.json
|
||||||
jq --argjson exceptions '[
|
jq --argjson exceptions '[
|
||||||
"lib/arm64-v8a/libc++_shared.so",
|
|
||||||
"lib/armeabi-v7a/libc++_shared.so",
|
"lib/armeabi-v7a/libc++_shared.so",
|
||||||
"lib/x86/libc++_shared.so",
|
"lib/x86/libc++_shared.so"
|
||||||
"lib/x86_64/libc++_shared.so"
|
|
||||||
]' '
|
]' '
|
||||||
to_entries
|
to_entries
|
||||||
| map(select(.value.fortify_source == "no" and (.key as $lib | $exceptions | index($lib) | not)))
|
| map(select(.value.fortify_source == "no" and (.key as $lib | $exceptions | index($lib) | not)))
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,5 @@
|
|||||||
*.iml
|
*.iml
|
||||||
.gradle
|
.gradle
|
||||||
.kotlin
|
|
||||||
/local.properties
|
/local.properties
|
||||||
/.idea/*
|
/.idea/*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
36
Gemfile.lock
36
Gemfile.lock
@@ -5,28 +5,23 @@ GEM
|
|||||||
addressable (2.8.0)
|
addressable (2.8.0)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.4.0)
|
aws-eventstream (1.1.0)
|
||||||
aws-partitions (1.1196.0)
|
aws-partitions (1.354.0)
|
||||||
aws-sdk-core (3.240.0)
|
aws-sdk-core (3.104.3)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.1)
|
||||||
base64
|
jmespath (~> 1.0)
|
||||||
bigdecimal
|
aws-sdk-kms (1.36.0)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
aws-sdk-core (~> 3, >= 3.99.0)
|
||||||
logger
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-kms (1.118.0)
|
aws-sdk-s3 (1.78.0)
|
||||||
aws-sdk-core (~> 3, >= 3.239.1)
|
aws-sdk-core (~> 3, >= 3.104.3)
|
||||||
aws-sigv4 (~> 1.5)
|
|
||||||
aws-sdk-s3 (1.208.0)
|
|
||||||
aws-sdk-core (~> 3, >= 3.234.0)
|
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sigv4 (1.12.1)
|
aws-sigv4 (1.2.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
babosa (1.0.3)
|
babosa (1.0.3)
|
||||||
base64 (0.3.0)
|
|
||||||
bigdecimal (4.0.1)
|
|
||||||
claide (1.0.3)
|
claide (1.0.3)
|
||||||
colored (1.2)
|
colored (1.2)
|
||||||
colored2 (3.1.2)
|
colored2 (3.1.2)
|
||||||
@@ -118,10 +113,9 @@ GEM
|
|||||||
http-cookie (1.0.3)
|
http-cookie (1.0.3)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.1)
|
||||||
json (2.3.1)
|
json (2.3.1)
|
||||||
jwt (2.2.1)
|
jwt (2.2.1)
|
||||||
logger (1.7.0)
|
|
||||||
memoist (0.16.2)
|
memoist (0.16.2)
|
||||||
mini_magick (4.10.1)
|
mini_magick (4.10.1)
|
||||||
mini_mime (1.0.2)
|
mini_mime (1.0.2)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Features
|
|||||||
- Search for places
|
- Search for places
|
||||||
- Advanced filtering options, including saved filter profiles
|
- Advanced filtering options, including saved filter profiles
|
||||||
- Favorites list, also with availability information
|
- Favorites list, also with availability information
|
||||||
|
- Integrated price comparison using [Chargeprice.app](https://chargeprice.app) (only in Europe)
|
||||||
- Android Auto & Android Automotive OS integration
|
- Android Auto & Android Automotive OS integration
|
||||||
- No ads, fully open source
|
- No ads, fully open source
|
||||||
- Compatible with Android 5.0 and above
|
- Compatible with Android 5.0 and above
|
||||||
@@ -85,6 +86,10 @@ Sponsors
|
|||||||
Many users currently support the development EVMap with their donations. You can find more
|
Many users currently support the development EVMap with their donations. You can find more
|
||||||
information on the [Donate page](https://ev-map.app/donate/) on the EVMap website.
|
information on the [Donate page](https://ev-map.app/donate/) on the EVMap website.
|
||||||
|
|
||||||
<a href="https://www.jawg.io"><img src="https://www.jawg.io/static/Blue@10x-9cdc4596e4e59acbd9ead55e9c28613e.png" alt="JawgMaps" height="38"/></a><br>
|
<a href="https://www.jawg.io"><img src="https://www.jawg.io/static/Blue@10x-9cdc4596e4e59acbd9ead55e9c28613e.png" alt="JawgMaps" height="58"/></a><br>
|
||||||
Since May 2024, **JawgMaps** provide their OpenStreetMap vector map tiles service to EVMap for
|
Since May 2024, **JawgMaps** provides their OpenStreetMap vector map tiles service to EVMap for
|
||||||
free, i.e. the background map displayed in the app if OpenStreetMap is selected as the data source.
|
free, i.e. the background map displayed in the app if OpenStreetMap is selected as the data source.
|
||||||
|
|
||||||
|
<a href="https://chargeprice.app"><img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/powered_by_chargeprice.svg" alt="Powered by Chargeprice" height="58"/></a><br>
|
||||||
|
Since April 2021, **Chargeprice.app** provide their price comparison API at a greatly reduced
|
||||||
|
price for EVMap. This data is used in EVMap's price comparison feature.
|
||||||
@@ -4,9 +4,8 @@
|
|||||||
<string name="jawg_key" translatable="false">ci</string>
|
<string name="jawg_key" translatable="false">ci</string>
|
||||||
<string name="arcgis_key" translatable="false">ci</string>
|
<string name="arcgis_key" translatable="false">ci</string>
|
||||||
<string name="goingelectric_key" translatable="false">ci</string>
|
<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="openchargemap_key" translatable="false">ci</string>
|
||||||
<string name="nobil_key" translatable="false">ci</string>
|
|
||||||
<string name="fronyx_key" translatable="false">ci</string>
|
<string name="fronyx_key" translatable="false">ci</string>
|
||||||
<string name="acra_credentials" translatable="false">ci:ci</string>
|
<string name="acra_credentials" translatable="false">ci:ci</string>
|
||||||
<string name="evmap_key" translatable="false">ci</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg id="Ebene_1" data-name="Ebene 1" xmlns="http://www.w3.org/2000/svg" version="1.1"
|
|
||||||
viewBox="0 0 108 108">
|
|
||||||
<defs>
|
|
||||||
<style>
|
|
||||||
.cls-1 {
|
|
||||||
fill: #000;
|
|
||||||
stroke-width: 0px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<path class="cls-1"
|
|
||||||
d="M53.9,28c-8.8,0-15.9,7.1-15.9,15.9s13.4,18.2,15,35.3c0,.5.5.9,1,.9s.9-.4,1-.9c1.6-17.1,15-23.3,15-35.3-.1-8.8-7.2-15.9-16-15.9ZM59,43.1l-6.1,10.5v-7.9h-2.6v-9.6s8.8,0,8.7,0l-3.5,7h3.5Z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 529 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
d="M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 395 B |
@@ -1,231 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -4,10 +4,13 @@ import json
|
|||||||
build_types = ["fossNormalRelease", "fossAutomotiveRelease"]
|
build_types = ["fossNormalRelease", "fossAutomotiveRelease"]
|
||||||
|
|
||||||
for build_type in build_types:
|
for build_type in build_types:
|
||||||
|
result = subprocess.run(["gradlew.bat", f"generateLibraryDefinitions{build_type.capitalize()}"],
|
||||||
|
capture_output=True)
|
||||||
|
|
||||||
data = json.load(
|
data = json.load(
|
||||||
open(f"app/build/generated/aboutLibraries/{build_type}/res/raw/aboutlibraries.json"))
|
open(f"app/build/generated/aboutLibraries/{build_type}/res/raw/aboutlibraries.json"))
|
||||||
|
|
||||||
with open(f"licenses_{build_type}_appning.csv", "w") as f:
|
with open(f"licenses_{build_type}.csv", "w") as f:
|
||||||
f.write("component_name;license_title;license_url;public_repository;copyrights\n")
|
f.write("component_name;license_title;license_url;public_repository;copyrights\n")
|
||||||
for lib in data["libraries"]:
|
for lib in data["libraries"]:
|
||||||
license = data["licenses"][lib["licenses"][0]] if len(lib["licenses"]) > 0 else None
|
license = data["licenses"][lib["licenses"][0]] if len(lib["licenses"]) > 0 else None
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import java.util.Base64
|
import java.util.Base64
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.adarshr.test-logger") version "4.0.0"
|
id("com.adarshr.test-logger") version "3.1.0"
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
@@ -17,18 +17,18 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "net.vonforst.evmap"
|
applicationId = "net.vonforst.evmap"
|
||||||
compileSdk = 36
|
compileSdk = 35
|
||||||
minSdk = 23
|
minSdk = 21
|
||||||
targetSdk = 36
|
targetSdk = 35
|
||||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||||
versionCode = 270
|
versionCode = 256
|
||||||
versionName = "2.1.0"
|
versionName = "1.9.18"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
|
||||||
|
|
||||||
ksp {
|
ksp {
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isRunningOnCI = System.getenv("CI") == "true"
|
val isRunningOnCI = System.getenv("CI") == "true"
|
||||||
@@ -78,7 +78,6 @@ android {
|
|||||||
productFlavors {
|
productFlavors {
|
||||||
create("foss") {
|
create("foss") {
|
||||||
dimension = "dependencies"
|
dimension = "dependencies"
|
||||||
isDefault = true
|
|
||||||
}
|
}
|
||||||
create("google") {
|
create("google") {
|
||||||
dimension = "dependencies"
|
dimension = "dependencies"
|
||||||
@@ -86,7 +85,6 @@ android {
|
|||||||
}
|
}
|
||||||
create("normal") {
|
create("normal") {
|
||||||
dimension = "automotive"
|
dimension = "automotive"
|
||||||
isDefault = true
|
|
||||||
}
|
}
|
||||||
create("automotive") {
|
create("automotive") {
|
||||||
dimension = "automotive"
|
dimension = "automotive"
|
||||||
@@ -129,34 +127,12 @@ android {
|
|||||||
|
|
||||||
// add API keys from environment variable if not set in apikeys.xml
|
// add API keys from environment variable if not set in apikeys.xml
|
||||||
applicationVariants.all {
|
applicationVariants.all {
|
||||||
var evmapKey =
|
|
||||||
System.getenv("EVMAP_API_KEY") ?: project.findProperty("EVMAP_API_KEY")?.toString()
|
|
||||||
if (evmapKey == null && project.hasProperty("EVMAP_API_KEY_ENCRYPTED")) {
|
|
||||||
evmapKey = decode(
|
|
||||||
project.findProperty("EVMAP_API_KEY_ENCRYPTED").toString(),
|
|
||||||
"FmK.d,-f*p+rD+WK!eds"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (evmapKey != null) {
|
|
||||||
resValue("string", "evmap_key", evmapKey)
|
|
||||||
}
|
|
||||||
val goingelectricKey =
|
val goingelectricKey =
|
||||||
System.getenv("GOINGELECTRIC_API_KEY") ?: project.findProperty("GOINGELECTRIC_API_KEY")
|
System.getenv("GOINGELECTRIC_API_KEY") ?: project.findProperty("GOINGELECTRIC_API_KEY")
|
||||||
?.toString()
|
?.toString()
|
||||||
if (goingelectricKey != null) {
|
if (goingelectricKey != null) {
|
||||||
resValue("string", "goingelectric_key", goingelectricKey)
|
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 =
|
var openchargemapKey =
|
||||||
System.getenv("OPENCHARGEMAP_API_KEY") ?: project.findProperty("OPENCHARGEMAP_API_KEY")
|
System.getenv("OPENCHARGEMAP_API_KEY") ?: project.findProperty("OPENCHARGEMAP_API_KEY")
|
||||||
?.toString()
|
?.toString()
|
||||||
@@ -208,6 +184,18 @@ android {
|
|||||||
if (arcgisKey != null) {
|
if (arcgisKey != null) {
|
||||||
resValue("string", "arcgis_key", jawgKey)
|
resValue("string", "arcgis_key", jawgKey)
|
||||||
}
|
}
|
||||||
|
var chargepriceKey =
|
||||||
|
System.getenv("CHARGEPRICE_API_KEY") ?: project.findProperty("CHARGEPRICE_API_KEY")
|
||||||
|
?.toString()
|
||||||
|
if (chargepriceKey == null && project.hasProperty("CHARGEPRICE_API_KEY_ENCRYPTED")) {
|
||||||
|
chargepriceKey = decode(
|
||||||
|
project.findProperty("CHARGEPRICE_API_KEY_ENCRYPTED").toString(),
|
||||||
|
"FmK.d,-f*p+rD+WK!eds"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (chargepriceKey != null) {
|
||||||
|
resValue("string", "chargeprice_key", chargepriceKey)
|
||||||
|
}
|
||||||
var fronyxKey =
|
var fronyxKey =
|
||||||
System.getenv("FRONYX_API_KEY") ?: project.findProperty("FRONYX_API_KEY")?.toString()
|
System.getenv("FRONYX_API_KEY") ?: project.findProperty("FRONYX_API_KEY")?.toString()
|
||||||
if (fronyxKey == null && project.hasProperty("FRONYX_API_KEY_ENCRYPTED")) {
|
if (fronyxKey == null && project.hasProperty("FRONYX_API_KEY_ENCRYPTED")) {
|
||||||
@@ -268,21 +256,18 @@ configurations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
aboutLibraries {
|
aboutLibraries {
|
||||||
license {
|
allowedLicenses = arrayOf(
|
||||||
allowedLicenses = setOf(
|
"Apache-2.0", "mit", "BSD-2-Clause", "BSD-3-Clause", "EPL-1.0",
|
||||||
"Apache-2.0", "mit", "BSD-2-Clause", "BSD-3-Clause", "EPL-1.0",
|
"asdkl", // Android SDK
|
||||||
"asdkl", // Android SDK
|
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
|
||||||
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
|
"Google Maps Platform Terms of Service", // Google Maps SDK
|
||||||
"Google Maps Platform Terms of Service", // Google Maps SDK
|
"provided without support or warranty", // org.json
|
||||||
"Unicode/ICU License", "Unicode-3.0", // icu4j
|
"Unicode/ICU License", "Unicode-3.0", // icu4j
|
||||||
"Bouncy Castle Licence", // bcprov
|
"Bouncy Castle Licence", // bcprov
|
||||||
"CDDL + GPLv2 with classpath exception", // javax.annotation-api
|
"CDDL + GPLv2 with classpath exception", // javax.annotation-api
|
||||||
)
|
)
|
||||||
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
excludeFields = arrayOf("generated")
|
||||||
}
|
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
|
||||||
export {
|
|
||||||
excludeFields = setOf("generated")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -296,93 +281,101 @@ dependencies {
|
|||||||
val testGoogleImplementation by configurations
|
val testGoogleImplementation by configurations
|
||||||
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
|
||||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||||
implementation("androidx.core:core-ktx:1.17.0")
|
implementation("androidx.core:core-ktx:1.13.1")
|
||||||
implementation("androidx.core:core-splashscreen:1.2.0")
|
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||||
implementation("androidx.activity:activity-ktx:1.11.0")
|
implementation("androidx.activity:activity-ktx:1.9.0")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.8.9")
|
implementation("androidx.fragment:fragment-ktx:1.7.1")
|
||||||
implementation("androidx.cardview:cardview:1.0.0")
|
implementation("androidx.cardview:cardview:1.0.0")
|
||||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
implementation("com.google.android.material:material:1.13.0")
|
implementation("com.google.android.material:material:1.12.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.4.0")
|
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||||
implementation("androidx.browser:browser:1.9.0")
|
implementation("androidx.browser:browser:1.8.0")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
||||||
implementation("androidx.security:security-crypto:1.1.0")
|
implementation("androidx.security:security-crypto:1.1.0-alpha06")
|
||||||
implementation("androidx.work:work-runtime-ktx:2.10.5")
|
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||||
implementation("com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b")
|
implementation("com.squareup.retrofit2:retrofit:2.9.0")
|
||||||
implementation("com.squareup.retrofit2:retrofit:3.0.0")
|
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
|
||||||
implementation("com.squareup.retrofit2:converter-moshi:3.0.0")
|
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
implementation("com.squareup.okhttp3:okhttp-urlconnection:4.12.0")
|
implementation("com.squareup.okhttp3:okhttp-urlconnection:4.12.0")
|
||||||
implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
|
implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
|
||||||
implementation("com.squareup.moshi:moshi-adapters:1.15.2")
|
implementation("com.squareup.moshi:moshi-adapters:1.15.2")
|
||||||
implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
|
implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
|
||||||
implementation("io.coil-kt:coil:2.7.0")
|
implementation("io.coil-kt:coil:2.6.0")
|
||||||
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
|
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
|
||||||
implementation("com.mikepenz:aboutlibraries-core:$aboutLibsVersion")
|
implementation("com.mikepenz:aboutlibraries-core:$aboutLibsVersion")
|
||||||
implementation("com.mikepenz:aboutlibraries:$aboutLibsVersion")
|
implementation("com.mikepenz:aboutlibraries:$aboutLibsVersion")
|
||||||
implementation("com.airbnb.android:lottie:6.6.10")
|
implementation("com.airbnb.android:lottie:4.1.0")
|
||||||
implementation("io.michaelrocks.bimap:bimap:1.1.0")
|
implementation("io.michaelrocks.bimap:bimap:1.1.0")
|
||||||
|
implementation("com.google.guava:guava:29.0-android")
|
||||||
implementation("com.github.pengrad:mapscaleview:1.6.0")
|
implementation("com.github.pengrad:mapscaleview:1.6.0")
|
||||||
implementation("com.github.romandanylyk:PageIndicatorView:b1bad589b5")
|
implementation("com.github.romandanylyk:PageIndicatorView:b1bad589b5")
|
||||||
implementation("com.github.ev-map:locale-config-x:58b036abf4")
|
implementation("com.github.erfansn:locale-config-x:1.0.1")
|
||||||
|
|
||||||
// Android Auto
|
// Android Auto
|
||||||
val carAppVersion = "1.7.0"
|
val carAppVersion = "1.7.0-rc01"
|
||||||
implementation("androidx.car.app:app:$carAppVersion")
|
implementation("androidx.car.app:app:$carAppVersion")
|
||||||
normalImplementation("androidx.car.app:app-projected:$carAppVersion")
|
normalImplementation("androidx.car.app:app-projected:$carAppVersion")
|
||||||
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
|
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
|
||||||
|
|
||||||
// AnyMaps
|
// AnyMaps
|
||||||
val anyMapsVersion = "65e06c4c9a"
|
val anyMapsVersion = "a3290b148d"
|
||||||
implementation("com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion")
|
implementation("com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion")
|
||||||
googleImplementation("com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion")
|
googleImplementation("com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion")
|
||||||
googleImplementation("com.google.android.gms:play-services-maps:19.2.0")
|
googleImplementation("com.google.android.gms:play-services-maps:19.0.0")
|
||||||
implementation("com.github.ev-map.AnyMaps:anymaps-maplibre:$anyMapsVersion") {
|
implementation("com.github.ev-map.AnyMaps:anymaps-maplibre:$anyMapsVersion") {
|
||||||
// duplicates classes from mapbox-sdk-services
|
// duplicates classes from mapbox-sdk-services
|
||||||
exclude("org.maplibre.gl", "android-sdk-geojson")
|
exclude("org.maplibre.gl", "android-sdk-geojson")
|
||||||
}
|
}
|
||||||
implementation("org.maplibre.gl:android-sdk:10.3.5") {
|
implementation("org.maplibre.gl:android-sdk:10.3.4") {
|
||||||
exclude("org.maplibre.gl", "android-sdk-geojson")
|
exclude("org.maplibre.gl", "android-sdk-geojson")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google Places
|
// Google Places
|
||||||
googleImplementation("com.google.android.libraries.places:places:3.5.0")
|
googleImplementation("com.google.android.libraries.places:places:3.5.0")
|
||||||
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2")
|
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
|
||||||
|
|
||||||
// Mapbox Geocoding
|
// Mapbox Geocoding
|
||||||
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.8.0")
|
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0")
|
||||||
|
|
||||||
// navigation library
|
// navigation library
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
|
implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
|
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
|
||||||
|
|
||||||
// viewmodel library
|
// viewmodel library
|
||||||
val lifecycleVersion = "2.9.2"
|
val lifecycle_version = "2.8.1"
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
|
||||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
|
||||||
|
|
||||||
// room library
|
// room library
|
||||||
val roomVersion = "2.7.2"
|
val room_version = "2.7.1"
|
||||||
implementation("androidx.room:room-runtime:$roomVersion")
|
implementation("androidx.room:room-runtime:$room_version")
|
||||||
ksp("androidx.room:room-compiler:$roomVersion")
|
ksp("androidx.room:room-compiler:$room_version")
|
||||||
implementation("androidx.room:room-ktx:$roomVersion")
|
implementation("androidx.room:room-ktx:$room_version")
|
||||||
implementation("com.github.anboralabs:spatia-room:1.0.1")
|
implementation("com.github.anboralabs:spatia-room:0.3.0") {
|
||||||
|
exclude("com.github.dalgarins", "android-spatialite")
|
||||||
|
}
|
||||||
|
// forked version with upgraded sqlite & libxml
|
||||||
|
// https://github.com/dalgarins/android-spatialite/pull/10
|
||||||
|
implementation("com.github.ev-map:android-spatialite:31495dcd81")
|
||||||
|
|
||||||
// billing library
|
// billing library
|
||||||
val billingVersion = "7.0.0"
|
val billing_version = "7.0.0"
|
||||||
googleImplementation("com.android.billingclient:billing:$billingVersion")
|
googleImplementation("com.android.billingclient:billing:$billing_version")
|
||||||
googleImplementation("com.android.billingclient:billing-ktx:$billingVersion")
|
googleImplementation("com.android.billingclient:billing-ktx:$billing_version")
|
||||||
|
|
||||||
// ACRA (crash reporting)
|
// ACRA (crash reporting)
|
||||||
val acraVersion = "5.12.0"
|
val acraVersion = "5.11.1"
|
||||||
implementation("ch.acra:acra-http:$acraVersion")
|
implementation("ch.acra:acra-http:$acraVersion")
|
||||||
implementation("ch.acra:acra-dialog:$acraVersion")
|
implementation("ch.acra:acra-dialog:$acraVersion")
|
||||||
implementation("ch.acra:acra-limiter:$acraVersion")
|
implementation("ch.acra:acra-limiter:$acraVersion")
|
||||||
|
|
||||||
// debug tools
|
// debug tools
|
||||||
|
debugImplementation("com.facebook.flipper:flipper:0.238.0")
|
||||||
|
debugImplementation("com.facebook.soloader:soloader:0.10.5")
|
||||||
|
debugImplementation("com.facebook.flipper:flipper-network-plugin:0.238.0")
|
||||||
debugImplementation("com.jakewharton.timber:timber:5.0.1")
|
debugImplementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
|
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
|
||||||
|
|
||||||
@@ -390,18 +383,20 @@ dependencies {
|
|||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
|
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
|
||||||
//noinspection GradleDependency
|
//noinspection GradleDependency
|
||||||
testImplementation("org.robolectric:robolectric:4.16")
|
testImplementation("org.json:json:20080701")
|
||||||
testImplementation("androidx.test:core:1.7.0")
|
testImplementation("org.robolectric:robolectric:4.11.1")
|
||||||
|
testImplementation("androidx.test:core:1.5.0")
|
||||||
testImplementation("androidx.arch.core:core-testing:2.2.0")
|
testImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||||
testImplementation("androidx.car.app:app-testing:$carAppVersion")
|
testImplementation("androidx.car.app:app-testing:$carAppVersion")
|
||||||
|
testImplementation("androidx.test:core:1.5.0")
|
||||||
|
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.3.0")
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
|
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
|
||||||
|
|
||||||
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
|
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
|
||||||
|
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decode(s: String, key: String): String {
|
fun decode(s: String, key: String): String {
|
||||||
|
|||||||
@@ -1,997 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 23,
|
|
||||||
"identityHash": "e9e169ba4257824c82e4acb030730e97",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "ChargeLocation",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `coordinates` BLOB NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `license` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, PRIMARY KEY(`id`, `dataSource`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dataSource",
|
|
||||||
"columnName": "dataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "coordinates",
|
|
||||||
"columnName": "coordinates",
|
|
||||||
"affinity": "BLOB",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargepoints",
|
|
||||||
"columnName": "chargepoints",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "network",
|
|
||||||
"columnName": "network",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "url",
|
|
||||||
"columnName": "url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "editUrl",
|
|
||||||
"columnName": "editUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "verified",
|
|
||||||
"columnName": "verified",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "barrierFree",
|
|
||||||
"columnName": "barrierFree",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "operator",
|
|
||||||
"columnName": "operator",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "generalInformation",
|
|
||||||
"columnName": "generalInformation",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "amenities",
|
|
||||||
"columnName": "amenities",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "locationDescription",
|
|
||||||
"columnName": "locationDescription",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "photos",
|
|
||||||
"columnName": "photos",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargecards",
|
|
||||||
"columnName": "chargecards",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "license",
|
|
||||||
"columnName": "license",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "networkUrl",
|
|
||||||
"columnName": "networkUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargerUrl",
|
|
||||||
"columnName": "chargerUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "timeRetrieved",
|
|
||||||
"columnName": "timeRetrieved",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isDetailed",
|
|
||||||
"columnName": "isDetailed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "address.city",
|
|
||||||
"columnName": "city",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "address.country",
|
|
||||||
"columnName": "country",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "address.postcode",
|
|
||||||
"columnName": "postcode",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "address.street",
|
|
||||||
"columnName": "street",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "faultReport.created",
|
|
||||||
"columnName": "fault_report_created",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "faultReport.description",
|
|
||||||
"columnName": "fault_report_description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.twentyfourSeven",
|
|
||||||
"columnName": "twentyfourSeven",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.description",
|
|
||||||
"columnName": "description",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.monday.start",
|
|
||||||
"columnName": "mostart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.monday.end",
|
|
||||||
"columnName": "moend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.tuesday.start",
|
|
||||||
"columnName": "tustart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.tuesday.end",
|
|
||||||
"columnName": "tuend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.wednesday.start",
|
|
||||||
"columnName": "westart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.wednesday.end",
|
|
||||||
"columnName": "weend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.thursday.start",
|
|
||||||
"columnName": "thstart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.thursday.end",
|
|
||||||
"columnName": "thend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.friday.start",
|
|
||||||
"columnName": "frstart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.friday.end",
|
|
||||||
"columnName": "frend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.saturday.start",
|
|
||||||
"columnName": "sastart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.saturday.end",
|
|
||||||
"columnName": "saend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.sunday.start",
|
|
||||||
"columnName": "sustart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.sunday.end",
|
|
||||||
"columnName": "suend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.holiday.start",
|
|
||||||
"columnName": "hostart",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "openinghours.days.holiday.end",
|
|
||||||
"columnName": "hoend",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "cost.freecharging",
|
|
||||||
"columnName": "freecharging",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "cost.freeparking",
|
|
||||||
"columnName": "freeparking",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "cost.descriptionShort",
|
|
||||||
"columnName": "descriptionShort",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "cost.descriptionLong",
|
|
||||||
"columnName": "descriptionLong",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargepriceData.country",
|
|
||||||
"columnName": "chargepricecountry",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargepriceData.network",
|
|
||||||
"columnName": "chargepricenetwork",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargepriceData.plugTypes",
|
|
||||||
"columnName": "chargepriceplugTypes",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "Favorite",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "favoriteId",
|
|
||||||
"columnName": "favoriteId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargerId",
|
|
||||||
"columnName": "chargerId",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "chargerDataSource",
|
|
||||||
"columnName": "chargerDataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"favoriteId"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_Favorite_chargerId_chargerDataSource",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"chargerId",
|
|
||||||
"chargerDataSource"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_Favorite_chargerId_chargerDataSource` ON `${TABLE_NAME}` (`chargerId`, `chargerDataSource`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "ChargeLocation",
|
|
||||||
"onDelete": "NO ACTION",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"chargerId",
|
|
||||||
"chargerDataSource"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "BooleanFilterValue",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "key",
|
|
||||||
"columnName": "key",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "value",
|
|
||||||
"columnName": "value",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dataSource",
|
|
||||||
"columnName": "dataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "profile",
|
|
||||||
"columnName": "profile",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"key",
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_BooleanFilterValue_profile_dataSource",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_BooleanFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "FilterProfile",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "MultipleChoiceFilterValue",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "key",
|
|
||||||
"columnName": "key",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "values",
|
|
||||||
"columnName": "values",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "all",
|
|
||||||
"columnName": "all",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dataSource",
|
|
||||||
"columnName": "dataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "profile",
|
|
||||||
"columnName": "profile",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"key",
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_MultipleChoiceFilterValue_profile_dataSource",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_MultipleChoiceFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "FilterProfile",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "SliderFilterValue",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "key",
|
|
||||||
"columnName": "key",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "value",
|
|
||||||
"columnName": "value",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dataSource",
|
|
||||||
"columnName": "dataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "profile",
|
|
||||||
"columnName": "profile",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"key",
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_SliderFilterValue_profile_dataSource",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SliderFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": [
|
|
||||||
{
|
|
||||||
"table": "FilterProfile",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"columns": [
|
|
||||||
"profile",
|
|
||||||
"dataSource"
|
|
||||||
],
|
|
||||||
"referencedColumns": [
|
|
||||||
"id",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "FilterProfile",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `id` INTEGER NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`dataSource`, `id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dataSource",
|
|
||||||
"columnName": "dataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "order",
|
|
||||||
"columnName": "order",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"dataSource",
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_FilterProfile_dataSource_name",
|
|
||||||
"unique": true,
|
|
||||||
"columnNames": [
|
|
||||||
"dataSource",
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_FilterProfile_dataSource_name` ON `${TABLE_NAME}` (`dataSource`, `name`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "RecentAutocompletePlace",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dataSource",
|
|
||||||
"columnName": "dataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "timestamp",
|
|
||||||
"columnName": "timestamp",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "primaryText",
|
|
||||||
"columnName": "primaryText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "secondaryText",
|
|
||||||
"columnName": "secondaryText",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "latLng",
|
|
||||||
"columnName": "latLng",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "viewport",
|
|
||||||
"columnName": "viewport",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "types",
|
|
||||||
"columnName": "types",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id",
|
|
||||||
"dataSource"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "GEPlug",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"name"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "GENetwork",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"name"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "GEChargeCard",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "url",
|
|
||||||
"columnName": "url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "OCMConnectionType",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `formalName` TEXT, `discontinued` INTEGER, `obsolete` INTEGER, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "formalName",
|
|
||||||
"columnName": "formalName",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "discontinued",
|
|
||||||
"columnName": "discontinued",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "obsolete",
|
|
||||||
"columnName": "obsolete",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "OCMCountry",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isoCode` TEXT NOT NULL, `continentCode` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isoCode",
|
|
||||||
"columnName": "isoCode",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "continentCode",
|
|
||||||
"columnName": "continentCode",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "OCMOperator",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "websiteUrl",
|
|
||||||
"columnName": "websiteUrl",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "title",
|
|
||||||
"columnName": "title",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "contactEmail",
|
|
||||||
"columnName": "contactEmail",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "contactTelephone1",
|
|
||||||
"columnName": "contactTelephone1",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "contactTelephone2",
|
|
||||||
"columnName": "contactTelephone2",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "OSMNetwork",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "name",
|
|
||||||
"columnName": "name",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": false,
|
|
||||||
"columnNames": [
|
|
||||||
"name"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [],
|
|
||||||
"foreignKeys": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tableName": "SavedRegion",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`region` BLOB NOT NULL, `dataSource` TEXT NOT NULL, `timeRetrieved` INTEGER NOT NULL, `filters` TEXT, `isDetailed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldPath": "region",
|
|
||||||
"columnName": "region",
|
|
||||||
"affinity": "BLOB",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "dataSource",
|
|
||||||
"columnName": "dataSource",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "timeRetrieved",
|
|
||||||
"columnName": "timeRetrieved",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "filters",
|
|
||||||
"columnName": "filters",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "isDetailed",
|
|
||||||
"columnName": "isDetailed",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldPath": "id",
|
|
||||||
"columnName": "id",
|
|
||||||
"affinity": "INTEGER",
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"primaryKey": {
|
|
||||||
"autoGenerate": true,
|
|
||||||
"columnNames": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"indices": [
|
|
||||||
{
|
|
||||||
"name": "index_SavedRegion_filters_dataSource",
|
|
||||||
"unique": false,
|
|
||||||
"columnNames": [
|
|
||||||
"filters",
|
|
||||||
"dataSource"
|
|
||||||
],
|
|
||||||
"orders": [],
|
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SavedRegion_filters_dataSource` ON `${TABLE_NAME}` (`filters`, `dataSource`)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"foreignKeys": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"views": [],
|
|
||||||
"setupQueries": [
|
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e9e169ba4257824c82e4acb030730e97')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,928 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 24,
|
|
||||||
"identityHash": "b2b3f39d450f4f7c8280ca850161bbb3",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"tableName": "ChargeLocation",
|
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `coordinates` BLOB NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `license` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER NOT NULL, `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": "url",
|
|
||||||
"columnName": "url",
|
|
||||||
"affinity": "TEXT",
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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": "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, 'b2b3f39d450f4f7c8280ca850161bbb3')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,938 +0,0 @@
|
|||||||
{
|
|
||||||
"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')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,938 +0,0 @@
|
|||||||
{
|
|
||||||
"formatVersion": 1,
|
|
||||||
"database": {
|
|
||||||
"version": 28,
|
|
||||||
"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')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package net.vonforst.evmap.storage
|
package com.johan.evmap.storage
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package net.vonforst.evmap.storage
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
|
||||||
import androidx.test.core.app.ApplicationProvider
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
|
||||||
import net.vonforst.evmap.model.ChargeLocationCluster
|
|
||||||
import net.vonforst.evmap.model.ChargepointListItem
|
|
||||||
import net.vonforst.evmap.model.Coordinate
|
|
||||||
import net.vonforst.evmap.ui.cluster
|
|
||||||
import org.junit.After
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import java.time.Instant
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ChargeLocationsDaoTest {
|
|
||||||
private lateinit var database: AppDatabase
|
|
||||||
private lateinit var dao: ChargeLocationsDao
|
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
var instantExecutorRule = InstantTaskExecutorRule()
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
|
||||||
database = AppDatabase.createInMemory(context)
|
|
||||||
dao = database.chargeLocationsDao()
|
|
||||||
}
|
|
||||||
|
|
||||||
@After
|
|
||||||
fun tearDown() {
|
|
||||||
database.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testClustering() {
|
|
||||||
val lat1 = 53.0
|
|
||||||
val lng1 = 9.0
|
|
||||||
val lat2 = 54.0
|
|
||||||
val lng2 = 10.0
|
|
||||||
|
|
||||||
val chargeLocations = (0..100).map { i ->
|
|
||||||
val lat = Random.nextDouble(lat1, lat2)
|
|
||||||
val lng = Random.nextDouble(lng1, lng2)
|
|
||||||
ChargeLocation(
|
|
||||||
i.toLong(),
|
|
||||||
"test",
|
|
||||||
"test",
|
|
||||||
Coordinate(lat, lng),
|
|
||||||
null,
|
|
||||||
emptyList(),
|
|
||||||
null,
|
|
||||||
"https://google.com",
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null, null, null, null, null, null, null, null, Instant.now(), true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
runBlocking {
|
|
||||||
dao.insert(*chargeLocations.toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
val zoom = 10f
|
|
||||||
|
|
||||||
val clusteredInMemory = cluster(chargeLocations, zoom).sorted()
|
|
||||||
val clusteredInDB = runBlocking {
|
|
||||||
dao.getChargeLocationsClustered(lat1, lat2, lng1, lng2, "test", 0L, zoom)
|
|
||||||
}.sorted()
|
|
||||||
assertEquals(clusteredInMemory.size, clusteredInDB.size)
|
|
||||||
clusteredInDB.zip(clusteredInMemory).forEach { (a, b) ->
|
|
||||||
when (a) {
|
|
||||||
is ChargeLocation -> {
|
|
||||||
assertTrue(b is ChargeLocation)
|
|
||||||
assertEquals(a, b)
|
|
||||||
}
|
|
||||||
is ChargeLocationCluster -> {
|
|
||||||
assertTrue(b is ChargeLocationCluster)
|
|
||||||
assertEquals(a.clusterCount, (b as ChargeLocationCluster).clusterCount)
|
|
||||||
assertEquals(a.coordinates.lat, b.coordinates.lat, 1e-5)
|
|
||||||
assertEquals(a.coordinates.lng, b.coordinates.lng, 1e-5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<ChargepointListItem>.sorted() = sortedBy {
|
|
||||||
when (it) {
|
|
||||||
is ChargeLocationCluster -> it.coordinates.lat
|
|
||||||
is ChargeLocation -> it.coordinates.lat
|
|
||||||
else -> 0.0
|
|
||||||
}
|
|
||||||
}.sortedBy {
|
|
||||||
when (it) {
|
|
||||||
is ChargeLocationCluster -> it.coordinates.lng
|
|
||||||
is ChargeLocation -> it.coordinates.lng
|
|
||||||
else -> 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="grant_on_phone">Povolit</string>
|
<string name="grant_on_phone">Povolit</string>
|
||||||
<string name="auto_location_permission_needed">Pro spuštění aplikace EVMap ve vašem autě musíte povolit přístup k vaší poloze.</string>
|
<string name="auto_location_permission_needed">Pro spuštění aplikace EVMap ve vašem autě musíte povolit přístup k vaší poloze.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="grant_on_phone">Zulassen</string>
|
<string name="grant_on_phone">Zulassen</string>
|
||||||
<string name="auto_location_permission_needed">Um EVMap auf deinem Fahrzeug zu nutzen, braucht die App Zugriff auf deinen Standort.</string>
|
<string name="auto_location_permission_needed">Um EVMap auf deinem Fahrzeug zu nutzen, braucht die App Zugriff auf deinen Standort.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="grant_on_phone">Luba</string>
|
|
||||||
<string name="auto_location_permission_needed">Et EVMap toimiks sinu autos, palun luba tal asukohta tuvastada.</string>
|
|
||||||
</resources>
|
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="grant_on_phone">Autoriser</string>
|
<string name="grant_on_phone">Autoriser</string>
|
||||||
<string name="auto_location_permission_needed">Pour exécuter EVMap sur Android Auto, vous devez autoriser l\'accès à votre emplacement.</string>
|
<string name="auto_location_permission_needed">Pour exécuter EVMap sur Android Auto, vous devez autoriser l\'accès à votre emplacement.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="grant_on_phone">Consenti</string>
|
|
||||||
<string name="auto_location_permission_needed">Per eseguire EVMap sulla propria auto, è necessario concedere l\'accesso alla propria posizione.</string>
|
|
||||||
</resources>
|
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="auto_location_permission_needed">Du må du innvilge posisjonstilgang for å kjøre EVMap i bilen din.</string>
|
<string name="auto_location_permission_needed">Du må du innvilge posisjonstilgang for å kjøre EVMap i bilen din.</string>
|
||||||
<string name="grant_on_phone">Tillat</string>
|
<string name="grant_on_phone">Tillat</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="grant_on_phone">Toestaan</string>
|
<string name="grant_on_phone">Toestaan</string>
|
||||||
<string name="auto_location_permission_needed">Om EVmap te gebruiken in je wagen, moet je toegang geven tot je locatie.</string>
|
<string name="auto_location_permission_needed">Om EVmap te gebruiken in je wagen, moet je toegang geven tot je locatie.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="grant_on_phone">Permitir</string>
|
<string name="grant_on_phone">Permitir</string>
|
||||||
<string name="auto_location_permission_needed">Para usar o EVMap no seu carro, permita o acesso à sua localização.</string>
|
<string name="auto_location_permission_needed">Para usar o EVMap no seu carro, permita o acesso à sua localização.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources></resources>
|
||||||
</resources>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?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>
|
|
||||||
9
app/src/debug/AndroidManifest.xml
Normal file
9
app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
|
||||||
|
android:exported="true" />
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -2,15 +2,44 @@ package net.vonforst.evmap
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import com.facebook.flipper.android.AndroidFlipperClient
|
||||||
|
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
|
||||||
|
import com.facebook.flipper.plugins.inspector.DescriptorMapping
|
||||||
|
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
|
||||||
|
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
|
||||||
|
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
|
||||||
|
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
|
||||||
|
import com.facebook.soloader.SoLoader
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
private val networkFlipperPlugin = NetworkFlipperPlugin()
|
||||||
|
|
||||||
fun addDebugInterceptors(context: Context) {
|
fun addDebugInterceptors(context: Context) {
|
||||||
if (Build.FINGERPRINT == "robolectric") return
|
if (Build.FINGERPRINT == "robolectric") return
|
||||||
|
|
||||||
|
SoLoader.init(context, false)
|
||||||
|
val client = AndroidFlipperClient.getInstance(context)
|
||||||
|
client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()))
|
||||||
|
client.addPlugin(networkFlipperPlugin)
|
||||||
|
client.addPlugin(DatabasesFlipperPlugin(context))
|
||||||
|
client.addPlugin(SharedPreferencesFlipperPlugin(context))
|
||||||
|
client.start()
|
||||||
|
|
||||||
Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder {
|
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder {
|
||||||
|
// Flipper does not work during unit tests - so check whether we are running tests first
|
||||||
|
var isRunningTest = true
|
||||||
|
try {
|
||||||
|
Class.forName("org.junit.Test")
|
||||||
|
} catch (e: ClassNotFoundException) {
|
||||||
|
isRunningTest = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRunningTest) {
|
||||||
|
this.addNetworkInterceptor(FlipperOkhttpInterceptor(networkFlipperPlugin))
|
||||||
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
5
app/src/debug/res/values/donottranslate.xml
Normal file
5
app/src/debug/res/values/donottranslate.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="chargeprice_api_url">https://staging-api.chargeprice.app/v1/</string>
|
||||||
|
<string name="chargeprice_key">20c0d68918c9dc96c564784b711a6570</string>
|
||||||
|
</resources>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">EVMap (debug)</string>
|
<string name="app_name">EVMap</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -3,4 +3,4 @@
|
|||||||
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj zasláním finančního daru vývojáři.</string>
|
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj zasláním finančního daru vývojáři.</string>
|
||||||
<string name="donate_paypal">Přispět pomocí PayPalu</string>
|
<string name="donate_paypal">Přispět pomocí PayPalu</string>
|
||||||
<string name="data_sources_hint">Mapová data v aplikaci poskytuje služba OpenStreetMap.</string>
|
<string name="data_sources_hint">Mapová data v aplikaci poskytuje služba OpenStreetMap.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -3,4 +3,4 @@
|
|||||||
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.</string>
|
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.</string>
|
||||||
<string name="donate_paypal">Mit PayPal spenden</string>
|
<string name="donate_paypal">Mit PayPal spenden</string>
|
||||||
<string name="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap.</string>
|
<string name="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="donations_info" formatted="false">Kas EVMap on sulle kasulik? Oma arendajale saadetava rahalise toetusega edendad ka arendustegevust.</string>
|
|
||||||
<string name="donate_paypal">Toeta PayPali abil</string>
|
|
||||||
<string name="data_sources_hint">Selles rakenduses näidatavad kaardiandmed on pärit OpenStreetMapist.</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile ? Soutenez son développement en envoyant un don au développeur.</string>
|
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile \? Soutenez son développement en envoyant un don au développeur.</string>
|
||||||
<string name="data_sources_hint">Les données cartographiques de l\'application sont fournies par OpenStreetMap.</string>
|
<string name="data_sources_hint">Les données cartographiques de l\'application sont fournies par OpenStreetMap.</string>
|
||||||
<string name="donate_paypal">Faire un don avec PayPal</string>
|
<string name="donate_paypal">Faire un don avec PayPal</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="donations_info" formatted="false">Trovi utile EVMap? Sostieni il suo sviluppo inviando una donazione allo sviluppatore.</string>
|
|
||||||
<string name="donate_paypal">Dona attraverso PayPal</string>
|
|
||||||
<string name="data_sources_hint">I dati cartografici dell\'applicazione sono forniti da OpenStreetMap.</string>
|
|
||||||
</resources>
|
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="donate_paypal">Doner med PayPal</string>
|
<string name="donate_paypal">Doner med PayPal</string>
|
||||||
<string name="data_sources_hint">Kartdata i programmet tilbys av OpenStreetMap.</string>
|
<string name="data_sources_hint">Kartdata i programmet tilbys av OpenStreetMap.</string>
|
||||||
<string name="donations_info" formatted="false">Synes du EVMap er nyttig? Støtt utviklingen ved å sende en slant til utvikleren.</string>
|
<string name="donations_info" formatted="false">Synes du EVMap er nyttig\? Støtt utviklingen ved å sende en slant til utvikleren.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Vond je EVMap nuttig? Je kan de ontwikkeling ondersteunen door een donatie te sturen naar de ontwikkelaar.</string>
|
<string name="donations_info" formatted="false">Vond je EVMap nuttig\? Je kan de ontwikkeling ondersteunen door een donatie te sturen naar de ontwikkelaar.</string>
|
||||||
<string name="donate_paypal">Doneer via PayPal</string>
|
<string name="donate_paypal">Doneer via PayPal</string>
|
||||||
<string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap.</string>
|
<string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="data_sources_hint">Os dados do mapa são fornecidos pelo OpenStreetMap.</string>
|
<string name="data_sources_hint">Os dados do mapa são fornecidos pelo OpenStreetMap.</string>
|
||||||
<string name="donate_paypal">Doar com o PayPal</string>
|
<string name="donate_paypal">Doar com o PayPal</string>
|
||||||
<string name="donations_info" formatted="false">Acha que o EVMap é útil? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.</string>
|
<string name="donations_info" formatted="false">Acha que o EVMap é útil\? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="donations_info" formatted="false">Har du nytta av EVMap? 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>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string-array name="pref_map_provider_names">
|
<string-array name="pref_map_provider_names">
|
||||||
<item>@string/pref_provider_osm</item>
|
<item>@string/pref_provider_osm_mapbox</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="pref_map_provider_values" translatable="false">
|
<string-array name="pref_map_provider_values" translatable="false">
|
||||||
<item>mapbox</item>
|
<item>mapbox</item>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj posláním finančního daru vývojáři. \n \nGoogle si z každého daru strhne 15 %.</string>
|
<string name="donations_info" formatted="false">Pomohla vám EVMap? Podpořte její vývoj posláním finančního daru vývojáři.
|
||||||
|
\n
|
||||||
|
\nGoogle si z každého daru strhne 15 %.</string>
|
||||||
<string name="data_sources_hint">V nastavení můžete také pro mapová data přepínat mezi službami Mapy Google a OpenStreetMap.</string>
|
<string name="data_sources_hint">V nastavení můžete také pro mapová data přepínat mezi službami Mapy Google a OpenStreetMap.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 15% Gebühren ab.</string>
|
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 15% Gebühren ab.</string>
|
||||||
<string name="data_sources_hint">In den Einstellungen kannst du auch zwischen Google Maps und OpenStreetMap für die Kartendaten wechseln.</string>
|
<string name="data_sources_hint">In den Einstellungen kannst du auch zwischen Google Maps und OpenStreetMap für die Kartendaten wechseln.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="data_sources_hint">Seadistustes saad valida kahe kaardiandmete allika vahel: Google Maps ja OpenStreetMap.</string>
|
|
||||||
<string name="donations_info" formatted="false">EVMap on sinu jaoks kasulik? Toeta edasist arendust oma rahalise panusega.\n\nGoogle võtab igast toestussummast teenustasuna 15%.</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile ? Soutenez son développement en envoyant un don au développeur. \n \nGoogle prend 15% sur chaque don.</string>
|
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile \? Soutenez son développement en envoyant un don au développeur.
|
||||||
|
\n
|
||||||
|
\nGoogle prend 15% sur chaque don.</string>
|
||||||
<string name="data_sources_hint">Dans les paramètres, vous pouvez également choisir entre Google Maps et OpenStreetMap pour les données cartographiques.</string>
|
<string name="data_sources_hint">Dans les paramètres, vous pouvez également choisir entre Google Maps et OpenStreetMap pour les données cartographiques.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="donations_info" formatted="false">Trovi utile EVMap? Sostieni il suo sviluppo inviando una donazione allo sviluppatore.\n\nGoogle si prende il 15% su ogni donazione.</string>
|
|
||||||
<string name="data_sources_hint">Nelle impostazioni si può anche scegliere tra Google Maps e OpenStreetMap per i dati cartografici.</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Synes du EVMap er nyttig? Støtt utviklingen ved å sende penger til utvikleren. \n \nGoogle tar 15% av alle donasjoner.</string>
|
<string name="donations_info" formatted="false">Synes du EVMap er nyttig\? Støtt utviklingen ved å sende penger til utvikleren.
|
||||||
|
\n
|
||||||
|
\nGoogle tar 15% av alle donasjoner.</string>
|
||||||
<string name="data_sources_hint">I innstillingene kan du også bytte mellom Google Maps og OpenStreetMap for kartdata.</string>
|
<string name="data_sources_hint">I innstillingene kan du også bytte mellom Google Maps og OpenStreetMap for kartdata.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Vind je EVMap nuttig? Je kan de ontwikkeling steunen via een donatie aan de ontwikkelaar. \n \nGoogle houdt 15% in van elke donatie.</string>
|
<string name="donations_info" formatted="false">Vind je EVMap nuttig\? Je kan de ontwikkeling steunen via een donatie aan de ontwikkelaar.
|
||||||
|
\n
|
||||||
|
\nGoogle houdt 15% in van elke donatie.</string>
|
||||||
<string name="data_sources_hint">In de instellingen kan je ook wisselen tussen Google Maps en OpenStreetMap voor de kaartgegevens.</string>
|
<string name="data_sources_hint">In de instellingen kan je ook wisselen tussen Google Maps en OpenStreetMap voor de kaartgegevens.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="donations_info" formatted="false">Acha que o EVMap é útil? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app. \n \nA Google cobra 15% de cada doação.</string>
|
<string name="donations_info" formatted="false">Acha que o EVMap é útil\? Apoie a manutenção e desenvolvimento com uma doação para o desenvolvedor da app.
|
||||||
|
\n
|
||||||
|
\nA Google cobra 15% de cada doação.</string>
|
||||||
<string name="data_sources_hint">Também pode mudar entre o Google Maps e OpenStreetMap nas definições da app.</string>
|
<string name="data_sources_hint">Também pode mudar entre o Google Maps e OpenStreetMap nas definições da app.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources></resources>
|
||||||
</resources>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="donations_info" formatted="false">Har du nytta av EVMap? 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>
|
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
|
<uses-permission android:name="androidx.car.app.MAP_TEMPLATES" />
|
||||||
<uses-permission android:name="androidx.car.app.ACCESS_SURFACE" />
|
|
||||||
<uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
|
<uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
|
||||||
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED" />
|
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED" />
|
||||||
|
|
||||||
@@ -46,7 +45,8 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme"
|
||||||
|
android:enableOnBackInvokedCallback="true">
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.mapbox.ACCESS_TOKEN"
|
android:name="com.mapbox.ACCESS_TOKEN"
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.clustering;
|
||||||
|
|
||||||
|
import com.car2go.maps.model.LatLng;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of ClusterItems that are nearby each other.
|
||||||
|
*/
|
||||||
|
public interface Cluster<T extends ClusterItem> {
|
||||||
|
LatLng getPosition();
|
||||||
|
|
||||||
|
Collection<T> getItems();
|
||||||
|
|
||||||
|
int getSize();
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.clustering;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.car2go.maps.model.LatLng;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClusterItem represents a marker on the map.
|
||||||
|
*/
|
||||||
|
public interface ClusterItem {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The position of this marker. This must always return the same value.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
LatLng getPosition();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The title of this marker.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
String getTitle();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The description of this marker.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
String getSnippet();
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.maps.android.clustering.algo;
|
||||||
|
|
||||||
|
import com.google.maps.android.clustering.ClusterItem;
|
||||||
|
|
||||||
|
import java.util.concurrent.locks.ReadWriteLock;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base Algorithm class that implements lock/unlock functionality.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractAlgorithm<T extends ClusterItem> implements Algorithm<T> {
|
||||||
|
|
||||||
|
private final ReadWriteLock mLock = new ReentrantReadWriteLock();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void lock() {
|
||||||
|
mLock.writeLock().lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unlock() {
|
||||||
|
mLock.writeLock().unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.clustering.algo;
|
||||||
|
|
||||||
|
import com.google.maps.android.clustering.Cluster;
|
||||||
|
import com.google.maps.android.clustering.ClusterItem;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logic for computing clusters
|
||||||
|
*/
|
||||||
|
public interface Algorithm<T extends ClusterItem> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an item to the algorithm
|
||||||
|
*
|
||||||
|
* @param item the item to be added
|
||||||
|
* @return true if the algorithm contents changed as a result of the call
|
||||||
|
*/
|
||||||
|
boolean addItem(T item);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a collection of items to the algorithm
|
||||||
|
*
|
||||||
|
* @param items the items to be added
|
||||||
|
* @return true if the algorithm contents changed as a result of the call
|
||||||
|
*/
|
||||||
|
boolean addItems(Collection<T> items);
|
||||||
|
|
||||||
|
void clearItems();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an item from the algorithm
|
||||||
|
*
|
||||||
|
* @param item the item to be removed
|
||||||
|
* @return true if this algorithm contained the specified element (or equivalently, if this
|
||||||
|
* algorithm changed as a result of the call).
|
||||||
|
*/
|
||||||
|
boolean removeItem(T item);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the provided item in the algorithm
|
||||||
|
*
|
||||||
|
* @param item the item to be updated
|
||||||
|
* @return true if the item existed in the algorithm and was updated, or false if the item did
|
||||||
|
* not exist in the algorithm and the algorithm contents remain unchanged.
|
||||||
|
*/
|
||||||
|
boolean updateItem(T item);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a collection of items from the algorithm
|
||||||
|
*
|
||||||
|
* @param items the items to be removed
|
||||||
|
* @return true if this algorithm contents changed as a result of the call
|
||||||
|
*/
|
||||||
|
boolean removeItems(Collection<T> items);
|
||||||
|
|
||||||
|
Set<? extends Cluster<T>> getClusters(float zoom);
|
||||||
|
|
||||||
|
Collection<T> getItems();
|
||||||
|
|
||||||
|
void setMaxDistanceBetweenClusteredItems(int maxDistance);
|
||||||
|
|
||||||
|
int getMaxDistanceBetweenClusteredItems();
|
||||||
|
|
||||||
|
void lock();
|
||||||
|
|
||||||
|
void unlock();
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.clustering.algo;
|
||||||
|
|
||||||
|
import com.car2go.maps.model.LatLng;
|
||||||
|
import com.google.maps.android.clustering.Cluster;
|
||||||
|
import com.google.maps.android.clustering.ClusterItem;
|
||||||
|
import com.google.maps.android.geometry.Bounds;
|
||||||
|
import com.google.maps.android.geometry.Point;
|
||||||
|
import com.google.maps.android.projection.SphericalMercatorProjection;
|
||||||
|
import com.google.maps.android.quadtree.PointQuadTree;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple clustering algorithm with O(nlog n) performance. Resulting clusters are not
|
||||||
|
* hierarchical.
|
||||||
|
* <p/>
|
||||||
|
* High level algorithm:<br>
|
||||||
|
* 1. Iterate over items in the order they were added (candidate clusters).<br>
|
||||||
|
* 2. Create a cluster with the center of the item. <br>
|
||||||
|
* 3. Add all items that are within a certain distance to the cluster. <br>
|
||||||
|
* 4. Move any items out of an existing cluster if they are closer to another cluster. <br>
|
||||||
|
* 5. Remove those items from the list of candidate clusters.
|
||||||
|
* <p/>
|
||||||
|
* Clusters have the center of the first element (not the centroid of the items within it).
|
||||||
|
*/
|
||||||
|
public class NonHierarchicalDistanceBasedAlgorithm<T extends ClusterItem> extends AbstractAlgorithm<T> {
|
||||||
|
private static final int DEFAULT_MAX_DISTANCE_AT_ZOOM = 100; // essentially 100 dp.
|
||||||
|
|
||||||
|
private int mMaxDistance = DEFAULT_MAX_DISTANCE_AT_ZOOM;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any modifications should be synchronized on mQuadTree.
|
||||||
|
*/
|
||||||
|
private final Collection<QuadItem<T>> mItems = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any modifications should be synchronized on mQuadTree.
|
||||||
|
*/
|
||||||
|
private final PointQuadTree<QuadItem<T>> mQuadTree = new PointQuadTree<>(0, 1, 0, 1);
|
||||||
|
|
||||||
|
private static final SphericalMercatorProjection PROJECTION = new SphericalMercatorProjection(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an item to the algorithm
|
||||||
|
*
|
||||||
|
* @param item the item to be added
|
||||||
|
* @return true if the algorithm contents changed as a result of the call
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean addItem(T item) {
|
||||||
|
boolean result;
|
||||||
|
final QuadItem<T> quadItem = new QuadItem<>(item);
|
||||||
|
synchronized (mQuadTree) {
|
||||||
|
result = mItems.add(quadItem);
|
||||||
|
if (result) {
|
||||||
|
mQuadTree.add(quadItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a collection of items to the algorithm
|
||||||
|
*
|
||||||
|
* @param items the items to be added
|
||||||
|
* @return true if the algorithm contents changed as a result of the call
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean addItems(Collection<T> items) {
|
||||||
|
boolean result = false;
|
||||||
|
for (T item : items) {
|
||||||
|
boolean individualResult = addItem(item);
|
||||||
|
if (individualResult) {
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearItems() {
|
||||||
|
synchronized (mQuadTree) {
|
||||||
|
mItems.clear();
|
||||||
|
mQuadTree.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an item from the algorithm
|
||||||
|
*
|
||||||
|
* @param item the item to be removed
|
||||||
|
* @return true if this algorithm contained the specified element (or equivalently, if this
|
||||||
|
* algorithm changed as a result of the call).
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean removeItem(T item) {
|
||||||
|
boolean result;
|
||||||
|
// QuadItem delegates hashcode() and equals() to its item so,
|
||||||
|
// removing any QuadItem to that item will remove the item
|
||||||
|
final QuadItem<T> quadItem = new QuadItem<>(item);
|
||||||
|
synchronized (mQuadTree) {
|
||||||
|
result = mItems.remove(quadItem);
|
||||||
|
if (result) {
|
||||||
|
mQuadTree.remove(quadItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a collection of items from the algorithm
|
||||||
|
*
|
||||||
|
* @param items the items to be removed
|
||||||
|
* @return true if this algorithm contents changed as a result of the call
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean removeItems(Collection<T> items) {
|
||||||
|
boolean result = false;
|
||||||
|
synchronized (mQuadTree) {
|
||||||
|
for (T item : items) {
|
||||||
|
// QuadItem delegates hashcode() and equals() to its item so,
|
||||||
|
// removing any QuadItem to that item will remove the item
|
||||||
|
final QuadItem<T> quadItem = new QuadItem<>(item);
|
||||||
|
boolean individualResult = mItems.remove(quadItem);
|
||||||
|
if (individualResult) {
|
||||||
|
mQuadTree.remove(quadItem);
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the provided item in the algorithm
|
||||||
|
*
|
||||||
|
* @param item the item to be updated
|
||||||
|
* @return true if the item existed in the algorithm and was updated, or false if the item did
|
||||||
|
* not exist in the algorithm and the algorithm contents remain unchanged.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean updateItem(T item) {
|
||||||
|
// TODO - Can this be optimized to update the item in-place if the location hasn't changed?
|
||||||
|
boolean result;
|
||||||
|
synchronized (mQuadTree) {
|
||||||
|
result = removeItem(item);
|
||||||
|
if (result) {
|
||||||
|
// Only add the item if it was removed (to help prevent accidental duplicates on map)
|
||||||
|
result = addItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<? extends Cluster<T>> getClusters(float zoom) {
|
||||||
|
final int discreteZoom = (int) zoom;
|
||||||
|
|
||||||
|
final double zoomSpecificSpan = mMaxDistance / Math.pow(2, discreteZoom) / 256;
|
||||||
|
|
||||||
|
final Set<QuadItem<T>> visitedCandidates = new HashSet<>();
|
||||||
|
final Set<Cluster<T>> results = new HashSet<>();
|
||||||
|
final Map<QuadItem<T>, Double> distanceToCluster = new HashMap<>();
|
||||||
|
final Map<QuadItem<T>, StaticCluster<T>> itemToCluster = new HashMap<>();
|
||||||
|
|
||||||
|
synchronized (mQuadTree) {
|
||||||
|
for (QuadItem<T> candidate : getClusteringItems(mQuadTree, zoom)) {
|
||||||
|
if (visitedCandidates.contains(candidate)) {
|
||||||
|
// Candidate is already part of another cluster.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bounds searchBounds = createBoundsFromSpan(candidate.getPoint(), zoomSpecificSpan);
|
||||||
|
Collection<QuadItem<T>> clusterItems;
|
||||||
|
clusterItems = mQuadTree.search(searchBounds);
|
||||||
|
if (clusterItems.size() == 1) {
|
||||||
|
// Only the current marker is in range. Just add the single item to the results.
|
||||||
|
results.add(candidate);
|
||||||
|
visitedCandidates.add(candidate);
|
||||||
|
distanceToCluster.put(candidate, 0d);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
StaticCluster<T> cluster = new StaticCluster<>(candidate.mClusterItem.getPosition());
|
||||||
|
results.add(cluster);
|
||||||
|
|
||||||
|
for (QuadItem<T> clusterItem : clusterItems) {
|
||||||
|
Double existingDistance = distanceToCluster.get(clusterItem);
|
||||||
|
double distance = distanceSquared(clusterItem.getPoint(), candidate.getPoint());
|
||||||
|
if (existingDistance != null) {
|
||||||
|
// Item already belongs to another cluster. Check if it's closer to this cluster.
|
||||||
|
if (existingDistance < distance) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Move item to the closer cluster.
|
||||||
|
itemToCluster.get(clusterItem).remove(clusterItem.mClusterItem);
|
||||||
|
}
|
||||||
|
distanceToCluster.put(clusterItem, distance);
|
||||||
|
cluster.add(clusterItem.mClusterItem);
|
||||||
|
itemToCluster.put(clusterItem, cluster);
|
||||||
|
}
|
||||||
|
visitedCandidates.addAll(clusterItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Collection<QuadItem<T>> getClusteringItems(PointQuadTree<QuadItem<T>> quadTree, float zoom) {
|
||||||
|
return mItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<T> getItems() {
|
||||||
|
final Set<T> items = new LinkedHashSet<>();
|
||||||
|
synchronized (mQuadTree) {
|
||||||
|
for (QuadItem<T> quadItem : mItems) {
|
||||||
|
items.add(quadItem.mClusterItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMaxDistanceBetweenClusteredItems(int maxDistance) {
|
||||||
|
mMaxDistance = maxDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMaxDistanceBetweenClusteredItems() {
|
||||||
|
return mMaxDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double distanceSquared(Point a, Point b) {
|
||||||
|
return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bounds createBoundsFromSpan(Point p, double span) {
|
||||||
|
// TODO: Use a span that takes into account the visual size of the marker, not just its
|
||||||
|
// LatLng.
|
||||||
|
double halfSpan = span / 2;
|
||||||
|
return new Bounds(
|
||||||
|
p.x - halfSpan, p.x + halfSpan,
|
||||||
|
p.y - halfSpan, p.y + halfSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class QuadItem<T extends ClusterItem> implements PointQuadTree.Item, Cluster<T> {
|
||||||
|
private final T mClusterItem;
|
||||||
|
private final Point mPoint;
|
||||||
|
private final LatLng mPosition;
|
||||||
|
private Set<T> singletonSet;
|
||||||
|
|
||||||
|
private QuadItem(T item) {
|
||||||
|
mClusterItem = item;
|
||||||
|
mPosition = item.getPosition();
|
||||||
|
mPoint = PROJECTION.toPoint(mPosition);
|
||||||
|
singletonSet = Collections.singleton(mClusterItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Point getPoint() {
|
||||||
|
return mPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LatLng getPosition() {
|
||||||
|
return mPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<T> getItems() {
|
||||||
|
return singletonSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSize() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return mClusterItem.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (!(other instanceof QuadItem<?>)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((QuadItem<?>) other).mClusterItem.equals(mClusterItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.clustering.algo;
|
||||||
|
|
||||||
|
import com.car2go.maps.model.LatLng;
|
||||||
|
import com.google.maps.android.clustering.Cluster;
|
||||||
|
import com.google.maps.android.clustering.ClusterItem;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cluster whose center is determined upon creation.
|
||||||
|
*/
|
||||||
|
public class StaticCluster<T extends ClusterItem> implements Cluster<T> {
|
||||||
|
private final LatLng mCenter;
|
||||||
|
private final List<T> mItems = new ArrayList<T>();
|
||||||
|
|
||||||
|
public StaticCluster(LatLng center) {
|
||||||
|
mCenter = center;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean add(T t) {
|
||||||
|
return mItems.add(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LatLng getPosition() {
|
||||||
|
return mCenter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean remove(T t) {
|
||||||
|
return mItems.remove(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<T> getItems() {
|
||||||
|
return mItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSize() {
|
||||||
|
return mItems.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "StaticCluster{" +
|
||||||
|
"mCenter=" + mCenter +
|
||||||
|
", mItems.size=" + mItems.size() +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return mCenter.hashCode() + mItems.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (!(other instanceof StaticCluster<?>)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((StaticCluster<?>) other).mCenter.equals(mCenter)
|
||||||
|
&& ((StaticCluster<?>) other).mItems.equals(mItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.geometry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an area in the cartesian plane.
|
||||||
|
*/
|
||||||
|
public class Bounds {
|
||||||
|
public final double minX;
|
||||||
|
public final double minY;
|
||||||
|
|
||||||
|
public final double maxX;
|
||||||
|
public final double maxY;
|
||||||
|
|
||||||
|
public final double midX;
|
||||||
|
public final double midY;
|
||||||
|
|
||||||
|
public Bounds(double minX, double maxX, double minY, double maxY) {
|
||||||
|
this.minX = minX;
|
||||||
|
this.minY = minY;
|
||||||
|
this.maxX = maxX;
|
||||||
|
this.maxY = maxY;
|
||||||
|
|
||||||
|
midX = (minX + maxX) / 2;
|
||||||
|
midY = (minY + maxY) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(double x, double y) {
|
||||||
|
return minX <= x && x <= maxX && minY <= y && y <= maxY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(Point point) {
|
||||||
|
return contains(point.x, point.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean intersects(double minX, double maxX, double minY, double maxY) {
|
||||||
|
return minX < this.maxX && this.minX < maxX && minY < this.maxY && this.minY < maxY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean intersects(Bounds bounds) {
|
||||||
|
return intersects(bounds.minX, bounds.maxX, bounds.minY, bounds.maxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(Bounds bounds) {
|
||||||
|
return bounds.minX >= minX && bounds.maxX <= maxX && bounds.minY >= minY && bounds.maxY <= maxY;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.geometry;
|
||||||
|
|
||||||
|
public class Point {
|
||||||
|
public final double x;
|
||||||
|
public final double y;
|
||||||
|
|
||||||
|
public Point(double x, double y) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Point{" +
|
||||||
|
"x=" + x +
|
||||||
|
", y=" + y +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.projection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated since 0.2. Use {@link com.google.maps.android.geometry.Point} instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public class Point extends com.google.maps.android.geometry.Point {
|
||||||
|
public Point(double x, double y) {
|
||||||
|
super(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.projection;
|
||||||
|
|
||||||
|
import com.car2go.maps.model.LatLng;
|
||||||
|
|
||||||
|
public class SphericalMercatorProjection {
|
||||||
|
final double mWorldWidth;
|
||||||
|
|
||||||
|
public SphericalMercatorProjection(final double worldWidth) {
|
||||||
|
mWorldWidth = worldWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public Point toPoint(final LatLng latLng) {
|
||||||
|
final double x = latLng.longitude / 360 + .5;
|
||||||
|
final double siny = Math.sin(Math.toRadians(latLng.latitude));
|
||||||
|
final double y = 0.5 * Math.log((1 + siny) / (1 - siny)) / -(2 * Math.PI) + .5;
|
||||||
|
|
||||||
|
return new Point(x * mWorldWidth, y * mWorldWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LatLng toLatLng(com.google.maps.android.geometry.Point point) {
|
||||||
|
final double x = point.x / mWorldWidth - 0.5;
|
||||||
|
final double lng = x * 360;
|
||||||
|
|
||||||
|
double y = .5 - (point.y / mWorldWidth);
|
||||||
|
final double lat = 90 - Math.toDegrees(Math.atan(Math.exp(-y * 2 * Math.PI)) * 2);
|
||||||
|
|
||||||
|
return new LatLng(lat, lng);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.maps.android.quadtree;
|
||||||
|
|
||||||
|
import com.google.maps.android.geometry.Bounds;
|
||||||
|
import com.google.maps.android.geometry.Point;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A quad tree which tracks items with a Point geometry.
|
||||||
|
* See http://en.wikipedia.org/wiki/Quadtree for details on the data structure.
|
||||||
|
* This class is not thread safe.
|
||||||
|
*/
|
||||||
|
public class PointQuadTree<T extends PointQuadTree.Item> {
|
||||||
|
public interface Item {
|
||||||
|
Point getPoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bounds of this quad.
|
||||||
|
*/
|
||||||
|
private final Bounds mBounds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The depth of this quad in the tree.
|
||||||
|
*/
|
||||||
|
private final int mDepth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of elements to store in a quad before splitting.
|
||||||
|
*/
|
||||||
|
private final static int MAX_ELEMENTS = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The elements inside this quad, if any.
|
||||||
|
*/
|
||||||
|
private Set<T> mItems;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum depth.
|
||||||
|
*/
|
||||||
|
private final static int MAX_DEPTH = 40;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Child quads.
|
||||||
|
*/
|
||||||
|
private List<PointQuadTree<T>> mChildren = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new quad tree with specified bounds.
|
||||||
|
*
|
||||||
|
* @param minX
|
||||||
|
* @param maxX
|
||||||
|
* @param minY
|
||||||
|
* @param maxY
|
||||||
|
*/
|
||||||
|
public PointQuadTree(double minX, double maxX, double minY, double maxY) {
|
||||||
|
this(new Bounds(minX, maxX, minY, maxY));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PointQuadTree(Bounds bounds) {
|
||||||
|
this(bounds, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PointQuadTree(double minX, double maxX, double minY, double maxY, int depth) {
|
||||||
|
this(new Bounds(minX, maxX, minY, maxY), depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PointQuadTree(Bounds bounds, int depth) {
|
||||||
|
mBounds = bounds;
|
||||||
|
mDepth = depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert an item.
|
||||||
|
*/
|
||||||
|
public void add(T item) {
|
||||||
|
Point point = item.getPoint();
|
||||||
|
if (this.mBounds.contains(point.x, point.y)) {
|
||||||
|
insert(point.x, point.y, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insert(double x, double y, T item) {
|
||||||
|
if (this.mChildren != null) {
|
||||||
|
if (y < mBounds.midY) {
|
||||||
|
if (x < mBounds.midX) { // top left
|
||||||
|
mChildren.get(0).insert(x, y, item);
|
||||||
|
} else { // top right
|
||||||
|
mChildren.get(1).insert(x, y, item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (x < mBounds.midX) { // bottom left
|
||||||
|
mChildren.get(2).insert(x, y, item);
|
||||||
|
} else {
|
||||||
|
mChildren.get(3).insert(x, y, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mItems == null) {
|
||||||
|
mItems = new LinkedHashSet<>();
|
||||||
|
}
|
||||||
|
mItems.add(item);
|
||||||
|
if (mItems.size() > MAX_ELEMENTS && mDepth < MAX_DEPTH) {
|
||||||
|
split();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split this quad.
|
||||||
|
*/
|
||||||
|
private void split() {
|
||||||
|
mChildren = new ArrayList<PointQuadTree<T>>(4);
|
||||||
|
mChildren.add(new PointQuadTree<T>(mBounds.minX, mBounds.midX, mBounds.minY, mBounds.midY, mDepth + 1));
|
||||||
|
mChildren.add(new PointQuadTree<T>(mBounds.midX, mBounds.maxX, mBounds.minY, mBounds.midY, mDepth + 1));
|
||||||
|
mChildren.add(new PointQuadTree<T>(mBounds.minX, mBounds.midX, mBounds.midY, mBounds.maxY, mDepth + 1));
|
||||||
|
mChildren.add(new PointQuadTree<T>(mBounds.midX, mBounds.maxX, mBounds.midY, mBounds.maxY, mDepth + 1));
|
||||||
|
|
||||||
|
Set<T> items = mItems;
|
||||||
|
mItems = null;
|
||||||
|
|
||||||
|
for (T item : items) {
|
||||||
|
// re-insert items into child quads.
|
||||||
|
insert(item.getPoint().x, item.getPoint().y, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the given item from the set.
|
||||||
|
*
|
||||||
|
* @return whether the item was removed.
|
||||||
|
*/
|
||||||
|
public boolean remove(T item) {
|
||||||
|
Point point = item.getPoint();
|
||||||
|
if (this.mBounds.contains(point.x, point.y)) {
|
||||||
|
return remove(point.x, point.y, item);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean remove(double x, double y, T item) {
|
||||||
|
if (this.mChildren != null) {
|
||||||
|
if (y < mBounds.midY) {
|
||||||
|
if (x < mBounds.midX) { // top left
|
||||||
|
return mChildren.get(0).remove(x, y, item);
|
||||||
|
} else { // top right
|
||||||
|
return mChildren.get(1).remove(x, y, item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (x < mBounds.midX) { // bottom left
|
||||||
|
return mChildren.get(2).remove(x, y, item);
|
||||||
|
} else {
|
||||||
|
return mChildren.get(3).remove(x, y, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mItems == null) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return mItems.remove(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all points from the quadTree
|
||||||
|
*/
|
||||||
|
public void clear() {
|
||||||
|
mChildren = null;
|
||||||
|
if (mItems != null) {
|
||||||
|
mItems.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for all items within a given bounds.
|
||||||
|
*/
|
||||||
|
public Collection<T> search(Bounds searchBounds) {
|
||||||
|
final List<T> results = new ArrayList<T>();
|
||||||
|
search(searchBounds, results);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void search(Bounds searchBounds, Collection<T> results) {
|
||||||
|
if (!mBounds.intersects(searchBounds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.mChildren != null) {
|
||||||
|
for (PointQuadTree<T> quad : mChildren) {
|
||||||
|
quad.search(searchBounds, results);
|
||||||
|
}
|
||||||
|
} else if (mItems != null) {
|
||||||
|
if (searchBounds.contains(mBounds)) {
|
||||||
|
results.addAll(mItems);
|
||||||
|
} else {
|
||||||
|
for (T item : mItems) {
|
||||||
|
if (searchBounds.contains(item.getPoint())) {
|
||||||
|
results.add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,10 @@ import android.os.Build
|
|||||||
import androidx.work.Configuration
|
import androidx.work.Configuration
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.NetworkType
|
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import net.vonforst.evmap.storage.CleanupCacheWorker
|
import net.vonforst.evmap.storage.CleanupCacheWorker
|
||||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||||
import net.vonforst.evmap.storage.UpdateFullDownloadWorker
|
|
||||||
import net.vonforst.evmap.ui.updateAppLocale
|
import net.vonforst.evmap.ui.updateAppLocale
|
||||||
import net.vonforst.evmap.ui.updateNightMode
|
import net.vonforst.evmap.ui.updateNightMode
|
||||||
import org.acra.config.dialog
|
import org.acra.config.dialog
|
||||||
@@ -70,7 +68,6 @@ class EvMapApplication : Application(), Configuration.Provider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val workManager = WorkManager.getInstance(this)
|
|
||||||
val cleanupCacheRequest = PeriodicWorkRequestBuilder<CleanupCacheWorker>(Duration.ofDays(1))
|
val cleanupCacheRequest = PeriodicWorkRequestBuilder<CleanupCacheWorker>(Duration.ofDays(1))
|
||||||
.setConstraints(Constraints.Builder().apply {
|
.setConstraints(Constraints.Builder().apply {
|
||||||
setRequiresBatteryNotLow(true)
|
setRequiresBatteryNotLow(true)
|
||||||
@@ -78,24 +75,9 @@ class EvMapApplication : Application(), Configuration.Provider {
|
|||||||
setRequiresDeviceIdle(true)
|
setRequiresDeviceIdle(true)
|
||||||
}
|
}
|
||||||
}.build()).build()
|
}.build()).build()
|
||||||
workManager.enqueueUniquePeriodicWork(
|
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
|
||||||
"CleanupCacheWorker", ExistingPeriodicWorkPolicy.UPDATE, cleanupCacheRequest
|
"CleanupCacheWorker", ExistingPeriodicWorkPolicy.UPDATE, cleanupCacheRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
val updateFullDownloadRequest =
|
|
||||||
PeriodicWorkRequestBuilder<UpdateFullDownloadWorker>(Duration.ofDays(7))
|
|
||||||
.setConstraints(Constraints.Builder().apply {
|
|
||||||
setRequiresBatteryNotLow(true)
|
|
||||||
setRequiredNetworkType(NetworkType.UNMETERED)
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
setRequiresDeviceIdle(true)
|
|
||||||
}
|
|
||||||
}.build()).build()
|
|
||||||
workManager.enqueueUniquePeriodicWork(
|
|
||||||
"UpdateOsmWorker",
|
|
||||||
ExistingPeriodicWorkPolicy.UPDATE,
|
|
||||||
updateFullDownloadRequest
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override val workManagerConfiguration = Configuration.Builder().build()
|
override val workManagerConfiguration = Configuration.Builder().build()
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.splashscreen.SplashScreen
|
import androidx.core.splashscreen.SplashScreen
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@@ -56,7 +55,6 @@ class MapsActivity : AppCompatActivity(),
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val splashScreen = installSplashScreen()
|
val splashScreen = installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
WindowCompat.enableEdgeToEdge(window)
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_maps)
|
setContentView(R.layout.activity_maps)
|
||||||
|
|
||||||
@@ -285,7 +283,7 @@ class MapsActivity : AppCompatActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openUrl(url: String, rootView: View, preferBrowser: Boolean = false) {
|
fun openUrl(url: String, rootView: View, preferBrowser: Boolean = true) {
|
||||||
val intent = CustomTabsIntent.Builder()
|
val intent = CustomTabsIntent.Builder()
|
||||||
.setDefaultColorSchemeParams(
|
.setDefaultColorSchemeParams(
|
||||||
CustomTabColorSchemeParams.Builder()
|
CustomTabColorSchemeParams.Builder()
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
package net.vonforst.evmap.adapter
|
package net.vonforst.evmap.adapter
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.chip.Chip
|
||||||
import net.vonforst.evmap.BR
|
import net.vonforst.evmap.BR
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||||
|
import net.vonforst.evmap.api.chargeprice.ChargePrice
|
||||||
|
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||||
|
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
|
||||||
|
import net.vonforst.evmap.api.chargeprice.ChargepriceTag
|
||||||
|
import net.vonforst.evmap.databinding.ItemChargepriceBinding
|
||||||
|
import net.vonforst.evmap.databinding.ItemChargepriceVehicleChipBinding
|
||||||
|
import net.vonforst.evmap.databinding.ItemConnectorButtonBinding
|
||||||
import net.vonforst.evmap.model.Chargepoint
|
import net.vonforst.evmap.model.Chargepoint
|
||||||
|
import net.vonforst.evmap.ui.CheckableConstraintLayout
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
interface Equatable {
|
interface Equatable {
|
||||||
@@ -95,4 +106,141 @@ class ConnectorDetailsAdapter : DataBindingAdapter<ConnectorDetailsAdapter.Conne
|
|||||||
Equatable
|
Equatable
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int = R.layout.dialog_connector_details_item
|
override fun getItemViewType(position: Int): Int = R.layout.dialog_connector_details_item
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChargepriceAdapter :
|
||||||
|
DataBindingAdapter<ChargePrice>() {
|
||||||
|
|
||||||
|
val viewPool = RecyclerView.RecycledViewPool()
|
||||||
|
var meta: ChargepriceChargepointMeta? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
var myTariffs: Set<String>? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
var myTariffsAll: Boolean? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<ChargePrice> {
|
||||||
|
val holder = super.onCreateViewHolder(parent, viewType)
|
||||||
|
val binding = holder.binding as ItemChargepriceBinding
|
||||||
|
binding.rvTags.apply {
|
||||||
|
adapter = ChargepriceTagsAdapter()
|
||||||
|
layoutManager =
|
||||||
|
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false).apply {
|
||||||
|
recycleChildrenOnDetach = true
|
||||||
|
}
|
||||||
|
itemAnimator = null
|
||||||
|
setRecycledViewPool(viewPool)
|
||||||
|
}
|
||||||
|
return holder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(holder: ViewHolder<ChargePrice>, item: ChargePrice) {
|
||||||
|
super.bind(holder, item)
|
||||||
|
(holder.binding as ItemChargepriceBinding).apply {
|
||||||
|
this.meta = this@ChargepriceAdapter.meta
|
||||||
|
this.myTariffs = this@ChargepriceAdapter.myTariffs
|
||||||
|
this.myTariffsAll = this@ChargepriceAdapter.myTariffsAll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
|
||||||
|
private var checkedItem: Int? = 0
|
||||||
|
|
||||||
|
var enabledConnectors: List<String>? = null
|
||||||
|
get() = field
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
checkedItem?.let {
|
||||||
|
if (value != null && getItem(it).type !in value) {
|
||||||
|
checkedItem = currentList.indexOfFirst {
|
||||||
|
it.type in value
|
||||||
|
}.takeIf { it != -1 }
|
||||||
|
onCheckedItemChangedListener?.invoke(getCheckedItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int = R.layout.item_connector_button
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder<Chargepoint>, position: Int) {
|
||||||
|
val item = getItem(position)
|
||||||
|
super.bind(holder, item)
|
||||||
|
val binding = holder.binding as ItemConnectorButtonBinding
|
||||||
|
binding.enabled = enabledConnectors?.let { item.type in it } ?: true
|
||||||
|
val root = binding.root as CheckableConstraintLayout
|
||||||
|
root.setOnCheckedChangeListener { _, _ -> }
|
||||||
|
root.isChecked = checkedItem == position
|
||||||
|
root.setOnClickListener {
|
||||||
|
root.isChecked = true
|
||||||
|
}
|
||||||
|
root.setOnCheckedChangeListener { _, checked: Boolean ->
|
||||||
|
if (checked) {
|
||||||
|
checkedItem = holder.bindingAdapterPosition.takeIf { it != -1 }
|
||||||
|
root.post {
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
getCheckedItem()?.let { onCheckedItemChangedListener?.invoke(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCheckedItem(): Chargepoint? = checkedItem?.let { getItem(it) }
|
||||||
|
|
||||||
|
fun setCheckedItem(item: Chargepoint?) {
|
||||||
|
checkedItem = item?.let { currentList.indexOf(item) }.takeIf { it != -1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
var onCheckedItemChangedListener: ((Chargepoint?) -> Unit)? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChargepriceTagsAdapter() :
|
||||||
|
DataBindingAdapter<ChargepriceTag>() {
|
||||||
|
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice_tag
|
||||||
|
}
|
||||||
|
|
||||||
|
class CheckableChargepriceCarAdapter : DataBindingAdapter<ChargepriceCar>() {
|
||||||
|
private var checkedItem: ChargepriceCar? = null
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int = R.layout.item_chargeprice_vehicle_chip
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder<ChargepriceCar>, position: Int) {
|
||||||
|
val item = getItem(position)
|
||||||
|
super.bind(holder, item)
|
||||||
|
val binding = holder.binding as ItemChargepriceVehicleChipBinding
|
||||||
|
val root = binding.root as Chip
|
||||||
|
root.isChecked = checkedItem == item
|
||||||
|
root.setOnClickListener {
|
||||||
|
root.isChecked = true
|
||||||
|
}
|
||||||
|
root.setOnCheckedChangeListener { _, checked: Boolean ->
|
||||||
|
if (checked && item != checkedItem) {
|
||||||
|
checkedItem = item
|
||||||
|
root.post {
|
||||||
|
notifyDataSetChanged()
|
||||||
|
onCheckedItemChangedListener?.invoke(getCheckedItem()!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCheckedItem(): ChargepriceCar? = checkedItem
|
||||||
|
|
||||||
|
fun setCheckedItem(item: ChargepriceCar?) {
|
||||||
|
checkedItem = item
|
||||||
|
}
|
||||||
|
|
||||||
|
var onCheckedItemChangedListener: ((ChargepriceCar?) -> Unit)? = null
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,6 @@ import net.vonforst.evmap.joinToSpannedString
|
|||||||
import net.vonforst.evmap.model.ChargeCard
|
import net.vonforst.evmap.model.ChargeCard
|
||||||
import net.vonforst.evmap.model.ChargeCardId
|
import net.vonforst.evmap.model.ChargeCardId
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
import net.vonforst.evmap.model.ChargeLocation
|
||||||
import net.vonforst.evmap.model.Coordinate
|
|
||||||
import net.vonforst.evmap.model.OpeningHoursDays
|
import net.vonforst.evmap.model.OpeningHoursDays
|
||||||
import net.vonforst.evmap.plus
|
import net.vonforst.evmap.plus
|
||||||
import net.vonforst.evmap.ui.currency
|
import net.vonforst.evmap.ui.currency
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import com.car2go.maps.model.LatLng
|
|||||||
import com.car2go.maps.model.LatLngBounds
|
import com.car2go.maps.model.LatLngBounds
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
|
||||||
import net.vonforst.evmap.api.nobil.NobilApiWrapper
|
|
||||||
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
|
||||||
import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper
|
|
||||||
import net.vonforst.evmap.model.*
|
import net.vonforst.evmap.model.*
|
||||||
import net.vonforst.evmap.viewmodel.Resource
|
import net.vonforst.evmap.viewmodel.Resource
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -60,27 +58,6 @@ interface ChargepointApi<out T : ReferenceData> {
|
|||||||
* Duration we are limited to if there is a required API local cache time limit.
|
* Duration we are limited to if there is a required API local cache time limit.
|
||||||
*/
|
*/
|
||||||
val cacheLimit: Duration
|
val cacheLimit: Duration
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this API supports querying for chargers at the backend
|
|
||||||
*
|
|
||||||
* This determines whether the getChargepoints, getChargepointsRadius and getChargepointDetail functions are supported.
|
|
||||||
*/
|
|
||||||
val supportsOnlineQueries: Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether this API supports downloading the whole dataset into local storage
|
|
||||||
*
|
|
||||||
* This determines whether the getAllChargepoints function is supported.
|
|
||||||
*/
|
|
||||||
val supportsFullDownload: Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches all available chargers from this API.
|
|
||||||
*
|
|
||||||
* This may take a long time and should only be used when the user explicitly wants to download all chargers.
|
|
||||||
*/
|
|
||||||
suspend fun fullDownload(): FullDownloadResult<T>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StringProvider {
|
interface StringProvider {
|
||||||
@@ -95,13 +72,6 @@ fun Context.stringProvider() = object : StringProvider {
|
|||||||
|
|
||||||
fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
|
fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
"nobil" -> {
|
|
||||||
NobilApiWrapper(
|
|
||||||
ctx.getString(
|
|
||||||
R.string.nobil_key
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
"openchargemap" -> {
|
"openchargemap" -> {
|
||||||
OpenChargeMapApiWrapper(
|
OpenChargeMapApiWrapper(
|
||||||
ctx.getString(
|
ctx.getString(
|
||||||
@@ -109,7 +79,6 @@ fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"goingelectric" -> {
|
"goingelectric" -> {
|
||||||
GoingElectricApiWrapper(
|
GoingElectricApiWrapper(
|
||||||
ctx.getString(
|
ctx.getString(
|
||||||
@@ -117,11 +86,6 @@ fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
"openstreetmap" -> {
|
|
||||||
OpenStreetMapApiWrapper()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,20 +100,4 @@ data class ChargepointList(val items: List<ChargepointListItem>, val isComplete:
|
|||||||
companion object {
|
companion object {
|
||||||
fun empty() = ChargepointList(emptyList(), true)
|
fun empty() = ChargepointList(emptyList(), true)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result returned from fullDownload() function.
|
|
||||||
*
|
|
||||||
* Note that [chargers] is implemented as a [Sequence] so that downloaded chargers can be saved
|
|
||||||
* while they are being parsed instead of having to keep all of them in RAM at once.
|
|
||||||
*
|
|
||||||
* [progress] is updated regularly to indicate the current download progress.
|
|
||||||
* [referenceData] will typically only be available once the download is completed, i.e. you have
|
|
||||||
* iterated over the whole sequence of [chargers].
|
|
||||||
*/
|
|
||||||
interface FullDownloadResult<out T : ReferenceData> {
|
|
||||||
val chargers: Sequence<ChargeLocation>
|
|
||||||
val progress: Float
|
|
||||||
val referenceData: T
|
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,18 @@
|
|||||||
package net.vonforst.evmap.api
|
package net.vonforst.evmap.api
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.RateLimiter
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
import kotlin.time.TimeSource
|
|
||||||
|
|
||||||
|
|
||||||
class RateLimitInterceptor : Interceptor {
|
class RateLimitInterceptor : Interceptor {
|
||||||
private val rateLimiter = SimpleRateLimiter(3.0)
|
private val rateLimiter = RateLimiter.create(3.0)
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
if (request.url.host == "ui-map.shellrecharge.com") {
|
if (request.url.host == "ui-map.shellrecharge.com") {
|
||||||
// limit requests sent to NewMotion to 3 per second
|
// limit requests sent to NewMotion to 3 per second
|
||||||
rateLimiter.acquire()
|
rateLimiter.acquire(1)
|
||||||
|
|
||||||
var response: Response = chain.proceed(request)
|
var response: Response = chain.proceed(request)
|
||||||
// 403 is how the NewMotion API indicates a rate limit error
|
// 403 is how the NewMotion API indicates a rate limit error
|
||||||
@@ -32,27 +30,4 @@ class RateLimitInterceptor : Interceptor {
|
|||||||
return chain.proceed(request)
|
return chain.proceed(request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
internal class SimpleRateLimiter(private val permitsPerSecond: Double) {
|
|
||||||
private val interval: Duration = (1.0 / permitsPerSecond).seconds
|
|
||||||
private var nextAvailable = TimeSource.Monotonic.markNow()
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun acquire() {
|
|
||||||
val now = TimeSource.Monotonic.markNow()
|
|
||||||
if (now < nextAvailable) {
|
|
||||||
val waitTime = nextAvailable - now
|
|
||||||
waitTime.sleep()
|
|
||||||
nextAvailable += interval
|
|
||||||
} else {
|
|
||||||
nextAvailable = now + interval
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Duration.sleep() {
|
|
||||||
if (this.isPositive()) {
|
|
||||||
Thread.sleep(this.inWholeMilliseconds, (this.inWholeNanoseconds % 1_000_000).toInt())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,54 @@
|
|||||||
package net.vonforst.evmap.api
|
package net.vonforst.evmap.api
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
import net.vonforst.evmap.model.Chargepoint
|
import net.vonforst.evmap.model.Chargepoint
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.json.JSONArray
|
||||||
|
import java.io.IOException
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
operator fun <T> JSONArray.iterator(): Iterator<T> =
|
||||||
|
(0 until length()).asSequence().map {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
get(it) as T
|
||||||
|
}.iterator()
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
suspend fun Call.await(): Response {
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
enqueue(object : Callback {
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
continuation.resume(response) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
if (continuation.isCancelled) return
|
||||||
|
continuation.resumeWithException(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
try {
|
||||||
|
cancel()
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
//Ignore cancel exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val plugNames = mapOf(
|
private val plugNames = mapOf(
|
||||||
Chargepoint.TYPE_1 to R.string.plug_type_1,
|
Chargepoint.TYPE_1 to R.string.plug_type_1,
|
||||||
Chargepoint.TYPE_2_UNKNOWN to R.string.plug_type_2,
|
Chargepoint.TYPE_2_UNKNOWN to R.string.plug_type_2,
|
||||||
Chargepoint.TYPE_2_PLUG to R.string.plug_type_2_tethered,
|
Chargepoint.TYPE_2_PLUG to R.string.plug_type_2,
|
||||||
Chargepoint.TYPE_2_SOCKET to R.string.plug_type_2,
|
Chargepoint.TYPE_2_SOCKET to R.string.plug_type_2,
|
||||||
Chargepoint.TYPE_3A to R.string.plug_type_3a,
|
Chargepoint.TYPE_3 to R.string.plug_type_3,
|
||||||
Chargepoint.TYPE_3C to R.string.plug_type_3c,
|
|
||||||
Chargepoint.CCS_UNKNOWN to R.string.plug_ccs,
|
Chargepoint.CCS_UNKNOWN to R.string.plug_ccs,
|
||||||
Chargepoint.CCS_TYPE_1 to R.string.plug_ccs,
|
Chargepoint.CCS_TYPE_1 to R.string.plug_ccs,
|
||||||
Chargepoint.CCS_TYPE_2 to R.string.plug_ccs,
|
Chargepoint.CCS_TYPE_2 to R.string.plug_ccs,
|
||||||
@@ -64,7 +101,7 @@ fun iconForPlugType(type: String): Int =
|
|||||||
Chargepoint.CEE_ROT -> R.drawable.ic_connector_cee_rot
|
Chargepoint.CEE_ROT -> R.drawable.ic_connector_cee_rot
|
||||||
Chargepoint.TYPE_1 -> R.drawable.ic_connector_typ1
|
Chargepoint.TYPE_1 -> R.drawable.ic_connector_typ1
|
||||||
// TODO: add other connectors
|
// TODO: add other connectors
|
||||||
else -> R.drawable.ic_connector_unknown
|
else -> 0
|
||||||
}
|
}
|
||||||
|
|
||||||
val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 75, 100, 150, 200, 250, 300, 350)
|
val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 75, 100, 150, 200, 250, 300, 350)
|
||||||
|
|||||||
@@ -175,7 +175,6 @@ class AvailabilityRepository(context: Context) {
|
|||||||
RheinenergieAvailabilityDetector(okhttp),
|
RheinenergieAvailabilityDetector(okhttp),
|
||||||
teslaOwnerAvailabilityDetector,
|
teslaOwnerAvailabilityDetector,
|
||||||
TeslaGuestAvailabilityDetector(okhttp),
|
TeslaGuestAvailabilityDetector(okhttp),
|
||||||
NobilAvailabilityDetector(okhttp, context),
|
|
||||||
EnBwAvailabilityDetector(okhttp),
|
EnBwAvailabilityDetector(okhttp),
|
||||||
NewMotionAvailabilityDetector(okhttp)
|
NewMotionAvailabilityDetector(okhttp)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.squareup.moshi.FromJson
|
|||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.ToJson
|
import com.squareup.moshi.ToJson
|
||||||
|
import net.vonforst.evmap.api.availability.tesla.LocalTimeAdapter
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
import net.vonforst.evmap.model.ChargeLocation
|
||||||
import net.vonforst.evmap.model.Chargepoint
|
import net.vonforst.evmap.model.Chargepoint
|
||||||
import net.vonforst.evmap.utils.distanceBetween
|
import net.vonforst.evmap.utils.distanceBetween
|
||||||
@@ -14,6 +15,7 @@ import retrofit2.http.GET
|
|||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.time.LocalTime
|
||||||
|
|
||||||
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
||||||
private const val maxDistance = 60 // max distance between reported positions in meters
|
private const val maxDistance = 60 // max distance between reported positions in meters
|
||||||
@@ -200,10 +202,9 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
|||||||
val id = index.toLong()
|
val id = index.toLong()
|
||||||
val power = connector.maxPowerInKw ?: 0.0
|
val power = connector.maxPowerInKw ?: 0.0
|
||||||
val type = when (connector.plugTypeName) {
|
val type = when (connector.plugTypeName) {
|
||||||
"Typ 3A" -> Chargepoint.TYPE_3A
|
"Typ 3A" -> Chargepoint.TYPE_3
|
||||||
"Typ 3C \"Scame\"" -> Chargepoint.TYPE_3C
|
|
||||||
"Typ 2" -> Chargepoint.TYPE_2_UNKNOWN
|
"Typ 2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||||
"Typ 1 Steckdose" -> Chargepoint.TYPE_1
|
"Typ 1" -> Chargepoint.TYPE_1
|
||||||
"Steckdose(D)" -> Chargepoint.SCHUKO
|
"Steckdose(D)" -> Chargepoint.SCHUKO
|
||||||
"CCS (Typ 1)" -> Chargepoint.CCS_TYPE_1 // US CCS, aka type1_combo
|
"CCS (Typ 1)" -> Chargepoint.CCS_TYPE_1 // US CCS, aka type1_combo
|
||||||
"CCS (Typ 2)" -> Chargepoint.CCS_TYPE_2 // EU CCS, aka type2_combo
|
"CCS (Typ 2)" -> Chargepoint.CCS_TYPE_2 // EU CCS, aka type2_combo
|
||||||
@@ -242,8 +243,8 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||||
val country = charger.chargepriceData?.country ?: charger.address?.country
|
val country = charger.chargepriceData?.country
|
||||||
|
?: charger.address?.country ?: return false
|
||||||
return when (charger.dataSource) {
|
return when (charger.dataSource) {
|
||||||
// list of countries as of 2023/04/14, according to
|
// list of countries as of 2023/04/14, according to
|
||||||
// https://www.enbw.com/elektromobilitaet/produkte/ladetarife
|
// https://www.enbw.com/elektromobilitaet/produkte/ladetarife
|
||||||
@@ -266,7 +267,6 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
|||||||
"Spanien",
|
"Spanien",
|
||||||
"Tschechien"
|
"Tschechien"
|
||||||
) && charger.network != "Tesla Supercharger"
|
) && charger.network != "Tesla Supercharger"
|
||||||
"nobil" -> charger.network != "Tesla"
|
|
||||||
"openchargemap" -> country in listOf(
|
"openchargemap" -> country in listOf(
|
||||||
"DE",
|
"DE",
|
||||||
"AT",
|
"AT",
|
||||||
@@ -286,12 +286,6 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
|||||||
"ES",
|
"ES",
|
||||||
"CZ"
|
"CZ"
|
||||||
) && charger.chargepriceData?.network !in listOf("23", "3534")
|
) && charger.chargepriceData?.network !in listOf("23", "3534")
|
||||||
/* TODO: OSM usually does not have the country tagged. Therefore we currently just use
|
|
||||||
a bounding box to determine whether the charger is roughly in Europe */
|
|
||||||
"openstreetmap" -> charger.coordinates.lat in 35.0..72.0
|
|
||||||
&& charger.coordinates.lng in 25.0..65.0
|
|
||||||
&& charger.operator !in listOf("Tesla, Inc.", "Tesla")
|
|
||||||
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package net.vonforst.evmap.api.availability
|
package net.vonforst.evmap.api.availability
|
||||||
|
|
||||||
|
import androidx.car.app.model.DateTimeWithZone
|
||||||
import com.squareup.moshi.FromJson
|
import com.squareup.moshi.FromJson
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
@@ -12,8 +13,12 @@ import retrofit2.Retrofit
|
|||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.util.Locale
|
import java.time.format.DateTimeParseException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
||||||
private const val maxDistance = 60 // max distance between reported positions in meters
|
private const val maxDistance = 60 // max distance between reported positions in meters
|
||||||
@@ -61,9 +66,8 @@ interface NewMotionApi {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class NMElectricalProperties(val powerType: String, val voltage: Int, val amperage: Int, val maxElectricPower: Double?) {
|
data class NMElectricalProperties(val powerType: String, val voltage: Int, val amperage: Int) {
|
||||||
fun getPower(): Double {
|
fun getPower(): Double {
|
||||||
maxElectricPower?.let { return it }
|
|
||||||
val phases = when (powerType) {
|
val phases = when (powerType) {
|
||||||
"AC1Phase" -> 1
|
"AC1Phase" -> 1
|
||||||
"AC3Phase" -> 3
|
"AC3Phase" -> 3
|
||||||
@@ -176,7 +180,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
|||||||
val id = connector.uid
|
val id = connector.uid
|
||||||
val power = connector.electricalProperties.getPower()
|
val power = connector.electricalProperties.getPower()
|
||||||
val type = when (connector.connectorType.lowercase(Locale.ROOT)) {
|
val type = when (connector.connectorType.lowercase(Locale.ROOT)) {
|
||||||
"type3" -> Chargepoint.TYPE_3C
|
"type3" -> Chargepoint.TYPE_3
|
||||||
"type2" -> Chargepoint.TYPE_2_UNKNOWN
|
"type2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||||
"type1" -> Chargepoint.TYPE_1
|
"type1" -> Chargepoint.TYPE_1
|
||||||
"domestic" -> Chargepoint.SCHUKO
|
"domestic" -> Chargepoint.SCHUKO
|
||||||
@@ -221,9 +225,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
|||||||
// NewMotion is our fallback
|
// NewMotion is our fallback
|
||||||
return when (charger.dataSource) {
|
return when (charger.dataSource) {
|
||||||
"goingelectric" -> charger.network != "Tesla Supercharger"
|
"goingelectric" -> charger.network != "Tesla Supercharger"
|
||||||
"nobil" -> charger.network != "Tesla"
|
|
||||||
"openchargemap" -> charger.chargepriceData?.network !in listOf("23", "3534")
|
"openchargemap" -> charger.chargepriceData?.network !in listOf("23", "3534")
|
||||||
"openstreetmap" -> charger.operator !in listOf("Tesla, Inc.", "Tesla")
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
package net.vonforst.evmap.api.availability
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.squareup.moshi.FromJson
|
|
||||||
import com.squareup.moshi.JsonClass
|
|
||||||
import com.squareup.moshi.Moshi
|
|
||||||
import com.squareup.moshi.ToJson
|
|
||||||
import net.vonforst.evmap.R
|
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import retrofit2.http.Header
|
|
||||||
import retrofit2.http.Path
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
internal class InstantStringAdapter {
|
|
||||||
@FromJson
|
|
||||||
fun fromJson(value: String?): Instant? = value?.let {
|
|
||||||
Instant.parse(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ToJson
|
|
||||||
fun toJson(value: Instant?): String? = value?.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NobilRealtimeApi {
|
|
||||||
@GET("{nobilId}")
|
|
||||||
suspend fun getAvailability(
|
|
||||||
@Path("nobilId") nobilId: String,
|
|
||||||
@Header("X-Api-Key") apiKey: String
|
|
||||||
): List<NobilChargepointState>
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun create(client: OkHttpClient): NobilRealtimeApi {
|
|
||||||
val retrofit = Retrofit.Builder()
|
|
||||||
.baseUrl("https://api.ev-map.app/nobil/api/realtime/")
|
|
||||||
.addConverterFactory(
|
|
||||||
MoshiConverterFactory.create(
|
|
||||||
Moshi.Builder().add(InstantStringAdapter()).build()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.client(client)
|
|
||||||
.build()
|
|
||||||
return retrofit.create(NobilRealtimeApi::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class NobilChargepointState(
|
|
||||||
val evseUid: String,
|
|
||||||
val status: String,
|
|
||||||
val timestamp: Instant
|
|
||||||
)
|
|
||||||
|
|
||||||
class NobilAvailabilityDetector(client: OkHttpClient, context: Context) :
|
|
||||||
BaseAvailabilityDetector(client) {
|
|
||||||
val api = NobilRealtimeApi.create(client)
|
|
||||||
val apiKey = context.getString(R.string.evmap_key)
|
|
||||||
|
|
||||||
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
|
|
||||||
val nobilId = when (location.address?.country) {
|
|
||||||
"Norway" -> "NOR"
|
|
||||||
"Sweden" -> "SWE"
|
|
||||||
else -> throw AvailabilityDetectorException("nobil: unsupported country")
|
|
||||||
} + "_%05d".format(location.id)
|
|
||||||
|
|
||||||
val availability = api.getAvailability(nobilId, apiKey)
|
|
||||||
if (availability.isEmpty()) {
|
|
||||||
throw AvailabilityDetectorException("nobil: no real-time data available")
|
|
||||||
}
|
|
||||||
return ChargeLocationStatus(
|
|
||||||
location.chargepointsMerged.associateWith { cp ->
|
|
||||||
cp.evseUIds!!.map { evseUId ->
|
|
||||||
when (availability.find { it.evseUid == evseUId }?.status) {
|
|
||||||
"AVAILABLE" -> ChargepointStatus.AVAILABLE
|
|
||||||
"BLOCKED" -> ChargepointStatus.OCCUPIED
|
|
||||||
"CHARGING" -> ChargepointStatus.CHARGING
|
|
||||||
"INOPERATIVE" -> ChargepointStatus.FAULTED
|
|
||||||
"OUTOFORDER" -> ChargepointStatus.FAULTED
|
|
||||||
"PLANNED" -> ChargepointStatus.FAULTED
|
|
||||||
"REMOVED" -> ChargepointStatus.FAULTED
|
|
||||||
"RESERVED" -> ChargepointStatus.OCCUPIED
|
|
||||||
"UNKNOWN" -> ChargepointStatus.UNKNOWN
|
|
||||||
else -> ChargepointStatus.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Nobil",
|
|
||||||
location.chargepointsMerged.associateWith { cp ->
|
|
||||||
if (cp.evseIds != null) cp.evseIds.map { it ?: "??" } else listOf()
|
|
||||||
},
|
|
||||||
lastChange = location.chargepointsMerged.associateWith { cp ->
|
|
||||||
cp.evseUIds!!.map { evseUId ->
|
|
||||||
availability.find { it.evseUid == evseUId }?.timestamp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
|
||||||
return when (charger.dataSource) {
|
|
||||||
"nobil" -> charger.chargepoints.any { it.evseUIds?.isNotEmpty() == true }
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,10 +23,6 @@ class TeslaGuestAvailabilityDetector(
|
|||||||
private var api = TeslaChargingGuestGraphQlApi.create(client, baseUrl)
|
private var api = TeslaChargingGuestGraphQlApi.create(client, baseUrl)
|
||||||
|
|
||||||
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
|
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
|
||||||
if (location.chargepoints.isEmpty() || location.chargepoints.any { !it.hasKnownPower() }) {
|
|
||||||
throw AvailabilityDetectorException("no candidates found.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val results = cuaApi.getTeslaLocations()
|
val results = cuaApi.getTeslaLocations()
|
||||||
|
|
||||||
val result =
|
val result =
|
||||||
@@ -86,40 +82,6 @@ class TeslaGuestAvailabilityDetector(
|
|||||||
}
|
}
|
||||||
val details = detailsA.await()
|
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 scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
|
||||||
val scV2CCSConnectors = location.chargepoints.filter {
|
val scV2CCSConnectors = location.chargepoints.filter {
|
||||||
it.type in listOf(
|
it.type in listOf(
|
||||||
@@ -187,7 +149,7 @@ class TeslaGuestAvailabilityDetector(
|
|||||||
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
|
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
|
||||||
val labelsMap = detailsMap.mapValues { it.value.map { it.label } }
|
val labelsMap = detailsMap.mapValues { it.value.map { it.label } }
|
||||||
|
|
||||||
val pricing = details.pricing?.copy(memberRates = guestPricing.await()?.userRates)
|
val pricing = details.pricing.copy(memberRates = guestPricing.await()?.userRates)
|
||||||
|
|
||||||
return ChargeLocationStatus(
|
return ChargeLocationStatus(
|
||||||
statusMap,
|
statusMap,
|
||||||
@@ -200,9 +162,7 @@ class TeslaGuestAvailabilityDetector(
|
|||||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||||
return when (charger.dataSource) {
|
return when (charger.dataSource) {
|
||||||
"goingelectric" -> charger.network == "Tesla Supercharger"
|
"goingelectric" -> charger.network == "Tesla Supercharger"
|
||||||
"nobil" -> charger.network == "Tesla"
|
|
||||||
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
||||||
"openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla")
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,6 @@ class TeslaOwnerAvailabilityDetector(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
|
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
|
||||||
if (location.chargepoints.isEmpty() || location.chargepoints.any { !it.hasKnownPower() }) {
|
|
||||||
throw AvailabilityDetectorException("no candidates found.")
|
|
||||||
}
|
|
||||||
|
|
||||||
val api = initApi()
|
val api = initApi()
|
||||||
val req = TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesRequest(
|
val req = TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesRequest(
|
||||||
TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesVariables(
|
TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesVariables(
|
||||||
@@ -45,7 +41,8 @@ class TeslaOwnerAvailabilityDetector(
|
|||||||
TeslaChargingOwnershipGraphQlApi.Coordinate(
|
TeslaChargingOwnershipGraphQlApi.Coordinate(
|
||||||
location.coordinates.lat - coordRange,
|
location.coordinates.lat - coordRange,
|
||||||
location.coordinates.lng + coordRange
|
location.coordinates.lng + coordRange
|
||||||
)
|
),
|
||||||
|
TeslaChargingOwnershipGraphQlApi.OpenToNonTeslasFilterValue(false)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -67,54 +64,7 @@ class TeslaOwnerAvailabilityDetector(
|
|||||||
)
|
)
|
||||||
).data.charging.site ?: throw AvailabilityDetectorException("no candidates found.")
|
).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 scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
|
||||||
val scV2CCSConnectors = location.chargepoints.filter {
|
val scV2CCSConnectors = location.chargepoints.filter {
|
||||||
it.type in listOf(
|
it.type in listOf(
|
||||||
@@ -212,9 +162,7 @@ class TeslaOwnerAvailabilityDetector(
|
|||||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||||
return when (charger.dataSource) {
|
return when (charger.dataSource) {
|
||||||
"goingelectric" -> charger.network == "Tesla Supercharger"
|
"goingelectric" -> charger.network == "Tesla Supercharger"
|
||||||
"nobil" -> charger.network == "Tesla"
|
|
||||||
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
||||||
"openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla")
|
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,9 +115,9 @@ interface TeslaChargingGuestGraphQlApi {
|
|||||||
val activeOutages: List<Outage>?,
|
val activeOutages: List<Outage>?,
|
||||||
val chargerList: List<ChargerDetail>,
|
val chargerList: List<ChargerDetail>,
|
||||||
val trtId: Long,
|
val trtId: Long,
|
||||||
val maxPowerKw: Int?,
|
val maxPowerKw: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
val pricing: Pricing?,
|
val pricing: Pricing,
|
||||||
val publicStallCount: Int
|
val publicStallCount: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -100,8 +100,7 @@ interface TeslaAuthenticationApi {
|
|||||||
.appendQueryParameter("code_challenge_method", "S256")
|
.appendQueryParameter("code_challenge_method", "S256")
|
||||||
.appendQueryParameter("redirect_uri", "https://auth.tesla.com/void/callback")
|
.appendQueryParameter("redirect_uri", "https://auth.tesla.com/void/callback")
|
||||||
.appendQueryParameter("response_type", "code")
|
.appendQueryParameter("response_type", "code")
|
||||||
.appendQueryParameter("scope", "openid email offline_access phone")
|
.appendQueryParameter("scope", "openid email offline_access")
|
||||||
.appendQueryParameter("is_in_app", "true")
|
|
||||||
.appendQueryParameter("state", "123").build()
|
.appendQueryParameter("state", "123").build()
|
||||||
|
|
||||||
val resultUrlPrefix = "https://auth.tesla.com/void/callback"
|
val resultUrlPrefix = "https://auth.tesla.com/void/callback"
|
||||||
@@ -184,7 +183,7 @@ interface TeslaChargingOwnershipGraphQlApi {
|
|||||||
val userLocation: Coordinate,
|
val userLocation: Coordinate,
|
||||||
val northwestCorner: Coordinate,
|
val northwestCorner: Coordinate,
|
||||||
val southeastCorner: Coordinate,
|
val southeastCorner: Coordinate,
|
||||||
val filters: List<String> = emptyList(),
|
val openToNonTeslasFilter: OpenToNonTeslasFilterValue,
|
||||||
val languageCode: String = "en",
|
val languageCode: String = "en",
|
||||||
val countryCode: String = "US",
|
val countryCode: String = "US",
|
||||||
//val vin: String = "",
|
//val vin: String = "",
|
||||||
|
|||||||
@@ -1,16 +1,99 @@
|
|||||||
package net.vonforst.evmap.api.chargeprice
|
package net.vonforst.evmap.api.chargeprice
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||||
|
import jsonapi.Document
|
||||||
|
import jsonapi.JsonApiFactory
|
||||||
|
import jsonapi.retrofit.DocumentConverterFactory
|
||||||
|
import net.vonforst.evmap.BuildConfig
|
||||||
|
import net.vonforst.evmap.addDebugInterceptors
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
import net.vonforst.evmap.model.ChargeLocation
|
||||||
import java.util.Locale
|
import okhttp3.Cache
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
|
import retrofit2.http.Body
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Header
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
interface ChargepriceApi {
|
interface ChargepriceApi {
|
||||||
|
@POST("charge_prices")
|
||||||
|
suspend fun getChargePrices(
|
||||||
|
@Body @jsonapi.retrofit.Document request: ChargepriceRequest,
|
||||||
|
@Header("Accept-Language") language: String
|
||||||
|
): Document<List<ChargePrice>>
|
||||||
|
|
||||||
|
@GET("vehicles")
|
||||||
|
@jsonapi.retrofit.Document
|
||||||
|
suspend fun getVehicles(): List<ChargepriceCar>
|
||||||
|
|
||||||
|
@GET("tariffs")
|
||||||
|
@jsonapi.retrofit.Document
|
||||||
|
suspend fun getTariffs(): List<ChargepriceTariff>
|
||||||
|
|
||||||
|
@POST("user_feedback")
|
||||||
|
suspend fun userFeedback(@Body @jsonapi.retrofit.Document feedback: ChargepriceUserFeedback)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val cacheSize = 1L * 1024 * 1024 // 1MB
|
||||||
val supportedLanguages = setOf("de", "en", "fr", "nl")
|
val supportedLanguages = setOf("de", "en", "fr", "nl")
|
||||||
|
|
||||||
private val DATA_SOURCE_GOINGELECTRIC = "going_electric"
|
private val DATA_SOURCE_GOINGELECTRIC = "going_electric"
|
||||||
private val DATA_SOURCE_OPENCHARGEMAP = "open_charge_map"
|
private val DATA_SOURCE_OPENCHARGEMAP = "open_charge_map"
|
||||||
|
|
||||||
|
private val jsonApiAdapterFactory = JsonApiFactory.Builder()
|
||||||
|
.addType(ChargepriceRequest::class.java)
|
||||||
|
.addType(ChargepriceTariff::class.java)
|
||||||
|
.addType(ChargepriceBrand::class.java)
|
||||||
|
.addType(ChargePrice::class.java)
|
||||||
|
.addType(ChargepriceCar::class.java)
|
||||||
|
.build()
|
||||||
|
val moshi = Moshi.Builder()
|
||||||
|
.add(jsonApiAdapterFactory)
|
||||||
|
.add(
|
||||||
|
PolymorphicJsonAdapterFactory.of(ChargepriceUserFeedback::class.java, "type")
|
||||||
|
.withSubtype(ChargepriceMissingPriceFeedback::class.java, "missing_price")
|
||||||
|
.withSubtype(ChargepriceWrongPriceFeedback::class.java, "wrong_price")
|
||||||
|
.withSubtype(ChargepriceMissingVehicleFeedback::class.java, "missing_vehicle")
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
apikey: String,
|
||||||
|
baseurl: String = "https://api.chargeprice.app/v1/",
|
||||||
|
context: Context? = null
|
||||||
|
): ChargepriceApi {
|
||||||
|
val client = OkHttpClient.Builder().apply {
|
||||||
|
addInterceptor { chain ->
|
||||||
|
// add API key to every request
|
||||||
|
val original = chain.request()
|
||||||
|
val new = original.newBuilder()
|
||||||
|
.header("API-Key", apikey)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.build()
|
||||||
|
chain.proceed(new)
|
||||||
|
}
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
addDebugInterceptors()
|
||||||
|
}
|
||||||
|
if (context != null) {
|
||||||
|
cache(Cache(context.cacheDir, cacheSize))
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
val retrofit = Retrofit.Builder()
|
||||||
|
.baseUrl(baseurl)
|
||||||
|
.addConverterFactory(DocumentConverterFactory.create())
|
||||||
|
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||||
|
.client(client)
|
||||||
|
.build()
|
||||||
|
return retrofit.create(ChargepriceApi::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getChargepriceLanguage(): String {
|
fun getChargepriceLanguage(): String {
|
||||||
val locale = Locale.getDefault().language
|
val locale = Locale.getDefault().language
|
||||||
return if (supportedLanguages.contains(locale)) {
|
return if (supportedLanguages.contains(locale)) {
|
||||||
|
|||||||
@@ -0,0 +1,466 @@
|
|||||||
|
package net.vonforst.evmap.api.chargeprice
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Patterns
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import jsonapi.*
|
||||||
|
import kotlinx.parcelize.Parceler
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.parcelize.WriteWith
|
||||||
|
import net.vonforst.evmap.R
|
||||||
|
import net.vonforst.evmap.adapter.Equatable
|
||||||
|
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||||
|
import net.vonforst.evmap.model.ChargeLocation
|
||||||
|
import net.vonforst.evmap.model.Chargepoint
|
||||||
|
import net.vonforst.evmap.ui.currency
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.floor
|
||||||
|
|
||||||
|
|
||||||
|
@Resource("charge_price_request")
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ChargepriceRequest(
|
||||||
|
@Json(name = "data_adapter")
|
||||||
|
val dataAdapter: String,
|
||||||
|
val station: ChargepriceStation,
|
||||||
|
val options: ChargepriceOptions,
|
||||||
|
@ToMany("tariffs")
|
||||||
|
val tariffs: List<ChargepriceTariff>? = null,
|
||||||
|
@ToOne("vehicle")
|
||||||
|
val vehicle: ChargepriceCar? = null,
|
||||||
|
@RelationshipsObject var relationships: Relationships? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ChargepriceStation(
|
||||||
|
val longitude: Double,
|
||||||
|
val latitude: Double,
|
||||||
|
val country: String?,
|
||||||
|
val network: String?,
|
||||||
|
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepoint>
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun fromEvmap(
|
||||||
|
charger: ChargeLocation,
|
||||||
|
compatibleConnectors: List<String>,
|
||||||
|
): ChargepriceStation {
|
||||||
|
if (charger.chargepriceData == null) throw IllegalArgumentException()
|
||||||
|
|
||||||
|
val plugTypes =
|
||||||
|
charger.chargepriceData.plugTypes ?: charger.chargepoints.map { it.type }
|
||||||
|
return ChargepriceStation(
|
||||||
|
charger.coordinates.lng,
|
||||||
|
charger.coordinates.lat,
|
||||||
|
charger.chargepriceData.country,
|
||||||
|
charger.chargepriceData.network,
|
||||||
|
charger.chargepoints.zip(plugTypes)
|
||||||
|
.filter { equivalentPlugTypes(it.first.type).any { it in compatibleConnectors } }
|
||||||
|
.map { ChargepriceChargepoint(it.first.power ?: 0.0, it.second) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ChargepriceChargepoint(
|
||||||
|
val power: Double,
|
||||||
|
val plug: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ChargepriceOptions(
|
||||||
|
@Json(name = "max_monthly_fees") val maxMonthlyFees: Double? = null,
|
||||||
|
val energy: Double? = null,
|
||||||
|
val duration: Int? = null,
|
||||||
|
@Json(name = "battery_range") val batteryRange: List<Double>? = null,
|
||||||
|
@Json(name = "car_ac_phases") val carAcPhases: Int? = null,
|
||||||
|
val currency: String? = null,
|
||||||
|
@Json(name = "start_time") val startTime: Int? = null,
|
||||||
|
@Json(name = "allow_unbalanced_load") val allowUnbalancedLoad: Boolean? = null,
|
||||||
|
@Json(name = "provider_customer_tariffs") val providerCustomerTariffs: Boolean? = null,
|
||||||
|
@Json(name = "show_price_unavailable") val showPriceUnavailable: Boolean? = null,
|
||||||
|
@Json(name = "show_all_brand_restricted_tariffs") val showAllBrandRestrictedTariffs: Boolean? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Resource("tariff")
|
||||||
|
@Parcelize
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ChargepriceTariff(
|
||||||
|
@Id val id_: String?,
|
||||||
|
val provider: String,
|
||||||
|
val name: String,
|
||||||
|
@Json(name = "direct_payment")
|
||||||
|
val directPayment: Boolean = false,
|
||||||
|
@Json(name = "provider_customer_tariff")
|
||||||
|
val providerCustomerTariff: Boolean = false,
|
||||||
|
@Json(name = "supported_countries")
|
||||||
|
val supportedCountries: Set<String>,
|
||||||
|
@Json(name = "charge_card_id")
|
||||||
|
val chargeCardId: String?, // GE charge card ID
|
||||||
|
) : Parcelable {
|
||||||
|
val id: String
|
||||||
|
get() = id_!!
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
@Resource("car")
|
||||||
|
@Parcelize
|
||||||
|
data class ChargepriceCar(
|
||||||
|
@Id val id_: String?,
|
||||||
|
val name: String,
|
||||||
|
val brand: String,
|
||||||
|
|
||||||
|
@Json(name = "dc_charge_ports")
|
||||||
|
val dcChargePorts: List<String>,
|
||||||
|
|
||||||
|
@Json(name = "usable_battery_size")
|
||||||
|
val usableBatterySize: Float,
|
||||||
|
|
||||||
|
@Json(name = "ac_max_power")
|
||||||
|
val acMaxPower: Float,
|
||||||
|
|
||||||
|
@Json(name = "dc_max_power")
|
||||||
|
val dcMaxPower: Float?
|
||||||
|
) : Equatable, Parcelable {
|
||||||
|
fun formatSpecs(): String = buildString {
|
||||||
|
append("%.0f kWh".format(usableBatterySize))
|
||||||
|
append(" | ")
|
||||||
|
append("AC %.0f kW".format(acMaxPower))
|
||||||
|
dcMaxPower?.let {
|
||||||
|
append(" | ")
|
||||||
|
append("DC %.0f kW".format(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val acConnectors = listOf(
|
||||||
|
Chargepoint.CEE_BLAU,
|
||||||
|
Chargepoint.CEE_ROT,
|
||||||
|
Chargepoint.SCHUKO,
|
||||||
|
Chargepoint.TYPE_1,
|
||||||
|
Chargepoint.TYPE_2_UNKNOWN,
|
||||||
|
Chargepoint.TYPE_2_SOCKET,
|
||||||
|
Chargepoint.TYPE_2_PLUG
|
||||||
|
)
|
||||||
|
private val plugMapping = mapOf(
|
||||||
|
"ccs" to Chargepoint.CCS_UNKNOWN,
|
||||||
|
"tesla_suc" to Chargepoint.SUPERCHARGER,
|
||||||
|
"tesla_ccs" to Chargepoint.CCS_UNKNOWN,
|
||||||
|
"chademo" to Chargepoint.CHADEMO
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val id: String
|
||||||
|
get() = id_!!
|
||||||
|
|
||||||
|
val compatibleEvmapConnectors: List<String>
|
||||||
|
get() = dcChargePorts.mapNotNull {
|
||||||
|
plugMapping[it]
|
||||||
|
}.plus(acConnectors)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
@Resource("brand")
|
||||||
|
@Parcelize
|
||||||
|
data class ChargepriceBrand(
|
||||||
|
@Id val id: String?
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
@Resource("charge_price")
|
||||||
|
@Parcelize
|
||||||
|
data class ChargePrice(
|
||||||
|
val provider: String,
|
||||||
|
@Json(name = "tariff_name")
|
||||||
|
val tariffName: String,
|
||||||
|
val url: String,
|
||||||
|
@Json(name = "monthly_min_sales")
|
||||||
|
val monthlyMinSales: Double = 0.0,
|
||||||
|
@Json(name = "total_monthly_fee")
|
||||||
|
val totalMonthlyFee: Double = 0.0,
|
||||||
|
@Json(name = "flat_rate")
|
||||||
|
val flatRate: Boolean = false,
|
||||||
|
|
||||||
|
@Json(name = "direct_payment")
|
||||||
|
val directPayment: Boolean = false,
|
||||||
|
|
||||||
|
@Json(name = "provider_customer_tariff")
|
||||||
|
val providerCustomerTariff: Boolean = false,
|
||||||
|
val currency: String,
|
||||||
|
|
||||||
|
@Json(name = "start_time")
|
||||||
|
val startTime: Int = 0,
|
||||||
|
val tags: List<ChargepriceTag>,
|
||||||
|
|
||||||
|
@Json(name = "charge_point_prices")
|
||||||
|
val chargepointPrices: List<ChargepointPrice>,
|
||||||
|
|
||||||
|
@Json(name = "branding")
|
||||||
|
val branding: ChargepriceBranding? = null,
|
||||||
|
|
||||||
|
@RelationshipsObject
|
||||||
|
val relationships: @WriteWith<RelationshipsParceler>() Relationships? = null,
|
||||||
|
) : Equatable, Cloneable, Parcelable {
|
||||||
|
val tariffId: String?
|
||||||
|
get() = (relationships?.get("tariff") as? Relationship.ToOne)?.data?.id
|
||||||
|
|
||||||
|
fun formatMonthlyFees(ctx: Context): String {
|
||||||
|
return listOfNotNull(
|
||||||
|
if (totalMonthlyFee > 0) {
|
||||||
|
ctx.getString(R.string.chargeprice_base_fee, totalMonthlyFee, currency(currency))
|
||||||
|
} else null,
|
||||||
|
if (monthlyMinSales > 0) {
|
||||||
|
ctx.getString(R.string.chargeprice_min_spend, monthlyMinSales, currency(currency))
|
||||||
|
} else null
|
||||||
|
).joinToString(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parceler implementation for the Relationships object.
|
||||||
|
* Note that this ignores certain fields that we don't need (links, meta, etc.)
|
||||||
|
*/
|
||||||
|
internal object RelationshipsParceler : Parceler<Relationships?> {
|
||||||
|
override fun create(parcel: Parcel): Relationships? {
|
||||||
|
if (parcel.readInt() == 0) return null
|
||||||
|
|
||||||
|
val nMembers = parcel.readInt()
|
||||||
|
val members = (0 until nMembers).associate { _ ->
|
||||||
|
val key = parcel.readString()!!
|
||||||
|
val value = if (parcel.readInt() == 0) {
|
||||||
|
val type = parcel.readString()
|
||||||
|
val id = parcel.readString()
|
||||||
|
val ri = if (type != null && id != null) {
|
||||||
|
ResourceIdentifier(type, id)
|
||||||
|
} else null
|
||||||
|
Relationship.ToOne(ri)
|
||||||
|
} else {
|
||||||
|
val size = parcel.readInt()
|
||||||
|
val ris = (0 until size).map { _ ->
|
||||||
|
val type = parcel.readString()!!
|
||||||
|
val id = parcel.readString()!!
|
||||||
|
ResourceIdentifier(type, id)
|
||||||
|
}
|
||||||
|
Relationship.ToMany(ris)
|
||||||
|
}
|
||||||
|
key to value
|
||||||
|
}
|
||||||
|
|
||||||
|
return Relationships(members)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun Relationships?.write(parcel: Parcel, flags: Int) {
|
||||||
|
if (this == null) {
|
||||||
|
parcel.writeInt(0)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
parcel.writeInt(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
parcel.writeInt(members.size)
|
||||||
|
for (member in this.members) {
|
||||||
|
parcel.writeString(member.key)
|
||||||
|
when (val value = member.value) {
|
||||||
|
is Relationship.ToOne -> {
|
||||||
|
parcel.writeInt(0)
|
||||||
|
parcel.writeString(value.data?.type)
|
||||||
|
parcel.writeString(value.data?.id)
|
||||||
|
}
|
||||||
|
is Relationship.ToMany -> {
|
||||||
|
parcel.writeInt(1)
|
||||||
|
parcel.writeInt(value.data.size)
|
||||||
|
for (ri in value.data) {
|
||||||
|
parcel.writeString(ri.type)
|
||||||
|
parcel.writeString(ri.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
@Parcelize
|
||||||
|
data class ChargepointPrice(
|
||||||
|
val power: Double,
|
||||||
|
val plug: String,
|
||||||
|
val price: Double?,
|
||||||
|
@Json(name = "price_distribution") val priceDistribution: PriceDistribution,
|
||||||
|
@Json(name = "blocking_fee_start") val blockingFeeStart: Int?,
|
||||||
|
@Json(name = "no_price_reason") var noPriceReason: String?
|
||||||
|
) : Parcelable {
|
||||||
|
fun formatDistribution(ctx: Context): String {
|
||||||
|
fun percent(value: Double): String {
|
||||||
|
return ctx.getString(R.string.percent_format, value * 100) + "\u00a0"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun time(value: Int): String {
|
||||||
|
val h = floor(value.toDouble() / 60).toInt()
|
||||||
|
val min = ceil(value.toDouble() % 60).toInt()
|
||||||
|
return if (h == 0 && min > 0) "${min}min";
|
||||||
|
// be slightly sloppy (3:01 is shown as 3h) to save space
|
||||||
|
else if (h > 0 && (min == 0 || min == 1)) "${h}h";
|
||||||
|
else "%d:%02dh".format(h, min)
|
||||||
|
}
|
||||||
|
|
||||||
|
// based on https://github.com/chargeprice/chargeprice-client/blob/d420bb2f216d9ad91a210a36dd0859a368a8229a/src/views/priceList.js
|
||||||
|
with(priceDistribution) {
|
||||||
|
return listOfNotNull(
|
||||||
|
if (session != null && session > 0.0) {
|
||||||
|
(if (session < 1) percent(session) else "") + ctx.getString(R.string.chargeprice_session_fee)
|
||||||
|
} else null,
|
||||||
|
if (kwh != null && kwh > 0.0 && !isOnlyKwh) {
|
||||||
|
(if (kwh < 1) percent(kwh) else "") + ctx.getString(R.string.chargeprice_per_kwh)
|
||||||
|
} else null,
|
||||||
|
if (minute != null && minute > 0.0) {
|
||||||
|
(if (minute < 1) percent(minute) else "") + ctx.getString(R.string.chargeprice_per_minute) +
|
||||||
|
if (blockingFeeStart != null) {
|
||||||
|
" (${
|
||||||
|
ctx.getString(
|
||||||
|
R.string.chargeprice_blocking_fee,
|
||||||
|
time(blockingFeeStart)
|
||||||
|
)
|
||||||
|
})"
|
||||||
|
} else ""
|
||||||
|
} else null,
|
||||||
|
if ((minute == null || minute == 0.0) && blockingFeeStart != null) {
|
||||||
|
ctx.getString(R.string.chargeprice_blocking_fee, time(blockingFeeStart))
|
||||||
|
} else null
|
||||||
|
).joinToString(" +\u00a0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
@Parcelize
|
||||||
|
data class ChargepriceBranding(
|
||||||
|
@Json(name = "background_color") val backgroundColor: String,
|
||||||
|
@Json(name = "text_color") val textColor: String,
|
||||||
|
@Json(name = "logo_url") val logoUrl: String
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
@Parcelize
|
||||||
|
data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) :
|
||||||
|
Parcelable {
|
||||||
|
val isOnlyKwh
|
||||||
|
get() = kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
@Parcelize
|
||||||
|
data class ChargepriceTag(val kind: String, val text: String, val url: String?) : Equatable,
|
||||||
|
Parcelable
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ChargepriceMeta(
|
||||||
|
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepointMeta>
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class ChargepriceInclude {
|
||||||
|
@Json(name = "filter")
|
||||||
|
FILTER,
|
||||||
|
@Json(name = "always")
|
||||||
|
ALWAYS,
|
||||||
|
@Json(name = "exclusive")
|
||||||
|
EXCLUSIVE
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
@Parcelize
|
||||||
|
data class ChargepriceRequestTariffMeta(
|
||||||
|
val include: ChargepriceInclude
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class ChargepriceChargepointMeta(
|
||||||
|
val power: Double,
|
||||||
|
val plug: String,
|
||||||
|
val energy: Double,
|
||||||
|
val duration: Double
|
||||||
|
)
|
||||||
|
|
||||||
|
@Resource("user_feedback")
|
||||||
|
sealed class ChargepriceUserFeedback(
|
||||||
|
val notes: String,
|
||||||
|
val email: String,
|
||||||
|
val context: String,
|
||||||
|
val language: String
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
if (email.isBlank() || email.length > 100 || !Patterns.EMAIL_ADDRESS.matcher(email)
|
||||||
|
.matches()
|
||||||
|
) {
|
||||||
|
throw IllegalArgumentException("invalid email")
|
||||||
|
}
|
||||||
|
if (!ChargepriceApi.supportedLanguages.contains(language)) {
|
||||||
|
throw IllegalArgumentException("invalid language")
|
||||||
|
}
|
||||||
|
if (context.length > 500) throw IllegalArgumentException("invalid context")
|
||||||
|
if (notes.length > 1000) throw IllegalArgumentException("invalid notes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
@Resource(type = "missing_price")
|
||||||
|
class ChargepriceMissingPriceFeedback(
|
||||||
|
val tariff: String,
|
||||||
|
val cpo: String,
|
||||||
|
val price: String,
|
||||||
|
@Json(name = "poi_link") val poiLink: String,
|
||||||
|
notes: String,
|
||||||
|
email: String,
|
||||||
|
context: String,
|
||||||
|
language: String
|
||||||
|
) : ChargepriceUserFeedback(notes, email, context, language) {
|
||||||
|
init {
|
||||||
|
if (tariff.isBlank() || tariff.length > 100) throw IllegalArgumentException("invalid tariff")
|
||||||
|
if (cpo.length > 200) throw IllegalArgumentException("invalid cpo")
|
||||||
|
if (price.isBlank() || price.length > 100) throw IllegalArgumentException("invalid price")
|
||||||
|
if (poiLink.isBlank() || poiLink.length > 200) throw IllegalArgumentException("invalid poiLink")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
@Resource(type = "wrong_price")
|
||||||
|
class ChargepriceWrongPriceFeedback(
|
||||||
|
val tariff: String,
|
||||||
|
val cpo: String,
|
||||||
|
@Json(name = "displayed_price") val displayedPrice: String,
|
||||||
|
@Json(name = "actual_price") val actualPrice: String,
|
||||||
|
@Json(name = "poi_link") val poiLink: String,
|
||||||
|
notes: String,
|
||||||
|
email: String,
|
||||||
|
context: String,
|
||||||
|
language: String,
|
||||||
|
) : ChargepriceUserFeedback(notes, email, context, language) {
|
||||||
|
init {
|
||||||
|
if (tariff.length > 100) throw IllegalArgumentException("invalid tariff")
|
||||||
|
if (cpo.length > 200) throw IllegalArgumentException("invalid cpo")
|
||||||
|
if (displayedPrice.length > 100) throw IllegalArgumentException("invalid displayedPrice")
|
||||||
|
if (actualPrice.length > 100) throw IllegalArgumentException("invalid actualPrice")
|
||||||
|
if (poiLink.length > 200) throw IllegalArgumentException("invalid poiLink")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
@Resource(type = "missing_vehicle")
|
||||||
|
class ChargepriceMissingVehicleFeedback(
|
||||||
|
val brand: String,
|
||||||
|
val model: String,
|
||||||
|
notes: String,
|
||||||
|
email: String,
|
||||||
|
context: String,
|
||||||
|
language: String,
|
||||||
|
) : ChargepriceUserFeedback(notes, email, context, language) {
|
||||||
|
init {
|
||||||
|
if (brand.length > 100) throw IllegalArgumentException("invalid brand")
|
||||||
|
if (model.length > 100) throw IllegalArgumentException("invalid model")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package net.vonforst.evmap.api.fronyx
|
package net.vonforst.evmap.api.fronyx
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import com.squareup.moshi.JsonDataException
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
|
import net.vonforst.evmap.api.availability.AvailabilityDetectorException
|
||||||
|
import net.vonforst.evmap.api.availability.AvailabilityRepository
|
||||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||||
import net.vonforst.evmap.api.nameForPlugType
|
import net.vonforst.evmap.api.nameForPlugType
|
||||||
@@ -10,6 +13,8 @@ import net.vonforst.evmap.model.ChargeLocation
|
|||||||
import net.vonforst.evmap.model.Chargepoint
|
import net.vonforst.evmap.model.Chargepoint
|
||||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||||
import net.vonforst.evmap.viewmodel.Resource
|
import net.vonforst.evmap.viewmodel.Resource
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import java.io.IOException
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalTime
|
import java.time.LocalTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@@ -50,9 +55,9 @@ class PredictionRepository(private val context: Context) {
|
|||||||
evseIds: Map<Chargepoint, List<String>>,
|
evseIds: Map<Chargepoint, List<String>>,
|
||||||
filteredConnectors: Set<String>?
|
filteredConnectors: Set<String>?
|
||||||
): Resource<List<FronyxEvseIdResponse>> {
|
): Resource<List<FronyxEvseIdResponse>> {
|
||||||
return Resource.success(null)
|
if (!prefs.predictionEnabled) return Resource.success(null)
|
||||||
|
|
||||||
/*val allEvseIds =
|
val allEvseIds =
|
||||||
evseIds.filterKeys {
|
evseIds.filterKeys {
|
||||||
FronyxApi.isChargepointSupported(charger, it) &&
|
FronyxApi.isChargepointSupported(charger, it) &&
|
||||||
filteredConnectors?.let { filtered ->
|
filteredConnectors?.let { filtered ->
|
||||||
@@ -84,7 +89,7 @@ class PredictionRepository(private val context: Context) {
|
|||||||
// malformed JSON response from fronyx API
|
// malformed JSON response from fronyx API
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return Resource.error(e.message, null)
|
return Resource.error(e.message, null)
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildPredictionGraph(
|
private fun buildPredictionGraph(
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import net.vonforst.evmap.addDebugInterceptors
|
|||||||
import net.vonforst.evmap.api.ChargepointApi
|
import net.vonforst.evmap.api.ChargepointApi
|
||||||
import net.vonforst.evmap.api.ChargepointList
|
import net.vonforst.evmap.api.ChargepointList
|
||||||
import net.vonforst.evmap.api.FiltersSQLQuery
|
import net.vonforst.evmap.api.FiltersSQLQuery
|
||||||
import net.vonforst.evmap.api.FullDownloadResult
|
|
||||||
import net.vonforst.evmap.api.StringProvider
|
import net.vonforst.evmap.api.StringProvider
|
||||||
import net.vonforst.evmap.api.mapPower
|
import net.vonforst.evmap.api.mapPower
|
||||||
import net.vonforst.evmap.api.mapPowerInverse
|
import net.vonforst.evmap.api.mapPowerInverse
|
||||||
@@ -160,12 +159,6 @@ class GoingElectricApiWrapper(
|
|||||||
override val name = "GoingElectric.de"
|
override val name = "GoingElectric.de"
|
||||||
override val id = "goingelectric"
|
override val id = "goingelectric"
|
||||||
override val cacheLimit = Duration.ofDays(1)
|
override val cacheLimit = Duration.ofDays(1)
|
||||||
override val supportsOnlineQueries = true
|
|
||||||
override val supportsFullDownload = false
|
|
||||||
|
|
||||||
override suspend fun fullDownload(): FullDownloadResult<GEReferenceData> {
|
|
||||||
throw NotImplementedError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getChargepoints(
|
override suspend fun getChargepoints(
|
||||||
referenceData: ReferenceData,
|
referenceData: ReferenceData,
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ data class GEChargeLocation(
|
|||||||
address.convert(),
|
address.convert(),
|
||||||
chargepoints.map { it.convert() },
|
chargepoints.map { it.convert() },
|
||||||
network,
|
network,
|
||||||
"https://www.goingelectric.de/",
|
|
||||||
"https:${url}",
|
"https:${url}",
|
||||||
"https:${url}edit/",
|
"https:${url}edit/",
|
||||||
faultReport?.convert(),
|
faultReport?.convert(),
|
||||||
@@ -89,7 +88,6 @@ data class GEChargeLocation(
|
|||||||
locationDescription,
|
locationDescription,
|
||||||
photos?.map { it.convert(apikey) },
|
photos?.map { it.convert(apikey) },
|
||||||
chargecards?.map { it.convert() },
|
chargecards?.map { it.convert() },
|
||||||
null,
|
|
||||||
openinghours?.convert(),
|
openinghours?.convert(),
|
||||||
cost?.convert(),
|
cost?.convert(),
|
||||||
null,
|
null,
|
||||||
@@ -210,7 +208,7 @@ data class GEChargepoint(val type: String, val power: Double, val count: Int) {
|
|||||||
return when (type) {
|
return when (type) {
|
||||||
Chargepoint.TYPE_1 -> "Typ1"
|
Chargepoint.TYPE_1 -> "Typ1"
|
||||||
Chargepoint.TYPE_2_UNKNOWN -> "Typ2"
|
Chargepoint.TYPE_2_UNKNOWN -> "Typ2"
|
||||||
Chargepoint.TYPE_3C -> "Typ3"
|
Chargepoint.TYPE_3 -> "Typ3"
|
||||||
Chargepoint.CCS_UNKNOWN -> "CCS"
|
Chargepoint.CCS_UNKNOWN -> "CCS"
|
||||||
Chargepoint.CCS_TYPE_2 -> "Typ2"
|
Chargepoint.CCS_TYPE_2 -> "Typ2"
|
||||||
Chargepoint.SCHUKO -> "Schuko"
|
Chargepoint.SCHUKO -> "Schuko"
|
||||||
@@ -227,7 +225,7 @@ data class GEChargepoint(val type: String, val power: Double, val count: Int) {
|
|||||||
return when (type) {
|
return when (type) {
|
||||||
"Typ1" -> Chargepoint.TYPE_1
|
"Typ1" -> Chargepoint.TYPE_1
|
||||||
"Typ2" -> Chargepoint.TYPE_2_UNKNOWN
|
"Typ2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||||
"Typ3" -> Chargepoint.TYPE_3C
|
"Typ3" -> Chargepoint.TYPE_3
|
||||||
"Tesla Supercharger CCS" -> Chargepoint.CCS_UNKNOWN
|
"Tesla Supercharger CCS" -> Chargepoint.CCS_UNKNOWN
|
||||||
"CCS" -> Chargepoint.CCS_UNKNOWN
|
"CCS" -> Chargepoint.CCS_UNKNOWN
|
||||||
"Schuko" -> Chargepoint.SCHUKO
|
"Schuko" -> Chargepoint.SCHUKO
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,354 +0,0 @@
|
|||||||
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("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")
|
|
||||||
}
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
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) {
|
|
||||||
"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 evseUId = if (attribs["27"]?.attrVal is String) listOf(attribs["27"]?.attrVal.toString()) 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, evseUId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ import net.vonforst.evmap.addDebugInterceptors
|
|||||||
import net.vonforst.evmap.api.ChargepointApi
|
import net.vonforst.evmap.api.ChargepointApi
|
||||||
import net.vonforst.evmap.api.ChargepointList
|
import net.vonforst.evmap.api.ChargepointList
|
||||||
import net.vonforst.evmap.api.FiltersSQLQuery
|
import net.vonforst.evmap.api.FiltersSQLQuery
|
||||||
import net.vonforst.evmap.api.FullDownloadResult
|
|
||||||
import net.vonforst.evmap.api.StringProvider
|
import net.vonforst.evmap.api.StringProvider
|
||||||
import net.vonforst.evmap.api.mapPower
|
import net.vonforst.evmap.api.mapPower
|
||||||
import net.vonforst.evmap.api.mapPowerInverse
|
import net.vonforst.evmap.api.mapPowerInverse
|
||||||
@@ -131,12 +130,6 @@ class OpenChargeMapApiWrapper(
|
|||||||
|
|
||||||
override val name = "OpenChargeMap.org"
|
override val name = "OpenChargeMap.org"
|
||||||
override val id = "openchargemap"
|
override val id = "openchargemap"
|
||||||
override val supportsOnlineQueries = true
|
|
||||||
override val supportsFullDownload = false
|
|
||||||
|
|
||||||
override suspend fun fullDownload(): FullDownloadResult<OCMReferenceData> {
|
|
||||||
throw NotImplementedError()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) =
|
private fun formatMultipleChoice(value: MultipleChoiceFilterValue?) =
|
||||||
if (value == null || value.all) null else value.values.joinToString(",")
|
if (value == null || value.all) null else value.values.joinToString(",")
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ data class OCMChargepoint(
|
|||||||
addressInfo.toAddress(refData),
|
addressInfo.toAddress(refData),
|
||||||
connections.map { it.convert(refData) },
|
connections.map { it.convert(refData) },
|
||||||
operatorInfo?.title ?: refData.operators.find { it.id == operatorId }?.title,
|
operatorInfo?.title ?: refData.operators.find { it.id == operatorId }?.title,
|
||||||
"https://openchargemap.org/",
|
|
||||||
"https://map.openchargemap.io/?id=$id",
|
"https://map.openchargemap.io/?id=$id",
|
||||||
"https://map.openchargemap.io/?id=$id",
|
"https://map.openchargemap.io/?id=$id",
|
||||||
convertFaultReport(),
|
convertFaultReport(),
|
||||||
@@ -77,8 +76,7 @@ data class OCMChargepoint(
|
|||||||
mediaItems?.mapNotNull { it.convert() },
|
mediaItems?.mapNotNull { it.convert() },
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
cost?.let { Cost(descriptionShort = it) },
|
||||||
cost?.takeIf { it.isNotBlank() }.let { Cost(descriptionShort = it) },
|
|
||||||
dataProvider?.let { "© ${it.title}" + if (it.license != null) ". ${it.license}" else "" },
|
dataProvider?.let { "© ${it.title}" + if (it.license != null) ". ${it.license}" else "" },
|
||||||
ChargepriceData(
|
ChargepriceData(
|
||||||
addressInfo.countryISOCode(refData),
|
addressInfo.countryISOCode(refData),
|
||||||
@@ -182,8 +180,8 @@ data class OCMConnection(
|
|||||||
25L -> Chargepoint.TYPE_2_SOCKET
|
25L -> Chargepoint.TYPE_2_SOCKET
|
||||||
1036L -> Chargepoint.TYPE_2_PLUG
|
1036L -> Chargepoint.TYPE_2_PLUG
|
||||||
1L -> Chargepoint.TYPE_1
|
1L -> Chargepoint.TYPE_1
|
||||||
36L -> Chargepoint.TYPE_3A
|
36L -> Chargepoint.TYPE_3
|
||||||
26L -> Chargepoint.TYPE_3C
|
26L -> Chargepoint.TYPE_3
|
||||||
else -> title ?: ""
|
else -> title ?: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
package net.vonforst.evmap.api.openstreetmap
|
|
||||||
|
|
||||||
import com.squareup.moshi.FromJson
|
|
||||||
import com.squareup.moshi.JsonReader
|
|
||||||
import com.squareup.moshi.Moshi
|
|
||||||
import com.squareup.moshi.ToJson
|
|
||||||
import com.squareup.moshi.rawType
|
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import retrofit2.Converter
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
import java.time.Instant
|
|
||||||
import kotlin.math.floor
|
|
||||||
|
|
||||||
internal class InstantAdapter {
|
|
||||||
@FromJson
|
|
||||||
fun fromJson(value: Double?): Instant? = value?.let {
|
|
||||||
val seconds = floor(it).toLong()
|
|
||||||
val nanos = ((value - seconds) * 1e9).toLong()
|
|
||||||
Instant.ofEpochSecond(seconds, nanos)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ToJson
|
|
||||||
fun toJson(value: Instant?): Double? = value?.let {
|
|
||||||
it.epochSecond.toDouble() + it.nano / 1e9
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class OSMConverterFactory(val moshi: Moshi) : Converter.Factory() {
|
|
||||||
override fun responseBodyConverter(
|
|
||||||
type: Type,
|
|
||||||
annotations: Array<out Annotation>,
|
|
||||||
retrofit: Retrofit
|
|
||||||
): Converter<ResponseBody, *>? {
|
|
||||||
if (type.rawType != OSMDocument::class.java) return null
|
|
||||||
|
|
||||||
val instantAdapter = moshi.adapter(Instant::class.java)
|
|
||||||
val osmChargingStationAdapter = moshi.adapter(OSMChargingStation::class.java)
|
|
||||||
val longAdapter = moshi.adapter(Long::class.java)
|
|
||||||
return Converter<ResponseBody, OSMDocument> { body ->
|
|
||||||
val reader = JsonReader.of(body.source())
|
|
||||||
reader.beginObject()
|
|
||||||
|
|
||||||
var timestamp: Instant? = null
|
|
||||||
var doc: Sequence<OSMChargingStation>? = null
|
|
||||||
var count: Long? = null
|
|
||||||
while (reader.hasNext()) {
|
|
||||||
when (reader.nextName()) {
|
|
||||||
"timestamp" -> timestamp = instantAdapter.fromJson(reader)!!
|
|
||||||
"count" -> count = longAdapter.fromJson(reader)!!
|
|
||||||
"elements" -> {
|
|
||||||
doc = sequence {
|
|
||||||
reader.beginArray()
|
|
||||||
while (reader.hasNext()) {
|
|
||||||
yield(osmChargingStationAdapter.fromJson(reader)!!)
|
|
||||||
}
|
|
||||||
reader.endArray()
|
|
||||||
reader.close()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OSMDocument(timestamp!!, count!!, doc!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
package net.vonforst.evmap.api.openstreetmap
|
|
||||||
|
|
||||||
import android.database.DatabaseUtils
|
|
||||||
import com.car2go.maps.model.LatLng
|
|
||||||
import com.car2go.maps.model.LatLngBounds
|
|
||||||
import com.squareup.moshi.Moshi
|
|
||||||
import net.vonforst.evmap.BuildConfig
|
|
||||||
import net.vonforst.evmap.R
|
|
||||||
import net.vonforst.evmap.addDebugInterceptors
|
|
||||||
import net.vonforst.evmap.api.ChargepointApi
|
|
||||||
import net.vonforst.evmap.api.ChargepointList
|
|
||||||
import net.vonforst.evmap.api.FiltersSQLQuery
|
|
||||||
import net.vonforst.evmap.api.FullDownloadResult
|
|
||||||
import net.vonforst.evmap.api.StringProvider
|
|
||||||
import net.vonforst.evmap.api.mapPower
|
|
||||||
import net.vonforst.evmap.api.mapPowerInverse
|
|
||||||
import net.vonforst.evmap.api.nameForPlugType
|
|
||||||
import net.vonforst.evmap.api.openchargemap.ZonedDateTimeAdapter
|
|
||||||
import net.vonforst.evmap.api.powerSteps
|
|
||||||
import net.vonforst.evmap.model.BooleanFilter
|
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
|
||||||
import net.vonforst.evmap.model.Chargepoint
|
|
||||||
import net.vonforst.evmap.model.Filter
|
|
||||||
import net.vonforst.evmap.model.FilterValue
|
|
||||||
import net.vonforst.evmap.model.FilterValues
|
|
||||||
import net.vonforst.evmap.model.MultipleChoiceFilter
|
|
||||||
import net.vonforst.evmap.model.ReferenceData
|
|
||||||
import net.vonforst.evmap.model.SliderFilter
|
|
||||||
import net.vonforst.evmap.model.getBooleanValue
|
|
||||||
import net.vonforst.evmap.model.getMultipleChoiceValue
|
|
||||||
import net.vonforst.evmap.model.getSliderValue
|
|
||||||
import net.vonforst.evmap.viewmodel.Resource
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import retrofit2.Response
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import java.io.IOException
|
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
interface OpenStreetMapApi {
|
|
||||||
@GET("charging-stations-osm.json")
|
|
||||||
suspend fun getAllChargingStations(): Response<OSMDocument>
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val moshi = Moshi.Builder()
|
|
||||||
.add(ZonedDateTimeAdapter())
|
|
||||||
.add(InstantAdapter())
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun create(
|
|
||||||
baseurl: String = "https://osm.ev-map.app/"
|
|
||||||
): OpenStreetMapApi {
|
|
||||||
val client = OkHttpClient.Builder().apply {
|
|
||||||
if (BuildConfig.DEBUG) addDebugInterceptors()
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val retrofit = Retrofit.Builder()
|
|
||||||
.baseUrl(baseurl)
|
|
||||||
.addConverterFactory(OSMConverterFactory(moshi))
|
|
||||||
.client(client)
|
|
||||||
.build()
|
|
||||||
return retrofit.create(OpenStreetMapApi::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class OpenStreetMapApiWrapper(baseurl: String = "https://osm.ev-map.app/") :
|
|
||||||
ChargepointApi<OSMReferenceData> {
|
|
||||||
override val name = "OpenStreetMap"
|
|
||||||
override val id = "openstreetmap"
|
|
||||||
override val cacheLimit = Duration.ofDays(300L)
|
|
||||||
override val supportsOnlineQueries = false
|
|
||||||
override val supportsFullDownload = true
|
|
||||||
|
|
||||||
val api = OpenStreetMapApi.create(baseurl)
|
|
||||||
|
|
||||||
override suspend fun getChargepoints(
|
|
||||||
referenceData: ReferenceData,
|
|
||||||
bounds: LatLngBounds,
|
|
||||||
zoom: Float,
|
|
||||||
useClustering: Boolean,
|
|
||||||
filters: FilterValues?
|
|
||||||
): Resource<ChargepointList> {
|
|
||||||
throw NotImplementedError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getChargepointsRadius(
|
|
||||||
referenceData: ReferenceData,
|
|
||||||
location: LatLng,
|
|
||||||
radius: Int,
|
|
||||||
zoom: Float,
|
|
||||||
useClustering: Boolean,
|
|
||||||
filters: FilterValues?
|
|
||||||
): Resource<ChargepointList> {
|
|
||||||
throw NotImplementedError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getChargepointDetail(
|
|
||||||
referenceData: ReferenceData,
|
|
||||||
id: Long
|
|
||||||
): Resource<ChargeLocation> {
|
|
||||||
throw NotImplementedError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getReferenceData(): Resource<OSMReferenceData> {
|
|
||||||
throw NotImplementedError()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFilters(
|
|
||||||
referenceData: ReferenceData,
|
|
||||||
sp: StringProvider
|
|
||||||
): List<Filter<FilterValue>> {
|
|
||||||
|
|
||||||
val plugs = listOf(
|
|
||||||
Chargepoint.TYPE_1,
|
|
||||||
Chargepoint.CCS_TYPE_1,
|
|
||||||
Chargepoint.TYPE_2_SOCKET,
|
|
||||||
Chargepoint.TYPE_2_PLUG,
|
|
||||||
Chargepoint.CCS_TYPE_2,
|
|
||||||
Chargepoint.CHADEMO,
|
|
||||||
Chargepoint.SUPERCHARGER,
|
|
||||||
Chargepoint.CEE_BLAU,
|
|
||||||
Chargepoint.CEE_ROT,
|
|
||||||
Chargepoint.SCHUKO
|
|
||||||
)
|
|
||||||
val plugMap = plugs.associateWith { plug ->
|
|
||||||
nameForPlugType(sp, plug)
|
|
||||||
}
|
|
||||||
|
|
||||||
val refData = referenceData as OSMReferenceData
|
|
||||||
val networkMap = refData.networks.associateWith { it }
|
|
||||||
|
|
||||||
return listOf(
|
|
||||||
BooleanFilter(sp.getString(R.string.filter_free), "freecharging"),
|
|
||||||
BooleanFilter(sp.getString(R.string.filter_free_parking), "freeparking"),
|
|
||||||
BooleanFilter(sp.getString(R.string.filter_open_247), "open_247"),
|
|
||||||
SliderFilter(
|
|
||||||
sp.getString(R.string.filter_min_power), "min_power",
|
|
||||||
powerSteps.size - 1,
|
|
||||||
mapping = ::mapPower,
|
|
||||||
inverseMapping = ::mapPowerInverse,
|
|
||||||
unit = "kW"
|
|
||||||
),
|
|
||||||
MultipleChoiceFilter(
|
|
||||||
sp.getString(R.string.filter_connectors), "connectors",
|
|
||||||
plugMap,
|
|
||||||
commonChoices = setOf(
|
|
||||||
Chargepoint.TYPE_1,
|
|
||||||
Chargepoint.TYPE_2_SOCKET,
|
|
||||||
Chargepoint.TYPE_2_PLUG,
|
|
||||||
Chargepoint.CCS_TYPE_1,
|
|
||||||
Chargepoint.CCS_TYPE_2,
|
|
||||||
Chargepoint.CHADEMO
|
|
||||||
),
|
|
||||||
manyChoices = true
|
|
||||||
),
|
|
||||||
MultipleChoiceFilter(
|
|
||||||
sp.getString(R.string.filter_networks), "networks",
|
|
||||||
networkMap, manyChoices = true
|
|
||||||
),
|
|
||||||
SliderFilter(
|
|
||||||
sp.getString(R.string.filter_min_connectors),
|
|
||||||
"min_connectors",
|
|
||||||
10,
|
|
||||||
min = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convertFiltersToSQL(
|
|
||||||
filters: FilterValues,
|
|
||||||
referenceData: ReferenceData
|
|
||||||
): FiltersSQLQuery {
|
|
||||||
if (filters.isEmpty()) return FiltersSQLQuery("", false, false)
|
|
||||||
var requiresChargepointQuery = false
|
|
||||||
|
|
||||||
val result = StringBuilder()
|
|
||||||
if (filters.getBooleanValue("freecharging") == true) {
|
|
||||||
result.append(" AND freecharging IS 1")
|
|
||||||
}
|
|
||||||
if (filters.getBooleanValue("freeparking") == true) {
|
|
||||||
result.append(" AND freeparking IS 1")
|
|
||||||
}
|
|
||||||
if (filters.getBooleanValue("open_247") == true) {
|
|
||||||
result.append(" AND twentyfourSeven IS 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
val minPower = filters.getSliderValue("min_power")
|
|
||||||
if (minPower != null && minPower > 0) {
|
|
||||||
result.append(" AND json_extract(cp.value, '$.power') >= ${minPower}")
|
|
||||||
requiresChargepointQuery = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val connectors = filters.getMultipleChoiceValue("connectors")
|
|
||||||
if (connectors != null && !connectors.all) {
|
|
||||||
val connectorsList = if (connectors.values.size == 0) {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
connectors.values.joinToString(",") {
|
|
||||||
DatabaseUtils.sqlEscapeString(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.append(" AND json_extract(cp.value, '$.type') IN (${connectorsList})")
|
|
||||||
requiresChargepointQuery = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val minConnectors = filters.getSliderValue("min_connectors")
|
|
||||||
if (minConnectors != null && minConnectors > 1) {
|
|
||||||
result.append(" GROUP BY ChargeLocation.id HAVING SUM(json_extract(cp.value, '$.count')) >= $minConnectors")
|
|
||||||
requiresChargepointQuery = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val networks = filters.getMultipleChoiceValue("networks")
|
|
||||||
if (networks != null && !networks.all) {
|
|
||||||
val networksList = if (networks.values.size == 0) {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
networks.values.joinToString(",") { DatabaseUtils.sqlEscapeString(it) }
|
|
||||||
}
|
|
||||||
result.append(" AND network IN (${networksList})")
|
|
||||||
}
|
|
||||||
|
|
||||||
return FiltersSQLQuery(result.toString(), requiresChargepointQuery, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun fullDownload(): FullDownloadResult<OSMReferenceData> {
|
|
||||||
val response = api.getAllChargingStations()
|
|
||||||
if (!response.isSuccessful) {
|
|
||||||
throw IOException(response.message())
|
|
||||||
} else {
|
|
||||||
val body = response.body()!!
|
|
||||||
return OSMFullDownloadResult(body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class OSMReferenceData(val networks: List<String>) : ReferenceData()
|
|
||||||
|
|
||||||
class OSMFullDownloadResult(private val body: OSMDocument) : FullDownloadResult<OSMReferenceData> {
|
|
||||||
private var downloadProgress = 0f
|
|
||||||
private var refData: OSMReferenceData? = null
|
|
||||||
|
|
||||||
override val chargers: Sequence<ChargeLocation>
|
|
||||||
get() {
|
|
||||||
val time = body.timestamp
|
|
||||||
val networks = mutableListOf<String>()
|
|
||||||
|
|
||||||
return sequence {
|
|
||||||
body.elements.forEachIndexed { i, it ->
|
|
||||||
val charger = it.convert(time)
|
|
||||||
yield(charger)
|
|
||||||
downloadProgress = i.toFloat() / body.count
|
|
||||||
charger.network?.let { networks.add(it) }
|
|
||||||
}
|
|
||||||
refData = OSMReferenceData(networks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override val progress: Float
|
|
||||||
get() = downloadProgress
|
|
||||||
override val referenceData: OSMReferenceData
|
|
||||||
get() = refData
|
|
||||||
?: throw UnsupportedOperationException("referenceData is only available once download is complete")
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,7 @@ package net.vonforst.evmap.api.openstreetmap
|
|||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import kotlinx.parcelize.Parcelize
|
import net.vonforst.evmap.model.*
|
||||||
import net.vonforst.evmap.model.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.OpeningHours
|
|
||||||
import okhttp3.internal.immutableListOf
|
import okhttp3.internal.immutableListOf
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
@@ -47,7 +40,6 @@ private val SOCKET_TYPES = immutableListOf(
|
|||||||
// Tesla
|
// Tesla
|
||||||
OsmSocket("tesla_standard", null),
|
OsmSocket("tesla_standard", null),
|
||||||
OsmSocket("tesla_supercharger", Chargepoint.SUPERCHARGER),
|
OsmSocket("tesla_supercharger", Chargepoint.SUPERCHARGER),
|
||||||
OsmSocket("tesla_supercharger_ccs", Chargepoint.CCS_UNKNOWN),
|
|
||||||
|
|
||||||
// CEE
|
// CEE
|
||||||
OsmSocket("cee_blue", Chargepoint.CEE_BLAU), // Also known as "caravan socket"
|
OsmSocket("cee_blue", Chargepoint.CEE_BLAU), // Also known as "caravan socket"
|
||||||
@@ -66,12 +58,6 @@ private val SOCKET_TYPES = immutableListOf(
|
|||||||
OsmSocket("sev1011_t25", null),
|
OsmSocket("sev1011_t25", null),
|
||||||
)
|
)
|
||||||
|
|
||||||
data class OSMDocument(
|
|
||||||
val timestamp: Instant,
|
|
||||||
val count: Long,
|
|
||||||
val elements: Sequence<OSMChargingStation>
|
|
||||||
)
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class OSMChargingStation(
|
data class OSMChargingStation(
|
||||||
// Unique numeric ID
|
// Unique numeric ID
|
||||||
@@ -101,10 +87,9 @@ data class OSMChargingStation(
|
|||||||
"openstreetmap",
|
"openstreetmap",
|
||||||
getName(),
|
getName(),
|
||||||
Coordinate(lat, lon),
|
Coordinate(lat, lon),
|
||||||
getAddress(),
|
null, // TODO: Can we determine this with overpass?
|
||||||
getChargepoints(),
|
getChargepoints(),
|
||||||
tags["network"],
|
tags["network"],
|
||||||
"https://www.openstreetmap.org/",
|
|
||||||
"https://www.openstreetmap.org/node/$id",
|
"https://www.openstreetmap.org/node/$id",
|
||||||
"https://www.openstreetmap.org/edit?node=$id",
|
"https://www.openstreetmap.org/edit?node=$id",
|
||||||
null,
|
null,
|
||||||
@@ -114,7 +99,6 @@ data class OSMChargingStation(
|
|||||||
tags["description"],
|
tags["description"],
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
getPhotos(),
|
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
getOpeningHours(),
|
getOpeningHours(),
|
||||||
@@ -122,24 +106,11 @@ data class OSMChargingStation(
|
|||||||
"© OpenStreetMap contributors",
|
"© OpenStreetMap contributors",
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
tags["website"],
|
null,
|
||||||
dataFetchTimestamp,
|
dataFetchTimestamp,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getAddress(): Address? {
|
|
||||||
val city = tags["addr:city"]
|
|
||||||
val country = tags["addr:country"]
|
|
||||||
val postcode = tags["addr:postcode"]
|
|
||||||
val street = tags["addr:street"]
|
|
||||||
val housenumber = tags["addr:housenumber"] ?: tags["addr:housename"]
|
|
||||||
return if (listOf(city, country, postcode, street, housenumber).any { it != null }) {
|
|
||||||
Address(city, country, postcode, "$street $housenumber")
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the name for this charging station.
|
* Return the name for this charging station.
|
||||||
*/
|
*/
|
||||||
@@ -194,7 +165,7 @@ data class OSMChargingStation(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCost(): Cost {
|
private fun getCost(): Cost? {
|
||||||
val freecharging = when (tags["fee"]?.lowercase()) {
|
val freecharging = when (tags["fee"]?.lowercase()) {
|
||||||
"yes", "y" -> false
|
"yes", "y" -> false
|
||||||
"no", "n" -> true
|
"no", "n" -> true
|
||||||
@@ -205,28 +176,7 @@ data class OSMChargingStation(
|
|||||||
"yes", "y", "interval" -> false
|
"yes", "y", "interval" -> false
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
val description = listOfNotNull(tags["charge"], tags["charge:conditional"]).ifEmpty { null }
|
return Cost(freecharging, freeparking)
|
||||||
?.joinToString("\n")
|
|
||||||
return Cost(freecharging, freeparking, null, description)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPhotos(): List<ChargerPhoto> {
|
|
||||||
val photos = mutableListOf<ChargerPhoto>()
|
|
||||||
for (i in -1..9) {
|
|
||||||
val url = tags["image" + if (i >= 0) ":$i" else ""]
|
|
||||||
if (url != null) {
|
|
||||||
if (url.startsWith("https://i.imgur.com")) {
|
|
||||||
ImgurChargerPhoto.create(url)?.let { photos.add(it) }
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
TODO: Imgur seems to be by far the most common image hoster (650 images),
|
|
||||||
followed by Mapillary (450, requires an API key to retrieve images)
|
|
||||||
Other than that, we have Google Photos, Wikimedia Commons (100-150 images each).
|
|
||||||
And there are some other links to various sites, but not all are valid links pointing directly to a JPEG file...
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return photos
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -245,46 +195,10 @@ data class OSMChargingStation(
|
|||||||
if (rawOutput == null) {
|
if (rawOutput == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val kwPattern = Regex("([0-9.,]+)\\s*(kW|kVA)", setOf(RegexOption.IGNORE_CASE))
|
val pattern = Regex("([0-9.,]+)\\s*(kW|kVA)", setOf(RegexOption.IGNORE_CASE))
|
||||||
kwPattern.matchEntire(rawOutput)?.let { matchResult ->
|
val matchResult = pattern.matchEntire(rawOutput) ?: return null
|
||||||
val numberString = matchResult.groupValues[1].replace(',', '.')
|
val numberString = matchResult.groupValues[1].replace(',', '.')
|
||||||
return numberString.toDoubleOrNull()
|
return numberString.toDoubleOrNull()
|
||||||
}
|
|
||||||
|
|
||||||
val numberPattern = Regex("([0-9.,]+)")
|
|
||||||
numberPattern.matchEntire(rawOutput)?.let { matchResult ->
|
|
||||||
// just a number is mapped without unit
|
|
||||||
val numberString = matchResult.groupValues[1].replace(',', '.')
|
|
||||||
val number = numberString.toDoubleOrNull()
|
|
||||||
return number?.let {
|
|
||||||
// assume kW if the number is < 1000, otherwise assume W and convert to kW
|
|
||||||
if (number < 1000) number else number / 1000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
class ImgurChargerPhoto(override val id: String) : ChargerPhoto(id) {
|
|
||||||
override fun getUrl(height: Int?, width: Int?, size: Int?, allowOriginal: Boolean): String {
|
|
||||||
return if (allowOriginal) {
|
|
||||||
"https://i.imgur.com/$id.jpg"
|
|
||||||
} else {
|
|
||||||
val value = width ?: size ?: height
|
|
||||||
"https://i.imgur.com/${id}_d.jpg?maxwidth=$value"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val regex = Regex("https?://i.imgur.com/([\\w\\d]+)(?:_d)?.(?:webp|jpg)")
|
|
||||||
|
|
||||||
fun create(url: String): ImgurChargerPhoto? {
|
|
||||||
val id = regex.find(url)?.groups?.get(1)?.value
|
|
||||||
return id?.let { ImgurChargerPhoto(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,6 @@ import androidx.lifecycle.DefaultLifecycleObserver
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.car2go.maps.model.LatLng
|
import com.car2go.maps.model.LatLng
|
||||||
import net.vonforst.evmap.R
|
import net.vonforst.evmap.R
|
||||||
import net.vonforst.evmap.autocomplete.PlaceWithBounds
|
|
||||||
import net.vonforst.evmap.location.FusionEngine
|
import net.vonforst.evmap.location.FusionEngine
|
||||||
import net.vonforst.evmap.location.LocationEngine
|
import net.vonforst.evmap.location.LocationEngine
|
||||||
import net.vonforst.evmap.location.Priority
|
import net.vonforst.evmap.location.Priority
|
||||||
@@ -109,7 +108,6 @@ class CarAppService : androidx.car.app.CarAppService() {
|
|||||||
@ExperimentalCarApi
|
@ExperimentalCarApi
|
||||||
class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver {
|
class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver {
|
||||||
private val TAG = "EVMapSession"
|
private val TAG = "EVMapSession"
|
||||||
lateinit var intent: Intent
|
|
||||||
var mapScreen: LocationAwareScreen? = null
|
var mapScreen: LocationAwareScreen? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
@@ -133,12 +131,8 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateScreen(intent: Intent): Screen {
|
override fun onCreateScreen(intent: Intent): Screen {
|
||||||
this.intent = intent
|
|
||||||
val mapScreen = if (supportsNewMapScreen(carContext) && prefs.androidAutoNewMapScreenEnabled) {
|
val mapScreen = MapScreen(carContext, this)
|
||||||
MapScreen(carContext, this)
|
|
||||||
} else {
|
|
||||||
LegacyMapScreen(carContext, this)
|
|
||||||
}
|
|
||||||
val screens = mutableListOf<Screen>(mapScreen)
|
val screens = mutableListOf<Screen>(mapScreen)
|
||||||
|
|
||||||
handleActionsIntent(intent)?.let {
|
handleActionsIntent(intent)?.let {
|
||||||
@@ -198,7 +192,7 @@ class EVMapSession(val cas: CarAppService) : Session(), DefaultLifecycleObserver
|
|||||||
val lon = it.getQueryParameter("longitude")?.toDouble()
|
val lon = it.getQueryParameter("longitude")?.toDouble()
|
||||||
val name = it.getQueryParameter("name")
|
val name = it.getQueryParameter("name")
|
||||||
if (lat != null && lon != null) {
|
if (lat != null && lon != null) {
|
||||||
prefs.placeSearchResultAndroidAuto = PlaceWithBounds(LatLng(lat, lon), null)
|
prefs.placeSearchResultAndroidAuto = LatLng(lat, lon)
|
||||||
prefs.placeSearchResultAndroidAutoName = name ?: "%.4f,%.4f".format(lat, lon)
|
prefs.placeSearchResultAndroidAutoName = name ?: "%.4f,%.4f".format(lat, lon)
|
||||||
return null
|
return null
|
||||||
} else if (name != null) {
|
} else if (name != null) {
|
||||||
|
|||||||
402
app/src/main/java/net/vonforst/evmap/auto/ChargepriceScreen.kt
Normal file
402
app/src/main/java/net/vonforst/evmap/auto/ChargepriceScreen.kt
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
package net.vonforst.evmap.auto
|
||||||
|
|
||||||
|
import androidx.car.app.CarContext
|
||||||
|
import androidx.car.app.CarToast
|
||||||
|
import androidx.car.app.Screen
|
||||||
|
import androidx.car.app.annotations.ExperimentalCarApi
|
||||||
|
import androidx.car.app.constraints.ConstraintManager
|
||||||
|
import androidx.car.app.hardware.CarHardwareManager
|
||||||
|
import androidx.car.app.hardware.info.Model
|
||||||
|
import androidx.car.app.model.Action
|
||||||
|
import androidx.car.app.model.ActionStrip
|
||||||
|
import androidx.car.app.model.CarIcon
|
||||||
|
import androidx.car.app.model.ItemList
|
||||||
|
import androidx.car.app.model.ListTemplate
|
||||||
|
import androidx.car.app.model.Row
|
||||||
|
import androidx.car.app.model.SectionedItemList
|
||||||
|
import androidx.car.app.model.Template
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.github.erfansn.localeconfigx.currentOrDefaultLocale
|
||||||
|
import jsonapi.Meta
|
||||||
|
import jsonapi.Relationship
|
||||||
|
import jsonapi.Relationships
|
||||||
|
import jsonapi.ResourceIdentifier
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import net.vonforst.evmap.R
|
||||||
|
import net.vonforst.evmap.api.chargeprice.ChargePrice
|
||||||
|
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||||
|
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
|
||||||
|
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
|
||||||
|
import net.vonforst.evmap.api.chargeprice.ChargepriceInclude
|
||||||
|
import net.vonforst.evmap.api.chargeprice.ChargepriceMeta
|
||||||
|
import net.vonforst.evmap.api.chargeprice.ChargepriceOptions
|
||||||
|
import net.vonforst.evmap.api.chargeprice.ChargepriceRequest
|
||||||
|
import net.vonforst.evmap.api.chargeprice.ChargepriceRequestTariffMeta
|
||||||
|
import net.vonforst.evmap.api.chargeprice.ChargepriceStation
|
||||||
|
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||||
|
import net.vonforst.evmap.api.nameForPlugType
|
||||||
|
import net.vonforst.evmap.api.stringProvider
|
||||||
|
import net.vonforst.evmap.model.ChargeLocation
|
||||||
|
import net.vonforst.evmap.model.Chargepoint
|
||||||
|
import net.vonforst.evmap.storage.AppDatabase
|
||||||
|
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||||
|
import net.vonforst.evmap.ui.currency
|
||||||
|
import net.vonforst.evmap.ui.time
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import java.io.IOException
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@ExperimentalCarApi
|
||||||
|
class ChargepriceScreen(ctx: CarContext, val session: EVMapSession, val charger: ChargeLocation) :
|
||||||
|
Screen(ctx) {
|
||||||
|
private val prefs = PreferenceDataSource(ctx)
|
||||||
|
private val db = AppDatabase.getInstance(carContext)
|
||||||
|
private val api by lazy {
|
||||||
|
ChargepriceApi.create(
|
||||||
|
carContext.getString(R.string.chargeprice_key),
|
||||||
|
carContext.getString(R.string.chargeprice_api_url)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private var prices: List<ChargePrice>? = null
|
||||||
|
private var meta: ChargepriceChargepointMeta? = null
|
||||||
|
private var chargepoint: Chargepoint? = null
|
||||||
|
private val maxRows = ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_LIST)
|
||||||
|
private var errorMessage: String? = null
|
||||||
|
private val batteryRange = prefs.chargepriceBatteryRangeAndroidAuto
|
||||||
|
|
||||||
|
override fun onGetTemplate(): Template {
|
||||||
|
if (prices == null) loadData()
|
||||||
|
|
||||||
|
return ListTemplate.Builder().apply {
|
||||||
|
setTitle(
|
||||||
|
carContext.getString(
|
||||||
|
R.string.chargeprice_battery_range,
|
||||||
|
batteryRange[0],
|
||||||
|
batteryRange[1]
|
||||||
|
) + " · " + carContext.getString(R.string.powered_by_chargeprice)
|
||||||
|
)
|
||||||
|
setHeaderAction(Action.BACK)
|
||||||
|
if (prices == null && errorMessage == null) {
|
||||||
|
setLoading(true)
|
||||||
|
} else {
|
||||||
|
val header = meta?.let { meta ->
|
||||||
|
chargepoint?.let { chargepoint ->
|
||||||
|
"${
|
||||||
|
nameForPlugType(
|
||||||
|
carContext.stringProvider(),
|
||||||
|
chargepoint.type
|
||||||
|
)
|
||||||
|
} ${chargepoint.formatPower(carContext.currentOrDefaultLocale)} ${
|
||||||
|
carContext.getString(
|
||||||
|
R.string.chargeprice_stats,
|
||||||
|
meta.energy,
|
||||||
|
time(meta.duration.roundToInt()),
|
||||||
|
meta.energy / meta.duration * 60
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val myTariffs = prefs.chargepriceMyTariffs
|
||||||
|
val myTariffsAll = prefs.chargepriceMyTariffsAll
|
||||||
|
|
||||||
|
val prices = prices?.take(maxRows)
|
||||||
|
if (prices != null && prices.isNotEmpty() && !myTariffsAll && myTariffs != null) {
|
||||||
|
val (myPrices, otherPrices) = prices.partition { price -> price.tariffId in myTariffs }
|
||||||
|
val myPricesList = buildPricesList(myPrices)
|
||||||
|
val otherPricesList = buildPricesList(otherPrices)
|
||||||
|
if (myPricesList.items.isNotEmpty() && otherPricesList.items.isNotEmpty()) {
|
||||||
|
addSectionedList(
|
||||||
|
SectionedItemList.create(
|
||||||
|
myPricesList,
|
||||||
|
(header?.let { it + "\n" } ?: "") +
|
||||||
|
carContext.getString(R.string.chargeprice_header_my_tariffs)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
addSectionedList(
|
||||||
|
SectionedItemList.create(
|
||||||
|
otherPricesList,
|
||||||
|
carContext.getString(R.string.chargeprice_header_other_tariffs)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val list =
|
||||||
|
if (myPricesList.items.isNotEmpty()) myPricesList else otherPricesList
|
||||||
|
if (header != null) {
|
||||||
|
addSectionedList(SectionedItemList.create(list, header))
|
||||||
|
} else {
|
||||||
|
setSingleList(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val list = buildPricesList(prices)
|
||||||
|
if (header != null && list.items.isNotEmpty()) {
|
||||||
|
addSectionedList(SectionedItemList.create(list, header))
|
||||||
|
} else {
|
||||||
|
setSingleList(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setActionStrip(
|
||||||
|
ActionStrip.Builder().addAction(
|
||||||
|
Action.Builder().setIcon(
|
||||||
|
CarIcon.Builder(
|
||||||
|
IconCompat.createWithResource(
|
||||||
|
carContext,
|
||||||
|
R.drawable.ic_chargeprice
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
).setOnClickListener {
|
||||||
|
openUrl(carContext, session.cas, ChargepriceApi.getPoiUrl(charger))
|
||||||
|
}.build()
|
||||||
|
).build()
|
||||||
|
)
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildPricesList(prices: List<ChargePrice>?): ItemList {
|
||||||
|
return ItemList.Builder().apply {
|
||||||
|
setNoItemsMessage(
|
||||||
|
errorMessage
|
||||||
|
?: carContext.getString(R.string.chargeprice_no_tariffs_found)
|
||||||
|
)
|
||||||
|
prices?.forEach { price ->
|
||||||
|
addItem(Row.Builder().apply {
|
||||||
|
setTitle(formatProvider(price))
|
||||||
|
addText(formatPrice(price))
|
||||||
|
}.build())
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatProvider(price: ChargePrice): String {
|
||||||
|
if (!price.tariffName.startsWith(price.provider)) {
|
||||||
|
return price.provider + " " + price.tariffName
|
||||||
|
} else {
|
||||||
|
return price.tariffName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatPrice(price: ChargePrice): String {
|
||||||
|
val amount = price.chargepointPrices.first().price
|
||||||
|
?: return "${carContext.getString(R.string.chargeprice_price_not_available)} (${price.chargepointPrices.first().noPriceReason})"
|
||||||
|
val totalPrice = carContext.getString(
|
||||||
|
R.string.charge_price_format,
|
||||||
|
amount,
|
||||||
|
currency(price.currency)
|
||||||
|
)
|
||||||
|
val kwhPrice = if (amount > 0f) {
|
||||||
|
carContext.getString(
|
||||||
|
if (price.chargepointPrices[0].priceDistribution.isOnlyKwh) {
|
||||||
|
R.string.charge_price_kwh_format
|
||||||
|
} else {
|
||||||
|
R.string.charge_price_average_format
|
||||||
|
},
|
||||||
|
amount / meta!!.energy,
|
||||||
|
currency(price.currency)
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
val monthlyFees = if (price.totalMonthlyFee > 0 || price.monthlyMinSales > 0) {
|
||||||
|
price.formatMonthlyFees(carContext)
|
||||||
|
} else null
|
||||||
|
var text = totalPrice
|
||||||
|
if (kwhPrice != null && monthlyFees != null) {
|
||||||
|
text += " ($kwhPrice, $monthlyFees)"
|
||||||
|
} else if (kwhPrice != null) {
|
||||||
|
text += " ($kwhPrice)"
|
||||||
|
} else if (monthlyFees != null) {
|
||||||
|
text += " ($monthlyFees)"
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadData() {
|
||||||
|
if (supportsCarApiLevel3(carContext)) {
|
||||||
|
val exec = ContextCompat.getMainExecutor(carContext)
|
||||||
|
val hardwareMan =
|
||||||
|
carContext.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||||
|
hardwareMan.carInfo.fetchModel(exec) { model ->
|
||||||
|
loadPrices(model)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loadPrices(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadPrices(model: Model?) {
|
||||||
|
val dataAdapter = ChargepriceApi.getDataAdapter(charger)
|
||||||
|
val manufacturer = getVehicleBrand(model?.manufacturer?.value)
|
||||||
|
val modelName = getVehicleModel(model?.manufacturer?.value, model?.name?.value)
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
val car = determineVehicle(manufacturer, modelName)
|
||||||
|
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
|
||||||
|
|
||||||
|
if (cpStation.chargePoints.isEmpty()) {
|
||||||
|
errorMessage =
|
||||||
|
carContext.getString(R.string.chargeprice_no_compatible_connectors)
|
||||||
|
invalidate()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = api.getChargePrices(
|
||||||
|
ChargepriceRequest(
|
||||||
|
dataAdapter = dataAdapter,
|
||||||
|
station = cpStation,
|
||||||
|
vehicle = car,
|
||||||
|
options = ChargepriceOptions(
|
||||||
|
batteryRange = batteryRange.map { it.toDouble() },
|
||||||
|
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
|
||||||
|
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
|
||||||
|
currency = prefs.chargepriceCurrency,
|
||||||
|
allowUnbalancedLoad = prefs.chargepriceAllowUnbalancedLoad,
|
||||||
|
showPriceUnavailable = true
|
||||||
|
),
|
||||||
|
relationships = if (!prefs.chargepriceMyTariffsAll) {
|
||||||
|
val myTariffs = prefs.chargepriceMyTariffs ?: emptySet()
|
||||||
|
Relationships(
|
||||||
|
"tariffs" to Relationship.ToMany(
|
||||||
|
myTariffs.map {
|
||||||
|
ResourceIdentifier(
|
||||||
|
"tariff",
|
||||||
|
id = it
|
||||||
|
)
|
||||||
|
},
|
||||||
|
meta = Meta.from(
|
||||||
|
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS),
|
||||||
|
ChargepriceApi.moshi
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
), ChargepriceApi.getChargepriceLanguage()
|
||||||
|
)
|
||||||
|
|
||||||
|
val myTariffs = prefs.chargepriceMyTariffs
|
||||||
|
|
||||||
|
// choose the highest power chargepoint
|
||||||
|
// (we have already filtered so that only compatible ones are included)
|
||||||
|
val chargepoint = cpStation.chargePoints.maxByOrNull { it.power }
|
||||||
|
|
||||||
|
val index = cpStation.chargePoints.indexOf(chargepoint)
|
||||||
|
this@ChargepriceScreen.chargepoint =
|
||||||
|
charger.chargepoints.filter { equivalentPlugTypes(it.type).any { it in car.compatibleEvmapConnectors } }[index]
|
||||||
|
|
||||||
|
if (chargepoint == null) {
|
||||||
|
errorMessage =
|
||||||
|
carContext.getString(R.string.chargeprice_no_compatible_connectors)
|
||||||
|
invalidate()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val metaMapped =
|
||||||
|
result.meta!!.map(ChargepriceMeta::class.java, ChargepriceApi.moshi)!!
|
||||||
|
meta = metaMapped.chargePoints.maxByOrNull { it.power }
|
||||||
|
|
||||||
|
prices = result.data!!.mapNotNull { cp ->
|
||||||
|
val filteredPrices =
|
||||||
|
cp.chargepointPrices.filter {
|
||||||
|
it.plug == chargepoint.plug && it.power == chargepoint.power
|
||||||
|
}
|
||||||
|
if (filteredPrices.isEmpty()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
cp.copy(
|
||||||
|
chargepointPrices = filteredPrices
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sortedBy { it.chargepointPrices.first().price ?: Double.MAX_VALUE }
|
||||||
|
.sortedByDescending {
|
||||||
|
prefs.chargepriceMyTariffsAll ||
|
||||||
|
myTariffs != null && it.tariffId in myTariffs
|
||||||
|
}
|
||||||
|
invalidate()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
CarToast.makeText(
|
||||||
|
carContext,
|
||||||
|
R.string.chargeprice_connection_error,
|
||||||
|
CarToast.LENGTH_LONG
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} catch (e: HttpException) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
CarToast.makeText(
|
||||||
|
carContext,
|
||||||
|
R.string.chargeprice_connection_error,
|
||||||
|
CarToast.LENGTH_LONG
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
} catch (e: NoVehicleSelectedException) {
|
||||||
|
errorMessage = carContext.getString(R.string.chargeprice_select_car_first)
|
||||||
|
invalidate()
|
||||||
|
} catch (e: VehicleUnknownException) {
|
||||||
|
errorMessage = carContext.getString(
|
||||||
|
R.string.auto_chargeprice_vehicle_unknown,
|
||||||
|
manufacturer,
|
||||||
|
modelName
|
||||||
|
)
|
||||||
|
invalidate()
|
||||||
|
} catch (e: VehicleAmbiguousException) {
|
||||||
|
errorMessage = carContext.getString(
|
||||||
|
R.string.auto_chargeprice_vehicle_ambiguous,
|
||||||
|
manufacturer,
|
||||||
|
modelName
|
||||||
|
)
|
||||||
|
invalidate()
|
||||||
|
} catch (e: VehicleUnavailableException) {
|
||||||
|
errorMessage =
|
||||||
|
carContext.getString(R.string.auto_chargeprice_vehicle_unavailable)
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NoVehicleSelectedException : Exception()
|
||||||
|
private class VehicleUnknownException : Exception()
|
||||||
|
private class VehicleAmbiguousException : Exception()
|
||||||
|
private class VehicleUnavailableException : Exception()
|
||||||
|
|
||||||
|
private suspend fun determineVehicle(
|
||||||
|
manufacturer: String?,
|
||||||
|
modelName: String?
|
||||||
|
): ChargepriceCar {
|
||||||
|
var vehicles = api.getVehicles().filter {
|
||||||
|
it.id in prefs.chargepriceMyVehicles
|
||||||
|
}
|
||||||
|
if (vehicles.isEmpty()) {
|
||||||
|
throw NoVehicleSelectedException()
|
||||||
|
} else if (vehicles.size > 1) {
|
||||||
|
if (manufacturer != null) {
|
||||||
|
vehicles = vehicles.filter {
|
||||||
|
it.brand.lowercase() == getVehicleBrand(manufacturer)?.lowercase()
|
||||||
|
}
|
||||||
|
if (vehicles.isEmpty()) {
|
||||||
|
throw VehicleUnknownException()
|
||||||
|
} else if (vehicles.size > 1) {
|
||||||
|
if (modelName != null) {
|
||||||
|
vehicles = vehicles.filter {
|
||||||
|
it.name.lowercase().startsWith(modelName.lowercase())
|
||||||
|
}
|
||||||
|
if (vehicles.isEmpty()) {
|
||||||
|
throw VehicleUnknownException()
|
||||||
|
} else if (vehicles.size > 1) {
|
||||||
|
throw VehicleAmbiguousException()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw VehicleAmbiguousException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw VehicleUnavailableException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vehicles[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package net.vonforst.evmap.auto
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
@@ -10,8 +11,10 @@ import android.net.Uri
|
|||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
|
import android.util.Log
|
||||||
import androidx.car.app.CarContext
|
import androidx.car.app.CarContext
|
||||||
import androidx.car.app.CarToast
|
import androidx.car.app.CarToast
|
||||||
|
import androidx.car.app.HostException
|
||||||
import androidx.car.app.Screen
|
import androidx.car.app.Screen
|
||||||
import androidx.car.app.annotations.ExperimentalCarApi
|
import androidx.car.app.annotations.ExperimentalCarApi
|
||||||
import androidx.car.app.constraints.ConstraintManager
|
import androidx.car.app.constraints.ConstraintManager
|
||||||
@@ -32,6 +35,7 @@ import androidx.core.text.HtmlCompat
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
|
import com.github.erfansn.localeconfigx.currentOrDefaultLocale
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -50,7 +54,9 @@ import net.vonforst.evmap.api.availability.tesla.Pricing
|
|||||||
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
|
||||||
import net.vonforst.evmap.api.createApi
|
import net.vonforst.evmap.api.createApi
|
||||||
import net.vonforst.evmap.api.fronyx.PredictionData
|
import net.vonforst.evmap.api.fronyx.PredictionData
|
||||||
import net.vonforst.evmap.api.fronyx.PredictionRepository
|
import net.vonforst.evmap.api.iconForPlugType
|
||||||
|
import net.vonforst.evmap.api.nameForPlugType
|
||||||
|
import net.vonforst.evmap.api.stringProvider
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
import net.vonforst.evmap.model.ChargeLocation
|
||||||
import net.vonforst.evmap.model.Cost
|
import net.vonforst.evmap.model.Cost
|
||||||
import net.vonforst.evmap.model.FaultReport
|
import net.vonforst.evmap.model.FaultReport
|
||||||
@@ -60,8 +66,8 @@ import net.vonforst.evmap.storage.AppDatabase
|
|||||||
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
||||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||||
|
import net.vonforst.evmap.ui.availabilityText
|
||||||
import net.vonforst.evmap.ui.getMarkerTint
|
import net.vonforst.evmap.ui.getMarkerTint
|
||||||
import net.vonforst.evmap.utils.formatDMS
|
|
||||||
import net.vonforst.evmap.viewmodel.Status
|
import net.vonforst.evmap.viewmodel.Status
|
||||||
import net.vonforst.evmap.viewmodel.awaitFinished
|
import net.vonforst.evmap.viewmodel.awaitFinished
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
@@ -70,6 +76,8 @@ import java.time.format.FormatStyle
|
|||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
private const val TAG = "ChargerDetailScreen"
|
||||||
|
|
||||||
@ExperimentalCarApi
|
@ExperimentalCarApi
|
||||||
class ChargerDetailScreen(
|
class ChargerDetailScreen(
|
||||||
ctx: CarContext,
|
ctx: CarContext,
|
||||||
@@ -89,7 +97,7 @@ class ChargerDetailScreen(
|
|||||||
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
||||||
private val availabilityRepo = AvailabilityRepository(ctx)
|
private val availabilityRepo = AvailabilityRepository(ctx)
|
||||||
|
|
||||||
private val predictionRepo = PredictionRepository(ctx)
|
//private val predictionRepo = PredictionRepository(ctx)
|
||||||
private val timeFormat = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
private val timeFormat = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||||
|
|
||||||
private val imageSize = 128 // images should be 128dp according to docs
|
private val imageSize = 128 // images should be 128dp according to docs
|
||||||
@@ -135,32 +143,38 @@ class ChargerDetailScreen(
|
|||||||
.setFlags(Action.FLAG_PRIMARY)
|
.setFlags(Action.FLAG_PRIMARY)
|
||||||
.setBackgroundColor(CarColor.PRIMARY)
|
.setBackgroundColor(CarColor.PRIMARY)
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
navigateToCharger(carContext, session.cas, charger)
|
navigateToCharger(charger)
|
||||||
}
|
}
|
||||||
.build())
|
.build())
|
||||||
if (ChargepriceApi.isChargerSupported(charger)) {
|
if (ChargepriceApi.isChargerSupported(charger)) {
|
||||||
addAction(
|
addAction(
|
||||||
Action.Builder()
|
Action.Builder()
|
||||||
|
.setIcon(
|
||||||
|
CarIcon.Builder(
|
||||||
|
IconCompat.createWithResource(
|
||||||
|
carContext,
|
||||||
|
R.drawable.ic_chargeprice
|
||||||
|
)
|
||||||
|
).build()
|
||||||
|
)
|
||||||
.setTitle(carContext.getString(R.string.auto_prices))
|
.setTitle(carContext.getString(R.string.auto_prices))
|
||||||
.setOnClickListener {
|
.setOnClickListener {
|
||||||
if (!prefs.chargepriceRemoval2025DialogShown) {
|
if (prefs.chargepriceNativeIntegration) {
|
||||||
screenManager.push(
|
screenManager.push(
|
||||||
TextDialogScreen(
|
ChargepriceScreen(
|
||||||
carContext,
|
carContext,
|
||||||
R.string.chargeprice_removal_2025_dialog_title,
|
session,
|
||||||
R.string.chargeprice_removal_2025_dialog_detail
|
charger
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
prefs.chargepriceRemoval2025DialogShown = true
|
} else {
|
||||||
return@setOnClickListener
|
val intent = Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
Uri.parse(ChargepriceApi.getPoiUrl(charger))
|
||||||
|
)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
session.cas.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
val intent = Intent(
|
|
||||||
Intent.ACTION_VIEW,
|
|
||||||
Uri.parse(ChargepriceApi.getPoiUrl(charger))
|
|
||||||
)
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
session.cas.startActivity(intent)
|
|
||||||
}
|
}
|
||||||
.build())
|
.build())
|
||||||
}
|
}
|
||||||
@@ -252,7 +266,7 @@ class ChargerDetailScreen(
|
|||||||
|
|
||||||
// Row 1: address + chargepoints
|
// Row 1: address + chargepoints
|
||||||
rows.add(Row.Builder().apply {
|
rows.add(Row.Builder().apply {
|
||||||
setTitle(charger.address?.toString() ?: charger.coordinates.formatDMS())
|
setTitle(charger.address.toString())
|
||||||
|
|
||||||
if (photo == null) {
|
if (photo == null) {
|
||||||
// show just the icon
|
// show just the icon
|
||||||
@@ -272,7 +286,7 @@ class ChargerDetailScreen(
|
|||||||
Row.IMAGE_TYPE_LARGE
|
Row.IMAGE_TYPE_LARGE
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
addText(generateChargepointsText(charger, availability, carContext))
|
addText(generateChargepointsText(charger))
|
||||||
}.build())
|
}.build())
|
||||||
if (maxRows <= 3) {
|
if (maxRows <= 3) {
|
||||||
// row 2: operator + cost + fault report
|
// row 2: operator + cost + fault report
|
||||||
@@ -485,6 +499,47 @@ class ChargerDetailScreen(
|
|||||||
return string
|
return string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun generateChargepointsText(charger: ChargeLocation): SpannableStringBuilder {
|
||||||
|
val chargepointsText = SpannableStringBuilder()
|
||||||
|
charger.chargepointsMerged.forEachIndexed { i, cp ->
|
||||||
|
chargepointsText.apply {
|
||||||
|
if (i > 0) append(" · ")
|
||||||
|
append("${cp.count}× ")
|
||||||
|
val plugIcon = iconForPlugType(cp.type)
|
||||||
|
if (plugIcon != 0) {
|
||||||
|
append(
|
||||||
|
nameForPlugType(carContext.stringProvider(), cp.type),
|
||||||
|
CarIconSpan.create(
|
||||||
|
CarIcon.Builder(
|
||||||
|
IconCompat.createWithResource(
|
||||||
|
carContext,
|
||||||
|
plugIcon
|
||||||
|
)
|
||||||
|
).setTint(
|
||||||
|
CarColor.createCustom(Color.WHITE, Color.BLACK)
|
||||||
|
).build()
|
||||||
|
),
|
||||||
|
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
append(nameForPlugType(carContext.stringProvider(), cp.type))
|
||||||
|
}
|
||||||
|
cp.formatPower(carContext.currentOrDefaultLocale)?.let {
|
||||||
|
append(" ")
|
||||||
|
append(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
availability?.status?.get(cp)?.let { status ->
|
||||||
|
chargepointsText.append(
|
||||||
|
" (${availabilityText(status)}/${cp.count})",
|
||||||
|
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chargepointsText
|
||||||
|
}
|
||||||
|
|
||||||
private fun generateOperatorText(charger: ChargeLocation) =
|
private fun generateOperatorText(charger: ChargeLocation) =
|
||||||
if (charger.operator != null && charger.network != null) {
|
if (charger.operator != null && charger.network != null) {
|
||||||
if (charger.operator.contains(charger.network)) {
|
if (charger.operator.contains(charger.network)) {
|
||||||
@@ -502,6 +557,58 @@ class ChargerDetailScreen(
|
|||||||
carContext.getString(R.string.unknown_operator)
|
carContext.getString(R.string.unknown_operator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun navigateToCharger(charger: ChargeLocation) {
|
||||||
|
var success = navigateCarApp(charger)
|
||||||
|
if (!success && BuildConfig.FLAVOR_automotive == "automotive") {
|
||||||
|
// on AAOS, some OEMs' navigation apps might not support
|
||||||
|
success = navigateRegularApp(charger)
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
CarToast.makeText(carContext, R.string.no_maps_app_found, CarToast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateCarApp(charger: ChargeLocation): Boolean {
|
||||||
|
val coord = charger.coordinates
|
||||||
|
val intent =
|
||||||
|
Intent(
|
||||||
|
CarContext.ACTION_NAVIGATE,
|
||||||
|
Uri.parse("geo:${coord.lat},${coord.lng}")
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
carContext.startCarApp(intent)
|
||||||
|
return true
|
||||||
|
} catch (e: HostException) {
|
||||||
|
Log.w(TAG, "Could not start navigation using car app intent")
|
||||||
|
Log.w(TAG, intent.toString())
|
||||||
|
e.printStackTrace()
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.w(TAG, "Could not start navigation using car app intent")
|
||||||
|
Log.w(TAG, intent.toString())
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateRegularApp(charger: ChargeLocation): Boolean {
|
||||||
|
val coord = charger.coordinates
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
intent.data = Uri.parse(
|
||||||
|
"geo:${coord.lat},${coord.lng}?q=${coord.lat},${coord.lng}(${
|
||||||
|
Uri.encode(charger.name)
|
||||||
|
})"
|
||||||
|
)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
if (intent.resolveActivity(carContext.packageManager) != null) {
|
||||||
|
carContext.startActivity(intent)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Could not start navigation using regular intent")
|
||||||
|
Log.w(TAG, intent.toString())
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadCharger() {
|
private fun loadCharger() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
favorite = db.favoritesDao().findFavorite(chargerSparse.id, chargerSparse.dataSource)
|
favorite = db.favoritesDao().findFavorite(chargerSparse.id, chargerSparse.dataSource)
|
||||||
@@ -567,7 +674,7 @@ class ChargerDetailScreen(
|
|||||||
|
|
||||||
invalidate()
|
invalidate()
|
||||||
|
|
||||||
prediction = predictionRepo.getPredictionData(charger, availability)
|
//prediction = predictionRepo.getPredictionData(charger, availability)
|
||||||
|
|
||||||
invalidate()
|
invalidate()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,249 +0,0 @@
|
|||||||
package net.vonforst.evmap.auto
|
|
||||||
|
|
||||||
import android.location.Location
|
|
||||||
import android.text.SpannableString
|
|
||||||
import android.text.SpannableStringBuilder
|
|
||||||
import android.text.Spanned
|
|
||||||
import androidx.car.app.CarContext
|
|
||||||
import androidx.car.app.annotations.ExperimentalCarApi
|
|
||||||
import androidx.car.app.hardware.info.EnergyLevel
|
|
||||||
import androidx.car.app.model.Action
|
|
||||||
import androidx.car.app.model.CarColor
|
|
||||||
import androidx.car.app.model.CarIcon
|
|
||||||
import androidx.car.app.model.CarIconSpan
|
|
||||||
import androidx.car.app.model.CarLocation
|
|
||||||
import androidx.car.app.model.CarText
|
|
||||||
import androidx.car.app.model.DistanceSpan
|
|
||||||
import androidx.car.app.model.ForegroundCarColorSpan
|
|
||||||
import androidx.car.app.model.ItemList
|
|
||||||
import androidx.car.app.model.Metadata
|
|
||||||
import androidx.car.app.model.Pane
|
|
||||||
import androidx.car.app.model.Place
|
|
||||||
import androidx.car.app.model.PlaceMarker
|
|
||||||
import androidx.car.app.model.Row
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import net.vonforst.evmap.R
|
|
||||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
|
||||||
import net.vonforst.evmap.model.FILTERS_FAVORITES
|
|
||||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
|
||||||
import net.vonforst.evmap.ui.availabilityText
|
|
||||||
import net.vonforst.evmap.ui.getMarkerTint
|
|
||||||
import net.vonforst.evmap.utils.distanceBetween
|
|
||||||
import net.vonforst.evmap.utils.formatDecimal
|
|
||||||
import java.time.ZonedDateTime
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
interface ChargerListDelegate : ItemList.OnItemVisibilityChangedListener {
|
|
||||||
val locationError: Boolean
|
|
||||||
val loadingError: Boolean
|
|
||||||
val maxRows: Int
|
|
||||||
val filterStatus: Long
|
|
||||||
val location: Location?
|
|
||||||
val energyLevel: EnergyLevel?
|
|
||||||
fun onChargerClick(charger: ChargeLocation)
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalCarApi
|
|
||||||
class ChargerListFormatter(
|
|
||||||
val carContext: CarContext,
|
|
||||||
val screen: ChargerListDelegate,
|
|
||||||
val cas: CarAppService
|
|
||||||
) {
|
|
||||||
private val iconGen = ChargerIconGenerator(carContext, null, height = 96)
|
|
||||||
var favorites: Set<Long> = emptySet()
|
|
||||||
|
|
||||||
fun buildChargerList(
|
|
||||||
chargers: List<ChargeLocation>?,
|
|
||||||
availabilities: Map<Long, Pair<ZonedDateTime, ChargeLocationStatus?>>
|
|
||||||
): ItemList? {
|
|
||||||
return if (chargers != null) {
|
|
||||||
val chargerList = chargers.take(screen.maxRows)
|
|
||||||
val builder = ItemList.Builder()
|
|
||||||
// only show the city if not all chargers are in the same city
|
|
||||||
val showCity = chargerList.map { it.address?.city }.distinct().size > 1
|
|
||||||
chargerList.forEach { charger ->
|
|
||||||
builder.addItem(
|
|
||||||
formatCharger(
|
|
||||||
charger,
|
|
||||||
availabilities,
|
|
||||||
showCity,
|
|
||||||
charger.id in favorites
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
builder.setNoItemsMessage(
|
|
||||||
carContext.getString(
|
|
||||||
if (screen.filterStatus == FILTERS_FAVORITES) {
|
|
||||||
R.string.auto_no_favorites_found
|
|
||||||
} else {
|
|
||||||
R.string.auto_no_chargers_found
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
builder.setOnItemsVisibilityChangedListener(screen)
|
|
||||||
builder.build()
|
|
||||||
} else {
|
|
||||||
if (screen.loadingError) {
|
|
||||||
val builder = ItemList.Builder()
|
|
||||||
builder.setNoItemsMessage(
|
|
||||||
carContext.getString(R.string.connection_error)
|
|
||||||
)
|
|
||||||
builder.build()
|
|
||||||
} else if (screen.locationError) {
|
|
||||||
val builder = ItemList.Builder()
|
|
||||||
builder.setNoItemsMessage(
|
|
||||||
carContext.getString(R.string.location_error)
|
|
||||||
)
|
|
||||||
builder.build()
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun formatCharger(
|
|
||||||
charger: ChargeLocation,
|
|
||||||
availabilities: Map<Long, Pair<ZonedDateTime, ChargeLocationStatus?>>,
|
|
||||||
showCity: Boolean,
|
|
||||||
isFavorite: Boolean
|
|
||||||
): Row {
|
|
||||||
val markerTint = getMarkerTint(charger)
|
|
||||||
val backgroundTint = if ((charger.maxPower ?: 0.0) > 100) {
|
|
||||||
R.color.charger_100kw_dark // slightly darker color for better contrast
|
|
||||||
} else {
|
|
||||||
markerTint
|
|
||||||
}
|
|
||||||
val color = ContextCompat.getColor(carContext, backgroundTint)
|
|
||||||
val place =
|
|
||||||
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
|
|
||||||
.setMarker(
|
|
||||||
PlaceMarker.Builder()
|
|
||||||
.setColor(CarColor.createCustom(color, color))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val icon = iconGen.getBitmap(
|
|
||||||
markerTint,
|
|
||||||
fault = charger.faultReport != null,
|
|
||||||
multi = charger.isMulti(),
|
|
||||||
fav = isFavorite
|
|
||||||
)
|
|
||||||
val iconSpan =
|
|
||||||
CarIconSpan.create(CarIcon.Builder(IconCompat.createWithBitmap(icon)).build())
|
|
||||||
|
|
||||||
return Row.Builder().apply {
|
|
||||||
// only show the city if not all chargers are in the same city (-> showCity == true)
|
|
||||||
// and the city is not already contained in the charger name
|
|
||||||
val title = SpannableStringBuilder().apply {
|
|
||||||
append(" ", iconSpan, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)
|
|
||||||
append(" ")
|
|
||||||
append(charger.name)
|
|
||||||
}
|
|
||||||
if (showCity && charger.address?.city != null && charger.address.city !in charger.name) {
|
|
||||||
val titleWithCity = SpannableStringBuilder().apply {
|
|
||||||
append("", iconSpan, SpannableString.SPAN_INCLUSIVE_EXCLUSIVE)
|
|
||||||
append(" ")
|
|
||||||
append("${charger.name} · ${charger.address.city}")
|
|
||||||
}
|
|
||||||
setTitle(CarText.Builder(titleWithCity).addVariant(title).build())
|
|
||||||
} else {
|
|
||||||
setTitle(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
val text = SpannableStringBuilder()
|
|
||||||
|
|
||||||
// distance
|
|
||||||
screen.location?.let {
|
|
||||||
val distanceMeters = distanceBetween(
|
|
||||||
it.latitude, it.longitude,
|
|
||||||
charger.coordinates.lat, charger.coordinates.lng
|
|
||||||
)
|
|
||||||
text.append(
|
|
||||||
"distance",
|
|
||||||
DistanceSpan.create(
|
|
||||||
roundValueToDistance(
|
|
||||||
distanceMeters,
|
|
||||||
screen.energyLevel?.distanceDisplayUnit?.value,
|
|
||||||
carContext
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// power
|
|
||||||
val power = charger.maxPower
|
|
||||||
if (power != null) {
|
|
||||||
if (text.isNotEmpty()) text.append(" · ")
|
|
||||||
text.append("${power.roundToInt()} kW")
|
|
||||||
}
|
|
||||||
|
|
||||||
// availability
|
|
||||||
availabilities[charger.id]?.second?.let { av ->
|
|
||||||
val status = av.status.values.flatten()
|
|
||||||
val available = availabilityText(status)
|
|
||||||
val total = charger.chargepoints.sumOf { it.count }
|
|
||||||
|
|
||||||
if (text.isNotEmpty()) text.append(" · ")
|
|
||||||
text.append(
|
|
||||||
"$available/$total",
|
|
||||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
addText(text)
|
|
||||||
setMetadata(
|
|
||||||
Metadata.Builder()
|
|
||||||
.setPlace(place)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
setOnClickListener {
|
|
||||||
screen.onChargerClick(charger)
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun buildSingleCharger(
|
|
||||||
charger: ChargeLocation,
|
|
||||||
availability: ChargeLocationStatus?,
|
|
||||||
onClick: () -> Unit
|
|
||||||
) = Pane.Builder().apply {
|
|
||||||
val icon = iconGen.getBitmap(
|
|
||||||
getMarkerTint(charger),
|
|
||||||
fault = charger.faultReport != null,
|
|
||||||
multi = charger.isMulti(),
|
|
||||||
fav = charger.id in favorites
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
addRow(Row.Builder().apply {
|
|
||||||
setImage(CarIcon.Builder(IconCompat.createWithBitmap(icon)).build())
|
|
||||||
setTitle(charger.address?.toString() ?: charger.coordinates.formatDecimal())
|
|
||||||
addText(generateChargepointsText(charger, availability, carContext))
|
|
||||||
}.build())
|
|
||||||
addAction(Action.Builder().apply {
|
|
||||||
setTitle(carContext.getString(R.string.show_more))
|
|
||||||
setOnClickListener(onClick)
|
|
||||||
}.build())
|
|
||||||
addAction(Action.Builder().apply {
|
|
||||||
setIcon(
|
|
||||||
CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
carContext,
|
|
||||||
R.drawable.ic_navigation
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
)
|
|
||||||
setTitle(carContext.getString(R.string.navigate))
|
|
||||||
setBackgroundColor(CarColor.PRIMARY)
|
|
||||||
setOnClickListener {
|
|
||||||
navigateToCharger(carContext, cas, charger)
|
|
||||||
}
|
|
||||||
}.build())
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
@@ -15,10 +15,8 @@ import androidx.car.app.model.CarColor
|
|||||||
import androidx.car.app.model.CarIcon
|
import androidx.car.app.model.CarIcon
|
||||||
import androidx.car.app.model.CarText
|
import androidx.car.app.model.CarText
|
||||||
import androidx.car.app.model.ForegroundCarColorSpan
|
import androidx.car.app.model.ForegroundCarColorSpan
|
||||||
import androidx.car.app.model.Header
|
|
||||||
import androidx.car.app.model.ItemList
|
import androidx.car.app.model.ItemList
|
||||||
import androidx.car.app.model.ListTemplate
|
import androidx.car.app.model.ListTemplate
|
||||||
import androidx.car.app.model.MessageTemplate
|
|
||||||
import androidx.car.app.model.Pane
|
import androidx.car.app.model.Pane
|
||||||
import androidx.car.app.model.PaneTemplate
|
import androidx.car.app.model.PaneTemplate
|
||||||
import androidx.car.app.model.ParkedOnlyOnClickListener
|
import androidx.car.app.model.ParkedOnlyOnClickListener
|
||||||
@@ -243,23 +241,20 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
|||||||
|
|
||||||
)
|
)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
val confirmScreen = ConfirmDeleteScreen(carContext, it) {
|
lifecycleScope.launch {
|
||||||
lifecycleScope.launch {
|
db.filterProfileDao().delete(it)
|
||||||
db.filterProfileDao().delete(it)
|
if (prefs.filterStatus == it.id) {
|
||||||
if (prefs.filterStatus == it.id) {
|
prefs.filterStatus = FILTERS_DISABLED
|
||||||
prefs.filterStatus = FILTERS_DISABLED
|
|
||||||
}
|
|
||||||
CarToast.makeText(
|
|
||||||
carContext,
|
|
||||||
carContext.getString(
|
|
||||||
R.string.deleted_item,
|
|
||||||
it.name
|
|
||||||
),
|
|
||||||
CarToast.LENGTH_SHORT
|
|
||||||
).show()
|
|
||||||
}
|
}
|
||||||
|
CarToast.makeText(
|
||||||
|
carContext,
|
||||||
|
carContext.getString(
|
||||||
|
R.string.deleted_item,
|
||||||
|
it.name
|
||||||
|
),
|
||||||
|
CarToast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
}
|
}
|
||||||
screenManager.push(confirmScreen)
|
|
||||||
}
|
}
|
||||||
}.build())
|
}.build())
|
||||||
}
|
}
|
||||||
@@ -304,31 +299,6 @@ class FilterScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConfirmDeleteScreen(ctx: CarContext, val profile: FilterProfile, val deleteListener: () -> Unit): Screen(ctx) {
|
|
||||||
override fun onGetTemplate(): Template {
|
|
||||||
val message = carContext.getString(R.string.auto_confirm_delete_profile, profile.name)
|
|
||||||
return MessageTemplate.Builder(message)
|
|
||||||
.setHeader(Header.Builder()
|
|
||||||
.setTitle(carContext.getString(R.string.delete))
|
|
||||||
.setStartHeaderAction(Action.BACK)
|
|
||||||
.build())
|
|
||||||
.addAction(Action.Builder()
|
|
||||||
.setTitle(carContext.getString(R.string.delete))
|
|
||||||
.setBackgroundColor(CarColor.PRIMARY)
|
|
||||||
.setFlags(Action.FLAG_PRIMARY)
|
|
||||||
.setOnClickListener {
|
|
||||||
deleteListener()
|
|
||||||
screenManager.pop()
|
|
||||||
}
|
|
||||||
.build())
|
|
||||||
.addAction(Action.Builder()
|
|
||||||
.setTitle(carContext.getString(R.string.cancel))
|
|
||||||
.setOnClickListener { screenManager.pop() }
|
|
||||||
.build())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@androidx.car.app.annotations.ExperimentalCarApi
|
@androidx.car.app.annotations.ExperimentalCarApi
|
||||||
class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
||||||
private val vm = FilterViewModel(carContext.applicationContext as Application)
|
private val vm = FilterViewModel(carContext.applicationContext as Application)
|
||||||
@@ -390,21 +360,18 @@ class EditFiltersScreen(ctx: CarContext) : Screen(ctx) {
|
|||||||
|
|
||||||
)
|
)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
val confirmScreen = ConfirmDeleteScreen(carContext, currentProfile) {
|
lifecycleScope.launch {
|
||||||
lifecycleScope.launch {
|
vm.deleteCurrentProfile()
|
||||||
vm.deleteCurrentProfile()
|
CarToast.makeText(
|
||||||
CarToast.makeText(
|
carContext,
|
||||||
carContext,
|
carContext.getString(
|
||||||
carContext.getString(
|
R.string.deleted_item,
|
||||||
R.string.deleted_item,
|
currentProfile.name
|
||||||
currentProfile.name
|
),
|
||||||
),
|
CarToast.LENGTH_SHORT
|
||||||
CarToast.LENGTH_SHORT
|
).show()
|
||||||
).show()
|
screenManager.pop()
|
||||||
screenManager.pop()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
screenManager.push(confirmScreen)
|
|
||||||
}
|
}
|
||||||
}.build())
|
}.build())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,532 +0,0 @@
|
|||||||
package net.vonforst.evmap.auto
|
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.location.Location
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import androidx.car.app.CarContext
|
|
||||||
import androidx.car.app.Screen
|
|
||||||
import androidx.car.app.constraints.ConstraintManager
|
|
||||||
import androidx.car.app.hardware.CarHardwareManager
|
|
||||||
import androidx.car.app.hardware.info.CarInfo
|
|
||||||
import androidx.car.app.hardware.info.CarSensors
|
|
||||||
import androidx.car.app.hardware.info.Compass
|
|
||||||
import androidx.car.app.hardware.info.EnergyLevel
|
|
||||||
import androidx.car.app.model.Action
|
|
||||||
import androidx.car.app.model.ActionStrip
|
|
||||||
import androidx.car.app.model.CarColor
|
|
||||||
import androidx.car.app.model.CarIcon
|
|
||||||
import androidx.car.app.model.CarLocation
|
|
||||||
import androidx.car.app.model.OnContentRefreshListener
|
|
||||||
import androidx.car.app.model.Place
|
|
||||||
import androidx.car.app.model.PlaceListMapTemplate
|
|
||||||
import androidx.car.app.model.PlaceMarker
|
|
||||||
import androidx.car.app.model.Template
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.car2go.maps.model.LatLng
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import net.vonforst.evmap.BuildConfig
|
|
||||||
import net.vonforst.evmap.R
|
|
||||||
import net.vonforst.evmap.api.availability.AvailabilityRepository
|
|
||||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
|
||||||
import net.vonforst.evmap.api.createApi
|
|
||||||
import net.vonforst.evmap.api.stringProvider
|
|
||||||
import net.vonforst.evmap.model.ChargeLocation
|
|
||||||
import net.vonforst.evmap.model.FILTERS_FAVORITES
|
|
||||||
import net.vonforst.evmap.model.FilterValue
|
|
||||||
import net.vonforst.evmap.model.FilterWithValue
|
|
||||||
import net.vonforst.evmap.storage.AppDatabase
|
|
||||||
import net.vonforst.evmap.storage.ChargeLocationsRepository
|
|
||||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
|
||||||
import net.vonforst.evmap.utils.bearingBetween
|
|
||||||
import net.vonforst.evmap.utils.distanceBetween
|
|
||||||
import net.vonforst.evmap.utils.headingDiff
|
|
||||||
import net.vonforst.evmap.viewmodel.Status
|
|
||||||
import net.vonforst.evmap.viewmodel.await
|
|
||||||
import net.vonforst.evmap.viewmodel.awaitFinished
|
|
||||||
import net.vonforst.evmap.viewmodel.filtersWithValue
|
|
||||||
import retrofit2.HttpException
|
|
||||||
import java.io.IOException
|
|
||||||
import java.time.Duration
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.ZonedDateTime
|
|
||||||
import kotlin.collections.set
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main map screen showing either nearby chargers or favorites
|
|
||||||
*
|
|
||||||
* Legacy implementation for Car App API level < 7
|
|
||||||
*/
|
|
||||||
@androidx.car.app.annotations.ExperimentalCarApi
|
|
||||||
class LegacyMapScreen(ctx: CarContext, val session: EVMapSession) :
|
|
||||||
Screen(ctx), LocationAwareScreen, OnContentRefreshListener,
|
|
||||||
ChargerListDelegate, DefaultLifecycleObserver {
|
|
||||||
|
|
||||||
private val db = AppDatabase.getInstance(carContext)
|
|
||||||
private var prefs = PreferenceDataSource(ctx)
|
|
||||||
private val repo =
|
|
||||||
ChargeLocationsRepository(createApi(prefs.dataSource, ctx), lifecycleScope, db, prefs)
|
|
||||||
private val availabilityRepo = AvailabilityRepository(ctx)
|
|
||||||
|
|
||||||
private var updateCoroutine: Job? = null
|
|
||||||
private var availabilityUpdateCoroutine: Job? = null
|
|
||||||
|
|
||||||
private var visibleStart: Int? = null
|
|
||||||
private var visibleEnd: Int? = null
|
|
||||||
|
|
||||||
override var location: Location? = null
|
|
||||||
private var lastDistanceUpdateTime: Instant? = null
|
|
||||||
private var lastChargersUpdateTime: Instant? = null
|
|
||||||
private var chargers: List<ChargeLocation>? = null
|
|
||||||
private val favorites = db.favoritesDao().getAllFavorites()
|
|
||||||
override var loadingError = false
|
|
||||||
override var locationError = false
|
|
||||||
private val searchRadius = 5 // kilometers
|
|
||||||
private val distanceUpdateThreshold = Duration.ofSeconds(15)
|
|
||||||
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
|
|
||||||
private val chargersUpdateThresholdDistance = 500 // meters
|
|
||||||
private val chargersUpdateThresholdTime = Duration.ofSeconds(30)
|
|
||||||
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus?>> =
|
|
||||||
HashMap()
|
|
||||||
override val maxRows =
|
|
||||||
min(ctx.getContentLimit(ConstraintManager.CONTENT_LIMIT_TYPE_PLACE_LIST), 25)
|
|
||||||
private val supportsRefresh = ctx.isAppDrivenRefreshSupported
|
|
||||||
|
|
||||||
override var filterStatus = prefs.filterStatus
|
|
||||||
|
|
||||||
private var filtersWithValue: List<FilterWithValue<FilterValue>>? = null
|
|
||||||
|
|
||||||
private val carInfo: CarInfo by lazy {
|
|
||||||
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
|
|
||||||
}
|
|
||||||
private val carSensors: CarSensors by lazy { carContext.patchedCarSensors }
|
|
||||||
override var energyLevel: EnergyLevel? = null
|
|
||||||
private var heading: Compass? = null
|
|
||||||
private val permissions = if (BuildConfig.FLAVOR_automotive == "automotive") {
|
|
||||||
listOf(
|
|
||||||
"android.car.permission.CAR_ENERGY",
|
|
||||||
"android.car.permission.CAR_ENERGY_PORTS",
|
|
||||||
"android.car.permission.READ_CAR_DISPLAY_UNITS",
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
listOf(
|
|
||||||
"com.google.android.gms.permission.CAR_FUEL"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var searchLocation: LatLng? = null
|
|
||||||
|
|
||||||
private val formatter = ChargerListFormatter(ctx, this, session.cas)
|
|
||||||
|
|
||||||
init {
|
|
||||||
lifecycle.addObserver(this)
|
|
||||||
marker = MapScreen.MARKER
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGetTemplate(): Template {
|
|
||||||
session.mapScreen = this
|
|
||||||
return PlaceListMapTemplate.Builder().apply {
|
|
||||||
setTitle(
|
|
||||||
prefs.placeSearchResultAndroidAutoName?.let {
|
|
||||||
carContext.getString(R.string.auto_chargers_near_location, it)
|
|
||||||
} ?: carContext.getString(
|
|
||||||
if (filterStatus == FILTERS_FAVORITES) {
|
|
||||||
R.string.auto_favorites
|
|
||||||
} else {
|
|
||||||
R.string.auto_chargers_closeby
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (prefs.placeSearchResultAndroidAutoName != null) {
|
|
||||||
searchLocation?.let {
|
|
||||||
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).apply {
|
|
||||||
if (prefs.placeSearchResultAndroidAutoName != null) {
|
|
||||||
setMarker(
|
|
||||||
PlaceMarker.Builder()
|
|
||||||
.setColor(CarColor.PRIMARY)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.build())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
location?.let {
|
|
||||||
setAnchor(Place.Builder(CarLocation.create(it.latitude, it.longitude)).build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
formatter.buildChargerList(chargers, availabilities)?.let {
|
|
||||||
setItemList(it)
|
|
||||||
} ?: setLoading(true)
|
|
||||||
setCurrentLocationEnabled(true)
|
|
||||||
setHeaderAction(Action.APP_ICON)
|
|
||||||
val filtersCount = if (filterStatus == FILTERS_FAVORITES) 1 else {
|
|
||||||
filtersWithValue?.count {
|
|
||||||
!it.value.hasSameValueAs(it.filter.defaultValue())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setActionStrip(
|
|
||||||
ActionStrip.Builder()
|
|
||||||
.addAction(
|
|
||||||
Action.Builder()
|
|
||||||
.setIcon(
|
|
||||||
CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
carContext,
|
|
||||||
R.drawable.ic_settings
|
|
||||||
)
|
|
||||||
).setTint(CarColor.DEFAULT).build()
|
|
||||||
)
|
|
||||||
.setOnClickListener {
|
|
||||||
screenManager.push(SettingsScreen(carContext, session))
|
|
||||||
session.mapScreen = null
|
|
||||||
}
|
|
||||||
.build())
|
|
||||||
.addAction(Action.Builder().apply {
|
|
||||||
setIcon(
|
|
||||||
CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
carContext,
|
|
||||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
|
||||||
R.drawable.ic_search_off
|
|
||||||
} else {
|
|
||||||
R.drawable.ic_search
|
|
||||||
}
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
|
|
||||||
)
|
|
||||||
setOnClickListener {
|
|
||||||
if (prefs.placeSearchResultAndroidAuto != null) {
|
|
||||||
prefs.placeSearchResultAndroidAutoName = null
|
|
||||||
prefs.placeSearchResultAndroidAuto = null
|
|
||||||
if (!supportsRefresh) {
|
|
||||||
screenManager.pushForResult(DummyReturnScreen(carContext)) {
|
|
||||||
chargers = null
|
|
||||||
loadChargers()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
chargers = null
|
|
||||||
loadChargers()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
screenManager.pushForResult(
|
|
||||||
PlaceSearchScreen(
|
|
||||||
carContext,
|
|
||||||
session
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
chargers = null
|
|
||||||
loadChargers()
|
|
||||||
}
|
|
||||||
session.mapScreen = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.build())
|
|
||||||
.addAction(
|
|
||||||
Action.Builder()
|
|
||||||
.setIcon(
|
|
||||||
CarIcon.Builder(
|
|
||||||
IconCompat.createWithResource(
|
|
||||||
carContext,
|
|
||||||
R.drawable.ic_filter
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.setTint(if (filtersCount != null && filtersCount > 0) CarColor.SECONDARY else CarColor.DEFAULT)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.setOnClickListener {
|
|
||||||
screenManager.push(FilterScreen(carContext, session))
|
|
||||||
session.mapScreen = null
|
|
||||||
}
|
|
||||||
.build())
|
|
||||||
.build())
|
|
||||||
if (carContext.carAppApiLevel >= 5 ||
|
|
||||||
(BuildConfig.FLAVOR_automotive == "automotive" && carContext.carAppApiLevel >= 4)
|
|
||||||
) {
|
|
||||||
setOnContentRefreshListener(this@LegacyMapScreen)
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChargerClick(charger: ChargeLocation) {
|
|
||||||
screenManager.push(ChargerDetailScreen(carContext, charger, session))
|
|
||||||
session.mapScreen = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateLocation(location: Location) {
|
|
||||||
if (location.latitude == this.location?.latitude
|
|
||||||
&& location.longitude == this.location?.longitude
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val previousLocation = this.location
|
|
||||||
this.location = location
|
|
||||||
if (previousLocation == null) {
|
|
||||||
loadChargers()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val now = Instant.now()
|
|
||||||
if (lastDistanceUpdateTime == null ||
|
|
||||||
Duration.between(lastDistanceUpdateTime, now) > distanceUpdateThreshold
|
|
||||||
) {
|
|
||||||
lastDistanceUpdateTime = now
|
|
||||||
// update displayed distances
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// if chargers are searched around current location, consider app-driven refresh
|
|
||||||
val searchLocation =
|
|
||||||
if (prefs.placeSearchResultAndroidAuto == null) searchLocation else null
|
|
||||||
val distance = searchLocation?.let {
|
|
||||||
distanceBetween(
|
|
||||||
it.latitude, it.longitude, location.latitude, location.longitude
|
|
||||||
)
|
|
||||||
} ?: 0.0
|
|
||||||
if (supportsRefresh && (lastChargersUpdateTime == null ||
|
|
||||||
Duration.between(
|
|
||||||
lastChargersUpdateTime,
|
|
||||||
now
|
|
||||||
) > chargersUpdateThresholdTime) && (distance > chargersUpdateThresholdDistance)
|
|
||||||
) {
|
|
||||||
onContentRefreshRequested()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadChargers() {
|
|
||||||
val location = location ?: return
|
|
||||||
|
|
||||||
val searchLocation =
|
|
||||||
prefs.placeSearchResultAndroidAuto?.latLng ?: LatLng.fromLocation(location)
|
|
||||||
this.searchLocation = searchLocation
|
|
||||||
|
|
||||||
updateCoroutine = lifecycleScope.launch {
|
|
||||||
loadingError = false
|
|
||||||
try {
|
|
||||||
filterStatus = prefs.filterStatus
|
|
||||||
val filterValues =
|
|
||||||
db.filterValueDao().getFilterValuesAsync(filterStatus, prefs.dataSource)
|
|
||||||
val filters = repo.getFiltersAsync(carContext.stringProvider())
|
|
||||||
filtersWithValue = filtersWithValue(filters, filterValues)
|
|
||||||
|
|
||||||
val apiId = repo.api.value!!.id
|
|
||||||
|
|
||||||
// load chargers
|
|
||||||
if (filterStatus == FILTERS_FAVORITES) {
|
|
||||||
val chargers = favorites.await().map { it.charger }.sortedBy {
|
|
||||||
distanceBetween(
|
|
||||||
location.latitude, location.longitude,
|
|
||||||
it.coordinates.lat, it.coordinates.lng
|
|
||||||
)
|
|
||||||
}
|
|
||||||
this@LegacyMapScreen.chargers = chargers
|
|
||||||
} else {
|
|
||||||
// try multiple search radii until we have enough chargers
|
|
||||||
var chargers: List<ChargeLocation>? = null
|
|
||||||
val radiusValues = listOf(searchRadius, searchRadius * 10, searchRadius * 50)
|
|
||||||
for (radius in radiusValues) {
|
|
||||||
val response = repo.getChargepointsRadius(
|
|
||||||
searchLocation,
|
|
||||||
radius,
|
|
||||||
filtersWithValue
|
|
||||||
).awaitFinished()
|
|
||||||
if (response.status == Status.ERROR && if (radius == radiusValues.last()) response.data.isNullOrEmpty() else response.data == null) {
|
|
||||||
loadingError = true
|
|
||||||
this@LegacyMapScreen.chargers = null
|
|
||||||
invalidate()
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
chargers = response.data
|
|
||||||
if (prefs.placeSearchResultAndroidAutoName == null) {
|
|
||||||
chargers = headingFilter(
|
|
||||||
chargers,
|
|
||||||
searchLocation
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (chargers == null || chargers.size >= maxRows) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this@LegacyMapScreen.chargers = chargers
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCoroutine = null
|
|
||||||
lastChargersUpdateTime = Instant.now()
|
|
||||||
lastDistanceUpdateTime = Instant.now()
|
|
||||||
invalidate()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
loadingError = true
|
|
||||||
invalidate()
|
|
||||||
} catch (e: HttpException) {
|
|
||||||
loadingError = true
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters by heading if heading available and enabled
|
|
||||||
*/
|
|
||||||
private fun headingFilter(
|
|
||||||
chargers: List<ChargeLocation>?,
|
|
||||||
searchLocation: LatLng
|
|
||||||
): List<ChargeLocation>? {
|
|
||||||
// use compass heading if available, otherwise fall back to GPS
|
|
||||||
val location = location
|
|
||||||
val heading = heading?.orientations?.value?.get(0)
|
|
||||||
?: if (location?.hasBearing() == true) location.bearing else null
|
|
||||||
return heading?.let {
|
|
||||||
if (!prefs.showChargersAheadAndroidAuto) return@let chargers
|
|
||||||
|
|
||||||
chargers?.filter {
|
|
||||||
val bearing = bearingBetween(
|
|
||||||
searchLocation.latitude,
|
|
||||||
searchLocation.longitude,
|
|
||||||
it.coordinates.lat,
|
|
||||||
it.coordinates.lng
|
|
||||||
)
|
|
||||||
val diff = headingDiff(bearing, heading.toDouble())
|
|
||||||
abs(diff) < 30
|
|
||||||
}
|
|
||||||
} ?: chargers
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
|
|
||||||
val isUpdate = this.energyLevel == null
|
|
||||||
this.energyLevel = energyLevel
|
|
||||||
if (isUpdate) invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCompassUpdated(compass: Compass) {
|
|
||||||
this.heading = compass
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart(owner: LifecycleOwner) {
|
|
||||||
setupListeners()
|
|
||||||
session.requestLocationUpdates()
|
|
||||||
locationError = false
|
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
|
||||||
if (location == null) {
|
|
||||||
locationError = true
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
}, 5000)
|
|
||||||
|
|
||||||
// Reloading chargers in onStart does not seem to count towards content limit.
|
|
||||||
// So let's do this so the user gets fresh chargers when re-entering the app.
|
|
||||||
if (prefs.dataSource != repo.api.value?.id) {
|
|
||||||
repo.api.value = createApi(prefs.dataSource, carContext)
|
|
||||||
}
|
|
||||||
invalidate()
|
|
||||||
loadChargers()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupListeners() {
|
|
||||||
val exec = ContextCompat.getMainExecutor(carContext)
|
|
||||||
if (supportsCarApiLevel3(carContext)) {
|
|
||||||
carSensors.addCompassListener(
|
|
||||||
CarSensors.UPDATE_RATE_NORMAL,
|
|
||||||
exec,
|
|
||||||
::onCompassUpdated
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!permissions.all {
|
|
||||||
ContextCompat.checkSelfPermission(
|
|
||||||
carContext,
|
|
||||||
it
|
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
|
||||||
})
|
|
||||||
return
|
|
||||||
|
|
||||||
if (supportsCarApiLevel3(carContext)) {
|
|
||||||
println("Setting up energy level listener")
|
|
||||||
carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop(owner: LifecycleOwner) {
|
|
||||||
// Reloading chargers in onStart does not seem to count towards content limit.
|
|
||||||
// So let's do this so the user gets fresh chargers when re-entering the app.
|
|
||||||
// Deleting the data already in onStop makes sure that we show a loading screen directly
|
|
||||||
// (i.e. onGetTemplate is not called while the old data is still there)
|
|
||||||
chargers = null
|
|
||||||
availabilities.clear()
|
|
||||||
location = null
|
|
||||||
removeListeners()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removeListeners() {
|
|
||||||
if (supportsCarApiLevel3(carContext)) {
|
|
||||||
println("Removing energy level listener")
|
|
||||||
carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
|
||||||
carSensors.removeCompassListener(::onCompassUpdated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onContentRefreshRequested() {
|
|
||||||
loadChargers()
|
|
||||||
availabilities.clear()
|
|
||||||
|
|
||||||
val start = visibleStart
|
|
||||||
val end = visibleEnd
|
|
||||||
if (start != null && end != null) {
|
|
||||||
onItemVisibilityChanged(start, end)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemVisibilityChanged(startIndex: Int, endIndex: Int) {
|
|
||||||
// when the list is scrolled, load corresponding availabilities
|
|
||||||
if (startIndex == visibleStart && endIndex == visibleEnd && availabilities.isNotEmpty()) return
|
|
||||||
if (startIndex == -1 || endIndex == -1) return
|
|
||||||
if (availabilityUpdateCoroutine != null) return
|
|
||||||
|
|
||||||
visibleEnd = endIndex
|
|
||||||
visibleStart = startIndex
|
|
||||||
|
|
||||||
// remove outdated availabilities
|
|
||||||
availabilities = availabilities.filter {
|
|
||||||
Duration.between(
|
|
||||||
it.value.first,
|
|
||||||
ZonedDateTime.now()
|
|
||||||
) <= availabilityUpdateThreshold
|
|
||||||
}.toMutableMap()
|
|
||||||
|
|
||||||
// update availabilities
|
|
||||||
availabilityUpdateCoroutine = lifecycleScope.launch {
|
|
||||||
delay(300L)
|
|
||||||
|
|
||||||
val chargers = chargers ?: return@launch
|
|
||||||
if (chargers.isEmpty()) return@launch
|
|
||||||
|
|
||||||
val tasks = chargers.subList(
|
|
||||||
min(startIndex, chargers.size - 1),
|
|
||||||
min(endIndex, chargers.size - 1)
|
|
||||||
).mapNotNull {
|
|
||||||
// update only if not yet stored
|
|
||||||
if (!availabilities.containsKey(it.id)) {
|
|
||||||
lifecycleScope.async {
|
|
||||||
val availability = availabilityRepo.getAvailability(it).data
|
|
||||||
val date = ZonedDateTime.now()
|
|
||||||
availabilities[it.id] = date to availability
|
|
||||||
}
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
if (tasks.isNotEmpty()) {
|
|
||||||
tasks.awaitAll()
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
availabilityUpdateCoroutine = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package net.vonforst.evmap.auto
|
|
||||||
|
|
||||||
import androidx.car.app.CarContext
|
|
||||||
import androidx.car.app.Screen
|
|
||||||
import androidx.car.app.annotations.ExperimentalCarApi
|
|
||||||
import androidx.car.app.model.Action
|
|
||||||
import androidx.car.app.model.Header
|
|
||||||
import androidx.car.app.model.ItemList
|
|
||||||
import androidx.car.app.model.ListTemplate
|
|
||||||
import androidx.car.app.model.ParkedOnlyOnClickListener
|
|
||||||
import androidx.car.app.model.Row
|
|
||||||
import androidx.car.app.model.Template
|
|
||||||
import com.car2go.maps.AttributionClickListener
|
|
||||||
import net.vonforst.evmap.R
|
|
||||||
|
|
||||||
@ExperimentalCarApi
|
|
||||||
class MapAttributionScreen(
|
|
||||||
ctx: CarContext,
|
|
||||||
val session: EVMapSession,
|
|
||||||
val attributions: List<AttributionClickListener.Attribution>
|
|
||||||
) : Screen(ctx) {
|
|
||||||
override fun onGetTemplate(): Template {
|
|
||||||
return ListTemplate.Builder()
|
|
||||||
.setHeader(
|
|
||||||
Header.Builder()
|
|
||||||
.setStartHeaderAction(Action.BACK)
|
|
||||||
.setTitle(carContext.getString(R.string.maplibre_attributionsDialogTitle))
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.setSingleList(ItemList.Builder().apply {
|
|
||||||
attributions.forEach { attr ->
|
|
||||||
addItem(
|
|
||||||
Row.Builder()
|
|
||||||
.setTitle(attr.title)
|
|
||||||
.setBrowsable(true)
|
|
||||||
.setOnClickListener(
|
|
||||||
ParkedOnlyOnClickListener.create {
|
|
||||||
openUrl(carContext, session.cas, attr.url)
|
|
||||||
}).build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.build())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,300 +0,0 @@
|
|||||||
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
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.SystemClock
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import androidx.car.app.CarContext
|
|
||||||
import androidx.car.app.SurfaceCallback
|
|
||||||
import androidx.car.app.SurfaceContainer
|
|
||||||
import androidx.car.app.annotations.RequiresCarApi
|
|
||||||
import androidx.core.animation.doOnEnd
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
|
||||||
import com.car2go.maps.AnyMap
|
|
||||||
import com.car2go.maps.AnyMap.CancelableCallback
|
|
||||||
import com.car2go.maps.CameraUpdate
|
|
||||||
import com.car2go.maps.MapContainerView
|
|
||||||
import com.car2go.maps.MapFactory
|
|
||||||
import com.car2go.maps.OnMapReadyCallback
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import net.vonforst.evmap.BuildConfig
|
|
||||||
import net.vonforst.evmap.R
|
|
||||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
|
||||||
import kotlin.math.hypot
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import kotlin.math.roundToLong
|
|
||||||
|
|
||||||
|
|
||||||
class MapSurfaceCallback(val ctx: CarContext, val lifecycleScope: LifecycleCoroutineScope) :
|
|
||||||
SurfaceCallback, OnMapReadyCallback {
|
|
||||||
private val VIRTUAL_DISPLAY_NAME = "evmap_map"
|
|
||||||
private val VELOCITY_THRESHOLD_IGNORE_FLING = 1000
|
|
||||||
|
|
||||||
private val prefs = PreferenceDataSource(ctx)
|
|
||||||
|
|
||||||
private lateinit var virtualDisplay: VirtualDisplay
|
|
||||||
lateinit var presentation: Presentation
|
|
||||||
private lateinit var mapView: MapContainerView
|
|
||||||
private var width: Int = 0
|
|
||||||
private var height: Int = 0
|
|
||||||
private var visibleArea: Rect? = null
|
|
||||||
private var map: AnyMap? = null
|
|
||||||
private val mapCallbacks = mutableListOf<OnMapReadyCallback>()
|
|
||||||
|
|
||||||
private var flingAnimator: ValueAnimator? = null
|
|
||||||
private var idle = true
|
|
||||||
private var idleDelay: Job? = null
|
|
||||||
var cameraMoveStartedListener: (() -> Unit)? = null
|
|
||||||
var cameraIdleListener: (() -> Unit)? = null
|
|
||||||
|
|
||||||
|
|
||||||
override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
|
|
||||||
if (surfaceContainer.surface == null || surfaceContainer.dpi == 0 || surfaceContainer.height == 0 || surfaceContainer.width == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.FINGERPRINT.contains("emulator") || Build.FINGERPRINT.contains("sdk_gcar")) {
|
|
||||||
// fix for MapLibre in Android Automotive Emulators
|
|
||||||
System.setProperty("ro.kernel.qemu", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
width = surfaceContainer.width
|
|
||||||
height = surfaceContainer.height
|
|
||||||
virtualDisplay = ContextCompat
|
|
||||||
.getSystemService(ctx, DisplayManager::class.java)!!
|
|
||||||
.createVirtualDisplay(
|
|
||||||
VIRTUAL_DISPLAY_NAME,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
(surfaceContainer.dpi * when (getMapProvider()) {
|
|
||||||
"mapbox" -> 1.6
|
|
||||||
"google" -> 1.0
|
|
||||||
else -> 1.0
|
|
||||||
}).roundToInt(),
|
|
||||||
surfaceContainer.surface,
|
|
||||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
|
|
||||||
)
|
|
||||||
presentation = Presentation(ctx, virtualDisplay.display, R.style.AppTheme)
|
|
||||||
|
|
||||||
mapView = createMap(presentation.context)
|
|
||||||
mapView.onCreate(null)
|
|
||||||
mapView.onResume()
|
|
||||||
|
|
||||||
presentation.setContentView(mapView)
|
|
||||||
presentation.show()
|
|
||||||
|
|
||||||
mapView.getMapAsync(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMapProvider(): String = if (BuildConfig.FLAVOR_automotive == "automotive") {
|
|
||||||
// Google Maps SDK is not available on AAOS (not even AAOS with GAS, so far)
|
|
||||||
"mapbox"
|
|
||||||
} else prefs.mapProvider
|
|
||||||
|
|
||||||
override fun onVisibleAreaChanged(visibleArea: Rect) {
|
|
||||||
Log.d("MapSurfaceCallback", "visible area: $visibleArea")
|
|
||||||
this.visibleArea = visibleArea
|
|
||||||
updateVisibleArea()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStableAreaChanged(stableArea: Rect) {
|
|
||||||
Log.d("MapSurfaceCallback", "stable area: $stableArea")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
|
|
||||||
mapView.onPause()
|
|
||||||
mapView.onStop()
|
|
||||||
mapView.onDestroy()
|
|
||||||
map = null
|
|
||||||
|
|
||||||
presentation.dismiss()
|
|
||||||
virtualDisplay.release()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresCarApi(2)
|
|
||||||
override fun onScroll(distanceX: Float, distanceY: Float) {
|
|
||||||
flingAnimator?.cancel()
|
|
||||||
val map = map ?: return
|
|
||||||
map.moveCamera(map.cameraUpdateFactory.scrollBy(distanceX, distanceY))
|
|
||||||
dispatchCameraMoveStarted()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresCarApi(2)
|
|
||||||
override fun onFling(velocityX: Float, velocityY: Float) {
|
|
||||||
val map = map ?: return
|
|
||||||
val screenDensity: Float = presentation.resources.displayMetrics.density
|
|
||||||
|
|
||||||
// calculate velocity vector for xy dimensions, independent from screen size
|
|
||||||
val velocityXY =
|
|
||||||
hypot((velocityX / screenDensity).toDouble(), (velocityY / screenDensity).toDouble())
|
|
||||||
if (velocityXY < VELOCITY_THRESHOLD_IGNORE_FLING) {
|
|
||||||
// ignore short flings, these can occur when other gestures just have finished executing
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
idleDelay?.cancel()
|
|
||||||
|
|
||||||
val offsetX = velocityX / 10
|
|
||||||
val offsetY = velocityY / 10
|
|
||||||
val animationTime = (velocityXY / 10).roundToLong()
|
|
||||||
|
|
||||||
flingAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
|
||||||
duration = animationTime
|
|
||||||
interpolator = LinearOutSlowInInterpolator()
|
|
||||||
|
|
||||||
var last = 0f
|
|
||||||
addUpdateListener {
|
|
||||||
val current = it.animatedFraction
|
|
||||||
val diff = last - current
|
|
||||||
map.moveCamera(map.cameraUpdateFactory.scrollBy(diff * offsetX, diff * offsetY))
|
|
||||||
last = current
|
|
||||||
}
|
|
||||||
start()
|
|
||||||
|
|
||||||
doOnEnd { dispatchCameraIdle() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresCarApi(2)
|
|
||||||
override fun onScale(focusX: Float, focusY: Float, scaleFactor: Float) {
|
|
||||||
flingAnimator?.cancel()
|
|
||||||
val map = map ?: return
|
|
||||||
|
|
||||||
val (x, y) = offsetScreen(focusX, focusY)
|
|
||||||
val offsetX = (x - mapView.width / 2) * (scaleFactor - 1f)
|
|
||||||
val offsetY = (y - mapView.height / 2) * (scaleFactor - 1f)
|
|
||||||
|
|
||||||
Log.i("MapSurfaceCallback", "focus: $focusX, $focusY, scaleFactor: $scaleFactor")
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun animateCamera(update: CameraUpdate) {
|
|
||||||
val map = map ?: return
|
|
||||||
map.animateCamera(update, object : CancelableCallback {
|
|
||||||
override fun onFinish() {
|
|
||||||
dispatchCameraIdle()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancel() {
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dispatchCameraMoveStarted() {
|
|
||||||
if (idle) {
|
|
||||||
idle = false
|
|
||||||
cameraMoveStartedListener?.invoke()
|
|
||||||
}
|
|
||||||
idleDelay?.cancel()
|
|
||||||
idleDelay = lifecycleScope.launch {
|
|
||||||
delay(500)
|
|
||||||
dispatchCameraIdle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dispatchCameraIdle() {
|
|
||||||
idle = true
|
|
||||||
cameraIdleListener?.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresCarApi(5)
|
|
||||||
override fun onClick(x: Float, y: Float) {
|
|
||||||
flingAnimator?.cancel()
|
|
||||||
val downTime: Long = SystemClock.uptimeMillis()
|
|
||||||
val eventTime: Long = downTime + 100
|
|
||||||
val (xOffset, yOffset) = offsetScreen(x, y)
|
|
||||||
|
|
||||||
val downEvent = MotionEvent.obtain(
|
|
||||||
downTime,
|
|
||||||
downTime,
|
|
||||||
MotionEvent.ACTION_DOWN,
|
|
||||||
xOffset,
|
|
||||||
yOffset,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
mapView.dispatchTouchEvent(downEvent)
|
|
||||||
downEvent.recycle()
|
|
||||||
val upEvent = MotionEvent.obtain(
|
|
||||||
downTime,
|
|
||||||
eventTime,
|
|
||||||
MotionEvent.ACTION_UP,
|
|
||||||
xOffset,
|
|
||||||
yOffset,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
mapView.dispatchTouchEvent(upEvent)
|
|
||||||
upEvent.recycle()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun offsetScreen(x: Float, y: Float): Pair<Float, Float> {
|
|
||||||
if (BuildConfig.FLAVOR_automotive != "automotive") {
|
|
||||||
return x to y
|
|
||||||
}
|
|
||||||
|
|
||||||
// On AAOS, touch locations don't seem to take into account system bar insets
|
|
||||||
// related: https://issuetracker.google.com/issues/256905247
|
|
||||||
val resId = ctx.resources.getIdentifier("status_bar_height", "dimen", "android")
|
|
||||||
val yOffset = resId.takeIf { it > 0 }?.let { ctx.resources.getDimensionPixelSize(it) } ?: 0
|
|
||||||
|
|
||||||
val xOffset = if (Build.MODEL == "AIVI2 R FULL DOM" && width > height) {
|
|
||||||
// Renault 5 left system bar
|
|
||||||
120
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
return x + xOffset to y + yOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createMap(ctx: Context): MapContainerView {
|
|
||||||
val priority = arrayOf(
|
|
||||||
when (getMapProvider()) {
|
|
||||||
"mapbox" -> MapFactory.MAPLIBRE
|
|
||||||
"google" -> MapFactory.GOOGLE
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
MapFactory.GOOGLE,
|
|
||||||
MapFactory.MAPLIBRE
|
|
||||||
)
|
|
||||||
return MapFactory.createMap(ctx, priority).view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMapReady(anyMap: AnyMap) {
|
|
||||||
this.map = anyMap
|
|
||||||
updateVisibleArea()
|
|
||||||
mapCallbacks.forEach { it.onMapReady(anyMap) }
|
|
||||||
mapCallbacks.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateVisibleArea() {
|
|
||||||
visibleArea?.let {
|
|
||||||
map?.setPadding(it.left, it.top, width - it.right, height - it.bottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMapAsync(callback: OnMapReadyCallback) {
|
|
||||||
mapCallbacks.add(callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user