Skip to content

Instantly share code, notes, and snippets.

@ryancoughlin
Created October 17, 2025 15:36
Show Gist options
  • Save ryancoughlin/56bec17afff84f71ab641013ff301fc7 to your computer and use it in GitHub Desktop.
Save ryancoughlin/56bec17afff84f71ab641013ff301fc7 to your computer and use it in GitHub Desktop.
import SwiftUI
import MapboxMaps
/// Renders the primary dataset visualization (data layer, visual layer, contours, special layers)
struct DatasetLayers: MapContent {
let dataset: Dataset
let entry: TimeEntry
let region: RegionMetadata
let renderingState: MapRenderingState
let isOnline: Bool
let eddyFeatures: [OceanFeature]
let cacheService: CacheService
var body: some MapContent {
let regionId = region.id
let coordinates = MapRegion.getImageCoordinates(for: region)
// 1. Data layer (for crosshair reading)
createDataLayer(entry: entry, regionId: regionId)
// 2. Visual layer (COG > Image)
createVisualLayer(dataset: dataset, entry: entry, regionId: regionId, coordinates: coordinates)
// 3. Contour layer (if supported)
if dataset.type != .eddys && renderingState.contoursEnabled && dataset.hasContours && hasContourData(entry) {
createContourLayer(dataset: dataset, entry: entry, regionId: regionId)
}
// 4. Special layers (eddys, currents)
createSpecialLayers(dataset: dataset, entry: entry, regionId: regionId)
}
// MARK: - Helper Methods
private func hasContourData(_ entry: TimeEntry) -> Bool {
return (entry.layers.pmtiles_contours != nil && !entry.layers.pmtiles_contours!.url.isEmpty) ||
(entry.layers.contours != nil && !entry.layers.contours!.isEmpty)
}
// MARK: - Data Layer
@MapContentBuilder
private func createDataLayer(entry: TimeEntry, regionId: String) -> some MapContent {
let sourceId = "data-source-\(regionId)"
let layerId = "data-layer"
if isOnline {
// Online: Prefer PMTiles
if let pmtilesData = entry.layers.pmtiles_data, !pmtilesData.source.isEmpty {
VectorSource(id: sourceId)
.tiles([AppConstants.pmtilesTileURL(from: pmtilesData)])
.maxzoom(8)
.prefetchZoomDelta(6)
CircleLayer(id: layerId, source: sourceId)
.sourceLayer(pmtilesData.layerName)
.circleRadius(4)
.circleOpacity(0)
.circleColor(StyleColor(.systemRed))
.slot(.middle)
} else if let dataURL = entry.layers.data, !dataURL.isEmpty {
// Fallback to GeoJSON
let resolvedURL = cacheService.resolveURL(dataURL)
if let urlObject = URL(string: resolvedURL) {
GeoJSONSource(id: sourceId)
.data(.url(urlObject))
CircleLayer(id: layerId, source: sourceId)
.circleRadius(1)
.circleOpacity(0)
.circleColor(StyleColor(.systemRed))
.slot(.middle)
}
}
} else {
// Offline: Use cached GeoJSON only (PMTiles require network)
if let dataURL = entry.layers.data, !dataURL.isEmpty {
let resolvedURL = cacheService.resolveURL(dataURL)
if let urlObject = URL(string: resolvedURL) {
GeoJSONSource(id: sourceId)
.data(.url(urlObject))
CircleLayer(id: layerId, source: sourceId)
.circleRadius(1)
.circleOpacity(0)
.slot(.middle)
}
}
}
}
// MARK: - Visual Layer
@MapContentBuilder
private func createVisualLayer(dataset: Dataset, entry: TimeEntry, regionId: String, coordinates: [[Double]]) -> some MapContent {
let sourceId = "visual-source-\(dataset.id)-\(regionId)-\(entry.id)"
let layerId = "visual-layer-\(dataset.id)-\(regionId)-\(entry.id)"
let renderDecision = computeRenderDecision(dataset: dataset, entry: entry)
switch renderDecision {
case .useImage:
createImageLayer(sourceId: sourceId, layerId: layerId, entry: entry, coordinates: coordinates)
case .useCOG:
createCOGLayer(sourceId: sourceId, layerId: layerId)
case .noVisualLayer:
EmptyMapContent()
}
}
private enum RenderDecision {
case useImage
case useCOG
case noVisualLayer
}
private var datasetsExcludedFromCOG: Set<DatasetType> {
[.chlorophyll, .eddys]
}
private func computeRenderDecision(dataset: Dataset, entry: TimeEntry) -> RenderDecision {
if !isOnline {
return entry.layers.image != nil && !entry.layers.image!.isEmpty ? .useImage : .noVisualLayer
}
if let cogURL = entry.layers.cog, !cogURL.isEmpty, !datasetsExcludedFromCOG.contains(dataset.type) {
return .useCOG
} else if let imageURL = entry.layers.image, !imageURL.isEmpty {
return .useImage
}
return .noVisualLayer
}
@MapContentBuilder
private func createImageLayer(sourceId: String, layerId: String, entry: TimeEntry, coordinates: [[Double]]) -> some MapContent {
if let imageURL = entry.layers.image, !imageURL.isEmpty {
let resolvedURL = cacheService.resolveURL(imageURL)
ImageSource(id: sourceId)
.url(resolvedURL)
.coordinates(coordinates)
RasterLayer(id: layerId, source: sourceId)
.rasterOpacity(1.0)
.slot(.middle)
}
}
@MapContentBuilder
private func createCOGLayer(sourceId: String, layerId: String) -> some MapContent {
if let tileURL = renderingState.cogTileURL {
RasterSource(id: sourceId)
.tiles([tileURL])
.minzoom(0)
.maxzoom(16)
.prefetchZoomDelta(6)
RasterLayer(id: layerId, source: sourceId)
.rasterOpacity(1.0)
.rasterFadeDuration(0.3)
.rasterOpacityTransition(StyleTransition(duration: 0.3, delay: 0))
.slot(.middle)
}
}
// MARK: - Contour Layer
@MapContentBuilder
private func createContourLayer(dataset: Dataset, entry: TimeEntry, regionId: String) -> some MapContent {
if let range = renderingState.contourRange,
let color = renderingState.contourColor,
let opacity = renderingState.contourOpacity {
// Compute IDs explicitly at call site
let sourceId = "\(dataset.type.rawValue)-contour-source-\(regionId)"
let layerId = "\(dataset.type.rawValue)-contour-layer-\(regionId)-\(entry.id)"
let contourState = ContourLayerState(
color: color,
opacity: opacity,
valueRange: range.min...range.max,
datasetType: dataset.type,
resolvedURL: cacheService.resolveURL(entry.layers.contours ?? ""),
sourceLayer: entry.layers.pmtiles_contours?.layerName,
sourceId: sourceId,
layerId: layerId
)
// Create source
if isOnline {
// Online: Prefer PMTiles
if let pmtilesContours = entry.layers.pmtiles_contours, !pmtilesContours.source.isEmpty {
VectorSource(id: contourState.sourceId)
.tiles([AppConstants.pmtilesTileURL(from: pmtilesContours)])
.maxzoom(8)
.volatile(false)
.prefetchZoomDelta(6)
} else if let contoursURL = entry.layers.contours, !contoursURL.isEmpty {
// Fallback to GeoJSON
let resolvedURL = cacheService.resolveURL(contoursURL)
if let urlObject = URL(string: resolvedURL) {
GeoJSONSource(id: contourState.sourceId)
.data(.url(urlObject))
}
}
} else {
// Offline: Use cached GeoJSON only (PMTiles require network)
if let contoursURL = entry.layers.contours, !contoursURL.isEmpty {
let resolvedURL = cacheService.resolveURL(contoursURL)
if let urlObject = URL(string: resolvedURL) {
GeoJSONSource(id: contourState.sourceId)
.data(.url(urlObject))
}
}
}
// Render appropriate contour layer
switch dataset.type {
case .sst:
SSTContourLayer(state: contourState)
case .salinity:
SalinityContourLayer(state: contourState)
case .mld:
MLDContourLayer(state: contourState)
default:
EmptyMapContent()
}
}
}
// MARK: - Special Layers
@MapContentBuilder
private func createSpecialLayers(dataset: Dataset, entry: TimeEntry, regionId: String) -> some MapContent {
switch dataset.type {
case .eddys:
if let contoursURL = entry.layers.contours, !contoursURL.isEmpty,
let range = renderingState.contourRange,
let color = renderingState.contourColor {
// Compute IDs explicitly at call site
let sourceId = "eddys-contour-source-\(regionId)"
let layerId = "eddys-contour-layer-\(regionId)-\(entry.id)"
let contourState = ContourLayerState(
color: color,
opacity: 1.0,
valueRange: range.min...range.max,
datasetType: .eddys,
resolvedURL: cacheService.resolveURL(contoursURL),
sourceLayer: entry.layers.pmtiles_contours?.layerName,
sourceId: sourceId,
layerId: layerId
)
// Create source
if isOnline {
// Online: Prefer PMTiles
if let pmtilesContours = entry.layers.pmtiles_contours, !pmtilesContours.source.isEmpty {
VectorSource(id: contourState.sourceId)
.tiles([AppConstants.pmtilesTileURL(from: pmtilesContours)])
.maxzoom(8)
.volatile(false)
.prefetchZoomDelta(6)
} else if !contoursURL.isEmpty {
// Fallback to GeoJSON
let resolvedURL = cacheService.resolveURL(contoursURL)
if let urlObject = URL(string: resolvedURL) {
GeoJSONSource(id: contourState.sourceId)
.data(.url(urlObject))
}
}
} else {
// Offline: Use cached GeoJSON only (PMTiles require network)
if !contoursURL.isEmpty {
let resolvedURL = cacheService.resolveURL(contoursURL)
if let urlObject = URL(string: resolvedURL) {
GeoJSONSource(id: contourState.sourceId)
.data(.url(urlObject))
}
}
}
EddyContourLayer(state: contourState)
// Render eddy features
EddyLayer(
cacheService: cacheService,
sshURL: contoursURL,
opacity: 1.0,
minValue: range.min,
maxValue: range.max,
color: color,
entryId: entry.id,
datasetId: nil,
regionId: regionId,
pmtilesData: entry.layers.pmtiles_contours,
features: eddyFeatures
)
}
case .currents:
if isOnline {
// Online: Prefer PMTiles
if let pmtilesData = entry.layers.pmtiles_data, !pmtilesData.url.isEmpty, !pmtilesData.source.isEmpty {
CurrentsLayer(
cacheService: cacheService,
pmtilesData: pmtilesData,
dataURL: nil,
opacity: 1.0,
entryId: entry.id,
datasetId: nil
)
} else if let dataURL = entry.layers.data, !dataURL.isEmpty {
// Fallback to GeoJSON
CurrentsLayer(
cacheService: cacheService,
pmtilesData: nil,
dataURL: dataURL,
opacity: 1.0,
entryId: entry.id,
datasetId: nil
)
}
} else {
// Offline: Use cached GeoJSON only (PMTiles require network)
if let dataURL = entry.layers.data, !dataURL.isEmpty {
CurrentsLayer(
cacheService: cacheService,
pmtilesData: nil,
dataURL: dataURL,
opacity: 1.0,
entryId: entry.id,
datasetId: nil
)
}
}
default:
EmptyMapContent()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment