aboutsummaryrefslogtreecommitdiff
path: root/Map
diff options
context:
space:
mode:
Diffstat (limited to 'Map')
-rw-r--r--Map/Assets.xcassets/AccentColor.colorset/Contents.json9
-rw-r--r--Map/Assets.xcassets/AppIcon.appiconset/Contents.json10
-rw-r--r--Map/Assets.xcassets/AppIcon.appiconset/icon_128x128.pngbin0 -> 5641 bytes
-rw-r--r--Map/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.pngbin0 -> 13167 bytes
-rw-r--r--Map/Assets.xcassets/AppIcon.appiconset/icon_16x16.pngbin0 -> 628 bytes
-rw-r--r--Map/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.pngbin0 -> 1367 bytes
-rw-r--r--Map/Assets.xcassets/AppIcon.appiconset/icon_256x256.pngbin0 -> 12374 bytes
-rw-r--r--Map/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.pngbin0 -> 29179 bytes
-rw-r--r--Map/Assets.xcassets/AppIcon.appiconset/icon_32x32.pngbin0 -> 1367 bytes
-rw-r--r--Map/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.pngbin0 -> 2285 bytes
-rw-r--r--Map/Assets.xcassets/AppIcon.appiconset/icon_512x512.pngbin0 -> 29179 bytes
-rw-r--r--Map/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.pngbin0 -> 74672 bytes
-rw-r--r--Map/ContentView.swift97
-rw-r--r--Map/Extensions/Binding+unwrap.swift12
-rw-r--r--Map/Extensions/Date+format.swift12
-rw-r--r--Map/Extensions/Map+parse.swift255
-rw-r--r--Map/Extensions/NSImage+writePNG.swift25
-rw-r--r--Map/Info.plist2
-rw-r--r--Map/Map.entitlements18
-rw-r--r--Map/MapApp.swift20
-rw-r--r--Map/MapDetail.swift44
-rw-r--r--Map/MapRender.swift125
-rw-r--r--Map/MapRenderComponents/MapAxes.swift89
-rw-r--r--Map/MapRenderComponents/MapBlockers.swift49
-rw-r--r--Map/MapRenderComponents/MapColor.swift45
-rw-r--r--Map/MapRenderComponents/MapEdges.swift92
-rw-r--r--Map/MapRenderComponents/MapOpportunities.swift84
-rw-r--r--Map/MapRenderComponents/MapStages.swift60
-rw-r--r--Map/MapRenderComponents/MapVertices.swift49
-rw-r--r--Map/Persistence.swift58
-rw-r--r--Map/Stage.swift108
-rw-r--r--Map/State/AppState.swift103
-rw-r--r--Map/State/Persistence.swift42
-rw-r--r--Map/State/Store.swift18
-rw-r--r--Map/Views/ContentView.swift104
-rw-r--r--Map/Views/DefaultMapView.swift23
-rw-r--r--Map/Views/MapApp.swift21
-rw-r--r--Map/Views/MapDetail.swift101
-rw-r--r--Map/Views/MapRender.swift61
-rw-r--r--Map/Views/MapTextEditor.swift71
-rw-r--r--Map/Views/Stage.swift161
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
new file mode 100644
index 0000000..b4618d4
--- /dev/null
+++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
Binary files differ
diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
new file mode 100644
index 0000000..dfdf396
--- /dev/null
+++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
Binary files differ
diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
new file mode 100644
index 0000000..fb22da2
--- /dev/null
+++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
Binary files differ
diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
new file mode 100644
index 0000000..a2ea184
--- /dev/null
+++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
Binary files differ
diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
new file mode 100644
index 0000000..cbe1cfc
--- /dev/null
+++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
Binary files differ
diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
new file mode 100644
index 0000000..64bc6b5
--- /dev/null
+++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
Binary files differ
diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
new file mode 100644
index 0000000..a2ea184
--- /dev/null
+++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
Binary files differ
diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
new file mode 100644
index 0000000..5663b5e
--- /dev/null
+++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
Binary files differ
diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
new file mode 100644
index 0000000..64bc6b5
--- /dev/null
+++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
Binary files differ
diff --git a/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
new file mode 100644
index 0000000..bfa4365
--- /dev/null
+++ b/Map/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
Binary files differ
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 }
+}