Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/app/event-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ export class EventHandlerManager implements AppModule {
this.boundFullscreenHandler = () => {
fullscreenBtn.textContent = document.fullscreenElement ? '\u26F6' : '\u26F6';
fullscreenBtn.classList.toggle('active', !!document.fullscreenElement);
this.syncMapAfterLayoutChange();
};
document.addEventListener('fullscreenchange', this.boundFullscreenHandler);
}
Expand Down Expand Up @@ -685,6 +686,16 @@ export class EventHandlerManager implements AppModule {
};
}

private syncMapAfterLayoutChange(delayMs = 320): void {
const sync = () => {
this.ctx.map?.setIsResizing(false);
this.ctx.map?.render();
};

requestAnimationFrame(sync);
window.setTimeout(sync, delayMs);
}

private async exitFullscreenForNavigation(): Promise<void> {
const fullscreenDocument = this.getFullscreenDocument();
if (!fullscreenDocument.fullscreenElement && !fullscreenDocument.webkitFullscreenElement) return;
Expand Down Expand Up @@ -1171,8 +1182,7 @@ export class EventHandlerManager implements AppModule {
document.body.classList.toggle('live-news-fullscreen-active', isFullscreen);
btn.innerHTML = isFullscreen ? shrinkSvg : expandSvg;
btn.title = isFullscreen ? 'Exit fullscreen' : 'Fullscreen';
// Notify map so globe (and deck.gl) can resize after CSS transition completes
setTimeout(() => this.ctx.map?.setIsResizing(false), 320);
this.syncMapAfterLayoutChange();
};

btn.addEventListener('click', toggle);
Expand Down
101 changes: 59 additions & 42 deletions src/components/DeckGLMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4056,11 +4056,7 @@ export class DeckGLMap {
this.container.appendChild(popup);
}

private createLegend(): void {
const legend = document.createElement('div');
legend.className = 'map-legend deckgl-legend';

// SVG shapes for different marker types
private getLegendItems(): Array<{ shape: string; label: string }> {
const shapes = {
circle: (color: string) => `<svg width="12" height="12" viewBox="0 0 12 12"><circle cx="6" cy="6" r="5" fill="${color}"/></svg>`,
triangle: (color: string) => `<svg width="12" height="12" viewBox="0 0 12 12"><polygon points="6,1 11,10 1,10" fill="${color}"/></svg>`,
Expand All @@ -4069,49 +4065,64 @@ export class DeckGLMap {
};

const isLight = getCurrentTheme() === 'light';
const legendItems = SITE_VARIANT === 'tech'
? [
{ shape: shapes.circle(isLight ? 'rgb(22, 163, 74)' : 'rgb(0, 255, 150)'), label: t('components.deckgl.legend.startupHub') },
{ shape: shapes.circle('rgb(100, 200, 255)'), label: t('components.deckgl.legend.techHQ') },
{ shape: shapes.circle(isLight ? 'rgb(180, 120, 0)' : 'rgb(255, 200, 0)'), label: t('components.deckgl.legend.accelerator') },
{ shape: shapes.circle('rgb(150, 100, 255)'), label: t('components.deckgl.legend.cloudRegion') },
{ shape: shapes.square('rgb(136, 68, 255)'), label: t('components.deckgl.legend.datacenter') },
]
: SITE_VARIANT === 'finance'
? [
{ shape: shapes.circle('rgb(255, 215, 80)'), label: t('components.deckgl.legend.stockExchange') },
{ shape: shapes.circle('rgb(0, 220, 150)'), label: t('components.deckgl.legend.financialCenter') },
{ shape: shapes.hexagon('rgb(255, 210, 80)'), label: t('components.deckgl.legend.centralBank') },
{ shape: shapes.square('rgb(255, 150, 80)'), label: t('components.deckgl.legend.commodityHub') },
{ shape: shapes.triangle('rgb(80, 170, 255)'), label: t('components.deckgl.legend.waterway') },
]
: SITE_VARIANT === 'happy'
? [
{ shape: shapes.circle('rgb(34, 197, 94)'), label: 'Positive Event' },
{ shape: shapes.circle('rgb(234, 179, 8)'), label: 'Breakthrough' },
{ shape: shapes.circle('rgb(74, 222, 128)'), label: 'Act of Kindness' },
{ shape: shapes.circle('rgb(255, 100, 50)'), label: 'Natural Event' },
{ shape: shapes.square('rgb(34, 180, 100)'), label: 'Happy Country' },
{ shape: shapes.circle('rgb(74, 222, 128)'), label: 'Species Recovery Zone' },
{ shape: shapes.circle('rgb(255, 200, 50)'), label: 'Renewable Installation' },
{ shape: shapes.circle('rgb(160, 100, 255)'), label: t('components.deckgl.legend.aircraft') },
]
: [
{ shape: shapes.circle('rgb(255, 68, 68)'), label: t('components.deckgl.legend.highAlert') },
{ shape: shapes.circle('rgb(255, 165, 0)'), label: t('components.deckgl.legend.elevated') },
{ shape: shapes.circle(isLight ? 'rgb(180, 120, 0)' : 'rgb(255, 255, 0)'), label: t('components.deckgl.legend.monitoring') },
{ shape: shapes.triangle('rgb(68, 136, 255)'), label: t('components.deckgl.legend.base') },
{ shape: shapes.hexagon(isLight ? 'rgb(180, 120, 0)' : 'rgb(255, 220, 0)'), label: t('components.deckgl.legend.nuclear') },
{ shape: shapes.square('rgb(136, 68, 255)'), label: t('components.deckgl.legend.datacenter') },
{ shape: shapes.circle('rgb(160, 100, 255)'), label: t('components.deckgl.legend.aircraft') },
];
if (SITE_VARIANT === 'tech') {
const techLegendItems: Array<{ shape: string; label: string; layer: keyof MapLayers }> = [
{ shape: shapes.circle(isLight ? 'rgb(22, 163, 74)' : 'rgb(0, 255, 150)'), label: t('components.deckgl.legend.startupHub'), layer: 'startupHubs' },
{ shape: shapes.circle('rgb(100, 200, 255)'), label: t('components.deckgl.legend.techHQ'), layer: 'techHQs' },
{ shape: shapes.circle(isLight ? 'rgb(180, 120, 0)' : 'rgb(255, 200, 0)'), label: t('components.deckgl.legend.accelerator'), layer: 'accelerators' },
{ shape: shapes.circle('rgb(150, 100, 255)'), label: t('components.deckgl.legend.cloudRegion'), layer: 'cloudRegions' },
{ shape: shapes.square('rgb(136, 68, 255)'), label: t('components.deckgl.legend.datacenter'), layer: 'datacenters' },
];
return techLegendItems
.filter(({ layer }) => this.state.layers[layer])
.map(({ shape, label }) => ({ shape, label }));
}

if (SITE_VARIANT === 'finance') {
return [
{ shape: shapes.circle('rgb(255, 215, 80)'), label: t('components.deckgl.legend.stockExchange') },
{ shape: shapes.circle('rgb(0, 220, 150)'), label: t('components.deckgl.legend.financialCenter') },
{ shape: shapes.hexagon('rgb(255, 210, 80)'), label: t('components.deckgl.legend.centralBank') },
{ shape: shapes.square('rgb(255, 150, 80)'), label: t('components.deckgl.legend.commodityHub') },
{ shape: shapes.triangle('rgb(80, 170, 255)'), label: t('components.deckgl.legend.waterway') },
];
}

if (SITE_VARIANT === 'happy') {
return [
{ shape: shapes.circle('rgb(34, 197, 94)'), label: 'Positive Event' },
{ shape: shapes.circle('rgb(234, 179, 8)'), label: 'Breakthrough' },
{ shape: shapes.circle('rgb(74, 222, 128)'), label: 'Act of Kindness' },
{ shape: shapes.circle('rgb(255, 100, 50)'), label: 'Natural Event' },
{ shape: shapes.square('rgb(34, 180, 100)'), label: 'Happy Country' },
{ shape: shapes.circle('rgb(74, 222, 128)'), label: 'Species Recovery Zone' },
{ shape: shapes.circle('rgb(255, 200, 50)'), label: 'Renewable Installation' },
{ shape: shapes.circle('rgb(160, 100, 255)'), label: t('components.deckgl.legend.aircraft') },
];
}

return [
{ shape: shapes.circle('rgb(255, 68, 68)'), label: t('components.deckgl.legend.highAlert') },
{ shape: shapes.circle('rgb(255, 165, 0)'), label: t('components.deckgl.legend.elevated') },
{ shape: shapes.circle(isLight ? 'rgb(180, 120, 0)' : 'rgb(255, 255, 0)'), label: t('components.deckgl.legend.monitoring') },
{ shape: shapes.triangle('rgb(68, 136, 255)'), label: t('components.deckgl.legend.base') },
{ shape: shapes.hexagon(isLight ? 'rgb(180, 120, 0)' : 'rgb(255, 220, 0)'), label: t('components.deckgl.legend.nuclear') },
{ shape: shapes.square('rgb(136, 68, 255)'), label: t('components.deckgl.legend.datacenter') },
{ shape: shapes.circle('rgb(160, 100, 255)'), label: t('components.deckgl.legend.aircraft') },
];
}

private updateLegend(): void {
const legend = this.container.querySelector('.deckgl-legend') as HTMLDivElement | null;
if (!legend) return;

const legendItems = this.getLegendItems();
legend.style.display = legendItems.length === 0 && !this.state.layers.ciiChoropleth ? 'none' : '';
legend.innerHTML = `
<span class="legend-label-title">${t('components.deckgl.legend.title')}</span>
${legendItems.map(({ shape, label }) => `<span class="legend-item">${shape}<span class="legend-label">${label}</span></span>`).join('')}
`;

// CII choropleth gradient legend (shown when layer is active)
const ciiLegend = document.createElement('div');
ciiLegend.className = 'cii-choropleth-legend';
ciiLegend.id = 'ciiChoroplethLegend';
Expand All @@ -4126,8 +4137,13 @@ export class DeckGLMap {
</div>
`;
legend.appendChild(ciiLegend);
}

private createLegend(): void {
const legend = document.createElement('div');
legend.className = 'map-legend deckgl-legend';
this.container.appendChild(legend);
this.updateLegend();
}

// Public API methods (matching MapComponent interface)
Expand All @@ -4141,6 +4157,7 @@ export class DeckGLMap {
}
this.renderRafId = requestAnimationFrame(() => {
this.renderRafId = null;
this.updateLegend();
this.updateLayers();
});
}
Expand Down
29 changes: 29 additions & 0 deletions tests/map-fullscreen-resize.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, '..');
const src = readFileSync(join(root, 'src', 'app', 'event-handlers.ts'), 'utf-8');

describe('map fullscreen resize sync', () => {
it('defines a shared layout-sync helper for fullscreen transitions', () => {
assert.match(src, /private syncMapAfterLayoutChange\(delayMs = 320\): void \{/);
assert.match(src, /requestAnimationFrame\(sync\)/);
assert.match(src, /window\.setTimeout\(sync, delayMs\)/);
});

it('re-syncs the map after browser fullscreen changes', () => {
const fullscreenHandlerBlock = src.match(/this\.boundFullscreenHandler = \(\) => \{([\s\S]*?)\n\s*\};/);
assert.ok(fullscreenHandlerBlock, 'Expected fullscreenchange handler block');
assert.match(fullscreenHandlerBlock[1], /this\.syncMapAfterLayoutChange\(\)/);
});

it('re-syncs the map after map-panel fullscreen toggles', () => {
const mapFullscreenBlock = src.match(/const toggle = \(\) => \{([\s\S]*?)\n\s*\};/);
assert.ok(mapFullscreenBlock, 'Expected map fullscreen toggle block');
assert.match(mapFullscreenBlock[1], /this\.syncMapAfterLayoutChange\(\)/);
});
});
Loading