Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 26c990af authored by Fynn Godau's avatar Fynn Godau
Browse files

Apply review

parent 5c2f1e53
Loading
Loading
Loading
Loading
+85 −53
Original line number Diff line number Diff line
@@ -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
@@ -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)
                            }
                        }
                    }
@@ -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.
 */
@@ -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())
                }
@@ -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())
                }
@@ -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())
    }
}