-
Notifications
You must be signed in to change notification settings - Fork 12
[New] Sample: Navigate map view and identify features with keyboard #786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
chriswebb09
wants to merge
65
commits into
v.next
Choose a base branch
from
chrisw/navigate-and-interact-with-keyboard
base: v.next
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 55 commits
Commits
Show all changes
65 commits
Select commit
Hold shift + click to select a range
56dff8a
initial commit
chriswebb09 a4dee50
update sample
chriswebb09 7f82f45
update readme
chriswebb09 29c72c1
update readme
chriswebb09 12b2a93
update
chriswebb09 5f5135e
update
chriswebb09 c6bb865
update for script error
chriswebb09 262df01
update
chriswebb09 025101a
add in comma
chriswebb09 90662b0
update
chriswebb09 3f82dfc
update
chriswebb09 7994419
update
chriswebb09 028fc69
Apply suggestions from code review
chriswebb09 2816732
fix optional error
chriswebb09 0314c69
Apply suggestions from code review
chriswebb09 eefe293
update
chriswebb09 ed6ad2d
update
chriswebb09 f6c315f
Apply suggestions from code review
chriswebb09 c2cb7e9
Apply suggestions from code review
chriswebb09 8eb53ac
update indentation
chriswebb09 3d16479
update formatting
chriswebb09 86e16a2
Potential fix for pull request finding
chriswebb09 92129d9
update remove textfield
chriswebb09 823c370
update
chriswebb09 9932321
update
chriswebb09 306038d
cleanup
chriswebb09 e49a67d
Apply suggestions from code review
chriswebb09 caaa93a
fix error
chriswebb09 a75a2c1
cleanup
chriswebb09 a43a17f
update
chriswebb09 f3a118c
cleanup
chriswebb09 6759ded
Apply suggestions from code review
chriswebb09 9d70469
Merge branch 'v.next' into chrisw/navigate-and-interact-with-keyboard
chriswebb09 a45d9e0
Apply suggestions from code review
chriswebb09 c3fcd46
make method names more idiomatic, add in comments, make envelope reap…
chriswebb09 b8d2ed3
Apply suggestions from code review
chriswebb09 fbccaeb
simplify code
chriswebb09 9b7f48d
update tipkit initialization
chriswebb09 7148266
update
chriswebb09 63ebd9a
fix formatting
chriswebb09 5ef82e6
Apply suggestions from code review
chriswebb09 02f2e40
revert change to README
chriswebb09 8b715d9
add bac additional info section
chriswebb09 d85ecd4
Merge remote-tracking branch 'refs/remotes/origin/chrisw/navigate-and…
chriswebb09 e555b4a
fix ofr linting error in README
chriswebb09 c16eb60
update envelope logic
chriswebb09 49a79b7
update halo wddwxr logic
chriswebb09 5879ba0
hide and show keyboard tip depending when keyboard is active
chriswebb09 f0e8d34
update
chriswebb09 d8e399e
cleanup
chriswebb09 9cad02e
Potential fix for pull request finding
chriswebb09 939730b
update map scale and only show keyboard tip when keyboard is active
chriswebb09 6f07fd9
Apply suggestions from code review
chriswebb09 c4e515b
update README url for swift
chriswebb09 c8e6a28
add changes for review
chriswebb09 30d9c7f
Apply suggestions from code review
chriswebb09 89bfc9e
Apply suggestions from code review
chriswebb09 3853780
stash
chriswebb09 779f23d
stash 2
chriswebb09 7431fe4
update keyboard actions
chriswebb09 3d47da4
update tips text
chriswebb09 5511dfb
remove unused code
chriswebb09 210a9a2
Apply suggestions from code review
chriswebb09 4f9563c
Apply suggestions from code review
chriswebb09 a9b5ded
update keyboard functionality
chriswebb09 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
209 changes: 209 additions & 0 deletions
209
...dentify features with keyboard/NavigateMapAndIdentifyFeaturesWithKeyboardView.Model.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| // Copyright 2026 Esri | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // https://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| import ArcGIS | ||
| import Foundation | ||
| import SwiftUI | ||
| import UIKit.UIColor | ||
|
|
||
| extension NavigateMapAndIdentifyFeaturesWithKeyboardView { | ||
| /// The model for the sample. | ||
| @MainActor | ||
| @Observable | ||
| final class Model { | ||
| /// The maximum number of features that can be identified with number keys. | ||
| private static let maximumNumberedFeatures = 9 | ||
|
|
||
| /// Buffer distance around the selection geometry to account for edge cases. | ||
| private static let selectionBufferDistance = LinearUnit.meters.convert(to: .meters, value: 60) | ||
|
|
||
| /// Attribute used to title and label each feature. | ||
| private static let nameAttribute = "name" | ||
|
|
||
| /// Colors used for the marker, selection halo, and label. | ||
| private static let markerFillColor = UIColor(red: 11 / 255, green: 79 / 255, blue: 138 / 255, alpha: 1) | ||
| static let selectionHaloColor = Color(red: 190 / 255, green: 24 / 255, blue: 93 / 255) | ||
| private static let labelTextColor = UIColor(red: 31 / 255, green: 35 / 255, blue: 40 / 255, alpha: 1) | ||
|
|
||
| /// The map displayed in the map view. | ||
| let map: Map | ||
|
|
||
| /// The feature table of restaurants in Redlands. | ||
| private let restaurantsTable = ServiceFeatureTable(url: .redlandsRestaurants) | ||
|
|
||
| /// The feature layer holding the restaurants displayed and identified by the sample. | ||
| private let restaurantsLayer: FeatureLayer | ||
|
|
||
| /// The overlay for the numbered 1-9 labels. | ||
| let labelOverlay = GraphicsOverlay() | ||
|
|
||
| /// Features currently in the area of interest, indexed by the matching number key. | ||
| private(set) var numberedFeatures: [Feature] = [] | ||
|
|
||
| /// A Boolean value indicating whether more than nine features are selected. | ||
| private(set) var hasMoreThanNineSelectedFeatures = false | ||
|
|
||
| init() { | ||
| restaurantsLayer = FeatureLayer(featureTable: restaurantsTable) | ||
| restaurantsLayer.renderer = SimpleRenderer(symbol: Self.restaurantSymbol) | ||
|
|
||
| let map = Map(basemapStyle: .arcGISLightGray) | ||
| map.initialViewpoint = Viewpoint( | ||
| center: Point(x: -117.1825, y: 34.0556, spatialReference: .wgs84), | ||
| scale: 4_000 | ||
| ) | ||
| map.addOperationalLayer(restaurantsLayer) | ||
| self.map = map | ||
| } | ||
|
|
||
| /// Ensures the feature table is fully loaded before querying. | ||
| func ensureLayerLoaded() async throws { | ||
| // Load the feature table to ensure metadata and features are available | ||
| try await restaurantsTable.load() | ||
| } | ||
|
chriswebb09 marked this conversation as resolved.
|
||
|
|
||
| func selectFeatures( | ||
| intersecting selectionGeometry: Geometry?, | ||
| containsScreenPoint: (CGPoint) -> Bool, | ||
| screenPointFor: (Point) -> CGPoint? | ||
| ) async throws { | ||
| clearSelection() | ||
|
|
||
| guard let selectionGeometry else { return } | ||
|
|
||
| let unorderedFeatures = try await makeFeatures(intersecting: selectionGeometry) | ||
| .filter { orderedFeature in | ||
| guard let screenPoint = screenPointFor(orderedFeature.anchor) else { return false } | ||
| return containsScreenPoint(screenPoint) | ||
| } | ||
| let orderedFeatures = unorderedFeatures.sorted { lhs, rhs in | ||
| let lhsScreenPoint = screenPointFor(lhs.anchor) | ||
| let rhsScreenPoint = screenPointFor(rhs.anchor) | ||
|
|
||
| switch (lhsScreenPoint, rhsScreenPoint) { | ||
| case let (l?, r?): | ||
| if l.y != r.y { return l.y < r.y } | ||
| if l.x != r.x { return l.x < r.x } | ||
| return Self.isEarlierInGeographicReadingOrder(lhs.anchor, than: rhs.anchor) | ||
| case (nil, nil): | ||
| return Self.isEarlierInGeographicReadingOrder(lhs.anchor, than: rhs.anchor) | ||
| case (nil, _?): | ||
| return false | ||
| case (_?, nil): | ||
| return true | ||
| } | ||
| } | ||
|
chriswebb09 marked this conversation as resolved.
Outdated
|
||
|
|
||
| hasMoreThanNineSelectedFeatures = orderedFeatures.count > Self.maximumNumberedFeatures | ||
|
|
||
| // Select all intersecting features; only the first 9 are numbered/labeled. | ||
| restaurantsLayer.selectFeatures(orderedFeatures.map(\.feature)) | ||
|
|
||
| addNumberedLabels(for: orderedFeatures) | ||
| } | ||
|
chriswebb09 marked this conversation as resolved.
|
||
|
|
||
| /// Clears selected features, label graphics, and numbered feature state. | ||
| func clearSelection() { | ||
| restaurantsLayer.clearSelection() | ||
| labelOverlay.removeAllGraphics() | ||
| numberedFeatures.removeAll() | ||
| hasMoreThanNineSelectedFeatures = false | ||
| } | ||
|
|
||
| /// Reads the feature's name attribute, returning the fallback when it is missing or blank. | ||
| /// - Parameters: | ||
| /// - feature: The feature whose name is read. | ||
| /// - fallback: The value to return when the feature has no name. | ||
| /// - Returns: The feature's name, or the fallback if no name exists. | ||
| func name(for feature: Feature, fallback: String?) -> String? { | ||
| guard let name = feature.attributes[Self.nameAttribute] as? String, | ||
| !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { | ||
| return fallback | ||
| } | ||
| return name | ||
| } | ||
|
|
||
| /// Queries restaurant features intersecting the given geometry. | ||
| /// - Parameter geometry: The geometry used to query restaurant features. | ||
| /// - Returns: The restaurant features with their map positions. | ||
| private func makeFeatures(intersecting geometry: Geometry) async throws -> [OrderedFeature] { | ||
| // Add a buffer to catch features near the rectangle edges. | ||
| // This accounts for projection distortions and rendering tolerances. | ||
| let queryGeometry = GeometryEngine.buffer(around: geometry, distance: Self.selectionBufferDistance) ?? geometry | ||
|
|
||
| let queryParameters = QueryParameters() | ||
| queryParameters.geometry = queryGeometry | ||
| queryParameters.spatialRelationship = .intersects | ||
| queryParameters.maxFeatures = 1000 | ||
|
|
||
| let queryResult = try await restaurantsTable.queryFeatures(using: queryParameters) | ||
| let allFeatures = Array(queryResult.features()) | ||
|
|
||
| return allFeatures | ||
| .compactMap { feature -> OrderedFeature? in | ||
| guard let anchor = feature.geometry as? Point else { return nil } | ||
| return OrderedFeature(feature: feature, anchor: anchor) | ||
| } | ||
| } | ||
|
|
||
| /// Returns whether a map point is earlier in north-to-south, west-to-east order. | ||
| private static func isEarlierInGeographicReadingOrder(_ lhs: Point, than rhs: Point) -> Bool { | ||
| if lhs.y != rhs.y { return lhs.y > rhs.y } | ||
| return lhs.x < rhs.x | ||
| } | ||
|
|
||
| /// Adds numbered text labels for the first restaurant features. | ||
| /// - Parameter orderedFeatures: The ordered restaurant features to label. | ||
| private func addNumberedLabels(for orderedFeatures: [OrderedFeature]) { | ||
| let numberedOrderedFeatures = orderedFeatures.prefix(Self.maximumNumberedFeatures) | ||
|
|
||
| for (offset, orderedFeature) in numberedOrderedFeatures.enumerated() { | ||
| let number = offset + 1 | ||
| let text = name(for: orderedFeature.feature, fallback: nil).map { "\(number): \($0)" } ?? "\(number)" | ||
| let labelSymbol = TextSymbol( | ||
| text: text, | ||
| color: Self.labelTextColor, | ||
| size: 15, | ||
| horizontalAlignment: .center, | ||
| verticalAlignment: .top | ||
| ) | ||
| labelSymbol.haloColor = .white | ||
| labelSymbol.haloWidth = 2 | ||
| labelSymbol.offsetY = -14 | ||
| labelOverlay.addGraphic(Graphic(geometry: orderedFeature.anchor, symbol: labelSymbol)) | ||
| numberedFeatures.append(orderedFeature.feature) | ||
| } | ||
| } | ||
|
|
||
| /// The restaurant marker symbol. | ||
| private static var restaurantSymbol: SimpleMarkerSymbol { | ||
| let symbol = SimpleMarkerSymbol(style: .circle, color: markerFillColor, size: 12) | ||
| symbol.outline = SimpleLineSymbol(style: .solid, color: .white, width: 1.5) | ||
| return symbol | ||
| } | ||
|
|
||
| /// A feature and its map location. | ||
| private struct OrderedFeature { | ||
| let feature: Feature | ||
| let anchor: Point | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private extension URL { | ||
| /// The URL of the Redlands restaurants feature service. | ||
| static var redlandsRestaurants: URL { | ||
| URL(string: "https://services2.arcgis.com/ZQgQTuoyBrtmoGdP/arcgis/rest/services/redlands_food/FeatureServer/0")! | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.