From 01a54aaf48d52d30371dc264531c9d0d95d8398c Mon Sep 17 00:00:00 2001 From: Eugene Maksymenko Date: Wed, 26 Jun 2024 18:04:42 +0300 Subject: [PATCH] Fix Tactical Graphics sector calculation based on level of details. --- README.md | 2 +- build.gradle.kts | 8 +- worldwind/build.gradle.kts | 2 +- .../milstd2525/MilStd2525TacticalGraphic.kt | 10 ++- .../render/AbstractSurfaceRenderable.kt | 23 ++---- .../render/program/TriangleShaderProgram.kt | 2 +- .../AbstractMilStd2525TacticalGraphic.kt | 82 ++++++++++--------- .../MilStd2525TacticalGraphic.kt | 44 +++++----- .../milstd2525/MilStd2525TacticalGraphic.kt | 31 ++++--- 9 files changed, 109 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index e61e7420d..4b7bdca61 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ repositories { } dependencies { - implementation 'earth.worldwind:worldwind:1.5.17' + implementation 'earth.worldwind:worldwind:1.5.19' } ``` diff --git a/build.gradle.kts b/build.gradle.kts index 06ee2fd4c..00b87bc3c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,22 +1,22 @@ plugins { - val kotlinVersion = "1.9.23" + val kotlinVersion = "1.9.24" kotlin("multiplatform") version kotlinVersion apply false kotlin("plugin.serialization") version kotlinVersion apply false kotlin("android") version kotlinVersion apply false - id("org.jetbrains.dokka") version "1.9.10" apply false + id("org.jetbrains.dokka") version "1.9.20" apply false id("com.android.library") apply false id("com.android.application") apply false } buildscript { dependencies { - classpath("dev.icerock.moko:resources-generator:0.24.0-beta-2") + classpath("dev.icerock.moko:resources-generator:0.24.1") } } allprojects { group = "earth.worldwind" - version = "1.5.17" + version = "1.5.19" extra.apply { set("minSdk", 21) diff --git a/worldwind/build.gradle.kts b/worldwind/build.gradle.kts index 191e3d6f1..d9f2f3cb8 100644 --- a/worldwind/build.gradle.kts +++ b/worldwind/build.gradle.kts @@ -40,7 +40,7 @@ kotlin { } sourceSets { val mockkVersion = "1.13.10" - val mokoVersion = "0.24.0-beta-2" + val mokoVersion = "0.24.1" val ktorVersion = "2.3.10" val ormliteVersion = "6.1" val commonMain by getting { diff --git a/worldwind/src/androidMain/kotlin/earth/worldwind/shape/milstd2525/MilStd2525TacticalGraphic.kt b/worldwind/src/androidMain/kotlin/earth/worldwind/shape/milstd2525/MilStd2525TacticalGraphic.kt index 78a47c999..d1282467d 100644 --- a/worldwind/src/androidMain/kotlin/earth/worldwind/shape/milstd2525/MilStd2525TacticalGraphic.kt +++ b/worldwind/src/androidMain/kotlin/earth/worldwind/shape/milstd2525/MilStd2525TacticalGraphic.kt @@ -22,7 +22,7 @@ import kotlin.math.roundToInt actual open class MilStd2525TacticalGraphic @JvmOverloads actual constructor( sidc: String, locations: List, boundingSector: Sector, modifiers: Map?, attributes: Map? -) : AbstractMilStd2525TacticalGraphic(sidc, locations, boundingSector, modifiers, attributes) { +) : AbstractMilStd2525TacticalGraphic(sidc, boundingSector, modifiers, attributes) { protected lateinit var controlPoints: ArrayList protected lateinit var pointUL: Point2D.Double @@ -34,7 +34,11 @@ actual open class MilStd2525TacticalGraphic @JvmOverloads actual constructor( MilStd2525.graphicModifiersFromSparseArray(modifiers), MilStd2525.attributesFromSparseArray(attributes) ) - override fun transformLocations(locations: List) { + init { + setAnchorLocations(locations) + } + + fun setAnchorLocations(locations: List) { if (this::controlPoints.isInitialized) controlPoints.clear() else controlPoints = ArrayList() for (location in locations) controlPoints.add(Point2D.Double(location.longitude.inDegrees, location.latitude.inDegrees)) var left = controlPoints[0].x @@ -57,6 +61,7 @@ actual open class MilStd2525TacticalGraphic @JvmOverloads actual constructor( } } if (this::pointUL.isInitialized) pointUL.setLocation(left, top) else pointUL = Point2D.Double(left, top) + reset() } override fun makeRenderables(scale: Double): List { @@ -92,7 +97,6 @@ actual open class MilStd2525TacticalGraphic @JvmOverloads actual constructor( val outlines = mutableListOf() for (i in mss.symbolShapes.indices) convertShapeToRenderables(mss.symbolShapes[i], mss, ipc, shapes, outlines) for (i in mss.modifierShapes.indices) convertShapeToRenderables(mss.modifierShapes[i], mss, ipc, shapes, outlines) - invalidateExtent() // Regenerate extent in next frame due to sector may be extended by real shape measures return outlines + shapes } diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/render/AbstractSurfaceRenderable.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/render/AbstractSurfaceRenderable.kt index 108864e07..d51b39ed1 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/render/AbstractSurfaceRenderable.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/render/AbstractSurfaceRenderable.kt @@ -5,17 +5,14 @@ import earth.worldwind.geom.Sector import earth.worldwind.globe.Globe abstract class AbstractSurfaceRenderable(sector: Sector, displayName: String? = null) : AbstractRenderable(displayName) { - var sector = Sector(sector) - set(value) { - field.copy(value) - invalidateExtent() - } + val sector = Sector(sector) protected val extent by lazy { BoundingBox() } protected val heightLimits by lazy { FloatArray(2) } protected var heightLimitsTimestamp = 0L protected var extentExaggeration = 0.0f protected var extentGlobeState: Globe.State? = null protected var extentGlobeOffset: Globe.Offset? = null + protected val extentSector = Sector() protected open fun getExtent(rc: RenderContext): BoundingBox { val globe = rc.globe @@ -27,15 +24,16 @@ abstract class AbstractSurfaceRenderable(sector: Sector, displayName: String? = val state = rc.globeState val offset = rc.globe.offset if (timestamp != heightLimitsTimestamp || ve != extentExaggeration - || state != extentGlobeState || offset != extentGlobeOffset ) { + || state != extentGlobeState || offset != extentGlobeOffset || extentSector != sector) { val minHeight = heightLimits[0] * ve val maxHeight = heightLimits[1] * ve extent.setToSector(sector, globe, minHeight, maxHeight) + heightLimitsTimestamp = timestamp + extentExaggeration = ve + extentGlobeState = state + extentGlobeOffset = offset + extentSector.copy(sector) } - heightLimitsTimestamp = timestamp - extentExaggeration = ve - extentGlobeState = state - extentGlobeOffset = offset return extent } @@ -47,9 +45,4 @@ abstract class AbstractSurfaceRenderable(sector: Sector, displayName: String? = // check for valid height limits if (heightLimits[0] > heightLimits[1]) heightLimits.fill(0f) } - - protected open fun invalidateExtent() { - heightLimitsTimestamp = 0L - extentExaggeration = 0.0f - } } \ No newline at end of file diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/render/program/TriangleShaderProgram.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/render/program/TriangleShaderProgram.kt index b4d70caa8..692ef5da8 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/render/program/TriangleShaderProgram.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/render/program/TriangleShaderProgram.kt @@ -106,7 +106,7 @@ open class TriangleShaderProgram : AbstractShaderProgram() { protected val color = Color() protected var opacity = 1.0f protected var lineWidth = 1.0f - protected var invMiterLengthCutoff = 1 / 5.0f // shouldn't be greater than 1.0 or equal 0.0 + protected var invMiterLengthCutoff = 1.0f protected var screenX = 0.0f protected var screenY = 0.0f diff --git a/worldwind/src/commonMain/kotlin/earth/worldwind/shape/milstd2525/AbstractMilStd2525TacticalGraphic.kt b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/milstd2525/AbstractMilStd2525TacticalGraphic.kt index ab064c561..1c93c62ec 100644 --- a/worldwind/src/commonMain/kotlin/earth/worldwind/shape/milstd2525/AbstractMilStd2525TacticalGraphic.kt +++ b/worldwind/src/commonMain/kotlin/earth/worldwind/shape/milstd2525/AbstractMilStd2525TacticalGraphic.kt @@ -1,19 +1,21 @@ package earth.worldwind.shape.milstd2525 -import earth.worldwind.geom.* +import earth.worldwind.geom.AltitudeMode +import earth.worldwind.geom.Angle +import earth.worldwind.geom.Location +import earth.worldwind.geom.Sector import earth.worldwind.render.AbstractSurfaceRenderable import earth.worldwind.render.RenderContext import earth.worldwind.render.Renderable import earth.worldwind.shape.* import earth.worldwind.shape.milstd2525.MilStd2525.labelScaleThreshold -import earth.worldwind.util.Logger import kotlin.jvm.JvmStatic import kotlin.math.PI import kotlin.math.ln import kotlin.math.roundToInt abstract class AbstractMilStd2525TacticalGraphic( - protected val sidc: String, locations: List, boundingSector: Sector, + protected val sidc: String, protected val boundingSector: Sector, modifiers: Map?, attributes: Map?, ) : AbstractSurfaceRenderable(boundingSector), Highlightable { override var isHighlighted = false @@ -27,18 +29,17 @@ abstract class AbstractMilStd2525TacticalGraphic( field = value reset() } - private var minScale = 0.0 - private var maxScale = 0.0 + private var minScale = Double.MIN_VALUE + private var maxScale = Double.MAX_VALUE private val lodBuffer = mutableMapOf>() + private val lodSector = mutableMapOf() protected companion object { - const val MAX_WIDTH_DP = 0.0005 - const val MIN_WIDTH_DP = 0.000015 + const val MAX_WIDTH_DP = 1e-3 + const val MIN_WIDTH_DP = 1e-5 const val HIGHLIGHT_FACTOR = 2f private const val ZERO_LEVEL_PX = 256 - private val forwardRay = Line() - private val lookAtPoint = Vec3() @JvmStatic fun defaultBoundingSector(locations: List) = Sector().apply { locations.forEach { l -> union(l) } } @@ -50,28 +51,35 @@ abstract class AbstractMilStd2525TacticalGraphic( 2 * PI * equatorialRadius / ZERO_LEVEL_PX / (1 shl lod) } - init { setGeometry(locations, boundingSector) } + init { + recalculateScaleLimits() + } - fun setGeometry(locations: List, boundingSector: Sector = defaultBoundingSector(locations)) { - require(locations.isNotEmpty()) { - Logger.logMessage(Logger.ERROR, "MilStd2525TacticalGraphic", "constructor", "missingList") - } - transformLocations(locations) - sector = boundingSector - reset() + fun setBoundingSector(sector: Sector) { + boundingSector.copy(sector) + recalculateScaleLimits() } override fun doRender(rc: RenderContext) { + // Get the current map scale based on observation range. + val currentScale = rc.pixelSize * rc.densityFactor + // Limit scale based on clipping sector diagonal size + val limitedScale = currentScale.coerceIn(minScale, maxScale) + // Get renderables for current LoD + val equatorialRadius = rc.globe.equatorialRadius + val lod = computeNearestLoD(equatorialRadius, limitedScale) + // Set sector based on selected lod + sector.copy(lodSector[lod] ?: boundingSector) + // Check if tactical graphics visible val terrainSector = rc.terrain.sector if (!terrainSector.isEmpty && terrainSector.intersects(sector) && getExtent(rc).intersectsFrustum(rc.frustum)) { - // Get the current map scale based on observation range. - val currentScale = rc.pixelSize * rc.densityFactor - // Limit scale based on clipping sector diagonal size - val limitedScale = currentScale.coerceIn(minScale, maxScale) - // Get renderables for current LoD - val equatorialRadius = rc.globe.equatorialRadius - val lod = computeNearestLoD(equatorialRadius, limitedScale) - val shapes = lodBuffer[lod] ?: makeRenderables(computeLoDScale(equatorialRadius, lod)).also { lodBuffer[lod] = it } + val shapes = lodBuffer[lod] ?: run { + sector.setEmpty() // Prepare bounding box to be extended by real graphics measures + makeRenderables(computeLoDScale(equatorialRadius, lod)).also { + lodBuffer[lod] = it + lodSector[lod] = Sector(sector) // Remember real bounding box based on LoD + } + } // Draw available shapes for (renderable in shapes) { if (renderable is Highlightable) renderable.isHighlighted = isHighlighted @@ -80,19 +88,12 @@ abstract class AbstractMilStd2525TacticalGraphic( } } - override fun invalidateExtent() { - super.invalidateExtent() - // Recalculate scale limits according to new sector - val diagonalDistance = Location(sector.maxLatitude, sector.minLongitude) - .greatCircleDistance(Location(sector.minLatitude, sector.maxLongitude)) - minScale = diagonalDistance / MAX_WIDTH_DP - maxScale = diagonalDistance / MIN_WIDTH_DP - } - - protected open fun reset() = lodBuffer.clear() + protected abstract fun makeRenderables(scale: Double): List // Platform dependent implementation - abstract fun transformLocations(locations: List) - abstract fun makeRenderables(scale: Double): List + protected fun reset() { + lodBuffer.clear() + lodSector.clear() + } protected fun applyShapeAttributes(shape: AbstractShape) = shape.apply { altitudeMode = AltitudeMode.CLAMP_TO_GROUND @@ -110,4 +111,11 @@ abstract class AbstractMilStd2525TacticalGraphic( rotationMode = OrientationMode.RELATIVE_TO_GLOBE pickDelegate = this@AbstractMilStd2525TacticalGraphic } + + private fun recalculateScaleLimits() { + val diagonalDistance = Location(boundingSector.minLatitude, boundingSector.minLongitude) + .greatCircleDistance(Location(boundingSector.maxLatitude, boundingSector.maxLongitude)) + minScale = diagonalDistance / MAX_WIDTH_DP + maxScale = diagonalDistance / MIN_WIDTH_DP + } } \ No newline at end of file diff --git a/worldwind/src/jsMain/kotlin/earth/worldwind/shape.milstd2525/MilStd2525TacticalGraphic.kt b/worldwind/src/jsMain/kotlin/earth/worldwind/shape.milstd2525/MilStd2525TacticalGraphic.kt index 3a6ea850c..eab178f6e 100644 --- a/worldwind/src/jsMain/kotlin/earth/worldwind/shape.milstd2525/MilStd2525TacticalGraphic.kt +++ b/worldwind/src/jsMain/kotlin/earth/worldwind/shape.milstd2525/MilStd2525TacticalGraphic.kt @@ -7,26 +7,23 @@ import earth.worldwind.geom.Position import earth.worldwind.geom.Sector import earth.worldwind.render.Font import earth.worldwind.render.Renderable +import earth.worldwind.render.image.ImageSource import earth.worldwind.shape.* import earth.worldwind.util.Logger +import kotlin.math.roundToInt actual open class MilStd2525TacticalGraphic actual constructor( sidc: String, locations: List, boundingSector: Sector, modifiers: Map?, attributes: Map? -) : AbstractMilStd2525TacticalGraphic(sidc, locations, boundingSector, modifiers, attributes) { +) : AbstractMilStd2525TacticalGraphic(sidc, boundingSector, modifiers, attributes) { protected lateinit var controlPoints: java.util.ArrayList protected lateinit var pointUL: armyc2.c2sd.graphics2d.Point2D - protected companion object { - fun convertColor(color: Color) = earth.worldwind.render.Color( - color.getRed().toInt(), - color.getGreen().toInt(), - color.getBlue().toInt(), - color.getAlpha().toInt() - ) + init { + setAnchorLocations(locations) } - override fun transformLocations(locations: List) { + fun setAnchorLocations(locations: List) { if (this::controlPoints.isInitialized) controlPoints.clear() else controlPoints = java.util.ArrayList() for (location in locations) controlPoints.add(armyc2.c2sd.graphics2d.Point2D(location.longitude.inDegrees, location.latitude.inDegrees)) val point0 = controlPoints.get(0) ?: return @@ -49,7 +46,7 @@ actual open class MilStd2525TacticalGraphic actual constructor( } } if (this::pointUL.isInitialized) pointUL.setLocation(left, top) else pointUL = armyc2.c2sd.graphics2d.Point2D(left, top) - pointUL.setLocation(left, top) + reset() } override fun makeRenderables(scale: Double): List { @@ -85,7 +82,6 @@ actual open class MilStd2525TacticalGraphic actual constructor( val outlines = mutableListOf() for (i in 0 until mss.getSymbolShapes().size()) convertShapeToRenderables(mss.getSymbolShapes().get(i)!!, mss, ipc, shapes, outlines) for (i in 0 until mss.getModifierShapes().size()) convertShapeToRenderables(mss.getModifierShapes().get(i)!!, mss, ipc, shapes, outlines) - invalidateExtent() // Regenerate extent in next frame due to sector may be extended by real shape measures return outlines + shapes } @@ -98,15 +94,14 @@ actual open class MilStd2525TacticalGraphic actual constructor( outlineWidth = MilStd2525.graphicsLineWidth (si.getLineColor() ?: si.getFillColor())?.let { outlineColor = convertColor(it) } ?: return (si.getFillColor() ?: si.getLineColor())?.let { interiorColor = convertColor(it) } ?: return - // TODO Fill dash pattern -// val stroke = shape.getStroke() -// if (stroke is armyc2.c2sd.graphics2d.BasicStroke) { -// val dash = stroke.getDashArray() -// if (!dash.isNullOrEmpty()) outlineImageSource = ImageSource.fromLineStipple( -// // TODO How to correctly interpret dash array? -// factor = dash[0].roundToInt(), pattern = 0xF0F0.toShort() -// ) -// } + val stroke = si.getStroke() + if (stroke is armyc2.c2sd.graphics2d.BasicStroke) { + val dash = stroke.getDashArray() + if (!dash.isNullOrEmpty()) outlineImageSource = ImageSource.fromLineStipple( + // TODO How to correctly interpret dash array? + factor = dash[0].roundToInt(), pattern = 0xF0F0.toShort() + ) + } } val hasOutline = MilStd2525.graphicsOutlineWidth != 0f val outlineAttributes = if (hasOutline) ShapeAttributes(shapeAttributes).apply { @@ -160,4 +155,13 @@ actual open class MilStd2525TacticalGraphic actual constructor( else -> Logger.logMessage(Logger.ERROR, "MilStd2525TacticalGraphic", "convertShapeToRenderables", "unknownShapeType") } } + + protected companion object { + fun convertColor(color: Color) = earth.worldwind.render.Color( + color.getRed().toInt(), + color.getGreen().toInt(), + color.getBlue().toInt(), + color.getAlpha().toInt() + ) + } } \ No newline at end of file diff --git a/worldwind/src/jvmMain/kotlin/earth/worldwind/shape/milstd2525/MilStd2525TacticalGraphic.kt b/worldwind/src/jvmMain/kotlin/earth/worldwind/shape/milstd2525/MilStd2525TacticalGraphic.kt index 04fb20e10..c282b7248 100644 --- a/worldwind/src/jvmMain/kotlin/earth/worldwind/shape/milstd2525/MilStd2525TacticalGraphic.kt +++ b/worldwind/src/jvmMain/kotlin/earth/worldwind/shape/milstd2525/MilStd2525TacticalGraphic.kt @@ -10,18 +10,25 @@ import earth.worldwind.geom.Sector import earth.worldwind.render.Color import earth.worldwind.render.Font import earth.worldwind.render.Renderable +import earth.worldwind.render.image.ImageSource import earth.worldwind.shape.* import earth.worldwind.util.Logger +import java.awt.BasicStroke import java.awt.geom.Point2D +import kotlin.math.roundToInt actual open class MilStd2525TacticalGraphic @JvmOverloads actual constructor( sidc: String, locations: List, boundingSector: Sector, modifiers: Map?, attributes: Map? -) : AbstractMilStd2525TacticalGraphic(sidc, locations, boundingSector, modifiers, attributes) { +) : AbstractMilStd2525TacticalGraphic(sidc, boundingSector, modifiers, attributes) { protected lateinit var controlPoints: ArrayList protected lateinit var pointUL: Point2D.Double - override fun transformLocations(locations: List) { + init { + setAnchorLocations(locations) + } + + fun setAnchorLocations(locations: List) { if (this::controlPoints.isInitialized) controlPoints.clear() else controlPoints = ArrayList() for (location in locations) controlPoints.add(Point2D.Double(location.longitude.inDegrees, location.latitude.inDegrees)) var left = controlPoints[0].x @@ -44,7 +51,7 @@ actual open class MilStd2525TacticalGraphic @JvmOverloads actual constructor( } } if (this::pointUL.isInitialized) pointUL.setLocation(left, top) else pointUL = Point2D.Double(left, top) - pointUL.setLocation(left, top) + reset() } override fun makeRenderables(scale: Double): List { @@ -80,7 +87,6 @@ actual open class MilStd2525TacticalGraphic @JvmOverloads actual constructor( val outlines = mutableListOf() for (i in mss.symbolShapes.indices) convertShapeToRenderables(mss.symbolShapes[i], mss, ipc, shapes, outlines) for (i in mss.modifierShapes.indices) convertShapeToRenderables(mss.modifierShapes[i], mss, ipc, shapes, outlines) - invalidateExtent() // Regenerate extent in next frame due to sector may be extended by real shape measures return outlines + shapes } @@ -93,15 +99,14 @@ actual open class MilStd2525TacticalGraphic @JvmOverloads actual constructor( outlineWidth = MilStd2525.graphicsLineWidth (si.lineColor ?: si.fillColor)?.let { outlineColor = Color(it.rgb) } ?: return (si.fillColor ?: si.lineColor)?.let { interiorColor = Color(it.rgb) } ?: return - // TODO Fill dash pattern -// val stroke = shape.stroke -// if (stroke is BasicStroke) { -// val dash = stroke.dashArray -// if (dash != null && dash.isNotEmpty()) outlineImageSource = ImageSource.fromLineStipple( -// // TODO How to correctly interpret dash array? -// factor = dash[0].roundToInt(), pattern = 0xF0F0.toShort() -// ) -// } + val stroke = si.stroke + if (stroke is BasicStroke) { + val dash = stroke.dashArray + if (dash != null && dash.isNotEmpty()) outlineImageSource = ImageSource.fromLineStipple( + // TODO How to correctly interpret dash array? + factor = dash[0].roundToInt(), pattern = 0xF0F0.toShort() + ) + } } val hasOutline = MilStd2525.graphicsOutlineWidth != 0f val outlineAttributes = if (hasOutline) ShapeAttributes(shapeAttributes).apply {