Compare commits

..

9 Commits

Author SHA1 Message Date
johan12345
2e1e22a44b adjust disk size 2024-05-12 22:29:49 +02:00
johan12345
d638a88f4d update SDK 2024-05-12 18:57:02 +02:00
johan12345
b9c08a8e75 change screenshot size 2024-05-12 15:13:11 +02:00
johan12345
ab2a77d25a remove CleanStatusBar (not needed on ATD) 2024-05-12 14:33:45 +02:00
johan12345
e0ddc9f734 use androidx takeScreenshot 2024-05-10 17:52:28 +02:00
johan12345
168fa244d3 fix name of output file 2024-05-10 00:08:25 +02:00
johan12345
76388ccc6e no emulator metrics 2024-05-09 23:37:16 +02:00
johan12345
76aac7dae4 use ATD system image 2024-05-09 23:35:02 +02:00
johan12345
ee8b586079 implement automatic screenshot generation 2024-05-09 23:17:09 +02:00
352 changed files with 6872 additions and 12306 deletions

View File

@@ -1,11 +1,9 @@
<resources>
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">ci</string>
<string name="mapbox_key" translatable="false">ci</string>
<string name="jawg_key" translatable="false">ci</string>
<string name="arcgis_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="nobil_key" translatable="false">ci</string>
<string name="fronyx_key" translatable="false">ci</string>
<string name="acra_credentials" translatable="false">ci:ci</string>
</resources>

View File

@@ -16,7 +16,7 @@ jobs:
- name: Set up Java environment
uses: actions/setup-java@v4
with:
java-version: 21
java-version: 17
distribution: 'zulu'
cache: 'gradle'
- name: Decrypt keystore
@@ -24,25 +24,19 @@ jobs:
- name: Extract version code
run: echo "VERSION_CODE=$(grep -o "^\s*versionCode\s*=\s*[0-9]\+" app/build.gradle.kts | awk '{ print $3 }' | tr -d \''"\\')" >> $GITHUB_ENV
- name: Build app release & export licenses
- name: Build app release
env:
GOINGELECTRIC_API_KEY: ${{ secrets.GOINGELECTRIC_API_KEY }}
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
CHARGEPRICE_API_KEY: ${{ secrets.CHARGEPRICE_API_KEY }}
MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }}
JAWG_API_KEY: ${{ secrets.JAWG_API_KEY }}
ARCGIS_API_KEY: ${{ secrets.ARCGIS_API_KEY }}
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }}
ACRA_CRASHREPORT_CREDENTIALS: ${{ secrets.ACRA_CRASHREPORT_CREDENTIALS }}
NOBIL_API_KEY: ${{ secrets.NOBIL_API_KEY }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEYSTORE_ALIAS: ${{ secrets.KEYSTORE_ALIAS }}
KEYSTORE_ALIAS_PASSWORD: ${{ secrets.KEYSTORE_ALIAS_PASSWORD }}
run: ./gradlew exportLibraryDefinitions assembleRelease --no-daemon
- name: Export licenses in Appning format
run: python3 _ci/export_licenses_appning.py
run: ./gradlew assembleRelease --no-daemon
- name: release
uses: actions/create-release@v1
@@ -92,40 +86,3 @@ jobs:
asset_path: app/build/outputs/apk/fossAutomotive/release/app-foss-automotive-release.apk
asset_name: app-foss-automotive-release.apk
asset_content_type: application/vnd.android.package-archive
- name: upload Licenses
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/generated/aboutLibraries/aboutlibraries.json
asset_name: aboutlibraries.json
asset_content_type: application/json
- name: upload Licenses Appning
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: licenses_fossAutomotiveRelease_appning.csv
asset_name: licenses_fossAutomotiveRelease_appning.csv
asset_content_type: text/csv
- name: upload Licenses Appning
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: licenses_fossNormalRelease_appning.csv
asset_name: licenses_fossNormalRelease_appning.csv
asset_content_type: text/csv
- name: Trigger Website update
run: |
curl -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ github.token }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/ev-map/ev-map.github.io/dispatches \
-d "{\"event_type\": \"trigger-workflow\"}"

115
.github/workflows/screenshots.yml vendored Normal file
View File

@@ -0,0 +1,115 @@
on:
push:
branches:
- '*'
name: Generate Screenshots
jobs:
screenshot:
name: Generate screenshots
runs-on: ubuntu-latest
strategy:
matrix:
device:
- profile: pixel_6
api-level: 34
display_size: 1080x2336 # subtracted the 64px bottom navigation bar
type: phone
- profile: pixel_tablet
api-level: 34
display_size: 2560x1488 # subtracted the 64px navigation bar and 48px status bar
type: tenInch
env:
ANDROID_USER_HOME: /home/runner/.android
steps:
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Check out code
uses: actions/checkout@v4
- name: Retrieve debug keystore
env:
DEBUG_KEYSTORE_BASE64: ${{ secrets.DEBUG_KEYSTORE_BASE64 }}
run: |
mkdir ~/.config/.android
echo $DEBUG_KEYSTORE_BASE64 | base64 --decode > ~/.config/.android/debug.keystore
- name: Set up Java environment
uses: actions/setup-java@v3
with:
java-version: 17
distribution: 'zulu'
cache: 'gradle'
- name: Setup Android SDK
run: |
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --install "cmdline-tools;latest"
rm -r $ANDROID_HOME/cmdline-tools/latest
mv $ANDROID_HOME/cmdline-tools/latest-2 $ANDROID_HOME/cmdline-tools/latest
$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --version
- name: AVD cache
uses: actions/cache@v3
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: ${{ runner.os }}-avd-api${{ matrix.device.api-level }}-${{ matrix.device.profile }}
- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.device.api-level }}
target: google_atd
arch: x86_64
profile: ${{ matrix.device.profile }}
force-avd-creation: false
ram-size: 2048M
disk-size: 4096M
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics
disable-animations: true
script: echo "Generated AVD snapshot for caching."
- name: Build app
run: ./gradlew assembleGoogleNormalDebug assembleGoogleNormalAndroidTest
env:
GOINGELECTRIC_API_KEY: ${{ secrets.GOINGELECTRIC_API_KEY }}
OPENCHARGEMAP_API_KEY: ${{ secrets.OPENCHARGEMAP_API_KEY }}
MAPBOX_API_KEY: ${{ secrets.MAPBOX_API_KEY }}
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
FRONYX_API_KEY: ${{ secrets.FRONYX_API_KEY }}
ACRA_CRASHREPORT_CREDENTIALS: ${{ secrets.ACRA_CRASHREPORT_CREDENTIALS }}
- name: Run emulator and generate screenshots
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.device.api-level }}
target: google_atd
arch: x86_64
profile: ${{ matrix.device.profile }}
force-avd-creation: false
ram-size: 2048M
disk-size: 4096M
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-metrics
disable-animations: true
script: |
adb shell pm clear net.vonforst.evmap.debug || true
adb shell wm size ${{ matrix.device.display_size }}
adb logcat -c
adb logcat *:E &
fastlane screengrab --app_apk_path app/build/outputs/apk/googleNormal/debug/app-google-normal-debug.apk --test_apk_path app/build/outputs/apk/androidTest/googleNormal/debug/app-google-normal-debug-androidTest.apk --tests_package_name=net.vonforst.evmap.debug.test --app_package_name net.vonforst.evmap.debug -p net.vonforst.evmap.screenshot --use_timestamp_suffix false --clear_previous_screenshots true --device_type=${{ matrix.device.type }} -q en-US,de-DE,fr-FR,nb-rNO,nl-NL,pt-PT,ro-RO
- name: Upload screenshots as artifacts
uses: actions/upload-artifact@v3
with:
name: screenshots-${{ matrix.device.profile }}-${{ matrix.device.api-level }}
path: fastlane/metadata/android

View File

@@ -21,12 +21,12 @@ jobs:
- name: Set up Java environment
uses: actions/setup-java@v4
with:
java-version: 21
java-version: 17
distribution: 'zulu'
cache: 'gradle'
- name: Copy apikeys.xml
run: cp _ci/apikeys-ci.xml app/src/main/res/values/apikeys.xml
run: cp .github/workflows/apikeys-ci.xml app/src/main/res/values/apikeys.xml
- name: Build app
run: ./gradlew assemble${{ matrix.buildvariant }}Debug --no-daemon
@@ -34,55 +34,3 @@ jobs:
run: ./gradlew test${{ matrix.buildvariant }}DebugUnitTest --no-daemon
- name: Run Android Lint
run: ./gradlew lint${{ matrix.buildvariant }}Debug --no-daemon
- name: Check licenses
run: ./gradlew exportLibraryDefinitions --no-daemon
apk_check:
name: Release APK checks (${{ matrix.buildvariant }})
runs-on: ubuntu-latest
strategy:
matrix:
buildvariant: [ FossNormal, FossAutomotive, GoogleNormal, GoogleAutomotive ]
steps:
- name: Install checksec
run: sudo apt install -y checksec
- name: Check out code
uses: actions/checkout@v4
- name: Set up Java environment
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'zulu'
cache: 'gradle'
- name: Copy apikeys.xml
run: cp _ci/apikeys-ci.xml app/src/main/res/values/apikeys.xml
- name: Build app
run: ./gradlew assemble${{ matrix.buildvariant }}Release --no-daemon
- name: Unpack native libraries from APK
run: |
VARIANT_FILENAME=$(echo ${{ matrix.buildvariant }} | sed -E 's/([a-z])([A-Z])/\1-\2/g' | tr 'A-Z' 'a-z')
VARIANT_FOLDER=$(echo ${{ matrix.buildvariant }} | sed -E 's/^([A-Z])/\L\1/')
APK_FILE="app/build/outputs/apk/$VARIANT_FOLDER/release/app-$VARIANT_FILENAME-release-unsigned.apk"
unzip $APK_FILE "lib/*"
- name: Run checksec on native libraries
run: |
checksec --output=json --dir=lib > checksec_output.json
jq --argjson exceptions '[
"lib/armeabi-v7a/libc++_shared.so",
"lib/x86/libc++_shared.so"
]' '
to_entries
| map(select(.value.fortify_source == "no" and (.key as $lib | $exceptions | index($lib) | not)))
| if length > 0 then
error("The following libraries do not have fortify enabled (and are not in the exception list): " + (map(.key) | join(", ")))
else
"All libraries have fortify enabled or are in the exception list."
end
' checksec_output.json

4
.gitignore vendored
View File

@@ -1,6 +1,5 @@
*.iml
.gradle
.kotlin
/local.properties
/.idea/*
.DS_Store
@@ -13,5 +12,4 @@ apikeys.xml
/app/**/*.apk
/_img/connectors/*.ai
api-7125266970515251116-798419-8e2dda660c80.json
output-metadata.json
licenses_*.csv
output-metadata.json

View File

@@ -24,8 +24,7 @@ Features
- Android Auto & Android Automotive OS integration
- No ads, fully open source
- Compatible with Android 5.0 and above
- Can use Google Maps or OpenStreetMap as map backends - the version available on F-Droid only uses
OSM.
- Can use Google Maps or Mapbox (OpenStreetMap) as map backends - the version available on F-Droid only uses Mapbox.
Screenshots
-----------
@@ -43,13 +42,12 @@ EVMap uses and put them into the app in the form of a resource file called `apik
`app/src/main/res/values`. You can find more information on which API keys are necessary for which
features and how they can be obtained in our [documentation page](doc/api_keys.md).
There are four different build flavors, `googleNormal`, `fossNormal`, `googleAutomotive`, and
`fossAutomotive`.
- The `foss` variants only use OSM data for the base map and place search. They should run on most Android devices, even those without Google Play Services.
There are three different build flavors, `googleNormal`, `fossNormal` and `googleAutomotive`.
- The `foss` variants only use Mapbox data and should run on most Android devices, even without
Google Play Services.
- `fossNormal` is intended to run on smartphones and tablets, and also includes the Android
Auto app for use on the car display (however Android Auto may not work if the app is not
installed from Google Play, see https://github.com/ev-map/EVMap/issues/319).
Auto app for use on the car display (however for that to work, the Android Auto app is
necessary, which in turn does require Google Play Services).
- `fossAutomotive` can be installed directly on
[Android Automotive OS (AAOS)](https://source.android.com/docs/automotive/start/what_automotive)
headunits without Google services.
@@ -77,22 +75,5 @@ You can use our [Weblate page](https://hosted.weblate.org/projects/evmap/) to he
into new languages.
<a href="https://hosted.weblate.org/engage/evmap/">
<img src="https://hosted.weblate.org/widgets/evmap/-/open-graph.png" width="400" alt="Translation status" />
<img src="https://hosted.weblate.org/widgets/evmap/-/open-graph.png" width="500" alt="Translation status" />
</a>
Sponsors
--------
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.
<a href="https://www.jawg.io"><img src="https://www.jawg.io/static/Blue@10x-9cdc4596e4e59acbd9ead55e9c28613e.png" alt="JawgMaps" height="38"/></a><br>
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.
<a href="https://chargeprice.app"><img src="https://raw.githubusercontent.com/ev-map/EVMap/master/_img/powered_by_chargeprice.svg" alt="Powered by Chargeprice" height="38"/></a><br>
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.
<a href="https://www.jetbrains.com/community/opensource/"><img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg" alt="JetBrains logo" height="38"/></a><br>
As part of its support program for Open-source projects, **JetBrains** supports the development of EVMap since December 2023 with a license of their software suite.

View File

@@ -1,20 +0,0 @@
import subprocess
import json
build_types = ["fossNormalRelease", "fossAutomotiveRelease"]
for build_type in build_types:
data = json.load(
open(f"app/build/generated/aboutLibraries/{build_type}/res/raw/aboutlibraries.json"))
with open(f"licenses_{build_type}_appning.csv", "w") as f:
f.write("component_name;license_title;license_url;public_repository;copyrights\n")
for lib in data["libraries"]:
license = data["licenses"][lib["licenses"][0]] if len(lib["licenses"]) > 0 else None
license_name = license["name"] if license is not None else " "
license_url = license["url"] if license is not None else " "
copyrights = ", ".join([dev["name"] for dev in lib["developers"] if "name" in dev])
if copyrights == "":
copyrights = " "
repo_url = lib['scm']['url'] if 'scm' in lib else ''
f.write(f"{lib['name']};{license_name};{license_url};\"{copyrights}\";{repo_url}\n")

View File

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

View File

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

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -1,73 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Ebene_2" data-name="Ebene 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<style>
.cls-1 {
fill: #00e676;
}
.cls-1, .cls-2, .cls-3, .cls-4, .cls-5, .cls-6, .cls-7, .cls-8 {
stroke-width: 0px;
}
.cls-2 {
fill: rgba(255, 255, 255, .2);
}
.cls-3 {
fill: #ffb300;
}
.cls-4 {
fill: #000;
isolation: isolate;
opacity: .45;
}
.cls-5 {
fill: #fff;
}
.cls-6 {
fill: #90a4ae;
}
.cls-7 {
fill: #546e7a;
}
.cls-8 {
fill: rgba(62, 39, 35, .2);
}
</style>
</defs>
<g id="Layer_1" data-name="Layer 1">
<g>
<rect class="cls-5" width="512" height="512" />
<g>
<g>
<path class="cls-3"
d="M159.42,338.98l-6.43-56.15-9.81,1.01,6.43,56.15,9.81-1.01ZM194.26,334.92l-6.43-56.15-9.81,1.01,6.43,56.15,9.81-1.01Z" />
<path class="cls-6"
d="M212.53,411.37c-3.04,3.72-5.41,6.09-5.75,6.43-8.79,7.1-15.9,9.13-21.65,6.43-10.15-5.07-9.47-24.02-9.13-26.05l7.1.34c-.34,5.41.68,16.91,5.41,19.28,2.71,1.35,7.44-.34,13.53-5.41h0s19.62-19.62,15.56-35.18c-4.74-18.6,16.91-45.33,24.02-54.46l1.01-1.01,5.75,4.4-1.01,1.35c-21.99,27.06-24.35,40.93-22.66,48.03,3.38,13.53-5.75,28.08-12.18,35.85Z" />
<path class="cls-6"
d="M137.78,338.3l2.71,23,21.31,14.21,28.75-3.04,17.59-18.6-2.71-23-67.65,7.44Z" />
<path class="cls-7"
d="M190.21,372.47l-28.75,3.04,6.09,25.37,22.66-2.71v-25.71h0ZM210.84,311.58l2.37,20.97-82.53,9.47-2.37-20.97,82.53-9.47Z" />
</g>
<g>
<g>
<path class="cls-1"
d="M275.45,80.22c-59.19,0-107.23,48.03-107.23,107.23,0,80.84,90.31,123.12,101.14,238.47.34,3.38,3.04,5.75,6.43,5.75s6.09-2.37,6.43-5.75c10.82-115.34,101.14-157.63,101.14-238.47-.68-59.53-48.71-107.23-107.9-107.23Z" />
<path class="cls-2"
d="M275.45,82.58c58.86,0,106.55,47.36,107.23,105.87v-1.01c0-59.19-48.03-107.23-107.23-107.23s-107.23,47.69-107.23,107.23v1.01c.68-58.52,48.37-105.87,107.23-105.87h0Z" />
<path class="cls-8"
d="M281.87,423.21c-.34,3.38-3.04,5.75-6.43,5.75s-6.09-2.37-6.43-5.75c-10.49-115.01-100.12-157.29-100.8-237.12v1.69c0,80.84,90.31,123.12,101.14,238.47.34,3.38,3.04,5.75,6.43,5.75s6.09-2.37,6.43-5.75c10.82-115.34,101.14-157.63,101.14-238.47v-1.69c-1.35,79.83-90.99,122.11-101.48,237.12h0Z" />
</g>
<path class="cls-4"
d="M250.75,135.01v64.94h17.59v53.11l41.27-71.03h-23.68l23.68-47.36c.34.34-58.86.34-58.86.34Z" />
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

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

View File

@@ -1,42 +1,33 @@
import java.util.Base64
plugins {
id("com.adarshr.test-logger") version "4.0.0"
id("com.adarshr.test-logger") version "3.1.0"
id("com.android.application")
id("kotlin-android")
id("kotlin-parcelize")
id("kotlin-kapt")
id("com.google.devtools.ksp").version("2.0.21-1.0.28")
id("androidx.navigation.safeargs.kotlin")
id("com.mikepenz.aboutlibraries.plugin")
}
android {
useLibrary("android.car")
defaultConfig {
applicationId = "net.vonforst.evmap"
compileSdk = 36
compileSdk = 34
minSdk = 21
targetSdk = 36
targetSdk = 34
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode = 268
versionName = "2.0.2"
versionCode = 212
versionName = "1.8.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
val isRunningOnCI = System.getenv("CI") == "true"
val isCIKeystoreAvailable = System.getenv("KEYSTORE_PASSWORD") != null
signingConfigs {
create("release") {
if (isRunningOnCI && isCIKeystoreAvailable) {
val isRunningOnCI = System.getenv("CI") == "true"
if (isRunningOnCI) {
// configure keystore
storeFile = file("../_ci/keystore.jks")
storePassword = System.getenv("KEYSTORE_PASSWORD")
@@ -53,16 +44,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = if (isRunningOnCI && !isCIKeystoreAvailable) {
null
} else {
signingConfigs.getByName("release")
}
}
create("releaseAutomotivePackageName") {
// Faurecia Aptoide requires the automotive variant to use a separate package name
initWith(getByName("release"))
applicationIdSuffix = ".automotive"
signingConfig = signingConfigs.getByName("release")
}
debug {
applicationIdSuffix = ".debug"
@@ -70,15 +52,10 @@ android {
}
}
sourceSets {
getByName("releaseAutomotivePackageName").setRoot("src/release")
}
flavorDimensions += listOf("dependencies", "automotive")
productFlavors {
create("foss") {
dimension = "dependencies"
isDefault = true
}
create("google") {
dimension = "dependencies"
@@ -86,7 +63,6 @@ android {
}
create("normal") {
dimension = "automotive"
isDefault = true
}
create("automotive") {
dimension = "automotive"
@@ -98,12 +74,18 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs>().configureEach {
kotlinOptions {
jvmTarget = "1.8"
}
}
buildFeatures {
@@ -135,17 +117,6 @@ android {
if (goingelectricKey != null) {
resValue("string", "goingelectric_key", goingelectricKey)
}
var nobilKey =
System.getenv("NOBIL_API_KEY") ?: project.findProperty("NOBIL_API_KEY")?.toString()
if (nobilKey == null && project.hasProperty("NOBIL_API_KEY_ENCRYPTED")) {
nobilKey = decode(
project.findProperty("NOBIL_API_KEY_ENCRYPTED").toString(),
"FmK.d,-f*p+rD+WK!eds"
)
}
if (nobilKey != null) {
resValue("string", "nobil_key", nobilKey)
}
var openchargemapKey =
System.getenv("OPENCHARGEMAP_API_KEY") ?: project.findProperty("OPENCHARGEMAP_API_KEY")
?.toString()
@@ -175,27 +146,17 @@ android {
if (mapboxKey != null) {
resValue("string", "mapbox_key", mapboxKey)
}
var jawgKey =
System.getenv("JAWG_API_KEY") ?: project.findProperty("JAWG_API_KEY")?.toString()
if (jawgKey == null && project.hasProperty("JAWG_API_KEY_ENCRYPTED")) {
jawgKey = decode(
project.findProperty("JAWG_API_KEY_ENCRYPTED").toString(),
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 (jawgKey != null) {
resValue("string", "jawg_key", jawgKey)
}
var arcgisKey =
System.getenv("ARCGIS_API_KEY") ?: project.findProperty("ARCGIS_API_KEY")?.toString()
if (arcgisKey == null && project.hasProperty("ARCGIS_API_KEY_ENCRYPTED")) {
arcgisKey = decode(
project.findProperty("ARCGIS_API_KEY_ENCRYPTED").toString(),
"FmK.d,-f*p+rD+WK!eds"
)
}
if (arcgisKey != null) {
resValue("string", "arcgis_key", jawgKey)
if (chargepriceKey != null) {
resValue("string", "chargeprice_key", chargepriceKey)
}
var fronyxKey =
System.getenv("FRONYX_API_KEY") ?: project.findProperty("FRONYX_API_KEY")?.toString()
@@ -235,43 +196,19 @@ android {
}
}
androidComponents {
beforeVariants { variantBuilder ->
if (variantBuilder.buildType == "releaseAutomotivePackageName"
&& !variantBuilder.productFlavors.containsAll(
listOf(
"automotive" to "automotive",
"dependencies" to "foss"
)
)
) {
// releaseAutomotivePackageName type is only needed for fossAutomotive
variantBuilder.enable = false
}
}
}
configurations {
create("googleNormalImplementation") {}
create("googleAutomotiveImplementation") {}
}
aboutLibraries {
license {
allowedLicenses = setOf(
"Apache-2.0", "mit", "BSD-2-Clause", "BSD-3-Clause", "EPL-1.0",
"asdkl", // Android SDK
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
"Google Maps Platform Terms of Service", // Google Maps SDK
"Unicode/ICU License", "Unicode-3.0", // icu4j
"Bouncy Castle Licence", // bcprov
"CDDL + GPLv2 with classpath exception", // javax.annotation-api
)
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
}
export {
excludeFields = setOf("generated")
}
allowedLicenses = arrayOf(
"Apache-2.0", "mit", "BSD-2-Clause",
"asdkl", // Android SDK
"Dual OpenSSL and SSLeay License", // Android NDK OpenSSL
"Google Maps Platform Terms of Service" // Google Maps SDK
)
strictMode = com.mikepenz.aboutlibraries.plugin.StrictMode.FAIL
}
dependencies {
@@ -285,118 +222,132 @@ dependencies {
val testGoogleImplementation by configurations
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.activity:activity-ktx:1.11.0")
implementation("androidx.fragment:fragment-ktx:1.8.9")
implementation("androidx.activity:activity-ktx:1.8.2")
implementation("androidx.fragment:fragment-ktx:1.6.2")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("com.google.android.material:material:1.13.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.recyclerview:recyclerview:1.4.0")
implementation("androidx.browser:browser:1.9.0")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.browser:browser:1.7.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.viewpager2:viewpager2:1.1.0")
implementation("androidx.security:security-crypto:1.1.0")
implementation("androidx.work:work-runtime-ktx:2.10.5")
implementation("androidx.security:security-crypto:1.1.0-alpha06")
implementation("androidx.work:work-runtime-ktx:2.9.0")
implementation("com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b")
implementation("com.squareup.retrofit2:retrofit:3.0.0")
implementation("com.squareup.retrofit2:converter-moshi:3.0.0")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("com.squareup.okhttp3:okhttp-urlconnection:4.12.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
implementation("com.squareup.moshi:moshi-adapters:1.15.2")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("com.squareup.okhttp3:okhttp-urlconnection:4.11.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
implementation("com.squareup.moshi:moshi-adapters:1.15.0")
implementation("com.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
implementation("io.coil-kt:coil:2.7.0")
implementation("io.coil-kt:coil:2.4.0")
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
implementation("com.mikepenz:aboutlibraries-core:$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("com.google.guava:guava:29.0-android")
implementation("com.github.pengrad:mapscaleview:1.6.0")
implementation("com.github.romandanylyk:PageIndicatorView:b1bad589b5")
implementation("com.github.ev-map:locale-config-x:58b036abf4")
// Android Auto
val carAppVersion = "1.7.0"
val carAppVersion = "1.4.0-rc02"
implementation("androidx.car.app:app:$carAppVersion")
normalImplementation("androidx.car.app:app-projected:$carAppVersion")
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
// AnyMaps
val anyMapsVersion = "65e06c4c9a"
val anyMapsVersion = "4854581f72"
implementation("com.github.ev-map.AnyMaps:anymaps-base:$anyMapsVersion")
googleImplementation("com.github.ev-map.AnyMaps:anymaps-google:$anyMapsVersion")
googleImplementation("com.google.android.gms:play-services-maps:19.2.0")
implementation("com.github.ev-map.AnyMaps:anymaps-maplibre:$anyMapsVersion") {
// duplicates classes from mapbox-sdk-services
exclude("org.maplibre.gl", "android-sdk-geojson")
googleImplementation("com.google.android.gms:play-services-maps:18.2.0")
implementation("com.github.ev-map.AnyMaps:anymaps-mapbox:$anyMapsVersion") {
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-accounts")
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-telemetry")
exclude(group = "com.google.android.gms", module = "play-services-location")
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-core")
}
implementation("org.maplibre.gl:android-sdk:10.3.5") {
exclude("org.maplibre.gl", "android-sdk-geojson")
// original version of mapbox-android-core
googleImplementation("com.mapbox.mapboxsdk:mapbox-android-core:2.0.1")
// patched version that removes build-time dependency on GMS (-> no Google location services)
fossImplementation("com.github.ev-map:mapbox-events-android:a21c324501")
implementation("com.mapbox.mapboxsdk:mapbox-android-sdk") {
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-accounts")
exclude(group = "com.mapbox.mapboxsdk", module = "mapbox-android-telemetry")
version {
strictly("9.1.0-SNAPSHOT")
}
}
// Google Places
googleImplementation("com.google.android.libraries.places:places:3.5.0")
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2")
googleImplementation("com.google.android.libraries.places:places:3.3.0")
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.3")
// Mapbox Geocoding
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.8.0")
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0")
// navigation library
implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
// viewmodel library
val lifecycleVersion = "2.9.2"
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
val lifecycle_version = "2.6.2"
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
// room library
val roomVersion = "2.7.2"
implementation("androidx.room:room-runtime:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
implementation("com.github.anboralabs:spatia-room:0.3.0") {
exclude("com.github.dalgarins", "android-spatialite")
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
kapt("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-ktx:$room_version")
implementation("com.github.anboralabs:spatia-room:0.2.9") {
exclude(group = "com.github.dalgarins", module = "android-spatialite")
}
// forked version with upgraded sqlite & libxml & 16 KB page size support
// https://github.com/dalgarins/android-spatialite/pull/11
// https://github.com/dalgarins/android-spatialite/pull/12
implementation("io.github.ev-map:android-spatialite:2.2.1-alpha")
implementation("com.github.EV-map:android-spatialite:e5495c83ad") // version with minSdk increased to 21 & FORTIFY_SOURCE enabled
// billing library
val billingVersion = "7.0.0"
googleImplementation("com.android.billingclient:billing:$billingVersion")
googleImplementation("com.android.billingclient:billing-ktx:$billingVersion")
val billing_version = "6.1.0"
googleImplementation("com.android.billingclient:billing:$billing_version")
googleImplementation("com.android.billingclient:billing-ktx:$billing_version")
// ACRA (crash reporting)
val acraVersion = "5.12.0"
val acraVersion = "5.11.1"
implementation("ch.acra:acra-http:$acraVersion")
implementation("ch.acra:acra-dialog:$acraVersion")
implementation("ch.acra:acra-limiter:$acraVersion")
// debug tools
debugImplementation("com.jakewharton.timber:timber:5.0.1")
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
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("androidx.test.espresso:espresso-idling-resource:3.5.1")
// testing
testImplementation("junit:junit:4.13.2")
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
testImplementation("com.squareup.okhttp3:mockwebserver:4.11.0")
//noinspection GradleDependency
testImplementation("org.robolectric:robolectric:4.16")
testImplementation("androidx.test:core:1.7.0")
testImplementation("org.json:json:20080701")
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.car.app:app-testing:$carAppVersion")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.5.1")
androidTestImplementation("androidx.test:rules:1.5.0")
androidTestImplementation("androidx.arch.core:core-testing:2.2.0")
androidTestImplementation("tools.fastlane:screengrab:2.1.1")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.15.0")
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 {

View File

@@ -1,904 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 22,
"identityHash": "5dbaaa5adf8cb9b6e8a8314bb7766447",
"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"
},
{
"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": "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": "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, '5dbaaa5adf8cb9b6e8a8314bb7766447')"
]
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,112 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* 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 net.vonforst.evmap.screenshot
import android.view.View
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.FragmentActivity
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.IdlingResource
import androidx.test.ext.junit.rules.ActivityScenarioRule
import java.util.UUID
/**
* An espresso idling resource implementation that reports idle status for all data binding
* layouts. Data Binding uses a mechanism to post messages which Espresso doesn't track yet.
*
* Since this application only uses fragments, the resource only checks the fragments and their
* children instead of the whole view tree.
*
* Tracking bug: https://github.com/android/android-test/issues/317
*/
class DataBindingIdlingResource(
activityScenarioRule: ActivityScenarioRule<out FragmentActivity>
) : IdlingResource {
// list of registered callbacks
private val idlingCallbacks = mutableListOf<IdlingResource.ResourceCallback>()
// give it a unique id to workaround an espresso bug where you cannot register/unregister
// an idling resource w/ the same name.
private val id = UUID.randomUUID().toString()
// holds whether isIdle is called and the result was false. We track this to avoid calling
// onTransitionToIdle callbacks if Espresso never thought we were idle in the first place.
private var wasNotIdle = false
lateinit var activity: FragmentActivity
override fun getName() = "DataBinding $id"
init {
monitorActivity(activityScenarioRule.scenario)
}
override fun isIdleNow(): Boolean {
val idle = !getBindings().any { it.hasPendingBindings() }
@Suppress("LiftReturnOrAssignment")
if (idle) {
if (wasNotIdle) {
// notify observers to avoid espresso race detector
idlingCallbacks.forEach { it.onTransitionToIdle() }
}
wasNotIdle = false
} else {
wasNotIdle = true
// check next frame
activity.findViewById<View>(android.R.id.content).postDelayed({
isIdleNow
}, 16)
}
return idle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
idlingCallbacks.add(callback)
}
/**
* Sets the activity from an [ActivityScenario] to be used from [DataBindingIdlingResource].
*/
private fun monitorActivity(
activityScenario: ActivityScenario<out FragmentActivity>
) {
activityScenario.onActivity {
this.activity = it
}
}
/**
* Find all binding classes in all currently available fragments.
*/
private fun getBindings(): List<ViewDataBinding> {
val fragments = (activity as? FragmentActivity)
?.supportFragmentManager
?.fragments
val bindings =
fragments?.mapNotNull {
it.view?.getBinding()
} ?: emptyList()
val childrenBindings = fragments?.flatMap { it.childFragmentManager.fragments }
?.mapNotNull { it.view?.getBinding() } ?: emptyList()
return bindings + childrenBindings
}
}
private fun View.getBinding(): ViewDataBinding? = DataBindingUtil.getBinding(this)

View File

@@ -0,0 +1,155 @@
package net.vonforst.evmap.screenshot
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.content.Intent
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.pressBack
import androidx.test.espresso.contrib.DrawerActions
import androidx.test.espresso.contrib.NavigationViewActions
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import androidx.test.rule.GrantPermissionRule.grant
import kotlinx.coroutines.runBlocking
import net.vonforst.evmap.EXTRA_CHARGER_ID
import net.vonforst.evmap.EXTRA_LAT
import net.vonforst.evmap.EXTRA_LON
import net.vonforst.evmap.EspressoIdlingResource
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.api.goingelectric.GEReferenceData
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.PreferenceDataSource
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.locale.LocaleTestRule
import java.time.Instant
@RunWith(AndroidJUnit4::class)
class ScreenshotTest {
companion object {
@JvmStatic
@BeforeClass
fun beforeAll() {
IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource)
Screengrab.setDefaultScreenshotStrategy { screenshotName, screenshotCallback ->
screenshotCallback.screenshotCaptured(
screenshotName,
androidx.test.core.app.takeScreenshot()
)
}
val context = InstrumentationRegistry.getInstrumentation().targetContext
val prefs = PreferenceDataSource(context)
prefs.dataSourceSet = true
prefs.welcomeDialogShown = true
prefs.privacyAccepted = true
prefs.opensourceDonationsDialogLastShown = Instant.now()
prefs.chargepriceMyVehicles = setOf("b58bc94d-d929-ad71-d95b-08b877bf76ba")
prefs.appStartCounter = 0
prefs.mapProvider = "google"
// insert favorites
val db = AppDatabase.getInstance(context)
val api = GoingElectricApiWrapper(
context.getString(R.string.goingelectric_key),
context = context
)
val ids = listOf(70774L to true, 40315L to true, 65330L to true, 62489L to false)
runBlocking {
val refData = api.getReferenceData().data as GEReferenceData
ids.forEachIndexed { i, (id, favorite) ->
val detail = api.getChargepointDetail(refData, id).data!!
db.chargeLocationsDao().insert(detail)
if (db.favoritesDao().findFavorite(id, "goingelectric") == null && favorite) {
db.favoritesDao().insert(
Favorite(
chargerId = id,
chargerDataSource = "goingelectric"
)
)
}
}
}
}
}
@get:Rule
val localeTestRule = LocaleTestRule()
@get:Rule
val activityRule: ActivityScenarioRule<MapsActivity> = ActivityScenarioRule(
Intent(
InstrumentationRegistry.getInstrumentation().targetContext,
MapsActivity::class.java
).apply {
putExtra(EXTRA_CHARGER_ID, 62489L)
putExtra(EXTRA_LAT, 53.099512)
putExtra(EXTRA_LON, 9.981884)
})
@get:Rule
val permissionRule: GrantPermissionRule = grant(ACCESS_FINE_LOCATION)
@Before
fun setUp() {
IdlingRegistry.getInstance().register(DataBindingIdlingResource(activityRule))
}
@Test
fun testTakeScreenshot() {
Thread.sleep(15000L)
Screengrab.screenshot("01_map_google")
onView(withId(R.id.topPart)).perform(click())
Thread.sleep(1000L)
Screengrab.screenshot("02_detail")
onView(withId(R.id.btnChargeprice)).perform(click())
Thread.sleep(5000L)
Screengrab.screenshot("03_prices")
onView(isRoot()).perform(pressBack())
Thread.sleep(500L)
onView(isRoot()).perform(pressBack())
Thread.sleep(2000L)
onView(withId(R.id.menu_filter)).perform(click())
Thread.sleep(1000L)
onView(withText(R.string.menu_edit_filters)).perform(click())
Thread.sleep(1000L)
Screengrab.screenshot("05_filters")
onView(isRoot()).perform(pressBack())
onView(withId(R.id.drawer_layout)).perform(DrawerActions.open())
onView(withId(R.id.nav_view)).perform(NavigationViewActions.navigateTo(R.id.favs))
Thread.sleep(10000L)
Screengrab.screenshot("04_favorites")
val context = InstrumentationRegistry.getInstrumentation().targetContext
PreferenceDataSource(context).mapProvider = "mapbox"
onView(isRoot()).perform(pressBack())
Thread.sleep(5000L)
Screengrab.screenshot("01_map_mapbox")
}
}

View File

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

View File

@@ -1,171 +0,0 @@
import android.car.Car
import android.car.VehiclePropertyIds
import android.car.VehicleUnit
import android.car.hardware.CarPropertyValue
import android.car.hardware.property.CarPropertyManager
import android.car.hardware.property.CarPropertyManager.CarPropertyEventCallback
import androidx.annotation.OptIn
import androidx.car.app.CarContext
import androidx.car.app.annotations.ExperimentalCarApi
import androidx.car.app.hardware.CarHardwareManager
import androidx.car.app.hardware.common.CarUnit
import androidx.car.app.hardware.common.CarValue
import androidx.car.app.hardware.common.OnCarDataAvailableListener
import androidx.car.app.hardware.info.CarInfo
import androidx.car.app.hardware.info.EnergyLevel
import androidx.car.app.hardware.info.EnergyProfile
import androidx.car.app.hardware.info.EvStatus
import androidx.car.app.hardware.info.Mileage
import androidx.car.app.hardware.info.Model
import androidx.car.app.hardware.info.Speed
import androidx.car.app.hardware.info.TollCard
import java.util.concurrent.Executor
val CarContext.patchedCarInfo: CarInfo
get() = CarInfoWrapper(this)
class CarInfoWrapper(ctx: CarContext) : CarInfo {
private val wrapped =
(ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager).carInfo
private val carPropertyManager = try {
val car = Car.createCar(ctx)
car.getCarManager(Car.PROPERTY_SERVICE) as CarPropertyManager
} catch (e: NoClassDefFoundError) {
null
}
private val callbacks = mutableMapOf<OnCarDataAvailableListener<*>, CarPropertyEventCallback>()
override fun fetchModel(executor: Executor, listener: OnCarDataAvailableListener<Model>) =
wrapped.fetchModel(executor, listener)
override fun fetchEnergyProfile(
executor: Executor,
listener: OnCarDataAvailableListener<EnergyProfile>
) = wrapped.fetchEnergyProfile(executor, listener)
override fun addTollListener(
executor: Executor,
listener: OnCarDataAvailableListener<TollCard>
) = wrapped.addTollListener(executor, listener)
override fun removeTollListener(listener: OnCarDataAvailableListener<TollCard>) =
wrapped.removeTollListener(listener)
override fun addEnergyLevelListener(
executor: Executor,
listener: OnCarDataAvailableListener<EnergyLevel>
) = wrapped.addEnergyLevelListener(executor, listener)
override fun removeEnergyLevelListener(listener: OnCarDataAvailableListener<EnergyLevel>) =
wrapped.removeEnergyLevelListener(listener)
override fun addSpeedListener(executor: Executor, listener: OnCarDataAvailableListener<Speed>) {
// TODO: This is a emporary workaround until Car App Library 1.7.0 is released - previous versions would crash if the car reported an invalid speed display unit
carPropertyManager ?: return
val callback = object : CarPropertyEventCallback {
private var speedRaw: CarPropertyValue<Float>? = null
private var speedDisplay: CarPropertyValue<Float>? = null
private var speedUnit: CarPropertyValue<Int>? = null
override fun onChangeEvent(value: CarPropertyValue<*>?) {
when (value?.propertyId) {
VehiclePropertyIds.PERF_VEHICLE_SPEED -> speedRaw =
value as CarPropertyValue<Float>?
VehiclePropertyIds.PERF_VEHICLE_SPEED_DISPLAY -> speedDisplay =
value as CarPropertyValue<Float>?
VehiclePropertyIds.VEHICLE_SPEED_DISPLAY_UNITS -> speedUnit =
value as CarPropertyValue<Int>?
}
executor.execute {
listener.onCarDataAvailable(Speed.Builder().apply {
speedRaw?.let {
setRawSpeedMetersPerSecond(
CarValue(
it.value,
it.timestamp,
if (it.value != null) CarValue.STATUS_SUCCESS else CarValue.STATUS_UNKNOWN
)
)
}
speedDisplay?.let {
setDisplaySpeedMetersPerSecond(
CarValue(
it.value,
it.timestamp,
if (it.value != null) CarValue.STATUS_SUCCESS else CarValue.STATUS_UNKNOWN
)
)
}
speedUnit?.let {
val unit = when (it.value) {
VehicleUnit.METER_PER_SEC -> CarUnit.METERS_PER_SEC
VehicleUnit.MILES_PER_HOUR -> CarUnit.MILES_PER_HOUR
VehicleUnit.KILOMETERS_PER_HOUR -> CarUnit.KILOMETERS_PER_HOUR
else -> null
}
setSpeedDisplayUnit(
CarValue(
unit,
it.timestamp,
if (unit != null) CarValue.STATUS_SUCCESS else CarValue.STATUS_UNKNOWN
)
)
}
}.build())
}
}
override fun onErrorEvent(propertyId: Int, areaId: Int) {
listener.onCarDataAvailable(
Speed.Builder()
.setRawSpeedMetersPerSecond(CarValue(null, 0, CarValue.STATUS_UNKNOWN))
.setDisplaySpeedMetersPerSecond(CarValue(null, 0, CarValue.STATUS_UNKNOWN))
.setSpeedDisplayUnit(CarValue(null, 0, CarValue.STATUS_UNKNOWN))
.build()
)
}
}
carPropertyManager.registerCallback(
callback,
VehiclePropertyIds.PERF_VEHICLE_SPEED,
CarPropertyManager.SENSOR_RATE_NORMAL
)
carPropertyManager.registerCallback(
callback,
VehiclePropertyIds.PERF_VEHICLE_SPEED_DISPLAY,
CarPropertyManager.SENSOR_RATE_NORMAL
)
carPropertyManager.registerCallback(
callback,
VehiclePropertyIds.VEHICLE_SPEED_DISPLAY_UNITS,
CarPropertyManager.SENSOR_RATE_NORMAL
)
}
override fun removeSpeedListener(listener: OnCarDataAvailableListener<Speed>) {
val callback = callbacks[listener] ?: return
carPropertyManager?.unregisterCallback(callback)
}
override fun addMileageListener(
executor: Executor,
listener: OnCarDataAvailableListener<Mileage>
) = wrapped.addMileageListener(executor, listener)
override fun removeMileageListener(listener: OnCarDataAvailableListener<Mileage>) =
wrapped.removeMileageListener(listener)
@OptIn(ExperimentalCarApi::class)
override fun addEvStatusListener(
executor: Executor,
listener: OnCarDataAvailableListener<EvStatus>
) = wrapped.addEvStatusListener(executor, listener)
@OptIn(ExperimentalCarApi::class)
override fun removeEvStatusListener(listener: OnCarDataAvailableListener<EvStatus>) =
wrapped.removeEvStatusListener(listener)
}

View File

@@ -2,4 +2,4 @@
<resources>
<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>
</resources>
</resources>

View File

@@ -2,4 +2,4 @@
<resources>
<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>
</resources>
</resources>

View File

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

View File

@@ -2,4 +2,4 @@
<resources>
<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>
</resources>
</resources>

View File

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

View File

@@ -2,4 +2,4 @@
<resources>
<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>
</resources>
</resources>

View File

@@ -2,4 +2,4 @@
<resources>
<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>
</resources>
</resources>

View File

@@ -2,4 +2,4 @@
<resources>
<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>
</resources>
</resources>

View File

@@ -1,3 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
<resources></resources>

View File

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

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions needed for fastlane screengrab -->
<!-- Allows unlocking your device and activating its screen so UI tests can succeed -->
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Allows for storing and retrieving screenshots -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Allows changing locales -->
<uses-permission
android:name="android.permission.CHANGE_CONFIGURATION"
tools:ignore="ProtectedPermissions" />
<!-- Allows changing SystemUI demo mode -->
<uses-permission
android:name="android.permission.DUMP"
tools:ignore="ProtectedPermissions" />
<application>
<activity
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
android:exported="true" />
</application>
</manifest>

View File

@@ -2,15 +2,63 @@ package net.vonforst.evmap
import android.content.Context
import android.os.Build
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.idling.CountingIdlingResource
import com.facebook.flipper.android.AndroidFlipperClient
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
import com.facebook.flipper.plugins.inspector.DescriptorMapping
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
import com.facebook.soloader.SoLoader
import okhttp3.OkHttpClient
import timber.log.Timber
private val networkFlipperPlugin = NetworkFlipperPlugin()
fun addDebugInterceptors(context: Context) {
if (Build.FINGERPRINT == "robolectric") return
Timber.plant(Timber.DebugTree())
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()
}
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
}
/**
* Contains a static reference to [IdlingResource], only available in the 'debug' build type.
*/
object EspressoIdlingResource {
private const val RESOURCE = "GLOBAL"
@JvmField
val countingIdlingResource = CountingIdlingResource(RESOURCE)
fun increment() {
countingIdlingResource.increment()
}
fun decrement() {
if (!countingIdlingResource.isIdleNow) {
countingIdlingResource.decrement()
}
}
}

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

View File

@@ -5,6 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.transition.MaterialSharedAxis
@@ -42,7 +43,7 @@ class DonateFragment : DonateFragmentBase() {
)
binding.btnDonate.setOnClickListener {
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link), binding.root)
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link))
}
setupReferrals(referrals)

View File

@@ -2,5 +2,5 @@
<resources>
<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="data_sources_hint">Mapová data v aplikaci poskytuje služba OpenStreetMap.</string>
</resources>
<string name="data_sources_hint">Mapová data v aplikaci poskytuje služba OpenStreetMap (Mapbox).</string>
</resources>

View File

@@ -2,5 +2,5 @@
<resources>
<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="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap.</string>
</resources>
<string name="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap (Mapbox).</string>
</resources>

View File

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

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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="data_sources_hint">Les données cartographiques de l\'application sont fournies par OpenStreetMap.</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 (Mapbox).</string>
<string name="donate_paypal">Faire un don avec PayPal</string>
</resources>
</resources>

View File

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

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donate_paypal">Doner med PayPal</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>
</resources>
<string name="data_sources_hint">Kartdata i programmet tilbys av OpenStreetMap (Mapbox).</string>
<string name="donations_info" formatted="false">Synes du EVMap er nyttig\? Støtt utviklingen ved å sende en slant til utvikleren.</string>
</resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap.</string>
</resources>
<string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap (Mapbox).</string>
</resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<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 (Mapbox).</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>
</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.</string>
</resources>

View File

@@ -2,5 +2,5 @@
<resources>
<string name="donations_info" formatted="false">Crezi ca EVMap este util? Sprijina dezvoltarea printr-o donatie pentru dezvoltator.</string>
<string name="donate_paypal">Doneaza cu PayPal</string>
<string name="data_sources_hint">Hartile din aplicatie sunt furnizate de OpenStreetMap.</string>
<string name="data_sources_hint">Hartile din aplicatie sunt furnizate de OpenStreetMap (Mapbox).</string>
</resources>

View File

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

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_map_provider_names">
<item>@string/pref_provider_osm</item>
<item>@string/pref_provider_osm_mapbox</item>
</string-array>
<string-array name="pref_map_provider_values" translatable="false">
<item>mapbox</item>

View File

@@ -2,5 +2,5 @@
<resources>
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.</string>
<string name="donate_paypal">Donate with PayPal</string>
<string name="data_sources_hint">Map data in the app is provided by OpenStreetMap.</string>
<string name="data_sources_hint">Map data in the app is provided by OpenStreetMap (Mapbox).</string>
</resources>

View File

@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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="data_sources_hint">V nastavení můžete také pro mapová data přepínat mezi službami Mapy Google a OpenStreetMap.</string>
</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="data_sources_hint">V nastavení můžete také pro mapová data přepínat mezi službami Mapy Google a OpenStreetMap (Mapbox).</string>
</resources>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<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="data_sources_hint">In den Einstellungen kannst du auch zwischen Google Maps und OpenStreetMap für die Kartendaten wechseln.</string>
</resources>
<string name="data_sources_hint">In den Einstellungen kannst du auch zwischen Google Maps und OpenStreetMap (Mapbox) für die Kartendaten wechseln.</string>
</resources>

View File

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

View File

@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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="data_sources_hint">Dans les paramètres, vous pouvez également choisir entre Google Maps et OpenStreetMap pour les données cartographiques.</string>
</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="data_sources_hint">Dans les paramètres, vous pouvez également choisir entre Google Maps et OpenStreetMap (Mapbox) pour les données cartographiques.</string>
</resources>

View File

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

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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="data_sources_hint">I innstillingene kan du også bytte mellom Google Maps og OpenStreetMap (Mapbox) for kartdata.</string>
</resources>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<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="data_sources_hint">I innstillingene kan du også bytte mellom Google Maps og OpenStreetMap for kartdata.</string>
</resources>

View File

@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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="data_sources_hint">In de instellingen kan je ook wisselen tussen Google Maps en OpenStreetMap voor de kaartgegevens.</string>
</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="data_sources_hint">In de instellingen kan je ook wisselen tussen Google Maps en OpenStreetMap (Mapbox) voor de kaartgegevens.</string>
</resources>

View File

@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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="data_sources_hint">Também pode mudar entre o Google Maps e OpenStreetMap nas definições da app.</string>
</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="data_sources_hint">Também pode mudar entre o Google Maps e OpenStreetMap (Mapbox) nas definições da app.</string>
</resources>

View File

@@ -1,3 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
<resources></resources>

View File

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

View File

@@ -2,7 +2,7 @@
<resources>
<string-array name="pref_map_provider_names">
<item>@string/pref_provider_google_maps</item>
<item>@string/pref_provider_osm</item>
<item>@string/pref_provider_osm_mapbox</item>
</string-array>
<string-array name="pref_map_provider_values" translatable="false">
<item>google</item>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 15% off every donation.</string>
<string name="data_sources_hint">In the settings you can also switch between Google Maps and OpenStreetMap for the map data.</string>
<string name="data_sources_hint">In the settings you can also switch between Google Maps and OpenStreetMap (Mapbox) for the map data.</string>
</resources>

View File

@@ -9,7 +9,6 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<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.ACCESS_SURFACE" />
<uses-permission android:name="com.google.android.gms.permission.CAR_FUEL" />
<uses-permission android:name="com.google.android.gms.permission.CAR_SPEED" />
@@ -25,10 +24,6 @@
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<package android:name="com.google.android.projection.gearhead" />
<package android:name="com.google.android.apps.automotive.templates.host" />
@@ -52,14 +47,6 @@
android:name="com.mapbox.ACCESS_TOKEN"
android:value="@string/mapbox_key" />
<meta-data
android:name="io.jawg.ACCESS_TOKEN"
android:value="@string/jawg_key" />
<meta-data
android:name="com.arcgis.ACCESS_TOKEN"
android:value="@string/arcgis_key" />
<activity
android:name=".MapsActivity"
android:label="@string/app_name"
@@ -352,8 +339,9 @@
android:exported="true"
android:foregroundServiceType="location">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
<category android:name="androidx.car.app.category.POI" />
<action
android:name="androidx.car.app.CarAppService"
android:category="androidx.car.app.category.POI" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 +
'}';
}
}

View File

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

View File

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

View File

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

View File

@@ -3,15 +3,9 @@ package net.vonforst.evmap
import android.app.Activity
import android.app.Application
import android.os.Build
import androidx.work.Configuration
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.*
import net.vonforst.evmap.storage.CleanupCacheWorker
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.storage.UpdateFullDownloadWorker
import net.vonforst.evmap.ui.updateAppLocale
import net.vonforst.evmap.ui.updateNightMode
import org.acra.config.dialog
@@ -30,7 +24,7 @@ class EvMapApplication : Application(), Configuration.Provider {
// Convert to new AppCompat storage for app language
val lang = prefs.language
if (lang != null) {
if (lang != null && lang !in listOf("", "default")) {
updateAppLocale(lang)
prefs.language = null
}
@@ -70,7 +64,6 @@ class EvMapApplication : Application(), Configuration.Provider {
}
}
val workManager = WorkManager.getInstance(this)
val cleanupCacheRequest = PeriodicWorkRequestBuilder<CleanupCacheWorker>(Duration.ofDays(1))
.setConstraints(Constraints.Builder().apply {
setRequiresBatteryNotLow(true)
@@ -78,24 +71,9 @@ class EvMapApplication : Application(), Configuration.Provider {
setRequiresDeviceIdle(true)
}
}.build()).build()
workManager.enqueueUniquePeriodicWork(
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"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()

View File

@@ -1,10 +1,9 @@
package net.vonforst.evmap
import android.app.ActivityOptions
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -13,12 +12,12 @@ import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.navigation.NavController
@@ -46,21 +45,22 @@ const val EXTRA_DONATE = "donate"
class MapsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
interface FragmentCallback {
fun getRootView(): View
}
private var reenterState: Bundle? = null
private lateinit var navController: NavController
private lateinit var navHostFragment: NavHostFragment
lateinit var appBarConfiguration: AppBarConfiguration
var fragmentCallback: FragmentCallback? = null
private lateinit var prefs: PreferenceDataSource
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
WindowCompat.enableEdgeToEdge(window)
setContentView(R.layout.activity_maps)
val drawerLayout = findViewById<DrawerLayout>(R.id.drawer_layout)
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.map,
@@ -68,9 +68,9 @@ class MapsActivity : AppCompatActivity(),
R.id.about,
R.id.settings
),
drawerLayout
findViewById<DrawerLayout>(R.id.drawer_layout)
)
navHostFragment =
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
val navGraph = navController.navInflater.inflate(R.navigation.nav_graph)
@@ -88,17 +88,6 @@ class MapsActivity : AppCompatActivity(),
checkPlayServices(this)
navController.setGraph(navGraph, MapFragmentArgs(appStart = true).toBundle())
var deepLink: PendingIntent? = null
navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id == R.id.onboarding) {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
} else {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
}
}
if (!prefs.welcomeDialogShown || !prefs.dataSourceSet) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// wait for splash screen animation to finish on first start
@@ -116,104 +105,110 @@ class MapsActivity : AppCompatActivity(),
}
})
}
} else if (intent?.scheme == "geo") {
val query = intent.data?.query?.split("=")?.get(1)
val coords = getLocationFromIntent(intent)
navGraph.setStartDestination(R.id.onboarding)
navController.graph = navGraph
return
} else if (!prefs.privacyAccepted) {
navGraph.setStartDestination(R.id.onboarding)
navController.graph = navGraph
return
} else {
navGraph.setStartDestination(R.id.map)
navController.setGraph(navGraph, MapFragmentArgs(appStart = true).toBundle())
var deepLink: PendingIntent? = null
if (coords != null) {
val lat = coords[0]
val lon = coords[1]
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(latLng = LatLng(lat, lon)).toBundle())
.createPendingIntent()
} else if (!query.isNullOrEmpty()) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(locationName = query).toBundle())
.createPendingIntent()
}
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
val id = intent.data?.pathSegments?.lastOrNull()?.toLongOrNull()
if (id != null) {
if (prefs.dataSource != "goingelectric") {
prefs.dataSource = "goingelectric"
Toast.makeText(
this,
getString(
R.string.data_source_switched_to,
getString(R.string.data_source_goingelectric)
),
Toast.LENGTH_LONG
).show()
if (intent?.scheme == "geo") {
val query = intent.data?.query?.split("=")?.get(1)
val coords = getLocationFromIntent(intent)
if (coords != null) {
val lat = coords[0]
val lon = coords[1]
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(latLng = LatLng(lat, lon)).toBundle())
.createPendingIntent()
} else if (!query.isNullOrEmpty()) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(locationName = query).toBundle())
.createPendingIntent()
}
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
}
} else if (intent?.scheme == "https" && intent?.data?.host in listOf(
"openchargemap.org",
"map.openchargemap.io"
)
) {
val id = when (intent.data?.host) {
"openchargemap.org" -> intent.data?.pathSegments?.lastOrNull()?.toLongOrNull()
"map.openchargemap.io" -> intent.data?.getQueryParameter("id")?.toLongOrNull()
else -> null
}
if (id != null) {
if (prefs.dataSource != "openchargemap") {
prefs.dataSource = "openchargemap"
Toast.makeText(
this,
getString(
R.string.data_source_switched_to,
getString(R.string.data_source_openchargemap)
),
Toast.LENGTH_LONG
).show()
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
if (id != null) {
if (prefs.dataSource != "goingelectric") {
prefs.dataSource = "goingelectric"
Toast.makeText(
this,
getString(
R.string.data_source_switched_to,
getString(R.string.data_source_goingelectric)
),
Toast.LENGTH_LONG
).show()
}
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
}
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
}
} else if (intent.scheme == "net.vonforst.evmap") {
intent.data?.let {
if (it.host == "find_charger") {
val lat = it.getQueryParameter("latitude")?.toDouble()
val lon = it.getQueryParameter("longitude")?.toDouble()
val name = it.getQueryParameter("name")
if (lat != null && lon != null) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(
MapFragmentArgs(
latLng = LatLng(lat, lon),
locationName = name
).toBundle()
)
.createPendingIntent()
} else if (name != null) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(locationName = name).toBundle())
.createPendingIntent()
} else if (intent?.scheme == "https" && intent?.data?.host in listOf("openchargemap.org", "map.openchargemap.io")) {
val id = when (intent.data?.host) {
"openchargemap.org" -> intent.data?.pathSegments?.last()?.toLongOrNull()
"map.openchargemap.io" -> intent.data?.getQueryParameter("id")?.toLongOrNull()
else -> null
}
if (id != null) {
if (prefs.dataSource != "openchargemap") {
prefs.dataSource = "openchargemap"
Toast.makeText(
this,
getString(
R.string.data_source_switched_to,
getString(R.string.data_source_openchargemap)
),
Toast.LENGTH_LONG
).show()
}
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(chargerId = id).toBundle())
.createPendingIntent()
}
} else if (intent.scheme == "net.vonforst.evmap") {
intent.data?.let {
if (it.host == "find_charger") {
val lat = it.getQueryParameter("latitude")?.toDouble()
val lon = it.getQueryParameter("longitude")?.toDouble()
val name = it.getQueryParameter("name")
if (lat != null && lon != null) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(
MapFragmentArgs(
latLng = LatLng(lat, lon),
locationName = name
).toBundle()
)
.createPendingIntent()
} else if (name != null) {
deepLink = navController.createDeepLink()
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.map)
.setArguments(MapFragmentArgs(locationName = name).toBundle())
.createPendingIntent()
}
}
}
}
} else if (intent.hasExtra(EXTRA_CHARGER_ID)) {
deepLink = navController.createDeepLink()
.setDestination(R.id.map)
.setArguments(
MapFragmentArgs(
} else if (intent.hasExtra(EXTRA_CHARGER_ID)) {
navController.navigate(
R.id.map, MapFragmentArgs(
chargerId = intent.getLongExtra(EXTRA_CHARGER_ID, 0),
latLng = LatLng(
intent.getDoubleExtra(EXTRA_LAT, 0.0),
@@ -221,23 +216,23 @@ class MapsActivity : AppCompatActivity(),
)
).toBundle()
)
.createPendingIntent()
} else if (intent.hasExtra(EXTRA_FAVORITES)) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
.setDestination(R.id.favs)
.createPendingIntent()
} else if (intent.hasExtra(EXTRA_DONATE)) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
.setDestination(R.id.donate)
.createPendingIntent()
}
} else if (intent.hasExtra(EXTRA_FAVORITES)) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
.setDestination(R.id.favs)
.createPendingIntent()
} else if (intent.hasExtra(EXTRA_DONATE)) {
deepLink = navController.createDeepLink()
.setGraph(navGraph)
.setDestination(R.id.donate)
.createPendingIntent()
}
deepLink?.send()
deepLink?.send()
}
}
fun navigateTo(charger: ChargeLocation, rootView: View) {
fun navigateTo(charger: ChargeLocation) {
// google maps navigation
val coord = charger.coordinates
val intent = Intent(Intent.ACTION_VIEW)
@@ -247,11 +242,11 @@ class MapsActivity : AppCompatActivity(),
startActivity(intent)
} else {
// fallback: generic geo intent
showLocation(charger, rootView)
showLocation(charger)
}
}
fun showLocation(charger: ChargeLocation, rootView: View) {
fun showLocation(charger: ChargeLocation) {
val coord = charger.coordinates
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(
@@ -259,33 +254,20 @@ class MapsActivity : AppCompatActivity(),
Uri.encode(charger.name)
})"
)
val resolveInfo =
packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
val pkg =
resolveInfo?.activityInfo?.packageName.takeIf { it != "android" && it != packageName }
if (pkg == null) {
// There is no default maps app or EVMap itself is the current default, fall back to app chooser
val chooserIntent = Intent.createChooser(intent, null).apply {
putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(componentName))
}
startActivity(chooserIntent)
return
}
intent.setPackage(pkg)
try {
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
} else {
val cb = fragmentCallback ?: return
Snackbar.make(
rootView,
cb.getRootView(),
R.string.no_maps_app_found,
Snackbar.LENGTH_SHORT
).show()
}
}
fun openUrl(url: String, rootView: View, preferBrowser: Boolean = false) {
fun openUrl(url: String, preferBrowser: Boolean = true) {
val pkg = CustomTabsClient.getPackageName(this, null)
val intent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
@@ -293,49 +275,17 @@ class MapsActivity : AppCompatActivity(),
.build()
)
.build()
val uri = Uri.parse(url)
val viewIntent = Intent(Intent.ACTION_VIEW, uri)
if (preferBrowser) {
// EVMap may be set as default app for this link, but we want to open it in a browser
// try to find default web browser
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://"))
val resolveInfo =
packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY)
val pkg = resolveInfo?.activityInfo?.packageName.takeIf { it != "android" }
if (pkg == null) {
// There is no default browser, fall back to app chooser
val chooserIntent = Intent.createChooser(viewIntent, null).apply {
putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(componentName))
}
val targets: List<ResolveInfo> = packageManager.queryIntentActivities(
viewIntent,
PackageManager.MATCH_DEFAULT_ONLY
)
// add missing browsers (if EVMap is already set as default, Android might not find other browsers with the specific intent)
val browsers = packageManager.queryIntentActivities(
browserIntent,
PackageManager.MATCH_DEFAULT_ONLY
)
val extraIntents = browsers.filter { browser ->
targets.find { it.activityInfo.packageName == browser.activityInfo.packageName } == null
}.map { browser ->
Intent(Intent.ACTION_VIEW, uri).apply {
setPackage(browser.activityInfo.packageName)
}
}
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toTypedArray())
startActivity(chooserIntent)
return
}
intent.intent.setPackage(pkg)
pkg?.let {
// prefer to open URL in custom tab, even if native app
// available (such as EVMap itself)
if (preferBrowser) intent.intent.setPackage(pkg)
}
try {
intent.launchUrl(this, uri)
intent.launchUrl(this, Uri.parse(url))
} catch (e: ActivityNotFoundException) {
val cb = fragmentCallback ?: return
Snackbar.make(
rootView,
cb.getRootView(),
R.string.no_browser_app_found,
Snackbar.LENGTH_SHORT
).show()

View File

@@ -16,10 +16,7 @@ import android.text.SpannableStringBuilder
import android.text.SpannedString
import android.text.TextUtils
import android.text.style.StyleSpan
import android.view.View
import android.view.ViewTreeObserver
import net.vonforst.evmap.storage.PreferenceDataSource
import java.util.Currency
import java.util.Locale
fun Bundle.optDouble(name: String): Double? {
@@ -142,14 +139,4 @@ fun PackageManager.isAppInstalled(packageName: String): Boolean {
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
fun currencyDisplayName(code: String) = "${Currency.getInstance(code).displayName} ($code)"
inline fun View.waitForLayout(crossinline f: () -> Unit) =
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
})
}

View File

@@ -1,16 +1,27 @@
package net.vonforst.evmap.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.Chip
import net.vonforst.evmap.BR
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.chargeprice.ChargePrice
import net.vonforst.evmap.api.chargeprice.ChargepriceCar
import net.vonforst.evmap.api.chargeprice.ChargepriceChargepointMeta
import net.vonforst.evmap.api.chargeprice.ChargepriceTag
import net.vonforst.evmap.databinding.ItemChargepriceBinding
import net.vonforst.evmap.databinding.ItemChargepriceVehicleChipBinding
import net.vonforst.evmap.databinding.ItemConnectorButtonBinding
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.ui.CheckableConstraintLayout
import java.time.Instant
interface Equatable {
@@ -95,4 +106,141 @@ class ConnectorDetailsAdapter : DataBindingAdapter<ConnectorDetailsAdapter.Conne
Equatable
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
}

View File

@@ -9,12 +9,12 @@ import androidx.core.text.buildSpannedString
import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.tesla.Pricing
import net.vonforst.evmap.api.availability.tesla.Rates
import net.vonforst.evmap.api.availability.tesla.TeslaChargingOwnershipGraphQlApi
import net.vonforst.evmap.bold
import net.vonforst.evmap.joinToSpannedString
import net.vonforst.evmap.model.ChargeCard
import net.vonforst.evmap.model.ChargeCardId
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Coordinate
import net.vonforst.evmap.model.OpeningHoursDays
import net.vonforst.evmap.plus
import net.vonforst.evmap.ui.currency

View File

@@ -5,15 +5,16 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.ImageView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.load
import coil.memory.MemoryCache
import net.vonforst.evmap.BuildConfig
import net.vonforst.evmap.R
import net.vonforst.evmap.model.ChargerPhoto
import net.vonforst.evmap.waitForLayout
class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener? = null) :
@@ -39,9 +40,12 @@ class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener?
val item = getItem(position)
if (holder.view.height == 0) {
holder.view.waitForLayout {
loadImage(item, holder)
}
holder.view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
holder.view.viewTreeObserver.removeOnGlobalLayoutListener(this)
loadImage(item, holder)
}
})
} else {
loadImage(item, holder)
}
@@ -67,7 +71,7 @@ class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener?
memoryKeys[item.id] = metadata.memoryCacheKey
}
)
allowHardware(false)
allowHardware(!BuildConfig.DEBUG)
}
}
}

View File

@@ -5,9 +5,7 @@ import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import net.vonforst.evmap.R
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.nobil.NobilApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.api.openstreetmap.OpenStreetMapApiWrapper
import net.vonforst.evmap.model.*
import net.vonforst.evmap.viewmodel.Resource
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.
*/
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 {
@@ -95,13 +72,6 @@ fun Context.stringProvider() = object : StringProvider {
fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
return when (type) {
"nobil" -> {
NobilApiWrapper(
ctx.getString(
R.string.nobil_key
)
)
}
"openchargemap" -> {
OpenChargeMapApiWrapper(
ctx.getString(
@@ -109,7 +79,6 @@ fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
)
)
}
"goingelectric" -> {
GoingElectricApiWrapper(
ctx.getString(
@@ -117,11 +86,6 @@ fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
)
)
}
"openstreetmap" -> {
OpenStreetMapApiWrapper()
}
else -> throw IllegalArgumentException()
}
}
@@ -136,20 +100,4 @@ data class ChargepointList(val items: List<ChargepointListItem>, val isComplete:
companion object {
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
}

View File

@@ -1,20 +1,18 @@
package net.vonforst.evmap.api
import com.google.common.util.concurrent.RateLimiter
import okhttp3.Interceptor
import okhttp3.Response
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.TimeSource
class RateLimitInterceptor : Interceptor {
private val rateLimiter = SimpleRateLimiter(3.0)
private val rateLimiter = RateLimiter.create(3.0)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.url.host == "ui-map.shellrecharge.com") {
// limit requests sent to NewMotion to 3 per second
rateLimiter.acquire()
rateLimiter.acquire(1)
var response: Response = chain.proceed(request)
// 403 is how the NewMotion API indicates a rate limit error
@@ -32,27 +30,4 @@ class RateLimitInterceptor : Interceptor {
return chain.proceed(request)
}
}
}
internal class SimpleRateLimiter(private val permitsPerSecond: Double) {
private val interval: Duration = (1.0 / permitsPerSecond).seconds
private var nextAvailable = TimeSource.Monotonic.markNow()
@Synchronized
fun acquire() {
val now = TimeSource.Monotonic.markNow()
if (now < nextAvailable) {
val waitTime = nextAvailable - now
waitTime.sleep()
nextAvailable += interval
} else {
nextAvailable = now + interval
}
}
}
fun Duration.sleep() {
if (this.isPositive()) {
Thread.sleep(this.inWholeMilliseconds, (this.inWholeNanoseconds % 1_000_000).toInt())
}
}

View File

@@ -1,17 +1,54 @@
package net.vonforst.evmap.api
import androidx.annotation.DrawableRes
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine
import net.vonforst.evmap.R
import net.vonforst.evmap.model.Chargepoint
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
import org.json.JSONArray
import java.io.IOException
import kotlin.coroutines.resumeWithException
import kotlin.math.abs
operator fun <T> JSONArray.iterator(): Iterator<T> =
(0 until length()).asSequence().map {
@Suppress("UNCHECKED_CAST")
get(it) as T
}.iterator()
@ExperimentalCoroutinesApi
suspend fun Call.await(): Response {
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
continuation.resume(response) {}
}
override fun onFailure(call: Call, e: IOException) {
if (continuation.isCancelled) return
continuation.resumeWithException(e)
}
})
continuation.invokeOnCancellation {
try {
cancel()
} catch (ex: Throwable) {
//Ignore cancel exception
}
}
}
}
private val plugNames = mapOf(
Chargepoint.TYPE_1 to R.string.plug_type_1,
Chargepoint.TYPE_2_UNKNOWN to R.string.plug_type_2,
Chargepoint.TYPE_2_PLUG to R.string.plug_type_2_tethered,
Chargepoint.TYPE_2_PLUG 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_3C to R.string.plug_type_3c,
Chargepoint.TYPE_3 to R.string.plug_type_3,
Chargepoint.CCS_UNKNOWN to R.string.plug_ccs,
Chargepoint.CCS_TYPE_1 to R.string.plug_ccs,
Chargepoint.CCS_TYPE_2 to R.string.plug_ccs,
@@ -64,7 +101,7 @@ fun iconForPlugType(type: String): Int =
Chargepoint.CEE_ROT -> R.drawable.ic_connector_cee_rot
Chargepoint.TYPE_1 -> R.drawable.ic_connector_typ1
// 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)

View File

@@ -7,6 +7,8 @@ import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.addDebugInterceptors
import net.vonforst.evmap.api.RateLimitInterceptor
import net.vonforst.evmap.api.await
import net.vonforst.evmap.api.chargeprice.ChargepriceApi
import net.vonforst.evmap.api.equivalentPlugTypes
import net.vonforst.evmap.cartesianProduct
import net.vonforst.evmap.model.ChargeLocation
@@ -16,6 +18,7 @@ import net.vonforst.evmap.viewmodel.Resource
import okhttp3.Cache
import okhttp3.JavaNetCookieJar
import okhttp3.OkHttpClient
import okhttp3.Request
import retrofit2.HttpException
import java.io.IOException
import java.net.CookieManager
@@ -38,6 +41,16 @@ interface AvailabilityDetector {
abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : AvailabilityDetector {
protected val radius = 150 // max radius in meters
protected suspend fun httpGet(url: String): String {
val request = Request.Builder().url(url).build()
val response = client.newCall(request).await()
if (!response.isSuccessful) throw IOException(response.message)
val str = response.body!!.string()
return str
}
protected fun getCorrespondingChargepoint(
cps: Iterable<Chargepoint>, type: String, power: Double
): Chargepoint? {

View File

@@ -4,6 +4,7 @@ import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson
import net.vonforst.evmap.api.availability.tesla.LocalTimeAdapter
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Chargepoint
import net.vonforst.evmap.utils.distanceBetween
@@ -14,6 +15,7 @@ import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
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 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 power = connector.maxPowerInKw ?: 0.0
val type = when (connector.plugTypeName) {
"Typ 3A" -> Chargepoint.TYPE_3A
"Typ 3C \"Scame\"" -> Chargepoint.TYPE_3C
"Typ 3A" -> Chargepoint.TYPE_3
"Typ 2" -> Chargepoint.TYPE_2_UNKNOWN
"Typ 1 Steckdose" -> Chargepoint.TYPE_1
"Typ 1" -> Chargepoint.TYPE_1
"Steckdose(D)" -> Chargepoint.SCHUKO
"CCS (Typ 1)" -> Chargepoint.CCS_TYPE_1 // US CCS, aka type1_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 {
val country = charger.chargepriceData?.country ?: charger.address?.country
val country = charger.chargepriceData?.country
?: charger.address?.country ?: return false
return when (charger.dataSource) {
// list of countries as of 2023/04/14, according to
// https://www.enbw.com/elektromobilitaet/produkte/ladetarife
@@ -266,7 +267,6 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
"Spanien",
"Tschechien"
) && charger.network != "Tesla Supercharger"
"nobil" -> charger.network != "Tesla"
"openchargemap" -> country in listOf(
"DE",
"AT",
@@ -286,12 +286,6 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
"ES",
"CZ"
) && 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
}
}

View File

@@ -1,5 +1,6 @@
package net.vonforst.evmap.api.availability
import androidx.car.app.model.DateTimeWithZone
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
@@ -12,8 +13,12 @@ import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
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 maxDistance = 60 // max distance between reported positions in meters
@@ -175,7 +180,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
val id = connector.uid
val power = connector.electricalProperties.getPower()
val type = when (connector.connectorType.lowercase(Locale.ROOT)) {
"type3" -> Chargepoint.TYPE_3C
"type3" -> Chargepoint.TYPE_3
"type2" -> Chargepoint.TYPE_2_UNKNOWN
"type1" -> Chargepoint.TYPE_1
"domestic" -> Chargepoint.SCHUKO
@@ -220,9 +225,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
// NewMotion is our fallback
return when (charger.dataSource) {
"goingelectric" -> charger.network != "Tesla Supercharger"
"nobil" -> charger.network != "Tesla"
"openchargemap" -> charger.chargepriceData?.network !in listOf("23", "3534")
"openstreetmap" -> charger.operator !in listOf("Tesla, Inc.", "Tesla")
else -> false
}
}

View File

@@ -23,10 +23,6 @@ class TeslaGuestAvailabilityDetector(
private var api = TeslaChargingGuestGraphQlApi.create(client, baseUrl)
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 result =
@@ -56,70 +52,37 @@ class TeslaGuestAvailabilityDetector(
val (detailsA, guestPricing) = coroutineScope {
val details = async {
api.getSiteDetails(
TeslaChargingGuestGraphQlApi.GetSiteDetailsRequest(
TeslaChargingGuestGraphQlApi.GetSiteDetailsVariables(
api.getChargingSiteDetails(
TeslaChargingGuestGraphQlApi.GetChargingSiteDetailsRequest(
TeslaChargingGuestGraphQlApi.GetChargingSiteInformationVariables(
TeslaChargingGuestGraphQlApi.Identifier(
TeslaChargingGuestGraphQlApi.ChargingSiteIdentifier(
trtId, TeslaChargingGuestGraphQlApi.Experience.ADHOC
trtId
)
),
TeslaChargingGuestGraphQlApi.Experience.ADHOC
)
)
).data.chargingNetwork?.site
?: throw AvailabilityDetectorException("no candidates found.")
).data.site ?: throw AvailabilityDetectorException("no candidates found.")
}
val guestPricing = async {
api.getSiteDetails(
TeslaChargingGuestGraphQlApi.GetSiteDetailsRequest(
TeslaChargingGuestGraphQlApi.GetSiteDetailsVariables(
api.getChargingSiteDetails(
TeslaChargingGuestGraphQlApi.GetChargingSiteDetailsRequest(
TeslaChargingGuestGraphQlApi.GetChargingSiteInformationVariables(
TeslaChargingGuestGraphQlApi.Identifier(
TeslaChargingGuestGraphQlApi.ChargingSiteIdentifier(
trtId, TeslaChargingGuestGraphQlApi.Experience.GUEST
trtId
)
),
TeslaChargingGuestGraphQlApi.Experience.GUEST
)
)
).data.chargingNetwork?.site?.pricing
).data.site?.pricing
}
details to guestPricing
}
val details = detailsA.await()
if (location.dataSource == "nobil") {
// TODO: Lots of copy & paste here. The main difference for nobil data
// is that V2 chargers don't have duplicated connectors.
var detailsSorted = details.chargerList
.sortedBy { c -> c.labelLetter }
.sortedBy { c -> c.labelNumber }
if (detailsSorted.size != location.chargepoints.size) {
// TODO: Tesla data could also be missing for connectors
throw AvailabilityDetectorException("charger has unknown connectors")
}
val detailsMap =
mutableMapOf<Chargepoint, List<TeslaChargingGuestGraphQlApi.ChargerDetail>>()
var i = 0
for (connector in location.chargepointsMerged) {
detailsMap[connector] =
detailsSorted.subList(i, i + connector.count)
i += connector.count
}
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
val labelsMap = detailsMap.mapValues { it.value.map { it.label } }
val pricing = details.pricing?.copy(memberRates = guestPricing.await()?.userRates)
return ChargeLocationStatus(
statusMap,
"Tesla",
labels = labelsMap,
extraData = pricing
)
}
val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
val scV2CCSConnectors = location.chargepoints.filter {
it.type in listOf(
@@ -140,9 +103,12 @@ class TeslaGuestAvailabilityDetector(
"charger has unknown connectors"
)
var detailsSorted = details.chargerList
.sortedBy { c -> c.labelLetter }
.sortedBy { c -> c.labelNumber }
val chargerDetails = details.chargersAvailable.chargerDetails
val chargers = details.chargers.associateBy { it.id }
var detailsSorted = chargerDetails
.sortedBy { chargers[it.id]?.labelLetter }
.sortedBy { chargers[it.id]?.labelNumber }
if (detailsSorted.size != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) {
// apparently some connectors are missing in Tesla data
@@ -154,7 +120,7 @@ class TeslaGuestAvailabilityDetector(
detailsSorted + List(numMissing) {
TeslaChargingGuestGraphQlApi.ChargerDetail(
ChargerAvailability.UNKNOWN,
"", ""
""
)
}
} else {
@@ -185,9 +151,9 @@ class TeslaGuestAvailabilityDetector(
}
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 { chargers[it.id]?.label } }
val pricing = details.pricing?.copy(memberRates = guestPricing.await()?.userRates)
val pricing = details.pricing.copy(memberRates = guestPricing.await()?.userRates)
return ChargeLocationStatus(
statusMap,
@@ -200,9 +166,7 @@ class TeslaGuestAvailabilityDetector(
override fun isChargerSupported(charger: ChargeLocation): Boolean {
return when (charger.dataSource) {
"goingelectric" -> charger.network == "Tesla Supercharger"
"nobil" -> charger.network == "Tesla"
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
"openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla")
else -> false
}
}

View File

@@ -2,6 +2,7 @@ package net.vonforst.evmap.api.availability
import net.vonforst.evmap.api.availability.tesla.ChargerAvailability
import net.vonforst.evmap.api.availability.tesla.TeslaAuthenticationApi
import net.vonforst.evmap.api.availability.tesla.TeslaChargingGuestGraphQlApi
import net.vonforst.evmap.api.availability.tesla.TeslaChargingOwnershipGraphQlApi
import net.vonforst.evmap.api.availability.tesla.asTeslaCoord
import net.vonforst.evmap.model.ChargeLocation
@@ -29,10 +30,6 @@ class TeslaOwnerAvailabilityDetector(
}
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 req = TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesRequest(
TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesVariables(
@@ -45,7 +42,8 @@ class TeslaOwnerAvailabilityDetector(
TeslaChargingOwnershipGraphQlApi.Coordinate(
location.coordinates.lat - coordRange,
location.coordinates.lng + coordRange
)
),
TeslaChargingOwnershipGraphQlApi.OpenToNonTeslasFilterValue(false)
)
)
)
@@ -61,60 +59,13 @@ class TeslaOwnerAvailabilityDetector(
val details = api.getChargingSiteInformation(
TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationRequest(
TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationVariables(
TeslaChargingOwnershipGraphQlApi.ChargingSiteIdentifier(result.locationGUID),
TeslaChargingOwnershipGraphQlApi.ChargingSiteIdentifier(result.id.text),
TeslaChargingOwnershipGraphQlApi.VehicleMakeType.NON_TESLA
)
)
).data.charging.site ?: throw AvailabilityDetectorException("no candidates found.")
if (location.dataSource == "nobil") {
// TODO: Lots of copy & paste here. The main difference for nobil data
// is that V2 chargers don't have duplicated connectors.s
val chargerDetails = details.siteDynamic.chargerDetails
val chargers = details.siteStatic.chargers.associateBy { it.id }
var detailsSorted = chargerDetails
.sortedBy { c -> c.charger.labelLetter ?: chargers[c.charger.id]?.labelLetter }
.sortedBy { c -> c.charger.labelNumber ?: chargers[c.charger.id]?.labelNumber }
if (detailsSorted.size != location.chargepoints.size) {
// TODO: Code below suggests tesla data could also be missing for
// connectors
throw AvailabilityDetectorException("Tesla API chargepoints do not match data source")
}
val congestionHistogram = details.congestionPriceHistogram?.let { cph ->
val indexOfMidnight = cph.dataAttributes.indexOfFirst { it.label == "12AM" }
indexOfMidnight.takeIf { it >= 0 }?.let { index ->
val data = cph.data.toMutableList()
Collections.rotate(data, -index)
data
}
}
val detailsMap =
emptyMap<Chargepoint, List<TeslaChargingOwnershipGraphQlApi.ChargerDetail>>().toMutableMap()
var i = 0
for (connector in location.chargepointsMerged) {
detailsMap[connector] =
detailsSorted.subList(i, i + connector.count)
i += connector.count
}
val statusMap = detailsMap.mapValues { it.value.map { it.availability.toStatus() } }
val labelsMap = detailsMap.mapValues {
it.value.map {
it.charger.label?.value ?: chargers[it.charger.id]?.label?.value
}
}
return ChargeLocationStatus(
statusMap,
"Tesla",
labels = labelsMap,
congestionHistogram = congestionHistogram,
extraData = details.pricing
)
}
val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
val scV2CCSConnectors = location.chargepoints.filter {
it.type in listOf(
@@ -212,9 +163,7 @@ class TeslaOwnerAvailabilityDetector(
override fun isChargerSupported(charger: ChargeLocation): Boolean {
return when (charger.dataSource) {
"goingelectric" -> charger.network == "Tesla Supercharger"
"nobil" -> charger.network == "Tesla"
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
"openstreetmap" -> charger.operator in listOf("Tesla, Inc.", "Tesla")
else -> false
}
}

View File

@@ -58,7 +58,7 @@ data class Rates(
@JsonClass(generateAdapter = true)
data class Pricebook(
val charging: PricebookDetails,
val parking: PricebookDetails?,
val parking: PricebookDetails,
val priceBookID: Long?
)

View File

@@ -1,8 +1,12 @@
package net.vonforst.evmap.api.availability.tesla
import com.squareup.moshi.Json
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import okhttp3.CacheControl
import okhttp3.OkHttpClient
import retrofit2.Retrofit
@@ -11,6 +15,7 @@ import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import java.lang.reflect.Type
import java.util.concurrent.TimeUnit
interface TeslaCuaApi {
@@ -66,22 +71,24 @@ interface TeslaCuaApi {
interface TeslaChargingGuestGraphQlApi {
@POST("graphql")
suspend fun getSiteDetails(
@Body request: GetSiteDetailsRequest,
@Query("operationName") operationName: String = "GetSiteDetails"
suspend fun getChargingSiteDetails(
@Body request: GetChargingSiteDetailsRequest,
@Query("operationName") operationName: String = "getGuestChargingSiteDetails"
): GetChargingSiteDetailsResponse
@JsonClass(generateAdapter = true)
data class GetSiteDetailsRequest(
override val variables: GetSiteDetailsVariables,
override val operationName: String = "GetSiteDetails",
data class GetChargingSiteDetailsRequest(
override val variables: GetChargingSiteInformationVariables,
override val operationName: String = "getGuestChargingSiteDetails",
override val query: String =
"\n query GetSiteDetails(\$siteId: SiteIdInput!) {\n chargingNetwork {\n site(siteId: \$siteId) {\n address {\n countryCode\n }\n chargerList {\n id\n label\n availability\n }\n holdAmount {\n amount\n currencyCode\n }\n maxPowerKw\n name\n programType\n publicStallCount\n trtId\n pricing {\n userRates {\n activePricebook {\n charging {\n ...ChargingRate\n }\n parking {\n ...ChargingRate\n }\n congestion {\n ...ChargingRate\n }\n }\n }\n }\n }\n }\n}\n \n fragment ChargingRate on ChargingUserRate {\n uom\n rates\n buckets {\n start\n end\n }\n bucketUom\n currencyCode\n programType\n vehicleMakeType\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n}\n "
"\n query getGuestChargingSiteDetails(\$identifier: ChargingSiteIdentifierInput!, \$deviceLocale: String!, \$experience: ChargingExperienceEnum!) {\n site(\n identifier: \$identifier\n deviceLocale: \$deviceLocale\n experience: \$experience\n ) {\n activeOutages\n address {\n countryCode\n }\n chargers {\n id\n label\n }\n chargersAvailable {\n chargerDetails {\n id\n availability\n }\n }\n holdAmount {\n holdAmount\n currencyCode\n }\n maxPowerKw\n name\n programType\n publicStallCount\n id\n pricing(experience: \$experience) {\n userRates {\n activePricebook {\n charging {\n uom\n rates\n buckets {\n start\n end\n }\n bucketUom\n currencyCode\n programType\n vehicleMakeType\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n }\n parking {\n uom\n rates\n buckets {\n start\n end\n }\n bucketUom\n currencyCode\n programType\n vehicleMakeType\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n }\n congestion {\n uom\n rates\n buckets {\n start\n end\n }\n bucketUom\n currencyCode\n programType\n vehicleMakeType\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n }\n }\n }\n }\n }\n}\n "
) : GraphQlRequest()
@JsonClass(generateAdapter = true)
data class GetSiteDetailsVariables(
val siteId: Identifier,
data class GetChargingSiteInformationVariables(
val identifier: Identifier,
val experience: Experience,
val deviceLocale: String = "de-DE",
)
enum class Experience {
@@ -90,22 +97,22 @@ interface TeslaChargingGuestGraphQlApi {
@JsonClass(generateAdapter = true)
data class Identifier(
val byTrtId: ChargingSiteIdentifier
val siteId: ChargingSiteIdentifier
)
@JsonClass(generateAdapter = true)
data class ChargingSiteIdentifier(
val trtId: Long,
val chargingExperience: Experience,
val programType: String = "PTSCH",
val locale: String = "de-DE",
val id: Long,
val siteType: SiteType = SiteType.SUPERCHARGER
)
@JsonClass(generateAdapter = true)
data class GetChargingSiteDetailsResponse(val data: GetChargingSiteDetailsResponseDataNetwork)
enum class SiteType {
@Json(name = "SITE_TYPE_SUPERCHARGER")
SUPERCHARGER
}
@JsonClass(generateAdapter = true)
data class GetChargingSiteDetailsResponseDataNetwork(val chargingNetwork: GetChargingSiteDetailsResponseData?)
data class GetChargingSiteDetailsResponse(val data: GetChargingSiteDetailsResponseData)
@JsonClass(generateAdapter = true)
data class GetChargingSiteDetailsResponseData(val site: ChargingSiteInformation?)
@@ -113,19 +120,19 @@ interface TeslaChargingGuestGraphQlApi {
@JsonClass(generateAdapter = true)
data class ChargingSiteInformation(
val activeOutages: List<Outage>?,
val chargerList: List<ChargerDetail>,
val trtId: Long,
val maxPowerKw: Int?,
val chargers: List<ChargerId>,
val chargersAvailable: ChargersAvailable,
val id: Long,
val maxPowerKw: Int,
val name: String,
val pricing: Pricing?,
val pricing: Pricing,
val publicStallCount: Int
)
@JsonClass(generateAdapter = true)
data class ChargerDetail(
val availability: ChargerAvailability,
data class ChargerId(
val id: String,
val label: String?,
val id: String
) {
val labelNumber
get() = label?.replace(Regex("""\D"""), "")?.toInt()
@@ -133,6 +140,15 @@ interface TeslaChargingGuestGraphQlApi {
get() = label?.replace(Regex("""\d"""), "")
}
@JsonClass(generateAdapter = true)
data class ChargersAvailable(val chargerDetails: List<ChargerDetail>)
@JsonClass(generateAdapter = true)
data class ChargerDetail(
val availability: ChargerAvailability,
val id: String
)
companion object {
fun create(
client: OkHttpClient,

View File

@@ -16,6 +16,7 @@ import retrofit2.http.POST
import retrofit2.http.Query
import java.security.MessageDigest
import java.security.SecureRandom
import java.time.LocalTime
interface TeslaAuthenticationApi {
@POST("oauth2/v3/token")
@@ -100,8 +101,7 @@ interface TeslaAuthenticationApi {
.appendQueryParameter("code_challenge_method", "S256")
.appendQueryParameter("redirect_uri", "https://auth.tesla.com/void/callback")
.appendQueryParameter("response_type", "code")
.appendQueryParameter("scope", "openid email offline_access phone")
.appendQueryParameter("is_in_app", "true")
.appendQueryParameter("scope", "openid email offline_access")
.appendQueryParameter("state", "123").build()
val resultUrlPrefix = "https://auth.tesla.com/void/callback"
@@ -131,8 +131,8 @@ interface TeslaOwnerApi {
// add API key to every request
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $token")
.header("User-Agent", "okhttp/4.11.0")
.header("x-tesla-user-agent", "TeslaApp/4.44.5-3304/3a5d531cc3/android/27")
.header("User-Agent", "okhttp/4.9.2")
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27")
.header("Accept", "*/*")
.build()
chain.proceed(request)
@@ -173,7 +173,7 @@ interface TeslaChargingOwnershipGraphQlApi {
override val variables: GetNearbyChargingSitesVariables,
override val operationName: String = "GetNearbyChargingSites",
override val query: String =
"\n query GetNearbyChargingSites(\$args: GetNearbyChargingSitesRequestType!) {\n charging {\n nearbySites(args: \$args) {\n sitesAndDistances {\n ...ChargingNearbySitesFragment\n }\n }\n }\n}\n \n fragment ChargingNearbySitesFragment on ChargerSiteAndDistanceType {\n availableStalls {\n value\n }\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n drivingDistanceMiles {\n value\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n haversineDistanceMiles {\n value\n }\n id {\n text\n }\n locationGUID\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n trtId {\n value\n }\n totalStalls {\n value\n }\n siteType\n accessType\n waitEstimateBucket\n hasHighCongestion\n teslaExclusive\n amenities\n chargingAccessibility\n ownerType\n isThirdPartySite\n usabilityArchetype\n accessHours {\n shouldDisplay\n openNow\n hour\n }\n isMagicDockSupportedSite\n hasParkingBenefit\n hasTou\n}\n \n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n"
"\n query GetNearbyChargingSites(\$args: GetNearbyChargingSitesRequestType!) {\n charging {\n nearbySites(args: \$args) {\n sitesAndDistances {\n ...ChargingNearbySitesFragment\n }\n }\n }\n}\n \n fragment ChargingNearbySitesFragment on ChargerSiteAndDistanceType {\n activeOutages {\n message\n nonTeslasAffectedOnly {\n value\n }\n }\n availableStalls {\n value\n }\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n drivingDistanceMiles {\n value\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n haversineDistanceMiles {\n value\n }\n id {\n text\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n trtId {\n value\n }\n totalStalls {\n value\n }\n siteType\n accessType\n waitEstimateBucket\n hasHighCongestion\n}\n \n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n "
) : GraphQlRequest()
@JsonClass(generateAdapter = true)
@@ -184,7 +184,7 @@ interface TeslaChargingOwnershipGraphQlApi {
val userLocation: Coordinate,
val northwestCorner: Coordinate,
val southeastCorner: Coordinate,
val filters: List<String> = emptyList(),
val openToNonTeslasFilter: OpenToNonTeslasFilterValue,
val languageCode: String = "en",
val countryCode: String = "US",
//val vin: String = "",
@@ -202,7 +202,7 @@ interface TeslaChargingOwnershipGraphQlApi {
override val variables: GetChargingSiteInformationVariables,
override val operationName: String = "getChargingSiteInformation",
override val query: String =
"\n query getChargingSiteInformation(\$id: ChargingSiteIdentifierInputType!, \$vehicleMakeType: ChargingVehicleMakeTypeEnum, \$deviceCountry: String!, \$deviceLanguage: String!) {\n charging {\n site(\n id: \$id\n deviceCountry: \$deviceCountry\n deviceLanguage: \$deviceLanguage\n vehicleMakeType: \$vehicleMakeType\n ) {\n siteStatic {\n ...SiteStaticFragmentNoHoldAmt\n }\n siteDynamic {\n ...SiteDynamicFragment\n }\n pricing(vehicleMakeType: \$vehicleMakeType) {\n userRates {\n ...ChargingActiveRateFragment\n }\n memberRates {\n ...ChargingActiveRateFragment\n }\n hasMembershipPricing\n hasMSPPricing\n canDisplayCombinedComparison\n }\n holdAmount(vehicleMakeType: \$vehicleMakeType) {\n currencyCode\n holdAmount\n }\n congestionPriceHistogram(vehicleMakeType: \$vehicleMakeType) {\n ...CongestionPriceHistogramFragment\n }\n upsellingBanner(vehicleMakeType: \$vehicleMakeType) {\n header\n caption\n backgroundImageUrl\n routeName\n }\n nacsOnlyAssets {\n banner {\n header\n caption\n link\n }\n disclaimer {\n text\n sheetTitle\n sheetContent\n }\n }\n enableChargingSiteReportIssue\n }\n }\n}\n \n fragment SiteStaticFragmentNoHoldAmt on ChargingSiteStaticType {\n address {\n ...AddressFragment\n }\n amenities\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n id {\n text\n }\n locationGUID\n accessCode {\n value\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n name\n openToPublic\n chargers {\n id {\n text\n }\n label {\n value\n }\n }\n publicStallCount\n timeZone {\n id\n version\n }\n fastchargeSiteId {\n value\n }\n siteType\n accessType\n isThirdPartySite\n isMagicDockSupportedSite\n trtId {\n value\n }\n siteDisclaimer\n chargingAccessibility\n accessHours {\n shouldDisplay\n openNow\n hour\n }\n isCanvasSite\n ownerDisclaimer\n chargingFeesDisclaimer {\n title\n description\n }\n idleFeesDisclaimer {\n title\n description\n }\n}\n \n fragment AddressFragment on EnergySvcAddressType {\n streetNumber {\n value\n }\n street {\n value\n }\n district {\n value\n }\n city {\n value\n }\n state {\n value\n }\n postalCode {\n value\n }\n country\n}\n \n\n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n \n\n fragment SiteDynamicFragment on ChargingSiteDynamicType {\n id {\n text\n }\n chargersAvailable {\n value\n }\n chargerDetails {\n charger {\n id {\n text\n }\n label {\n value\n }\n name\n }\n availability\n stateOfCharge\n chargerDisabled\n }\n waitEstimateBucket\n currentCongestion\n usabilityArchetype\n}\n \n\n fragment ChargingActiveRateFragment on ChargingActiveRateType {\n activePricebook {\n charging {\n dynamicRates {\n enabled\n }\n ...ChargingUserRateFragment\n }\n parking {\n ...ChargingUserRateFragment\n }\n congestion {\n ...ChargingUserRateFragment\n }\n service {\n ...ChargingUserRateFragment\n }\n electricity {\n ...ChargingUserRateFragment\n }\n priceBookID\n }\n}\n \n fragment ChargingUserRateFragment on ChargingUserRateType {\n currencyCode\n programType\n rates\n buckets {\n start\n end\n }\n bucketUom\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n uom\n vehicleMakeType\n stateOfCharge\n congestionGracePeriodSecs\n congestionPercent\n}\n \n\n fragment CongestionPriceHistogramFragment on HistogramData {\n axisLabels {\n index\n value\n }\n regionLabels {\n index\n value {\n ...ChargingPriceFragment\n ... on HistogramRegionLabelValueString {\n value\n }\n }\n }\n chargingUom\n parkingUom\n parkingRate {\n ...ChargingPriceFragment\n }\n data\n activeBar\n maxRateIndex\n whenRateChanges\n dataAttributes {\n congestionThreshold\n label\n }\n}\n \n fragment ChargingPriceFragment on ChargingPrice {\n currencyCode\n price\n}\n"
"\n query getChargingSiteInformation(\$id: ChargingSiteIdentifierInputType!, \$vehicleMakeType: ChargingVehicleMakeTypeEnum, \$deviceCountry: String!, \$deviceLanguage: String!) {\n charging {\n site(\n id: \$id\n deviceCountry: \$deviceCountry\n deviceLanguage: \$deviceLanguage\n vehicleMakeType: \$vehicleMakeType\n ) {\n siteStatic {\n ...SiteStaticFragmentNoHoldAmt\n }\n siteDynamic {\n ...SiteDynamicFragment\n }\n pricing(vehicleMakeType: \$vehicleMakeType) {\n userRates {\n ...ChargingActiveRateFragment\n }\n memberRates {\n ...ChargingActiveRateFragment\n }\n hasMembershipPricing\n hasMSPPricing\n canDisplayCombinedComparison\n }\n holdAmount(vehicleMakeType: \$vehicleMakeType) {\n currencyCode\n holdAmount\n }\n congestionPriceHistogram(vehicleMakeType: \$vehicleMakeType) {\n ...CongestionPriceHistogramFragment\n }\n }\n }\n}\n \n fragment SiteStaticFragmentNoHoldAmt on ChargingSiteStaticType {\n address {\n ...AddressFragment\n }\n amenities\n centroid {\n ...EnergySvcCoordinateTypeFields\n }\n entryPoint {\n ...EnergySvcCoordinateTypeFields\n }\n id {\n text\n }\n accessCode {\n value\n }\n localizedSiteName {\n value\n }\n maxPowerKw {\n value\n }\n name\n openToPublic\n chargers {\n id {\n text\n }\n label {\n value\n }\n }\n publicStallCount\n timeZone {\n id\n version\n }\n fastchargeSiteId {\n value\n }\n siteType\n accessType\n isMagicDockSupportedSite\n trtId {\n value\n }\n}\n \n fragment AddressFragment on EnergySvcAddressType {\n streetNumber {\n value\n }\n street {\n value\n }\n district {\n value\n }\n city {\n value\n }\n state {\n value\n }\n postalCode {\n value\n }\n country\n}\n \n\n fragment EnergySvcCoordinateTypeFields on EnergySvcCoordinateType {\n latitude\n longitude\n}\n \n\n fragment SiteDynamicFragment on ChargingSiteDynamicType {\n id {\n text\n }\n activeOutages {\n message\n nonTeslasAffectedOnly {\n value\n }\n }\n chargersAvailable {\n value\n }\n chargerDetails {\n charger {\n id {\n text\n }\n label {\n value\n }\n name\n }\n availability\n }\n waitEstimateBucket\n currentCongestion\n}\n \n\n fragment ChargingActiveRateFragment on ChargingActiveRateType {\n activePricebook {\n charging {\n ...ChargingUserRateFragment\n }\n parking {\n ...ChargingUserRateFragment\n }\n priceBookID\n }\n}\n \n fragment ChargingUserRateFragment on ChargingUserRateType {\n currencyCode\n programType\n rates\n buckets {\n start\n end\n }\n bucketUom\n touRates {\n enabled\n activeRatesByTime {\n startTime\n endTime\n rates\n }\n }\n uom\n vehicleMakeType\n}\n \n\n fragment CongestionPriceHistogramFragment on HistogramData {\n axisLabels {\n index\n value\n }\n regionLabels {\n index\n value {\n ...ChargingPriceFragment\n ... on HistogramRegionLabelValueString {\n value\n }\n }\n }\n chargingUom\n parkingUom\n parkingRate {\n ...ChargingPriceFragment\n }\n data\n activeBar\n maxRateIndex\n whenRateChanges\n dataAttributes {\n congestionThreshold\n label\n }\n}\n \n fragment ChargingPriceFragment on ChargingPrice {\n currencyCode\n price\n}\n"
) : GraphQlRequest()
@JsonClass(generateAdapter = true)
@@ -217,11 +217,11 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true)
data class ChargingSiteIdentifier(
val id: String,
val type: ChargingSiteIdentifierType = ChargingSiteIdentifierType.LOCATION_GUID
val type: ChargingSiteIdentifierType = ChargingSiteIdentifierType.SITE_ID
)
enum class ChargingSiteIdentifierType {
SITE_ID, LOCATION_GUID
SITE_ID
}
enum class VehicleMakeType {
@@ -242,6 +242,7 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true)
data class ChargingSite(
val activeOutages: List<Outage>,
val availableStalls: Value<Int>?,
val centroid: Coordinate,
val drivingDistanceMiles: Value<Double>?,
@@ -250,8 +251,7 @@ interface TeslaChargingOwnershipGraphQlApi {
val id: Text,
val localizedSiteName: Value<String>,
val maxPowerKw: Value<Int>,
val totalStalls: Value<Int>,
val locationGUID: String
val totalStalls: Value<Int>
// TODO: siteType, accessType
)
@@ -274,6 +274,7 @@ interface TeslaChargingOwnershipGraphQlApi {
@JsonClass(generateAdapter = true)
data class SiteDynamic(
val activeOutages: List<Outage>,
val chargerDetails: List<ChargerDetail>,
val chargersAvailable: Value<Int>?,
val currentCongestion: Double,
@@ -372,8 +373,8 @@ interface TeslaChargingOwnershipGraphQlApi {
// add API key to every request
val request = chain.request().newBuilder()
.header("Authorization", "Bearer $t")
.header("User-Agent", "okhttp/4.11.0")
.header("x-tesla-user-agent", "TeslaApp/4.44.5-3304/3a5d531cc3/android/27")
.header("User-Agent", "okhttp/4.9.2")
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27")
.header("Accept", "*/*")
.build()
chain.proceed(request)

View File

@@ -1,16 +1,99 @@
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 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 {
@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 {
private val cacheSize = 1L * 1024 * 1024 // 1MB
val supportedLanguages = setOf("de", "en", "fr", "nl")
private val DATA_SOURCE_GOINGELECTRIC = "going_electric"
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 {
val locale = Locale.getDefault().language
return if (supportedLanguages.contains(locale)) {

View File

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

View File

@@ -1,7 +1,10 @@
package net.vonforst.evmap.api.fronyx
import android.content.Context
import com.squareup.moshi.JsonDataException
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.equivalentPlugTypes
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.storage.PreferenceDataSource
import net.vonforst.evmap.viewmodel.Resource
import retrofit2.HttpException
import java.io.IOException
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
@@ -50,9 +55,9 @@ class PredictionRepository(private val context: Context) {
evseIds: Map<Chargepoint, List<String>>,
filteredConnectors: Set<String>?
): Resource<List<FronyxEvseIdResponse>> {
return Resource.success(null)
if (!prefs.predictionEnabled) return Resource.success(null)
/*val allEvseIds =
val allEvseIds =
evseIds.filterKeys {
FronyxApi.isChargepointSupported(charger, it) &&
filteredConnectors?.let { filtered ->
@@ -84,7 +89,7 @@ class PredictionRepository(private val context: Context) {
// malformed JSON response from fronyx API
e.printStackTrace()
return Resource.error(e.message, null)
}*/
}
}
private fun buildPredictionGraph(

View File

@@ -15,7 +15,6 @@ 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
@@ -160,12 +159,6 @@ class GoingElectricApiWrapper(
override val name = "GoingElectric.de"
override val id = "goingelectric"
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(
referenceData: ReferenceData,
@@ -459,10 +452,7 @@ class GoingElectricApiWrapper(
if (responses.map { it.isSuccessful }.all { it }
&& plugsResponse.body()!!.status == STATUS_OK
&& chargeCardsResponse.body()!!.status == STATUS_OK
&& networksResponse.body()!!.status == STATUS_OK
&& plugsResponse.body()!!.result != null
&& chargeCardsResponse.body()!!.result != null
&& networksResponse.body()!!.result != null) {
&& networksResponse.body()!!.status == STATUS_OK) {
Resource.success(
GEReferenceData(
plugsResponse.body()!!.result!!,

View File

@@ -77,7 +77,6 @@ data class GEChargeLocation(
address.convert(),
chargepoints.map { it.convert() },
network,
"https://www.goingelectric.de/",
"https:${url}",
"https:${url}edit/",
faultReport?.convert(),
@@ -89,7 +88,6 @@ data class GEChargeLocation(
locationDescription,
photos?.map { it.convert(apikey) },
chargecards?.map { it.convert() },
null,
openinghours?.convert(),
cost?.convert(),
null,
@@ -210,7 +208,7 @@ data class GEChargepoint(val type: String, val power: Double, val count: Int) {
return when (type) {
Chargepoint.TYPE_1 -> "Typ1"
Chargepoint.TYPE_2_UNKNOWN -> "Typ2"
Chargepoint.TYPE_3C -> "Typ3"
Chargepoint.TYPE_3 -> "Typ3"
Chargepoint.CCS_UNKNOWN -> "CCS"
Chargepoint.CCS_TYPE_2 -> "Typ2"
Chargepoint.SCHUKO -> "Schuko"
@@ -227,7 +225,7 @@ data class GEChargepoint(val type: String, val power: Double, val count: Int) {
return when (type) {
"Typ1" -> Chargepoint.TYPE_1
"Typ2" -> Chargepoint.TYPE_2_UNKNOWN
"Typ3" -> Chargepoint.TYPE_3C
"Typ3" -> Chargepoint.TYPE_3
"Tesla Supercharger CCS" -> Chargepoint.CCS_UNKNOWN
"CCS" -> Chargepoint.CCS_UNKNOWN
"Schuko" -> Chargepoint.SCHUKO

View File

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

View File

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

View File

@@ -1,345 +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 evseId = if (attribs["28"]?.attrVal is String) listOf(attribs["28"]?.attrVal.toString()) else null
return Chargepoint(connectionType, connectionPower, 1, connectionCurrent, connectionVoltage, evseId)
}
}
}
@JsonClass(generateAdapter = true)
data class NobilChargerStationData(
@Json(name = "id") val id: Long,
@Json(name = "name") val name: String,
@Json(name = "ocpidb_mapping_stasjon_id") val ocpiId: String?,
@Json(name = "Street") val street: String?,
@Json(name = "House_number") val houseNumber: String,
@Json(name = "Zipcode") val zipCode: String?,
@Json(name = "City") val city: String?,
@Json(name = "Municipality_ID") val municipalityId: String,
@Json(name = "Municipality") val municipality: String,
@Json(name = "County_ID") val countyId: String,
@Json(name = "County") val county: String,
@Json(name = "Description_of_location") val description: String?,
@Json(name = "Owned_by") val ownedBy: String?,
@Json(name = "Operator") val operator: String?,
@Json(name = "Number_charging_points") val numChargePoints: Int,
@Json(name = "Position") val position: Coordinate,
@Json(name = "Image") val image: String,
@Json(name = "Available_charging_points") val availableChargePoints: Int,
@Json(name = "User_comment") val userComment: String?,
@Json(name = "Contact_info") val contactInfo: String?,
@Json(name = "Created") val created: LocalDateTime,
@Json(name = "Updated") val updated: LocalDateTime,
@Json(name = "Station_status") val stationStatus: Int,
@Json(name = "Land_code") val landCode: String,
@Json(name = "International_id") val internationalId: String
)
@JsonClass(generateAdapter = true)
data class NobilChargerStationAttributes(
@Json(name = "st") val st: Map<String, NobilChargerStationGenericAttribute>,
@Json(name = "conn") val conn: Map<String, Map<String, NobilChargerStationGenericAttribute>>
)
@JsonClass(generateAdapter = true)
data class NobilChargerStationGenericAttribute(
@Json(name = "attrtypeid") val attrTypeId: String,
@Json(name = "attrname") val attrName: String,
@Json(name = "attrvalid") val attrValId: String,
@Json(name = "trans") val attrTrans: String,
@Json(name = "attrval") val attrVal: Any
)
@Parcelize
@JsonClass(generateAdapter = true)
class NobilChargerPhotoAdapter(override val id: String) :
ChargerPhoto(id) {
override fun getUrl(height: Int?, width: Int?, size: Int?, allowOriginal: Boolean): String {
val maxSize = size ?: max(height, width)
return "https://www.nobil.no/img/ladestasjonbilder/" +
when (maxSize) {
in 0..50 -> "tn_$id"
else -> id
}
}
}

View File

@@ -11,7 +11,6 @@ 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
@@ -131,12 +130,6 @@ class OpenChargeMapApiWrapper(
override val name = "OpenChargeMap.org"
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?) =
if (value == null || value.all) null else value.values.joinToString(",")
@@ -251,8 +244,6 @@ class OpenChargeMapApiWrapper(
return Resource.success(ChargepointList(result, data.size < 499))
} catch (e: IOException) {
return Resource.error(e.message, null)
} catch (e: HttpException) {
return Resource.error(e.message, null)
}
}

View File

@@ -64,7 +64,6 @@ data class OCMChargepoint(
addressInfo.toAddress(refData),
connections.map { it.convert(refData) },
operatorInfo?.title ?: refData.operators.find { it.id == operatorId }?.title,
"https://openchargemap.org/",
"https://map.openchargemap.io/?id=$id",
"https://map.openchargemap.io/?id=$id",
convertFaultReport(),
@@ -77,8 +76,7 @@ data class OCMChargepoint(
mediaItems?.mapNotNull { it.convert() },
null,
null,
null,
cost?.takeIf { it.isNotBlank() }.let { Cost(descriptionShort = it) },
cost?.let { Cost(descriptionShort = it) },
dataProvider?.let { "© ${it.title}" + if (it.license != null) ". ${it.license}" else "" },
ChargepriceData(
addressInfo.countryISOCode(refData),
@@ -182,8 +180,8 @@ data class OCMConnection(
25L -> Chargepoint.TYPE_2_SOCKET
1036L -> Chargepoint.TYPE_2_PLUG
1L -> Chargepoint.TYPE_1
36L -> Chargepoint.TYPE_3A
26L -> Chargepoint.TYPE_3C
36L -> Chargepoint.TYPE_3
26L -> Chargepoint.TYPE_3
else -> title ?: ""
}
}
@@ -256,7 +254,7 @@ data class OCMUserComment(
@Json(name = "ID") val id: Long,
@Json(name = "CommentTypeID") val commentTypeId: Long,
@Json(name = "Comment") val comment: String?,
@Json(name = "UserName") val userName: String?,
@Json(name = "UserName") val userName: String,
@Json(name = "DateCreated") val dateCreated: ZonedDateTime
)

View File

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

Some files were not shown because too many files have changed in this diff Show More