// 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 Combine import CoreData import CoreGraphics import SwiftUI struct MapRenderView: View { @Binding var document: MapDocument @Binding var evolution: StageType @AppStorage("showMapBackground") var showMapBackground: Bool = true @AppStorage("useCustomFont") var useCustomFont: Bool = false @AppStorage("customFontName") var customFontName: String = "Helvetica" var stage: Stage { Stage.stages(evolution) } @State private var parsedMap: ParsedMap = ParsedMap.empty @State private var smartLabelPositions: [Int: CGPoint] = [:] @State private var parseTask: Task? let mapSize = Dimensions.Map.size let padding = Dimensions.Map.padding let lineWidth = CGFloat(0.5) let vertexSize = CGSize(width: 25.0, height: 25.0) var onDragVertex: (Vertex, CGFloat, CGFloat) -> Void = { _, _, _ in } private func parseMapInBackground() { parseTask?.cancel() let currentText = document.text parseTask = Task { // Parse on background thread let parsed = await Task.detached(priority: .userInitiated) { MapParser.parse(content: currentText) }.value // Calculate smart positions on background thread let positions = await Task.detached(priority: .userInitiated) { var positions: [Int: CGPoint] = [:] let axisLines = [ Line(start: CGPoint(x: 0, y: 0), end: CGPoint(x: 0, y: mapSize.height)), Line( start: CGPoint(x: 0, y: mapSize.height), end: CGPoint(x: mapSize.width, y: mapSize.height)), ] for vertex in parsed.vertices { positions[vertex.id] = SmartLabelPositioning.calculateOptimalLabelPosition( for: vertex, mapSize: mapSize, vertexSize: vertexSize, edges: parsed.edges, opportunities: parsed.opportunities, inertias: parsed.inertias, axisLines: axisLines, allVertices: parsed.vertices, notes: parsed.notes ) } return positions }.value // Update UI on main thread if !Task.isCancelled { await MainActor.run { parsedMap = parsed smartLabelPositions = positions } } } } var body: some View { ZStack(alignment: .topLeading) { Path { path in path.addRect( CGRect( x: -padding, y: -padding, width: mapSize.width + padding * 2, height: mapSize.height + padding * 4)) }.fill(.white) // The order Matters. Think of this as layers. if showMapBackground { MapBackground(mapSize: mapSize, lineWidth: lineWidth, stages: parsedMap.stages) } MapAxes( mapSize: mapSize, lineWidth: lineWidth, evolution: stage, stages: parsedMap.stages) Group { MapStages(mapSize: mapSize, lineWidth: lineWidth, stages: parsedMap.stages) MapEdges( mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, edges: parsedMap.edges) MapOpportunities( mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, opportunities: parsedMap.opportunities) MapIntertias(mapSize: mapSize, vertexSize: vertexSize, inertias: parsedMap.inertias) }.mask { MapMask( mapSize: mapSize, vertexSize: vertexSize, vertices: parsedMap.vertices, labelPositions: smartLabelPositions) } MapVertices( mapSize: mapSize, vertexSize: vertexSize, vertices: parsedMap.vertices, labelPositions: smartLabelPositions, onDragVertex: onDragVertex) MapGroups(mapSize: mapSize, vertexSize: vertexSize, groups: parsedMap.groups).drawingGroup( opaque: true ).opacity(0.1) MapNotes( mapSize: mapSize, lineWidth: lineWidth, notes: parsedMap.notes) } .id("\(useCustomFont)-\(customFontName)") .offset(x: padding, y: padding).frame( width: mapSize.width + 2 * padding, height: mapSize.height + 2 * padding, alignment: .topLeading ) .onAppear { parseMapInBackground() } .onChange(of: document.text) { _, _ in parseMapInBackground() } .onDisappear { parseTask?.cancel() } } func h(_ dimension: CGFloat) -> CGFloat { max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0)) } func w(_ dimension: CGFloat) -> CGFloat { max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0)) } } #Preview { MapRenderView( document: Binding.constant(MapDocument(text: "")), evolution: Binding.constant(StageType.general) ) }