Skip to content

Commit

Permalink
adds url length check
Browse files Browse the repository at this point in the history
  • Loading branch information
ivoberger committed Sep 10, 2019
1 parent 5f09dc2 commit 7f52b05
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 79 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ plugins {
}

android {
compileSdkVersion(28)
compileSdkVersion(29)

defaultConfig {
minSdkVersion(21)
targetSdkVersion(28)
targetSdkVersion(29)
versionCode = 1
versionName = "1.0"
}
Expand Down Expand Up @@ -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)
}

This file was deleted.

3 changes: 0 additions & 3 deletions android/src/main/res/values/strings.xml

This file was deleted.

This file was deleted.

16 changes: 6 additions & 10 deletions buildSrc/src/main/kotlin/Libs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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
*/
Expand Down
6 changes: 2 additions & 4 deletions buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
40 changes: 40 additions & 0 deletions core/src/main/java/com/ivoberger/statikgmapsapi/core/Location.kt
Original file line number Diff line number Diff line change
@@ -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<Double, Double>.toLocation() = Location(first, second)

/**
* Creates a [List] of [Location] from a [List] of [Pair] of [Double]
*/
fun List<Pair<Double, Double>>.toLocations() = map { it.toLocation() }


internal fun List<Location>.toUrlParam(): String = fold("") { param, pair ->
"${if (param.isNotBlank()) "$param|" else ""}${pair.latitude},${pair.longitude}"
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -8,15 +12,15 @@ package com.ivoberger.statikgmapsapi.core
* @receiver path as pair of doubles
* @return encoded polyline
*/
fun List<Pair<Double, Double>>.encode(): String {
fun List<Location>.encode(): String {
val result: MutableList<String> = 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)
Expand Down Expand Up @@ -54,4 +58,46 @@ private fun splitIntoChunks(toEncode: Int): List<Int> {
return chunks
}

/**
* https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
*/
internal fun List<Location>.simplify(epsilon: Double): List<Location> {
// 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<Location> = subList(0, index + 1).simplify(epsilon)
val recResults2: List<Location> = 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)
)


Original file line number Diff line number Diff line change
Expand Up @@ -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<Double, Double>? = 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.
Expand All @@ -88,13 +88,15 @@ class StatikGMapsUrl(
*/
var zoom: Int? = null

var markers: List<Pair<Double, Double>> = listOf()
var path: List<Pair<Double, Double>> = listOf()
var visible: List<Pair<Double, Double>> = listOf()
var markers: List<Location> = listOf()
var path: List<Location> = listOf()
var visible: List<Location> = 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()
Expand All @@ -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<Pair<String, Any?>> = mutableListOf()
val params: MutableMap<String, Any?> = mutableMapOf()

params += "key" to apiKey

Expand All @@ -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<Pair<String, Any?>>): String {
var url = "${if (https) "https" else "http"}://$baseUrl"
url = "$url${params.filter { it.second != null }.joinToString(
"&",
Expand All @@ -156,14 +171,25 @@ class StatikGMapsUrl(
return url
}

private fun simplifyPath(url: String, params: MutableMap<String, Any?>): 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<Pair<Double, Double>>.toUrlParam(): String = fold("") { param, pair ->
"${if (param.isNotBlank()) "$param|" else ""}${pair.first},${pair.second}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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()))
}
}

0 comments on commit 7f52b05

Please sign in to comment.