]> git.r.bdr.sh - rbdr/map/commitdiff
Release 1.1.0 1.1.0
authorRuben Beltran del Rio <redacted>
Thu, 4 Feb 2021 22:46:55 +0000 (23:46 +0100)
committerRuben Beltran del Rio <redacted>
Thu, 4 Feb 2021 22:46:55 +0000 (23:46 +0100)
20 files changed:
Map.xcodeproj/project.pbxproj
Map/Debouncer.swift [new file with mode: 0644]
Map/Extensions/Map+parse.swift
Map/MapColor.swift [moved from Map/MapRenderComponents/MapColor.swift with 59% similarity]
Map/MapParser/MapParser.swift [new file with mode: 0644]
Map/MapParser/Strategies/BlockerParserStrategy.swift [new file with mode: 0644]
Map/MapParser/Strategies/EdgeParserStrategy.swift [new file with mode: 0644]
Map/MapParser/Strategies/OpportunityParserStrategy.swift [new file with mode: 0644]
Map/MapParser/Strategies/StageParserStrategy.swift [new file with mode: 0644]
Map/MapParser/Strategies/VertexParserStrategy.swift [new file with mode: 0644]
Map/MapRenderComponents/MapAxes.swift
Map/MapRenderComponents/MapVertices.swift
Map/State/AppState.swift
Map/State/Stage.swift [moved from Map/Views/Stage.swift with 51% similarity]
Map/Views/ContentView.swift
Map/Views/DefaultMapView.swift
Map/Views/EvolutionPicker.swift [new file with mode: 0644]
Map/Views/MapDetail.swift
Map/Views/MapRender.swift
Map/Views/MapTextEditor.swift

index 275bb9e25733a738c6b66e703e813aaa7a423a84..12a5d1cd1a8d87137cfe730c9498a950e91d20ee 100644 (file)
                B539516C25CB0C9300959F72 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539516B25CB0C9200959F72 /* Store.swift */; };
                B539517425CB0CA400959F72 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539517325CB0CA400959F72 /* AppState.swift */; };
                B539518125CB2D7A00959F72 /* MapTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539518025CB2D7A00959F72 /* MapTextEditor.swift */; };
+               B5CF75C925CC19FC003BFF3D /* EvolutionPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75C825CC19FC003BFF3D /* EvolutionPicker.swift */; };
+               B5CF75CF25CC7965003BFF3D /* MapParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75CE25CC7965003BFF3D /* MapParser.swift */; };
+               B5CF75D825CC79BC003BFF3D /* VertexParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75D725CC79BC003BFF3D /* VertexParserStrategy.swift */; };
+               B5CF75DD25CC79D7003BFF3D /* EdgeParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75DC25CC79D7003BFF3D /* EdgeParserStrategy.swift */; };
+               B5CF75E225CC79ED003BFF3D /* BlockerParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75E125CC79ED003BFF3D /* BlockerParserStrategy.swift */; };
+               B5CF75EA25CC7A13003BFF3D /* OpportunityParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75E925CC7A13003BFF3D /* OpportunityParserStrategy.swift */; };
+               B5CF75EF25CC7A4A003BFF3D /* StageParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75EE25CC7A4A003BFF3D /* StageParserStrategy.swift */; };
+               B5CF75F725CC97CA003BFF3D /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75F625CC97CA003BFF3D /* Debouncer.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
                B539516B25CB0C9200959F72 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
                B539517325CB0CA400959F72 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
                B539518025CB2D7A00959F72 /* MapTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTextEditor.swift; sourceTree = "<group>"; };
+               B5CF75C825CC19FC003BFF3D /* EvolutionPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvolutionPicker.swift; sourceTree = "<group>"; };
+               B5CF75CE25CC7965003BFF3D /* MapParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapParser.swift; sourceTree = "<group>"; };
+               B5CF75D725CC79BC003BFF3D /* VertexParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VertexParserStrategy.swift; sourceTree = "<group>"; };
+               B5CF75DC25CC79D7003BFF3D /* EdgeParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeParserStrategy.swift; sourceTree = "<group>"; };
+               B5CF75E125CC79ED003BFF3D /* BlockerParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockerParserStrategy.swift; sourceTree = "<group>"; };
+               B5CF75E925CC7A13003BFF3D /* OpportunityParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpportunityParserStrategy.swift; sourceTree = "<group>"; };
+               B5CF75EE25CC7A4A003BFF3D /* StageParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StageParserStrategy.swift; sourceTree = "<group>"; };
+               B5CF75F625CC97CA003BFF3D /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
                                B523C76625CA071B00C44061 /* MapVertices.swift */,
                                B523C76B25CA0DFA00C44061 /* MapEdges.swift */,
                                B523C77025CA121300C44061 /* MapBlockers.swift */,
-                               B523C77525CA181100C44061 /* MapColor.swift */,
                                B523C77D25CA294C00C44061 /* MapOpportunities.swift */,
                        );
                        path = MapRenderComponents;
                B526257025C874F9003E73B7 /* Map */ = {
                        isa = PBXGroup;
                        children = (
+                               B523C77525CA181100C44061 /* MapColor.swift */,
+                               B5CF75CD25CC7953003BFF3D /* MapParser */,
                                B539516A25CB0C7800959F72 /* State */,
                                B52625B425C87D54003E73B7 /* Extensions */,
                                B539517925CB0D6100959F72 /* Views */,
                                B526258025C874FA003E73B7 /* Map.entitlements */,
                                B526257C25C874FA003E73B7 /* Map.xcdatamodeld */,
                                B526257725C874FA003E73B7 /* Preview Content */,
+                               B5CF75F625CC97CA003BFF3D /* Debouncer.swift */,
                        );
                        path = Map;
                        sourceTree = "<group>";
                B539516A25CB0C7800959F72 /* State */ = {
                        isa = PBXGroup;
                        children = (
+                               B52625C525C8BD2A003E73B7 /* Stage.swift */,
                                B526257A25C874FA003E73B7 /* Persistence.swift */,
                                B539516B25CB0C9200959F72 /* Store.swift */,
                                B539517325CB0CA400959F72 /* AppState.swift */,
                                B526257125C874F9003E73B7 /* MapApp.swift */,
                                B526257325C874F9003E73B7 /* ContentView.swift */,
                                B52625A525C876C3003E73B7 /* MapDetail.swift */,
-                               B52625C525C8BD2A003E73B7 /* Stage.swift */,
                                B523C74A25C9C1BA00C44061 /* DefaultMapView.swift */,
                                B539518025CB2D7A00959F72 /* MapTextEditor.swift */,
                                B52625AF25C87C14003E73B7 /* MapRender.swift */,
+                               B5CF75C825CC19FC003BFF3D /* EvolutionPicker.swift */,
                        );
                        path = Views;
                        sourceTree = "<group>";
                };
+               B5CF75CD25CC7953003BFF3D /* MapParser */ = {
+                       isa = PBXGroup;
+                       children = (
+                               B5CF75D625CC79A4003BFF3D /* Strategies */,
+                               B5CF75CE25CC7965003BFF3D /* MapParser.swift */,
+                       );
+                       path = MapParser;
+                       sourceTree = "<group>";
+               };
+               B5CF75D625CC79A4003BFF3D /* Strategies */ = {
+                       isa = PBXGroup;
+                       children = (
+                               B5CF75D725CC79BC003BFF3D /* VertexParserStrategy.swift */,
+                               B5CF75DC25CC79D7003BFF3D /* EdgeParserStrategy.swift */,
+                               B5CF75E125CC79ED003BFF3D /* BlockerParserStrategy.swift */,
+                               B5CF75E925CC7A13003BFF3D /* OpportunityParserStrategy.swift */,
+                               B5CF75EE25CC7A4A003BFF3D /* StageParserStrategy.swift */,
+                       );
+                       path = Strategies;
+                       sourceTree = "<group>";
+               };
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
                                B52625B025C87C14003E73B7 /* MapRender.swift in Sources */,
                                B523C77E25CA294C00C44061 /* MapOpportunities.swift in Sources */,
                                B52625AB25C87909003E73B7 /* Date+format.swift in Sources */,
+                               B5CF75D825CC79BC003BFF3D /* VertexParserStrategy.swift in Sources */,
+                               B5CF75EA25CC7A13003BFF3D /* OpportunityParserStrategy.swift in Sources */,
                                B523C77125CA121300C44061 /* MapBlockers.swift in Sources */,
                                B523C76725CA071B00C44061 /* MapVertices.swift in Sources */,
+                               B5CF75CF25CC7965003BFF3D /* MapParser.swift in Sources */,
+                               B5CF75E225CC79ED003BFF3D /* BlockerParserStrategy.swift in Sources */,
                                B539517425CB0CA400959F72 /* AppState.swift in Sources */,
                                B523C75A25C9FD4900C44061 /* MapAxes.swift in Sources */,
                                B539518125CB2D7A00959F72 /* MapTextEditor.swift in Sources */,
                                B52625C625C8BD2A003E73B7 /* Stage.swift in Sources */,
                                B523C73D25C98D9800C44061 /* NSImage+writePNG.swift in Sources */,
                                B526257B25C874FA003E73B7 /* Persistence.swift in Sources */,
+                               B5CF75EF25CC7A4A003BFF3D /* StageParserStrategy.swift in Sources */,
                                B523C77625CA181100C44061 /* MapColor.swift in Sources */,
+                               B5CF75DD25CC79D7003BFF3D /* EdgeParserStrategy.swift in Sources */,
                                B526257425C874F9003E73B7 /* ContentView.swift in Sources */,
                                B526257E25C874FA003E73B7 /* Map.xcdatamodeld in Sources */,
+                               B5CF75C925CC19FC003BFF3D /* EvolutionPicker.swift in Sources */,
                                B523C74B25C9C1BA00C44061 /* DefaultMapView.swift in Sources */,
                                B539516C25CB0C9300959F72 /* Store.swift in Sources */,
                                B526257225C874F9003E73B7 /* MapApp.swift in Sources */,
                                B52625A625C876C3003E73B7 /* MapDetail.swift in Sources */,
                                B523C76225CA05A300C44061 /* MapStages.swift in Sources */,
+                               B5CF75F725CC97CA003BFF3D /* Debouncer.swift in Sources */,
                                B523C76C25CA0DFA00C44061 /* MapEdges.swift in Sources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                                        "@executable_path/../Frameworks",
                                );
                                MACOSX_DEPLOYMENT_TARGET = 11;
-                               MARKETING_VERSION = 1.0.0;
+                               MARKETING_VERSION = 1.1.0;
                                PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.Map;
                                PRODUCT_NAME = "$(TARGET_NAME)";
                                SWIFT_VERSION = 5.0;
                                        "@executable_path/../Frameworks",
                                );
                                MACOSX_DEPLOYMENT_TARGET = 11;
-                               MARKETING_VERSION = 1.0.0;
+                               MARKETING_VERSION = 1.1.0;
                                PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.Map;
                                PRODUCT_NAME = "$(TARGET_NAME)";
                                SWIFT_VERSION = 5.0;
diff --git a/Map/Debouncer.swift b/Map/Debouncer.swift
new file mode 100644 (file)
index 0000000..f119d8a
--- /dev/null
@@ -0,0 +1,21 @@
+import Foundation
+
+class Debouncer {
+
+  // MARK: - Properties
+  private let queue = DispatchQueue.main
+  private var workItem = DispatchWorkItem(block: {})
+  private var interval: TimeInterval
+
+  // MARK: - Initializer
+  init(seconds: TimeInterval) {
+    self.interval = seconds
+  }
+
+  // MARK: - Debouncing function
+  func debounce(action: @escaping (() -> Void)) {
+    workItem.cancel()
+    workItem = DispatchWorkItem(block: { action() })
+    queue.asyncAfter(deadline: .now() + interval, execute: workItem)
+  }
+}
index d82ba99173f534750026aacbb4f01362d00a84f2..904f492cb49a063b0726b144b39c9caf305c375e 100644 (file)
-import CoreGraphics
-import Foundation
-
-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 blockers: [Blocker]
-  let opportunities: [Opportunity]
-  let stages: [CGFloat]
-}
-
-struct Vertex {
-  let id: Int
-  let label: String
-  let position: CGPoint
-}
-
-struct MapEdge {
-  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: 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
-}
-
-func parseEdges(_ text: String, vertices: [String: CGPoint]) -> [MapEdge] {
-
-  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))
+extension Map {
+  static func parse(content: String) -> ParsedMap {
+
+    let parsers = [
+      AnyMapParserStrategy(VertexParserStrategy()),
+      AnyMapParserStrategy(EdgeParserStrategy()),
+      AnyMapParserStrategy(BlockerParserStrategy()),
+      AnyMapParserStrategy(OpportunityParserStrategy()),
+      AnyMapParserStrategy(StageParserStrategy()),
+    ]
+    let builder = MapBuilder()
+
+    let lines = content.split(whereSeparator: \.isNewline)
+
+    for (index, line) in lines.enumerated() {
+      for parser in parsers {
+        if parser.canHandle(line: String(line)) {
+          let (type, object) = parser.handle(
+            index: index, line: String(line), vertices: builder.vertices)
+          builder.addObjectToMap(type: type, object: object)
+          break
         }
       }
     }
-  }
-
-  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)
-  }
-}
-
-extension Map {
-  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)
+    return builder.build()
   }
 }
similarity index 59%
rename from Map/MapRenderComponents/MapColor.swift
rename to Map/MapColor.swift
index ce3d4533d8d46f7c515508eb4caf82328cfbe2e3..98dd80484b1135275e5e82760d317f9743cd1c77 100644 (file)
@@ -7,6 +7,7 @@ struct MapColor {
   let blocker: Color
   let opportunity: Color
   let stages: StageColor
+  let syntax: SyntaxColor
 
   static func colorForScheme(_ colorScheme: ColorScheme) -> MapColor {
     if colorScheme == .dark {
@@ -20,7 +21,13 @@ struct MapColor {
           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)))
+          iv: Color(.sRGB, red: 0.14, green: 0.22, blue: 0.31)),
+        syntax: SyntaxColor(
+          vertex: NSColor(srgbRed: 0.41, green: 0.84, blue: 0.96, alpha: 1.0),
+          number: NSColor(srgbRed: 0.85, green: 0.78, blue: 0.49, alpha: 1.0),
+          option: NSColor(srgbRed: 1.0, green: 0.48, blue: 0.7, alpha: 1.0),  // #FE7AB3
+          symbol: NSColor(srgbRed: 0.85, green: 0.73, blue: 1.0, alpha: 1.0)  // #DABBFF
+        ))
     } else {
       return MapColor(
         foreground: Color(.sRGB, red: 0.13, green: 0.13, blue: 0.13),
@@ -32,7 +39,13 @@ struct MapColor {
           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)))
+          iv: Color(.sRGB, red: 0.93, green: 0.96, blue: 1.00)),
+        syntax: SyntaxColor(
+          vertex: NSColor(srgbRed: 0.11, green: 0.42, blue: 0.57, alpha: 1.0),
+          number: NSColor(srgbRed: 0.27, green: 0.31, blue: 0.87, alpha: 1.0),
+          option: NSColor(srgbRed: 0.68, green: 0.24, blue: 0.64, alpha: 1.0),
+          symbol: NSColor(srgbRed: 0.29, green: 0.13, blue: 0.69, alpha: 1.0)
+        ))
     }
   }
 }
@@ -43,3 +56,10 @@ struct StageColor {
   let iii: Color
   let iv: Color
 }
+
+struct SyntaxColor {
+  let vertex: NSColor
+  let number: NSColor
+  let option: NSColor
+  let symbol: NSColor
+}
diff --git a/Map/MapParser/MapParser.swift b/Map/MapParser/MapParser.swift
new file mode 100644 (file)
index 0000000..3528eb5
--- /dev/null
@@ -0,0 +1,139 @@
+import CoreGraphics
+import Foundation
+
+// MARK: - Types
+
+struct MapParsingPatterns {
+  static let vertex = try! NSRegularExpression(
+    pattern:
+      "([^\\(]+?)[\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(?:\\[(.*?)\\])?",
+    options: .caseInsensitive)
+  static let edge = try! NSRegularExpression(
+    pattern: "(.+?)[\\s]*-([->])[\\s]*(.+)", options: .caseInsensitive)
+  static let blocker = try! NSRegularExpression(
+    pattern: "\\[(Blocker)\\][\\s]*(.+)", options: .caseInsensitive)
+  static let opportunity = try! NSRegularExpression(
+    pattern: "\\[(Opportunity)\\][\\s]*(.+)[\\s]+([-+])[\\s]*([0-9]+.?[0-9]*)",
+    options: .caseInsensitive)
+  static let stage = try! NSRegularExpression(
+    pattern: "\\[(I{1,3})\\][\\s]*([0-9]+.?[0-9]*)", options: .caseInsensitive)
+}
+
+struct ParsedMap {
+  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
+  var shape: VertexShape = .circle
+}
+
+enum VertexShape: String {
+  case circle
+  case square
+  case triangle
+  case x
+}
+
+struct MapEdge {
+  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
+}
+
+struct StageDimensions {
+  let index: Int
+  let dimensions: CGFloat
+}
+
+private let defaultDimensions: [CGFloat] = [
+  25.0,
+  50.0,
+  75.0,
+]
+
+// MARK: - MapParserStrategy protocol
+
+protocol MapParserStrategy {
+  func canHandle(line: String) -> Bool
+  func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any)
+}
+
+struct AnyMapParserStrategy: MapParserStrategy {
+
+  private let base: MapParserStrategy
+
+  init<T: MapParserStrategy>(_ base: T) {
+    self.base = base
+  }
+
+  func canHandle(line: String) -> Bool {
+    return base.canHandle(line: line)
+  }
+  func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) {
+    return base.handle(index: index, line: line, vertices: vertices)
+  }
+}
+
+// MARK: - Map Builder
+
+class MapBuilder {
+  var vertices: [String: Vertex] = [:]
+  private var edges: [MapEdge] = []
+  private var blockers: [Blocker] = []
+  private var opportunities: [Opportunity] = []
+  private var stages: [CGFloat] = defaultDimensions
+
+  func addObjectToMap(type: Any.Type, object: Any) {
+    if type == Vertex.self {
+      let vertex = object as! Vertex
+      vertices[vertex.label] = vertex
+    }
+
+    if type == MapEdge.self {
+      let edge = object as! MapEdge
+      edges.append(edge)
+    }
+
+    if type == Blocker.self {
+      let blocker = object as! Blocker
+      blockers.append(blocker)
+    }
+
+    if type == Opportunity.self {
+      let opportunity = object as! Opportunity
+      opportunities.append(opportunity)
+    }
+
+    if type == StageDimensions.self {
+      let stageDimensions = object as! StageDimensions
+      stages[stageDimensions.index] = stageDimensions.dimensions
+
+    }
+  }
+
+  func build() -> ParsedMap {
+    let mappedVertices = vertices.map { label, vertex in return vertex }
+    return ParsedMap(
+      vertices: mappedVertices, edges: edges, blockers: blockers, opportunities: opportunities,
+      stages: stages)
+  }
+}
diff --git a/Map/MapParser/Strategies/BlockerParserStrategy.swift b/Map/MapParser/Strategies/BlockerParserStrategy.swift
new file mode 100644 (file)
index 0000000..d9b6f22
--- /dev/null
@@ -0,0 +1,26 @@
+import Foundation
+
+struct BlockerParserStrategy: MapParserStrategy {
+  private let regex = MapParsingPatterns.blocker
+
+  func canHandle(line: String) -> Bool {
+    let range = NSRange(location: 0, length: line.utf16.count)
+    let matches = regex.matches(in: String(line), options: [], range: range)
+    return matches.count > 0 && matches[0].numberOfRanges == 3
+  }
+
+  func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) {
+    let range = NSRange(location: 0, length: line.utf16.count)
+    let matches = regex.matches(in: String(line), options: [], range: range)
+
+    let match = matches[0]
+    let vertexA = String(line[Range(match.range(at: 2), in: line)!])
+
+    if let vertex = vertices[vertexA] {
+      let blocker = Blocker(id: index, position: vertex.position)
+      return (Blocker.self, blocker)
+    }
+
+    return (NSObject.self, NSObject())  // No matching object
+  }
+}
diff --git a/Map/MapParser/Strategies/EdgeParserStrategy.swift b/Map/MapParser/Strategies/EdgeParserStrategy.swift
new file mode 100644 (file)
index 0000000..0df1ff4
--- /dev/null
@@ -0,0 +1,32 @@
+import Foundation
+
+struct EdgeParserStrategy: MapParserStrategy {
+  private let regex = MapParsingPatterns.edge
+
+  func canHandle(line: String) -> Bool {
+    let range = NSRange(location: 0, length: line.utf16.count)
+    let matches = regex.matches(in: String(line), options: [], range: range)
+    return matches.count > 0 && matches[0].numberOfRanges == 4
+  }
+
+  func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) {
+    let range = NSRange(location: 0, length: line.utf16.count)
+    let matches = regex.matches(in: String(line), options: [], range: range)
+
+    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] {
+        let edge = MapEdge(
+          id: index, origin: origin.position, destination: destination.position,
+          arrowhead: arrowhead)
+        return (MapEdge.self, edge)
+      }
+    }
+
+    return (NSObject.self, NSObject())  // No matching object
+  }
+}
diff --git a/Map/MapParser/Strategies/OpportunityParserStrategy.swift b/Map/MapParser/Strategies/OpportunityParserStrategy.swift
new file mode 100644 (file)
index 0000000..104fd30
--- /dev/null
@@ -0,0 +1,33 @@
+import Foundation
+
+struct OpportunityParserStrategy: MapParserStrategy {
+  private let regex = MapParsingPatterns.opportunity
+
+  func canHandle(line: String) -> Bool {
+    let range = NSRange(location: 0, length: line.utf16.count)
+    let matches = regex.matches(in: String(line), options: [], range: range)
+    return matches.count > 0 && matches[0].numberOfRanges == 5
+  }
+
+  func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) {
+    let range = NSRange(location: 0, length: line.utf16.count)
+    let matches = regex.matches(in: String(line), options: [], range: range)
+
+    let match = matches[0]
+    let multiplier = CGFloat(
+      String(line[Range(match.range(at: 3), in: line)!]) == "-" ? -1.0 : 1.0)
+    let vertex = String(line[Range(match.range(at: 2), in: line)!])
+    let opportunityString = String(line[Range(match.range(at: 4), in: line)!])
+    let opportunity = CGFloat(
+      truncating: NumberFormatter().number(from: opportunityString) ?? 0.0)
+
+    if let origin = vertices[vertex] {
+      let destination = CGPoint(
+        x: origin.position.x + opportunity * multiplier, y: origin.position.y)
+      let opportunity = Opportunity(id: index, origin: origin.position, destination: destination)
+      return (Opportunity.self, opportunity)
+    }
+
+    return (NSObject.self, NSObject())  // No matching object
+  }
+}
diff --git a/Map/MapParser/Strategies/StageParserStrategy.swift b/Map/MapParser/Strategies/StageParserStrategy.swift
new file mode 100644 (file)
index 0000000..42535fc
--- /dev/null
@@ -0,0 +1,24 @@
+import Foundation
+
+struct StageParserStrategy: MapParserStrategy {
+  private let regex = MapParsingPatterns.stage
+
+  func canHandle(line: String) -> Bool {
+    let range = NSRange(location: 0, length: line.utf16.count)
+    let matches = regex.matches(in: String(line), options: [], range: range)
+    return matches.count > 0 && matches[0].numberOfRanges == 3
+  }
+
+  func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) {
+    let range = NSRange(location: 0, length: line.utf16.count)
+    let matches = regex.matches(in: String(line), options: [], range: range)
+
+    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)
+
+    let stageDimensions = StageDimensions(index: stage.count - 1, dimensions: dimensions)
+    return (StageDimensions.self, stageDimensions)
+  }
+}
diff --git a/Map/MapParser/Strategies/VertexParserStrategy.swift b/Map/MapParser/Strategies/VertexParserStrategy.swift
new file mode 100644 (file)
index 0000000..10c94a2
--- /dev/null
@@ -0,0 +1,36 @@
+import Foundation
+
+struct VertexParserStrategy: MapParserStrategy {
+  private let regex = MapParsingPatterns.vertex
+
+  func canHandle(line: String) -> Bool {
+    let range = NSRange(location: 0, length: line.utf16.count)
+    let matches = regex.matches(in: String(line), options: [], range: range)
+    return matches.count > 0 && matches[0].numberOfRanges >= 4
+  }
+
+  func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) {
+    let range = NSRange(location: 0, length: line.utf16.count)
+    let matches = regex.matches(in: String(line), options: [], range: range)
+
+    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)
+
+    var vertex = Vertex(
+      id: index,
+      label: key,
+      position: CGPoint(x: x, y: y)
+    )
+
+    if let range = Range(match.range(at: 4), in: line) {
+      let shapeString = String(line[range])
+      vertex.shape = VertexShape(rawValue: shapeString.lowercased()) ?? .circle
+    }
+
+    return (Vertex.self, vertex)
+  }
+}
index e7d65f4677afb1ff642bfd9e9eecee958cdc51a3..a6e2f870074800b7ef41a8bd1fcad15ab4d1ed6e 100644 (file)
@@ -8,7 +8,7 @@ struct MapAxes: View {
   let lineWidth: CGFloat
   let evolution: Stage
   let stages: [CGFloat]
-  let stageHeight = CGFloat(50.0)
+  let stageHeight = CGFloat(100.0)
   let padding = CGFloat(5.0)
 
   var color: Color {
@@ -40,13 +40,13 @@ struct MapAxes: View {
       Text("Uncharted")
         .font(.title3)
         .foregroundColor(color)
-        .frame(width: mapSize.width / 4, height: stageHeight, alignment: .topLeading)
-        .offset(CGSize(width: 0.0, height: -stageHeight / 2.0))
+        .frame(width: mapSize.width / 4, height: stageHeight / 2.0, alignment: .topLeading)
+        .offset(CGSize(width: 0.0, height: -stageHeight / 4.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))
+        .frame(width: mapSize.width / 4, height: stageHeight / 2.0, alignment: .topLeading)
+        .offset(CGSize(width: mapSize.width - 100.0, height: -stageHeight / 4.0))
 
       Text(evolution.i)
         .font(.title3)
index 71f85be82a1b477d71db522c771db4efb21cd84f..24d49b8095061abc8567298fcaa3828c50128b96 100644 (file)
@@ -15,12 +15,7 @@ struct MapVertices: View {
 
   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)
+      getVertexShape(vertex).fill(color.foreground)
       Text(vertex.label).foregroundColor(color.secondary).offset(
         CGSize(
           width: w(vertex.position.x) + vertexSize.width + padding,
@@ -35,6 +30,52 @@ struct MapVertices: View {
   func w(_ dimension: CGFloat) -> CGFloat {
     max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0))
   }
+
+  func getVertexShape(_ vertex: Vertex) -> Path {
+    switch vertex.shape {
+    case .circle:
+      return Path { path in
+        path.addEllipse(
+          in: CGRect(
+            origin: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y)), size: vertexSize
+          ))
+      }
+    case .square:
+      return Path { path in
+        path.addRect(
+          CGRect(
+            x: w(vertex.position.x), y: h(vertex.position.y), width: vertexSize.width,
+            height: vertexSize.height
+          ))
+      }
+    case .triangle:
+      return Path { path in
+        path.move(to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y) + vertexSize.height))
+        path.addLine(
+          to: CGPoint(
+            x: w(vertex.position.x) + vertexSize.width, y: h(vertex.position.y) + vertexSize.height)
+        )
+        path.addLine(
+          to: CGPoint(x: w(vertex.position.x) + vertexSize.width / 2.0, y: h(vertex.position.y)))
+        path.addLine(
+          to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y) + vertexSize.height))
+        path.closeSubpath()
+      }
+    case .x:
+      return Path { path in
+        path.move(to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y)))
+        path.addLine(
+          to: CGPoint(
+            x: w(vertex.position.x) + vertexSize.width, y: h(vertex.position.y) + vertexSize.height)
+        )
+        path.closeSubpath()
+        path.move(to: CGPoint(x: w(vertex.position.x) + vertexSize.width, y: h(vertex.position.y)))
+        path.addLine(
+          to: CGPoint(x: w(vertex.position.x), y: h(vertex.position.y) + vertexSize.height))
+        path.closeSubpath()
+      }.strokedPath(StrokeStyle(lineWidth: 5.0, lineCap: .butt))
+    }
+  }
 }
 
 struct MapVertices_Previews: PreviewProvider {
index e10efea00ab5ec1eeed8989fae02345d436c3789..cbc9ba6223c4329ca107dea8b89205d5c8f75001 100644 (file)
@@ -3,14 +3,13 @@ import Foundation
 import SwiftUI
 
 struct AppState {
-  var selectedMap: Map? = nil
+  var selectedEvolution: StageType = .general
   var mapBeingDeleted: Map? = nil
 }
 
 enum AppAction {
-  case selectMap(map: Map?)
-  case deleteMap(map: Map)
-  case exportMapAsImage(map: Map, evolution: StageType)
+  case selectEvolution(evolution: StageType)
+  case exportMapAsImage(map: Map)
   case exportMapAsText(map: Map)
 }
 
@@ -18,16 +17,10 @@ func appStateReducer(state: inout AppState, action: AppAction) {
 
   switch action {
 
-  case .selectMap(let map):
-    state.selectedMap = map
+  case .selectEvolution(let evolution):
+    state.selectedEvolution = evolution
 
-  case .deleteMap(let map):
-    let context = PersistenceController.shared.container.viewContext
-
-    context.delete(map)
-    try? context.save()
-
-  case .exportMapAsImage(let map, let evolution):
+  case .exportMapAsImage(let map):
     let window = NSWindow(
       contentRect: .init(
         origin: .zero,
@@ -44,7 +37,8 @@ func appStateReducer(state: inout AppState, action: AppAction) {
     window.isMovableByWindowBackground = true
     window.makeKeyAndOrderFront(nil)
 
-    let renderView = MapRenderView(map: map, evolution: Stage.stages(evolution))
+    let renderView = MapRenderView(
+      content: map.content ?? "", evolution: Stage.stages(state.selectedEvolution))
 
     let view = NSHostingView(rootView: renderView)
     window.contentView = view
similarity index 51%
rename from Map/Views/Stage.swift
rename to Map/State/Stage.swift
index 23432ad5382aea691d4021232d4a03b7ff480410..26b1929b1122dd4ea24c214112663aa2a97a2ee9 100644 (file)
@@ -8,7 +8,7 @@ struct Stage {
     switch type {
     case .general:
       return Stage(
-        i: "Genesis", ii: "Custom Built", iii: "Product (+rental)", iv: "Commodity (+utility)")
+        i: "Genesis", ii: "Custom", iii: "Product (+rental)", iv: "Commodity (+utility)")
     case .practice:
       return Stage(
         i: "Novel", ii: "Emerging", iii: "Good", iv: "Best")
@@ -20,40 +20,59 @@ struct 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")
+        i: "Rare", ii: "Slowly Increasing", iii: "Rapidly Increasing",
+        iv: "Widespread in the applicable market / ecosystem")
     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)")
+        i: "Poorly Understood / exploring the unknown",
+        ii: "Rapid Increase In Learning / discovery becomes refining",
+        iii: "Rapid increase in use / increasing 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")
+        i:
+          "Describe the wonder of the thing / the discovery of some marvel / a new land / an unknown frontier",
+        ii:
+          "Focused on build / construct / awareness and learning / many models of explanation / no accepted forms / a wild west",
+        iii:
+          "Maintenance / operations / installation / comparison between competing forms / feature analysis",
+        iv: "Focused on use / increasingly an accepted, almost invisible component")
     case .market:
       return Stage(
-        i: "Undefined Market", ii: "Forming Market", iii: "Growing Market", iv: "Mature Market")
+        i: "Undefined Market",
+        ii: "Forming Market / an array of competing forms and models of understanding",
+        iii: "Growing Market / consolidation to a few competing but more accepted forms",
+        iv: "Mature Market / stabilised to an accepted form")
     case .knowledgeManagement:
       return Stage(
-        i: "Uncertain", ii: "Learning on use", iii: "Learning on operation", iv: "Known / accepted")
+        i: "Uncertain", ii: "Learning on use / focused on testing prediction",
+        iii: "Learning on operation / using prediction / verification", 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")
+        i: "Chaotic (non-linear) / domain of the \"crazy\"", ii: "Domain of \"experts\"",
+        iii: "Increasing expectation of use / domain of \"professionals\"",
+        iv: "Ordered (appearance of being linear) / trivial / formula to be applied")
     case .userPerception:
       return Stage(
-        i: "Different / confusing / exciting / surprising", ii: "Leading edge / emerging",
-        iii: "Increasingly common / disappointed if not used", iv: "Standard / expected")
+        i: "Different / confusing / exciting / surprising / dangerous",
+        ii: "Leading edge / emerging / unceirtanty over results",
+        iii: "Increasingly common / disappointed if not used or available / feeling left behind",
+        iv: "Standard / expected / feeling of shock if not used")
     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")
+        i: "Future source of competitive advantage / unpredictable / unknown",
+        ii: "Seen as a scompetitive advantage / a differential / ROI / case examples",
+        iii: "Advantage through implementation / features / this model is better than that",
+        iv: "Cost of doing business / accepted / specific defined models")
     case .focusOfValue:
       return Stage(
-        i: "High future worth", ii: "Seeking profit / ROI", iii: "High profitability",
-        iv: "High volume / reducing margin")
+        i: "High future worth but immediate investment",
+        ii: "Seeking ways to profit and a ROI / seeking confirmation of value",
+        iii:
+          "High profitability per unit / a valuable model / a feeling of understanding / focus on exploitation",
+        iv:
+          "High volume / reducing margin / important but invisible / an essential component of something more complex"
+      )
     case .understanding:
       return Stage(
         i: "Poorly Understood / unpredictable",
@@ -64,12 +83,15 @@ struct Stage {
       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")
+        iii: "Competing models / feature difference / evidential support",
+        iv: "Essential / any advantage is operational / accepted norm")
     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")
+        i: "High / tolerated / assumed to be wrong",
+        ii: "Moderate / unsurprising if wrong but disappointed",
+        iii:
+          "Not tolerated / focus on constant improvement / assumed to be in the right direction / resistance to changing the model",
+        iv: "Surprised by failure / focus on operational efficiency")
     case .marketAction:
       return Stage(
         i: "Gambling / driven by gut", ii: "Exploring a \"found\" value",
@@ -140,9 +162,11 @@ enum StageType: String, CaseIterable, Identifiable {
   case practice
   case data
   case knowledge
+
   case ubiquity
   case certainty
   case publicationTypes
+
   case market
   case knowledgeManagement
   case marketPerception
@@ -155,7 +179,17 @@ enum StageType: String, CaseIterable, Identifiable {
   case marketAction
   case efficiency
   case decisionDrivers
+
   case behavior
 
   var id: String { self.rawValue }
+
+  static let types: [StageType] = [.general, .practice, .data, .knowledge]
+  static let characteristics: [StageType] = [.ubiquity, .certainty, .publicationTypes]
+  static let properties: [StageType] = [
+    .market, .knowledgeManagement, .marketPerception, .userPerception,
+    .perceptionInIndustry, .focusOfValue, .understanding, .comparison, .failure,
+    .marketAction, .efficiency, .decisionDrivers,
+  ]
+  static let custom: [StageType] = [.behavior]
 }
index 4f55c27c0680623d2a9be6724fe18c5d64fd0c79..cf1f145ab46c3d840d94fcfd65750269f8c71244 100644 (file)
@@ -11,6 +11,8 @@ import SwiftUI
 struct ContentView: View {
   @Environment(\.managedObjectContext) private var viewContext
 
+  @EnvironmentObject var store: AppStore
+
   @FetchRequest(
     sortDescriptors: [NSSortDescriptor(keyPath: \Map.createdAt, ascending: true)],
     animation: .default)
index 66914f12a245442c81e2dbd3cab9f5638724d252..1f624d2daace7df3e61adf60c7a16a8061e2520a 100644 (file)
@@ -17,7 +17,7 @@ struct DefaultMapView: View {
 
 struct DefaultMapView_Previews: PreviewProvider {
   static var previews: some View {
-    MapDetailView(map: Map()).environment(
+    DefaultMapView().environment(
       \.managedObjectContext, PersistenceController.preview.container.viewContext)
   }
 }
diff --git a/Map/Views/EvolutionPicker.swift b/Map/Views/EvolutionPicker.swift
new file mode 100644 (file)
index 0000000..815f575
--- /dev/null
@@ -0,0 +1,48 @@
+//
+//  EvolutionPicker.swift
+//  Map
+//
+//  Created by Ruben Beltran del Rio on 2/4/21.
+//
+
+import SwiftUI
+
+struct EvolutionPicker: View {
+
+  @EnvironmentObject private var store: AppStore
+
+  private var selectedEvolution: Binding<StageType> {
+    Binding(
+      get: { store.state.selectedEvolution },
+      set: { evolution in
+        store.send(.selectEvolution(evolution: evolution))
+      }
+    )
+  }
+
+  var body: some View {
+    Picker("Evolution", selection: selectedEvolution) {
+      ForEach(StageType.types) { stage in
+        Text(Stage.title(stage)).tag(stage).padding(4.0)
+      }
+      Divider()
+      ForEach(StageType.characteristics) { stage in
+        Text(Stage.title(stage)).tag(stage).padding(4.0)
+      }
+      Divider()
+      ForEach(StageType.properties) { stage in
+        Text(Stage.title(stage)).tag(stage).padding(4.0)
+      }
+      Divider()
+      ForEach(StageType.custom) { stage in
+        Text(Stage.title(stage)).tag(stage).padding(4.0)
+      }
+    }.padding(.horizontal, 8.0).padding(.vertical, 4.0)
+  }
+}
+
+struct EvolutionPicker_Previews: PreviewProvider {
+  static var previews: some View {
+    EvolutionPicker()
+  }
+}
index a8ea3d17fa054611e46f4a5ae66c34299973e6f9..6ba92bb76f9fff10ad9f0ad20cea2c983f41dd1a 100644 (file)
@@ -38,8 +38,6 @@ struct MapDetailView: View {
     )
   }
 
-  @State private var selectedEvolution = StageType.general
-
   var body: some View {
     if map.uuid != nil {
       VSplitView {
@@ -59,14 +57,10 @@ struct MapDetailView: View {
               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)
+          EvolutionPicker()
 
           ZStack(alignment: .topLeading) {
-            MapTextEditor(text: content).onChange(of: map.content) { _ in
+            MapTextEditor(text: content, colorScheme: colorScheme).onChange(of: map.content) { _ in
               try? viewContext.save()
             }
             .background(mapColor.background)
@@ -76,7 +70,8 @@ struct MapDetailView: View {
             5.0)
         }.padding(.horizontal, 8.0)
         ScrollView([.horizontal, .vertical]) {
-          MapRenderView(map: map, evolution: Stage.stages(selectedEvolution))
+          MapRenderView(
+            content: content.wrappedValue, evolution: Stage.stages(store.state.selectedEvolution))
         }.background(mapColor.background)
       }
     } else {
@@ -89,7 +84,7 @@ struct MapDetailView: View {
   }
 
   private func saveImage() {
-    store.send(.exportMapAsImage(map: map, evolution: selectedEvolution))
+    store.send(.exportMapAsImage(map: map))
   }
 }
 
index b35b7241b8ee7cb0a3d750f8065902d119e2804f..7623b6179a7504c6c845c191f54cf08abbedce1e 100644 (file)
@@ -5,6 +5,7 @@
 //  Created by Ruben Beltran del Rio on 2/1/21.
 //
 
+import Combine
 import CoreData
 import CoreGraphics
 import SwiftUI
@@ -13,7 +14,7 @@ struct MapRenderView: View {
 
   @Environment(\.colorScheme) var colorScheme
 
-  @ObservedObject var map: Map
+  var content: String
   let evolution: Stage
 
   let mapSize = CGSize(width: 1300.0, height: 1000.0)
@@ -23,7 +24,7 @@ struct MapRenderView: View {
   let padding = CGFloat(30.0)
 
   var parsedMap: ParsedMap {
-    return map.parse()
+    return Map.parse(content: content)
   }
 
   var body: some View {
@@ -33,7 +34,7 @@ struct MapRenderView: View {
         path.addRect(
           CGRect(
             x: -padding, y: -padding, width: mapSize.width + padding * 2,
-            height: mapSize.height + padding * 2))
+            height: mapSize.height + padding * 4))
       }.fill(MapColor.colorForScheme(colorScheme).background)
 
       MapStages(mapSize: mapSize, lineWidth: lineWidth, stages: parsedMap.stages)
@@ -48,14 +49,14 @@ struct MapRenderView: View {
         mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, edges: parsedMap.edges)
     }.frame(
       width: mapSize.width,
-      height: mapSize.height, alignment: .topLeading
+      height: mapSize.height + 2 * padding, alignment: .topLeading
     ).padding(padding)
   }
 }
 
 struct MapRenderView_Previews: PreviewProvider {
   static var previews: some View {
-    MapDetailView(map: Map()).environment(
+    MapRenderView(content: "", evolution: Stage.stages(.general)).environment(
       \.managedObjectContext, PersistenceController.preview.container.viewContext)
   }
 }
index 7251c59460d8b4c6fe306c688cc5d70796edc033..e030d641e1bd7243c4ee174160859109e0bbacae 100644 (file)
@@ -4,9 +4,19 @@ import SwiftUI
 class MapTextEditorController: NSViewController {
 
   @Binding var text: String
+  var colorScheme: ColorScheme
 
-  init(text: Binding<String>) {
+  private let vertexRegex = MapParsingPatterns.vertex
+  private let edgeRegex = MapParsingPatterns.edge
+  private let blockerRegex = MapParsingPatterns.blocker
+  private let opportunityRegex = MapParsingPatterns.opportunity
+  private let stageRegex = MapParsingPatterns.stage
+
+  private let debouncer: Debouncer = Debouncer(seconds: 0.2)
+
+  init(text: Binding<String>, colorScheme: ColorScheme) {
     self._text = text
+    self.colorScheme = colorScheme
     super.init(nibName: nil, bundle: nil)
   }
 
@@ -21,6 +31,7 @@ class MapTextEditorController: NSViewController {
     scrollView.translatesAutoresizingMaskIntoConstraints = false
 
     textView.delegate = self
+    textView.textStorage?.delegate = self
     textView.string = self.text
     textView.isEditable = true
     textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular)
@@ -30,6 +41,10 @@ class MapTextEditorController: NSViewController {
   override func viewDidAppear() {
     self.view.window?.makeFirstResponder(self.view)
   }
+
+  func updateColorScheme(_ colorScheme: ColorScheme) {
+    self.colorScheme = colorScheme
+  }
 }
 
 extension MapTextEditorController: NSTextViewDelegate {
@@ -53,19 +68,80 @@ extension MapTextEditorController: NSTextViewDelegate {
   }
 }
 
+extension MapTextEditorController: NSTextStorageDelegate {
+  override func textStorageDidProcessEditing(_ obj: Notification) {
+    if let textStorage = obj.object as? NSTextStorage {
+      debouncer.debounce {
+        DispatchQueue.main.async {
+          self.colorizeText(textStorage: textStorage)
+        }
+      }
+    }
+  }
+
+  private func colorizeText(textStorage: NSTextStorage) {
+    let range = NSMakeRange(0, textStorage.length)
+    var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range)
+    let colors = MapColor.colorForScheme(colorScheme)
+
+    for match in matches {
+      textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 1))
+      textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 2))
+      textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 3))
+      textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 4))
+    }
+
+    matches = edgeRegex.matches(in: textStorage.string, options: [], range: range)
+
+    for match in matches {
+      textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 1))
+      let arrowRange = match.range(at: 2)
+      textStorage.addAttributes(
+        [.foregroundColor: colors.syntax.symbol],
+        range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1))
+      textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 3))
+    }
+
+    matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range)
+
+    for match in matches {
+      textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 1))
+      textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 2))
+      textStorage.addAttributes([.foregroundColor: colors.syntax.symbol], range: match.range(at: 3))
+      textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 4))
+    }
+
+    matches = blockerRegex.matches(in: textStorage.string, options: [], range: range)
+
+    for match in matches {
+      textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 1))
+      textStorage.addAttributes([.foregroundColor: colors.syntax.vertex], range: match.range(at: 2))
+    }
+
+    matches = stageRegex.matches(in: textStorage.string, options: [], range: range)
+
+    for match in matches {
+      textStorage.addAttributes([.foregroundColor: colors.syntax.option], range: match.range(at: 1))
+      textStorage.addAttributes([.foregroundColor: colors.syntax.number], range: match.range(at: 2))
+    }
+  }
+}
+
 struct MapTextEditor: NSViewControllerRepresentable {
 
   @Binding var text: String
+  let colorScheme: ColorScheme
 
   func makeNSViewController(
     context: NSViewControllerRepresentableContext<MapTextEditor>
   ) -> MapTextEditorController {
-    return MapTextEditorController(text: $text)
+    return MapTextEditorController(text: $text, colorScheme: colorScheme)
   }
 
   func updateNSViewController(
     _ nsViewController: MapTextEditorController,
     context: NSViewControllerRepresentableContext<MapTextEditor>
   ) {
+    nsViewController.updateColorScheme(context.environment.colorScheme)
   }
 }