<commit_before>//
//  LabelLayout.swift
//  tapmap
//
//  Created by <NAME> on 2021-01-16.
//  Copyright © 2021 Wildbrain. All rights reserved.
//

import Foundation
import CoreGraphics.CGGeometry
import UIKit.UIScreen

enum LayoutAnchor {
	case NE
	case SE
	case NW
	case SW
	case Center
	
	var next: LayoutAnchor? {
		switch self {
			case .NE: return .SE
			case .SE: return .NW
			case .NW: return .SW
			case .SW: return nil
			case .Center: return nil
		}
	}
}

struct LabelMarker: Comparable {
	let name: String
	let ownerHash: Int
	let worldPos: Vertex
	let kind: GeoPlace.Kind
	let rank: Int
	
	init(for poi: GeoPlace) {
		name = poi.name
		ownerHash = poi.hashValue
		worldPos = poi.location
		kind = poi.kind
		rank = poi.rank
	}
	
	var displayText: NSString {
		return ((kind == .Region) ? name.uppercased() : name) as NSString
	}
	
	var font: UIFont {
		switch kind {
			case .Region:
				switch rank {
					case 0: return UIFont(name: "HelveticaNeue-Bold", size: 20.0)!	// $ Move to stylesheet
					case 1: return UIFont(name: "HelveticaNeue-Bold", size: 16.0)!
					default: return UIFont(name: "HelveticaNeue-Bold", size: 12.0)!
				}
			case .Capital: return UIFont(name: "HelveticaNeue-Bold", size: 14.0)!
			case .City: return UIFont(name: "HelveticaNeue-Bold", size: 12.0)!
			case .Town: return UIFont(name: "HelveticaNeue-Bold", size: 10.0)!
		}
	}
	
	static func < (lhs: LabelMarker, rhs: LabelMarker) -> Bool {
		let lhsScore = lhs.rank - (lhs.kind == .Region ? 1 : 0)	// Value regions one step higher
		let rhsScore = rhs.rank - (rhs.kind == .Region ? 1 : 0)
		return lhsScore < rhsScore
	}
}

// $ Can split out Codable into extension on QuadTree
struct LabelPlacement: Codable, Hashable {
	enum CodingKeys: CodingKey {
		case aabb
	}
	var markerHash: Int = 0
	let aabb: Aabb
	var anchor: LayoutAnchor = .NE
	var debugName: String = "Unknown"
	
	init(markerHash: Int, aabb: Aabb, anchor: LayoutAnchor, debugName: String) {
		self.markerHash = markerHash
		self.aabb = aabb
		self.anchor = anchor
		self.debugName = debugName
	}
	
	func hash(into hasher: inout Hasher) {
		hasher.combine(markerHash)
	}
}

typealias MeasureFunction = (LabelMarker) -> (w: Float, h: Float)
typealias LabelLayout = [Int : LabelPlacement]

class LabelLayoutEngine {
	let maxLabels: Int
	let space: Aabb
	let measure: MeasureFunction
	var labelMargin: Float = 3.0
	var labelDistance: Float = 2.0
	var orderedLayout: [LabelPlacement] = []
	var labelSizeCache: [Int : (w: Float, h: Float)] = [:]	// $ Limit size of this
	
	init(maxLabels: Int, space: Aabb, measure: @escaping MeasureFunction) {
		self.maxLabels = maxLabels
		self.space = space
		self.measure = measure
	}
	
	// $ Re-implement zoom culling as a 3D box sweeping through point cloud
	
	func layoutLabels(markers: [Int: LabelMarker],
										projection project: (Vertex) -> CGPoint) -> (layout: LabelLayout, removed: [Int]) {
		// Collision detection structure for screen-space layout
		var labelQuadTree = QuadTree<LabelPlacement>(minX: space.minX,
																								 minY: space.minY,
																								 maxX: space.maxX,
																								 maxY: space.maxY,
																								 maxDepth: 6)
//		print("Layout start")
		var workingSet = markers
		var removedFromLayout: [Int] = []
		// Move and insert the previously placed labels in their established order
		for (i, placement) in orderedLayout.enumerated() {
			let marker = workingSet[placement.markerHash]!

			let result = layoutLabel(marker: marker, in: labelQuadTree, startAnchor: placement.anchor, project: project)
			
			if let (labelBox, layoutBox, anchor) = result {
				labelQuadTree.insert(value: placement, region: layoutBox, clipToBounds: true)
				orderedLayout[i] = LabelPlacement(markerHash: placement.markerHash, aabb: labelBox, anchor: anchor, debugName: marker.name)
//				print("  \(marker.name) stayed in layout on \(anchor) \(labelBox) @ \(i)")
			} else {
//				print("- \(marker.name) fell out of layout @ \(i)")
				removedFromLayout.append(placement.markerHash)
			}
			workingSet.removeValue(forKey: placement.markerHash)
		}
		
		removedFromLayout.forEach { removeLayout(for: $0) }
		
		// Layout new incoming markers
		let markersToLayout = Array(workingSet.values).sorted(by: <)
		for marker in markersToLayout {<commit_msg>Fixed flickering bug - existing labels were not re-projected when re-inserted into the layout<commit_after>//
//  LabelLayout.swift
//  tapmap
//
//  Created by <NAME> on 2021-01-16.
//  Copyright © 2021 Wildbrain. All rights reserved.
//

import Foundation
import CoreGraphics.CGGeometry
import UIKit.UIScreen

enum LayoutAnchor {
	case NE
	case SE
	case NW
	case SW
	case Center
	
	var next: LayoutAnchor? {
		switch self {
			case .NE: return .SE
			case .SE: return .NW
			case .NW: return .SW
			case .SW: return nil
			case .Center: return nil
		}
	}
}

struct LabelMarker: Comparable {
	let name: String
	let ownerHash: Int
	let worldPos: Vertex
	let kind: GeoPlace.Kind
	let rank: Int
	
	init(for poi: GeoPlace) {
		name = poi.name
		ownerHash = poi.hashValue
		worldPos = poi.location
		kind = poi.kind
		rank = poi.rank
	}
	
	var displayText: NSString {
		return ((kind == .Region) ? name.uppercased() : name) as NSString
	}
	
	var font: UIFont {
		switch kind {
			case .Region:
				switch rank {
					case 0: return UIFont(name: "HelveticaNeue-Bold", size: 20.0)!	// $ Move to stylesheet
					case 1: return UIFont(name: "HelveticaNeue-Bold", size: 16.0)!
					default: return UIFont(name: "HelveticaNeue-Bold", size: 12.0)!
				}
			case .Capital: return UIFont(name: "HelveticaNeue-Bold", size: 14.0)!
			case .City: return UIFont(name: "HelveticaNeue-Bold", size: 12.0)!
			case .Town: return UIFont(name: "HelveticaNeue-Bold", size: 10.0)!
		}
	}
	
	static func < (lhs: LabelMarker, rhs: LabelMarker) -> Bool {
		let lhsScore = lhs.rank - (lhs.kind == .Region ? 1 : 0)	// Value regions one step higher
		let rhsScore = rhs.rank - (rhs.kind == .Region ? 1 : 0)
		return lhsScore < rhsScore
	}
}

// $ Can split out Codable into extension on QuadTree
struct LabelPlacement: Codable, Hashable {
	enum CodingKeys: CodingKey {
		case aabb
	}
	var markerHash: Int = 0
	let aabb: Aabb
	var anchor: LayoutAnchor = .NE
	var debugName: String = "Unknown"
	
	init(markerHash: Int, aabb: Aabb, anchor: LayoutAnchor, debugName: String) {
		self.markerHash = markerHash
		self.aabb = aabb
		self.anchor = anchor
		self.debugName = debugName
	}
	
	func hash(into hasher: inout Hasher) {
		hasher.combine(markerHash)
	}
}

typealias MeasureFunction = (LabelMarker) -> (w: Float, h: Float)
typealias LabelLayout = [Int : LabelPlacement]

class LabelLayoutEngine {
	let maxLabels: Int
	let space: Aabb
	let measure: MeasureFunction
	var labelMargin: Float = 3.0
	var labelDistance: Float = 2.0
	var orderedLayout: [LabelPlacement] = []
	var labelSizeCache: [Int : (w: Float, h: Float)] = [:]	// $ Limit size of this
	
	init(maxLabels: Int, space: Aabb, measure: @escaping MeasureFunction) {
		self.maxLabels = maxLabels
		self.space = space
		self.measure = measure
	}
	
	// $ Re-implement zoom culling as a 3D box sweeping through point cloud
	
	func layoutLabels(markers: [Int: LabelMarker],
										projection project: (Vertex) -> CGPoint) -> (layout: LabelLayout, removed: [Int]) {
		// Collision detection structure for screen-space layout
		var labelQuadTree = QuadTree<LabelPlacement>(minX: space.minX,
																								 minY: space.minY,
																								 maxX: space.maxX,
																								 maxY: space.maxY,
																								 maxDepth: 6)
//		print("Layout start")
		var workingSet = markers
		var removedFromLayout: [Int] = []
		// Move and insert the previously placed labels in their established order
		for (i, placement) in orderedLayout.enumerated() {
			let marker = workingSet[placement.markerHash]!

			let result = layoutLabel(marker: marker, in: labelQuadTree, startAnchor: placement.anchor, project: project)
			
			if let (labelBox, layoutBox, anchor) = result {
				let reprojectedPlacement = LabelPlacement(markerHash: marker.ownerHash, aabb: labelBox, anchor: anchor, debugName: marker.name)
				labelQuadTree.insert(value: reprojectedPlacement, region: layoutBox, clipToBounds: true)
				orderedLayout[i] = LabelPlacement(markerHash: placement.markerHash, aabb: labelBox, anchor: anchor, debugName: marker.name)
//				print("  \(marker.name) stayed in layout on \(anchor) \(labelBox) @ \(i)")
			} else {
//				print("- \(marker.name) fell out of layout @ \(i)")
				removedFromLayout.append(placement.markerHash)
			}
			workingSet.removeValue(forKey: placement.markerHash)
		}
		
		removedFromLayout.forEach { removeLayout(for: $0) }
		
		// Layout new incoming markers
		let markersToLayout = Array(workingSet.values).sorted(by: <)
		for marker in markersToLayout {
{'fraction_non_alphanumeric': 0.11345347387509917, 'fraction_numerical': 0.009973931769239488, 'mean_word_length': 3.2751937984496124, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 15, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5697058', 'n_tokens_mistral': 3264, 'n_tokens_neox': 2880, 'n_words': 872}