From b8e7472299f725c98f349a1d36bc4029bd839979 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Fri, 6 Nov 2026 18:05:23 -0800 Subject: [PATCH 1/2] Default Circle strokeWidth to 1 and GroundImage opacity to 1.0, like their states The component form and the state form of the same overlay drew differently when nothing was passed: Circle and GroundImage components used strokeWidth 2 / opacity 0.5 while their states used 1 / 1.0, and PolygonState used strokeWidth 2 while the Polygon component used 1. Defaults are now the same for both forms on React, Android and iOS: Circle / Polygon / Polyline strokeWidth = 1, GroundImage opacity = 1.0. The API baseline changes only in these three default values. --- Sources/MapConductorCore/MapLayerItems.swift | 7 ++-- Sources/MapConductorCore/MapShapeItems.swift | 6 ++-- .../polygon/PolygonState.swift | 2 +- .../ShapeDefaultsTests.swift | 35 +++++++++++++++++++ api/MapConductorCore.api.swift | 8 ++--- docs/PolygonState.swift.md | 4 +-- 6 files changed, 47 insertions(+), 15 deletions(-) create mode 100644 Tests/MapConductorCoreTests/ShapeDefaultsTests.swift diff --git a/Sources/MapConductorCore/MapLayerItems.swift b/Sources/MapConductorCore/MapLayerItems.swift index 80df610..7c0a9b2 100644 --- a/Sources/MapConductorCore/MapLayerItems.swift +++ b/Sources/MapConductorCore/MapLayerItems.swift @@ -14,9 +14,8 @@ public struct GroundImage: MapOverlayItemProtocol, Identifiable { public init( bounds: GeoRectBounds, image: UIImage, - // android-sdk の `GroundImage` コンポーザブル(opacity = 0.5f)と同じ既定値。 - // `GroundImageState` 側の既定は 3 者とも 1.0 で、コンポーネントだけ 0.5。 - opacity: Double = 0.5, + // `GroundImageState` と同じ既定値(3 プラットフォームとも 1.0)。 + opacity: Double = 1.0, tileSize: Int = 512, id: String? = nil, extra: Any? = nil, @@ -39,7 +38,7 @@ public struct GroundImage: MapOverlayItemProtocol, Identifiable { public init( bounds: GeoRectBounds, imageUrl: URL, - opacity: Double = 0.5, + opacity: Double = 1.0, tileSize: Int = 512, id: String? = nil, extra: Any? = nil, diff --git a/Sources/MapConductorCore/MapShapeItems.swift b/Sources/MapConductorCore/MapShapeItems.swift index a6b916e..bfce2dd 100644 --- a/Sources/MapConductorCore/MapShapeItems.swift +++ b/Sources/MapConductorCore/MapShapeItems.swift @@ -192,10 +192,8 @@ public struct Circle: MapOverlayItemProtocol, Identifiable { geodesic: Bool = true, clickable: Bool = true, strokeColor: UIColor = .red, - // android-sdk の `Circle` コンポーザブル(strokeWidth = 2.dp)と同じ既定値。 - // `CircleState` 側の既定は 3 者とも 1 で、コンポーネントだけ 2 という - // 差はプラットフォーム共通(Polygon も同じ構図)。 - strokeWidth: Double = 2.0, + // `CircleState` と同じ既定値(3 プラットフォームとも 1)。 + strokeWidth: Double = 1.0, fillColor: UIColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.5), id: String? = nil, zIndex: Int? = nil, diff --git a/Sources/MapConductorCore/polygon/PolygonState.swift b/Sources/MapConductorCore/polygon/PolygonState.swift index c156d7f..52db6d8 100644 --- a/Sources/MapConductorCore/polygon/PolygonState.swift +++ b/Sources/MapConductorCore/polygon/PolygonState.swift @@ -59,7 +59,7 @@ public final class PolygonState: ObservableObject, Identifiable, Equatable, Hash holes: [[GeoPointProtocol]] = [], id: String? = nil, strokeColor: UIColor = .black, - strokeWidth: Double = 2.0, + strokeWidth: Double = 1.0, fillColor: UIColor = .clear, geodesic: Bool = false, zIndex: Int = 0, diff --git a/Tests/MapConductorCoreTests/ShapeDefaultsTests.swift b/Tests/MapConductorCoreTests/ShapeDefaultsTests.swift new file mode 100644 index 0000000..5edc070 --- /dev/null +++ b/Tests/MapConductorCoreTests/ShapeDefaultsTests.swift @@ -0,0 +1,35 @@ +import UIKit +import XCTest +@testable import MapConductorCore + +/// 図形・地表画像の既定値。コンポーネント形(`Circle(center:...)` など)と state 形で +/// 同じ値になるよう 3 プラットフォームで揃えている: +/// Circle / Polygon / Polyline の strokeWidth = 1、GroundImage の opacity = 1.0。 +/// js-sdk-core の `ShapeDefaults.test.mjs`、android-sdk の `ShapeDefaultsTest` と同じ観点。 +final class ShapeDefaultsTests: XCTestCase { + private let p1 = GeoPoint(latitude: 35.0, longitude: 139.0) + private let p2 = GeoPoint(latitude: 35.0, longitude: 140.0) + private let p3 = GeoPoint(latitude: 36.0, longitude: 140.0) + + func testCircleStrokeWidthDefaultsTo1() { + XCTAssertEqual(CircleState(center: p1, radiusMeters: 100).strokeWidth, 1.0) + XCTAssertEqual(Circle(center: p1, radiusMeters: 100).state.strokeWidth, 1.0) + } + + func testPolygonStrokeWidthDefaultsTo1() { + XCTAssertEqual(PolygonState(points: [p1, p2, p3]).strokeWidth, 1.0) + XCTAssertEqual(Polygon(points: [p1, p2, p3]).state.strokeWidth, 1.0) + } + + func testPolylineStrokeWidthDefaultsTo1() { + XCTAssertEqual(PolylineState(points: [p1, p2]).strokeWidth, 1.0) + XCTAssertEqual(Polyline(points: [p1, p2]).state.strokeWidth, 1.0) + } + + func testGroundImageOpacityDefaultsTo1() { + let bounds = GeoRectBounds(southWest: p1, northEast: p3) + let image = UIImage() + XCTAssertEqual(GroundImageState(bounds: bounds, image: image).opacity, 1.0) + XCTAssertEqual(GroundImage(bounds: bounds, image: image).state.opacity, 1.0) + } +} diff --git a/api/MapConductorCore.api.swift b/api/MapConductorCore.api.swift index b66fe00..ecc15d9 100644 --- a/api/MapConductorCore.api.swift +++ b/api/MapConductorCore.api.swift @@ -70,8 +70,8 @@ public struct GroundImage : MapConductorCore::MapOverlayItemProtocol, Swift::Ide public let id: Swift::String public let state: MapConductorCore::GroundImageState public init(state: MapConductorCore::GroundImageState) - public init(bounds: MapConductorCore::GeoRectBounds, image: UIKit::UIImage, opacity: Swift::Double = 0.5, tileSize: Swift::Int = 512, id: Swift::String? = nil, extra: Any? = nil, onClick: MapConductorCore::OnGroundImageEventHandler? = nil) - public init(bounds: MapConductorCore::GeoRectBounds, imageUrl: Foundation::URL, opacity: Swift::Double = 0.5, tileSize: Swift::Int = 512, id: Swift::String? = nil, extra: Any? = nil, onClick: MapConductorCore::OnGroundImageEventHandler? = nil) + public init(bounds: MapConductorCore::GeoRectBounds, image: UIKit::UIImage, opacity: Swift::Double = 1.0, tileSize: Swift::Int = 512, id: Swift::String? = nil, extra: Any? = nil, onClick: MapConductorCore::OnGroundImageEventHandler? = nil) + public init(bounds: MapConductorCore::GeoRectBounds, imageUrl: Foundation::URL, opacity: Swift::Double = 1.0, tileSize: Swift::Int = 512, id: Swift::String? = nil, extra: Any? = nil, onClick: MapConductorCore::OnGroundImageEventHandler? = nil) public func append(to content: inout MapConductorCore::MapViewContent) public typealias ID = Swift::String } @@ -139,7 +139,7 @@ public struct Circle : MapConductorCore::MapOverlayItemProtocol, Swift::Identifi public let id: Swift::String public let state: MapConductorCore::CircleState public init(state: MapConductorCore::CircleState) - public init(center: any MapConductorCore::GeoPointProtocol, radiusMeters: Swift::Double, geodesic: Swift::Bool = true, clickable: Swift::Bool = true, strokeColor: UIKit::UIColor = .red, strokeWidth: Swift::Double = 2.0, fillColor: UIKit::UIColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.5), id: Swift::String? = nil, zIndex: Swift::Int? = nil, extra: Any? = nil, onClick: MapConductorCore::OnCircleEventHandler? = nil) + public init(center: any MapConductorCore::GeoPointProtocol, radiusMeters: Swift::Double, geodesic: Swift::Bool = true, clickable: Swift::Bool = true, strokeColor: UIKit::UIColor = .red, strokeWidth: Swift::Double = 1.0, fillColor: UIKit::UIColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.5), id: Swift::String? = nil, zIndex: Swift::Int? = nil, extra: Any? = nil, onClick: MapConductorCore::OnCircleEventHandler? = nil) public func append(to content: inout MapConductorCore::MapViewContent) public typealias ID = Swift::String } @@ -2445,7 +2445,7 @@ final public class PolygonState : Combine::ObservableObject, Swift::Identifiable @available(macOS 11.0, iOS 14.0, tvOS 14.0, watchOS 7.0, *) set } - public init(points: [any MapConductorCore::GeoPointProtocol], holes: [[any MapConductorCore::GeoPointProtocol]] = [], id: Swift::String? = nil, strokeColor: UIKit::UIColor = .black, strokeWidth: Swift::Double = 2.0, fillColor: UIKit::UIColor = .clear, geodesic: Swift::Bool = false, zIndex: Swift::Int = 0, extra: Any? = nil, onClick: MapConductorCore::OnPolygonEventHandler? = nil) + public init(points: [any MapConductorCore::GeoPointProtocol], holes: [[any MapConductorCore::GeoPointProtocol]] = [], id: Swift::String? = nil, strokeColor: UIKit::UIColor = .black, strokeWidth: Swift::Double = 1.0, fillColor: UIKit::UIColor = .clear, geodesic: Swift::Bool = false, zIndex: Swift::Int = 0, extra: Any? = nil, onClick: MapConductorCore::OnPolygonEventHandler? = nil) final public func copy(points: [any MapConductorCore::GeoPointProtocol]? = nil, holes: [[any MapConductorCore::GeoPointProtocol]]? = nil, id: Swift::String? = nil, strokeColor: UIKit::UIColor? = nil, strokeWidth: Swift::Double? = nil, fillColor: UIKit::UIColor? = nil, geodesic: Swift::Bool? = nil, zIndex: Swift::Int? = nil, extra: Any? = nil, onClick: MapConductorCore::OnPolygonEventHandler? = nil) -> MapConductorCore::PolygonState final public func fingerPrint() -> MapConductorCore::PolygonFingerPrint final public func asFlow() -> Combine::AnyPublisher diff --git a/docs/PolygonState.swift.md b/docs/PolygonState.swift.md index 0d3673d..51c70c4 100644 --- a/docs/PolygonState.swift.md +++ b/docs/PolygonState.swift.md @@ -23,7 +23,7 @@ public final class PolygonState: ObservableObject, Identifiable, Equatable, Hash points: [GeoPointProtocol], id: String? = nil, strokeColor: UIColor = .black, - strokeWidth: Double = 2.0, + strokeWidth: Double = 1.0, fillColor: UIColor = .clear, geodesic: Bool = false, zIndex: Int = 0, @@ -49,7 +49,7 @@ public final class PolygonState: ObservableObject, Identifiable, Equatable, Hash - Description: Color of the polygon border. - `strokeWidth` - Type: `Double` - - Default: `2.0` + - Default: `1.0` - Description: Border width in points. - `fillColor` - Type: `UIColor` From 8d047f8070142a57933c49f98c4b39973ca31389 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Fri, 6 Nov 2026 18:05:23 -0800 Subject: [PATCH 2/2] Document computeSignedArea's sign: clockwise is positive Spherical and WGS84Geodesic return a positive area for a clockwise ring and a negative one for a counter-clockwise ring (north-up), as on Android and the web. Say so in the doc comments and pin it with a test. Behaviour and the API baseline are unchanged. --- .../spherical/Spherical.swift | 3 +++ .../spherical/WGS84Geodesic.swift | 3 +++ .../SignedAreaSignTests.swift | 27 +++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 Tests/MapConductorCoreTests/SignedAreaSignTests.swift diff --git a/Sources/MapConductorCore/spherical/Spherical.swift b/Sources/MapConductorCore/spherical/Spherical.swift index 4ee95b5..5fedda8 100644 --- a/Sources/MapConductorCore/spherical/Spherical.swift +++ b/Sources/MapConductorCore/spherical/Spherical.swift @@ -92,6 +92,9 @@ public enum Spherical { abs(computeSignedArea(path)) } + /// Signed area of a closed path in square meters. + /// Positive for a clockwise ring, negative for a counter-clockwise ring + /// (as seen on a north-up map, e.g. SW -> NW -> NE -> SE is clockwise and positive). public static func computeSignedArea(_ path: [GeoPointProtocol]) -> Double { guard path.count >= 3 else { return 0.0 } var area = 0.0 diff --git a/Sources/MapConductorCore/spherical/WGS84Geodesic.swift b/Sources/MapConductorCore/spherical/WGS84Geodesic.swift index d26bd1c..512b8df 100644 --- a/Sources/MapConductorCore/spherical/WGS84Geodesic.swift +++ b/Sources/MapConductorCore/spherical/WGS84Geodesic.swift @@ -95,6 +95,9 @@ public enum WGS84Geodesic { /// `Spherical.computeSignedArea` — matching the ellipsoidal area to well under /// 0.01% for typical polygons. Edges are treated as authalic-sphere arcs; the /// higher-order geodesic-edge terms of Karney's exact method are omitted. + /// + /// The sign follows `Spherical.computeSignedArea`: positive for a clockwise + /// ring, negative for a counter-clockwise ring (as seen on a north-up map). public static func computeSignedArea(_ path: [GeoPointProtocol]) -> Double { guard path.count >= 3 else { return 0.0 } var area = 0.0 diff --git a/Tests/MapConductorCoreTests/SignedAreaSignTests.swift b/Tests/MapConductorCoreTests/SignedAreaSignTests.swift new file mode 100644 index 0000000..ef653a4 --- /dev/null +++ b/Tests/MapConductorCoreTests/SignedAreaSignTests.swift @@ -0,0 +1,27 @@ +import XCTest +@testable import MapConductorCore + +/// computeSignedArea の符号の約束: 北が上の地図で時計回りのリングが正、反時計回りが負。 +/// js-sdk-core の `SignedAreaSign.test.mjs`、android-sdk の `SignedAreaSignTest` と同じ観点。 +final class SignedAreaSignTests: XCTestCase { + private let sw = GeoPoint(latitude: 35.0, longitude: 139.0) + private let se = GeoPoint(latitude: 35.0, longitude: 140.0) + private let ne = GeoPoint(latitude: 36.0, longitude: 140.0) + private let nw = GeoPoint(latitude: 36.0, longitude: 139.0) + + private func check(_ signedArea: ([GeoPointProtocol]) -> Double, file: StaticString = #filePath, line: UInt = #line) { + let cw = signedArea([sw, nw, ne, se]) + let ccw = signedArea([sw, se, ne, nw]) + XCTAssertGreaterThan(cw, 0, file: file, line: line) + XCTAssertLessThan(ccw, 0, file: file, line: line) + XCTAssertEqual(cw, -ccw, accuracy: 1e-3, file: file, line: line) + } + + func testSphericalClockwiseIsPositive() { + check(Spherical.computeSignedArea) + } + + func testWGS84GeodesicClockwiseIsPositive() { + check(WGS84Geodesic.computeSignedArea) + } +}