mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-24 07:37:46 -05:00
Compare commits
472 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc22b16111 | ||
|
|
f41ea230de | ||
|
|
ceb5081757 | ||
|
|
28bb8cef5f | ||
|
|
ba17cb989a | ||
|
|
d08aaa3325 | ||
|
|
0f24608d2a | ||
|
|
92e9539286 | ||
|
|
b373f49180 | ||
|
|
ec8728a253 | ||
|
|
9ca470cd46 | ||
|
|
38a1bf2da5 | ||
|
|
5c1dad82b1 | ||
|
|
5647820f3e | ||
|
|
092a3e50bc | ||
|
|
7b27fe2cac | ||
|
|
8991cb4e4a | ||
|
|
66d6aee97e | ||
|
|
3a5646a3ac | ||
|
|
14eadef10d | ||
|
|
cea0878267 | ||
|
|
2b4c0829a8 | ||
|
|
8e9d9d15c4 | ||
|
|
ca9a7df8b0 | ||
|
|
51aecd179c | ||
|
|
6781989266 | ||
|
|
872d3c5143 | ||
|
|
69622c6816 | ||
|
|
15fdac6348 | ||
|
|
6c206c7a25 | ||
|
|
8f49b1f238 | ||
|
|
31bd2b7dd4 | ||
|
|
5524d14562 | ||
|
|
5a360a7ee0 | ||
|
|
98d3c91686 | ||
|
|
12c1c6a5ec | ||
|
|
21e23efb50 | ||
|
|
f6f2b15f41 | ||
|
|
c3776758b3 | ||
|
|
6d9e34667c | ||
|
|
24b94a055e | ||
|
|
1d2a7e4af9 | ||
|
|
fa86c7c15a | ||
|
|
4cd9872d0f | ||
|
|
1e78ffce7e | ||
|
|
3eaa97ea4f | ||
|
|
adaf2f0c87 | ||
|
|
5802526d14 | ||
|
|
fe731f71e8 | ||
|
|
c22173b79e | ||
|
|
82a5730aed | ||
|
|
3386092bf8 | ||
|
|
1318126780 | ||
|
|
abf9165602 | ||
|
|
2c35df6360 | ||
|
|
4ed046df7a | ||
|
|
a20f25af17 | ||
|
|
b2a2114c88 | ||
|
|
c2896ade45 | ||
|
|
45983bce7f | ||
|
|
d0fffb1a97 | ||
|
|
4819a10d03 | ||
|
|
8a0f7e79f0 | ||
|
|
c727d9f1b8 | ||
|
|
e5d0ebbbb5 | ||
|
|
ee4b5e7319 | ||
|
|
fecde441f1 | ||
|
|
cb1543cb4a | ||
|
|
276daac607 | ||
|
|
f7d39a1ba5 | ||
|
|
fa09b9188e | ||
|
|
b31e55f130 | ||
|
|
c494b0d5e2 | ||
|
|
272b86ff88 | ||
|
|
32de28bc1c | ||
|
|
4cd6c44ba1 | ||
|
|
3265694c51 | ||
|
|
529be2cc34 | ||
|
|
00862b66a1 | ||
|
|
cabaa42772 | ||
|
|
1663607171 | ||
|
|
126c47bbc1 | ||
|
|
b93d01f96d | ||
|
|
7fb5df29e4 | ||
|
|
b878d37982 | ||
|
|
0f7aa44d8e | ||
|
|
d8e1c36993 | ||
|
|
03f613fa4b | ||
|
|
aba533e553 | ||
|
|
307af88f01 | ||
|
|
8478948d5f | ||
|
|
7e96c9e5a7 | ||
|
|
44bd2c6159 | ||
|
|
7d2a19b0a3 | ||
|
|
3414a7581c | ||
|
|
df47f7b4c1 | ||
|
|
a08e2ab7e9 | ||
|
|
c1351ce935 | ||
|
|
b4a1a8b546 | ||
|
|
3865e6c33d | ||
|
|
091b0f5ac3 | ||
|
|
1148200f37 | ||
|
|
1847e8b771 | ||
|
|
bbfe8e2bb2 | ||
|
|
983d368a78 | ||
|
|
4a6a34db3a | ||
|
|
35ddece698 | ||
|
|
36c6a4053d | ||
|
|
104913b3c4 | ||
|
|
5cc510fe22 | ||
|
|
4250eb2ba8 | ||
|
|
1db82db066 | ||
|
|
d6a8fbee7d | ||
|
|
23e2f0baad | ||
|
|
ea4fb37f30 | ||
|
|
094f38ac87 | ||
|
|
b84d13d42b | ||
|
|
845bd2e5ca | ||
|
|
0b68ddb939 | ||
|
|
93ff5592e6 | ||
|
|
fbdda89219 | ||
|
|
f409ded73a | ||
|
|
d5a0e16675 | ||
|
|
99a93b202b | ||
|
|
32726993de | ||
|
|
2218cc6b3a | ||
|
|
66564d81d7 | ||
|
|
eb819a793e | ||
|
|
deb101bcee | ||
|
|
176664d7ab | ||
|
|
7327943ec4 | ||
|
|
61a7b358c0 | ||
|
|
1845508512 | ||
|
|
5e13134ee7 | ||
|
|
b193e27022 | ||
|
|
ba601a8027 | ||
|
|
26de099baa | ||
|
|
4cc54bd376 | ||
|
|
4c5cc187cd | ||
|
|
5a6c185a31 | ||
|
|
4f02935b45 | ||
|
|
e7ecb37040 | ||
|
|
cc40b7e988 | ||
|
|
8b02660c34 | ||
|
|
6e5f894c8a | ||
|
|
2d8327f6a6 | ||
|
|
964a65611b | ||
|
|
1f804efc04 | ||
|
|
44c7e1a705 | ||
|
|
528454cd2c | ||
|
|
868901d592 | ||
|
|
e934c2b7d9 | ||
|
|
08ccd593e6 | ||
|
|
9b924af4ff | ||
|
|
3d16a90579 | ||
|
|
36916fa8e3 | ||
|
|
3ef69ebb02 | ||
|
|
33fe7c0da3 | ||
|
|
1fee260d1c | ||
|
|
f1bb37ea68 | ||
|
|
0680bdab21 | ||
|
|
d044de1231 | ||
|
|
667914f9d7 | ||
|
|
8de16d1130 | ||
|
|
68ca30ef4e | ||
|
|
a0c4a8d4c1 | ||
|
|
18b7127034 | ||
|
|
5913d6a912 | ||
|
|
0615d06680 | ||
|
|
463de7365d | ||
|
|
aa00f8b7f0 | ||
|
|
31754eca0c | ||
|
|
ac8fc813f8 | ||
|
|
4c202d7ff2 | ||
|
|
2e29e1f108 | ||
|
|
363345f9c2 | ||
|
|
2074c73bf7 | ||
|
|
fae373d125 | ||
|
|
ff388a0d9b | ||
|
|
002a6c4b3b | ||
|
|
911358eca0 | ||
|
|
a43a8c8e60 | ||
|
|
380150c851 | ||
|
|
e6c3ebdcde | ||
|
|
5fb58d25bc | ||
|
|
948fb137c1 | ||
|
|
4578311d1f | ||
|
|
7a9cdcac21 | ||
|
|
9df61042af | ||
|
|
7ff21d9b5f | ||
|
|
c6490471de | ||
|
|
75ba9d9978 | ||
|
|
1637d20806 | ||
|
|
5f60b73712 | ||
|
|
60871904be | ||
|
|
bb240afd21 | ||
|
|
8c6c27fd28 | ||
|
|
03728f0677 | ||
|
|
cfe40e1972 | ||
|
|
a51052d9a3 | ||
|
|
36af713252 | ||
|
|
77245c1e10 | ||
|
|
bc90d23bd3 | ||
|
|
a6c482d5d3 | ||
|
|
ca829be72b | ||
|
|
09980598f7 | ||
|
|
08f617d23b | ||
|
|
968bffd3aa | ||
|
|
0ea0a2454e | ||
|
|
6fc7652079 | ||
|
|
eacdb7294f | ||
|
|
9e63c0dcf8 | ||
|
|
cf20dc4089 | ||
|
|
bfe1667a69 | ||
|
|
069b825ee7 | ||
|
|
b058b3399c | ||
|
|
fcad13758a | ||
|
|
5f2bf26bc2 | ||
|
|
1e7bcd73a9 | ||
|
|
5ebd0a8abe | ||
|
|
a1fb480ff0 | ||
|
|
cafc477964 | ||
|
|
c90f32f1ff | ||
|
|
0b266ee0aa | ||
|
|
d0e8a24e5c | ||
|
|
8cda9ee469 | ||
|
|
ea3c552bca | ||
|
|
ffdcecd6ab | ||
|
|
2650086594 | ||
|
|
aa07da1002 | ||
|
|
ce618f81e4 | ||
|
|
9d196f645b | ||
|
|
d28f14b932 | ||
|
|
35002aad86 | ||
|
|
7601140bb8 | ||
|
|
dde3ff26ba | ||
|
|
8cbf0a0589 | ||
|
|
0382f63e72 | ||
|
|
23d47142c0 | ||
|
|
5130b93e99 | ||
|
|
520d753b16 | ||
|
|
be4115af33 | ||
|
|
17efe716d5 | ||
|
|
32c32ed934 | ||
|
|
e911eb78c4 | ||
|
|
efacd462db | ||
|
|
41be3e0180 | ||
|
|
8a5ad2b1e8 | ||
|
|
b723556dd9 | ||
|
|
17a82591a5 | ||
|
|
fa040928e8 | ||
|
|
c11178810e | ||
|
|
ef8bce5197 | ||
|
|
398f159e27 | ||
|
|
6ab3ba2ed2 | ||
|
|
f59fd9b3aa | ||
|
|
9e18c62d9d | ||
|
|
3626c9a72f | ||
|
|
36805d8224 | ||
|
|
b1dee90068 | ||
|
|
dfc7de75ad | ||
|
|
32c7774a3a | ||
|
|
02ef25b961 | ||
|
|
e535e77b7a | ||
|
|
5b0b4e4337 | ||
|
|
a6bbf635c5 | ||
|
|
591f99dea4 | ||
|
|
0c5bd69205 | ||
|
|
72e98cf611 | ||
|
|
0fefffda2f | ||
|
|
49e555ef04 | ||
|
|
d6d1e915ee | ||
|
|
546d7a11ce | ||
|
|
4849944c23 | ||
|
|
77b38661dd | ||
|
|
3723ee161b | ||
|
|
1d3efe5295 | ||
|
|
f011944135 | ||
|
|
1d81bb5d37 | ||
|
|
e8adb759a6 | ||
|
|
f4384b4b60 | ||
|
|
1d63e37467 | ||
|
|
b5b0254bdd | ||
|
|
6514197920 | ||
|
|
4c5388350f | ||
|
|
20e9e43f0d | ||
|
|
541646dda9 | ||
|
|
b9354e77a9 | ||
|
|
65fa54ef36 | ||
|
|
6e419849b1 | ||
|
|
c4c6f09a05 | ||
|
|
3b602c03c4 | ||
|
|
012e5d7362 | ||
|
|
4f5007ca0d | ||
|
|
b40a74aa37 | ||
|
|
ce172bd4b0 | ||
|
|
b3bbca576f | ||
|
|
72ccd99c1f | ||
|
|
dd5c8659df | ||
|
|
8f7f9e5a09 | ||
|
|
8256981b8d | ||
|
|
5649ef202f | ||
|
|
ed9c729684 | ||
|
|
add66811ae | ||
|
|
2df5710910 | ||
|
|
a513a8d6d4 | ||
|
|
a55e4df62d | ||
|
|
d540faa179 | ||
|
|
3d69d3e50c | ||
|
|
fce27f0c19 | ||
|
|
da3b9643bc | ||
|
|
b690d9744d | ||
|
|
70387ec350 | ||
|
|
5a331df232 | ||
|
|
5dc5e1f43f | ||
|
|
360e7767bd | ||
|
|
5f0c9fd31d | ||
|
|
536c884f23 | ||
|
|
7daf5a0adb | ||
|
|
862f2b06d8 | ||
|
|
198a9ecc48 | ||
|
|
2762a32105 | ||
|
|
8a83a80e75 | ||
|
|
75e8569964 | ||
|
|
00b26d224f | ||
|
|
836f42b299 | ||
|
|
3de994f09d | ||
|
|
d78eda9d97 | ||
|
|
ed4be05aed | ||
|
|
45de4c8ff0 | ||
|
|
b5b785be07 | ||
|
|
0b8589d599 | ||
|
|
ba757831f3 | ||
|
|
1990152836 | ||
|
|
50fd433439 | ||
|
|
381e6f3d98 | ||
|
|
9c5582f19c | ||
|
|
1c16d8cbb6 | ||
|
|
1734f1c09e | ||
|
|
ebf0f82597 | ||
|
|
85a38a6da1 | ||
|
|
60ca97179c | ||
|
|
4413cba9fa | ||
|
|
e2e6a3060b | ||
|
|
7e507cad70 | ||
|
|
3b8aca3eff | ||
|
|
7a525d3f28 | ||
|
|
b0d7f465bc | ||
|
|
5bf839d4e9 | ||
|
|
73247b5a23 | ||
|
|
526addd010 | ||
|
|
287c5f60bc | ||
|
|
b68ab8f960 | ||
|
|
17d8bfc46a | ||
|
|
640f98c90f | ||
|
|
b1cf9809f6 | ||
|
|
c171c859af | ||
|
|
75c8114676 | ||
|
|
1d10ffeb52 | ||
|
|
0e278bfedb | ||
|
|
c6395feaa3 | ||
|
|
4b38c0de2d | ||
|
|
22d24f3bd0 | ||
|
|
d8e8475666 | ||
|
|
c3148796d4 | ||
|
|
8551966348 | ||
|
|
5f8be9dc0c | ||
|
|
0dc1a0270e | ||
|
|
609d984df1 | ||
|
|
5830965d3a | ||
|
|
b613b4d626 | ||
|
|
2e96ebbcd1 | ||
|
|
579ce088dc | ||
|
|
be15be00bd | ||
|
|
3266c623eb | ||
|
|
f6feb2cf8c | ||
|
|
ac1a0e01e3 | ||
|
|
7b038ad850 | ||
|
|
d02c9cc005 | ||
|
|
c83ecf1e5a | ||
|
|
8287084818 | ||
|
|
8f433a02a0 | ||
|
|
de6890e27e | ||
|
|
55b3a10919 | ||
|
|
08b6902020 | ||
|
|
7183475f31 | ||
|
|
ca6ff94c1f | ||
|
|
c02c259162 | ||
|
|
a44718ded2 | ||
|
|
4f268f5e83 | ||
|
|
99b4841545 | ||
|
|
7ad7d7da30 | ||
|
|
0e80f2bf82 | ||
|
|
b5a6ceb5f9 | ||
|
|
c655cae405 | ||
|
|
fb2e510220 | ||
|
|
c170270557 | ||
|
|
e7b42e2c19 | ||
|
|
fc85e631c9 | ||
|
|
7192c9ebfa | ||
|
|
c652265ea1 | ||
|
|
0320238dc9 | ||
|
|
e814c088bf | ||
|
|
b60b2d70b9 | ||
|
|
3905656ea7 | ||
|
|
1f88e5fbdd | ||
|
|
646469e9ea | ||
|
|
cce7c69d74 | ||
|
|
bc91c0571b | ||
|
|
a83102a97e | ||
|
|
f52a98540c | ||
|
|
e0d97e7219 | ||
|
|
3bbd20a57e | ||
|
|
3279c5eceb | ||
|
|
03d958ac2c | ||
|
|
b1fd370101 | ||
|
|
bdc96fcd57 | ||
|
|
0d54e17eb4 | ||
|
|
b1d0081fb7 | ||
|
|
1134499532 | ||
|
|
0417ade802 | ||
|
|
8fafabf6a8 | ||
|
|
1b3c35e94f | ||
|
|
23a3adc500 | ||
|
|
16c2dcc938 | ||
|
|
f322974e52 | ||
|
|
50ae2123e9 | ||
|
|
72894399f6 | ||
|
|
77014d754f | ||
|
|
66dbd6426f | ||
|
|
e4127f4a56 | ||
|
|
f9bf8b80f7 | ||
|
|
67eeb47d5f | ||
|
|
3c6a7cd536 | ||
|
|
31e3509369 | ||
|
|
b03f765216 | ||
|
|
9222dec613 | ||
|
|
71c36fbc8f | ||
|
|
830477e664 | ||
|
|
3ce91a9c50 | ||
|
|
a3b2b94b25 | ||
|
|
a7770e1c1b | ||
|
|
fcd51307cb | ||
|
|
ba4a9c29b2 | ||
|
|
3463177ad2 | ||
|
|
09deaf5080 | ||
|
|
23f429bbea | ||
|
|
1184d3b6cc | ||
|
|
c95a60807b | ||
|
|
4b8cf82843 | ||
|
|
f33b9e8117 | ||
|
|
cbc3040807 | ||
|
|
92619ea95e | ||
|
|
a7007284ff | ||
|
|
7fce566052 | ||
|
|
0c44b4b074 | ||
|
|
a652d96f74 | ||
|
|
8a9b3ad948 | ||
|
|
c48f33e265 | ||
|
|
8ba4897026 | ||
|
|
42916d71ca | ||
|
|
5ca7524e8b | ||
|
|
c37f72a26b | ||
|
|
6f0113c50d | ||
|
|
f99ea7ca9e | ||
|
|
788db0c10f | ||
|
|
db2213a50f | ||
|
|
ace4126035 | ||
|
|
d5d6e4f314 | ||
|
|
55999d15e6 | ||
|
|
b61ca609d3 | ||
|
|
b0afad2144 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
||||
github: johan12345
|
||||
custom: 'https://paypal.me/johan98'
|
||||
custom: ['https://paypal.me/johan98', 'https://ev-map.app/donate/']
|
||||
|
||||
56
.github/workflows/release.yml
vendored
56
.github/workflows/release.yml
vendored
@@ -11,31 +11,38 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Java environment
|
||||
uses: actions/setup-java@v2
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: 'zulu'
|
||||
cache: 'gradle'
|
||||
- name: Decrypt keystore
|
||||
run: openssl aes-256-cbc -K ${{ secrets.encrypted_53968681344a_key }} -iv ${{ secrets.encrypted_53968681344a_iv }} -in _ci/keystore.jks.enc -out _ci/keystore.jks -d
|
||||
- name: Extract version code
|
||||
run: echo "VERSION_CODE=$(grep -o "^\s*versionCode\s\+[0-9]*" app/build.gradle | awk '{ print $2 }' | tr -d \''"\\')" >> $GITHUB_ENV
|
||||
run: echo "VERSION_CODE=$(grep -o "^\s*versionCode\s*=\s*[0-9]\+" app/build.gradle.kts | awk '{ print $3 }' | tr -d \''"\\')" >> $GITHUB_ENV
|
||||
|
||||
- name: Build app release
|
||||
- name: Build app release & export licenses
|
||||
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 assembleRelease --no-daemon
|
||||
run: ./gradlew exportLibraryDefinitions assembleRelease --no-daemon
|
||||
|
||||
- name: Export licenses in Appning format
|
||||
run: python3 _ci/export_licenses_appning.py
|
||||
|
||||
- name: release
|
||||
uses: actions/create-release@v1
|
||||
@@ -85,3 +92,40 @@ jobs:
|
||||
asset_path: app/build/outputs/apk/fossAutomotive/release/app-foss-automotive-release.apk
|
||||
asset_name: app-foss-automotive-release.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
- name: upload Licenses
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/generated/aboutLibraries/aboutlibraries.json
|
||||
asset_name: aboutlibraries.json
|
||||
asset_content_type: application/json
|
||||
- name: upload Licenses Appning
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: licenses_fossAutomotiveRelease_appning.csv
|
||||
asset_name: licenses_fossAutomotiveRelease_appning.csv
|
||||
asset_content_type: text/csv
|
||||
- name: upload Licenses Appning
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: licenses_fossNormalRelease_appning.csv
|
||||
asset_name: licenses_fossNormalRelease_appning.csv
|
||||
asset_content_type: text/csv
|
||||
|
||||
- name: Trigger Website update
|
||||
run: |
|
||||
curl -L \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ github.token }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/ev-map/ev-map.github.io/dispatches \
|
||||
-d "{\"event_type\": \"trigger-workflow\"}"
|
||||
|
||||
62
.github/workflows/tests.yml
vendored
62
.github/workflows/tests.yml
vendored
@@ -16,17 +16,17 @@ jobs:
|
||||
buildvariant: [ FossNormal, FossAutomotive, GoogleNormal, GoogleAutomotive ]
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Java environment
|
||||
uses: actions/setup-java@v2
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
java-version: 21
|
||||
distribution: 'zulu'
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Copy apikeys.xml
|
||||
run: cp .github/workflows/apikeys-ci.xml app/src/main/res/values/apikeys.xml
|
||||
run: cp _ci/apikeys-ci.xml app/src/main/res/values/apikeys.xml
|
||||
|
||||
- name: Build app
|
||||
run: ./gradlew assemble${{ matrix.buildvariant }}Debug --no-daemon
|
||||
@@ -34,3 +34,57 @@ 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/arm64-v8a/libc++_shared.so",
|
||||
"lib/armeabi-v7a/libc++_shared.so",
|
||||
"lib/x86/libc++_shared.so",
|
||||
"lib/x86_64/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
4
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
*.iml
|
||||
.gradle
|
||||
.kotlin
|
||||
/local.properties
|
||||
/.idea/*
|
||||
.DS_Store
|
||||
@@ -12,4 +13,5 @@ apikeys.xml
|
||||
/app/**/*.apk
|
||||
/_img/connectors/*.ai
|
||||
api-7125266970515251116-798419-8e2dda660c80.json
|
||||
output-metadata.json
|
||||
output-metadata.json
|
||||
licenses_*.csv
|
||||
36
Gemfile.lock
36
Gemfile.lock
@@ -5,23 +5,28 @@ GEM
|
||||
addressable (2.8.0)
|
||||
public_suffix (>= 2.0.2, < 5.0)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.354.0)
|
||||
aws-sdk-core (3.104.3)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.36.0)
|
||||
aws-sdk-core (~> 3, >= 3.99.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.78.0)
|
||||
aws-sdk-core (~> 3, >= 3.104.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1196.0)
|
||||
aws-sdk-core (3.240.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.208.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.3)
|
||||
base64 (0.3.0)
|
||||
bigdecimal (4.0.1)
|
||||
claide (1.0.3)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
@@ -113,9 +118,10 @@ GEM
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.6.1)
|
||||
jmespath (1.6.2)
|
||||
json (2.3.1)
|
||||
jwt (2.2.1)
|
||||
logger (1.7.0)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.10.1)
|
||||
mini_mime (1.0.2)
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2023 Johan von Forstner and contributors
|
||||
Copyright (c) 2020-2024 Johan von Forstner and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
32
README.md
32
README.md
@@ -20,11 +20,11 @@ Features
|
||||
- Search for places
|
||||
- Advanced filtering options, including saved filter profiles
|
||||
- Favorites list, also with availability information
|
||||
- Integrated price comparison using [Chargeprice.app](https://chargeprice.app) (only in Europe)
|
||||
- Android Auto & Android Automotive OS integration
|
||||
- No ads, fully open source
|
||||
- Compatible with Android 5.0 and above
|
||||
- Can use Google Maps or Mapbox (OpenStreetMap) as map backends - the version available on F-Droid only uses Mapbox.
|
||||
- Can use Google Maps or OpenStreetMap as map backends - the version available on F-Droid only uses
|
||||
OSM.
|
||||
|
||||
Screenshots
|
||||
-----------
|
||||
@@ -37,17 +37,18 @@ Development setup
|
||||
The App is developed using Android Studio and should pretty much work out-of-the-box when you clone
|
||||
the Git repository and open the project with Android Studio.
|
||||
|
||||
The only exception is that you need to obtain some free API keys for the different data sources that
|
||||
The only exception is that you need to obtain some API keys for the different data sources that
|
||||
EVMap uses and put them into the app in the form of a resource file called `apikeys.xml` under
|
||||
`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 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.
|
||||
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.
|
||||
- `fossNormal` is intended to run on smartphones and tablets, and also includes the Android
|
||||
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).
|
||||
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).
|
||||
- `fossAutomotive` can be installed directly on
|
||||
[Android Automotive OS (AAOS)](https://source.android.com/docs/automotive/start/what_automotive)
|
||||
headunits without Google services.
|
||||
@@ -75,5 +76,18 @@ 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="500" alt="Translation status" />
|
||||
<img src="https://hosted.weblate.org/widgets/evmap/-/open-graph.png" width="400" 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://www.jetbrains.com/community/opensource/"><img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg" alt="JetBrains logo" height="38"/></a><br>
|
||||
As part of its support program for Open-source projects, **JetBrains** supports the development of EVMap since December 2023 with a license of their software suite.
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<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>
|
||||
20
_ci/export_licenses_appning.py
Normal file
20
_ci/export_licenses_appning.py
Normal file
@@ -0,0 +1,20 @@
|
||||
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")
|
||||
14
_img/appicon_monochrome.svg
Normal file
14
_img/appicon_monochrome.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 529 B |
4
_img/connectors/connector_unknown.svg
Normal file
4
_img/connectors/connector_unknown.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 395 B |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 14 KiB |
73
_img/ic_launcher-playstore.svg
Normal file
73
_img/ic_launcher-playstore.svg
Normal file
@@ -0,0 +1,73 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
231
_misc/taginfo.json
Normal file
231
_misc/taginfo.json
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"data_format": 1,
|
||||
"data_url": "https://raw.githubusercontent.com/ev-map/evmap/master/_misc/taginfo.json",
|
||||
"data_updated": "20250921T140000Z",
|
||||
"project": {
|
||||
"name": "EVMap",
|
||||
"description": "Find electric vehicle chargers comfortably using your Android phone.",
|
||||
"project_url": "https://ev-map.app/",
|
||||
"doc_url": "https://github.com/ev-map/evmap-osm",
|
||||
"icon_url": "https://avatars.githubusercontent.com/u/115927597?s=32",
|
||||
"contact_name": "Johan von Forstner",
|
||||
"contact_email": "evmap@vonforst.net"
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"key": "amenity",
|
||||
"value": "charging_station",
|
||||
"description": "Used to display charging stations."
|
||||
},
|
||||
{
|
||||
"key": "name"
|
||||
},
|
||||
{
|
||||
"key": "network"
|
||||
},
|
||||
{
|
||||
"key": "authentication:none",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "operator"
|
||||
},
|
||||
{
|
||||
"key": "description"
|
||||
},
|
||||
{
|
||||
"key": "website"
|
||||
},
|
||||
{
|
||||
"key": "addr:city"
|
||||
},
|
||||
{
|
||||
"key": "addr:country"
|
||||
},
|
||||
{
|
||||
"key": "addr:postcode"
|
||||
},
|
||||
{
|
||||
"key": "addr:street"
|
||||
},
|
||||
{
|
||||
"key": "addr:housenumber"
|
||||
},
|
||||
{
|
||||
"key": "addr:housename"
|
||||
},
|
||||
{
|
||||
"key": "socket:type1"
|
||||
},
|
||||
{
|
||||
"key": "socket:type1:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:type1_combo"
|
||||
},
|
||||
{
|
||||
"key": "socket:type1_combo:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2_cable"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2_cable:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2_combo"
|
||||
},
|
||||
{
|
||||
"key": "socket:type2_combo:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:chademo"
|
||||
},
|
||||
{
|
||||
"key": "socket:chademo:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_standard"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_standard:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_supercharger"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_supercharger:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_supercharger_ccs"
|
||||
},
|
||||
{
|
||||
"key": "socket:tesla_supercharger_ccs:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_blue"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_blue:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_16a"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_16a:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_32a"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_32a:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_63a"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_63a:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_125a"
|
||||
},
|
||||
{
|
||||
"key": "socket:cee_red_125a:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:schuko"
|
||||
},
|
||||
{
|
||||
"key": "socket:schuko:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t13"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t13:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t15"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t15:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t23"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t23:output"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t25"
|
||||
},
|
||||
{
|
||||
"key": "socket:sev1011_t25:output"
|
||||
},
|
||||
{
|
||||
"key": "opening_hours",
|
||||
"value": "24/7"
|
||||
},
|
||||
{
|
||||
"key": "fee",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "fee",
|
||||
"value": "no"
|
||||
},
|
||||
{
|
||||
"key": "parking:fee",
|
||||
"value": "yes"
|
||||
},
|
||||
{
|
||||
"key": "parking:fee",
|
||||
"value": "no"
|
||||
},
|
||||
{
|
||||
"key": "charge"
|
||||
},
|
||||
{
|
||||
"key": "charge:conditional"
|
||||
},
|
||||
{
|
||||
"key": "image"
|
||||
},
|
||||
{
|
||||
"key": "image:0"
|
||||
},
|
||||
{
|
||||
"key": "image:1"
|
||||
},
|
||||
{
|
||||
"key": "image:2"
|
||||
},
|
||||
{
|
||||
"key": "image:3"
|
||||
},
|
||||
{
|
||||
"key": "image:4"
|
||||
},
|
||||
{
|
||||
"key": "image:5"
|
||||
},
|
||||
{
|
||||
"key": "image:6"
|
||||
},
|
||||
{
|
||||
"key": "image:7"
|
||||
},
|
||||
{
|
||||
"key": "image:8"
|
||||
},
|
||||
{
|
||||
"key": "image:9"
|
||||
}
|
||||
]
|
||||
}
|
||||
293
app/build.gradle
293
app/build.gradle
@@ -1,293 +0,0 @@
|
||||
plugins {
|
||||
id 'com.adarshr.test-logger' version '3.1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'androidx.navigation.safeargs.kotlin'
|
||||
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
|
||||
apply plugin: 'pt.jcosta.resourceplaceholders'
|
||||
|
||||
def supportedLocales = "en,de,fr,nb-rNO,nl,pt,ro"
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||
versionCode 192
|
||||
versionName "1.6.6"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs supportedLocales.split(',')
|
||||
buildConfigField("String", "supportedLocales", '"' + supportedLocales + '"')
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
def isRunningOnCI = System.getenv("CI") == "true"
|
||||
if (isRunningOnCI) {
|
||||
// configure keystore
|
||||
storeFile = file("../_ci/keystore.jks")
|
||||
storePassword = System.getenv("KEYSTORE_PASSWORD")
|
||||
keyAlias = System.getenv("KEYSTORE_ALIAS")
|
||||
keyPassword = System.getenv("KEYSTORE_ALIAS_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
debuggable true
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "dependencies", "automotive"
|
||||
productFlavors {
|
||||
foss {
|
||||
dimension "dependencies"
|
||||
}
|
||||
google {
|
||||
dimension "dependencies"
|
||||
versionNameSuffix "-google"
|
||||
}
|
||||
normal {
|
||||
dimension "automotive"
|
||||
}
|
||||
automotive {
|
||||
dimension "automotive"
|
||||
versionNameSuffix "-automotive"
|
||||
versionCode defaultConfig.versionCode + 1
|
||||
minSdkVersion 29
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
}
|
||||
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KaptGenerateStubs).configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
viewBinding true
|
||||
}
|
||||
lint {
|
||||
disable 'NullSafeMutableLiveData'
|
||||
warning 'MissingTranslation'
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.includeAndroidResources true
|
||||
}
|
||||
|
||||
resourcePlaceholders {
|
||||
files = ['xml/shortcuts.xml']
|
||||
}
|
||||
namespace 'net.vonforst.evmap'
|
||||
|
||||
// add API keys from environment variable if not set in apikeys.xml
|
||||
applicationVariants.all { variant ->
|
||||
ext.env = System.getenv()
|
||||
def goingelectricKey = env.GOINGELECTRIC_API_KEY ?: project.findProperty("GOINGELECTRIC_API_KEY")
|
||||
if (goingelectricKey != null) {
|
||||
variant.resValue "string", "goingelectric_key", goingelectricKey
|
||||
}
|
||||
def openchargemapKey = env.OPENCHARGEMAP_API_KEY ?: project.findProperty("OPENCHARGEMAP_API_KEY")
|
||||
if (openchargemapKey == null && project.hasProperty("OPENCHARGEMAP_API_KEY_ENCRYPTED")) {
|
||||
openchargemapKey = decode(project.findProperty("OPENCHARGEMAP_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
|
||||
}
|
||||
if (openchargemapKey != null) {
|
||||
variant.resValue "string", "openchargemap_key", openchargemapKey
|
||||
}
|
||||
def googleMapsKey = env.GOOGLE_MAPS_API_KEY ?: project.findProperty("GOOGLE_MAPS_API_KEY")
|
||||
if (googleMapsKey != null && variant.flavorName.startsWith('google')) {
|
||||
variant.resValue "string", "google_maps_key", googleMapsKey
|
||||
}
|
||||
def mapboxKey = env.MAPBOX_API_KEY ?: project.findProperty("MAPBOX_API_KEY")
|
||||
if (mapboxKey == null && project.hasProperty("MAPBOX_API_KEY_ENCRYPTED")) {
|
||||
mapboxKey = decode(project.findProperty("MAPBOX_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
|
||||
}
|
||||
if (mapboxKey != null) {
|
||||
variant.resValue "string", "mapbox_key", mapboxKey
|
||||
}
|
||||
def chargepriceKey = env.CHARGEPRICE_API_KEY ?: project.findProperty("CHARGEPRICE_API_KEY")
|
||||
if (chargepriceKey == null && project.hasProperty("CHARGEPRICE_API_KEY_ENCRYPTED")) {
|
||||
chargepriceKey = decode(project.findProperty("CHARGEPRICE_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
|
||||
}
|
||||
if (chargepriceKey != null) {
|
||||
variant.resValue "string", "chargeprice_key", chargepriceKey
|
||||
}
|
||||
def fronyxKey = env.FRONYX_API_KEY ?: project.findProperty("FRONYX_API_KEY")
|
||||
if (fronyxKey == null && project.hasProperty("FRONYX_API_KEY_ENCRYPTED")) {
|
||||
fronyxKey = decode(project.findProperty("FRONYX_API_KEY_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds")
|
||||
}
|
||||
if (fronyxKey != null) {
|
||||
variant.resValue "string", "fronyx_key", fronyxKey
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst 'lib/x86/libc++_shared.so'
|
||||
pickFirst 'lib/arm64-v8a/libc++_shared.so'
|
||||
pickFirst 'lib/x86_64/libc++_shared.so'
|
||||
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
googleNormalImplementation {}
|
||||
googleAutomotiveImplementation {}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation "androidx.activity:activity-ktx:1.7.2"
|
||||
implementation "androidx.fragment:fragment-ktx:1.5.7"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
||||
implementation 'androidx.browser:browser:1.5.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
|
||||
implementation "androidx.work:work-runtime-ktx:2.8.1"
|
||||
implementation 'com.github.ev-map:CustomBottomSheetBehavior:e48f73ea7b'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
|
||||
implementation 'com.squareup.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.4.0'
|
||||
implementation 'com.github.ev-map:StfalconImageViewer:5082ebd392'
|
||||
implementation "com.mikepenz:aboutlibraries-core:$about_libs_version"
|
||||
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
|
||||
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'
|
||||
|
||||
// Android Auto
|
||||
def carAppVersion = '1.3.0-rc01'
|
||||
implementation "androidx.car.app:app:$carAppVersion"
|
||||
normalImplementation "androidx.car.app:app-projected:$carAppVersion"
|
||||
automotiveImplementation "androidx.car.app:app-automotive:$carAppVersion"
|
||||
|
||||
// AnyMaps
|
||||
def anyMapsVersion = '8f1226e1c5'
|
||||
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:18.1.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'
|
||||
}
|
||||
// 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'
|
||||
|
||||
// Google Places
|
||||
googleImplementation 'com.google.android.libraries.places:places:3.1.0'
|
||||
googleImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.4'
|
||||
|
||||
// Mapbox Geocoding
|
||||
implementation 'com.mapbox.mapboxsdk:mapbox-sdk-services:5.5.0'
|
||||
|
||||
// navigation library
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
// viewmodel library
|
||||
def lifecycle_version = "2.6.1"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
|
||||
// room library
|
||||
def room_version = "2.5.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.7'
|
||||
|
||||
// billing library
|
||||
def billing_version = "6.0.0"
|
||||
googleImplementation "com.android.billingclient:billing:$billing_version"
|
||||
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
|
||||
|
||||
// ACRA (crash reporting)
|
||||
def acraVersion = "5.8.4"
|
||||
implementation("ch.acra:acra-mail:$acraVersion")
|
||||
implementation("ch.acra:acra-dialog:$acraVersion")
|
||||
implementation("ch.acra:acra-limiter:$acraVersion")
|
||||
|
||||
// debug tools
|
||||
debugImplementation 'com.facebook.flipper:flipper:0.190.0'
|
||||
debugImplementation 'com.facebook.soloader:soloader:0.10.5'
|
||||
debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.190.0'
|
||||
|
||||
// testing
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:4.11.0"
|
||||
//noinspection GradleDependency
|
||||
testImplementation 'org.json:json:20080701'
|
||||
testImplementation 'org.robolectric:robolectric:4.9.2'
|
||||
testImplementation 'androidx.test:core:1.5.0'
|
||||
testImplementation 'androidx.arch.core:core-testing:2.2.0'
|
||||
|
||||
// testing for car app
|
||||
testGoogleImplementation "androidx.car.app:app-testing:$carAppVersion"
|
||||
testGoogleImplementation 'org.robolectric:robolectric:4.9.2'
|
||||
testGoogleImplementation 'androidx.test:core:1.5.0'
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
androidTestImplementation 'androidx.arch.core:core-testing:2.2.0'
|
||||
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.15.0"
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
}
|
||||
|
||||
private static String decode(String s, String key) {
|
||||
return new String(xorWithKey(s.decodeBase64(), key.getBytes()), "UTF-8");
|
||||
}
|
||||
|
||||
private static byte[] xorWithKey(byte[] a, byte[] key) {
|
||||
byte[] out = new byte[a.length];
|
||||
for (int i = 0; i < a.length; i++) {
|
||||
out[i] = (byte) (a[i] ^ key[i%key.length]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
406
app/build.gradle.kts
Normal file
406
app/build.gradle.kts
Normal file
@@ -0,0 +1,406 @@
|
||||
import java.util.Base64
|
||||
|
||||
plugins {
|
||||
id("com.adarshr.test-logger") version "4.0.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
|
||||
minSdk = 23
|
||||
targetSdk = 36
|
||||
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
|
||||
versionCode = 268
|
||||
versionName = "2.0.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) {
|
||||
// configure keystore
|
||||
storeFile = file("../_ci/keystore.jks")
|
||||
storePassword = System.getenv("KEYSTORE_PASSWORD")
|
||||
keyAlias = System.getenv("KEYSTORE_ALIAS")
|
||||
keyPassword = System.getenv("KEYSTORE_ALIAS_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
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"
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
isDebuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("releaseAutomotivePackageName").setRoot("src/release")
|
||||
}
|
||||
|
||||
flavorDimensions += listOf("dependencies", "automotive")
|
||||
productFlavors {
|
||||
create("foss") {
|
||||
dimension = "dependencies"
|
||||
isDefault = true
|
||||
}
|
||||
create("google") {
|
||||
dimension = "dependencies"
|
||||
versionNameSuffix = "-google"
|
||||
}
|
||||
create("normal") {
|
||||
dimension = "automotive"
|
||||
isDefault = true
|
||||
}
|
||||
create("automotive") {
|
||||
dimension = "automotive"
|
||||
versionNameSuffix = "-automotive"
|
||||
versionCode = defaultConfig.versionCode!! + 1
|
||||
minSdk = 29
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
viewBinding = true
|
||||
buildConfig = true
|
||||
}
|
||||
lint {
|
||||
disable += listOf("NullSafeMutableLiveData")
|
||||
warning += listOf("MissingTranslation")
|
||||
}
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
||||
namespace = "net.vonforst.evmap"
|
||||
|
||||
// add API keys from environment variable if not set in apikeys.xml
|
||||
applicationVariants.all {
|
||||
val goingelectricKey =
|
||||
System.getenv("GOINGELECTRIC_API_KEY") ?: project.findProperty("GOINGELECTRIC_API_KEY")
|
||||
?.toString()
|
||||
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()
|
||||
if (openchargemapKey == null && project.hasProperty("OPENCHARGEMAP_API_KEY_ENCRYPTED")) {
|
||||
openchargemapKey = decode(
|
||||
project.findProperty("OPENCHARGEMAP_API_KEY_ENCRYPTED").toString(),
|
||||
"FmK.d,-f*p+rD+WK!eds"
|
||||
)
|
||||
}
|
||||
if (openchargemapKey != null) {
|
||||
resValue("string", "openchargemap_key", openchargemapKey)
|
||||
}
|
||||
val googleMapsKey =
|
||||
System.getenv("GOOGLE_MAPS_API_KEY") ?: project.findProperty("GOOGLE_MAPS_API_KEY")
|
||||
?.toString()
|
||||
if (googleMapsKey != null && flavorName.startsWith("google")) {
|
||||
resValue("string", "google_maps_key", googleMapsKey)
|
||||
}
|
||||
var mapboxKey =
|
||||
System.getenv("MAPBOX_API_KEY") ?: project.findProperty("MAPBOX_API_KEY")?.toString()
|
||||
if (mapboxKey == null && project.hasProperty("MAPBOX_API_KEY_ENCRYPTED")) {
|
||||
mapboxKey = decode(
|
||||
project.findProperty("MAPBOX_API_KEY_ENCRYPTED").toString(),
|
||||
"FmK.d,-f*p+rD+WK!eds"
|
||||
)
|
||||
}
|
||||
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(),
|
||||
"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)
|
||||
}
|
||||
var fronyxKey =
|
||||
System.getenv("FRONYX_API_KEY") ?: project.findProperty("FRONYX_API_KEY")?.toString()
|
||||
if (fronyxKey == null && project.hasProperty("FRONYX_API_KEY_ENCRYPTED")) {
|
||||
fronyxKey = decode(
|
||||
project.findProperty("FRONYX_API_KEY_ENCRYPTED").toString(),
|
||||
"FmK.d,-f*p+rD+WK!eds"
|
||||
)
|
||||
}
|
||||
if (fronyxKey != null) {
|
||||
resValue("string", "fronyx_key", fronyxKey)
|
||||
}
|
||||
var acraKey = System.getenv("ACRA_CRASHREPORT_CREDENTIALS")
|
||||
?: project.findProperty("ACRA_CRASHREPORT_CREDENTIALS")?.toString()
|
||||
if (acraKey == null && project.hasProperty("ACRA_CRASHREPORT_CREDENTIALS_ENCRYPTED")) {
|
||||
acraKey = decode(
|
||||
project.findProperty("ACRA_CRASHREPORT_CREDENTIALS_ENCRYPTED").toString(),
|
||||
"FmK.d,-f*p+rD+WK!eds"
|
||||
)
|
||||
}
|
||||
if (acraKey != null) {
|
||||
resValue("string", "acra_credentials", acraKey)
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
pickFirsts.addAll(
|
||||
listOf(
|
||||
"lib/x86/libc++_shared.so",
|
||||
"lib/arm64-v8a/libc++_shared.so",
|
||||
"lib/x86_64/libc++_shared.so",
|
||||
"lib/armeabi-v7a/libc++_shared.so"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val kotlinVersion: String by rootProject.extra
|
||||
val aboutLibsVersion: String by rootProject.extra
|
||||
val navVersion: String by rootProject.extra
|
||||
val normalImplementation by configurations
|
||||
val googleImplementation by configurations
|
||||
val automotiveImplementation by configurations
|
||||
val fossImplementation by configurations
|
||||
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.core:core-splashscreen:1.2.0")
|
||||
implementation("androidx.activity:activity-ktx:1.11.0")
|
||||
implementation("androidx.fragment:fragment-ktx:1.8.9")
|
||||
implementation("androidx.cardview:cardview:1.0.0")
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
implementation("com.google.android.material:material:1.13.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.4.0")
|
||||
implementation("androidx.browser:browser:1.9.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
||||
implementation("androidx.security:security-crypto:1.1.0")
|
||||
implementation("androidx.work:work-runtime-ktx:2.10.5")
|
||||
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.markomilos.jsonapi:jsonapi-retrofit:1.1.0")
|
||||
implementation("io.coil-kt:coil:2.7.0")
|
||||
implementation("com.github.ev-map:StfalconImageViewer:5082ebd392")
|
||||
implementation("com.mikepenz:aboutlibraries-core:$aboutLibsVersion")
|
||||
implementation("com.mikepenz:aboutlibraries:$aboutLibsVersion")
|
||||
implementation("com.airbnb.android:lottie:6.6.10")
|
||||
implementation("io.michaelrocks.bimap:bimap:1.1.0")
|
||||
implementation("com.github.pengrad:mapscaleview:1.6.0")
|
||||
implementation("com.github.romandanylyk:PageIndicatorView:b1bad589b5")
|
||||
implementation("com.github.ev-map:locale-config-x:58b036abf4")
|
||||
|
||||
// Android Auto
|
||||
val carAppVersion = "1.7.0"
|
||||
implementation("androidx.car.app:app:$carAppVersion")
|
||||
normalImplementation("androidx.car.app:app-projected:$carAppVersion")
|
||||
automotiveImplementation("androidx.car.app:app-automotive:$carAppVersion")
|
||||
|
||||
// AnyMaps
|
||||
val anyMapsVersion = "65e06c4c9a"
|
||||
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")
|
||||
}
|
||||
implementation("org.maplibre.gl:android-sdk:10.3.5") {
|
||||
exclude("org.maplibre.gl", "android-sdk-geojson")
|
||||
}
|
||||
|
||||
// Google Places
|
||||
googleImplementation("com.google.android.libraries.places:places:3.5.0")
|
||||
googleImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2")
|
||||
|
||||
// Mapbox Geocoding
|
||||
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.8.0")
|
||||
|
||||
// navigation library
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
|
||||
|
||||
// viewmodel library
|
||||
val lifecycleVersion = "2.9.2"
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
|
||||
|
||||
// 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:1.0.1")
|
||||
|
||||
// billing library
|
||||
val billingVersion = "7.0.0"
|
||||
googleImplementation("com.android.billingclient:billing:$billingVersion")
|
||||
googleImplementation("com.android.billingclient:billing-ktx:$billingVersion")
|
||||
|
||||
// ACRA (crash reporting)
|
||||
val acraVersion = "5.12.0"
|
||||
implementation("ch.acra:acra-http:$acraVersion")
|
||||
implementation("ch.acra:acra-dialog:$acraVersion")
|
||||
implementation("ch.acra:acra-limiter:$acraVersion")
|
||||
|
||||
// debug tools
|
||||
debugImplementation("com.jakewharton.timber:timber:5.0.1")
|
||||
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
|
||||
|
||||
// testing
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
|
||||
//noinspection GradleDependency
|
||||
testImplementation("org.robolectric:robolectric:4.16")
|
||||
testImplementation("androidx.test:core:1.7.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.arch.core:core-testing:2.2.0")
|
||||
|
||||
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
|
||||
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||
}
|
||||
|
||||
fun decode(s: String, key: String): String {
|
||||
return String(xorWithKey(Base64.getDecoder().decode(s), key.toByteArray()), Charsets.UTF_8)
|
||||
}
|
||||
|
||||
fun xorWithKey(a: ByteArray, key: ByteArray): ByteArray {
|
||||
val out = ByteArray(a.size)
|
||||
for (i in a.indices) {
|
||||
out[i] = (a[i].toInt() xor key[i % key.size].toInt()).toByte()
|
||||
}
|
||||
return out
|
||||
}
|
||||
6
app/lint.xml
Normal file
6
app/lint.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="MissingQuantity">
|
||||
<ignore regexp=".*?Czech.*?many" />
|
||||
</issue>
|
||||
</lint>
|
||||
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
@@ -1,6 +1,6 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
904
app/schemas/net.vonforst.evmap.storage.AppDatabase/22.json
Normal file
904
app/schemas/net.vonforst.evmap.storage.AppDatabase/22.json
Normal file
@@ -0,0 +1,904 @@
|
||||
{
|
||||
"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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
997
app/schemas/net.vonforst.evmap.storage.AppDatabase/23.json
Normal file
997
app/schemas/net.vonforst.evmap.storage.AppDatabase/23.json
Normal file
@@ -0,0 +1,997 @@
|
||||
{
|
||||
"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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
928
app/schemas/net.vonforst.evmap.storage.AppDatabase/24.json
Normal file
928
app/schemas/net.vonforst.evmap.storage.AppDatabase/24.json
Normal file
@@ -0,0 +1,928 @@
|
||||
{
|
||||
"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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
938
app/schemas/net.vonforst.evmap.storage.AppDatabase/27.json
Normal file
938
app/schemas/net.vonforst.evmap.storage.AppDatabase/27.json
Normal file
@@ -0,0 +1,938 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 27,
|
||||
"identityHash": "84f71cce385c444726ba336834ddf6b4",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "ChargeLocation",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `coordinates` BLOB NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `dataSourceUrl` TEXT NOT NULL, `url` TEXT, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `accessibility` TEXT, `license` TEXT, `networkUrl` TEXT, `chargerUrl` TEXT, `timeRetrieved` INTEGER NOT NULL, `isDetailed` INTEGER NOT NULL, `coordinatesProjected` BLOB NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, PRIMARY KEY(`id`, `dataSource`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "coordinates",
|
||||
"columnName": "coordinates",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepoints",
|
||||
"columnName": "chargepoints",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "network",
|
||||
"columnName": "network",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSourceUrl",
|
||||
"columnName": "dataSourceUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "editUrl",
|
||||
"columnName": "editUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "verified",
|
||||
"columnName": "verified",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "barrierFree",
|
||||
"columnName": "barrierFree",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "operator",
|
||||
"columnName": "operator",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "generalInformation",
|
||||
"columnName": "generalInformation",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "amenities",
|
||||
"columnName": "amenities",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "locationDescription",
|
||||
"columnName": "locationDescription",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photos",
|
||||
"columnName": "photos",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargecards",
|
||||
"columnName": "chargecards",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessibility",
|
||||
"columnName": "accessibility",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "license",
|
||||
"columnName": "license",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "networkUrl",
|
||||
"columnName": "networkUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargerUrl",
|
||||
"columnName": "chargerUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeRetrieved",
|
||||
"columnName": "timeRetrieved",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDetailed",
|
||||
"columnName": "isDetailed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "coordinatesProjected",
|
||||
"columnName": "coordinatesProjected",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.city",
|
||||
"columnName": "city",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.country",
|
||||
"columnName": "country",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.postcode",
|
||||
"columnName": "postcode",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "address.street",
|
||||
"columnName": "street",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "faultReport.created",
|
||||
"columnName": "fault_report_created",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "faultReport.description",
|
||||
"columnName": "fault_report_description",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.twentyfourSeven",
|
||||
"columnName": "twentyfourSeven",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.monday.start",
|
||||
"columnName": "mostart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.monday.end",
|
||||
"columnName": "moend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.tuesday.start",
|
||||
"columnName": "tustart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.tuesday.end",
|
||||
"columnName": "tuend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.wednesday.start",
|
||||
"columnName": "westart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.wednesday.end",
|
||||
"columnName": "weend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.thursday.start",
|
||||
"columnName": "thstart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.thursday.end",
|
||||
"columnName": "thend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.friday.start",
|
||||
"columnName": "frstart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.friday.end",
|
||||
"columnName": "frend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.saturday.start",
|
||||
"columnName": "sastart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.saturday.end",
|
||||
"columnName": "saend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.sunday.start",
|
||||
"columnName": "sustart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.sunday.end",
|
||||
"columnName": "suend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.holiday.start",
|
||||
"columnName": "hostart",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "openinghours.days.holiday.end",
|
||||
"columnName": "hoend",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.freecharging",
|
||||
"columnName": "freecharging",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.freeparking",
|
||||
"columnName": "freeparking",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.descriptionShort",
|
||||
"columnName": "descriptionShort",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "cost.descriptionLong",
|
||||
"columnName": "descriptionLong",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.country",
|
||||
"columnName": "chargepricecountry",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.network",
|
||||
"columnName": "chargepricenetwork",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargepriceData.plugTypes",
|
||||
"columnName": "chargepriceplugTypes",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "Favorite",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "favoriteId",
|
||||
"columnName": "favoriteId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargerId",
|
||||
"columnName": "chargerId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "chargerDataSource",
|
||||
"columnName": "chargerDataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"favoriteId"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_Favorite_chargerId_chargerDataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"chargerId",
|
||||
"chargerDataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_Favorite_chargerId_chargerDataSource` ON `${TABLE_NAME}` (`chargerId`, `chargerDataSource`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "ChargeLocation",
|
||||
"onDelete": "NO ACTION",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"chargerId",
|
||||
"chargerDataSource"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "BooleanFilterValue",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "profile",
|
||||
"columnName": "profile",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"key",
|
||||
"profile",
|
||||
"dataSource"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_BooleanFilterValue_profile_dataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_BooleanFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "FilterProfile",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "MultipleChoiceFilterValue",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `values` TEXT NOT NULL, `all` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "values",
|
||||
"columnName": "values",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "all",
|
||||
"columnName": "all",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "profile",
|
||||
"columnName": "profile",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"key",
|
||||
"profile",
|
||||
"dataSource"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_MultipleChoiceFilterValue_profile_dataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_MultipleChoiceFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "FilterProfile",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "SliderFilterValue",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `profile` INTEGER NOT NULL, PRIMARY KEY(`key`, `profile`, `dataSource`), FOREIGN KEY(`profile`, `dataSource`) REFERENCES `FilterProfile`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "key",
|
||||
"columnName": "key",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "profile",
|
||||
"columnName": "profile",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"key",
|
||||
"profile",
|
||||
"dataSource"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_SliderFilterValue_profile_dataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SliderFilterValue_profile_dataSource` ON `${TABLE_NAME}` (`profile`, `dataSource`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "FilterProfile",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"profile",
|
||||
"dataSource"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "FilterProfile",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `id` INTEGER NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`dataSource`, `id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "order",
|
||||
"columnName": "order",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"dataSource",
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_FilterProfile_dataSource_name",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"dataSource",
|
||||
"name"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_FilterProfile_dataSource_name` ON `${TABLE_NAME}` (`dataSource`, `name`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "RecentAutocompletePlace",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `dataSource` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `primaryText` TEXT NOT NULL, `secondaryText` TEXT NOT NULL, `latLng` TEXT NOT NULL, `viewport` TEXT, `types` TEXT NOT NULL, PRIMARY KEY(`id`, `dataSource`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "primaryText",
|
||||
"columnName": "primaryText",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "secondaryText",
|
||||
"columnName": "secondaryText",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "latLng",
|
||||
"columnName": "latLng",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "viewport",
|
||||
"columnName": "viewport",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "types",
|
||||
"columnName": "types",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"dataSource"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GEPlug",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GENetwork",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "GEChargeCard",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMConnectionType",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `formalName` TEXT, `discontinued` INTEGER, `obsolete` INTEGER, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "formalName",
|
||||
"columnName": "formalName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "discontinued",
|
||||
"columnName": "discontinued",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "obsolete",
|
||||
"columnName": "obsolete",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMCountry",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `isoCode` TEXT NOT NULL, `continentCode` TEXT, `title` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isoCode",
|
||||
"columnName": "isoCode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "continentCode",
|
||||
"columnName": "continentCode",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OCMOperator",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `websiteUrl` TEXT, `title` TEXT NOT NULL, `contactEmail` TEXT, `contactTelephone1` TEXT, `contactTelephone2` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "websiteUrl",
|
||||
"columnName": "websiteUrl",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactEmail",
|
||||
"columnName": "contactEmail",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactTelephone1",
|
||||
"columnName": "contactTelephone1",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "contactTelephone2",
|
||||
"columnName": "contactTelephone2",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "OSMNetwork",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "SavedRegion",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`region` BLOB NOT NULL, `dataSource` TEXT NOT NULL, `timeRetrieved` INTEGER NOT NULL, `filters` TEXT, `isDetailed` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "region",
|
||||
"columnName": "region",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dataSource",
|
||||
"columnName": "dataSource",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timeRetrieved",
|
||||
"columnName": "timeRetrieved",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "filters",
|
||||
"columnName": "filters",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isDetailed",
|
||||
"columnName": "isDetailed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_SavedRegion_filters_dataSource",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"filters",
|
||||
"dataSource"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_SavedRegion_filters_dataSource` ON `${TABLE_NAME}` (`filters`, `dataSource`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '84f71cce385c444726ba336834ddf6b4')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.johan.evmap.storage
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
171
app/src/automotive/java/net/vonforst/evmap/auto/CarInfo.kt
Normal file
171
app/src/automotive/java/net/vonforst/evmap/auto/CarInfo.kt
Normal file
@@ -0,0 +1,171 @@
|
||||
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)
|
||||
}
|
||||
5
app/src/automotive/res/values-cs/strings.xml
Normal file
5
app/src/automotive/res/values-cs/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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>
|
||||
@@ -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>
|
||||
|
||||
5
app/src/automotive/res/values-et/strings.xml
Normal file
5
app/src/automotive/res/values-et/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="grant_on_phone">Luba</string>
|
||||
<string name="auto_location_permission_needed">Et EVMap toimiks sinu autos, palun luba tal asukohta tuvastada.</string>
|
||||
</resources>
|
||||
@@ -2,4 +2,4 @@
|
||||
<resources>
|
||||
<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>
|
||||
|
||||
5
app/src/automotive/res/values-it/strings.xml
Normal file
5
app/src/automotive/res/values-it/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="grant_on_phone">Consenti</string>
|
||||
<string name="auto_location_permission_needed">Per eseguire EVMap sulla propria auto, è necessario concedere l\'accesso alla propria posizione.</string>
|
||||
</resources>
|
||||
@@ -2,4 +2,4 @@
|
||||
<resources>
|
||||
<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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
</resources>
|
||||
|
||||
5
app/src/automotive/res/values-sv/strings.xml
Normal file
5
app/src/automotive/res/values-sv/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="grant_on_phone">Tillåt</string>
|
||||
<string name="auto_location_permission_needed">Du måste tillåta platsåtkomst för att använda EVMap i din bil.</string>
|
||||
</resources>
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
|
||||
android:exported="true" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -2,41 +2,15 @@ package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.facebook.flipper.android.AndroidFlipperClient
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
|
||||
import com.facebook.soloader.SoLoader
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
private val networkFlipperPlugin = NetworkFlipperPlugin()
|
||||
import timber.log.Timber
|
||||
|
||||
fun addDebugInterceptors(context: Context) {
|
||||
if (Build.FINGERPRINT == "robolectric") return
|
||||
|
||||
SoLoader.init(context, false)
|
||||
val client = AndroidFlipperClient.getInstance(context)
|
||||
client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()))
|
||||
client.addPlugin(networkFlipperPlugin)
|
||||
client.addPlugin(DatabasesFlipperPlugin(context))
|
||||
client.addPlugin(SharedPreferencesFlipperPlugin(context))
|
||||
client.start()
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
|
||||
fun OkHttpClient.Builder.addDebugInterceptors(): OkHttpClient.Builder {
|
||||
// Flipper does not work during unit tests - so check whether we are running tests first
|
||||
var isRunningTest = true
|
||||
try {
|
||||
Class.forName("org.junit.Test")
|
||||
} catch (e: ClassNotFoundException) {
|
||||
isRunningTest = false
|
||||
}
|
||||
|
||||
if (!isRunningTest) {
|
||||
this.addNetworkInterceptor(FlipperOkhttpInterceptor(networkFlipperPlugin))
|
||||
}
|
||||
return this
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<?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>
|
||||
@@ -3,10 +3,12 @@ package net.vonforst.evmap
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun init(context: Context) {
|
||||
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun checkPlayServices(activity: Activity): Boolean {
|
||||
return true
|
||||
}
|
||||
@@ -5,16 +5,17 @@ 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
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.FragmentDonateBinding
|
||||
import net.vonforst.evmap.databinding.FragmentDonateReferralBinding
|
||||
|
||||
class DonateFragment : Fragment() {
|
||||
class DonateFragment : DonateFragmentBase() {
|
||||
private lateinit var binding: FragmentDonateBinding
|
||||
private lateinit var referrals: FragmentDonateReferralBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -28,6 +29,7 @@ class DonateFragment : Fragment() {
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentDonateBinding.inflate(inflater, container, false)
|
||||
referrals = binding.referrals
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -40,7 +42,9 @@ class DonateFragment : Fragment() {
|
||||
)
|
||||
|
||||
binding.btnDonate.setOnClickListener {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link))
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link), binding.root)
|
||||
}
|
||||
|
||||
setupReferrals(referrals)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/linearLayout2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
@@ -21,31 +19,55 @@
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnDonate"
|
||||
style="@style/Widget.Material3.Button.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/donate_paypal"
|
||||
app:icon="@drawable/ic_paypal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView20" />
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView20"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/donations_info"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnDonate"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar_container"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnDonate"
|
||||
style="@style/Widget.Material3.Button.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:text="@string/donate_paypal"
|
||||
app:icon="@drawable/ic_paypal"
|
||||
app:layout_constraintBottom_toTopOf="@id/referrals"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView20" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView20"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/donations_info"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnDonate"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<include
|
||||
android:id="@+id/referrals"
|
||||
layout="@layout/fragment_donate_referral"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="36dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/btnDonate" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
6
app/src/foss/res/values-cs/strings.xml
Normal file
6
app/src/foss/res/values-cs/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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>
|
||||
@@ -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 (Mapbox).</string>
|
||||
</resources>
|
||||
<string name="data_sources_hint">Die Kartendaten für die App stammen von OpenStreetMap.</string>
|
||||
</resources>
|
||||
|
||||
6
app/src/foss/res/values-et/strings.xml
Normal file
6
app/src/foss/res/values-et/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Kas EVMap on sulle kasulik? Oma arendajale saadetava rahalise toetusega edendad ka arendustegevust.</string>
|
||||
<string name="donate_paypal">Toeta PayPali abil</string>
|
||||
<string name="data_sources_hint">Selles rakenduses näidatavad kaardiandmed on pärit OpenStreetMapist.</string>
|
||||
</resources>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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 (Mapbox).</string>
|
||||
<string name="donations_info" formatted="false">Trouvez-vous EVMap utile ? Soutenez son développement en envoyant un don au développeur.</string>
|
||||
<string name="data_sources_hint">Les données cartographiques de l\'application sont fournies par OpenStreetMap.</string>
|
||||
<string name="donate_paypal">Faire un don avec PayPal</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
6
app/src/foss/res/values-it/strings.xml
Normal file
6
app/src/foss/res/values-it/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?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>
|
||||
@@ -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 (Mapbox).</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.</string>
|
||||
<string name="donations_info" formatted="false">Synes du EVMap er nyttig? Støtt utviklingen ved å sende en slant til utvikleren.</string>
|
||||
</resources>
|
||||
@@ -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 (Mapbox).</string>
|
||||
</resources>
|
||||
<string name="data_sources_hint">De kaartgegevens zijn afkomstig van OpenStreetMap.</string>
|
||||
</resources>
|
||||
|
||||
@@ -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 (Mapbox).</string>
|
||||
<string name="data_sources_hint">Os dados do mapa são fornecidos pelo OpenStreetMap.</string>
|
||||
<string name="donate_paypal">Doar com o PayPal</string>
|
||||
<string name="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>
|
||||
|
||||
@@ -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 (Mapbox).</string>
|
||||
<string name="data_sources_hint">Hartile din aplicatie sunt furnizate de OpenStreetMap.</string>
|
||||
</resources>
|
||||
|
||||
6
app/src/foss/res/values-sv/strings.xml
Normal file
6
app/src/foss/res/values-sv/strings.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Har du nytta av EVMap? Stöd utvecklingen genom att skicka en donation till utvecklaren.</string>
|
||||
<string name="donate_paypal">Donera med PayPal</string>
|
||||
<string name="data_sources_hint">Kartdata i appen tillhandahålls av OpenStreetMap.</string>
|
||||
</resources>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_map_provider_names">
|
||||
<item>@string/pref_provider_osm_mapbox</item>
|
||||
<item>@string/pref_provider_osm</item>
|
||||
</string-array>
|
||||
<string-array name="pref_map_provider_values" translatable="false">
|
||||
<item>mapbox</item>
|
||||
|
||||
@@ -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 (Mapbox).</string>
|
||||
<string name="data_sources_hint">Map data in the app is provided by OpenStreetMap.</string>
|
||||
</resources>
|
||||
@@ -6,11 +6,10 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
@@ -18,12 +17,17 @@ import com.google.android.material.transition.MaterialSharedAxis
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.DonationAdapter
|
||||
import net.vonforst.evmap.adapter.SingleViewAdapter
|
||||
import net.vonforst.evmap.databinding.FragmentDonateBinding
|
||||
import net.vonforst.evmap.databinding.FragmentDonateHeaderBinding
|
||||
import net.vonforst.evmap.databinding.FragmentDonateReferralBinding
|
||||
import net.vonforst.evmap.viewmodel.DonateViewModel
|
||||
|
||||
class DonateFragment : Fragment() {
|
||||
class DonateFragment : DonateFragmentBase() {
|
||||
private lateinit var binding: FragmentDonateBinding
|
||||
private val vm: DonateViewModel by viewModels()
|
||||
private lateinit var header: FragmentDonateHeaderBinding
|
||||
private lateinit var referrals: FragmentDonateReferralBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -40,6 +44,9 @@ class DonateFragment : Fragment() {
|
||||
binding.lifecycleOwner = this
|
||||
binding.vm = vm
|
||||
|
||||
header = FragmentDonateHeaderBinding.inflate(inflater, container, false)
|
||||
referrals = FragmentDonateReferralBinding.inflate(inflater, container, false)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -51,25 +58,33 @@ class DonateFragment : Fragment() {
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
binding.productsList.apply {
|
||||
adapter = DonationAdapter().apply {
|
||||
onClickListener = {
|
||||
vm.startPurchase(it, requireActivity())
|
||||
}
|
||||
val donationAdapter = DonationAdapter().apply {
|
||||
onClickListener = {
|
||||
vm.startPurchase(it, requireActivity())
|
||||
}
|
||||
}
|
||||
binding.productsList.apply {
|
||||
val joinedAdapter = ConcatAdapter(
|
||||
SingleViewAdapter(header.root),
|
||||
donationAdapter,
|
||||
SingleViewAdapter(referrals.root)
|
||||
)
|
||||
adapter = joinedAdapter
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
}
|
||||
|
||||
vm.products.observe(viewLifecycleOwner) {
|
||||
print(it)
|
||||
donationAdapter.submitList(it.data)
|
||||
}
|
||||
|
||||
vm.purchaseSuccessful.observe(viewLifecycleOwner, Observer {
|
||||
vm.purchaseSuccessful.observe(viewLifecycleOwner) {
|
||||
Snackbar.make(view, R.string.donation_successful, Snackbar.LENGTH_LONG).show()
|
||||
})
|
||||
vm.purchaseFailed.observe(viewLifecycleOwner, Observer {
|
||||
}
|
||||
vm.purchaseFailed.observe(viewLifecycleOwner) {
|
||||
Snackbar.make(view, R.string.donation_failed, Snackbar.LENGTH_LONG).show()
|
||||
})
|
||||
}
|
||||
|
||||
setupReferrals(referrals)
|
||||
|
||||
// Workaround for AndroidX bug: https://github.com/material-components/material-components-android/issues/1984
|
||||
view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.windowBackground))
|
||||
|
||||
@@ -35,29 +35,16 @@
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView20"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/donations_info"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolbar_container" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/products_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:data="@{vm.products.data}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView20"
|
||||
tools:listitem="@layout/item_donation" />
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar_container"
|
||||
tools:itemCount="1"
|
||||
tools:listitem="@layout/fragment_donate_preview" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar3"
|
||||
|
||||
10
app/src/google/res/layout/fragment_donate_header.xml
Normal file
10
app/src/google/res/layout/fragment_donate_header.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView android:id="@+id/textView20"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/donations_info"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android" />
|
||||
16
app/src/google/res/layout/fragment_donate_preview.xml
Normal file
16
app/src/google/res/layout/fragment_donate_preview.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/fragment_donate_header" />
|
||||
|
||||
<include layout="@layout/item_donation" />
|
||||
|
||||
<include layout="@layout/item_donation" />
|
||||
|
||||
<include layout="@layout/item_donation" />
|
||||
|
||||
<include layout="@layout/fragment_donate_referral" />
|
||||
</LinearLayout>
|
||||
5
app/src/google/res/values-cs/strings.xml
Normal file
5
app/src/google/res/values-cs/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?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>
|
||||
@@ -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 (Mapbox) für die Kartendaten wechseln.</string>
|
||||
</resources>
|
||||
<string name="data_sources_hint">In den Einstellungen kannst du auch zwischen Google Maps und OpenStreetMap für die Kartendaten wechseln.</string>
|
||||
</resources>
|
||||
|
||||
5
app/src/google/res/values-et/strings.xml
Normal file
5
app/src/google/res/values-et/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="data_sources_hint">Seadistustes saad valida kahe kaardiandmete allika vahel: Google Maps ja OpenStreetMap.</string>
|
||||
<string name="donations_info" formatted="false">EVMap on sinu jaoks kasulik? Toeta edasist arendust oma rahalise panusega.\n\nGoogle võtab igast toestussummast teenustasuna 15%.</string>
|
||||
</resources>
|
||||
@@ -1,7 +1,5 @@
|
||||
<?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 (Mapbox) 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 pour les données cartographiques.</string>
|
||||
</resources>
|
||||
|
||||
5
app/src/google/res/values-it/strings.xml
Normal file
5
app/src/google/res/values-it/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">Trovi utile EVMap? Sostieni il suo sviluppo inviando una donazione allo sviluppatore.\n\nGoogle si prende il 15% su ogni donazione.</string>
|
||||
<string name="data_sources_hint">Nelle impostazioni si può anche scegliere tra Google Maps e OpenStreetMap per i dati cartografici.</string>
|
||||
</resources>
|
||||
@@ -1,7 +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 (Mapbox) for kartdata.</string>
|
||||
</resources>
|
||||
5
app/src/google/res/values-nb/strings.xml
Normal file
5
app/src/google/res/values-nb/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?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>
|
||||
@@ -1,7 +1,5 @@
|
||||
<?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 (Mapbox) 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 voor de kaartgegevens.</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<?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 (Mapbox) 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 nas definições da app.</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
</resources>
|
||||
|
||||
5
app/src/google/res/values-sv/strings.xml
Normal file
5
app/src/google/res/values-sv/strings.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="donations_info" formatted="false">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>
|
||||
@@ -2,7 +2,7 @@
|
||||
<resources>
|
||||
<string-array name="pref_map_provider_names">
|
||||
<item>@string/pref_provider_google_maps</item>
|
||||
<item>@string/pref_provider_osm_mapbox</item>
|
||||
<item>@string/pref_provider_osm</item>
|
||||
</string-array>
|
||||
<string-array name="pref_map_provider_values" translatable="false">
|
||||
<item>google</item>
|
||||
|
||||
@@ -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 (Mapbox) for the map data.</string>
|
||||
<string name="data_sources_hint">In the settings you can also switch between Google Maps and OpenStreetMap for the map data.</string>
|
||||
</resources>
|
||||
@@ -7,7 +7,9 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<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" />
|
||||
|
||||
@@ -23,9 +25,14 @@
|
||||
<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" />
|
||||
<package android:name="com.google.android.apps.maps" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
@@ -39,13 +46,20 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:localeConfig="@xml/locales_config">
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<meta-data
|
||||
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"
|
||||
@@ -272,6 +286,10 @@
|
||||
android:host="openchargemap.org"
|
||||
android:pathPattern="/site/poi/details/..*"
|
||||
android:scheme="https" />
|
||||
<data
|
||||
android:host="map.openchargemap.io"
|
||||
android:path="/"
|
||||
android:scheme="https" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
@@ -289,6 +307,10 @@
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".auto.OAuthLoginActivity">
|
||||
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
@@ -327,11 +349,11 @@
|
||||
android:name=".auto.CarAppService"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="location">
|
||||
<intent-filter>
|
||||
<action
|
||||
android:name="androidx.car.app.CarAppService"
|
||||
android:category="androidx.car.app.category.POI" />
|
||||
<action android:name="androidx.car.app.CarAppService" />
|
||||
<category android:name="androidx.car.app.category.POI" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* 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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import androidx.work.*
|
||||
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 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
|
||||
import org.acra.config.httpSender
|
||||
import org.acra.config.limiter
|
||||
import org.acra.config.mailSender
|
||||
import org.acra.data.StringFormat
|
||||
import org.acra.ktx.initAcra
|
||||
import org.acra.sender.HttpSender
|
||||
import java.time.Duration
|
||||
|
||||
class EvMapApplication : Application(), Configuration.Provider {
|
||||
@@ -22,7 +30,7 @@ class EvMapApplication : Application(), Configuration.Provider {
|
||||
|
||||
// Convert to new AppCompat storage for app language
|
||||
val lang = prefs.language
|
||||
if (lang != null && lang !in listOf("", "default")) {
|
||||
if (lang != null) {
|
||||
updateAppLocale(lang)
|
||||
prefs.language = null
|
||||
}
|
||||
@@ -33,10 +41,15 @@ class EvMapApplication : Application(), Configuration.Provider {
|
||||
if (!BuildConfig.DEBUG) {
|
||||
initAcra {
|
||||
buildConfigClass = BuildConfig::class.java
|
||||
reportFormat = StringFormat.KEY_VALUE_LIST
|
||||
|
||||
mailSender {
|
||||
mailTo = "evmap+crashreport@vonforst.net"
|
||||
// Vehicles often don't have an email app, so use HTTP to send instead
|
||||
reportFormat = StringFormat.JSON
|
||||
httpSender {
|
||||
uri = getString(R.string.acra_backend_url)
|
||||
val creds = getString(R.string.acra_credentials).split(":")
|
||||
basicAuthLogin = creds[0]
|
||||
basicAuthPassword = creds[1]
|
||||
httpMethod = HttpSender.Method.POST
|
||||
}
|
||||
|
||||
dialog {
|
||||
@@ -45,6 +58,10 @@ class EvMapApplication : Application(), Configuration.Provider {
|
||||
commentPrompt = getString(R.string.crash_report_comment_prompt)
|
||||
resIcon = R.drawable.ic_launcher_foreground
|
||||
resTheme = R.style.AppTheme
|
||||
if (BuildConfig.FLAVOR_automotive == "automotive") {
|
||||
reportDialogClass =
|
||||
Class.forName("androidx.car.app.activity.CarAppActivity") as Class<out Activity>?
|
||||
}
|
||||
}
|
||||
|
||||
limiter {
|
||||
@@ -53,6 +70,7 @@ class EvMapApplication : Application(), Configuration.Provider {
|
||||
}
|
||||
}
|
||||
|
||||
val workManager = WorkManager.getInstance(this)
|
||||
val cleanupCacheRequest = PeriodicWorkRequestBuilder<CleanupCacheWorker>(Duration.ofDays(1))
|
||||
.setConstraints(Constraints.Builder().apply {
|
||||
setRequiresBatteryNotLow(true)
|
||||
@@ -60,12 +78,25 @@ class EvMapApplication : Application(), Configuration.Provider {
|
||||
setRequiresDeviceIdle(true)
|
||||
}
|
||||
}.build()).build()
|
||||
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
|
||||
"CleanupCacheWorker", ExistingPeriodicWorkPolicy.REPLACE, cleanupCacheRequest
|
||||
workManager.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 fun getWorkManagerConfiguration(): Configuration {
|
||||
return Configuration.Builder().build()
|
||||
}
|
||||
override val workManagerConfiguration = Configuration.Builder().build()
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package net.vonforst.evmap
|
||||
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
|
||||
@@ -11,12 +13,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
|
||||
@@ -44,22 +46,21 @@ 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,
|
||||
@@ -67,9 +68,9 @@ class MapsActivity : AppCompatActivity(),
|
||||
R.id.about,
|
||||
R.id.settings
|
||||
),
|
||||
findViewById<DrawerLayout>(R.id.drawer_layout)
|
||||
drawerLayout
|
||||
)
|
||||
val navHostFragment =
|
||||
navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||
navController = navHostFragment.navController
|
||||
val navGraph = navController.navInflater.inflate(R.navigation.nav_graph)
|
||||
@@ -87,6 +88,17 @@ 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
|
||||
@@ -104,133 +116,128 @@ class MapsActivity : AppCompatActivity(),
|
||||
}
|
||||
})
|
||||
}
|
||||
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
|
||||
} else if (intent?.scheme == "geo") {
|
||||
val query = intent.data?.query?.split("=")?.get(1)
|
||||
val coords = getLocationFromIntent(intent)
|
||||
|
||||
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()
|
||||
}
|
||||
} 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()
|
||||
}
|
||||
} else if (intent?.scheme == "https" && intent?.data?.host == "openchargemap.org") {
|
||||
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
|
||||
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)) {
|
||||
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(
|
||||
chargerId = intent.getLongExtra(EXTRA_CHARGER_ID, 0),
|
||||
latLng = LatLng(
|
||||
intent.getDoubleExtra(EXTRA_LAT, 0.0),
|
||||
intent.getDoubleExtra(EXTRA_LON, 0.0)
|
||||
)
|
||||
).toBundle()
|
||||
)
|
||||
.setArguments(MapFragmentArgs(latLng = LatLng(lat, lon)).toBundle())
|
||||
.createPendingIntent()
|
||||
} else if (intent.hasExtra(EXTRA_FAVORITES)) {
|
||||
} else if (!query.isNullOrEmpty()) {
|
||||
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)
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragmentArgs(locationName = query).toBundle())
|
||||
.createPendingIntent()
|
||||
}
|
||||
|
||||
deepLink?.send()
|
||||
} 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()
|
||||
}
|
||||
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()
|
||||
}
|
||||
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(
|
||||
chargerId = intent.getLongExtra(EXTRA_CHARGER_ID, 0),
|
||||
latLng = LatLng(
|
||||
intent.getDoubleExtra(EXTRA_LAT, 0.0),
|
||||
intent.getDoubleExtra(EXTRA_LON, 0.0)
|
||||
)
|
||||
).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()
|
||||
}
|
||||
|
||||
deepLink?.send()
|
||||
}
|
||||
|
||||
fun navigateTo(charger: ChargeLocation) {
|
||||
fun navigateTo(charger: ChargeLocation, rootView: View) {
|
||||
// google maps navigation
|
||||
val coord = charger.coordinates
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
@@ -240,11 +247,11 @@ class MapsActivity : AppCompatActivity(),
|
||||
startActivity(intent)
|
||||
} else {
|
||||
// fallback: generic geo intent
|
||||
showLocation(charger)
|
||||
showLocation(charger, rootView)
|
||||
}
|
||||
}
|
||||
|
||||
fun showLocation(charger: ChargeLocation) {
|
||||
fun showLocation(charger: ChargeLocation, rootView: View) {
|
||||
val coord = charger.coordinates
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse(
|
||||
@@ -252,20 +259,33 @@ class MapsActivity : AppCompatActivity(),
|
||||
Uri.encode(charger.name)
|
||||
})"
|
||||
)
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
|
||||
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 {
|
||||
startActivity(intent)
|
||||
} else {
|
||||
val cb = fragmentCallback ?: return
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Snackbar.make(
|
||||
cb.getRootView(),
|
||||
rootView,
|
||||
R.string.no_maps_app_found,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun openUrl(url: String) {
|
||||
val pkg = CustomTabsClient.getPackageName(this, null)
|
||||
fun openUrl(url: String, rootView: View, preferBrowser: Boolean = false) {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
@@ -273,17 +293,49 @@ class MapsActivity : AppCompatActivity(),
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
pkg?.let {
|
||||
// prefer to open URL in custom tab, even if native app
|
||||
// available (such as EVMap itself)
|
||||
|
||||
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)
|
||||
}
|
||||
try {
|
||||
intent.launchUrl(this, Uri.parse(url))
|
||||
intent.launchUrl(this, uri)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
val cb = fragmentCallback ?: return
|
||||
Snackbar.make(
|
||||
cb.getRootView(),
|
||||
rootView,
|
||||
R.string.no_browser_app_found,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Typeface
|
||||
import android.icu.util.LocaleData
|
||||
import android.icu.util.ULocale
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.*
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.SpannedString
|
||||
import android.text.TextUtils
|
||||
import android.text.style.StyleSpan
|
||||
import java.util.*
|
||||
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? {
|
||||
if (!this.containsKey(name)) return null
|
||||
@@ -88,9 +99,25 @@ fun Context.isDarkMode() =
|
||||
|
||||
const val kmPerMile = 1.609344
|
||||
const val meterPerFt = 0.3048
|
||||
const val ftPerMile = 5280
|
||||
const val ydPerMile = 1760
|
||||
|
||||
fun shouldUseImperialUnits(ctx: Context): Boolean {
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
return when (prefs.units) {
|
||||
"metric" -> false
|
||||
"imperial" -> true
|
||||
else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
when (LocaleData.getMeasurementSystem(ULocale.getDefault())) {
|
||||
LocaleData.MeasurementSystem.US, LocaleData.MeasurementSystem.UK -> true
|
||||
LocaleData.MeasurementSystem.SI -> false
|
||||
else -> false
|
||||
}
|
||||
} else {
|
||||
return Locale.getDefault().country in listOf("US", "GB", "MM", "LR")
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldUseImperialUnits(): Boolean {
|
||||
return Locale.getDefault().country in listOf("US", "GB", "MM", "LR")
|
||||
}
|
||||
|
||||
fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): PackageInfo =
|
||||
@@ -98,4 +125,31 @@ fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): Pa
|
||||
getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION") getPackageInfo(packageName, flags)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int = 0): ApplicationInfo =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(flags.toLong()))
|
||||
} else {
|
||||
@Suppress("DEPRECATION") getApplicationInfo(packageName, flags)
|
||||
}
|
||||
|
||||
|
||||
fun PackageManager.isAppInstalled(packageName: String): Boolean {
|
||||
return try {
|
||||
getApplicationInfoCompat(packageName, 0).enabled
|
||||
} 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()
|
||||
}
|
||||
})
|
||||
@@ -5,22 +5,13 @@ 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 {
|
||||
override fun equals(other: Any?): Boolean
|
||||
@@ -30,6 +21,7 @@ abstract class DataBindingAdapter<T : Equatable>(getKey: ((T) -> Any)? = null) :
|
||||
ListAdapter<T, DataBindingAdapter.ViewHolder<T>>(DiffCallback(getKey)) {
|
||||
|
||||
var onClickListener: ((T) -> Unit)? = null
|
||||
var onLongClickListener: ((T) -> Boolean)? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder<T> {
|
||||
val layoutInflater = LayoutInflater.from(parent.context)
|
||||
@@ -54,6 +46,12 @@ abstract class DataBindingAdapter<T : Equatable>(getKey: ((T) -> Any)? = null) :
|
||||
listener(item)
|
||||
}
|
||||
}
|
||||
if (onLongClickListener != null) {
|
||||
holder.binding.root.setOnLongClickListener {
|
||||
val listener = onLongClickListener ?: return@setOnLongClickListener false
|
||||
return@setOnLongClickListener listener(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DiffCallback<T : Equatable>(val getKey: ((T) -> Any)?) : DiffUtil.ItemCallback<T>() {
|
||||
@@ -87,139 +85,14 @@ class ConnectorAdapter : DataBindingAdapter<ConnectorAdapter.ChargepointWithAvai
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_connector
|
||||
}
|
||||
|
||||
class ChargepriceAdapter() :
|
||||
DataBindingAdapter<ChargePrice>() {
|
||||
class ConnectorDetailsAdapter : DataBindingAdapter<ConnectorDetailsAdapter.ConnectorDetails>() {
|
||||
data class ConnectorDetails(
|
||||
val status: ChargepointStatus?,
|
||||
val evseId: String?,
|
||||
val label: String?,
|
||||
val lastChange: Instant?
|
||||
) :
|
||||
Equatable
|
||||
|
||||
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
|
||||
override fun getItemViewType(position: Int): Int = R.layout.dialog_connector_details_item
|
||||
}
|
||||
@@ -7,12 +7,14 @@ import android.text.style.StyleSpan
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.text.buildSpannedString
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.TeslaGraphQlApi
|
||||
import net.vonforst.evmap.api.availability.tesla.Pricing
|
||||
import net.vonforst.evmap.api.availability.tesla.Rates
|
||||
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
|
||||
@@ -47,7 +49,7 @@ fun buildDetails(
|
||||
loc: ChargeLocation?,
|
||||
chargeCards: Map<Long, ChargeCard>?,
|
||||
filteredChargeCards: Set<Long>?,
|
||||
teslaPricing: TeslaGraphQlApi.Pricing?,
|
||||
teslaPricing: Pricing?,
|
||||
ctx: Context
|
||||
): List<DetailsAdapter.Detail> {
|
||||
if (loc == null) return emptyList()
|
||||
@@ -139,7 +141,7 @@ fun buildDetails(
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatTeslaParkingFee(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
|
||||
fun formatTeslaParkingFee(teslaPricing: Pricing, ctx: Context) =
|
||||
teslaPricing.memberRates?.activePricebook?.parking?.let { parkingFee ->
|
||||
ctx.getString(
|
||||
R.string.tesla_pricing_blocking_fee,
|
||||
@@ -147,7 +149,7 @@ private fun formatTeslaParkingFee(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Co
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatTeslaPricing(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Context) =
|
||||
fun formatTeslaPricing(teslaPricing: Pricing, ctx: Context) =
|
||||
buildSpannedString {
|
||||
teslaPricing.memberRates?.let { memberRates ->
|
||||
append(
|
||||
@@ -168,7 +170,7 @@ private fun formatTeslaPricing(teslaPricing: TeslaGraphQlApi.Pricing, ctx: Conte
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTeslaPricingRates(rates: TeslaGraphQlApi.Rates, ctx: Context) =
|
||||
private fun formatTeslaPricingRates(rates: Rates, ctx: Context) =
|
||||
buildSpannedString {
|
||||
val timeFmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
if (rates.activePricebook.charging.touRates.enabled) {
|
||||
|
||||
@@ -5,16 +5,15 @@ 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) :
|
||||
@@ -40,12 +39,9 @@ class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener?
|
||||
val item = getItem(position)
|
||||
|
||||
if (holder.view.height == 0) {
|
||||
holder.view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
holder.view.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
loadImage(item, holder)
|
||||
}
|
||||
})
|
||||
holder.view.waitForLayout {
|
||||
loadImage(item, holder)
|
||||
}
|
||||
} else {
|
||||
loadImage(item, holder)
|
||||
}
|
||||
@@ -71,7 +67,7 @@ class GalleryAdapter(context: Context, val itemClickListener: ItemClickListener?
|
||||
memoryKeys[item.id] = metadata.memoryCacheKey
|
||||
}
|
||||
)
|
||||
allowHardware(!BuildConfig.DEBUG)
|
||||
allowHardware(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ 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
|
||||
@@ -58,6 +60,27 @@ 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 {
|
||||
@@ -72,6 +95,13 @@ fun Context.stringProvider() = object : StringProvider {
|
||||
|
||||
fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
|
||||
return when (type) {
|
||||
"nobil" -> {
|
||||
NobilApiWrapper(
|
||||
ctx.getString(
|
||||
R.string.nobil_key
|
||||
)
|
||||
)
|
||||
}
|
||||
"openchargemap" -> {
|
||||
OpenChargeMapApiWrapper(
|
||||
ctx.getString(
|
||||
@@ -79,6 +109,7 @@ fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"goingelectric" -> {
|
||||
GoingElectricApiWrapper(
|
||||
ctx.getString(
|
||||
@@ -86,6 +117,11 @@ fun createApi(type: String, ctx: Context): ChargepointApi<ReferenceData> {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"openstreetmap" -> {
|
||||
OpenStreetMapApiWrapper()
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
@@ -100,4 +136,20 @@ 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
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
package net.vonforst.evmap.api
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.TimeSource
|
||||
|
||||
|
||||
class RateLimitInterceptor : Interceptor {
|
||||
private val rateLimiter = RateLimiter.create(3.0)
|
||||
private val rateLimiter = SimpleRateLimiter(3.0)
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
if (request.url.host == "ui-map.shellrecharge.com") {
|
||||
// limit requests sent to NewMotion to 3 per second
|
||||
rateLimiter.acquire(1)
|
||||
rateLimiter.acquire()
|
||||
|
||||
var response: Response = chain.proceed(request)
|
||||
// 403 is how the NewMotion API indicates a rate limit error
|
||||
@@ -30,4 +32,27 @@ class RateLimitInterceptor : Interceptor {
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class SimpleRateLimiter(private val permitsPerSecond: Double) {
|
||||
private val interval: Duration = (1.0 / permitsPerSecond).seconds
|
||||
private var nextAvailable = TimeSource.Monotonic.markNow()
|
||||
|
||||
@Synchronized
|
||||
fun acquire() {
|
||||
val now = TimeSource.Monotonic.markNow()
|
||||
if (now < nextAvailable) {
|
||||
val waitTime = nextAvailable - now
|
||||
waitTime.sleep()
|
||||
nextAvailable += interval
|
||||
} else {
|
||||
nextAvailable = now + interval
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Duration.sleep() {
|
||||
if (this.isPositive()) {
|
||||
Thread.sleep(this.inWholeMilliseconds, (this.inWholeNanoseconds % 1_000_000).toInt())
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,17 @@
|
||||
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,
|
||||
Chargepoint.TYPE_2_PLUG to R.string.plug_type_2_tethered,
|
||||
Chargepoint.TYPE_2_SOCKET to R.string.plug_type_2,
|
||||
Chargepoint.TYPE_3 to R.string.plug_type_3,
|
||||
Chargepoint.TYPE_3A to R.string.plug_type_3a,
|
||||
Chargepoint.TYPE_3C to R.string.plug_type_3c,
|
||||
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,
|
||||
@@ -101,7 +64,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 -> 0
|
||||
else -> R.drawable.ic_connector_unknown
|
||||
}
|
||||
|
||||
val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 75, 100, 150, 200, 250, 300, 350)
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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.equivalentPlugTypes
|
||||
import net.vonforst.evmap.cartesianProduct
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.storage.EncryptedPreferenceDataStore
|
||||
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
|
||||
import java.net.CookiePolicy
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
interface AvailabilityDetector {
|
||||
@@ -36,16 +38,6 @@ 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? {
|
||||
@@ -136,7 +128,9 @@ data class ChargeLocationStatus(
|
||||
val status: Map<Chargepoint, List<ChargepointStatus>>,
|
||||
val source: String,
|
||||
val evseIds: Map<Chargepoint, List<String>>? = null,
|
||||
val labels: Map<Chargepoint, List<String?>>? = null,
|
||||
val congestionHistogram: List<Double>? = null,
|
||||
val lastChange: Map<Chargepoint, List<Instant?>>? = null,
|
||||
val extraData: Any? = null // API-specific data
|
||||
) {
|
||||
fun applyFilters(connectors: Set<String>?, minPower: Int?): ChargeLocationStatus {
|
||||
@@ -152,51 +146,73 @@ data class ChargeLocationStatus(
|
||||
val totalChargepoints = status.map { it.key.count }.sum()
|
||||
}
|
||||
|
||||
enum class ChargepointStatus {
|
||||
@Parcelize
|
||||
enum class ChargepointStatus : Parcelable {
|
||||
AVAILABLE, UNKNOWN, CHARGING, OCCUPIED, FAULTED
|
||||
}
|
||||
|
||||
class AvailabilityDetectorException(message: String) : Exception(message)
|
||||
|
||||
class NotSignedInException : IOException("not signed in")
|
||||
|
||||
private val cookieManager = CookieManager().apply {
|
||||
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
|
||||
}
|
||||
|
||||
class AvailabilityRepository(context: Context) {
|
||||
private val cacheSize = 5L * 1024 * 1024 // 5MB
|
||||
private val okhttp = OkHttpClient.Builder()
|
||||
.addInterceptor(RateLimitInterceptor())
|
||||
.addDebugInterceptors()
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.cookieJar(JavaNetCookieJar(cookieManager))
|
||||
.cache(Cache(context.cacheDir, cacheSize))
|
||||
.build()
|
||||
private val teslaOwnerAvailabilityDetector =
|
||||
TeslaOwnerAvailabilityDetector(okhttp, EncryptedPreferenceDataStore(context))
|
||||
private val availabilityDetectors = listOf(
|
||||
RheinenergieAvailabilityDetector(okhttp),
|
||||
TeslaAvailabilityDetector(okhttp, EncryptedPreferenceDataStore(context)),
|
||||
teslaOwnerAvailabilityDetector,
|
||||
TeslaGuestAvailabilityDetector(okhttp),
|
||||
EnBwAvailabilityDetector(okhttp),
|
||||
NewMotionAvailabilityDetector(okhttp)
|
||||
)
|
||||
|
||||
suspend fun getAvailability(charger: ChargeLocation): Resource<ChargeLocationStatus> {
|
||||
var value: Resource<ChargeLocationStatus>? = null
|
||||
var result: ChargeLocationStatus? = null
|
||||
var exception: Throwable? = null
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
for (ad in availabilityDetectors) {
|
||||
if (!ad.isChargerSupported(charger)) continue
|
||||
try {
|
||||
value = Resource.success(ad.getAvailability(charger))
|
||||
result = ad.getAvailability(charger)
|
||||
break
|
||||
} catch (e: IOException) {
|
||||
value = Resource.error(e.message, null)
|
||||
exception = exception.takeIf { it is NotSignedInException } ?: e
|
||||
e.printStackTrace()
|
||||
} catch (e: HttpException) {
|
||||
value = Resource.error(e.message, null)
|
||||
exception = exception.takeIf { it is NotSignedInException } ?: e
|
||||
e.printStackTrace()
|
||||
} catch (e: AvailabilityDetectorException) {
|
||||
value = Resource.error(e.message, null)
|
||||
exception = exception.takeIf { it is NotSignedInException } ?: e
|
||||
e.printStackTrace()
|
||||
} catch (e: NotSignedInException) {
|
||||
exception = e
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
return value ?: Resource.error(null, null)
|
||||
result?.let {
|
||||
return Resource.success(it)
|
||||
}
|
||||
return Resource.error(exception?.message, null)
|
||||
}
|
||||
|
||||
fun isSupercharger(charger: ChargeLocation) =
|
||||
teslaOwnerAvailabilityDetector.isChargerSupported(charger)
|
||||
|
||||
fun isTeslaSupported(charger: ChargeLocation) =
|
||||
teslaOwnerAvailabilityDetector.isChargerSupported(charger) && teslaOwnerAvailabilityDetector.isSignedIn()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.ToJson
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
@@ -10,6 +13,7 @@ import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
import java.time.Instant
|
||||
|
||||
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
|
||||
@@ -53,7 +57,8 @@ interface EnBwApi {
|
||||
data class EnBwChargePoint(
|
||||
val evseId: String?,
|
||||
val status: String,
|
||||
val connectors: List<EnBwConnector>
|
||||
val connectors: List<EnBwConnector>,
|
||||
val state: EnBwState?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@@ -70,6 +75,11 @@ interface EnBwApi {
|
||||
val upperRightLon: Double
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class EnBwState(
|
||||
val updatedAt: Instant?
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun create(client: OkHttpClient, baseUrl: String? = null): EnBwApi {
|
||||
val clientWithInterceptor = client.newBuilder()
|
||||
@@ -85,7 +95,11 @@ interface EnBwApi {
|
||||
}.build()
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl ?: "https://enbw-emp.azure-api.net/emobility-public-api/api/v1/")
|
||||
.addConverterFactory(MoshiConverterFactory.create())
|
||||
.addConverterFactory(
|
||||
MoshiConverterFactory.create(
|
||||
Moshi.Builder().add(InstantAdapter()).build()
|
||||
)
|
||||
)
|
||||
.client(clientWithInterceptor)
|
||||
.build()
|
||||
return retrofit.create(EnBwApi::class.java)
|
||||
@@ -93,6 +107,23 @@ interface EnBwApi {
|
||||
}
|
||||
}
|
||||
|
||||
internal class InstantAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: Long?): Instant? = value?.let {
|
||||
Instant.ofEpochMilli(it)
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: Instant?): Long? = value?.toEpochMilli()
|
||||
}
|
||||
|
||||
data class EnBwStatus(
|
||||
val conn: EnBwApi.EnBwConnector,
|
||||
val status: String,
|
||||
val evseId: String?,
|
||||
val lastChange: Instant?
|
||||
)
|
||||
|
||||
class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
BaseAvailabilityDetector(client) {
|
||||
val api = EnBwApi.create(client, baseUrl)
|
||||
@@ -105,20 +136,19 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
var markers =
|
||||
api.getMarkers(lng - coordRange, lng + coordRange, lat - coordRange, lat + coordRange)
|
||||
|
||||
while (markers.any { it.grouped }) {
|
||||
markers = markers.flatMap {
|
||||
if (it.grouped) {
|
||||
api.getMarkers(
|
||||
it.viewPort.lowerLeftLon,
|
||||
it.viewPort.upperRightLon,
|
||||
it.viewPort.lowerLeftLat,
|
||||
it.viewPort.upperRightLat
|
||||
)
|
||||
} else {
|
||||
listOf(it)
|
||||
}
|
||||
markers = markers.flatMap {
|
||||
if (it.grouped) {
|
||||
api.getMarkers(
|
||||
it.viewPort.lowerLeftLon,
|
||||
it.viewPort.upperRightLon,
|
||||
it.viewPort.lowerLeftLat,
|
||||
it.viewPort.upperRightLat
|
||||
)
|
||||
} else {
|
||||
listOf(it)
|
||||
}
|
||||
}
|
||||
if (markers.any { it.grouped }) throw AvailabilityDetectorException("markers still grouped")
|
||||
|
||||
val nearest = markers.minByOrNull { marker ->
|
||||
distanceBetween(marker.lat, marker.lon, lat, lng)
|
||||
@@ -158,20 +188,22 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
|
||||
val connectorStatus = details.flatMap { it.chargePoints }.flatMap { cp ->
|
||||
cp.connectors.map { connector ->
|
||||
Triple(connector, cp.status, cp.evseId)
|
||||
EnBwStatus(connector, cp.status, cp.evseId, cp.state?.updatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
val enbwConnectors = mutableMapOf<Long, Pair<Double, String>>()
|
||||
val enbwStatus = mutableMapOf<Long, ChargepointStatus>()
|
||||
val enbwEvseId = mutableMapOf<Long, String>()
|
||||
connectorStatus.forEachIndexed { index, (connector, statusStr, evseId) ->
|
||||
val enbwLastChange = mutableMapOf<Long, Instant?>()
|
||||
connectorStatus.forEachIndexed { index, (connector, statusStr, evseId, updatedAt) ->
|
||||
val id = index.toLong()
|
||||
val power = connector.maxPowerInKw ?: 0.0
|
||||
val type = when (connector.plugTypeName) {
|
||||
"Typ 3A" -> Chargepoint.TYPE_3
|
||||
"Typ 3A" -> Chargepoint.TYPE_3A
|
||||
"Typ 3C \"Scame\"" -> Chargepoint.TYPE_3C
|
||||
"Typ 2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||
"Typ 1" -> Chargepoint.TYPE_1
|
||||
"Typ 1 Steckdose" -> 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
|
||||
@@ -188,6 +220,7 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
}
|
||||
enbwConnectors[id] = power to type
|
||||
enbwStatus[id] = status
|
||||
enbwLastChange[id] = updatedAt
|
||||
evseId?.let { enbwEvseId[id] = it }
|
||||
}
|
||||
|
||||
@@ -198,16 +231,19 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
val evseIds = if (enbwEvseId.size == enbwStatus.size) match.mapValues { entry ->
|
||||
entry.value.map { enbwEvseId[it]!! }
|
||||
} else null
|
||||
val lastChange =
|
||||
if (enbwLastChange.size == enbwStatus.size) match.mapValues { entry -> entry.value.map { enbwLastChange[it] } } else null
|
||||
return ChargeLocationStatus(
|
||||
chargepointStatus,
|
||||
"EnBW",
|
||||
evseIds
|
||||
evseIds,
|
||||
lastChange = lastChange
|
||||
)
|
||||
}
|
||||
|
||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||
val country = charger.chargepriceData?.country
|
||||
?: charger.address?.country ?: return false
|
||||
val country = charger.chargepriceData?.country ?: charger.address?.country
|
||||
|
||||
return when (charger.dataSource) {
|
||||
// list of countries as of 2023/04/14, according to
|
||||
// https://www.enbw.com/elektromobilitaet/produkte/ladetarife
|
||||
@@ -230,6 +266,7 @@ class EnBwAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
"Spanien",
|
||||
"Tschechien"
|
||||
) && charger.network != "Tesla Supercharger"
|
||||
"nobil" -> charger.network != "Tesla"
|
||||
"openchargemap" -> country in listOf(
|
||||
"DE",
|
||||
"AT",
|
||||
@@ -249,6 +286,12 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.ToJson
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
@@ -9,7 +12,8 @@ import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import java.util.*
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Locale
|
||||
|
||||
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
|
||||
@@ -42,7 +46,12 @@ interface NewMotionApi {
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NMEvse(val evseId: String?, val status: String, val connectors: List<NMConnector>)
|
||||
data class NMEvse(
|
||||
val evseId: String?,
|
||||
val status: String,
|
||||
val connectors: List<NMConnector>,
|
||||
val updated: ZonedDateTime?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NMConnector(
|
||||
@@ -52,8 +61,9 @@ interface NewMotionApi {
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class NMElectricalProperties(val powerType: String, val voltage: Int, val amperage: Int) {
|
||||
data class NMElectricalProperties(val powerType: String, val voltage: Int, val amperage: Int, val maxElectricPower: Double?) {
|
||||
fun getPower(): Double {
|
||||
maxElectricPower?.let { return it }
|
||||
val phases = when (powerType) {
|
||||
"AC1Phase" -> 1
|
||||
"AC3Phase" -> 3
|
||||
@@ -78,7 +88,11 @@ interface NewMotionApi {
|
||||
fun create(client: OkHttpClient, baseUrl: String? = null): NewMotionApi {
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl ?: "https://ui-map.shellrecharge.com/api/map/v2/")
|
||||
.addConverterFactory(MoshiConverterFactory.create())
|
||||
.addConverterFactory(
|
||||
MoshiConverterFactory.create(
|
||||
Moshi.Builder().add(ZonedDateTimeAdapter()).build()
|
||||
)
|
||||
)
|
||||
.client(client)
|
||||
.build()
|
||||
return retrofit.create(NewMotionApi::class.java)
|
||||
@@ -86,6 +100,21 @@ interface NewMotionApi {
|
||||
}
|
||||
}
|
||||
|
||||
internal class ZonedDateTimeAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String): ZonedDateTime? = ZonedDateTime.parse(value)
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: ZonedDateTime): String = value.toString()
|
||||
}
|
||||
|
||||
data class NmStatus(
|
||||
val conn: NewMotionApi.NMConnector,
|
||||
val status: String,
|
||||
val evseId: String?,
|
||||
val updated: ZonedDateTime?
|
||||
)
|
||||
|
||||
class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = null) :
|
||||
BaseAvailabilityDetector(client) {
|
||||
val api = NewMotionApi.create(client, baseUrl)
|
||||
@@ -111,9 +140,9 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
throw AvailabilityDetectorException("no candidates found")
|
||||
}
|
||||
|
||||
if (nearest.evseCount < location.totalChargepoints) {
|
||||
markers = if (nearest.evseCount < location.totalChargepoints) {
|
||||
// combine related stations
|
||||
markers = markers.filter { marker ->
|
||||
markers.filter { marker ->
|
||||
distanceBetween(
|
||||
marker.coordinates.latitude,
|
||||
marker.coordinates.longitude,
|
||||
@@ -122,7 +151,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
) < maxDistance
|
||||
}
|
||||
} else {
|
||||
markers = listOf(nearest)
|
||||
listOf(nearest)
|
||||
}
|
||||
|
||||
// load details
|
||||
@@ -135,18 +164,19 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
}
|
||||
val connectorStatus = details.flatMap { it.evses }.flatMap { evse ->
|
||||
evse.connectors.map { connector ->
|
||||
Triple(connector, evse.status, evse.evseId)
|
||||
NmStatus(connector, evse.status, evse.evseId, evse.updated)
|
||||
}
|
||||
}
|
||||
|
||||
val nmConnectors = mutableMapOf<Long, Pair<Double, String>>()
|
||||
val nmStatus = mutableMapOf<Long, ChargepointStatus>()
|
||||
val nmEvseId = mutableMapOf<Long, String>()
|
||||
connectorStatus.forEach { (connector, statusStr, evseId) ->
|
||||
val nmUpdated = mutableMapOf<Long, ZonedDateTime>()
|
||||
connectorStatus.forEach { (connector, statusStr, evseId, updated) ->
|
||||
val id = connector.uid
|
||||
val power = connector.electricalProperties.getPower()
|
||||
val type = when (connector.connectorType.lowercase(Locale.ROOT)) {
|
||||
"type3" -> Chargepoint.TYPE_3
|
||||
"type3" -> Chargepoint.TYPE_3C
|
||||
"type2" -> Chargepoint.TYPE_2_UNKNOWN
|
||||
"type1" -> Chargepoint.TYPE_1
|
||||
"domestic" -> Chargepoint.SCHUKO
|
||||
@@ -168,6 +198,7 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
nmConnectors.put(id, power to type)
|
||||
nmStatus.put(id, status)
|
||||
evseId?.let { nmEvseId[id] = it }
|
||||
updated?.let { nmUpdated[id] = it }
|
||||
}
|
||||
|
||||
val match = matchChargepoints(nmConnectors, location.chargepointsMerged)
|
||||
@@ -177,10 +208,12 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
val evseIds = if (nmEvseId.size == nmStatus.size) match.mapValues { entry ->
|
||||
entry.value.map { nmEvseId[it]!! }
|
||||
} else null
|
||||
val updated = match.mapValues { entry -> entry.value.map { nmUpdated[it]?.toInstant() } }
|
||||
return ChargeLocationStatus(
|
||||
chargepointStatus,
|
||||
"NewMotion",
|
||||
evseIds
|
||||
evseIds,
|
||||
lastChange = updated
|
||||
)
|
||||
}
|
||||
|
||||
@@ -188,7 +221,9 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import net.vonforst.evmap.api.availability.tesla.ChargerAvailability
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaChargingGuestGraphQlApi
|
||||
import net.vonforst.evmap.api.availability.tesla.TeslaCuaApi
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
||||
|
||||
class TeslaGuestAvailabilityDetector(
|
||||
client: OkHttpClient,
|
||||
baseUrl: String? = null
|
||||
) :
|
||||
BaseAvailabilityDetector(client) {
|
||||
|
||||
private var cuaApi = TeslaCuaApi.create(client, baseUrl)
|
||||
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 =
|
||||
results.minByOrNull {
|
||||
if (it.latitude != null && it.longitude != null) {
|
||||
distanceBetween(
|
||||
it.latitude,
|
||||
it.longitude,
|
||||
location.coordinates.lat,
|
||||
location.coordinates.lng
|
||||
)
|
||||
} else Double.POSITIVE_INFINITY
|
||||
} ?: throw AvailabilityDetectorException("no candidates found.")
|
||||
|
||||
val resultDetails = try {
|
||||
cuaApi.getTeslaLocation(result.locationId)
|
||||
} catch (e: JsonDataException) {
|
||||
// instead of a single location, this may also return an empty JSON list []. This is hard to fix with Moshi
|
||||
if (e.message == "Expected BEGIN_OBJECT but was BEGIN_ARRAY at path \$") {
|
||||
throw AvailabilityDetectorException("no candidates found.")
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
val trtId = resultDetails.trtId?.toLongOrNull()
|
||||
?: throw AvailabilityDetectorException("charger data not available through guest API")
|
||||
|
||||
val (detailsA, guestPricing) = coroutineScope {
|
||||
val details = async {
|
||||
api.getSiteDetails(
|
||||
TeslaChargingGuestGraphQlApi.GetSiteDetailsRequest(
|
||||
TeslaChargingGuestGraphQlApi.GetSiteDetailsVariables(
|
||||
TeslaChargingGuestGraphQlApi.Identifier(
|
||||
TeslaChargingGuestGraphQlApi.ChargingSiteIdentifier(
|
||||
trtId, TeslaChargingGuestGraphQlApi.Experience.ADHOC
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
).data.chargingNetwork?.site
|
||||
?: throw AvailabilityDetectorException("no candidates found.")
|
||||
}
|
||||
val guestPricing = async {
|
||||
api.getSiteDetails(
|
||||
TeslaChargingGuestGraphQlApi.GetSiteDetailsRequest(
|
||||
TeslaChargingGuestGraphQlApi.GetSiteDetailsVariables(
|
||||
TeslaChargingGuestGraphQlApi.Identifier(
|
||||
TeslaChargingGuestGraphQlApi.ChargingSiteIdentifier(
|
||||
trtId, TeslaChargingGuestGraphQlApi.Experience.GUEST
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
).data.chargingNetwork?.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(
|
||||
Chargepoint.CCS_TYPE_2,
|
||||
Chargepoint.CCS_UNKNOWN
|
||||
) && it.power != null && it.power <= 150
|
||||
}
|
||||
if (scV2CCSConnectors.sumOf { it.count } != 0 && scV2CCSConnectors.sumOf { it.count } != scV2Connectors.sumOf { it.count }) {
|
||||
throw AvailabilityDetectorException("number of V2 connectors does not match number of V2 CCS connectors")
|
||||
}
|
||||
val scV3Connectors = location.chargepoints.filter {
|
||||
it.type in listOf(
|
||||
Chargepoint.CCS_TYPE_2,
|
||||
Chargepoint.CCS_UNKNOWN
|
||||
) && it.power != null && it.power > 150
|
||||
}
|
||||
if (location.totalChargepoints != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } + scV2CCSConnectors.sumOf { it.count }) throw AvailabilityDetectorException(
|
||||
"charger has unknown connectors"
|
||||
)
|
||||
|
||||
var detailsSorted = details.chargerList
|
||||
.sortedBy { c -> c.labelLetter }
|
||||
.sortedBy { c -> c.labelNumber }
|
||||
|
||||
if (detailsSorted.size != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) {
|
||||
// apparently some connectors are missing in Tesla data
|
||||
// If we have just one type of charger, we can still match
|
||||
val numMissing =
|
||||
scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } - detailsSorted.size
|
||||
if ((scV2Connectors.isEmpty() || scV3Connectors.isEmpty()) && numMissing > 0) {
|
||||
detailsSorted =
|
||||
detailsSorted + List(numMissing) {
|
||||
TeslaChargingGuestGraphQlApi.ChargerDetail(
|
||||
ChargerAvailability.UNKNOWN,
|
||||
"", ""
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw AvailabilityDetectorException("Tesla API chargepoints do not match data source")
|
||||
}
|
||||
}
|
||||
|
||||
val detailsMap =
|
||||
mutableMapOf<Chargepoint, List<TeslaChargingGuestGraphQlApi.ChargerDetail>>()
|
||||
var i = 0
|
||||
for (connector in scV2Connectors) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
if (scV2CCSConnectors.isNotEmpty()) {
|
||||
i = 0
|
||||
for (connector in scV2CCSConnectors) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
}
|
||||
for (connector in scV3Connectors) {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
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.TeslaChargingOwnershipGraphQlApi
|
||||
import net.vonforst.evmap.api.availability.tesla.asTeslaCoord
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import okhttp3.OkHttpClient
|
||||
import java.time.Instant
|
||||
import java.util.Collections
|
||||
|
||||
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
||||
|
||||
class TeslaOwnerAvailabilityDetector(
|
||||
private val client: OkHttpClient,
|
||||
private val tokenStore: TokenStore,
|
||||
private val baseUrl: String? = null
|
||||
) :
|
||||
BaseAvailabilityDetector(client) {
|
||||
|
||||
private val authApi = TeslaAuthenticationApi.create(client, null)
|
||||
private var api: TeslaChargingOwnershipGraphQlApi? = null
|
||||
|
||||
interface TokenStore {
|
||||
var teslaRefreshToken: String?
|
||||
var teslaAccessToken: String?
|
||||
var teslaAccessTokenExpiry: Long
|
||||
}
|
||||
|
||||
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(
|
||||
TeslaChargingOwnershipGraphQlApi.GetNearbyChargingSitesArgs(
|
||||
location.coordinates.asTeslaCoord(),
|
||||
TeslaChargingOwnershipGraphQlApi.Coordinate(
|
||||
location.coordinates.lat + coordRange,
|
||||
location.coordinates.lng - coordRange
|
||||
),
|
||||
TeslaChargingOwnershipGraphQlApi.Coordinate(
|
||||
location.coordinates.lat - coordRange,
|
||||
location.coordinates.lng + coordRange
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
val results = api.getNearbyChargingSites(
|
||||
req,
|
||||
req.operationName
|
||||
).data.charging?.nearbySites?.sitesAndDistances
|
||||
?: throw AvailabilityDetectorException("no candidates found.")
|
||||
val result =
|
||||
results.minByOrNull { it.haversineDistanceMiles.value }
|
||||
?: throw AvailabilityDetectorException("no candidates found.")
|
||||
|
||||
val details = api.getChargingSiteInformation(
|
||||
TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationRequest(
|
||||
TeslaChargingOwnershipGraphQlApi.GetChargingSiteInformationVariables(
|
||||
TeslaChargingOwnershipGraphQlApi.ChargingSiteIdentifier(result.locationGUID),
|
||||
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(
|
||||
Chargepoint.CCS_TYPE_2,
|
||||
Chargepoint.CCS_UNKNOWN
|
||||
) && it.power != null && it.power <= 150
|
||||
}
|
||||
if (scV2CCSConnectors.sumOf { it.count } != 0 && scV2CCSConnectors.sumOf { it.count } != scV2Connectors.sumOf { it.count }) {
|
||||
throw AvailabilityDetectorException("number of V2 connectors does not match number of V2 CCS connectors")
|
||||
}
|
||||
val scV3Connectors = location.chargepoints.filter {
|
||||
it.type in listOf(
|
||||
Chargepoint.CCS_TYPE_2,
|
||||
Chargepoint.CCS_UNKNOWN
|
||||
) && it.power != null && it.power > 150
|
||||
}
|
||||
if (location.totalChargepoints != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } + scV2CCSConnectors.sumOf { it.count }) throw AvailabilityDetectorException(
|
||||
"charger has unknown connectors"
|
||||
)
|
||||
|
||||
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 != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) {
|
||||
// apparently some connectors are missing in Tesla data
|
||||
// If we have just one type of charger, we can still match
|
||||
val numMissing =
|
||||
scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } - detailsSorted.size
|
||||
if ((scV2Connectors.isEmpty() || scV3Connectors.isEmpty()) && numMissing > 0) {
|
||||
detailsSorted =
|
||||
detailsSorted + List(numMissing) {
|
||||
TeslaChargingOwnershipGraphQlApi.ChargerDetail(
|
||||
ChargerAvailability.UNKNOWN,
|
||||
TeslaChargingOwnershipGraphQlApi.ChargerId(
|
||||
TeslaChargingOwnershipGraphQlApi.Text(""),
|
||||
null,
|
||||
null
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw AvailabilityDetectorException("Tesla API chargepoints do not match data source")
|
||||
}
|
||||
}
|
||||
|
||||
val detailsMap =
|
||||
emptyMap<Chargepoint, List<TeslaChargingOwnershipGraphQlApi.ChargerDetail>>().toMutableMap()
|
||||
var i = 0
|
||||
for (connector in scV2Connectors) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
if (scV2CCSConnectors.isNotEmpty()) {
|
||||
i = 0
|
||||
for (connector in scV2CCSConnectors) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
}
|
||||
for (connector in scV3Connectors) {
|
||||
detailsMap[connector] =
|
||||
detailsSorted.subList(i, i + connector.count)
|
||||
i += connector.count
|
||||
}
|
||||
|
||||
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 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
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initApi(): TeslaChargingOwnershipGraphQlApi {
|
||||
|
||||
return api ?: run {
|
||||
val newApi = TeslaChargingOwnershipGraphQlApi.create(client, baseUrl) {
|
||||
val now = Instant.now().epochSecond
|
||||
val token =
|
||||
tokenStore.teslaAccessToken.takeIf { tokenStore.teslaAccessTokenExpiry > now }
|
||||
?: run {
|
||||
val refreshToken = tokenStore.teslaRefreshToken
|
||||
?: throw NotSignedInException()
|
||||
val response =
|
||||
authApi.getToken(
|
||||
TeslaAuthenticationApi.RefreshTokenRequest(
|
||||
refreshToken
|
||||
)
|
||||
)
|
||||
tokenStore.teslaAccessToken = response.accessToken
|
||||
tokenStore.teslaAccessTokenExpiry = now + response.expiresIn
|
||||
response.accessToken
|
||||
}
|
||||
token
|
||||
}
|
||||
api = newApi
|
||||
newApi
|
||||
}
|
||||
}
|
||||
|
||||
fun isSignedIn() = tokenStore.teslaRefreshToken != null
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package net.vonforst.evmap.api.availability.tesla
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.ToJson
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.model.Coordinate
|
||||
import java.time.LocalTime
|
||||
|
||||
sealed class GraphQlRequest {
|
||||
abstract val operationName: String
|
||||
abstract val query: String
|
||||
abstract val variables: Any?
|
||||
}
|
||||
|
||||
fun Coordinate.asTeslaCoord() =
|
||||
TeslaChargingOwnershipGraphQlApi.Coordinate(this.lat, this.lng)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Outage(val message: String /* TODO: */)
|
||||
|
||||
enum class ChargerAvailability {
|
||||
@Json(name = "CHARGER_AVAILABILITY_AVAILABLE")
|
||||
AVAILABLE,
|
||||
|
||||
@Json(name = "CHARGER_AVAILABILITY_OCCUPIED")
|
||||
OCCUPIED,
|
||||
|
||||
@Json(name = "CHARGER_AVAILABILITY_DOWN")
|
||||
DOWN,
|
||||
|
||||
@Json(name = "CHARGER_AVAILABILITY_UNKNOWN")
|
||||
UNKNOWN;
|
||||
|
||||
fun toStatus() = when (this) {
|
||||
AVAILABLE -> ChargepointStatus.AVAILABLE
|
||||
OCCUPIED -> ChargepointStatus.OCCUPIED
|
||||
DOWN -> ChargepointStatus.FAULTED
|
||||
UNKNOWN -> ChargepointStatus.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Pricing(
|
||||
val canDisplayCombinedComparison: Boolean?,
|
||||
val hasMSPPricing: Boolean?,
|
||||
val hasMembershipPricing: Boolean?,
|
||||
val memberRates: Rates?, // rates for Tesla drivers & non-Tesla drivers with subscription
|
||||
val userRates: Rates? // rates without subscription
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Rates(
|
||||
val activePricebook: Pricebook
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Pricebook(
|
||||
val charging: PricebookDetails,
|
||||
val parking: PricebookDetails?,
|
||||
val priceBookID: Long?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PricebookDetails(
|
||||
val bucketUom: String, // unit of measurement for buckets (typically "kw")
|
||||
val buckets: List<Bucket>, // buckets of charging power (used for minute-based pricing)
|
||||
val currencyCode: String,
|
||||
val programType: String,
|
||||
val rates: List<Double>,
|
||||
val touRates: TouRates,
|
||||
val uom: String, // unit of measurement ("kwh" or "min")
|
||||
val vehicleMakeType: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Bucket(
|
||||
val start: Int,
|
||||
val end: Int
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TouRates(
|
||||
val activeRatesByTime: List<ActiveRatesByTime>,
|
||||
val enabled: Boolean
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ActiveRatesByTime(
|
||||
val startTime: LocalTime,
|
||||
val endTime: LocalTime,
|
||||
val rates: List<Double>
|
||||
)
|
||||
|
||||
internal class LocalTimeAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String?): LocalTime? = value?.let {
|
||||
if (it == "24:00") LocalTime.MAX else LocalTime.parse(it)
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: LocalTime?): String? = value?.toString()
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package net.vonforst.evmap.api.availability.tesla
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.OkHttpClient
|
||||
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.util.concurrent.TimeUnit
|
||||
|
||||
interface TeslaCuaApi {
|
||||
@GET("tesla-locations")
|
||||
suspend fun getTeslaLocations(
|
||||
@Query("translate") translate: String = "en_US",
|
||||
@Query("usetrt") usetrt: Boolean = true,
|
||||
): List<TeslaLocation>
|
||||
|
||||
@GET("tesla-location")
|
||||
suspend fun getTeslaLocation(
|
||||
@Query("id") id: String,
|
||||
@Query("translate") translate: String = "en_US",
|
||||
@Query("usetrt") usetrt: Boolean = true
|
||||
): TeslaLocation
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TeslaLocation(
|
||||
val latitude: Double?,
|
||||
val longitude: Double?,
|
||||
@Json(name = "location_id") val locationId: String,
|
||||
val title: String?,
|
||||
@Json(name = "location_type") val locationType: List<String>,
|
||||
val trtId: String?
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun create(
|
||||
client: OkHttpClient,
|
||||
baseUrl: String? = null
|
||||
): TeslaCuaApi {
|
||||
val clientWithInterceptor = client.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
// increase cache duration to 24h (useful for the large getTeslaLocations request)
|
||||
val request = chain.request().newBuilder()
|
||||
.cacheControl(CacheControl.Builder().maxStale(24, TimeUnit.HOURS).build())
|
||||
.build()
|
||||
chain.proceed(request)
|
||||
}.build()
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl ?: "https://www.tesla.com/cua-api/")
|
||||
.addConverterFactory(
|
||||
MoshiConverterFactory.create(
|
||||
Moshi.Builder().add(LocalTimeAdapter()).build()
|
||||
)
|
||||
)
|
||||
.client(clientWithInterceptor)
|
||||
.build()
|
||||
return retrofit.create(TeslaCuaApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface TeslaChargingGuestGraphQlApi {
|
||||
@POST("graphql")
|
||||
suspend fun getSiteDetails(
|
||||
@Body request: GetSiteDetailsRequest,
|
||||
@Query("operationName") operationName: String = "GetSiteDetails"
|
||||
): GetChargingSiteDetailsResponse
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetSiteDetailsRequest(
|
||||
override val variables: GetSiteDetailsVariables,
|
||||
override val operationName: String = "GetSiteDetails",
|
||||
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 "
|
||||
) : GraphQlRequest()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetSiteDetailsVariables(
|
||||
val siteId: Identifier,
|
||||
)
|
||||
|
||||
enum class Experience {
|
||||
ADHOC, GUEST
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Identifier(
|
||||
val byTrtId: ChargingSiteIdentifier
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargingSiteIdentifier(
|
||||
val trtId: Long,
|
||||
val chargingExperience: Experience,
|
||||
val programType: String = "PTSCH",
|
||||
val locale: String = "de-DE",
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteDetailsResponse(val data: GetChargingSiteDetailsResponseDataNetwork)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteDetailsResponseDataNetwork(val chargingNetwork: GetChargingSiteDetailsResponseData?)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteDetailsResponseData(val site: ChargingSiteInformation?)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargingSiteInformation(
|
||||
val activeOutages: List<Outage>?,
|
||||
val chargerList: List<ChargerDetail>,
|
||||
val trtId: Long,
|
||||
val maxPowerKw: Int?,
|
||||
val name: String,
|
||||
val pricing: Pricing?,
|
||||
val publicStallCount: Int
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargerDetail(
|
||||
val availability: ChargerAvailability,
|
||||
val label: String?,
|
||||
val id: String
|
||||
) {
|
||||
val labelNumber
|
||||
get() = label?.replace(Regex("""\D"""), "")?.toInt()
|
||||
val labelLetter
|
||||
get() = label?.replace(Regex("""\d"""), "")
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(
|
||||
client: OkHttpClient,
|
||||
baseUrl: String? = null
|
||||
): TeslaChargingGuestGraphQlApi {
|
||||
val retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl ?: "https://www.tesla.com/de_DE/charging/guest/api/")
|
||||
.addConverterFactory(
|
||||
MoshiConverterFactory.create(
|
||||
Moshi.Builder().add(LocalTimeAdapter()).build()
|
||||
)
|
||||
)
|
||||
.client(client)
|
||||
.build()
|
||||
return retrofit.create(TeslaChargingGuestGraphQlApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,12 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
package net.vonforst.evmap.api.availability.tesla
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.ToJson
|
||||
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.model.Chargepoint
|
||||
import net.vonforst.evmap.model.Coordinate
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
@@ -18,14 +14,8 @@ import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
import java.io.IOException
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.time.Instant
|
||||
import java.time.LocalTime
|
||||
import java.util.Collections
|
||||
|
||||
private const val coordRange = 0.005 // range of latitude and longitude for loading the map
|
||||
|
||||
interface TeslaAuthenticationApi {
|
||||
@POST("oauth2/v3/token")
|
||||
@@ -102,6 +92,19 @@ interface TeslaAuthenticationApi {
|
||||
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
|
||||
)
|
||||
}
|
||||
|
||||
fun buildSignInUri(codeChallenge: String): Uri =
|
||||
Uri.parse("https://auth.tesla.com/oauth2/v3/authorize").buildUpon()
|
||||
.appendQueryParameter("client_id", "ownerapi")
|
||||
.appendQueryParameter("code_challenge", codeChallenge)
|
||||
.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("state", "123").build()
|
||||
|
||||
val resultUrlPrefix = "https://auth.tesla.com/void/callback"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,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.9.2")
|
||||
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27")
|
||||
.header("User-Agent", "okhttp/4.11.0")
|
||||
.header("x-tesla-user-agent", "TeslaApp/4.44.5-3304/3a5d531cc3/android/27")
|
||||
.header("Accept", "*/*")
|
||||
.build()
|
||||
chain.proceed(request)
|
||||
@@ -144,7 +147,7 @@ interface TeslaOwnerApi {
|
||||
}
|
||||
}
|
||||
|
||||
interface TeslaGraphQlApi {
|
||||
interface TeslaChargingOwnershipGraphQlApi {
|
||||
@POST("/graphql")
|
||||
suspend fun getNearbyChargingSites(
|
||||
@Body request: GetNearbyChargingSitesRequest,
|
||||
@@ -170,7 +173,7 @@ interface TeslaGraphQlApi {
|
||||
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 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 "
|
||||
"\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"
|
||||
) : GraphQlRequest()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@@ -181,7 +184,7 @@ interface TeslaGraphQlApi {
|
||||
val userLocation: Coordinate,
|
||||
val northwestCorner: Coordinate,
|
||||
val southeastCorner: Coordinate,
|
||||
val openToNonTeslasFilter: OpenToNonTeslasFilterValue,
|
||||
val filters: List<String> = emptyList(),
|
||||
val languageCode: String = "en",
|
||||
val countryCode: String = "US",
|
||||
//val vin: String = "",
|
||||
@@ -199,7 +202,7 @@ interface TeslaGraphQlApi {
|
||||
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 }\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"
|
||||
"\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"
|
||||
) : GraphQlRequest()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@@ -214,28 +217,22 @@ interface TeslaGraphQlApi {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargingSiteIdentifier(
|
||||
val id: String,
|
||||
val type: ChargingSiteIdentifierType = ChargingSiteIdentifierType.SITE_ID
|
||||
val type: ChargingSiteIdentifierType = ChargingSiteIdentifierType.LOCATION_GUID
|
||||
)
|
||||
|
||||
enum class ChargingSiteIdentifierType {
|
||||
SITE_ID
|
||||
SITE_ID, LOCATION_GUID
|
||||
}
|
||||
|
||||
enum class VehicleMakeType {
|
||||
TESLA, NON_TESLA
|
||||
}
|
||||
|
||||
sealed class GraphQlRequest {
|
||||
abstract val operationName: String
|
||||
abstract val query: String
|
||||
abstract val variables: Any?
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetNearbyChargingSitesResponse(val data: GetNearbyChargingSitesResponseData)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetNearbyChargingSitesResponseData(val charging: GetNearbyChargingSitesResponseDataCharging)
|
||||
data class GetNearbyChargingSitesResponseData(val charging: GetNearbyChargingSitesResponseDataCharging?)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetNearbyChargingSitesResponseDataCharging(val nearbySites: GetNearbyChargingSitesResponseDataChargingNearbySites)
|
||||
@@ -245,7 +242,6 @@ interface TeslaGraphQlApi {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargingSite(
|
||||
val activeOutages: List<Outage>,
|
||||
val availableStalls: Value<Int>?,
|
||||
val centroid: Coordinate,
|
||||
val drivingDistanceMiles: Value<Double>?,
|
||||
@@ -254,19 +250,11 @@ interface TeslaGraphQlApi {
|
||||
val id: Text,
|
||||
val localizedSiteName: Value<String>,
|
||||
val maxPowerKw: Value<Int>,
|
||||
val totalStalls: Value<Int>
|
||||
val totalStalls: Value<Int>,
|
||||
val locationGUID: String
|
||||
// TODO: siteType, accessType
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Outage(val message: String /* TODO: */)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Value<T : Any>(val value: T)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Text(val text: String)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteInformationResponse(val data: GetChargingSiteInformationResponseData)
|
||||
|
||||
@@ -274,7 +262,7 @@ interface TeslaGraphQlApi {
|
||||
data class GetChargingSiteInformationResponseData(val charging: GetChargingSiteInformationResponseDataCharging)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetChargingSiteInformationResponseDataCharging(val site: ChargingSiteInformation)
|
||||
data class GetChargingSiteInformationResponseDataCharging(val site: ChargingSiteInformation?)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargingSiteInformation(
|
||||
@@ -286,7 +274,6 @@ interface TeslaGraphQlApi {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SiteDynamic(
|
||||
val activeOutages: List<Outage>,
|
||||
val chargerDetails: List<ChargerDetail>,
|
||||
val chargersAvailable: Value<Int>?,
|
||||
val currentCongestion: Double,
|
||||
@@ -294,24 +281,24 @@ interface TeslaGraphQlApi {
|
||||
val waitEstimateBucket: WaitEstimateBucket
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargerId(
|
||||
val id: Text,
|
||||
val label: Value<String>?,
|
||||
val name: String?
|
||||
) {
|
||||
val labelNumber
|
||||
get() = label?.value?.replace(Regex("""\D"""), "")?.toInt()
|
||||
val labelLetter
|
||||
get() = label?.value?.replace(Regex("""\d"""), "")
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargerDetail(
|
||||
val availability: ChargerAvailability,
|
||||
val charger: ChargerId
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargerId(
|
||||
val id: Text,
|
||||
val label: Value<String>,
|
||||
val name: String?
|
||||
) {
|
||||
val labelNumber
|
||||
get() = label.value.replace(Regex("""\D"""), "").toInt()
|
||||
val labelLetter
|
||||
get() = label.value.replace(Regex("""\d"""), "")
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SiteStatic(
|
||||
val accessCode: Value<String>?,
|
||||
@@ -329,58 +316,6 @@ interface TeslaGraphQlApi {
|
||||
// TODO: siteType, accessType, address, amenities, timeZone
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Pricing(
|
||||
val canDisplayCombinedComparison: Boolean,
|
||||
val hasMSPPricing: Boolean,
|
||||
val hasMembershipPricing: Boolean,
|
||||
val memberRates: Rates?, // rates for Tesla drivers & non-Tesla drivers with subscription
|
||||
val userRates: Rates? // rates without subscription
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Rates(
|
||||
val activePricebook: Pricebook
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Pricebook(
|
||||
val charging: PricebookDetails,
|
||||
val parking: PricebookDetails,
|
||||
val priceBookID: Long
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PricebookDetails(
|
||||
val bucketUom: String, // unit of measurement for buckets (typically "kw")
|
||||
val buckets: List<Bucket>, // buckets of charging power (used for minute-based pricing)
|
||||
val currencyCode: String,
|
||||
val programType: String,
|
||||
val rates: List<Double>,
|
||||
val touRates: TouRates,
|
||||
val uom: String, // unit of measurement ("kwh" or "min")
|
||||
val vehicleMakeType: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Bucket(
|
||||
val start: Int,
|
||||
val end: Int
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TouRates(
|
||||
val activeRatesByTime: List<ActiveRatesByTime>,
|
||||
val enabled: Boolean
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ActiveRatesByTime(
|
||||
val startTime: LocalTime,
|
||||
val endTime: LocalTime,
|
||||
val rates: List<Double>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CongestionPriceHistogram(
|
||||
val data: List<Double>,
|
||||
@@ -393,25 +328,11 @@ interface TeslaGraphQlApi {
|
||||
val label: String // "1AM", "2AM", etc.
|
||||
)
|
||||
|
||||
enum class ChargerAvailability {
|
||||
@Json(name = "CHARGER_AVAILABILITY_AVAILABLE")
|
||||
AVAILABLE,
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Value<T : Any>(val value: T)
|
||||
|
||||
@Json(name = "CHARGER_AVAILABILITY_OCCUPIED")
|
||||
OCCUPIED,
|
||||
|
||||
@Json(name = "CHARGER_AVAILABILITY_DOWN")
|
||||
DOWN,
|
||||
@Json(name = "CHARGER_AVAILABILITY_UNKNOWN")
|
||||
UNKNOWN;
|
||||
|
||||
fun toStatus() = when (this) {
|
||||
AVAILABLE -> ChargepointStatus.AVAILABLE
|
||||
OCCUPIED -> ChargepointStatus.OCCUPIED
|
||||
DOWN -> ChargepointStatus.FAULTED
|
||||
UNKNOWN -> ChargepointStatus.UNKNOWN
|
||||
}
|
||||
}
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Text(val text: String)
|
||||
|
||||
enum class WaitEstimateBucket {
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_NO_WAIT")
|
||||
@@ -432,6 +353,9 @@ interface TeslaGraphQlApi {
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_APPROXIMATELY_20_MINUTES")
|
||||
APPROXIMATELY_20_MINUTES,
|
||||
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_GREATER_THAN_25_MINUTES")
|
||||
GREATER_THAN_25_MINUTES,
|
||||
|
||||
@Json(name = "WAIT_ESTIMATE_BUCKET_UNKNOWN")
|
||||
UNKNOWN
|
||||
}
|
||||
@@ -441,15 +365,15 @@ interface TeslaGraphQlApi {
|
||||
client: OkHttpClient,
|
||||
baseUrl: String? = null,
|
||||
token: suspend () -> String
|
||||
): TeslaGraphQlApi {
|
||||
): TeslaChargingOwnershipGraphQlApi {
|
||||
val clientWithInterceptor = client.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
val t = runBlocking { token() }
|
||||
// add API key to every request
|
||||
val request = chain.request().newBuilder()
|
||||
.header("Authorization", "Bearer $t")
|
||||
.header("User-Agent", "okhttp/4.9.2")
|
||||
.header("x-tesla-user-agent", "TeslaApp/4.19.5-1667/3a5d531cc3/android/27")
|
||||
.header("User-Agent", "okhttp/4.11.0")
|
||||
.header("x-tesla-user-agent", "TeslaApp/4.44.5-3304/3a5d531cc3/android/27")
|
||||
.header("Accept", "*/*")
|
||||
.build()
|
||||
chain.proceed(request)
|
||||
@@ -463,182 +387,7 @@ interface TeslaGraphQlApi {
|
||||
)
|
||||
.client(clientWithInterceptor)
|
||||
.build()
|
||||
return retrofit.create(TeslaGraphQlApi::class.java)
|
||||
return retrofit.create(TeslaChargingOwnershipGraphQlApi::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class LocalTimeAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: String?): LocalTime? = value?.let {
|
||||
if (it == "24:00") LocalTime.MAX else LocalTime.parse(it)
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: LocalTime?): String? = value?.toString()
|
||||
}
|
||||
|
||||
fun Coordinate.asTeslaCoord() =
|
||||
TeslaGraphQlApi.Coordinate(this.lat, this.lng)
|
||||
|
||||
class TeslaAvailabilityDetector(
|
||||
private val client: OkHttpClient,
|
||||
private val tokenStore: TokenStore,
|
||||
private val baseUrl: String? = null
|
||||
) :
|
||||
BaseAvailabilityDetector(client) {
|
||||
|
||||
private val authApi = TeslaAuthenticationApi.create(client, null)
|
||||
private var api: TeslaGraphQlApi? = null
|
||||
|
||||
interface TokenStore {
|
||||
var teslaRefreshToken: String?
|
||||
var teslaAccessToken: String?
|
||||
var teslaAccessTokenExpiry: Long
|
||||
}
|
||||
|
||||
override suspend fun getAvailability(location: ChargeLocation): ChargeLocationStatus {
|
||||
val api = initApi()
|
||||
val req = TeslaGraphQlApi.GetNearbyChargingSitesRequest(
|
||||
TeslaGraphQlApi.GetNearbyChargingSitesVariables(
|
||||
TeslaGraphQlApi.GetNearbyChargingSitesArgs(
|
||||
location.coordinates.asTeslaCoord(),
|
||||
TeslaGraphQlApi.Coordinate(
|
||||
location.coordinates.lat + coordRange,
|
||||
location.coordinates.lng - coordRange
|
||||
),
|
||||
TeslaGraphQlApi.Coordinate(
|
||||
location.coordinates.lat - coordRange,
|
||||
location.coordinates.lng + coordRange
|
||||
),
|
||||
TeslaGraphQlApi.OpenToNonTeslasFilterValue(false)
|
||||
)
|
||||
)
|
||||
)
|
||||
val results = api.getNearbyChargingSites(
|
||||
req,
|
||||
req.operationName
|
||||
).data.charging.nearbySites.sitesAndDistances
|
||||
val result =
|
||||
results.minByOrNull { it.haversineDistanceMiles.value }
|
||||
?: throw AvailabilityDetectorException("no candidates found.")
|
||||
|
||||
val details = api.getChargingSiteInformation(
|
||||
TeslaGraphQlApi.GetChargingSiteInformationRequest(
|
||||
TeslaGraphQlApi.GetChargingSiteInformationVariables(
|
||||
TeslaGraphQlApi.ChargingSiteIdentifier(result.id.text),
|
||||
TeslaGraphQlApi.VehicleMakeType.NON_TESLA
|
||||
)
|
||||
)
|
||||
).data.charging.site
|
||||
|
||||
|
||||
val scV2Connectors = location.chargepoints.filter { it.type == Chargepoint.SUPERCHARGER }
|
||||
val scV2CCSConnectors = location.chargepoints.filter {
|
||||
it.type in listOf(
|
||||
Chargepoint.CCS_TYPE_2,
|
||||
Chargepoint.CCS_UNKNOWN
|
||||
) && it.power != null && it.power <= 150
|
||||
}
|
||||
if (scV2CCSConnectors.sumOf { it.count } != 0 && scV2CCSConnectors.sumOf { it.count } != scV2Connectors.sumOf { it.count }) {
|
||||
throw AvailabilityDetectorException("number of V2 connectors does not match number of V2 CCS connectors")
|
||||
}
|
||||
val scV3Connectors = location.chargepoints.filter {
|
||||
it.type in listOf(
|
||||
Chargepoint.CCS_TYPE_2,
|
||||
Chargepoint.CCS_UNKNOWN
|
||||
) && it.power != null && it.power > 150
|
||||
}
|
||||
if (location.totalChargepoints != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } + scV2CCSConnectors.sumOf { it.count }) throw AvailabilityDetectorException(
|
||||
"charger has unknown connectors"
|
||||
)
|
||||
|
||||
var statusSorted = details.siteDynamic.chargerDetails.sortedBy { it.charger.labelLetter }
|
||||
.sortedBy { it.charger.labelNumber }.map { it.availability }
|
||||
if (statusSorted.size != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) {
|
||||
// apparently some connectors are missing in Tesla data
|
||||
// If we have just one type of charger, we can still match
|
||||
val numMissing =
|
||||
scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } - statusSorted.size
|
||||
if (scV2Connectors.isEmpty() || scV3Connectors.isEmpty() && numMissing > 0) {
|
||||
statusSorted =
|
||||
statusSorted + List(numMissing) { TeslaGraphQlApi.ChargerAvailability.UNKNOWN }
|
||||
} else {
|
||||
throw AvailabilityDetectorException("Tesla API chargepoints do not match data source")
|
||||
}
|
||||
}
|
||||
|
||||
val statusMap = emptyMap<Chargepoint, List<ChargepointStatus>>().toMutableMap()
|
||||
var i = 0
|
||||
for (connector in scV2Connectors) {
|
||||
statusMap[connector] =
|
||||
statusSorted.subList(i, i + connector.count).map { it.toStatus() }
|
||||
i += connector.count
|
||||
}
|
||||
if (scV2CCSConnectors.isNotEmpty()) {
|
||||
i = 0
|
||||
for (connector in scV2CCSConnectors) {
|
||||
statusMap[connector] =
|
||||
statusSorted.subList(i, i + connector.count).map { it.toStatus() }
|
||||
i += connector.count
|
||||
}
|
||||
}
|
||||
for (connector in scV3Connectors) {
|
||||
statusMap[connector] =
|
||||
statusSorted.subList(i, i + connector.count).map { it.toStatus() }
|
||||
i += connector.count
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return ChargeLocationStatus(
|
||||
statusMap,
|
||||
"Tesla",
|
||||
congestionHistogram = congestionHistogram,
|
||||
extraData = details.pricing
|
||||
)
|
||||
}
|
||||
|
||||
override fun isChargerSupported(charger: ChargeLocation): Boolean {
|
||||
return when (charger.dataSource) {
|
||||
"goingelectric" -> charger.network == "Tesla Supercharger"
|
||||
"openchargemap" -> charger.chargepriceData?.network in listOf("23", "3534")
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initApi(): TeslaGraphQlApi {
|
||||
|
||||
return api ?: run {
|
||||
val newApi = TeslaGraphQlApi.create(client, baseUrl) {
|
||||
val now = Instant.now().epochSecond
|
||||
val token =
|
||||
tokenStore.teslaAccessToken.takeIf { tokenStore.teslaAccessTokenExpiry > now }
|
||||
?: run {
|
||||
val refreshToken = tokenStore.teslaRefreshToken
|
||||
?: throw IOException("not signed in")
|
||||
val response =
|
||||
authApi.getToken(
|
||||
TeslaAuthenticationApi.RefreshTokenRequest(
|
||||
refreshToken
|
||||
)
|
||||
)
|
||||
tokenStore.teslaAccessToken = response.accessToken
|
||||
tokenStore.teslaAccessTokenExpiry = now + response.expiresIn
|
||||
response.accessToken
|
||||
}
|
||||
token
|
||||
}
|
||||
api = newApi
|
||||
newApi
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,99 +1,16 @@
|
||||
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 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.*
|
||||
import java.util.Locale
|
||||
|
||||
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)) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user