Skip to content

Commit 51bd3f9

Browse files
Merge pull request #86 from TaskarCenterAtUW/feature/manage-changesets
Work on Changesets in API transmission
2 parents 8c9201b + 81d8ad2 commit 51bd3f9

File tree

4 files changed

+228
-51
lines changed

4 files changed

+228
-51
lines changed

IOSAccessAssessment.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
A3C22FD32CF194A600533BF7 /* CGImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C22FD22CF194A200533BF7 /* CGImageUtils.swift */; };
2727
A3C22FD52CF1A97700533BF7 /* SegmentationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C22FD42CF1A97300533BF7 /* SegmentationModel.swift */; };
2828
A3C22FD82CF2F0C300533BF7 /* DequeModule in Frameworks */ = {isa = PBXBuildFile; productRef = A3C22FD72CF2F0C300533BF7 /* DequeModule */; };
29+
CA924A932CEB9AB000FCA928 /* ChangesetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA924A922CEB9AB000FCA928 /* ChangesetService.swift */; };
2930
CAA947762CDE6FBD000C6918 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA947752CDE6FBB000C6918 /* LoginView.swift */; };
3031
CAA947792CDE700A000C6918 /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA947782CDE7007000C6918 /* AuthService.swift */; };
3132
CAA9477B2CDE70D9000C6918 /* KeychainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9477A2CDE70D5000C6918 /* KeychainService.swift */; };
@@ -84,6 +85,7 @@
8485
A3C22FD02CF1643E00533BF7 /* SharedImageData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedImageData.swift; sourceTree = "<group>"; };
8586
A3C22FD22CF194A200533BF7 /* CGImageUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGImageUtils.swift; sourceTree = "<group>"; };
8687
A3C22FD42CF1A97300533BF7 /* SegmentationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentationModel.swift; sourceTree = "<group>"; };
88+
CA924A922CEB9AB000FCA928 /* ChangesetService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangesetService.swift; sourceTree = "<group>"; };
8789
CAA947752CDE6FBB000C6918 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
8890
CAA947782CDE7007000C6918 /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = "<group>"; };
8991
CAA9477A2CDE70D5000C6918 /* KeychainService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainService.swift; sourceTree = "<group>"; };
@@ -237,6 +239,7 @@
237239
CAF812BB2CF78F7C00D44B84 /* NetworkError.swift */,
238240
CAA9477A2CDE70D5000C6918 /* KeychainService.swift */,
239241
CAA947782CDE7007000C6918 /* AuthService.swift */,
242+
CA924A922CEB9AB000FCA928 /* ChangesetService.swift */,
240243
);
241244
path = Services;
242245
sourceTree = "<group>";
@@ -430,6 +433,7 @@
430433
CAF812BC2CF78F8100D44B84 /* NetworkError.swift in Sources */,
431434
DAA7F8BD2CA67A5A003666D8 /* CameraViewController.swift in Sources */,
432435
55659C082BB785CB0094DF01 /* CameraController.swift in Sources */,
436+
CA924A932CEB9AB000FCA928 /* ChangesetService.swift in Sources */,
433437
DAA7F8C82CA76527003666D8 /* CIImageUtils.swift in Sources */,
434438
55659C142BB786700094DF01 /* AnnotationView.swift in Sources */,
435439
CAA947792CDE700A000C6918 /* AuthService.swift in Sources */,
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//
2+
// ChangesetService.swift
3+
// IOSAccessAssessment
4+
//
5+
// Created by Mariana Piz on 11.11.2024.
6+
//
7+
8+
import Foundation
9+
10+
struct NodeData {
11+
let latitude: Double
12+
let longitude: Double
13+
}
14+
15+
class ChangesetService {
16+
17+
private enum Constants {
18+
static let baseUrl = "https://osm.workspaces-stage.sidewalks.washington.edu/api/0.6"
19+
static let workspaceId = "168"
20+
}
21+
22+
static let shared = ChangesetService()
23+
private init() {}
24+
25+
private let accessToken = KeychainService().getValue(for: .accessToken)
26+
private(set) var changesetId: String?
27+
28+
func openChangeset(completion: @escaping (Result<String, Error>) -> Void) {
29+
guard let url = URL(string: "\(Constants.baseUrl)/changeset/create"),
30+
let accessToken
31+
else { return }
32+
33+
var request = URLRequest(url: url)
34+
request.httpMethod = "PUT"
35+
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
36+
request.setValue(Constants.workspaceId, forHTTPHeaderField: "X-Workspace")
37+
request.setValue("application/xml", forHTTPHeaderField: "Content-Type")
38+
39+
guard let xmlData = """
40+
<osm>
41+
<changeset>
42+
<tag k="created_by" v="GIG 1.0(4)" />
43+
<tag k="comment" v="iOS OSM client" />
44+
</changeset>
45+
</osm>
46+
"""
47+
.data(using: .utf8)
48+
else {
49+
print("Failed to create XML data.")
50+
return
51+
}
52+
53+
request.httpBody = xmlData
54+
55+
URLSession.shared.dataTask(with: request) { data, response, error in
56+
if let error = error {
57+
completion(.failure(error))
58+
return
59+
}
60+
61+
if let data = data, let changesetId = String(data: data, encoding: .utf8) {
62+
self.changesetId = changesetId
63+
completion(.success(changesetId))
64+
} else {
65+
completion(.failure(NSError(domain: "ChangesetError",
66+
code: -1,
67+
userInfo: [NSLocalizedDescriptionKey: "Failed to open changeset"])))
68+
}
69+
}.resume()
70+
}
71+
72+
func uploadChanges(nodeData: NodeData, completion: @escaping (Result<Void, Error>) -> Void) {
73+
guard let changesetId,
74+
let accessToken,
75+
let url = URL(string: "\(Constants.baseUrl)/changeset/\(changesetId)/upload")
76+
else { return }
77+
78+
let xmlContent =
79+
"""
80+
<osmChange version="0.6" generator="GIG Change generator">
81+
<create>
82+
<node id="-1" lat="\(nodeData.latitude)" lon="\(nodeData.longitude)" changeset="\(changesetId)" />
83+
</create>
84+
</osmChange>
85+
"""
86+
87+
var request = URLRequest(url: url)
88+
request.httpMethod = "POST"
89+
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
90+
request.setValue(Constants.workspaceId, forHTTPHeaderField: "X-Workspace")
91+
request.setValue("application/xml", forHTTPHeaderField: "Content-Type")
92+
request.httpBody = xmlContent.data(using: .utf8)
93+
94+
URLSession.shared.dataTask(with: request) { data, response, error in
95+
if let error = error {
96+
completion(.failure(error))
97+
return
98+
}
99+
completion(.success(()))
100+
}.resume()
101+
}
102+
103+
func closeChangeset(completion: @escaping (Result<Void, Error>) -> Void) {
104+
guard let changesetId, let accessToken else { return }
105+
106+
guard let url = URL(string: "\(Constants.baseUrl)/changeset/\(changesetId)/close") else { return }
107+
108+
var request = URLRequest(url: url)
109+
request.httpMethod = "PUT"
110+
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
111+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
112+
113+
URLSession.shared.dataTask(with: request) { data, response, error in
114+
if let error = error {
115+
completion(.failure(error))
116+
return
117+
}
118+
self.changesetId = nil
119+
120+
completion(.success(()))
121+
}.resume()
122+
}
123+
124+
}

IOSAccessAssessment/Views/AnnotationView.swift

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,16 @@ struct AnnotationView: View {
9494

9595
Button(action: {
9696
objectLocation.calcLocation(sharedImageData: sharedImageData, index: index)
97-
self.nextSegment()
9897
selectedOption = nil
98+
uploadChanges()
99+
100+
if index == selection.count - 1 {
101+
closeChangeset()
102+
} else {
103+
nextSegment()
104+
}
99105
}) {
100-
Text("Next")
106+
Text(index == selection.count - 1 ? "Finish" : "Next")
101107
}
102108
.padding()
103109
}
@@ -157,6 +163,35 @@ struct AnnotationView: View {
157163
func calculateProgress() -> Float {
158164
return Float(self.index) / Float(self.sharedImageData.segmentedIndices.count)
159165
}
166+
167+
private func uploadChanges() {
168+
guard let nodeLatitude = objectLocation.latitude,
169+
let nodeLongitude = objectLocation.longitude
170+
else { return }
171+
172+
let nodeData = NodeData(latitude: nodeLatitude, longitude: nodeLongitude)
173+
174+
ChangesetService.shared.uploadChanges(nodeData: nodeData) { result in
175+
switch result {
176+
case .success:
177+
print("Changes uploaded successfully.")
178+
case .failure(let error):
179+
print("Failed to upload changes: \(error.localizedDescription)")
180+
}
181+
}
182+
}
183+
184+
private func closeChangeset() {
185+
ChangesetService.shared.closeChangeset { result in
186+
switch result {
187+
case .success:
188+
print("Changeset closed successfully.")
189+
case .failure(let error):
190+
print("Failed to close changeset: \(error.localizedDescription)")
191+
}
192+
}
193+
}
194+
160195
}
161196

162197
struct ClassSelectionView: View {

IOSAccessAssessment/Views/ContentView.swift

Lines changed: 63 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -29,61 +29,64 @@ struct ContentView: View {
2929
var objectLocation = ObjectLocation()
3030

3131
var body: some View {
32-
VStack {
33-
if manager?.dataAvailable ?? false{
34-
ZStack {
35-
HostedCameraViewController(session: manager!.controller.captureSession,
36-
frameRect: VerticalFrame.getColumnFrame(
37-
width: UIScreen.main.bounds.width,
38-
height: UIScreen.main.bounds.height,
39-
row: 0)
40-
)
41-
HostedSegmentationViewController(segmentationImage: $segmentationModel.maskedSegmentationResults,
42-
frameRect: VerticalFrame.getColumnFrame(
43-
width: UIScreen.main.bounds.width,
44-
height: UIScreen.main.bounds.height,
45-
row: 1)
46-
)
47-
}
48-
Button {
49-
segmentationModel.performPerClassSegmentationRequest(with: sharedImageData.cameraImage!)
50-
objectLocation.setLocationAndHeading()
51-
manager?.stopStream()
52-
} label: {
53-
Image(systemName: "camera.circle.fill")
54-
.resizable()
55-
.frame(width: 60, height: 60)
56-
.foregroundColor(.white)
57-
}
32+
VStack {
33+
if manager?.dataAvailable ?? false{
34+
ZStack {
35+
HostedCameraViewController(session: manager!.controller.captureSession,
36+
frameRect: VerticalFrame.getColumnFrame(
37+
width: UIScreen.main.bounds.width,
38+
height: UIScreen.main.bounds.height,
39+
row: 0)
40+
)
41+
HostedSegmentationViewController(segmentationImage: $segmentationModel.maskedSegmentationResults,
42+
frameRect: VerticalFrame.getColumnFrame(
43+
width: UIScreen.main.bounds.width,
44+
height: UIScreen.main.bounds.height,
45+
row: 1)
46+
)
5847
}
59-
else {
60-
VStack {
61-
SpinnerView()
62-
Text("Camera settings in progress")
63-
.padding(.top, 20)
64-
}
48+
Button {
49+
segmentationModel.performPerClassSegmentationRequest(with: sharedImageData.cameraImage!)
50+
objectLocation.setLocationAndHeading()
51+
manager?.stopStream()
52+
} label: {
53+
Image(systemName: "camera.circle.fill")
54+
.resizable()
55+
.frame(width: 60, height: 60)
56+
.foregroundColor(.white)
6557
}
6658
}
67-
.navigationDestination(isPresented: $navigateToAnnotationView) {
68-
AnnotationView(objectLocation: objectLocation,
69-
classes: Constants.ClassConstants.classes,
70-
selection: selection
71-
)
72-
}
73-
.navigationBarTitle("Camera View", displayMode: .inline)
74-
.onAppear {
75-
if (manager == nil) {
76-
segmentationModel.updateSegmentationRequest(selection: selection, completion: updateSharedImageSegmentation)
77-
segmentationModel.updatePerClassSegmentationRequest(selection: selection,
78-
completion: updatePerClassImageSegmentation)
79-
manager = CameraManager(sharedImageData: sharedImageData, segmentationModel: segmentationModel)
80-
} else {
81-
manager?.resumeStream()
59+
else {
60+
VStack {
61+
SpinnerView()
62+
Text("Camera settings in progress")
63+
.padding(.top, 20)
8264
}
8365
}
84-
.onDisappear {
85-
manager?.stopStream()
66+
}
67+
.navigationDestination(isPresented: $navigateToAnnotationView) {
68+
AnnotationView(
69+
objectLocation: objectLocation,
70+
classes: Constants.ClassConstants.classes,
71+
selection: selection
72+
)
73+
}
74+
.navigationBarTitle("Camera View", displayMode: .inline)
75+
.onAppear {
76+
openChangeset()
77+
78+
if (manager == nil) {
79+
segmentationModel.updateSegmentationRequest(selection: selection, completion: updateSharedImageSegmentation)
80+
segmentationModel.updatePerClassSegmentationRequest(selection: selection,
81+
completion: updatePerClassImageSegmentation)
82+
manager = CameraManager(sharedImageData: sharedImageData, segmentationModel: segmentationModel)
83+
} else {
84+
manager?.resumeStream()
8685
}
86+
}
87+
.onDisappear {
88+
manager?.stopStream()
89+
}
8790
}
8891

8992
// Callbacks to the SegmentationModel
@@ -108,4 +111,15 @@ struct ContentView: View {
108111
fatalError("Unable to process per-class segmentation \(error.localizedDescription)")
109112
}
110113
}
114+
115+
private func openChangeset() {
116+
ChangesetService.shared.openChangeset { result in
117+
switch result {
118+
case .success(let changesetId):
119+
print("Opened changeset with ID: \(changesetId)")
120+
case .failure(let error):
121+
print("Failed to open changeset: \(error.localizedDescription)")
122+
}
123+
}
124+
}
111125
}

0 commit comments

Comments
 (0)