2135 lines
102 KiB
TypeScript
2135 lines
102 KiB
TypeScript
import { useEffect, useRef, useState, useCallback } from 'react';
|
||
import { Link } from 'react-router-dom';
|
||
import maplibregl from 'maplibre-gl';
|
||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||
import {
|
||
ArrowUpRight, Box, Braces, Building2, Check, CheckCircle2, ChevronRight, Code2,
|
||
Compass, Database, Download, Eye, HardDrive,
|
||
KeyRound, Map, MapPin, Menu, Mountain, Navigation,
|
||
Route, Search, Server, ShieldCheck, Sparkles, Sun, TrendingDown, TrendingUp, X, Zap
|
||
} from 'lucide-react';
|
||
import { useLanguage } from '../i18n';
|
||
|
||
// ─── MapLibre RTL Text Plugin (Arabic / Hebrew label support) ───
|
||
try {
|
||
maplibregl.setRTLTextPlugin(
|
||
'/rtl-plugin.js',
|
||
true // lazy load
|
||
);
|
||
} catch (e) {
|
||
// Plugin already loaded or error — safe to ignore
|
||
}
|
||
|
||
const copy = {
|
||
ar: {
|
||
nav: ['المنصة', 'الخدمات', 'الجدوى', 'التضاريس', 'التغطية', 'للمطورين', 'البنية'],
|
||
login: 'لوحة المطور',
|
||
badge: 'بنية مكانية سيادية لمنتجات تتحرك',
|
||
titleA: 'ابنِ فهمًا',
|
||
titleB: 'للمكان.',
|
||
subtitle: 'خرائط، بحث، مسارات وبيانات مكانية سيادية عبر API وحزم تطوير للويب والموبايل. وفّر 0.30$ في كل رحلة واحصل على سيادة بيانات كاملة في الشرق الأوسط.',
|
||
start: 'ابدأ من لوحة المطور',
|
||
explore: 'استكشف القدرات',
|
||
platform: 'منصة واحدة من الفكرة إلى التشغيل',
|
||
platformText: 'ابدأ بتجربة الواجهة، أنشئ مفتاحك، راقب الاستخدام واختر خطتك من نفس الحساب.',
|
||
services: 'الخدمات الأساسية',
|
||
roiKicker: '03 / UNIT ECONOMICS & PRICING',
|
||
roiTitle: 'مقارنة أسعار الطلبات (Requests) والجدوى لمنصات النقل',
|
||
roiSubtitle: 'مقارنة تفصيلية لكل 1,000 طلب بين واجهات Google Maps والمنصات الأخرى مقابل بنية سيرو السحابية الثابتة، مع تحليل استهلاك الرحلة المكتملة في تطبيقات التنقل والتوصيل.',
|
||
dailyRidesLabel: 'عدد الرحلات المكتملة يوميًا',
|
||
googleCostLabel: 'تكلفة واجهات Google Maps الشهرية',
|
||
siroCostLabel: 'تكلفة بنية سيرو السحابية الثابتة',
|
||
monthlySavingsLabel: 'الوفر الشهري الصافي',
|
||
annualSavingsLabel: 'الوفر السنوي التراكمي',
|
||
unitMarginLabel: 'وفر مباشر لكل رحلة ينعكس في هامش الربح',
|
||
terrainKicker: '04 / 3D TERRAIN & SOLAR SIMULATION',
|
||
terrainTitle: 'محاكاة التضاريس ثلاثية الأبعاد وزاوية الشمس الفلكية',
|
||
terrainSubtitle: 'محرك تضاريس Terrarium DEM مع محاكاة فورية لمسار الشمس الفلكي وزوايا الظلال (315° NW) وخطوط الكنتور الطبوغرافية لمعالم الأردن والمنطقة (وادي رم، البترا، البحر الميت، عمّان).',
|
||
solarTimeLabel: 'وقت اليوم وموقع الشمس الفلكي',
|
||
coverageKicker: '05 / REGIONAL INFRASTRUCTURE & EDGE DEPLOYMENT',
|
||
coverageTitle: 'بنية تحتية إقليمية عالية التوافر ونشر سحابي مخصص وميداني',
|
||
coverageSubtitle: 'تغطية مكانية شاملة لـ 4 دول إقليمية (الأردن، العراق، سوريا، مصر) مع إمكانية النشر الداخلي في السحابة الخاصة للعميل لضمان أعلى مستويات الأداء وسرعة الاستجابة.',
|
||
developers: 'جاهزة لفريقك التقني',
|
||
infra: 'بنية مفتوحة، منتج متكامل',
|
||
infraText: 'نعتمد تقنيات مفتوحة داخل بنية نديرها ونطوّر فوقها تجربة متكاملة للمطور والعميل.',
|
||
cta: 'المكان جزء من منتجك. اجعله أفضل.',
|
||
ctaText: 'أنشئ مشروعك، جرّب الواجهات، ثم انتقل إلى الخطة المناسبة عندما تصبح جاهزًا.',
|
||
},
|
||
en: {
|
||
nav: ['Platform', 'Services', 'Economics', '3D Terrain', 'Infrastructure', 'Developers', 'Architecture'],
|
||
login: 'Developer Console',
|
||
badge: 'High-availability regional location infrastructure',
|
||
titleA: 'Build a better',
|
||
titleB: 'sense of place.',
|
||
subtitle: 'Sovereign maps, search, routing and spatial data through APIs and SDKs for web and mobile. Predictable fixed cloud pricing with 100% data sovereignty in MENA.',
|
||
start: 'Open developer console',
|
||
explore: 'Explore capabilities',
|
||
platform: 'One platform, from idea to operation',
|
||
platformText: 'Try the API, create a key, monitor usage and manage your plan from one account.',
|
||
services: 'Core services',
|
||
roiKicker: '03 / UNIT ECONOMICS & PRICING',
|
||
roiTitle: 'Per-Request Pricing Comparison & Mobility Unit Economics',
|
||
roiSubtitle: 'A transparent per-1,000 requests pricing comparison between Google Maps and Siro Maps fixed cloud infrastructure, with complete trip-level consumption breakdown.',
|
||
dailyRidesLabel: 'Daily Completed Rides',
|
||
googleCostLabel: 'Monthly Google Maps Cost',
|
||
siroCostLabel: 'Monthly Siro Dedicated Infra Cost',
|
||
monthlySavingsLabel: 'Net Monthly Savings',
|
||
annualSavingsLabel: 'Cumulative Annual Savings',
|
||
unitMarginLabel: 'Direct gross margin saved per completed trip',
|
||
terrainKicker: '04 / 3D TERRAIN & SOLAR SIMULATION',
|
||
terrainTitle: '3D Terrarium DEM & Astronomical Solar Shadow Simulation',
|
||
terrainSubtitle: 'Dynamic elevation models with real-time astronomical solar azimuth/elevation calculation, 315° NW hillshading, and 10m/50m topographic contours calibrated for MENA terrain.',
|
||
solarTimeLabel: 'Time of Day & Solar Azimuth / Elevation',
|
||
coverageKicker: '05 / REGIONAL INFRASTRUCTURE & EDGE DEPLOYMENT',
|
||
coverageTitle: 'High-Availability Regional Infrastructure & Private Cloud Deployment',
|
||
coverageSubtitle: 'Comprehensive regional geospatial coverage across Jordan, Iraq, Syria, and Egypt with on-premise & private cloud deployment options for sub-10ms latency and high resilience.',
|
||
developers: 'Ready for your engineering team',
|
||
infra: 'Open foundation. Integrated product.',
|
||
infraText: 'We build on open technology and operate the layers around it as one coherent developer and customer experience.',
|
||
cta: 'Location is part of your product. Make it better.',
|
||
ctaText: 'Create a project, test the APIs, then move to the right plan when you are ready.',
|
||
}
|
||
};
|
||
|
||
const serviceData = {
|
||
ar: [
|
||
['خرائط وطبقات', 'خرائط متجهة، أنماط متعددة، تضاريس وعرض ثلاثي الأبعاد لتجربة تحمل هوية منتجك.'],
|
||
['البحث والترميز الجغرافي', 'بحث وإكمال تلقائي وتحويل بين العنوان والإحداثيات مع سياق محلي.'],
|
||
['المسارات والتنقّل', 'مسارات للسيارة والدراجة والمشي، وعرض الرحلة داخل تطبيقاتك.'],
|
||
['البيانات المكانية', 'اربط نقاطك ومناطقك وحركة الأصول بالخرائط والتحليلات.'],
|
||
],
|
||
en: [
|
||
['Maps & layers', 'Vector maps, multiple styles, terrain and 3D rendering shaped around your product.'],
|
||
['Search & geocoding', 'Search, autocomplete and address-coordinate conversion with local context.'],
|
||
['Routing & mobility', 'Routes for driving, cycling and walking, rendered inside your applications.'],
|
||
['Spatial data', 'Connect places, boundaries and asset movement to maps and analytics.'],
|
||
]
|
||
};
|
||
|
||
const sdkData = [
|
||
['JavaScript / TypeScript', 'npm install intaleq-maps-gl', 'JS'],
|
||
['Flutter', 'intaleq_maps: ^2.3.0', 'FL'],
|
||
['iOS / Swift', 'import IntaleqMaps', 'SW'],
|
||
['Android / Kotlin', 'import com.intaleq.maps.*', 'KT'],
|
||
];
|
||
|
||
// Siro Sovereign Map Styles (100% Regional Cloud - Zero External Third-Party Tiles)
|
||
const HERO_MAP_STYLES: Record<string, string> = {
|
||
obsidian: '/style-dark.json',
|
||
daylight: '/style.json',
|
||
terrain: '/style-3d-terrain.json',
|
||
tourism: '/style-tourism.json',
|
||
};
|
||
|
||
const LANDMARKS = [
|
||
{ nameAr: 'عمان (التلال السبع)', nameEn: 'Amman 7 Hills', center: [35.9284, 31.9539] as [number, number], zoom: 12.8, pitch: 48, bearing: -18 },
|
||
{ nameAr: 'وادي رم (جبال الشراة)', nameEn: 'Wadi Rum 3D', center: [35.4337, 29.5763] as [number, number], zoom: 12.5, pitch: 65, bearing: -35 },
|
||
{ nameAr: 'البتراء (الممرات الصخرية)', nameEn: 'Petra Canyons', center: [35.4444, 30.3285] as [number, number], zoom: 13.0, pitch: 62, bearing: -20 },
|
||
{ nameAr: 'حوض البحر الميت', nameEn: 'Dead Sea Basin', center: [35.5800, 31.5000] as [number, number], zoom: 12.2, pitch: 55, bearing: 15 },
|
||
];
|
||
|
||
const SOLAR_LIGHT_PRESETS = [
|
||
{ id: 'dawn', labelAr: 'الشروق', labelEn: 'Dawn', icon: '🌅', color: '#ffc880', intensity: 0.65, position: [1.3, 90, 20] as [number, number, number] },
|
||
{ id: 'noon', labelAr: 'الظهيرة', labelEn: 'Noon', icon: '☀️', color: '#ffffff', intensity: 0.9, position: [1.0, 180, 75] as [number, number, number] },
|
||
{ id: 'sunset', labelAr: 'الغروب الذهبي', labelEn: 'Sunset', icon: '🌇', color: '#ff8a50', intensity: 0.75, position: [1.4, 270, 15] as [number, number, number] },
|
||
{ id: 'night', labelAr: 'الليل السيادي', labelEn: 'Night', icon: '🌙', color: '#c8f87a', intensity: 0.35, position: [1.2, 210, 45] as [number, number, number] },
|
||
];
|
||
|
||
function HeroLiveMap() {
|
||
const mapContainer = useRef<HTMLDivElement>(null);
|
||
const mapInstance = useRef<maplibregl.Map | null>(null);
|
||
const [theme, setTheme] = useState<'obsidian' | 'daylight' | 'terrain' | 'tourism'>('terrain');
|
||
const [isOrbiting, setIsOrbiting] = useState<boolean>(true);
|
||
const [solarPhase, setSolarPhase] = useState<'dawn' | 'noon' | 'sunset' | 'night'>('sunset');
|
||
const [activeLandmarkIdx, setActiveLandmarkIdx] = useState<number>(0);
|
||
const animFrameRef = useRef<number | null>(null);
|
||
const isDraggingRef = useRef<boolean>(false);
|
||
|
||
const addRouteLayers = (map: maplibregl.Map) => {
|
||
if (map.getSource('hero-route')) return;
|
||
|
||
map.addSource('hero-route', {
|
||
type: 'geojson',
|
||
data: {
|
||
type: 'Feature',
|
||
properties: {},
|
||
geometry: {
|
||
type: 'LineString',
|
||
coordinates: [
|
||
[35.8601, 31.9539],
|
||
[35.8850, 31.9600],
|
||
[35.9083, 31.9632],
|
||
[35.9350, 31.9800],
|
||
[35.9900, 32.0200],
|
||
[36.0942, 32.0608]
|
||
]
|
||
}
|
||
}
|
||
});
|
||
|
||
if (!map.getLayer('hero-route-glow')) {
|
||
map.addLayer({
|
||
id: 'hero-route-glow',
|
||
type: 'line',
|
||
source: 'hero-route',
|
||
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
||
paint: {
|
||
'line-color': '#c8f87a',
|
||
'line-width': 8,
|
||
'line-opacity': 0.4,
|
||
'line-blur': 4
|
||
}
|
||
});
|
||
}
|
||
|
||
if (!map.getLayer('hero-route-core')) {
|
||
map.addLayer({
|
||
id: 'hero-route-core',
|
||
type: 'line',
|
||
source: 'hero-route',
|
||
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
||
paint: {
|
||
'line-color': '#c8f87a',
|
||
'line-width': 3.5
|
||
}
|
||
});
|
||
}
|
||
|
||
const startEl = document.createElement('div');
|
||
startEl.className = 'hero-map-pin start';
|
||
new maplibregl.Marker({ element: startEl }).setLngLat([35.8601, 31.9539]).addTo(map);
|
||
|
||
const endEl = document.createElement('div');
|
||
endEl.className = 'hero-map-pin end';
|
||
new maplibregl.Marker({ element: endEl }).setLngLat([36.0942, 32.0608]).addTo(map);
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (!mapContainer.current || mapInstance.current) return;
|
||
|
||
try {
|
||
const map = new maplibregl.Map({
|
||
container: mapContainer.current,
|
||
style: HERO_MAP_STYLES[theme],
|
||
center: LANDMARKS[0].center,
|
||
zoom: LANDMARKS[0].zoom,
|
||
pitch: LANDMARKS[0].pitch,
|
||
bearing: LANDMARKS[0].bearing,
|
||
attributionControl: false,
|
||
maxPitch: 70,
|
||
...({ antialias: true } as any)
|
||
});
|
||
|
||
map.on('load', () => {
|
||
// Force resize to ensure tiles render immediately
|
||
map.resize();
|
||
try {
|
||
map.setTerrain({ source: 'terrain-dem', exaggeration: 1.8 });
|
||
} catch {}
|
||
addRouteLayers(map);
|
||
// Apply initial lighting
|
||
const preset = SOLAR_LIGHT_PRESETS.find(p => p.id === solarPhase);
|
||
if (preset) {
|
||
try {
|
||
map.setLight({
|
||
anchor: 'viewport',
|
||
color: preset.color,
|
||
intensity: preset.intensity,
|
||
position: preset.position
|
||
});
|
||
} catch {}
|
||
}
|
||
});
|
||
|
||
// Secondary resize after a short delay to catch layout shifts
|
||
map.once('idle', () => { map.resize(); });
|
||
|
||
map.on('mousedown', () => { isDraggingRef.current = true; });
|
||
map.on('touchstart', () => { isDraggingRef.current = true; });
|
||
map.on('mouseup', () => { isDraggingRef.current = false; });
|
||
map.on('touchend', () => { isDraggingRef.current = false; });
|
||
|
||
mapInstance.current = map;
|
||
} catch (e) {
|
||
console.warn('HeroLiveMap init error', e);
|
||
}
|
||
|
||
return () => {
|
||
if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
|
||
if (mapInstance.current) {
|
||
mapInstance.current.remove();
|
||
mapInstance.current = null;
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
// Continuous Camera Orbit (360° circular motion) - only when enabled
|
||
useEffect(() => {
|
||
if (!isOrbiting) {
|
||
if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
|
||
return;
|
||
}
|
||
|
||
let running = true;
|
||
const orbitStep = () => {
|
||
if (!running) return;
|
||
if (mapInstance.current && !isDraggingRef.current) {
|
||
const curBearing = mapInstance.current.getBearing();
|
||
mapInstance.current.setBearing((curBearing + 0.12) % 360);
|
||
}
|
||
animFrameRef.current = requestAnimationFrame(orbitStep);
|
||
};
|
||
|
||
animFrameRef.current = requestAnimationFrame(orbitStep);
|
||
return () => {
|
||
running = false;
|
||
if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
|
||
};
|
||
}, [isOrbiting]);
|
||
|
||
// Handle Theme Switching
|
||
const handleSwitchTheme = (t: 'obsidian' | 'daylight' | 'terrain' | 'tourism') => {
|
||
setTheme(t);
|
||
const map = mapInstance.current;
|
||
if (!map) return;
|
||
|
||
const curCenter = map.getCenter();
|
||
const curZoom = map.getZoom();
|
||
const curPitch = map.getPitch();
|
||
const curBearing = map.getBearing();
|
||
|
||
map.setStyle(HERO_MAP_STYLES[t]);
|
||
map.once('style.load', () => {
|
||
addRouteLayers(map);
|
||
if (t === 'terrain') {
|
||
try {
|
||
map.setTerrain({ source: 'terrain-dem', exaggeration: 1.8 });
|
||
} catch {}
|
||
} else {
|
||
try {
|
||
map.setTerrain(null as any);
|
||
} catch {}
|
||
}
|
||
map.jumpTo({
|
||
center: curCenter,
|
||
zoom: curZoom,
|
||
pitch: curPitch,
|
||
bearing: curBearing
|
||
});
|
||
// Re-apply light
|
||
const preset = SOLAR_LIGHT_PRESETS.find(p => p.id === solarPhase);
|
||
if (preset) {
|
||
try {
|
||
map.setLight({
|
||
anchor: 'viewport',
|
||
color: preset.color,
|
||
intensity: preset.intensity,
|
||
position: preset.position
|
||
});
|
||
} catch {}
|
||
}
|
||
});
|
||
};
|
||
|
||
// Handle Solar Light Simulation
|
||
const handleSolarChange = (phaseId: 'dawn' | 'noon' | 'sunset' | 'night') => {
|
||
setSolarPhase(phaseId);
|
||
const preset = SOLAR_LIGHT_PRESETS.find(p => p.id === phaseId);
|
||
if (!preset || !mapInstance.current) return;
|
||
try {
|
||
mapInstance.current.setLight({
|
||
anchor: 'viewport',
|
||
color: preset.color,
|
||
intensity: preset.intensity,
|
||
position: preset.position
|
||
});
|
||
} catch {}
|
||
};
|
||
|
||
// Handle Landmark Fly-to
|
||
const handleFlyToLandmark = (idx: number) => {
|
||
setActiveLandmarkIdx(idx);
|
||
const wasOrbiting = isOrbiting;
|
||
setIsOrbiting(false); // Stop orbit so flyTo completes smoothly
|
||
const lm = LANDMARKS[idx];
|
||
if (!mapInstance.current) return;
|
||
mapInstance.current.flyTo({
|
||
center: lm.center,
|
||
zoom: lm.zoom,
|
||
pitch: lm.pitch,
|
||
bearing: lm.bearing,
|
||
duration: 1400
|
||
});
|
||
if (wasOrbiting) {
|
||
setTimeout(() => setIsOrbiting(true), 1500);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="map-scene hero-real-map" aria-label="Siro Sovereign Live Interactive Map" style={{ position: 'relative' }}>
|
||
<div ref={mapContainer} style={{ width: '100%', height: '100%', borderRadius: '12px' }} />
|
||
<div className={`solar-ambient-overlay phase-${solarPhase}`} />
|
||
|
||
{/* Primary Style Selector Bar */}
|
||
<div className="hero-map-controls">
|
||
<button
|
||
type="button"
|
||
className={`hero-style-btn ${theme === 'obsidian' ? 'active' : ''}`}
|
||
onClick={() => handleSwitchTheme('obsidian')}
|
||
>
|
||
🌙 الليلي السيادي
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={`hero-style-btn ${theme === 'daylight' ? 'active' : ''}`}
|
||
onClick={() => handleSwitchTheme('daylight')}
|
||
>
|
||
☀️ النهاري المتقدم
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={`hero-style-btn ${theme === 'terrain' ? 'active' : ''}`}
|
||
onClick={() => handleSwitchTheme('terrain')}
|
||
>
|
||
⛰️ التضاريس والجبال 3D
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={`hero-style-btn orbit-btn ${isOrbiting ? 'active' : ''}`}
|
||
onClick={() => setIsOrbiting(!isOrbiting)}
|
||
title="دوران الكاميرا الدائري 360 درجة"
|
||
>
|
||
🔄 {isOrbiting ? 'إيقاف الدوران' : 'تشغيل الدوران'}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Secondary Interactive Toolbar: Solar Phase & Landmark Quick-fly */}
|
||
<div className="hero-map-subbar">
|
||
<div className="hero-map-pill-group">
|
||
<span className="hero-bar-label">محاكاة الضوء:</span>
|
||
{SOLAR_LIGHT_PRESETS.map((p) => (
|
||
<button
|
||
key={p.id}
|
||
type="button"
|
||
className={`hero-pill-btn ${solarPhase === p.id ? 'active' : ''}`}
|
||
onClick={() => handleSolarChange(p.id as any)}
|
||
>
|
||
{p.icon} {p.labelAr}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="hero-map-pill-group">
|
||
<span className="hero-bar-label">المعالم:</span>
|
||
{LANDMARKS.map((lm, idx) => (
|
||
<button
|
||
key={lm.nameEn}
|
||
type="button"
|
||
className={`hero-pill-btn ${activeLandmarkIdx === idx ? 'active' : ''}`}
|
||
onClick={() => handleFlyToLandmark(idx)}
|
||
>
|
||
{lm.nameAr.split(' ')[0]}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="map-float-card">
|
||
<span>SOVEREIGN CLOUD ROUTING ENGINE</span>
|
||
<strong>{LANDMARKS[activeLandmarkIdx].nameAr}</strong>
|
||
<small>SIRO Maps Platform • Sovereign Vector & 3D Topography</small>
|
||
</div>
|
||
<div className="map-coordinates">
|
||
{LANDMARKS[activeLandmarkIdx].center[1].toFixed(4)}° N<br />
|
||
{LANDMARKS[activeLandmarkIdx].center[0].toFixed(4)}° E
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Section 06 Dedicated Showcase Live Map (Controlled by ROUTES, TERRAIN, DATA)
|
||
function ShowcaseLiveMap({ activeLayer }: { activeLayer: number }) {
|
||
const mapContainer = useRef<HTMLDivElement>(null);
|
||
const mapInstance = useRef<maplibregl.Map | null>(null);
|
||
const animFrameRef = useRef<number | null>(null);
|
||
const markersRef = useRef<maplibregl.Marker[]>([]);
|
||
const courierAnimRef = useRef<number | null>(null);
|
||
const [routeStats, setRouteStats] = useState<{ km: string; min: string; routeName: string } | null>(null);
|
||
|
||
// Clear all markers from the map
|
||
const clearMarkers = useCallback(() => {
|
||
markersRef.current.forEach(m => m.remove());
|
||
markersRef.current = [];
|
||
}, []);
|
||
|
||
// Fetch real route from API and draw actual road geometry
|
||
const loadRealRoute = useCallback(async (map: maplibregl.Map) => {
|
||
const fromLat = 31.9539, fromLng = 35.8601;
|
||
const toLat = 32.0608, toLng = 36.0942;
|
||
|
||
try {
|
||
const res = await fetch(
|
||
`/api/maps/route?fromLat=${fromLat}&fromLng=${fromLng}&toLat=${toLat}&toLng=${toLng}&profile=car&steps=true&alternatives=false`
|
||
);
|
||
const data = await res.json();
|
||
|
||
// Extract actual route geometry from API response
|
||
let coords: [number, number][] = [];
|
||
if (data?.geometry?.coordinates) {
|
||
coords = data.geometry.coordinates;
|
||
} else if (data?.routes?.[0]?.geometry?.coordinates) {
|
||
coords = data.routes[0].geometry.coordinates;
|
||
} else if (data?.points) {
|
||
// GraphHopper format
|
||
coords = data.points.coordinates || [];
|
||
}
|
||
|
||
const distKm = data?.distance ? (data.distance / 1000).toFixed(1) : data?.routes?.[0]?.distance ? (data.routes[0].distance / 1000).toFixed(1) : '28.4';
|
||
const durMin = data?.time ? Math.round(data.time / 60000).toString() : data?.routes?.[0]?.duration ? Math.round(data.routes[0].duration / 60).toString() : '32';
|
||
setRouteStats({ km: distKm, min: durMin, routeName: 'عمّان ← الزرقاء' });
|
||
|
||
// Fallback to realistic curved path if API returned no coordinates
|
||
if (!coords.length) {
|
||
coords = [
|
||
[35.8601, 31.9539], [35.8720, 31.9558], [35.8850, 31.9600],
|
||
[35.8980, 31.9615], [35.9083, 31.9632], [35.9200, 31.9680],
|
||
[35.9350, 31.9780], [35.9480, 31.9860], [35.9600, 31.9950],
|
||
[35.9750, 32.0050], [35.9900, 32.0200], [36.0100, 32.0300],
|
||
[36.0300, 32.0400], [36.0550, 32.0500], [36.0780, 32.0570],
|
||
[36.0942, 32.0608]
|
||
];
|
||
}
|
||
|
||
// Draw the route
|
||
const geojson: any = {
|
||
type: 'Feature',
|
||
properties: {},
|
||
geometry: { type: 'LineString', coordinates: coords }
|
||
};
|
||
|
||
if (map.getSource('showcase-route')) {
|
||
(map.getSource('showcase-route') as maplibregl.GeoJSONSource).setData(geojson);
|
||
} else {
|
||
map.addSource('showcase-route', { type: 'geojson', data: geojson });
|
||
map.addLayer({
|
||
id: 'showcase-route-glow',
|
||
type: 'line',
|
||
source: 'showcase-route',
|
||
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
||
paint: { 'line-color': '#c8f87a', 'line-width': 12, 'line-opacity': 0.3, 'line-blur': 6 }
|
||
});
|
||
map.addLayer({
|
||
id: 'showcase-route-core',
|
||
type: 'line',
|
||
source: 'showcase-route',
|
||
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
||
paint: {
|
||
'line-color': '#c8f87a',
|
||
'line-width': 4.5,
|
||
'line-dasharray': [0, 0] // solid line
|
||
}
|
||
});
|
||
}
|
||
|
||
// Add start/end markers
|
||
clearMarkers();
|
||
const startEl = document.createElement('div');
|
||
startEl.className = 'hero-map-pin start';
|
||
startEl.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14"><circle cx="7" cy="7" r="5" fill="#c8f87a" stroke="#0a0a0a" stroke-width="2"/></svg>';
|
||
const startMarker = new maplibregl.Marker({ element: startEl })
|
||
.setLngLat(coords[0] as [number, number])
|
||
.addTo(map);
|
||
|
||
const endEl = document.createElement('div');
|
||
endEl.className = 'hero-map-pin end';
|
||
endEl.innerHTML = '<svg width="18" height="18" viewBox="0 0 18 18"><polygon points="9,2 16,16 2,16" fill="#f5b042" stroke="#0a0a0a" stroke-width="1.5"/></svg>';
|
||
const endMarker = new maplibregl.Marker({ element: endEl })
|
||
.setLngLat(coords[coords.length - 1] as [number, number])
|
||
.addTo(map);
|
||
|
||
// Animated car marker that moves along the route
|
||
const courierEl = document.createElement('div');
|
||
courierEl.className = 'showcase-car-marker';
|
||
const courierMarker = new maplibregl.Marker({ element: courierEl })
|
||
.setLngLat(coords[0] as [number, number])
|
||
.addTo(map);
|
||
|
||
markersRef.current.push(startMarker, endMarker, courierMarker);
|
||
|
||
// Animate courier along route
|
||
let step = 0;
|
||
const totalSteps = coords.length;
|
||
const animateCourier = () => {
|
||
if (step < totalSteps) {
|
||
courierMarker.setLngLat(coords[step] as [number, number]);
|
||
step++;
|
||
courierAnimRef.current = window.setTimeout(animateCourier, 180);
|
||
} else {
|
||
step = 0; // Loop
|
||
courierAnimRef.current = window.setTimeout(animateCourier, 1200);
|
||
}
|
||
};
|
||
courierAnimRef.current = window.setTimeout(animateCourier, 600);
|
||
|
||
} catch {
|
||
// Fallback static route if API unreachable
|
||
setRouteStats({ km: '28.4', min: '32', routeName: 'عمّان ← الزرقاء' });
|
||
const fallbackCoords: [number, number][] = [
|
||
[35.8601, 31.9539], [35.8850, 31.9600], [35.9083, 31.9632],
|
||
[35.9350, 31.9800], [35.9600, 31.9950], [35.9900, 32.0200],
|
||
[36.0300, 32.0400], [36.0942, 32.0608]
|
||
];
|
||
if (!map.getSource('showcase-route')) {
|
||
map.addSource('showcase-route', {
|
||
type: 'geojson',
|
||
data: {
|
||
type: 'Feature', properties: {},
|
||
geometry: { type: 'LineString', coordinates: fallbackCoords }
|
||
}
|
||
});
|
||
map.addLayer({ id: 'showcase-route-glow', type: 'line', source: 'showcase-route', layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': '#c8f87a', 'line-width': 10, 'line-opacity': 0.35, 'line-blur': 5 } });
|
||
map.addLayer({ id: 'showcase-route-core', type: 'line', source: 'showcase-route', layout: { 'line-join': 'round', 'line-cap': 'round' }, paint: { 'line-color': '#c8f87a', 'line-width': 4 } });
|
||
}
|
||
clearMarkers();
|
||
const s = document.createElement('div'); s.className = 'hero-map-pin start';
|
||
markersRef.current.push(new maplibregl.Marker({ element: s }).setLngLat(fallbackCoords[0]).addTo(map));
|
||
const e = document.createElement('div'); e.className = 'hero-map-pin end';
|
||
markersRef.current.push(new maplibregl.Marker({ element: e }).setLngLat(fallbackCoords[fallbackCoords.length - 1]).addTo(map));
|
||
}
|
||
}, [clearMarkers]);
|
||
|
||
// Add data overlay layers (POI clusters, heatmap)
|
||
const addDataLayers = useCallback((map: maplibregl.Map) => {
|
||
// Create a GeoJSON source with interesting POI data points around Amman
|
||
const poiData: any = {
|
||
type: 'FeatureCollection',
|
||
features: [
|
||
{ type: 'Feature', properties: { name: 'المدرّج الروماني', cat: 'heritage' }, geometry: { type: 'Point', coordinates: [35.9344, 31.9515] } },
|
||
{ type: 'Feature', properties: { name: 'جبل القلعة', cat: 'heritage' }, geometry: { type: 'Point', coordinates: [35.9338, 31.9540] } },
|
||
{ type: 'Feature', properties: { name: 'عبدون مول', cat: 'commercial' }, geometry: { type: 'Point', coordinates: [35.8730, 31.9537] } },
|
||
{ type: 'Feature', properties: { name: 'سيتي مول', cat: 'commercial' }, geometry: { type: 'Point', coordinates: [35.8515, 31.9760] } },
|
||
{ type: 'Feature', properties: { name: 'مطار الملكة علياء', cat: 'transport' }, geometry: { type: 'Point', coordinates: [35.9932, 31.7225] } },
|
||
{ type: 'Feature', properties: { name: 'الجامعة الأردنية', cat: 'education' }, geometry: { type: 'Point', coordinates: [35.8740, 32.0170] } },
|
||
{ type: 'Feature', properties: { name: 'شارع الرينبو', cat: 'leisure' }, geometry: { type: 'Point', coordinates: [35.9220, 31.9625] } },
|
||
{ type: 'Feature', properties: { name: 'حدائق الملك حسين', cat: 'park' }, geometry: { type: 'Point', coordinates: [35.8580, 32.0070] } },
|
||
{ type: 'Feature', properties: { name: 'البوليفارد', cat: 'commercial' }, geometry: { type: 'Point', coordinates: [35.8920, 31.9850] } },
|
||
{ type: 'Feature', properties: { name: 'وسط البلد', cat: 'heritage' }, geometry: { type: 'Point', coordinates: [35.9310, 31.9530] } },
|
||
{ type: 'Feature', properties: { name: 'مجمع جبر', cat: 'transport' }, geometry: { type: 'Point', coordinates: [35.9440, 31.9562] } },
|
||
{ type: 'Feature', properties: { name: 'الشميساني', cat: 'commercial' }, geometry: { type: 'Point', coordinates: [35.8950, 31.9700] } },
|
||
]
|
||
};
|
||
|
||
if (!map.getSource('data-pois')) {
|
||
map.addSource('data-pois', { type: 'geojson', data: poiData });
|
||
|
||
// Heatmap layer
|
||
map.addLayer({
|
||
id: 'data-heatmap',
|
||
type: 'heatmap',
|
||
source: 'data-pois',
|
||
maxzoom: 15,
|
||
paint: {
|
||
'heatmap-weight': 1,
|
||
'heatmap-intensity': 1.2,
|
||
'heatmap-radius': 35,
|
||
'heatmap-color': [
|
||
'interpolate', ['linear'], ['heatmap-density'],
|
||
0, 'rgba(200,248,122,0)',
|
||
0.3, 'rgba(200,248,122,0.25)',
|
||
0.5, 'rgba(200,248,122,0.45)',
|
||
0.7, 'rgba(245,176,66,0.65)',
|
||
1.0, 'rgba(245,135,113,0.85)'
|
||
],
|
||
'heatmap-opacity': 0.7
|
||
}
|
||
});
|
||
|
||
// Circle markers at higher zoom
|
||
map.addLayer({
|
||
id: 'data-poi-circles',
|
||
type: 'circle',
|
||
source: 'data-pois',
|
||
minzoom: 12,
|
||
paint: {
|
||
'circle-radius': 7,
|
||
'circle-color': '#c8f87a',
|
||
'circle-stroke-color': '#0a0a0a',
|
||
'circle-stroke-width': 2,
|
||
'circle-opacity': 0.9
|
||
}
|
||
});
|
||
|
||
// POI labels
|
||
map.addLayer({
|
||
id: 'data-poi-labels',
|
||
type: 'symbol',
|
||
source: 'data-pois',
|
||
minzoom: 13,
|
||
layout: {
|
||
'text-field': ['get', 'name'],
|
||
'text-font': ['Noto Sans Regular'],
|
||
'text-size': 11,
|
||
'text-offset': [0, 1.5],
|
||
'text-anchor': 'top'
|
||
},
|
||
paint: {
|
||
'text-color': '#c8f87a',
|
||
'text-halo-color': '#0a0a0a',
|
||
'text-halo-width': 1.5
|
||
}
|
||
});
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!mapContainer.current || mapInstance.current) return;
|
||
|
||
try {
|
||
const map = new maplibregl.Map({
|
||
container: mapContainer.current,
|
||
style: '/style-dark.json',
|
||
center: [35.9284, 31.9539],
|
||
zoom: 12.8,
|
||
pitch: 45,
|
||
bearing: -18,
|
||
attributionControl: false,
|
||
...({ antialias: true } as any)
|
||
});
|
||
|
||
map.on('load', () => {
|
||
map.resize();
|
||
loadRealRoute(map);
|
||
});
|
||
|
||
map.once('idle', () => { map.resize(); });
|
||
|
||
mapInstance.current = map;
|
||
} catch (e) {
|
||
console.warn('ShowcaseLiveMap init error', e);
|
||
}
|
||
|
||
return () => {
|
||
if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
|
||
if (courierAnimRef.current) clearTimeout(courierAnimRef.current);
|
||
clearMarkers();
|
||
if (mapInstance.current) {
|
||
mapInstance.current.remove();
|
||
mapInstance.current = null;
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
// React to activeLayer changes (0 = ROUTES, 1 = TERRAIN, 2 = DATA)
|
||
useEffect(() => {
|
||
const map = mapInstance.current;
|
||
if (!map) return;
|
||
|
||
if (animFrameRef.current) {
|
||
cancelAnimationFrame(animFrameRef.current);
|
||
animFrameRef.current = null;
|
||
}
|
||
if (courierAnimRef.current) {
|
||
clearTimeout(courierAnimRef.current);
|
||
courierAnimRef.current = null;
|
||
}
|
||
|
||
if (activeLayer === 0) {
|
||
// 01 / ROUTES — Real API route with animated courier
|
||
map.setStyle('/style-dark.json');
|
||
map.once('style.load', () => {
|
||
try { map.setTerrain(null as any); } catch {}
|
||
map.flyTo({ center: [35.9284, 31.9539], zoom: 12.8, pitch: 45, bearing: -18, duration: 1400 });
|
||
clearMarkers();
|
||
loadRealRoute(map);
|
||
});
|
||
} else if (activeLayer === 1) {
|
||
// 02 / TERRAIN (Wadi Rum Mountain Peaks + 3D DEM + Orbit)
|
||
clearMarkers();
|
||
setRouteStats(null);
|
||
map.setStyle('/style-3d-terrain.json');
|
||
map.once('style.load', () => {
|
||
try {
|
||
map.setTerrain({ source: 'terrain-dem', exaggeration: 1.8 });
|
||
} catch {}
|
||
map.flyTo({ center: [35.4337, 29.5763], zoom: 12.6, pitch: 65, bearing: -35, duration: 1500 });
|
||
|
||
// Continuous smooth rotation around mountain relief
|
||
const orbit = () => {
|
||
if (!mapInstance.current) return;
|
||
const b = mapInstance.current.getBearing();
|
||
mapInstance.current.setBearing((b + 0.14) % 360);
|
||
animFrameRef.current = requestAnimationFrame(orbit);
|
||
};
|
||
animFrameRef.current = requestAnimationFrame(orbit);
|
||
});
|
||
} else if (activeLayer === 2) {
|
||
// 03 / DATA — POI heatmap, clusters, solar lighting
|
||
clearMarkers();
|
||
setRouteStats(null);
|
||
map.setStyle('/style.json');
|
||
map.once('style.load', () => {
|
||
try { map.setTerrain(null as any); } catch {}
|
||
map.flyTo({ center: [35.9106, 31.9539], zoom: 13.2, pitch: 48, bearing: 25, duration: 1400 });
|
||
try {
|
||
map.setLight({
|
||
anchor: 'viewport',
|
||
color: '#ffffff',
|
||
intensity: 0.85,
|
||
position: [1.2, 210, 45]
|
||
});
|
||
} catch {}
|
||
addDataLayers(map);
|
||
});
|
||
}
|
||
}, [activeLayer, loadRealRoute, clearMarkers, addDataLayers]);
|
||
|
||
return (
|
||
<div className="map-scene hero-real-map" style={{ height: '100%', minHeight: '480px' }} aria-label="Showcase Live Map">
|
||
<div ref={mapContainer} style={{ width: '100%', height: '100%', borderRadius: '12px' }} />
|
||
<div className="map-float-card">
|
||
<span>{activeLayer === 0 ? '01 / ROUTES ENGINE' : activeLayer === 1 ? '02 / 3D TERRAIN & DEM' : '03 / SOVEREIGN DATA & LIGHT'}</span>
|
||
<strong>
|
||
{activeLayer === 0 ? (routeStats?.routeName || 'شبكة الطرق والملاحة الحية') : activeLayer === 1 ? 'جبال وادي رم والتضاريس الحقيقية' : 'محاكاة الضوء والبيانات المكانية'}
|
||
</strong>
|
||
<small>
|
||
{activeLayer === 0 && routeStats ? `${routeStats.km} km · ${routeStats.min} min • Siro Sovereign Routing Engine` : activeLayer === 0 ? 'Siro Routing • 100% Sovereign Cloud' : activeLayer === 1 ? 'Terrarium DEM • Exaggeration 1.8x' : 'Heatmap POIs • Solar Raytracing'}
|
||
</small>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Section 04 Real 3D Terrain Map (replaces SVG placeholder)
|
||
interface TerrainLiveMapProps {
|
||
terrainLocationIdx: number;
|
||
sunHour: number;
|
||
terrainExaggeration: number;
|
||
activeTerrain: { nameAr: string; nameEn: string; elev: string; descAr: string; descEn: string; offset: number };
|
||
sunInfo: { elevation: number; azimuth: number; name: string; color: string; filter: string };
|
||
ar: boolean;
|
||
}
|
||
|
||
const TERRAIN_LOCATIONS = [
|
||
{ center: [35.4337, 29.5763] as [number, number], zoom: 12.5, pitch: 65, bearing: -35 }, // Wadi Rum
|
||
{ center: [35.4444, 30.3285] as [number, number], zoom: 13.0, pitch: 62, bearing: -20 }, // Petra
|
||
{ center: [35.5800, 31.5000] as [number, number], zoom: 12.2, pitch: 55, bearing: 15 }, // Dead Sea
|
||
{ center: [35.9284, 31.9539] as [number, number], zoom: 13.5, pitch: 55, bearing: -18 }, // Amman
|
||
];
|
||
|
||
function TerrainLiveMap({ terrainLocationIdx, sunHour, terrainExaggeration, activeTerrain, sunInfo, ar }: TerrainLiveMapProps) {
|
||
const mapContainer = useRef<HTMLDivElement>(null);
|
||
const mapInstance = useRef<maplibregl.Map | null>(null);
|
||
|
||
// Initialize the 3D terrain map
|
||
useEffect(() => {
|
||
if (!mapContainer.current || mapInstance.current) return;
|
||
|
||
const loc = TERRAIN_LOCATIONS[terrainLocationIdx] || TERRAIN_LOCATIONS[0];
|
||
|
||
try {
|
||
const map = new maplibregl.Map({
|
||
container: mapContainer.current,
|
||
style: '/style-3d-terrain.json',
|
||
center: loc.center,
|
||
zoom: loc.zoom,
|
||
pitch: loc.pitch,
|
||
bearing: loc.bearing,
|
||
attributionControl: false,
|
||
maxPitch: 75,
|
||
...({ antialias: true } as any)
|
||
});
|
||
|
||
map.on('load', () => {
|
||
map.resize();
|
||
try {
|
||
map.setTerrain({ source: 'terrain-dem', exaggeration: terrainExaggeration });
|
||
} catch {}
|
||
|
||
if (map.getLayer('terrain-3d-hillshading')) {
|
||
try {
|
||
map.setPaintProperty('terrain-3d-hillshading', 'hillshade-illumination-direction', sunInfo.azimuth);
|
||
map.setPaintProperty('terrain-3d-hillshading', 'hillshade-highlight-color', sunInfo.color);
|
||
map.setPaintProperty('terrain-3d-hillshading', 'hillshade-exaggeration', Math.min(1.0, terrainExaggeration * 0.45));
|
||
} catch {}
|
||
}
|
||
});
|
||
|
||
map.once('idle', () => { map.resize(); });
|
||
mapInstance.current = map;
|
||
} catch (e) {
|
||
console.warn('TerrainLiveMap init error', e);
|
||
}
|
||
|
||
return () => {
|
||
if (mapInstance.current) {
|
||
mapInstance.current.remove();
|
||
mapInstance.current = null;
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
// React to location changes smoothly
|
||
useEffect(() => {
|
||
const map = mapInstance.current;
|
||
if (!map) return;
|
||
const loc = TERRAIN_LOCATIONS[terrainLocationIdx] || TERRAIN_LOCATIONS[0];
|
||
map.flyTo({
|
||
center: loc.center,
|
||
zoom: loc.zoom,
|
||
pitch: loc.pitch,
|
||
bearing: loc.bearing,
|
||
duration: 1600
|
||
});
|
||
}, [terrainLocationIdx]);
|
||
|
||
// React to exaggeration changes
|
||
useEffect(() => {
|
||
const map = mapInstance.current;
|
||
if (!map) return;
|
||
try {
|
||
map.setTerrain({ source: 'terrain-dem', exaggeration: terrainExaggeration });
|
||
} catch {}
|
||
if (map.getLayer('terrain-3d-hillshading')) {
|
||
try {
|
||
map.setPaintProperty('terrain-3d-hillshading', 'hillshade-exaggeration', Math.min(1.0, terrainExaggeration * 0.45));
|
||
} catch {}
|
||
}
|
||
}, [terrainExaggeration]);
|
||
|
||
// React to sun hour / lighting changes
|
||
useEffect(() => {
|
||
const map = mapInstance.current;
|
||
if (!map) return;
|
||
if (map.getLayer('terrain-3d-hillshading')) {
|
||
try {
|
||
map.setPaintProperty('terrain-3d-hillshading', 'hillshade-illumination-direction', sunInfo.azimuth);
|
||
map.setPaintProperty('terrain-3d-hillshading', 'hillshade-highlight-color', sunInfo.color);
|
||
} catch {}
|
||
}
|
||
}, [sunHour, sunInfo.azimuth, sunInfo.color]);
|
||
|
||
// Dynamic ambient lighting phase
|
||
const getPhaseClass = () => {
|
||
if (sunHour < 6 || sunHour >= 20) return 'phase-night';
|
||
if (sunHour >= 6 && sunHour <= 8.5) return 'phase-dawn';
|
||
if (sunHour > 8.5 && sunHour <= 16.5) return 'phase-noon';
|
||
return 'phase-sunset';
|
||
};
|
||
|
||
return (
|
||
<div className="terrain-visual-window" style={{ position: 'relative' }}>
|
||
<div ref={mapContainer} style={{ width: '100%', height: '100%', borderRadius: '12px', minHeight: '420px' }} />
|
||
<div className={`solar-ambient-overlay ${getPhaseClass()}`} />
|
||
<div className="terrain-float-meta">
|
||
<span>ELEVATION: {activeTerrain.elev} ({terrainExaggeration}x EXAGGERATION)</span>
|
||
<strong>{ar ? activeTerrain.nameAr : activeTerrain.nameEn}</strong>
|
||
<small>{ar ? activeTerrain.descAr : activeTerrain.descEn}</small>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 04.B / Dedicated Amman 3D Buildings & Sun/Shadow Simulation
|
||
function AmmanShadowSimulation({ ar }: { ar: boolean }) {
|
||
const mapContainer = useRef<HTMLDivElement>(null);
|
||
const mapInstance = useRef<maplibregl.Map | null>(null);
|
||
const [sunHour, setSunHour] = useState<number>(17.0); // 17:00 default (Golden Hour)
|
||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||
const playIntervalRef = useRef<number | null>(null);
|
||
|
||
const AMMAN_ABDALI_BUILDINGS = {
|
||
type: 'FeatureCollection' as const,
|
||
features: [
|
||
{
|
||
type: 'Feature' as const,
|
||
properties: { name: ar ? 'برج عمّان روتانا' : 'Amman Rotana Tower', height: 188, base: 0, color: '#f0f7e6' },
|
||
geometry: {
|
||
type: 'Polygon' as const,
|
||
coordinates: [[[35.9080, 31.9631], [35.9088, 31.9631], [35.9088, 31.9637], [35.9080, 31.9637], [35.9080, 31.9631]]]
|
||
}
|
||
},
|
||
{
|
||
type: 'Feature' as const,
|
||
properties: { name: ar ? 'فندق دبليو عمّان' : 'W Amman Hotel Tower', height: 125, base: 0, color: '#d8eed2' },
|
||
geometry: {
|
||
type: 'Polygon' as const,
|
||
coordinates: [[[35.9068, 31.9622], [35.9075, 31.9622], [35.9075, 31.9628], [35.9068, 31.9628], [35.9068, 31.9622]]]
|
||
}
|
||
},
|
||
{
|
||
type: 'Feature' as const,
|
||
properties: { name: ar ? 'بوابة البوليفارد' : 'Boulevard Gateway Towers', height: 90, base: 0, color: '#c4e2bd' },
|
||
geometry: {
|
||
type: 'Polygon' as const,
|
||
coordinates: [[[35.9095, 31.9642], [35.9103, 31.9642], [35.9103, 31.9649], [35.9095, 31.9649], [35.9095, 31.9642]]]
|
||
}
|
||
},
|
||
{
|
||
type: 'Feature' as const,
|
||
properties: { name: ar ? 'المركز الطبي للعبدلي' : 'Abdali Medical Center', height: 75, base: 0, color: '#bcdbb5' },
|
||
geometry: {
|
||
type: 'Polygon' as const,
|
||
coordinates: [[[35.9055, 31.9615], [35.9063, 31.9615], [35.9063, 31.9621], [35.9055, 31.9621], [35.9055, 31.9615]]]
|
||
}
|
||
},
|
||
{
|
||
type: 'Feature' as const,
|
||
properties: { name: ar ? 'بوليفارد العبدلي' : 'The Boulevard Complex', height: 48, base: 0, color: '#a8cba0' },
|
||
geometry: {
|
||
type: 'Polygon' as const,
|
||
coordinates: [[[35.9075, 31.9633], [35.9093, 31.9633], [35.9093, 31.9641], [35.9075, 31.9641], [35.9075, 31.9633]]]
|
||
}
|
||
},
|
||
{
|
||
type: 'Feature' as const,
|
||
properties: { name: ar ? 'عبدلي مول' : 'Abdali Mall', height: 42, base: 0, color: '#96bc8e' },
|
||
geometry: {
|
||
type: 'Polygon' as const,
|
||
coordinates: [[[35.9048, 31.9628], [35.9065, 31.9628], [35.9065, 31.9643], [35.9048, 31.9643], [35.9048, 31.9628]]]
|
||
}
|
||
}
|
||
]
|
||
};
|
||
|
||
const getSunData = (h: number) => {
|
||
const elev = Math.max(3, Math.round(Math.sin(((h - 6) / 12) * Math.PI) * 75));
|
||
const az = Math.round(80 + ((h - 6) / 12) * 190);
|
||
const shadowRatio = (1 / Math.tan((Math.max(3, elev) * Math.PI) / 180)).toFixed(1);
|
||
const rotanaShadowMeters = Math.round(188 * parseFloat(shadowRatio));
|
||
|
||
let phaseName = ar ? 'الظهيرة' : 'Noon Peak';
|
||
let lightColor = '#ffffff';
|
||
let lightIntensity = 0.95;
|
||
let lux = '98,000 Lux';
|
||
|
||
if (h < 7.5) {
|
||
phaseName = ar ? 'الشروق الفلكي' : 'Astronomical Dawn';
|
||
lightColor = '#ff9966';
|
||
lightIntensity = 0.7;
|
||
lux = '22,000 Lux';
|
||
} else if (h >= 7.5 && h < 11.5) {
|
||
phaseName = ar ? 'الصباح المبكر' : 'Morning Rays';
|
||
lightColor = '#ffeacc';
|
||
lightIntensity = 0.85;
|
||
lux = '65,000 Lux';
|
||
} else if (h >= 11.5 && h <= 14.5) {
|
||
phaseName = ar ? 'شمس الظهيرة العمودية' : 'Vertical Solar Noon';
|
||
lightColor = '#ffffff';
|
||
lightIntensity = 1.0;
|
||
lux = '110,000 Lux';
|
||
} else if (h > 14.5 && h < 18) {
|
||
phaseName = ar ? 'العصر والغروب الذهبي' : 'Golden Hour';
|
||
lightColor = '#ff9f43';
|
||
lightIntensity = 0.8;
|
||
lux = '38,000 Lux';
|
||
} else {
|
||
phaseName = ar ? 'الغسق والليل' : 'Twilight & Dusk';
|
||
lightColor = '#4a90e2';
|
||
lightIntensity = 0.4;
|
||
lux = '1,200 Lux';
|
||
}
|
||
|
||
return { elev, az, shadowRatio, rotanaShadowMeters, phaseName, lightColor, lightIntensity, lux };
|
||
};
|
||
|
||
const sunData = getSunData(sunHour);
|
||
|
||
useEffect(() => {
|
||
if (!mapContainer.current || mapInstance.current) return;
|
||
|
||
try {
|
||
const map = new maplibregl.Map({
|
||
container: mapContainer.current,
|
||
style: '/style-3d-terrain.json',
|
||
center: [35.9084, 31.9632], // Amman Abdali / Rotana Tower
|
||
zoom: 15.6,
|
||
pitch: 62,
|
||
bearing: -28,
|
||
attributionControl: false,
|
||
maxPitch: 75,
|
||
...({ antialias: true } as any)
|
||
});
|
||
|
||
map.on('load', () => {
|
||
map.resize();
|
||
try {
|
||
map.setTerrain({ source: 'terrain-dem', exaggeration: 1.5 });
|
||
} catch {}
|
||
|
||
if (!map.getSource('amman-buildings-src')) {
|
||
map.addSource('amman-buildings-src', {
|
||
type: 'geojson',
|
||
data: AMMAN_ABDALI_BUILDINGS
|
||
});
|
||
|
||
map.addLayer({
|
||
id: 'amman-buildings-3d',
|
||
type: 'fill-extrusion',
|
||
source: 'amman-buildings-src',
|
||
paint: {
|
||
'fill-extrusion-color': ['get', 'color'],
|
||
'fill-extrusion-height': ['get', 'height'],
|
||
'fill-extrusion-base': ['get', 'base'],
|
||
'fill-extrusion-opacity': 0.95
|
||
}
|
||
});
|
||
}
|
||
|
||
try {
|
||
map.setLight({
|
||
anchor: 'viewport',
|
||
color: sunData.lightColor,
|
||
intensity: sunData.lightIntensity,
|
||
position: [1.5, sunData.az, sunData.elev]
|
||
});
|
||
} catch {}
|
||
});
|
||
|
||
map.once('idle', () => { map.resize(); });
|
||
mapInstance.current = map;
|
||
} catch (e) {
|
||
console.warn('AmmanShadowSimulation init error', e);
|
||
}
|
||
|
||
return () => {
|
||
if (playIntervalRef.current) clearInterval(playIntervalRef.current);
|
||
if (mapInstance.current) {
|
||
mapInstance.current.remove();
|
||
mapInstance.current = null;
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
const map = mapInstance.current;
|
||
if (!map) return;
|
||
try {
|
||
map.setLight({
|
||
anchor: 'viewport',
|
||
color: sunData.lightColor,
|
||
intensity: sunData.lightIntensity,
|
||
position: [1.5, sunData.az, sunData.elev]
|
||
});
|
||
} catch {}
|
||
}, [sunHour, sunData.az, sunData.elev, sunData.lightColor, sunData.lightIntensity]);
|
||
|
||
const togglePlay = () => {
|
||
if (isPlaying) {
|
||
if (playIntervalRef.current) clearInterval(playIntervalRef.current);
|
||
setIsPlaying(false);
|
||
} else {
|
||
setIsPlaying(true);
|
||
playIntervalRef.current = window.setInterval(() => {
|
||
setSunHour(prev => {
|
||
const next = prev + 0.25;
|
||
return next > 20 ? 6 : next;
|
||
});
|
||
}, 250);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="amman-shadow-layout">
|
||
<div className="amman-shadow-controls">
|
||
<div className="shadow-hud-header">
|
||
<h3><Building2 size={20} /> {ar ? 'محاكاة ظلال أبراج عمّان ومسار الشمس' : 'Amman Towers 3D Solar Shadow Simulation'}</h3>
|
||
<span className="shadow-time-pill">
|
||
{Math.floor(sunHour)}:{Math.round((sunHour % 1) * 60).toString().padStart(2, '0')}
|
||
</span>
|
||
</div>
|
||
|
||
<div>
|
||
<label style={{ fontSize: '11px', color: 'var(--muted)', display: 'block', marginBottom: '8px' }}>
|
||
{ar ? 'شريط تحول الألوان ومسار الشمس الفلكي (06:00 إلى 20:00)' : 'Astronomical Solar Trajectory Spectrum (06:00 to 20:00)'}
|
||
</label>
|
||
<input
|
||
type="range"
|
||
min="6"
|
||
max="20"
|
||
step="0.25"
|
||
value={sunHour}
|
||
onChange={(e) => setSunHour(Number(e.target.value))}
|
||
className="sun-spectrum-slider"
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}>
|
||
<button
|
||
type="button"
|
||
className={`shadow-play-btn ${isPlaying ? 'active' : ''}`}
|
||
onClick={togglePlay}
|
||
>
|
||
{isPlaying ? '⏸️ ' + (ar ? 'إيقاف دورة الشمس' : 'Pause Cycle') : '▶️ ' + (ar ? 'تشغيل دورة الشمس التلقائية' : 'Play 24h Sun Cycle')}
|
||
</button>
|
||
{[
|
||
{ h: 7, label: ar ? 'شروق' : 'Dawn' },
|
||
{ h: 12, label: ar ? 'ظهيرة' : 'Noon' },
|
||
{ h: 17, label: ar ? 'عصر ذهبي' : 'Golden' },
|
||
{ h: 19.5, label: ar ? 'غسق' : 'Dusk' },
|
||
].map(p => (
|
||
<button
|
||
key={p.h}
|
||
type="button"
|
||
className={`roi-preset-btn ${Math.abs(sunHour - p.h) < 0.5 ? 'active' : ''}`}
|
||
onClick={() => setSunHour(p.h)}
|
||
>
|
||
{p.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="shadow-telemetry-grid">
|
||
<div className="shadow-telemetry-card">
|
||
<small>{ar ? 'حالة الإضاءة الفلكية' : 'Solar Lighting Phase'}</small>
|
||
<b style={{ color: sunData.lightColor }}>{sunData.phaseName}</b>
|
||
<span>{sunData.lux}</span>
|
||
</div>
|
||
<div className="shadow-telemetry-card">
|
||
<small>{ar ? 'زاوية الارتفاع والسمت' : 'Elevation & Azimuth'}</small>
|
||
<b>{sunData.elev}° Alt · {sunData.az}° Az</b>
|
||
<span>{ar ? 'اتجاه الظل: عكس زاوية الشمس' : 'Shadow: Opposite to sun'}</span>
|
||
</div>
|
||
<div className="shadow-telemetry-card">
|
||
<small>{ar ? 'معامل طول الظل' : 'Shadow Length Factor'}</small>
|
||
<b>{sunData.shadowRatio}x {ar ? 'الارتفاع' : 'Height'}</b>
|
||
<span>{ar ? 'نسبة الاستطالة الأرضية' : 'Ground projection ratio'}</span>
|
||
</div>
|
||
<div className="shadow-telemetry-card">
|
||
<small>{ar ? 'طول ظل برج روتانا (188م)' : 'Rotana Tower Shadow (188m)'}</small>
|
||
<b style={{ color: 'var(--lime)' }}>{sunData.rotanaShadowMeters} {ar ? 'متر' : 'm'}</b>
|
||
<span>{ar ? 'امتداد الظل الفعلي على الأرض' : 'True ground shadow reach'}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ fontSize: '11px', color: 'var(--muted)', lineHeight: '1.6', borderTop: '1px solid #2d3b29', paddingTop: '14px' }}>
|
||
{ar
|
||
? 'محاكاة دقيقة لحركة الظلال على برج عمّان روتانا (188م) وفندق W والبوليفارد، محسوبة استناداً إلى خط العرض 31.96° شمالاً وزاوية السمت الشمسية اللحظية.'
|
||
: 'Real-time raytraced shadow projection across Amman Rotana (188m), W Amman, and The Boulevard computed for latitude 31.96° N.'}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="amman-shadow-visual">
|
||
<div ref={mapContainer} style={{ width: '100%', height: '100%' }} />
|
||
<div className="shadow-float-badge">
|
||
<span>AMMAN 3D ARCHITECTURAL MESH</span>
|
||
<strong>{ar ? 'أبراج العبدلي وبرج روتانا (188م)' : 'Amman Abdali & Rotana Tower (188m)'}</strong>
|
||
<small>{ar ? `زاوية السمت: ${sunData.az}° • الظل: ${sunData.rotanaShadowMeters}م` : `Azimuth: ${sunData.az}° • Shadow: ${sunData.rotanaShadowMeters}m`}</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const COMPETITORS = {
|
||
google: {
|
||
id: 'google',
|
||
name: 'Google Maps Platform',
|
||
costPerRide: 0.30,
|
||
labelAr: 'Google Maps ($0.30 / رحلة)',
|
||
labelEn: 'Google Maps ($0.30 / ride)',
|
||
noteAr: 'تسعير تراكمي لكل طلب (Pay-per-hit) يتضاعف مع نمو حجم الرحلات اليومية.',
|
||
noteEn: 'Pay-per-hit micro-billing scaling linearly with completed ride volume.'
|
||
},
|
||
mapbox: {
|
||
id: 'mapbox',
|
||
name: 'Mapbox',
|
||
costPerRide: 0.18,
|
||
labelAr: 'Mapbox ($0.18 / رحلة)',
|
||
labelEn: 'Mapbox ($0.18 / ride)',
|
||
noteAr: 'تكلفة استهلاك مبنية على MAU والملاحة وطلبات البحث الدائمة.',
|
||
noteEn: 'Metered consumption based on MAU, Directions API, and stored geocoding.'
|
||
},
|
||
here: {
|
||
id: 'here',
|
||
name: 'HERE Technologies',
|
||
costPerRide: 0.20,
|
||
labelAr: 'HERE Technologies ($0.20 / رحلة)',
|
||
labelEn: 'HERE Technologies ($0.20 / ride)',
|
||
noteAr: 'تسعير المعاملات مع رسوم إضافية على مصفوفة المسافات والتوجيه المتقدم.',
|
||
noteEn: 'Transaction-based billing with surcharges on matrix and fleet telematics.'
|
||
},
|
||
esri: {
|
||
id: 'esri',
|
||
name: 'Esri ArcGIS',
|
||
costPerRide: 0.25,
|
||
labelAr: 'Esri ArcGIS ($0.25 / رحلة)',
|
||
labelEn: 'Esri ArcGIS ($0.25 / ride)',
|
||
noteAr: 'ترخيص GIS مكلف جداً لعمليات التوصيل والمطابقة اللحظية للسائقين.',
|
||
noteEn: 'Heavy GIS licensing with high penalty rates on stored spatial queries.'
|
||
},
|
||
tomtom: {
|
||
id: 'tomtom',
|
||
name: 'TomTom Maps',
|
||
costPerRide: 0.007,
|
||
labelAr: 'TomTom Maps ($0.007 / رحلة)',
|
||
labelEn: 'TomTom Maps ($0.007 / ride)',
|
||
noteAr: 'أقل مزود عالمي في السوق، وسيرو تمنحك نصف تكلفته ($0.0035 / رحلة) مع سيادة كاملة.',
|
||
noteEn: 'Lowest global provider; Siro delivers 50% lower unit cost ($0.0035 / ride) with full sovereignty.'
|
||
}
|
||
};
|
||
|
||
const SIRO_INFRA_TIERS = [
|
||
{
|
||
minRides: 0,
|
||
maxRides: 10000,
|
||
monthlyCost: 350,
|
||
tierNameAr: 'باقة الانطلاق والنمو (Starter Tier)',
|
||
tierNameEn: 'Sovereign Starter Tier',
|
||
specsAr: 'اتفاقية مستوى خدمة 99.9% مع زمن استجابة فائق السرعة أقل من 10ms',
|
||
specsEn: '99.9% SLA with sub-10ms query latency',
|
||
qpsCapacityAr: 'سعة استيعاب حتى 2,100 طلب/دقيقة',
|
||
qpsCapacityEn: 'Up to 2,100 requests/min capacity',
|
||
deploymentAr: 'سحابة سيادية مخصصة مع عزل بيانات 100%',
|
||
deploymentEn: 'Dedicated Sovereign Cloud with 100% Data Isolation'
|
||
},
|
||
{
|
||
minRides: 10001,
|
||
maxRides: 30000,
|
||
monthlyCost: 750,
|
||
tierNameAr: 'باقة الأساطيل المتقدمة (Fleet Tier)',
|
||
tierNameEn: 'Advanced Fleet Tier',
|
||
specsAr: 'اتفاقية مستوى خدمة 99.95% مع دعم فني متواصل 24/7',
|
||
specsEn: '99.95% SLA with 24/7 continuous enterprise support',
|
||
qpsCapacityAr: 'سعة استيعاب حتى 5,400 طلب/دقيقة',
|
||
qpsCapacityEn: 'Up to 5,400 requests/min capacity',
|
||
deploymentAr: 'عنقود سيادي ثنائي متزامن ضد الانقطاع',
|
||
deploymentEn: 'Dual-Zone Redundant Sovereign Mesh'
|
||
},
|
||
{
|
||
minRides: 30001,
|
||
maxRides: 75000,
|
||
monthlyCost: 1400,
|
||
tierNameAr: 'باقة التوسع الإقليمي (Scale Tier)',
|
||
tierNameEn: 'Regional Scale Tier',
|
||
specsAr: 'اتفاقية مستوى خدمة 99.99% مع توجيه ذكي للمسارات وحماية الهوامش',
|
||
specsEn: '99.99% SLA with AI-powered routing & gross margin protection',
|
||
qpsCapacityAr: 'سعة استيعاب حتى 15,000 طلب/دقيقة',
|
||
qpsCapacityEn: 'Up to 15,000 requests/min capacity',
|
||
deploymentAr: 'بنية موزعة إقليمياً مع موازنة أحمال ذكية',
|
||
deploymentEn: 'Regionally Distributed Load-Balanced Mesh'
|
||
},
|
||
{
|
||
minRides: 75001,
|
||
maxRides: 200000,
|
||
monthlyCost: 2800,
|
||
tierNameAr: 'باقة المؤسسات الموسعة (Enterprise Fleet)',
|
||
tierNameEn: 'Enterprise Mobility Fleet',
|
||
specsAr: 'اتفاقية مستوى خدمة 99.995% مع تخصيص نماذج الملاحة الإقليمية',
|
||
specsEn: '99.995% SLA with customized regional routing parameters',
|
||
qpsCapacityAr: 'سعة استيعاب حتى 45,000 طلب/دقيقة',
|
||
qpsCapacityEn: 'Up to 45,000 requests/min capacity',
|
||
deploymentAr: 'توسع تلقائي فوري وفق ذروة الطلب اليومية',
|
||
deploymentEn: 'Dynamic Auto-Scaling Private Sovereign VPC'
|
||
},
|
||
{
|
||
minRides: 200001,
|
||
maxRides: 500000,
|
||
monthlyCost: 5500,
|
||
tierNameAr: 'الشبكة السيادية المؤسسية (Enterprise Sovereign)',
|
||
tierNameEn: 'Enterprise Sovereign Cloud',
|
||
specsAr: 'اتفاقية مستوى خدمة 99.999% مع مدير حساب تقني واستشاري مخصص',
|
||
specsEn: '99.999% SLA with dedicated technical account strategist',
|
||
qpsCapacityAr: 'سعة استيعاب حتى 108,000 طلب/دقيقة',
|
||
qpsCapacityEn: 'Up to 108,000 requests/min capacity',
|
||
deploymentAr: 'نشر متكرر عبر مراكز بيانات محلية متعددة',
|
||
deploymentEn: 'Multi-Region Sovereign Redundancy'
|
||
},
|
||
{
|
||
minRides: 500001,
|
||
maxRides: 1000000,
|
||
monthlyCost: 9900,
|
||
tierNameAr: 'الشبكة السيادية الوطنية (National Sovereign Grid)',
|
||
tierNameEn: 'National Sovereign Grid',
|
||
specsAr: 'اتفاقية خدمة حكومية ومؤسسية عليا مع سيادة وطنية مطلقة على البيانات',
|
||
specsEn: 'Mission-critical government & national infrastructure SLA',
|
||
qpsCapacityAr: 'سعة استيعاب حتى 270,000+ طلب/دقيقة',
|
||
qpsCapacityEn: 'Up to 270,000+ requests/min throughput',
|
||
deploymentAr: 'نشر مخصص في السحابة الخاصة للعميل مع عزل كامل',
|
||
deploymentEn: 'Dedicated Customer Private Cloud / VPC'
|
||
}
|
||
];
|
||
|
||
export default function PublicHome() {
|
||
const { language, toggle } = useLanguage();
|
||
const c = copy[language];
|
||
const ar = language === 'ar';
|
||
const [menuOpen, setMenuOpen] = useState(false);
|
||
const [activeLayer, setActiveLayer] = useState(0);
|
||
const [isAutoCycle, setIsAutoCycle] = useState(true);
|
||
const [cycleProgress, setCycleProgress] = useState(0);
|
||
|
||
useEffect(() => {
|
||
if (!isAutoCycle) return;
|
||
const interval = window.setInterval(() => {
|
||
setCycleProgress(prev => {
|
||
if (prev >= 100) {
|
||
setActiveLayer(curr => (curr + 1) % 3);
|
||
return 0;
|
||
}
|
||
return prev + 1.4;
|
||
});
|
||
}, 100);
|
||
return () => clearInterval(interval);
|
||
}, [isAutoCycle]);
|
||
|
||
// Unit Economics State
|
||
const [dailyRides, setDailyRides] = useState(25000);
|
||
const [selectedCompetitor, setSelectedCompetitor] = useState<'google' | 'mapbox' | 'here' | 'esri' | 'tomtom'>('google');
|
||
|
||
const currentTier = SIRO_INFRA_TIERS.find(t => dailyRides >= t.minRides && dailyRides <= t.maxRides) || SIRO_INFRA_TIERS[SIRO_INFRA_TIERS.length - 1];
|
||
// Siro unit price: $0.0035 per completed ride (50% of lowest competitor TomTom at $0.007, and 94% cheaper than Google!)
|
||
const siroCostPerRide = 0.0035;
|
||
const siroMonthly = Math.round(dailyRides * siroCostPerRide * 30);
|
||
const compInfo = COMPETITORS[selectedCompetitor];
|
||
const competitorMonthly = Math.round(dailyRides * compInfo.costPerRide * 30);
|
||
const monthlySavings = competitorMonthly - siroMonthly;
|
||
const annualSavings = monthlySavings * 12;
|
||
const savingsPercent = Math.round((monthlySavings / competitorMonthly) * 100);
|
||
|
||
// 3D Terrain & Sun Simulation State
|
||
const [sunHour, setSunHour] = useState(17.5); // 17:30 default (Golden Hour)
|
||
const [terrainExaggeration, setTerrainExaggeration] = useState(2.0); // Default 2x as requested!
|
||
const [terrainLocationIdx, setTerrainLocationIdx] = useState(0);
|
||
|
||
const terrainLocations = [
|
||
{ nameAr: 'وادي رم - الجبال والشواهق', nameEn: 'Wadi Rum - Monoliths', elev: '+900m to +1,734m', descAr: 'تضاريس جرانيتية حادة وظلال صحراوية عميقة', descEn: 'Sharp granitic escarpments & deep desert shadows', offset: 0 },
|
||
{ nameAr: 'البترا - السيق ووادي موسى', nameEn: 'Petra - Siq & Canyons', elev: '+850m to +1,150m', descAr: 'وديان رملية ملتوية وكتل صخرية شاهقة', descEn: 'Winding sandstone gorges & towering facades', offset: 25 },
|
||
{ nameAr: 'البحر الميت - الصدع الأردني', nameEn: 'Dead Sea - Rift Valley', elev: '-430m to +300m', descAr: 'أعمق نقطة يابسة على وجه الأرض مع انحدارات صدعية', descEn: 'Lowest continental depression on Earth with steep fault lines', offset: -35 },
|
||
{ nameAr: 'عمّان - التلال السبعة', nameEn: 'Amman - Seven Hills', elev: '+750m to +1,050m', descAr: 'تضاريس جبلية حضرية وكثافة عمرانية متعددة المناسيب', descEn: 'Multi-level urban undulating valley topography', offset: 15 },
|
||
];
|
||
const activeTerrain = terrainLocations[terrainLocationIdx];
|
||
|
||
const getSunParams = (h: number) => {
|
||
// Calibrated for Amman/MENA ~32°N
|
||
const elevation = Math.round(Math.sin(((h - 6) / 12) * Math.PI) * 72);
|
||
const azimuth = Math.round(80 + ((h - 6) / 12) * 190);
|
||
let name = ar ? 'الظهيرة' : 'Noon';
|
||
let color = '#fffbcf';
|
||
let filter = 'brightness(1.1) contrast(1.05)';
|
||
if (h < 8) {
|
||
name = ar ? 'الفجر / الشروق' : 'Dawn / Sunrise';
|
||
color = '#ff9e79';
|
||
filter = 'brightness(0.9) sepia(0.35) hue-rotate(-15deg)';
|
||
} else if (h >= 16 && h < 19) {
|
||
name = ar ? 'العصر الذهبي' : 'Golden Hour';
|
||
color = '#f5b042';
|
||
filter = 'brightness(0.95) sepia(0.55) hue-rotate(10deg)';
|
||
} else if (h >= 19 || h < 6) {
|
||
name = ar ? 'الليل الفلكي' : 'Night';
|
||
color = '#4a69bd';
|
||
filter = 'brightness(0.5) contrast(1.2) hue-rotate(180deg)';
|
||
}
|
||
return { elevation, azimuth, name, color, filter };
|
||
};
|
||
const sunInfo = getSunParams(sunHour);
|
||
|
||
// Regional Sovereign Coverage Data
|
||
const countries = [
|
||
{ name: ar ? 'المملكة الأردنية الهاشمية' : 'Jordan', code: 'JO', pois: '28,450', km: '98,000 km', flag: '🇯🇴', desc: ar ? 'تغطية طبوغرافية كاملة مع شبكة طرق وادي رم والبترا' : 'Full topographic coverage, Wadi Rum & Petra road graphs' },
|
||
{ name: ar ? 'جمهورية العراق' : 'Iraq', code: 'IQ', pois: '22,100', km: '142,000 km', flag: '🇮🇶', desc: ar ? 'تغطية بغداد، البصرة، أربيل، الموصل والمحاور اللوجستية' : 'Baghdad, Basra, Erbil, Mosul & national logistics corridors' },
|
||
{ name: ar ? 'الجمهورية العربية السورية' : 'Syria', code: 'SY', pois: '14,800', km: '74,000 km', flag: '🇸🇾', desc: ar ? 'دمشق، حلب، حمص واللاذقية مع معالم التراث الموثقة' : 'Damascus, Aleppo, Homs, Latakia & verified heritage' },
|
||
{ name: ar ? 'جمهورية مصر العربية' : 'Egypt', code: 'EG', pois: '45,200', km: '210,000 km', flag: '🇪🇬', desc: ar ? 'القاهرة الكبرى، الإسكندرية، الدلتا ومحاور الصعيد' : 'Greater Cairo, Alexandria, Delta & Upper Egypt highways' },
|
||
];
|
||
|
||
// Manual tabs control - no aggressive auto-switch
|
||
const icons = [Map, Search, Route, Database];
|
||
|
||
return <div className="public-site">
|
||
<header className="public-nav shell">
|
||
<a href="#top" className="brand"><img src="/siro_maps_logo.png" alt="SIRO" className="brand-logo-img" /><span>SIRO<small>MAPS</small></span></a>
|
||
<nav className={menuOpen ? 'open' : ''}>
|
||
{c.nav.map((item, index) => <a key={item} href={`#section-${index + 1}`} onClick={() => setMenuOpen(false)}>{item}</a>)}
|
||
</nav>
|
||
<div className="nav-actions"><button className="language-button" onClick={toggle}>{language === 'ar' ? 'EN' : 'ع'}</button><Link className="nav-console" to="/console">{c.login}<ArrowUpRight size={15} /></Link><button className="menu-button" onClick={() => setMenuOpen(v => !v)} aria-label="Menu">{menuOpen ? <X /> : <Menu />}</button></div>
|
||
</header>
|
||
|
||
<main id="top">
|
||
{/* Hero Section */}
|
||
<section className="hero shell">
|
||
<div className="hero-copy">
|
||
<div className="eyebrow"><Sparkles size={14} />{c.badge}</div>
|
||
<h1>{c.titleA}<br /><em>{c.titleB}</em></h1>
|
||
<p>{c.subtitle}</p>
|
||
<div className="hero-actions">
|
||
<Link className="button lime" to="/console">{c.start}<ArrowUpRight size={17} /></Link>
|
||
<a className="button ghost" href="#section-2">{c.explore}<ChevronRight size={17} /></a>
|
||
</div>
|
||
<div className="hero-platforms">
|
||
<span>WEB (MAPLIBRE)</span>
|
||
<span>FLUTTER</span>
|
||
<span>SWIFT</span>
|
||
<span>KOTLIN</span>
|
||
<span>REST API</span>
|
||
<span>OFFLINE SQLITE</span>
|
||
</div>
|
||
</div>
|
||
<HeroLiveMap />
|
||
</section>
|
||
|
||
{/* 01 / Platform Workflow */}
|
||
<section id="section-1" className="workflow shell">
|
||
<div>
|
||
<span className="section-kicker">01 / PLATFORM</span>
|
||
<h2>{c.platform}</h2>
|
||
<p>{c.platformText}</p>
|
||
</div>
|
||
<div className="workflow-track">
|
||
{[[KeyRound, 'API KEY'], [Braces, 'PLAYGROUND'], [Zap, 'USAGE'], [Building2, 'SUBSCRIPTION']].map(([Icon, label], i) => {
|
||
const I = Icon as typeof KeyRound;
|
||
return <div key={String(label)}><span>{i + 1}</span><I /><b>{label as string}</b>{i < 3 && <i />}</div>;
|
||
})}
|
||
</div>
|
||
</section>
|
||
|
||
{/* 02 / Capabilities */}
|
||
<section id="section-2" className="section shell">
|
||
<div className="section-title">
|
||
<span className="section-kicker">02 / CAPABILITIES</span>
|
||
<h2>{c.services}</h2>
|
||
</div>
|
||
<div className="service-grid">
|
||
{serviceData[language].map(([title, body], index) => {
|
||
const Icon = icons[index];
|
||
const capabilityModes = ['layers', 'search', 'routing', 'spatial'];
|
||
return <article key={title}>
|
||
<div><Icon size={24} /><span>0{index + 1}</span></div>
|
||
<h3>{title}</h3>
|
||
<p>{body}</p>
|
||
<Link to={`/console/playground?mode=${capabilityModes[index]}`}>{ar ? 'جرّبها في المختبر' : 'Try in playground'}<ArrowUpRight size={15} /></Link>
|
||
</article>;
|
||
})}
|
||
</div>
|
||
</section>
|
||
|
||
{/* 03 / Unit Economics & ROI Calculator */}
|
||
<section id="section-3" className="roi-section shell">
|
||
<div className="section-title">
|
||
<span className="section-kicker">{c.roiKicker}</span>
|
||
<h2>{c.roiTitle}</h2>
|
||
<p>{c.roiSubtitle}</p>
|
||
</div>
|
||
|
||
<div className="roi-calculator-container">
|
||
<div className="roi-slider-panel">
|
||
{/* Competitor Selector */}
|
||
<div className="competitor-switch-box">
|
||
<small>{ar ? 'اختر المزود للمقارنة المباشرة' : 'SELECT PROVIDER TO BENCHMARK'}</small>
|
||
<div className="competitor-switch">
|
||
{Object.keys(COMPETITORS).map((key) => {
|
||
const comp = COMPETITORS[key as keyof typeof COMPETITORS];
|
||
return (
|
||
<button
|
||
key={key}
|
||
type="button"
|
||
className={`competitor-btn ${selectedCompetitor === key ? 'active' : ''}`}
|
||
onClick={() => setSelectedCompetitor(key as any)}
|
||
>
|
||
<span>{comp.name}</span>
|
||
<small>(${comp.costPerRide})</small>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="roi-slider-header">
|
||
<span><Navigation size={18} /> {c.dailyRidesLabel}</span>
|
||
<strong className="roi-value-pill">{dailyRides.toLocaleString()} {ar ? 'رحلة / يوم' : 'rides / day'}</strong>
|
||
</div>
|
||
<input
|
||
type="range"
|
||
min="5000"
|
||
max="1000000"
|
||
step="5000"
|
||
value={dailyRides}
|
||
onChange={(e) => setDailyRides(Number(e.target.value))}
|
||
className="roi-slider"
|
||
/>
|
||
<div className="roi-presets">
|
||
{[5000, 15000, 25000, 50000, 100000, 250000, 500000, 1000000].map((preset) => (
|
||
<button
|
||
key={preset}
|
||
className={`roi-preset-btn ${dailyRides === preset ? 'active' : ''}`}
|
||
onClick={() => setDailyRides(preset)}
|
||
>
|
||
{preset >= 1000000 ? `${preset / 1000000}M` : `${preset / 1000}K`} {ar ? 'رحلة' : 'rides'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="roi-badge-box">
|
||
<ShieldCheck size={20} />
|
||
<div>
|
||
<b>{c.unitMarginLabel}</b>
|
||
<p>{ar ? 'حساب مبني على 18-25 طلباً لكل رحلة مكتملة (بحث، إكمال، مصفوفة مسافات، ملاحة، تايلز متجهة).' : 'Based on 18-25 distinct API calls per completed ride across the trip lifecycle.'}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="roi-results-grid">
|
||
<div className="roi-result-card legacy">
|
||
<span className="card-kicker">{compInfo.name.toUpperCase()}</span>
|
||
<h3>${competitorMonthly.toLocaleString()}</h3>
|
||
<small>{ar ? `تكلفة ${compInfo.name} الشهرية` : `Monthly ${compInfo.name} Cost`}</small>
|
||
<div className="roi-bar-wrapper">
|
||
<div className="roi-bar legacy-bar" style={{ width: '100%' }} />
|
||
</div>
|
||
<p className="roi-footnote">{ar ? compInfo.noteAr : compInfo.noteEn}</p>
|
||
</div>
|
||
|
||
<div className="roi-result-card sovereign">
|
||
<span className="card-kicker">SIRO SOVEREIGN PRICING</span>
|
||
<h3>${siroMonthly.toLocaleString()}</h3>
|
||
<small>{ar ? 'تكلفة سيرو الشهرية ($0.0035 / رحلة)' : 'Monthly Siro Cost ($0.0035 / ride)'}</small>
|
||
<div className="roi-bar-wrapper">
|
||
<div className="roi-bar sovereign-bar" style={{ width: `${Math.max(3, (siroMonthly / competitorMonthly) * 100)}%` }} />
|
||
</div>
|
||
<div className="tier-spec-card">
|
||
<div className="tier-name">
|
||
<span>{ar ? currentTier.tierNameAr : currentTier.tierNameEn}</span>
|
||
<span className="tier-qps">{ar ? currentTier.qpsCapacityAr : currentTier.qpsCapacityEn}</span>
|
||
</div>
|
||
<div className="tier-specs">{ar ? currentTier.specsAr : currentTier.specsEn}</div>
|
||
<div className="tier-deploy">{ar ? currentTier.deploymentAr : currentTier.deploymentEn}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="roi-result-card highlight">
|
||
<div className="roi-savings-badge"><TrendingUp size={16} /> {ar ? `وفر ${savingsPercent}% مباشر` : `${savingsPercent}% Direct Savings`}</div>
|
||
<span className="card-kicker">{c.monthlySavingsLabel}</span>
|
||
<h2 className="savings-amount">${monthlySavings.toLocaleString()} <span>/ {ar ? 'شهريًا' : 'mo'}</span></h2>
|
||
<div className="annual-stat">
|
||
<span>{c.annualSavingsLabel}:</span>
|
||
<strong>${annualSavings.toLocaleString()}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 03.1 / Enterprise Market Benchmark Table (Full Width) */}
|
||
<div className="market-study-full-wrapper">
|
||
<div className="market-study-header">
|
||
<div>
|
||
<span className="section-kicker">03.1 / MARKET BENCHMARK & API PRICING</span>
|
||
<h3>{ar ? 'مقارنة تسعير واجهات الخرائط لكل 1,000 طلب (1,000 Requests) وتكلفة الرحلة' : 'Enterprise Map API Pricing (per 1,000 Requests) & Trip Economics'}</h3>
|
||
<p>
|
||
{ar
|
||
? 'مقارنة معيارية تفصيلية مبنية على الأسعار الرسمية المعلنة لكل ألف طلب (1k requests) واحتساب تكلفة الـ 7 طلبات الأساسية لكل رحلة مكتملة ومضاعفتها لكل 1,000 رحلة.'
|
||
: 'Detailed benchmark based on published per-1,000 requests pricing, calculated for the 7 core API calls per completed ride and scaled to 1,000 completed rides.'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="table-scroll-container">
|
||
<table className="pricing-comp-table">
|
||
<thead>
|
||
<tr>
|
||
<th>{ar ? 'واجهة البرمجة / المعيار' : 'API Service / Metric'}</th>
|
||
<th>Google Maps</th>
|
||
<th>Mapbox</th>
|
||
<th>HERE Technologies</th>
|
||
<th>Esri ArcGIS</th>
|
||
<th>TomTom</th>
|
||
<th className="siro-col">{ar ? 'سيرو (Siro Sovereign)' : 'Siro Sovereign Cloud'}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><strong>{ar ? 'نموذج الفوترة' : 'Billing Model'}</strong></td>
|
||
<td className="google-price">{ar ? 'ضريبة لكل طلب' : 'Pay-per-hit'}</td>
|
||
<td className="competitor-price">{ar ? 'استهلاك MAU + طلبات' : 'MAU + Metered'}</td>
|
||
<td className="competitor-price">{ar ? 'معاملات متدرجة' : 'Transactions'}</td>
|
||
<td className="competitor-price">{ar ? 'ترخيص GIS استهلاكي' : 'Metered Credits'}</td>
|
||
<td className="competitor-price">{ar ? 'دفع حسب النمو' : 'Pay-as-you-grow'}</td>
|
||
<td className="siro-price siro-col">{ar ? 'دفع حسب الاستهلاك (نصف السعر) أو باقة ثابتة' : 'Pay-as-you-grow (50% off) or Fixed Tier'}</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>{ar ? 'البحث والإكمال Places Autocomplete (لكل 1,000 طلب)' : 'Places Autocomplete (1k)'}</strong></td>
|
||
<td className="google-price">$2.83</td>
|
||
<td className="competitor-price">$2.50</td>
|
||
<td className="competitor-price">$2.50</td>
|
||
<td className="competitor-price">$3.00</td>
|
||
<td className="competitor-price">$0.50</td>
|
||
<td className="siro-price siro-col"><strong>$0.25</strong> <small style={{ color: 'var(--lime)', fontSize: '10px' }}>({ar ? 'وفر 50% عن أقل منافس' : '50% vs lowest'})</small></td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>{ar ? 'تفاصيل الأماكن Places Details / Geocode (لكل 1,000 طلب)' : 'Places Details (1k)'}</strong></td>
|
||
<td className="google-price">$17.00</td>
|
||
<td className="competitor-price">$12.50</td>
|
||
<td className="competitor-price">$10.00</td>
|
||
<td className="competitor-price">$15.00</td>
|
||
<td className="competitor-price">$1.50</td>
|
||
<td className="siro-price siro-col"><strong>$0.75</strong> <small style={{ color: 'var(--lime)', fontSize: '10px' }}>({ar ? 'وفر 50% عن أقل منافس' : '50% vs lowest'})</small></td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>{ar ? 'الترميز العكسي Reverse Geocoding (لكل 1,000 طلب)' : 'Reverse Geocoding (1k)'}</strong></td>
|
||
<td className="google-price">$5.00</td>
|
||
<td className="competitor-price">$4.00</td>
|
||
<td className="competitor-price">$4.00</td>
|
||
<td className="competitor-price">$40.00</td>
|
||
<td className="competitor-price">$0.50</td>
|
||
<td className="siro-price siro-col"><strong>$0.25</strong> <small style={{ color: 'var(--lime)', fontSize: '10px' }}>({ar ? 'وفر 50% عن أقل منافس' : '50% vs lowest'})</small></td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>{ar ? 'المسارات والملاحة Directions API (لكل 1,000 طلب)' : 'Directions & Routing (1k)'}</strong></td>
|
||
<td className="google-price">$5.00 - $10.00</td>
|
||
<td className="competitor-price">$4.00 - $8.00</td>
|
||
<td className="competitor-price">$4.00 - $6.00</td>
|
||
<td className="competitor-price">$10.00 - $20.00</td>
|
||
<td className="competitor-price">$1.00 - $2.00</td>
|
||
<td className="siro-price siro-col"><strong>$0.50</strong> <small style={{ color: 'var(--lime)', fontSize: '10px' }}>({ar ? 'وفر 50% عن أقل منافس' : '50% vs lowest'})</small></td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>{ar ? 'مصفوفة المسافات Distance Matrix (لكل 1,000 طلب)' : 'Distance Matrix (1k)'}</strong></td>
|
||
<td className="google-price">$10.00</td>
|
||
<td className="competitor-price">$6.00</td>
|
||
<td className="competitor-price">$8.00</td>
|
||
<td className="competitor-price">$10.00</td>
|
||
<td className="competitor-price">$2.00</td>
|
||
<td className="siro-price siro-col"><strong>$1.00</strong> <small style={{ color: 'var(--lime)', fontSize: '10px' }}>({ar ? 'وفر 50% عن أقل منافس' : '50% vs lowest'})</small></td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>{ar ? 'عرض الخرائط المتجهة Vector Maps (لكل 1,000 جلسة)' : 'Dynamic Vector Maps (1k)'}</strong></td>
|
||
<td className="google-price">$7.00</td>
|
||
<td className="competitor-price">$5.00</td>
|
||
<td className="competitor-price">$4.50</td>
|
||
<td className="competitor-price">$5.00</td>
|
||
<td className="competitor-price">$0.50</td>
|
||
<td className="siro-price siro-col"><strong>$0.25</strong> <small style={{ color: 'var(--lime)', fontSize: '10px' }}>({ar ? 'وفر 50% عن أقل منافس' : '50% vs lowest'})</small></td>
|
||
</tr>
|
||
<tr style={{ background: 'rgba(200, 248, 122, 0.04)' }}>
|
||
<td>
|
||
<strong>{ar ? 'تكلفة الـ 7 طلبات للرحلة الواحدة (7 API Calls / Single Ride)' : 'Cost of 7 API Calls / Single Ride'}</strong>
|
||
<br />
|
||
<small style={{ color: 'var(--muted)', fontSize: '10px' }}>
|
||
{ar ? '(2 إكمال + 1 تفاصيل + 1 ترميز عكسي + 1 مصفوفة + 1 مسار + 1 خريطة)' : '(2 Autocomplete + 1 Details + 1 Rev-Geo + 1 Matrix + 1 Route + 1 Map)'}
|
||
</small>
|
||
</td>
|
||
<td className="google-price">~$0.055 <span style={{ fontSize: '10px', color: '#f58771' }}>({ar ? 'وتصل $0.28 مع التتبع' : 'up to $0.28 with live tracking'})</span></td>
|
||
<td className="competitor-price">~$0.039 <span style={{ fontSize: '10px', color: '#f5a623' }}>({ar ? 'وتصل $0.18' : 'up to $0.18'})</span></td>
|
||
<td className="competitor-price">~$0.037 <span style={{ fontSize: '10px', color: '#f5a623' }}>({ar ? 'وتصل $0.20' : 'up to $0.20'})</span></td>
|
||
<td className="competitor-price">~$0.091 <span style={{ fontSize: '10px', color: '#f5a623' }}>({ar ? 'وتصل $0.25' : 'up to $0.25'})</span></td>
|
||
<td className="competitor-price">~$0.007 <span style={{ fontSize: '10px', color: '#f5a623' }}>({ar ? 'وتصل $0.08' : 'up to $0.08'})</span></td>
|
||
<td className="siro-price siro-col">
|
||
<strong style={{ fontSize: '14px', color: 'var(--lime)' }}>~$0.0035</strong>
|
||
<br />
|
||
<small style={{ color: 'var(--lime)', fontSize: '10px' }}>{ar ? '(نصف أقل منافس بالسوق)' : '(50% below lowest competitor)'}</small>
|
||
</td>
|
||
</tr>
|
||
<tr style={{ background: 'rgba(200, 248, 122, 0.08)', fontWeight: 700 }}>
|
||
<td>
|
||
<strong>{ar ? 'تكلفة كل 1,000 رحلة مكتملة (7,000 طلب API)' : 'Cost per 1,000 Completed Rides (7,000 Calls)'}</strong>
|
||
<br />
|
||
<small style={{ color: 'var(--muted)', fontSize: '10px' }}>
|
||
{ar ? '1,000 رحلة × 7 طلبات = 7,000 طلب API فعلي' : '1,000 rides × 7 calls = 7,000 API requests'}
|
||
</small>
|
||
</td>
|
||
<td className="google-price">$54.66 – $280.00</td>
|
||
<td className="competitor-price">$38.50 – $180.00</td>
|
||
<td className="competitor-price">$36.50 – $200.00</td>
|
||
<td className="competitor-price">$91.00 – $250.00</td>
|
||
<td className="competitor-price">$7.00 – $80.00</td>
|
||
<td className="siro-price siro-col">
|
||
<strong style={{ fontSize: '15px', color: 'var(--lime)' }}>$3.50</strong>
|
||
<br />
|
||
<small style={{ color: 'var(--lime)', fontSize: '10px' }}>{ar ? '(نصف توم توم ووفر 94% عن جوجل)' : '(50% vs TomTom, 94% vs Google)'}</small>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>{ar ? 'سيادة البيانات داخل المنطقة' : 'MENA Data Sovereignty'}</strong></td>
|
||
<td>❌ {ar ? 'سحابة خارجية' : 'Global Cloud'}</td>
|
||
<td>❌ {ar ? 'سحابة خارجية' : 'Global Cloud'}</td>
|
||
<td>❌ {ar ? 'سحابة خارجية' : 'Global Cloud'}</td>
|
||
<td>❌ {ar ? 'سحابة خارجية' : 'Global Cloud'}</td>
|
||
<td>❌ {ar ? 'سحابة خارجية' : 'Global Cloud'}</td>
|
||
<td className="siro-price siro-col">✅ {ar ? 'سيادية 100%' : '100% Sovereign'}</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>{ar ? 'نشر مخصص في السحابة الخاصة (VPC)' : 'Private Cloud / VPC Option'}</strong></td>
|
||
<td>❌ {ar ? 'غير متاح' : 'Not Available'}</td>
|
||
<td>⚠️ {ar ? 'مكلف جداً ($100K+)' : 'Enterprise ($100k+)'}</td>
|
||
<td>⚠️ {ar ? 'عقود معقدة' : 'Complex Contracts'}</td>
|
||
<td>⚠️ {ar ? 'ترخيص GIS باهظ' : 'Heavy GIS License'}</td>
|
||
<td>❌ {ar ? 'غير متاح' : 'Not Available'}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 03.2 / Ride Lifecycle 7-Calls Anatomy Box (Full Width) */}
|
||
<div className="ride-anatomy-box full-width">
|
||
<h4>{ar ? 'تشريح استهلاك الـ 7 طلبات للرحلة المكتملة (The 7-API Call Anatomy of a Single Ride)' : 'The 7-API Call Anatomy of a Single Ride & The $55–$280/1k Rides Reality'}</h4>
|
||
<p>
|
||
{ar
|
||
? 'في تطبيقات النقل والتوصيل (Ride-hailing & Delivery)، تتطلب كل رحلة واحدة ناجحة 7 طلبات API أساسية موزعة على دورة حياة الرحلة (بحث الموقع، تأكيد الوجهة، الترميز العكسي، مصفوفة المسافات، رسم المسار، وعرض الخريطة). وعند حساب تكلفة كل 1,000 رحلة (أي 7,000 طلب API)، تتراوح فاتورة Google Maps ما بين $54.66 إلى $280.00، في حين تحافظ سيرو على تكلفة ثابتة تماماً دون أي ضريبة تراكمية على نمو الرحلات.'
|
||
: 'In real-world mobility and delivery apps, a single completed ride consumes 7 distinct API calls across the trip lifecycle. When scaled to 1,000 completed rides (7,000 API calls), legacy providers charge between $54.66 and $280.00 with Google Maps, compared to $0.00 incremental cost with Siro Sovereign Cloud.'}
|
||
</p>
|
||
<div className="anatomy-steps-grid">
|
||
<div className="anatomy-step">
|
||
<span>STEP 01</span>
|
||
<strong>{ar ? 'تحديد الانطلاق والوجهة' : 'Pickup & Dropoff'}</strong>
|
||
<small>{ar ? '2× Autocomplete + 1× Geocode' : '2× Autocomplete + 1× Geocode'}</small>
|
||
<b>~$0.022</b>
|
||
</div>
|
||
<div className="anatomy-step">
|
||
<span>STEP 02</span>
|
||
<strong>{ar ? 'الترميز العكسي للسائق' : 'Driver Rev-Geocode'}</strong>
|
||
<small>{ar ? '1× Reverse Geocode' : '1× Reverse Geocoding'}</small>
|
||
<b>~$0.005</b>
|
||
</div>
|
||
<div className="anatomy-step">
|
||
<span>STEP 03</span>
|
||
<strong>{ar ? 'مطابقة السائق (ETA)' : 'Driver Matching (ETA)'}</strong>
|
||
<small>{ar ? '1× Distance Matrix لـ 5-10 سائقين' : '1× Distance Matrix (5-10 elements)'}</small>
|
||
<b>~$0.010</b>
|
||
</div>
|
||
<div className="anatomy-step">
|
||
<span>STEP 04</span>
|
||
<strong>{ar ? 'رسم المسار والملاحة' : 'Routing & Navigation'}</strong>
|
||
<small>{ar ? '1× Directions API مع بدائل' : '1× Directions API with traffic'}</small>
|
||
<b>~$0.010</b>
|
||
</div>
|
||
<div className="anatomy-step">
|
||
<span>STEP 05</span>
|
||
<strong>{ar ? 'عرض الخريطة والتتبع' : 'Vector Map Session'}</strong>
|
||
<small>{ar ? '1× Dynamic Map + تايلز متجهة' : '1× Dynamic Vector Map load'}</small>
|
||
<b>~$0.007</b>
|
||
</div>
|
||
<div className="anatomy-step highlight-step">
|
||
<span>TOTAL (1 RIDE)</span>
|
||
<strong style={{ color: 'var(--lime)' }}>{ar ? 'تكلفة الرحلة (7 طلبات)' : 'Single Ride (7 Calls)'}</strong>
|
||
<small>{ar ? 'سيرو: ~$0.0035 (نصف أقل منافس)' : 'Siro: ~$0.0035 (50% vs lowest)'}</small>
|
||
<b style={{ color: 'var(--lime)', fontSize: '13px' }}>~$0.0035 <span style={{ color: '#f58771', fontSize: '10px' }}>({ar ? 'مقابل $0.055' : 'vs $0.055'})</span></b>
|
||
</div>
|
||
<div className="anatomy-step highlight-step" style={{ borderInlineStart: '2px solid var(--lime)' }}>
|
||
<span>PER 1,000 RIDES</span>
|
||
<strong style={{ color: 'var(--lime)' }}>{ar ? 'لكل 1,000 رحلة (7k طلب)' : 'Per 1k Rides (7k Calls)'}</strong>
|
||
<small>{ar ? 'سيرو: $3.50 فقط (وفر 50% إلى 94%)' : 'Siro: $3.50 (50% to 94% savings)'}</small>
|
||
<b style={{ color: 'var(--lime)', fontSize: '13px' }}>$3.50 <span style={{ color: '#f58771', fontSize: '10px' }}>({ar ? 'مقابل $55-$280' : 'vs $55-$280'})</span></b>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 04 / 3D Terrain & Solar Shadow Simulation */}
|
||
<section id="section-4" className="terrain-sim-section shell">
|
||
<div className="section-title">
|
||
<span className="section-kicker">{c.terrainKicker}</span>
|
||
<h2>{c.terrainTitle}</h2>
|
||
<p>{c.terrainSubtitle}</p>
|
||
</div>
|
||
|
||
<div className="terrain-sim-layout">
|
||
<div className="terrain-controls-panel">
|
||
{/* Location Selector */}
|
||
<div className="terrain-location-selector">
|
||
<span className="section-kicker">{ar ? 'اختر المعلم الطبوغرافي' : 'SELECT TOPOGRAPHIC LANDMARK'}</span>
|
||
<div className="terrain-loc-buttons">
|
||
{terrainLocations.map((loc, i) => (
|
||
<button
|
||
key={loc.nameEn}
|
||
type="button"
|
||
className={`roi-preset-btn ${terrainLocationIdx === i ? 'active' : ''}`}
|
||
onClick={() => setTerrainLocationIdx(i)}
|
||
>
|
||
{ar ? loc.nameAr.split(' - ')[0] : loc.nameEn.split(' - ')[0]}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Sun Time Slider */}
|
||
<div className="sun-dial-header">
|
||
<Sun size={20} />
|
||
<span>{c.solarTimeLabel}</span>
|
||
<span className="sun-time-display">{Math.floor(sunHour)}:{Math.round((sunHour % 1) * 60).toString().padStart(2, '0')}</span>
|
||
</div>
|
||
|
||
<input
|
||
type="range"
|
||
min="5"
|
||
max="22"
|
||
step="0.5"
|
||
value={sunHour}
|
||
onChange={(e) => setSunHour(Number(e.target.value))}
|
||
className="roi-slider sun-slider"
|
||
/>
|
||
|
||
<div className="sun-presets">
|
||
{[
|
||
{ h: 6.5, label: ar ? 'الشروق' : 'Dawn' },
|
||
{ h: 12.5, label: ar ? 'الظهيرة' : 'Noon' },
|
||
{ h: 17.5, label: ar ? 'العصر الذهبي' : 'Golden' },
|
||
{ h: 21.0, label: ar ? 'الليل' : 'Night' },
|
||
].map(preset => (
|
||
<button
|
||
key={preset.h}
|
||
className={`roi-preset-btn ${sunHour === preset.h ? 'active' : ''}`}
|
||
onClick={() => setSunHour(preset.h)}
|
||
>
|
||
{preset.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Terrain Exaggeration Slider (Default 2x) */}
|
||
<div className="exaggeration-box">
|
||
<div className="exaggeration-header">
|
||
<span><Mountain size={15} /> {ar ? 'مقياس تضخيم الارتفاعات (Exaggeration)' : 'Terrain Exaggeration'}</span>
|
||
<strong>{terrainExaggeration}x</strong>
|
||
</div>
|
||
<div className="roi-presets">
|
||
{[1.0, 1.5, 2.0, 3.0].map(val => (
|
||
<button
|
||
key={val}
|
||
type="button"
|
||
className={`roi-preset-btn ${terrainExaggeration === val ? 'active' : ''}`}
|
||
onClick={() => setTerrainExaggeration(val)}
|
||
>
|
||
{val}x
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="solar-telemetry-grid">
|
||
<div className="telemetry-item">
|
||
<small>{ar ? 'حالة الإضاءة' : 'Lighting Phase'}</small>
|
||
<b style={{ color: sunInfo.color }}>{sunInfo.name}</b>
|
||
</div>
|
||
<div className="telemetry-item">
|
||
<small>{ar ? 'زاوية الارتفاع الفلكي' : 'Solar Elevation'}</small>
|
||
<b>{sunInfo.elevation}°</b>
|
||
</div>
|
||
<div className="telemetry-item">
|
||
<small>{ar ? 'السمت الشمسي (Azimuth)' : 'Solar Azimuth'}</small>
|
||
<b>{sunInfo.azimuth}° NW</b>
|
||
</div>
|
||
<div className="telemetry-item">
|
||
<small>{ar ? 'نطاق الارتفاع الحقيقي' : 'True Elevation'}</small>
|
||
<b>{activeTerrain.elev}</b>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="terrain-spec-list">
|
||
<div className="spec-row"><Mountain size={15} /> <span>Terrarium DEM RGB Height Encoding ({terrainExaggeration}x)</span></div>
|
||
<div className="spec-row"><Compass size={15} /> <span>Dynamic Hillshade 315° NW Shadow Raytracing</span></div>
|
||
<div className="spec-row"><Eye size={15} /> <span>{ar ? activeTerrain.descAr : activeTerrain.descEn}</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<TerrainLiveMap
|
||
terrainLocationIdx={terrainLocationIdx}
|
||
sunHour={sunHour}
|
||
terrainExaggeration={terrainExaggeration}
|
||
activeTerrain={activeTerrain}
|
||
sunInfo={sunInfo}
|
||
ar={ar}
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 04.B / Amman 3D Architectural Shadow & Solar Simulation */}
|
||
<section id="section-amman-shadow" className="amman-shadow-section shell">
|
||
<AmmanShadowSimulation ar={ar} />
|
||
</section>
|
||
|
||
{/* 05 / Sovereign Regional Coverage & Offline Graph */}
|
||
<section id="section-5" className="coverage-section shell">
|
||
<div className="section-title">
|
||
<span className="section-kicker">{c.coverageKicker}</span>
|
||
<h2>{c.coverageTitle}</h2>
|
||
<p>{c.coverageSubtitle}</p>
|
||
</div>
|
||
|
||
<div className="coverage-grid">
|
||
{countries.map(country => (
|
||
<article key={country.code} className="country-card">
|
||
<header className="country-header">
|
||
<span className="country-flag">{country.flag}</span>
|
||
<div>
|
||
<h3>{country.name}</h3>
|
||
<small className="country-code">{country.code} / SOVEREIGN DATA</small>
|
||
</div>
|
||
</header>
|
||
<p className="country-desc">{country.desc}</p>
|
||
<div className="country-metrics">
|
||
<div>
|
||
<small>{ar ? 'نقاط موثقة (POIs)' : 'Verified POIs'}</small>
|
||
<b>{country.pois}</b>
|
||
</div>
|
||
<div>
|
||
<small>{ar ? 'شبكة الطرق' : 'Road Graph'}</small>
|
||
<b>{country.km}</b>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
))}
|
||
</div>
|
||
|
||
<div className="offline-graph-banner">
|
||
<div className="offline-badge"><HardDrive size={22} /></div>
|
||
<div className="offline-content">
|
||
<h4>{ar ? 'بنية تحتية سيادية ونشر سحابي خاص (Private Cloud & On-Premise VPC)' : 'High-Availability Regional Infrastructure & Private Cloud Deployment'}</h4>
|
||
<p>{ar ? 'توفر منصة سيرو خيارات نشر مرنة تشمل بيئات السحابة الخاصة بالعميل (AWS / GCP / Azure) أو داخل مراكز البيانات المحلية (On-Premise VPC) بزمن استجابة فائق الدقة أقل من 10ms مع التوافق التام مع متطلبات سيادة البيانات والمعايير المؤسسية دون أي اعتمادية على شبكات توزيع خارجية عابرة للحدود.' : 'Siro provides flexible enterprise deployment topologies including client VPC (AWS / GCP / Azure) and local sovereign data centers with sub-10ms response times, 100% regional data sovereignty, and zero dependency on external cross-border CDNs.'}</p>
|
||
</div>
|
||
<Link to="/console/documentation" className="button ghost">
|
||
{ar ? 'خيارات النشر المؤسسي' : 'Enterprise Deployment Specs'} <ArrowUpRight size={15} />
|
||
</Link>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 06 / Live Map Experience Showcase */}
|
||
<section id="section-6" className="layer-showcase shell">
|
||
<div className="layer-copy">
|
||
<div className="layer-tabs-header">
|
||
<span className="section-kicker">06 / LIVE MAP EXPERIENCE</span>
|
||
<button
|
||
type="button"
|
||
className="layer-auto-toggle"
|
||
onClick={() => setIsAutoCycle(!isAutoCycle)}
|
||
>
|
||
{isAutoCycle ? '⏸️ ' + (ar ? 'إيقاف التبديل التلقائي' : 'Pause Auto') : '▶️ ' + (ar ? 'تشغيل التبديل التلقائي' : 'Play Auto')}
|
||
</button>
|
||
</div>
|
||
{isAutoCycle && (
|
||
<div className="layer-progress-bar">
|
||
<div className="layer-progress-fill" style={{ width: `${cycleProgress}%` }} />
|
||
</div>
|
||
)}
|
||
<h2>{ar ? 'الخريطة واجهة حيّة، وليست صورة.' : 'The map is a live interface, not an image.'}</h2>
|
||
<p>{ar ? 'بدّل بين المسار والتضاريس والبيانات. كل طبقة تضيف معنى جديدًا لتطبيقك.' : 'Move between routes, terrain and data. Each layer adds new meaning to your product.'}</p>
|
||
<div className="layer-tabs">
|
||
{['ROUTES', 'TERRAIN', 'DATA'].map((label, index) => (
|
||
<button
|
||
key={label}
|
||
className={activeLayer === index ? 'active' : ''}
|
||
onClick={() => {
|
||
setActiveLayer(index);
|
||
setCycleProgress(0);
|
||
}}
|
||
>
|
||
<span>0{index + 1}</span>{label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className={`layer-visual layer-${activeLayer}`}>
|
||
<ShowcaseLiveMap activeLayer={activeLayer} />
|
||
<div className="terrain-stack"><i /><i /><i /></div>
|
||
<div className="data-nodes"><i /><i /><i /><i /></div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 07 / Developer First */}
|
||
<section id="section-7" className="section shell">
|
||
<div className="section-title split">
|
||
<div><span className="section-kicker">07 / DEVELOPER FIRST</span><h2>{c.developers}</h2></div>
|
||
<Link className="button ghost" to="/console/documentation">{ar ? 'افتح التوثيق' : 'Open documentation'}<ArrowUpRight size={16} /></Link>
|
||
</div>
|
||
<div className="sdk-grid">
|
||
{sdkData.map(([name, code, mark]) => (
|
||
<article key={name}>
|
||
<span>{mark}</span>
|
||
<h3>{name}</h3>
|
||
<code dir="ltr">{code}</code>
|
||
<Check size={16} />
|
||
</article>
|
||
))}
|
||
</div>
|
||
<div className="developer-bar">
|
||
<div>
|
||
<Code2 />
|
||
<span>
|
||
<b>REST API & OpenAPI</b>
|
||
<small>{ar ? 'مرجع OpenAPI التفاعلي وتجربة حية' : 'OpenAPI reference and live testing'}</small>
|
||
</span>
|
||
</div>
|
||
<div>
|
||
<Box />
|
||
<span>
|
||
<b>Postman Collection</b>
|
||
<small>{ar ? 'تحميل واستيراد وتجربة آمنة دون مفاتيح' : 'Download, import and test safely'}</small>
|
||
</span>
|
||
</div>
|
||
<div className="dev-bar-actions">
|
||
<a href="/api/docs" target="_blank" rel="noreferrer" className="button ghost">
|
||
OpenAPI / Swagger <ArrowUpRight size={15} />
|
||
</a>
|
||
<a href="/siro-maps.postman_collection.json" download className="button lime">
|
||
<Download size={15} /> {ar ? 'تحميل Postman' : 'Download Postman'}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 08 / Enterprise Sovereign Platform */}
|
||
<section id="section-8" className="infrastructure shell" style={{ display: 'block', paddingBlock: '80px' }}>
|
||
<div style={{ maxWidth: '780px', marginBottom: '20px' }}>
|
||
<span className="section-kicker">08 / ENTERPRISE SOVEREIGN PLATFORM</span>
|
||
<h2>{ar ? 'منصة مكانية سيادية متكاملة للمؤسسات' : 'Integrated Sovereign Geospatial Platform'}</h2>
|
||
<p>{ar ? 'بنية تحتية هندسية عالية التوافر ومصممة لتشغيل الخدمات اللوجستية والتنقل وحماية هوامش الربح مع سيادة كاملة على البيانات داخل المنطقة.' : 'High-availability infrastructure engineered for logistics and mobility, protecting unit margins with 100% regional data sovereignty.'}</p>
|
||
</div>
|
||
<div className="enterprise-platform-grid">
|
||
<div className="enterprise-platform-card">
|
||
<div className="card-icon"><ShieldCheck size={22} /></div>
|
||
<h3>{ar ? 'سيادة البيانات بنسبة 100%' : '100% Data Sovereignty'}</h3>
|
||
<p>{ar ? 'حفظ البيانات المكانية وتتبع الأساطيل داخل الحدود الإقليمية مع الامتثال الصارم للقوانين المحلية، دون أي مشاركة مع خوادم خارجية.' : 'All spatial data and fleet telemetry are retained regionally with strict local regulatory compliance.'}</p>
|
||
<span className="card-tag">SOVEREIGN·STORAGE</span>
|
||
</div>
|
||
<div className="enterprise-platform-card">
|
||
<div className="card-icon"><Zap size={22} /></div>
|
||
<h3>{ar ? 'استجابة فائقة أقل من 10ms' : 'Sub-10ms Edge Latency'}</h3>
|
||
<p>{ar ? 'مراكز بيانات إقليمية سريعة تضمن استجابة فورية لتحميل مربعات الخرائط وحساب مسارات المركبات والبحث الجغرافي دون تأخير.' : 'Regional high-speed edge nodes deliver instant vector tiles, search responses and route computations.'}</p>
|
||
<span className="card-tag"><10MS·EDGE</span>
|
||
</div>
|
||
<div className="enterprise-platform-card">
|
||
<div className="card-icon"><Server size={22} /></div>
|
||
<h3>{ar ? 'نشر سحابي خاص ومعزول' : 'Air-Gapped & Private Cloud'}</h3>
|
||
<p>{ar ? 'إمكانية النشر الداخلي الكامل (On-Premise) في السحابة الخاصة بالعميل مع عزل تام للقطاعات الحكومية والمؤسسات الحساسة.' : 'Full on-premise and isolated private cloud deployment options for defense, government and enterprise.'}</p>
|
||
<span className="card-tag">AIR-GAPPED·READY</span>
|
||
</div>
|
||
<div className="enterprise-platform-card">
|
||
<div className="card-icon"><TrendingDown size={22} /></div>
|
||
<h3>{ar ? 'تكلفة سحابية ثابتة وحماية الهامش' : 'Fixed Predictable Cloud Cost'}</h3>
|
||
<p>{ar ? 'اشتراك سحابي شهري ثابت يزيل مفاجآت فواتير الاستهلاك لكل طلب ويوفر أكثر من 0.30$ في كل رحلة مكتملة.' : 'Predictable flat monthly cloud pricing eliminating per-call billing spikes and saving $0.30+ per trip.'}</p>
|
||
<span className="card-tag">FIXED·PRICING</span>
|
||
</div>
|
||
<div className="enterprise-platform-card">
|
||
<div className="card-icon"><Navigation size={22} /></div>
|
||
<h3>{ar ? 'محرك ملاحة وأساطيل فائق الدقة' : 'High-Precision Fleet Engine'}</h3>
|
||
<p>{ar ? 'توجيه ذكي متعدد الوسائط للسيارات والشاحنات والدراجات مع خوارزميات مطابقة الطرق (Map Matching) وتتبع السائقين اللحظي.' : 'Multi-modal routing with map-matching and live driver telemetry designed for scale.'}</p>
|
||
<span className="card-tag">FLEET·TELEMETRY</span>
|
||
</div>
|
||
<div className="enterprise-platform-card">
|
||
<div className="card-icon"><CheckCircle2 size={22} /></div>
|
||
<h3>{ar ? 'ضمان تشغيل مؤسسي 99.99%' : '99.99% Enterprise Uptime'}</h3>
|
||
<p>{ar ? 'بنية تحتية موزعة مع استعادة تلقائية عند الأعطال (Failover) وتكرار جغرافي مستمر يضمن استمرارية الأعمال على مدار الساعة.' : 'Distributed multi-zone architecture with automatic failover and high-resilience SLA.'}</p>
|
||
<span className="card-tag">99.99%·SLA</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Final Call To Action */}
|
||
<section className="final-cta shell">
|
||
<MapPin />
|
||
<h2>{c.cta}</h2>
|
||
<p>{c.ctaText}</p>
|
||
<Link className="button lime" to="/console">{c.start}<ArrowUpRight size={17} /></Link>
|
||
</section>
|
||
</main>
|
||
|
||
<footer className="shell">
|
||
<div className="brand"><img src="/siro_maps_logo.png" alt="SIRO" className="brand-logo-img" /><span>SIRO<small>MAPS</small></span></div>
|
||
<span>© 2026 SIRO MAPS · SOVEREIGN MENA GEOSPATIAL PLATFORM</span>
|
||
<div>
|
||
<a href="#section-3">{ar ? 'الجدوى' : 'Economics'}</a>
|
||
<a href="#section-7">SDKs</a>
|
||
<Link to="/console/billing">{ar ? 'الخطط' : 'Plans'}</Link>
|
||
<Link to="/console/documentation">{ar ? 'التوثيق' : 'Docs'}</Link>
|
||
</div>
|
||
</footer>
|
||
</div>;
|
||
}
|
||
|