From c843d34f56c207abcf4b93e424125eea2dc601e0 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Fri, 4 Jul 2025 17:06:28 +0200 Subject: Upgrade to Swift 6 --- .../Colors/Theme/JustWhite.colorset/Contents.json | 20 ++ Map/Business/Constants.swift | 19 ++ Map/Business/MapParser/MapParser.swift | 215 +++++++++++++++++++++ .../Strategies/BlockerParserStrategy.swift | 40 ++++ .../MapParser/Strategies/EdgeParserStrategy.swift | 46 +++++ .../MapParser/Strategies/GroupParserStrategy.swift | 51 +++++ .../MapParser/Strategies/NoteParserStrategy.swift | 45 +++++ .../Strategies/OpportunityParserStrategy.swift | 47 +++++ .../MapParser/Strategies/StageParserStrategy.swift | 38 ++++ .../Strategies/VertexParserStrategy.swift | 50 +++++ Map/Business/Stage.swift | 209 ++++++++++++++++++++ Map/Data/Debouncer.swift | 35 ++++ Map/Data/MapDocument.swift | 18 +- Map/Data/Stage.swift | 209 -------------------- Map/Info.plist | 6 + Map/Logic/Constants.swift | 19 -- Map/Logic/Debouncer.swift | 35 ---- Map/Logic/MapParser/MapParser.swift | 215 --------------------- .../Strategies/BlockerParserStrategy.swift | 40 ---- .../MapParser/Strategies/EdgeParserStrategy.swift | 46 ----- .../MapParser/Strategies/GroupParserStrategy.swift | 51 ----- .../MapParser/Strategies/NoteParserStrategy.swift | 45 ----- .../Strategies/OpportunityParserStrategy.swift | 47 ----- .../MapParser/Strategies/StageParserStrategy.swift | 38 ---- .../Strategies/VertexParserStrategy.swift | 50 ----- .../Base Components/EvolutionPicker.swift | 11 +- .../Base Components/MapRender/MapAxes.swift | 16 +- .../Base Components/MapRender/MapNotes.swift | 6 +- .../Base Components/MapRender/MapVertices.swift | 6 +- .../Base Components/MapTextEditor.swift | 2 +- Map/Presentation/Base Components/SearchBar.swift | 6 +- .../MapRender/MapRenderView.swift | 4 +- Map/Presentation/EvolutionPicker.swift | 41 ---- Map/Presentation/MapEditor.swift | 10 +- Map/Presentation/Theme/Color+theme.swift | 17 +- Map/Presentation/Theme/Dimensions.swift | 46 ++++- Map/Presentation/Theme/Font+theme.swift | 41 +++- Map/Presentation/Theme/LibertinusSerif-Regular.otf | Bin 0 -> 337132 bytes Map/Presentation/Theme/Ronzino-Medium.otf | Bin 0 -> 61604 bytes Map/Presentation/Theme/Ronzino-Regular.otf | Bin 0 -> 59968 bytes 40 files changed, 964 insertions(+), 876 deletions(-) create mode 100644 Map/Assets.xcassets/Colors/Theme/JustWhite.colorset/Contents.json create mode 100644 Map/Business/Constants.swift create mode 100644 Map/Business/MapParser/MapParser.swift create mode 100644 Map/Business/MapParser/Strategies/BlockerParserStrategy.swift create mode 100644 Map/Business/MapParser/Strategies/EdgeParserStrategy.swift create mode 100644 Map/Business/MapParser/Strategies/GroupParserStrategy.swift create mode 100644 Map/Business/MapParser/Strategies/NoteParserStrategy.swift create mode 100644 Map/Business/MapParser/Strategies/OpportunityParserStrategy.swift create mode 100644 Map/Business/MapParser/Strategies/StageParserStrategy.swift create mode 100644 Map/Business/MapParser/Strategies/VertexParserStrategy.swift create mode 100644 Map/Business/Stage.swift create mode 100644 Map/Data/Debouncer.swift delete mode 100644 Map/Data/Stage.swift delete mode 100644 Map/Logic/Constants.swift delete mode 100644 Map/Logic/Debouncer.swift delete mode 100644 Map/Logic/MapParser/MapParser.swift delete mode 100644 Map/Logic/MapParser/Strategies/BlockerParserStrategy.swift delete mode 100644 Map/Logic/MapParser/Strategies/EdgeParserStrategy.swift delete mode 100644 Map/Logic/MapParser/Strategies/GroupParserStrategy.swift delete mode 100644 Map/Logic/MapParser/Strategies/NoteParserStrategy.swift delete mode 100644 Map/Logic/MapParser/Strategies/OpportunityParserStrategy.swift delete mode 100644 Map/Logic/MapParser/Strategies/StageParserStrategy.swift delete mode 100644 Map/Logic/MapParser/Strategies/VertexParserStrategy.swift delete mode 100644 Map/Presentation/EvolutionPicker.swift create mode 100644 Map/Presentation/Theme/LibertinusSerif-Regular.otf create mode 100644 Map/Presentation/Theme/Ronzino-Medium.otf create mode 100644 Map/Presentation/Theme/Ronzino-Regular.otf (limited to 'Map') diff --git a/Map/Assets.xcassets/Colors/Theme/JustWhite.colorset/Contents.json b/Map/Assets.xcassets/Colors/Theme/JustWhite.colorset/Contents.json new file mode 100644 index 0000000..97650a1 --- /dev/null +++ b/Map/Assets.xcassets/Colors/Theme/JustWhite.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Map/Business/Constants.swift b/Map/Business/Constants.swift new file mode 100644 index 0000000..1c6f54d --- /dev/null +++ b/Map/Business/Constants.swift @@ -0,0 +1,19 @@ +// 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. + +struct Constants { + static let kMaxZoom = 2.0 + static let kMinZoom = 0.1 +} 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(_ 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) + } +} diff --git a/Map/Business/Stage.swift b/Map/Business/Stage.swift new file mode 100644 index 0000000..4f737a1 --- /dev/null +++ b/Map/Business/Stage.swift @@ -0,0 +1,209 @@ +// 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. +struct Stage { + let i: String + let ii: String + let iii: String + let iv: String + + static func stages(_ type: StageType) -> Stage { + switch type { + case .general: + return Stage( + i: "Genesis", ii: "Custom", iii: "Product (+rental)", iv: "Commodity (+utility)") + case .practice: + return Stage( + i: "Novel", ii: "Emerging", iii: "Good", iv: "Best") + case .data: + return Stage( + i: "Unmodelled", ii: "Divergent", iii: "Convergent", iv: "Modelled") + case .knowledge: + return Stage( + i: "Concept", ii: "Hypothesis", iii: "Theory", iv: "Accepted") + case .ubiquity: + return Stage( + i: "Rare", ii: "Slowly Increasing", iii: "Rapidly Increasing", + iv: "Widespread in the applicable market / ecosystem") + case .certainty: + return Stage( + 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: + "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 / 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 / focused on testing prediction", + iii: "Learning on operation / using prediction / verification", iv: "Known / accepted") + case .marketPerception: + return Stage( + 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 / 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: "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 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", + ii: "Increasing understanding / development of measures", + iii: "Increasing education / constant refinement of needs / measures", + iv: "Believed to be well defined / stable / measurable") + case .comparison: + return Stage( + i: "Constantly changing / a differential / unstable", + ii: "Learning from others / testing the water / some evidential support", + iii: "Competing models / feature difference / evidential support", + iv: "Essential / any advantage is operational / accepted norm") + case .failure: + return Stage( + 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", + iii: "Market analysis / listening to customers", iv: "Metric driven / build what is needed") + case .efficiency: + return Stage( + i: "Reducing the cost of change (experimentation)", ii: "Reducing cost of waste (Learning)", + iii: "Reducing cost of waste (Learning)", iv: "Reducing cost of deviation (Volume)") + case .decisionDrivers: + return Stage( + i: "Heritage / culture", ii: "Analyses & synthesis", iii: "Analyses & synthesis", + iv: "Previous Experience") + case .behavior: + return Stage( + i: "Uncertain when to use", ii: "Learning when to use", iii: "Learning through use", + iv: "Known / common usage") + } + } + + static func title(_ type: StageType) -> String { + switch type { + case .general: + return "Activities" + case .practice: + return "Practice" + case .data: + return "Data" + case .knowledge: + return "Knowledge" + case .ubiquity: + return "Ubiquity" + case .certainty: + return "Certainty" + case .publicationTypes: + return "Publication Types" + case .market: + return "Market" + case .knowledgeManagement: + return "Knowledge Management" + case .marketPerception: + return "Market Perception" + case .userPerception: + return "User Perception" + case .perceptionInIndustry: + return "Perception In Industry" + case .focusOfValue: + return "Focus Of Value" + case .understanding: + return "Understanding" + case .comparison: + return "Comparison" + case .failure: + return "Failure" + case .marketAction: + return "Market Action" + case .efficiency: + return "Efficiency" + case .decisionDrivers: + return "Decision Drivers" + case .behavior: + return "Behavior" + } + } +} + +enum StageType: String, CaseIterable, Identifiable { + case general + case practice + case data + case knowledge + + case ubiquity + case certainty + case publicationTypes + + case market + case knowledgeManagement + case marketPerception + case userPerception + case perceptionInIndustry + case focusOfValue + case understanding + case comparison + case failure + 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] +} diff --git a/Map/Data/Debouncer.swift b/Map/Data/Debouncer.swift new file mode 100644 index 0000000..8d40e99 --- /dev/null +++ b/Map/Data/Debouncer.swift @@ -0,0 +1,35 @@ +// 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 + +class Debouncer { + + // MARK: - Properties + private let queue = DispatchQueue.global(qos: .utility) + 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) + } +} diff --git a/Map/Data/MapDocument.swift b/Map/Data/MapDocument.swift index 1b43efb..3c6d352 100644 --- a/Map/Data/MapDocument.swift +++ b/Map/Data/MapDocument.swift @@ -16,7 +16,7 @@ import SwiftUI import UniformTypeIdentifiers extension UTType { - static var exampleText: UTType { + static var wmap: UTType { UTType(importedAs: "systems.tranquil.map.wmap") } } @@ -24,11 +24,23 @@ extension UTType { struct MapDocument: FileDocument { var text: String - init(text: String = "Hello, world!") { + init( + text: String = """ + Anchor (60, 5) [x] + Node A (78, 23) + Node B (40, 30) [square] + Node C (65, 70) + + Anchor -> Node A + Anchor -> Node B + Node A -> Node C + Node B -> Node C + """ + ) { self.text = text } - static var readableContentTypes: [UTType] { [.exampleText] } + static var readableContentTypes: [UTType] { [.wmap] } init(configuration: ReadConfiguration) throws { guard let data = configuration.file.regularFileContents, diff --git a/Map/Data/Stage.swift b/Map/Data/Stage.swift deleted file mode 100644 index 4f737a1..0000000 --- a/Map/Data/Stage.swift +++ /dev/null @@ -1,209 +0,0 @@ -// 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. -struct Stage { - let i: String - let ii: String - let iii: String - let iv: String - - static func stages(_ type: StageType) -> Stage { - switch type { - case .general: - return Stage( - i: "Genesis", ii: "Custom", iii: "Product (+rental)", iv: "Commodity (+utility)") - case .practice: - return Stage( - i: "Novel", ii: "Emerging", iii: "Good", iv: "Best") - case .data: - return Stage( - i: "Unmodelled", ii: "Divergent", iii: "Convergent", iv: "Modelled") - case .knowledge: - return Stage( - i: "Concept", ii: "Hypothesis", iii: "Theory", iv: "Accepted") - case .ubiquity: - return Stage( - i: "Rare", ii: "Slowly Increasing", iii: "Rapidly Increasing", - iv: "Widespread in the applicable market / ecosystem") - case .certainty: - return Stage( - 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: - "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 / 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 / focused on testing prediction", - iii: "Learning on operation / using prediction / verification", iv: "Known / accepted") - case .marketPerception: - return Stage( - 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 / 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: "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 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", - ii: "Increasing understanding / development of measures", - iii: "Increasing education / constant refinement of needs / measures", - iv: "Believed to be well defined / stable / measurable") - case .comparison: - return Stage( - i: "Constantly changing / a differential / unstable", - ii: "Learning from others / testing the water / some evidential support", - iii: "Competing models / feature difference / evidential support", - iv: "Essential / any advantage is operational / accepted norm") - case .failure: - return Stage( - 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", - iii: "Market analysis / listening to customers", iv: "Metric driven / build what is needed") - case .efficiency: - return Stage( - i: "Reducing the cost of change (experimentation)", ii: "Reducing cost of waste (Learning)", - iii: "Reducing cost of waste (Learning)", iv: "Reducing cost of deviation (Volume)") - case .decisionDrivers: - return Stage( - i: "Heritage / culture", ii: "Analyses & synthesis", iii: "Analyses & synthesis", - iv: "Previous Experience") - case .behavior: - return Stage( - i: "Uncertain when to use", ii: "Learning when to use", iii: "Learning through use", - iv: "Known / common usage") - } - } - - static func title(_ type: StageType) -> String { - switch type { - case .general: - return "Activities" - case .practice: - return "Practice" - case .data: - return "Data" - case .knowledge: - return "Knowledge" - case .ubiquity: - return "Ubiquity" - case .certainty: - return "Certainty" - case .publicationTypes: - return "Publication Types" - case .market: - return "Market" - case .knowledgeManagement: - return "Knowledge Management" - case .marketPerception: - return "Market Perception" - case .userPerception: - return "User Perception" - case .perceptionInIndustry: - return "Perception In Industry" - case .focusOfValue: - return "Focus Of Value" - case .understanding: - return "Understanding" - case .comparison: - return "Comparison" - case .failure: - return "Failure" - case .marketAction: - return "Market Action" - case .efficiency: - return "Efficiency" - case .decisionDrivers: - return "Decision Drivers" - case .behavior: - return "Behavior" - } - } -} - -enum StageType: String, CaseIterable, Identifiable { - case general - case practice - case data - case knowledge - - case ubiquity - case certainty - case publicationTypes - - case market - case knowledgeManagement - case marketPerception - case userPerception - case perceptionInIndustry - case focusOfValue - case understanding - case comparison - case failure - 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] -} diff --git a/Map/Info.plist b/Map/Info.plist index e862423..07f3108 100644 --- a/Map/Info.plist +++ b/Map/Info.plist @@ -2,6 +2,12 @@ + UIAppFonts + + Ronzino-Regular.otf + Ronzino-Medium.otf + LibertinusSerif-Regular.otf + SUEnableInstallerLauncherService SUPublicEDKey diff --git a/Map/Logic/Constants.swift b/Map/Logic/Constants.swift deleted file mode 100644 index 1c6f54d..0000000 --- a/Map/Logic/Constants.swift +++ /dev/null @@ -1,19 +0,0 @@ -// 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. - -struct Constants { - static let kMaxZoom = 2.0 - static let kMinZoom = 0.1 -} diff --git a/Map/Logic/Debouncer.swift b/Map/Logic/Debouncer.swift deleted file mode 100644 index 8d40e99..0000000 --- a/Map/Logic/Debouncer.swift +++ /dev/null @@ -1,35 +0,0 @@ -// 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 - -class Debouncer { - - // MARK: - Properties - private let queue = DispatchQueue.global(qos: .utility) - 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) - } -} diff --git a/Map/Logic/MapParser/MapParser.swift b/Map/Logic/MapParser/MapParser.swift deleted file mode 100644 index e94de77..0000000 --- a/Map/Logic/MapParser/MapParser.swift +++ /dev/null @@ -1,215 +0,0 @@ -// 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(_ 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/Logic/MapParser/Strategies/BlockerParserStrategy.swift b/Map/Logic/MapParser/Strategies/BlockerParserStrategy.swift deleted file mode 100644 index 4bd2ce4..0000000 --- a/Map/Logic/MapParser/Strategies/BlockerParserStrategy.swift +++ /dev/null @@ -1,40 +0,0 @@ -// 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/Logic/MapParser/Strategies/EdgeParserStrategy.swift b/Map/Logic/MapParser/Strategies/EdgeParserStrategy.swift deleted file mode 100644 index cc29b49..0000000 --- a/Map/Logic/MapParser/Strategies/EdgeParserStrategy.swift +++ /dev/null @@ -1,46 +0,0 @@ -// 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/Logic/MapParser/Strategies/GroupParserStrategy.swift b/Map/Logic/MapParser/Strategies/GroupParserStrategy.swift deleted file mode 100644 index 96de365..0000000 --- a/Map/Logic/MapParser/Strategies/GroupParserStrategy.swift +++ /dev/null @@ -1,51 +0,0 @@ -// 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/Logic/MapParser/Strategies/NoteParserStrategy.swift b/Map/Logic/MapParser/Strategies/NoteParserStrategy.swift deleted file mode 100644 index d06c768..0000000 --- a/Map/Logic/MapParser/Strategies/NoteParserStrategy.swift +++ /dev/null @@ -1,45 +0,0 @@ -// 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/Logic/MapParser/Strategies/OpportunityParserStrategy.swift b/Map/Logic/MapParser/Strategies/OpportunityParserStrategy.swift deleted file mode 100644 index 25e6cc4..0000000 --- a/Map/Logic/MapParser/Strategies/OpportunityParserStrategy.swift +++ /dev/null @@ -1,47 +0,0 @@ -// 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/Logic/MapParser/Strategies/StageParserStrategy.swift b/Map/Logic/MapParser/Strategies/StageParserStrategy.swift deleted file mode 100644 index 6e4a341..0000000 --- a/Map/Logic/MapParser/Strategies/StageParserStrategy.swift +++ /dev/null @@ -1,38 +0,0 @@ -// 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/Logic/MapParser/Strategies/VertexParserStrategy.swift b/Map/Logic/MapParser/Strategies/VertexParserStrategy.swift deleted file mode 100644 index 0ca6fc1..0000000 --- a/Map/Logic/MapParser/Strategies/VertexParserStrategy.swift +++ /dev/null @@ -1,50 +0,0 @@ -// 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) - } -} diff --git a/Map/Presentation/Base Components/EvolutionPicker.swift b/Map/Presentation/Base Components/EvolutionPicker.swift index 2768540..2df4a7f 100644 --- a/Map/Presentation/Base Components/EvolutionPicker.swift +++ b/Map/Presentation/Base Components/EvolutionPicker.swift @@ -21,21 +21,22 @@ struct EvolutionPicker: View { var body: some View { Picker("Evolution", selection: $selectedEvolution) { ForEach(StageType.types) { stage in - Text(Stage.title(stage)).font(.Theme.body).tag(stage).padding(4.0) + Text(Stage.title(stage)).font(.Theme.Body.regular).tag(stage).padding(4.0) } Divider() ForEach(StageType.characteristics) { stage in - Text(Stage.title(stage)).font(.Theme.body).tag(stage).padding(4.0).font(.Theme.body) + Text(Stage.title(stage)).font(.Theme.Body.regular).tag(stage).padding(4.0).font( + .Theme.Body.regular) } Divider() ForEach(StageType.properties) { stage in - Text(Stage.title(stage)).font(.Theme.body).tag(stage).padding(4.0) + Text(Stage.title(stage)).font(.Theme.Body.regular).tag(stage).padding(4.0) } Divider() ForEach(StageType.custom) { stage in - Text(Stage.title(stage)).font(.Theme.body).tag(stage).padding(4.0) + Text(Stage.title(stage)).font(.Theme.Body.regular).tag(stage).padding(4.0) } - }.font(.Theme.body).padding(.horizontal, 8.0).padding(.vertical, 4.0) + }.font(.Theme.Body.regular).padding(.horizontal, 8.0).padding(.vertical, 4.0) } } diff --git a/Map/Presentation/Base Components/MapRender/MapAxes.swift b/Map/Presentation/Base Components/MapRender/MapAxes.swift index 4f64135..1c0a147 100644 --- a/Map/Presentation/Base Components/MapRender/MapAxes.swift +++ b/Map/Presentation/Base Components/MapRender/MapAxes.swift @@ -36,11 +36,11 @@ struct MapAxes: View { }.stroke(Color.Map.axisColor, lineWidth: lineWidth * 2) // Y Labels - Text("Visible").font(.Theme.axisLabel).foregroundColor(.Map.labelColor).rotationEffect( + Text("Visible").font(.Theme.Map.axisLabel).foregroundColor(.Map.labelColor).rotationEffect( Angle(degrees: -90.0) ) .offset(CGSize(width: -35.0, height: 0.0)) - Text("Invisible").font(.Theme.axisLabel).foregroundColor(.Map.labelColor).rotationEffect( + Text("Invisible").font(.Theme.Map.axisLabel).foregroundColor(.Map.labelColor).rotationEffect( Angle(degrees: -90.0) ) .offset(CGSize(width: -40.0, height: mapSize.height - 20)) @@ -48,36 +48,36 @@ struct MapAxes: View { // X Labels Text("Uncharted") - .font(.Theme.axisLabel) + .font(.Theme.Map.axisLabel) .foregroundColor(.Map.labelColor) .frame(width: mapSize.width / 4, height: stageHeight / 2.0, alignment: .topLeading) .offset(CGSize(width: 0.0, height: -stageHeight / 4.0)) Text("Industrialised") - .font(.Theme.axisLabel) + .font(.Theme.Map.axisLabel) .foregroundColor(.Map.labelColor) .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(.Theme.axisLabel) + .font(.Theme.Map.axisLabel) .foregroundColor(.Map.labelColor) .frame(width: w(stages[0]), height: stageHeight, alignment: .topLeading) .offset(CGSize(width: 0.0, height: mapSize.height + padding)) Text(evolution.ii) - .font(.Theme.axisLabel) + .font(.Theme.Map.axisLabel) .foregroundColor(.Map.labelColor) .frame(width: w(stages[1]) - w(stages[0]), height: stageHeight, alignment: .topLeading) .offset(CGSize(width: w(stages[0]), height: mapSize.height + padding)) Text(evolution.iii) - .font(.Theme.axisLabel) + .font(.Theme.Map.axisLabel) .foregroundColor(.Map.labelColor) .frame(width: w(stages[2]) - w(stages[1]), height: stageHeight, alignment: .topLeading) .offset(CGSize(width: w(stages[1]), height: mapSize.height + padding)) Text(evolution.iv) - .font(.Theme.axisLabel) + .font(.Theme.Map.axisLabel) .foregroundColor(.Map.labelColor) .frame(width: mapSize.width - w(stages[2]), height: stageHeight, alignment: .topLeading) .offset(CGSize(width: w(stages[2]), height: mapSize.height + padding)) diff --git a/Map/Presentation/Base Components/MapRender/MapNotes.swift b/Map/Presentation/Base Components/MapRender/MapNotes.swift index 0119154..16c61ba 100644 --- a/Map/Presentation/Base Components/MapRender/MapNotes.swift +++ b/Map/Presentation/Base Components/MapRender/MapNotes.swift @@ -24,8 +24,10 @@ struct MapNotes: View { var body: some View { ForEach(notes, id: \.id) { note in - Text(note.text.replacingOccurrences(of: "\\n", with: "\n")).font(.Theme.note) - .padding(2.0) + Text(note.text.replacingOccurrences(of: "\\n", with: "\n")) + .font(.Theme.Map.note) + .lineSpacing(Dimensions.LineHeight.Map.note - Dimensions.FontSize.Map.note) + .padding(Dimensions.Spacing.cozy) .background(.white) .foregroundColor(.Map.labelColor) .border(Color.Map.vertexColor, width: lineWidth) diff --git a/Map/Presentation/Base Components/MapRender/MapVertices.swift b/Map/Presentation/Base Components/MapRender/MapVertices.swift index d8e3b88..1f6cf1a 100644 --- a/Map/Presentation/Base Components/MapRender/MapVertices.swift +++ b/Map/Presentation/Base Components/MapRender/MapVertices.swift @@ -28,7 +28,11 @@ struct MapVertices: View { ForEach(vertices, id: \.id) { vertex in ZStack(alignment: .topLeading) { getVertexShape(vertex).fill(Color.Map.vertexColor) - Text(vertex.label.replacingOccurrences(of: "\\n", with: "\n")).font(.Theme.vertexLabel) + Text(vertex.label.replacingOccurrences(of: "\\n", with: "\n")) + .font(.Theme.Map.vertexLabel) + .lineSpacing( + Dimensions.LineHeight.Map.vertexLabel - Dimensions.FontSize.Map.vertexLabel + ) .foregroundColor(.Map.labelColor) .shadow(color: .white, radius: 0, x: -0.5, y: -0.5) .shadow(color: .white, radius: 0, x: 0.5, y: 0.5) diff --git a/Map/Presentation/Base Components/MapTextEditor.swift b/Map/Presentation/Base Components/MapTextEditor.swift index ff98203..3c5c824 100644 --- a/Map/Presentation/Base Components/MapTextEditor.swift +++ b/Map/Presentation/Base Components/MapTextEditor.swift @@ -150,7 +150,7 @@ extension MapTextEditorController: NSTextStorageDelegate { } } - private func colorizeText(textStorage: NSTextStorage) { + nonisolated private func colorizeText(textStorage: NSTextStorage) { let range = NSMakeRange(0, textStorage.length) var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range) diff --git a/Map/Presentation/Base Components/SearchBar.swift b/Map/Presentation/Base Components/SearchBar.swift index e5182c5..cd2c378 100644 --- a/Map/Presentation/Base Components/SearchBar.swift +++ b/Map/Presentation/Base Components/SearchBar.swift @@ -48,19 +48,19 @@ struct SearchBar: View { Spacer() Button(action: onPrevious) { Image(systemName: "chevron.left") - .font(.Theme.smallControl) + .font(.Theme.SmallControl.regular) }.keyboardShortcut( "g", modifiers: EventModifiers([.command, .shift]) ).help("Find Previous (⇧⌘G)") Button(action: onNext) { Image(systemName: "chevron.right") - .font(.Theme.smallControl) + .font(.Theme.SmallControl.regular) }.keyboardShortcut( "g", modifiers: EventModifiers([.command]) ).help("Find Next (⌘G)") Button(action: onDismiss) { Text("Done") - .font(.Theme.smallControl) + .font(.Theme.SmallControl.regular) }.keyboardShortcut(.escape, modifiers: EventModifiers()) .help("Done (⎋)") }.padding(4.0) diff --git a/Map/Presentation/Complex Components/MapRender/MapRenderView.swift b/Map/Presentation/Complex Components/MapRender/MapRenderView.swift index 1ca1ffe..780290b 100644 --- a/Map/Presentation/Complex Components/MapRender/MapRenderView.swift +++ b/Map/Presentation/Complex Components/MapRender/MapRenderView.swift @@ -30,8 +30,8 @@ struct MapRenderView: View { MapParser.parse(content: document.text) } - let mapSize = Dimensions.mapSize - let padding = Dimensions.mapPadding + let mapSize = Dimensions.Map.size + let padding = Dimensions.Map.padding let lineWidth = CGFloat(0.5) let vertexSize = CGSize(width: 25.0, height: 25.0) diff --git a/Map/Presentation/EvolutionPicker.swift b/Map/Presentation/EvolutionPicker.swift deleted file mode 100644 index 0f4c954..0000000 --- a/Map/Presentation/EvolutionPicker.swift +++ /dev/null @@ -1,41 +0,0 @@ -import SwiftUI - -struct EvolutionPicker: View { - - @EnvironmentObject private var store: AppStore - - private var selectedEvolution: Binding { - 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)).font(.theme.body).tag(stage).padding(4.0) - } - Divider() - ForEach(StageType.characteristics) { stage in - Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0).font(.theme.body) - } - Divider() - ForEach(StageType.properties) { stage in - Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0) - } - Divider() - ForEach(StageType.custom) { stage in - Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0) - } - }.font(.theme.body).padding(.horizontal, 8.0).padding(.vertical, 4.0) - } -} - -struct EvolutionPicker_Previews: PreviewProvider { - static var previews: some View { - EvolutionPicker() - } -} diff --git a/Map/Presentation/MapEditor.swift b/Map/Presentation/MapEditor.swift index 5cda8f9..1e25ce8 100644 --- a/Map/Presentation/MapEditor.swift +++ b/Map/Presentation/MapEditor.swift @@ -102,8 +102,8 @@ struct MapEditor: View { MapRenderView( document: $document, evolution: $selectedEvolution, onDragVertex: onDragVertex ).scaleEffect(zoom, anchor: .center).frame( - width: (Dimensions.mapSize.width + 2 * Dimensions.mapPadding) * zoom, - height: (Dimensions.mapSize.height + 2 * Dimensions.mapPadding) * zoom) + width: (Dimensions.Map.size.width + 2 * Dimensions.Map.padding) * zoom, + height: (Dimensions.Map.size.height + 2 * Dimensions.Map.padding) * zoom) }.background(Color.UI.background) .gesture( MagnificationGesture() @@ -125,16 +125,16 @@ struct MapEditor: View { value: $zoom, in: zoomRange, step: 0.1, label: { Text(formatZoom(zoom)) - .font(.Theme.smallControl) + .font(.Theme.SmallControl.regular) }, minimumValueLabel: { Image(systemName: "minus.magnifyingglass") - .font(.Theme.smallControl) + .font(.Theme.SmallControl.regular) .help("Zoom Out (⌘-)") }, maximumValueLabel: { Image(systemName: "plus.magnifyingglass") - .font(.Theme.smallControl) + .font(.Theme.SmallControl.regular) .help("Zoom In (⌘+)") } ).frame(width: 200).padding(.trailing, 10.0) diff --git a/Map/Presentation/Theme/Color+theme.swift b/Map/Presentation/Theme/Color+theme.swift index feac76a..ee80e82 100644 --- a/Map/Presentation/Theme/Color+theme.swift +++ b/Map/Presentation/Theme/Color+theme.swift @@ -16,6 +16,9 @@ import SwiftUI extension Color { struct Theme { + + // MARK: - Base Palette + static let darkSlate = Color("Dark Slate") static let jasperRed = Color("Jasper Red") static let olympicBlue = Color("Olympic Blue") @@ -28,6 +31,15 @@ extension Color { static let darkerNeutralGray = Color("Darker Neutral Gray") } + // MARK: - General + + struct UI { + static let foreground = Color("Foreground") + static let background = Color("Background") + } + + // MARK: - The Map + struct Map { static let labelColor = Color.Theme.darkSlate static let axisColor = Color.Theme.darkSlate @@ -44,9 +56,4 @@ extension Color { Color.Theme.hermosaPink, ] } - - struct UI { - static let foreground = Color("Foreground") - static let background = Color("Background") - } } diff --git a/Map/Presentation/Theme/Dimensions.swift b/Map/Presentation/Theme/Dimensions.swift index 99a6207..2e1fd69 100644 --- a/Map/Presentation/Theme/Dimensions.swift +++ b/Map/Presentation/Theme/Dimensions.swift @@ -15,6 +15,48 @@ import Foundation struct Dimensions { - static let mapSize = CGSize(width: 1300.0, height: 1000.0) - static let mapPadding: CGFloat = 42.0 + + // MARK: - Fonts + + struct FontSize { + static let body: CGFloat = 16 + static let title: CGFloat = 32 + static let caption: CGFloat = 14 + static let smallControl: CGFloat = 9 + + struct Map { + static let note: CGFloat = 12 + static let axisLabel: CGFloat = 14 + static let vertexLabel: CGFloat = 12 + } + } + + struct LineHeight { + static let body: CGFloat = 18 + static let title: CGFloat = 38 + static let caption: CGFloat = 18 + static let smallControl: CGFloat = 18 + + struct Map { + static let note: CGFloat = 18 + static let axisLabel: CGFloat = 18 + static let vertexLabel: CGFloat = 18 + } + } + + // MARK: - Spacing + + struct Spacing { + static let coziest: CGFloat = 3.0 + static let cozy: CGFloat = 5.0 + static let regular: CGFloat = 8.0 + static let loose: CGFloat = 12.0 + static let indulgent: CGFloat = 16.0 + } + + // MARK: - The Map + struct Map { + static let size = CGSize(width: 1300.0, height: 1000.0) + static let padding: CGFloat = 42.0 + } } diff --git a/Map/Presentation/Theme/Font+theme.swift b/Map/Presentation/Theme/Font+theme.swift index 1a9abb8..2fb56f7 100644 --- a/Map/Presentation/Theme/Font+theme.swift +++ b/Map/Presentation/Theme/Font+theme.swift @@ -15,6 +15,19 @@ import SwiftUI extension Font { + + static func libertinus(size: CGFloat) -> Font { + return .custom("Libertinus Serif", size: size) + } + + static func ronzino(size: CGFloat) -> Font { + return .custom("Ronzino", size: size) + } + + static func ronzinoMedium(size: CGFloat) -> Font { + return .custom("RonzinoMedium", size: size) + } + public struct Theme { // Map @@ -22,8 +35,30 @@ extension Font { static let axisLabel = Font.system(size: 14, design: .serif) static let vertexLabel = Font.system(size: 12, design: .serif) - // UI - static let smallControl = Font.system(size: 9) - static let body = Font.system(size: 13) + struct Body { + static let regular = Font.ronzino(size: Dimensions.FontSize.body).weight(.regular) + static let emphasized = Font.ronzino(size: Dimensions.FontSize.body).weight(.medium) + } + + struct Title { + static let emphasized = Font.libertinus(size: Dimensions.FontSize.title).weight(.medium) + } + + struct Caption { + static let regular = Font.ronzino(size: Dimensions.FontSize.caption).weight(.regular) + static let emphasized = Font.ronzino(size: Dimensions.FontSize.caption).weight(.medium) + } + + struct SmallControl { + static let regular = Font.ronzino(size: Dimensions.FontSize.smallControl).weight(.regular) + } + + struct Map { + static let note = Font.libertinus(size: Dimensions.FontSize.Map.note).weight(.regular) + static let axisLabel = Font.libertinus(size: Dimensions.FontSize.Map.axisLabel).weight( + .regular) + static let vertexLabel = Font.libertinus(size: Dimensions.FontSize.Map.vertexLabel).weight( + .regular) + } } } diff --git a/Map/Presentation/Theme/LibertinusSerif-Regular.otf b/Map/Presentation/Theme/LibertinusSerif-Regular.otf new file mode 100644 index 0000000..508f3c0 Binary files /dev/null and b/Map/Presentation/Theme/LibertinusSerif-Regular.otf differ diff --git a/Map/Presentation/Theme/Ronzino-Medium.otf b/Map/Presentation/Theme/Ronzino-Medium.otf new file mode 100644 index 0000000..0be8dd5 Binary files /dev/null and b/Map/Presentation/Theme/Ronzino-Medium.otf differ diff --git a/Map/Presentation/Theme/Ronzino-Regular.otf b/Map/Presentation/Theme/Ronzino-Regular.otf new file mode 100644 index 0000000..8b70471 Binary files /dev/null and b/Map/Presentation/Theme/Ronzino-Regular.otf differ -- cgit