mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-24 07:37:46 -05:00
Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87421e450a | ||
|
|
479917fad1 | ||
|
|
dfaf841160 | ||
|
|
c18ea5b15d | ||
|
|
62116473c8 | ||
|
|
bc8106bd81 | ||
|
|
7bd89b9ecb | ||
|
|
898b61945e | ||
|
|
38e022b547 | ||
|
|
b8c438503c | ||
|
|
2ca6a8e3e8 | ||
|
|
0ae201e363 | ||
|
|
9e0f535a13 | ||
|
|
d4a6789b00 | ||
|
|
d9415ed7a0 | ||
|
|
778d7293f4 | ||
|
|
19a8b5c9fe | ||
|
|
7f3c481dcb | ||
|
|
8a54b5cb05 | ||
|
|
91b3234a45 | ||
|
|
ab7cbc981b | ||
|
|
a2c1a2cf82 | ||
|
|
167ede4e62 | ||
|
|
63900996e7 | ||
|
|
c626f3d5a5 | ||
|
|
8779e65846 | ||
|
|
0c8bf84e56 | ||
|
|
90972cf933 | ||
|
|
7d9a9605fb | ||
|
|
a0bc0f2981 | ||
|
|
f3b4c8a8ff | ||
|
|
6a8220c1c2 | ||
|
|
84c28748a4 | ||
|
|
7c29b619a5 | ||
|
|
ccfdbbe826 | ||
|
|
7052ce3c3c | ||
|
|
d73ca8aa9d | ||
|
|
64703a8c28 | ||
|
|
eb54658bf4 | ||
|
|
54d1c8ba61 | ||
|
|
1c04f6211f | ||
|
|
45497f9208 | ||
|
|
140c634397 | ||
|
|
be1b3813a9 | ||
|
|
f7ed7f1e93 | ||
|
|
0df72ac4ad | ||
|
|
d041513516 | ||
|
|
1effba77d1 | ||
|
|
df79f02e1d | ||
|
|
c4d44f9ddf | ||
|
|
6bec397133 | ||
|
|
474b621af0 | ||
|
|
36aeb201ca | ||
|
|
76a241d691 | ||
|
|
0f7bf7913f | ||
|
|
d11925eb33 | ||
|
|
6ac49fd84d | ||
|
|
097b7941a2 | ||
|
|
23b87e69c0 | ||
|
|
3bb5521c18 | ||
|
|
76f7b97c1f | ||
|
|
50de0009c7 | ||
|
|
f906846fcc | ||
|
|
b50225af32 | ||
|
|
8abd5219aa | ||
|
|
71f9a25c5a | ||
|
|
b5f4314795 | ||
|
|
034196b9fa | ||
|
|
72d7f7dc57 | ||
|
|
7fec02b468 | ||
|
|
8eacee8a71 | ||
|
|
95dd8cce52 | ||
|
|
45dd40faa7 | ||
|
|
e9ac39301d | ||
|
|
8b8713e4c5 | ||
|
|
d023facb2f | ||
|
|
e2e15692bb | ||
|
|
abde18d61f | ||
|
|
b32fa6600d | ||
|
|
1de1699d51 | ||
|
|
a618c4106f | ||
|
|
6ad8389ecf | ||
|
|
38d07abf0e | ||
|
|
884172b9f8 | ||
|
|
2208e093e7 | ||
|
|
a2041653bc | ||
|
|
394cbdfc8b | ||
|
|
7759c230db | ||
|
|
cdc575ff33 | ||
|
|
cb250de79e | ||
|
|
c7885ae729 | ||
|
|
024b56952d | ||
|
|
75b2240247 | ||
|
|
d8f011b64b | ||
|
|
a1760a35ff | ||
|
|
e5e5f8ef3c | ||
|
|
b5a4fe2dc8 | ||
|
|
676e703a52 | ||
|
|
b9997cbb5a | ||
|
|
2558052f4f | ||
|
|
980c8cc0af | ||
|
|
ffb6740da8 | ||
|
|
2e9112f5c2 | ||
|
|
3c709fa3c5 | ||
|
|
11c868af66 | ||
|
|
e3ea72bac6 | ||
|
|
d01371f6e9 | ||
|
|
6130b190e1 | ||
|
|
128d156306 | ||
|
|
f855874d56 | ||
|
|
92ebf6c1e5 | ||
|
|
1e98be0f8f | ||
|
|
c0bec92d4c | ||
|
|
71ecd492e9 | ||
|
|
fcac8f91ad | ||
|
|
795c96d901 | ||
|
|
cc76310b2b | ||
|
|
2a6ac0ac1b | ||
|
|
8673efd1cd | ||
|
|
ae40b8c634 | ||
|
|
0cdb12711d | ||
|
|
69ccc55ad4 | ||
|
|
304f46e189 | ||
|
|
01f06621f4 | ||
|
|
f986a68db8 | ||
|
|
441e78d807 | ||
|
|
6481d651a0 | ||
|
|
9a7db8997a | ||
|
|
d94053261c | ||
|
|
39dc50724e | ||
|
|
34fe126fd0 | ||
|
|
1f81a11ad1 | ||
|
|
74b74dcd07 | ||
|
|
ec623c9396 | ||
|
|
c10c59e3b1 | ||
|
|
2bd5f746ed | ||
|
|
fbc15f2925 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -8,5 +8,7 @@
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
apikeys.xml
|
||||
/app/release/app-release.aab
|
||||
/_img/connectors/*.ai
|
||||
/app/**/*.aab
|
||||
/app/**/*.apk
|
||||
/_img/connectors/*.ai
|
||||
api-7125266970515251116-798419-8e2dda660c80.json
|
||||
@@ -8,10 +8,11 @@ env:
|
||||
global:
|
||||
- secure: KYdFlMarsyXw+OHht1Atp+Kirbw9O09Ck14EjFuKb1eNtknurZ/tGEXuD+8xWh1W8W21kgHEG7s3rzru53t29buz+FW9f+ZmhEWXFP3OydyvXLw4BAVVOjm6xG2uHX/8MOGLJNM7cfaF25EPQ+kznHe84R29KaLH90mNRr2lPa4VnfbcnvDStiVaez/vJ72UoYSP5HICAzoF70yC3ZvvCK1hZv71UIysCbFE2IkxvMhG9OOGebdnRmFssaRCrvfRLjitobcLzkPWzZZIqdjNASf8/iAxX8VgGBYfVj8ID06AfMrtgXNJRCvcD0LICraQ+WPUbikMunRieGO8PNHSB5vKdPoC50aLUa0RoRb4G3QM1pR2A8xAFlIJFX2R7iY+2t24L9hRFqB98+QoQzutfkAI1T0rzem/wtpZpuan+bDawDJEHGCeYbE0aPDAl6lytgrEE9fRgV3c1jJLQzu0xIWG8YLl3iMg0hL+c0wCKXoeqrfCFS6kYmmG7W+rQp4tCZifvRbWfAXwIPQieffKxqdEuUwiUsYxdzCu9v9uU3nflEOLLuRgeMP3gV8mpur9b5GztpkfgfzcAqsF+NiY01kYgGtrgCYlMy0TxASE+UuALrtkQtU01wwhs9RH7Az0Ib3C+MT5DTjxHQCYETIViocmNEG2vfAbgHazCpGAhcY=
|
||||
- secure: Hoko50vP+Mwm/O4CWvPvjMxd1gGhi+Bultjyy1WpludSmZFCfKz45Mj1EqzeYk6MeMLvGOEkSLB5wjdXdgJ9j5gEOF5K34k4vESJA7+DqDO3I7Xw9cnWgOXdFqB0qGHar0TVP3Dfg7ZcRgtmeX6t2uoELFLiS9GvTnbZXk4PCUybd979Xi8XHjEQV7+3EZbSjtsI4GFeIK1rrjd0I+UM88zYrWnz1KhdCjWvQb4iZjo+ib6NmGGEMqR7jJPRZz3KB01Y+n5h21qq9/Nv31zQIqt2B5nRxy3vBvvqKapgprIk+hVOpNnBU8w89uWUU6tYUeFk0t7z1TYWjgaBrMmGCM+aKkQ2q5F/ygNzDwB+KkpJx709Yhf0ZX8g2yvkdz+Ok7moYuvmrOPOf4E/U3BlfZxbGtRD2bGYbDgHLFYlTn6v5J2kJHDJAz31yvF5jvJejDaPp2IBVfoMRy7ZJFmGUNHGd9Se6bRwxS++AoobP5WDrBiUXNe2KKDMs3e7vbaO+hLbZ9XHpjeWIJGUfvtTee8EHZqF/8A3ju53V4/R0ehlOv2UZbpYNqcmwrsy9/R4pgMfDkG3Q054LmYrxD8DIC9b8excVMwWRP5aQ1TqZnxO2B1/vJU87RcnGl3jekeHTdHXbRq9BMV4dAdPfB9X3nGIi2GgV0iTBk+24xccOc0=
|
||||
- secure: RsN/2nBVv0byMzwchuAhDti1AWKECg3Uzqk6Ahgaqg02zx8GHj60j0qNax7KuMUg70X3G4b3m8ZzndAU0wcJ98UyIku7ofUYgXHm0XYKTJwiyyhrKJ8pZ4qoeCoRkitIGIitlb84fSufVamcoLyLNbLUsO5OzL+Uscyhq/BwAhyOhj5FB9DM+GE9ntanQ4m3k4guMyMctR9CGS6Tk4LKJ1WCm0AnjTPalc+we+7yORY2J/9d6VHLKfaFXbpuWmrdnFfDZqxqcUODsxPVE0LuUtXyKQDTjjfQc8106Z/z19AJS+oLm/2UND84PD3MqsjX6EX0/9k3fY0OsoCuQAivDRmtevQ0bDQrTAyeBLcfnPCw/MYiJWyBcaYBAYK42EAfsFTDBRIduFB/Dpvg+8GuLZSdm4xVYpTlQ2pMtlGNWIGIRQsjk9LZ8swq8QBMiiF/bpMGKdfmnyQj8jkEOCWaAzkR9O+4E4Y7PuBENef9XuV0hLMryCrML2YXigxAKkEUOPhfnG+AbEY+g2kAMp+2EtXaG3tsGxoMhEYA+uRd3o2cacQMMwRpnePH5DYg6F05mrvdHPpt+P9UR1iHaHmjBPAYeksmrdP8bS088zcnePgiL6+6N0m3+l8Krihmxg5RyWjnH18IwX4RO+xg4x3cW+zaScCDbbotDMEYtChF+Hs=
|
||||
before_install:
|
||||
- openssl aes-256-cbc -K $encrypted_53968681344a_key -iv $encrypted_53968681344a_iv -in _ci/keystore.jks.enc -out _ci/keystore.jks -d
|
||||
script:
|
||||
- "./gradlew lintDebug testDebugUnitTest"
|
||||
- "./gradlew lintFossDebug testFossDebugUnitTest lintGoogleDebug testGoogleDebugUnitTest"
|
||||
- "./gradlew assembleRelease"
|
||||
before_cache:
|
||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||
@@ -25,7 +26,9 @@ deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: B+V5Fz8k9HbpecyMjpJuLr8aVBrdwtDBDkQh4YQ8nu+Da4AiYwEJZseWXhOWs+oms0gNen9bBxsakQQKu7GKYDs8gIXZZtANWsc0gse8xo+cYT7NqEM3jP6mM3ytAv7VNRX3N2cdL7xazELK3/5+mghfORAAdXXYKUFGG5eTKoML8zgdPVN8E9QFqiusLXqoKhxOMCSE4NS+Di7CGlUmnidRTWg6yxhE085zljmYv2owS0NRbr5a4/zW6Z9xZPALGAqsOvIvpZHuOC2s0eMJWMmYGkK/Ws/LAVxfj4U+YkFp9hlZC0zEg/JoS19Gf57QmEu+vsoQ3uOBYBFv9NPI/R9kVH6o0hcOxId3J0u+ewSGWuceGLRpizXuMxKIvLTS5j6GWkxdSieWjwh/OuVB+ciAHNM31B7GP4FWnfz0ZaEVxI/tPenNipZdl9oXdyyBQQ00vPlYp0jT80XhaMh5rDwWMUPaEjRafvymcNyqZ0iVOr0rq1CbdT92STMSmA1U3/rmhtCMD5IGD0b+gQl+VpPKe1QXViYftVxCGL+s4ke4DUZD7HR20fGs8zu61Elnwci1HufbetKFL5TmxoKSLkWFSkzrtBaJnEruZIxhNUMkUL2UPynaOcPNzLoumjHXrUb3m3s0yE4OFelmJ6mJfXswP38sS8kj3wB7R/gC4rw=
|
||||
file: app/build/outputs/apk/release/app-release.apk
|
||||
file:
|
||||
- app/build/outputs/apk/foss/release/app-foss-release.apk
|
||||
- app/build/outputs/apk/google/release/app-google-release.apk
|
||||
on:
|
||||
repo: johan12345/EVMap
|
||||
tags: true
|
||||
|
||||
178
Gemfile.lock
Normal file
178
Gemfile.lock
Normal file
@@ -0,0 +1,178 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.2)
|
||||
addressable (2.7.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-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.3)
|
||||
claide (1.0.3)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander-fastlane (4.4.6)
|
||||
highline (~> 1.7.2)
|
||||
declarative (0.0.20)
|
||||
declarative-option (0.1.0)
|
||||
digest-crc (0.6.1)
|
||||
rake (~> 13.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.7.6)
|
||||
emoji_regex (3.0.0)
|
||||
excon (0.76.0)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-cookie_jar (0.0.6)
|
||||
faraday (>= 0.7.4)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday_middleware (1.0.0)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.2.0)
|
||||
fastlane (2.156.1)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.3, < 3.0.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored
|
||||
commander-fastlane (>= 4.4.6, < 5.0.0)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-api-client (>= 0.37.0, < 0.39.0)
|
||||
google-cloud-storage (>= 1.15.0, < 2.0.0)
|
||||
highline (>= 1.7.2, < 2.0.0)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (~> 2.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.3)
|
||||
simctl (~> 1.6.3)
|
||||
slack-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (>= 1.4.5, < 2.0.0)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3)
|
||||
gh_inspector (1.1.3)
|
||||
google-api-client (0.38.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 0.9)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.0)
|
||||
signet (~> 0.12)
|
||||
google-cloud-core (1.5.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.3.3)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.0.1)
|
||||
google-cloud-storage (1.27.0)
|
||||
addressable (~> 2.5)
|
||||
digest-crc (~> 0.4)
|
||||
google-api-client (~> 0.33)
|
||||
google-cloud-core (~> 1.2)
|
||||
googleauth (~> 0.9)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (0.13.1)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.14)
|
||||
highline (1.7.10)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
jmespath (1.4.0)
|
||||
json (2.3.1)
|
||||
jwt (2.2.1)
|
||||
memoist (0.16.2)
|
||||
mini_magick (4.10.1)
|
||||
mini_mime (1.0.2)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.0.0)
|
||||
nanaimo (0.3.0)
|
||||
naturally (2.2.0)
|
||||
os (1.1.1)
|
||||
plist (3.5.0)
|
||||
public_suffix (4.0.5)
|
||||
rake (13.0.1)
|
||||
representable (3.0.4)
|
||||
declarative (< 0.1.0)
|
||||
declarative-option (< 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rouge (2.0.7)
|
||||
rubyzip (2.3.0)
|
||||
security (0.1.3)
|
||||
signet (0.14.0)
|
||||
addressable (~> 2.3)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.8)
|
||||
CFPropertyList
|
||||
naturally
|
||||
slack-notifier (2.3.2)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.1)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7-x64-mingw32)
|
||||
unicode-display_width (1.7.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.18.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty-travis-formatter (1.0.0)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
x64-mingw32
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.2
|
||||
27
_img/map_marker_charging_multiple.svg
Normal file
27
_img/map_marker_charging_multiple.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
viewBox="0 0 233.8 368.4" style="enable-background:new 0 0 233.8 368.4;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#B5B5B5;}
|
||||
.st2{fill:#808080;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M109.8,0h13.6c33.9,1.9,67.1,18.5,87.7,45.8c13.5,17.2,21,38.6,22.7,60.3v8.1c-0.8,42.1-27.7,76.6-51,109.4
|
||||
c-26.2,37-50.4,77.3-57.1,122.9c-1.8,7.7,0.4,18.5-8.9,22c-2.2-1.7-4.7-3.1-6.2-5.4c-2.7-25.5-9.1-50.7-20-73.9
|
||||
c-12.3-27.1-29.5-51.6-47-75.6C33,199,23,184.2,14.7,168.3c-13-23.8-17.9-51.9-12.5-78.6C6.6,68.6,17.6,49.1,32.8,34
|
||||
C53.3,14,81.1,1.8,109.8,0z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="st1"
|
||||
points="143.2,109.4 123.5,143.2 123.5,181.3 166.9,106.9 144.7,106.9 " />
|
||||
<path class="st1"
|
||||
d="M122.2,101.9h16.7h5.7l22.3-44.6c0,0-10.2,0-22.4,0l-1.1,2.2L122.2,101.9z" />
|
||||
<path class="st2" d="M138.9,57.3c-9.7,0-19.8,0-26.4,0c-2.5,0-5.1,0-7.6,0c-8.2,0-16.1,0-21.4,0c-4.1,0-6.6,0-6.6,0v68.2h18.6v55.8
|
||||
l43.4-74.4h-24.8L138.9,57.3z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
6
_img/paypal.svg
Normal file
6
_img/paypal.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24px"
|
||||
height="24px">
|
||||
<path
|
||||
d="M20.055,7.713c-0.677-0.842-1.673-1.41-2.764-1.615C16.773,3.971,14.877,3,13.045,3H7.057C6.425,3,5.878,3.409,5.689,4.009 c-0.015,0.04-0.026,0.082-0.036,0.125L3.034,16.262c-0.009,0.041-0.015,0.083-0.019,0.125C3.006,16.449,3,16.513,3,16.56 C3,17.354,3.648,18,4.444,18h2.316l-0.267,1.262c-0.008,0.04-0.014,0.081-0.018,0.121c-0.009,0.063-0.016,0.126-0.016,0.173 C6.461,20.353,7.109,21,7.905,21h3.259c0.056,0,0.111-0.005,0.166-0.015c0.549-0.063,1.011-0.437,1.191-0.963 c0.021-0.05,0.038-0.103,0.05-0.156L13.475,16h1.398c3.365,0,5.38-1.445,5.989-4.295C21.278,9.752,20.653,8.456,20.055,7.713z M5.137,16L7.512,5h5.533c0.293,0,1.5,0.061,2.078,1.013h-4.706c-0.626,0-1.17,0.401-1.363,0.99c-0.019,0.049-0.033,0.1-0.043,0.151 l-1.034,5.093L7.183,16H5.137z M18.906,11.287C18.5,13.188,17.293,14,14.873,14h-1.857c-0.823,0-1.338,0.652-1.405,1.198L10.721,19 H8.594l1.271-6h1.444c4.259,0,5.665-2.394,6.094-4.402c0.027-0.128,0.045-0.256,0.057-0.382c0.378,0.151,0.749,0.393,1.038,0.751 C18.971,9.557,19.108,10.337,18.906,11.287z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -13,14 +13,23 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 7
|
||||
versionName "0.0.7"
|
||||
versionCode 26
|
||||
versionName "0.3.4a"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release
|
||||
release {
|
||||
def isRunningOnTravis = System.getenv("CI") == "true"
|
||||
if (isRunningOnTravis) {
|
||||
// configure keystore
|
||||
storeFile = file("../_ci/keystore.jks")
|
||||
storePassword = System.getenv("keystore_password")
|
||||
keyAlias = System.getenv("keystore_alias")
|
||||
keyPassword = System.getenv("keystore_alias_password")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -35,13 +44,16 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
def isRunningOnTravis = System.getenv("CI") == "true"
|
||||
if (isRunningOnTravis) {
|
||||
// configure keystore
|
||||
signingConfigs.release.storeFile = file("../_ci/keystore.jks")
|
||||
signingConfigs.release.storePassword = System.getenv("keystore_password")
|
||||
signingConfigs.release.keyAlias = System.getenv("keystore_alias")
|
||||
signingConfigs.release.keyPassword = System.getenv("keystore_alias_password")
|
||||
flavorDimensions "dependencies"
|
||||
productFlavors {
|
||||
foss {
|
||||
dimension "dependencies"
|
||||
versionNameSuffix "-foss"
|
||||
}
|
||||
google {
|
||||
dimension "dependencies"
|
||||
versionNameSuffix "-google"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -61,36 +73,37 @@ android {
|
||||
// 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
|
||||
def goingelectricKey = env.GOINGELECTRIC_API_KEY ?: project.findProperty("GOINGELECTRIC_API_KEY")
|
||||
if (goingelectricKey != null) {
|
||||
variant.resValue "string", "goingelectric_key", goingelectricKey
|
||||
}
|
||||
def googleMapsKey = env.GOOGLE_MAPS_API_KEY
|
||||
if (googleMapsKey != null) {
|
||||
def googleMapsKey = env.GOOGLE_MAPS_API_KEY ?: project.findProperty("GOOGLE_MAPS_API_KEY")
|
||||
if (googleMapsKey != null && variant.flavorName == 'google') {
|
||||
variant.resValue "string", "google_maps_key", googleMapsKey
|
||||
}
|
||||
def mapboxKey = env.MAPBOX_API_KEY ?: project.findProperty("MAPBOX_API_KEY")
|
||||
if (mapboxKey != null) {
|
||||
variant.resValue "string", "mapbox_key", mapboxKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.core:core-ktx:1.2.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.core:core-ktx:1.3.1'
|
||||
implementation "androidx.activity:activity-ktx:1.1.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.2.4"
|
||||
implementation "androidx.fragment:fragment-ktx:1.2.5"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.core:core:1.3.0-rc01'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.core:core:1.3.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.google.maps.android:android-maps-utils:0.5'
|
||||
implementation 'androidx.browser:browser:1.2.0'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
|
||||
implementation 'com.google.android.gms:play-services-maps:17.0.0'
|
||||
implementation 'com.google.android.gms:play-services-location:17.0.0'
|
||||
implementation 'com.google.android.libraries.places:places:2.2.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.7.2'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.7.2'
|
||||
implementation 'com.squareup.moshi:moshi-kotlin:1.9.2'
|
||||
@@ -100,9 +113,36 @@ dependencies {
|
||||
implementation "com.mikepenz:aboutlibraries:$about_libs_version"
|
||||
implementation 'com.airbnb.android:lottie:3.4.0'
|
||||
implementation 'io.michaelrocks:bimap:1.0.2'
|
||||
implementation 'com.mapzen.android:lost:3.0.2'
|
||||
|
||||
// AnyMaps
|
||||
def anyMapsVersion = '894f6d62ac'
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
|
||||
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"
|
||||
|
||||
// Google Maps v3 Beta
|
||||
googleImplementation 'com.google.android.libraries.maps:maps:3.1.0-beta'
|
||||
googleImplementation name:'places-maps-sdk-3.1.0-beta', ext:'aar'
|
||||
googleImplementation 'com.android.volley:volley:1.1.1'
|
||||
googleImplementation 'com.google.android.gms:play-services-base:17.3.0'
|
||||
googleImplementation 'com.google.android.gms:play-services-basement:17.3.0'
|
||||
googleImplementation 'com.google.android.gms:play-services-gcm:17.0.0'
|
||||
googleImplementation 'com.google.android.gms:play-services-location:17.0.0'
|
||||
googleImplementation 'com.google.android.gms:play-services-tasks:17.1.0'
|
||||
googleImplementation 'com.google.auto.value:auto-value-annotations:1.6.3'
|
||||
googleImplementation 'com.google.code.gson:gson:2.8.6'
|
||||
googleImplementation 'com.google.android.datatransport:transport-runtime:2.2.3'
|
||||
googleImplementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
|
||||
// Mapbox places (autocomplete)
|
||||
implementation('com.mapbox.mapboxsdk:mapbox-android-plugin-places-v9:0.12.0') {
|
||||
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-accounts'
|
||||
exclude group: 'com.mapbox.mapboxsdk', module: 'mapbox-android-telemetry'
|
||||
}
|
||||
|
||||
// navigation library
|
||||
def nav_version = "2.3.0-alpha06"
|
||||
def nav_version = "2.3.0"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
@@ -118,14 +158,14 @@ dependencies {
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
|
||||
// billing library
|
||||
def billing_version = "2.2.0"
|
||||
implementation "com.android.billingclient:billing:$billing_version"
|
||||
implementation "com.android.billingclient:billing-ktx:$billing_version"
|
||||
def billing_version = "3.0.0"
|
||||
googleImplementation "com.android.billingclient:billing:$billing_version"
|
||||
googleImplementation "com.android.billingclient:billing-ktx:$billing_version"
|
||||
|
||||
// debug tools
|
||||
implementation 'com.facebook.stetho:stetho:1.5.1'
|
||||
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:3.14.7"
|
||||
//noinspection GradleDependency
|
||||
testImplementation 'org.json:json:20080701'
|
||||
@@ -134,5 +174,5 @@ dependencies {
|
||||
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.9.2"
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.5'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9'
|
||||
}
|
||||
BIN
app/libs/places-maps-sdk-3.1.0-beta.aar
Normal file
BIN
app/libs/places-maps-sdk-3.1.0-beta.aar
Normal file
Binary file not shown.
12
app/src/foss/java/net/vonforst/evmap/Inits.kt
Normal file
12
app/src/foss/java/net/vonforst/evmap/Inits.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
|
||||
fun init(context: Context) {
|
||||
|
||||
}
|
||||
|
||||
fun checkPlayServices(activity: Activity): Boolean {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package net.vonforst.evmap.autocomplete
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import com.mapbox.geojson.BoundingBox
|
||||
import com.mapbox.geojson.Point
|
||||
import com.mapbox.mapboxsdk.plugins.places.autocomplete.PlaceAutocomplete
|
||||
import com.mapbox.mapboxsdk.plugins.places.autocomplete.model.PlaceOptions
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.fragment.REQUEST_AUTOCOMPLETE
|
||||
import net.vonforst.evmap.viewmodel.PlaceWithBounds
|
||||
|
||||
|
||||
fun launchAutocomplete(fragment: Fragment) {
|
||||
val placeOptions = PlaceOptions.builder()
|
||||
.build(PlaceOptions.MODE_CARDS)
|
||||
|
||||
val intent = PlaceAutocomplete.IntentBuilder()
|
||||
.accessToken(fragment.getString(R.string.mapbox_key))
|
||||
.placeOptions(placeOptions)
|
||||
.build(fragment.requireActivity())
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||
fragment.startActivityForResult(intent, REQUEST_AUTOCOMPLETE)
|
||||
|
||||
// show keyboard
|
||||
val imm = fragment.requireContext()
|
||||
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.toggleSoftInput(0, 0)
|
||||
}
|
||||
|
||||
fun handleAutocompleteResult(intent: Intent): PlaceWithBounds? {
|
||||
val place = PlaceAutocomplete.getPlace(intent) ?: return null
|
||||
val bbox = place.bbox()?.toLatLngBounds()
|
||||
val center = place.center()!!.toLatLng()
|
||||
return PlaceWithBounds(center, bbox)
|
||||
}
|
||||
|
||||
private fun BoundingBox.toLatLngBounds(): LatLngBounds {
|
||||
return LatLngBounds(
|
||||
southwest().toLatLng(),
|
||||
northeast().toLatLng()
|
||||
)
|
||||
}
|
||||
|
||||
private fun Point.toLatLng(): LatLng = LatLng(this.latitude(), this.longitude())
|
||||
@@ -0,0 +1,37 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
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 kotlinx.android.synthetic.foss.fragment_donate.*
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
class DonateFragment : Fragment() {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_donate, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
(requireActivity() as AppCompatActivity).setSupportActionBar(toolbar)
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
btnDonate.setOnClickListener {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.paypal_link))
|
||||
}
|
||||
}
|
||||
}
|
||||
51
app/src/foss/res/layout/fragment_donate.xml
Normal file
51
app/src/foss/res/layout/fragment_donate.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnDonate"
|
||||
style="@style/Widget.MaterialComponents.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" />
|
||||
|
||||
<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>
|
||||
8
app/src/foss/res/values-de/values.xml
Normal file
8
app/src/foss/res/values-de/values.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_map_provider_names">
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<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 30% Gebühren ab.</string>
|
||||
<string name="donate_paypal">Mit PayPal spenden</string>
|
||||
</resources>
|
||||
13
app/src/foss/res/values/values.xml
Normal file
13
app/src/foss/res/values/values.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_map_provider_names">
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string-array name="pref_map_provider_values" tranlatable="false">
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
<string name="pref_map_provider_default" translatable="false">mapbox</string>
|
||||
<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="paypal_link" translatable="false">https://paypal.me/johan98</string>
|
||||
</resources>
|
||||
10
app/src/google/AndroidManifest.xml
Normal file
10
app/src/google/AndroidManifest.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="net.vonforst.evmap">
|
||||
|
||||
<application>
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="@string/google_maps_key" />
|
||||
</application>
|
||||
</manifest>
|
||||
27
app/src/google/java/net/vonforst/evmap/Inits.kt
Normal file
27
app/src/google/java/net/vonforst/evmap/Inits.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.libraries.places.api.Places
|
||||
|
||||
fun init(context: Context) {
|
||||
Places.initialize(context, context.getString(R.string.google_maps_key));
|
||||
}
|
||||
|
||||
fun checkPlayServices(activity: Activity): Boolean {
|
||||
val request = 9000
|
||||
val apiAvailability = GoogleApiAvailability.getInstance()
|
||||
val resultCode = apiAvailability.isGooglePlayServicesAvailable(activity)
|
||||
if (resultCode != ConnectionResult.SUCCESS) {
|
||||
if (apiAvailability.isUserResolvableError(resultCode)) {
|
||||
apiAvailability.getErrorDialog(activity, resultCode, request).show()
|
||||
} else {
|
||||
Log.d("EVMap", "This device is not supported.")
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.viewmodel.DonationItem
|
||||
|
||||
class DonationAdapter() : DataBindingAdapter<DonationItem>() {
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_donation
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package net.vonforst.evmap.autocomplete
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.car2go.maps.google.adapter.AnyMapAdapter
|
||||
import com.google.android.libraries.places.api.model.Place
|
||||
import com.google.android.libraries.places.widget.Autocomplete
|
||||
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode
|
||||
import net.vonforst.evmap.fragment.REQUEST_AUTOCOMPLETE
|
||||
import net.vonforst.evmap.viewmodel.PlaceWithBounds
|
||||
|
||||
fun launchAutocomplete(fragment: Fragment) {
|
||||
val fields = listOf(Place.Field.LAT_LNG, Place.Field.VIEWPORT)
|
||||
val intent: Intent = Autocomplete.IntentBuilder(
|
||||
AutocompleteActivityMode.OVERLAY, fields
|
||||
)
|
||||
.build(fragment.requireActivity())
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||
fragment.startActivityForResult(intent, REQUEST_AUTOCOMPLETE)
|
||||
|
||||
// show keyboard
|
||||
val imm = fragment.requireContext()
|
||||
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.toggleSoftInput(0, 0)
|
||||
}
|
||||
|
||||
fun handleAutocompleteResult(intent: Intent): PlaceWithBounds? {
|
||||
val place = Autocomplete.getPlaceFromIntent(intent)
|
||||
return PlaceWithBounds(AnyMapAdapter.adapt(place.latLng), AnyMapAdapter.adapt(place.viewport))
|
||||
}
|
||||
@@ -20,12 +20,12 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
override fun onBillingServiceDisconnected() {
|
||||
}
|
||||
|
||||
override fun onBillingSetupFinished(p0: BillingResult?) {
|
||||
override fun onBillingSetupFinished(p0: BillingResult) {
|
||||
loadProducts()
|
||||
|
||||
// consume pending purchases
|
||||
val purchases = billingClient.queryPurchases(BillingClient.SkuType.INAPP)
|
||||
purchases.purchasesList.forEach {
|
||||
purchases.purchasesList?.forEach {
|
||||
if (!it.isAcknowledged) {
|
||||
consumePurchase(it.purchaseToken, false)
|
||||
}
|
||||
@@ -53,8 +53,11 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
|
||||
)
|
||||
.build()
|
||||
billingClient.querySkuDetailsAsync(params) { result, details ->
|
||||
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
products.value = Resource.success(details.map { DonationItem(it) })
|
||||
if (result.responseCode == BillingClient.BillingResponseCode.OK && details != null) {
|
||||
products.value = Resource.success(details
|
||||
.sortedBy { it.priceAmountMicros }
|
||||
.map { DonationItem(it) }
|
||||
)
|
||||
} else {
|
||||
products.value = Resource.error(result.debugMessage, null)
|
||||
}
|
||||
8
app/src/google/res/values-de/values.xml
Normal file
8
app/src/google/res/values-de/values.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_map_provider_names">
|
||||
<item>Google Maps</item>
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<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 30% Gebühren ab.</string>
|
||||
</resources>
|
||||
13
app/src/google/res/values/values.xml
Normal file
13
app/src/google/res/values/values.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pref_map_provider_names">
|
||||
<item>Google Maps</item>
|
||||
<item>OpenStreetMap (Mapbox)</item>
|
||||
</string-array>
|
||||
<string-array name="pref_map_provider_values" tranlatable="false">
|
||||
<item>google</item>
|
||||
<item>mapbox</item>
|
||||
</string-array>
|
||||
<string name="pref_map_provider_default" translatable="false">google</string>
|
||||
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 30% off every donation.</string>
|
||||
</resources>
|
||||
@@ -2,6 +2,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="net.vonforst.evmap">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
|
||||
<application
|
||||
@@ -13,21 +14,14 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<!--
|
||||
The API key for Google Maps-based APIs is defined as a string resource.
|
||||
(See the file "res/values/apikeys.xml").
|
||||
Note that the API key is linked to the encryption key used to sign the APK.
|
||||
You need a different API key for each encryption key, including the release key that is used to
|
||||
sign the APK for publishing.
|
||||
You can define the keys for the debug and release targets in src/debug/ and src/release/.
|
||||
-->
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="@string/google_maps_key" />
|
||||
android:name="com.mapbox.ACCESS_TOKEN"
|
||||
android:value="@string/mapbox_key" />
|
||||
|
||||
<activity
|
||||
android:name=".MapsActivity"
|
||||
android:label="@string/title_activity_maps">
|
||||
android:label="@string/title_activity_maps"
|
||||
android:theme="@style/AppTheme.LaunchScreen">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.maps.android.clustering;
|
||||
|
||||
import com.car2go.maps.model.LatLng;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* A collection of ClusterItems that are nearby each other.
|
||||
*/
|
||||
public interface Cluster<T extends ClusterItem> {
|
||||
LatLng getPosition();
|
||||
|
||||
Collection<T> getItems();
|
||||
|
||||
int getSize();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.maps.android.clustering;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.car2go.maps.model.LatLng;
|
||||
|
||||
/**
|
||||
* ClusterItem represents a marker on the map.
|
||||
*/
|
||||
public interface ClusterItem {
|
||||
|
||||
/**
|
||||
* The position of this marker. This must always return the same value.
|
||||
*/
|
||||
@NonNull
|
||||
LatLng getPosition();
|
||||
|
||||
/**
|
||||
* The title of this marker.
|
||||
*/
|
||||
@Nullable
|
||||
String getTitle();
|
||||
|
||||
/**
|
||||
* The description of this marker.
|
||||
*/
|
||||
@Nullable
|
||||
String getSnippet();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2020 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.maps.android.clustering.algo;
|
||||
|
||||
import com.google.maps.android.clustering.ClusterItem;
|
||||
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
/**
|
||||
* Base Algorithm class that implements lock/unlock functionality.
|
||||
*/
|
||||
public abstract class AbstractAlgorithm<T extends ClusterItem> implements Algorithm<T> {
|
||||
|
||||
private final ReadWriteLock mLock = new ReentrantReadWriteLock();
|
||||
|
||||
@Override
|
||||
public void lock() {
|
||||
mLock.writeLock().lock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlock() {
|
||||
mLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.maps.android.clustering.algo;
|
||||
|
||||
import com.google.maps.android.clustering.Cluster;
|
||||
import com.google.maps.android.clustering.ClusterItem;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Logic for computing clusters
|
||||
*/
|
||||
public interface Algorithm<T extends ClusterItem> {
|
||||
|
||||
/**
|
||||
* Adds an item to the algorithm
|
||||
*
|
||||
* @param item the item to be added
|
||||
* @return true if the algorithm contents changed as a result of the call
|
||||
*/
|
||||
boolean addItem(T item);
|
||||
|
||||
/**
|
||||
* Adds a collection of items to the algorithm
|
||||
*
|
||||
* @param items the items to be added
|
||||
* @return true if the algorithm contents changed as a result of the call
|
||||
*/
|
||||
boolean addItems(Collection<T> items);
|
||||
|
||||
void clearItems();
|
||||
|
||||
/**
|
||||
* Removes an item from the algorithm
|
||||
*
|
||||
* @param item the item to be removed
|
||||
* @return true if this algorithm contained the specified element (or equivalently, if this
|
||||
* algorithm changed as a result of the call).
|
||||
*/
|
||||
boolean removeItem(T item);
|
||||
|
||||
/**
|
||||
* Updates the provided item in the algorithm
|
||||
*
|
||||
* @param item the item to be updated
|
||||
* @return true if the item existed in the algorithm and was updated, or false if the item did
|
||||
* not exist in the algorithm and the algorithm contents remain unchanged.
|
||||
*/
|
||||
boolean updateItem(T item);
|
||||
|
||||
/**
|
||||
* Removes a collection of items from the algorithm
|
||||
*
|
||||
* @param items the items to be removed
|
||||
* @return true if this algorithm contents changed as a result of the call
|
||||
*/
|
||||
boolean removeItems(Collection<T> items);
|
||||
|
||||
Set<? extends Cluster<T>> getClusters(float zoom);
|
||||
|
||||
Collection<T> getItems();
|
||||
|
||||
void setMaxDistanceBetweenClusteredItems(int maxDistance);
|
||||
|
||||
int getMaxDistanceBetweenClusteredItems();
|
||||
|
||||
void lock();
|
||||
|
||||
void unlock();
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.maps.android.clustering.algo;
|
||||
|
||||
import com.car2go.maps.model.LatLng;
|
||||
import com.google.maps.android.clustering.Cluster;
|
||||
import com.google.maps.android.clustering.ClusterItem;
|
||||
import com.google.maps.android.geometry.Bounds;
|
||||
import com.google.maps.android.geometry.Point;
|
||||
import com.google.maps.android.projection.SphericalMercatorProjection;
|
||||
import com.google.maps.android.quadtree.PointQuadTree;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A simple clustering algorithm with O(nlog n) performance. Resulting clusters are not
|
||||
* hierarchical.
|
||||
* <p/>
|
||||
* High level algorithm:<br>
|
||||
* 1. Iterate over items in the order they were added (candidate clusters).<br>
|
||||
* 2. Create a cluster with the center of the item. <br>
|
||||
* 3. Add all items that are within a certain distance to the cluster. <br>
|
||||
* 4. Move any items out of an existing cluster if they are closer to another cluster. <br>
|
||||
* 5. Remove those items from the list of candidate clusters.
|
||||
* <p/>
|
||||
* Clusters have the center of the first element (not the centroid of the items within it).
|
||||
*/
|
||||
public class NonHierarchicalDistanceBasedAlgorithm<T extends ClusterItem> extends AbstractAlgorithm<T> {
|
||||
private static final int DEFAULT_MAX_DISTANCE_AT_ZOOM = 100; // essentially 100 dp.
|
||||
|
||||
private int mMaxDistance = DEFAULT_MAX_DISTANCE_AT_ZOOM;
|
||||
|
||||
/**
|
||||
* Any modifications should be synchronized on mQuadTree.
|
||||
*/
|
||||
private final Collection<QuadItem<T>> mItems = new LinkedHashSet<>();
|
||||
|
||||
/**
|
||||
* Any modifications should be synchronized on mQuadTree.
|
||||
*/
|
||||
private final PointQuadTree<QuadItem<T>> mQuadTree = new PointQuadTree<>(0, 1, 0, 1);
|
||||
|
||||
private static final SphericalMercatorProjection PROJECTION = new SphericalMercatorProjection(1);
|
||||
|
||||
/**
|
||||
* Adds an item to the algorithm
|
||||
*
|
||||
* @param item the item to be added
|
||||
* @return true if the algorithm contents changed as a result of the call
|
||||
*/
|
||||
@Override
|
||||
public boolean addItem(T item) {
|
||||
boolean result;
|
||||
final QuadItem<T> quadItem = new QuadItem<>(item);
|
||||
synchronized (mQuadTree) {
|
||||
result = mItems.add(quadItem);
|
||||
if (result) {
|
||||
mQuadTree.add(quadItem);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a collection of items to the algorithm
|
||||
*
|
||||
* @param items the items to be added
|
||||
* @return true if the algorithm contents changed as a result of the call
|
||||
*/
|
||||
@Override
|
||||
public boolean addItems(Collection<T> items) {
|
||||
boolean result = false;
|
||||
for (T item : items) {
|
||||
boolean individualResult = addItem(item);
|
||||
if (individualResult) {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearItems() {
|
||||
synchronized (mQuadTree) {
|
||||
mItems.clear();
|
||||
mQuadTree.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the algorithm
|
||||
*
|
||||
* @param item the item to be removed
|
||||
* @return true if this algorithm contained the specified element (or equivalently, if this
|
||||
* algorithm changed as a result of the call).
|
||||
*/
|
||||
@Override
|
||||
public boolean removeItem(T item) {
|
||||
boolean result;
|
||||
// QuadItem delegates hashcode() and equals() to its item so,
|
||||
// removing any QuadItem to that item will remove the item
|
||||
final QuadItem<T> quadItem = new QuadItem<>(item);
|
||||
synchronized (mQuadTree) {
|
||||
result = mItems.remove(quadItem);
|
||||
if (result) {
|
||||
mQuadTree.remove(quadItem);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a collection of items from the algorithm
|
||||
*
|
||||
* @param items the items to be removed
|
||||
* @return true if this algorithm contents changed as a result of the call
|
||||
*/
|
||||
@Override
|
||||
public boolean removeItems(Collection<T> items) {
|
||||
boolean result = false;
|
||||
synchronized (mQuadTree) {
|
||||
for (T item : items) {
|
||||
// QuadItem delegates hashcode() and equals() to its item so,
|
||||
// removing any QuadItem to that item will remove the item
|
||||
final QuadItem<T> quadItem = new QuadItem<>(item);
|
||||
boolean individualResult = mItems.remove(quadItem);
|
||||
if (individualResult) {
|
||||
mQuadTree.remove(quadItem);
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the provided item in the algorithm
|
||||
*
|
||||
* @param item the item to be updated
|
||||
* @return true if the item existed in the algorithm and was updated, or false if the item did
|
||||
* not exist in the algorithm and the algorithm contents remain unchanged.
|
||||
*/
|
||||
@Override
|
||||
public boolean updateItem(T item) {
|
||||
// TODO - Can this be optimized to update the item in-place if the location hasn't changed?
|
||||
boolean result;
|
||||
synchronized (mQuadTree) {
|
||||
result = removeItem(item);
|
||||
if (result) {
|
||||
// Only add the item if it was removed (to help prevent accidental duplicates on map)
|
||||
result = addItem(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<? extends Cluster<T>> getClusters(float zoom) {
|
||||
final int discreteZoom = (int) zoom;
|
||||
|
||||
final double zoomSpecificSpan = mMaxDistance / Math.pow(2, discreteZoom) / 256;
|
||||
|
||||
final Set<QuadItem<T>> visitedCandidates = new HashSet<>();
|
||||
final Set<Cluster<T>> results = new HashSet<>();
|
||||
final Map<QuadItem<T>, Double> distanceToCluster = new HashMap<>();
|
||||
final Map<QuadItem<T>, StaticCluster<T>> itemToCluster = new HashMap<>();
|
||||
|
||||
synchronized (mQuadTree) {
|
||||
for (QuadItem<T> candidate : getClusteringItems(mQuadTree, zoom)) {
|
||||
if (visitedCandidates.contains(candidate)) {
|
||||
// Candidate is already part of another cluster.
|
||||
continue;
|
||||
}
|
||||
|
||||
Bounds searchBounds = createBoundsFromSpan(candidate.getPoint(), zoomSpecificSpan);
|
||||
Collection<QuadItem<T>> clusterItems;
|
||||
clusterItems = mQuadTree.search(searchBounds);
|
||||
if (clusterItems.size() == 1) {
|
||||
// Only the current marker is in range. Just add the single item to the results.
|
||||
results.add(candidate);
|
||||
visitedCandidates.add(candidate);
|
||||
distanceToCluster.put(candidate, 0d);
|
||||
continue;
|
||||
}
|
||||
StaticCluster<T> cluster = new StaticCluster<>(candidate.mClusterItem.getPosition());
|
||||
results.add(cluster);
|
||||
|
||||
for (QuadItem<T> clusterItem : clusterItems) {
|
||||
Double existingDistance = distanceToCluster.get(clusterItem);
|
||||
double distance = distanceSquared(clusterItem.getPoint(), candidate.getPoint());
|
||||
if (existingDistance != null) {
|
||||
// Item already belongs to another cluster. Check if it's closer to this cluster.
|
||||
if (existingDistance < distance) {
|
||||
continue;
|
||||
}
|
||||
// Move item to the closer cluster.
|
||||
itemToCluster.get(clusterItem).remove(clusterItem.mClusterItem);
|
||||
}
|
||||
distanceToCluster.put(clusterItem, distance);
|
||||
cluster.add(clusterItem.mClusterItem);
|
||||
itemToCluster.put(clusterItem, cluster);
|
||||
}
|
||||
visitedCandidates.addAll(clusterItems);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
protected Collection<QuadItem<T>> getClusteringItems(PointQuadTree<QuadItem<T>> quadTree, float zoom) {
|
||||
return mItems;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<T> getItems() {
|
||||
final Set<T> items = new LinkedHashSet<>();
|
||||
synchronized (mQuadTree) {
|
||||
for (QuadItem<T> quadItem : mItems) {
|
||||
items.add(quadItem.mClusterItem);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxDistanceBetweenClusteredItems(int maxDistance) {
|
||||
mMaxDistance = maxDistance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxDistanceBetweenClusteredItems() {
|
||||
return mMaxDistance;
|
||||
}
|
||||
|
||||
private double distanceSquared(Point a, Point b) {
|
||||
return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
|
||||
}
|
||||
|
||||
private Bounds createBoundsFromSpan(Point p, double span) {
|
||||
// TODO: Use a span that takes into account the visual size of the marker, not just its
|
||||
// LatLng.
|
||||
double halfSpan = span / 2;
|
||||
return new Bounds(
|
||||
p.x - halfSpan, p.x + halfSpan,
|
||||
p.y - halfSpan, p.y + halfSpan);
|
||||
}
|
||||
|
||||
protected static class QuadItem<T extends ClusterItem> implements PointQuadTree.Item, Cluster<T> {
|
||||
private final T mClusterItem;
|
||||
private final Point mPoint;
|
||||
private final LatLng mPosition;
|
||||
private Set<T> singletonSet;
|
||||
|
||||
private QuadItem(T item) {
|
||||
mClusterItem = item;
|
||||
mPosition = item.getPosition();
|
||||
mPoint = PROJECTION.toPoint(mPosition);
|
||||
singletonSet = Collections.singleton(mClusterItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Point getPoint() {
|
||||
return mPoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LatLng getPosition() {
|
||||
return mPosition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<T> getItems() {
|
||||
return singletonSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mClusterItem.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (!(other instanceof QuadItem<?>)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((QuadItem<?>) other).mClusterItem.equals(mClusterItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.maps.android.clustering.algo;
|
||||
|
||||
import com.car2go.maps.model.LatLng;
|
||||
import com.google.maps.android.clustering.Cluster;
|
||||
import com.google.maps.android.clustering.ClusterItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A cluster whose center is determined upon creation.
|
||||
*/
|
||||
public class StaticCluster<T extends ClusterItem> implements Cluster<T> {
|
||||
private final LatLng mCenter;
|
||||
private final List<T> mItems = new ArrayList<T>();
|
||||
|
||||
public StaticCluster(LatLng center) {
|
||||
mCenter = center;
|
||||
}
|
||||
|
||||
public boolean add(T t) {
|
||||
return mItems.add(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LatLng getPosition() {
|
||||
return mCenter;
|
||||
}
|
||||
|
||||
public boolean remove(T t) {
|
||||
return mItems.remove(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<T> getItems() {
|
||||
return mItems;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize() {
|
||||
return mItems.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StaticCluster{" +
|
||||
"mCenter=" + mCenter +
|
||||
", mItems.size=" + mItems.size() +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return mCenter.hashCode() + mItems.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (!(other instanceof StaticCluster<?>)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((StaticCluster<?>) other).mCenter.equals(mCenter)
|
||||
&& ((StaticCluster<?>) other).mItems.equals(mItems);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.maps.android.geometry;
|
||||
|
||||
/**
|
||||
* Represents an area in the cartesian plane.
|
||||
*/
|
||||
public class Bounds {
|
||||
public final double minX;
|
||||
public final double minY;
|
||||
|
||||
public final double maxX;
|
||||
public final double maxY;
|
||||
|
||||
public final double midX;
|
||||
public final double midY;
|
||||
|
||||
public Bounds(double minX, double maxX, double minY, double maxY) {
|
||||
this.minX = minX;
|
||||
this.minY = minY;
|
||||
this.maxX = maxX;
|
||||
this.maxY = maxY;
|
||||
|
||||
midX = (minX + maxX) / 2;
|
||||
midY = (minY + maxY) / 2;
|
||||
}
|
||||
|
||||
public boolean contains(double x, double y) {
|
||||
return minX <= x && x <= maxX && minY <= y && y <= maxY;
|
||||
}
|
||||
|
||||
public boolean contains(Point point) {
|
||||
return contains(point.x, point.y);
|
||||
}
|
||||
|
||||
public boolean intersects(double minX, double maxX, double minY, double maxY) {
|
||||
return minX < this.maxX && this.minX < maxX && minY < this.maxY && this.minY < maxY;
|
||||
}
|
||||
|
||||
public boolean intersects(Bounds bounds) {
|
||||
return intersects(bounds.minX, bounds.maxX, bounds.minY, bounds.maxY);
|
||||
}
|
||||
|
||||
public boolean contains(Bounds bounds) {
|
||||
return bounds.minX >= minX && bounds.maxX <= maxX && bounds.minY >= minY && bounds.maxY <= maxY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.maps.android.geometry;
|
||||
|
||||
public class Point {
|
||||
public final double x;
|
||||
public final double y;
|
||||
|
||||
public Point(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Point{" +
|
||||
"x=" + x +
|
||||
", y=" + y +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.maps.android.projection;
|
||||
|
||||
/**
|
||||
* @deprecated since 0.2. Use {@link com.google.maps.android.geometry.Point} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public class Point extends com.google.maps.android.geometry.Point {
|
||||
public Point(double x, double y) {
|
||||
super(x, y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.maps.android.projection;
|
||||
|
||||
import com.car2go.maps.model.LatLng;
|
||||
|
||||
public class SphericalMercatorProjection {
|
||||
final double mWorldWidth;
|
||||
|
||||
public SphericalMercatorProjection(final double worldWidth) {
|
||||
mWorldWidth = worldWidth;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public Point toPoint(final LatLng latLng) {
|
||||
final double x = latLng.longitude / 360 + .5;
|
||||
final double siny = Math.sin(Math.toRadians(latLng.latitude));
|
||||
final double y = 0.5 * Math.log((1 + siny) / (1 - siny)) / -(2 * Math.PI) + .5;
|
||||
|
||||
return new Point(x * mWorldWidth, y * mWorldWidth);
|
||||
}
|
||||
|
||||
public LatLng toLatLng(com.google.maps.android.geometry.Point point) {
|
||||
final double x = point.x / mWorldWidth - 0.5;
|
||||
final double lng = x * 360;
|
||||
|
||||
double y = .5 - (point.y / mWorldWidth);
|
||||
final double lat = 90 - Math.toDegrees(Math.atan(Math.exp(-y * 2 * Math.PI)) * 2);
|
||||
|
||||
return new LatLng(lat, lng);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.maps.android.quadtree;
|
||||
|
||||
import com.google.maps.android.geometry.Bounds;
|
||||
import com.google.maps.android.geometry.Point;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A quad tree which tracks items with a Point geometry.
|
||||
* See http://en.wikipedia.org/wiki/Quadtree for details on the data structure.
|
||||
* This class is not thread safe.
|
||||
*/
|
||||
public class PointQuadTree<T extends PointQuadTree.Item> {
|
||||
public interface Item {
|
||||
Point getPoint();
|
||||
}
|
||||
|
||||
/**
|
||||
* The bounds of this quad.
|
||||
*/
|
||||
private final Bounds mBounds;
|
||||
|
||||
/**
|
||||
* The depth of this quad in the tree.
|
||||
*/
|
||||
private final int mDepth;
|
||||
|
||||
/**
|
||||
* Maximum number of elements to store in a quad before splitting.
|
||||
*/
|
||||
private final static int MAX_ELEMENTS = 50;
|
||||
|
||||
/**
|
||||
* The elements inside this quad, if any.
|
||||
*/
|
||||
private Set<T> mItems;
|
||||
|
||||
/**
|
||||
* Maximum depth.
|
||||
*/
|
||||
private final static int MAX_DEPTH = 40;
|
||||
|
||||
/**
|
||||
* Child quads.
|
||||
*/
|
||||
private List<PointQuadTree<T>> mChildren = null;
|
||||
|
||||
/**
|
||||
* Creates a new quad tree with specified bounds.
|
||||
*
|
||||
* @param minX
|
||||
* @param maxX
|
||||
* @param minY
|
||||
* @param maxY
|
||||
*/
|
||||
public PointQuadTree(double minX, double maxX, double minY, double maxY) {
|
||||
this(new Bounds(minX, maxX, minY, maxY));
|
||||
}
|
||||
|
||||
public PointQuadTree(Bounds bounds) {
|
||||
this(bounds, 0);
|
||||
}
|
||||
|
||||
private PointQuadTree(double minX, double maxX, double minY, double maxY, int depth) {
|
||||
this(new Bounds(minX, maxX, minY, maxY), depth);
|
||||
}
|
||||
|
||||
private PointQuadTree(Bounds bounds, int depth) {
|
||||
mBounds = bounds;
|
||||
mDepth = depth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an item.
|
||||
*/
|
||||
public void add(T item) {
|
||||
Point point = item.getPoint();
|
||||
if (this.mBounds.contains(point.x, point.y)) {
|
||||
insert(point.x, point.y, item);
|
||||
}
|
||||
}
|
||||
|
||||
private void insert(double x, double y, T item) {
|
||||
if (this.mChildren != null) {
|
||||
if (y < mBounds.midY) {
|
||||
if (x < mBounds.midX) { // top left
|
||||
mChildren.get(0).insert(x, y, item);
|
||||
} else { // top right
|
||||
mChildren.get(1).insert(x, y, item);
|
||||
}
|
||||
} else {
|
||||
if (x < mBounds.midX) { // bottom left
|
||||
mChildren.get(2).insert(x, y, item);
|
||||
} else {
|
||||
mChildren.get(3).insert(x, y, item);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (mItems == null) {
|
||||
mItems = new LinkedHashSet<>();
|
||||
}
|
||||
mItems.add(item);
|
||||
if (mItems.size() > MAX_ELEMENTS && mDepth < MAX_DEPTH) {
|
||||
split();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Split this quad.
|
||||
*/
|
||||
private void split() {
|
||||
mChildren = new ArrayList<PointQuadTree<T>>(4);
|
||||
mChildren.add(new PointQuadTree<T>(mBounds.minX, mBounds.midX, mBounds.minY, mBounds.midY, mDepth + 1));
|
||||
mChildren.add(new PointQuadTree<T>(mBounds.midX, mBounds.maxX, mBounds.minY, mBounds.midY, mDepth + 1));
|
||||
mChildren.add(new PointQuadTree<T>(mBounds.minX, mBounds.midX, mBounds.midY, mBounds.maxY, mDepth + 1));
|
||||
mChildren.add(new PointQuadTree<T>(mBounds.midX, mBounds.maxX, mBounds.midY, mBounds.maxY, mDepth + 1));
|
||||
|
||||
Set<T> items = mItems;
|
||||
mItems = null;
|
||||
|
||||
for (T item : items) {
|
||||
// re-insert items into child quads.
|
||||
insert(item.getPoint().x, item.getPoint().y, item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given item from the set.
|
||||
*
|
||||
* @return whether the item was removed.
|
||||
*/
|
||||
public boolean remove(T item) {
|
||||
Point point = item.getPoint();
|
||||
if (this.mBounds.contains(point.x, point.y)) {
|
||||
return remove(point.x, point.y, item);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean remove(double x, double y, T item) {
|
||||
if (this.mChildren != null) {
|
||||
if (y < mBounds.midY) {
|
||||
if (x < mBounds.midX) { // top left
|
||||
return mChildren.get(0).remove(x, y, item);
|
||||
} else { // top right
|
||||
return mChildren.get(1).remove(x, y, item);
|
||||
}
|
||||
} else {
|
||||
if (x < mBounds.midX) { // bottom left
|
||||
return mChildren.get(2).remove(x, y, item);
|
||||
} else {
|
||||
return mChildren.get(3).remove(x, y, item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (mItems == null) {
|
||||
return false;
|
||||
} else {
|
||||
return mItems.remove(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all points from the quadTree
|
||||
*/
|
||||
public void clear() {
|
||||
mChildren = null;
|
||||
if (mItems != null) {
|
||||
mItems.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for all items within a given bounds.
|
||||
*/
|
||||
public Collection<T> search(Bounds searchBounds) {
|
||||
final List<T> results = new ArrayList<T>();
|
||||
search(searchBounds, results);
|
||||
return results;
|
||||
}
|
||||
|
||||
private void search(Bounds searchBounds, Collection<T> results) {
|
||||
if (!mBounds.intersects(searchBounds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mChildren != null) {
|
||||
for (PointQuadTree<T> quad : mChildren) {
|
||||
quad.search(searchBounds, results);
|
||||
}
|
||||
} else if (mItems != null) {
|
||||
if (searchBounds.contains(mBounds)) {
|
||||
results.addAll(mItems);
|
||||
} else {
|
||||
for (T item : mItems) {
|
||||
if (searchBounds.contains(item.getPoint())) {
|
||||
results.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
268
app/src/main/java/com/google/maps/android/ui/IconGenerator.java
Normal file
268
app/src/main/java/com/google/maps/android/ui/IconGenerator.java
Normal file
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* 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.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import net.vonforst.evmap.R;
|
||||
|
||||
/**
|
||||
* IconGenerator generates icons that contain text (or custom content) within an info
|
||||
* window-like shape.
|
||||
* <p/>
|
||||
* The icon {@link Bitmap}s generated by the factory should be used in conjunction with a
|
||||
* BitmapDescriptorFactory.
|
||||
* <p/>
|
||||
* This class is not thread safe.
|
||||
*/
|
||||
public class IconGenerator {
|
||||
private final Context mContext;
|
||||
|
||||
private ViewGroup mContainer;
|
||||
private RotationLayout mRotationLayout;
|
||||
private TextView mTextView;
|
||||
private View mContentView;
|
||||
|
||||
private int mRotation;
|
||||
|
||||
private float mAnchorU = 0.5f;
|
||||
private float mAnchorV = 1f;
|
||||
|
||||
/**
|
||||
* Creates a new IconGenerator with the default style.
|
||||
*/
|
||||
public IconGenerator(Context context) {
|
||||
mContext = context;
|
||||
mContainer = (ViewGroup) LayoutInflater.from(mContext).inflate(R.layout.amu_text_bubble, null);
|
||||
mRotationLayout = (RotationLayout) mContainer.getChildAt(0);
|
||||
mContentView = mTextView = (TextView) mRotationLayout.findViewById(R.id.amu_text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text content, then creates an icon with the current style.
|
||||
*
|
||||
* @param text the text content to display inside the icon.
|
||||
*/
|
||||
public Bitmap makeIcon(CharSequence text) {
|
||||
if (mTextView != null) {
|
||||
mTextView.setText(text);
|
||||
}
|
||||
|
||||
return makeIcon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an icon with the current content and style.
|
||||
* <p/>
|
||||
* This method is useful if a custom view has previously been set, or if text content is not
|
||||
* applicable.
|
||||
*/
|
||||
public Bitmap makeIcon() {
|
||||
int measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||
mContainer.measure(measureSpec, measureSpec);
|
||||
|
||||
int measuredWidth = mContainer.getMeasuredWidth();
|
||||
int measuredHeight = mContainer.getMeasuredHeight();
|
||||
|
||||
mContainer.layout(0, 0, measuredWidth, measuredHeight);
|
||||
|
||||
if (mRotation == 1 || mRotation == 3) {
|
||||
measuredHeight = mContainer.getMeasuredWidth();
|
||||
measuredWidth = mContainer.getMeasuredHeight();
|
||||
}
|
||||
|
||||
Bitmap r = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888);
|
||||
r.eraseColor(Color.TRANSPARENT);
|
||||
|
||||
Canvas canvas = new Canvas(r);
|
||||
|
||||
switch (mRotation) {
|
||||
case 0:
|
||||
// do nothing
|
||||
break;
|
||||
case 1:
|
||||
canvas.translate(measuredWidth, 0);
|
||||
canvas.rotate(90);
|
||||
break;
|
||||
case 2:
|
||||
canvas.rotate(180, measuredWidth / 2, measuredHeight / 2);
|
||||
break;
|
||||
case 3:
|
||||
canvas.translate(0, measuredHeight);
|
||||
canvas.rotate(270);
|
||||
break;
|
||||
}
|
||||
mContainer.draw(canvas);
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the child view for the icon.
|
||||
* <p/>
|
||||
* If the view contains a {@link TextView} with the id "text", operations such as {@link
|
||||
* #setTextAppearance} and {@link #makeIcon(CharSequence)} will operate upon that {@link TextView}.
|
||||
*/
|
||||
public void setContentView(View contentView) {
|
||||
mRotationLayout.removeAllViews();
|
||||
mRotationLayout.addView(contentView);
|
||||
mContentView = contentView;
|
||||
final View view = mRotationLayout.findViewById(R.id.amu_text);
|
||||
mTextView = view instanceof TextView ? (TextView) view : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the contents of the icon.
|
||||
*
|
||||
* @param degrees the amount the contents should be rotated, as a multiple of 90 degrees.
|
||||
*/
|
||||
public void setContentRotation(int degrees) {
|
||||
mRotationLayout.setViewRotation(degrees);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the icon.
|
||||
*
|
||||
* @param degrees the amount the icon should be rotated, as a multiple of 90 degrees.
|
||||
*/
|
||||
public void setRotation(int degrees) {
|
||||
mRotation = ((degrees + 360) % 360) / 90;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return u coordinate of the anchor, with rotation applied.
|
||||
*/
|
||||
public float getAnchorU() {
|
||||
return rotateAnchor(mAnchorU, mAnchorV);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return v coordinate of the anchor, with rotation applied.
|
||||
*/
|
||||
public float getAnchorV() {
|
||||
return rotateAnchor(mAnchorV, mAnchorU);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates the anchor around (u, v) = (0, 0).
|
||||
*/
|
||||
private float rotateAnchor(float u, float v) {
|
||||
switch (mRotation) {
|
||||
case 0:
|
||||
return u;
|
||||
case 1:
|
||||
return 1 - v;
|
||||
case 2:
|
||||
return 1 - u;
|
||||
case 3:
|
||||
return v;
|
||||
}
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text color, size, style, hint color, and highlight color from the specified
|
||||
* <code>TextAppearance</code> resource.
|
||||
*
|
||||
* @param resid the identifier of the resource.
|
||||
*/
|
||||
public void setTextAppearance(Context context, int resid) {
|
||||
if (mTextView != null) {
|
||||
mTextView.setTextAppearance(context, resid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text color, size, style, hint color, and highlight color from the specified
|
||||
* <code>TextAppearance</code> resource.
|
||||
*
|
||||
* @param resid the identifier of the resource.
|
||||
*/
|
||||
public void setTextAppearance(int resid) {
|
||||
setTextAppearance(mContext, resid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the background to a given Drawable, or remove the background.
|
||||
*
|
||||
* @param background the Drawable to use as the background, or null to remove the background.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
// View#setBackgroundDrawable is compatible with pre-API level 16 (Jelly Bean).
|
||||
public void setBackground(Drawable background) {
|
||||
mContainer.setBackgroundDrawable(background);
|
||||
|
||||
// Force setting of padding.
|
||||
// setBackgroundDrawable does not call setPadding if the background has 0 padding.
|
||||
if (background != null) {
|
||||
Rect rect = new Rect();
|
||||
background.getPadding(rect);
|
||||
mContainer.setPadding(rect.left, rect.top, rect.right, rect.bottom);
|
||||
} else {
|
||||
mContainer.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the padding of the content view. The default padding of the content view (i.e. text
|
||||
* view) is 5dp top/bottom and 10dp left/right.
|
||||
*
|
||||
* @param left the left padding in pixels.
|
||||
* @param top the top padding in pixels.
|
||||
* @param right the right padding in pixels.
|
||||
* @param bottom the bottom padding in pixels.
|
||||
*/
|
||||
public void setContentPadding(int left, int top, int right, int bottom) {
|
||||
mContentView.setPadding(left, top, right, bottom);
|
||||
}
|
||||
|
||||
public static final int STYLE_DEFAULT = 1;
|
||||
public static final int STYLE_WHITE = 2;
|
||||
public static final int STYLE_RED = 3;
|
||||
public static final int STYLE_BLUE = 4;
|
||||
public static final int STYLE_GREEN = 5;
|
||||
public static final int STYLE_PURPLE = 6;
|
||||
public static final int STYLE_ORANGE = 7;
|
||||
|
||||
private static int getStyleColor(int style) {
|
||||
switch (style) {
|
||||
default:
|
||||
case STYLE_DEFAULT:
|
||||
case STYLE_WHITE:
|
||||
return 0xffffffff;
|
||||
case STYLE_RED:
|
||||
return 0xffcc0000;
|
||||
case STYLE_BLUE:
|
||||
return 0xff0099cc;
|
||||
case STYLE_GREEN:
|
||||
return 0xff669900;
|
||||
case STYLE_PURPLE:
|
||||
return 0xff9933cc;
|
||||
case STYLE_ORANGE:
|
||||
return 0xffff8800;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.maps.android.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
/**
|
||||
* RotationLayout rotates the contents of the layout by multiples of 90 degrees.
|
||||
* <p/>
|
||||
* May not work with padding.
|
||||
*/
|
||||
public class RotationLayout extends FrameLayout {
|
||||
private int mRotation;
|
||||
|
||||
public RotationLayout(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public RotationLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public RotationLayout(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
if (mRotation == 1 || mRotation == 3) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
|
||||
} else {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param degrees the rotation, in degrees.
|
||||
*/
|
||||
public void setViewRotation(int degrees) {
|
||||
mRotation = ((degrees + 360) % 360) / 90;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void dispatchDraw(Canvas canvas) {
|
||||
if (mRotation == 0) {
|
||||
super.dispatchDraw(canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mRotation == 1) {
|
||||
canvas.translate(getWidth(), 0);
|
||||
canvas.rotate(90, getWidth() / 2, 0);
|
||||
canvas.translate(getHeight() / 2, getWidth() / 2);
|
||||
} else if (mRotation == 2) {
|
||||
canvas.rotate(180, getWidth() / 2, getHeight() / 2);
|
||||
} else {
|
||||
canvas.translate(0, getHeight());
|
||||
canvas.rotate(270, getWidth() / 2, 0);
|
||||
canvas.translate(getHeight() / 2, -getWidth() / 2);
|
||||
}
|
||||
|
||||
super.dispatchDraw(canvas);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
|
||||
public class SquareTextView extends AppCompatTextView {
|
||||
private int mOffsetTop = 0;
|
||||
private int mOffsetLeft = 0;
|
||||
|
||||
public SquareTextView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public SquareTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public SquareTextView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
int width = getMeasuredWidth();
|
||||
int height = getMeasuredHeight();
|
||||
int dimension = Math.max(width, height);
|
||||
if (width > height) {
|
||||
mOffsetTop = width - height;
|
||||
mOffsetLeft = 0;
|
||||
} else {
|
||||
mOffsetTop = 0;
|
||||
mOffsetLeft = height - width;
|
||||
}
|
||||
setMeasuredDimension(dimension, dimension);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
canvas.translate(mOffsetLeft / 2, mOffsetTop / 2);
|
||||
super.draw(canvas);
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@ package net.vonforst.evmap
|
||||
|
||||
import android.app.Application
|
||||
import com.facebook.stetho.Stetho
|
||||
import com.google.android.libraries.places.api.Places
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.updateNightMode
|
||||
|
||||
class EvMapApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
updateNightMode(PreferenceDataSource(this))
|
||||
Stetho.initializeWithDefaults(this);
|
||||
Places.initialize(getApplicationContext(), getString(R.string.google_maps_key));
|
||||
init(applicationContext)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.findNavController
|
||||
@@ -14,6 +17,8 @@ import com.google.android.material.navigation.NavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.utils.LocaleContextWrapper
|
||||
|
||||
|
||||
const val REQUEST_LOCATION_PERMISSION = 1
|
||||
|
||||
@@ -28,7 +33,17 @@ class MapsActivity : AppCompatActivity() {
|
||||
var fragmentCallback: FragmentCallback? = null
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
return super.attachBaseContext(
|
||||
LocaleContextWrapper.wrap(
|
||||
newBase, PreferenceDataSource(newBase).language
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// set theme to AppTheme to end launch screen
|
||||
setTheme(R.style.AppTheme)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_maps)
|
||||
@@ -38,43 +53,52 @@ class MapsActivity : AppCompatActivity() {
|
||||
setOf(
|
||||
R.id.map,
|
||||
R.id.favs,
|
||||
R.id.about
|
||||
R.id.about,
|
||||
R.id.settings
|
||||
),
|
||||
findViewById<DrawerLayout>(R.id.drawer_layout)
|
||||
)
|
||||
findViewById<NavigationView>(R.id.nav_view).setupWithNavController(navController)
|
||||
|
||||
prefs = PreferenceDataSource(this)
|
||||
|
||||
checkPlayServices(this)
|
||||
}
|
||||
|
||||
fun navigateTo(charger: ChargeLocation) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
val coord = charger.coordinates
|
||||
|
||||
// google maps navigation
|
||||
val coord = charger.coordinates
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse("google.navigation:q=${coord.lat},${coord.lng}")
|
||||
val pm = packageManager
|
||||
if (intent.resolveActivity(pm) != null && prefs.navigateUseMaps) {
|
||||
if (prefs.navigateUseMaps && intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent);
|
||||
} else {
|
||||
// fallback: generic geo intent
|
||||
intent.data = Uri.parse("geo:0,0?q=${coord.lat},${coord.lng}(${charger.name})")
|
||||
if (intent.resolveActivity(pm) != null) {
|
||||
startActivity(intent);
|
||||
} else {
|
||||
val cb = fragmentCallback ?: return
|
||||
Snackbar.make(
|
||||
cb.getRootView(),
|
||||
R.string.no_maps_app_found,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
showLocation(charger)
|
||||
}
|
||||
}
|
||||
|
||||
fun showLocation(charger: ChargeLocation) {
|
||||
val coord = charger.coordinates
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.data = Uri.parse("geo:0,0?q=${coord.lat},${coord.lng}(${charger.name})")
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
startActivity(intent);
|
||||
} else {
|
||||
val cb = fragmentCallback ?: return
|
||||
Snackbar.make(
|
||||
cb.getRootView(),
|
||||
R.string.no_maps_app_found,
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openUrl(url: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
startActivity(intent)
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(ContextCompat.getColor(this, R.color.colorPrimary))
|
||||
.build()
|
||||
intent.launchUrl(this, Uri.parse(url))
|
||||
}
|
||||
|
||||
fun shareUrl(url: String) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package net.vonforst.evmap
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.*
|
||||
import android.text.style.StyleSpan
|
||||
|
||||
fun Bundle.optDouble(name: String): Double? {
|
||||
if (!this.containsKey(name)) return null
|
||||
@@ -14,4 +17,38 @@ fun Bundle.optLong(name: String): Long? {
|
||||
|
||||
val lng = this.getLong(name, Long.MIN_VALUE)
|
||||
return if (lng == Long.MIN_VALUE) null else lng
|
||||
}
|
||||
|
||||
fun <T> Iterable<T>.joinToSpannedString(
|
||||
separator: CharSequence = ", ",
|
||||
prefix: CharSequence = "",
|
||||
postfix: CharSequence = "",
|
||||
limit: Int = -1,
|
||||
truncated: CharSequence = "...",
|
||||
transform: ((T) -> CharSequence)? = null
|
||||
): CharSequence {
|
||||
return SpannedString(
|
||||
joinTo(
|
||||
SpannableStringBuilder(),
|
||||
separator,
|
||||
prefix,
|
||||
postfix,
|
||||
limit,
|
||||
truncated,
|
||||
transform
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
operator fun CharSequence.plus(other: CharSequence): CharSequence {
|
||||
return TextUtils.concat(this, other)
|
||||
}
|
||||
|
||||
fun String.bold(): CharSequence {
|
||||
return SpannableString(this).apply {
|
||||
setSpan(
|
||||
StyleSpan(Typeface.BOLD), 0, this.length,
|
||||
Spannable.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,24 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.children
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.Observable
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
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.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceBinding
|
||||
import net.vonforst.evmap.databinding.ItemFilterSliderBinding
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
import net.vonforst.evmap.viewmodel.FavoritesViewModel
|
||||
|
||||
interface Equatable {
|
||||
override fun equals(other: Any?): Boolean;
|
||||
}
|
||||
|
||||
abstract class DataBindingAdapter<T : Equatable>() :
|
||||
ListAdapter<T, DataBindingAdapter.ViewHolder<T>>(DiffCallback()) {
|
||||
abstract class DataBindingAdapter<T : Equatable>(getKey: ((T) -> Any)? = null) :
|
||||
ListAdapter<T, DataBindingAdapter.ViewHolder<T>>(DiffCallback(getKey)) {
|
||||
|
||||
var onClickListener: ((T) -> Unit)? = null
|
||||
|
||||
@@ -47,14 +39,20 @@ abstract class DataBindingAdapter<T : Equatable>() :
|
||||
open fun bind(holder: ViewHolder<T>, item: T) {
|
||||
holder.binding.setVariable(BR.item, item)
|
||||
holder.binding.executePendingBindings()
|
||||
holder.binding.root.setOnClickListener {
|
||||
val listener = onClickListener ?: return@setOnClickListener
|
||||
listener(item)
|
||||
if (onClickListener != null) {
|
||||
holder.binding.root.setOnClickListener {
|
||||
val listener = onClickListener ?: return@setOnClickListener
|
||||
listener(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DiffCallback<T : Equatable> : DiffUtil.ItemCallback<T>() {
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = oldItem === newItem
|
||||
class DiffCallback<T : Equatable>(val getKey: ((T) -> Any)?) : DiffUtil.ItemCallback<T>() {
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = if (getKey != null) {
|
||||
(getKey)(oldItem) == (getKey)(newItem)
|
||||
} else {
|
||||
oldItem === newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = oldItem == newItem
|
||||
}
|
||||
@@ -80,62 +78,6 @@ class ConnectorAdapter : DataBindingAdapter<ConnectorAdapter.ChargepointWithAvai
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_connector
|
||||
}
|
||||
|
||||
class DetailAdapter : DataBindingAdapter<DetailAdapter.Detail>() {
|
||||
data class Detail(
|
||||
val icon: Int,
|
||||
val contentDescription: Int,
|
||||
val text: CharSequence,
|
||||
val detailText: CharSequence? = null,
|
||||
val links: Boolean = true
|
||||
) : Equatable
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_detail
|
||||
}
|
||||
|
||||
fun buildDetails(loc: ChargeLocation?, ctx: Context): List<DetailAdapter.Detail> {
|
||||
if (loc == null) return emptyList()
|
||||
|
||||
return listOfNotNull(
|
||||
DetailAdapter.Detail(
|
||||
R.drawable.ic_address,
|
||||
R.string.address,
|
||||
loc.address.toString(),
|
||||
loc.locationDescription
|
||||
),
|
||||
if (loc.operator != null) DetailAdapter.Detail(
|
||||
R.drawable.ic_operator,
|
||||
R.string.operator,
|
||||
loc.operator
|
||||
) else null,
|
||||
if (loc.network != null) DetailAdapter.Detail(
|
||||
R.drawable.ic_network,
|
||||
R.string.network,
|
||||
loc.network
|
||||
) else null,
|
||||
// TODO: separate layout for opening hours with expandable details
|
||||
if (loc.openinghours != null) DetailAdapter.Detail(
|
||||
R.drawable.ic_hours,
|
||||
R.string.hours,
|
||||
loc.openinghours.getStatusText(ctx),
|
||||
loc.openinghours.description
|
||||
) else null,
|
||||
if (loc.cost != null) DetailAdapter.Detail(
|
||||
R.drawable.ic_cost,
|
||||
R.string.cost,
|
||||
loc.cost.getStatusText(ctx),
|
||||
loc.cost.descriptionLong ?: loc.cost.descriptionShort
|
||||
)
|
||||
else null,
|
||||
DetailAdapter.Detail(
|
||||
R.drawable.ic_location,
|
||||
R.string.coordinates,
|
||||
loc.coordinates.formatDMS(),
|
||||
loc.coordinates.formatDecimal(),
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class FavoritesAdapter(val vm: FavoritesViewModel) :
|
||||
DataBindingAdapter<FavoritesViewModel.FavoritesListItem>() {
|
||||
@@ -146,160 +88,4 @@ class FavoritesAdapter(val vm: FavoritesViewModel) :
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_favorite
|
||||
|
||||
override fun getItemId(position: Int): Long = getItem(position).charger.id
|
||||
}
|
||||
|
||||
class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
val itemids = mutableMapOf<String, Long>()
|
||||
var maxId = 0L
|
||||
|
||||
override fun getItemViewType(position: Int): Int = when (getItem(position).filter) {
|
||||
is BooleanFilter -> R.layout.item_filter_boolean
|
||||
is MultipleChoiceFilter -> R.layout.item_filter_multiple_choice
|
||||
is SliderFilter -> R.layout.item_filter_slider
|
||||
}
|
||||
|
||||
override fun bind(
|
||||
holder: ViewHolder<FilterWithValue<FilterValue>>,
|
||||
item: FilterWithValue<FilterValue>
|
||||
) {
|
||||
super.bind(holder, item)
|
||||
when (item.value) {
|
||||
is SliderFilterValue -> {
|
||||
setupSlider(
|
||||
holder.binding as ItemFilterSliderBinding,
|
||||
item.filter as SliderFilter, item.value
|
||||
)
|
||||
}
|
||||
is MultipleChoiceFilterValue -> {
|
||||
setupMultipleChoice(
|
||||
holder.binding as ItemFilterMultipleChoiceBinding,
|
||||
item.filter as MultipleChoiceFilter, item.value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMultipleChoice(
|
||||
binding: ItemFilterMultipleChoiceBinding,
|
||||
filter: MultipleChoiceFilter,
|
||||
value: MultipleChoiceFilterValue
|
||||
) {
|
||||
val inflater = LayoutInflater.from(binding.root.context)
|
||||
value.values.toList().forEach {
|
||||
// delete values that cannot be selected anymore
|
||||
if (it !in filter.choices.keys) value.values.remove(it)
|
||||
}
|
||||
|
||||
fun updateButtons() {
|
||||
value.all = value.values == filter.choices.keys
|
||||
binding.btnAll.isEnabled = !value.all
|
||||
binding.btnNone.isEnabled = value.values.isNotEmpty()
|
||||
}
|
||||
|
||||
val chips = mutableMapOf<String, Chip>()
|
||||
binding.chipGroup.children.forEach {
|
||||
if (it.id != R.id.chipMore) binding.chipGroup.removeView(it)
|
||||
}
|
||||
filter.choices.entries.sortedByDescending {
|
||||
it.key in value.values
|
||||
}.sortedByDescending {
|
||||
if (filter.commonChoices != null) it.key in filter.commonChoices else false
|
||||
}.forEach { choice ->
|
||||
val chip = inflater.inflate(
|
||||
R.layout.item_filter_multiple_choice_chip,
|
||||
binding.chipGroup,
|
||||
false
|
||||
) as Chip
|
||||
chip.text = choice.value
|
||||
chip.isChecked = choice.key in value.values || value.all
|
||||
if (value.all && choice.key !in value.values) value.values.add(choice.key)
|
||||
|
||||
chip.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
value.values.add(choice.key)
|
||||
} else {
|
||||
value.values.remove(choice.key)
|
||||
}
|
||||
updateButtons()
|
||||
}
|
||||
|
||||
if (filter.commonChoices != null && choice.key !in filter.commonChoices
|
||||
&& !(chip.isChecked && !value.all) && !binding.showingAll
|
||||
) {
|
||||
chip.visibility = View.GONE
|
||||
} else {
|
||||
chip.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
binding.chipGroup.addView(chip, binding.chipGroup.childCount - 1)
|
||||
chips[choice.key] = chip
|
||||
}
|
||||
|
||||
binding.btnAll.setOnClickListener {
|
||||
value.all = true
|
||||
value.values.addAll(filter.choices.keys)
|
||||
chips.values.forEach { it.isChecked = true }
|
||||
updateButtons()
|
||||
}
|
||||
binding.btnNone.setOnClickListener {
|
||||
value.all = true
|
||||
value.values.addAll(filter.choices.keys)
|
||||
chips.values.forEach { it.isChecked = false }
|
||||
updateButtons()
|
||||
}
|
||||
binding.chipMore.setOnClickListener {
|
||||
binding.showingAll = !binding.showingAll
|
||||
chips.forEach { (key, chip) ->
|
||||
if (filter.commonChoices != null && key !in filter.commonChoices
|
||||
&& !(chip.isChecked && !value.all) && !binding.showingAll
|
||||
) {
|
||||
chip.visibility = View.GONE
|
||||
} else {
|
||||
chip.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
updateButtons()
|
||||
}
|
||||
|
||||
private fun setupSlider(
|
||||
binding: ItemFilterSliderBinding,
|
||||
filter: SliderFilter,
|
||||
value: SliderFilterValue
|
||||
) {
|
||||
binding.progress = filter.inverseMapping(value.value)
|
||||
binding.mappedValue = value.value
|
||||
|
||||
binding.addOnPropertyChangedCallback(object :
|
||||
Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
when (propertyId) {
|
||||
BR.progress -> {
|
||||
val mapped = filter.mapping(binding.progress)
|
||||
value.value = mapped
|
||||
binding.mappedValue = mapped
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
val key = getItem(position).filter.key
|
||||
var value = itemids[key]
|
||||
if (value == null) {
|
||||
maxId++
|
||||
value = maxId
|
||||
itemids[key] = maxId
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
class DonationAdapter() : DataBindingAdapter<DonationItem>() {
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_donation
|
||||
}
|
||||
139
app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt
Normal file
139
app/src/main/java/net/vonforst/evmap/adapter/DetailsAdapter.kt
Normal file
@@ -0,0 +1,139 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.text.HtmlCompat
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCardId
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.OpeningHoursDays
|
||||
import net.vonforst.evmap.bold
|
||||
import net.vonforst.evmap.joinToSpannedString
|
||||
import net.vonforst.evmap.plus
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
|
||||
class DetailsAdapter : DataBindingAdapter<DetailsAdapter.Detail>() {
|
||||
data class Detail(
|
||||
val icon: Int,
|
||||
val contentDescription: Int,
|
||||
val text: CharSequence,
|
||||
val detailText: CharSequence? = null,
|
||||
val links: Boolean = true,
|
||||
val clickable: Boolean = false,
|
||||
val hoursDays: OpeningHoursDays? = null
|
||||
) : Equatable
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val item = getItem(position)
|
||||
if (item.hoursDays != null) {
|
||||
return R.layout.item_detail_openinghours
|
||||
} else {
|
||||
return R.layout.item_detail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun buildDetails(
|
||||
loc: ChargeLocation?,
|
||||
chargeCards: Map<Long, ChargeCard>?,
|
||||
filteredChargeCards: Set<Long>?,
|
||||
ctx: Context
|
||||
): List<DetailsAdapter.Detail> {
|
||||
if (loc == null) return emptyList()
|
||||
|
||||
return listOfNotNull(
|
||||
DetailsAdapter.Detail(
|
||||
R.drawable.ic_address,
|
||||
R.string.address,
|
||||
loc.address.toString(),
|
||||
loc.locationDescription
|
||||
),
|
||||
if (loc.operator != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_operator,
|
||||
R.string.operator,
|
||||
loc.operator
|
||||
) else null,
|
||||
if (loc.network != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_network,
|
||||
R.string.network,
|
||||
loc.network
|
||||
) else null,
|
||||
if (loc.faultReport != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_fault_report,
|
||||
R.string.fault_report,
|
||||
loc.faultReport.created?.let {
|
||||
ctx.getString(
|
||||
R.string.fault_report_date,
|
||||
loc.faultReport.created
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
|
||||
)
|
||||
} ?: "",
|
||||
loc.faultReport.description?.let {
|
||||
HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
} ?: "",
|
||||
clickable = true
|
||||
) else null,
|
||||
if (loc.openinghours != null && !loc.openinghours.isEmpty) DetailsAdapter.Detail(
|
||||
R.drawable.ic_hours,
|
||||
R.string.hours,
|
||||
loc.openinghours.getStatusText(ctx),
|
||||
loc.openinghours.description,
|
||||
hoursDays = loc.openinghours.days
|
||||
) else null,
|
||||
if (loc.cost != null) DetailsAdapter.Detail(
|
||||
R.drawable.ic_cost,
|
||||
R.string.cost,
|
||||
loc.cost.getStatusText(ctx),
|
||||
loc.cost.descriptionLong ?: loc.cost.descriptionShort
|
||||
)
|
||||
else null,
|
||||
if (loc.chargecards != null && loc.chargecards.isNotEmpty()) DetailsAdapter.Detail(
|
||||
R.drawable.ic_payment,
|
||||
R.string.charge_cards,
|
||||
ctx.resources.getQuantityString(
|
||||
R.plurals.charge_cards_compatible_num,
|
||||
loc.chargecards.size, loc.chargecards.size
|
||||
),
|
||||
formatChargeCards(loc.chargecards, chargeCards, filteredChargeCards, ctx),
|
||||
clickable = true
|
||||
) else null,
|
||||
DetailsAdapter.Detail(
|
||||
R.drawable.ic_location,
|
||||
R.string.coordinates,
|
||||
loc.coordinates.formatDMS(),
|
||||
loc.coordinates.formatDecimal(),
|
||||
links = false,
|
||||
clickable = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun formatChargeCards(
|
||||
chargecards: List<ChargeCardId>,
|
||||
chargecardData: Map<Long, ChargeCard>?,
|
||||
filteredChargeCards: Set<Long>?,
|
||||
ctx: Context
|
||||
): CharSequence {
|
||||
if (chargecardData == null) return ""
|
||||
|
||||
val maxItems = 5
|
||||
var result = chargecards
|
||||
.sortedByDescending { filteredChargeCards?.contains(it.id) }
|
||||
.take(maxItems)
|
||||
.mapNotNull {
|
||||
val name = chargecardData[it.id]?.name ?: return@mapNotNull null
|
||||
if (filteredChargeCards?.contains(it.id) == true) {
|
||||
name.bold()
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}.joinToSpannedString()
|
||||
if (chargecards.size > maxItems) {
|
||||
result += " " + ctx.getString(R.string.and_n_others, chargecards.size - maxItems)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
227
app/src/main/java/net/vonforst/evmap/adapter/FiltersAdapter.kt
Normal file
227
app/src/main/java/net/vonforst/evmap/adapter/FiltersAdapter.kt
Normal file
@@ -0,0 +1,227 @@
|
||||
package net.vonforst.evmap.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.children
|
||||
import androidx.databinding.Observable
|
||||
import com.google.android.material.chip.Chip
|
||||
import net.vonforst.evmap.BR
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceBinding
|
||||
import net.vonforst.evmap.databinding.ItemFilterMultipleChoiceLargeBinding
|
||||
import net.vonforst.evmap.databinding.ItemFilterSliderBinding
|
||||
import net.vonforst.evmap.fragment.MultiSelectDialog
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
import kotlin.math.max
|
||||
|
||||
class FiltersAdapter : DataBindingAdapter<FilterWithValue<FilterValue>>() {
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
val itemids = mutableMapOf<String, Long>()
|
||||
var maxId = 0L
|
||||
|
||||
override fun getItemViewType(position: Int): Int =
|
||||
when (val filter = getItem(position).filter) {
|
||||
is BooleanFilter -> R.layout.item_filter_boolean
|
||||
is MultipleChoiceFilter -> {
|
||||
if (filter.manyChoices) {
|
||||
R.layout.item_filter_multiple_choice_large
|
||||
} else {
|
||||
R.layout.item_filter_multiple_choice
|
||||
}
|
||||
}
|
||||
is SliderFilter -> R.layout.item_filter_slider
|
||||
}
|
||||
|
||||
override fun bind(
|
||||
holder: ViewHolder<FilterWithValue<FilterValue>>,
|
||||
item: FilterWithValue<FilterValue>
|
||||
) {
|
||||
super.bind(holder, item)
|
||||
when (item.value) {
|
||||
is SliderFilterValue -> {
|
||||
setupSlider(
|
||||
holder.binding as ItemFilterSliderBinding,
|
||||
item.filter as SliderFilter, item.value
|
||||
)
|
||||
}
|
||||
is MultipleChoiceFilterValue -> {
|
||||
val filter = item.filter as MultipleChoiceFilter
|
||||
if (filter.manyChoices) {
|
||||
setupMultipleChoiceMany(
|
||||
holder.binding as ItemFilterMultipleChoiceLargeBinding,
|
||||
filter, item.value
|
||||
)
|
||||
} else {
|
||||
setupMultipleChoice(
|
||||
holder.binding as ItemFilterMultipleChoiceBinding,
|
||||
filter, item.value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupMultipleChoice(
|
||||
binding: ItemFilterMultipleChoiceBinding,
|
||||
filter: MultipleChoiceFilter,
|
||||
value: MultipleChoiceFilterValue
|
||||
) {
|
||||
// TODO: this implementation seems to be buggy
|
||||
val inflater = LayoutInflater.from(binding.root.context)
|
||||
value.values.toList().forEach {
|
||||
// delete values that cannot be selected anymore
|
||||
if (it !in filter.choices.keys) value.values.remove(it)
|
||||
}
|
||||
|
||||
fun updateButtons() {
|
||||
value.all = value.values == filter.choices.keys
|
||||
binding.btnAll.isEnabled = !value.all
|
||||
binding.btnNone.isEnabled = value.values.isNotEmpty()
|
||||
}
|
||||
|
||||
val chips = mutableMapOf<String, Chip>()
|
||||
|
||||
// reuse existing chips in layout
|
||||
val reuseChips = binding.chipGroup.children.filter {
|
||||
it.id != R.id.chipMore
|
||||
}.toMutableList()
|
||||
binding.chipGroup.children.forEach {
|
||||
if (it.id != R.id.chipMore) binding.chipGroup.removeView(it)
|
||||
}
|
||||
filter.choices.entries.sortedByDescending {
|
||||
it.key in value.values
|
||||
}.sortedByDescending {
|
||||
if (filter.commonChoices != null) it.key in filter.commonChoices else false
|
||||
}.forEach { choice ->
|
||||
var reused = false
|
||||
val chip = if (reuseChips.size > 0) {
|
||||
reused = true
|
||||
reuseChips.removeAt(0) as Chip
|
||||
} else {
|
||||
inflater.inflate(
|
||||
R.layout.item_filter_multiple_choice_chip,
|
||||
binding.chipGroup,
|
||||
false
|
||||
) as Chip
|
||||
}
|
||||
chip.text = choice.value
|
||||
chip.isChecked = choice.key in value.values || value.all
|
||||
if (value.all && choice.key !in value.values) value.values.add(choice.key)
|
||||
|
||||
chip.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
value.values.add(choice.key)
|
||||
} else {
|
||||
value.values.remove(choice.key)
|
||||
}
|
||||
updateButtons()
|
||||
}
|
||||
|
||||
if (filter.commonChoices != null && choice.key !in filter.commonChoices
|
||||
&& !(chip.isChecked && !value.all) && !binding.showingAll
|
||||
) {
|
||||
chip.visibility = View.GONE
|
||||
} else {
|
||||
chip.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (!reused) binding.chipGroup.addView(chip, binding.chipGroup.childCount - 1)
|
||||
chips[choice.key] = chip
|
||||
}
|
||||
// delete surplus reusable chips
|
||||
reuseChips.forEach {
|
||||
binding.chipGroup.removeView(it)
|
||||
}
|
||||
|
||||
binding.btnAll.setOnClickListener {
|
||||
value.all = true
|
||||
value.values.addAll(filter.choices.keys)
|
||||
chips.values.forEach { it.isChecked = true }
|
||||
updateButtons()
|
||||
}
|
||||
binding.btnNone.setOnClickListener {
|
||||
value.all = true
|
||||
value.values.addAll(filter.choices.keys)
|
||||
chips.values.forEach { it.isChecked = false }
|
||||
updateButtons()
|
||||
}
|
||||
binding.chipMore.setOnClickListener {
|
||||
binding.showingAll = !binding.showingAll
|
||||
chips.forEach { (key, chip) ->
|
||||
if (filter.commonChoices != null && key !in filter.commonChoices
|
||||
&& !(chip.isChecked && !value.all) && !binding.showingAll
|
||||
) {
|
||||
chip.visibility = View.GONE
|
||||
} else {
|
||||
chip.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
updateButtons()
|
||||
}
|
||||
|
||||
private fun setupMultipleChoiceMany(
|
||||
binding: ItemFilterMultipleChoiceLargeBinding,
|
||||
filter: MultipleChoiceFilter,
|
||||
value: MultipleChoiceFilterValue
|
||||
) {
|
||||
if (value.all) {
|
||||
value.values = filter.choices.keys.toMutableSet()
|
||||
binding.notifyPropertyChanged(BR.item)
|
||||
}
|
||||
|
||||
binding.btnEdit.setOnClickListener {
|
||||
val dialog =
|
||||
MultiSelectDialog.getInstance(
|
||||
filter.name,
|
||||
filter.choices,
|
||||
value.values,
|
||||
commonChoices = filter.commonChoices
|
||||
)
|
||||
dialog.okListener = { selected ->
|
||||
value.values = selected.toMutableSet()
|
||||
value.all = value.values == filter.choices.keys
|
||||
binding.item = binding.item
|
||||
}
|
||||
dialog.show((binding.root.context as AppCompatActivity).supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSlider(
|
||||
binding: ItemFilterSliderBinding,
|
||||
filter: SliderFilter,
|
||||
value: SliderFilterValue
|
||||
) {
|
||||
binding.progress =
|
||||
max(filter.inverseMapping(value.value) - filter.min, 0)
|
||||
binding.mappedValue = filter.mapping(binding.progress + filter.min)
|
||||
|
||||
binding.addOnPropertyChangedCallback(object :
|
||||
Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
when (propertyId) {
|
||||
BR.progress -> {
|
||||
val mapped = filter.mapping(binding.progress + filter.min)
|
||||
value.value = mapped
|
||||
binding.mappedValue = mapped
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
val key = getItem(position).filter.key
|
||||
var value = itemids[key]
|
||||
if (value == null) {
|
||||
maxId++
|
||||
value = maxId
|
||||
itemids[key] = maxId
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package net.vonforst.evmap.api.goingelectric
|
||||
|
||||
import com.squareup.moshi.*
|
||||
import java.lang.reflect.Type
|
||||
import java.time.Instant
|
||||
import java.time.LocalTime
|
||||
|
||||
|
||||
@@ -65,7 +66,8 @@ internal class ChargepointListItemJsonAdapter(val moshi: Moshi) :
|
||||
}
|
||||
|
||||
internal class JsonObjectOrFalseAdapter<T> private constructor(
|
||||
private val objectDelegate: JsonAdapter<T>?
|
||||
private val objectDelegate: JsonAdapter<T>,
|
||||
private val clazz: Class<*>
|
||||
) : JsonAdapter<T>() {
|
||||
|
||||
class Factory() : JsonAdapter.Factory {
|
||||
@@ -73,34 +75,40 @@ internal class JsonObjectOrFalseAdapter<T> private constructor(
|
||||
type: Type,
|
||||
annotations: Set<Annotation>?,
|
||||
moshi: Moshi
|
||||
): JsonAdapter<*>? {
|
||||
): JsonAdapter<Any>? {
|
||||
val clazz = Types.getRawType(type)
|
||||
return when (hasJsonObjectOrFalseAnnotation(
|
||||
annotations
|
||||
)) {
|
||||
false -> null
|
||||
true -> JsonObjectOrFalseAdapter(
|
||||
moshi.adapter(clazz)
|
||||
moshi.adapter(type), clazz
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun fromJson(reader: JsonReader) = when (reader.peek()) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun fromJson(reader: JsonReader): T? = when (reader.peek()) {
|
||||
JsonReader.Token.BOOLEAN -> when (reader.nextBoolean()) {
|
||||
false -> null // Response was false
|
||||
else ->
|
||||
throw IllegalStateException("Non-false boolean for @JsonObjectOrFalse field")
|
||||
else -> {
|
||||
if (this.clazz == FaultReport::class.java) {
|
||||
FaultReport(null, null) as T
|
||||
} else {
|
||||
throw IllegalStateException("Non-false boolean for @JsonObjectOrFalse field")
|
||||
}
|
||||
}
|
||||
}
|
||||
JsonReader.Token.BEGIN_OBJECT -> objectDelegate?.fromJson(reader)
|
||||
JsonReader.Token.STRING -> objectDelegate?.fromJson(reader)
|
||||
JsonReader.Token.NUMBER -> objectDelegate?.fromJson(reader)
|
||||
JsonReader.Token.BEGIN_OBJECT -> objectDelegate.fromJson(reader)
|
||||
JsonReader.Token.BEGIN_ARRAY -> objectDelegate.fromJson(reader)
|
||||
JsonReader.Token.STRING -> objectDelegate.fromJson(reader)
|
||||
JsonReader.Token.NUMBER -> objectDelegate.fromJson(reader)
|
||||
else ->
|
||||
throw IllegalStateException("Non-object-non-boolean value for @JsonObjectOrFalse field")
|
||||
}
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: T?) =
|
||||
objectDelegate?.toJson(writer, value) ?: Unit
|
||||
override fun toJson(writer: JsonWriter, value: T?) = objectDelegate.toJson(writer, value)
|
||||
}
|
||||
|
||||
private fun hasJsonObjectOrFalseAnnotation(annotations: Set<Annotation>?) =
|
||||
@@ -139,4 +147,14 @@ internal class HoursAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class InstantAdapter {
|
||||
@FromJson
|
||||
fun fromJson(value: Long?): Instant? = value?.let {
|
||||
Instant.ofEpochSecond(it)
|
||||
}
|
||||
|
||||
@ToJson
|
||||
fun toJson(value: Instant?): Long? = value?.epochSecond
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package net.vonforst.evmap.api.goingelectric
|
||||
import android.content.Context
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Call
|
||||
@@ -17,13 +18,19 @@ interface GoingElectricApi {
|
||||
suspend fun getChargepoints(
|
||||
@Query("sw_lat") swlat: Double, @Query("sw_lng") sw_lng: Double,
|
||||
@Query("ne_lat") ne_lat: Double, @Query("ne_lng") ne_lng: Double,
|
||||
@Query("clustering") clustering: Boolean,
|
||||
@Query("zoom") zoom: Float,
|
||||
@Query("cluster_distance") clusterDistance: Int,
|
||||
@Query("freecharging") freecharging: Boolean,
|
||||
@Query("freeparking") freeparking: Boolean,
|
||||
@Query("min_power") minPower: Int,
|
||||
@Query("plugs") plugs: String?
|
||||
@Query("clustering") clustering: Boolean = false,
|
||||
@Query("cluster_distance") clusterDistance: Int? = null,
|
||||
@Query("freecharging") freecharging: Boolean = false,
|
||||
@Query("freeparking") freeparking: Boolean = false,
|
||||
@Query("min_power") minPower: Int = 0,
|
||||
@Query("plugs") plugs: String? = null,
|
||||
@Query("chargecards") chargecards: String? = null,
|
||||
@Query("networks") networks: String? = null,
|
||||
@Query("startkey") startkey: Int? = null,
|
||||
@Query("open_twentyfourseven") open247: Boolean = false,
|
||||
@Query("barrierfree") barrierfree: Boolean = false,
|
||||
@Query("exclude_faults") excludeFaults: Boolean = false
|
||||
): Response<ChargepointList>
|
||||
|
||||
@GET("chargepoints/")
|
||||
@@ -36,7 +43,7 @@ interface GoingElectricApi {
|
||||
suspend fun getNetworks(): Response<StringList>
|
||||
|
||||
@GET("chargepoints/chargecardlist/")
|
||||
suspend fun getChargeCards(): Response<StringList>
|
||||
suspend fun getChargeCards(): Response<ChargeCardList>
|
||||
|
||||
companion object {
|
||||
private val cacheSize = 10L * 1024 * 1024; // 10MB
|
||||
@@ -54,7 +61,9 @@ interface GoingElectricApi {
|
||||
original = original.newBuilder().url(url).build()
|
||||
chain.proceed(original)
|
||||
}
|
||||
addNetworkInterceptor(StethoInterceptor())
|
||||
if (BuildConfig.DEBUG) {
|
||||
addNetworkInterceptor(StethoInterceptor())
|
||||
}
|
||||
if (context != null) {
|
||||
cache(Cache(context.getCacheDir(), cacheSize))
|
||||
}
|
||||
@@ -64,6 +73,7 @@ interface GoingElectricApi {
|
||||
.add(ChargepointListItemJsonAdapterFactory())
|
||||
.add(JsonObjectOrFalseAdapter.Factory())
|
||||
.add(HoursAdapter())
|
||||
.add(InstantAdapter())
|
||||
.build()
|
||||
|
||||
val retrofit = Retrofit.Builder()
|
||||
|
||||
@@ -12,8 +12,11 @@ import kotlinx.android.parcel.Parcelize
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import java.time.DayOfWeek
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
@@ -21,7 +24,8 @@ import kotlin.math.floor
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepointList(
|
||||
val status: String,
|
||||
val chargelocations: List<ChargepointListItem>
|
||||
val chargelocations: List<ChargepointListItem>,
|
||||
@JsonObjectOrFalse val startkey: Int?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@@ -30,6 +34,12 @@ data class StringList(
|
||||
val result: List<String>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargeCardList(
|
||||
val status: String,
|
||||
val result: List<ChargeCard>
|
||||
)
|
||||
|
||||
sealed class ChargepointListItem
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@@ -42,7 +52,7 @@ data class ChargeLocation(
|
||||
val chargepoints: List<Chargepoint>,
|
||||
@JsonObjectOrFalse val network: String?,
|
||||
val url: String,
|
||||
// @Json(name = "fault_report") val faultReport: Boolean, <- Object or false in detail, true or false in overview
|
||||
@Embedded(prefix = "fault_report_") @JsonObjectOrFalse @Json(name = "fault_report") val faultReport: FaultReport?,
|
||||
val verified: Boolean,
|
||||
// only shown in details:
|
||||
@JsonObjectOrFalse val operator: String?,
|
||||
@@ -50,15 +60,26 @@ data class ChargeLocation(
|
||||
@JsonObjectOrFalse @Json(name = "ladeweile") val amenities: String?,
|
||||
@JsonObjectOrFalse @Json(name = "location_description") val locationDescription: String?,
|
||||
val photos: List<ChargerPhoto>?,
|
||||
//val chargecards: Boolean?
|
||||
@JsonObjectOrFalse val chargecards: List<ChargeCardId>?,
|
||||
@Embedded val openinghours: OpeningHours?,
|
||||
@Embedded val cost: Cost?
|
||||
) : ChargepointListItem(), Equatable {
|
||||
/**
|
||||
* maximum power available from this charger.
|
||||
*/
|
||||
val maxPower: Double
|
||||
get() {
|
||||
return chargepoints.map { it.power }.max() ?: 0.0
|
||||
return maxPower()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum power available from certain connectors of this charger.
|
||||
*/
|
||||
fun maxPower(connectors: Set<String>? = null): Double {
|
||||
return chargepoints.filter { connectors?.contains(it.type) ?: true }
|
||||
.map { it.power }.max() ?: 0.0
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges chargepoints if they have the same plug and power
|
||||
*
|
||||
@@ -107,6 +128,10 @@ data class OpeningHours(
|
||||
@JsonObjectOrFalse val description: String?,
|
||||
@Embedded val days: OpeningHoursDays?
|
||||
) {
|
||||
val isEmpty: Boolean
|
||||
get() = description == "Leider noch keine Informationen zu Öffnungszeiten vorhanden."
|
||||
&& days == null && !twentyfourSeven
|
||||
|
||||
fun getStatusText(ctx: Context): CharSequence {
|
||||
if (twentyfourSeven) {
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.open_247), 0)
|
||||
@@ -134,8 +159,6 @@ data class OpeningHours(
|
||||
), 0
|
||||
)
|
||||
}
|
||||
} else if (description != null) {
|
||||
return description
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
@@ -155,9 +178,12 @@ data class OpeningHoursDays(
|
||||
) {
|
||||
fun getHoursForDate(date: LocalDate): Hours {
|
||||
// TODO: check for holidays
|
||||
return getHoursForDayOfWeek(date.dayOfWeek)
|
||||
}
|
||||
|
||||
fun getHoursForDayOfWeek(dayOfWeek: DayOfWeek?): Hours {
|
||||
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
|
||||
return when (date.dayOfWeek) {
|
||||
return when (dayOfWeek) {
|
||||
DayOfWeek.MONDAY -> monday
|
||||
DayOfWeek.TUESDAY -> tuesday
|
||||
DayOfWeek.WEDNESDAY -> wednesday
|
||||
@@ -165,6 +191,7 @@ data class OpeningHoursDays(
|
||||
DayOfWeek.FRIDAY -> friday
|
||||
DayOfWeek.SATURDAY -> saturday
|
||||
DayOfWeek.SUNDAY -> sunday
|
||||
null -> holiday
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,7 +199,16 @@ data class OpeningHoursDays(
|
||||
data class Hours(
|
||||
val start: LocalTime?,
|
||||
val end: LocalTime?
|
||||
)
|
||||
) {
|
||||
override fun toString(): String {
|
||||
if (start != null && end != null) {
|
||||
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
return "${start.format(fmt)} - ${end.format(fmt)}"
|
||||
} else {
|
||||
return "closed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Parcelize
|
||||
@@ -242,4 +278,20 @@ data class Chargepoint(val type: String, val power: Double, val count: Int) : Eq
|
||||
const val CEE_BLAU = "CEE Blau"
|
||||
const val CEE_ROT = "CEE Rot"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FaultReport(val created: Instant?, val description: String?)
|
||||
|
||||
@Entity
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargeCard(
|
||||
@Json(name = "card_id") @PrimaryKey val id: Long,
|
||||
val name: String,
|
||||
val url: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargeCardId(
|
||||
val id: Long
|
||||
)
|
||||
@@ -41,6 +41,10 @@ class AboutFragment : PreferenceFragmentCompat() {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.privacy_link))
|
||||
true
|
||||
}
|
||||
"faq" -> {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.faq_link))
|
||||
true
|
||||
}
|
||||
"oss_licenses" -> {
|
||||
LibsBuilder()
|
||||
.withLicenseShown(true)
|
||||
@@ -55,6 +59,14 @@ class AboutFragment : PreferenceFragmentCompat() {
|
||||
findNavController().navigate(R.id.action_about_to_donateFragment)
|
||||
true
|
||||
}
|
||||
"twitter" -> {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.twitter_url))
|
||||
true
|
||||
}
|
||||
"goingelectric" -> {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.goingelectric_forum_url))
|
||||
true
|
||||
}
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package net.vonforst.evmap.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -16,9 +15,9 @@ import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.gms.location.FusedLocationProviderClient
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.mapzen.android.lost.api.LocationServices
|
||||
import com.mapzen.android.lost.api.LostApiClient
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.FavoritesAdapter
|
||||
@@ -26,9 +25,9 @@ import net.vonforst.evmap.databinding.FragmentFavoritesBinding
|
||||
import net.vonforst.evmap.viewmodel.FavoritesViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
|
||||
class FavoritesFragment : Fragment() {
|
||||
class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
private lateinit var binding: FragmentFavoritesBinding
|
||||
private lateinit var fusedLocationClient: FusedLocationProviderClient
|
||||
private lateinit var locationClient: LostApiClient
|
||||
|
||||
private val vm: FavoritesViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory {
|
||||
@@ -51,7 +50,8 @@ class FavoritesFragment : Fragment() {
|
||||
binding.lifecycleOwner = this
|
||||
binding.vm = vm
|
||||
|
||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext())
|
||||
locationClient = LostApiClient.Builder(requireContext())
|
||||
.addConnectionCallbacks(this).build()
|
||||
|
||||
return binding.root
|
||||
}
|
||||
@@ -82,16 +82,23 @@ class FavoritesFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
|
||||
locationClient.connect()
|
||||
}
|
||||
|
||||
override fun onConnected() {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
fusedLocationClient.lastLocation.addOnSuccessListener { location: Location? ->
|
||||
if (location != null) {
|
||||
vm.location.value = LatLng(location.latitude, location.longitude)
|
||||
}
|
||||
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient)
|
||||
if (location != null) {
|
||||
vm.location.value = LatLng(location.latitude, location.longitude)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnectionSuspended() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package net.vonforst.evmap.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.databinding.DataBindingUtil
|
||||
@@ -18,8 +17,6 @@ import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.FiltersAdapter
|
||||
import net.vonforst.evmap.databinding.FragmentFilterBinding
|
||||
import net.vonforst.evmap.ui.exitCircularReveal
|
||||
import net.vonforst.evmap.ui.startCircularReveal
|
||||
import net.vonforst.evmap.viewmodel.FilterViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
|
||||
@@ -44,12 +41,6 @@ class FilterFragment : Fragment() {
|
||||
binding.vm = vm
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object :
|
||||
OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
exitAfterTransition()
|
||||
}
|
||||
})
|
||||
|
||||
return binding.root
|
||||
}
|
||||
@@ -75,10 +66,8 @@ class FilterFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
|
||||
view.startCircularReveal()
|
||||
|
||||
toolbar.setNavigationOnClickListener {
|
||||
exitAfterTransition()
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,17 +81,11 @@ class FilterFragment : Fragment() {
|
||||
R.id.menu_apply -> {
|
||||
lifecycleScope.launch {
|
||||
vm.saveFilterValues()
|
||||
exitAfterTransition()
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun exitAfterTransition() {
|
||||
view?.exitCircularReveal {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.Manifest.permission.ACCESS_FINE_LOCATION
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.view.*
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.RequiresPermission
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.SharedElementCallback
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
@@ -29,31 +32,37 @@ import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.google.android.gms.location.FusedLocationProviderClient
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import com.google.android.gms.maps.CameraUpdateFactory
|
||||
import com.google.android.gms.maps.GoogleMap
|
||||
import com.google.android.gms.maps.OnMapReadyCallback
|
||||
import com.google.android.gms.maps.SupportMapFragment
|
||||
import com.google.android.gms.maps.model.*
|
||||
import com.google.android.libraries.places.api.model.Place
|
||||
import com.google.android.libraries.places.widget.Autocomplete
|
||||
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode
|
||||
import androidx.transition.TransitionManager
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.MapFragment
|
||||
import com.car2go.maps.OnMapReadyCallback
|
||||
import com.car2go.maps.model.BitmapDescriptor
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.Marker
|
||||
import com.car2go.maps.model.MarkerOptions
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.transition.MaterialArcMotion
|
||||
import com.google.android.material.transition.MaterialContainerTransform
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
|
||||
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
|
||||
import com.mapzen.android.lost.api.LocationServices
|
||||
import com.mapzen.android.lost.api.LostApiClient
|
||||
import io.michaelrocks.bimap.HashBiMap
|
||||
import io.michaelrocks.bimap.MutableBiMap
|
||||
import kotlinx.android.synthetic.main.fragment_map.*
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.adapter.ConnectorAdapter
|
||||
import net.vonforst.evmap.adapter.DetailAdapter
|
||||
import net.vonforst.evmap.adapter.DetailsAdapter
|
||||
import net.vonforst.evmap.adapter.GalleryAdapter
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocationCluster
|
||||
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
|
||||
import net.vonforst.evmap.autocomplete.handleAutocompleteResult
|
||||
import net.vonforst.evmap.autocomplete.launchAutocomplete
|
||||
import net.vonforst.evmap.databinding.FragmentMapBinding
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||
import net.vonforst.evmap.ui.ClusterIconGenerator
|
||||
import net.vonforst.evmap.ui.MarkerAnimator
|
||||
@@ -65,7 +74,8 @@ const val ARG_CHARGER_ID = "chargerId"
|
||||
const val ARG_LAT = "lat"
|
||||
const val ARG_LON = "lon"
|
||||
|
||||
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback {
|
||||
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback,
|
||||
LostApiClient.ConnectionCallbacks {
|
||||
private lateinit var binding: FragmentMapBinding
|
||||
private val vm: MapViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory {
|
||||
@@ -76,12 +86,17 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
})
|
||||
private val galleryVm: GalleryViewModel by activityViewModels()
|
||||
private var map: GoogleMap? = null
|
||||
private lateinit var fusedLocationClient: FusedLocationProviderClient
|
||||
private lateinit var mapFragment: MapFragment
|
||||
private var map: AnyMap? = null
|
||||
private lateinit var locationClient: LostApiClient
|
||||
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
|
||||
private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior
|
||||
private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap()
|
||||
private var clusterMarkers: List<Marker> = emptyList()
|
||||
private var searchResultMarker: Marker? = null
|
||||
private var searchResultIcon: BitmapDescriptor? = null
|
||||
private var connectionErrorSnackbar: Snackbar? = null
|
||||
private var previousChargepointIds: Set<Long>? = null
|
||||
|
||||
private lateinit var clusterIconGenerator: ClusterIconGenerator
|
||||
private lateinit var chargerIconGenerator: ChargerIconGenerator
|
||||
@@ -89,11 +104,19 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
private lateinit var favToggle: MenuItem
|
||||
private val backPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
val value = vm.layersMenuOpen.value
|
||||
if (value != null && value) {
|
||||
closeLayersMenu()
|
||||
return
|
||||
}
|
||||
|
||||
val state = bottomSheetBehavior.state
|
||||
if (state != STATE_COLLAPSED && state != STATE_HIDDEN) {
|
||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||
} else if (state == STATE_COLLAPSED) {
|
||||
vm.chargerSparse.value = null
|
||||
} else if (state == STATE_HIDDEN) {
|
||||
vm.searchResult.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,10 +130,37 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.lifecycleOwner = this
|
||||
binding.vm = vm
|
||||
|
||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext())
|
||||
mapFragment = MapFragment()
|
||||
val provider = PreferenceDataSource(requireContext()).mapProvider
|
||||
mapFragment.setPriority(
|
||||
arrayOf(
|
||||
when (provider) {
|
||||
"mapbox" -> MapFragment.MAPBOX
|
||||
"google" -> MapFragment.GOOGLE
|
||||
else -> null
|
||||
},
|
||||
MapFragment.GOOGLE,
|
||||
MapFragment.MAPBOX
|
||||
)
|
||||
)
|
||||
requireActivity().supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.map, mapFragment)
|
||||
.commit()
|
||||
|
||||
// reset map-related stuff (map provider may have changed)
|
||||
map = null
|
||||
markers.clear()
|
||||
clusterMarkers = emptyList()
|
||||
searchResultMarker = null
|
||||
searchResultIcon = null
|
||||
|
||||
|
||||
locationClient = LostApiClient.Builder(requireContext())
|
||||
.addConnectionCallbacks(this)
|
||||
.build()
|
||||
locationClient.connect()
|
||||
clusterIconGenerator = ClusterIconGenerator(requireContext())
|
||||
chargerIconGenerator = ChargerIconGenerator(requireContext())
|
||||
animator = MarkerAnimator(chargerIconGenerator)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
postponeEnterTransition()
|
||||
@@ -119,6 +169,18 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.detailAppBar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.systemWindowInsetTop
|
||||
}
|
||||
|
||||
// margin of layers button
|
||||
val density = resources.displayMetrics.density
|
||||
// status bar height + toolbar height + margin
|
||||
val margin =
|
||||
insets.systemWindowInsetTop + (48 * density).toInt() + (24 * density).toInt()
|
||||
binding.fabLayers.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = margin
|
||||
}
|
||||
binding.layersSheet.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = margin
|
||||
}
|
||||
insets
|
||||
}
|
||||
|
||||
@@ -135,7 +197,6 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
|
||||
mapFragment.getMapAsync(this)
|
||||
bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet)
|
||||
detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
|
||||
@@ -163,13 +224,17 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
private fun setupClickListeners() {
|
||||
binding.fabLocate.setOnClickListener {
|
||||
if (!hasLocationPermission()) {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
ACCESS_FINE_LOCATION
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestPermissions(
|
||||
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
|
||||
arrayOf(ACCESS_FINE_LOCATION),
|
||||
REQUEST_LOCATION_PERMISSION
|
||||
)
|
||||
} else {
|
||||
enableLocation(true, true)
|
||||
enableLocation(moveTo = true, animate = true)
|
||||
}
|
||||
}
|
||||
binding.fabDirections.setOnClickListener {
|
||||
@@ -180,27 +245,25 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.fabLayers.setOnClickListener {
|
||||
openLayersMenu()
|
||||
}
|
||||
binding.detailView.goingelectricButton.setOnClickListener {
|
||||
val charger = vm.charger.value?.data
|
||||
if (charger != null) {
|
||||
(activity as? MapsActivity)?.openUrl("https:${charger.url}")
|
||||
}
|
||||
}
|
||||
binding.detailView.btnChargeprice.setOnClickListener {
|
||||
val charger = vm.charger.value?.data ?: return@setOnClickListener
|
||||
(activity as? MapsActivity)?.openUrl(
|
||||
"https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=going_electric")
|
||||
}
|
||||
binding.detailView.topPart.setOnClickListener {
|
||||
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
|
||||
}
|
||||
binding.search.setOnClickListener {
|
||||
val fields = listOf(Place.Field.LAT_LNG)
|
||||
val intent: Intent = Autocomplete.IntentBuilder(
|
||||
AutocompleteActivityMode.OVERLAY, fields
|
||||
)
|
||||
.build(requireContext())
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||
startActivityForResult(intent, REQUEST_AUTOCOMPLETE)
|
||||
|
||||
val imm =
|
||||
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.toggleSoftInput(0, 0)
|
||||
launchAutocomplete(this)
|
||||
}
|
||||
binding.detailAppBar.toolbar.setNavigationOnClickListener {
|
||||
bottomSheetBehavior.state = STATE_COLLAPSED
|
||||
@@ -223,6 +286,32 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
|
||||
private fun openLayersMenu() {
|
||||
binding.fabLayers.tag = false
|
||||
val materialTransform = MaterialContainerTransform().apply {
|
||||
startView = binding.fabLayers
|
||||
endView = binding.layersSheet
|
||||
setPathMotion(MaterialArcMotion())
|
||||
duration = 250
|
||||
scrimColor = Color.TRANSPARENT
|
||||
}
|
||||
TransitionManager.beginDelayedTransition(binding.root, materialTransform)
|
||||
vm.layersMenuOpen.value = true
|
||||
}
|
||||
|
||||
private fun closeLayersMenu() {
|
||||
binding.fabLayers.tag = true
|
||||
val materialTransform = MaterialContainerTransform().apply {
|
||||
startView = binding.layersSheet
|
||||
endView = binding.fabLayers
|
||||
setPathMotion(MaterialArcMotion())
|
||||
duration = 200
|
||||
scrimColor = Color.TRANSPARENT
|
||||
}
|
||||
TransitionManager.beginDelayedTransition(binding.root, materialTransform)
|
||||
vm.layersMenuOpen.value = false
|
||||
}
|
||||
|
||||
private fun toggleFavorite() {
|
||||
val favs = vm.favorites.value ?: return
|
||||
val charger = vm.chargerSparse.value ?: return
|
||||
@@ -242,7 +331,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
vm.bottomSheetState.value = newState
|
||||
backPressedCallback.isEnabled = newState != STATE_HIDDEN
|
||||
updateBackPressedCallback()
|
||||
|
||||
if (vm.layersMenuOpen.value!! && newState !in listOf(BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING, BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN, BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED)) {
|
||||
closeLayersMenu()
|
||||
}
|
||||
}
|
||||
})
|
||||
vm.chargerSparse.observe(viewLifecycleOwner, Observer {
|
||||
@@ -259,20 +352,91 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
unhighlightAllMarkers()
|
||||
}
|
||||
})
|
||||
vm.chargepoints.observe(viewLifecycleOwner, Observer {
|
||||
val chargepoints = it.data
|
||||
if (chargepoints != null) updateMap(chargepoints)
|
||||
vm.chargepoints.observe(viewLifecycleOwner, Observer { res ->
|
||||
when (res.status) {
|
||||
Status.ERROR -> {
|
||||
val view = view ?: return@Observer
|
||||
|
||||
connectionErrorSnackbar?.dismiss()
|
||||
connectionErrorSnackbar = Snackbar
|
||||
.make(view, R.string.connection_error, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAction(R.string.retry) {
|
||||
connectionErrorSnackbar?.dismiss()
|
||||
vm.reloadChargepoints()
|
||||
}
|
||||
connectionErrorSnackbar!!.show()
|
||||
}
|
||||
Status.SUCCESS -> {
|
||||
connectionErrorSnackbar?.dismiss()
|
||||
}
|
||||
Status.LOADING -> {
|
||||
}
|
||||
}
|
||||
|
||||
val chargepoints = res.data
|
||||
if (chargepoints != null) {
|
||||
updateMap(chargepoints)
|
||||
}
|
||||
})
|
||||
vm.favorites.observe(viewLifecycleOwner, Observer {
|
||||
updateFavoriteToggle()
|
||||
})
|
||||
vm.searchResult.observe(viewLifecycleOwner, Observer { place ->
|
||||
val map = this.map ?: return@Observer
|
||||
searchResultMarker?.remove()
|
||||
searchResultMarker = null
|
||||
|
||||
if (place != null) {
|
||||
if (place.viewport != null) {
|
||||
map.animateCamera(map.cameraUpdateFactory.newLatLngBounds(place.viewport, 0))
|
||||
} else {
|
||||
map.animateCamera(map.cameraUpdateFactory.newLatLngZoom(place.latLng, 12f))
|
||||
}
|
||||
|
||||
if (searchResultIcon == null) {
|
||||
searchResultIcon =
|
||||
map.bitmapDescriptorFactory.fromResource(R.drawable.ic_map_marker)
|
||||
}
|
||||
searchResultMarker = map.addMarker(
|
||||
MarkerOptions()
|
||||
.position(place.latLng)
|
||||
.icon(searchResultIcon)
|
||||
.anchor(0.5f, 1f)
|
||||
)
|
||||
}
|
||||
|
||||
updateBackPressedCallback()
|
||||
})
|
||||
vm.layersMenuOpen.observe(viewLifecycleOwner, Observer { open ->
|
||||
binding.fabLayers.visibility = if (open) View.GONE else View.VISIBLE
|
||||
binding.layersSheet.visibility = if (open) View.VISIBLE else View.GONE
|
||||
updateBackPressedCallback()
|
||||
})
|
||||
vm.mapType.observe(viewLifecycleOwner, Observer {
|
||||
map?.setMapType(it)
|
||||
})
|
||||
vm.mapTrafficEnabled.observe(viewLifecycleOwner, Observer {
|
||||
map?.setTrafficEnabled(it)
|
||||
})
|
||||
|
||||
updateBackPressedCallback()
|
||||
}
|
||||
|
||||
private fun updateBackPressedCallback() {
|
||||
backPressedCallback.isEnabled =
|
||||
vm.bottomSheetState.value != null && vm.bottomSheetState.value != STATE_HIDDEN
|
||||
|| vm.searchResult.value != null
|
||||
|| (vm.layersMenuOpen.value ?: false)
|
||||
}
|
||||
|
||||
private fun unhighlightAllMarkers() {
|
||||
markers.forEach { (m, c) ->
|
||||
m.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(c)
|
||||
getMarkerTint(c, vm.filteredConnectors.value),
|
||||
highlight = false,
|
||||
fault = c.faultReport != null,
|
||||
multi = getMarkerMulti(c, vm.filteredConnectors.value)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -283,7 +447,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
// highlight this marker
|
||||
marker.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(charger), highlight = true
|
||||
getMarkerTint(charger, vm.filteredConnectors.value),
|
||||
highlight = true,
|
||||
fault = charger.faultReport != null,
|
||||
multi = getMarkerMulti(charger, vm.filteredConnectors.value)
|
||||
)
|
||||
)
|
||||
animator.animateMarkerBounce(marker)
|
||||
@@ -293,13 +460,31 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
if (m != marker) {
|
||||
m.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(c)
|
||||
getMarkerTint(c, vm.filteredConnectors.value),
|
||||
highlight = false,
|
||||
fault = c.faultReport != null,
|
||||
multi = getMarkerMulti(c, vm.filteredConnectors.value)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMarkerMulti(charger: ChargeLocation, filteredConnectors: Set<String>?): Boolean {
|
||||
var chargepoints = charger.chargepointsMerged
|
||||
.filter { filteredConnectors?.contains(it.type) ?: true }
|
||||
if (charger.maxPower(filteredConnectors) >= 43) {
|
||||
// fast charger -> only count fast chargers
|
||||
chargepoints = chargepoints.filter { it.power >= 43 }
|
||||
}
|
||||
val connectors = chargepoints.map { it.type }.distinct().toSet()
|
||||
|
||||
// check if there is more than one plug for any connector type
|
||||
val chargepointsPerConnector =
|
||||
connectors.map { conn -> chargepoints.filter { it.type == conn }.sumBy { it.count } }
|
||||
return chargepointsPerConnector.any { it > 1 }
|
||||
}
|
||||
|
||||
private fun updateFavoriteToggle() {
|
||||
val favs = vm.favorites.value ?: return
|
||||
val charger = vm.chargerSparse.value ?: return
|
||||
@@ -357,7 +542,24 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
|
||||
binding.detailView.details.apply {
|
||||
adapter = DetailAdapter()
|
||||
adapter = DetailsAdapter().apply {
|
||||
onClickListener = {
|
||||
val charger = vm.chargerDetails.value?.data
|
||||
if (charger != null) {
|
||||
when (it.icon) {
|
||||
R.drawable.ic_location -> {
|
||||
(activity as? MapsActivity)?.showLocation(charger)
|
||||
}
|
||||
R.drawable.ic_fault_report -> {
|
||||
(activity as? MapsActivity)?.openUrl("https:${charger.url}")
|
||||
}
|
||||
R.drawable.ic_payment -> {
|
||||
showPaymentMethodsDialog(charger)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
itemAnimator = null
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
@@ -370,9 +572,37 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMapReady(map: GoogleMap) {
|
||||
private fun showPaymentMethodsDialog(charger: ChargeLocation) {
|
||||
val activity = activity ?: return
|
||||
val chargecardData = vm.chargeCardMap.value ?: return
|
||||
val chargecards = charger.chargecards ?: return
|
||||
val filteredChargeCards = vm.filteredChargeCards.value
|
||||
|
||||
val data = chargecards.mapNotNull { chargecardData[it.id] }
|
||||
.sortedBy { it.name }
|
||||
.sortedByDescending { filteredChargeCards?.contains(it.id) }
|
||||
val names = data.map {
|
||||
if (filteredChargeCards?.contains(it.id) == true) {
|
||||
it.name.bold()
|
||||
} else {
|
||||
it.name
|
||||
}
|
||||
}
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.charge_cards)
|
||||
.setItems(names.toTypedArray()) { _, i ->
|
||||
val card = data[i]
|
||||
(activity as? MapsActivity)?.openUrl("https:${card.url}")
|
||||
}.show()
|
||||
}
|
||||
|
||||
override fun onMapReady(map: AnyMap) {
|
||||
this.map = map
|
||||
map.uiSettings.isTiltGesturesEnabled = false;
|
||||
chargerIconGenerator = ChargerIconGenerator(requireContext(), map.bitmapDescriptorFactory)
|
||||
animator = MarkerAnimator(chargerIconGenerator)
|
||||
map.uiSettings.setTiltGesturesEnabled(false)
|
||||
map.setIndoorEnabled(false)
|
||||
map.uiSettings.setIndoorLevelPickerEnabled(false)
|
||||
map.setOnCameraIdleListener {
|
||||
vm.mapPosition.value = MapPosition(
|
||||
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
|
||||
@@ -386,7 +616,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
in clusterMarkers -> {
|
||||
val newZoom = map.cameraPosition.zoom + 2
|
||||
map.animateCamera(CameraUpdateFactory.newLatLngZoom(marker.position, newZoom))
|
||||
map.animateCamera(
|
||||
map.cameraUpdateFactory.newLatLngZoom(
|
||||
marker.position,
|
||||
newZoom
|
||||
)
|
||||
)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
@@ -394,7 +629,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
}
|
||||
map.setOnMapClickListener {
|
||||
vm.chargerSparse.value = null
|
||||
if (backPressedCallback.isEnabled) {
|
||||
backPressedCallback.handleOnBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
// set padding so that compass is not obstructed by toolbar
|
||||
@@ -402,9 +639,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
map.setMapStyle(
|
||||
if (mode == Configuration.UI_MODE_NIGHT_YES) {
|
||||
MapStyleOptions.loadRawResourceStyle(context, R.raw.maps_night_mode)
|
||||
} else null
|
||||
if (mode == Configuration.UI_MODE_NIGHT_YES) AnyMap.Style.DARK else AnyMap.Style.NORMAL
|
||||
)
|
||||
|
||||
|
||||
@@ -415,12 +650,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
if (position != null) {
|
||||
val cameraUpdate =
|
||||
CameraUpdateFactory.newLatLngZoom(position.bounds.center, position.zoom)
|
||||
map.cameraUpdateFactory.newLatLngZoom(position.bounds.center, position.zoom)
|
||||
map.moveCamera(cameraUpdate)
|
||||
positionSet = true
|
||||
} else if (lat != null && lon != null) {
|
||||
// show given position
|
||||
val cameraUpdate = CameraUpdateFactory.newLatLngZoom(LatLng(lat, lon), 16f)
|
||||
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(LatLng(lat, lon), 16f)
|
||||
map.moveCamera(cameraUpdate)
|
||||
|
||||
// show charger detail after chargers were loaded
|
||||
@@ -441,13 +676,18 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
|
||||
positionSet = true
|
||||
}
|
||||
if (hasLocationPermission()) {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
enableLocation(!positionSet, false)
|
||||
positionSet = true
|
||||
}
|
||||
if (!positionSet) {
|
||||
// center the camera on Europe
|
||||
val cameraUpdate = CameraUpdateFactory.newLatLngZoom(LatLng(50.113388, 9.252536), 3.5f)
|
||||
val cameraUpdate =
|
||||
map.cameraUpdateFactory.newLatLngZoom(LatLng(50.113388, 9.252536), 3.5f)
|
||||
map.moveCamera(cameraUpdate)
|
||||
}
|
||||
|
||||
@@ -456,35 +696,32 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@RequiresPermission(ACCESS_FINE_LOCATION)
|
||||
private fun enableLocation(moveTo: Boolean, animate: Boolean) {
|
||||
val map = this.map ?: return
|
||||
map.isMyLocationEnabled = true
|
||||
map.setMyLocationEnabled(true)
|
||||
vm.myLocationEnabled.value = true
|
||||
map.uiSettings.isMyLocationButtonEnabled = false
|
||||
if (moveTo) {
|
||||
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
|
||||
if (location != null) {
|
||||
val latLng = LatLng(location.latitude, location.longitude)
|
||||
val camUpdate = CameraUpdateFactory.newLatLngZoom(latLng, 13f)
|
||||
if (animate) {
|
||||
map.animateCamera(camUpdate)
|
||||
} else {
|
||||
map.moveCamera(camUpdate)
|
||||
}
|
||||
}
|
||||
map.uiSettings.setMyLocationButtonEnabled(false)
|
||||
if (moveTo && locationClient.isConnected) {
|
||||
moveToCurrentLocation(map, animate)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(ACCESS_FINE_LOCATION)
|
||||
private fun moveToCurrentLocation(map: AnyMap, animate: Boolean) {
|
||||
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient)
|
||||
if (location != null) {
|
||||
val latLng = LatLng(location.latitude, location.longitude)
|
||||
val camUpdate = map.cameraUpdateFactory.newLatLngZoom(latLng, 13f)
|
||||
if (animate) {
|
||||
map.animateCamera(camUpdate)
|
||||
} else {
|
||||
map.moveCamera(camUpdate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasLocationPermission(): Boolean {
|
||||
val context = context ?: return false
|
||||
return ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun updateMap(chargepoints: List<ChargepointListItem>) {
|
||||
val map = this.map ?: return
|
||||
clusterMarkers.forEach { it.remove() }
|
||||
@@ -493,44 +730,85 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val chargers = chargepoints.filterIsInstance<ChargeLocation>()
|
||||
|
||||
val chargepointIds = chargers.map { it.id }.toSet()
|
||||
// remove markers that disappeared
|
||||
markers.entries.toList().forEach {
|
||||
if (!chargepointIds.contains(it.value.id)) {
|
||||
if (it.key.isVisible) {
|
||||
val tint = getMarkerTint(it.value)
|
||||
val highlight = it.value == vm.chargerSparse.value
|
||||
animator.animateMarkerDisappear(it.key, tint, highlight)
|
||||
} else {
|
||||
it.key.remove()
|
||||
}
|
||||
markers.remove(it.key)
|
||||
}
|
||||
}
|
||||
// add new markers
|
||||
chargers.filter {
|
||||
!markers.containsValue(it)
|
||||
}.forEach { charger ->
|
||||
val tint = getMarkerTint(charger)
|
||||
val highlight = charger == vm.chargerSparse.value
|
||||
val marker = map.addMarker(
|
||||
MarkerOptions()
|
||||
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
|
||||
.icon(
|
||||
chargerIconGenerator.getBitmapDescriptor(tint, highlight = highlight)
|
||||
)
|
||||
|
||||
// update icons of existing markers (connector filter may have changed)
|
||||
for ((marker, charger) in markers) {
|
||||
marker.setIcon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
getMarkerTint(charger, vm.filteredConnectors.value),
|
||||
highlight = charger == vm.chargerSparse.value,
|
||||
fault = charger.faultReport != null,
|
||||
multi = getMarkerMulti(charger, vm.filteredConnectors.value)
|
||||
)
|
||||
)
|
||||
animator.animateMarkerAppear(marker, tint, highlight)
|
||||
markers[marker] = charger
|
||||
}
|
||||
|
||||
if (chargers.toSet() != markers.values) {
|
||||
// remove markers that disappeared
|
||||
val bounds = map.projection.visibleRegion.latLngBounds
|
||||
markers.entries.toList().forEach {
|
||||
val marker = it.key
|
||||
val charger = it.value
|
||||
if (!chargepointIds.contains(charger.id)) {
|
||||
// animate marker if it is visible, otherwise remove immediately
|
||||
if (bounds.contains(marker.position)) {
|
||||
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
|
||||
val highlight = charger == vm.chargerSparse.value
|
||||
val fault = charger.faultReport != null
|
||||
val multi = getMarkerMulti(charger, vm.filteredConnectors.value)
|
||||
animator.animateMarkerDisappear(marker, tint, highlight, fault, multi)
|
||||
} else {
|
||||
animator.deleteMarker(marker)
|
||||
}
|
||||
markers.remove(marker)
|
||||
}
|
||||
}
|
||||
// add new markers
|
||||
val map1 = markers.values.map { it.id }
|
||||
for (charger in chargers) {
|
||||
if (!map1.contains(charger.id)) {
|
||||
val tint = getMarkerTint(charger, vm.filteredConnectors.value)
|
||||
val highlight = charger == vm.chargerSparse.value
|
||||
val fault = charger.faultReport != null
|
||||
val multi = getMarkerMulti(charger, vm.filteredConnectors.value)
|
||||
val marker = map.addMarker(
|
||||
MarkerOptions()
|
||||
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))
|
||||
.icon(
|
||||
chargerIconGenerator.getBitmapDescriptor(
|
||||
tint,
|
||||
0,
|
||||
255,
|
||||
highlight,
|
||||
fault,
|
||||
multi
|
||||
)
|
||||
)
|
||||
.anchor(0.5f, 1f)
|
||||
)
|
||||
animator.animateMarkerAppear(marker, tint, highlight, fault, multi)
|
||||
markers[marker] = charger
|
||||
}
|
||||
}
|
||||
previousChargepointIds = chargepointIds
|
||||
}
|
||||
clusterMarkers = clusters.map { cluster ->
|
||||
map.addMarker(
|
||||
MarkerOptions()
|
||||
.position(LatLng(cluster.coordinates.lat, cluster.coordinates.lng))
|
||||
.icon(BitmapDescriptorFactory.fromBitmap(clusterIconGenerator.makeIcon(cluster.clusterCount.toString())))
|
||||
.icon(
|
||||
map.bitmapDescriptorFactory.fromBitmap(
|
||||
clusterIconGenerator.makeIcon(
|
||||
cluster.clusterCount.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
.anchor(0.5f, 0.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
@@ -539,7 +817,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
when (requestCode) {
|
||||
REQUEST_LOCATION_PERMISSION -> {
|
||||
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
|
||||
enableLocation(true, true)
|
||||
enableLocation(moveTo = true, animate = true)
|
||||
}
|
||||
}
|
||||
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
@@ -561,29 +839,56 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
})
|
||||
}
|
||||
filterView?.setOnClickListener {
|
||||
onOptionsItemSelected(filterItem)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_filter -> {
|
||||
requireView().findNavController().navigate(
|
||||
R.id.action_map_to_filterFragment
|
||||
)
|
||||
true
|
||||
val popup = PopupMenu(requireContext(), it, Gravity.END)
|
||||
popup.menuInflater.inflate(R.menu.popup_filter, popup.menu)
|
||||
popup.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.menu_edit_filters -> {
|
||||
requireView().findNavController().navigate(
|
||||
R.id.action_map_to_filterFragment
|
||||
)
|
||||
true
|
||||
}
|
||||
R.id.menu_filters_active -> {
|
||||
vm.filtersActive.value = !vm.filtersActive.value!!
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
|
||||
val checkItem = popup.menu.findItem(R.id.menu_filters_active)
|
||||
vm.filtersActive.observe(viewLifecycleOwner, Observer {
|
||||
checkItem.isChecked = it
|
||||
})
|
||||
popup.show()
|
||||
}
|
||||
|
||||
filterView?.setOnLongClickListener {
|
||||
// enable/disable filters
|
||||
vm.filtersActive.value = !vm.filtersActive.value!!
|
||||
// haptic feedback
|
||||
filterView.performHapticFeedback(
|
||||
HapticFeedbackConstants.LONG_PRESS,
|
||||
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
|
||||
)
|
||||
// show snackbar
|
||||
Snackbar.make(
|
||||
requireView(), if (vm.filtersActive.value!!) {
|
||||
R.string.filters_activated
|
||||
} else {
|
||||
R.string.filters_deactivated
|
||||
}, Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_AUTOCOMPLETE -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val place = Autocomplete.getPlaceFromIntent(data!!)
|
||||
val zoom = 12f
|
||||
map?.animateCamera(CameraUpdateFactory.newLatLngZoom(place.latLng, zoom))
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
vm.searchResult.value = handleAutocompleteResult(data)
|
||||
}
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
@@ -619,4 +924,20 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnected() {
|
||||
val map = this.map ?: return
|
||||
if (vm.myLocationEnabled.value == true) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
moveToCurrentLocation(map, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnectionSuspended() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatDialogFragment
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import kotlinx.android.synthetic.main.dialog_multi_select.*
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.DataBindingAdapter
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.collections.HashSet
|
||||
|
||||
class MultiSelectDialog : AppCompatDialogFragment() {
|
||||
companion object {
|
||||
fun getInstance(
|
||||
title: String,
|
||||
data: Map<String, String>,
|
||||
selected: Set<String>,
|
||||
commonChoices: Set<String>?
|
||||
): MultiSelectDialog {
|
||||
val dialog = MultiSelectDialog()
|
||||
dialog.arguments = Bundle().apply {
|
||||
putString("title", title)
|
||||
putSerializable("data", HashMap(data))
|
||||
putSerializable("selected", HashSet(selected))
|
||||
if (commonChoices != null) putSerializable("commonChoices", HashSet(commonChoices))
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
|
||||
var okListener: ((Set<String>) -> Unit)? = null
|
||||
var cancelListener: (() -> Unit)? = null
|
||||
private lateinit var items: List<MultiSelectItem>
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.dialog_multi_select, container)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
// dialog with 95% screen height
|
||||
dialog?.window?.setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
(resources.displayMetrics.heightPixels * 0.95).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val args = requireArguments()
|
||||
val data = args.getSerializable("data") as HashMap<String, String>
|
||||
val selected = args.getSerializable("selected") as HashSet<String>
|
||||
val title = args.getString("title")
|
||||
val commonChoices = if (args.containsKey("commonChoices")) {
|
||||
args.getSerializable("commonChoices") as HashSet<String>
|
||||
} else null
|
||||
|
||||
dialogTitle.text = title
|
||||
val adapter = Adapter()
|
||||
list.adapter = adapter
|
||||
list.layoutManager = LinearLayoutManager(view.context)
|
||||
|
||||
items = data.entries.toList()
|
||||
.sortedBy { it.value.toLowerCase(Locale.getDefault()) }
|
||||
.sortedByDescending { commonChoices?.contains(it.key) == true }
|
||||
.map { MultiSelectItem(it.key, it.value, it.key in selected) }
|
||||
adapter.submitList(items)
|
||||
|
||||
etSearch.doAfterTextChanged { text ->
|
||||
adapter.submitList(search(items, text.toString()))
|
||||
}
|
||||
|
||||
btnCancel.setOnClickListener {
|
||||
cancelListener?.let { listener ->
|
||||
listener()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
btnOK.setOnClickListener {
|
||||
okListener?.let { listener ->
|
||||
val result = items.filter { it.selected }.map { it.key }.toSet()
|
||||
listener(result)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
btnAll.setOnClickListener {
|
||||
items = items.map { MultiSelectItem(it.key, it.name, true) }
|
||||
adapter.submitList(search(items, etSearch.text.toString()))
|
||||
}
|
||||
btnNone.setOnClickListener {
|
||||
items = items.map { MultiSelectItem(it.key, it.name, false) }
|
||||
adapter.submitList(search(items, etSearch.text.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun search(
|
||||
items: List<MultiSelectItem>,
|
||||
text: String
|
||||
): List<MultiSelectItem> {
|
||||
return items.filter { item ->
|
||||
// search for string within name
|
||||
text.toLowerCase(Locale.getDefault()) in item.name.toLowerCase(Locale.getDefault())
|
||||
}
|
||||
}
|
||||
|
||||
class Adapter() : DataBindingAdapter<MultiSelectItem>({ it.key }) {
|
||||
override fun getItemViewType(position: Int) = R.layout.dialog_multi_select_item
|
||||
}
|
||||
|
||||
data class MultiSelectItem(val key: String, val name: String, var selected: Boolean) : Equatable
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
@@ -9,12 +10,18 @@ import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.updateNightMode
|
||||
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
class SettingsFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
prefs = PreferenceDataSource(requireContext())
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
@@ -33,4 +40,29 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
when (key) {
|
||||
"language" -> {
|
||||
activity?.let {
|
||||
it.finish();
|
||||
it.startActivity(it.intent);
|
||||
}
|
||||
}
|
||||
"darkmode" -> {
|
||||
updateNightMode(prefs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
preferenceManager.sharedPreferences
|
||||
.unregisterOnSharedPreferenceChangeListener(this)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.vonforst.evmap.navigation
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.AttributeSet
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.Navigator
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
@Navigator.Name("chrome")
|
||||
class ChromeCustomTabsNavigator(
|
||||
private val context: Context
|
||||
) : Navigator<ChromeCustomTabsNavigator.Destination>() {
|
||||
|
||||
override fun createDestination() =
|
||||
Destination(this)
|
||||
|
||||
override fun navigate(
|
||||
destination: Destination,
|
||||
args: Bundle?,
|
||||
navOptions: NavOptions?,
|
||||
navigatorExtras: Extras?
|
||||
): NavDestination? {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(ContextCompat.getColor(context, R.color.colorPrimary))
|
||||
.build()
|
||||
intent.launchUrl(context, destination.url!!)
|
||||
return null // Do not add to the back stack, managed by Chrome Custom Tabs
|
||||
}
|
||||
|
||||
override fun popBackStack() = true // Managed by Chrome Custom Tabs
|
||||
|
||||
@NavDestination.ClassType(Activity::class)
|
||||
class Destination(navigator: Navigator<out NavDestination>) : NavDestination(navigator) {
|
||||
var url: Uri? = null
|
||||
|
||||
override fun onInflate(context: Context, attrs: AttributeSet) {
|
||||
super.onInflate(context, attrs)
|
||||
context.withStyledAttributes(attrs, R.styleable.ChromeCustomTabsNavigator, 0, 0) {
|
||||
url = Uri.parse(getString(R.styleable.ChromeCustomTabsNavigator_url))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package net.vonforst.evmap.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
|
||||
class NavHostFragment : NavHostFragment() {
|
||||
override fun onCreateNavController(navController: NavController) {
|
||||
super.onCreateNavController(navController)
|
||||
navController.navigatorProvider.addNavigator(
|
||||
ChromeCustomTabsNavigator(
|
||||
requireContext()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Dao
|
||||
interface ChargeCardDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg chargeCards: ChargeCard)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg chargeCards: ChargeCard)
|
||||
|
||||
@Query("SELECT * FROM chargeCard")
|
||||
fun getAllChargeCards(): LiveData<List<ChargeCard>>
|
||||
}
|
||||
|
||||
class ChargeCardRepository(
|
||||
private val api: GoingElectricApi, private val scope: CoroutineScope,
|
||||
private val dao: ChargeCardDao, private val prefs: PreferenceDataSource
|
||||
) {
|
||||
fun getChargeCards(): LiveData<List<ChargeCard>> {
|
||||
scope.launch {
|
||||
updateChargeCards()
|
||||
}
|
||||
return dao.getAllChargeCards()
|
||||
}
|
||||
|
||||
private suspend fun updateChargeCards() {
|
||||
if (Duration.between(prefs.lastChargeCardUpdate, Instant.now()) < Duration.ofDays(1)) return
|
||||
|
||||
try {
|
||||
val response = api.getChargeCards()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (card in response.body()!!.result) {
|
||||
dao.insert(card)
|
||||
}
|
||||
|
||||
prefs.lastChargeCardUpdate = Instant.now()
|
||||
} catch (e: IOException) {
|
||||
// ignore, and retry next time
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.viewmodel.BooleanFilterValue
|
||||
import net.vonforst.evmap.viewmodel.MultipleChoiceFilterValue
|
||||
@@ -18,20 +19,27 @@ import net.vonforst.evmap.viewmodel.SliderFilterValue
|
||||
BooleanFilterValue::class,
|
||||
MultipleChoiceFilterValue::class,
|
||||
SliderFilterValue::class,
|
||||
Plug::class
|
||||
], version = 5
|
||||
Plug::class,
|
||||
Network::class,
|
||||
ChargeCard::class
|
||||
], version = 8
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun chargeLocationsDao(): ChargeLocationsDao
|
||||
abstract fun filterValueDao(): FilterValueDao
|
||||
abstract fun plugDao(): PlugDao
|
||||
abstract fun networkDao(): NetworkDao
|
||||
abstract fun chargeCardDao(): ChargeCardDao
|
||||
|
||||
companion object {
|
||||
private lateinit var context: Context
|
||||
private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db")
|
||||
.addMigrations(MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5)
|
||||
.addMigrations(
|
||||
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
|
||||
MIGRATION_7, MIGRATION_8
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -86,5 +94,31 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_6 = object : Migration(5, 6) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.beginTransaction()
|
||||
try {
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `fault_report_created` INTEGER")
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `fault_report_description` TEXT")
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_7 = object : Migration(6, 7) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `Network` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))")
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS `ChargeCard` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))")
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_8 = object : Migration(7, 8) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE `ChargeLocation` ADD `chargecards` TEXT")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
app/src/main/java/net/vonforst/evmap/storage/NetworkDao.kt
Normal file
56
app/src/main/java/net/vonforst/evmap/storage/NetworkDao.kt
Normal file
@@ -0,0 +1,56 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@Entity
|
||||
data class Network(@PrimaryKey val name: String)
|
||||
|
||||
@Dao
|
||||
interface NetworkDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg networks: Network)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg networks: Network)
|
||||
|
||||
@Query("SELECT * FROM network")
|
||||
fun getAllNetworks(): LiveData<List<Network>>
|
||||
}
|
||||
|
||||
class NetworkRepository(
|
||||
private val api: GoingElectricApi, private val scope: CoroutineScope,
|
||||
private val dao: NetworkDao, private val prefs: PreferenceDataSource
|
||||
) {
|
||||
fun getNetworks(): LiveData<List<Network>> {
|
||||
scope.launch {
|
||||
updateNetworks()
|
||||
}
|
||||
return dao.getAllNetworks()
|
||||
}
|
||||
|
||||
private suspend fun updateNetworks() {
|
||||
if (Duration.between(prefs.lastNetworkUpdate, Instant.now()) < Duration.ofDays(1)) return
|
||||
|
||||
try {
|
||||
val response = api.getNetworks()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (name in response.body()!!.result) {
|
||||
dao.insert(Network(name))
|
||||
}
|
||||
|
||||
prefs.lastNetworkUpdate = Instant.now()
|
||||
} catch (e: IOException) {
|
||||
// ignore, and retry next time
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import androidx.room.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@@ -37,13 +38,19 @@ class PlugRepository(
|
||||
private suspend fun updatePlugs() {
|
||||
if (Duration.between(prefs.lastPlugUpdate, Instant.now()) < Duration.ofDays(1)) return
|
||||
|
||||
val response = api.getPlugs()
|
||||
if (!response.isSuccessful) return
|
||||
try {
|
||||
val response = api.getPlugs()
|
||||
if (!response.isSuccessful) return
|
||||
|
||||
for (name in response.body()!!.result) {
|
||||
dao.insert(Plug(name))
|
||||
for (name in response.body()!!.result) {
|
||||
dao.insert(Plug(name))
|
||||
}
|
||||
|
||||
prefs.lastPlugUpdate = Instant.now()
|
||||
} catch (e: IOException) {
|
||||
// ignore, and retry next time
|
||||
e.printStackTrace()
|
||||
return
|
||||
}
|
||||
|
||||
prefs.lastPlugUpdate = Instant.now()
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,10 @@ package net.vonforst.evmap.storage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import net.vonforst.evmap.R
|
||||
import java.time.Instant
|
||||
|
||||
class PreferenceDataSource(context: Context) {
|
||||
class PreferenceDataSource(val context: Context) {
|
||||
val sp = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
var navigateUseMaps: Boolean
|
||||
@@ -18,4 +19,34 @@ class PreferenceDataSource(context: Context) {
|
||||
set(value) {
|
||||
sp.edit().putLong("last_plug_update", value.toEpochMilli()).apply()
|
||||
}
|
||||
|
||||
var lastNetworkUpdate: Instant
|
||||
get() = Instant.ofEpochMilli(sp.getLong("last_network_update", 0L))
|
||||
set(value) {
|
||||
sp.edit().putLong("last_network_update", value.toEpochMilli()).apply()
|
||||
}
|
||||
|
||||
var lastChargeCardUpdate: Instant
|
||||
get() = Instant.ofEpochMilli(sp.getLong("last_chargecard_update", 0L))
|
||||
set(value) {
|
||||
sp.edit().putLong("last_chargecard_update", value.toEpochMilli()).apply()
|
||||
}
|
||||
|
||||
var filtersActive: Boolean
|
||||
get() = sp.getBoolean("filters_active", true)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("filters_active", value).apply()
|
||||
}
|
||||
|
||||
val language: String
|
||||
get() = sp.getString("language", "default")!!
|
||||
|
||||
val darkmode: String
|
||||
get() = sp.getString("darkmode", "default")!!
|
||||
|
||||
val mapProvider: String
|
||||
get() = sp.getString(
|
||||
"map_provider",
|
||||
context.getString(R.string.pref_map_provider_default)
|
||||
)!!
|
||||
}
|
||||
@@ -3,8 +3,10 @@ package net.vonforst.evmap.storage
|
||||
import androidx.room.TypeConverter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCardId
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.api.goingelectric.ChargerPhoto
|
||||
import java.time.Instant
|
||||
import java.time.LocalTime
|
||||
|
||||
class Converters {
|
||||
@@ -17,6 +19,10 @@ class Converters {
|
||||
val type = Types.newParameterizedType(List::class.java, ChargerPhoto::class.java)
|
||||
moshi.adapter<List<ChargerPhoto>>(type)
|
||||
}
|
||||
private val chargeCardIdListAdapter by lazy {
|
||||
val type = Types.newParameterizedType(List::class.java, ChargeCardId::class.java)
|
||||
moshi.adapter<List<ChargeCardId>>(type)
|
||||
}
|
||||
private val stringSetAdapter by lazy {
|
||||
val type = Types.newParameterizedType(Set::class.java, String::class.java)
|
||||
moshi.adapter<Set<String>>(type)
|
||||
@@ -42,6 +48,16 @@ class Converters {
|
||||
return chargerPhotoListAdapter.fromJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromChargeCardIdList(value: List<ChargeCardId>?): String {
|
||||
return chargeCardIdListAdapter.toJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toChargeCardIdList(value: String?): List<ChargeCardId>? {
|
||||
return value?.let { chargeCardIdListAdapter.fromJson(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromLocalTime(value: LocalTime?): String? {
|
||||
return value?.toString()
|
||||
@@ -49,11 +65,23 @@ class Converters {
|
||||
|
||||
@TypeConverter
|
||||
fun toLocalTime(value: String?): LocalTime? {
|
||||
return value.let {
|
||||
return value?.let {
|
||||
LocalTime.parse(it)
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromInstant(value: Instant?): Long? {
|
||||
return value?.toEpochMilli()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toInstant(value: Long?): Instant? {
|
||||
return value?.let {
|
||||
Instant.ofEpochMilli(it)
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromStringSet(value: Set<String>?): String {
|
||||
return stringSetAdapter.toJson(value)
|
||||
|
||||
@@ -3,9 +3,12 @@ package net.vonforst.evmap.ui
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.use
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -14,6 +17,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
@BindingAdapter("goneUnless")
|
||||
@@ -97,6 +101,36 @@ fun setBackgroundTintAvailability(view: View, available: List<ChargepointStatus>
|
||||
view.backgroundTintList = ColorStateList.valueOf(availabilityColor(available, view.context))
|
||||
}
|
||||
|
||||
@BindingAdapter("selectableItemBackground")
|
||||
fun applySelectableItemBackground(view: View, apply: Boolean) {
|
||||
if (apply) {
|
||||
view.context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
|
||||
view.background = it.getDrawable(0)
|
||||
}
|
||||
} else {
|
||||
view.background = null
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("htmlText")
|
||||
fun setHtmlTextValue(textView: TextView, htmlText: String?) {
|
||||
if (htmlText == null) {
|
||||
textView.text = null
|
||||
} else {
|
||||
textView.text = HtmlCompat.fromHtml(htmlText, HtmlCompat.FROM_HTML_MODE_LEGACY)
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("android:layout_marginTop")
|
||||
fun setTopMargin(view: View, topMargin: Float) {
|
||||
val layoutParams = view.layoutParams as MarginLayoutParams
|
||||
layoutParams.setMargins(
|
||||
layoutParams.leftMargin, topMargin.roundToInt(),
|
||||
layoutParams.rightMargin, layoutParams.bottomMargin
|
||||
)
|
||||
view.layoutParams = layoutParams
|
||||
}
|
||||
|
||||
private fun availabilityColor(
|
||||
status: List<ChargepointStatus>?,
|
||||
context: Context
|
||||
|
||||
41
app/src/main/java/net/vonforst/evmap/ui/Clustering.kt
Normal file
41
app/src/main/java/net/vonforst/evmap/ui/Clustering.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
package net.vonforst.evmap.ui;
|
||||
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.google.maps.android.clustering.ClusterItem
|
||||
import com.google.maps.android.clustering.algo.NonHierarchicalDistanceBasedAlgorithm
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocationCluster
|
||||
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
|
||||
import net.vonforst.evmap.api.goingelectric.Coordinate
|
||||
|
||||
|
||||
fun cluster(
|
||||
result: List<ChargepointListItem>,
|
||||
zoom: Float,
|
||||
clusterDistance: Int
|
||||
): List<ChargepointListItem> {
|
||||
val clusters = result.filterIsInstance<ChargeLocationCluster>()
|
||||
val locations = result.filterIsInstance<ChargeLocation>()
|
||||
|
||||
val clusterItems = locations.map { ChargepointClusterItem(it) }
|
||||
|
||||
val algo = NonHierarchicalDistanceBasedAlgorithm<ChargepointClusterItem>()
|
||||
algo.maxDistanceBetweenClusteredItems = clusterDistance
|
||||
algo.addItems(clusterItems)
|
||||
return algo.getClusters(zoom).map {
|
||||
if (it.size == 1) {
|
||||
it.items.first().charger
|
||||
} else {
|
||||
ChargeLocationCluster(it.size, Coordinate(it.position.latitude, it.position.longitude))
|
||||
}
|
||||
} + clusters
|
||||
}
|
||||
|
||||
private class ChargepointClusterItem(val charger: ChargeLocation) : ClusterItem {
|
||||
override fun getSnippet(): String? = null
|
||||
|
||||
override fun getTitle(): String? = charger.name
|
||||
|
||||
override fun getPosition(): LatLng = LatLng(charger.coordinates.lat, charger.coordinates.lng)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
|
||||
|
||||
|
||||
class HideOnExpandFabBehavior(context: Context, attrs: AttributeSet) :
|
||||
FloatingActionButton.Behavior(context, attrs) {
|
||||
|
||||
override fun onStartNestedScroll(
|
||||
coordinatorLayout: CoordinatorLayout,
|
||||
child: FloatingActionButton,
|
||||
directTargetChild: View,
|
||||
target: View,
|
||||
axes: Int,
|
||||
type: Int
|
||||
): Boolean {
|
||||
return axes == ViewCompat.SCROLL_AXIS_VERTICAL || super.onStartNestedScroll(
|
||||
coordinatorLayout,
|
||||
child,
|
||||
directTargetChild,
|
||||
target,
|
||||
axes,
|
||||
type
|
||||
)
|
||||
}
|
||||
|
||||
override fun layoutDependsOn(
|
||||
parent: CoordinatorLayout,
|
||||
child: FloatingActionButton,
|
||||
dependency: View
|
||||
): Boolean {
|
||||
if (dependency is NestedScrollView) {
|
||||
try {
|
||||
val behavior = BottomSheetBehaviorGoogleMapsLike.from<View>(dependency)
|
||||
behavior.addBottomSheetCallback(object :
|
||||
BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
|
||||
}
|
||||
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
onDependentViewChanged(parent, child, dependency)
|
||||
}
|
||||
})
|
||||
return true
|
||||
} catch (e: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDependentViewChanged(
|
||||
parent: CoordinatorLayout,
|
||||
child: FloatingActionButton,
|
||||
dependency: View
|
||||
): Boolean {
|
||||
val behavior = BottomSheetBehaviorGoogleMapsLike.from<View>(dependency)
|
||||
when (behavior.state) {
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING -> {
|
||||
|
||||
}
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN -> {
|
||||
if (child.tag as? Boolean != false) child.show()
|
||||
}
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED -> {
|
||||
if (child.tag as? Boolean != false) child.show()
|
||||
}
|
||||
else -> {
|
||||
child.hide()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onNestedScroll(
|
||||
coordinatorLayout: CoordinatorLayout,
|
||||
child: FloatingActionButton,
|
||||
target: View,
|
||||
dxConsumed: Int,
|
||||
dyConsumed: Int,
|
||||
dxUnconsumed: Int,
|
||||
dyUnconsumed: Int,
|
||||
type: Int,
|
||||
consumed: IntArray
|
||||
) {
|
||||
super.onNestedScroll(
|
||||
coordinatorLayout,
|
||||
child,
|
||||
target,
|
||||
dxConsumed,
|
||||
dyConsumed,
|
||||
dxUnconsumed,
|
||||
dyUnconsumed,
|
||||
type,
|
||||
consumed
|
||||
)
|
||||
if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
|
||||
// User scrolled down and the FAB is currently visible -> hide the FAB
|
||||
child.hide();
|
||||
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
|
||||
// User scrolled up and the FAB is currently not visible -> show the FAB
|
||||
child.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@ import androidx.annotation.ColorRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import com.google.android.gms.maps.model.BitmapDescriptor
|
||||
import com.google.android.gms.maps.model.BitmapDescriptorFactory
|
||||
import com.car2go.maps.BitmapDescriptorFactory
|
||||
import com.car2go.maps.model.BitmapDescriptor
|
||||
import com.google.maps.android.ui.IconGenerator
|
||||
import com.google.maps.android.ui.SquareTextView
|
||||
import net.vonforst.evmap.R
|
||||
@@ -32,7 +32,7 @@ class ClusterIconGenerator(context: Context) : IconGenerator(context) {
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
id = com.google.maps.android.R.id.amu_text
|
||||
id = R.id.amu_text
|
||||
setPadding(twelveDpi, twelveDpi, twelveDpi, twelveDpi)
|
||||
TextViewCompat.setTextAppearance(this, R.style.TextAppearance_AppCompat)
|
||||
setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
@@ -41,24 +41,30 @@ class ClusterIconGenerator(context: Context) : IconGenerator(context) {
|
||||
}
|
||||
|
||||
|
||||
class ChargerIconGenerator(val context: Context) {
|
||||
data class BitmapData(val tint: Int, val scale: Int, val alpha: Int, val highlight: Boolean)
|
||||
class ChargerIconGenerator(val context: Context, val factory: BitmapDescriptorFactory) {
|
||||
data class BitmapData(
|
||||
val tint: Int,
|
||||
val scale: Int,
|
||||
val alpha: Int,
|
||||
val highlight: Boolean,
|
||||
val fault: Boolean,
|
||||
val multi: Boolean
|
||||
)
|
||||
|
||||
val cacheSize = 4 * 1024 * 1024; // 4MiB
|
||||
val cache = object : LruCache<BitmapData, Bitmap>(cacheSize) {
|
||||
override fun sizeOf(key: BitmapData, value: Bitmap): Int {
|
||||
return value.byteCount
|
||||
}
|
||||
}
|
||||
val oversize = 1f // increase to add padding for overshoot scale animation
|
||||
val cacheSize = 840; // 840 items: 21 sizes, 5 colors, highlight, fault, multi on/off
|
||||
val cache = LruCache<BitmapData, BitmapDescriptor>(cacheSize)
|
||||
val oversize = 1.4f // increase to add padding for fault icon or scale > 1
|
||||
val icon = R.drawable.ic_map_marker_charging
|
||||
val multiIcon = R.drawable.ic_map_marker_charging_multiple
|
||||
val highlightIcon = R.drawable.ic_map_marker_highlight
|
||||
val highlightIconMulti = R.drawable.ic_map_marker_charging_highlight_multiple
|
||||
val faultIcon = R.drawable.ic_map_marker_fault
|
||||
|
||||
init {
|
||||
preloadCache()
|
||||
}
|
||||
|
||||
fun preloadCache() {
|
||||
private fun preloadCache() {
|
||||
// pre-generates images for scale from 0 to 255 for all possible tint colors
|
||||
val tints = listOf(
|
||||
R.color.charger_100kw,
|
||||
@@ -67,11 +73,14 @@ class ChargerIconGenerator(val context: Context) {
|
||||
R.color.charger_11kw,
|
||||
R.color.charger_low
|
||||
)
|
||||
for (highlight in listOf(false, true)) {
|
||||
for (tint in tints) {
|
||||
for (scale in 0..20) {
|
||||
val data = BitmapData(tint, scale, 255, highlight)
|
||||
cache.put(data, generateBitmap(data))
|
||||
for (fault in listOf(false, true)) {
|
||||
for (highlight in listOf(false, true)) {
|
||||
for (multi in listOf(false, true)) {
|
||||
for (tint in tints) {
|
||||
for (scale in 0..20) {
|
||||
getBitmapDescriptor(tint, scale, 255, highlight, fault, multi)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,27 +90,31 @@ class ChargerIconGenerator(val context: Context) {
|
||||
@ColorRes tint: Int,
|
||||
scale: Int = 20,
|
||||
alpha: Int = 255,
|
||||
highlight: Boolean = false
|
||||
highlight: Boolean = false,
|
||||
fault: Boolean = false,
|
||||
multi: Boolean = false
|
||||
): BitmapDescriptor? {
|
||||
val data = BitmapData(tint, scale, alpha, highlight)
|
||||
val data = BitmapData(tint, scale, alpha, highlight, fault, multi)
|
||||
val cachedImg = cache[data]
|
||||
return if (cachedImg != null) {
|
||||
BitmapDescriptorFactory.fromBitmap(cachedImg)
|
||||
cachedImg
|
||||
} else {
|
||||
val bitmap = generateBitmap(data)
|
||||
cache.put(data, bitmap)
|
||||
BitmapDescriptorFactory.fromBitmap(bitmap)
|
||||
val bmd = factory.fromBitmap(bitmap)
|
||||
cache.put(data, bmd)
|
||||
bmd
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateBitmap(data: BitmapData): Bitmap {
|
||||
val vd: Drawable = context.getDrawable(icon)!!
|
||||
val icon = if (data.multi) multiIcon else icon
|
||||
val vd: Drawable = ContextCompat.getDrawable(context, icon)!!
|
||||
|
||||
DrawableCompat.setTint(vd, ContextCompat.getColor(context, data.tint));
|
||||
DrawableCompat.setTintMode(vd, PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
val leftPadding = vd.intrinsicWidth * (oversize - 1) / 2
|
||||
val topPadding = vd.intrinsicWidth * (oversize - 1)
|
||||
val topPadding = vd.intrinsicHeight * (oversize - 1)
|
||||
vd.setBounds(
|
||||
leftPadding.toInt(), topPadding.toInt(),
|
||||
leftPadding.toInt() + vd.intrinsicWidth,
|
||||
@@ -126,7 +139,8 @@ class ChargerIconGenerator(val context: Context) {
|
||||
vd.draw(canvas)
|
||||
|
||||
if (data.highlight) {
|
||||
val highlightDrawable = context.getDrawable(highlightIcon)!!
|
||||
val hIcon = if (data.multi) highlightIconMulti else highlightIcon
|
||||
val highlightDrawable = ContextCompat.getDrawable(context, hIcon)!!
|
||||
highlightDrawable.setBounds(
|
||||
leftPadding.toInt(), topPadding.toInt(),
|
||||
leftPadding.toInt() + vd.intrinsicWidth,
|
||||
@@ -136,6 +150,21 @@ class ChargerIconGenerator(val context: Context) {
|
||||
highlightDrawable.draw(canvas)
|
||||
}
|
||||
|
||||
if (data.fault) {
|
||||
val faultDrawable = ContextCompat.getDrawable(context, faultIcon)!!
|
||||
val faultSize = 0.75
|
||||
val faultShift = 0.25
|
||||
val base = vd.intrinsicWidth
|
||||
faultDrawable.setBounds(
|
||||
(leftPadding.toInt() + base * (1 - faultSize + faultShift)).toInt(),
|
||||
(topPadding.toInt() - base * faultShift).toInt(),
|
||||
(leftPadding.toInt() + base * (1 + faultShift)).toInt(),
|
||||
(topPadding.toInt() + base * (faultSize - faultShift)).toInt()
|
||||
)
|
||||
faultDrawable.alpha = data.alpha
|
||||
faultDrawable.draw(canvas)
|
||||
}
|
||||
|
||||
return bm
|
||||
}
|
||||
}
|
||||
@@ -5,46 +5,56 @@ import android.view.animation.BounceInterpolator
|
||||
import androidx.core.animation.addListener
|
||||
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
|
||||
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
|
||||
import com.google.android.gms.maps.model.Marker
|
||||
import com.car2go.maps.model.Marker
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import kotlin.math.max
|
||||
|
||||
fun getMarkerTint(charger: ChargeLocation): Int = when {
|
||||
charger.maxPower >= 100 -> R.color.charger_100kw
|
||||
charger.maxPower >= 43 -> R.color.charger_43kw
|
||||
charger.maxPower >= 20 -> R.color.charger_20kw
|
||||
charger.maxPower >= 11 -> R.color.charger_11kw
|
||||
fun getMarkerTint(
|
||||
charger: ChargeLocation,
|
||||
connectors: Set<String>?
|
||||
): Int = when {
|
||||
charger.maxPower(connectors) >= 100 -> R.color.charger_100kw
|
||||
charger.maxPower(connectors) >= 43 -> R.color.charger_43kw
|
||||
charger.maxPower(connectors) >= 20 -> R.color.charger_20kw
|
||||
charger.maxPower(connectors) >= 11 -> R.color.charger_11kw
|
||||
else -> R.color.charger_low
|
||||
}
|
||||
|
||||
class MarkerAnimator(val gen: ChargerIconGenerator) {
|
||||
val animatingMarkers = hashMapOf<Marker, ValueAnimator>()
|
||||
private val animatingMarkers = hashMapOf<Marker, ValueAnimator>()
|
||||
|
||||
fun animateMarkerAppear(
|
||||
marker: Marker,
|
||||
tint: Int,
|
||||
highlight: Boolean
|
||||
highlight: Boolean,
|
||||
fault: Boolean,
|
||||
multi: Boolean
|
||||
) {
|
||||
animatingMarkers[marker]?.cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
animatingMarkers[marker]?.let {
|
||||
it.cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
}
|
||||
|
||||
val anim = ValueAnimator.ofInt(0, 20).apply {
|
||||
duration = 250
|
||||
interpolator = LinearOutSlowInInterpolator()
|
||||
addUpdateListener { animationState ->
|
||||
if (!marker.isVisible) {
|
||||
cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
return@addUpdateListener
|
||||
}
|
||||
val scale = animationState.animatedValue as Int
|
||||
marker.setIcon(
|
||||
gen.getBitmapDescriptor(tint, scale = scale, highlight = highlight)
|
||||
gen.getBitmapDescriptor(
|
||||
tint,
|
||||
scale = scale,
|
||||
highlight = highlight,
|
||||
fault = fault,
|
||||
multi = multi
|
||||
)
|
||||
)
|
||||
}
|
||||
addListener(onEnd = {
|
||||
animatingMarkers.remove(marker)
|
||||
}, onCancel = {
|
||||
animatingMarkers.remove(marker)
|
||||
})
|
||||
}
|
||||
animatingMarkers[marker] = anim
|
||||
@@ -54,50 +64,68 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
|
||||
fun animateMarkerDisappear(
|
||||
marker: Marker,
|
||||
tint: Int,
|
||||
highlight: Boolean
|
||||
highlight: Boolean,
|
||||
fault: Boolean,
|
||||
multi: Boolean
|
||||
) {
|
||||
animatingMarkers[marker]?.cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
animatingMarkers[marker]?.let {
|
||||
it.cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
}
|
||||
|
||||
val anim = ValueAnimator.ofInt(20, 0).apply {
|
||||
duration = 200
|
||||
interpolator = FastOutLinearInInterpolator()
|
||||
addUpdateListener { animationState ->
|
||||
if (!marker.isVisible) {
|
||||
cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
return@addUpdateListener
|
||||
}
|
||||
val scale = animationState.animatedValue as Int
|
||||
marker.setIcon(
|
||||
gen.getBitmapDescriptor(tint, scale = scale, highlight = highlight)
|
||||
gen.getBitmapDescriptor(
|
||||
tint,
|
||||
scale = scale,
|
||||
highlight = highlight,
|
||||
fault = fault,
|
||||
multi = multi
|
||||
)
|
||||
)
|
||||
}
|
||||
addListener(onEnd = {
|
||||
animatingMarkers.remove(marker)
|
||||
marker.remove()
|
||||
animatingMarkers.remove(marker)
|
||||
}, onCancel = {
|
||||
marker.remove()
|
||||
animatingMarkers.remove(marker)
|
||||
})
|
||||
}
|
||||
animatingMarkers[marker] = anim
|
||||
anim.start()
|
||||
}
|
||||
|
||||
fun deleteMarker(marker: Marker) {
|
||||
animatingMarkers[marker]?.let {
|
||||
it.cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
}
|
||||
marker.remove()
|
||||
}
|
||||
|
||||
fun animateMarkerBounce(marker: Marker) {
|
||||
animatingMarkers[marker]?.cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
animatingMarkers[marker]?.let {
|
||||
it.cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
}
|
||||
|
||||
val anim = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = 700
|
||||
interpolator = BounceInterpolator()
|
||||
addUpdateListener { state ->
|
||||
if (!marker.isVisible) {
|
||||
cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
return@addUpdateListener
|
||||
}
|
||||
val t = max(1f - state.animatedValue as Float, 0f) / 2
|
||||
marker.setAnchor(0.5f, 1.0f + t)
|
||||
}
|
||||
addListener(onEnd = {
|
||||
animatingMarkers.remove(marker)
|
||||
}, onCancel = {
|
||||
animatingMarkers.remove(marker)
|
||||
})
|
||||
}
|
||||
animatingMarkers[marker] = anim
|
||||
anim.start()
|
||||
|
||||
14
app/src/main/java/net/vonforst/evmap/ui/NightModeUtils.kt
Normal file
14
app/src/main/java/net/vonforst/evmap/ui/NightModeUtils.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package net.vonforst.evmap.ui
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
fun updateNightMode(prefs: PreferenceDataSource) {
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (prefs.darkmode) {
|
||||
"on" -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
"off" -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package net.vonforst.evmap.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import java.util.*
|
||||
|
||||
|
||||
class LocaleContextWrapper(base: Context?) : ContextWrapper(base) {
|
||||
companion object {
|
||||
fun wrap(context: Context, language: String): ContextWrapper {
|
||||
val sysConfig: Configuration = context.applicationContext.resources.configuration
|
||||
val appConfig: Configuration = context.resources.configuration
|
||||
var ctx = context
|
||||
|
||||
|
||||
if (language == "" || language == "default") {
|
||||
// set default locale
|
||||
Locale.setDefault(ConfigurationCompat.getLocales(sysConfig)[0])
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
appConfig.setLocales(sysConfig.locales)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
appConfig.locale = sysConfig.locale
|
||||
}
|
||||
} else {
|
||||
// set selected locale
|
||||
val locale = Locale(language)
|
||||
Locale.setDefault(locale)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
appConfig.setLocale(locale)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
appConfig.locale = locale
|
||||
}
|
||||
}
|
||||
|
||||
ctx = context.createConfigurationContext(appConfig)
|
||||
return LocaleContextWrapper(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.*
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLng
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -10,17 +10,15 @@ import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeCard
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.Plug
|
||||
import net.vonforst.evmap.storage.PlugRepository
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.storage.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.cast
|
||||
|
||||
val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 100, 150, 200, 250, 300, 350)
|
||||
val powerSteps = listOf(0, 2, 3, 7, 11, 22, 43, 50, 75, 100, 150, 200, 250, 300, 350)
|
||||
internal fun mapPower(i: Int) = powerSteps[i]
|
||||
internal fun mapPowerInverse(power: Int) = powerSteps
|
||||
.mapIndexed { index, v -> abs(v - power) to index }
|
||||
@@ -28,7 +26,9 @@ internal fun mapPowerInverse(power: Int) = powerSteps
|
||||
|
||||
internal fun getFilters(
|
||||
application: Application,
|
||||
plugs: LiveData<List<Plug>>
|
||||
plugs: LiveData<List<Plug>>,
|
||||
networks: LiveData<List<Network>>,
|
||||
chargeCards: LiveData<List<ChargeCard>>
|
||||
): LiveData<List<Filter<FilterValue>>> {
|
||||
return MediatorLiveData<List<Filter<FilterValue>>>().apply {
|
||||
val plugNames = mapOf(
|
||||
@@ -42,49 +42,84 @@ internal fun getFilters(
|
||||
Chargepoint.CEE_BLAU to application.getString(R.string.plug_cee_blau),
|
||||
Chargepoint.CEE_ROT to application.getString(R.string.plug_cee_rot)
|
||||
)
|
||||
addSource(plugs) { plugs ->
|
||||
val plugMap = plugs.map { plug ->
|
||||
plug.name to (plugNames[plug.name] ?: plug.name)
|
||||
}.toMap()
|
||||
value = listOf(
|
||||
BooleanFilter(application.getString(R.string.filter_free), "freecharging"),
|
||||
BooleanFilter(application.getString(R.string.filter_free_parking), "freeparking"),
|
||||
SliderFilter(
|
||||
application.getString(R.string.filter_min_power), "min_power",
|
||||
powerSteps.size - 1,
|
||||
mapping = ::mapPower,
|
||||
inverseMapping = ::mapPowerInverse,
|
||||
unit = "kW"
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_connectors), "connectors",
|
||||
plugMap,
|
||||
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO)
|
||||
),
|
||||
SliderFilter(
|
||||
application.getString(R.string.filter_min_connectors),
|
||||
"min_connectors",
|
||||
10
|
||||
)
|
||||
)
|
||||
listOf(plugs, networks, chargeCards).forEach { source ->
|
||||
addSource(source) { _ ->
|
||||
buildFilters(plugs, plugNames, networks, chargeCards, application)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
|
||||
plugs: LiveData<List<Plug>>,
|
||||
plugNames: Map<String, String>,
|
||||
networks: LiveData<List<Network>>,
|
||||
chargeCards: LiveData<List<ChargeCard>>,
|
||||
application: Application
|
||||
) {
|
||||
val plugMap = plugs.value?.map { plug ->
|
||||
plug.name to (plugNames[plug.name] ?: plug.name)
|
||||
}?.toMap() ?: return
|
||||
val networkMap = networks.value?.map { it.name to it.name }?.toMap() ?: return
|
||||
val chargecardMap = chargeCards.value?.map { it.id.toString() to it.name }?.toMap() ?: return
|
||||
value = listOf(
|
||||
BooleanFilter(application.getString(R.string.filter_free), "freecharging"),
|
||||
BooleanFilter(application.getString(R.string.filter_free_parking), "freeparking"),
|
||||
BooleanFilter(application.getString(R.string.filter_open_247), "open_247"),
|
||||
SliderFilter(
|
||||
application.getString(R.string.filter_min_power), "min_power",
|
||||
powerSteps.size - 1,
|
||||
mapping = ::mapPower,
|
||||
inverseMapping = ::mapPowerInverse,
|
||||
unit = "kW"
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_connectors), "connectors",
|
||||
plugMap,
|
||||
commonChoices = setOf(Chargepoint.TYPE_2, Chargepoint.CCS, Chargepoint.CHADEMO),
|
||||
manyChoices = true
|
||||
),
|
||||
SliderFilter(
|
||||
application.getString(R.string.filter_min_connectors),
|
||||
"min_connectors",
|
||||
10,
|
||||
min = 1
|
||||
),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_networks), "networks",
|
||||
networkMap, manyChoices = true
|
||||
),
|
||||
BooleanFilter(application.getString(R.string.filter_barrierfree), "barrierfree"),
|
||||
MultipleChoiceFilter(
|
||||
application.getString(R.string.filter_chargecards), "chargecards",
|
||||
chargecardMap, manyChoices = true
|
||||
),
|
||||
BooleanFilter(application.getString(R.string.filter_exclude_faults), "exclude_faults")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
internal fun filtersWithValue(
|
||||
filters: LiveData<List<Filter<FilterValue>>>,
|
||||
filterValues: LiveData<List<FilterValue>>
|
||||
filterValues: LiveData<List<FilterValue>>,
|
||||
active: LiveData<Boolean>? = null
|
||||
): MediatorLiveData<List<FilterWithValue<out FilterValue>>> =
|
||||
MediatorLiveData<List<FilterWithValue<out FilterValue>>>().apply {
|
||||
listOf(filters, filterValues).forEach {
|
||||
listOf(filters, filterValues, active).forEach {
|
||||
if (it == null) return@forEach
|
||||
addSource(it) {
|
||||
val filters = filters.value ?: return@addSource
|
||||
val values = filterValues.value ?: return@addSource
|
||||
value = filters.map { filter ->
|
||||
val value =
|
||||
values.find { it.key == filter.key } ?: filter.defaultValue()
|
||||
FilterWithValue(filter, filter.valueClass.cast(value))
|
||||
value = if (active != null && !active.value!!) {
|
||||
filters.map { filter ->
|
||||
FilterWithValue(filter, filter.defaultValue())
|
||||
}
|
||||
} else {
|
||||
val values = filterValues.value ?: return@addSource
|
||||
filters.map { filter ->
|
||||
val value =
|
||||
values.find { it.key == filter.key } ?: filter.defaultValue()
|
||||
FilterWithValue(filter, filter.valueClass.cast(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,9 +134,14 @@ class FilterViewModel(application: Application, geApiKey: String) :
|
||||
private val plugs: LiveData<List<Plug>> by lazy {
|
||||
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
|
||||
}
|
||||
|
||||
private val networks: LiveData<List<Network>> by lazy {
|
||||
NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks()
|
||||
}
|
||||
private val chargeCards: LiveData<List<ChargeCard>> by lazy {
|
||||
ChargeCardRepository(api, viewModelScope, db.chargeCardDao(), prefs).getChargeCards()
|
||||
}
|
||||
private val filters: LiveData<List<Filter<FilterValue>>> by lazy {
|
||||
getFilters(application, plugs)
|
||||
getFilters(application, plugs, networks, chargeCards)
|
||||
}
|
||||
|
||||
private val filterValues: LiveData<List<FilterValue>> by lazy {
|
||||
@@ -136,7 +176,8 @@ data class MultipleChoiceFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val choices: Map<String, String>,
|
||||
val commonChoices: Set<String>? = null
|
||||
val commonChoices: Set<String>? = null,
|
||||
val manyChoices: Boolean = false
|
||||
) : Filter<MultipleChoiceFilterValue>() {
|
||||
override val valueClass: KClass<MultipleChoiceFilterValue> = MultipleChoiceFilterValue::class
|
||||
override fun defaultValue() = MultipleChoiceFilterValue(key, mutableSetOf(), true)
|
||||
@@ -146,12 +187,13 @@ data class SliderFilter(
|
||||
override val name: String,
|
||||
override val key: String,
|
||||
val max: Int,
|
||||
val min: Int = 0,
|
||||
val mapping: ((Int) -> Int) = { it },
|
||||
val inverseMapping: ((Int) -> Int) = { it },
|
||||
val unit: String? = ""
|
||||
) : Filter<SliderFilterValue>() {
|
||||
override val valueClass: KClass<SliderFilterValue> = SliderFilterValue::class
|
||||
override fun defaultValue() = SliderFilterValue(key, 0)
|
||||
override fun defaultValue() = SliderFilterValue(key, min)
|
||||
}
|
||||
|
||||
sealed class FilterValue : BaseObservable(), Equatable {
|
||||
@@ -188,4 +230,4 @@ data class SliderFilterValue(
|
||||
var value: Int
|
||||
) : FilterValue()
|
||||
|
||||
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable
|
||||
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable
|
||||
|
||||
@@ -2,25 +2,36 @@ package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.*
|
||||
import com.google.android.gms.maps.model.LatLngBounds
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.ChargepointList
|
||||
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.Plug
|
||||
import net.vonforst.evmap.storage.PlugRepository
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.api.goingelectric.*
|
||||
import net.vonforst.evmap.storage.*
|
||||
import net.vonforst.evmap.ui.cluster
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
data class MapPosition(val bounds: LatLngBounds, val zoom: Float)
|
||||
|
||||
data class PlaceWithBounds(val latLng: LatLng, val viewport: LatLngBounds?)
|
||||
|
||||
internal fun getClusterDistance(zoom: Float): Int? {
|
||||
return when (zoom) {
|
||||
in 0.0..7.0 -> 100
|
||||
in 7.0..11.5 -> 75
|
||||
in 11.5..12.5 -> 60
|
||||
in 12.5..13.0 -> 45
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
class MapViewModel(application: Application, geApiKey: String) : AndroidViewModel(application) {
|
||||
private var api = GoingElectricApi.create(geApiKey, context = application)
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
@@ -40,10 +51,27 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
private val plugs: LiveData<List<Plug>> by lazy {
|
||||
PlugRepository(api, viewModelScope, db.plugDao(), prefs).getPlugs()
|
||||
}
|
||||
private val filters = getFilters(application, plugs)
|
||||
private val networks: LiveData<List<Network>> by lazy {
|
||||
NetworkRepository(api, viewModelScope, db.networkDao(), prefs).getNetworks()
|
||||
}
|
||||
private val chargeCards: LiveData<List<ChargeCard>> by lazy {
|
||||
ChargeCardRepository(api, viewModelScope, db.chargeCardDao(), prefs).getChargeCards()
|
||||
}
|
||||
private val filters = getFilters(application, plugs, networks, chargeCards)
|
||||
|
||||
private val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
|
||||
filtersWithValue(filters, filterValues)
|
||||
filtersWithValue(filters, filterValues, filtersActive)
|
||||
}
|
||||
|
||||
val chargeCardMap: LiveData<Map<Long, ChargeCard>> by lazy {
|
||||
MediatorLiveData<Map<Long, ChargeCard>>().apply {
|
||||
value = null
|
||||
addSource(chargeCards) {
|
||||
value = chargeCards.value?.map {
|
||||
it.id to it
|
||||
}?.toMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val filtersCount: LiveData<Int> by lazy {
|
||||
@@ -62,13 +90,17 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
value = Resource.loading(emptyList())
|
||||
listOf(mapPosition, filtersWithValue).forEach {
|
||||
addSource(it) {
|
||||
val pos = mapPosition.value ?: return@addSource
|
||||
val filters = filtersWithValue.value ?: return@addSource
|
||||
loadChargepoints(pos, filters)
|
||||
reloadChargepoints()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val filteredConnectors: MutableLiveData<Set<String>> by lazy {
|
||||
MutableLiveData<Set<String>>()
|
||||
}
|
||||
val filteredChargeCards: MutableLiveData<Set<Long>> by lazy {
|
||||
MutableLiveData<Set<Long>>()
|
||||
}
|
||||
|
||||
val chargerSparse: MutableLiveData<ChargeLocation> by lazy {
|
||||
MutableLiveData<ChargeLocation>()
|
||||
@@ -112,11 +144,45 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
val myLocationEnabled: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>()
|
||||
}
|
||||
val layersMenuOpen: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>().apply {
|
||||
value = false
|
||||
}
|
||||
}
|
||||
|
||||
val favorites: LiveData<List<ChargeLocation>> by lazy {
|
||||
db.chargeLocationsDao().getAllChargeLocations()
|
||||
}
|
||||
|
||||
val searchResult: MutableLiveData<PlaceWithBounds> by lazy {
|
||||
MutableLiveData<PlaceWithBounds>()
|
||||
}
|
||||
|
||||
val mapType: MutableLiveData<AnyMap.Type> by lazy {
|
||||
MutableLiveData<AnyMap.Type>().apply {
|
||||
value = AnyMap.Type.NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
val mapTrafficEnabled: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>().apply {
|
||||
value = false
|
||||
}
|
||||
}
|
||||
|
||||
val filtersActive: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>().apply {
|
||||
value = prefs.filtersActive
|
||||
observeForever {
|
||||
prefs.filtersActive = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setMapType(type: AnyMap.Type) {
|
||||
mapType.value = type
|
||||
}
|
||||
|
||||
fun insertFavorite(charger: ChargeLocation) {
|
||||
viewModelScope.launch {
|
||||
db.chargeLocationsDao().insert(charger)
|
||||
@@ -129,6 +195,12 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
}
|
||||
}
|
||||
|
||||
fun reloadChargepoints() {
|
||||
val pos = mapPosition.value ?: return
|
||||
val filters = filtersWithValue.value ?: return
|
||||
loadChargepoints(pos, filters)
|
||||
}
|
||||
|
||||
private fun loadChargepoints(
|
||||
mapPosition: MapPosition,
|
||||
filters: List<FilterWithValue<out FilterValue>>
|
||||
@@ -136,10 +208,15 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
chargepointLoader?.cancel()
|
||||
|
||||
chargepoints.value = Resource.loading(chargepoints.value?.data)
|
||||
filteredConnectors.value = null
|
||||
filteredChargeCards.value = null
|
||||
val bounds = mapPosition.bounds
|
||||
val zoom = mapPosition.zoom
|
||||
chargepointLoader = viewModelScope.launch {
|
||||
chargepoints.value = getChargepointsWithFilters(bounds, zoom, filters)
|
||||
val result = getChargepointsWithFilters(bounds, zoom, filters)
|
||||
filteredConnectors.value = result.second
|
||||
filteredChargeCards.value = result.third
|
||||
chargepoints.value = result.first
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,54 +224,133 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
filters: List<FilterWithValue<out FilterValue>>
|
||||
): Resource<List<ChargepointListItem>> {
|
||||
val freecharging =
|
||||
(filters.find { it.value.key == "freecharging" }!!.value as BooleanFilterValue).value
|
||||
val freeparking =
|
||||
(filters.find { it.value.key == "freeparking" }!!.value as BooleanFilterValue).value
|
||||
val minPower =
|
||||
(filters.find { it.value.key == "min_power" }!!.value as SliderFilterValue).value
|
||||
val minConnectors =
|
||||
(filters.find { it.value.key == "min_connectors" }!!.value as SliderFilterValue).value
|
||||
): Triple<Resource<List<ChargepointListItem>>, Set<String>?, Set<Long>?> {
|
||||
val freecharging = getBooleanValue(filters, "freecharging")
|
||||
val freeparking = getBooleanValue(filters, "freeparking")
|
||||
val open247 = getBooleanValue(filters, "open_247")
|
||||
val barrierfree = getBooleanValue(filters, "barrierfree")
|
||||
val excludeFaults = getBooleanValue(filters, "exclude_faults")
|
||||
val minPower = getSliderValue(filters, "min_power")
|
||||
val minConnectors = getSliderValue(filters, "min_connectors")
|
||||
|
||||
val connectorsVal =
|
||||
filters.find { it.value.key == "connectors" }!!.value as MultipleChoiceFilterValue
|
||||
val connectors = if (connectorsVal.all) null else connectorsVal.values.joinToString(",")
|
||||
val connectorsVal = getMultipleChoiceValue(filters, "connectors")
|
||||
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Resource.success(emptyList())
|
||||
return Triple(Resource.success(emptyList()), null, null)
|
||||
}
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
val filteredConnectors = if (connectorsVal.all) null else connectorsVal.values
|
||||
|
||||
val chargeCardsVal = getMultipleChoiceValue(filters, "chargecards")
|
||||
if (chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) {
|
||||
// no chargeCards chosen
|
||||
return Triple(Resource.success(emptyList()), filteredConnectors, null)
|
||||
}
|
||||
val chargeCards = formatMultipleChoice(chargeCardsVal)
|
||||
val filteredChargeCards =
|
||||
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }.toSet()
|
||||
|
||||
val networksVal = getMultipleChoiceValue(filters, "networks")
|
||||
if (networksVal.values.isEmpty() && !networksVal.all) {
|
||||
// no networks chosen
|
||||
return Triple(Resource.success(emptyList()), filteredConnectors, filteredChargeCards)
|
||||
}
|
||||
val networks = formatMultipleChoice(networksVal)
|
||||
|
||||
// do not use clustering if filters need to be applied locally.
|
||||
val useClustering = minConnectors <= 1
|
||||
val useClustering = zoom < 13
|
||||
val geClusteringAvailable = minConnectors <= 1
|
||||
val useGeClustering = useClustering && geClusteringAvailable
|
||||
val clusterDistance = if (useClustering) getClusterDistance(zoom) else null
|
||||
|
||||
val response = api.getChargepoints(
|
||||
bounds.southwest.latitude, bounds.southwest.longitude,
|
||||
bounds.northeast.latitude, bounds.northeast.longitude,
|
||||
clustering = zoom < 13 && useClustering, zoom = zoom,
|
||||
clusterDistance = 40, freecharging = freecharging, minPower = minPower,
|
||||
freeparking = freeparking, plugs = connectors
|
||||
)
|
||||
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
return Resource.error(response.message(), chargepoints.value?.data)
|
||||
} else {
|
||||
val data = response.body()!!.chargelocations.filter { it ->
|
||||
// apply filters which GoingElectric does not support natively
|
||||
if (it is ChargeLocation) {
|
||||
it.chargepoints
|
||||
.filter { it.power >= minPower }
|
||||
.filter { if (!connectorsVal.all) it.type in connectorsVal.values else true }
|
||||
.sumBy { it.count } >= minConnectors
|
||||
var startkey: Int? = null
|
||||
val data = mutableListOf<ChargepointListItem>()
|
||||
do {
|
||||
// load all pages of the response
|
||||
try {
|
||||
val response = api.getChargepoints(
|
||||
bounds.southwest.latitude,
|
||||
bounds.southwest.longitude,
|
||||
bounds.northeast.latitude,
|
||||
bounds.northeast.longitude,
|
||||
clustering = useGeClustering,
|
||||
zoom = zoom,
|
||||
clusterDistance = clusterDistance,
|
||||
freecharging = freecharging,
|
||||
minPower = minPower,
|
||||
freeparking = freeparking,
|
||||
open247 = open247,
|
||||
barrierfree = barrierfree,
|
||||
excludeFaults = excludeFaults,
|
||||
plugs = connectors,
|
||||
chargecards = chargeCards,
|
||||
networks = networks,
|
||||
startkey = startkey
|
||||
)
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
return Triple(
|
||||
Resource.error(response.message(), chargepoints.value?.data),
|
||||
null,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
true
|
||||
val body = response.body()!!
|
||||
data.addAll(body.chargelocations)
|
||||
startkey = body.startkey
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
return Triple(
|
||||
Resource.error(e.message, chargepoints.value?.data),
|
||||
filteredConnectors,
|
||||
filteredChargeCards
|
||||
)
|
||||
}
|
||||
} while (startkey != null && startkey < 10000)
|
||||
|
||||
return Resource.success(data)
|
||||
var result = data.filter { it ->
|
||||
// apply filters which GoingElectric does not support natively
|
||||
if (it is ChargeLocation) {
|
||||
it.chargepoints
|
||||
.filter { it.power >= minPower }
|
||||
.filter { if (!connectorsVal.all) it.type in connectorsVal.values else true }
|
||||
.sumBy { it.count } >= minConnectors
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
if (!geClusteringAvailable && useClustering) {
|
||||
// apply local clustering if server side clustering is not available
|
||||
Dispatchers.IO.run {
|
||||
result = cluster(result, zoom, clusterDistance!!)
|
||||
}
|
||||
}
|
||||
|
||||
return Triple(Resource.success(result), filteredConnectors, filteredChargeCards)
|
||||
}
|
||||
|
||||
private fun formatMultipleChoice(connectorsVal: MultipleChoiceFilterValue) =
|
||||
if (connectorsVal.all) null else connectorsVal.values.joinToString(",")
|
||||
|
||||
private fun getBooleanValue(
|
||||
filters: List<FilterWithValue<out FilterValue>>,
|
||||
key: String
|
||||
) = (filters.find { it.value.key == key }!!.value as BooleanFilterValue).value
|
||||
|
||||
private fun getSliderValue(
|
||||
filters: List<FilterWithValue<out FilterValue>>,
|
||||
key: String
|
||||
) = (filters.find { it.value.key == key }!!.value as SliderFilterValue).value
|
||||
|
||||
private fun getMultipleChoiceFilter(
|
||||
filters: List<FilterWithValue<out FilterValue>>,
|
||||
key: String
|
||||
) = filters.find { it.value.key == key }!!.filter as MultipleChoiceFilter
|
||||
|
||||
private fun getMultipleChoiceValue(
|
||||
filters: List<FilterWithValue<out FilterValue>>,
|
||||
key: String
|
||||
) = filters.find { it.value.key == key }!!.value as MultipleChoiceFilterValue
|
||||
|
||||
private suspend fun loadAvailability(charger: ChargeLocation) {
|
||||
availability.value = Resource.loading(null)
|
||||
availability.value = getAvailability(charger)
|
||||
|
||||
9
app/src/main/res/drawable/expand_toggle.xml
Normal file
9
app/src/main/res/drawable/expand_toggle.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?selectableItemBackgroundBorderless"/>
|
||||
<item android:drawable="@drawable/expand_toggle_icon"
|
||||
android:top="4dp"
|
||||
android:left="4dp"
|
||||
android:right="4dp"
|
||||
android:bottom="4dp"/>
|
||||
</layer-list>
|
||||
5
app/src/main/res/drawable/expand_toggle_icon.xml
Normal file
5
app/src/main/res/drawable/expand_toggle_icon.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="false" android:drawable="@drawable/ic_expand" />
|
||||
<item android:state_checked="true" android:drawable="@drawable/ic_collapse" />
|
||||
</selector>
|
||||
10
app/src/main/res/drawable/ic_add.xml
Normal file
10
app/src/main/res/drawable/ic_add.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||
</vector>
|
||||
49
app/src/main/res/drawable/ic_appicon_splashscreen.xml
Normal file
49
app/src/main/res/drawable/ic_appicon_splashscreen.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="144.3dp"
|
||||
android:height="270.5dp"
|
||||
android:viewportWidth="144.3"
|
||||
android:viewportHeight="270.5">
|
||||
<path
|
||||
android:pathData="M33.9,100l-2.5,-21.7l-3.8,0.4l2.5,21.7L33.9,100zM47.4,98.5l-2.5,-21.7l-3.8,0.4l2.5,21.7L47.4,98.5z"
|
||||
android:fillColor="#FFB300" />
|
||||
<path
|
||||
android:pathData="M54.5,128c-1.2,1.4 -2.1,2.4 -2.2,2.5c-3.4,2.7 -6.1,3.5 -8.4,2.5c-3.9,-2 -3.7,-9.3 -3.5,-10.1l2.7,0.1c-0.1,2.1 0.3,6.5 2.1,7.5c1,0.5 2.9,-0.1 5.2,-2.1l0,0c0,0 7.6,-7.6 6,-13.6c-1.8,-7.2 6.5,-17.5 9.3,-21.1l0.4,-0.4l2.2,1.7l-0.4,0.5c-8.5,10.5 -9.4,15.8 -8.8,18.6C60.5,119.4 57,125 54.5,128z"
|
||||
android:fillColor="#90A4AE" />
|
||||
<path
|
||||
android:pathData="M25.6,99.8l1,8.9l8.2,5.5L46,113l6.8,-7.2l-1,-8.9L25.6,99.8z"
|
||||
android:fillColor="#90A4AE" />
|
||||
<path
|
||||
android:pathData="M45.8,113l-11.1,1.2l2.4,9.8l8.8,-1V113L45.8,113zM53.8,89.4l0.9,8.1l-31.9,3.7l-0.9,-8.1L53.8,89.4z"
|
||||
android:fillColor="#546E7A" />
|
||||
<path
|
||||
android:pathData="M78.8,0C55.9,0 37.3,18.6 37.3,41.5c0,31.3 34.9,47.6 39.1,92.2c0.1,1.3 1.2,2.2 2.5,2.2s2.4,-0.9 2.5,-2.2c4.2,-44.6 39.1,-60.9 39.1,-92.2C120.3,18.4 101.7,0 78.8,0z"
|
||||
android:fillColor="#00E676" />
|
||||
<path
|
||||
android:pathData="M78.8,0.9c22.8,0 41.2,18.3 41.5,40.9c0,-0.1 0,-0.3 0,-0.4C120.3,18.6 101.7,0 78.8,0S37.3,18.4 37.3,41.5c0,0.1 0,0.3 0,0.4C37.6,19.2 56,0.9 78.8,0.9L78.8,0.9z"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.2" />
|
||||
<path
|
||||
android:pathData="M81.3,132.6c-0.1,1.3 -1.2,2.2 -2.5,2.2c-1.3,0 -2.4,-0.9 -2.5,-2.2c-4.1,-44.5 -38.7,-60.8 -39,-91.7c0,0.3 0,0.4 0,0.7c0,31.3 34.9,47.6 39.1,92.2c0.1,1.3 1.2,2.2 2.5,2.2c1.3,0 2.4,-0.9 2.5,-2.2c4.2,-44.6 39.1,-60.9 39.1,-92.2c0,-0.3 0,-0.4 0,-0.7C120,71.8 85.3,88.1 81.3,132.6L81.3,132.6z"
|
||||
android:fillColor="#3E2723"
|
||||
android:fillAlpha="0.2" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M69.3,21.2v25.1h6.8v20.5l16,-27.5h-9.2L92,21.1C92.1,21.2 69.3,21.2 69.3,21.2z"
|
||||
android:strokeAlpha="0.45"
|
||||
android:fillAlpha="0.45" />
|
||||
<path
|
||||
android:fillColor="?android:textColorSecondary"
|
||||
android:pathData="M19.2,244.2H2.8v14.1h18.8v2.4H0v-34.1h21.5v2.4H2.8v12.8h16.4V244.2z" />
|
||||
<path
|
||||
android:fillColor="?android:textColorSecondary"
|
||||
android:pathData="M37.2,254.9l0.7,2.3h0.1l0.7,-2.3L49,226.6h3l-12.7,34.1h-2.6l-12.7,-34.1h3L37.2,254.9z" />
|
||||
<path
|
||||
android:fillColor="?android:textColorSecondary"
|
||||
android:pathData="M60.9,226.6l12.5,30h0.1l12.6,-30h3.7v34.1h-2.8v-15.1l0.2,-14.9l-0.1,0l-12.7,30h-1.9l-12.7,-29.9l-0.1,0l0.3,14.8v15.1h-2.8v-34.1H60.9z" />
|
||||
<path
|
||||
android:fillColor="?android:textColorSecondary"
|
||||
android:pathData="M114.1,260.7c-0.2,-0.9 -0.3,-1.6 -0.4,-2.2s-0.1,-1.3 -0.1,-1.9c-0.9,1.3 -2.2,2.4 -3.8,3.3s-3.3,1.3 -5.3,1.3c-2.5,0 -4.4,-0.7 -5.8,-2s-2.1,-3.1 -2.1,-5.3c0,-2.3 1,-4.2 3,-5.6s4.8,-2.1 8.2,-2.1h5.6v-3.1c0,-1.8 -0.6,-3.2 -1.7,-4.3s-2.8,-1.5 -4.9,-1.5c-2,0 -3.6,0.5 -4.9,1.5s-1.9,2.2 -1.9,3.6l-2.6,0l0,-0.1c-0.1,-1.9 0.8,-3.6 2.6,-5.1s4.1,-2.2 6.9,-2.2c2.8,0 5,0.7 6.8,2.1s2.6,3.5 2.6,6.1v12.5c0,0.9 0.1,1.8 0.2,2.6s0.3,1.7 0.5,2.5H114.1zM104.9,258.7c2,0 3.8,-0.5 5.3,-1.4s2.7,-2.2 3.4,-3.6v-5.3H108c-2.5,0 -4.6,0.5 -6.1,1.6s-2.3,2.4 -2.3,4c0,1.4 0.5,2.5 1.4,3.4S103.3,258.7 104.9,258.7z" />
|
||||
<path
|
||||
android:fillColor="?android:textColorSecondary"
|
||||
android:pathData="M144.3,248.7c0,3.8 -0.9,6.8 -2.6,9.1s-4.1,3.4 -7.1,3.4c-1.8,0 -3.3,-0.3 -4.7,-1s-2.4,-1.6 -3.3,-2.9v13.1h-2.8v-35.1h2.4l0.4,3.9c0.8,-1.4 1.9,-2.5 3.3,-3.3s2.9,-1.1 4.7,-1.1c3,0 5.4,1.2 7.1,3.6s2.6,5.7 2.6,9.7V248.7zM141.5,248.2c0,-3.2 -0.6,-5.8 -1.9,-7.9c-1.3,-2 -3.2,-3 -5.6,-3c-1.9,0 -3.4,0.4 -4.6,1.3c-1.2,0.9 -2.1,2.1 -2.7,3.5v12.2c0.6,1.4 1.6,2.5 2.8,3.3s2.7,1.2 4.5,1.2c2.5,0 4.3,-0.9 5.6,-2.8c1.3,-1.8 1.9,-4.3 1.9,-7.3V248.2z" />
|
||||
</vector>
|
||||
4
app/src/main/res/drawable/ic_chargeprice.xml
Normal file
4
app/src/main/res/drawable/ic_chargeprice.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<vector android:height="15.811624dp" android:viewportHeight="131.5"
|
||||
android:viewportWidth="199.6" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M197.544,65.685l-9.2,-4.8l8,-4.2c2.7,-1.4 2.7,-3.8 0,-5.2l-8.6,-4.5l8.6,-4.5c2.7,-1.4 2.7,-3.8 0,-5.2l-68.9,-36.1c-2.7,-1.4 -7.2,-1.4 -9.9,0l-115.5,59.7c-2.7,1.4 -2.7,3.7 0,5.1l8.8,4.5l-8.8,4.6c-2.7,1.4 -2.7,3.7 0,5.1l9.4,4.8l-8.2,4.3c-2.7,1.4 -2.7,3.7 0,5.1l70.4,36.2c2.7,1.4 7.2,1.4 9.9,0l114,-59.6C200.344,69.385 200.344,67.085 197.544,65.685L197.544,65.685zM123.144,18.785L105.844,38.685c-0.9,1 -0.6,2.3 0.6,2.9l13.7,7.1c1.2,0.6 1.2,1.6 0,2.2l-43.1,22.3c-1.2,0.6 -1.4,0.3 -0.6,-0.7l17.3,-19.9c0.9,-1 0.6,-2.3 -0.6,-2.9l-13.7,-7.1c-1.2,-0.6 -1.2,-1.6 0,-2.2l43.1,-22.3C123.744,17.485 123.944,17.785 123.144,18.785L123.144,18.785z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_collapse.xml
Normal file
10
app/src/main/res/drawable/ic_collapse.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_edit.xml
Normal file
10
app/src/main/res/drawable/ic_edit.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_expand.xml
Normal file
10
app/src/main/res/drawable/ic_expand.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_fault_report.xml
Normal file
10
app/src/main/res/drawable/ic_fault_report.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_layers.xml
Normal file
10
app/src/main/res/drawable/ic_layers.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11.99,18.54l-7.37,-5.73L3,14.07l9,7 9,-7 -1.63,-1.27zM12,16l7.36,-5.73L21,9l-9,-7 -9,7 1.63,1.27L12,16zM12,4.53L17.74,9 12,13.47 6.26,9 12,4.53z" />
|
||||
</vector>
|
||||
12
app/src/main/res/drawable/ic_map_marker.xml
Normal file
12
app/src/main/res/drawable/ic_map_marker.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector android:height="44.11976dp"
|
||||
android:viewportHeight="368.4"
|
||||
android:viewportWidth="233.8"
|
||||
android:width="28dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="#00e676"
|
||||
android:pathData="M109.8,0h13.6c33.9,1.9 67.1,18.5 87.7,45.8c13.5,17.2 21,38.6 22.7,60.3v8.1c-0.8,42.1 -27.7,76.6 -51,109.4c-26.2,37 -50.4,77.3 -57.1,122.9c-1.8,7.7 0.4,18.5 -8.9,22c-2.2,-1.7 -4.7,-3.1 -6.2,-5.4c-2.7,-25.5 -9.1,-50.7 -20,-73.9c-12.3,-27.1 -29.5,-51.6 -47,-75.6C33,199 23,184.2 14.7,168.3c-13,-23.8 -17.9,-51.9 -12.5,-78.6c4.4,-21.1 15.4,-40.6 30.6,-55.7C53.3,14 81.1,1.8 109.8,0zM107.2,74.1c-16.1,3.6 -29.6,17.8 -31,34.5c-1.7,15.5 7.4,31.3 21.3,38.1c15.1,8 34.9,5.5 47.5,-6c11.8,-10.4 16,-28.3 9.9,-42.9C147.6,79.7 126,69.3 107.2,74.1z" />
|
||||
<path
|
||||
android:fillColor="#007e41"
|
||||
android:pathData="M107.2,74.1c18.9,-4.8 40.4,5.5 47.7,23.7c6.1,14.5 1.9,32.5 -9.9,42.9c-12.6,11.5 -32.4,14 -47.5,6c-13.9,-6.8 -23,-22.6 -21.3,-38.1C77.6,92 91.1,77.7 107.2,74.1z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,17 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="28dp"
|
||||
android:height="44.11976dp"
|
||||
android:viewportWidth="233.8"
|
||||
android:viewportHeight="368.4">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.8"
|
||||
android:pathData="M143.2,109.4l-19.7,33.8l0,38.1l43.4,-74.4l-22.2,0z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillAlpha="0.8"
|
||||
android:pathData="M122.2,101.9h16.7h5.7l22.3,-44.6c0,0 -10.2,0 -22.4,0l-1.1,2.2L122.2,101.9z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M138.9,57.3c-9.7,0 -19.8,0 -26.4,0c-2.5,0 -5.1,0 -7.6,0c-8.2,0 -16.1,0 -21.4,0c-4.1,0 -6.6,0 -6.6,0v68.2h18.6v55.8l43.4,-74.4h-24.8L138.9,57.3z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector android:height="44.11976dp"
|
||||
android:viewportHeight="368.4"
|
||||
android:viewportWidth="233.8"
|
||||
android:width="28dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M109.8,0h13.6c33.9,1.9 67.1,18.5 87.7,45.8c13.5,17.2 21,38.6 22.7,60.3v8.1c-0.8,42.1 -27.7,76.6 -51,109.4c-26.2,37 -50.4,77.3 -57.1,122.9c-1.8,7.7 0.4,18.5 -8.9,22c-2.2,-1.7 -4.7,-3.1 -6.2,-5.4c-2.7,-25.5 -9.1,-50.7 -20,-73.9c-12.3,-27.1 -29.5,-51.6 -47,-75.6C33,199 23,184.2 14.7,168.3c-13,-23.8 -17.9,-51.9 -12.5,-78.6C6.6,68.6 17.6,49.1 32.8,34C53.3,14 81.1,1.8 109.8,0z" />
|
||||
<path
|
||||
android:fillColor="#B5B5B5"
|
||||
android:pathData="M143.2,109.4l-19.7,33.8l0,38.1l43.4,-74.4l-22.2,0z" />
|
||||
<path
|
||||
android:fillColor="#B5B5B5"
|
||||
android:pathData="M122.2,101.9h16.7h5.7l22.3,-44.6c0,0 -10.2,0 -22.4,0l-1.1,2.2L122.2,101.9z" />
|
||||
<path
|
||||
android:fillColor="#808080"
|
||||
android:pathData="M138.9,57.3c-9.7,0 -19.8,0 -26.4,0c-2.5,0 -5.1,0 -7.6,0c-8.2,0 -16.1,0 -21.4,0c-4.1,0 -6.6,0 -6.6,0v68.2h18.6v55.8l43.4,-74.4h-24.8L138.9,57.3z" />
|
||||
</vector>
|
||||
12
app/src/main/res/drawable/ic_map_marker_fault.xml
Normal file
12
app/src/main/res/drawable/ic_map_marker_fault.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M 1 21 h 22 L 12 2 L 1 21 z" />
|
||||
<path
|
||||
android:fillColor="#FF9100"
|
||||
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_payment.xml
Normal file
10
app/src/main/res/drawable/ic_payment.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,4L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,6c0,-1.11 -0.89,-2 -2,-2zM20,18L4,18v-6h16v6zM20,8L4,8L4,6h16v2z" />
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_paypal.xml
Normal file
9
app/src/main/res/drawable/ic_paypal.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M20.055,7.713c-0.677,-0.842 -1.673,-1.41 -2.764,-1.615C16.773,3.971 14.877,3 13.045,3H7.057C6.425,3 5.878,3.409 5.689,4.009c-0.015,0.04 -0.026,0.082 -0.036,0.125L3.034,16.262c-0.009,0.041 -0.015,0.083 -0.019,0.125C3.006,16.449 3,16.513 3,16.56C3,17.354 3.648,18 4.444,18h2.316l-0.267,1.262c-0.008,0.04 -0.014,0.081 -0.018,0.121c-0.009,0.063 -0.016,0.126 -0.016,0.173C6.461,20.353 7.109,21 7.905,21h3.259c0.056,0 0.111,-0.005 0.166,-0.015c0.549,-0.063 1.011,-0.437 1.191,-0.963c0.021,-0.05 0.038,-0.103 0.05,-0.156L13.475,16h1.398c3.365,0 5.38,-1.445 5.989,-4.295C21.278,9.752 20.653,8.456 20.055,7.713zM5.137,16L7.512,5h5.533c0.293,0 1.5,0.061 2.078,1.013h-4.706c-0.626,0 -1.17,0.401 -1.363,0.99c-0.019,0.049 -0.033,0.1 -0.043,0.151l-1.034,5.093L7.183,16H5.137zM18.906,11.287C18.5,13.188 17.293,14 14.873,14h-1.857c-0.823,0 -1.338,0.652 -1.405,1.198L10.721,19H8.594l1.271,-6h1.444c4.259,0 5.665,-2.394 6.094,-4.402c0.027,-0.128 0.045,-0.256 0.057,-0.382c0.378,0.151 0.749,0.393 1.038,0.751C18.971,9.557 19.108,10.337 18.906,11.287z"
|
||||
android:fillColor="#000000" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_search.xml
Normal file
10
app/src/main/res/drawable/ic_search.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
|
||||
</vector>
|
||||
8
app/src/main/res/drawable/launch_screen.xml
Normal file
8
app/src/main/res/drawable/launch_screen.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<item
|
||||
android:drawable="@drawable/ic_appicon_splashscreen"
|
||||
android:gravity="center" />
|
||||
</layer-list>
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:name="net.vonforst.evmap.navigation.NavHostFragment"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
|
||||
39
app/src/main/res/layout/amu_text_bubble.xml
Normal file
39
app/src/main/res/layout/amu_text_bubble.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.maps.android.ui.RotationLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@id/amu_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:paddingBottom="5dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="10dp"
|
||||
android:paddingTop="5dp" />
|
||||
|
||||
</com.google.maps.android.ui.RotationLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -9,10 +9,14 @@
|
||||
|
||||
<import type="net.vonforst.evmap.api.goingelectric.Chargepoint" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.goingelectric.ChargeCard" />
|
||||
|
||||
<import type="net.vonforst.evmap.api.availability.ChargeLocationStatus" />
|
||||
|
||||
<import type="net.vonforst.evmap.adapter.DataBindingAdaptersKt" />
|
||||
|
||||
<import type="net.vonforst.evmap.adapter.DetailsAdapterKt" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.Resource" />
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.Status" />
|
||||
@@ -25,12 +29,22 @@
|
||||
name="availability"
|
||||
type="Resource<ChargeLocationStatus>" />
|
||||
|
||||
<variable
|
||||
name="chargeCards"
|
||||
type="java.util.Map<Long, ChargeCard>" />
|
||||
|
||||
<variable
|
||||
name="filteredChargeCards"
|
||||
type="java.util.Set<Long>" />
|
||||
|
||||
</data>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardCornerRadius="@dimen/detail_corner_radius"
|
||||
android:layout_marginBottom="@dimen/detail_corner_radius_negative"
|
||||
android:paddingBottom="@dimen/detail_corner_radius"
|
||||
app:cardElevation="6dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
@@ -125,6 +139,8 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@{charger.data.amenities}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:autoLink="web"
|
||||
android:linksClickable="true"
|
||||
app:goneUnless="@{charger.data.amenities != null}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
@@ -153,6 +169,8 @@
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@{charger.data.generalInformation}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:autoLink="web"
|
||||
android:linksClickable="true"
|
||||
app:goneUnless="@{charger.data.generalInformation != null}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
@@ -180,11 +198,11 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:data="@{DataBindingAdaptersKt.buildDetails(charger.data, context)}"
|
||||
app:data="@{DetailsAdapterKt.buildDetails(charger.data, chargeCards, filteredChargeCards, context)}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView13"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btnChargeprice"
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/item_detail" />
|
||||
|
||||
@@ -230,6 +248,18 @@
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toTopOf="@+id/textView" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnChargeprice"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/go_to_chargeprice"
|
||||
app:icon="@drawable/ic_chargeprice"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView13" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
94
app/src/main/res/layout/dialog_multi_select.xml
Normal file
94
app/src/main/res/layout/dialog_multi_select.xml
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialogTitle"
|
||||
style="@style/MaterialAlertDialog.MaterialComponents.Title.Text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnAll"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Select Something" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/btnOK"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tilSearch" />
|
||||
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnAll"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/all"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/dialogTitle"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnNone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnOK"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/ok"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnCancel"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/cancel"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnOK" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnNone"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/none"
|
||||
app:layout_constraintBaseline_toBaselineOf="@+id/btnAll"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilSearch"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:hint="@string/search"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/btnAll"
|
||||
app:startIconDrawable="@drawable/ic_search">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etSearch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionDone"
|
||||
android:singleLine="true" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
27
app/src/main/res/layout/dialog_multi_select_item.xml
Normal file
27
app/src/main/res/layout/dialog_multi_select_item.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.fragment.MultiSelectItem" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="MultiSelectItem" />
|
||||
</data>
|
||||
|
||||
<CheckBox
|
||||
android:id="@android:id/text1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/listPreferredItemHeightSmall"
|
||||
android:layout_marginStart="?attr/dialogPreferredPadding"
|
||||
android:layout_marginEnd="?attr/dialogPreferredPadding"
|
||||
android:paddingStart="20dp"
|
||||
android:ellipsize="marquee"
|
||||
android:text="@{item.name}"
|
||||
android:checked="@={item.selected}"
|
||||
tools:text="Item"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
</layout>
|
||||
@@ -19,12 +19,10 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<fragment
|
||||
<FrameLayout
|
||||
android:id="@+id/map"
|
||||
android:name="com.google.android.gms.maps.SupportMapFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MapsActivity" />
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
@@ -120,13 +118,15 @@
|
||||
app:behavior_peekHeight="@dimen/peek_height"
|
||||
app:bottomsheetbehavior_defaultState="stateHidden"
|
||||
app:layout_behavior="@string/BottomSheetBehaviorGoogleMapsLike"
|
||||
tools:bottomsheetbehavior_defaultState="stateAnchorPoint">
|
||||
tools:bottomsheetbehavior_defaultState="stateHidden">
|
||||
|
||||
<include
|
||||
android:id="@+id/detail_view"
|
||||
layout="@layout/detail_view"
|
||||
app:charger="@{vm.charger}"
|
||||
app:availability="@{vm.availability}" />
|
||||
app:availability="@{vm.availability}"
|
||||
app:chargeCards="@{vm.chargeCardMap}"
|
||||
app:filteredChargeCards="@{vm.filteredChargeCards}" />
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
@@ -148,5 +148,39 @@
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="@string/MergedAppBarLayoutBehavior"
|
||||
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab_layers"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginTop="96dp"
|
||||
android:elevation="-1dp"
|
||||
app:tint="?android:colorControlNormal"
|
||||
app:backgroundTint="?android:colorBackground"
|
||||
app:borderWidth="0dp"
|
||||
app:fabSize="mini"
|
||||
app:srcCompat="@drawable/ic_layers"
|
||||
app:layout_behavior="net.vonforst.evmap.ui.HideOnExpandFabBehavior"/>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/layers_sheet"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="200dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginTop="96dp"
|
||||
android:elevation="-1dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<include
|
||||
android:id="@+id/layers"
|
||||
layout="@layout/map_layers"
|
||||
app:vm="@{vm}" />
|
||||
</androidx.cardview.widget.CardView>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
@@ -8,12 +8,14 @@
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="net.vonforst.evmap.adapter.DetailAdapter.Detail" />
|
||||
type="net.vonforst.evmap.adapter.DetailsAdapter.Detail" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="@{item.clickable}"
|
||||
app:selectableItemBackground="@{item.clickable}">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView9"
|
||||
@@ -23,7 +25,6 @@
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:maxLines="1"
|
||||
android:text="@{item.text}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
@@ -40,7 +41,7 @@
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:contentDescription="@{item.contentDescription}"
|
||||
android:tint="?colorPrimary"
|
||||
app:tint="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@{item.icon}"
|
||||
|
||||
203
app/src/main/res/layout/item_detail_openinghours.xml
Normal file
203
app/src/main/res/layout/item_detail_openinghours.xml
Normal file
@@ -0,0 +1,203 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="android.text.util.Linkify" />
|
||||
|
||||
<import type="java.time.DayOfWeek" />
|
||||
|
||||
<import type="android.transition.TransitionManager" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="net.vonforst.evmap.adapter.DetailsAdapter.Detail" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="@{item.clickable}"
|
||||
app:selectableItemBackground="@{item.clickable}">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView9"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="18dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:text="@{item.text}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/expandToggle"
|
||||
app:layout_constraintStart_toEndOf="@+id/imageView3"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="Lorem ipsum" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:contentDescription="@{item.contentDescription}"
|
||||
app:tint="?colorPrimary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@{item.icon}"
|
||||
tools:srcCompat="@drawable/ic_address" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView8"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:autoLink="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
|
||||
android:linksClickable="@{item.links}"
|
||||
android:text="@{item.detailText}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:goneUnless="@{item.detailText != null}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/expandToggle"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView9"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="Lorem ipsum" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_mon"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:dayOfWeek="@{DayOfWeek.MONDAY}"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView8" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_tue"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:dayOfWeek="@{DayOfWeek.TUESDAY}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@id/hours_mon" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_wed"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:dayOfWeek="@{DayOfWeek.WEDNESDAY}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@id/hours_tue" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_thu"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:dayOfWeek="@{DayOfWeek.THURSDAY}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@id/hours_wed" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_fri"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:dayOfWeek="@{DayOfWeek.FRIDAY}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@id/hours_thu" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_sat"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:dayOfWeek="@{DayOfWeek.SATURDAY}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@id/hours_fri" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_sun"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:dayOfWeek="@{DayOfWeek.SUNDAY}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@id/hours_sat" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hours_holiday"
|
||||
layout="@layout/item_detail_openinghours_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:goneUnless="@{expandToggle.checked}"
|
||||
app:dayOfWeek="@{null}"
|
||||
app:hours="@{item.hoursDays}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView9"
|
||||
app:layout_constraintTop_toBottomOf="@id/hours_sun" />
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/expandToggle"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginTop="@{item.detailText != null ? @dimen/expand_toggle_padding_large : @dimen/expand_toggle_padding_small}"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/expand_toggle"
|
||||
android:onClick="@{() -> TransitionManager.beginDelayedTransition(container)}"
|
||||
android:textOff=""
|
||||
android:textOn=""
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
48
app/src/main/res/layout/item_detail_openinghours_item.xml
Normal file
48
app/src/main/res/layout/item_detail_openinghours_item.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="java.time.format.TextStyle" />
|
||||
|
||||
<import type="java.util.Locale" />
|
||||
|
||||
<variable
|
||||
name="hours"
|
||||
type="net.vonforst.evmap.api.goingelectric.OpeningHoursDays" />
|
||||
|
||||
<variable
|
||||
name="dayOfWeek"
|
||||
type="java.time.DayOfWeek" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView24"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{dayOfWeek != null ? dayOfWeek.getDisplayName(TextStyle.FULL, context.resources.configuration.locale) : @string/holiday}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Montag" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView25"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="100dp"
|
||||
android:text="@{hours.getHoursForDayOfWeek(dayOfWeek).toString().equals("closed") ? @string/closed_unfmt : hours.getHoursForDayOfWeek(dayOfWeek).toString()}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="07:00-21:00" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</layout>
|
||||
@@ -34,10 +34,12 @@
|
||||
android:id="@+id/textView2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@{item.charger.address.toString()}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintEnd_toStartOf="@+id/textView7"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView15"
|
||||
tools:text="Beispielstraße 10, 12345 Berlin" />
|
||||
@@ -46,10 +48,12 @@
|
||||
android:id="@+id/textView3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@{item.charger.formatChargepoints()}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintEnd_toStartOf="@+id/textView7"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView2"
|
||||
tools:text="2x Typ 2 22 kW" />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user