diff options
| author | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-07-04 17:06:28 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-07-04 17:06:28 +0200 |
| commit | c843d34f56c207abcf4b93e424125eea2dc601e0 (patch) | |
| tree | 415dc2d9be4be31e290e1dd1dedb0b630c5e8fc6 /Map/Business/MapParser | |
| parent | ed10ac191df473c92c4fec495aafa7f569d108c8 (diff) | |
Upgrade to Swift 6
Diffstat (limited to 'Map/Business/MapParser')
8 files changed, 532 insertions, 0 deletions
diff --git a/Map/Business/MapParser/MapParser.swift b/Map/Business/MapParser/MapParser.swift new file mode 100644 index 0000000..e94de77 --- /dev/null +++ b/Map/Business/MapParser/MapParser.swift @@ -0,0 +1,215 @@ +// Copyright (C) 2024 Rubén Beltrán del Río + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see https://map.tranquil.systems. +import CoreGraphics +import Foundation + +struct MapParser { + static func parse(content: String) -> ParsedMap { + + let parsers = [ + AnyMapParserStrategy(NoteParserStrategy()), + AnyMapParserStrategy(VertexParserStrategy()), + AnyMapParserStrategy(EdgeParserStrategy()), + AnyMapParserStrategy(BlockerParserStrategy()), + AnyMapParserStrategy(OpportunityParserStrategy()), + AnyMapParserStrategy(StageParserStrategy()), + AnyMapParserStrategy(GroupParserStrategy()), + ] + 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 builder.build() + } +} + +// MARK: - Types + +struct MapParsingPatterns { + static let vertex = try! NSRegularExpression( + pattern: + "^([^\\(\\[\\]]*?)[\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(?:\\[(.*?)\\])?[\\s]*$", + options: [.caseInsensitive, .anchorsMatchLines]) + static let edge = try! NSRegularExpression( + pattern: "^(.+?)[\\s]*-([->])[\\s]*(.+)", options: [.caseInsensitive, .anchorsMatchLines]) + static let blocker = try! NSRegularExpression( + pattern: "^\\[(Blocker)\\][\\s]*(.+)", options: [.caseInsensitive, .anchorsMatchLines]) + static let opportunity = try! NSRegularExpression( + pattern: "^\\[(Evolution)\\][\\s]*(.+)[\\s]+([-+])[\\s]*([0-9]+.?[0-9]*)", + options: [.caseInsensitive, .anchorsMatchLines]) + static let note = try! NSRegularExpression( + pattern: + "^\\[(Note)\\][\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(.*)", + options: [.caseInsensitive, .anchorsMatchLines]) + static let stage = try! NSRegularExpression( + pattern: "^\\[(I{1,3})\\][\\s]*([0-9]+.?[0-9]*)", + options: [.caseInsensitive, .anchorsMatchLines]) + static let group = try! NSRegularExpression( + pattern: "^\\[(Group)\\][\\s]*(.+)", options: [.caseInsensitive, .anchorsMatchLines]) +} + +struct ParsedMap { + let vertices: [Vertex] + let edges: [MapEdge] + let blockers: [Blocker] + let opportunities: [Opportunity] + let notes: [Note] + let stages: [CGFloat] + let groups: [[Vertex]] + + static let empty: ParsedMap = ParsedMap( + vertices: [], edges: [], blockers: [], opportunities: [], notes: [], stages: defaultDimensions, + groups: []) +} + +struct Vertex: Identifiable, Hashable { + let id: Int + let label: String + let position: CGPoint + var shape: VertexShape = .circle +} + +struct Note { + let id: Int + let position: CGPoint + let text: String +} + +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 notes: [Note] = [] + private var stages: [CGFloat] = defaultDimensions + private var groups: [[Vertex]] = [] + + 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 == Note.self { + let note = object as! Note + notes.append(note) + } + + if type == StageDimensions.self { + let stageDimensions = object as! StageDimensions + stages[stageDimensions.index] = stageDimensions.dimensions + } + + if type == [Vertex].self { + let group = object as! [Vertex] + groups.append(group) + } + } + + func build() -> ParsedMap { + let mappedVertices = vertices.map { label, vertex in return vertex } + return ParsedMap( + vertices: mappedVertices, edges: edges, blockers: blockers, opportunities: opportunities, + notes: notes, + stages: stages, groups: groups) + } +} diff --git a/Map/Business/MapParser/Strategies/BlockerParserStrategy.swift b/Map/Business/MapParser/Strategies/BlockerParserStrategy.swift new file mode 100644 index 0000000..4bd2ce4 --- /dev/null +++ b/Map/Business/MapParser/Strategies/BlockerParserStrategy.swift @@ -0,0 +1,40 @@ +// Copyright (C) 2024 Rubén Beltrán del Río + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see https://map.tranquil.systems. +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/Business/MapParser/Strategies/EdgeParserStrategy.swift b/Map/Business/MapParser/Strategies/EdgeParserStrategy.swift new file mode 100644 index 0000000..cc29b49 --- /dev/null +++ b/Map/Business/MapParser/Strategies/EdgeParserStrategy.swift @@ -0,0 +1,46 @@ +// Copyright (C) 2024 Rubén Beltrán del Río + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see https://map.tranquil.systems. +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/Business/MapParser/Strategies/GroupParserStrategy.swift b/Map/Business/MapParser/Strategies/GroupParserStrategy.swift new file mode 100644 index 0000000..96de365 --- /dev/null +++ b/Map/Business/MapParser/Strategies/GroupParserStrategy.swift @@ -0,0 +1,51 @@ +// Copyright (C) 2024 Rubén Beltrán del Río + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see https://map.tranquil.systems. +import Foundation + +struct GroupParserStrategy: MapParserStrategy { + private let regex = MapParsingPatterns.group + + 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] + var groupVertices: [Vertex] = [] + let vertexIdString = String(line[Range(match.range(at: 2), in: line)!]) + let vertexIds = vertexIdString.split(separator: ",", omittingEmptySubsequences: true).map( + String.init + ).map({ vertexId in + vertexId.trimmingCharacters(in: .whitespacesAndNewlines) + }) + + for vertexId in vertexIds { + if let vertex = vertices[vertexId] { + groupVertices.append(vertex) + } + } + + if groupVertices.count > 0 { + return ([Vertex].self, groupVertices) + } + + return (NSObject.self, NSObject()) + } +} diff --git a/Map/Business/MapParser/Strategies/NoteParserStrategy.swift b/Map/Business/MapParser/Strategies/NoteParserStrategy.swift new file mode 100644 index 0000000..d06c768 --- /dev/null +++ b/Map/Business/MapParser/Strategies/NoteParserStrategy.swift @@ -0,0 +1,45 @@ +// Copyright (C) 2024 Rubén Beltrán del Río + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see https://map.tranquil.systems. +import Foundation + +struct NoteParserStrategy: MapParserStrategy { + private let regex = MapParsingPatterns.note + + 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 text = String(line[Range(match.range(at: 4), 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 note = Note( + id: index, + position: CGPoint(x: x, y: y), + text: text + ) + + return (Note.self, note) + } +} diff --git a/Map/Business/MapParser/Strategies/OpportunityParserStrategy.swift b/Map/Business/MapParser/Strategies/OpportunityParserStrategy.swift new file mode 100644 index 0000000..25e6cc4 --- /dev/null +++ b/Map/Business/MapParser/Strategies/OpportunityParserStrategy.swift @@ -0,0 +1,47 @@ +// Copyright (C) 2024 Rubén Beltrán del Río + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see https://map.tranquil.systems. +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/Business/MapParser/Strategies/StageParserStrategy.swift b/Map/Business/MapParser/Strategies/StageParserStrategy.swift new file mode 100644 index 0000000..6e4a341 --- /dev/null +++ b/Map/Business/MapParser/Strategies/StageParserStrategy.swift @@ -0,0 +1,38 @@ +// Copyright (C) 2024 Rubén Beltrán del Río + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see https://map.tranquil.systems. +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/Business/MapParser/Strategies/VertexParserStrategy.swift b/Map/Business/MapParser/Strategies/VertexParserStrategy.swift new file mode 100644 index 0000000..0ca6fc1 --- /dev/null +++ b/Map/Business/MapParser/Strategies/VertexParserStrategy.swift @@ -0,0 +1,50 @@ +// Copyright (C) 2024 Rubén Beltrán del Río + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see https://map.tranquil.systems. +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) + } +} |