Skip to content

Instantly share code, notes, and snippets.

@MainasuK
Created November 17, 2023 04:02
Show Gist options
  • Save MainasuK/3e0ea0c70529407b7c06b52ac433c192 to your computer and use it in GitHub Desktop.
Save MainasuK/3e0ea0c70529407b7c06b52ac433c192 to your computer and use it in GitHub Desktop.
Code for Atlas map view
//
// MapView.swift
// Atlas
//
// Created by MainasuK on 2022-4-10.
//
import os.log
import Combine
import CoreDataStack
import SwiftUI
import AtlasCore
import TakumiSDK
import AVKit
import Kingfisher
struct MapView: View {
let logger = Logger(subsystem: "MapView", category: "View")
@ObservedObject var viewModel: ViewModel
var offset: CGSize {
get { viewModel.offset }
nonmutating set { viewModel.offset = newValue }
}
var location: CGPoint {
get { viewModel.location }
nonmutating set { viewModel.location = newValue }
}
var scale: CGFloat { viewModel.scale }
var origin: CGPoint {
CGPoint(
x: (mapSize.width - padding.width) / 2 - viewModel.map.detail.origin.x,
y: mapSize.height / 2 - viewModel.map.detail.origin.y
)
}
var padding: CGSize { viewModel.map.detail.padding }
var mapSize: CGSize { viewModel.map.detail.size }
var dragGesture: some Gesture {
DragGesture()
.onChanged { value in
offset = CGSize(
width: location.x + value.translation.width / scale,
height: location.y + value.translation.height / scale
)
}
.onEnded { value in
location = CGPoint(x: offset.width, y: offset.height)
}
}
var gridItems: [GridItem] {
let minColumn = MapService.columnOfMap(id: viewModel.map.id)
let count = min(minColumn, viewModel.mapAssets.count)
return Array(repeating: GridItem(spacing: .zero), count: count)
}
var mapView: some View {
ZStack(alignment: .bottomTrailing) {
HStack(spacing: .zero) {
LazyVGrid(columns: gridItems, spacing: .zero) {
ForEach(viewModel.mapAssets, id: \.self) { asset in
Image(nsImage: asset)
.resizable()
.aspectRatio(contentMode: .fit)
}
}
} // end HStack
Text("\(mapSize.debugDescription)")
}
}
@ViewBuilder
var floorView: some View {
ForEach(viewModel.pointGroups.reversed(), id: \.id) { group in
let _focusMapOverlay = viewModel.isMapOverlayFocus()
let opacity: CGFloat = {
let isDisplay = viewModel.isMapOverlayDisplay(group: group)
guard isDisplay else { return 0 }
if let focusMapOverlay = _focusMapOverlay {
return focusMapOverlay.id == group.id ? 1.0 : 0.5
}
let isHide = viewModel.isHideGroups.contains(where: { $0.id == group.id })
if isHide {
return 0
}
return 1
}()
ForEach(group.floors.reversed(), id: \.id) { floor in
let x = CGFloat(floor.overlay.lx + floor.overlay.rx) / 2
let y = CGFloat(floor.overlay.ly + floor.overlay.ry) / 2
let width = abs(floor.overlay.rx - floor.overlay.lx)
let height = abs(floor.overlay.ry - floor.overlay.ly)
let zIndex: Double = {
var index = Double(group.id)
if let focusMapOverlay = _focusMapOverlay, focusMapOverlay.id == group.id {
index += 10_000_000.0
}
if viewModel.isFocusFloors[floor.id] == true {
index += 1_000_000.0
}
return index
}()
let url = URL(string: floor.overlay.url)
KFImage(url)
.onSuccess { result in
if let url = url {
viewModel.floorOverlayStore[url] = result.image
}
}
.resizable()
.frame(width: width, height: height)
.shadow(color: .black, radius: 40)
.offset(x: x, y: y)
.zIndex(zIndex)
}
.compositingGroup()
.opacity(opacity)
}
}
static var pointImageDimension: CGFloat = 22
@ViewBuilder
var pointView: some View {
Canvas { context, size in
// print("offset: \(offset), scale: \(scale)")
// sparkle
let center = CGPoint(x: 0.5 * size.width, y: 0.5 * size.height)
let image = context.resolve(Image(systemName: "sparkle"))
context.draw(image, at: center)
#if DEBUG
let frameInMap = viewModel.convertFrameFromCanvasToMap()
let frameInMapText = context.resolve(
Text("\(frameInMap.origin.debugDescription)\n\(frameInMap.size.debugDescription)")
.font(.title)
.foregroundStyle(.red)
)
context.draw(frameInMapText, in: CGRect(origin: .zero, size: CGSize(width: 500, height: 100)))
#endif
let resolveImageDictionary: [String: GraphicsContext.ResolvedImage] = {
var dictionary: [String: GraphicsContext.ResolvedImage] = [:]
for (key, image) in viewModel.labelIcons {
let icon = Image(nsImage: image)
dictionary[key] = context.resolve(icon)
}
return dictionary
}()
let rect = CGRect(origin: .zero, size: size)
let offset = self.offset
let scale = self.scale
let hidePointsWhenMarked = viewModel.context.preference.hidePointsWhenMarked
let hidePointsOtherThanSoloState = viewModel.context.preference.hidePointsOtherThanSoloState
for point in viewModel.mapPointFetchedResultsController.objects {
let location = MapView.ViewModel.converMapPoint(
CGPoint(x: point.x, y: point.y),
canvasSize: size,
offset: offset,
scale: scale
)
guard rect.contains(location) else { continue }
#if DEBUG
// context.draw(image, at: location)
#endif
guard let label = viewModel.labelDictionary[point.labelID]?.first,
let icon = resolveImageDictionary[label.icon]
else { continue }
let isSolo = (label.parent?.isSolo ?? false) || label.isSolo
if isSolo {
// do nothing
} else {
let isMute = (label.parent?.isMute ?? false) || label.isMute
guard !isMute else { continue }
}
let isMarked = viewModel.markPoints.contains(point.id)
let rect = CGRect(
x: location.x - 0.5 * MapView.pointImageDimension,
y: location.y - 0.5 * MapView.pointImageDimension,
width: MapView.pointImageDimension,
height: MapView.pointImageDimension
)
if MapView.ViewModel.pointShouldHidden(
context: viewModel.context,
isMarked: isMarked,
isSolo: isSolo,
hidePointsWhenMarked: hidePointsWhenMarked,
hidePointsOtherThanSoloState: hidePointsOtherThanSoloState
) {
continue
}
let frame = AVMakeRect(aspectRatio: image.size, insideRect: rect)
let extendFrame = frame.insetBy(dx: -1, dy: -1)
let isInOverlay = viewModel.isPointInOverlay[point.id] ?? false
// background
context.fill(Ellipse().path(in: extendFrame), with: .color(.white.opacity(isMarked ? 0.5 : 1.0)))
context.fill(Ellipse().path(in: frame), with: .color(.black.opacity(isMarked ? 0.5 : 1.0)))
if isInOverlay {
context.stroke(Ellipse().path(in: extendFrame), with: .color(.black.opacity(isMarked ? 0.5 : 1.0)), style: StrokeStyle(lineWidth: 2, dash: [4]))
}
// icon
var imageContext = context
if isMarked {
imageContext.opacity = 0.5
}
imageContext.clip(to: Ellipse().path(in: rect))
imageContext.draw(icon, in: frame)
// let text = context.resolve(Text("\(point.x, format: .number.precision(.fractionLength(2))), \(point.y, format: .number.precision(.fractionLength(2)))"))
// context.draw(text, in: frame.offsetBy(dx: 0, dy: 20).insetBy(dx: -50, dy: 0))
} // end for … in …
}
.frame(maxWidth: 4096, maxHeight: 4096)
}
var body: some View {
GeometryReader { proxy in
ZStack {
Color.clear
.background {
ZStack {
mapView
.border(.white, width: 20)
.overlay {
if viewModel.isMapOverlayFocus() != nil {
Color.black
.opacity(0.5)
}
}
.offset(x: origin.x + 0.5 * padding.width, y: origin.y)
Color.red
.opacity(0.1)
.frame(width: 20, height: 20)
.clipShape(Circle())
floorView
// anchorView
}
.frame(width: viewModel.map.detail.size.width, height: viewModel.map.detail.size.height)
}
.offset(offset)
.scaleEffect(viewModel.scale)
pointView
Color.clear
.preference(key: ViewFramePreferenceKey.self, value: proxy.frame(in: .global))
.onPreferenceChange(ViewFramePreferenceKey.self) { frame in
viewModel.frameInWindow = frame
}
.overlay {
if let mapPointListViewModel = viewModel.mapPointListViewModel, let point = mapPointListViewModel.points.first {
MapPointListView(viewModel: mapPointListViewModel)
.cornerRadius(12)
.shadow(radius: 4)
.overlay(alignment: .topLeading) {
let rect = CGRect(x: 0, y: 0, width: 8, height: 22)
Path { path in
path.move(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
}
.fill(Color(.windowBackgroundColor))
.frame(width: rect.width, height: rect.height)
.offset(x: -rect.width, y: 70 - 0.5 * rect.height)
}
.offset(CGSize(width: point.x * viewModel.scale, height: point.y * viewModel.scale))
.offset(CGSize(width: offset.width * viewModel.scale, height: offset.height * viewModel.scale))
.offset(
x: 0.5 * MapPointListView.width + 20, // 20pt padding
y: 0.5 * MapPointListView.height - 70
)
}
}
} // end ZStack
.gesture(dragGesture)
.onHover(perform: { isHover in
if viewModel.mouseExited != isHover {
viewModel.mouseExited = !isHover
}
})
.onChange(of: viewModel) {
offset = .zero
location = .zero
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): reset offset")
} // end ZStack
.alert("Please login the app to sync map", isPresented: $viewModel.isNotAuthorizedAlertDisplay) {
SettingsLink {
Button {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): open settings")
viewModel.isNotAuthorizedAlertDisplay = false
} label: {
Text("Open Settings")
}
}
Button {
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): cancel alert")
viewModel.isNotAuthorizedAlertDisplay = false
} label: {
Text("Cancel")
}
}
} // end GeometryReader
.onHover { isHover in
viewModel.mouseEnterMapView = isHover
}
.overlay(alignment: .bottom) {
VStack {
let hasNewOverlay: Bool = {
let isHoverOnGroups = viewModel.isHoverOnGroups
let isPinGroups = viewModel.isPinGroups
for isHoverOnGroup in isHoverOnGroups {
if !isPinGroups.contains(where: { $0.id == isHoverOnGroup.id }) { return true }
}
return false
}()
if hasNewOverlay {
Text("Click to pin the overlay")
.padding()
.background(.regularMaterial)
.clipShape(Capsule())
}
if !viewModel.isPinGroups.isEmpty {
VStack(alignment: .leading) {
ForEach(viewModel.isPinGroups, id: \.id) { group in
HStack {
// highlight
Button {
viewModel.focusMapOverlay(group: group)
} label: {
let isFocus = viewModel.isFocusGroup?.id == group.id
Image(systemName: "square.stack.3d.up.fill")
.symbolRenderingMode(SymbolRenderingMode.hierarchical)
.foregroundStyle(isFocus ? Color.accentColor : Color.secondary, Color.secondary)
}
// hide
Button {
viewModel.hideMapOverlay(group: group)
} label: {
let isHide = viewModel.isHideGroups.contains { $0.id == group.id }
Image(systemName: "square.3.layers.3d.slash")
.symbolRenderingMode(SymbolRenderingMode.hierarchical)
.foregroundStyle(isHide ? Color.accentColor : Color.secondary, Color.secondary)
}
ForEach(group.floors, id: \.id) { floor in
Button {
viewModel.focusMapFloor(group: group, floor: floor)
} label: {
let name = floor.name.isEmpty ? "No Name" : floor.name
let isFocus = viewModel.isFocusFloors[floor.id] == true
Text(name)
.foregroundColor(isFocus ? Color.accentColor : Color.primary)
}
}
Button {
viewModel.removePinMapOverlay(group: group)
} label: {
Image(systemName: "xmark.circle.fill")
}
.alignmentGuide(HorizontalAlignment.trailing) { dimension in
dimension[.trailing]
}
}
} // end ForEach
} // VStack
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.thinMaterial)
.cornerRadius(8)
} // end if
Color.clear.frame(height: CGFloat.leastNonzeroMagnitude)
} // end VStack
}
} // end body
}
public struct ViewFramePreferenceKey: PreferenceKey {
public static let defaultValue: CGRect = .zero
public static func reduce(
value: inout CGRect,
nextValue: () -> CGRect
) {
value = nextValue()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment