]>
Commit | Line | Data |
---|---|---|
1 | // Copyright (C) 2024 Rubén Beltrán del Río | |
2 | ||
3 | // This program is free software: you can redistribute it and/or modify | |
4 | // it under the terms of the GNU General Public License as published by | |
5 | // the Free Software Foundation, either version 3 of the License, or | |
6 | // (at your option) any later version. | |
7 | ||
8 | // This program is distributed in the hope that it will be useful, | |
9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of | |
10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
11 | // GNU General Public License for more details. | |
12 | ||
13 | // You should have received a copy of the GNU General Public License | |
14 | // along with this program. If not, see https://map.tranquil.systems. | |
15 | import CoreGraphics | |
16 | import Foundation | |
17 | ||
18 | struct MapParser { | |
19 | static func parse(content: String) -> ParsedMap { | |
20 | ||
21 | let parsers = [ | |
22 | AnyMapParserStrategy(NoteParserStrategy()), | |
23 | AnyMapParserStrategy(VertexParserStrategy()), | |
24 | AnyMapParserStrategy(EdgeParserStrategy()), | |
25 | AnyMapParserStrategy(BlockerParserStrategy()), | |
26 | AnyMapParserStrategy(OpportunityParserStrategy()), | |
27 | AnyMapParserStrategy(StageParserStrategy()), | |
28 | AnyMapParserStrategy(GroupParserStrategy()), | |
29 | ] | |
30 | let builder = MapBuilder() | |
31 | ||
32 | let lines = content.split(whereSeparator: \.isNewline) | |
33 | ||
34 | for (index, line) in lines.enumerated() { | |
35 | for parser in parsers { | |
36 | if parser.canHandle(line: String(line)) { | |
37 | let (type, object) = parser.handle( | |
38 | index: index, line: String(line), vertices: builder.vertices) | |
39 | builder.addObjectToMap(type: type, object: object) | |
40 | break | |
41 | } | |
42 | } | |
43 | } | |
44 | ||
45 | return builder.build() | |
46 | } | |
47 | } | |
48 | ||
49 | // MARK: - Types | |
50 | ||
51 | struct MapParsingPatterns { | |
52 | static let vertex = try! NSRegularExpression( | |
53 | pattern: | |
54 | "^([^\\(\\[\\]]*?)[\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(?:\\[(.*?)\\])?[\\s]*$", | |
55 | options: [.caseInsensitive, .anchorsMatchLines]) | |
56 | static let edge = try! NSRegularExpression( | |
57 | pattern: "^(.+?)[\\s]*-([->])[\\s]*(.+)", options: [.caseInsensitive, .anchorsMatchLines]) | |
58 | static let blocker = try! NSRegularExpression( | |
59 | pattern: "^\\[(Blocker)\\][\\s]*(.+)", options: [.caseInsensitive, .anchorsMatchLines]) | |
60 | static let opportunity = try! NSRegularExpression( | |
61 | pattern: "^\\[(Evolution)\\][\\s]*(.+)[\\s]+([-+])[\\s]*([0-9]+.?[0-9]*)", | |
62 | options: [.caseInsensitive, .anchorsMatchLines]) | |
63 | static let note = try! NSRegularExpression( | |
64 | pattern: | |
65 | "^\\[(Note)\\][\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(.*)", | |
66 | options: [.caseInsensitive, .anchorsMatchLines]) | |
67 | static let stage = try! NSRegularExpression( | |
68 | pattern: "^\\[(I{1,3})\\][\\s]*([0-9]+.?[0-9]*)", | |
69 | options: [.caseInsensitive, .anchorsMatchLines]) | |
70 | static let group = try! NSRegularExpression( | |
71 | pattern: "^\\[(Group)\\][\\s]*(.+)", options: [.caseInsensitive, .anchorsMatchLines]) | |
72 | } | |
73 | ||
74 | struct ParsedMap { | |
75 | let vertices: [Vertex] | |
76 | let edges: [MapEdge] | |
77 | let blockers: [Blocker] | |
78 | let opportunities: [Opportunity] | |
79 | let notes: [Note] | |
80 | let stages: [CGFloat] | |
81 | let groups: [[Vertex]] | |
82 | ||
83 | static let empty: ParsedMap = ParsedMap( | |
84 | vertices: [], edges: [], blockers: [], opportunities: [], notes: [], stages: defaultDimensions, | |
85 | groups: []) | |
86 | } | |
87 | ||
88 | struct Vertex: Identifiable, Hashable { | |
89 | let id: Int | |
90 | let label: String | |
91 | let position: CGPoint | |
92 | var shape: VertexShape = .circle | |
93 | } | |
94 | ||
95 | struct Note { | |
96 | let id: Int | |
97 | let position: CGPoint | |
98 | let text: String | |
99 | } | |
100 | ||
101 | enum VertexShape: String { | |
102 | case circle | |
103 | case square | |
104 | case triangle | |
105 | case x | |
106 | } | |
107 | ||
108 | struct MapEdge { | |
109 | let id: Int | |
110 | let origin: CGPoint | |
111 | let destination: CGPoint | |
112 | let arrowhead: Bool | |
113 | } | |
114 | ||
115 | struct Blocker { | |
116 | let id: Int | |
117 | let position: CGPoint | |
118 | } | |
119 | ||
120 | struct Opportunity { | |
121 | let id: Int | |
122 | let origin: CGPoint | |
123 | let destination: CGPoint | |
124 | } | |
125 | ||
126 | struct StageDimensions { | |
127 | let index: Int | |
128 | let dimensions: CGFloat | |
129 | } | |
130 | ||
131 | private let defaultDimensions: [CGFloat] = [ | |
132 | 25.0, | |
133 | 50.0, | |
134 | 75.0, | |
135 | ] | |
136 | ||
137 | // MARK: - MapParserStrategy protocol | |
138 | ||
139 | protocol MapParserStrategy { | |
140 | func canHandle(line: String) -> Bool | |
141 | func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) | |
142 | } | |
143 | ||
144 | struct AnyMapParserStrategy: MapParserStrategy { | |
145 | ||
146 | private let base: MapParserStrategy | |
147 | ||
148 | init<T: MapParserStrategy>(_ base: T) { | |
149 | self.base = base | |
150 | } | |
151 | ||
152 | func canHandle(line: String) -> Bool { | |
153 | return base.canHandle(line: line) | |
154 | } | |
155 | func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) { | |
156 | return base.handle(index: index, line: line, vertices: vertices) | |
157 | } | |
158 | } | |
159 | ||
160 | // MARK: - Map Builder | |
161 | ||
162 | class MapBuilder { | |
163 | var vertices: [String: Vertex] = [:] | |
164 | private var edges: [MapEdge] = [] | |
165 | private var blockers: [Blocker] = [] | |
166 | private var opportunities: [Opportunity] = [] | |
167 | private var notes: [Note] = [] | |
168 | private var stages: [CGFloat] = defaultDimensions | |
169 | private var groups: [[Vertex]] = [] | |
170 | ||
171 | func addObjectToMap(type: Any.Type, object: Any) { | |
172 | if type == Vertex.self { | |
173 | let vertex = object as! Vertex | |
174 | vertices[vertex.label] = vertex | |
175 | } | |
176 | ||
177 | if type == MapEdge.self { | |
178 | let edge = object as! MapEdge | |
179 | edges.append(edge) | |
180 | } | |
181 | ||
182 | if type == Blocker.self { | |
183 | let blocker = object as! Blocker | |
184 | blockers.append(blocker) | |
185 | } | |
186 | ||
187 | if type == Opportunity.self { | |
188 | let opportunity = object as! Opportunity | |
189 | opportunities.append(opportunity) | |
190 | } | |
191 | ||
192 | if type == Note.self { | |
193 | let note = object as! Note | |
194 | notes.append(note) | |
195 | } | |
196 | ||
197 | if type == StageDimensions.self { | |
198 | let stageDimensions = object as! StageDimensions | |
199 | stages[stageDimensions.index] = stageDimensions.dimensions | |
200 | } | |
201 | ||
202 | if type == [Vertex].self { | |
203 | let group = object as! [Vertex] | |
204 | groups.append(group) | |
205 | } | |
206 | } | |
207 | ||
208 | func build() -> ParsedMap { | |
209 | let mappedVertices = vertices.map { label, vertex in return vertex } | |
210 | return ParsedMap( | |
211 | vertices: mappedVertices, edges: edges, blockers: blockers, opportunities: opportunities, | |
212 | notes: notes, | |
213 | stages: stages, groups: groups) | |
214 | } | |
215 | } |