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)) } } } } 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) } }