Merge pull request #45 from accius/Modular-Staging

Modular staging
pull/65/head
accius 2 days ago committed by GitHub
commit 990e30e5a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -285,6 +285,7 @@ const noaaCache = {
kindex: { data: null, timestamp: 0 }, kindex: { data: null, timestamp: 0 },
sunspots: { data: null, timestamp: 0 }, sunspots: { data: null, timestamp: 0 },
xray: { data: null, timestamp: 0 }, xray: { data: null, timestamp: 0 },
aurora: { data: null, timestamp: 0 },
solarIndices: { data: null, timestamp: 0 } solarIndices: { data: null, timestamp: 0 }
}; };
const NOAA_CACHE_TTL = 5 * 60 * 1000; // 5 minutes const NOAA_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
@ -670,15 +671,38 @@ app.get('/api/dxpeditions', async (req, res) => {
// NOAA Space Weather - X-Ray Flux // NOAA Space Weather - X-Ray Flux
app.get('/api/noaa/xray', async (req, res) => { app.get('/api/noaa/xray', async (req, res) => {
try { try {
const response = await fetch('https://services.swpc.noaa.gov/json/goes/primary/xrays-7-day.json'); if (noaaCache.xray.data && (Date.now() - noaaCache.xray.timestamp) < NOAA_CACHE_TTL) {
return res.json(noaaCache.xray.data);
}
const response = await fetch('https://services.swpc.noaa.gov/json/goes/primary/xrays-6-hour.json');
const data = await response.json(); const data = await response.json();
noaaCache.xray = { data, timestamp: Date.now() };
res.json(data); res.json(data);
} catch (error) { } catch (error) {
console.error('NOAA X-Ray API error:', error.message); console.error('NOAA X-Ray API error:', error.message);
if (noaaCache.xray.data) return res.json(noaaCache.xray.data);
res.status(500).json({ error: 'Failed to fetch X-ray data' }); res.status(500).json({ error: 'Failed to fetch X-ray data' });
} }
}); });
// NOAA OVATION Aurora Forecast
const AURORA_CACHE_TTL = 10 * 60 * 1000; // 10 minutes (NOAA updates every ~30 min)
app.get('/api/noaa/aurora', async (req, res) => {
try {
if (noaaCache.aurora.data && (Date.now() - noaaCache.aurora.timestamp) < AURORA_CACHE_TTL) {
return res.json(noaaCache.aurora.data);
}
const response = await fetch('https://services.swpc.noaa.gov/json/ovation_aurora_latest.json');
const data = await response.json();
noaaCache.aurora = { data, timestamp: Date.now() };
res.json(data);
} catch (error) {
console.error('NOAA Aurora API error:', error.message);
if (noaaCache.aurora.data) return res.json(noaaCache.aurora.data);
res.status(500).json({ error: 'Failed to fetch aurora data' });
}
});
// POTA Spots // POTA Spots
// POTA cache (2 minutes) // POTA cache (2 minutes)
let potaCache = { data: null, timestamp: 0 }; let potaCache = { data: null, timestamp: 0 };

@ -1,25 +1,81 @@
/** /**
* SolarPanel Component * SolarPanel Component
* Toggleable between live sun image from NASA SDO and solar indices display * Cycles between: Solar Image Solar Indices X-Ray Flux Chart
*/ */
import React, { useState } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
const MODES = ['image', 'indices', 'xray'];
const MODE_LABELS = { image: 'SOLAR', indices: 'SOLAR INDICES', xray: 'X-RAY FLUX' };
const MODE_ICONS = { image: '📊', indices: '📈', xray: '🖼️' };
const MODE_TITLES = { image: 'Show solar indices', indices: 'Show X-ray flux', xray: 'Show solar image' };
// Flare class from flux value (W/m²)
const getFlareClass = (flux) => {
if (!flux || flux <= 0) return { letter: '?', color: '#666', level: 0 };
if (flux >= 1e-4) return { letter: 'X', color: '#ff0000', level: 4 };
if (flux >= 1e-5) return { letter: 'M', color: '#ff6600', level: 3 };
if (flux >= 1e-6) return { letter: 'C', color: '#ffcc00', level: 2 };
if (flux >= 1e-7) return { letter: 'B', color: '#00cc88', level: 1 };
return { letter: 'A', color: '#4488ff', level: 0 };
};
// Format flux value for display
const formatFlux = (flux) => {
if (!flux || flux <= 0) return '--';
const cls = getFlareClass(flux);
const base = flux >= 1e-4 ? flux / 1e-4 :
flux >= 1e-5 ? flux / 1e-5 :
flux >= 1e-6 ? flux / 1e-6 :
flux >= 1e-7 ? flux / 1e-7 : flux / 1e-8;
return `${cls.letter}${base.toFixed(1)}`;
};
export const SolarPanel = ({ solarIndices }) => { export const SolarPanel = ({ solarIndices }) => {
const [showIndices, setShowIndices] = useState(() => { const [mode, setMode] = useState(() => {
try { try {
const saved = localStorage.getItem('openhamclock_solarPanelMode'); const saved = localStorage.getItem('openhamclock_solarPanelMode');
return saved === 'indices'; if (MODES.includes(saved)) return saved;
} catch (e) { return false; } // Migrate old boolean format
if (saved === 'indices') return 'indices';
return 'image';
} catch (e) { return 'image'; }
}); });
const [imageType, setImageType] = useState('0193'); const [imageType, setImageType] = useState('0193');
const [xrayData, setXrayData] = useState(null);
const [xrayLoading, setXrayLoading] = useState(false);
const toggleMode = () => { const cycleMode = () => {
const newMode = !showIndices; const nextIdx = (MODES.indexOf(mode) + 1) % MODES.length;
setShowIndices(newMode); const next = MODES[nextIdx];
try { setMode(next);
localStorage.setItem('openhamclock_solarPanelMode', newMode ? 'indices' : 'image'); try { localStorage.setItem('openhamclock_solarPanelMode', next); } catch (e) {}
} catch (e) {}
}; };
// Fetch X-ray data when xray mode is active
const fetchXray = useCallback(async () => {
try {
setXrayLoading(true);
const res = await fetch('/api/noaa/xray');
if (res.ok) {
const data = await res.json();
// Filter to 0.1-0.8nm (long wavelength, standard for flare classification)
const filtered = data.filter(d => d.energy === '0.1-0.8nm' && d.flux > 0);
setXrayData(filtered);
}
} catch (err) {
console.error('X-ray fetch error:', err);
} finally {
setXrayLoading(false);
}
}, []);
useEffect(() => {
if (mode === 'xray') {
fetchXray();
const interval = setInterval(fetchXray, 5 * 60 * 1000); // 5 min refresh
return () => clearInterval(interval);
}
}, [mode, fetchXray]);
const imageTypes = { const imageTypes = {
'0193': { name: 'AIA 193Å', desc: 'Corona' }, '0193': { name: 'AIA 193Å', desc: 'Corona' },
@ -40,12 +96,158 @@ export const SolarPanel = ({ solarIndices }) => {
return '#00ff88'; return '#00ff88';
}; };
// Get K-Index data - server returns 'kp' not 'kIndex'
const kpData = solarIndices?.data?.kp || solarIndices?.data?.kIndex; const kpData = solarIndices?.data?.kp || solarIndices?.data?.kIndex;
// X-Ray flux chart renderer
const renderXrayChart = () => {
if (xrayLoading && !xrayData) {
return <div style={{ textAlign: 'center', color: 'var(--text-muted)', padding: '20px' }}>Loading X-ray data...</div>;
}
if (!xrayData || xrayData.length === 0) {
return <div style={{ textAlign: 'center', color: 'var(--text-muted)', padding: '20px' }}>No X-ray data available</div>;
}
// Use last ~360 points (~6 hours at 1-min resolution)
const points = xrayData.slice(-360);
const currentFlux = points[points.length - 1]?.flux;
const currentClass = getFlareClass(currentFlux);
const peakFlux = Math.max(...points.map(p => p.flux));
const peakClass = getFlareClass(peakFlux);
// Chart dimensions
const W = 280, H = 130;
const padL = 28, padR = 6, padT = 8, padB = 18;
const chartW = W - padL - padR;
const chartH = H - padT - padB;
// Log scale: 1e-8 (A1.0) to 1e-3 (X10)
const logMin = -8, logMax = -3;
const logRange = logMax - logMin;
const fluxToY = (flux) => {
if (!flux || flux <= 0) return padT + chartH;
const log = Math.log10(flux);
const clamped = Math.max(logMin, Math.min(logMax, log));
return padT + chartH - ((clamped - logMin) / logRange) * chartH;
};
// Build SVG path
const pathD = points.map((p, i) => {
const x = padL + (i / (points.length - 1)) * chartW;
const y = fluxToY(p.flux);
return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`;
}).join(' ');
// Gradient fill path
const fillD = pathD + ` L${(padL + chartW).toFixed(1)},${(padT + chartH).toFixed(1)} L${padL},${(padT + chartH).toFixed(1)} Z`;
// Flare class threshold lines
const thresholds = [
{ flux: 1e-7, label: 'B', color: '#00cc88' },
{ flux: 1e-6, label: 'C', color: '#ffcc00' },
{ flux: 1e-5, label: 'M', color: '#ff6600' },
{ flux: 1e-4, label: 'X', color: '#ff0000' }
];
// Time labels
const firstTime = new Date(points[0]?.time_tag);
const lastTime = new Date(points[points.length - 1]?.time_tag);
const midTime = new Date((firstTime.getTime() + lastTime.getTime()) / 2);
const fmt = (d) => `${String(d.getUTCHours()).padStart(2, '0')}:${String(d.getUTCMinutes()).padStart(2, '0')}`;
return (
<div>
{/* Current level display */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '6px',
padding: '4px 8px',
background: 'var(--bg-tertiary)',
borderRadius: '6px'
}}>
<div>
<span style={{ fontSize: '10px', color: 'var(--text-muted)' }}>Current </span>
<span style={{
fontSize: '18px', fontWeight: '700', color: currentClass.color,
fontFamily: 'Orbitron, monospace'
}}>
{formatFlux(currentFlux)}
</span>
</div>
<div>
<span style={{ fontSize: '10px', color: 'var(--text-muted)' }}>6h Peak </span>
<span style={{
fontSize: '14px', fontWeight: '600', color: peakClass.color,
fontFamily: 'Orbitron, monospace'
}}>
{formatFlux(peakFlux)}
</span>
</div>
</div>
{/* SVG Chart */}
<svg width="100%" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid meet"
style={{ background: 'var(--bg-tertiary)', borderRadius: '6px' }}>
{/* Flare class background bands */}
{thresholds.map((t, i) => {
const y1 = fluxToY(t.flux);
const y0 = i === 0 ? padT + chartH : fluxToY(thresholds[i - 1].flux);
return (
<rect key={t.label} x={padL} y={y1} width={chartW} height={y0 - y1}
fill={t.color} opacity={0.06} />
);
})}
{/* X class band to top */}
<rect x={padL} y={padT} width={chartW}
height={fluxToY(1e-4) - padT} fill="#ff0000" opacity={0.06} />
{/* Threshold lines */}
{thresholds.map(t => {
const y = fluxToY(t.flux);
return (
<g key={t.label}>
<line x1={padL} y1={y} x2={padL + chartW} y2={y}
stroke={t.color} strokeWidth="0.5" strokeDasharray="3,3" opacity={0.5} />
<text x={padL + 2} y={y - 2} fill={t.color} fontSize="8" fontWeight="600"
fontFamily="JetBrains Mono, monospace">{t.label}</text>
</g>
);
})}
{/* Gradient fill under curve */}
<defs>
<linearGradient id="xrayGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={currentClass.color} stopOpacity="0.4" />
<stop offset="100%" stopColor={currentClass.color} stopOpacity="0.05" />
</linearGradient>
</defs>
<path d={fillD} fill="url(#xrayGrad)" />
{/* Flux line */}
<path d={pathD} fill="none" stroke={currentClass.color} strokeWidth="1.5" />
{/* Time axis labels */}
<text x={padL} y={H - 2} fill="var(--text-muted, #888)" fontSize="8"
fontFamily="JetBrains Mono, monospace">{fmt(firstTime)}</text>
<text x={padL + chartW / 2} y={H - 2} fill="var(--text-muted, #888)" fontSize="8"
fontFamily="JetBrains Mono, monospace" textAnchor="middle">{fmt(midTime)}</text>
<text x={padL + chartW} y={H - 2} fill="var(--text-muted, #888)" fontSize="8"
fontFamily="JetBrains Mono, monospace" textAnchor="end">{fmt(lastTime)} UTC</text>
</svg>
<div style={{ fontSize: '9px', color: 'var(--text-muted)', marginTop: '3px', textAlign: 'center' }}>
GOES 0.10.8nm 6hr
</div>
</div>
);
};
return ( return (
<div className="panel" style={{ padding: '8px' }}> <div className="panel" style={{ padding: '8px' }}>
{/* Header with toggle */} {/* Header with cycle button */}
<div style={{ <div style={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
@ -53,10 +255,10 @@ export const SolarPanel = ({ solarIndices }) => {
marginBottom: '6px' marginBottom: '6px'
}}> }}>
<span style={{ fontSize: '12px', color: 'var(--accent-amber)', fontWeight: '700' }}> <span style={{ fontSize: '12px', color: 'var(--accent-amber)', fontWeight: '700' }}>
{showIndices ? 'SOLAR INDICES' : 'SOLAR'} {MODE_LABELS[mode]}
</span> </span>
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}> <div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
{!showIndices && ( {mode === 'image' && (
<select <select
value={imageType} value={imageType}
onChange={(e) => setImageType(e.target.value)} onChange={(e) => setImageType(e.target.value)}
@ -77,7 +279,7 @@ export const SolarPanel = ({ solarIndices }) => {
</select> </select>
)} )}
<button <button
onClick={toggleMode} onClick={cycleMode}
style={{ style={{
background: 'var(--bg-tertiary)', background: 'var(--bg-tertiary)',
border: '1px solid var(--border-color)', border: '1px solid var(--border-color)',
@ -87,14 +289,14 @@ export const SolarPanel = ({ solarIndices }) => {
borderRadius: '3px', borderRadius: '3px',
cursor: 'pointer' cursor: 'pointer'
}} }}
title={showIndices ? 'Show solar image' : 'Show solar indices'} title={MODE_TITLES[mode]}
> >
{showIndices ? '🖼️' : '📊'} {MODE_ICONS[mode]}
</button> </button>
</div> </div>
</div> </div>
{showIndices ? ( {mode === 'indices' ? (
/* Solar Indices View */ /* Solar Indices View */
<div> <div>
{solarIndices?.data ? ( {solarIndices?.data ? (
@ -208,6 +410,9 @@ export const SolarPanel = ({ solarIndices }) => {
</div> </div>
)} )}
</div> </div>
) : mode === 'xray' ? (
/* X-Ray Flux Chart View */
renderXrayChart()
) : ( ) : (
/* Solar Image View */ /* Solar Image View */
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>

@ -648,7 +648,7 @@ export const WorldMap = ({
return ( return (
<div style={{ position: 'relative', height: '100%', minHeight: '200px' }}> <div style={{ position: 'relative', height: '100%', minHeight: '200px' }}>
<div ref={mapRef} style={{ height: '100%', width: '100%', borderRadius: '8px' }} /> <div ref={mapRef} style={{ height: '100%', width: '100%', borderRadius: '8px', background: mapStyle === 'countries' ? '#4a90d9' : undefined }} />
{/* Render all plugin layers */} {/* Render all plugin layers */}
{mapInstanceRef.current && getAllLayers().map(layerDef => ( {mapInstanceRef.current && getAllLayers().map(layerDef => (

@ -1,14 +1,15 @@
/** /**
* Layer Plugin Registry * Layer Plugin Registry
* Only Weather Radar for now
*/ */
import * as WXRadarPlugin from './layers/useWXRadar.js'; import * as WXRadarPlugin from './layers/useWXRadar.js';
import * as EarthquakesPlugin from './layers/useEarthquakes.js'; import * as EarthquakesPlugin from './layers/useEarthquakes.js';
import * as AuroraPlugin from './layers/useAurora.js';
const layerPlugins = [ const layerPlugins = [
WXRadarPlugin, WXRadarPlugin,
EarthquakesPlugin, EarthquakesPlugin,
AuroraPlugin,
]; ];
export function getAllLayers() { export function getAllLayers() {

@ -0,0 +1,250 @@
import { useState, useEffect, useRef } from 'react';
// NOAA OVATION Aurora Forecast - JSON grid data
// Endpoint: /api/noaa/aurora (proxied from services.swpc.noaa.gov/json/ovation_aurora_latest.json)
// Format: { "Forecast Time": "...", "coordinates": [[lon, lat, probability], ...] }
// Grid: 360 longitudes (0-359) × 181 latitudes (-90 to 90), probability 0-100
export const metadata = {
id: 'aurora',
name: 'Aurora Forecast',
description: 'NOAA OVATION aurora probability forecast (30-min)',
icon: '🌌',
category: 'space-weather',
defaultEnabled: false,
defaultOpacity: 0.6,
version: '2.0.0'
};
// Aurora color ramp: transparent → green → yellow → red
// Matches NOAA's official aurora visualization
function auroraCmap(probability) {
if (probability < 4) return null; // Skip very low values
// Normalize 4-100 to 0-1
const t = Math.min((probability - 4) / 80, 1);
let r, g, b, a;
if (t < 0.25) {
// Dark green to green
const s = t / 0.25;
r = 0;
g = Math.round(80 + s * 175);
b = Math.round(40 * (1 - s));
a = 0.3 + s * 0.3;
} else if (t < 0.5) {
// Green to yellow-green
const s = (t - 0.25) / 0.25;
r = Math.round(s * 200);
g = 255;
b = 0;
a = 0.6 + s * 0.15;
} else if (t < 0.75) {
// Yellow to orange
const s = (t - 0.5) / 0.25;
r = 255;
g = Math.round(255 - s * 120);
b = 0;
a = 0.75 + s * 0.1;
} else {
// Orange to red
const s = (t - 0.75) / 0.25;
r = 255;
g = Math.round(135 - s * 135);
b = Math.round(s * 30);
a = 0.85 + s * 0.15;
}
return { r, g, b, a };
}
function buildAuroraCanvas(coordinates) {
// Create a 360×181 canvas (1° resolution)
const canvas = document.createElement('canvas');
canvas.width = 360;
canvas.height = 181;
const ctx = canvas.getContext('2d');
// Clear to transparent
ctx.clearRect(0, 0, 360, 181);
const imageData = ctx.createImageData(360, 181);
const pixels = imageData.data;
// Process all coordinate points
for (let i = 0; i < coordinates.length; i++) {
const [lon, lat, prob] = coordinates[i];
if (prob < 4) continue; // Skip negligible
const color = auroraCmap(prob);
if (!color) continue;
// NOAA grid: lon 0-359, lat -90 to 90
// Canvas: x = lon (0-359), y = 0 is +90 (north), y = 180 is -90 (south)
const x = Math.round(lon) % 360;
const y = 90 - Math.round(lat); // Flip: lat 90 → y 0, lat -90 → y 180
if (x < 0 || x >= 360 || y < 0 || y >= 181) continue;
const idx = (y * 360 + x) * 4;
pixels[idx] = color.r;
pixels[idx + 1] = color.g;
pixels[idx + 2] = color.b;
pixels[idx + 3] = Math.round(color.a * 255);
}
ctx.putImageData(imageData, 0, 0);
// Scale up with a larger canvas for smoother rendering
const smoothCanvas = document.createElement('canvas');
smoothCanvas.width = 720;
smoothCanvas.height = 362;
const sctx = smoothCanvas.getContext('2d');
sctx.imageSmoothingEnabled = true;
sctx.imageSmoothingQuality = 'high';
sctx.drawImage(canvas, 0, 0, 720, 362);
return smoothCanvas.toDataURL('image/png');
}
export function useLayer({ enabled = false, opacity = 0.6, map = null }) {
const [overlayLayer, setOverlayLayer] = useState(null);
const [auroraData, setAuroraData] = useState(null);
const [forecastTime, setForecastTime] = useState(null);
const fetchingRef = useRef(false);
// Fetch aurora JSON data
useEffect(() => {
if (!enabled) return;
const fetchAurora = async () => {
if (fetchingRef.current) return;
fetchingRef.current = true;
try {
const res = await fetch('/api/noaa/aurora');
if (res.ok) {
const data = await res.json();
if (data.coordinates && data.coordinates.length > 0) {
setAuroraData(data.coordinates);
setForecastTime(data['Forecast Time'] || null);
}
}
} catch (err) {
console.error('Aurora data fetch error:', err);
} finally {
fetchingRef.current = false;
}
};
fetchAurora();
// Refresh every 10 minutes
const interval = setInterval(fetchAurora, 600000);
return () => clearInterval(interval);
}, [enabled]);
// Render overlay when data or map changes
useEffect(() => {
if (!map || typeof L === 'undefined') return;
// Remove existing
if (overlayLayer) {
try { map.removeLayer(overlayLayer); } catch (e) {}
setOverlayLayer(null);
}
if (!enabled || !auroraData) return;
try {
const dataUrl = buildAuroraCanvas(auroraData);
// NOAA grid: lon 0-359, lat -90 to 90
// Leaflet bounds: [[south, west], [north, east]]
// Shift by 180° so 0° longitude is centered properly
// The data starts at lon=0 (Greenwich), so the image spans [0, 360) in longitude
// We need two overlays or shift the data. Simplest: overlay from -180 to 180 with shifted image.
// Actually, L.imageOverlay with bounds [[-90, 0], [90, 360]] works because Leaflet wraps.
// But for proper centering, let's use [[-90, -180], [90, 180]] and shift the canvas.
// Build a shifted canvas where lon 0-179 goes to right half, lon 180-359 goes to left half
const shiftedCanvas = document.createElement('canvas');
shiftedCanvas.width = 720;
shiftedCanvas.height = 362;
const sctx = shiftedCanvas.getContext('2d');
sctx.imageSmoothingEnabled = true;
sctx.imageSmoothingQuality = 'high';
// Rebuild from raw data with shifted longitudes
const rawCanvas = document.createElement('canvas');
rawCanvas.width = 360;
rawCanvas.height = 181;
const rctx = rawCanvas.getContext('2d');
rctx.clearRect(0, 0, 360, 181);
const imageData = rctx.createImageData(360, 181);
const pixels = imageData.data;
for (let i = 0; i < auroraData.length; i++) {
const [lon, lat, prob] = auroraData[i];
if (prob < 4) continue;
const color = auroraCmap(prob);
if (!color) continue;
// Shift longitude: NOAA 0-359 → map -180 to 179
let x = Math.round(lon);
x = x >= 180 ? x - 180 : x + 180; // Shift so -180 maps to pixel 0
x = x % 360;
const y = 90 - Math.round(lat);
if (x < 0 || x >= 360 || y < 0 || y >= 181) continue;
const idx = (y * 360 + x) * 4;
pixels[idx] = color.r;
pixels[idx + 1] = color.g;
pixels[idx + 2] = color.b;
pixels[idx + 3] = Math.round(color.a * 255);
}
rctx.putImageData(imageData, 0, 0);
sctx.drawImage(rawCanvas, 0, 0, 720, 362);
const shiftedUrl = shiftedCanvas.toDataURL('image/png');
const overlay = L.imageOverlay(
shiftedUrl,
[[-90, -180], [90, 180]],
{
opacity: opacity,
zIndex: 210,
interactive: false
}
);
overlay.addTo(map);
setOverlayLayer(overlay);
} catch (err) {
console.error('Aurora overlay render error:', err);
}
return () => {
if (overlayLayer && map) {
try { map.removeLayer(overlayLayer); } catch (e) {}
}
};
}, [enabled, auroraData, map]);
// Update opacity
useEffect(() => {
if (overlayLayer) {
overlayLayer.setOpacity(opacity);
}
}, [opacity, overlayLayer]);
return {
layer: overlayLayer,
forecastTime,
refresh: () => {
setAuroraData(null);
fetchingRef.current = false;
}
};
}

@ -200,7 +200,7 @@ export const MAP_STYLES = {
}, },
countries: { countries: {
name: 'Countries', name: 'Countries',
url: 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}', url: 'https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}',
attribution: '&copy; Esri, Natural Earth', attribution: '&copy; Esri, Natural Earth',
countriesOverlay: true countriesOverlay: true
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.