Skip to content

Commit

Permalink
refactor: strongly typed expression AST (#191)
Browse files Browse the repository at this point in the history
  • Loading branch information
sargunv authored Dec 26, 2024
1 parent 82a287b commit 03aa838
Show file tree
Hide file tree
Showing 147 changed files with 4,446 additions and 3,533 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ tasks.register("generateDocs") {

dependencies {
dokka(project(":lib:maplibre-compose:"))
dokka(project(":lib:maplibre-compose-expressions:"))
dokka(project(":lib:maplibre-compose-material3:"))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ import dev.sargunv.maplibrecompose.compose.rememberCameraState
import dev.sargunv.maplibrecompose.compose.rememberStyleState
import dev.sargunv.maplibrecompose.compose.source.rememberGeoJsonSource
import dev.sargunv.maplibrecompose.core.CameraPosition
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.const
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.exponential
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.interpolate
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.zoom
import dev.sargunv.maplibrecompose.core.expression.LineCap
import dev.sargunv.maplibrecompose.core.expression.LineJoin
import dev.sargunv.maplibrecompose.demoapp.DEFAULT_STYLE
import dev.sargunv.maplibrecompose.demoapp.Demo
import dev.sargunv.maplibrecompose.demoapp.DemoMapControls
import dev.sargunv.maplibrecompose.demoapp.DemoOrnamentSettings
import dev.sargunv.maplibrecompose.demoapp.DemoScaffold
import dev.sargunv.maplibrecompose.demoapp.generated.Res
import dev.sargunv.maplibrecompose.expressions.dsl.const
import dev.sargunv.maplibrecompose.expressions.dsl.exponential
import dev.sargunv.maplibrecompose.expressions.dsl.interpolate
import dev.sargunv.maplibrecompose.expressions.dsl.zoom
import dev.sargunv.maplibrecompose.expressions.value.LineCap
import dev.sargunv.maplibrecompose.expressions.value.LineJoin
import io.github.dellisd.spatialk.geojson.Position
import org.jetbrains.compose.resources.ExperimentalResourceApi

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ import dev.sargunv.maplibrecompose.compose.rememberStyleState
import dev.sargunv.maplibrecompose.compose.source.rememberGeoJsonSource
import dev.sargunv.maplibrecompose.core.CameraMoveReason
import dev.sargunv.maplibrecompose.core.CameraPosition
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.const
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.offset
import dev.sargunv.maplibrecompose.core.source.Source
import dev.sargunv.maplibrecompose.demoapp.DEFAULT_STYLE
import dev.sargunv.maplibrecompose.demoapp.Demo
import dev.sargunv.maplibrecompose.demoapp.DemoMapControls
import dev.sargunv.maplibrecompose.demoapp.DemoOrnamentSettings
import dev.sargunv.maplibrecompose.demoapp.DemoScaffold
import dev.sargunv.maplibrecompose.demoapp.PositionVectorConverter
import dev.sargunv.maplibrecompose.expressions.dsl.const
import dev.sargunv.maplibrecompose.expressions.dsl.offset
import io.github.dellisd.spatialk.geojson.Point
import io.github.dellisd.spatialk.geojson.Position
import kotlin.math.roundToInt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ import dev.sargunv.maplibrecompose.compose.rememberCameraState
import dev.sargunv.maplibrecompose.compose.rememberStyleState
import dev.sargunv.maplibrecompose.compose.source.rememberGeoJsonSource
import dev.sargunv.maplibrecompose.core.CameraPosition
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.asNumber
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.asString
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.const
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.feature
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.not
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.offset
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.step
import dev.sargunv.maplibrecompose.core.source.GeoJsonOptions
import dev.sargunv.maplibrecompose.demoapp.DEFAULT_STYLE
import dev.sargunv.maplibrecompose.demoapp.Demo
import dev.sargunv.maplibrecompose.demoapp.DemoMapControls
import dev.sargunv.maplibrecompose.demoapp.DemoOrnamentSettings
import dev.sargunv.maplibrecompose.demoapp.DemoScaffold
import dev.sargunv.maplibrecompose.demoapp.generated.Res
import dev.sargunv.maplibrecompose.expressions.dsl.asNumber
import dev.sargunv.maplibrecompose.expressions.dsl.asString
import dev.sargunv.maplibrecompose.expressions.dsl.const
import dev.sargunv.maplibrecompose.expressions.dsl.feature
import dev.sargunv.maplibrecompose.expressions.dsl.not
import dev.sargunv.maplibrecompose.expressions.dsl.offset
import dev.sargunv.maplibrecompose.expressions.dsl.step
import io.github.dellisd.spatialk.geojson.Feature
import io.github.dellisd.spatialk.geojson.FeatureCollection
import io.github.dellisd.spatialk.geojson.Point
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,20 @@ import dev.sargunv.maplibrecompose.compose.rememberCameraState
import dev.sargunv.maplibrecompose.compose.rememberStyleState
import dev.sargunv.maplibrecompose.compose.source.rememberGeoJsonSource
import dev.sargunv.maplibrecompose.core.CameraPosition
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.Feature.get
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.asString
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.const
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.format
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.image
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.offset
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.span
import dev.sargunv.maplibrecompose.demoapp.DEFAULT_STYLE
import dev.sargunv.maplibrecompose.demoapp.Demo
import dev.sargunv.maplibrecompose.demoapp.DemoMapControls
import dev.sargunv.maplibrecompose.demoapp.DemoOrnamentSettings
import dev.sargunv.maplibrecompose.demoapp.DemoScaffold
import dev.sargunv.maplibrecompose.demoapp.generated.Res
import dev.sargunv.maplibrecompose.demoapp.generated.marker
import dev.sargunv.maplibrecompose.expressions.dsl.Feature.get
import dev.sargunv.maplibrecompose.expressions.dsl.asString
import dev.sargunv.maplibrecompose.expressions.dsl.const
import dev.sargunv.maplibrecompose.expressions.dsl.format
import dev.sargunv.maplibrecompose.expressions.dsl.image
import dev.sargunv.maplibrecompose.expressions.dsl.offset
import dev.sargunv.maplibrecompose.expressions.dsl.span
import io.github.dellisd.spatialk.geojson.Feature
import io.github.dellisd.spatialk.geojson.Position
import org.jetbrains.compose.resources.painterResource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import dev.sargunv.maplibrecompose.compose.layer.CircleLayer
import dev.sargunv.maplibrecompose.compose.layer.LineLayer
import dev.sargunv.maplibrecompose.compose.source.getBaseSource
import dev.sargunv.maplibrecompose.compose.source.rememberGeoJsonSource
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.const
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.exponential
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.interpolate
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.zoom
import dev.sargunv.maplibrecompose.core.expression.LineCap
import dev.sargunv.maplibrecompose.core.expression.LineJoin
import dev.sargunv.maplibrecompose.demoapp.generated.Res
import dev.sargunv.maplibrecompose.expressions.dsl.const
import dev.sargunv.maplibrecompose.expressions.dsl.exponential
import dev.sargunv.maplibrecompose.expressions.dsl.interpolate
import dev.sargunv.maplibrecompose.expressions.dsl.zoom
import dev.sargunv.maplibrecompose.expressions.value.LineCap
import dev.sargunv.maplibrecompose.expressions.value.LineJoin
import org.jetbrains.compose.resources.ExperimentalResourceApi

@Composable
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
androidx-activity = "1.9.3"
androidx-composeUi = "1.7.6"
androidx-navigation = "2.8.0-alpha11"
webview = "1.9.40"
kermit = "2.0.5"
kotlinx-coroutines = "1.9.0"
ktor = "3.0.3"
Expand All @@ -23,6 +24,7 @@ gradle-spotless = "7.0.0.BETA4"
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
androidx-composeUi-testManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-composeUi" }
androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
webview = { module = "io.github.kevinnzou:compose-webview-multiplatform", version.ref = "webview" }
kermit = { group = "co.touchlab", name = "kermit", version.ref = "kermit" }
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
Expand Down
16 changes: 16 additions & 0 deletions lib/maplibre-compose-expressions/MODULE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Module maplibre-compose-expressions

Utilities for creating MapLibre expressions with a type-safe Kotlin DSL.

# Package dev.sargunv.maplibrecompose.expressions.ast

The abstract syntax tree (AST) for the expression language.

# Package dev.sargunv.maplibrecompose.expressions.dsl

The Kotlin DSL for creating MapLibre expressions. This is the primary API you'll
be using to create expressions.

# Package dev.sargunv.maplibrecompose.expressions.value

The interfaces and enums defining the type system for MapLibre expressions.
54 changes: 54 additions & 0 deletions lib/maplibre-compose-expressions/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@file:OptIn(ExperimentalKotlinGradlePluginApi::class, ExperimentalComposeLibrary::class)

import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree

plugins {
id("library-conventions")
id("android-library-conventions")
id(libs.plugins.kotlin.multiplatform.get().pluginId)
id(libs.plugins.android.library.get().pluginId)
id(libs.plugins.compose.get().pluginId)
id(libs.plugins.mavenPublish.get().pluginId)
}

android { namespace = "dev.sargunv.maplibrecompose.expressions" }

mavenPublishing {
pom {
name = "MapLibre Compose Expressions"
description = "MapLibre expressions DSL for MapLibre Compose."
url = "https://github.com/sargunv/maplibre-compose"
}
}

kotlin {
androidTarget {
compilerOptions { jvmTarget.set(JvmTarget.JVM_11) }
instrumentedTestVariant.sourceSetTree.set(KotlinSourceSetTree.test)
publishLibraryVariants("release", "debug")
}
iosArm64()
iosSimulatorArm64()
iosX64()
jvm("desktop")

sourceSets {
commonMain.dependencies { implementation(compose.foundation) }

commonTest.dependencies {
implementation(kotlin("test"))
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}

androidUnitTest.dependencies { implementation(compose.desktop.currentOs) }

androidInstrumentedTest.dependencies {
implementation(compose.desktop.uiTestJUnit4)
implementation(libs.androidx.composeUi.testManifest)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package dev.sargunv.maplibrecompose.core.expression
package dev.sargunv.maplibrecompose.expressions

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.const
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.heatmapDensity
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.interpolate
import dev.sargunv.maplibrecompose.core.expression.ExpressionsDsl.linear

// helpers for default expression values
import dev.sargunv.maplibrecompose.expressions.ast.Expression
import dev.sargunv.maplibrecompose.expressions.dsl.const
import dev.sargunv.maplibrecompose.expressions.dsl.heatmapDensity
import dev.sargunv.maplibrecompose.expressions.dsl.interpolate
import dev.sargunv.maplibrecompose.expressions.dsl.linear
import dev.sargunv.maplibrecompose.expressions.value.ColorValue
import dev.sargunv.maplibrecompose.expressions.value.ListValue
import dev.sargunv.maplibrecompose.expressions.value.StringValue

public val ZeroPadding: PaddingValues.Absolute = PaddingValues.Absolute(0.dp)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package dev.sargunv.maplibrecompose.expressions

import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import dev.sargunv.maplibrecompose.expressions.ast.CompiledExpression
import dev.sargunv.maplibrecompose.expressions.ast.Expression
import dev.sargunv.maplibrecompose.expressions.value.FloatValue

/**
* The context used while converting a high-level [Expression] to a low-level [CompiledExpression].
*
* It defines how to resolve certain expressions (TextUnit, bitmaps) to their MapLibre counterparts.
* MapLibre Compose users should not need to implement this interface; it is used internally by the
* MapLibre Compose library.
*/
public interface ExpressionContext {
/** The scale factor to convert EMs to the desired unit */
public val emScale: Expression<FloatValue>

/** The scale factor to convert SPs to the desired unit */
public val spScale: Expression<FloatValue>

/** @return the resolved identifier for the [bitmap]. */
public fun resolveBitmap(bitmap: ImageBitmap): String

/** @return the resolved identifier for the [painter]. */
public fun resolvePainter(painter: Painter): String

/** A context where no complex types can be resolved. */
public object None : ExpressionContext {
override val emScale: Expression<FloatValue>
get() = error("TextUnit not allowed in this context")

override val spScale: Expression<FloatValue>
get() = error("TextUnit not allowed in this context")

override fun resolveBitmap(bitmap: ImageBitmap): String =
error("Bitmap not allowed in this context")

override fun resolvePainter(painter: Painter): String =
error("Painter not allowed in this context")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.sargunv.maplibrecompose.expressions.ast

import androidx.compose.ui.graphics.ImageBitmap
import dev.sargunv.maplibrecompose.expressions.ExpressionContext
import dev.sargunv.maplibrecompose.expressions.value.StringValue

/**
* A [Literal] representing an [ImageBitmap] value, which will be loaded as an image into the style
* upon compilation.
*/
public data class BitmapLiteral private constructor(override val value: ImageBitmap) :
Literal<StringValue, ImageBitmap> {
override fun compile(context: ExpressionContext): StringLiteral =
StringLiteral.of(context.resolveBitmap(value))

override fun visit(block: (Expression<*>) -> Unit): Unit = block(this)

public companion object {
public fun of(value: ImageBitmap): BitmapLiteral = BitmapLiteral(value)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dev.sargunv.maplibrecompose.expressions.ast

import dev.sargunv.maplibrecompose.expressions.value.BooleanValue

/** A [Literal] representing a [Boolean] value. */
public data class BooleanLiteral private constructor(override val value: Boolean) :
CompiledLiteral<BooleanValue, Boolean> {
override fun visit(block: (Expression<*>) -> Unit): Unit = block(this)

public companion object {
private val True: BooleanLiteral = BooleanLiteral(true)
private val False: BooleanLiteral = BooleanLiteral(false)

public fun of(value: Boolean): BooleanLiteral = if (value) True else False
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dev.sargunv.maplibrecompose.expressions.ast

import androidx.compose.ui.graphics.Color
import dev.sargunv.maplibrecompose.expressions.value.ColorValue

/** A [Literal] representing a [Color] value. */
public data class ColorLiteral private constructor(override val value: Color) :
CompiledLiteral<ColorValue, Color> {
override fun visit(block: (Expression<*>) -> Unit): Unit = block(this)

public companion object {
private val black = ColorLiteral(Color.Black)
private val white = ColorLiteral(Color.White)
private val transparent = ColorLiteral(Color.Transparent)

public fun of(value: Color): ColorLiteral =
when (value) {
Color.Black -> black
Color.White -> white
Color.Transparent -> transparent
else -> ColorLiteral(value)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package dev.sargunv.maplibrecompose.expressions.ast

import dev.sargunv.maplibrecompose.expressions.ExpressionContext
import dev.sargunv.maplibrecompose.expressions.value.ExpressionValue

/**
* An [Expression] reduced to only those data types supported by the MapLibre SDKs. This can be
* thought of as an intermediate representation between the high level expression DSL and the
* platform-specific encoding.
*/
public sealed interface CompiledExpression<out T : ExpressionValue> : Expression<T> {
override fun compile(context: ExpressionContext): CompiledExpression<T> = this

@Suppress("UNCHECKED_CAST")
override fun <X : ExpressionValue> cast(): CompiledExpression<X> = this as CompiledExpression<X>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dev.sargunv.maplibrecompose.expressions.ast

import dev.sargunv.maplibrecompose.expressions.value.ExpressionValue

/** A [Literal] representing an function call with args all of [CompiledExpression] */
public data class CompiledFunctionCall
private constructor(
val name: String,
val args: List<CompiledExpression<*>>,
val isLiteralArg: (Int) -> Boolean,
) : CompiledExpression<ExpressionValue> {
override fun visit(block: (Expression<*>) -> Unit) {
block(this)
args.forEach { it.visit(block) }
}

public companion object {
public fun of(
name: String,
args: List<CompiledExpression<*>>,
isLiteralArg: (Int) -> Boolean = { false },
): CompiledFunctionCall = CompiledFunctionCall(name, args, isLiteralArg)
}
}
Loading

0 comments on commit 03aa838

Please sign in to comment.