diff --git a/README.md b/README.md
index 6a70dc3..f170b18 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ The implementation follows [Google's officical specifications][google-api-specs]
## Features
-* [x] Ensure that Google's requirements for a valid url are met (except URL length)
+* [x] Ensure that Google's requirements for a valid url are met
* [x] Support all possbile [parameters][google-api-params]
* [x] Automatic check that map size is within [bounds][google-api-imagesize] (supports [premium plan][google-maps-premium])
* [x] Typesafe paremters: center, markers, path, viewport, zoom level, scale, map type, image format
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
index 5518c1f..7adda6c 100644
--- a/android/build.gradle.kts
+++ b/android/build.gradle.kts
@@ -9,11 +9,11 @@ plugins {
}
android {
- compileSdkVersion(28)
+ compileSdkVersion(29)
defaultConfig {
minSdkVersion(21)
- targetSdkVersion(28)
+ targetSdkVersion(29)
versionCode = 1
versionName = "1.0"
}
@@ -50,7 +50,5 @@ publishing {
dependencies {
implementation(Libs.kotlin_stdlib_jdk8)
- implementation("androidx.appcompat:appcompat:1.1.0")
- implementation("androidx.core:core-ktx:1.1.0")
testImplementation(Libs.junit)
}
diff --git a/android/src/androidTest/java/com/ivoberger/statikgmaps/android/ExampleInstrumentedTest.kt b/android/src/androidTest/java/com/ivoberger/statikgmaps/android/ExampleInstrumentedTest.kt
deleted file mode 100644
index e09d5b4..0000000
--- a/android/src/androidTest/java/com/ivoberger/statikgmaps/android/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.ivoberger.statikgmaps.android
-
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.ivoberger.statikgmaps.android.test", appContext.packageName)
- }
-}
diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
deleted file mode 100644
index 64fad88..0000000
--- a/android/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
- android
-
diff --git a/android/src/test/java/com/ivoberger/statikgmaps/android/ExampleUnitTest.kt b/android/src/test/java/com/ivoberger/statikgmaps/android/ExampleUnitTest.kt
deleted file mode 100644
index dfaca53..0000000
--- a/android/src/test/java/com/ivoberger/statikgmaps/android/ExampleUnitTest.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.ivoberger.statikgmaps.android
-
-import org.junit.Test
-
-import org.junit.Assert.*
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
- @Test
- fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
- }
-}
diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt
index ded64a7..14ac296 100644
--- a/buildSrc/src/main/kotlin/Libs.kt
+++ b/buildSrc/src/main/kotlin/Libs.kt
@@ -7,16 +7,6 @@ import kotlin.String
* `$ ./gradlew buildSrcVersions`
*/
object Libs {
- /**
- * https://developer.android.com/jetpack/androidx
- */
- const val appcompat: String = "androidx.appcompat:appcompat:" + Versions.appcompat
-
- /**
- * http://developer.android.com/tools/extras/support-library.html
- */
- const val core_ktx: String = "androidx.core:core-ktx:" + Versions.core_ktx
-
/**
* https://developer.android.com/studio
*/
@@ -37,6 +27,12 @@ object Libs {
"de.fayard.buildSrcVersions:de.fayard.buildSrcVersions.gradle.plugin:" +
Versions.de_fayard_buildsrcversions_gradle_plugin
+ /**
+ * https://github.com/wupdigital/android-maven-publish
+ */
+ const val android_maven_publish: String = "digital.wup:android-maven-publish:" +
+ Versions.android_maven_publish
+
/**
* http://junit.org
*/
diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt
index 4244243..c8312f2 100644
--- a/buildSrc/src/main/kotlin/Versions.kt
+++ b/buildSrc/src/main/kotlin/Versions.kt
@@ -12,10 +12,6 @@ import org.gradle.plugin.use.PluginDependencySpec
* YOU are responsible for updating manually the dependency version.
*/
object Versions {
- const val appcompat: String = "1.1.0"
-
- const val core_ktx: String = "1.1.0"
-
const val aapt2: String = "3.5.0-5435860"
const val com_android_tools_build_gradle: String = "3.5.0"
@@ -24,6 +20,8 @@ object Versions {
const val de_fayard_buildsrcversions_gradle_plugin: String = "0.4.2"
+ const val android_maven_publish: String = "3.6.2"
+
const val junit: String = "4.12"
const val org_jetbrains_dokka: String = "0.9.18"
diff --git a/core/src/main/java/com/ivoberger/statikgmapsapi/core/Location.kt b/core/src/main/java/com/ivoberger/statikgmapsapi/core/Location.kt
new file mode 100644
index 0000000..7b80131
--- /dev/null
+++ b/core/src/main/java/com/ivoberger/statikgmapsapi/core/Location.kt
@@ -0,0 +1,40 @@
+package com.ivoberger.statikgmapsapi.core
+
+/**
+ * Container to hold a location either by latitude and longitude or an address
+ * Latitude and longitude will be checked for validity, addresses won't.
+ */
+data class Location(
+ val latitude: Double? = null,
+ val longitude: Double? = null,
+ val address: String? = null
+) {
+ init {
+ // require coordinates to be set and in their valid ranges or an address to be set
+ require(
+ (latitude != null && longitude != null) || address != null
+ ) { "A location must be specified by latitude and longitude or a valid address" }
+ require(
+ (latitude != null && longitude != null && latitude in -90.0..90.0 && longitude in -180.0..180.0)
+ || address != null
+ ) { "Latitude must be between -90 and 90, longitude between -180 and 180" }
+ }
+
+ override fun toString() = address ?: "$latitude,$longitude"
+}
+
+
+/**
+ * Creates a [Location] from a [Pair] of [Double]
+ */
+fun Pair.toLocation() = Location(first, second)
+
+/**
+ * Creates a [List] of [Location] from a [List] of [Pair] of [Double]
+ */
+fun List>.toLocations() = map { it.toLocation() }
+
+
+internal fun List.toUrlParam(): String = fold("") { param, pair ->
+ "${if (param.isNotBlank()) "$param|" else ""}${pair.latitude},${pair.longitude}"
+}
diff --git a/core/src/main/java/com/ivoberger/statikgmapsapi/core/PolylineUtil.kt b/core/src/main/java/com/ivoberger/statikgmapsapi/core/PolylineUtil.kt
index 71195dc..5d0c6f4 100644
--- a/core/src/main/java/com/ivoberger/statikgmapsapi/core/PolylineUtil.kt
+++ b/core/src/main/java/com/ivoberger/statikgmapsapi/core/PolylineUtil.kt
@@ -1,5 +1,9 @@
package com.ivoberger.statikgmapsapi.core
+import kotlin.math.abs
+import kotlin.math.pow
+import kotlin.math.sqrt
+
/**
* Encodes a polyline using Google's polyline algorithm
@@ -8,15 +12,15 @@ package com.ivoberger.statikgmapsapi.core
* @receiver path as pair of doubles
* @return encoded polyline
*/
-fun List>.encode(): String {
+fun List.encode(): String {
val result: MutableList = mutableListOf()
var prevLat = 0
var prevLong = 0
for ((lat, lng) in this) {
- val iLat = (lat * 1e5).toInt()
- val iLong = (lng * 1e5).toInt()
+ val iLat = (lat!! * 1e5).toInt()
+ val iLong = (lng!! * 1e5).toInt()
val deltaLat = encodeValue(iLat - prevLat)
val deltaLong = encodeValue(iLong - prevLong)
@@ -54,4 +58,46 @@ private fun splitIntoChunks(toEncode: Int): List {
return chunks
}
+/**
+ * https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
+ */
+internal fun List.simplify(epsilon: Double): List {
+ // Find the point with the maximum distance
+ var dmax = 0.0
+ var index = 0
+ val end = this.size
+
+ for (i in 1..(end - 2)) {
+ val d = perpendicularDistance(this[i], this[0], this[end - 1])
+ if (d > dmax) {
+ index = i
+ dmax = d
+ }
+ }
+ // If max distance is greater than epsilon, recursively simplify
+ return if (dmax > epsilon) {
+ // Recursive call
+ val recResults1: List = subList(0, index + 1).simplify(epsilon)
+ val recResults2: List = subList(index, end).simplify(epsilon)
+
+ // Build the result list
+ listOf(recResults1.subList(0, recResults1.lastIndex), recResults2).flatMap { it.toList() }
+ } else {
+ listOf(this[0], this[end - 1])
+ }
+}
+
+private fun perpendicularDistance(
+ pt: Location, lineFrom: Location, lineTo: Location
+): Double =
+ abs(
+ (lineTo.longitude!! - lineFrom.longitude!!)
+ * (lineFrom.latitude!! - pt.latitude!!)
+ - (lineFrom.longitude - pt.longitude!!)
+ * (lineTo.latitude!! - lineFrom.latitude)
+ ) / sqrt(
+ (lineTo.longitude - lineFrom.longitude).pow(2.0)
+ + (lineTo.latitude - lineFrom.latitude).pow(2.0)
+ )
+
diff --git a/core/src/main/java/com/ivoberger/statikgmapsapi/core/StatikGMapsUrl.kt b/core/src/main/java/com/ivoberger/statikgmapsapi/core/StatikGMapsUrl.kt
index 6750f1d..d0552fe 100644
--- a/core/src/main/java/com/ivoberger/statikgmapsapi/core/StatikGMapsUrl.kt
+++ b/core/src/main/java/com/ivoberger/statikgmapsapi/core/StatikGMapsUrl.kt
@@ -79,7 +79,7 @@ class StatikGMapsUrl(
* This parameter takes a location as either latitude to longitude pair
* For more information, see TODO Create location class to accommodate both coordinates and addresses.
*/
- var center: Pair? = null
+ var center: Location? = null
/**
* (required if markers, path or visible not present)
* defines the zoom level of the map, which determines the magnification level of the map.
@@ -88,13 +88,15 @@ class StatikGMapsUrl(
*/
var zoom: Int? = null
- var markers: List> = listOf()
- var path: List> = listOf()
- var visible: List> = listOf()
+ var markers: List = listOf()
+ var path: List = listOf()
+ var visible: List = listOf()
var encodePath: Boolean = false
+ var simplifyPath = false
private val maxSizeStandard = 640
private val maxSizePremium = 2048
+ private val maxUrlLength = 8192
override fun toString(): String {
setUp()
@@ -113,7 +115,7 @@ class StatikGMapsUrl(
) { "Values for center and zoom or markers, path or visible are required" }
require(zoom == null || zoom!! in 0..20) { "zoom values are required to be >= 0 and <= 20" }
- val params: MutableList> = mutableListOf()
+ val params: MutableMap = mutableMapOf()
params += "key" to apiKey
@@ -138,9 +140,22 @@ class StatikGMapsUrl(
}
if (markers.isNotEmpty()) params += "markers" to markers.toUrlParam()
- if (path.isNotEmpty()) if (encodePath) params += "path" to "enc:${path.encode()}" else params += "path" to path.toUrlParam()
+ if (path.isNotEmpty()) params += "path" to if (encodePath) "enc:${path.encode()}" else path.toUrlParam()
if (visible.isNotEmpty()) params += "visible" to visible.toUrlParam()
+ val url = makeUrl(params.toList())
+
+ require(url.length <= maxUrlLength /* || simplifyPath */) {
+ val msg =
+ "The resulting url violated Google's length restriction and automatic simplification is off."
+ if (!encodePath) "$msg Encoding the path can help reduce url length"
+ else msg
+ }
+
+ return url
+ }
+
+ private fun makeUrl(params: List>): String {
var url = "${if (https) "https" else "http"}://$baseUrl"
url = "$url${params.filter { it.second != null }.joinToString(
"&",
@@ -156,14 +171,25 @@ class StatikGMapsUrl(
return url
}
+ private fun simplifyPath(url: String, params: MutableMap): String {
+ var epsilon = .1
+ var simplifiedUrl = url
+ var simplifiedPath = path
+
+ while (simplifiedUrl.length > maxUrlLength) {
+ simplifiedPath = simplifiedPath.simplify(epsilon)
+ params["path"] =
+ if (encodePath) "enc:${simplifiedPath.encode()}" else simplifiedPath.toUrlParam()
+ simplifiedUrl = makeUrl(params.toList())
+ epsilon += epsilon / 2
+ }
+ return simplifiedUrl
+ }
+
private fun downscale() {
val maxAllowedSize: Int = if (premiumPlan) maxSizePremium else maxSizeStandard
val maxActualSize = size!!.toList().max()!!
val scaleFactor: Float = maxAllowedSize / maxActualSize.toFloat()
size = (size!!.first * scaleFactor).toInt() to (size!!.second * scaleFactor).toInt()
}
-
- private fun List>.toUrlParam(): String = fold("") { param, pair ->
- "${if (param.isNotBlank()) "$param|" else ""}${pair.first},${pair.second}"
- }
}
diff --git a/core/src/test/kotlin/com/ivoberger/statikgmapsapi/core/DownscaleTest.kt b/core/src/test/kotlin/com/ivoberger/statikgmapsapi/core/DownscaleTest.kt
index a57a574..4fbc3f8 100644
--- a/core/src/test/kotlin/com/ivoberger/statikgmapsapi/core/DownscaleTest.kt
+++ b/core/src/test/kotlin/com/ivoberger/statikgmapsapi/core/DownscaleTest.kt
@@ -9,7 +9,7 @@ class DownscaleTest {
var origSize = 300 to 170
val url = StatikGMapsUrl("placeholder") {
size = origSize
- center = .0 to .0
+ center = (.0 to .0).toLocation()
zoom = 14
}
url.toString()
@@ -28,7 +28,7 @@ class DownscaleTest {
val origRatio = origSize.first / origSize.second.toFloat()
val url = StatikGMapsUrl("placeholder") {
size = origSize
- center = .0 to .0
+ center = (.0 to .0).toLocation()
zoom = 14
}
url.toString()
@@ -43,7 +43,7 @@ class DownscaleTest {
val origRatio = origSize.first / origSize.second.toFloat()
val url = StatikGMapsUrl("placeholder") {
size = origSize
- center = .0 to .0
+ center = (.0 to .0).toLocation()
zoom = 14
scale = 4
premiumPlan = true
diff --git a/core/src/test/kotlin/com/ivoberger/statikgmapsapi/core/PathSimplificationTest.kt b/core/src/test/kotlin/com/ivoberger/statikgmapsapi/core/PathSimplificationTest.kt
new file mode 100644
index 0000000..bf2ea73
--- /dev/null
+++ b/core/src/test/kotlin/com/ivoberger/statikgmapsapi/core/PathSimplificationTest.kt
@@ -0,0 +1,35 @@
+package com.ivoberger.statikgmapsapi.core
+
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class PathSimplificationTest {
+
+ @Test
+ fun notNecessary() {
+ val url = StatikGMapsUrl("placeholder") {
+ size = 300 to 170
+ path = listOf(
+ Location(.0, .0),
+ Location(.0, 10.0),
+ Location(5.0, .0),
+ Location(7.0, .6)
+ )
+ }
+ assertTrue(url.toString().endsWith(url.path.toUrlParam()))
+ }
+
+ @Test
+ fun compressionOnly() {
+ val url = StatikGMapsUrl("placeholder") {
+ size = 300 to 170
+ path = listOf(
+ Location(.0, .0),
+ Location(.0, 10.0),
+ Location(5.0, .0),
+ Location(7.0, .6)
+ )
+ }
+ assertTrue(url.toString().endsWith(url.path.toUrlParam()))
+ }
+}