All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.org.jetbrains.letsPlot.livemap.api.PathLayerBuilder.kt Maven / Gradle / Ivy

There is a newer version: 4.5.3-alpha1
Show newest version
/*
 * Copyright (c) 2023. JetBrains s.r.o.
 * Use of this source code is governed by the MIT license that can be found in the LICENSE file.
 */

package org.jetbrains.letsPlot.livemap.api

import org.jetbrains.letsPlot.commons.intern.math.toRadians
import org.jetbrains.letsPlot.commons.intern.spatial.Geodesic
import org.jetbrains.letsPlot.commons.intern.spatial.LonLat
import org.jetbrains.letsPlot.commons.intern.spatial.wrapPath
import org.jetbrains.letsPlot.commons.intern.typedGeometry.*
import org.jetbrains.letsPlot.commons.intern.typedGeometry.Transforms.transform
import org.jetbrains.letsPlot.commons.intern.typedGeometry.Transforms.transformPoints
import org.jetbrains.letsPlot.commons.intern.util.ArrowSupport
import org.jetbrains.letsPlot.commons.values.Color
import org.jetbrains.letsPlot.livemap.Client
import org.jetbrains.letsPlot.livemap.Client.Companion.px
import org.jetbrains.letsPlot.livemap.World
import org.jetbrains.letsPlot.livemap.chart.ChartElementComponent
import org.jetbrains.letsPlot.livemap.chart.ChartElementLocationComponent
import org.jetbrains.letsPlot.livemap.chart.GrowingPathEffect.GrowingPathEffectComponent
import org.jetbrains.letsPlot.livemap.chart.GrowingPathEffect.GrowingPathRenderer
import org.jetbrains.letsPlot.livemap.chart.IndexComponent
import org.jetbrains.letsPlot.livemap.chart.LocatorComponent
import org.jetbrains.letsPlot.livemap.chart.path.ArrowSpec
import org.jetbrains.letsPlot.livemap.chart.path.CurveRenderer
import org.jetbrains.letsPlot.livemap.chart.path.PathLocator
import org.jetbrains.letsPlot.livemap.chart.path.PathRenderer
import org.jetbrains.letsPlot.livemap.core.animation.Animation
import org.jetbrains.letsPlot.livemap.core.ecs.AnimationComponent
import org.jetbrains.letsPlot.livemap.core.ecs.EcsEntity
import org.jetbrains.letsPlot.livemap.core.ecs.addComponents
import org.jetbrains.letsPlot.livemap.core.layers.LayerKind
import org.jetbrains.letsPlot.livemap.core.util.EasingFunctions.LINEAR
import org.jetbrains.letsPlot.livemap.geocoding.NeedCalculateLocationComponent
import org.jetbrains.letsPlot.livemap.geocoding.NeedLocationComponent
import org.jetbrains.letsPlot.livemap.geometry.MicroTasks.RESAMPLING_PRECISION
import org.jetbrains.letsPlot.livemap.geometry.WorldGeometryComponent
import org.jetbrains.letsPlot.livemap.mapengine.LayerEntitiesComponent
import org.jetbrains.letsPlot.livemap.mapengine.MapProjection
import org.jetbrains.letsPlot.livemap.mapengine.RenderableComponent
import org.jetbrains.letsPlot.livemap.mapengine.placement.WorldDimensionComponent
import org.jetbrains.letsPlot.livemap.mapengine.placement.WorldOriginComponent

@LiveMapDsl
class PathLayerBuilder(
    val factory: FeatureEntityFactory,
    val mapProjection: MapProjection
)

fun FeatureLayerBuilder.paths(block: PathLayerBuilder.() -> Unit) {
    val layerEntity = myComponentManager
        .createEntity("map_layer_path")
        .addComponents {
            +layerManager.addLayer("geom_path", LayerKind.FEATURES)
            +LayerEntitiesComponent()
        }

    PathLayerBuilder(
        FeatureEntityFactory(layerEntity, panningPointsMaxCount = 15_000),
        mapProjection
    ).apply(block)
}

fun PathLayerBuilder.path(block: PathEntityBuilder.() -> Unit) {
    PathEntityBuilder(factory, mapProjection)
        .apply(block)
        .build(nonInteractive = false)
}

@LiveMapDsl
class PathEntityBuilder(
    private val myFactory: FeatureEntityFactory,
    private val myMapProjection: MapProjection
) {
    var sizeScalingRange: ClosedRange? = null
    var alphaScalingEnabled: Boolean = false
    var layerIndex: Int? = null
    var index: Int? = null
    var regionId: String = ""

    var lineDash: List = emptyList()
    var strokeColor: Color = Color.BLACK
    var strokeWidth: Double = 1.0

    lateinit var points: List>
    var flat: Boolean = false
    var geodesic: Boolean = false
    var animation: Int = 0
    var speed: Double = 0.0
    var flow: Double = 0.0
    var isCurve: Boolean = false

    // Arrow specification
    var arrowSpec: ArrowSpec? = null

    var sizeStart = 0.px
    var sizeEnd = 0.px
    var strokeStart = 0.px
    var strokeEnd = 0.px
    var spacer = 0.px

    fun build(nonInteractive: Boolean): EcsEntity? {
        // flat can't be geodesic
        val geodesic = if (flat) false else geodesic

        fun transformPath(points: List>): MultiLineString = when {
                flat ->
                    transformPoints(points, myMapProjection::apply, resamplingPrecision = null)
                        .let { wrapPath(it, World.DOMAIN) }
                        .let { MultiLineString(it.map(::LineString)) }

                else ->
                    wrapPath(points, LonLat.DOMAIN)
                        .let { MultiLineString(it.map(::LineString)) }
                        .let { transform(it, myMapProjection::apply, RESAMPLING_PRECISION.takeUnless { geodesic }) }
            }

        // location is never built on geodesic points - they alter minimal bbox too much
        val locGeometry = transformPath(points)
        val visGeometry = transformPath(points.takeUnless { geodesic } ?: Geodesic.createArcPath(points))

        // Calculate paddings based on the target size, spacer and arrow spec
        val targetSizeStart = sizeStart / 2.0 + strokeStart
        val targetSizeEnd = sizeEnd / 2.0 + strokeEnd

        val startArrowPadding = arrowSpec?.let {
            ArrowSupport.arrowPadding(
                angle = it.angle,
                onStart = it.isOnFirstEnd,
                onEnd = it.isOnLastEnd,
                atStart = true,
                strokeSize = strokeWidth
            ).px
        } ?: 0.px

        val endArrowPadding = arrowSpec?.let {
            ArrowSupport.arrowPadding(
                angle = it.angle,
                onStart = it.isOnFirstEnd,
                onEnd = it.isOnLastEnd,
                atStart = false,
                strokeSize = strokeWidth
            ).px
        } ?: 0.px

        // Total offsets
        val startPadding = targetSizeStart + spacer + startArrowPadding
        val endPadding = targetSizeEnd + spacer + endArrowPadding

        myFactory.incrementLayerPointsTotalCount(visGeometry.sumOf(LineString::size))
        return visGeometry.bbox?.let { bbox ->
            val entity = myFactory
                .createFeature("map_ent_path")
                .addComponents {
                    if (layerIndex != null && index != null) {
                        +IndexComponent(layerIndex!!, index!!)
                    }
                    +RenderableComponent().apply {
                        renderer = if (isCurve) CurveRenderer() else PathRenderer()
                    }
                    +ChartElementComponent().apply {
                        sizeScalingRange = [email protected]
                        alphaScalingEnabled = [email protected]
                        strokeColor = [email protected]
                        strokeWidth = [email protected]
                        lineDash = [email protected]()
                        arrowSpec = [email protected]
                        this.startPadding = startPadding
                        this.endPadding = endPadding
                    }
                    +ChartElementLocationComponent().apply {
                        geometry = Geometry.of(locGeometry)
                    }
                    +WorldOriginComponent(bbox.origin)
                    +WorldGeometryComponent().apply { geometry = Geometry.of(visGeometry) }
                    +WorldDimensionComponent(bbox.dimension)
                    +NeedLocationComponent
                    +NeedCalculateLocationComponent
                    if (!nonInteractive) {
                        +LocatorComponent(PathLocator)
                    }
                }

            if (animation == 2) {
                val animationEntity = entity.componentManager
                    .createEntity("map_ent_path_animation")
                    .addAnimationComponent {
                        duration = 5_000.0
                        easingFunction = LINEAR
                        direction = Animation.Direction.FORWARD
                        loop = Animation.Loop.KEEP_DIRECTION
                    }

                entity
                    .setComponent(RenderableComponent().apply {
                        renderer = GrowingPathRenderer()
                    })
                    .addGrowingPathEffectComponent { animationId = animationEntity.id }
            }

            return entity
        }
    }

    private fun EcsEntity.addAnimationComponent(block: AnimationComponent.() -> Unit): EcsEntity {
        return add(AnimationComponent().apply(block))
    }

    private fun EcsEntity.addGrowingPathEffectComponent(block: GrowingPathEffectComponent.() -> Unit): EcsEntity {
        return add(GrowingPathEffectComponent().apply(block))
    }
}

/**
 * @param angle - the angle of the arrow head in degrees
 * @param length - the length of the arrow head (px).
 * @param ends - {'last', 'first', 'both'}
 * @param type - {'open', 'closed'}
 * */
fun PathEntityBuilder.arrow(
    angle: Double = 30.0,
    length: Scalar = 10.px,
    ends: ArrowSpec.End = ArrowSpec.End.LAST,
    type: ArrowSpec.Type = ArrowSpec.Type.OPEN
) {
    arrowSpec = ArrowSpec(toRadians(angle), length, ends, type)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy