// Route map — editorial infographic with REAL country silhouettes (Natural Earth) // Source SVG viewBox: 800x307, equirectangular projection, lon_0=90 const { useEffect, useState } = React; const X_SCALE = 3.98, X_ORIGIN = 399.3; const Y_SCALE = 4.67, Y_ORIGIN = 348.4; const proj = (lon, lat) => ({ x: X_ORIGIN + (lon - 90) * X_SCALE, y: Y_ORIGIN - lat * Y_SCALE, }); function RouteMap(){ const rawCities = [ { id: "spb", name: "Санкт-Петербург", lon: 30.31, lat: 59.94, side: "ru", hub: true, tag: "головной офис", labelDir: "up" }, { id: "msk", name: "Москва", lon: 37.62, lat: 55.75, side: "ru", hub: true, labelDir: "down" }, { id: "ekb", name: "Екатеринбург", lon: 60.61, lat: 56.84, side: "ru", labelDir: "up" }, { id: "nsk", name: "Новосибирск", lon: 82.93, lat: 55.03, side: "ru", labelDir: "up" }, { id: "alm", name: "Алматы", lon: 76.95, lat: 43.26, side: "kz", labelDir: "left" }, { id: "ur", name: "Урумчи", lon: 87.62, lat: 43.83, side: "cn", labelDir: "right" }, { id: "bla", name: "Благовещенск", lon: 127.5, lat: 50.27, side: "ru", labelDir: "left" }, { id: "hh", name: "Хэйхэ", lon: 127.5, lat: 50.24, side: "cn", labelDir: "right" }, { id: "bj", name: "Пекин", lon: 116.4, lat: 39.9, side: "cn", labelDir: "right" }, { id: "yw", name: "Иу", lon: 120.07, lat: 29.31, side: "cn", labelDir: "right" }, { id: "sh", name: "Шанхай", lon: 121.47, lat: 31.23, side: "cn", labelDir: "right" }, { id: "gz", name: "Гуанчжоу", lon: 113.27, lat: 23.13, side: "cn", hub: true, tag: "наш склад", labelDir: "left" }, ]; const cities = rawCities.map(c => ({ ...c, ...proj(c.lon, c.lat) })); const lanes = [ { from: "gz", to: "msk", mode: "auto" }, { from: "gz", to: "spb", mode: "auto" }, { from: "yw", to: "ekb", mode: "auto" }, { from: "ur", to: "alm", mode: "auto" }, { from: "hh", to: "bla", mode: "auto" }, { from: "sh", to: "msk", mode: "sea" }, { from: "gz", to: "spb", mode: "air" }, { from: "bj", to: "nsk", mode: "auto" }, ]; const find = id => cities.find(c => c.id === id); const [t, setT] = useState(0); useEffect(() => { let raf; const start = performance.now(); const tick = (now) => { setT(((now - start) / 8000) % 1); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, []); function curvePath(a, b){ const mx = (a.x + b.x) / 2; const my = Math.min(a.y, b.y) - Math.abs(a.x - b.x) * 0.18 - 16; return { d: `M ${a.x} ${a.y} Q ${mx} ${my} ${b.x} ${b.y}`, mx, my }; } function pointOn(a, b, p, cx, cy){ const x = (1-p)*(1-p)*a.x + 2*(1-p)*p*cx + p*p*b.x; const y = (1-p)*(1-p)*a.y + 2*(1-p)*p*cy + p*p*b.y; return { x, y }; } // River paths (approximations using lon/lat sequences) const rivers = [ { name: "Волга", pts: [[37.5,57.3],[44.5,55.7],[47.5,53.4],[48.0,49.5],[47.0,46.4]] }, { name: "Обь", pts: [[83.0,54.3],[78.7,57.8],[72.0,62.0],[68.5,66.5],[68.0,71.0]] }, { name: "Енисей", pts: [[91.4,53.7],[92.8,58.5],[88.0,66.5],[83.0,71.0],[82.0,73.5]] }, { name: "Лена", pts: [[105.0,53.5],[114.0,58.5],[126.0,63.5],[127.0,71.0]] }, { name: "Амур", pts: [[117.0,49.6],[127.5,50.3],[135.0,48.5],[140.7,53.0]] }, { name: "Янцзы", pts: [[103.0,29.5],[110.0,30.6],[115.0,30.0],[121.5,31.7]] }, { name: "Хуанхэ", pts: [[103.0,36.0],[108.5,35.5],[112.0,34.8],[115.0,37.5],[119.0,37.8]] }, { name: "Меконг", pts: [[101.0,21.0],[103.5,18.0],[105.5,15.5],[106.0,11.5]] }, ]; const countryLabels = [ { iso: "RUS", name: "РОССИЯ", sub: "RUSSIAN FEDERATION", lon: 75, lat: 60, primary: true }, { iso: "CHN", name: "КИТАЙ", sub: "中华人民共和国", lon: 100, lat: 35, primary: true }, { iso: "KAZ", name: "КАЗАХСТАН", sub: null, lon: 67, lat: 49, hint: true }, { iso: "MNG", name: "МОНГОЛИЯ", sub: null, lon: 103, lat: 47, hint: true }, { iso: "JPN", name: "ЯПОНИЯ", sub: null, lon: 138, lat: 37, hint: true }, { iso: "IND", name: "ИНДИЯ", sub: null, lon: 79, lat: 23, hint: true, tiny: true }, ]; // Seas / oceans const seaLabels = [ { name: "К А С П И Й С К О Е М О Р Е", lon: 51, lat: 41 }, { name: "А Р А Л", lon: 60, lat: 45 }, { name: "ЖЁЛТОЕ МОРЕ", lon: 123, lat: 36 }, { name: "ЯПОНСКОЕ МОРЕ", lon: 134, lat: 41 }, { name: "ОХОТСКОЕ МОРЕ", lon: 148, lat: 56 }, { name: "Ю.-КИТ. МОРЕ", lon: 117, lat: 17 }, ]; const C = (window.COUNTRIES || {}); const allIso = Object.keys(C); const highlightSet = new Set(["RUS", "CHN"]); // Helper: build smooth river path through points (catmull-rom-ish via quadratic mids) function riverPath(pts){ const proj_pts = pts.map(([lon,lat]) => proj(lon, lat)); if (proj_pts.length < 2) return ""; let d = `M ${proj_pts[0].x} ${proj_pts[0].y}`; for (let i = 1; i < proj_pts.length; i++){ const p = proj_pts[i]; const prev = proj_pts[i-1]; const mx = (prev.x + p.x) / 2; const my = (prev.y + p.y) / 2; d += ` Q ${prev.x} ${prev.y} ${mx} ${my}`; } const last = proj_pts[proj_pts.length-1]; d += ` T ${last.x} ${last.y}`; return d; } // Label position helpers function cityLabelPos(c){ const dir = c.labelDir || (c.lon < 90 ? "left" : "right"); if (dir === "right") return { tx: 5, ty: 1.5, anchor: "start", tagY: 8 }; if (dir === "left") return { tx: -5, ty: 1.5, anchor: "end", tagY: 8 }; if (dir === "up") return { tx: 0, ty: -11, anchor: "middle", tagY: -5 }; if (dir === "down") return { tx: 0, ty: 9, anchor: "middle", tagY: 16 }; return { tx: 5, ty: 1.5, anchor: "start", tagY: 8 }; } return (
{/* Curve filter for warping the whole map area */} {/* Background grids */} {/* Latitude / longitude reference */} {[60, 50, 40, 30].map(lat => { const y = Y_ORIGIN - lat * Y_SCALE; return ( {lat}° ); })} {[40, 60, 80, 100, 120, 140].map(lon => { const x = X_ORIGIN + (lon - 90) * X_SCALE; return ( {lon}° ); })} {/* Sea labels (under land) */} {seaLabels.map((s, i) => { const p = proj(s.lon, s.lat); return {s.name}; })} {/* All countries — base layer */} {allIso.filter(iso => !highlightSet.has(iso)).map(iso => { const c = C[iso]; return ( ); })} {allIso.filter(iso => !highlightSet.has(iso)).map(iso => { const c = C[iso]; return ( ); })} {/* Highlighted countries */} {["RUS", "CHN"].map(iso => { const c = C[iso]; if (!c) return null; return ( ); })} {/* Rivers — disabled */} {rivers.map((r, i) => { const d = riverPath(r.pts); const mid = proj(r.pts[Math.floor(r.pts.length/2)][0], r.pts[Math.floor(r.pts.length/2)][1]); return ( {r.name} ); })} {/* Country labels */} {countryLabels.map(l => { const p = proj(l.lon, l.lat); if (l.primary) { return ( {l.name} {l.sub && {l.sub}} ); } return {l.name}; })} {/* Hub halos */} {cities.filter(c => c.hub).map(c => ( ))} {/* Lanes */} {lanes.map((ln, i) => { const a = find(ln.from), b = find(ln.to); if (!a || !b) return null; const { d, mx, my } = curvePath(a, b); const dash = ln.mode === "sea" ? "2.5 2.5" : ln.mode === "air" ? "0.5 2.5" : "none"; const p = (t + i * 0.09) % 1; const pt = pointOn(a, b, p, mx, my); return ( ); })} {/* Cities */} {cities.map(c => { const lp = cityLabelPos(c); return ( {c.hub ? ( <> ) : ( <> )} {c.name} {c.tag && ( {c.tag} )} ); })} {/* Compass */} N {/* Scale bar */} 0 1500 3000 км {/* Legend */} РЕЖИМЫ авто море авиа C/ONE · СЕТЬ МАРШРУТОВ · 2026.Q2 Natural Earth · eqc 90°
); } window.RouteMap = RouteMap;