Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android annotations: Implement CollisionGroup #2304

Open
wants to merge 4 commits into
base: android-annotations
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ internal class KAnnotationContainer
private val managers: MutableMap<Key, AnnotationManager<*, *>> = mutableMapOf()
private val bitmapUsers: MutableMap<Bitmap, MutableSet<KAnnotation<*>>> = mutableMapOf()

private val collisionGroups: MutableSet<CollisionGroup> = mutableSetOf()

@JvmName("setStyle")
internal fun setStyle(style: Style?) {
this.style = style
Expand All @@ -49,6 +51,25 @@ internal class KAnnotationContainer
addToManager(annotation)
}

@UiThread
fun add(collisionGroup: CollisionGroup) {

collisionGroups.add(collisionGroup)
collisionGroup.symbols.forEach {
it.icon()?.add(forAnnotation = it)
}

collisionGroup.key().let { key ->
(managers.getOrCreate(key) as SymbolManager).apply {

deleteAll()
addAll(collisionGroup.symbols)

collisionGroup.manager = this
}
}
}

fun KAnnotation<*>.icon(): Bitmap? = when (this) {
is Symbol -> icon?.image
is Line -> pattern
Expand All @@ -61,6 +82,14 @@ internal class KAnnotationContainer
bitmapUsers.getOrPut(this) { mutableSetOf() }.add(forAnnotation)
}

fun Bitmap.remove(forAnnotation: KAnnotation<*>) {
bitmapUsers[this]?.remove(forAnnotation)
if (bitmapUsers[this]?.isEmpty() == true) {
style?.removeImage(this.toString())
bitmapUsers.remove(this)
}
}

@UiThread
fun updateAll() {
managers.values.forEach {
Expand Down Expand Up @@ -90,6 +119,21 @@ internal class KAnnotationContainer
}
}

@UiThread
fun remove(collisionGroup: CollisionGroup) {
if (collisionGroups.remove(collisionGroup)) {

managers.keys.filter { it is CollisionGroupKey && it.collisionGroup == collisionGroup }.forEach {
managers[it]?.onDestroy()
managers.remove(it)
}

collisionGroup.symbols.forEach {
it.icon()?.remove(forAnnotation = it)
}
}
}

@UiThread
fun remove(annotation: KAnnotation<*>) {

Expand All @@ -106,13 +150,7 @@ internal class KAnnotationContainer
}

// Remove any icon if no other annotations are using it
annotation.icon()?.let {
bitmapUsers[it]?.remove(annotation)
if (bitmapUsers[it]?.isEmpty() == true) {
style?.removeImage(it.toString())
bitmapUsers.remove(it)
}
}
annotation.icon()?.remove(annotation)

// Destroy manager if no more annotations with same key remain
if (!groupAnnotations().containsKey(annotation.key())) {
Expand All @@ -123,6 +161,10 @@ internal class KAnnotationContainer

@UiThread
fun clear() {
collisionGroups.forEach {
it.manager = null
}
collisionGroups.clear()
managers.values.forEach {
it.onDestroy()
}
Expand All @@ -139,43 +181,49 @@ internal class KAnnotationContainer
val below = managers.keys.firstOrNull { it.z > key.z }?.let { managers[it] }?.layerId

when (key) {
is SymbolKey -> SymbolManager(
is SymbolKey, is CollisionGroupKey -> SymbolManager(
mapView,
mapLibreMap,
style,
belowLayerId = below,
draggableAnnotationController = draggableAnnotationController,
coreElementProvider = symbolElementProviderGenerator()
).apply {
// Non-collision group symbols do not interfere with each other
textAllowOverlap = true
iconAllowOverlap = true

// Apply NDD properties from key

iconTextFit = key.iconFitText.let { fitText ->
if (fitText.width && fitText.height) Property.ICON_TEXT_FIT_BOTH
else if (fitText.width) Property.ICON_TEXT_FIT_WIDTH
else if (fitText.height) Property.ICON_TEXT_FIT_HEIGHT
else Property.ICON_TEXT_FIT_NONE
}
iconTextFitPadding = key.iconFitText.padding.let { padding ->
arrayOf(padding.top, padding.right, padding.bottom, padding.left)
if (key is CollisionGroupKey) {
symbolSpacing = key.collisionGroup.symbolSpacing
symbolAvoidEdges = key.collisionGroup.symbolAvoidEdges
iconAllowOverlap = key.collisionGroup.iconAllowOverlap
iconIgnorePlacement = key.collisionGroup.iconIgnorePlacement
iconOptional = key.collisionGroup.iconOptional
iconPadding = key.collisionGroup.iconPadding
textPadding = key.collisionGroup.textPadding
textAllowOverlap = key.collisionGroup.textAllowOverlap
textIgnorePlacement = key.collisionGroup.textIgnorePlacement
textOptional = key.collisionGroup.textOptional
key.collisionGroup.textVariableAnchor?.map {
it.toString()
}?.toTypedArray().let {
textVariableAnchor = it
}
} else {
// Non-collision group symbols do not interfere with each other
// Collision group properties are handled in their `addOrUpdate` method
textAllowOverlap = true
iconAllowOverlap = true
}

iconKeepUpright = key.iconKeepUpright
iconPitchAlignment = when (key.iconPitchAlignment) {
Alignment.MAP -> Property.ICON_PITCH_ALIGNMENT_MAP
Alignment.VIEWPORT -> Property.ICON_PITCH_ALIGNMENT_VIEWPORT
null -> Property.ICON_PITCH_ALIGNMENT_AUTO
}
// Apply NDD properties from symbol key
when (key) {
is CollisionGroupKey -> {
if (key.collisionGroup.symbols.isEmpty()) return@apply

key.collisionGroup.symbols[0].key() as SymbolKey
}
is SymbolKey -> key
else -> throw IllegalStateException()
}.applyProperties(this)

textPitchAlignment = when (key.textPitchAlignment) {
Alignment.MAP -> Property.TEXT_PITCH_ALIGNMENT_MAP
Alignment.VIEWPORT -> Property.TEXT_PITCH_ALIGNMENT_VIEWPORT
null -> Property.TEXT_PITCH_ALIGNMENT_AUTO
}
textLineHeight = key.textLineHeight

}

Expand Down Expand Up @@ -253,7 +301,7 @@ internal class KAnnotationContainer
}?.also { put(key, it) }

@VisibleForTesting
internal val size get() = annotationList.size
internal val size get() = annotationList.size + collisionGroups.sumOf { it.symbols.size }

@VisibleForTesting
internal val managerCount get() = managers.size
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.maplibre.android.annotations.data.Alignment
import org.maplibre.android.annotations.data.Defaults
import org.maplibre.android.annotations.data.Icon
import org.maplibre.android.annotations.data.Translate
import org.maplibre.android.style.layers.Property

internal sealed interface Key {
val z: Int
Expand Down Expand Up @@ -38,6 +39,11 @@ internal data class CircleKey(
val pitchAlignment: Alignment
) : Key

data class CollisionGroupKey(
override val z: Int,
val collisionGroup: CollisionGroup
) : Key

internal fun KAnnotation<*>.key() = when (this) {
is Symbol -> SymbolKey(
zLayer,
Expand All @@ -56,4 +62,36 @@ internal fun KAnnotation<*>.key() = when (this) {
is Circle -> CircleKey(
zLayer, translate, pitchScale, pitchAlignment
)
}

internal fun CollisionGroup.key(): CollisionGroupKey = CollisionGroupKey(
this.zLayer,
this
)

internal fun SymbolKey.applyProperties(to: SymbolManager) {
to.iconTextFit = iconFitText.let { fitText ->
if (fitText.width && fitText.height) Property.ICON_TEXT_FIT_BOTH
else if (fitText.width) Property.ICON_TEXT_FIT_WIDTH
else if (fitText.height) Property.ICON_TEXT_FIT_HEIGHT
else Property.ICON_TEXT_FIT_NONE
}
to.iconTextFitPadding = iconFitText.padding.let { padding ->
arrayOf(padding.top, padding.right, padding.bottom, padding.left)
}

to.iconKeepUpright = iconKeepUpright
to.iconPitchAlignment = when (iconPitchAlignment) {
Alignment.MAP -> Property.ICON_PITCH_ALIGNMENT_MAP
Alignment.VIEWPORT -> Property.ICON_PITCH_ALIGNMENT_VIEWPORT
null -> Property.ICON_PITCH_ALIGNMENT_AUTO
}

to.textPitchAlignment = when (textPitchAlignment) {
Alignment.MAP -> Property.TEXT_PITCH_ALIGNMENT_MAP
Alignment.VIEWPORT -> Property.TEXT_PITCH_ALIGNMENT_VIEWPORT
null -> Property.TEXT_PITCH_ALIGNMENT_AUTO
}
to.textLineHeight = textLineHeight

}
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,9 @@ abstract class AnnotationManager<L : Layer, T : KAnnotation<*>> @UiThread intern
*/
@UiThread
fun deleteAll() {
annotations.forEach {
draggableAnnotationController.onAnnotationDeleted(it.value)
annotations.values.forEach {
draggableAnnotationController.onAnnotationDeleted(it)
it.detachFromManager()
}
annotations.clear()
updateSource()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package org.maplibre.android.annotations

import okhttp3.internal.toImmutableList
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably not use internal packages.

import org.maplibre.android.annotations.data.Anchor
import org.maplibre.android.annotations.data.Defaults
import kotlin.properties.Delegates

class CollisionGroup(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs some documentation.

symbols: List<Symbol> = emptyList(),
val zLayer: Int = Defaults.Z_LAYER,
symbolSpacing: Float = Defaults.COLLISION_GROUP_SYMBOL_SPACING,
symbolAvoidEdges: Boolean = Defaults.COLLISION_GROUP_SYMBOL_AVOID_EDGES,
iconAllowOverlap: Boolean = Defaults.COLLISION_GROUP_ICON_ALLOW_OVERLAP,
iconIgnorePlacement: Boolean = Defaults.COLLISION_GROUP_ICON_IGNORE_PLACEMENT,
iconOptional: Boolean = Defaults.COLLISION_GROUP_ICON_OPTIONAL,
iconPadding: Float = Defaults.COLLISION_GROUP_ICON_PADDING,
textPadding: Float = Defaults.COLLISION_GROUP_TEXT_PADDING,
textAllowOverlap: Boolean = Defaults.COLLISION_GROUP_TEXT_ALLOW_OVERLAP,
textIgnorePlacement: Boolean = Defaults.COLLISION_GROUP_TEXT_IGNORE_PLACEMENT,
textOptional: Boolean = Defaults.COLLISION_GROUP_TEXT_OPTIONAL,
textVariableAnchor: Array<Anchor>? = Defaults.COLLISION_GROUP_TEXT_VARIABLE_ANCHOR
) {

var symbols: List<Symbol> by Delegates.observable(
symbols.toImmutableList(),
onChange = { _, _, new ->
testDistinctSymbolKeys()
manager?.apply {
deleteAll()
if (new.isNotEmpty()) {
addAll(new)
(new[0].key() as SymbolKey).applyProperties(this)
}
}
}
)

var symbolSpacing: Float = symbolSpacing
set(value) {
field = value
manager?.symbolSpacing = value
}
var symbolAvoidEdges: Boolean = symbolAvoidEdges
set(value) {
field = value
manager?.symbolAvoidEdges = value
}
var iconAllowOverlap: Boolean = iconAllowOverlap
set(value) {
field = value
manager?.iconAllowOverlap = value
}
var iconIgnorePlacement: Boolean = iconIgnorePlacement
set(value) {
field = value
manager?.iconIgnorePlacement = value
}
var iconOptional: Boolean = iconOptional
set(value) {
field = value
manager?.iconOptional = value
}
var iconPadding: Float = iconPadding
set(value) {
field = value
manager?.iconPadding = value
}
var textPadding: Float = textPadding
set(value) {
field = value
manager?.textPadding = value
}
var textAllowOverlap: Boolean = textAllowOverlap
set(value) {
field = value
manager?.textAllowOverlap = value
}
var textIgnorePlacement: Boolean = textIgnorePlacement
set(value) {
field = value
manager?.textIgnorePlacement = value
}
var textOptional: Boolean = textOptional
set(value) {
field = value
manager?.textOptional = value
}
var textVariableAnchor: Array<Anchor>? = textVariableAnchor
set(value) {
field = value
manager?.textVariableAnchor = value?.map { it.toString() }?.toTypedArray()
}

// Set by AnnotationContainerKeys
internal var manager: SymbolManager? = null

init {
testDistinctSymbolKeys()

if (textVariableAnchor?.isEmpty() == true) {
throw IllegalArgumentException(
"An empty array has been provided as a text variable anchor. Please use `null` " +
"instead of an empty array to indicate that no alternative anchors are provided."
)
}
}

private fun testDistinctSymbolKeys() {
symbols.map {
it.key()
}.distinct().let {
if (it.size > 1) {
throw IllegalArgumentException(
"You have added symbols with conflicting Non-Data Driven (NDD) properties " +
"to this cluster group. Namely, the following sets of properties " +
"were found:\n${it.joinToString("; \n")}"
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ sealed class KAnnotation<T : Geometry>(
}
}

internal fun detachFromManager() {
if (attachedToManager != null) {
attachedToManager = null
id = 0L
}
}

/**
* Applies the given offset to the internal geometry, and applies this new Geometry to the annotation
* itself. Afterwards, the annotation updates.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ class LineManager @UiThread internal constructor(
* The display of line endings.
*/
var lineCap: String?
get() = layer.lineCap.value
get() = layer.lineCap?.value
set(value) {
val propertyValue: PropertyValue<*> = PropertyFactory.lineCap(value)
constantPropertyUsageMap[PROPERTY_LINE_CAP] = propertyValue
Expand Down
Loading
Loading