Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions api/android-for-arcgis.api
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ public final class com.mapconductor.arcgis.ArcGISGeoViewHolder$DefaultImpls {
public static <ActualMapView extends android.widget.FrameLayout, ActualMap extends com.arcgismaps.mapping.view.GeoView> void setNavigationEnabled(com.mapconductor.arcgis.ArcGISGeoViewHolder<ActualMapView, ActualMap>, boolean);
public static <ActualMapView extends android.widget.FrameLayout, ActualMap extends com.arcgismaps.mapping.view.GeoView> java.lang.Object fromScreenOffset-3MmeM6k(com.mapconductor.arcgis.ArcGISGeoViewHolder<ActualMapView, ActualMap>, long, kotlin.coroutines.Continuation<? super com.mapconductor.core.features.GeoPoint>);
public static <ActualMapView extends android.widget.FrameLayout, ActualMap extends com.arcgismaps.mapping.view.GeoView> com.mapconductor.core.features.GeoPoint fromScreenOffsetSync-k-4lQ0M(com.mapconductor.arcgis.ArcGISGeoViewHolder<ActualMapView, ActualMap>, long);
public static <ActualMapView extends android.widget.FrameLayout, ActualMap extends com.arcgismaps.mapping.view.GeoView> com.mapconductor.core.projection.ProjectedPoint toProjectedPoint(com.mapconductor.arcgis.ArcGISGeoViewHolder<ActualMapView, ActualMap>, com.mapconductor.core.features.GeoPointInterface);
public static <ActualMapView extends android.widget.FrameLayout, ActualMap extends com.arcgismaps.mapping.view.GeoView> com.mapconductor.core.features.GeoPoint fromProjectedPoint(com.mapconductor.arcgis.ArcGISGeoViewHolder<ActualMapView, ActualMap>, com.mapconductor.core.projection.ProjectedPoint);
public static <ActualMapView extends android.widget.FrameLayout, ActualMap extends com.arcgismaps.mapping.view.GeoView> com.mapconductor.core.projection.MapCoordinateSystem getCoordinateSystem(com.mapconductor.arcgis.ArcGISGeoViewHolder<ActualMapView, ActualMap>);
}
public final class com.mapconductor.arcgis.ArcGISGraphicsZIndex {
public com.mapconductor.arcgis.ArcGISGraphicsZIndex();
Expand Down Expand Up @@ -194,6 +197,9 @@ public final class com.mapconductor.arcgis.ArcGISMapView2DHolder implements com.
public com.arcgismaps.mapping.view.GeoView getGeoView();
public com.arcgismaps.geometry.SpatialReference getSpatialReference();
public java.util.List<com.arcgismaps.mapping.layers.Layer> getOperationalLayers();
public com.mapconductor.core.projection.MapCoordinateSystem getCoordinateSystem();
public com.mapconductor.core.projection.ProjectedPoint toProjectedPoint(com.mapconductor.core.features.GeoPointInterface);
public com.mapconductor.core.features.GeoPoint fromProjectedPoint(com.mapconductor.core.projection.ProjectedPoint);
public androidx.compose.ui.geometry.Offset toScreenOffset-x-9fifI(com.mapconductor.core.features.GeoPointInterface);
public java.lang.Object fromScreenOffset-3MmeM6k(long, kotlin.coroutines.Continuation<? super com.mapconductor.core.features.GeoPoint>);
public com.mapconductor.core.features.GeoPoint fromScreenOffsetSync-k-4lQ0M(long);
Expand Down Expand Up @@ -285,6 +291,9 @@ public final class com.mapconductor.arcgis.ArcGISMapViewHolder implements com.ma
public com.mapconductor.core.features.GeoPoint fromScreenOffsetSync-k-4lQ0M(long);
public boolean getUsesSceneView();
public void setNavigationEnabled(boolean);
public com.mapconductor.core.projection.ProjectedPoint toProjectedPoint(com.mapconductor.core.features.GeoPointInterface);
public com.mapconductor.core.features.GeoPoint fromProjectedPoint(com.mapconductor.core.projection.ProjectedPoint);
public com.mapconductor.core.projection.MapCoordinateSystem getCoordinateSystem();
public java.lang.Object getMapView();
public java.lang.Object getMap();
}
Expand Down Expand Up @@ -348,6 +357,7 @@ public final class com.mapconductor.arcgis.ArcGISOverlayStacking {
public static final com.mapconductor.arcgis.ArcGISOverlayStacking INSTANCE;
public final com.mapconductor.core.controller.OverlayStacking$Key getMARKER_TILE();
public final com.mapconductor.core.controller.OverlayStacking$Key getPOLYGON_TILE();
public final com.mapconductor.core.controller.OverlayStacking$Key getCIRCLE_TILE();
public final com.mapconductor.core.controller.OverlayStacking$Key getPOLYLINE_TILE();
public final com.mapconductor.core.controller.OverlayStacking$Key key(com.mapconductor.core.controller.OverlayKind, int);
public final void add(java.util.List<com.arcgismaps.mapping.layers.Layer>, com.arcgismaps.mapping.layers.Layer, com.mapconductor.core.controller.OverlayStacking$Key);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.mapconductor.arcgis

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.arcgismaps.ArcGISEnvironment
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.view.MapView
import com.mapconductor.core.features.GeoPoint
import com.mapconductor.core.projection.ProjectedUnit
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith

/**
* 2D の投影座標は地図の空間参照で返る。
*
* サンプルの 2D 地図は Web Mercator 固定なので、それ以外の空間参照(UTM・WGS84)は
* ここでしか通らない。
*/
@RunWith(AndroidJUnit4::class)
class ArcGISProjectedPointTest {
private val tokyo = GeoPoint(35.681236, 139.767125)

private fun holderWith(spatialReference: SpatialReference): ArcGISMapView2DHolder {
lateinit var holder: ArcGISMapView2DHolder
val context = InstrumentationRegistry.getInstrumentation().targetContext
InstrumentationRegistry.getInstrumentation().runOnMainSync {
ArcGISEnvironment.applicationContext = context.applicationContext
val map = MapView(context)
map.map = ArcGISMap(spatialReference)
val wrap = WrapMapView(context).also { it.arcGISMapView = map }
holder = ArcGISMapView2DHolder(wrap, map)
}
return holder
}

@Test
fun utmZone54NReturnsEastingNorthingInMeters() {
val holder = holderWith(SpatialReference(32654))
assertEquals("EPSG:32654", holder.coordinateSystem.code)
assertEquals(ProjectedUnit.Meters, holder.coordinateSystem.unit)

val projected = holder.toProjectedPoint(tokyo)
assertNotNull(projected)
// 東京駅は中央子午線(東経 141 度)の西 1.23 度: 偽東距 500000 m から約 11 万 m 西。
assertTrue("easting ${projected!!.x}", projected.x in 380_000.0..400_000.0)
assertTrue("northing ${projected.y}", projected.y in 3_940_000.0..3_960_000.0)

val back = holder.fromProjectedPoint(projected)
assertNotNull(back)
assertEquals(tokyo.latitude, back!!.latitude, 1e-7)
assertEquals(tokyo.longitude, back.longitude, 1e-7)
}

@Test
fun wgs84ReturnsDegrees() {
val holder = holderWith(SpatialReference.wgs84())
assertEquals("EPSG:4326", holder.coordinateSystem.code)
assertEquals(ProjectedUnit.Degrees, holder.coordinateSystem.unit)

val projected = holder.toProjectedPoint(tokyo)!!
assertEquals(tokyo.longitude, projected.x, 1e-9)
assertEquals(tokyo.latitude, projected.y, 1e-9)
}

@Test
fun webMercatorMatchesTheCoreDefault() {
val holder = holderWith(SpatialReference.webMercator())
assertEquals("EPSG:3857", holder.coordinateSystem.code)
assertEquals(ProjectedUnit.Meters, holder.coordinateSystem.unit)

val projected = holder.toProjectedPoint(tokyo)!!
assertEquals(15558805.18, projected.x, 0.01)
assertEquals(4256848.12, projected.y, 0.01)
}
}
77 changes: 77 additions & 0 deletions src/main/java/com/mapconductor/arcgis/ArcGISMapViewHolderImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package com.mapconductor.arcgis

import androidx.compose.ui.geometry.Offset
import androidx.lifecycle.LifecycleOwner
import com.arcgismaps.geometry.GeometryEngine
import com.arcgismaps.geometry.LinearUnit
import com.arcgismaps.geometry.LinearUnitId
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.layers.Layer
import com.arcgismaps.mapping.view.DoubleXY
Expand All @@ -11,6 +15,9 @@ import com.arcgismaps.mapping.view.SceneView
import com.mapconductor.core.features.GeoPoint
import com.mapconductor.core.features.GeoPointInterface
import com.mapconductor.core.map.MapViewHolderInterface
import com.mapconductor.core.projection.MapCoordinateSystem
import com.mapconductor.core.projection.ProjectedPoint
import com.mapconductor.core.projection.ProjectedUnit
import kotlin.math.abs
import kotlin.math.max
import android.content.Context
Expand Down Expand Up @@ -268,6 +275,24 @@ class ArcGISMapView2DHolder(
override val operationalLayers: MutableList<Layer>?
get() = runCatching { map.map?.operationalLayers }.getOrNull()

/**
* 2D の地図は空間参照を持つので、投影座標はその空間参照で返す(いまの 2D は
* Web Mercator 固定だが、地図の空間参照が変われば追従する)。地図がまだ無いときは
* 既定の EPSG:3857。3D([ArcGISMapViewHolder])は地球儀なので既定のまま。
*/
override val coordinateSystem: MapCoordinateSystem
get() = spatialReference?.toMapCoordinateSystem() ?: MapCoordinateSystem.WebMercator

override fun toProjectedPoint(position: GeoPointInterface): ProjectedPoint? {
val reference = spatialReference ?: return super.toProjectedPoint(position)
return projectToSpatialReference(position, reference)
}

override fun fromProjectedPoint(point: ProjectedPoint): GeoPoint? {
val reference = spatialReference ?: return super.fromProjectedPoint(point)
return unprojectFromSpatialReference(point, reference)
}

/**
* `GeoPoint.toPoint(spatialReference)` は経度緯度の値にその空間参照を「貼る」だけで座標変換は
* しない。2D の `ArcGISMap` は既定で Web メルカトルなので、ビューの空間参照をそのまま渡すと
Expand Down Expand Up @@ -351,3 +376,55 @@ internal fun Context.getArcGisApiKey(): String? =
.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
.metaData
?.getString("ARCGIS_API_KEY")

/**
* ArcGIS の空間参照 → [MapCoordinateSystem]。
*
* - WKID が 32767 以下なら EPSG の番号と同じなので `EPSG:<wkid>`。
* - Esri 独自の番号は `ESRI:<wkid>`。ただし Web Mercator の旧番号
* (102100 / 102113)は EPSG:3857 にそろえる。300.x の SDK には `latestWkid` が無い。
* - WKID を持たない(WKT だけで定義された)空間参照は null。
*/
private fun SpatialReference.toMapCoordinateSystem(): MapCoordinateSystem {
val code =
when (val wkid = wkid) {
0 -> null
102100, 102113 -> "EPSG:3857"
in 1..32767 -> "EPSG:$wkid"
else -> "ESRI:$wkid"
}
val unit =
when {
isGeographic -> ProjectedUnit.Degrees
(unit as? LinearUnit)?.linearUnitId == LinearUnitId.Meters -> ProjectedUnit.Meters
else -> ProjectedUnit.Other
}
return MapCoordinateSystem(code, unit)
}

/** 地理座標 → [spatialReference] の座標。ArcGIS が投影できないときは null。 */
private fun projectToSpatialReference(
position: GeoPointInterface,
spatialReference: SpatialReference,
): ProjectedPoint? {
val wgs84 = GeoPoint.from(position).toPoint(SpatialReference.wgs84())
val projected = runCatching { GeometryEngine.projectOrNull(wgs84, spatialReference) as? Point }.getOrNull()
return projected?.let { ProjectedPoint(it.x, it.y) }?.takeIf { it.x.isFinite() && it.y.isFinite() }
}

/**
* [spatialReference] の座標 → 地理座標。
*
* [toGeoPoint] は投影に失敗すると元の値をそのまま経度緯度として返すので使わない。
*/
private fun unprojectFromSpatialReference(
point: ProjectedPoint,
spatialReference: SpatialReference,
): GeoPoint? {
val source = Point(x = point.x, y = point.y, spatialReference = spatialReference)
val wgs84 =
runCatching { GeometryEngine.projectOrNull(source, SpatialReference.wgs84()) as? Point }.getOrNull()
?: return null
if (!wgs84.x.isFinite() || !wgs84.y.isFinite()) return null
return GeoPoint(latitude = wgs84.y, longitude = wgs84.x, altitude = 0.0)
}
Loading