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