Compare commits
19
Commits
f382a1b836
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64e0d5de03
|
||
|
|
2cf32128ba
|
||
|
|
2b900afc30
|
||
|
|
d059d031bd
|
||
|
|
caff1a07cc
|
||
|
|
fe766c8820
|
||
|
|
1e6a374e96
|
||
|
|
02b92308d0
|
||
|
|
a9c8a89aac | ||
|
|
a69d5644a5 | ||
|
|
eab348d187 | ||
|
|
1877455c70 | ||
|
|
0b6074aaba | ||
|
|
875d809189 | ||
|
|
3960f5cb5d | ||
|
|
07368a408c | ||
|
|
844da3ac93 | ||
|
|
af27e53150 | ||
|
|
9bced78550 |
@@ -50,10 +50,27 @@ jobs:
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew testDebugUnitTest
|
||||
|
||||
- name: Build APK
|
||||
run: |
|
||||
VERSION_CODE=$(git rev-list --count HEAD)
|
||||
./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE
|
||||
VERSION_BASE=$(git describe --tags --abbrev=0 | sed 's/^v//')
|
||||
VERSION_DEV=$(git rev-list --count $(git describe --tags --abbrev=0)..HEAD)
|
||||
if [ $VERSION_DEV -gt 0 ]; then
|
||||
VERSION_NAME="${VERSION_BASE}.${VERSION_DEV}"
|
||||
else
|
||||
VERSION_NAME="${VERSION_BASE}"
|
||||
fi
|
||||
./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: app/build/outputs/apk/signedRelease/app-signedRelease.apk
|
||||
compression-level: 0
|
||||
archive: false
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v3
|
||||
|
||||
+3
-1
@@ -22,4 +22,6 @@ app/release/
|
||||
captures/
|
||||
local.properties
|
||||
keystore.properties
|
||||
key.jks
|
||||
key.jks
|
||||
# generated map intermediates; the packed .bmap in assets is what ships
|
||||
mapdata/*.geojson
|
||||
|
||||
+7
-17
@@ -2,7 +2,6 @@ plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.10'
|
||||
id 'org.jetbrains.kotlin.plugin.compose' version '2.4.10'
|
||||
id 'com.mikepenz.aboutlibraries.plugin' version '15.2.0'
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -15,7 +14,7 @@ android {
|
||||
applicationId 'net.helcel.beans'
|
||||
minSdk = 28
|
||||
targetSdk = 37
|
||||
versionName "1.5"
|
||||
versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1.5"
|
||||
versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1
|
||||
}
|
||||
signingConfigs {
|
||||
@@ -74,16 +73,17 @@ android {
|
||||
kotlinCompilerExtensionVersion = "2.2.20"
|
||||
}
|
||||
|
||||
androidResources {
|
||||
// The app ships English strings only; without this the libraries drag
|
||||
// in some eighty locales of their own.
|
||||
localeFilters += ["en"]
|
||||
}
|
||||
|
||||
lint {
|
||||
disable 'UsingMaterialAndMaterial3Libraries'
|
||||
}
|
||||
|
||||
}
|
||||
aboutLibraries {
|
||||
library {
|
||||
exclusionPatterns = [~"androidx.*", ~"com.google.android.*", ~"org.jetbrains.*"]
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.compose.material3:material3:1.4.0'
|
||||
@@ -94,20 +94,10 @@ dependencies {
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0'
|
||||
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.compose.ui:ui'
|
||||
implementation "androidx.activity:activity-ktx:1.13.0"
|
||||
|
||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||
implementation 'com.google.android.material:material:1.14.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0'
|
||||
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
|
||||
implementation 'com.mikepenz:aboutlibraries:14.2.1'
|
||||
implementation 'com.mikepenz:aboutlibraries-compose-m3:15.2.0'
|
||||
implementation 'com.mikepenz:aboutlibraries-core:15.2.0'
|
||||
|
||||
|
||||
implementation platform('androidx.compose:compose-bom:2026.08.00')
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 3.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 3.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 4.0 MiB |
@@ -1,15 +1,16 @@
|
||||
package net.helcel.beans.activity
|
||||
|
||||
import android.graphics.drawable.PictureDrawable
|
||||
import android.os.Bundle
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
@@ -21,30 +22,42 @@ import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Percent
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.caverock.androidsvg.RenderOptions
|
||||
import com.github.chrisbanes.photoview.PhotoView
|
||||
import net.helcel.beans.BuildConfig
|
||||
import net.helcel.beans.activity.sub.EditPlaceDialog
|
||||
import net.helcel.beans.activity.sub.MapPickDialog
|
||||
import net.helcel.beans.activity.sub.applyDirectVisit
|
||||
import net.helcel.beans.activity.sub.commitVisitDialog
|
||||
import net.helcel.beans.countries.GeoLoc
|
||||
import net.helcel.beans.countries.GeoLocImporter
|
||||
import net.helcel.beans.countries.GeoLocTree
|
||||
import net.helcel.beans.helper.Data
|
||||
import net.helcel.beans.helper.Settings
|
||||
import net.helcel.beans.svg.CSSWrapper
|
||||
import net.helcel.beans.svg.SVGWrapper
|
||||
import net.helcel.beans.map.MapAssets
|
||||
import net.helcel.beans.map.MapStyle
|
||||
import net.helcel.beans.map.MapView
|
||||
import net.helcel.beans.map.MapWorld
|
||||
import net.helcel.beans.map.MapReader
|
||||
|
||||
|
||||
class MainScreen : ComponentActivity() {
|
||||
|
||||
private var psvg by mutableStateOf<SVGWrapper?>(null)
|
||||
private var css by mutableStateOf<CSSWrapper?>(null)
|
||||
private var world by mutableStateOf<MapWorld?>(null)
|
||||
private var loadToken = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -57,7 +70,15 @@ class MainScreen : ComponentActivity() {
|
||||
|
||||
setContent {
|
||||
SysTheme {
|
||||
Box(modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.primary).statusBarsPadding(),) {
|
||||
// Both bars: without the navigation inset the system buttons sit on top of
|
||||
// whatever is at the bottom of a screen, such as the About button.
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colors.primary)
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding(),
|
||||
) {
|
||||
AppNavHost()
|
||||
}
|
||||
}
|
||||
@@ -68,13 +89,7 @@ class MainScreen : ComponentActivity() {
|
||||
fun AppNavHost() {
|
||||
val navController = rememberNavController()
|
||||
NavHost(navController, startDestination = "main") {
|
||||
composable("main") {
|
||||
val currentPsvg = psvg
|
||||
val currentCss = css
|
||||
if (currentPsvg != null && currentCss != null) {
|
||||
MainScreenC(currentPsvg, currentCss, navController)
|
||||
}
|
||||
}
|
||||
composable("main") { MainScreenC(world, navController) }
|
||||
composable("settings") { SettingsMainScreen { navController.navigate("main") } }
|
||||
composable("edit") { EditScreen { navController.navigate("main") } }
|
||||
composable("stats") { StatsScreen { navController.navigate("main") } }
|
||||
@@ -82,7 +97,7 @@ class MainScreen : ComponentActivity() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainScreenC(psvg: SVGWrapper,css: CSSWrapper, nav: NavHostController){
|
||||
fun MainScreenC(world: MapWorld?, nav: NavHostController){
|
||||
SysTheme {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -102,40 +117,87 @@ class MainScreen : ComponentActivity() {
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Box(modifier = Modifier.padding(innerPadding)) {
|
||||
MapScreen(psvg, css)
|
||||
Box(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
|
||||
if (world == null) LoadingMap() else MapScreen(world)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MapScreen(psvg: SVGWrapper, css: CSSWrapper) {
|
||||
Box {
|
||||
val cssContent = css.get()
|
||||
val drawable = remember(psvg, css, cssContent) {
|
||||
val opt: RenderOptions = RenderOptions.create()
|
||||
opt.css(cssContent)
|
||||
PictureDrawable(psvg.get()?.renderToPicture(opt))
|
||||
}
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
PhotoView(ctx).apply {
|
||||
setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null)
|
||||
maximumScale = 64f
|
||||
scaleType = ImageView.ScaleType.FIT_CENTER
|
||||
}
|
||||
},
|
||||
update = { view ->
|
||||
view.setImageDrawable(drawable)
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
fun LoadingMap() {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = MaterialTheme.colors.primary,
|
||||
strokeWidth = 4.dp,
|
||||
modifier = Modifier.size(50.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MapScreen(world: MapWorld) {
|
||||
val ctx = LocalContext.current
|
||||
val visits by Data.visits.visitsFlow.collectAsState()
|
||||
val groups by Data.groups.groupsFlow.collectAsState()
|
||||
val land = MaterialTheme.colors.onBackground.toArgb()
|
||||
val background = MaterialTheme.colors.background.toArgb()
|
||||
val style = remember(visits, groups, land, background) {
|
||||
MapStyle.build(ctx, land, background)
|
||||
}
|
||||
val touchRadius = Settings.getTouchRadius(ctx)
|
||||
|
||||
var candidates by remember { mutableStateOf<List<GeoLoc>>(emptyList()) }
|
||||
var showColor by remember { mutableStateOf(false) }
|
||||
|
||||
// A place is either applied straight away, or the colour dialog picks
|
||||
// the group for it, exactly as it does from the edit list.
|
||||
fun select(loc: GeoLoc) {
|
||||
if (!applyDirectVisit(ctx, loc)) showColor = true
|
||||
}
|
||||
|
||||
if (candidates.isNotEmpty()) {
|
||||
MapPickDialog(
|
||||
candidates = candidates,
|
||||
onPick = { candidates = emptyList(); select(it) },
|
||||
onDismiss = { candidates = emptyList() },
|
||||
)
|
||||
}
|
||||
if (showColor) {
|
||||
EditPlaceDialog(false) { cleared ->
|
||||
showColor = false
|
||||
commitVisitDialog(cleared)
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
factory = { MapView(it) },
|
||||
update = { view ->
|
||||
view.world = world
|
||||
view.style = style
|
||||
view.touchRadiusDp = touchRadius
|
||||
// Always the tree, even for a single hit: landing on a region is
|
||||
// just as often a way of reaching the country around it.
|
||||
view.onPick = { picks ->
|
||||
val locs = picks.mapNotNull { GeoLocTree.find(it.code) }
|
||||
if (locs.isNotEmpty()) candidates = locs
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
/** Reloads the map asset, on a worker thread since it is a few megabytes. */
|
||||
fun refreshProjection() {
|
||||
psvg = SVGWrapper(this)
|
||||
css = CSSWrapper(this)
|
||||
val asset = MapAssets.assetFor(this)
|
||||
val token = ++loadToken
|
||||
world = null
|
||||
Thread {
|
||||
val parsed = assets.open(asset).use { MapReader.read(it) }
|
||||
runOnUiThread { if (token == loadToken) world = parsed }
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.RadioButton
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Slider
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -37,6 +38,7 @@ import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@@ -53,24 +55,26 @@ import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.helcel.beans.R
|
||||
import net.helcel.beans.activity.sub.AboutScreen
|
||||
import net.helcel.beans.activity.sub.EditPlaceColorDialog
|
||||
import net.helcel.beans.activity.sub.LicenseScreen
|
||||
import net.helcel.beans.countries.GeoLocImporter
|
||||
import net.helcel.beans.helper.DEFAULT_TOUCH_RADIUS
|
||||
import net.helcel.beans.helper.Data
|
||||
import net.helcel.beans.helper.defaultPreferences
|
||||
import net.helcel.beans.helper.MAX_TOUCH_RADIUS
|
||||
import net.helcel.beans.helper.Settings
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun SysTheme(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val prefs = defaultPreferences(context)
|
||||
val themeKey = prefs.getString(stringResource(R.string.key_theme), stringResource(R.string.system))
|
||||
val darkTheme = when (themeKey) {
|
||||
stringResource(R.string.system) -> isSystemInDarkTheme()
|
||||
@@ -99,28 +103,22 @@ fun SysTheme(
|
||||
isLight = !darkTheme,
|
||||
)
|
||||
|
||||
MaterialTheme(
|
||||
colors = m2colors,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun settingsNav(): NavHostController {
|
||||
val navController = rememberNavController()
|
||||
NavHost(navController, startDestination= "settings"){
|
||||
composable("settings"){SettingsScreen(navController)}
|
||||
composable("licenses"){ LicenseScreen() }
|
||||
composable("about"){ AboutScreen() }
|
||||
// Both, because the app mixes the two: most of it is Material 2, while the
|
||||
// dividers are Material 3 and would otherwise fall back to the default
|
||||
// light scheme.
|
||||
androidx.compose.material3.MaterialTheme(colorScheme = colorScheme) {
|
||||
MaterialTheme(
|
||||
colors = m2colors,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
return navController
|
||||
}
|
||||
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SettingsMainScreen(onExit: ()->Unit = {}) {
|
||||
val nav: NavHostController = settingsNav()
|
||||
val nav: NavHostController = rememberNavController()
|
||||
SysTheme {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@@ -140,23 +138,30 @@ fun SettingsMainScreen(onExit: ()->Unit = {}) {
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
// One host, inside the scaffold. Building it here and again as a
|
||||
// default argument left two of them: settings drawn twice, and
|
||||
// only the inner one ever navigating.
|
||||
Box(modifier = Modifier.padding(innerPadding)) {
|
||||
SettingsScreen()
|
||||
NavHost(nav, startDestination = "settings") {
|
||||
composable("settings") { SettingsScreen(nav) }
|
||||
composable("about") { AboutScreen() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(navController: NavHostController = settingsNav()) {
|
||||
fun SettingsScreen(navController: NavHostController) {
|
||||
val context = LocalContext.current
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val prefs = defaultPreferences(context)
|
||||
val keyTheme = stringResource(R.string.key_theme)
|
||||
val defaultTheme = stringResource(R.string.system)
|
||||
val keyProjection = stringResource(R.string.key_projection)
|
||||
val keyGroup = stringResource(R.string.key_group)
|
||||
val keyRegional = stringResource(R.string.key_regional)
|
||||
val keyCascadeStats = stringResource(R.string.key_cascade_stats)
|
||||
val keyTouchRadius = stringResource(R.string.key_touch_radius)
|
||||
val offString = stringResource(R.string.off)
|
||||
val onString = stringResource(R.string.on)
|
||||
|
||||
@@ -165,6 +170,9 @@ fun SettingsScreen(navController: NavHostController = settingsNav()) {
|
||||
var groups by remember { mutableStateOf(prefs.getString(keyGroup, offString)!!) }
|
||||
var regional by remember { mutableStateOf(prefs.getString(keyRegional, offString)!!) }
|
||||
var cascadeStats by remember { mutableStateOf(prefs.getString(keyCascadeStats, offString)!!) }
|
||||
var touchRadius by remember {
|
||||
mutableIntStateOf(prefs.getInt(keyTouchRadius, DEFAULT_TOUCH_RADIUS))
|
||||
}
|
||||
|
||||
var showGroupDialog by remember { mutableStateOf(false) }
|
||||
var showRegionalDialog by remember { mutableStateOf(false) }
|
||||
@@ -261,7 +269,11 @@ fun SettingsScreen(navController: NavHostController = settingsNav()) {
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
MultiPreference(
|
||||
arrayOf(stringResource(R.string.mercator), stringResource(R.string.azimuthalequidistant)),
|
||||
arrayOf(
|
||||
stringResource(R.string.mercator),
|
||||
stringResource(R.string.azimuthalequidistant),
|
||||
stringResource(R.string.loximuthal),
|
||||
),
|
||||
projection
|
||||
) { newProj ->
|
||||
projection = newProj
|
||||
@@ -322,6 +334,36 @@ fun SettingsScreen(navController: NavHostController = settingsNav()) {
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
item {
|
||||
Text(
|
||||
stringResource(R.string.pref_touch_radius),
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.pref_touch_radius_desc),
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Slider(
|
||||
value = touchRadius.toFloat(),
|
||||
onValueChange = { touchRadius = it.roundToInt() },
|
||||
onValueChangeFinished = { prefs.edit { putInt(keyTouchRadius, touchRadius) } },
|
||||
valueRange = 0f..MAX_TOUCH_RADIUS.toFloat(),
|
||||
steps = MAX_TOUCH_RADIUS - 1,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.pref_touch_radius_value, touchRadius),
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
modifier = Modifier.padding(start = 12.dp)
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
item {
|
||||
val launcher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument(),
|
||||
@@ -351,10 +393,6 @@ fun SettingsScreen(navController: NavHostController = settingsNav()) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
item {
|
||||
PreferenceButton(stringResource(R.string.licenses)) {
|
||||
if (navController.currentDestination?.route != "licenses")
|
||||
navController.navigate("licenses")
|
||||
}
|
||||
PreferenceButton(stringResource(R.string.about)) {
|
||||
if (navController.currentDestination?.route != "about")
|
||||
navController.navigate("about")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.helcel.beans.activity.sub
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -54,6 +55,44 @@ fun EditPlaceScreenPreview(){
|
||||
EditPlaceScreen(Group.EEE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a place straight away where a single group makes the choice obvious,
|
||||
* as tapping the checkbox does. Returns false when the group still has to be
|
||||
* picked from the colour dialog.
|
||||
*/
|
||||
fun applyDirectVisit(ctx: Context, loc: GeoLoc): Boolean {
|
||||
Data.selected_geoloc = loc
|
||||
Data.selected_group = null
|
||||
if (Data.groups.size() != 1 || !Settings.isSingleGroup(ctx)) return false
|
||||
val current = Data.visits.getVisited(loc)
|
||||
Data.visits.setVisited(
|
||||
loc,
|
||||
if (current == NO_GROUP || current == AUTO_GROUP) Data.groups.getUniqueEntry()!!.key
|
||||
else if (loc.children.any { Data.visits.getVisited(it) != NO_GROUP }) AUTO_GROUP
|
||||
else NO_GROUP,
|
||||
)
|
||||
syncVisited()
|
||||
Data.saveData()
|
||||
Data.selected_geoloc = null
|
||||
return true
|
||||
}
|
||||
|
||||
/** Stores whatever the colour dialog came back with. */
|
||||
fun commitVisitDialog(cleared: Boolean) {
|
||||
if (cleared) {
|
||||
Data.visits.setVisited(Data.selected_geoloc, NO_GROUP)
|
||||
syncVisited()
|
||||
Data.saveData()
|
||||
}
|
||||
if (Data.selected_group != null && Data.selected_geoloc != null) {
|
||||
Data.visits.setVisited(Data.selected_geoloc, Data.selected_group!!.key)
|
||||
syncVisited()
|
||||
Data.saveData()
|
||||
}
|
||||
Data.selected_geoloc = null
|
||||
Data.selected_group = null
|
||||
}
|
||||
|
||||
fun syncVisited(loc: GeoLoc?=World.WWW): Boolean {
|
||||
var changed = false
|
||||
loc?.children?.forEach { tt ->
|
||||
@@ -122,22 +161,7 @@ fun EditPlaceScreen(loc: GeoLoc, onExit:()->Unit={}) {
|
||||
if(showEdit)
|
||||
EditPlaceDialog(false) {
|
||||
showEdit = false
|
||||
if (it) {
|
||||
Data.visits.setVisited(Data.selected_geoloc, NO_GROUP)
|
||||
syncVisited()
|
||||
Data.saveData()
|
||||
|
||||
if (Data.selected_geoloc!=null && Data.selected_geoloc!!.children.any { itc-> Data.visits.getVisited(itc) != NO_GROUP }) {
|
||||
Data.clearing_geoloc = Data.selected_geoloc
|
||||
}
|
||||
}
|
||||
if (Data.selected_group != null && Data.selected_geoloc != null) {
|
||||
Data.visits.setVisited(Data.selected_geoloc, Data.selected_group!!.key)
|
||||
syncVisited()
|
||||
Data.saveData()
|
||||
}
|
||||
Data.selected_geoloc = null
|
||||
Data.selected_group = null
|
||||
commitVisitDialog(it)
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -167,22 +191,7 @@ fun EditPlaceScreen(loc: GeoLoc, onExit:()->Unit={}) {
|
||||
tabs.add(loc)
|
||||
}
|
||||
}, {
|
||||
Data.selected_geoloc = loc
|
||||
if (Data.groups.size() == 1 && Settings.isSingleGroup(ctx)) {
|
||||
Data.visits.setVisited(Data.selected_geoloc,
|
||||
if (it != ToggleableState.On) Data.groups.getUniqueEntry()!!.key
|
||||
else if(Data.selected_geoloc?.children?.any{ itc->
|
||||
Data.visits.getVisited(itc)!= NO_GROUP } == true) AUTO_GROUP
|
||||
else NO_GROUP
|
||||
)
|
||||
Data.saveData()
|
||||
Data.selected_group = null
|
||||
} else {
|
||||
Data.selected_group = null
|
||||
showEdit=true
|
||||
}
|
||||
syncVisited()
|
||||
Data.saveData()
|
||||
if (!applyDirectVisit(ctx, loc)) showEdit = true
|
||||
})
|
||||
|
||||
}
|
||||
@@ -196,7 +205,7 @@ fun EditPlaceScreen(loc: GeoLoc, onExit:()->Unit={}) {
|
||||
fun GeoLocRow(
|
||||
loc: GeoLoc,
|
||||
onClick: () -> Unit,
|
||||
onCheckedChange: (ToggleableState) -> Unit
|
||||
onToggle: () -> Unit,
|
||||
) {
|
||||
val visits by Data.visits.visitsFlow.collectAsState()
|
||||
val checked by remember(visits, loc) {
|
||||
@@ -235,7 +244,7 @@ fun GeoLocRow(
|
||||
|
||||
TriStateCheckbox(
|
||||
state = checked,
|
||||
onClick= { onCheckedChange(checked) },
|
||||
onClick = onToggle,
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = color,
|
||||
),
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package net.helcel.beans.activity.sub
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.mikepenz.aboutlibraries.ui.compose.DefaultChipColors
|
||||
import com.mikepenz.aboutlibraries.ui.compose.DefaultLibraryColors
|
||||
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import net.helcel.beans.R
|
||||
import net.helcel.beans.activity.SysTheme
|
||||
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LicenseScreen() {
|
||||
val libraries = produceLibraries(R.raw.aboutlibraries)
|
||||
SysTheme {
|
||||
LibrariesContainer(
|
||||
libraries = libraries.value,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
colors = DefaultLibraryColors(
|
||||
libraryBackgroundColor = MaterialTheme.colors.background,
|
||||
libraryContentColor = MaterialTheme.colors.onBackground,
|
||||
licenseChipColors = DefaultChipColors(
|
||||
containerColor = MaterialTheme.colors.primary,
|
||||
contentColor = MaterialTheme.colors.onPrimary,
|
||||
),
|
||||
versionChipColors = DefaultChipColors(
|
||||
containerColor = MaterialTheme.colors.secondary,
|
||||
contentColor = MaterialTheme.colors.onSecondary,
|
||||
),
|
||||
fundingChipColors = DefaultChipColors(
|
||||
containerColor = MaterialTheme.colors.secondary,
|
||||
contentColor = MaterialTheme.colors.onSecondary,
|
||||
),
|
||||
dialogConfirmButtonColor = MaterialTheme.colors.primary,
|
||||
dialogBackgroundColor = MaterialTheme.colors.onPrimary,
|
||||
dialogContentColor = MaterialTheme.colors.primary,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package net.helcel.beans.activity.sub
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import net.helcel.beans.R
|
||||
import net.helcel.beans.activity.SysTheme
|
||||
import net.helcel.beans.countries.GeoLoc
|
||||
import net.helcel.beans.countries.GeoLocTree
|
||||
import net.helcel.beans.helper.AUTO_GROUP
|
||||
import net.helcel.beans.helper.Data
|
||||
import net.helcel.beans.helper.NO_GROUP
|
||||
|
||||
private class PickRow(val loc: GeoLoc, val depth: Int)
|
||||
|
||||
/**
|
||||
* Turns the shapes a tap landed near into an indented list.
|
||||
*
|
||||
* Each candidate brings its continent and country along, so a tap that is
|
||||
* ambiguous between two regions can still be answered with "the whole country"
|
||||
* or "the whole continent" instead of only the regions themselves.
|
||||
*/
|
||||
private fun rowsFor(candidates: List<GeoLoc>): List<PickRow> {
|
||||
val rows = LinkedHashMap<String, PickRow>()
|
||||
candidates.forEach { candidate ->
|
||||
GeoLocTree.chain(candidate).forEachIndexed { depth, loc ->
|
||||
rows.getOrPut(loc.code) { PickRow(loc, depth) }
|
||||
}
|
||||
}
|
||||
return rows.values.toList()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MapPickDialog(
|
||||
candidates: List<GeoLoc>,
|
||||
onPick: (GeoLoc) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val visits by Data.visits.visitsFlow.collectAsState()
|
||||
val rows = remember(candidates) { rowsFor(candidates) }
|
||||
|
||||
SysTheme {
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
MaterialTheme.colors.background,
|
||||
RoundedCornerShape(corner = CornerSize(16.dp)),
|
||||
)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
text = stringResource(R.string.select_place),
|
||||
)
|
||||
Text(
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
text = stringResource(R.string.select_place_sub),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 360.dp),
|
||||
) {
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||
items(rows, key = { it.loc.code }) { row ->
|
||||
val group = visits.getOrElse(row.loc.code) { NO_GROUP }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onPick(row.loc) }
|
||||
.background(
|
||||
Color(88, 88, 88, 88),
|
||||
RoundedCornerShape(corner = CornerSize(16.dp)),
|
||||
)
|
||||
.padding(
|
||||
start = 8.dp + 16.dp * row.depth,
|
||||
top = 8.dp,
|
||||
end = 8.dp,
|
||||
bottom = 8.dp,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.background(
|
||||
if (group == NO_GROUP || group == AUTO_GROUP) {
|
||||
MaterialTheme.colors.onBackground
|
||||
} else {
|
||||
Color(
|
||||
Data.groups.getGroupFromKey(group).color.color
|
||||
)
|
||||
},
|
||||
CircleShape,
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
style = MaterialTheme.typography.body2,
|
||||
text = row.loc.fullName,
|
||||
// Long names wrap inside the row rather
|
||||
// than running off the end of it.
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ object GeoLocImporter {
|
||||
|
||||
fun importStates(ctx: Context, force: Boolean = false) {
|
||||
if (!Settings.isRegional(ctx) and !force) {
|
||||
GeoLocTree.rebuild()
|
||||
return
|
||||
}
|
||||
val fs = BufferedReader(InputStreamReader(ctx.assets.open("geoloc_state.txt")))
|
||||
@@ -26,6 +27,7 @@ object GeoLocImporter {
|
||||
}
|
||||
}
|
||||
}
|
||||
GeoLocTree.rebuild()
|
||||
}
|
||||
|
||||
fun clearStates() {
|
||||
@@ -39,6 +41,7 @@ object GeoLocImporter {
|
||||
}
|
||||
country.children.clear()
|
||||
}
|
||||
GeoLocTree.rebuild()
|
||||
Data.saveData()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.helcel.beans.countries
|
||||
|
||||
/**
|
||||
* Code to place lookup, plus the chain of parents above a place.
|
||||
*
|
||||
* Regions only exist once [GeoLocImporter] has read them, so this is rebuilt
|
||||
* whenever that set changes rather than derived once and cached forever.
|
||||
*/
|
||||
object GeoLocTree {
|
||||
|
||||
private var index: Map<String, GeoLoc> = emptyMap()
|
||||
private var parents: Map<String, GeoLoc> = emptyMap()
|
||||
|
||||
fun rebuild() {
|
||||
val byCode = HashMap<String, GeoLoc>()
|
||||
val parent = HashMap<String, GeoLoc>()
|
||||
byCode[World.WWW.code] = World.WWW
|
||||
World.WWW.children.forEach { child ->
|
||||
byCode[child.code] = child
|
||||
parent[child.code] = World.WWW
|
||||
child.children.forEach { country ->
|
||||
byCode[country.code] = country
|
||||
parent[country.code] = child
|
||||
country.children.forEach { state ->
|
||||
byCode[state.code] = state
|
||||
parent[state.code] = country
|
||||
}
|
||||
}
|
||||
}
|
||||
index = byCode
|
||||
parents = parent
|
||||
}
|
||||
|
||||
fun find(code: String): GeoLoc? {
|
||||
if (index.isEmpty()) rebuild()
|
||||
return index[code]
|
||||
}
|
||||
|
||||
/** [loc] and everything above it, world first, so it reads as a path. */
|
||||
fun chain(loc: GeoLoc): List<GeoLoc> {
|
||||
if (index.isEmpty()) rebuild()
|
||||
val path = ArrayList<GeoLoc>(4)
|
||||
var current: GeoLoc? = loc
|
||||
while (current != null && current != World.WWW) {
|
||||
path.add(current)
|
||||
current = parents[current.code]
|
||||
}
|
||||
return path.reversed()
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import net.helcel.beans.R
|
||||
import net.helcel.beans.countries.GeoLoc
|
||||
import java.util.HashMap
|
||||
@@ -23,7 +22,6 @@ object Data {
|
||||
|
||||
var selected_group : Groups.Group? = null
|
||||
var selected_geoloc: GeoLoc? = null
|
||||
var clearing_geoloc: GeoLoc? = null
|
||||
|
||||
private val groupsSerial = Groups.GroupsSerializer()
|
||||
private val visitsSerial = Visits.VisitsSerializer()
|
||||
@@ -31,7 +29,7 @@ object Data {
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
|
||||
fun loadData(ctx: Context, id:Int) {
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
sharedPreferences = defaultPreferences(ctx)
|
||||
|
||||
val groupsString = sharedPreferences.getString("groups_$id",null)
|
||||
val visitsString = sharedPreferences.getString("visits_$id",null)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package net.helcel.beans.helper
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
/**
|
||||
* The preference file opened directly, under the name and mode androidx's
|
||||
* PreferenceManager used. Settings written by earlier versions still load, and
|
||||
* the app no longer pulls in the preference UI framework for one call.
|
||||
*/
|
||||
fun defaultPreferences(ctx: Context): SharedPreferences =
|
||||
ctx.getSharedPreferences(ctx.packageName + "_preferences", Context.MODE_PRIVATE)
|
||||
@@ -2,17 +2,26 @@ package net.helcel.beans.helper
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import net.helcel.beans.R
|
||||
import net.helcel.beans.activity.MainScreen
|
||||
|
||||
/**
|
||||
* Default distance, in dp, around a tap that still counts as hitting a place.
|
||||
* Kept small because the radius is measured on screen: zoomed out to the whole
|
||||
* world, even a few dp reach across several countries.
|
||||
*/
|
||||
const val DEFAULT_TOUCH_RADIUS = 4
|
||||
|
||||
/** Largest tap radius the setting offers. */
|
||||
const val MAX_TOUCH_RADIUS = 48
|
||||
|
||||
object Settings {
|
||||
|
||||
private lateinit var sp: SharedPreferences
|
||||
private lateinit var mainActivity: MainScreen
|
||||
fun start(ctx: MainScreen) {
|
||||
mainActivity = ctx
|
||||
sp = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
sp = defaultPreferences(ctx)
|
||||
}
|
||||
|
||||
fun isSingleGroup(ctx: Context): Boolean {
|
||||
@@ -29,6 +38,14 @@ object Settings {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* How far around a tap, in dp, the map looks for other places. Anything
|
||||
* within it is offered as a candidate instead of painting straight away.
|
||||
*/
|
||||
fun getTouchRadius(ctx: Context): Float {
|
||||
return sp.getInt(ctx.getString(R.string.key_touch_radius), DEFAULT_TOUCH_RADIUS).toFloat()
|
||||
}
|
||||
|
||||
fun isCascadeStats(ctx: Context): Boolean {
|
||||
return getBooleanValue(
|
||||
ctx,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package net.helcel.beans.map
|
||||
|
||||
import android.content.Context
|
||||
import net.helcel.beans.R
|
||||
import net.helcel.beans.helper.defaultPreferences
|
||||
|
||||
/** Picks the map asset matching the projection the user chose. */
|
||||
object MapAssets {
|
||||
|
||||
fun assetFor(ctx: Context): String {
|
||||
val preferences = defaultPreferences(ctx)
|
||||
return when (
|
||||
preferences.getString(
|
||||
ctx.getString(R.string.key_projection),
|
||||
ctx.getString(R.string.mercator),
|
||||
)
|
||||
) {
|
||||
ctx.getString(R.string.azimuthalequidistant) -> "aeqd01.bmap"
|
||||
ctx.getString(R.string.loximuthal) -> "loxim01.bmap"
|
||||
else -> "webmercator01.bmap"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package net.helcel.beans.map
|
||||
|
||||
import android.graphics.Path
|
||||
import android.graphics.RectF
|
||||
import java.io.InputStream
|
||||
|
||||
private const val VERSION = 2
|
||||
|
||||
/**
|
||||
* Reads the packed map assets produced by `packmap.js`.
|
||||
*
|
||||
* The maps used to ship as SVG, which spends around four megabytes per
|
||||
* projection spelling coordinates out in decimal ASCII inside XML. The packed
|
||||
* form says the same thing in a fraction of that, and reading it is a straight
|
||||
* scan over a byte array instead of an XML parse with a float scanner on top.
|
||||
*
|
||||
* The format is documented in `packmap.js`; the two have to agree.
|
||||
*/
|
||||
object MapReader {
|
||||
|
||||
fun read(input: InputStream): MapWorld {
|
||||
val reader = ByteReader(input.readBytes())
|
||||
reader.readHeader()
|
||||
val width = reader.unsigned() * reader.scale
|
||||
val height = reader.unsigned() * reader.scale
|
||||
val count = reader.unsigned()
|
||||
val shapes = ArrayList<MapShape>(count)
|
||||
repeat(count) { shapes.add(reader.shape()) }
|
||||
return MapWorld(width, height, shapes)
|
||||
}
|
||||
}
|
||||
|
||||
private class ByteReader(private val data: ByteArray) {
|
||||
|
||||
private var pos = 0
|
||||
|
||||
/** One grid step in user units, read from the header. */
|
||||
var scale = 1f
|
||||
private set
|
||||
|
||||
fun readHeader() {
|
||||
require(
|
||||
data.size > 5 &&
|
||||
data[0] == 'B'.code.toByte() && data[1] == 'M'.code.toByte() &&
|
||||
data[2] == 'A'.code.toByte() && data[3] == 'P'.code.toByte()
|
||||
) { "not a packed map" }
|
||||
require(data[4].toInt() == VERSION) { "unsupported packed map version ${data[4]}" }
|
||||
pos = 5
|
||||
scale = 1f / unsigned()
|
||||
}
|
||||
|
||||
fun unsigned(): Int {
|
||||
var result = 0
|
||||
var shift = 0
|
||||
while (true) {
|
||||
val byte = data[pos++].toInt()
|
||||
result = result or ((byte and 0x7F) shl shift)
|
||||
if (byte and 0x80 == 0) return result
|
||||
shift += 7
|
||||
}
|
||||
}
|
||||
|
||||
/** Zigzag, because deltas run both ways and are almost always tiny. */
|
||||
fun signed(): Int {
|
||||
val value = unsigned()
|
||||
return if (value and 1 == 1) -((value ushr 1) + 1) else value ushr 1
|
||||
}
|
||||
|
||||
fun flag(): Boolean = data[pos++].toInt() != 0
|
||||
|
||||
fun text(): String {
|
||||
val length = unsigned()
|
||||
val value = String(data, pos, length, Charsets.UTF_8)
|
||||
pos += length
|
||||
return value
|
||||
}
|
||||
|
||||
fun shape(): MapShape {
|
||||
val code = text()
|
||||
val isState = flag()
|
||||
val ringCount = unsigned()
|
||||
|
||||
val rings = ArrayList<FloatArray>(ringCount)
|
||||
var nonZero: Path? = null
|
||||
var evenOdd: Path? = null
|
||||
var minX = Float.MAX_VALUE
|
||||
var minY = Float.MAX_VALUE
|
||||
var maxX = -Float.MAX_VALUE
|
||||
var maxY = -Float.MAX_VALUE
|
||||
|
||||
repeat(ringCount) {
|
||||
val points = unsigned()
|
||||
val useEvenOdd = flag()
|
||||
val ring = FloatArray(points * 2)
|
||||
// Each point is a delta from the one before it, the first from the
|
||||
// origin, so the whole ring decodes in one pass.
|
||||
var x = 0
|
||||
var y = 0
|
||||
for (i in 0 until points) {
|
||||
x += signed()
|
||||
y += signed()
|
||||
val fx = x * scale
|
||||
val fy = y * scale
|
||||
ring[i * 2] = fx
|
||||
ring[i * 2 + 1] = fy
|
||||
if (fx < minX) minX = fx
|
||||
if (fx > maxX) maxX = fx
|
||||
if (fy < minY) minY = fy
|
||||
if (fy > maxY) maxY = fy
|
||||
}
|
||||
rings.add(ring)
|
||||
|
||||
val path = if (useEvenOdd) {
|
||||
evenOdd ?: Path().also { it.fillType = Path.FillType.EVEN_ODD; evenOdd = it }
|
||||
} else {
|
||||
nonZero ?: Path().also { nonZero = it }
|
||||
}
|
||||
path.moveTo(ring[0], ring[1])
|
||||
var i = 2
|
||||
while (i < ring.size) {
|
||||
path.lineTo(ring[i], ring[i + 1])
|
||||
i += 2
|
||||
}
|
||||
path.close()
|
||||
}
|
||||
|
||||
return MapShape(
|
||||
code, isState, rings,
|
||||
RectF(minX, minY, maxX, maxY),
|
||||
nonZero, evenOdd,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package net.helcel.beans.map
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.RectF
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/** Draws a [MapWorld] onto a canvas through a pan/zoom transform. */
|
||||
class MapRenderer(private val map: MapWorld) {
|
||||
|
||||
private val fill = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
private val stroke = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
}
|
||||
private val inverse = Matrix()
|
||||
private val values = FloatArray(9)
|
||||
private val visible = RectF()
|
||||
|
||||
/** False when the border is too faint to be worth a draw call at all. */
|
||||
private var strokeVisible = true
|
||||
|
||||
/**
|
||||
* Paints the map into [canvas], mapping user units through [transform].
|
||||
* Shapes outside the canvas are skipped, which is what makes drawing cheap
|
||||
* once the map is zoomed in.
|
||||
*/
|
||||
fun draw(canvas: Canvas, transform: Matrix, style: MapStyle) {
|
||||
// SRC rather than blended: the bitmap is reused between renders.
|
||||
canvas.drawColor(style.background, PorterDuff.Mode.SRC)
|
||||
if (!transform.invert(inverse)) return
|
||||
visible.set(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat())
|
||||
inverse.mapRect(visible)
|
||||
|
||||
// Widths are given in device pixels, so undo the zoom the canvas is
|
||||
// about to apply to them, then let them grow slowly with it.
|
||||
transform.getValues(values)
|
||||
val scale = values[Matrix.MSCALE_X]
|
||||
val perPixel = 1f / scale
|
||||
val fitted = min(canvas.width / map.width, canvas.height / map.height)
|
||||
val zoom = (scale / fitted).coerceAtLeast(1f)
|
||||
val regionPx = strokeWidthAt(zoom, REGION_STROKE_STOPS)
|
||||
val countryPx = strokeWidthAt(zoom, COUNTRY_STROKE_STOPS)
|
||||
|
||||
canvas.save()
|
||||
canvas.concat(transform)
|
||||
setStroke(style.background, if (style.regional) regionPx else countryPx, perPixel)
|
||||
for ((code, color) in style.fills) {
|
||||
val shape = map.byCode[code] ?: continue
|
||||
if (!RectF.intersects(shape.bounds, visible)) continue
|
||||
fill.color = color
|
||||
drawShape(canvas, shape.nonZero, true)
|
||||
drawShape(canvas, shape.evenOdd, true)
|
||||
}
|
||||
|
||||
// With regions on, the country outlines come back on top as the thicker
|
||||
// borders between them.
|
||||
if (style.regional) {
|
||||
setStroke(style.background, countryPx, perPixel)
|
||||
for (shape in map.countries) {
|
||||
if (!RectF.intersects(shape.bounds, visible)) continue
|
||||
drawShape(canvas, shape.nonZero, false)
|
||||
drawShape(canvas, shape.evenOdd, false)
|
||||
}
|
||||
}
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a border width given in device pixels.
|
||||
*
|
||||
* Anything thinner than a pixel cannot be drawn as such: the rasteriser
|
||||
* turns it into a one pixel hairline at full strength, which is why every
|
||||
* border below that width used to come out the same weight no matter how
|
||||
* far out the map was. Below a pixel the width is carried as opacity
|
||||
* instead, so a quarter-pixel border reads as a quarter as dark.
|
||||
*/
|
||||
private fun setStroke(color: Int, devicePx: Float, perPixel: Float) {
|
||||
strokeVisible = devicePx > 0.004f
|
||||
if (!strokeVisible) return
|
||||
stroke.color = color
|
||||
if (devicePx < 1f) {
|
||||
stroke.strokeWidth = perPixel
|
||||
stroke.alpha = (devicePx * 255f).roundToInt().coerceIn(0, 255)
|
||||
} else {
|
||||
stroke.strokeWidth = perPixel * devicePx
|
||||
stroke.alpha = 255
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawShape(canvas: Canvas, path: Path?, filled: Boolean) {
|
||||
if (path == null) return
|
||||
if (filled) canvas.drawPath(path, fill)
|
||||
if (strokeVisible) canvas.drawPath(path, stroke)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package net.helcel.beans.map
|
||||
|
||||
import android.content.Context
|
||||
import net.helcel.beans.countries.GeoLoc
|
||||
import net.helcel.beans.countries.World
|
||||
import net.helcel.beans.helper.AUTO_GROUP
|
||||
import net.helcel.beans.helper.Data
|
||||
import net.helcel.beans.helper.NO_GROUP
|
||||
import net.helcel.beans.helper.Settings
|
||||
import kotlin.math.ln
|
||||
|
||||
/**
|
||||
* Border width in device pixels at each zoom, interpolated in between.
|
||||
*
|
||||
* Zoom counts how far in the map is from its fitted size, so 1 is the whole
|
||||
* world on screen and 64 is as far in as it goes. The stops step by fours and
|
||||
* are interpolated on a log axis, so the weight changes evenly as you pinch
|
||||
* rather than in a rush at one end.
|
||||
*
|
||||
* Region borders start at nothing on purpose: a sub-national border says very
|
||||
* little with the whole world in view, and drawing three and a half thousand of
|
||||
* them only muddies the land.
|
||||
*
|
||||
* zoom 1x 4x 16x 64x
|
||||
* region 0.0 0.5 1.0 2.0
|
||||
* country 1.0 1.5 3.0 6.0
|
||||
*/
|
||||
val STROKE_ZOOM_STOPS = floatArrayOf(1f, 4f, 16f, 64f)
|
||||
|
||||
val REGION_STROKE_STOPS = floatArrayOf(0f, 0.5f, 1f, 2f)
|
||||
|
||||
val COUNTRY_STROKE_STOPS = floatArrayOf(1f, 1.5f, 3f, 6f)
|
||||
|
||||
/** The width for [zoom], straight-line between the stops on a log axis. */
|
||||
fun strokeWidthAt(zoom: Float, widths: FloatArray): Float {
|
||||
if (zoom <= STROKE_ZOOM_STOPS.first()) return widths.first()
|
||||
for (i in 1 until STROKE_ZOOM_STOPS.size) {
|
||||
if (zoom <= STROKE_ZOOM_STOPS[i]) {
|
||||
val low = STROKE_ZOOM_STOPS[i - 1]
|
||||
val t = ln(zoom / low) / ln(STROKE_ZOOM_STOPS[i] / low)
|
||||
return widths[i - 1] + t * (widths[i] - widths[i - 1])
|
||||
}
|
||||
}
|
||||
return widths.last()
|
||||
}
|
||||
|
||||
/**
|
||||
* The colour every shape is drawn with, resolved once per render.
|
||||
*
|
||||
* This replaces the stylesheet the map used to be rendered through. A place
|
||||
* inherits the colour of its parent, so painting a whole continent still shows
|
||||
* up on each country inside it, and [fills] ends up holding exactly the shapes
|
||||
* that get drawn — which is also exactly what a tap may land on.
|
||||
*/
|
||||
class MapStyle(
|
||||
val regional: Boolean,
|
||||
val land: Int,
|
||||
val background: Int,
|
||||
val fills: Map<String, Int>,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun build(ctx: Context, land: Int, background: Int): MapStyle {
|
||||
val regional = Settings.isRegional(ctx)
|
||||
val fills = HashMap<String, Int>()
|
||||
World.WWW.children.forEach { child ->
|
||||
if (child.type == GeoLoc.LocType.COUNTRY) {
|
||||
// A country hanging straight off the world, such as Antarctica.
|
||||
addCountry(fills, child, colorOf(World.WWW, regional), regional, land)
|
||||
} else {
|
||||
val continent = colorOf(child, regional) ?: colorOf(World.WWW, regional)
|
||||
child.children.forEach { addCountry(fills, it, continent, regional, land) }
|
||||
}
|
||||
}
|
||||
return MapStyle(regional, land, background, fills)
|
||||
}
|
||||
|
||||
private fun addCountry(
|
||||
fills: HashMap<String, Int>,
|
||||
country: GeoLoc,
|
||||
inherited: Int?,
|
||||
regional: Boolean,
|
||||
land: Int,
|
||||
) {
|
||||
val color = colorOf(country, regional) ?: inherited
|
||||
// Regions are only drawn for countries that actually have some; the
|
||||
// rest keep their outline filled so the map stays complete.
|
||||
if (regional && country.children.isNotEmpty()) {
|
||||
country.children.forEach { state ->
|
||||
fills[state.code] = colorOf(state, regional) ?: color ?: land
|
||||
}
|
||||
} else {
|
||||
fills[country.code] = color ?: land
|
||||
}
|
||||
}
|
||||
|
||||
/** The group colour assigned to [loc], or `null` when it has none of its own. */
|
||||
private fun colorOf(loc: GeoLoc, regional: Boolean): Int? {
|
||||
val key = Data.visits.getVisited(loc)
|
||||
val group = Data.groups.getGroupFromKey(key)
|
||||
if (group.key != NO_GROUP) return group.color.color
|
||||
// A place marked automatically stands in for its children. That only
|
||||
// needs a colour of its own where the children are not drawn, and
|
||||
// never on a continent, which would swallow the whole map.
|
||||
val isParent = loc.type == GeoLoc.LocType.GROUP ||
|
||||
loc.type == GeoLoc.LocType.CUSTOM_GROUP ||
|
||||
loc.type == GeoLoc.LocType.WORLD
|
||||
if (key == AUTO_GROUP && !regional && !isParent) {
|
||||
return Data.groups.getGroupFromPos(0).second.color.color
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package net.helcel.beans.map
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Paint
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.SystemClock
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.view.View
|
||||
import android.widget.OverScroller
|
||||
import androidx.core.graphics.createBitmap
|
||||
import kotlin.math.min
|
||||
|
||||
/** A shape a tap landed on or near, with how far away it was in user units. */
|
||||
class MapPick(val code: String, val distance: Float)
|
||||
|
||||
/** How far past the fitted map a pinch may zoom in. */
|
||||
private const val MAX_ZOOM = 64f
|
||||
|
||||
/** How much a double tap zooms in by. */
|
||||
private const val DOUBLE_TAP_ZOOM = 3f
|
||||
|
||||
/** Time a gesture has to settle before the map is redrawn sharp again. */
|
||||
private const val RENDER_DELAY_MS = 80L
|
||||
|
||||
/** How long the map may stay stretched while a gesture keeps going. */
|
||||
private const val MAX_STALE_MS = 500L
|
||||
|
||||
/**
|
||||
* Most candidates a single tap will offer. A wide radius over a crowded part of
|
||||
* the map can reach dozens of places, which is more of a list than anyone wants
|
||||
* to read; the nearest few are what the tap was plausibly aiming at.
|
||||
*/
|
||||
private const val MAX_CANDIDATES = 16
|
||||
|
||||
/**
|
||||
* The map itself: pan, pinch and tap over a [MapWorld].
|
||||
*
|
||||
* Drawing every country as vectors costs far too much to do on each frame, so a
|
||||
* render lands in an off-screen bitmap on a worker thread and gestures just move
|
||||
* that bitmap around. Once the gesture settles the map is drawn again at the new
|
||||
* zoom, which is what keeps it sharp all the way in.
|
||||
*/
|
||||
@SuppressLint("ViewConstructor")
|
||||
class MapView(context: Context) : View(context) {
|
||||
|
||||
var world: MapWorld? = null
|
||||
set(value) {
|
||||
if (field === value) return
|
||||
field = value
|
||||
renderer = value?.let(::MapRenderer)
|
||||
fitToView()
|
||||
requestRender(0L)
|
||||
}
|
||||
|
||||
@Volatile
|
||||
var style: MapStyle? = null
|
||||
set(value) {
|
||||
if (field === value) return
|
||||
field = value
|
||||
requestRender(0L)
|
||||
}
|
||||
|
||||
/** How far from a tap, in dp, other shapes still count as candidates. */
|
||||
var touchRadiusDp: Float = 4f
|
||||
|
||||
/** Called on a tap with every nearby shape, nearest first. */
|
||||
var onPick: ((List<MapPick>) -> Unit)? = null
|
||||
|
||||
@Volatile
|
||||
private var renderer: MapRenderer? = null
|
||||
|
||||
private val scroller = OverScroller(context)
|
||||
|
||||
private val transform = Matrix()
|
||||
private val inverse = Matrix()
|
||||
private val values = FloatArray(9)
|
||||
private val point = FloatArray(2)
|
||||
private var minScale = 1f
|
||||
private var maxScale = 1f
|
||||
|
||||
private val blitPaint = Paint(Paint.FILTER_BITMAP_FLAG)
|
||||
private val blit = Matrix()
|
||||
|
||||
private val lock = Any()
|
||||
private var front: Bitmap? = null
|
||||
private var back: Bitmap? = null
|
||||
private val frontMatrix = Matrix()
|
||||
private val frontValues = FloatArray(9)
|
||||
private val pendingMatrix = Matrix()
|
||||
private var pendingWidth = 0
|
||||
private var pendingHeight = 0
|
||||
@Volatile private var lastRenderAt = 0L
|
||||
|
||||
private val renderThread = HandlerThread("map-render").apply { start() }
|
||||
private val renderHandler = Handler(renderThread.looper)
|
||||
private val renderTask = Runnable { render() }
|
||||
|
||||
private val scaleDetector = ScaleGestureDetector(
|
||||
context,
|
||||
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
val scale = currentScale()
|
||||
val target = (scale * detector.scaleFactor).coerceIn(minScale, maxScale)
|
||||
transform.postScale(target / scale, target / scale, detector.focusX, detector.focusY)
|
||||
clamp()
|
||||
invalidate()
|
||||
return true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private val gestureDetector = GestureDetector(
|
||||
context,
|
||||
object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
scroller.forceFinished(true)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onFling(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
velocityX: Float,
|
||||
velocityY: Float,
|
||||
): Boolean {
|
||||
val map = world ?: return false
|
||||
transform.getValues(values)
|
||||
val scale = values[Matrix.MSCALE_X]
|
||||
val x = values[Matrix.MTRANS_X].toInt()
|
||||
val y = values[Matrix.MTRANS_Y].toInt()
|
||||
val w = map.width * scale
|
||||
val h = map.height * scale
|
||||
scroller.forceFinished(true)
|
||||
scroller.fling(
|
||||
x, y, velocityX.toInt(), velocityY.toInt(),
|
||||
if (w > width) (width - w).toInt() else x, if (w > width) 0 else x,
|
||||
if (h > height) (height - h).toInt() else y, if (h > height) 0 else y,
|
||||
)
|
||||
postInvalidateOnAnimation()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float,
|
||||
): Boolean {
|
||||
transform.postTranslate(-distanceX, -distanceY)
|
||||
clamp()
|
||||
invalidate()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
pick(e.x, e.y)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||
val scale = currentScale()
|
||||
val target = if (scale >= maxScale * 0.99f) {
|
||||
minScale
|
||||
} else {
|
||||
min(scale * DOUBLE_TAP_ZOOM, maxScale)
|
||||
}
|
||||
transform.postScale(target / scale, target / scale, e.x, e.y)
|
||||
clamp()
|
||||
invalidate()
|
||||
return true
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
scaleDetector.onTouchEvent(event)
|
||||
gestureDetector.onTouchEvent(event)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun computeScroll() {
|
||||
if (!scroller.computeScrollOffset()) return
|
||||
transform.getValues(values)
|
||||
values[Matrix.MTRANS_X] = scroller.currX.toFloat()
|
||||
values[Matrix.MTRANS_Y] = scroller.currY.toFloat()
|
||||
transform.setValues(values)
|
||||
postInvalidateOnAnimation()
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
fitToView()
|
||||
requestRender(0L)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
val current = style
|
||||
canvas.drawColor(current?.background ?: 0)
|
||||
|
||||
var stale = true
|
||||
synchronized(lock) {
|
||||
val bitmap = front
|
||||
if (bitmap != null && !bitmap.isRecycled && frontMatrix.invert(blit)) {
|
||||
blit.postConcat(transform)
|
||||
canvas.drawBitmap(bitmap, blit, blitPaint)
|
||||
transform.getValues(values)
|
||||
stale = !values.contentEquals(frontValues)
|
||||
}
|
||||
}
|
||||
// Scheduling only ever happens from here or from a setter, both on the
|
||||
// main thread, so a render never starts while its bitmap is on screen.
|
||||
if (stale) {
|
||||
val overdue = SystemClock.uptimeMillis() - lastRenderAt > MAX_STALE_MS
|
||||
requestRender(if (overdue) 0L else RENDER_DELAY_MS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
scroller.forceFinished(true)
|
||||
renderHandler.removeCallbacks(renderTask)
|
||||
renderThread.quitSafely()
|
||||
// Dropped rather than recycled: a render may still be part way through
|
||||
// one of them, and the collector is happy to take them from here.
|
||||
synchronized(lock) {
|
||||
front = null
|
||||
back = null
|
||||
}
|
||||
}
|
||||
|
||||
/** Centres the map in the view and works out how far it may be zoomed. */
|
||||
private fun fitToView() {
|
||||
val map = world ?: return
|
||||
if (width == 0 || height == 0 || map.width <= 0f || map.height <= 0f) return
|
||||
val scale = min(width / map.width, height / map.height)
|
||||
minScale = scale
|
||||
maxScale = scale * MAX_ZOOM
|
||||
transform.setScale(scale, scale)
|
||||
transform.postTranslate(
|
||||
(width - map.width * scale) / 2f,
|
||||
(height - map.height * scale) / 2f,
|
||||
)
|
||||
}
|
||||
|
||||
private fun currentScale(): Float {
|
||||
transform.getValues(values)
|
||||
return values[Matrix.MSCALE_X]
|
||||
}
|
||||
|
||||
/** Keeps the map from being dragged away from the viewport. */
|
||||
private fun clamp() {
|
||||
val map = world ?: return
|
||||
transform.getValues(values)
|
||||
val scale = values[Matrix.MSCALE_X]
|
||||
val w = map.width * scale
|
||||
val h = map.height * scale
|
||||
values[Matrix.MTRANS_X] = if (w <= width) {
|
||||
(width - w) / 2f
|
||||
} else {
|
||||
values[Matrix.MTRANS_X].coerceIn(width - w, 0f)
|
||||
}
|
||||
values[Matrix.MTRANS_Y] = if (h <= height) {
|
||||
(height - h) / 2f
|
||||
} else {
|
||||
values[Matrix.MTRANS_Y].coerceIn(height - h, 0f)
|
||||
}
|
||||
transform.setValues(values)
|
||||
}
|
||||
|
||||
private fun requestRender(delay: Long) {
|
||||
if (width == 0 || height == 0 || renderer == null || style == null) return
|
||||
synchronized(lock) {
|
||||
pendingMatrix.set(transform)
|
||||
pendingWidth = width
|
||||
pendingHeight = height
|
||||
}
|
||||
renderHandler.removeCallbacks(renderTask)
|
||||
renderHandler.postDelayed(renderTask, delay)
|
||||
}
|
||||
|
||||
private fun render() {
|
||||
val matrix = Matrix()
|
||||
val w: Int
|
||||
val h: Int
|
||||
synchronized(lock) {
|
||||
matrix.set(pendingMatrix)
|
||||
w = pendingWidth
|
||||
h = pendingHeight
|
||||
}
|
||||
if (w <= 0 || h <= 0) return
|
||||
val current = renderer ?: return
|
||||
val currentStyle = style ?: return
|
||||
|
||||
var target = back
|
||||
if (target == null || target.width != w || target.height != h) {
|
||||
target?.recycle()
|
||||
target = createBitmap(w, h)
|
||||
back = target
|
||||
}
|
||||
current.draw(Canvas(target), matrix, currentStyle)
|
||||
|
||||
synchronized(lock) {
|
||||
back = front
|
||||
front = target
|
||||
frontMatrix.set(matrix)
|
||||
matrix.getValues(frontValues)
|
||||
}
|
||||
lastRenderAt = SystemClock.uptimeMillis()
|
||||
postInvalidate()
|
||||
}
|
||||
|
||||
/** Reports every shape within the tap radius of ([x], [y]), nearest first. */
|
||||
private fun pick(x: Float, y: Float) {
|
||||
val map = world ?: return
|
||||
val current = style ?: return
|
||||
val callback = onPick ?: return
|
||||
if (!transform.invert(inverse)) return
|
||||
|
||||
point[0] = x
|
||||
point[1] = y
|
||||
inverse.mapPoints(point)
|
||||
val radius = touchRadiusDp * resources.displayMetrics.density / currentScale()
|
||||
|
||||
val picks = ArrayList<MapPick>()
|
||||
for (code in current.fills.keys) {
|
||||
val shape = map.byCode[code] ?: continue
|
||||
val bounds = shape.bounds
|
||||
if (point[0] < bounds.left - radius || point[0] > bounds.right + radius) continue
|
||||
if (point[1] < bounds.top - radius || point[1] > bounds.bottom + radius) continue
|
||||
val distance = shape.distanceTo(point[0], point[1])
|
||||
if (distance <= radius) picks.add(MapPick(code, distance))
|
||||
}
|
||||
if (picks.isEmpty()) return
|
||||
picks.sortBy { it.distance }
|
||||
callback(if (picks.size > MAX_CANDIDATES) picks.subList(0, MAX_CANDIDATES) else picks)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package net.helcel.beans.map
|
||||
|
||||
import android.graphics.Path
|
||||
import android.graphics.RectF
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* One named piece of map geometry: a country outline or a single region.
|
||||
*
|
||||
* The geometry is kept twice on purpose. [nonZero] and [evenOdd] are what the
|
||||
* renderer hands to the canvas, while [rings] keeps the raw points because
|
||||
* hit testing needs the distance to an edge, which a [Path] cannot give us.
|
||||
*/
|
||||
class MapShape(
|
||||
val code: String,
|
||||
val isState: Boolean,
|
||||
val rings: List<FloatArray>,
|
||||
val bounds: RectF,
|
||||
val nonZero: Path?,
|
||||
val evenOdd: Path?,
|
||||
) {
|
||||
|
||||
/** True when ([x], [y]) falls inside the shape. Holes and lakes count as outside. */
|
||||
fun contains(x: Float, y: Float): Boolean {
|
||||
if (x < bounds.left || x > bounds.right || y < bounds.top || y > bounds.bottom) return false
|
||||
// Even-odd crossing count over every ring at once: rings never overlap, so
|
||||
// an island still reads as inside and a hole still reads as outside.
|
||||
var inside = false
|
||||
for (ring in rings) {
|
||||
var j = ring.size - 2
|
||||
var i = 0
|
||||
while (i < ring.size) {
|
||||
val yi = ring[i + 1]
|
||||
val yj = ring[j + 1]
|
||||
if ((yi > y) != (yj > y)) {
|
||||
val xi = ring[i]
|
||||
val xj = ring[j]
|
||||
if (x < xi + (y - yi) * (xj - xi) / (yj - yi)) inside = !inside
|
||||
}
|
||||
j = i
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
return inside
|
||||
}
|
||||
|
||||
/** Distance from ([x], [y]) to the shape, `0` when the point is inside it. */
|
||||
fun distanceTo(x: Float, y: Float): Float {
|
||||
if (contains(x, y)) return 0f
|
||||
var best = Float.MAX_VALUE
|
||||
for (ring in rings) {
|
||||
var j = ring.size - 2
|
||||
var i = 0
|
||||
while (i < ring.size) {
|
||||
val d = segmentDistanceSq(x, y, ring[j], ring[j + 1], ring[i], ring[i + 1])
|
||||
if (d < best) best = d
|
||||
j = i
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
return if (best == Float.MAX_VALUE) Float.MAX_VALUE else sqrt(best)
|
||||
}
|
||||
|
||||
private fun segmentDistanceSq(
|
||||
px: Float, py: Float,
|
||||
ax: Float, ay: Float,
|
||||
bx: Float, by: Float,
|
||||
): Float {
|
||||
val dx = bx - ax
|
||||
val dy = by - ay
|
||||
val len = dx * dx + dy * dy
|
||||
val t = if (len <= 0f) 0f else (((px - ax) * dx + (py - ay) * dy) / len).coerceIn(0f, 1f)
|
||||
val cx = px - (ax + t * dx)
|
||||
val cy = py - (ay + t * dy)
|
||||
return cx * cx + cy * cy
|
||||
}
|
||||
}
|
||||
|
||||
/** The whole map, in SVG user units, as a flat set of named shapes. */
|
||||
class MapWorld(val width: Float, val height: Float, val shapes: List<MapShape>) {
|
||||
|
||||
val byCode: Map<String, MapShape> = shapes.associateBy { it.code }
|
||||
|
||||
/** Country outlines, keyed by ISO code. Drawn as borders when regions are on. */
|
||||
val countries: List<MapShape> = shapes.filter { !it.isState }
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package net.helcel.beans.svg
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import net.helcel.beans.countries.World
|
||||
import net.helcel.beans.helper.AUTO_GROUP
|
||||
import net.helcel.beans.helper.Data.groups
|
||||
import net.helcel.beans.helper.Data.visits
|
||||
import net.helcel.beans.helper.NO_GROUP
|
||||
import net.helcel.beans.helper.Settings
|
||||
import net.helcel.beans.helper.Theme.colorToHex6
|
||||
|
||||
|
||||
class CSSWrapper(private val ctx: Context) {
|
||||
|
||||
private val continents: String = World.WWW.children.joinToString(",") { "#${it.code}2" }
|
||||
private val countries: String = World.WWW.children.joinToString(",") { itt ->
|
||||
itt.children.joinToString(",") { "#${it.code}2" }
|
||||
}
|
||||
private val regional: String = World.WWW.children.joinToString(",") { itt ->
|
||||
itt.children.joinToString(",") { "#${it.code}1" }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getBaseColors() : Pair<String, String> {
|
||||
val colorForeground = colorToHex6(MaterialTheme.colors.onBackground.toArgb().toDrawable())
|
||||
val colorBackground = colorToHex6(MaterialTheme.colors.background.toArgb().toDrawable())
|
||||
|
||||
return Pair(colorForeground, colorBackground)
|
||||
}
|
||||
|
||||
private var customCSS: String = ""
|
||||
|
||||
init {
|
||||
refresh()
|
||||
}
|
||||
|
||||
|
||||
private fun refresh() {
|
||||
val id = if (Settings.isRegional(ctx)) "1" else "2"
|
||||
customCSS = visits.getVisitedByValue().map { (k, v) ->
|
||||
(if (groups.getGroupFromKey(k).key != NO_GROUP) {
|
||||
v
|
||||
} else if (!Settings.isRegional(ctx) && k == AUTO_GROUP) {
|
||||
v.filter { it !in World.WWW.children.map { it1 -> it1.code } }
|
||||
} else {
|
||||
emptyList()
|
||||
}).takeIf { it.isNotEmpty() }
|
||||
?.joinToString(",") { "#${it}$id,#${it}" } + "{fill:${
|
||||
if (k == AUTO_GROUP) colorToHex6(groups.getGroupFromPos(0).second.color)
|
||||
else colorToHex6(groups.getGroupFromKey(k).color)
|
||||
};}"
|
||||
}.joinToString("")
|
||||
}
|
||||
@Composable
|
||||
fun get(): String {
|
||||
val (colorForeground,colorBackground) = getBaseColors()
|
||||
refresh()
|
||||
return if (Settings.isRegional(ctx)) {
|
||||
val countryRegionalCSS: String =
|
||||
"svg{fill:$colorForeground;stroke:$colorBackground;stroke-width:0.01;}" +
|
||||
"$continents,$countries{fill:none;stroke:$colorBackground;stroke-width:0.1;}"
|
||||
countryRegionalCSS + customCSS
|
||||
} else {
|
||||
val countryOnlyCSS: String =
|
||||
"svg{fill:$colorForeground;stroke:$colorBackground;stroke-width:0.1;}" +
|
||||
"${regional}{display:none;}"
|
||||
countryOnlyCSS + customCSS
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package net.helcel.beans.svg
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.caverock.androidsvg.SVG
|
||||
import net.helcel.beans.R
|
||||
|
||||
class SVGWrapper(ctx: Context) {
|
||||
|
||||
private val sharedPreferences: SharedPreferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
private val svgFile = when (sharedPreferences.getString(
|
||||
ctx.getString(R.string.key_projection),
|
||||
ctx.getString(R.string.mercator)
|
||||
)) {
|
||||
ctx.getString(R.string.azimuthalequidistant) -> "aeqd01.svg"
|
||||
ctx.getString(R.string.loximuthal) -> "loxim01.svg"
|
||||
ctx.getString(R.string.mercator) -> "webmercator01.svg"
|
||||
else -> "webmercator01.svg"
|
||||
}
|
||||
|
||||
private var svg: SVG? = SVG.getFromAsset(ctx.assets, svgFile)
|
||||
|
||||
fun get(): SVG? {
|
||||
return svg
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,6 @@
|
||||
android:scaleY="0.8918919"
|
||||
android:translateX="6.918919"
|
||||
android:translateY="6.918919">
|
||||
|
||||
<group
|
||||
android:scaleX="1.5"
|
||||
android:scaleY="1.5"
|
||||
android:translateX="10"
|
||||
android:translateY="10">
|
||||
<path
|
||||
android:pathData="M36,36m-28,0a28,28 0,1 1,56 0a28,28 0,1 1,-56 0"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
|
||||
</group>
|
||||
<group
|
||||
android:scaleX="1.25"
|
||||
android:scaleY="1.25"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Beans" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<style name="Theme.Beans" parent="android:Theme.Material.NoActionBar">
|
||||
<!-- Status bar color in dark mode -->
|
||||
<item name="android:statusBarColor">@color/blue</item>
|
||||
<!-- Window background in dark mode -->
|
||||
@@ -8,8 +8,5 @@
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
|
||||
<item name="colorPrimary">@color/blue</item>
|
||||
<item name="colorPrimaryDark">@color/blue</item>
|
||||
<item name="colorAccent">@color/blue</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -13,7 +13,6 @@
|
||||
<string name="key_stats">Statistics</string>
|
||||
<string name="counters">Prefer counters (#)</string>
|
||||
<string name="percentages">Prefer percentages (%)</string>
|
||||
<string name="licenses">Licenses</string>
|
||||
<string name="key_group">Groups</string>
|
||||
<string name="key_regional">Regional mode</string>
|
||||
<string name="key_regional_desc">Enable tracking of states and provinces</string>
|
||||
@@ -21,6 +20,10 @@
|
||||
<string name="pref_cascade_stats">Cascade statistics</string>
|
||||
<string name="pref_cascade_stats_desc">Include parents and children in the counts</string>
|
||||
<string name="key_group_desc">Enable multiple colors</string>
|
||||
<string name="key_touch_radius">key_touch_radius</string>
|
||||
<string name="pref_touch_radius">Tap radius</string>
|
||||
<string name="pref_touch_radius_desc">How far around a tap on the map other places should be considered</string>
|
||||
<string name="pref_touch_radius_value">%1$d dp</string>
|
||||
<string name="about">About</string>
|
||||
<string name="pref_category_appearance">Appearance</string>
|
||||
<string name="pref_category_features">Features</string>
|
||||
@@ -33,13 +36,14 @@
|
||||
<string name="beans_is_foss">Beans is free and open source software, licensed under the GNU General Public License (version 3 or later)</string>
|
||||
<string name="beans_repo_uri">https://github.com/helcel-net/beans</string>
|
||||
<string name="beans_repo">Project repository: %1$s\n Feel free to report issues or contribute.</string>
|
||||
<string name="foss_licenses">Free and open source dependencies and licenses</string>
|
||||
<string name="about_beans">About the Beans application</string>
|
||||
<string name="edit_group">Select the group to assign.</string>
|
||||
<string name="edit_group_sub">Long press on a group to edit its name and color.</string>
|
||||
<string name="select_group">Select the group to keep.</string>
|
||||
<string name="select_group_sub">All others will be deleted and its mappings reassigned to the group you choose here.</string>
|
||||
<string name="delete_group">Are your sure you want to delete this group and remove all its country mappings?</string>
|
||||
<string name="select_place">Select a place</string>
|
||||
<string name="select_place_sub">Tap the place to color, or a larger one that contains it.</string>
|
||||
<string name="delete_regions">Are you sure you want to disable regions and reassign all regional mappings to the corresponding countries?</string>
|
||||
|
||||
<string name="name">Name</string>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.Beans" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<style name="Theme.Beans" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<!-- Status bar color -->
|
||||
<item name="android:statusBarColor">@color/blue</item>
|
||||
<!-- Window background - shows through statusBarsPadding() area -->
|
||||
@@ -8,9 +8,5 @@
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
|
||||
<!-- Optional: Primary colors for the theme -->
|
||||
<item name="colorPrimary">@color/blue</item>
|
||||
<item name="colorPrimaryDark">@color/blue</item>
|
||||
<item name="colorAccent">@color/blue</item>
|
||||
</style>
|
||||
</resources>
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id 'com.android.application' version '9.4.0' apply false
|
||||
id 'com.android.application' version '9.3.2' apply false
|
||||
id 'com.android.library' version '9.3.2' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '2.4.10' apply false
|
||||
}
|
||||
+1
-51
@@ -1,10 +1,8 @@
|
||||
#!/bin/node
|
||||
|
||||
import {readFileSync,writeFileSync,existsSync} from 'fs'
|
||||
import {readFileSync,existsSync} from 'fs'
|
||||
import area from '@turf/area'
|
||||
import * as turf from '@turf/turf'
|
||||
import * as path from 'path';
|
||||
import { JSDOM } from 'jsdom';
|
||||
|
||||
|
||||
const countries =
|
||||
@@ -162,52 +160,4 @@ async function run(){
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function fixSvg(svgPath) {
|
||||
const countryToRegion = {};
|
||||
for (const [region, countries] of Object.entries(groups)) {
|
||||
countries.forEach(country => countryToRegion[country] = region);
|
||||
}
|
||||
const absoluteInputPath = path.resolve(svgPath);
|
||||
if (!existsSync(absoluteInputPath)) {
|
||||
throw new Error(`Input file not found at: ${absoluteInputPath}`);
|
||||
}
|
||||
const svgContent = readFileSync(absoluteInputPath, 'utf8');
|
||||
const dom = new JSDOM(svgContent, { contentType: 'image/svg+xml' });
|
||||
const document = dom.window.document;
|
||||
const svgRoot = document.querySelector('svg');
|
||||
if (!svgRoot) {
|
||||
throw new Error("Invalid or empty SVG structure encountered.");
|
||||
}
|
||||
if (svgRoot.getAttribute('data-processed') === 'true') {
|
||||
console.log(`Skipping: File at "${svgPath}" has already been processed.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const elementGroups = Array.from(svgRoot.querySelectorAll('g'));
|
||||
elementGroups.forEach(group => {
|
||||
const currentId = group.getAttribute('id') || '';
|
||||
const baseIsoCode = currentId.replace(/\d+$/, '');
|
||||
const regionKey = countryToRegion[baseIsoCode] || 'XXXX';
|
||||
let regionGroup = svgRoot.querySelector(`g[id="${regionKey}"]`);
|
||||
if (!regionGroup) {
|
||||
regionGroup = document.createElementNS('http://w3.org', 'g');
|
||||
regionGroup.setAttribute('id', regionKey);
|
||||
svgRoot.appendChild(regionGroup);
|
||||
}
|
||||
regionGroup.appendChild(group);
|
||||
});
|
||||
svgRoot.setAttribute('data-processed', 'true');
|
||||
const absoluteOutputPath = path.resolve(svgPath);
|
||||
let cleanXmlString = svgRoot.outerHTML;
|
||||
cleanXmlString = cleanXmlString.replace(/[\r\n]+/g, '');
|
||||
cleanXmlString = cleanXmlString.replace(/xmlns="http:\/\/w3\.org"\s?/g, '');
|
||||
cleanXmlString = cleanXmlString.replace(/xmlns="http:\/\/www\.w3\.org\/2000\/svg"\s?/g, '');
|
||||
cleanXmlString = cleanXmlString.replace('<svg', '<svg xmlns="http://www.w3.org/2000/svg"');
|
||||
writeFileSync(absoluteOutputPath, cleanXmlString, 'utf8');
|
||||
}
|
||||
|
||||
run()
|
||||
fixSvg("./app/src/main/assets/loxim01.svg")
|
||||
fixSvg("./app/src/main/assets/webmercator01.svg")
|
||||
fixSvg("./app/src/main/assets/aeqd01.svg")
|
||||
+18
-57
@@ -1,7 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
LOCAL_SVG_PATH="app/src/main/assets/"
|
||||
|
||||
GADM_VERSION="4.1"
|
||||
GADM_BASEPATH="https://geodata.ucdavis.edu/gadm"
|
||||
|
||||
@@ -80,43 +78,7 @@ download_1() {
|
||||
}
|
||||
|
||||
|
||||
toSVG_0() {
|
||||
local input_files=("ATA")
|
||||
|
||||
for country in "${countries[@]}"
|
||||
do
|
||||
input_file="./temp/0/${country}.json"
|
||||
if [ -f "$input_file" ]; then
|
||||
input_files+=("$input_file")
|
||||
else
|
||||
echo "Input file $input_file not found."
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
"$mapshaper" -i combine-files ${input_files[@]} -proj webmercator -simplify 0.005 weighted keep-shapes resolution=1200x1200 -o ./app/src/main/assets/mercator0.svg svg-data=GID_0,COUNTRY id-field=GID_0
|
||||
"$mapshaper" -i combine-files ${input_files[@]} -proj aeqd +lat_0=90 -simplify 0.005 weighted keep-shapes resolution=1200x1200 -o ./app/src/main/assets/aeqd0.svg svg-data=GID_0,COUNTRY id-field=GID_0
|
||||
}
|
||||
|
||||
toSVG_1() {
|
||||
input_files=("ATA")
|
||||
|
||||
for country in "${countries[@]}"
|
||||
do
|
||||
input_file="./temp/1/${country}.json"
|
||||
# input_file="./temp/1/gadm41_${country}_1.json"
|
||||
if [ -f "$input_file" ]; then
|
||||
input_files+=("$input_file")
|
||||
else
|
||||
echo "Input file $input_file not found."
|
||||
fi
|
||||
done
|
||||
|
||||
"$mapshaper" -i combine-files ${input_files[@]} -proj webmercator -simplify 0.005 weighted keep-shapes resolution=1200x1200 -o ./app/src/main/assets/mercator1.svg svg-data=GID_0,COUNTRY,GID,NAME id-field=GID
|
||||
"$mapshaper" -i combine-files ${input_files[@]} -proj aeqd +lat_0=90 -simplify 0.005 weighted keep-shapes resolution=1200x1200 -o ./app/src/main/assets/aeqd1.svg svg-data=GID_0,COUNTRY,GID,NAME id-field=GID
|
||||
}
|
||||
|
||||
generate_svg_map() {
|
||||
generate_map() {
|
||||
local OUT_FILE="$1" # First argument: Output destination path
|
||||
local PROJ_ARGS="$2" # Second argument: Projection parameters
|
||||
shift 2 # Remove the first two arguments, leaving only the files
|
||||
@@ -125,6 +87,9 @@ generate_svg_map() {
|
||||
echo "Generating: $OUT_FILE using projection [$PROJ_ARGS]"
|
||||
echo "Processing ${#FILES_TO_RUN[@]} files..."
|
||||
|
||||
# Each feature is tagged with the place it belongs to and whether it is a
|
||||
# country outline or one of its regions. packmap.js groups on that; nothing
|
||||
# downstream needs the features split into layers.
|
||||
local JS_PIPELINE_LOGIC="
|
||||
const conflicts = {
|
||||
'Z01': 'IND', 'Z04': 'IND', 'Z05': 'IND', 'Z07': 'IND', 'Z09': 'IND',
|
||||
@@ -133,32 +98,27 @@ generate_svg_map() {
|
||||
};
|
||||
let rawCode = GID_0 || 'UNK';
|
||||
let cCode = conflicts[rawCode] ? conflicts[rawCode] : rawCode;
|
||||
// Antarctica's boundary file carries no country code at either level.
|
||||
if (cCode === 'UNK') cCode = 'ATA';
|
||||
let isGidMissing = (!GID || GID === 'undefined' || GID === 'null' || GID === '');
|
||||
if (isGidMissing) {
|
||||
COUNTRY_GROUP = cCode+'2';
|
||||
} else {
|
||||
COUNTRY_GROUP = cCode+'1';
|
||||
}
|
||||
CODE = isGidMissing ? cCode : GID;
|
||||
LEVEL = isGidMissing ? 0 : 1;
|
||||
"
|
||||
|
||||
"$mapshaper" -i "${FILES_TO_RUN[@]}" combine-files \
|
||||
-snap \
|
||||
-merge-layers force\
|
||||
-proj $PROJ_ARGS densify \
|
||||
-simplify 2% weighted keep-shapes \
|
||||
-simplify 10% weighted keep-shapes \
|
||||
-filter-islands min-area=0 \
|
||||
-sort 'this.area' ascending \
|
||||
-each "$JS_PIPELINE_LOGIC" \
|
||||
-split COUNTRY_GROUP \
|
||||
-o "$OUT_FILE" \
|
||||
format=svg \
|
||||
id-field=GID \
|
||||
precision=0.1 \
|
||||
target=*
|
||||
format=geojson
|
||||
|
||||
}
|
||||
|
||||
toSVG_01() {
|
||||
build_maps() {
|
||||
input_files=()
|
||||
|
||||
input_files+=("./temp/1/ATA.json")
|
||||
@@ -178,9 +138,12 @@ toSVG_01() {
|
||||
fi
|
||||
done
|
||||
|
||||
generate_svg_map "./app/src/main/assets/loxim01.svg" "loxim" "${input_files[@]}"
|
||||
generate_svg_map "./app/src/main/assets/webmercator01.svg" "webmercator" "${input_files[@]}"
|
||||
generate_svg_map "./app/src/main/assets/aeqd01.svg" "aeqd +lat_0=90" "${input_files[@]}"
|
||||
generate_map "./mapdata/loxim01.geojson" "loxim" "${input_files[@]}"
|
||||
generate_map "./mapdata/webmercator01.geojson" "webmercator" "${input_files[@]}"
|
||||
generate_map "./mapdata/aeqd01.geojson" "aeqd +lat_0=90" "${input_files[@]}"
|
||||
|
||||
# The app reads the packed form; the GeoJSON above is only an intermediate.
|
||||
node packmap.js ./mapdata/loxim01.geojson ./mapdata/webmercator01.geojson ./mapdata/aeqd01.geojson
|
||||
}
|
||||
|
||||
do_1() {
|
||||
@@ -199,8 +162,6 @@ do_0() {
|
||||
}
|
||||
# do_0
|
||||
# do_1
|
||||
# toSVG_0
|
||||
# toSVG_1
|
||||
toSVG_01
|
||||
build_maps
|
||||
|
||||
#CUW, CCK, XCL, CXR, IOT, BVT, ABW, FLK, GIB, HMD, KIR, SXM, MDV, MCO, NIU, NFK, PCN, MAF, SGS, VAT
|
||||
-1
@@ -1,6 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionSha256Sum=9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
|
||||
-325
@@ -1,325 +0,0 @@
|
||||
var continents = {
|
||||
XAD:"EEE",
|
||||
ALA:"EEE",
|
||||
ALB:"EEE",
|
||||
AND:"EEE",
|
||||
AUT:"EEE",
|
||||
BLR:"EEE",
|
||||
BEL:"EEE",
|
||||
BIH:"EEE",
|
||||
BGR:"EEE",
|
||||
HRV:"EEE",
|
||||
CYP:"EEE",
|
||||
CZE:"EEE",
|
||||
DNK:"EEE",
|
||||
EST:"EEE",
|
||||
FRO:"EEE",
|
||||
FIN:"EEE",
|
||||
FRA:"EEE",
|
||||
DEU:"EEE",
|
||||
GIB:"EEE",
|
||||
GRC:"EEE",
|
||||
GGY:"EEE",
|
||||
HUN:"EEE",
|
||||
ISL:"EEE",
|
||||
IRL:"EEE",
|
||||
IMN:"EEE",
|
||||
ITA:"EEE",
|
||||
JEY:"EEE",
|
||||
KAZ:"EEE",
|
||||
XKO:"EEE",
|
||||
LVA:"EEE",
|
||||
LIE:"EEE",
|
||||
LTU:"EEE",
|
||||
LUX:"EEE",
|
||||
MLT:"EEE",
|
||||
MDA:"EEE",
|
||||
MCO:"EEE",
|
||||
MNE:"EEE",
|
||||
NLD:"EEE",
|
||||
MKD:"EEE",
|
||||
NOR:"EEE",
|
||||
POL:"EEE",
|
||||
PRT:"EEE",
|
||||
ROU:"EEE",
|
||||
RUS:"EEE",
|
||||
SMR:"EEE",
|
||||
SRB:"EEE",
|
||||
SVK:"EEE",
|
||||
SVN:"EEE",
|
||||
ESP:"EEE",
|
||||
SJM:"EEE",
|
||||
SWE:"EEE",
|
||||
CHE:"EEE",
|
||||
UKR:"EEE",
|
||||
GBR:"EEE",
|
||||
VAT:"EEE",
|
||||
|
||||
AFG:"ABB",
|
||||
ARM:"ABB",
|
||||
AZE:"ABB",
|
||||
BHR:"ABB",
|
||||
BGD:"ABB",
|
||||
BTN:"ABB",
|
||||
IOT:"ABB",
|
||||
BRN:"ABB",
|
||||
KHM:"ABB",
|
||||
CCK:"ABB",
|
||||
CHN:"ABB",
|
||||
CXR:"ABB",
|
||||
GEO:"ABB",
|
||||
HKG:"ABB",
|
||||
IND:"ABB",
|
||||
IDN:"ABB",
|
||||
IRN:"ABB",
|
||||
IRQ:"ABB",
|
||||
ISR:"ABB",
|
||||
JPN:"ABB",
|
||||
JOR:"ABB",
|
||||
KWT:"ABB",
|
||||
KGZ:"ABB",
|
||||
LAO:"ABB",
|
||||
LBN:"ABB",
|
||||
MAC:"ABB",
|
||||
MYS:"ABB",
|
||||
MDV:"ABB",
|
||||
MNG:"ABB",
|
||||
MMR:"ABB",
|
||||
NPL:"ABB",
|
||||
PRK:"ABB",
|
||||
OMN:"ABB",
|
||||
PAK:"ABB",
|
||||
PSE:"ABB",
|
||||
PHL:"ABB",
|
||||
QAT:"ABB",
|
||||
SAU:"ABB",
|
||||
SGP:"ABB",
|
||||
KOR:"ABB",
|
||||
LKA:"ABB",
|
||||
SYR:"ABB",
|
||||
TWN:"ABB",
|
||||
TJK:"ABB",
|
||||
THA:"ABB",
|
||||
TLS:"ABB",
|
||||
TUR:"ABB",
|
||||
TKM:"ABB",
|
||||
ARE:"ABB",
|
||||
UZB:"ABB",
|
||||
VNM:"ABB",
|
||||
YEM:"ABB",
|
||||
ZNC:"ABB",
|
||||
|
||||
DZA:"FFF",
|
||||
AGO:"FFF",
|
||||
BDI:"FFF",
|
||||
BEN:"FFF",
|
||||
BWA:"FFF",
|
||||
BFA:"FFF",
|
||||
BDI:"FFF",
|
||||
CPV:"FFF",
|
||||
CMR:"FFF",
|
||||
CAF:"FFF",
|
||||
TCD:"FFF",
|
||||
COM:"FFF",
|
||||
COG:"FFF",
|
||||
COD:"FFF",
|
||||
CIV:"FFF",
|
||||
DJI:"FFF",
|
||||
EGY:"FFF",
|
||||
GNQ:"FFF",
|
||||
ERI:"FFF",
|
||||
ETH:"FFF",
|
||||
ATF:"FFF",
|
||||
GAB:"FFF",
|
||||
GMB:"FFF",
|
||||
GHA:"FFF",
|
||||
GIN:"FFF",
|
||||
GNB:"FFF",
|
||||
KEN:"FFF",
|
||||
LSO:"FFF",
|
||||
LBR:"FFF",
|
||||
LBY:"FFF",
|
||||
MDG:"FFF",
|
||||
MWI:"FFF",
|
||||
MLI:"FFF",
|
||||
MRT:"FFF",
|
||||
MUS:"FFF",
|
||||
MYT:"FFF",
|
||||
MAR:"FFF",
|
||||
MOZ:"FFF",
|
||||
NAM:"FFF",
|
||||
NER:"FFF",
|
||||
NGA:"FFF",
|
||||
COD:"FFF",
|
||||
REU:"FFF",
|
||||
RWA:"FFF",
|
||||
STP:"FFF",
|
||||
SEN:"FFF",
|
||||
SYC:"FFF",
|
||||
SLE:"FFF",
|
||||
SOM:"FFF",
|
||||
ZAF:"FFF",
|
||||
SSD:"FFF",
|
||||
SHN:"FFF",
|
||||
SDN:"FFF",
|
||||
SWZ:"FFF",
|
||||
TZA:"FFF",
|
||||
TGO:"FFF",
|
||||
TUN:"FFF",
|
||||
UGA:"FFF",
|
||||
COD:"FFF",
|
||||
ZMB:"FFF",
|
||||
ZWE:"FFF",
|
||||
ESH:"FFF",
|
||||
|
||||
ABW:"NNN",
|
||||
AIA:"NNN",
|
||||
ATG:"NNN",
|
||||
BHS:"NNN",
|
||||
BRB:"NNN",
|
||||
BLZ:"NNN",
|
||||
BMU:"NNN",
|
||||
BES:"NNN",
|
||||
VGB:"NNN",
|
||||
CAN:"NNN",
|
||||
CYM:"NNN",
|
||||
XCL:"NNN",
|
||||
CRI:"NNN",
|
||||
CUB:"NNN",
|
||||
CUW:"NNN",
|
||||
DMA:"NNN",
|
||||
DOM:"NNN",
|
||||
SLV:"NNN",
|
||||
GRL:"NNN",
|
||||
GRD:"NNN",
|
||||
GLP:"NNN",
|
||||
GTM:"NNN",
|
||||
HTI:"NNN",
|
||||
HND:"NNN",
|
||||
JAM:"NNN",
|
||||
MTQ:"NNN",
|
||||
MEX:"NNN",
|
||||
MSR:"NNN",
|
||||
ANT:"NNN",
|
||||
CUW:"NNN",
|
||||
NIC:"NNN",
|
||||
PAN:"NNN",
|
||||
PRI:"NNN",
|
||||
BLM:"NNN",
|
||||
KNA:"NNN",
|
||||
LCA:"NNN",
|
||||
MAF:"NNN",
|
||||
SPM:"NNN",
|
||||
VCT:"NNN",
|
||||
SXM:"NNN",
|
||||
TTO:"NNN",
|
||||
TCA:"NNN",
|
||||
USA:"NNN",
|
||||
UMI:"NNN",
|
||||
VIR:"NNN",
|
||||
|
||||
ARG:"SRR",
|
||||
BOL:"SRR",
|
||||
BRA:"SRR",
|
||||
CHL:"SRR",
|
||||
COL:"SRR",
|
||||
ECU:"SRR",
|
||||
FLK:"SRR",
|
||||
GUF:"SRR",
|
||||
GUY:"SRR",
|
||||
PRY:"SRR",
|
||||
PER:"SRR",
|
||||
SUR:"SRR",
|
||||
URY:"SRR",
|
||||
VEN:"SRR",
|
||||
|
||||
ASM:"UUU",
|
||||
AUS:"UUU",
|
||||
COK:"UUU",
|
||||
FJI:"UUU",
|
||||
PYF:"UUU",
|
||||
GUM:"UUU",
|
||||
KIR:"UUU",
|
||||
MHL:"UUU",
|
||||
FSM:"UUU",
|
||||
NRU:"UUU",
|
||||
NCL:"UUU",
|
||||
NZL:"UUU",
|
||||
NIU:"UUU",
|
||||
NFK:"UUU",
|
||||
MNP:"UUU",
|
||||
PLW:"UUU",
|
||||
PNG:"UUU",
|
||||
PCN:"UUU",
|
||||
WSM:"UUU",
|
||||
SLB:"UUU",
|
||||
TKL:"UUU",
|
||||
TON:"UUU",
|
||||
TUV:"UUU",
|
||||
VUT:"UUU",
|
||||
WLF:"UUU",
|
||||
|
||||
ATA:"XXX",
|
||||
BVT:"XXX",
|
||||
HMD:"XXX",
|
||||
SGS:"XXX",
|
||||
}
|
||||
|
||||
import fs from "fs"
|
||||
import {JSDOM} from "jsdom"
|
||||
|
||||
function groupByContinent(svgFilePath) {
|
||||
// Read the SVG file
|
||||
fs.readFile(svgFilePath, 'utf-8', (err, data) => {
|
||||
if (err) {
|
||||
console.error('Error reading SVG file:', err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a virtual DOM with JSDOM
|
||||
const dom = new JSDOM(data);
|
||||
const document = dom.window.document;
|
||||
|
||||
// Select the root <svg> element
|
||||
const svgElement = document.querySelector('svg');
|
||||
|
||||
// Group <g> elements representing countries by continent
|
||||
const world = {}; // Renamed continents object to 'world'
|
||||
svgElement.querySelectorAll('g').forEach(countryGroup => {
|
||||
const countryId = countryGroup.getAttribute('id');
|
||||
if (countryId!= null){
|
||||
const continentId = continents[countryId.replace(/\d/g, "")] || "XXX"; // Remove numbers from country ID to get continent ID
|
||||
if (!world[continentId]) {
|
||||
world[continentId] = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
world[continentId].setAttribute('id', continentId);
|
||||
}
|
||||
world[continentId].innerHTML += countryGroup.outerHTML;
|
||||
}else{
|
||||
|
||||
console.log(countryId,countryGroup.outerHTML)
|
||||
}
|
||||
});
|
||||
|
||||
// Create new <g> elements for each continent
|
||||
svgElement.innerHTML = '';
|
||||
for (const continentId in world) {
|
||||
svgElement.appendChild(world[continentId]);
|
||||
}
|
||||
|
||||
// Output the modified SVG
|
||||
fs.writeFile(svgFilePath, svgElement.outerHTML, 'utf-8', err => {
|
||||
if (err) {
|
||||
console.error('Error writing to SVG file:', err);
|
||||
return;
|
||||
}
|
||||
console.log('SVG file updated successfully.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Example usage:
|
||||
const svgFileBasePath = "./app/src/main/assets/" ;
|
||||
groupByContinent(svgFileBasePath+ "webmercator01.svg");
|
||||
groupByContinent(svgFileBasePath+ "aeqd01.svg");
|
||||
groupByContinent(svgFileBasePath+ "loxim01.svg");
|
||||
+3
-1
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"scripts": {
|
||||
"pack": "node packmap.js mapdata/loxim01.geojson mapdata/webmercator01.geojson mapdata/aeqd01.geojson"
|
||||
},
|
||||
"dependencies": {
|
||||
"@turf/area": "^7.3.5",
|
||||
"@turf/turf": "^7.3.5",
|
||||
"jsdom": "^30.0.0",
|
||||
"mapshaper": "^0.7.22"
|
||||
},
|
||||
"type": "module"
|
||||
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
#!/bin/node
|
||||
|
||||
/**
|
||||
* Packs the generated map data into the compact binary the app reads.
|
||||
*
|
||||
* The app used to ship SVG, which spends about four megabytes per projection
|
||||
* spelling coordinates out in decimal ASCII inside XML, and needed the map
|
||||
* parsed as a document at startup. Nothing needs a document: the app wants a
|
||||
* list of places and the outlines that belong to them, which is what this
|
||||
* writes.
|
||||
*
|
||||
* Input is mapshaper's projected GeoJSON, where each feature carries:
|
||||
* CODE the place it belongs to, e.g. "FRA" or "FRA_CE"
|
||||
* LEVEL 0 for a country outline, 1 for one of its regions
|
||||
*
|
||||
* Output format. All integers are LEB128 varints and coordinates are in grid
|
||||
* steps, on a canvas MAP_WIDTH user units across:
|
||||
*
|
||||
* "BMAP" 0x02
|
||||
* precision uvarint, grid steps per user unit
|
||||
* width, height uvarint, in grid steps
|
||||
* shapeCount uvarint
|
||||
* per shape:
|
||||
* codeLength, code uvarint + UTF-8 bytes
|
||||
* flags 1 byte, bit 0 = is a region
|
||||
* ringCount uvarint
|
||||
* per ring:
|
||||
* pointCount uvarint
|
||||
* flags 1 byte, bit 0 = even-odd fill
|
||||
* pointCount pairs of dx, dy zigzag varint, from the previous point,
|
||||
* starting from the origin on each ring
|
||||
*/
|
||||
|
||||
import { readFileSync, writeFileSync } from 'fs'
|
||||
|
||||
/** Canvas width in user units. Stroke widths in the app are relative to it. */
|
||||
const MAP_WIDTH = 800
|
||||
|
||||
/** Blank edge left around the map, as mapshaper's own SVG output used to. */
|
||||
const MARGIN = 1
|
||||
|
||||
/**
|
||||
* Grid steps per user unit. Coordinates are snapped to this, so it sets how
|
||||
* fine the map can be; it is written into the header rather than agreed by
|
||||
* convention, so raising it only means regenerating the assets.
|
||||
*/
|
||||
const PRECISION = 20
|
||||
|
||||
function uvarint(bytes, value) {
|
||||
while (value >= 0x80) {
|
||||
bytes.push((value & 0x7f) | 0x80)
|
||||
value >>>= 7
|
||||
}
|
||||
bytes.push(value)
|
||||
}
|
||||
|
||||
function svarint(bytes, value) {
|
||||
uvarint(bytes, value < 0 ? -value * 2 - 1 : value * 2)
|
||||
}
|
||||
|
||||
/** Every polygon of a feature, outer rings and holes alike. */
|
||||
function ringsOf(geometry) {
|
||||
if (!geometry) return []
|
||||
if (geometry.type === 'Polygon') return [geometry.coordinates]
|
||||
if (geometry.type === 'MultiPolygon') return geometry.coordinates
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups features by the place they belong to. A country outline and its
|
||||
* regions arrive as separate features; the code is what ties them together.
|
||||
*/
|
||||
function collect(features) {
|
||||
const shapes = []
|
||||
const byCode = new Map()
|
||||
|
||||
for (const feature of features) {
|
||||
const code = feature.properties?.CODE
|
||||
if (!code) continue
|
||||
const polygons = ringsOf(feature.geometry)
|
||||
if (polygons.length === 0) continue
|
||||
|
||||
let shape = byCode.get(code)
|
||||
if (!shape) {
|
||||
shape = { code, isState: feature.properties.LEVEL === 1, rings: [], seen: new Set() }
|
||||
byCode.set(code, shape)
|
||||
shapes.push(shape)
|
||||
}
|
||||
// A polygon with holes only reads correctly under an even-odd fill, which
|
||||
// is the rule mapshaper's SVG output used to pick for the same reason.
|
||||
const evenOdd = polygons.some((polygon) => polygon.length > 1)
|
||||
|
||||
for (const polygon of polygons) {
|
||||
for (const ring of polygon) {
|
||||
if (ring.length >= 3) shape.rings.push({ ring, evenOdd })
|
||||
}
|
||||
}
|
||||
}
|
||||
return shapes
|
||||
}
|
||||
|
||||
/** Twice the signed area of a closed ring of `x, y` pairs. */
|
||||
function shoelace(points) {
|
||||
let sum = 0
|
||||
const count = points.length / 2
|
||||
for (let i = 0, j = count - 1; i < count; j = i++) {
|
||||
sum += points[j * 2] * points[i * 2 + 1] - points[i * 2] * points[j * 2 + 1]
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
/**
|
||||
* Snaps every ring to the output grid and throws away what that makes
|
||||
* redundant: points that land on the one before them, and rings left too small
|
||||
* to enclose anything. The simplification upstream works to about this
|
||||
* precision, so a good share of the raw points say nothing once rounded.
|
||||
*/
|
||||
function quantize(shapes, canvas) {
|
||||
const kept = []
|
||||
for (const shape of shapes) {
|
||||
const seen = new Set()
|
||||
const rings = []
|
||||
for (const { ring, evenOdd } of shape.rings) {
|
||||
const points = []
|
||||
let px = null
|
||||
let py = null
|
||||
for (const [rx, ry] of ring) {
|
||||
const x = canvas.x(rx)
|
||||
const y = canvas.y(ry)
|
||||
if (x === px && y === py) continue
|
||||
points.push(x, y)
|
||||
px = x
|
||||
py = y
|
||||
}
|
||||
if (points.length < 6) continue
|
||||
// Coordinates are integers now, so any ring that still encloses area
|
||||
// encloses at least half a grid square. One that comes out at exactly
|
||||
// zero is a sliver the rounding flattened, and it would draw nothing.
|
||||
if (shoelace(points) === 0) continue
|
||||
// Antarctica arrives twice over, once per admin level, because its source
|
||||
// carries no country code and both levels land on ATA. A ring counted
|
||||
// twice cancels itself out in the even-odd test the app hit tests with,
|
||||
// which would leave the shape impossible to tap.
|
||||
const key = points.join(',')
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
rings.push({ points, evenOdd })
|
||||
}
|
||||
if (rings.length > 0) kept.push({ code: shape.code, isState: shape.isState, rings })
|
||||
}
|
||||
// By code, which puts a country next to its own regions. They are neighbours
|
||||
// on the map, so their coordinates are similar, and the compressor in the APK
|
||||
// gets noticeably more out of the file for it.
|
||||
return kept.sort((a, b) => (a.code < b.code ? -1 : a.code > b.code ? 1 : 0))
|
||||
}
|
||||
|
||||
/** Projected metres in, user units on a MAP_WIDTH canvas out, y flipped. */
|
||||
function fitToCanvas(shapes) {
|
||||
let minX = Infinity
|
||||
let minY = Infinity
|
||||
let maxX = -Infinity
|
||||
let maxY = -Infinity
|
||||
for (const shape of shapes) {
|
||||
for (const { ring } of shape.rings) {
|
||||
for (const [x, y] of ring) {
|
||||
if (x < minX) minX = x
|
||||
if (x > maxX) maxX = x
|
||||
if (y < minY) minY = y
|
||||
if (y > maxY) maxY = y
|
||||
}
|
||||
}
|
||||
}
|
||||
const scale = (MAP_WIDTH - 2 * MARGIN) / (maxX - minX)
|
||||
return {
|
||||
width: MAP_WIDTH,
|
||||
height: 2 * MARGIN + (maxY - minY) * scale,
|
||||
x: (v) => Math.round((MARGIN + (v - minX) * scale) * PRECISION),
|
||||
y: (v) => Math.round((MARGIN + (maxY - v) * scale) * PRECISION),
|
||||
}
|
||||
}
|
||||
|
||||
function pack(path) {
|
||||
const geojson = JSON.parse(readFileSync(path, 'utf8'))
|
||||
const collected = collect(geojson.features ?? [])
|
||||
const canvas = fitToCanvas(collected)
|
||||
const shapes = quantize(collected, canvas)
|
||||
|
||||
const bytes = [0x42, 0x4d, 0x41, 0x50, 0x02] // "BMAP" v2
|
||||
uvarint(bytes, PRECISION)
|
||||
uvarint(bytes, Math.round(canvas.width * PRECISION))
|
||||
uvarint(bytes, Math.round(canvas.height * PRECISION))
|
||||
uvarint(bytes, shapes.length)
|
||||
|
||||
let rings = 0
|
||||
let points = 0
|
||||
for (const shape of shapes) {
|
||||
const code = Buffer.from(shape.code, 'utf8')
|
||||
uvarint(bytes, code.length)
|
||||
for (const byte of code) bytes.push(byte)
|
||||
bytes.push(shape.isState ? 1 : 0)
|
||||
uvarint(bytes, shape.rings.length)
|
||||
|
||||
for (const { points: ring, evenOdd } of shape.rings) {
|
||||
uvarint(bytes, ring.length / 2)
|
||||
bytes.push(evenOdd ? 1 : 0)
|
||||
let px = 0
|
||||
let py = 0
|
||||
for (let i = 0; i < ring.length; i += 2) {
|
||||
svarint(bytes, ring[i] - px)
|
||||
svarint(bytes, ring[i + 1] - py)
|
||||
px = ring[i]
|
||||
py = ring[i + 1]
|
||||
}
|
||||
rings++
|
||||
points += ring.length / 2
|
||||
}
|
||||
}
|
||||
|
||||
const out = path.replace(/^.*\//, './app/src/main/assets/').replace(/\.geojson$/, '.bmap')
|
||||
writeFileSync(out, Buffer.from(bytes))
|
||||
console.log(
|
||||
`${out}: ${shapes.length} shapes, ${rings} rings, ${points} points, ` +
|
||||
`${canvas.width}x${canvas.height.toFixed(1)}, ${bytes.length} bytes`,
|
||||
)
|
||||
}
|
||||
|
||||
const inputs = process.argv.slice(2)
|
||||
if (inputs.length === 0) {
|
||||
console.error('usage: node packmap.js <map>.geojson ...')
|
||||
process.exit(1)
|
||||
}
|
||||
inputs.forEach(pack)
|
||||
@@ -2,215 +2,149 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@asamuzakjp/css-color@^6.0.5":
|
||||
version "6.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@asamuzakjp/css-color/-/css-color-6.0.7.tgz#8f9f67452e6636930949abe047dd553b13276939"
|
||||
integrity sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==
|
||||
dependencies:
|
||||
"@csstools/css-calc" "^3.3.0"
|
||||
"@csstools/css-color-parser" "^4.1.10"
|
||||
"@csstools/css-parser-algorithms" "^4.0.0"
|
||||
"@csstools/css-tokenizer" "^4.0.0"
|
||||
lru-cache "^11.5.2"
|
||||
|
||||
"@asamuzakjp/dom-selector@^8.3.0":
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz#1e84c1ea1e12a921c1aa94da4b1f657168f09596"
|
||||
integrity sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==
|
||||
dependencies:
|
||||
bidi-js "^1.0.3"
|
||||
css-tree "^3.2.1"
|
||||
is-potential-custom-element-name "^1.0.1"
|
||||
lru-cache "^11.5.2"
|
||||
|
||||
"@bokuweb/zstd-wasm@^0.0.27":
|
||||
version "0.0.27"
|
||||
resolved "https://registry.yarnpkg.com/@bokuweb/zstd-wasm/-/zstd-wasm-0.0.27.tgz#89d8ed8c37528c4b9d11f47826b9f603346dfc4c"
|
||||
integrity sha512-GDm2uOTK3ESjnYmSeLQifJnBsRCWajKLvN32D2ZcQaaCIJI/Hse9s74f7APXjHit95S10UImsRGkTsbwHmrtmg==
|
||||
|
||||
"@bramus/specificity@^2.4.2":
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@bramus/specificity/-/specificity-2.4.2.tgz#aa8db8eb173fdee7324f82284833106adeecc648"
|
||||
integrity sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==
|
||||
"@inquirer/ansi@^2.0.8":
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/ansi/-/ansi-2.0.8.tgz#0308f3ed790dfa960f0f1f60045fa67e804de38e"
|
||||
integrity sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==
|
||||
|
||||
"@inquirer/checkbox@^5.2.4":
|
||||
version "5.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/checkbox/-/checkbox-5.2.4.tgz#0bc6965982da04b821862479733d9b2875b4b579"
|
||||
integrity sha512-oKT5RNeO9oKYizSyOxKb66IrKUsYVMQD2tnjllYW4wmSOe9WawfL3OE2p82JuvAyo+/nDIEem5P4Bm/ZIVYVew==
|
||||
dependencies:
|
||||
css-tree "^3.0.0"
|
||||
"@inquirer/ansi" "^2.0.8"
|
||||
"@inquirer/core" "^12.0.2"
|
||||
"@inquirer/figures" "^2.0.9"
|
||||
"@inquirer/type" "4.1.1"
|
||||
|
||||
"@csstools/color-helpers@^6.1.1":
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-6.1.1.tgz#1890f29a4347486b54048d24c6ab8fbef039ee61"
|
||||
integrity sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==
|
||||
|
||||
"@csstools/css-calc@^3.3.0":
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-3.3.0.tgz#33cc4bdbab8edf1b6e3ab8c1eba849c41a324f1a"
|
||||
integrity sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==
|
||||
|
||||
"@csstools/css-color-parser@^4.1.10":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-4.2.1.tgz#d2795d618e3d497f394277eede34f10d7adfad0e"
|
||||
integrity sha512-YpAJZhaHplYQkG8ib+/Fx5Y0eF2lVWi3tIvMJA6i39TLyUNp2439cifzW8VMjhlqrBjHzK5hVGugRRm2zTKI/A==
|
||||
"@inquirer/confirm@^6.3.1":
|
||||
version "6.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-6.3.1.tgz#8b226fa537838251028f77670f5280932a0e5783"
|
||||
integrity sha512-HvnOHal39DTenOVoRpIy8Z+n4YYfNa3Qhi/P8zFqn9/d1crpmQG0DPx34rwOeOFvotgKr9Vov/14OmKR/Iwfjw==
|
||||
dependencies:
|
||||
"@csstools/color-helpers" "^6.1.1"
|
||||
"@csstools/css-calc" "^3.3.0"
|
||||
"@inquirer/core" "^12.0.2"
|
||||
"@inquirer/type" "4.1.1"
|
||||
|
||||
"@csstools/css-parser-algorithms@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz#e1c65dc09378b42f26a111fca7f7075fc2c26164"
|
||||
integrity sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==
|
||||
|
||||
"@csstools/css-syntax-patches-for-csstree@^1.1.7":
|
||||
version "1.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.9.tgz#8c144089e7ad7cadc0393a5051345c3e6a8043dc"
|
||||
integrity sha512-iGGw4OsAYsS6pD29MdJ2bX/nJx65a04ZZiw6x+VwWlP2DdXf6f++Zmuv/OzALpdyfVhjbduIIF2cXM7HWBIe9A==
|
||||
|
||||
"@csstools/css-tokenizer@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz#798a33950d11226a0ebb6acafa60f5594424967f"
|
||||
integrity sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==
|
||||
|
||||
"@exodus/bytes@^1.11.0", "@exodus/bytes@^1.15.1", "@exodus/bytes@^1.6.0":
|
||||
version "1.15.1"
|
||||
resolved "https://registry.yarnpkg.com/@exodus/bytes/-/bytes-1.15.1.tgz#b13bc464ca162c17abf0837fb3a11aeab79e45d1"
|
||||
integrity sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==
|
||||
|
||||
"@inquirer/ansi@^2.0.7":
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/ansi/-/ansi-2.0.7.tgz#86de22810cac3ed406ec10f8d66016815b8226b4"
|
||||
integrity sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==
|
||||
|
||||
"@inquirer/checkbox@^5.2.3":
|
||||
version "5.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/checkbox/-/checkbox-5.2.3.tgz#3c2b2199c3e3d884a843a2c1280b253f958d5f8a"
|
||||
integrity sha512-XEYX2WA8SBkLPczL6/yXPHLPCvDoptmh9v56Cy05BSV1Smk1vWy19bTC4qJBuIffw7+6l4CcaYYzGqG60RfW1g==
|
||||
"@inquirer/core@^12.0.2":
|
||||
version "12.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-12.0.2.tgz#f75fbf5c60715078ae6c4c7602764335a581e2b0"
|
||||
integrity sha512-9pBhkxE14uUlnHhs8lOt7qVPtS4caRY6CjADl8COejl9Mf7w8i6Uoe3DrljCqYtmYM3Iv2Q6EmPxx/oTYbvTAQ==
|
||||
dependencies:
|
||||
"@inquirer/ansi" "^2.0.7"
|
||||
"@inquirer/core" "^12.0.1"
|
||||
"@inquirer/figures" "^2.0.8"
|
||||
"@inquirer/type" "^4.1.0"
|
||||
|
||||
"@inquirer/confirm@^6.3.0":
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-6.3.0.tgz#5c32d1bba3a0551dcbddc52628a22daef3f5a8cd"
|
||||
integrity sha512-pZHXJImFtERmSNMBHcjwuz8Ck5vEFEYNUZnwbb8aJpjHv/TwGuFErNxF2Hp8+V+pNJs2EYPMlyWscvFEqO9jOQ==
|
||||
dependencies:
|
||||
"@inquirer/core" "^12.0.1"
|
||||
"@inquirer/type" "^4.1.0"
|
||||
|
||||
"@inquirer/core@^12.0.1":
|
||||
version "12.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-12.0.1.tgz#a59c69f48c9a67d60ac821b6f12e667cb116ffb4"
|
||||
integrity sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==
|
||||
dependencies:
|
||||
"@inquirer/ansi" "^2.0.7"
|
||||
"@inquirer/figures" "^2.0.8"
|
||||
"@inquirer/type" "^4.1.0"
|
||||
"@inquirer/ansi" "^2.0.8"
|
||||
"@inquirer/figures" "^2.0.9"
|
||||
"@inquirer/type" "4.1.1"
|
||||
cli-width "^4.1.0"
|
||||
fast-wrap-ansi "^0.2.0"
|
||||
mute-stream "^3.0.0"
|
||||
signal-exit "^4.1.0"
|
||||
|
||||
"@inquirer/editor@^5.3.1":
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/editor/-/editor-5.3.1.tgz#a035364e93ecd03753129813b99f4961650844a2"
|
||||
integrity sha512-y43COoyVUjPWIobn2Qep/uI1drPS78aaZZZ9kVi94Tyu/GuW2N8d8Q4rifJXGAXCEAXCPTTMjD8gC1HyvM5ukA==
|
||||
"@inquirer/editor@^5.3.2":
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/editor/-/editor-5.3.2.tgz#202f27de311f624c0e5298e1c4bcf0c0bd93af63"
|
||||
integrity sha512-7ruKO5d/wxhGHxdCQ3eyP/aBNrPTZ+Ju7ERtnAwuIsu0RSLs6kotlTRkOxHatbD6sDaEEdwsD82pzFiT9ESIyQ==
|
||||
dependencies:
|
||||
"@inquirer/core" "^12.0.1"
|
||||
"@inquirer/external-editor" "^3.0.4"
|
||||
"@inquirer/type" "^4.1.0"
|
||||
"@inquirer/core" "^12.0.2"
|
||||
"@inquirer/external-editor" "^3.0.5"
|
||||
"@inquirer/type" "4.1.1"
|
||||
|
||||
"@inquirer/expand@^5.1.3":
|
||||
version "5.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/expand/-/expand-5.1.3.tgz#f587260168acb6733643665882195bbe4d2e3dc3"
|
||||
integrity sha512-3NQJiXNJ/aj9wiAsr7pECdp5Qe9J0X9YUJCKsaFXS+ddOxfL6J4AIl3w3T4Gq3kK0WsQY5GMoDokK5X94m6lHw==
|
||||
"@inquirer/expand@^5.1.4":
|
||||
version "5.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/expand/-/expand-5.1.4.tgz#27e1b2735ab7fa7fc5b4283e6c699f2bdfc6e880"
|
||||
integrity sha512-12o4aYnjWouoq0fyVhYIL+hwXAaga+kntA/wrijFmVvn2heinkdvkbEXRt1Ob/9IOJ285US5tntjfI/Ukt2YkA==
|
||||
dependencies:
|
||||
"@inquirer/core" "^12.0.1"
|
||||
"@inquirer/type" "^4.1.0"
|
||||
"@inquirer/core" "^12.0.2"
|
||||
"@inquirer/type" "4.1.1"
|
||||
|
||||
"@inquirer/external-editor@^3.0.4":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/external-editor/-/external-editor-3.0.4.tgz#6f61cfbdcc578530e6d322abe30921eba3e4e73a"
|
||||
integrity sha512-tZbbaK2ovq6vlrRBNQvjrypmrED/p5x2ncIHQ79cD55tei3dD96v5glMMA+6tiq7K104i/25DVYKWVPJuV6ptA==
|
||||
"@inquirer/external-editor@^3.0.5":
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/external-editor/-/external-editor-3.0.5.tgz#e05198b6d5dc87a8993fead6d9bcf92cd4863726"
|
||||
integrity sha512-f3QQJRIX5ZEneBHNUIuPjmbdzHnmRFJA8r2dkcb8q+OM5Uv5KtnuAttQumnrjcBVBM3mcTX1CkmtAkU58VRZxg==
|
||||
dependencies:
|
||||
chardet "^2.1.1"
|
||||
iconv-lite "^0.7.2"
|
||||
|
||||
"@inquirer/figures@^2.0.8":
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-2.0.8.tgz#d10c3a8a28896679797d9da5944f3093476aa0cd"
|
||||
integrity sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q==
|
||||
"@inquirer/figures@^2.0.9":
|
||||
version "2.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-2.0.9.tgz#8c04fdba3a78af0e57c0b3127cbd319ce604b1c1"
|
||||
integrity sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==
|
||||
|
||||
"@inquirer/input@^5.1.4":
|
||||
version "5.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/input/-/input-5.1.4.tgz#4aadb5f4f800e6b9f7a9ef79d42f78fb0ee1d42f"
|
||||
integrity sha512-3xQkQrOvgOzpSN2ciTVdRDlg1FWMCA8l+0KfB6SNlILoTCGzJTzO/gc0Rwjcb3usuGyKdaGtI6OiyMdeMeLWkg==
|
||||
"@inquirer/input@^5.1.5":
|
||||
version "5.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/input/-/input-5.1.5.tgz#62d13806666ae0d6dd2b2b06f3b934da9829aa8b"
|
||||
integrity sha512-2MkYYFD1Owt1eRBwHhsLSutysRodWasA4DVpl42KJKGmzwBTXRYAIXujisrt0UW0UohUlMjNjLfMDbw4emRbfw==
|
||||
dependencies:
|
||||
"@inquirer/core" "^12.0.1"
|
||||
"@inquirer/type" "^4.1.0"
|
||||
"@inquirer/core" "^12.0.2"
|
||||
"@inquirer/type" "4.1.1"
|
||||
|
||||
"@inquirer/number@^4.2.1":
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/number/-/number-4.2.1.tgz#34c302d434f78571cf6be54027d4016400d2e5bb"
|
||||
integrity sha512-5KaqwZNLRpUuWcoCrYghPP9TMaXL5v2Sk4xqePM7RCVegcJStoXdWibio60YIC1bec+z1fCyb67N6XPJIkZtGA==
|
||||
"@inquirer/number@^4.2.2":
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/number/-/number-4.2.2.tgz#e7ef93fc2fdebf0e3d1f65c030eb84ee92df39c5"
|
||||
integrity sha512-tjW1gMIQsFb/9Ie11i8SpuPARBdyCVfC5XOeVugWovrdm8CpjzN+A1CBX5QwCCN2mtNmpgDDCnuLCCT94cQ5zA==
|
||||
dependencies:
|
||||
"@inquirer/core" "^12.0.1"
|
||||
"@inquirer/type" "^4.1.0"
|
||||
"@inquirer/core" "^12.0.2"
|
||||
"@inquirer/type" "4.1.1"
|
||||
|
||||
"@inquirer/password@^5.2.0":
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/password/-/password-5.2.0.tgz#002db941c99ab3fa10b624d18decc3ee3400461c"
|
||||
integrity sha512-CvVcW09emkBESEOW+4R8CjLNkP3fB3XrjeL8CDvfpjgrJN+V9oerXmJAXXM3l+4xqYPD5Yaujzy/Ph0PLOdDuA==
|
||||
"@inquirer/password@^5.2.1":
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/password/-/password-5.2.1.tgz#a1917f5269a7c4cfbf29c4085ad611931786faf6"
|
||||
integrity sha512-InA6nJxMCXPavPBiTKYHIsYiWz/mkIsk1rkI6pKp5w18gVawqrZOj7eSzSmPJa3j0pVxXm6RlrFP1BE9YeOixA==
|
||||
dependencies:
|
||||
"@inquirer/ansi" "^2.0.7"
|
||||
"@inquirer/core" "^12.0.1"
|
||||
"@inquirer/type" "^4.1.0"
|
||||
"@inquirer/ansi" "^2.0.8"
|
||||
"@inquirer/core" "^12.0.2"
|
||||
"@inquirer/type" "4.1.1"
|
||||
|
||||
"@inquirer/prompts@^8.7.0":
|
||||
version "8.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/prompts/-/prompts-8.7.0.tgz#6bdf61e32b8cb64e7e39a187842d5887cb224701"
|
||||
integrity sha512-yQwBMYvpJ6jqrXtKiOwRD5XezjJoyt3VQvIyjsr5Arqb519nfIohOQymWVJ8/vEgg8xtZerrCsqlSaqt/LPC9A==
|
||||
"@inquirer/prompts@^8.7.1":
|
||||
version "8.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/prompts/-/prompts-8.7.1.tgz#c6cd870cc59ffdd9869b8d60bfdccc3c4dbde182"
|
||||
integrity sha512-xJIKWyrFNUKj3R5VEub/iKodG9Sh/MbCeApZUbATJgHTw8YoAJ2gOT78HlvO6NMFiRvjm6ySM7aiAew/zVY0Sw==
|
||||
dependencies:
|
||||
"@inquirer/checkbox" "^5.2.3"
|
||||
"@inquirer/confirm" "^6.3.0"
|
||||
"@inquirer/editor" "^5.3.1"
|
||||
"@inquirer/expand" "^5.1.3"
|
||||
"@inquirer/input" "^5.1.4"
|
||||
"@inquirer/number" "^4.2.1"
|
||||
"@inquirer/password" "^5.2.0"
|
||||
"@inquirer/rawlist" "^5.3.3"
|
||||
"@inquirer/search" "^4.3.1"
|
||||
"@inquirer/select" "^5.2.3"
|
||||
"@inquirer/checkbox" "^5.2.4"
|
||||
"@inquirer/confirm" "^6.3.1"
|
||||
"@inquirer/editor" "^5.3.2"
|
||||
"@inquirer/expand" "^5.1.4"
|
||||
"@inquirer/input" "^5.1.5"
|
||||
"@inquirer/number" "^4.2.2"
|
||||
"@inquirer/password" "^5.2.1"
|
||||
"@inquirer/rawlist" "^5.3.4"
|
||||
"@inquirer/search" "^4.3.2"
|
||||
"@inquirer/select" "^5.2.4"
|
||||
|
||||
"@inquirer/rawlist@^5.3.3":
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/rawlist/-/rawlist-5.3.3.tgz#efac1bd316bbed4bd58572dee4e2826bfc3336d0"
|
||||
integrity sha512-Mu7WrtmDLaXBDEyrRLS70SZgX9ZSm4Up1w0ZxiH8C1OOp9oaVCn2k8q3QGgmlnhsKYUhuaU3zFWhAP6wxkVIMA==
|
||||
"@inquirer/rawlist@^5.3.4":
|
||||
version "5.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/rawlist/-/rawlist-5.3.4.tgz#c18509b5d629cc1340ad224292c7b109f96ccbf4"
|
||||
integrity sha512-c4fDwOpQsjJDHjXdHJE1/xXH6w9/BAiyu3QKzH9Ww5Xm1q37mzyUNa20L7qdtM1ECi4e+oUEKjhK8hR37OMAMQ==
|
||||
dependencies:
|
||||
"@inquirer/core" "^12.0.1"
|
||||
"@inquirer/type" "^4.1.0"
|
||||
"@inquirer/core" "^12.0.2"
|
||||
"@inquirer/type" "4.1.1"
|
||||
|
||||
"@inquirer/search@^4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/search/-/search-4.3.1.tgz#e4c96e9e65ea6adcc78ed629c6a363085689968d"
|
||||
integrity sha512-0VWOvsHWI0rPj6CG70MoP4oXNCB6adcyN8bVFZXnh11eLDdPIK2f2XCmva78acPPDfJisfab7qNakwBh7hdBXw==
|
||||
"@inquirer/search@^4.3.2":
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/search/-/search-4.3.2.tgz#e8a0355e319af62d9303ef8316a23c3ed8440efb"
|
||||
integrity sha512-YIaEhWmfkdHEWOKwhF/oJar35FFTTrAdzNbJDo9lSsRT6E/obRPx9oVm4rkTMtlcJSqGYttNtLbcHQef9Qw2yw==
|
||||
dependencies:
|
||||
"@inquirer/core" "^12.0.1"
|
||||
"@inquirer/figures" "^2.0.8"
|
||||
"@inquirer/type" "^4.1.0"
|
||||
"@inquirer/core" "^12.0.2"
|
||||
"@inquirer/figures" "^2.0.9"
|
||||
"@inquirer/type" "4.1.1"
|
||||
|
||||
"@inquirer/select@^5.2.3":
|
||||
version "5.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/select/-/select-5.2.3.tgz#527f4290568c2851c9e0a7a03513e90b26ab1e80"
|
||||
integrity sha512-KuRTodDa6xBXX2noIpjuitpX/QT7Sfav7dIZ/OfUY54Hxg95nrGoshSzxx6Ey7qqLbImKdiGkSDt7KjPXjQgmA==
|
||||
"@inquirer/select@^5.2.4":
|
||||
version "5.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/select/-/select-5.2.4.tgz#0e80ae5595a975467160b7f1b9ca067fdd700f2f"
|
||||
integrity sha512-L9ubNaJdoBsiom/AfPKq0QLHTkhhCFCtszzdVidGremrlJjPZfGlmvp+IULKIbnYjtVFWnvb4mhiEdNTZ12ugA==
|
||||
dependencies:
|
||||
"@inquirer/ansi" "^2.0.7"
|
||||
"@inquirer/core" "^12.0.1"
|
||||
"@inquirer/figures" "^2.0.8"
|
||||
"@inquirer/type" "^4.1.0"
|
||||
"@inquirer/ansi" "^2.0.8"
|
||||
"@inquirer/core" "^12.0.2"
|
||||
"@inquirer/figures" "^2.0.9"
|
||||
"@inquirer/type" "4.1.1"
|
||||
|
||||
"@inquirer/type@^4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-4.1.0.tgz#98f607ae62c4c5b333ae1b45b629af62a3be6472"
|
||||
integrity sha512-FMiJpuHUG3Dk0ex+UIXkre7i+i4OcwHWk9YdcVtZHFwb/r2rnrU2ipTCNAB7A+QOP0ryzIcqOfy76fRyyvOEAw==
|
||||
"@inquirer/type@4.1.1":
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-4.1.1.tgz#f865ee99f39e0951a0279f9c45ad1d260c0ce0dd"
|
||||
integrity sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==
|
||||
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4":
|
||||
version "3.0.4"
|
||||
@@ -1979,9 +1913,9 @@
|
||||
integrity sha512-tdJz7jaWFu4nR+8b2B+CdPZ6811ighYylWsu2hpsivapzW058yP0KdfZuNY89IiRe5jbKvBGXN3LQdN2KPXVdQ==
|
||||
|
||||
"@types/node@*":
|
||||
version "26.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-26.4.0.tgz#4c4ca071c42241fe602741d02999f16b4e64c468"
|
||||
integrity sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==
|
||||
version "26.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-26.4.1.tgz#3d8dc80515894958448ee266cf5d6bc3e5205bd5"
|
||||
integrity sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==
|
||||
dependencies:
|
||||
undici-types "~8.3.0"
|
||||
|
||||
@@ -2060,13 +1994,6 @@ better-sqlite3@^12.10.0:
|
||||
bindings "^1.5.0"
|
||||
prebuild-install "^7.1.1"
|
||||
|
||||
bidi-js@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/bidi-js/-/bidi-js-1.0.3.tgz#6f8bcf3c877c4d9220ddf49b9bb6930c88f877d2"
|
||||
integrity sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==
|
||||
dependencies:
|
||||
require-from-string "^2.0.2"
|
||||
|
||||
bignumber.js@^9.1.0:
|
||||
version "9.3.1"
|
||||
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7"
|
||||
@@ -2189,14 +2116,6 @@ cookies@^0.8.0:
|
||||
depd "~2.0.0"
|
||||
keygrip "~1.1.0"
|
||||
|
||||
css-tree@^3.0.0, css-tree@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-3.2.1.tgz#86cac7011561272b30e6b1e042ba6ce047aa7518"
|
||||
integrity sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==
|
||||
dependencies:
|
||||
mdn-data "2.27.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
d3-array@^2.5.0:
|
||||
version "2.12.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.12.1.tgz#e20b41aafcdffdf5d50928004ececf815a465e81"
|
||||
@@ -2236,19 +2155,6 @@ d3-voronoi@1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.2.tgz#1687667e8f13a2d158c80c1480c5a29cb0d8973c"
|
||||
integrity sha512-RhGS1u2vavcO7ay7ZNAPo4xeDh/VYeGof3x5ZLJBQgYhLegxr3s5IykvWmJ94FTU6mcbtp4sloqZ54mP6R4Utw==
|
||||
|
||||
data-urls@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-7.0.0.tgz#6dce8b63226a1ecfdd907ce18a8ccfb1eee506d3"
|
||||
integrity sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==
|
||||
dependencies:
|
||||
whatwg-mimetype "^5.0.0"
|
||||
whatwg-url "^16.0.0"
|
||||
|
||||
decimal.js@^10.6.0:
|
||||
version "10.6.0"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a"
|
||||
integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==
|
||||
|
||||
decompress-response@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
|
||||
@@ -2318,11 +2224,6 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1:
|
||||
dependencies:
|
||||
once "^1.4.0"
|
||||
|
||||
entities@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-8.0.0.tgz#c1df5fe3602429747fa233d0dd26f142f0ce4743"
|
||||
integrity sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==
|
||||
|
||||
event-stream@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-4.0.1.tgz#4092808ec995d0dd75ea4580c1df6a74db2cde65"
|
||||
@@ -2528,13 +2429,6 @@ has-flag@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
|
||||
|
||||
html-encoding-sniffer@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz#f8d9390b3b348b50d4f61c16dd2ef5c05980a882"
|
||||
integrity sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==
|
||||
dependencies:
|
||||
"@exodus/bytes" "^1.6.0"
|
||||
|
||||
hyparquet-compressors@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/hyparquet-compressors/-/hyparquet-compressors-1.1.1.tgz#35ab916d4ee3306000279126a937e9a1633725a6"
|
||||
@@ -2607,14 +2501,14 @@ ini@~1.3.0:
|
||||
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||
|
||||
inquirer@^14.0.0:
|
||||
version "14.2.0"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-14.2.0.tgz#3c471cecda0e327d469316105b1f0330af59a020"
|
||||
integrity sha512-OUSQNF7um7AvgPyBiK6ENfjqEG434oeVkRG97U8foLftOfcJfKcTi5pMtagJe5ytANPH5TlbfVgEokAl6RSj6g==
|
||||
version "14.2.1"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-14.2.1.tgz#f6eab855d59748843352bc25085872c56351e75c"
|
||||
integrity sha512-jLO8986l0/jW+K2Y5BHAhrD6ztnm8MHQgnTI5BwKV6can/LN6xYBcLbtUSvDt1jXNTASwCZszZk0ZBUT5YmH6Q==
|
||||
dependencies:
|
||||
"@inquirer/ansi" "^2.0.7"
|
||||
"@inquirer/core" "^12.0.1"
|
||||
"@inquirer/prompts" "^8.7.0"
|
||||
"@inquirer/type" "^4.1.0"
|
||||
"@inquirer/ansi" "^2.0.8"
|
||||
"@inquirer/core" "^12.0.2"
|
||||
"@inquirer/prompts" "^8.7.1"
|
||||
"@inquirer/type" "4.1.1"
|
||||
mute-stream "^3.0.0"
|
||||
run-async "^4.0.6"
|
||||
|
||||
@@ -2640,11 +2534,6 @@ is-inside-container@^1.0.0:
|
||||
dependencies:
|
||||
is-docker "^3.0.0"
|
||||
|
||||
is-potential-custom-element-name@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
|
||||
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
|
||||
|
||||
is-wsl@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.1.tgz#327897b26832a3eb117da6c27492d04ca132594f"
|
||||
@@ -2657,33 +2546,6 @@ jpeg-js@^0.4.4:
|
||||
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
|
||||
integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==
|
||||
|
||||
jsdom@^30.0.0:
|
||||
version "30.0.1"
|
||||
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-30.0.1.tgz#1b0751cbd0abce86762c48697583b5e91433f6e4"
|
||||
integrity sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==
|
||||
dependencies:
|
||||
"@asamuzakjp/css-color" "^6.0.5"
|
||||
"@asamuzakjp/dom-selector" "^8.3.0"
|
||||
"@bramus/specificity" "^2.4.2"
|
||||
"@csstools/css-syntax-patches-for-csstree" "^1.1.7"
|
||||
"@exodus/bytes" "^1.15.1"
|
||||
css-tree "^3.2.1"
|
||||
data-urls "^7.0.0"
|
||||
decimal.js "^10.6.0"
|
||||
html-encoding-sniffer "^6.0.0"
|
||||
is-potential-custom-element-name "^1.0.1"
|
||||
lru-cache "^11.5.2"
|
||||
parse5 "^8.0.1"
|
||||
saxes "^6.0.0"
|
||||
symbol-tree "^3.2.4"
|
||||
tough-cookie "^6.0.2"
|
||||
undici "^8.9.0"
|
||||
w3c-xmlserializer "^5.0.0"
|
||||
webidl-conversions "^8.0.1"
|
||||
whatwg-mimetype "^5.0.0"
|
||||
whatwg-url "^17.1.0"
|
||||
xml-name-validator "^5.0.0"
|
||||
|
||||
jsonparse@^1.2.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
|
||||
@@ -2716,20 +2578,15 @@ lodash@4.18.1:
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c"
|
||||
integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==
|
||||
|
||||
lru-cache@^11.5.2:
|
||||
version "11.5.2"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.5.2.tgz#00e16665c90c620fba14a3c368732a976493f760"
|
||||
integrity sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==
|
||||
|
||||
map-stream@0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8"
|
||||
integrity sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==
|
||||
|
||||
mapshaper@^0.7.22:
|
||||
version "0.7.56"
|
||||
resolved "https://registry.yarnpkg.com/mapshaper/-/mapshaper-0.7.56.tgz#0be33ecefb44743cc0e0daaa96dd83e7bb815c7d"
|
||||
integrity sha512-jedBg0ItzkFkt053XxS3MFgyASSZr9tJaRzGPwQotm2aHvHI9gKYCrM9jzUUrZai0gkk/EaQFN18u1GALvyYBA==
|
||||
version "0.7.59"
|
||||
resolved "https://registry.yarnpkg.com/mapshaper/-/mapshaper-0.7.59.tgz#b36b37d158726a9eb3fe1764bd7ccad3894e60dd"
|
||||
integrity sha512-zXRNJZRqTe4CjlnHJf3vY/36z3b15vYaLI7tmQ/Ora0VIONOaNphFUPxOULcK0lWiu9em+qqn2wHgbG/YKS+qQ==
|
||||
dependencies:
|
||||
"@bokuweb/zstd-wasm" "^0.0.27"
|
||||
"@ngageoint/geopackage" "^4.2.6"
|
||||
@@ -2766,11 +2623,6 @@ mapshaper@^0.7.22:
|
||||
"@rollup/rollup-linux-x64-gnu" "^4.44.1"
|
||||
better-sqlite3 "^12.10.0"
|
||||
|
||||
mdn-data@2.27.1:
|
||||
version "2.27.1"
|
||||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.27.1.tgz#e37b9c50880b75366c4d40ac63d9bbcacdb61f0e"
|
||||
integrity sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==
|
||||
|
||||
mgrs@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mgrs/-/mgrs-1.0.0.tgz#fb91588e78c90025672395cb40b25f7cd6ad1829"
|
||||
@@ -2897,13 +2749,6 @@ parse-headers@^2.0.2:
|
||||
resolved "https://registry.yarnpkg.com/parse-headers/-/parse-headers-2.0.6.tgz#7940f0abe5fe65df2dd25d4ce8800cb35b49d01c"
|
||||
integrity sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==
|
||||
|
||||
parse5@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-8.0.1.tgz#f43bcd2cd683efe084075333e9ce0da7d06da31e"
|
||||
integrity sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==
|
||||
dependencies:
|
||||
entities "^8.0.0"
|
||||
|
||||
pause-stream@^0.0.11:
|
||||
version "0.0.11"
|
||||
resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
|
||||
@@ -2998,9 +2843,9 @@ proj4@2.8.0:
|
||||
wkt-parser "^1.3.1"
|
||||
|
||||
proj4@^2.4.4:
|
||||
version "2.21.0"
|
||||
resolved "https://registry.yarnpkg.com/proj4/-/proj4-2.21.0.tgz#dfe137658fd8c129818e800a95395efe531fa26b"
|
||||
integrity sha512-33HfDftqw8kY+Cl1dcL16SJuqTSzYxmz4re7Nmhk+fs1/N1fFrAkkF579msQTR/4fLeJVSNne8/gpoCz0fk1uw==
|
||||
version "2.22.0"
|
||||
resolved "https://registry.yarnpkg.com/proj4/-/proj4-2.22.0.tgz#f4fa16ed0b158923be8713d67bf62e5e1400693f"
|
||||
integrity sha512-MVvH09QrYm3A235rcX39WDiCKanrBiblu28l0KYIn/B2DofmYhvP/LLS43kE66jNDfx/P1l/nah3JmsAq8UgbQ==
|
||||
dependencies:
|
||||
mgrs "1.0.0"
|
||||
wkt-parser "^1.5.5"
|
||||
@@ -3018,11 +2863,6 @@ pump@^3.0.0:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
punycode@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
|
||||
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
|
||||
|
||||
queue@6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.1.tgz#abd5a5b0376912f070a25729e0b6a7d565683791"
|
||||
@@ -3124,11 +2964,6 @@ reproject@1.2.5:
|
||||
minimist "^1.2.0"
|
||||
proj4 "^2.4.4"
|
||||
|
||||
require-from-string@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
|
||||
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
|
||||
|
||||
resolve-protobuf-schema@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758"
|
||||
@@ -3176,13 +3011,6 @@ safe-buffer@^5.0.1, safe-buffer@~5.2.0:
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
saxes@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"
|
||||
integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
|
||||
dependencies:
|
||||
xmlchars "^2.2.0"
|
||||
|
||||
semver@^7.3.5:
|
||||
version "7.8.5"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69"
|
||||
@@ -3222,11 +3050,6 @@ slice-source@0.4.1:
|
||||
resolved "https://registry.yarnpkg.com/slice-source/-/slice-source-0.4.1.tgz#40a57ac03c6668b5da200e05378e000bf2a61d79"
|
||||
integrity sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==
|
||||
|
||||
source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
||||
splaytree-ts@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/splaytree-ts/-/splaytree-ts-1.0.2.tgz#34963704587aff45eaa09c24713f552bbf56e8f0"
|
||||
@@ -3279,11 +3102,6 @@ supports-color@^7.1.0:
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
symbol-tree@^3.2.4:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
|
||||
|
||||
tar-fs@^2.0.0:
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.5.tgz#33e9c29413dce0c58ada7ff77db4e5a30afffe70"
|
||||
@@ -3315,18 +3133,6 @@ tinyqueue@^2.0.3:
|
||||
resolved "https://registry.yarnpkg.com/tinyqueue/-/tinyqueue-2.0.3.tgz#64d8492ebf39e7801d7bd34062e29b45b2035f08"
|
||||
integrity sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==
|
||||
|
||||
tldts-core@^7.4.11:
|
||||
version "7.4.11"
|
||||
resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-7.4.11.tgz#a02fba29af72cbf9e658cb3a335f857efc9475f7"
|
||||
integrity sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==
|
||||
|
||||
tldts@^7.0.5:
|
||||
version "7.4.11"
|
||||
resolved "https://registry.yarnpkg.com/tldts/-/tldts-7.4.11.tgz#51d5a3feeb473e592edf671491a24fb23789eed2"
|
||||
integrity sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==
|
||||
dependencies:
|
||||
tldts-core "^7.4.11"
|
||||
|
||||
token-types@^4.1.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/token-types/-/token-types-4.2.1.tgz#0f897f03665846982806e138977dbe72d44df753"
|
||||
@@ -3349,20 +3155,6 @@ topojson-server@3.x:
|
||||
dependencies:
|
||||
commander "2"
|
||||
|
||||
tough-cookie@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-6.0.2.tgz#7b1f22fcf2daf06c4ff9d53ec1845f44c6627062"
|
||||
integrity sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==
|
||||
dependencies:
|
||||
tldts "^7.0.5"
|
||||
|
||||
tr46@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-6.0.0.tgz#f5a1ae546a0adb32a277a2278d0d17fa2f9093e6"
|
||||
integrity sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==
|
||||
dependencies:
|
||||
punycode "^2.3.1"
|
||||
|
||||
tslib@^2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
@@ -3390,11 +3182,6 @@ undici-types@~8.3.0:
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-8.3.0.tgz#44e9fc9f3244648cdea35e4f9bb2d681e9410809"
|
||||
integrity sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==
|
||||
|
||||
undici@^8.9.0:
|
||||
version "8.10.0"
|
||||
resolved "https://registry.yarnpkg.com/undici/-/undici-8.10.0.tgz#67ed7c4087f0f40fba7bef3a46f2be80572f2473"
|
||||
integrity sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==
|
||||
|
||||
unzipit@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unzipit/-/unzipit-2.0.0.tgz#9461b6a9b067ed9b13f9eda1bc5b93bc448307fa"
|
||||
@@ -3405,51 +3192,16 @@ util-deprecate@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||
|
||||
w3c-xmlserializer@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c"
|
||||
integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==
|
||||
dependencies:
|
||||
xml-name-validator "^5.0.0"
|
||||
|
||||
web-worker@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.5.0.tgz#71b2b0fbcc4293e8f0aa4f6b8a3ffebff733dcc5"
|
||||
integrity sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==
|
||||
|
||||
webidl-conversions@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz#0657e571fe6f06fcb15ca50ed1fdbcb495cd1686"
|
||||
integrity sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==
|
||||
|
||||
webworkify@1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/webworkify/-/webworkify-1.5.0.tgz#734ad87a774de6ebdd546e1d3e027da5b8f4a42c"
|
||||
integrity sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==
|
||||
|
||||
whatwg-mimetype@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz#d8232895dbd527ceaee74efd4162008fb8a8cf48"
|
||||
integrity sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==
|
||||
|
||||
whatwg-url@^16.0.0:
|
||||
version "16.0.1"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-16.0.1.tgz#047f7f4bd36ef76b7198c172d1b1cebc66f764dd"
|
||||
integrity sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==
|
||||
dependencies:
|
||||
"@exodus/bytes" "^1.11.0"
|
||||
tr46 "^6.0.0"
|
||||
webidl-conversions "^8.0.1"
|
||||
|
||||
whatwg-url@^17.1.0:
|
||||
version "17.1.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-17.1.0.tgz#693144cd2060d324e73b15a717d1f38ecfb3f02e"
|
||||
integrity sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==
|
||||
dependencies:
|
||||
"@exodus/bytes" "^1.15.1"
|
||||
tr46 "^6.0.0"
|
||||
webidl-conversions "^8.0.1"
|
||||
|
||||
wkt-parser@^1.3.1, wkt-parser@^1.5.5:
|
||||
version "1.5.6"
|
||||
resolved "https://registry.yarnpkg.com/wkt-parser/-/wkt-parser-1.5.6.tgz#3a19521914b2a21b7a8203590f5b180b9dba8dfa"
|
||||
@@ -3475,25 +3227,15 @@ wsl-utils@^1.0.0:
|
||||
is-wsl "^3.1.0"
|
||||
powershell-utils "^0.1.0"
|
||||
|
||||
xml-name-validator@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673"
|
||||
integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==
|
||||
|
||||
xml-utils@^1.10.2:
|
||||
version "1.10.2"
|
||||
resolved "https://registry.yarnpkg.com/xml-utils/-/xml-utils-1.10.2.tgz#436b39ccc25a663ce367ea21abb717afdea5d6b1"
|
||||
integrity sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA==
|
||||
|
||||
xmlchars@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
|
||||
|
||||
zarrita@^0.7.1:
|
||||
version "0.7.4"
|
||||
resolved "https://registry.yarnpkg.com/zarrita/-/zarrita-0.7.4.tgz#485fe2df3baa3ebd4b7da2098a844d229767748f"
|
||||
integrity sha512-NChufj86SceFj45z4CYjndmI98dphaG7hU8Wj0UGpMyT21o0V3HTY6zo87aSobYWts1tNFivSXzH2AR1AkaWjA==
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/zarrita/-/zarrita-0.7.5.tgz#74a4d5d5047cd1e930bffaed9234b62437a3fe13"
|
||||
integrity sha512-ZriOSEne+Wf2iuM0yjDZvUKpHNlug3dO7qJ6mAxXoOgsYV18DPOkAkwB9e0rf21bHcKcOffFuwoZwn5g6pvUeA==
|
||||
dependencies:
|
||||
"@zarrita/storage" "^0.2.0"
|
||||
numcodecs "^0.3.2"
|
||||
|
||||
Reference in New Issue
Block a user