diff options
Diffstat (limited to 'Map')
41 files changed, 1420 insertions, 548 deletions
diff --git a/Map/Assets.xcassets/AccentColor.colorset/Contents.json b/Map/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..c981db7 100644 --- a/Map/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Map/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,15 @@ { "colors" : [ { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.969", + "green" : "0.871", + "red" : "1.000" + } + }, "idiom" : "universal" } ], diff --git a/Map/Assets.xcassets/AppIcon.appiconset/Contents.json b/Map/Assets.xcassets/AppIcon.appiconset/Contents.json index 3f00db4..64dc11e 100644 --- a/Map/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Map/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,51 +1,61 @@ { "images" : [ { + "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { + "filename" : "icon_16x16@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { + "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { + "filename" : "icon_32x32@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { + "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { + "filename" : "icon_128x128@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { + "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { + "filename" : "icon_256x256@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { + "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { + "filename" : "icon_512x512@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128.png Binary files differnew file mode 100644 index 0000000..b4618d4 --- /dev/null +++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128.png diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png Binary files differnew file mode 100644 index 0000000..dfdf396 --- /dev/null +++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16.png Binary files differnew file mode 100644 index 0000000..fb22da2 --- /dev/null +++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16.png diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png Binary files differnew file mode 100644 index 0000000..a2ea184 --- /dev/null +++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256.png Binary files differnew file mode 100644 index 0000000..cbe1cfc --- /dev/null +++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256.png diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png Binary files differnew file mode 100644 index 0000000..64bc6b5 --- /dev/null +++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32.png Binary files differnew file mode 100644 index 0000000..a2ea184 --- /dev/null +++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32.png diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png Binary files differnew file mode 100644 index 0000000..5663b5e --- /dev/null +++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512.png Binary files differnew file mode 100644 index 0000000..64bc6b5 --- /dev/null +++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512.png diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png Binary files differnew file mode 100644 index 0000000..bfa4365 --- /dev/null +++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png diff --git a/Map/ContentView.swift b/Map/ContentView.swift deleted file mode 100644 index ae5b6cd..0000000 --- a/Map/ContentView.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// ContentView.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -import SwiftUI -import CoreData - -struct ContentView: View { - @Environment(\.managedObjectContext) private var viewContext - - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \Map.createdAt, ascending: true)], - animation: .default) - private var maps: FetchedResults<Map> - - var body: some View { - NavigationView { - List { - ForEach(maps) { map in - NavigationLink(destination: MapDetailView(map: map)) { - HStack { - Text(map.title ?? "Untitled Map") - Text(map.createdAt!.format()) - .font(.footnote) - .foregroundColor(Color.accentColor) - } - } - } - .onDelete(perform: deleteMaps) - } - .toolbar { - HStack { - Button(action: toggleSidebar) { - Label("Toggle Sidebar", systemImage: "sidebar.left") - } - Button(action: addMap) { - Label("Add Map", systemImage: "plus") - } - } - } - } - } - - private func toggleSidebar() { - NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil) - } - - private func addMap() { - withAnimation { - let newMap = Map(context: viewContext) - newMap.uuid = UUID() - newMap.createdAt = Date() - newMap.title = "Map \(newMap.createdAt!.format())" - newMap.content = "" - - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } - - private func deleteMaps(offsets: IndexSet) { - withAnimation { - offsets.map { maps[$0] }.forEach(viewContext.delete) - - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } -} - -private let mapFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .short - formatter.timeStyle = .medium - return formatter -}() - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/Map/Extensions/Binding+unwrap.swift b/Map/Extensions/Binding+unwrap.swift deleted file mode 100644 index 3498cc3..0000000 --- a/Map/Extensions/Binding+unwrap.swift +++ /dev/null @@ -1,12 +0,0 @@ -import SwiftUI - -extension Binding { - init(_ source: Binding<Value?>, _ defaultValue: Value) { - // Ensure a non-nil value in `source`. - if source.wrappedValue == nil { - source.wrappedValue = defaultValue - } - // Unsafe unwrap because *we* know it's non-nil now. - self.init(source)! - } -} diff --git a/Map/Extensions/Date+format.swift b/Map/Extensions/Date+format.swift index c12e67d..d29ed0f 100644 --- a/Map/Extensions/Date+format.swift +++ b/Map/Extensions/Date+format.swift @@ -8,10 +8,10 @@ import Foundation extension Date { - func format() -> String { - let formatter = DateFormatter() - formatter.dateStyle = .short - formatter.timeStyle = .medium - return formatter.string(from: self) - } + func format() -> String { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .medium + return formatter.string(from: self) + } } diff --git a/Map/Extensions/Map+parse.swift b/Map/Extensions/Map+parse.swift index 03e7993..d82ba99 100644 --- a/Map/Extensions/Map+parse.swift +++ b/Map/Extensions/Map+parse.swift @@ -1,106 +1,215 @@ import CoreGraphics import Foundation -let VERTEX_PATTERN = "([^\\(]+?)[\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)" -let EDGE_PATTERN = "(.+?)[\\s]*->[\\s]*(.+)" +let vertexPattern = + "([^\\(]+?)[\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)" +let edgePattern = "(.+?)[\\s]*-([->])[\\s]*(.+)" +let blockerPattern = "\\[Blocker\\][\\s]*(.+)" +let opportunityPattern = "\\[Opportunity\\][\\s]*(.+)[\\s]+([-+])[\\s]*([0-9]+.?[0-9]*)" +let stagePattern = "\\[(I{1,3})\\][\\s]*([0-9]+.?[0-9]*)" struct ParsedMap { - let vertices: [Vertex] - let edges: [MapEdge] + let vertices: [Vertex] + let edges: [MapEdge] + let blockers: [Blocker] + let opportunities: [Opportunity] + let stages: [CGFloat] } struct Vertex { - let id: Int - let label: String - let position: CGPoint + let id: Int + let label: String + let position: CGPoint } struct MapEdge { - let id: Int - let origin: CGPoint - let destination: CGPoint + let id: Int + let origin: CGPoint + let destination: CGPoint + let arrowhead: Bool } +struct Blocker { + let id: Int + let position: CGPoint +} + +struct Opportunity { + let id: Int + let origin: CGPoint + let destination: CGPoint +} + +let defaultDimensions: [CGFloat] = [ + 25.0, + 50.0, + 75.0, +] + // Extracts the vertices from the text func parseVertices(_ text: String) -> [String: CGPoint] { - - var result: [String: CGPoint] = [:] - let regex = try! NSRegularExpression(pattern: VERTEX_PATTERN, options: .caseInsensitive) - - let lines = text.split(whereSeparator: \.isNewline) - - for line in lines { - let range = NSRange(location: 0, length: line.utf16.count) - let matches = regex.matches(in: String(line), options: [], range: range) - - if matches.count > 0 && matches[0].numberOfRanges == 4{ - let match = matches[0]; - let key = String(line[Range(match.range(at: 1), in: line)!]) - let xString = String(line[Range(match.range(at: 2), in: line)!]) - let yString = String(line[Range(match.range(at: 3), in: line)!]) - let x = CGFloat(truncating: NumberFormatter().number(from:xString) ?? 0.0) - let y = CGFloat(truncating: NumberFormatter().number(from:yString) ?? 0.0) - let point = CGPoint(x: x, y: y) - - result[key] = point - } + var result: [String: CGPoint] = [:] + let regex = try! NSRegularExpression(pattern: vertexPattern, options: .caseInsensitive) + + let lines = text.split(whereSeparator: \.isNewline) + + for line in lines { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + if matches.count > 0 && matches[0].numberOfRanges == 4 { + + let match = matches[0] + let key = String(line[Range(match.range(at: 1), in: line)!]) + let xString = String(line[Range(match.range(at: 2), in: line)!]) + let yString = String(line[Range(match.range(at: 3), in: line)!]) + let x = CGFloat(truncating: NumberFormatter().number(from: xString) ?? 0.0) + let y = CGFloat(truncating: NumberFormatter().number(from: yString) ?? 0.0) + let point = CGPoint(x: x, y: y) + + result[key] = point } - - return result -} + } -// Extracts the edges from the text + return result +} func parseEdges(_ text: String, vertices: [String: CGPoint]) -> [MapEdge] { - - var result: [MapEdge] = [] - let regex = try! NSRegularExpression(pattern: EDGE_PATTERN, options: .caseInsensitive) - - let lines = text.split(whereSeparator: \.isNewline) - - for (index, line) in lines.enumerated() { - let range = NSRange(location: 0, length: line.utf16.count) - let matches = regex.matches(in: String(line), options: [], range: range) - - if matches.count > 0 && matches[0].numberOfRanges == 3 { - let match = matches[0]; - let vertexA = String(line[Range(match.range(at: 1), in: line)!]) - let vertexB = String(line[Range(match.range(at: 2), in: line)!]) - - if let origin = vertices[vertexA] { - if let destination = vertices[vertexB] { - result.append(MapEdge(id: index, origin: origin, destination: destination)) - } - } + var result: [MapEdge] = [] + let regex = try! NSRegularExpression(pattern: edgePattern, options: .caseInsensitive) + + let lines = text.split(whereSeparator: \.isNewline) + + for (index, line) in lines.enumerated() { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + if matches.count > 0 && matches[0].numberOfRanges == 4 { + + let match = matches[0] + let arrowhead = String(line[Range(match.range(at: 2), in: line)!]) == ">" + let vertexA = String(line[Range(match.range(at: 1), in: line)!]) + let vertexB = String(line[Range(match.range(at: 3), in: line)!]) + + if let origin = vertices[vertexA] { + if let destination = vertices[vertexB] { + result.append( + MapEdge(id: index, origin: origin, destination: destination, arrowhead: arrowhead)) } + } } - - return result + } + + return result +} + +func parseOpportunities(_ text: String, vertices: [String: CGPoint]) -> [Opportunity] { + + var result: [Opportunity] = [] + let regex = try! NSRegularExpression(pattern: opportunityPattern, options: .caseInsensitive) + + let lines = text.split(whereSeparator: \.isNewline) + + for (index, line) in lines.enumerated() { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + if matches.count > 0 && matches[0].numberOfRanges == 4 { + + let match = matches[0] + let multiplier = CGFloat( + String(line[Range(match.range(at: 2), in: line)!]) == "-" ? -1.0 : 1.0) + let vertex = String(line[Range(match.range(at: 1), in: line)!]) + let opportunityString = String(line[Range(match.range(at: 3), in: line)!]) + let opportunity = CGFloat( + truncating: NumberFormatter().number(from: opportunityString) ?? 0.0) + + if let origin = vertices[vertex] { + let destination = CGPoint(x: origin.x + opportunity * multiplier, y: origin.y) + result.append(Opportunity(id: index, origin: origin, destination: destination)) + } + } + } + + return result +} + +func parseBlockers(_ text: String, vertices: [String: CGPoint]) -> [Blocker] { + + var result: [Blocker] = [] + let regex = try! NSRegularExpression(pattern: blockerPattern, options: .caseInsensitive) + + let lines = text.split(whereSeparator: \.isNewline) + + for (index, line) in lines.enumerated() { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + if matches.count > 0 && matches[0].numberOfRanges == 2 { + + let match = matches[0] + let vertexA = String(line[Range(match.range(at: 1), in: line)!]) + + if let position = vertices[vertexA] { + result.append(Blocker(id: index, position: position)) + } + } + } + + return result +} + +func parseStages(_ text: String) -> [CGFloat] { + + var result = defaultDimensions + let regex = try! NSRegularExpression(pattern: stagePattern, options: .caseInsensitive) + + let lines = text.split(whereSeparator: \.isNewline) + + for line in lines { + let range = NSRange(location: 0, length: line.utf16.count) + let matches = regex.matches(in: String(line), options: [], range: range) + + if matches.count > 0 && matches[0].numberOfRanges == 3 { + + let match = matches[0] + let stage = String(line[Range(match.range(at: 1), in: line)!]) + let dimensionsString = String(line[Range(match.range(at: 2), in: line)!]) + let dimensions = CGFloat(truncating: NumberFormatter().number(from: dimensionsString) ?? 0.0) + + result[stage.count - 1] = dimensions + } + } + + return result } // Converts vetex dictionary to array func mapVertices(_ vertices: [String: CGPoint]) -> [Vertex] { - var i = 0 - return vertices.map { label, position in - i += 1; - return Vertex(id: i, label: label, position: position) - } + var i = 0 + return vertices.map { label, position in + i += 1 + return Vertex(id: i, label: label, position: position) + } } extension Map { - func parse() -> ParsedMap { - - let text = self.content ?? "" - let vertices = parseVertices(text) - let mappedVertices = mapVertices(vertices) - let edges = parseEdges(text, vertices: vertices) - print(mappedVertices) - print(edges) - - return ParsedMap(vertices: mappedVertices, edges: edges) - } + func parse() -> ParsedMap { + + let text = self.content ?? "" + let vertices = parseVertices(text) + let mappedVertices = mapVertices(vertices) + let edges = parseEdges(text, vertices: vertices) + let blockers = parseBlockers(text, vertices: vertices) + let opportunities = parseOpportunities(text, vertices: vertices) + let stages = parseStages(text) + + return ParsedMap( + vertices: mappedVertices, edges: edges, blockers: blockers, opportunities: opportunities, + stages: stages) + } } diff --git a/Map/Extensions/NSImage+writePNG.swift b/Map/Extensions/NSImage+writePNG.swift new file mode 100644 index 0000000..c24c0cf --- /dev/null +++ b/Map/Extensions/NSImage+writePNG.swift @@ -0,0 +1,25 @@ +import Cocoa + +extension NSImage { + public func writePNG(toURL url: URL) { + + guard let data = tiffRepresentation, + let rep = NSBitmapImageRep(data: data), + let imgData = rep.representation( + using: .png, properties: [.compressionFactor: NSNumber(floatLiteral: 1.0)]) + else { + + print( + "\(self.self) Error Function '\(#function)' Line: \(#line) No tiff rep found for image writing to \(url)" + ) + return + } + + do { + try imgData.write(to: url) + } catch let error { + print( + "\(self.self) Error Function '\(#function)' Line: \(#line) \(error.localizedDescription)") + } + } +} diff --git a/Map/Info.plist b/Map/Info.plist index 69c84ae..ca51786 100644 --- a/Map/Info.plist +++ b/Map/Info.plist @@ -15,7 +15,7 @@ <key>CFBundlePackageType</key> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <key>CFBundleShortVersionString</key> - <string>1.0</string> + <string>$(MARKETING_VERSION)</string> <key>CFBundleVersion</key> <string>1</string> <key>LSMinimumSystemVersion</key> diff --git a/Map/Map.entitlements b/Map/Map.entitlements index f2ef3ae..6060301 100644 --- a/Map/Map.entitlements +++ b/Map/Map.entitlements @@ -2,9 +2,19 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> - <key>com.apple.security.app-sandbox</key> - <true/> - <key>com.apple.security.files.user-selected.read-only</key> - <true/> + <key>com.apple.developer.aps-environment</key> + <string>development</string> + <key>com.apple.developer.icloud-container-identifiers</key> + <array> + <string>iCloud.pizza.unlimited.map</string> + </array> + <key>com.apple.developer.icloud-services</key> + <array> + <string>CloudKit</string> + </array> + <key>com.apple.security.app-sandbox</key> + <true/> + <key>com.apple.security.files.user-selected.read-write</key> + <true/> </dict> </plist> diff --git a/Map/MapApp.swift b/Map/MapApp.swift deleted file mode 100644 index 10b6bcb..0000000 --- a/Map/MapApp.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// MapApp.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -import SwiftUI - -@main -struct MapApp: App { - let persistenceController = PersistenceController.shared - - var body: some Scene { - WindowGroup { - ContentView() - .environment(\.managedObjectContext, persistenceController.container.viewContext) - } - } -} diff --git a/Map/MapDetail.swift b/Map/MapDetail.swift deleted file mode 100644 index 74e3c8d..0000000 --- a/Map/MapDetail.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// ContentView.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -import SwiftUI -import CoreData - -struct MapDetailView: View { - @Environment(\.managedObjectContext) private var viewContext - - @ObservedObject var map: Map - - @State private var selectedEvolution = StageType.General - - var body: some View { - VSplitView { - VStack { - TextField("Title", text: Binding($map.title, ""), onCommit: { - try? viewContext.save() - }) - Picker("Evolution", selection: $selectedEvolution) { - ForEach(StageType.allCases) { stage in - Text(Stage.title(stage)).tag(stage) - } - } - TextEditor(text: Binding($map.content, "")).onChange(of: map.content) { _ in - try? viewContext.save() - }.font(Font.system(size: 16, design: .monospaced)) - } - ScrollView([.horizontal, .vertical]) { - MapRenderView(map: map, evolution: Stage.stages(selectedEvolution)) - } - } - } -} - -struct MapDetailView_Previews: PreviewProvider { - static var previews: some View { - MapDetailView(map: Map()).environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/Map/MapRender.swift b/Map/MapRender.swift deleted file mode 100644 index 79d80b5..0000000 --- a/Map/MapRender.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// ContentView.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -import SwiftUI -import CoreData -import CoreGraphics - -struct MapRenderView: View { - @ObservedObject var map: Map - let evolution: Stage - - let VERTEX_SIZE = CGSize(width: 25.0, height: 25.0) - let ARROWHEAD_SIZE = CGFloat(10.0) - let LINE_WIDTH = CGFloat(1.0) - let PADDING = CGFloat(4.0) - let STAGE_FRAME = CGSize(width: 245, height: 50) - - var parsedMap: ParsedMap { - return map.parse() - } - - var body: some View { - ZStack(alignment: .topLeading) { - ZStack (alignment: .topLeading) { - // The Axes - Path { path in - path.move(to: CGPoint(x: 0, y: 0)) - path.addLine(to: CGPoint(x: 0, y: 1000)) - path.addLine(to: CGPoint(x: 1000, y: 1000)) - path.move(to: CGPoint(x: 1000, y: 1000)) - path.closeSubpath() - - }.strokedPath(StrokeStyle(lineWidth: LINE_WIDTH * 2)) - Text("Visible").font(.title3).rotationEffect(Angle(degrees: -90.0)).offset(CGSize(width: -35.0, height: 0.0)) - Text("Value Chain").font(.title).rotationEffect(Angle(degrees: -90.0)).offset(CGSize(width: -72.0, height: 480.0)) - Text("Invisible").font(.title3).rotationEffect(Angle(degrees: -90.0)).offset(CGSize(width: -40.0, height: 980.0)) - Text(evolution.I).font(.title3).frame(width: STAGE_FRAME.width, height: STAGE_FRAME.height, alignment: .topLeading).offset(CGSize(width: 0.0, height: 1000.0)) - Text(evolution.II).font(.title3).frame(width: STAGE_FRAME.width, height: STAGE_FRAME.height, alignment: .topLeading).offset(CGSize(width: 250.0, height: 1000.0)) - Text(evolution.III).font(.title3).frame(width: STAGE_FRAME.width, height: STAGE_FRAME.height, alignment: .topLeading).offset(CGSize(width: 500.0, height: 1000.0)) - Text(evolution.IV).font(.title3).frame(width: STAGE_FRAME.width, height: STAGE_FRAME.height, alignment: .topLeading).offset(CGSize(width: 750.0, height: 1000.0)) - }.offset(CGSize(width: 0.0, height: 0.0)) - - // The Lanes - Path { path in - path.move(to: CGPoint(x: 250, y: 0)) - path.addLine(to: CGPoint(x: 250, y: 1000)) - path.move(to: CGPoint(x: 500, y: 0)) - path.addLine(to: CGPoint(x: 500, y: 1000)) - path.move(to: CGPoint(x: 750, y: 0)) - path.addLine(to: CGPoint(x: 750, y: 1000)) - path.move(to: CGPoint(x: 250, y: 0)) - path.closeSubpath() - }.strokedPath(StrokeStyle(lineWidth: LINE_WIDTH, dash: [10.0])) - - Path { path in - path.addRect(CGRect(x: 0, y: 0, width: 250, height: 1000)) - }.fill(Color.red).opacity(0.1) - Path { path in - path.addRect(CGRect(x: 250, y: 0, width: 250, height: 1000)) - }.fill(Color.orange).opacity(0.1) - Path { path in - path.addRect(CGRect(x: 500, y: 0, width: 250, height: 1000)) - }.fill(Color.yellow).opacity(0.1) - Path { path in - path.addRect(CGRect(x: 750, y: 0, width: 250, height: 1000)) - }.fill(Color.green).opacity(0.1) - - // The Vertices - - ForEach(parsedMap.vertices, id: \.id) { vertex in - Path { path in - path.addEllipse(in: CGRect( - origin: vertex.position, size: VERTEX_SIZE - )) - } - Text(vertex.label).foregroundColor(Color.gray).offset(CGSize( - width: vertex.position.x + VERTEX_SIZE.width + PADDING, - height: vertex.position.y)) - } - - // The Edges - - ForEach(parsedMap.edges, id: \.id) { edge in - Path { path in - - let slope = (edge.destination.y - edge.origin.y) / (edge.destination.x - edge.origin.x) - let angle = atan(slope) - let multiplier = CGFloat(slope < 0 ? -1.0 : 1.0) - let upperAngle = angle - CGFloat.pi / 4.0 - let lowerAngle = angle + CGFloat.pi / 4.0 - - let offsetOrigin = CGPoint(x: edge.origin.x + multiplier * (VERTEX_SIZE.width / 2.0) * cos(angle), y: edge.origin.y + multiplier * (VERTEX_SIZE.height / 2.0) * sin(angle)) - let offsetDestination = CGPoint(x: edge.destination.x - multiplier * (VERTEX_SIZE.width / 2.0) * cos(angle), y: edge.destination.y - multiplier * (VERTEX_SIZE.height / 2.0) * sin(angle)) - - path.move(to: offsetOrigin) - path.addLine(to: offsetDestination) - - // Arrowheads - path.move(to: offsetDestination) - path.addLine(to: CGPoint(x: offsetDestination.x - multiplier * ARROWHEAD_SIZE * cos(upperAngle), y: - offsetDestination.y - multiplier * ARROWHEAD_SIZE * sin(upperAngle))) - - path.move(to: offsetDestination) - path.addLine(to: CGPoint(x: offsetDestination.x - multiplier * ARROWHEAD_SIZE * cos(lowerAngle), y: - offsetDestination.y - multiplier * ARROWHEAD_SIZE * sin(lowerAngle))) - path.move(to: offsetDestination) - path.closeSubpath() - }.applying(CGAffineTransform(translationX: VERTEX_SIZE.width / 2.0, y: VERTEX_SIZE.height / 2.0)).strokedPath(StrokeStyle(lineWidth: LINE_WIDTH)) - } - }.frame( - minWidth: 1050.0, maxWidth: .infinity, - minHeight: 1050.0, maxHeight: .infinity, alignment: .topLeading - ).padding(30.0) - } -} - -struct MapRenderView_Previews: PreviewProvider { - static var previews: some View { - MapDetailView(map: Map()).environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/Map/MapRenderComponents/MapAxes.swift b/Map/MapRenderComponents/MapAxes.swift new file mode 100644 index 0000000..e7d65f4 --- /dev/null +++ b/Map/MapRenderComponents/MapAxes.swift @@ -0,0 +1,89 @@ +import SwiftUI + +struct MapAxes: View { + + @Environment(\.colorScheme) var colorScheme + + let mapSize: CGSize + let lineWidth: CGFloat + let evolution: Stage + let stages: [CGFloat] + let stageHeight = CGFloat(50.0) + let padding = CGFloat(5.0) + + var color: Color { + MapColor.colorForScheme(colorScheme).foreground + } + + var body: some View { + ZStack(alignment: .topLeading) { + + // Axis Lines + Path { path in + path.move(to: CGPoint(x: 0, y: 0)) + path.addLine(to: CGPoint(x: 0, y: mapSize.height)) + path.addLine(to: CGPoint(x: mapSize.width, y: mapSize.height)) + path.move(to: CGPoint(x: mapSize.width, y: mapSize.height)) + path.closeSubpath() + }.stroke(color, lineWidth: lineWidth * 2) + + // Y Labels + Text("Visible").font(.title3).foregroundColor(color).rotationEffect(Angle(degrees: -90.0)) + .offset(CGSize(width: -35.0, height: 0.0)) + Text("Value Chain").font(.title).foregroundColor(color).rotationEffect(Angle(degrees: -90.0)) + .offset(CGSize(width: -72.0, height: mapSize.height / 2 - 20)) + Text("Invisible").font(.title3).foregroundColor(color).rotationEffect(Angle(degrees: -90.0)) + .offset(CGSize(width: -40.0, height: mapSize.height - 20)) + + // X Labels + + Text("Uncharted") + .font(.title3) + .foregroundColor(color) + .frame(width: mapSize.width / 4, height: stageHeight, alignment: .topLeading) + .offset(CGSize(width: 0.0, height: -stageHeight / 2.0)) + Text("Industrialised") + .font(.title3) + .foregroundColor(color) + .frame(width: mapSize.width / 4, height: stageHeight, alignment: .topLeading) + .offset(CGSize(width: mapSize.width - 100.0, height: -stageHeight / 2.0)) + + Text(evolution.i) + .font(.title3) + .foregroundColor(color) + .frame(width: w(stages[0]), height: stageHeight, alignment: .topLeading) + .offset(CGSize(width: 0.0, height: mapSize.height + padding)) + + Text(evolution.ii) + .font(.title3) + .foregroundColor(color) + .frame(width: w(stages[1]) - w(stages[0]), height: stageHeight, alignment: .topLeading) + .offset(CGSize(width: w(stages[0]), height: mapSize.height + padding)) + + Text(evolution.iii) + .font(.title3) + .foregroundColor(color) + .frame(width: w(stages[2]) - w(stages[1]), height: stageHeight, alignment: .topLeading) + .offset(CGSize(width: w(stages[1]), height: mapSize.height + padding)) + + Text(evolution.iv) + .font(.title3) + .foregroundColor(color) + .frame(width: mapSize.width - w(stages[2]), height: stageHeight, alignment: .topLeading) + .offset(CGSize(width: w(stages[2]), height: mapSize.height + padding)) + } + } + + func w(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) + } +} + +struct MapAxes_Previews: PreviewProvider { + static var previews: some View { + MapAxes( + mapSize: CGSize(width: 200.0, height: 200.0), lineWidth: CGFloat(1.0), + evolution: Stage.stages(.general), stages: [25.0, 50.0, 75.0] + ).padding(50.0) + } +} diff --git a/Map/MapRenderComponents/MapBlockers.swift b/Map/MapRenderComponents/MapBlockers.swift new file mode 100644 index 0000000..b668b30 --- /dev/null +++ b/Map/MapRenderComponents/MapBlockers.swift @@ -0,0 +1,49 @@ +import SwiftUI + +struct MapBlockers: View { + + @Environment(\.colorScheme) var colorScheme + + let mapSize: CGSize + let vertexSize: CGSize + let blockers: [Blocker] + + var color: Color { + MapColor.colorForScheme(colorScheme).blocker + } + + let cornerSize = CGSize(width: 2.0, height: 2.0) + + var body: some View { + ForEach(blockers, id: \.id) { vertex in + Path { path in + path.addRoundedRect( + in: CGRect( + origin: CGPoint( + x: w(vertex.position.x) + 3 * vertexSize.width, + y: h(vertex.position.y) - vertexSize.height * 2 / 3), + size: CGSize(width: vertexSize.width / 2, height: vertexSize.height * 2) + ), cornerSize: cornerSize) + }.fill(color) + } + } + + func h(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) + } + + func w(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) + } +} + +struct MapBlockers_Previews: PreviewProvider { + static var previews: some View { + MapBlockers( + mapSize: CGSize(width: 400.0, height: 400.0), vertexSize: CGSize(width: 25.0, height: 25.0), + blockers: [ + Blocker(id: 0, position: CGPoint(x: 50.0, y: 50.0)), + Blocker(id: 1, position: CGPoint(x: 10.0, y: 20.0)), + ]) + } +} diff --git a/Map/MapRenderComponents/MapColor.swift b/Map/MapRenderComponents/MapColor.swift new file mode 100644 index 0000000..ce3d453 --- /dev/null +++ b/Map/MapRenderComponents/MapColor.swift @@ -0,0 +1,45 @@ +import SwiftUI + +struct MapColor { + let foreground: Color + let background: Color + let secondary: Color + let blocker: Color + let opportunity: Color + let stages: StageColor + + static func colorForScheme(_ colorScheme: ColorScheme) -> MapColor { + if colorScheme == .dark { + return MapColor( + foreground: Color.white, + background: Color(.sRGB, red: 0.13, green: 0.13, blue: 0.13), + secondary: Color(.sRGB, red: 0.81, green: 0.78, blue: 0.79), + blocker: Color(.sRGB, red: 0.13, green: 0.13, blue: 0.13), + opportunity: Color(.sRGB, red: 1.0, green: 0.37, blue: 0.34), + stages: StageColor( + i: Color(.sRGB, red: 0.37, green: 0.16, blue: 0.25), + ii: Color(.sRGB, red: 0.30, green: 0.29, blue: 0.26), + iii: Color(.sRGB, red: 0.15, green: 0.29, blue: 0.23), + iv: Color(.sRGB, red: 0.14, green: 0.22, blue: 0.31))) + } else { + return MapColor( + foreground: Color(.sRGB, red: 0.13, green: 0.13, blue: 0.13), + background: Color.white, + secondary: Color.gray, + blocker: Color(.sRGB, red: 0.60, green: 0.52, blue: 0.51), + opportunity: Color(.sRGB, red: 1.0, green: 0.37, blue: 0.34), + stages: StageColor( + i: Color(.sRGB, red: 1.00, green: 0.93, blue: 0.97), + ii: Color(.sRGB, red: 1.00, green: 0.98, blue: 0.92), + iii: Color(.sRGB, red: 0.93, green: 1.00, blue: 0.97), + iv: Color(.sRGB, red: 0.93, green: 0.96, blue: 1.00))) + } + } +} + +struct StageColor { + let i: Color + let ii: Color + let iii: Color + let iv: Color +} diff --git a/Map/MapRenderComponents/MapEdges.swift b/Map/MapRenderComponents/MapEdges.swift new file mode 100644 index 0000000..d40d2aa --- /dev/null +++ b/Map/MapRenderComponents/MapEdges.swift @@ -0,0 +1,92 @@ +// +// MapEdges.swift +// Map +// +// Created by Ruben Beltran del Rio on 2/2/21. +// + +import SwiftUI + +struct MapEdges: View { + + @Environment(\.colorScheme) var colorScheme + + let mapSize: CGSize + let lineWidth: CGFloat + let vertexSize: CGSize + let edges: [MapEdge] + + let arrowheadSize = CGFloat(10.0) + + var color: Color { + MapColor.colorForScheme(colorScheme).foreground + } + + var body: some View { + ForEach(edges, id: \.id) { edge in + Path { path in + + // First we transform edges from percentage to map coordinates + let origin = CGPoint(x: w(edge.origin.x), y: h(edge.origin.y)) + let destination = CGPoint(x: w(edge.destination.x), y: h(edge.destination.y)) + + let slope = (destination.y - origin.y) / (destination.x - origin.x) + let angle = atan(slope) + let multiplier = CGFloat(slope < 0 ? -1.0 : 1.0) + let upperAngle = angle - CGFloat.pi / 4.0 + let lowerAngle = angle + CGFloat.pi / 4.0 + + let offsetOrigin = CGPoint( + x: origin.x + multiplier * (vertexSize.width / 2.0) * cos(angle), + y: origin.y + multiplier * (vertexSize.height / 2.0) * sin(angle)) + let offsetDestination = CGPoint( + x: destination.x - multiplier * (vertexSize.width / 2.0) * cos(angle), + y: destination.y - multiplier * (vertexSize.height / 2.0) * sin(angle)) + + path.move(to: offsetOrigin) + path.addLine(to: offsetDestination) + + if edge.arrowhead { + path.move(to: offsetDestination) + path.addLine( + to: CGPoint( + x: offsetDestination.x - multiplier * arrowheadSize * cos(upperAngle), + y: + offsetDestination.y - multiplier * arrowheadSize * sin(upperAngle))) + + path.move(to: offsetDestination) + path.addLine( + to: CGPoint( + x: offsetDestination.x - multiplier * arrowheadSize * cos(lowerAngle), + y: + offsetDestination.y - multiplier * arrowheadSize * sin(lowerAngle))) + } + path.move(to: offsetDestination) + path.closeSubpath() + }.applying( + CGAffineTransform(translationX: vertexSize.width / 2.0, y: vertexSize.height / 2.0) + ).stroke(color, lineWidth: lineWidth) + } + } + + func h(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) + } + + func w(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) + } +} + +struct MapEdges_Previews: PreviewProvider { + static var previews: some View { + MapEdges( + mapSize: CGSize(width: 400.0, height: 400.0), lineWidth: 1.0, + vertexSize: CGSize(width: 25.0, height: 25.0), + edges: [ + MapEdge( + id: 1, origin: CGPoint(x: 2.0, y: 34.0), destination: CGPoint(x: 23.0, y: 76.2), + arrowhead: true) + ]) + } +} diff --git a/Map/MapRenderComponents/MapOpportunities.swift b/Map/MapRenderComponents/MapOpportunities.swift new file mode 100644 index 0000000..68d3fb5 --- /dev/null +++ b/Map/MapRenderComponents/MapOpportunities.swift @@ -0,0 +1,84 @@ +// +// MapEdges.swift +// Map +// +// Created by Ruben Beltran del Rio on 2/2/21. +// + +import SwiftUI + +struct MapOpportunities: View { + + @Environment(\.colorScheme) var colorScheme + + let mapSize: CGSize + let lineWidth: CGFloat + let vertexSize: CGSize + let opportunities: [Opportunity] + + let arrowheadSize = CGFloat(10.0) + + var color: Color { + MapColor.colorForScheme(colorScheme).opportunity + } + + var body: some View { + ForEach(opportunities, id: \.id) { edge in + Path { path in + + // First we transform edges from percentage to map coordinates + let origin = CGPoint(x: w(edge.origin.x), y: h(edge.origin.y)) + let destination = CGPoint(x: w(edge.destination.x), y: h(edge.destination.y)) + + let multiplier = CGFloat(edge.destination.x > edge.origin.x ? 1.0 : -1.0) + let upperAngle = -CGFloat.pi / 4.0 + let lowerAngle = CGFloat.pi / 4.0 + + let offsetOrigin = CGPoint(x: origin.x + multiplier * (vertexSize.width / 2.0), y: origin.y) + let offsetDestination = CGPoint( + x: destination.x - multiplier * (vertexSize.width / 2.0), y: destination.y) + + path.move(to: offsetOrigin) + path.addLine(to: offsetDestination) + + path.move(to: offsetDestination) + path.addLine( + to: CGPoint( + x: offsetDestination.x - multiplier * arrowheadSize * cos(upperAngle), + y: + offsetDestination.y - multiplier * arrowheadSize * sin(upperAngle))) + + path.move(to: offsetDestination) + path.addLine( + to: CGPoint( + x: offsetDestination.x - multiplier * arrowheadSize * cos(lowerAngle), + y: + offsetDestination.y - multiplier * arrowheadSize * sin(lowerAngle))) + + path.move(to: offsetDestination) + path.closeSubpath() + }.applying( + CGAffineTransform(translationX: vertexSize.width / 2.0, y: vertexSize.height / 2.0) + ).strokedPath(StrokeStyle(lineWidth: lineWidth * 2, dash: [10.0])).stroke(color) + } + } + + func h(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) + } + + func w(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) + } +} + +struct MapOpportunities_Previews: PreviewProvider { + static var previews: some View { + MapOpportunities( + mapSize: CGSize(width: 400.0, height: 400.0), lineWidth: 1.0, + vertexSize: CGSize(width: 25.0, height: 25.0), + opportunities: [ + Opportunity(id: 1, origin: CGPoint(x: 2.0, y: 34.0), destination: CGPoint(x: 23.0, y: 76.2)) + ]) + } +} diff --git a/Map/MapRenderComponents/MapStages.swift b/Map/MapRenderComponents/MapStages.swift new file mode 100644 index 0000000..052a315 --- /dev/null +++ b/Map/MapRenderComponents/MapStages.swift @@ -0,0 +1,60 @@ +import SwiftUI + +struct MapStages: View { + + @Environment(\.colorScheme) var colorScheme + + let mapSize: CGSize + let lineWidth: CGFloat + let stages: [CGFloat] + let opacity = 0.1 + + var color: MapColor { + MapColor.colorForScheme(colorScheme) + } + + var body: some View { + + ZStack(alignment: .topLeading) { + Path { path in + path.addRect(CGRect(x: 0, y: 0, width: w(stages[0]), height: mapSize.height)) + }.fill(color.stages.i) + Path { path in + path.addRect( + CGRect(x: w(stages[0]), y: 0, width: w(stages[1]) - w(stages[0]), height: mapSize.height)) + }.fill(color.stages.ii) + Path { path in + path.addRect( + CGRect(x: w(stages[1]), y: 0, width: w(stages[2]) - w(stages[1]), height: mapSize.height)) + }.fill(color.stages.iii) + Path { path in + path.addRect( + CGRect(x: w(stages[2]), y: 0, width: mapSize.width - w(stages[2]), height: mapSize.height) + ) + }.fill(color.stages.iv) + + Path { path in + path.move(to: CGPoint(x: w(stages[0]), y: 0)) + path.addLine(to: CGPoint(x: w(stages[0]), y: mapSize.height)) + path.move(to: CGPoint(x: w(stages[1]), y: 0)) + path.addLine(to: CGPoint(x: w(stages[1]), y: mapSize.height)) + path.move(to: CGPoint(x: w(stages[2]), y: 0)) + path.addLine(to: CGPoint(x: w(stages[2]), y: mapSize.height)) + path.move(to: CGPoint(x: w(stages[0]), y: 0)) + path.closeSubpath() + }.strokedPath(StrokeStyle(lineWidth: lineWidth, dash: [10.0])).stroke(color.foreground) + } + } + + func w(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) + } +} + +struct MapStages_Previews: PreviewProvider { + static var previews: some View { + MapStages( + mapSize: CGSize(width: 200.0, height: 200.0), lineWidth: CGFloat(1.0), + stages: [25.0, 50.0, 75.0]) + } +} diff --git a/Map/MapRenderComponents/MapVertices.swift b/Map/MapRenderComponents/MapVertices.swift new file mode 100644 index 0000000..71f85be --- /dev/null +++ b/Map/MapRenderComponents/MapVertices.swift @@ -0,0 +1,49 @@ +import SwiftUI + +struct MapVertices: View { + + @Environment(\.colorScheme) var colorScheme + + let mapSize: CGSize + let vertexSize: CGSize + let vertices: [Vertex] + let padding = CGFloat(4.0) + + var color: MapColor { + MapColor.colorForScheme(colorScheme) + } + + var body: some View { + ForEach(vertices, id: \.id) { vertex in + Path { path in + path.addEllipse( + in: CGRect( + origin: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y)), size: vertexSize + )) + }.fill(color.foreground) + Text(vertex.label).foregroundColor(color.secondary).offset( + CGSize( + width: w(vertex.position.x) + vertexSize.width + padding, + height: h(vertex.position.y))) + } + } + + func h(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) + } + + func w(_ dimension: CGFloat) -> CGFloat { + max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) + } +} + +struct MapVertices_Previews: PreviewProvider { + static var previews: some View { + MapVertices( + mapSize: CGSize(width: 400.0, height: 400.0), vertexSize: CGSize(width: 25.0, height: 25.0), + vertices: [ + Vertex(id: 0, label: "A", position: CGPoint(x: 50.0, y: 50.0)), + Vertex(id: 0, label: "A", position: CGPoint(x: 10.0, y: 20.0)), + ]) + } +} diff --git a/Map/Persistence.swift b/Map/Persistence.swift deleted file mode 100644 index 49bb2d9..0000000 --- a/Map/Persistence.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// Persistence.swift -// Map -// -// Created by Ruben Beltran del Rio on 2/1/21. -// - -import CoreData - -struct PersistenceController { - static let shared = PersistenceController() - - static var preview: PersistenceController = { - let result = PersistenceController(inMemory: true) - let viewContext = result.container.viewContext - for _ in 0..<10 { - let newMap = Map(context: viewContext) - newMap.uuid = UUID() - newMap.createdAt = Date() - newMap.title = "Map \(newMap.createdAt!.format())" - newMap.content = "" - } - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - return result - }() - - let container: NSPersistentCloudKitContainer - - init(inMemory: Bool = false) { - container = NSPersistentCloudKitContainer(name: "Map") - if inMemory { - container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") - } - container.loadPersistentStores(completionHandler: { (storeDescription, error) in - if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ - fatalError("Unresolved error \(error), \(error.userInfo)") - } - }) - } -} diff --git a/Map/Stage.swift b/Map/Stage.swift deleted file mode 100644 index 4a53ada..0000000 --- a/Map/Stage.swift +++ /dev/null @@ -1,108 +0,0 @@ -struct Stage { - let I: String - let II: String - let III: String - let IV: String - - static func stages(_ type: StageType) -> Stage { - switch(type) { - case .General: - return Stage(I: "Genesis", II: "Custom Built", III: "Product (+rental)", IV: "Commodity (+utility)") - case .Ubiquity: - return Stage(I: "Rare", II: "Slowly Increasing Consumption", III: "Rapidly Increasing Consumption", IV: "Widespread and stabilising") - case .Certainty: - return Stage(I: "Poorly Understood", II: "Rapid Increase In Learning", III: "Rapid Increase in Use / fit for purpose", IV: "Commonly understood (in terms of use)") - case .PublicationTypes: - return Stage(I: "Normally describing the wonder of the thing", II: "Build / construct / awareness and learning", III: "Maintenance / operations / installation / feature", IV: "Focused on use") - case .Market: - return Stage(I: "Undefined Market", II: "Forming Market", III: "Growing Market", IV: "Mature Market") - case .KnowledgeManagement: - return Stage(I: "Uncertain", II: "Learning on use", III: "Learning on operation", IV: "Known / accepted") - case .MarketPerception: - return Stage(I: "Chaotic (non-linear)", II: "Domain of experts", III: "Increasing expectation of use", IV: "Ordered (appearance of being trivial) / trivial") - case .UserPerception: - return Stage(I: "Different / confusing / exciting / surprising", II: "Leading edge / emerging", III: "Increasingly common / disappointed if not used", IV: "Standard / expected") - case .PerceptionInIndustry: - return Stage(I: "Competitive advantage / unpredictable / unknown", II: "Competitive advantage / ROI / case examples", III: "Advantage through implementation / features", IV: "Cost of doing business") - case .FocusOfValue: - return Stage(I: "High future worth", II: "Seeking profit / ROI", III: "High profitability", IV: "High volume / reducing margin") - case .Understanding: - return Stage(I: "Poorly Understood / unpredictable", II: "Increasing understanding / development of measures", III: "Increasing education / constant refinement of needs / measures", IV: "Believed to be well defined / stable / measurable") - case .Comparison: - return Stage(I: "Constantly changing / a differential / unstable", II: "Learning from others / testing the water / some evidential support", III: "Feature difference", IV: "Essential / operational advantage") - case .Failure: - return Stage(I: "High / tolerated / assumed", II: "Moderate / unsurprising but disappointed", III: "Not tolerated, focus on constant improvement", IV: "Operational efficiency and surprised by failure") - case .MarketAction: - return Stage(I: "Gambling / driven by gut", II: "Exploring a \"found\" value", III: "Market analysis / listening to customers", IV: "Metric driven / build what is needed") - case .Efficiency: - return Stage(I: "Reducing the cost of change (experimentation)", II: "Reducing cost of waste (Learning)", III: "Reducing cost of waste (Learning)", IV: "Reducing cost of deviation (Volume)") - case .DecisionDrivers: - return Stage(I: "Heritage / culture", II: "Analyses & synthesis", III: "Analyses & synthesis", IV: "Previous Experience") - case .Behavior: - return Stage(I: "Needs to be pushed", II: "Shown in simple situations", III: "Shown in moderately complex situations ", IV: "Always shown") - } - } - - static func title(_ type: StageType) -> String { - switch(type) { - case .General: - return "General" - case .Ubiquity: - return "Ubiquity" - case .Certainty: - return "Certainty" - case .PublicationTypes: - return "Publication Types" - case .Market: - return "Market" - case .KnowledgeManagement: - return "Knowledge Management" - case .MarketPerception: - return "Market Perception" - case .UserPerception: - return "User Perception" - case .PerceptionInIndustry: - return "Perception In Industry" - case .FocusOfValue: - return "Focus Of Value" - case .Understanding: - return "Understanding" - case .Comparison: - return "Comparison" - case .Failure: - return "Failure" - case .MarketAction: - return "Market Action" - case .Efficiency: - return "Efficiency" - case .DecisionDrivers: - return "Decision Drivers" - case .Behavior: - return "Behavior" - } - } -} - -enum StageType: String, CaseIterable, Identifiable { - case General - case Ubiquity - case Certainty - case PublicationTypes - case Market - case KnowledgeManagement - case MarketPerception - case UserPerception - case PerceptionInIndustry - case FocusOfValue - case Understanding - case Comparison - case Failure - case MarketAction - case Efficiency - case DecisionDrivers - case Behavior - - var id: String { self.rawValue } -} - - diff --git a/Map/State/AppState.swift b/Map/State/AppState.swift new file mode 100644 index 0000000..e10efea --- /dev/null +++ b/Map/State/AppState.swift @@ -0,0 +1,103 @@ +import Cocoa +import Foundation +import SwiftUI + +struct AppState { + var selectedMap: Map? = nil + var mapBeingDeleted: Map? = nil +} + +enum AppAction { + case selectMap(map: Map?) + case deleteMap(map: Map) + case exportMapAsImage(map: Map, evolution: StageType) + case exportMapAsText(map: Map) +} + +func appStateReducer(state: inout AppState, action: AppAction) { + + switch action { + + case .selectMap(let map): + state.selectedMap = map + + case .deleteMap(let map): + let context = PersistenceController.shared.container.viewContext + + context.delete(map) + try? context.save() + + case .exportMapAsImage(let map, let evolution): + let window = NSWindow( + contentRect: .init( + origin: .zero, + size: .init( + width: NSScreen.main!.frame.width, + height: NSScreen.main!.frame.height)), + styleMask: [.closable], + backing: .buffered, + defer: false) + + window.title = map.title ?? "Untitled Map" + window.isOpaque = true + window.center() + window.isMovableByWindowBackground = true + window.makeKeyAndOrderFront(nil) + + let renderView = MapRenderView(map: map, evolution: Stage.stages(evolution)) + + let view = NSHostingView(rootView: renderView) + window.contentView = view + + let imageRepresentation = view.bitmapImageRepForCachingDisplay(in: view.bounds)! + view.cacheDisplay(in: view.bounds, to: imageRepresentation) + let image = NSImage(cgImage: imageRepresentation.cgImage!, size: view.bounds.size) + + let dialog = NSSavePanel() + + dialog.title = "Save Map" + dialog.showsResizeIndicator = false + dialog.canCreateDirectories = true + dialog.showsHiddenFiles = false + dialog.allowedFileTypes = ["png"] + dialog.nameFieldStringValue = map.title ?? "Untitled Map" + + if dialog.runModal() == NSApplication.ModalResponse.OK { + let result = dialog.url + + if result != nil { + + image.writePNG(toURL: result!) + print("saved at \(result!)") + } + } else { + print("Cancel") + } + window.orderOut(nil) + + case .exportMapAsText(let map): + let dialog = NSSavePanel() + + dialog.title = "Save Map Text" + dialog.showsResizeIndicator = false + dialog.canCreateDirectories = true + dialog.showsHiddenFiles = false + dialog.allowedFileTypes = ["txt"] + dialog.nameFieldStringValue = map.title ?? "Untitled Map" + + if let content = map.content { + + if dialog.runModal() == NSApplication.ModalResponse.OK { + let result = dialog.url + + if let result = result { + try? content.write(to: result, atomically: true, encoding: String.Encoding.utf8) + } + } else { + print("Cancel") + } + } + } +} + +typealias AppStore = Store<AppState, AppAction> diff --git a/Map/State/Persistence.swift b/Map/State/Persistence.swift new file mode 100644 index 0000000..1eb09e8 --- /dev/null +++ b/Map/State/Persistence.swift @@ -0,0 +1,42 @@ +import CoreData + +struct PersistenceController { + static let shared = PersistenceController() + + static var preview: PersistenceController = { + let result = PersistenceController(inMemory: true) + let viewContext = result.container.viewContext + for _ in 0..<10 { + let newMap = Map(context: viewContext) + newMap.uuid = UUID() + newMap.createdAt = Date() + newMap.title = "Map \(newMap.createdAt!.format())" + newMap.content = "" + } + do { + try viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + return result + }() + + let container: NSPersistentCloudKitContainer + + init(inMemory: Bool = false) { + container = NSPersistentCloudKitContainer(name: "Map") + + container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + container.viewContext.automaticallyMergesChangesFromParent = true + + if inMemory { + container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") + } + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + } +} diff --git a/Map/State/Store.swift b/Map/State/Store.swift new file mode 100644 index 0000000..7860f33 --- /dev/null +++ b/Map/State/Store.swift @@ -0,0 +1,18 @@ +import Foundation + +final class Store<State, Action>: ObservableObject { + @Published private(set) var state: State + + private let reducer: Reducer<State, Action> + + init(initialState: State, reducer: @escaping Reducer<State, Action>) { + self.state = initialState + self.reducer = reducer + } + + func send(_ action: Action) { + reducer(&state, action) + } +} + +typealias Reducer<State, Action> = (inout State, Action) -> Void diff --git a/Map/Views/ContentView.swift b/Map/Views/ContentView.swift new file mode 100644 index 0000000..4f55c27 --- /dev/null +++ b/Map/Views/ContentView.swift @@ -0,0 +1,104 @@ +// +// ContentView.swift +// Map +// +// Created by Ruben Beltran del Rio on 2/1/21. +// + +import CoreData +import SwiftUI + +struct ContentView: View { + @Environment(\.managedObjectContext) private var viewContext + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Map.createdAt, ascending: true)], + animation: .default) + private var maps: FetchedResults<Map> + + var body: some View { + NavigationView { + List { + if maps.count == 0 { + DefaultMapView() + } + ForEach(maps) { map in + NavigationLink(destination: MapDetailView(map: map)) { + HStack { + Text(map.title ?? "Untitled Map") + Spacer() + Text(mapFormatter.string(from: (map.createdAt ?? Date()))) + .font(.caption) + .padding(.vertical, 2.0) + .padding(.horizontal, 4.0) + .background(Color.accentColor) + .foregroundColor(Color.black) + .cornerRadius(2.0) + }.padding(.leading, 8.0) + } + } + .onDelete(perform: deleteMaps) + }.frame(minWidth: 250.0, alignment: .leading) + .toolbar { + HStack { + Button(action: toggleSidebar) { + Label("Toggle Sidebar", systemImage: "sidebar.left") + } + Button(action: addMap) { + Label("Add Map", systemImage: "plus") + } + } + } + } + } + + private func toggleSidebar() { + NSApp.keyWindow?.firstResponder?.tryToPerform( + #selector(NSSplitViewController.toggleSidebar(_:)), with: nil) + } + + private func addMap() { + withAnimation { + let newMap = Map(context: viewContext) + newMap.uuid = UUID() + newMap.createdAt = Date() + newMap.title = "Map \(newMap.createdAt!.format())" + newMap.content = "" + + do { + try viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + } + + private func deleteMaps(offsets: IndexSet) { + + withAnimation { + offsets.map { maps[$0] }.forEach(viewContext.delete) + + do { + try viewContext.save() + } catch { + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + } + } +} + +private let mapFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .none + return formatter +}() + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView().environment( + \.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/Map/Views/DefaultMapView.swift b/Map/Views/DefaultMapView.swift new file mode 100644 index 0000000..66914f1 --- /dev/null +++ b/Map/Views/DefaultMapView.swift @@ -0,0 +1,23 @@ +// +// ContentView.swift +// Map +// +// Created by Ruben Beltran del Rio on 2/1/21. +// + +import CoreData +import SwiftUI + +struct DefaultMapView: View { + + var body: some View { + Text("Select a map from the left hand side, or click on + to create one.") + } +} + +struct DefaultMapView_Previews: PreviewProvider { + static var previews: some View { + MapDetailView(map: Map()).environment( + \.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/Map/Views/MapApp.swift b/Map/Views/MapApp.swift new file mode 100644 index 0000000..0f8ae64 --- /dev/null +++ b/Map/Views/MapApp.swift @@ -0,0 +1,21 @@ +// +// MapApp.swift +// Map +// +// Created by Ruben Beltran del Rio on 2/1/21. +// + +import SwiftUI + +@main +struct MapApp: App { + let persistenceController = PersistenceController.shared + + var body: some Scene { + WindowGroup { + ContentView() + .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environmentObject(AppStore(initialState: AppState(), reducer: appStateReducer)) + }.windowStyle(HiddenTitleBarWindowStyle()) + } +} diff --git a/Map/Views/MapDetail.swift b/Map/Views/MapDetail.swift new file mode 100644 index 0000000..a8ea3d1 --- /dev/null +++ b/Map/Views/MapDetail.swift @@ -0,0 +1,101 @@ +// +// ContentView.swift +// Map +// +// Created by Ruben Beltran del Rio on 2/1/21. +// + +import CoreData +import SwiftUI + +struct MapDetailView: View { + @Environment(\.managedObjectContext) private var viewContext + @Environment(\.colorScheme) var colorScheme + + @EnvironmentObject var store: AppStore + + @ObservedObject var map: Map + + private var mapColor: MapColor { + MapColor.colorForScheme(colorScheme) + } + + private var title: Binding<String> { + Binding( + get: { map.title ?? "" }, + set: { title in + map.title = title + } + ) + } + + private var content: Binding<String> { + Binding( + get: { map.content ?? "" }, + set: { content in + map.content = content + } + ) + } + + @State private var selectedEvolution = StageType.general + + var body: some View { + if map.uuid != nil { + VSplitView { + VStack { + HStack { + TextField( + "Title", text: title, + onCommit: { + try? viewContext.save() + } + ).font(.title2).textFieldStyle(PlainTextFieldStyle()).padding(.vertical, 4.0).padding( + .leading, 4.0) + Button(action: saveText) { + Image(systemName: "doc.text") + }.padding(.vertical, 4.0).padding(.leading, 4.0) + Button(action: saveImage) { + Image(systemName: "photo") + }.padding(.vertical, 4.0).padding(.leading, 4.0).padding(.trailing, 8.0) + } + Picker("Evolution", selection: $selectedEvolution) { + ForEach(StageType.allCases) { stage in + Text(Stage.title(stage)).tag(stage).padding(4.0) + } + }.padding(.horizontal, 8.0).padding(.vertical, 4.0) + + ZStack(alignment: .topLeading) { + MapTextEditor(text: content).onChange(of: map.content) { _ in + try? viewContext.save() + } + .background(mapColor.background) + .foregroundColor(mapColor.foreground) + .frame(minHeight: 96.0) + }.padding(.top, 8.0).padding(.leading, 8.0).background(mapColor.background).cornerRadius( + 5.0) + }.padding(.horizontal, 8.0) + ScrollView([.horizontal, .vertical]) { + MapRenderView(map: map, evolution: Stage.stages(selectedEvolution)) + }.background(mapColor.background) + } + } else { + DefaultMapView() + } + } + + private func saveText() { + store.send(.exportMapAsText(map: map)) + } + + private func saveImage() { + store.send(.exportMapAsImage(map: map, evolution: selectedEvolution)) + } +} + +struct MapDetailView_Previews: PreviewProvider { + static var previews: some View { + MapDetailView(map: Map()).environment( + \.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/Map/Views/MapRender.swift b/Map/Views/MapRender.swift new file mode 100644 index 0000000..b35b724 --- /dev/null +++ b/Map/Views/MapRender.swift @@ -0,0 +1,61 @@ +// +// ContentView.swift +// Map +// +// Created by Ruben Beltran del Rio on 2/1/21. +// + +import CoreData +import CoreGraphics +import SwiftUI + +struct MapRenderView: View { + + @Environment(\.colorScheme) var colorScheme + + @ObservedObject var map: Map + let evolution: Stage + + let mapSize = CGSize(width: 1300.0, height: 1000.0) + + let lineWidth = CGFloat(1.0) + let vertexSize = CGSize(width: 25.0, height: 25.0) + let padding = CGFloat(30.0) + + var parsedMap: ParsedMap { + return map.parse() + } + + var body: some View { + ZStack(alignment: .topLeading) { + + Path { path in + path.addRect( + CGRect( + x: -padding, y: -padding, width: mapSize.width + padding * 2, + height: mapSize.height + padding * 2)) + }.fill(MapColor.colorForScheme(colorScheme).background) + + MapStages(mapSize: mapSize, lineWidth: lineWidth, stages: parsedMap.stages) + MapAxes( + mapSize: mapSize, lineWidth: lineWidth, evolution: evolution, stages: parsedMap.stages) + MapOpportunities( + mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, + opportunities: parsedMap.opportunities) + MapBlockers(mapSize: mapSize, vertexSize: vertexSize, blockers: parsedMap.blockers) + MapVertices(mapSize: mapSize, vertexSize: vertexSize, vertices: parsedMap.vertices) + MapEdges( + mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, edges: parsedMap.edges) + }.frame( + width: mapSize.width, + height: mapSize.height, alignment: .topLeading + ).padding(padding) + } +} + +struct MapRenderView_Previews: PreviewProvider { + static var previews: some View { + MapDetailView(map: Map()).environment( + \.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/Map/Views/MapTextEditor.swift b/Map/Views/MapTextEditor.swift new file mode 100644 index 0000000..7251c59 --- /dev/null +++ b/Map/Views/MapTextEditor.swift @@ -0,0 +1,71 @@ +import Cocoa +import SwiftUI + +class MapTextEditorController: NSViewController { + + @Binding var text: String + + init(text: Binding<String>) { + self._text = text + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + let scrollView = NSTextView.scrollableTextView() + let textView = scrollView.documentView as! NSTextView + + scrollView.translatesAutoresizingMaskIntoConstraints = false + + textView.delegate = self + textView.string = self.text + textView.isEditable = true + textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular) + self.view = scrollView + } + + override func viewDidAppear() { + self.view.window?.makeFirstResponder(self.view) + } +} + +extension MapTextEditorController: NSTextViewDelegate { + + func textDidChange(_ obj: Notification) { + if let textField = obj.object as? NSTextView { + self.text = textField.string + } + } + + func textView(_ view: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool + { + let range = Range(shouldChangeTextIn, in: view.string) + let target = view.string[range!] + + if target == "--" { + return false + } + + return true + } +} + +struct MapTextEditor: NSViewControllerRepresentable { + + @Binding var text: String + + func makeNSViewController( + context: NSViewControllerRepresentableContext<MapTextEditor> + ) -> MapTextEditorController { + return MapTextEditorController(text: $text) + } + + func updateNSViewController( + _ nsViewController: MapTextEditorController, + context: NSViewControllerRepresentableContext<MapTextEditor> + ) { + } +} diff --git a/Map/Views/Stage.swift b/Map/Views/Stage.swift new file mode 100644 index 0000000..23432ad --- /dev/null +++ b/Map/Views/Stage.swift @@ -0,0 +1,161 @@ +struct Stage { + let i: String + let ii: String + let iii: String + let iv: String + + static func stages(_ type: StageType) -> Stage { + switch type { + case .general: + return Stage( + i: "Genesis", ii: "Custom Built", iii: "Product (+rental)", iv: "Commodity (+utility)") + case .practice: + return Stage( + i: "Novel", ii: "Emerging", iii: "Good", iv: "Best") + case .data: + return Stage( + i: "Unmodelled", ii: "Divergent", iii: "Convergent", iv: "Modelled") + case .knowledge: + return Stage( + i: "Concept", ii: "Hypothesis", iii: "Theory", iv: "Accepted") + case .ubiquity: + return Stage( + i: "Rare", ii: "Slowly Increasing Consumption", iii: "Rapidly Increasing Consumption", + iv: "Widespread and stabilising") + case .certainty: + return Stage( + i: "Poorly Understood", ii: "Rapid Increase In Learning", + iii: "Rapid Increase in Use / fit for purpose", iv: "Commonly understood (in terms of use)") + case .publicationTypes: + return Stage( + i: "Normally describing the wonder of the thing", + ii: "Build / construct / awareness and learning", + iii: "Maintenance / operations / installation / feature", iv: "Focused on use") + case .market: + return Stage( + i: "Undefined Market", ii: "Forming Market", iii: "Growing Market", iv: "Mature Market") + case .knowledgeManagement: + return Stage( + i: "Uncertain", ii: "Learning on use", iii: "Learning on operation", iv: "Known / accepted") + case .marketPerception: + return Stage( + i: "Chaotic (non-linear)", ii: "Domain of experts", iii: "Increasing expectation of use", + iv: "Ordered (appearance of being trivial) / trivial") + case .userPerception: + return Stage( + i: "Different / confusing / exciting / surprising", ii: "Leading edge / emerging", + iii: "Increasingly common / disappointed if not used", iv: "Standard / expected") + case .perceptionInIndustry: + return Stage( + i: "Competitive advantage / unpredictable / unknown", + ii: "Competitive advantage / ROI / case examples", + iii: "Advantage through implementation / features", iv: "Cost of doing business") + case .focusOfValue: + return Stage( + i: "High future worth", ii: "Seeking profit / ROI", iii: "High profitability", + iv: "High volume / reducing margin") + case .understanding: + return Stage( + i: "Poorly Understood / unpredictable", + ii: "Increasing understanding / development of measures", + iii: "Increasing education / constant refinement of needs / measures", + iv: "Believed to be well defined / stable / measurable") + case .comparison: + return Stage( + i: "Constantly changing / a differential / unstable", + ii: "Learning from others / testing the water / some evidential support", + iii: "Feature difference", iv: "Essential / operational advantage") + case .failure: + return Stage( + i: "High / tolerated / assumed", ii: "Moderate / unsurprising but disappointed", + iii: "Not tolerated, focus on constant improvement", + iv: "Operational efficiency and surprised by failure") + case .marketAction: + return Stage( + i: "Gambling / driven by gut", ii: "Exploring a \"found\" value", + iii: "Market analysis / listening to customers", iv: "Metric driven / build what is needed") + case .efficiency: + return Stage( + i: "Reducing the cost of change (experimentation)", ii: "Reducing cost of waste (Learning)", + iii: "Reducing cost of waste (Learning)", iv: "Reducing cost of deviation (Volume)") + case .decisionDrivers: + return Stage( + i: "Heritage / culture", ii: "Analyses & synthesis", iii: "Analyses & synthesis", + iv: "Previous Experience") + case .behavior: + return Stage( + i: "Uncertain when to use", ii: "Learning when to use", iii: "Learning through use", + iv: "Known / common usage") + } + } + + static func title(_ type: StageType) -> String { + switch type { + case .general: + return "Activities" + case .practice: + return "Practice" + case .data: + return "Data" + case .knowledge: + return "Knowledge" + case .ubiquity: + return "Ubiquity" + case .certainty: + return "Certainty" + case .publicationTypes: + return "Publication Types" + case .market: + return "Market" + case .knowledgeManagement: + return "Knowledge Management" + case .marketPerception: + return "Market Perception" + case .userPerception: + return "User Perception" + case .perceptionInIndustry: + return "Perception In Industry" + case .focusOfValue: + return "Focus Of Value" + case .understanding: + return "Understanding" + case .comparison: + return "Comparison" + case .failure: + return "Failure" + case .marketAction: + return "Market Action" + case .efficiency: + return "Efficiency" + case .decisionDrivers: + return "Decision Drivers" + case .behavior: + return "Behavior" + } + } +} + +enum StageType: String, CaseIterable, Identifiable { + case general + case practice + case data + case knowledge + case ubiquity + case certainty + case publicationTypes + case market + case knowledgeManagement + case marketPerception + case userPerception + case perceptionInIndustry + case focusOfValue + case understanding + case comparison + case failure + case marketAction + case efficiency + case decisionDrivers + case behavior + + var id: String { self.rawValue } +} |