Loading play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Styles.kt +85 −53 Original line number Diff line number Diff line Loading @@ -22,6 +22,14 @@ const val TAG = "GmsMapStyles" const val KEY_METADATA_FEATURE_TYPE = "microg:gms-type-feature" const val KEY_METADATA_ELEMENT_TYPE = "microg:gms-type-element" const val SELECTOR_ALL = "all" const val SELECTOR_ELEMENT_LABEL_TEXT_FILL = "labels.text.fill" const val SELECTOR_ELEMENT_LABEL_TEXT_OUTLINE = "labels.text.outline" const val KEY_LAYER_METADATA = "metadata" const val KEY_LAYER_PAINT = "paint" fun getStyle(context: MapContext, storedMapType: Int, styleOptions: MapStyleOptions?): Style.Builder { // TODO: Serve map style resources locally Loading @@ -48,58 +56,22 @@ fun MapStyleOptions.apply(style: JSONObject) { val layerArray = style.getJSONArray("layers") // Apply operations in order operations@ for (operation in styleOperations.map { // Fill in default values for optional fields NonNullStyleOperation(it.featureType ?: "all", it.elementType ?: "all", it.stylers ?: emptyArray()) }) { operations@ for (operation in styleOperations.map { it.toNonNull() }) { // Reverse direction allows removing hidden layers layers@ for (i in layerArray.length() - 1 downTo 0) { // Test if layer has required fields (metadata and paint) val layer = layerArray.getJSONObject(i) if (layer.has("paint") && layer.has("metadata") && layer.getJSONObject("metadata") .let { it.has(KEY_METADATA_FEATURE_TYPE) && it.has(KEY_METADATA_ELEMENT_TYPE) } ) { val layerFeatureType = layer.getJSONObject("metadata").getString(KEY_METADATA_FEATURE_TYPE) val layerElementType = layer.getJSONObject("metadata").getString(KEY_METADATA_ELEMENT_TYPE) if (operation.featureType.startsWith("administrative") && operation.elementType.startsWith("geometry") ) { /* Per docs: * `administrative` selects all administrative areas. Styling affects only * the labels of administrative areas, not the geographical borders or fill. */ continue@operations } // Layer metadata always has the most concrete category; operation applies to all subcategories as well. if ((layerFeatureType.startsWith(operation.featureType) || operation.featureType == "all") && (layerElementType.startsWith(operation.elementType) || operation.elementType == "all") ) { // Here, operation should be applied to this layer. if (layer.layerHasRequiredFields()) { if (operation.isValid() && layer.matchesOperation(operation)) { Log.v(TAG, "applying ${Gson().toJson(operation)} to $layer") // Interpretation of visibility "simplified": hide labels, display geometry if ( // A styler sets the layer to be invisible operation.stylers.any { it.visibility == "off" } || // A styler sets the layer to simplified and we are working with a label (layerElementType.startsWith("labels") && operation.stylers.any { it.visibility == "simplified" }) ) { layerArray.remove(i) if (layer.layerShouldBeRemoved(operation)) { Log.v(TAG, "removing $layer") continue@layers } operation.stylers.forEach { styler -> when (operation.elementType) { "labels.text.fill" -> styler.applyTextFill(layer.getJSONObject("paint")) "labels.text.outline" -> styler.applyTextOutline(layer.getJSONObject("paint")) else -> styler.traverseApply(layer.getJSONObject("paint")) } layerArray.remove(i) } else { layer.applyOperation(operation) } } } Loading Loading @@ -128,6 +100,66 @@ class Styler( val weight: Int? ) /** * Constructs a `NonNullStyleOperation` out of the `StyleOperation` while filling null fields with * default values. */ fun StyleOperation.toNonNull() = NonNullStyleOperation(featureType ?: SELECTOR_ALL, elementType ?: SELECTOR_ALL, stylers ?: emptyArray()) /** * Returns false iff the operation is invalid. * * There is one invalid selector that is tested for – per docs: * "`administrative` selects all administrative areas. Styling affects only * the labels of administrative areas, not the geographical borders or fill." */ fun NonNullStyleOperation.isValid() = !(featureType.startsWith("administrative") && elementType.startsWith("geometry")) /** * True iff the layer represented by the JSON object should be modified according to the stylers in the operation. * * Layer metadata always has the most concrete category, while operation applies to all subcategories as well. * Therefore, we test if the operation is a substring of the layer's metadata – i.e. the layer's metadata contains * (more concretely: starts with) the operation's selector. */ fun JSONObject.matchesOperation(operation: NonNullStyleOperation) = (getJSONObject(KEY_LAYER_METADATA).getString(KEY_METADATA_FEATURE_TYPE).startsWith(operation.featureType) || operation.featureType == "all") && (getJSONObject(KEY_LAYER_METADATA).getString(KEY_METADATA_ELEMENT_TYPE).startsWith(operation.elementType) || operation.elementType == "all") /** * Layer has fields that allow applying style operations. */ fun JSONObject.layerHasRequiredFields() = has(KEY_LAYER_PAINT) && has(KEY_LAYER_METADATA) && getJSONObject(KEY_LAYER_METADATA).let { it.has(KEY_METADATA_FEATURE_TYPE) && it.has(KEY_METADATA_ELEMENT_TYPE) } /** * True iff the layer represented by the JSON object should be removed according to the provided style operation. * * Interpretation of visibility "simplified": hide labels, display geometry. */ fun JSONObject.layerShouldBeRemoved(operation: NonNullStyleOperation) = // A styler sets the layer to be invisible operation.stylers.any { it.visibility == "off" } || // A styler sets the layer to simplified and we are working with a label (getJSONObject("metadata").getString(KEY_METADATA_ELEMENT_TYPE) .startsWith("labels") && operation.stylers.any { it.visibility == "simplified" }) /** * Applies the provided style operation to the layer represented by the JSON object. */ fun JSONObject.applyOperation(operation: NonNullStyleOperation) = operation.stylers.forEach { styler -> when (operation.elementType) { SELECTOR_ELEMENT_LABEL_TEXT_FILL -> styler.applyTextFill(getJSONObject(KEY_LAYER_PAINT)) SELECTOR_ELEMENT_LABEL_TEXT_OUTLINE -> styler.applyTextOutline(getJSONObject(KEY_LAYER_PAINT)) else -> styler.traverse(getJSONObject(KEY_LAYER_PAINT)) } } /** * Returns true if string is likely to contain a color. */ Loading Loading @@ -283,13 +315,13 @@ fun Styler.applyColorChanges(color: Int): Int { /** * Traverse JSON object and replace any color strings according to styler */ fun Styler.traverseApply(json: JSONObject) { fun Styler.traverse(json: JSONObject) { // Traverse layer and replace any color strings json.keys().forEach { key -> json.get(key).let { when (it) { is JSONObject -> traverseApply(it) is JSONArray -> traverseApply(it) is JSONObject -> traverse(it) is JSONArray -> traverse(it) is String -> if (it.isColor()) { json.put(key, applyColorChanges(it.parseColor()).colorToString()) } Loading @@ -301,12 +333,12 @@ fun Styler.traverseApply(json: JSONObject) { /** * Traverse array and replace any color strings according to styler */ fun Styler.traverseApply(array: JSONArray) { fun Styler.traverse(array: JSONArray) { for (i in 0 until array.length()) { array.get(i).let { when (it) { is JSONObject -> traverseApply(it) is JSONArray -> traverseApply(it) is JSONObject -> traverse(it) is JSONArray -> traverse(it) is String -> if (it.isColor()) { array.put(i, applyColorChanges(it.parseColor()).colorToString()) } Loading @@ -317,16 +349,16 @@ fun Styler.traverseApply(array: JSONArray) { fun Styler.applyTextFill(paint: JSONObject) { if (paint.has("text-color")) when (val textColor = paint.get("text-color")) { is JSONObject -> traverseApply(textColor) is JSONArray -> traverseApply(textColor) is JSONObject -> traverse(textColor) is JSONArray -> traverse(textColor) is String -> paint.put("text-color", applyColorChanges(textColor.parseColor()).colorToString()) } } fun Styler.applyTextOutline(paint: JSONObject) { if (paint.has("text-halo-color")) when (val textOutline = paint.get("text-halo-color")) { is JSONObject -> traverseApply(textOutline) is JSONArray -> traverseApply(textOutline) is JSONObject -> traverse(textOutline) is JSONArray -> traverse(textOutline) is String -> paint.put("text-halo-color", applyColorChanges(textOutline.parseColor()).colorToString()) } } Loading
play-services-maps-core-mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/Styles.kt +85 −53 Original line number Diff line number Diff line Loading @@ -22,6 +22,14 @@ const val TAG = "GmsMapStyles" const val KEY_METADATA_FEATURE_TYPE = "microg:gms-type-feature" const val KEY_METADATA_ELEMENT_TYPE = "microg:gms-type-element" const val SELECTOR_ALL = "all" const val SELECTOR_ELEMENT_LABEL_TEXT_FILL = "labels.text.fill" const val SELECTOR_ELEMENT_LABEL_TEXT_OUTLINE = "labels.text.outline" const val KEY_LAYER_METADATA = "metadata" const val KEY_LAYER_PAINT = "paint" fun getStyle(context: MapContext, storedMapType: Int, styleOptions: MapStyleOptions?): Style.Builder { // TODO: Serve map style resources locally Loading @@ -48,58 +56,22 @@ fun MapStyleOptions.apply(style: JSONObject) { val layerArray = style.getJSONArray("layers") // Apply operations in order operations@ for (operation in styleOperations.map { // Fill in default values for optional fields NonNullStyleOperation(it.featureType ?: "all", it.elementType ?: "all", it.stylers ?: emptyArray()) }) { operations@ for (operation in styleOperations.map { it.toNonNull() }) { // Reverse direction allows removing hidden layers layers@ for (i in layerArray.length() - 1 downTo 0) { // Test if layer has required fields (metadata and paint) val layer = layerArray.getJSONObject(i) if (layer.has("paint") && layer.has("metadata") && layer.getJSONObject("metadata") .let { it.has(KEY_METADATA_FEATURE_TYPE) && it.has(KEY_METADATA_ELEMENT_TYPE) } ) { val layerFeatureType = layer.getJSONObject("metadata").getString(KEY_METADATA_FEATURE_TYPE) val layerElementType = layer.getJSONObject("metadata").getString(KEY_METADATA_ELEMENT_TYPE) if (operation.featureType.startsWith("administrative") && operation.elementType.startsWith("geometry") ) { /* Per docs: * `administrative` selects all administrative areas. Styling affects only * the labels of administrative areas, not the geographical borders or fill. */ continue@operations } // Layer metadata always has the most concrete category; operation applies to all subcategories as well. if ((layerFeatureType.startsWith(operation.featureType) || operation.featureType == "all") && (layerElementType.startsWith(operation.elementType) || operation.elementType == "all") ) { // Here, operation should be applied to this layer. if (layer.layerHasRequiredFields()) { if (operation.isValid() && layer.matchesOperation(operation)) { Log.v(TAG, "applying ${Gson().toJson(operation)} to $layer") // Interpretation of visibility "simplified": hide labels, display geometry if ( // A styler sets the layer to be invisible operation.stylers.any { it.visibility == "off" } || // A styler sets the layer to simplified and we are working with a label (layerElementType.startsWith("labels") && operation.stylers.any { it.visibility == "simplified" }) ) { layerArray.remove(i) if (layer.layerShouldBeRemoved(operation)) { Log.v(TAG, "removing $layer") continue@layers } operation.stylers.forEach { styler -> when (operation.elementType) { "labels.text.fill" -> styler.applyTextFill(layer.getJSONObject("paint")) "labels.text.outline" -> styler.applyTextOutline(layer.getJSONObject("paint")) else -> styler.traverseApply(layer.getJSONObject("paint")) } layerArray.remove(i) } else { layer.applyOperation(operation) } } } Loading Loading @@ -128,6 +100,66 @@ class Styler( val weight: Int? ) /** * Constructs a `NonNullStyleOperation` out of the `StyleOperation` while filling null fields with * default values. */ fun StyleOperation.toNonNull() = NonNullStyleOperation(featureType ?: SELECTOR_ALL, elementType ?: SELECTOR_ALL, stylers ?: emptyArray()) /** * Returns false iff the operation is invalid. * * There is one invalid selector that is tested for – per docs: * "`administrative` selects all administrative areas. Styling affects only * the labels of administrative areas, not the geographical borders or fill." */ fun NonNullStyleOperation.isValid() = !(featureType.startsWith("administrative") && elementType.startsWith("geometry")) /** * True iff the layer represented by the JSON object should be modified according to the stylers in the operation. * * Layer metadata always has the most concrete category, while operation applies to all subcategories as well. * Therefore, we test if the operation is a substring of the layer's metadata – i.e. the layer's metadata contains * (more concretely: starts with) the operation's selector. */ fun JSONObject.matchesOperation(operation: NonNullStyleOperation) = (getJSONObject(KEY_LAYER_METADATA).getString(KEY_METADATA_FEATURE_TYPE).startsWith(operation.featureType) || operation.featureType == "all") && (getJSONObject(KEY_LAYER_METADATA).getString(KEY_METADATA_ELEMENT_TYPE).startsWith(operation.elementType) || operation.elementType == "all") /** * Layer has fields that allow applying style operations. */ fun JSONObject.layerHasRequiredFields() = has(KEY_LAYER_PAINT) && has(KEY_LAYER_METADATA) && getJSONObject(KEY_LAYER_METADATA).let { it.has(KEY_METADATA_FEATURE_TYPE) && it.has(KEY_METADATA_ELEMENT_TYPE) } /** * True iff the layer represented by the JSON object should be removed according to the provided style operation. * * Interpretation of visibility "simplified": hide labels, display geometry. */ fun JSONObject.layerShouldBeRemoved(operation: NonNullStyleOperation) = // A styler sets the layer to be invisible operation.stylers.any { it.visibility == "off" } || // A styler sets the layer to simplified and we are working with a label (getJSONObject("metadata").getString(KEY_METADATA_ELEMENT_TYPE) .startsWith("labels") && operation.stylers.any { it.visibility == "simplified" }) /** * Applies the provided style operation to the layer represented by the JSON object. */ fun JSONObject.applyOperation(operation: NonNullStyleOperation) = operation.stylers.forEach { styler -> when (operation.elementType) { SELECTOR_ELEMENT_LABEL_TEXT_FILL -> styler.applyTextFill(getJSONObject(KEY_LAYER_PAINT)) SELECTOR_ELEMENT_LABEL_TEXT_OUTLINE -> styler.applyTextOutline(getJSONObject(KEY_LAYER_PAINT)) else -> styler.traverse(getJSONObject(KEY_LAYER_PAINT)) } } /** * Returns true if string is likely to contain a color. */ Loading Loading @@ -283,13 +315,13 @@ fun Styler.applyColorChanges(color: Int): Int { /** * Traverse JSON object and replace any color strings according to styler */ fun Styler.traverseApply(json: JSONObject) { fun Styler.traverse(json: JSONObject) { // Traverse layer and replace any color strings json.keys().forEach { key -> json.get(key).let { when (it) { is JSONObject -> traverseApply(it) is JSONArray -> traverseApply(it) is JSONObject -> traverse(it) is JSONArray -> traverse(it) is String -> if (it.isColor()) { json.put(key, applyColorChanges(it.parseColor()).colorToString()) } Loading @@ -301,12 +333,12 @@ fun Styler.traverseApply(json: JSONObject) { /** * Traverse array and replace any color strings according to styler */ fun Styler.traverseApply(array: JSONArray) { fun Styler.traverse(array: JSONArray) { for (i in 0 until array.length()) { array.get(i).let { when (it) { is JSONObject -> traverseApply(it) is JSONArray -> traverseApply(it) is JSONObject -> traverse(it) is JSONArray -> traverse(it) is String -> if (it.isColor()) { array.put(i, applyColorChanges(it.parseColor()).colorToString()) } Loading @@ -317,16 +349,16 @@ fun Styler.traverseApply(array: JSONArray) { fun Styler.applyTextFill(paint: JSONObject) { if (paint.has("text-color")) when (val textColor = paint.get("text-color")) { is JSONObject -> traverseApply(textColor) is JSONArray -> traverseApply(textColor) is JSONObject -> traverse(textColor) is JSONArray -> traverse(textColor) is String -> paint.put("text-color", applyColorChanges(textColor.parseColor()).colorToString()) } } fun Styler.applyTextOutline(paint: JSONObject) { if (paint.has("text-halo-color")) when (val textOutline = paint.get("text-halo-color")) { is JSONObject -> traverseApply(textOutline) is JSONArray -> traverseApply(textOutline) is JSONObject -> traverse(textOutline) is JSONArray -> traverse(textOutline) is String -> paint.put("text-halo-color", applyColorChanges(textOutline.parseColor()).colorToString()) } }