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
11 changes: 5 additions & 6 deletions .github/workflows/ios-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@
# UI_MAX_PARALLEL - simulators run at the same time (1 = serial)
#
# Optional repo secrets (Settings > Secrets and variables > Actions):
# KARTAVIEW_ACCESS_TOKEN, JAWG_ACCESS_TOKEN -> written to Secrets.xcconfig if it
# is missing on the runner; the build tolerates their absence (#include?).
# JAWG_ACCESS_TOKEN -> written to Secrets.xcconfig if it is missing on the
# runner; the build tolerates its absence (#include?). Photo-upload tests use a
# mock Panoramax server and need no token.

name: iOS Tests

Expand Down Expand Up @@ -118,16 +119,14 @@ jobs:

- name: Write Secrets.xcconfig (only if missing)
env:
KARTAVIEW_ACCESS_TOKEN: ${{ secrets.KARTAVIEW_ACCESS_TOKEN }}
JAWG_ACCESS_TOKEN: ${{ secrets.JAWG_ACCESS_TOKEN }}
run: |
set -euo pipefail
dest="GoInfoGame/GoInfoGame/Resources/Secrets.xcconfig"
if [ -f "$dest" ]; then
echo "$dest already present - leaving it untouched."
elif [ -n "${KARTAVIEW_ACCESS_TOKEN:-}" ] || [ -n "${JAWG_ACCESS_TOKEN:-}" ]; then
printf 'KARTAVIEW_ACCESS_TOKEN = %s\nJAWG_ACCESS_TOKEN = %s\n' \
"${KARTAVIEW_ACCESS_TOKEN:-}" "${JAWG_ACCESS_TOKEN:-}" > "$dest"
elif [ -n "${JAWG_ACCESS_TOKEN:-}" ]; then
printf 'JAWG_ACCESS_TOKEN = %s\n' "${JAWG_ACCESS_TOKEN:-}" > "$dest"
echo "Wrote $dest from repository secrets."
else
echo "No $dest and no token secrets - Config.xcconfig #include? tolerates this."
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,5 @@ GoInfoGame/.DS_Store

# Secrets (never commit)
GoInfoGame/GoInfoGame/Resources/Secrets.xcconfig
pointnmap-camera-teardown.patch
scripts/panoramax_demo.sh
88 changes: 14 additions & 74 deletions GoInfoGame/GoInfoGame.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

19 changes: 17 additions & 2 deletions GoInfoGame/GoInfoGame/DataBase/DBModels/StoredChangeset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,17 @@ class StoredChangeset: Object {
/// this to delete the element on undo instead of restoring `originalTags`.
@Persisted var isCreatedElement: Bool = false

/// A captured-but-not-yet-uploaded KartaView photo tied to this answer, saved to
/// A captured-but-not-yet-uploaded photo tied to this answer, saved to
/// disk by `QuestImageStorage`. Non-nil until `QuestSubmissionManager` uploads it
/// and writes the resulting URL into `tags[pendingImageTagKey]`, clearing both.
@Persisted var pendingImagePath: String?
/// Which tag key the uploaded photo's URL should be written to (e.g.
/// "ext:kartaview_url"). Only meaningful while `pendingImagePath` is non-nil.
/// "ext:street_images"). Only meaningful while `pendingImagePath` is non-nil.
@Persisted var pendingImageTagKey: String?
/// Title of the question the pending photo was taken for, sent with the upload.
@Persisted var pendingImageQuestion: String?
/// `quest_id` of that question.
@Persisted var pendingImageQuestId: Int?
@Persisted var retryCount: Int = 0
@Persisted var lastError: String?

Expand Down Expand Up @@ -120,8 +124,19 @@ struct StoredChangesetSnapshot {
let nodes: [Int64]
let pendingImagePath: String?
let pendingImageTagKey: String?
var pendingImageQuestion: String? = nil
var pendingImageQuestId: Int? = nil
let retryCount: Int

/// OSM-style id ("way/123", "node/456") used to refer to this element from a photo.
var osmElementId: String? {
switch elementType {
case .way: return "way/\(elementId)"
case .node: return "node/\(elementId)"
case .unknown: return nil
}
}

func asOSMNode(tags overrideTags: [String: String]) -> OSMNode {
var storage = originalTags
for (key, value) in overrideTags { storage[key] = value }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import RealmSwift
import CoreLocation

/// A feature (created via Add Feature, with any captured photos already saved to
/// disk) still waiting to be uploaded to Kartaview and created as an OSM node.
/// disk) still waiting to be uploaded to Panoramax and created as an OSM node.
/// Presence of a row here means the feature is pending — the row is deleted once
/// creation succeeds, which is what lets a feature captured offline (or interrupted
/// mid-upload) survive an app kill and be retried later from the sync button or on
Expand All @@ -24,6 +24,10 @@ class StoredFeatureDraft: Object {
@Persisted var createdAt: Date = Date()
@Persisted var lastError: String?
@Persisted var retryCount: Int = 0
/// Panoramax URL of each photo already uploaded, keyed by its index in `imagePaths`
/// ("0", "1", …; Realm map keys can't hold the "." of a file path), so a retry only
/// uploads the photos that haven't gone up yet.
@Persisted var uploadedPhotoURLs = Map<String, String>()
}

/// Plain-value mirror of a StoredFeatureDraft. Realm objects are confined to the
Expand All @@ -36,4 +40,8 @@ struct StoredFeatureDraftSnapshot {
let tags: [String: String]
let imagePaths: [String]
let coordinates: CLLocationCoordinate2D
/// When the draft was saved, i.e. when its photos were taken.
var createdAt: Date = Date()
/// Photos already on Panoramax, keyed by their index in `imagePaths`.
var uploadedPhotoURLs: [Int: String] = [:]
}
10 changes: 9 additions & 1 deletion GoInfoGame/GoInfoGame/DataBase/DBModels/StoredNoteDraft.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import RealmSwift
import CoreLocation

/// A note (with any captured photos already saved to disk) still waiting to be
/// uploaded to Kartaview and submitted to the notes API. Presence of a row here
/// uploaded to Panoramax and submitted to the notes API. Presence of a row here
/// means the note is pending — the row is deleted once submission succeeds, which
/// is what lets a note captured offline (or interrupted mid-upload) survive an app
/// kill and be retried later from the sync button or on next app launch.
Expand All @@ -21,6 +21,10 @@ class StoredNoteDraft: Object {
@Persisted var createdAt: Date = Date()
@Persisted var lastError: String?
@Persisted var retryCount: Int = 0
/// Panoramax URL of each photo already uploaded, keyed by its index in `imagePaths`
/// ("0", "1", …; Realm map keys can't hold the "." of a file path), so a retry only
/// uploads the photos that haven't gone up yet.
@Persisted var uploadedPhotoURLs = Map<String, String>()
}

/// Plain-value mirror of a StoredNoteDraft. Realm objects are confined to the thread/
Expand All @@ -31,4 +35,8 @@ struct StoredNoteDraftSnapshot {
let noteText: String
let imagePaths: [String]
let coordinates: CLLocationCoordinate2D
/// When the draft was saved, i.e. when its photos were taken.
var createdAt: Date = Date()
/// Photos already on Panoramax, keyed by their index in `imagePaths`.
var uploadedPhotoURLs: [Int: String] = [:]
}
70 changes: 62 additions & 8 deletions GoInfoGame/GoInfoGame/DataBase/DatabaseConnector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ class DatabaseConnector {
- parameter tags [String:String] tags changed with this
- Returns: An instance of `StoredChangeset`
*/
func createChangeset(id:Int, questType: String, type: StoredElementEnum, originalTags:[String:String], tags:[String:String], version: Int, iconName: String, point: CLLocationCoordinate2D? = nil, nodes: List<Int64>? = nil, pendingImagePath: String? = nil, pendingImageTagKey: String? = nil) -> StoredChangeset? {
func createChangeset(id:Int, questType: String, type: StoredElementEnum, originalTags:[String:String], tags:[String:String], version: Int, iconName: String, point: CLLocationCoordinate2D? = nil, nodes: List<Int64>? = nil, pendingImagePath: String? = nil, pendingImageTagKey: String? = nil, pendingImageQuestion: String? = nil, pendingImageQuestId: Int? = nil) -> StoredChangeset? {
let realm = try! Realm(configuration: RealmConfig.configuration)
let storedChangeset = StoredChangeset()
storedChangeset.elementId = id
Expand All @@ -400,6 +400,8 @@ class DatabaseConnector {
}
storedChangeset.pendingImagePath = pendingImagePath
storedChangeset.pendingImageTagKey = pendingImageTagKey
storedChangeset.pendingImageQuestion = pendingImageQuestion
storedChangeset.pendingImageQuestId = pendingImageQuestId

storedChangeset.timestamp = String(Date().timeIntervalSince1970)
for tag in tags {
Expand Down Expand Up @@ -524,10 +526,14 @@ class DatabaseConnector {
draft.latitude = coordinates.latitude
draft.longitude = coordinates.longitude
draft.createdAt = Date()
var saved = draft
try! realm.write {
realm.add(draft, update: .modified)
saved = realm.create(StoredNoteDraft.self, value: draft, update: .modified)
// A resubmit brings new photos, so URLs uploaded for the old ones no longer
// apply. `.modified` merges maps rather than replacing them, hence the clear.
saved.uploadedPhotoURLs.removeAll()
}
return draft
return saved
}

/// All notes still waiting to be uploaded/submitted, oldest first.
Expand All @@ -540,7 +546,9 @@ class DatabaseConnector {
id: $0.id,
noteText: $0.noteText,
imagePaths: Array($0.imagePaths),
coordinates: CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude)
coordinates: CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude),
createdAt: $0.createdAt,
uploadedPhotoURLs: Self.photoURLsByIndex($0.uploadedPhotoURLs)
)
}
}
Expand All @@ -564,6 +572,15 @@ class DatabaseConnector {
}
}

/// Records that one of the draft's photos is on Panoramax, so a retry skips it.
func attachNoteDraftPhotoURL(id: String, photoIndex: Int, url: String) {
let realm = try! Realm(configuration: RealmConfig.configuration)
guard let draft = realm.object(ofType: StoredNoteDraft.self, forPrimaryKey: id) else { return }
try! realm.write {
draft.uploadedPhotoURLs[String(photoIndex)] = url
}
}

func markNoteDraftFailed(id: String, error: String) {
let realm = try! Realm(configuration: RealmConfig.configuration)
guard let draft = realm.object(ofType: StoredNoteDraft.self, forPrimaryKey: id) else { return }
Expand All @@ -573,6 +590,15 @@ class DatabaseConnector {
}
}

/// A draft's stored "0", "1", … photo URL keys as photo indexes.
private static func photoURLsByIndex(_ map: Map<String, String>) -> [Int: String] {
var urls: [Int: String] = [:]
for (key, url) in map.toDictionary() {
if let index = Int(key) { urls[index] = url }
}
return urls
}

// MARK: - Feature drafts (offline queue for Add Feature)

/// Creates the pending feature draft, or if `id` already exists (a retry of a
Expand All @@ -591,10 +617,14 @@ class DatabaseConnector {
draft.latitude = coordinates.latitude
draft.longitude = coordinates.longitude
draft.createdAt = Date()
var saved = draft
try! realm.write {
realm.add(draft, update: .modified)
saved = realm.create(StoredFeatureDraft.self, value: draft, update: .modified)
// A resubmit brings new photos, so URLs uploaded for the old ones no longer
// apply. `.modified` merges maps rather than replacing them, hence the clear.
saved.uploadedPhotoURLs.removeAll()
}
return draft
return saved
}

/// All features still waiting to be uploaded/created, oldest first.
Expand All @@ -609,7 +639,9 @@ class DatabaseConnector {
iconName: $0.iconName,
tags: $0.tags.toDictionary(),
imagePaths: Array($0.imagePaths),
coordinates: CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude)
coordinates: CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude),
createdAt: $0.createdAt,
uploadedPhotoURLs: Self.photoURLsByIndex($0.uploadedPhotoURLs)
)
}
}
Expand All @@ -633,6 +665,15 @@ class DatabaseConnector {
}
}

/// Records that one of the draft's photos is on Panoramax, so a retry skips it.
func attachFeatureDraftPhotoURL(id: String, photoIndex: Int, url: String) {
let realm = try! Realm(configuration: RealmConfig.configuration)
guard let draft = realm.object(ofType: StoredFeatureDraft.self, forPrimaryKey: id) else { return }
try! realm.write {
draft.uploadedPhotoURLs[String(photoIndex)] = url
}
}

func markFeatureDraftFailed(id: String, error: String) {
let realm = try! Realm(configuration: RealmConfig.configuration)
guard let draft = realm.object(ofType: StoredFeatureDraft.self, forPrimaryKey: id) else { return }
Expand Down Expand Up @@ -667,6 +708,8 @@ class DatabaseConnector {
nodes: Array($0.nodes),
pendingImagePath: $0.pendingImagePath,
pendingImageTagKey: $0.pendingImageTagKey,
pendingImageQuestion: $0.pendingImageQuestion,
pendingImageQuestId: $0.pendingImageQuestId,
retryCount: $0.retryCount
)
}
Expand All @@ -683,6 +726,8 @@ class DatabaseConnector {
changeset.tags.setValue(url, forKey: tagKey)
changeset.pendingImagePath = nil
changeset.pendingImageTagKey = nil
changeset.pendingImageQuestion = nil
changeset.pendingImageQuestId = nil
}
}

Expand Down Expand Up @@ -720,6 +765,8 @@ class DatabaseConnector {
nodes: Array(changeset.nodes),
pendingImagePath: changeset.pendingImagePath,
pendingImageTagKey: changeset.pendingImageTagKey,
pendingImageQuestion: changeset.pendingImageQuestion,
pendingImageQuestId: changeset.pendingImageQuestId,
retryCount: changeset.retryCount
)
}
Expand All @@ -734,6 +781,8 @@ class DatabaseConnector {
try! realm.write {
changeset.pendingImagePath = nil
changeset.pendingImageTagKey = nil
changeset.pendingImageQuestion = nil
changeset.pendingImageQuestId = nil
}
return oldPath
}
Expand All @@ -746,7 +795,12 @@ struct RealmConfig {
// or have defaults, so no migration block content is needed for existing rows —
// Realm fills them in automatically — but the version itself must move, or a
// pre-existing installed schema-version-3 database won't match this class anymore.
static let configuration = Realm.Configuration(schemaVersion: 4) { migration, oldSchemaVersion in
// Bumped 4 -> 5 -> 6 for StoredChangeset.pendingImageQuestion/pendingImageQuestId
// (optional, no migration
// content needed).
// Bumped 6 -> 7 for StoredNoteDraft/StoredFeatureDraft.uploadedPhotoURLs (an empty
// map by default, no migration content needed).
static let configuration = Realm.Configuration(schemaVersion: 7) { migration, oldSchemaVersion in
if oldSchemaVersion < 1 {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
Expand Down
5 changes: 3 additions & 2 deletions GoInfoGame/GoInfoGame/FeatureImageStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ final class FeatureImageStorage {

@discardableResult
func save(_ image: UIImage) throws -> String {
guard let data = image.jpegData(compressionQuality: 0.9) else {
// Keeps the photo's capture heading (if any) in the file's EXIF for the upload.
guard let data = PhotoHeading.jpegData(for: image, compressionQuality: 0.9) else {
throw NSError(domain: "FeatureImageStorage", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to encode photo"])
}
let fileURL = directoryURL.appendingPathComponent("\(UUID().uuidString).jpg")
Expand All @@ -34,7 +35,7 @@ final class FeatureImageStorage {
}

func load(path: String) -> UIImage? {
UIImage(contentsOfFile: path)
PhotoHeading.loadImage(atPath: path)
}

func delete(paths: [String]) {
Expand Down
14 changes: 12 additions & 2 deletions GoInfoGame/GoInfoGame/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,18 @@
</array>
<key>APP_FORCE_UPDATE_URL</key>
<string>$(APP_FORCE_UPDATE_URL)</string>
<key>KARTAVIEW_ACCESS_TOKEN</key>
<string>$(KARTAVIEW_ACCESS_TOKEN)</string>
<key>PANORAMAX_BASE_URL_DEV</key>
<string>$(PANORAMAX_BASE_URL_DEV)</string>
<key>PANORAMAX_BASE_URL_STAGE</key>
<string>$(PANORAMAX_BASE_URL_STAGE)</string>
<key>PANORAMAX_BASE_URL_PROD</key>
<string>$(PANORAMAX_BASE_URL_PROD)</string>
<key>PANORAMAX_TOKEN_DEV</key>
<string>$(PANORAMAX_TOKEN_DEV)</string>
<key>PANORAMAX_TOKEN_STAGE</key>
<string>$(PANORAMAX_TOKEN_STAGE)</string>
<key>PANORAMAX_TOKEN_PROD</key>
<string>$(PANORAMAX_TOKEN_PROD)</string>
<key>JAWG_ACCESS_TOKEN</key>
<string>$(JAWG_ACCESS_TOKEN)</string>
<key>AppStpre_URL</key>
Expand Down
36 changes: 0 additions & 36 deletions GoInfoGame/GoInfoGame/Kartaview/Model/FinishUploadingModel.swift

This file was deleted.

Loading
Loading