// 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 RenderableMap { let vertices: [Vertex] let edges: [MapEdge] let inertias: [Inertia] let opportunities: [Opportunity] let notes: [Note] let stages: [CGFloat] let groups: [[Vertex]] static let empty: RenderableMap = RenderableMap( vertices: [], edges: [], inertias: [], opportunities: [], notes: [], stages: defaultDimensions, groups: []) static func from(_ entities: [Wmap.Entity]) -> RenderableMap { var vertices: [Vertex] = [] var edges: [MapEdge] = [] var inertias: [Inertia] = [] var opportunities: [Opportunity] = [] var notes: [Note] = [] var stages: [CGFloat] = [25.0, 50.0, 75.0] // default dimensions var groups: [[Vertex]] = [] // Build a vertex lookup for edges, groups, etc. var vertexMap: [String: Vertex] = [:] var vertexIdCounter = 0 for entity in entities { switch entity { case .vertex(let vertex): let newVertex = Vertex( id: vertexIdCounter, label: vertex.label, position: CGPoint(x: vertex.coordinates.0, y: vertex.coordinates.1), shape: convertShape(vertex.shape) ) vertices.append(newVertex) vertexMap[vertex.label.lowercased()] = newVertex vertexIdCounter += 1 case .note(let note): notes.append( Note( id: notes.count, position: CGPoint(x: note.coordinates.0, y: note.coordinates.1), text: note.text )) case .stage(let stage): let stageIndex = convertStageNumber(stage.number) - 1 if stageIndex >= 0 && stageIndex < 3 { stages[stageIndex] = stage.value } default: continue } } // Second pass for entities that depend on vertices for entity in entities { switch entity { case .edge(let edge): if let fromVertex = vertexMap[edge.from.lowercased()], let toVertex = vertexMap[edge.to.lowercased()] { edges.append( MapEdge( id: edges.count, origin: fromVertex.position, destination: toVertex.position, arrowhead: edge.isDirected )) } case .inertia(let inertia): if let vertex = vertexMap[inertia.vertex.lowercased()] { inertias.append( Inertia( id: inertias.count, position: vertex.position )) } case .evolution(let evolution): if let vertex = vertexMap[evolution.vertex.lowercased()] { let direction: CGFloat = evolution.isPositive ? 1.0 : -1.0 let destination = CGPoint( x: vertex.position.x + (direction * evolution.value), y: vertex.position.y ) opportunities.append( Opportunity( id: opportunities.count, origin: vertex.position, destination: destination )) } case .group(let group): var groupVertices: [Vertex] = [] for vertexName in group.vertices { if let vertex = vertexMap[vertexName.lowercased()] { groupVertices.append(vertex) } } if !groupVertices.isEmpty { groups.append(groupVertices) } default: continue } } return RenderableMap( vertices: vertices, edges: edges, inertias: inertias, opportunities: opportunities, notes: notes, stages: stages, groups: groups ) } private static func convertShape(_ shape: String?) -> VertexShape { guard let shape = shape?.lowercased() else { return .circle } switch shape { case "square": return .square case "triangle": return .triangle case "x": return .x default: return .circle } } private static func convertStageNumber(_ stageNumber: String) -> Int { switch stageNumber.lowercased() { case "i": return 1 case "ii": return 2 case "iii": return 3 case "iv": return 4 default: return 1 } } } 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 Inertia { 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, ]