const toXYZ = (longitude, latitude) => { const lon = (longitude * Math.PI) / 180; const lat = (latitude * Math.PI) / 180; const x = RADIUS * Math.cos(lat) * Math.cos(lon); const y = RADIUS * Math.cos(lat) * Math.sin(lon); const z = RADIUS * Math.sin(lat); return [x, y, z]; }; const toHomogeneousXYZ = (longitude, latitude) => { const xyz = toXYZ(longitude, latitude); xyz.unshift(0); return xyz; } const toLonLat = (x, y, z) => { const R = Math.hypot(x, y, z); const longitude = (Math.atan2(y, x) * 180) / Math.PI; // [-180, 180] const latitude = (Math.asin(Math.max(-1, Math.min(1, z / R))) * 180) / Math.PI; // [-90, 90] return [longitude, latitude]; }; const cross = (a, b) => [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0], ]; const normalVector = ([lon1, lat1], [lon2, lat2], normalize = false) => { const n = cross(toXYZ(lon1, lat1), toXYZ(lon2, lat2)); if (!normalize) return n; const len = Math.hypot(...n); if (len < 1e-12) return null; // points are parallel or antipodal: no unique normal const m = n.map(c => c / len); m.unshift(0) return m; } const toGeographicLabel = (lon, lat) => { return `${ lon.toFixed(1) }°|${ lat.toFixed(1)}°`; } // board commands const RADIUS = 6; const BOARD_SIZE = 14; const board = JXG.JSXGraph.initBoard( 'box', { boundingbox: [-BOARD_SIZE, BOARD_SIZE, BOARD_SIZE, -BOARD_SIZE], //axis: true }); const bound = [-10, 10]; const view = board.create('view3d', [[-10, -10], [19, 19], [bound, bound, bound]], { axesPosition: 'none', xPlaneRear: {visible: true}, yPlaneRear: {visible: true}, zPlaneRear: {visible: true}, // az: { slider: { visible: true } }, el: { slider: { visible: true } }, bank: { slider: { visible: true } }, } ); const origin = view.create('point3d', [0,0,0], {name:"O", fixed: true}); const earth = view.create('sphere3d', [origin, RADIUS]); const NORTH_POLE = [0, 90]; const north_pole = toHomogeneousXYZ(...NORTH_POLE); const equator = view.create('circle3d', [origin, north_pole, RADIUS], { strokeColor:"red", fixed: true }); const northPoint = view.create('point3d', [toXYZ(...NORTH_POLE)], { name:'N', fixed: true, highlight: true, showInfobox: false, }); const prime_meridian_normal = toHomogeneousXYZ(90, 0) const prime_meridian = view.create('circle3d', [origin, prime_meridian_normal, RADIUS], { strokeColor: "blue", fixed: true }); const TANGENT_COORDINATE = [-30, 60] const tangentPoint = view.create('point3d', [toXYZ(...TANGENT_COORDINATE)], { id:'T', fixed: true, highlight: true, showInfobox: false, }); tangentPoint.element2D.label.setText( () => "T = " + toGeographicLabel( ...toLonLat(tangentPoint.X(),tangentPoint.Y(), tangentPoint.Z() ) ) ); const tangentPlan = view.create('circle3d', [tangentPoint, () => [0, tangentPoint.X(), tangentPoint.Y(), tangentPoint.Z()], RADIUS], {fillColor:"red", fillOpacity:0.25, fixed: true} ); const TARGET_COORDINATE = [120, 0]; const targetPoint = view.create('point3d', [toXYZ(...TARGET_COORDINATE)], { id: 't', name:toGeographicLabel(...TARGET_COORDINATE), fixed: true, highlight: true, showInfobox: false, }); const routeNormalVector = normalVector(TANGENT_COORDINATE, TARGET_COORDINATE); const route = view.create('circle3d', [origin, routeNormalVector, RADIUS] ); const meridianNormalVector = normalVector(TANGENT_COORDINATE, NORTH_POLE); const meridianPlan = view.create('circle3d', [origin, meridianNormalVector, RADIUS]); const projectionLine = view.create('line3d', [targetPoint, toXYZ(...TANGENT_COORDINATE), [0, Infinity] ]); const ot = view.create('line3d', [origin, tangentPoint])