feat(map): enhance globe projection handling and improve altitude color representation (#14)
* feat(map): enhance globe projection handling and improve altitude color representation - Implemented elevation-aware pixel projection for globe mode in `projectLngLatElevationPixelDelta`. - Refactored north-up animation in `CameraController` to use `setBearing` for smoother transitions. - Added native GeoJSON support for globe zoom in `FlightLayers`, including dynamic opacity adjustments based on zoom levels. - Introduced globe-specific pitch and projection settings in `Map` component, ensuring consistent rendering. - Enhanced UI control panel with a visual separator for better organization. - Minor formatting adjustments in `altitudeToColor` function for improved readability. * feat(map): refactor elevation-aware projection handling for improved accuracy * feat(map): add dark terrain profile support and enhance map styling * feat: implement trail stitching for merging historical and live flight data - Added a new module `trail-stitching.ts` to handle the merging of sparse historical track data with high-frequency live trail data. - Introduced constants for thresholds and parameters to improve code readability and maintainability. - Implemented a main function `stitchHistoricalTrail` that processes flight tracks, applies smoothing, and merges live tail data. - Included utility functions for spherical interpolation and cubic easing for altitude transitions. - Ensured the final path is cleaned of spikes and sharp corners for a smoother representation. * feat: add centripetal Catmull-Rom spline interpolation for 3D flight trails - Implemented `catmullRomSpline3D` function to interpolate waypoints into a smooth 3D path. - Added helper functions for segment density calculation, safe linear interpolation, and endpoint reflection. - Included support for variable tension based on heading changes to enhance smoothness. - Introduced utility functions for linear interpolation between elevated points. * feat(map): enhance layer visibility handling for flight and selection layers * feat: enhance control panel with new tabs and settings - Added "Changelog" and "About" tabs to the control panel. - Introduced new icons for the added tabs using lucide-react. - Updated the styling of the control panel buttons and dialog. - Improved accessibility with aria-labels for buttons. feat: integrate hero banner in flight card - Added a HeroBanner component to display aircraft photos in the FlightCard. - Implemented loading and error states for the photo display. - Enhanced the layout and styling of the FlightCard for better user experience. fix: update keyboard shortcuts for search functionality - Added shortcut "⌘K" to open search from anywhere in the application. - Adjusted keyboard shortcut handling to prevent conflicts with input fields. fix: optimize flight tracking cache management - Introduced a maximum cache size for flight tracking to prevent memory growth. - Implemented a cache eviction strategy for stale entries. feat: add great-circle utilities for geographical calculations - Implemented functions for calculating haversine distance, great-circle interpolation, and densifying paths. - Added functionality to handle antimeridian crossings in geographical paths. refactor: streamline map styles and terrain handling - Consolidated terrain DEM source for both terrain mesh and hillshade. - Adjusted hillshade layer properties for better performance and visual fidelity. fix: improve bounding box calculations for flight queries - Enhanced longitude calculations to account for converging meridians at higher latitudes. - Ensured bounding box calculations are accurate across different latitudes. * feat(map): refine globe mode functionality and update trail settings
This commit is contained in:
234
src/components/map/aircraft-appearance.ts
Normal file
234
src/components/map/aircraft-appearance.ts
Normal file
@ -0,0 +1,234 @@
|
||||
// ── Category Styling ───────────────────────────────────────────────────
|
||||
|
||||
export const CATEGORY_TINT: Record<number, [number, number, number]> = {
|
||||
2: [100, 235, 180],
|
||||
3: [120, 225, 235],
|
||||
4: [255, 210, 120],
|
||||
5: [255, 185, 110],
|
||||
6: [255, 160, 120],
|
||||
7: [255, 120, 200],
|
||||
8: [140, 220, 160],
|
||||
9: [170, 210, 255],
|
||||
10: [220, 170, 255],
|
||||
11: [255, 150, 180],
|
||||
12: [180, 230, 160],
|
||||
14: [195, 165, 255],
|
||||
};
|
||||
|
||||
export function categorySizeMultiplier(category: number | null): number {
|
||||
switch (category) {
|
||||
case 2:
|
||||
return 0.88;
|
||||
case 3:
|
||||
return 0.96;
|
||||
case 4:
|
||||
return 1.08;
|
||||
case 5:
|
||||
return 1.18;
|
||||
case 6:
|
||||
return 1.28;
|
||||
case 7:
|
||||
return 1.04;
|
||||
case 8:
|
||||
return 0.86;
|
||||
case 9:
|
||||
case 12:
|
||||
return 0.8;
|
||||
case 10:
|
||||
return 1.15;
|
||||
case 14:
|
||||
return 0.72;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
export function tintAircraftColor(
|
||||
base: [number, number, number, number],
|
||||
category: number | null,
|
||||
): [number, number, number, number] {
|
||||
const tint = category !== null ? CATEGORY_TINT[category] : undefined;
|
||||
if (!tint) return base;
|
||||
|
||||
return [
|
||||
Math.round(base[0] * 0.58 + tint[0] * 0.42),
|
||||
Math.round(base[1] * 0.58 + tint[1] * 0.42),
|
||||
Math.round(base[2] * 0.58 + tint[2] * 0.42),
|
||||
base[3],
|
||||
];
|
||||
}
|
||||
|
||||
// ── Selection pulse timing ─────────────────────────────────────────────
|
||||
|
||||
export const PULSE_PERIOD_MS = 7000;
|
||||
export const RING_PERIOD_MS = 5500;
|
||||
|
||||
// ── Canvas Atlas Generators ────────────────────────────────────────────
|
||||
|
||||
export function createHaloAtlas(): HTMLCanvasElement {
|
||||
const size = 256;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
const c = size / 2;
|
||||
for (let r = 0; r < c; r++) {
|
||||
const norm = r / c;
|
||||
let alpha = 0;
|
||||
if (norm < 0.18) {
|
||||
alpha = 0;
|
||||
} else if (norm < 0.35) {
|
||||
const t = (norm - 0.18) / 0.17;
|
||||
alpha = t * t * 0.7;
|
||||
} else if (norm < 0.55) {
|
||||
alpha = 0.7 - ((norm - 0.35) / 0.2) * 0.3;
|
||||
} else {
|
||||
const t = (norm - 0.55) / 0.45;
|
||||
alpha = 0.4 * (1 - t) * (1 - t);
|
||||
}
|
||||
if (alpha < 0.003) continue;
|
||||
ctx.strokeStyle = `rgba(255,255,255,${alpha})`;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.arc(c, c, r, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
return canvas;
|
||||
}
|
||||
|
||||
export function createSoftRingAtlas(): HTMLCanvasElement {
|
||||
const size = 256;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
const c = size / 2;
|
||||
const ringCenter = c * 0.75;
|
||||
const ringWidth = c * 0.18;
|
||||
for (let r = 0; r < c; r++) {
|
||||
const dist = Math.abs(r - ringCenter);
|
||||
const falloff = Math.max(0, 1 - (dist / ringWidth) ** 2);
|
||||
const alpha = falloff * 0.85;
|
||||
if (alpha < 0.005) continue;
|
||||
ctx.strokeStyle = `rgba(255,255,255,${alpha})`;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.arc(c, c, r, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
return canvas;
|
||||
}
|
||||
|
||||
export function createAircraftAtlas(): HTMLCanvasElement {
|
||||
const size = 128;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
ctx.fillStyle = "#ffffff";
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(64, 6);
|
||||
ctx.lineTo(71, 19);
|
||||
ctx.lineTo(71, 33);
|
||||
ctx.lineTo(100, 44);
|
||||
ctx.lineTo(106, 52);
|
||||
ctx.lineTo(80, 53);
|
||||
ctx.lineTo(72, 56);
|
||||
ctx.lineTo(72, 88);
|
||||
ctx.lineTo(90, 101);
|
||||
ctx.lineTo(88, 108);
|
||||
ctx.lineTo(69, 99);
|
||||
ctx.lineTo(69, 121);
|
||||
ctx.lineTo(64, 126);
|
||||
ctx.lineTo(59, 121);
|
||||
ctx.lineTo(59, 99);
|
||||
ctx.lineTo(40, 108);
|
||||
ctx.lineTo(38, 101);
|
||||
ctx.lineTo(56, 88);
|
||||
ctx.lineTo(56, 56);
|
||||
ctx.lineTo(48, 53);
|
||||
ctx.lineTo(22, 52);
|
||||
ctx.lineTo(28, 44);
|
||||
ctx.lineTo(57, 33);
|
||||
ctx.lineTo(57, 19);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.globalCompositeOperation = "destination-out";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(64, 13);
|
||||
ctx.lineTo(67, 19);
|
||||
ctx.lineTo(64, 24);
|
||||
ctx.lineTo(61, 19);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// ── Icon Mappings ──────────────────────────────────────────────────────
|
||||
|
||||
export const HALO_MAPPING = {
|
||||
halo: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 256,
|
||||
height: 256,
|
||||
anchorX: 128,
|
||||
anchorY: 128,
|
||||
mask: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const RING_MAPPING = {
|
||||
ring: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 256,
|
||||
height: 256,
|
||||
anchorX: 128,
|
||||
anchorY: 128,
|
||||
mask: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const AIRCRAFT_ICON_MAPPING = {
|
||||
aircraft: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 128,
|
||||
height: 128,
|
||||
anchorX: 64,
|
||||
anchorY: 64,
|
||||
mask: true,
|
||||
},
|
||||
};
|
||||
|
||||
// ── Cached Atlas Data URLs ─────────────────────────────────────────────
|
||||
|
||||
let _haloCache: string | undefined;
|
||||
export function getHaloUrl(): string {
|
||||
if (typeof document === "undefined") return "";
|
||||
if (!_haloCache) _haloCache = createHaloAtlas().toDataURL();
|
||||
return _haloCache;
|
||||
}
|
||||
|
||||
let _ringCache: string | undefined;
|
||||
export function getRingUrl(): string {
|
||||
if (typeof document === "undefined") return "";
|
||||
if (!_ringCache) _ringCache = createSoftRingAtlas().toDataURL();
|
||||
return _ringCache;
|
||||
}
|
||||
|
||||
let _atlasCache: string | undefined;
|
||||
export function getAircraftAtlasUrl(): string {
|
||||
if (typeof document === "undefined") return "";
|
||||
if (!_atlasCache) _atlasCache = createAircraftAtlas().toDataURL();
|
||||
return _atlasCache;
|
||||
}
|
||||
Reference in New Issue
Block a user