// 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 (