Add custom dynamic entity data source

View on GitHub

Create a custom dynamic entity data source and display it using a dynamic entity layer.

Image of the add custom dynamic entity data source sample

Use case

Developers can create a CustomDynamicEntityDataSource to be able to visualize data from a variety of different feeds as dynamic entities using a DynamicEntityLayer. An example of this is in a mobile situational awareness app, where CustomDynamicEntityDataSource can be used to connect to peer-to-peer feeds in order to visualize real-time location tracks from teammates in the field.

How to use the sample

Run the sample to view the map and the dynamic entity layer displaying the latest observation from the custom data source. Tap on a dynamic entity to view its attributes in a callout.

How it works

  1. Create the metadata for the data source using DynamicEntityDataSourceInfo for a given unique entity ID field and an array of Field objects matching the fields in the data source.
  2. Create your custom feed type that conforms to CustomDynamicEntityFeed which will implement the data feed that will asynchronously emit CustomDynamicEntityFeedEvent.
  3. The feed should loop through the observations JSON and deserialize each observation into a Point object and a Dictionary<String, Any> containing the attributes.
  4. Use CustomDynamicEntityFeedEvent.newObservation(geometry:attributes:) to add each event to the feed.
  5. Create a custom data source using CustomDynamicEntityDataSource.

Relevant API

  • CustomDynamicEntityDataSource
  • CustomDynamicEntityFeed
  • CustomDynamicEntityFeedEvent
  • DynamicEntity
  • DynamicEntityDataSource
  • DynamicEntityLayer
  • LabelDefinition
  • TrackDisplayProperties

About the data

This sample uses a JSON Lines file containing observations of marine vessels in the Pacific North West hosted on ArcGIS Online.

Tags

data, dynamic, entity, label, labeling, live, real-time, stream, track

Sample Code

AddCustomDynamicEntityDataSourceView.swiftAddCustomDynamicEntityDataSourceView.swiftAddCustomDynamicEntityDataSourceView.Vessel.swift
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// Copyright 2023 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 SwiftUI

struct AddCustomDynamicEntityDataSourceView: View {
    /// A map with an ArcGIS oceans basemap style and a dynamic entity layer.
    @State private var map = makeMap()

    /// The dynamic entity layer that is displaying our custom data.
    private var dynamicEntityLayer: DynamicEntityLayer {
        map.operationalLayers.first as! DynamicEntityLayer
    }

    /// The point on the screen the user tapped.
    @State private var tappedScreenPoint: CGPoint?

    /// The placement of the callout.
    @State private var calloutPlacement: CalloutPlacement?

    var body: some View {
        MapViewReader { proxy in
            MapView(map: map)
                .onSingleTapGesture { screenPoint, _ in
                    tappedScreenPoint = screenPoint
                }
                .callout(placement: $calloutPlacement.animation(.default.speed(2))) { placement in
                    let attributes = (placement.geoElement as! DynamicEntityObservation).attributes
                    VStack(alignment: .leading) {
                        // Display all the attributes in the callout.
                        ForEach(attributes.sorted(by: { $0.key < $1.key }), id: \.key) { item in
                            Text("\(item.key): \(String(describing: item.value))")
                        }
                    }
                }
                .task(id: tappedScreenPoint) {
                    let newCalloutPlacement: CalloutPlacement?
                    if let tappedScreenPoint,
                       let identifyResult = try? await proxy.identify(
                        on: dynamicEntityLayer,
                        screenPoint: tappedScreenPoint,
                        tolerance: 2
                       ) {
                        // Set the callout placement to the observation that was tapped on.
                        newCalloutPlacement = identifyResult.geoElements.first.map { .geoElement($0) }
                    } else {
                        // Hides the callout.
                        newCalloutPlacement = nil
                    }
                    calloutPlacement = newCalloutPlacement
                }
        }
    }

    /// Makes a map with a dynamic entity layer.
    private static func makeMap() -> Map {
        let map = Map(basemapStyle: .arcGISOceans)
        map.initialViewpoint = Viewpoint(
            latitude: 47.984,
            longitude: -123.657,
            scale: 3e6
        )

        // The meta data for the custom dynamic entity data source.
        let info = DynamicEntityDataSourceInfo(
            entityIDFieldName: "MMSI",
            fields: [
                Field(type: .text, name: "MMSI", alias: "MMSI", length: 256),
                Field(type: .float64, name: "SOG", alias: "SOG", length: 8),
                Field(type: .float64, name: "COG", alias: "COG", length: 8),
                Field(type: .text, name: "VesselName", alias: "VesselName", length: 256),
                Field(type: .text, name: "CallSign", alias: "CallSign", length: 256)
            ]
        )

        info.spatialReference = .wgs84

        // Create our custom data source from our custom data feed.
        let customDataSource = CustomDynamicEntityDataSource(info: info) { VesselFeed() }

        let dynamicEntityLayer = DynamicEntityLayer(dataSource: customDataSource)

        // Set display tracking properties on the layer.
        let trackDisplayProperties = dynamicEntityLayer.trackDisplayProperties
        trackDisplayProperties.showsPreviousObservations = true
        trackDisplayProperties.showsTrackLine = true
        trackDisplayProperties.maximumObservations = 20

        // Create the label definition so we can show the vessel name on top of
        // each dynamic entity.
        let labelDefinition = LabelDefinition(
            labelExpression: SimpleLabelExpression(simpleExpression: "[VesselName]"),
            textSymbol: TextSymbol(color: .red, size: 12)
        )
        labelDefinition.placement = .pointAboveCenter

        dynamicEntityLayer.addLabelDefinition(labelDefinition)
        dynamicEntityLayer.labelsAreEnabled = true

        map.addOperationalLayer(dynamicEntityLayer)

        return map
    }
}

/// The vessel feed that is emitting custom dynamic entity events.
private struct VesselFeed: CustomDynamicEntityFeed {
    let events = URL.selectedVesselsDataSource.lines.map { line in
        // Delay observations to simulate live data.
        try await Task.sleep(nanoseconds: 10_000_000)

        let decoder = JSONDecoder()
        let vessel = try decoder.decode(
            AddCustomDynamicEntityDataSourceView.Vessel.self,
            from: line.data(using: .utf8)!
        )

        // The location of the vessel that was decoded from the JSON.
        let location = vessel.geometry

        // We successfully decoded the vessel JSON so we should
        // add that vessel as a new observation.
        return CustomDynamicEntityFeedEvent.newObservation(
            geometry: Point(x: location.x, y: location.y, spatialReference: .wgs84),
            attributes: vessel.attributes
        )
    }
}

private extension URL {
    /// The URL to the selected vessels JSON Lines data.
    static var selectedVesselsDataSource: URL {
        Bundle.main.url(
            forResource: "AIS_MarineCadastre_SelectedVessels_CustomDataSource",
            withExtension: "jsonl"
        )!
    }
}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.