2f47bfcfa57cd451db8574f2d637cae32442b880..94811ea59ca1b8f07284444736b6cc2eb4b35617
2026-10-18 Hong-Phuc Bui
Interactive UI
94811e diff | tree
2026-10-18 Hong-Phuc Bui
Interactive UI
250641 diff | tree
7 files added
291 ■■■■■ changed files
02-0-kurswinkel-benutzer-interactive/.gitignore patch | view | raw | blame | history
02-0-kurswinkel-benutzer-interactive/README.md 11 ●●●●● patch | view | raw | blame | history
02-0-kurswinkel-benutzer-interactive/common.css 27 ●●●●● patch | view | raw | blame | history
02-0-kurswinkel-benutzer-interactive/deploy.sh 23 ●●●●● patch | view | raw | blame | history
02-0-kurswinkel-benutzer-interactive/index.html 28 ●●●●● patch | view | raw | blame | history
02-0-kurswinkel-benutzer-interactive/sphere-calculator.js 27 ●●●●● patch | view | raw | blame | history
02-0-kurswinkel-benutzer-interactive/tangente.js 175 ●●●●● patch | view | raw | blame | history
02-0-kurswinkel-benutzer-interactive/.gitignore
02-0-kurswinkel-benutzer-interactive/README.md
New file
@@ -0,0 +1,11 @@
Lernziel:
* HTTP-Server
* Syntax von URLs (=Bestandteil eines URL)
* Relative/absolute Pfade
Task:
* Identifizieren Sie alle URL in der Datei `index.html`
* Identifizieren Sie die Bestandteile von einer URL.
  Welcher Bestandteil einer URL haben wir in eine ULR in `index.html` verwendet?
02-0-kurswinkel-benutzer-interactive/common.css
New file
@@ -0,0 +1,27 @@
html {
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
h1 {
    font-size: large;
}
.container {
    display: flex;
    gap: 20px;
    border: 0;
    margin: 0;
    padding: 0;
}
.column {
    flex: 1;
}
@media (max-width: 600px) {
    .container {
        flex-direction: column;
    }
}
02-0-kurswinkel-benutzer-interactive/deploy.sh
New file
@@ -0,0 +1,23 @@
sync_git() {
    SRC=$1
    DEST=$2
    echo $SRC
    echo $DEST
    mkdir -p $DEST
    ls $DEST
    rsync -aHAXxv --numeric-ids --delete --progress --exclude='.git' \
        --exclude='.gitignore' \
        --exclude='*/target' \
        --exclude='.idea' \
        --exclude='deploy.sh' \
        --exclude='.venv' \
        --exclude='site' \
        $SRC $DEST
    git -C $DEST add .
    git -C $DEST commit -a -m "Commit Datum: $(date --iso-8601=seconds)"
    git -C $DEST push origin main
}
sync_git ./ ../../codeberg.page/kurswinkel/
02-0-kurswinkel-benutzer-interactive/index.html
New file
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
    <script src="https://cdn.jsdelivr.net/npm/jsxgraph/distrib/jsxgraphcore.js"></script>
    <link rel="stylesheet" type="text/css"
          href="https://cdn.jsdelivr.net/npm/jsxgraph/distrib/jsxgraph.css">
    <link rel="stylesheet" type="text/css"
          href="./common.css"/>
</head>
<body>
<h1>Fluglinien mit Kurswinkel</h1>
<div class="container">
    <div class="column">
        <div id="box" class="jxgbox" style="width:800px; aspect-ratio: 1/1;"></div>
    </div>
    <div class="column">
        <div class="explain">
            <p>Drehe die Kugel so, dass der Startpunkt sich mit dem Punkt O deckt.
                (el = 1.05, az = 2.09)</p>
            <p>Der Winkel Nordpol-Startpunkt-Ziel ist der gesuchte Kurswinkel in der wahren Gestalt.</p>
        </div>
    </div>
</div>
<script type="module" src="tangente.js"></script>
</body>
</html>
02-0-kurswinkel-benutzer-interactive/sphere-calculator.js
New file
@@ -0,0 +1,27 @@
export const toRadians = (deg) => deg * Math.PI / 180;
export const toDegrees = (rad) => rad * 180 / Math.PI;
export const azimuth = (longitude1, latitude1, longitude2, latitude2) =>{
    const [lon1, lat1, lon2, lat2] = [longitude1, latitude1, longitude2, latitude2].map(toRadians);
    const _deltaLon = Math.abs(lon2 - lon1);
    const PI = Math.PI;
    const deltaLon = (_deltaLon) <= PI ? _deltaLon : 2*PI - _deltaLon;
    const q = (Math.tan(PI/4 + lat2/2))/(Math.tan(PI/4 + lat1/2));
    const ln = 1/Math.log(q);
    const alpha = Math.atan( deltaLon*ln ) * 180/PI; // degree!
    const eastCourse = _deltaLon <= PI;
    const direction = ( lon2>=lon1 &&  eastCourse) || (lon2<lon1 && !eastCourse);
    if(lat2 > lat1 && direction) {
        return alpha;
    } else if(lat2 < lat1 && direction) {
        return 180 - alpha;
    } else if(lat2 < lat1 && !direction) {
        return 180 + alpha;
    } else if(lat2 > lat1 && !direction) {
        return 360 - alpha;
    }
    if(lat2 === lat1) {
        return direction ? 90 : 270;
    }
}
02-0-kurswinkel-benutzer-interactive/tangente.js
New file
@@ -0,0 +1,175 @@
import {azimuth} from "./sphere-calculator.js";
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]) => {
    const n = cross(toXYZ(lon1, lat1), toXYZ(lon2, lat2));
    n.unshift(0);
    return n;
}
const toGeographicLabel = (lon, lat) => {
    return `${ lon.toFixed(1) }°|${ lat.toFixed(1)}°`;
}
const solveProjectionPoint = (startX, startY, startZ, targetX, targetY, targetZ) => {
    const t = targetX*targetX + targetY*targetY + targetZ*targetZ;
    const k = -(startX*targetX + startY*targetY + startZ*targetZ - t) / t;
    return [targetX + k * startX, targetY + k*startY, targetZ + k*startZ]
}
// 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 startLongitude = board.create("slider",
    [[-2, 10], [9, 10], [-180, -30, 180]],
    {name: "Start LON.", precision: 2}
);
const startLatitude = board.create("slider",
    [[-2, 9], [9, 9], [-90, 60, 90]],
    {name: "Start LAT.", precision: 2}
);
const targetLongitude = board.create("slider",
    [[-2, -10], [9, -10], [-180, 120, 180]],
    {name: "Ziel LON.", precision: 2}
);
const targetLatitude = board.create("slider",
    [[-2, -11], [9, -11], [-90, 0, 90]],
    {name: "Ziel LAT.", precision: 2}
);
const displayAzimuth = () => {
    const a = azimuth(startLongitude.Value(), startLatitude.Value(),
        targetLongitude.Value(), targetLatitude.Value()
    );
    return `Kurswinkel: ${a.toFixed(2)}°`;
}
const azimuthText = board.create('text', [8,6, displayAzimuth], {});
const view = board.create('view3d',
    [[-10, -10], [19, 19],
        [bound, bound, bound]],
    {
        axesPosition: 'none',
        xPlaneRear: {visible: false},
        yPlaneRear: {visible: false},
        zPlaneRear: {visible: false},
        //
        az:   { slider: { visible: true } },
        el:   { slider: { visible: true } },
        bank: { slider: { visible: true } },
    }
);
const origin = view.create('point3d', [0,0,0],
    {name:"O", fixed: true, highlight: false, showInfobox: false,}
);
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 startPoint = view.create('point3d',
    () => [...toXYZ(startLongitude.Value(), startLatitude.Value())],
    {id:'S', fixed: true, highlight: true, showInfobox: false,}
);
startPoint.element2D.label.setText( () => "Start: " +
    toGeographicLabel( ...toLonLat(startPoint.X(),startPoint.Y(), startPoint.Z()  ) ) );
const tangentPlan = view.create('circle3d',
    [startPoint, () => [0, startPoint.X(), startPoint.Y(), startPoint.Z()], RADIUS],
    {fillColor:"red", fillOpacity:0.25, fixed: true}
);
const targetPoint = view.create('point3d',
    () => [...toXYZ(targetLongitude.Value(), targetLatitude.Value())],
    {id: 'T', fixed: true, highlight: true, showInfobox: false,}
);
targetPoint.element2D.label.setText( () => "Ziel: " +
    toGeographicLabel( ...toLonLat(targetPoint.X(), targetPoint.Y(), targetPoint.Z()  ) ) );
const projectionPoint = view.create('point3d',
    () => solveProjectionPoint(
        startPoint.X(), startPoint.Y(), startPoint.Z(),
        targetPoint.X(), targetPoint.Y(), targetPoint.Z()
    ),
    {id: 'P', name:"Prj. Ziel", fixed: true, highlight: true, showInfobox: false, strokeColor:"black", fillColor: "gray"}
);
const routeNormalVector = () => {
    return normalVector(
        [startLongitude.Value(), startLatitude.Value()],
        [targetLongitude.Value(), targetLatitude.Value()]
    );
};
const route = view.create('circle3d', [origin, routeNormalVector, RADIUS] );
const meridianNormalVector = () => normalVector([startLongitude.Value(), startLatitude.Value()], NORTH_POLE);
const meridianPlan = view.create('circle3d', [origin, meridianNormalVector, RADIUS]);
const ot = view.create('line3d', [origin, startPoint])
const projectionDirectionVector = () => [startPoint.X(), startPoint.Y(), startPoint.Z()];
const projectionLine = view.create('line3d',
    [targetPoint, projectionDirectionVector,  [0, Infinity]],
    {strokeColor: "gray", dash: 2, strokeWidth: 1}
);