Folium-Karten: die Blockade war eine falsche Alternative

Der einzige Punkt, der in diesem Projekt je als "blockiert" gefuehrt wurde,
ist erledigt. Die Begruendung lautete: Die VRP-Instanz sei ein synthetisches
100 x 100-km-Raster ohne Geokoordinaten, eine echte Karte verlange also
ENTWEDER eine neue Instanz (und damit zwei Dutzend neue abgedruckte Zahlen)
ODER eine willkuerliche Verankerung.

Es gibt einen dritten Weg, und er ist exakt. Eine azimutal-aequidistante
Projektion um den Schwerpunkt des Rasters bildet die 17 Punkte so auf Laenge
und Breite ab, dass die Grosskreisdistanzen mit den Rasterdistanzen
uebereinstimmen:

  festes cos(Breite)        Median 0,360 %   Maximum 2,003 %
  cos der eigenen Breite    Median 0,351 %   Maximum 0,908 %
  azimutal-aequidistant     Median 0,000 %   Maximum 0,001 %

Ein Meter auf hundert Kilometer. Kein abgedruckter Wert aendert sich, die
Gesamtstrecke bleibt bei 619 km, das SVG ist byte-identisch geblieben.

Zwei Befunde stuetzten die Entscheidung: "100 x 100 km Raster" steht nur in
zwei Codekommentaren, nirgends im Fliesstext; und der didaktische Punkt der
Bildunterschrift - die sich kreuzenden Touren, die KEIN Fehler sind - wird auf
einer Karte deutlicher.

erzeuge_vrp_touren.py schreibt jetzt SVG UND Karte aus einem Lauf. Ein zweites
Skript haette erneut geloest, und die Routing-Bibliothek arbeitet heuristisch.
pruefe_projektion() bricht ab, wenn die Verzerrung 0,01 % ueberschreitet - die
Zahl im Buch wird bei jedem Lauf bewiesen.

Neu im Build: die Marke {karte:name}, aufgeloest zu einem <iframe> (Leaflets
Stylesheet soll nicht in die Buchseite greifen), im PDF zum Verweissatz - wie
bei {plotly:}. --check bewacht jetzt beide Markenarten auf Existenz der Datei;
gegengeprueft mit einer absichtlich falschen Marke.

Zwei Vorkehrungen, die ohne Messung nicht aufgefallen waeren:

* Folium vergibt jedem Element eine uuid4 - jeder Lauf erzeugte eine andere
  Datei. normalisiere() ersetzt sie durch fortlaufende Nummern; zwei Laeufe
  liefern jetzt dieselbe Pruefsumme.
* Folium bindet ZEHN CDN-Ressourcen ein (jQuery, Bootstrap, Glyphicons,
  FontAwesome, awesome-markers ...). Gebraucht werden ZWEI. leaflet.js und
  leaflet.css liegen jetzt in web_04/assets/, die uebrigen acht entfallen.
  Nachgeprueft: Die Karte benutzt ausschliesslich L.*-Aufrufe. Ohne Netz
  fehlen nur die Kacheln, Marker und Touren bleiben sichtbar.

Gegenrechnung an vier Strecken: Depot -> K1 misst auf der Karte 27,20 km, die
Distanzmatrix sagt 27 - die Differenz stammt aus deren int(), nicht aus der
Geographie.

Veroeffentlicht: 8 Dateien uebertragen, alle dreizehn HTTP-Proben richtig, die
Karte auf dem Server unter 200 erreichbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dschlueter 2026-09-08 20:38:16 +02:00
commit 5d5bdb6508
19 changed files with 2521 additions and 5 deletions

View file

@ -188,6 +188,31 @@ eine einzige lange Zeile mit eingebetteten Leerzeichenketten, aus der Pandoc son
Codeblock macht. Genau daran waren alle vier Figuren kaputt, bis es die Schlussabnahme fand. Codeblock macht. Genau daran waren alle vier Figuren kaputt, bis es die Schlussabnahme fand.
Im PDF steht stattdessen ein Hinweis auf die Website. Im PDF steht stattdessen ein Hinweis auf die Website.
## Karten
`{karte:name}` bindet `bilder_04/karten/<name>.html` als `<iframe>` ein — anders als bei
Plotly **nicht** inline, weil Leaflets Stylesheet sonst in die Buchseite hineingriffe. Im PDF
steht wieder ein Hinweis auf die Website. `--check` prüft für beide Markenarten, dass die
Datei existiert.
Erzeugt werden die Karten von demselben `bilder_04/erzeuge_*.py`, das auch das zugehörige SVG
schreibt — **aus einem Lauf**, sonst zeigten die beiden Darstellungen bei einem heuristischen
Solver womöglich verschiedene Lösungen. Zwei Vorkehrungen in `erzeuge_vrp_touren.py` sind
übertragbar:
* **Byte-Reproduzierbarkeit:** Folium vergibt jedem Element eine `uuid4`. `normalisiere()`
ersetzt sie durch fortlaufende Nummern — dieselbe Sorgfalt wie `svg.hashsalt` bei den
matplotlib-Bildern.
* **Netzunabhängigkeit:** Von Foliums zehn CDN-Einbindungen bleiben die zwei gebrauchten,
umgeschrieben auf `../../assets/leaflet.*` (mitgeliefert in `web_04/assets/`); die übrigen
acht entfallen, weil die Karte nur `CircleMarker`, `PolyLine` und `Tooltip` benutzt. Ohne
Netz fehlen nur die Kartenkacheln.
**Die Instanz ist synthetisch und bleibt es.** `projiziere()` legt das 100 × 100-km-Raster
azimutal-äquidistant auf eine reale Region; `pruefe_projektion()` bricht ab, wenn die
Abstände dabei um mehr als 0,01 % verzerrt würden (gemessen: 0,001 %). Kein abgedruckter Wert
ändert sich dadurch — das war die Bedingung, unter der die Karte überhaupt möglich wurde.
--- ---
## Veroeffentlichung ## Veroeffentlichung

656
OR_HTML_04/assets/leaflet.css generated Normal file
View file

@ -0,0 +1,656 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

6
OR_HTML_04/assets/leaflet.js generated Normal file

File diff suppressed because one or more lines are too long

View file

@ -282,6 +282,13 @@ article li { margin: .3em 0; }
border-radius: var(--radius); padding: .6em; overflow-x: auto; } border-radius: var(--radius); padding: .6em; overflow-x: auto; }
.plotly-figur .plotly-graph-div { min-width: 320px; } .plotly-figur .plotly-graph-div { min-width: 320px; }
/* Leaflet-Karte im iframe ({karte:...}). Der Rahmen kommt von hier, nicht vom
iframe selbst - dessen Standardrahmen passt zu keinem der beiden Themen. */
.karte-figur { display: block; width: 100%; height: 30rem; margin: 1.4em 0;
border: 1px solid var(--border); border-radius: var(--radius);
background: var(--surface); }
@media (max-width: 640px) { .karte-figur { height: 22rem; } }
/* Der Formel-Uebersetzer ist zweispaltig gedacht: links die Mathematik, /* Der Formel-Uebersetzer ist zweispaltig gedacht: links die Mathematik,
rechts der Klartext. Auf schmalen Displays wuerde die Formelspalte die rechts der Klartext. Auf schmalen Displays wuerde die Formelspalte die
Tabelle sonst aus dem Viewport druecken. */ Tabelle sonst aus dem Viewport druecken. */

View file

@ -0,0 +1,430 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<script src="../../assets/leaflet.js"></script>
<link rel="stylesheet" href="../../assets/leaflet.css"/>
<meta name="viewport" content="width=device-width,
initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<style>
#map_01 {
position: relative;
width: 100.0%;
height: 100.0%;
left: 0.0%;
top: 0.0%;
}
.leaflet-container { font-size: 1rem; }
</style>
<style>html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
</style>
<style>#map {
position:absolute;
top:0;
bottom:0;
right:0;
left:0;
}
</style>
<script>
L_NO_TOUCH = false;
L_DISABLE_3D = false;
</script>
</head>
<body>
<div class="folium-map" id="map_01" ></div>
</body>
<script>
var map_01 = L.map(
"map_01",
{
center: [50.99937073954281, 9.200100716327679],
crs: L.CRS.EPSG3857,
...{
"zoom": 9,
"zoomControl": true,
"preferCanvas": false,
}
}
);
L.control.scale().addTo(map_01);
var tile_layer_02 = L.tileLayer(
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
{
"minZoom": 0,
"maxZoom": 19,
"maxNativeZoom": 19,
"noWrap": false,
"attribution": "\u0026copy; \u003ca href=\"https://www.openstreetmap.org/copyright\"\u003eOpenStreetMap\u003c/a\u003e contributors",
"subdomains": "abc",
"detectRetina": false,
"tms": false,
"opacity": 1,
}
);
tile_layer_02.addTo(map_01);
var poly_line_03 = L.polyline(
[[50.8902981044233, 9.493705194696913], [50.53528002580979, 9.056780383216372], [50.70020093721884, 9.023183616953922], [50.899334211646234, 8.572820141079644], [50.828627944796516, 8.85527539693994], [50.8902981044233, 9.493705194696913]],
{"bubblingMouseEvents": true, "color": "#4338ca", "dashArray": null, "dashOffset": null, "fill": false, "fillColor": "#4338ca", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "noClip": false, "opacity": 0.85, "smoothFactor": 1.0, "stroke": true, "weight": 3}
).addTo(map_01);
poly_line_03.bindTooltip(
`<div>
Fahrzeug 1: 172 km, 10/10 Einheiten
</div>`,
{
"sticky": true,
}
);
var poly_line_04 = L.polyline(
[[50.8902981044233, 9.493705194696913], [51.202571214486355, 9.47729598790725], [51.23585035464649, 9.309049094203546], [51.36580069667539, 9.454691406682949], [51.2977516796715, 9.779278159065392], [50.8902981044233, 9.493705194696913]],
{"bubblingMouseEvents": true, "color": "#0891b2", "dashArray": null, "dashOffset": null, "fill": false, "fillColor": "#0891b2", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "noClip": false, "opacity": 0.85, "smoothFactor": 1.0, "stroke": true, "weight": 3}
).addTo(map_01);
poly_line_04.bindTooltip(
`<div>
Fahrzeug 2: 135 km, 9/10 Einheiten
</div>`,
{
"sticky": true,
}
);
var poly_line_05 = L.polyline(
[[50.8902981044233, 9.493705194696913], [51.12239321211423, 9.61645605992352], [51.0634545325666, 9.57161203934019], [50.81449346507728, 9.470641609414168], [50.8902981044233, 9.493705194696913]],
{"bubblingMouseEvents": true, "color": "#b45309", "dashArray": null, "dashOffset": null, "fill": false, "fillColor": "#b45309", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "noClip": false, "opacity": 0.85, "smoothFactor": 1.0, "stroke": true, "weight": 3}
).addTo(map_01);
poly_line_05.bindTooltip(
`<div>
Fahrzeug 3: 70 km, 8/10 Einheiten
</div>`,
{
"sticky": true,
}
);
var poly_line_06 = L.polyline(
[[50.8902981044233, 9.493705194696913], [51.32909155311604, 8.916255495545233], [51.37139606353655, 8.517488901092882], [51.108758805458066, 8.607477788041049], [50.553365253206685, 9.181091101072626], [50.67063451777786, 9.498609802394965], [50.8902981044233, 9.493705194696913]],
{"bubblingMouseEvents": true, "color": "#059669", "dashArray": null, "dashOffset": null, "fill": false, "fillColor": "#059669", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "noClip": false, "opacity": 0.85, "smoothFactor": 1.0, "stroke": true, "weight": 3}
).addTo(map_01);
poly_line_06.bindTooltip(
`<div>
Fahrzeug 4: 242 km, 10/10 Einheiten
</div>`,
{
"sticky": true,
}
);
var circle_marker_07 = L.circleMarker(
[51.12239321211423, 9.61645605992352],
{"bubblingMouseEvents": true, "color": "#b45309", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#b45309", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 6.8, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_07.bindTooltip(
`<div>
K1 — Bedarf 2, Zeitfenster 30120 min<br>Fahrzeug 3, Stopp 1
</div>`,
{
"sticky": true,
}
);
var circle_marker_08 = L.circleMarker(
[51.37139606353655, 8.517488901092882],
{"bubblingMouseEvents": true, "color": "#059669", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#059669", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 8.2, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_08.bindTooltip(
`<div>
K2 — Bedarf 3, Zeitfenster 60180 min<br>Fahrzeug 4, Stopp 2
</div>`,
{
"sticky": true,
}
);
var circle_marker_09 = L.circleMarker(
[51.202571214486355, 9.47729598790725],
{"bubblingMouseEvents": true, "color": "#0891b2", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#0891b2", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 5.4, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_09.bindTooltip(
`<div>
K3 — Bedarf 1, Zeitfenster 100240 min<br>Fahrzeug 2, Stopp 1
</div>`,
{
"sticky": true,
}
);
var circle_marker_10 = L.circleMarker(
[50.899334211646234, 8.572820141079644],
{"bubblingMouseEvents": true, "color": "#4338ca", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#4338ca", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 9.6, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_10.bindTooltip(
`<div>
K4 — Bedarf 4, Zeitfenster 150300 min<br>Fahrzeug 1, Stopp 3
</div>`,
{
"sticky": true,
}
);
var circle_marker_11 = L.circleMarker(
[51.32909155311604, 8.916255495545233],
{"bubblingMouseEvents": true, "color": "#059669", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#059669", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 6.8, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_11.bindTooltip(
`<div>
K5 — Bedarf 2, Zeitfenster 60180 min<br>Fahrzeug 4, Stopp 1
</div>`,
{
"sticky": true,
}
);
var circle_marker_12 = L.circleMarker(
[51.23585035464649, 9.309049094203546],
{"bubblingMouseEvents": true, "color": "#0891b2", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#0891b2", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 6.8, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_12.bindTooltip(
`<div>
K6 — Bedarf 2, Zeitfenster 120240 min<br>Fahrzeug 2, Stopp 2
</div>`,
{
"sticky": true,
}
);
var circle_marker_13 = L.circleMarker(
[50.70020093721884, 9.023183616953922],
{"bubblingMouseEvents": true, "color": "#4338ca", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#4338ca", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 8.2, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_13.bindTooltip(
`<div>
K7 — Bedarf 3, Zeitfenster 200360 min<br>Fahrzeug 1, Stopp 2
</div>`,
{
"sticky": true,
}
);
var circle_marker_14 = L.circleMarker(
[50.553365253206685, 9.181091101072626],
{"bubblingMouseEvents": true, "color": "#059669", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#059669", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 5.4, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_14.bindTooltip(
`<div>
K8 — Bedarf 1, Zeitfenster 300450 min<br>Fahrzeug 4, Stopp 4
</div>`,
{
"sticky": true,
}
);
var circle_marker_15 = L.circleMarker(
[51.0634545325666, 9.57161203934019],
{"bubblingMouseEvents": true, "color": "#b45309", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#b45309", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 6.8, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_15.bindTooltip(
`<div>
K9 — Bedarf 2, Zeitfenster 180300 min<br>Fahrzeug 3, Stopp 2
</div>`,
{
"sticky": true,
}
);
var circle_marker_16 = L.circleMarker(
[50.81449346507728, 9.470641609414168],
{"bubblingMouseEvents": true, "color": "#b45309", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#b45309", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 9.6, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_16.bindTooltip(
`<div>
K10 — Bedarf 4, Zeitfenster 240360 min<br>Fahrzeug 3, Stopp 3
</div>`,
{
"sticky": true,
}
);
var circle_marker_17 = L.circleMarker(
[51.2977516796715, 9.779278159065392],
{"bubblingMouseEvents": true, "color": "#0891b2", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#0891b2", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 8.2, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_17.bindTooltip(
`<div>
K11 — Bedarf 3, Zeitfenster 300480 min<br>Fahrzeug 2, Stopp 4
</div>`,
{
"sticky": true,
}
);
var circle_marker_18 = L.circleMarker(
[50.67063451777786, 9.498609802394965],
{"bubblingMouseEvents": true, "color": "#059669", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#059669", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 6.8, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_18.bindTooltip(
`<div>
K12 — Bedarf 2, Zeitfenster 360500 min<br>Fahrzeug 4, Stopp 5
</div>`,
{
"sticky": true,
}
);
var circle_marker_19 = L.circleMarker(
[50.53528002580979, 9.056780383216372],
{"bubblingMouseEvents": true, "color": "#4338ca", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#4338ca", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 5.4, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_19.bindTooltip(
`<div>
K13 — Bedarf 1, Zeitfenster 60200 min<br>Fahrzeug 1, Stopp 1
</div>`,
{
"sticky": true,
}
);
var circle_marker_20 = L.circleMarker(
[51.108758805458066, 8.607477788041049],
{"bubblingMouseEvents": true, "color": "#059669", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#059669", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 6.8, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_20.bindTooltip(
`<div>
K14 — Bedarf 2, Zeitfenster 120300 min<br>Fahrzeug 4, Stopp 3
</div>`,
{
"sticky": true,
}
);
var circle_marker_21 = L.circleMarker(
[51.36580069667539, 9.454691406682949],
{"bubblingMouseEvents": true, "color": "#0891b2", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#0891b2", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 8.2, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_21.bindTooltip(
`<div>
K15 — Bedarf 3, Zeitfenster 240400 min<br>Fahrzeug 2, Stopp 3
</div>`,
{
"sticky": true,
}
);
var circle_marker_22 = L.circleMarker(
[50.828627944796516, 8.85527539693994],
{"bubblingMouseEvents": true, "color": "#4338ca", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#4338ca", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 6.8, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_22.bindTooltip(
`<div>
K16 — Bedarf 2, Zeitfenster 300550 min<br>Fahrzeug 1, Stopp 4
</div>`,
{
"sticky": true,
}
);
var circle_marker_23 = L.circleMarker(
[50.8902981044233, 9.493705194696913],
{"bubblingMouseEvents": true, "color": "#be123c", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#be123c", "fillOpacity": 0.9, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 9, "stroke": true, "weight": 3}
).addTo(map_01);
circle_marker_23.bindTooltip(
`<div>
Depot — Schicht 0600 min
</div>`,
{
"sticky": true,
}
);
</script>
</html>

View file

@ -13184,6 +13184,9 @@ Umschlag -&gt; Kunde_2 5.0 10 3.00 15.00
<img src="bilder_04/kap_graphen_vrp_touren.svg" alt="Abb. 8.2: Die Lösung der Instanz aus VRP_Flotten_Routing.py: 16 Kunden, vier Fahrzeuge, 619 km. Beachten Sie, dass sich die Touren kreuzen. Bei einem reinen Tourenproblem wäre das ein sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zeitfenster erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von bilder_04/erzeuge_vrp_touren.py." /> <img src="bilder_04/kap_graphen_vrp_touren.svg" alt="Abb. 8.2: Die Lösung der Instanz aus VRP_Flotten_Routing.py: 16 Kunden, vier Fahrzeuge, 619 km. Beachten Sie, dass sich die Touren kreuzen. Bei einem reinen Tourenproblem wäre das ein sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zeitfenster erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von bilder_04/erzeuge_vrp_touren.py." />
<figcaption aria-hidden="true">Abb. 8.2: Die Lösung der Instanz aus <code>VRP_Flotten_Routing.py</code>: 16 Kunden, vier Fahrzeuge, 619 km. <strong>Beachten Sie, dass sich die Touren kreuzen.</strong> Bei einem reinen Tourenproblem wäre das ein sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zeitfenster erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von <code>bilder_04/erzeuge_vrp_touren.py</code>.</figcaption> <figcaption aria-hidden="true">Abb. 8.2: Die Lösung der Instanz aus <code>VRP_Flotten_Routing.py</code>: 16 Kunden, vier Fahrzeuge, 619 km. <strong>Beachten Sie, dass sich die Touren kreuzen.</strong> Bei einem reinen Tourenproblem wäre das ein sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zeitfenster erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von <code>bilder_04/erzeuge_vrp_touren.py</code>.</figcaption>
</figure> </figure>
<p><em>Eine interaktive Karte dieser Touren steht auf den Kapitelseiten der Website bereit.</em></p>
<p><strong>Was Sie auf der Karte sehen — und was nicht.</strong> Die Instanz ist synthetisch: 17 zufällige Punkte auf einem Quadrat von 100 × 100 km. Für die Karte wurden sie <em>lagegetreu</em> auf eine reale Region gelegt — mit einer azimutal-äquidistanten Projektion, die alle 136 Abstände zwischen den Punkten bis auf <strong>0,001 %</strong> erhält. Die Karte zeigt also dieselbe Instanz, mit der das Modell rechnet, nur an geographischen Koordinaten statt auf einem Zahlenraster; kein Wert in diesem Kapitel ändert sich dadurch.</p>
<p>Die Touren sind aus demselben Grund <strong>Luftlinien und keine Straßenrouten</strong>: Das Modell kennt keine Straßen, es rechnet mit den Abständen der Distanzmatrix. Eine Linie, die einer echten Straße folgte, wäre hübscher und würde eine Genauigkeit vortäuschen, die das Modell nicht hat. Wer Straßenentfernungen braucht, ersetzt die Matrix — <a href="#sec:graphen-denkfehler">Abschnitt 8.7</a> sagt, was dabei zu beachten ist.</p>
<h3 id="kurzzyklen-verhindern">Kurzzyklen verhindern</h3> <h3 id="kurzzyklen-verhindern">Kurzzyklen verhindern</h3>
<p>Ein naives Modell erlaubt <strong>Subtouren</strong>: isolierte Kreise, die das Depot nie anfahren. Die klassische Gegenmaßnahme ist die <strong>MTZ-Formulierung</strong> nach Miller, Tucker und Zemlin. Man führt Rangvariablen <span class="math inline">u_i</span> ein (die Position des Knotens in der Tour):</p> <p>Ein naives Modell erlaubt <strong>Subtouren</strong>: isolierte Kreise, die das Depot nie anfahren. Die klassische Gegenmaßnahme ist die <strong>MTZ-Formulierung</strong> nach Miller, Tucker und Zemlin. Man führt Rangvariablen <span class="math inline">u_i</span> ein (die Position des Knotens in der Tour):</p>
<p><span class="math display"> <p><span class="math display">

View file

@ -579,6 +579,9 @@ Umschlag -&gt; Kunde_2 5.0 10 3.00 15.00
<img src="bilder_04/kap_graphen_vrp_touren.svg" alt="Abb. 8.2: Die Lösung der Instanz aus VRP_Flotten_Routing.py: 16 Kunden, vier Fahrzeuge, 619 km. Beachten Sie, dass sich die Touren kreuzen. Bei einem reinen Tourenproblem wäre das ein sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zeitfenster erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von bilder_04/erzeuge_vrp_touren.py." /> <img src="bilder_04/kap_graphen_vrp_touren.svg" alt="Abb. 8.2: Die Lösung der Instanz aus VRP_Flotten_Routing.py: 16 Kunden, vier Fahrzeuge, 619 km. Beachten Sie, dass sich die Touren kreuzen. Bei einem reinen Tourenproblem wäre das ein sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zeitfenster erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von bilder_04/erzeuge_vrp_touren.py." />
<figcaption aria-hidden="true">Abb. 8.2: Die Lösung der Instanz aus <code>VRP_Flotten_Routing.py</code>: 16 Kunden, vier Fahrzeuge, 619 km. <strong>Beachten Sie, dass sich die Touren kreuzen.</strong> Bei einem reinen Tourenproblem wäre das ein sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zeitfenster erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von <code>bilder_04/erzeuge_vrp_touren.py</code>.</figcaption> <figcaption aria-hidden="true">Abb. 8.2: Die Lösung der Instanz aus <code>VRP_Flotten_Routing.py</code>: 16 Kunden, vier Fahrzeuge, 619 km. <strong>Beachten Sie, dass sich die Touren kreuzen.</strong> Bei einem reinen Tourenproblem wäre das ein sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zeitfenster erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von <code>bilder_04/erzeuge_vrp_touren.py</code>.</figcaption>
</figure> </figure>
<iframe class="karte-figur" loading="lazy" src="bilder_04/karten/kap_graphen_vrp_touren.html" title="Interaktive Karte: kap_graphen_vrp_touren"></iframe>
<p><strong>Was Sie auf der Karte sehen — und was nicht.</strong> Die Instanz ist synthetisch: 17 zufällige Punkte auf einem Quadrat von 100 × 100 km. Für die Karte wurden sie <em>lagegetreu</em> auf eine reale Region gelegt — mit einer azimutal-äquidistanten Projektion, die alle 136 Abstände zwischen den Punkten bis auf <strong>0,001 %</strong> erhält. Die Karte zeigt also dieselbe Instanz, mit der das Modell rechnet, nur an geographischen Koordinaten statt auf einem Zahlenraster; kein Wert in diesem Kapitel ändert sich dadurch.</p>
<p>Die Touren sind aus demselben Grund <strong>Luftlinien und keine Straßenrouten</strong>: Das Modell kennt keine Straßen, es rechnet mit den Abständen der Distanzmatrix. Eine Linie, die einer echten Straße folgte, wäre hübscher und würde eine Genauigkeit vortäuschen, die das Modell nicht hat. Wer Straßenentfernungen braucht, ersetzt die Matrix — <a href="#sec:graphen-denkfehler">Abschnitt 8.7</a> sagt, was dabei zu beachten ist.</p>
<h3 id="kurzzyklen-verhindern">Kurzzyklen verhindern</h3> <h3 id="kurzzyklen-verhindern">Kurzzyklen verhindern</h3>
<p>Ein naives Modell erlaubt <strong>Subtouren</strong>: isolierte Kreise, die das Depot nie anfahren. Die klassische Gegenmaßnahme ist die <strong>MTZ-Formulierung</strong> nach Miller, Tucker und Zemlin. Man führt Rangvariablen <span class="math inline">u_i</span> ein (die Position des Knotens in der Tour):</p> <p>Ein naives Modell erlaubt <strong>Subtouren</strong>: isolierte Kreise, die das Depot nie anfahren. Die klassische Gegenmaßnahme ist die <strong>MTZ-Formulierung</strong> nach Miller, Tucker und Zemlin. Man führt Rangvariablen <span class="math inline">u_i</span> ein (die Position des Knotens in der Tour):</p>
<p><span class="math display"> <p><span class="math display">

View file

@ -9587,6 +9587,12 @@ Das **Traveling Salesperson Problem (TSP)**`\index{TSP (Traveling Salesperson Pr
![Abb. 8.2: Die Lösung der Instanz aus `VRP_Flotten_Routing.py`: 16 Kunden, vier Fahrzeuge, 619 km. ![Abb. 8.2: Die Lösung der Instanz aus `VRP_Flotten_Routing.py`: 16 Kunden, vier Fahrzeuge, 619 km.
**Beachten Sie, dass sich die Touren kreuzen.** Bei einem reinen Tourenproblem wäre das ein sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zeitfenster erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von `bilder_04/erzeuge_vrp_touren.py`.](bilder_04/kap_graphen_vrp_touren.svg) **Beachten Sie, dass sich die Touren kreuzen.** Bei einem reinen Tourenproblem wäre das ein sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zeitfenster erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von `bilder_04/erzeuge_vrp_touren.py`.](bilder_04/kap_graphen_vrp_touren.svg)
*Eine interaktive Karte dieser Touren steht auf den Kapitelseiten der Website bereit.*
**Was Sie auf der Karte sehen — und was nicht.** Die Instanz ist synthetisch: 17 zufällige Punkte auf einem Quadrat von 100 × 100 km. Für die Karte wurden sie *lagegetreu* auf eine reale Region gelegt — mit einer azimutal-äquidistanten Projektion, die alle 136 Abstände zwischen den Punkten bis auf **0,001 %** erhält. Die Karte zeigt also dieselbe Instanz, mit der das Modell rechnet, nur an geographischen Koordinaten statt auf einem Zahlenraster; kein Wert in diesem Kapitel ändert sich dadurch.
Die Touren sind aus demselben Grund **Luftlinien und keine Straßenrouten**: Das Modell kennt keine Straßen, es rechnet mit den Abständen der Distanzmatrix. Eine Linie, die einer echten Straße folgte, wäre hübscher und würde eine Genauigkeit vortäuschen, die das Modell nicht hat. Wer Straßenentfernungen braucht, ersetzt die Matrix — [Abschnitt 8.7](#sec:graphen-denkfehler) sagt, was dabei zu beachten ist.
### Kurzzyklen verhindern ### Kurzzyklen verhindern
Ein naives Modell erlaubt **Subtouren**`\index{Subtour}`{=latex}: isolierte Kreise, die das Depot nie anfahren. Die klassische Gegenmaßnahme ist die **MTZ-Formulierung**`\index{MTZ-Formulierung}`{=latex} nach Miller, Tucker und Zemlin. Man führt Rangvariablen $u_i$ ein (die Position des Knotens in der Tour): Ein naives Modell erlaubt **Subtouren**`\index{Subtour}`{=latex}: isolierte Kreise, die das Depot nie anfahren. Die klassische Gegenmaßnahme ist die **MTZ-Formulierung**`\index{MTZ-Formulierung}`{=latex} nach Miller, Tucker und Zemlin. Man führt Rangvariablen $u_i$ ein (die Position des Knotens in der Tour):

View file

@ -493,6 +493,21 @@ sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zei
erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von
`bilder_04/erzeuge_vrp_touren.py`.](bilder_04/kap_graphen_vrp_touren.svg) `bilder_04/erzeuge_vrp_touren.py`.](bilder_04/kap_graphen_vrp_touren.svg)
{karte:kap_graphen_vrp_touren}
**Was Sie auf der Karte sehen — und was nicht.** Die Instanz ist synthetisch: 17 zufällige
Punkte auf einem Quadrat von 100 × 100 km. Für die Karte wurden sie *lagegetreu* auf eine
reale Region gelegt — mit einer azimutal-äquidistanten Projektion, die alle 136 Abstände
zwischen den Punkten bis auf **0,001 %** erhält. Die Karte zeigt also dieselbe Instanz, mit
der das Modell rechnet, nur an geographischen Koordinaten statt auf einem Zahlenraster; kein
Wert in diesem Kapitel ändert sich dadurch.
Die Touren sind aus demselben Grund **Luftlinien und keine Straßenrouten**: Das Modell kennt
keine Straßen, es rechnet mit den Abständen der Distanzmatrix. Eine Linie, die einer echten
Straße folgte, wäre hübscher und würde eine Genauigkeit vortäuschen, die das Modell nicht
hat. Wer Straßenentfernungen braucht, ersetzt die Matrix — {ref:sec:graphen-denkfehler} sagt,
was dabei zu beachten ist.
### Kurzzyklen verhindern ### Kurzzyklen verhindern
Ein naives Modell erlaubt **Subtouren**{idx:Subtour}: isolierte Kreise, die das Depot nie anfahren. Die Ein naives Modell erlaubt **Subtouren**{idx:Subtour}: isolierte Kreise, die das Depot nie anfahren. Die

View file

@ -390,6 +390,23 @@ def pruefe_dateien() -> list[str]:
f"folgt aber '{soll}' - die Lesekette ueberspringt eine Datei.") f"folgt aber '{soll}' - die Lesekette ueberspringt eine Datei.")
fehlend.append(name) fehlend.append(name)
# Marken fuer interaktive Figuren: Zeigt eine ins Leere, verschwindet sie
# auf der Website stillschweigend - im PDF steht dann ein Verweis auf eine
# Figur, die es nicht gibt.
for name in DATEIEN:
with open(os.path.join(HIER, name), encoding="utf-8") as f:
inhalt = f.read()
for marke, verzeichnis in ((PLOTLY_RE, PLOTLY_VERZ),
(KARTE_RE, KARTEN_VERZ)):
for treffer in marke.finditer(inhalt):
pfad = os.path.join(verzeichnis, f"{treffer.group(1)}.html")
if not os.path.exists(pfad):
zeile = inhalt[:treffer.start()].count("\n") + 1
print(f"FEHLER: {name}:{zeile}: '{treffer.group(0).strip()}' "
f"verweist auf eine nicht vorhandene Datei "
f"({os.path.relpath(pfad, BASIS)}).")
fehlend.append(f"{name}:{zeile}")
fehlend.extend(pruefe_assets()) fehlend.extend(pruefe_assets())
fehlend.extend(pruefe_titeltexte()) fehlend.extend(pruefe_titeltexte())
return fehlend return fehlend
@ -1088,6 +1105,7 @@ def baue_markdown() -> str:
gesamt = "\n\n---\n\n".join(teile) + "\n" + _STICHWORTREGISTER_BLOCK gesamt = "\n\n---\n\n".join(teile) + "\n" + _STICHWORTREGISTER_BLOCK
gesamt, _ = resolve_plotly(gesamt, fuer_html=False) gesamt, _ = resolve_plotly(gesamt, fuer_html=False)
gesamt = resolve_karte(gesamt, fuer_html=False)
return resolve_index(resolve_numbering(reflow_markdown(gesamt))) return resolve_index(resolve_numbering(reflow_markdown(gesamt)))
@ -1865,6 +1883,45 @@ PLOTLY_RE = re.compile(r"^\{plotly:([\w-]+)\}[ \t]*$", re.MULTILINE)
PLOTLY_VERZ = os.path.join(BILDER_VERZ, "plotly") PLOTLY_VERZ = os.path.join(BILDER_VERZ, "plotly")
# Dasselbe fuer Leaflet-Karten:
#
# {karte:kap_graphen_vrp_touren}
#
# Anders als die Plotly-Fragmente wird eine Karte NICHT eingebettet, sondern in
# einem <iframe> gezeigt. Zwei Gruende: Leaflets Stylesheet ist umfangreich und
# griffe sonst in die Buchseite hinein, und Foliums Ausgabe ist ein
# vollstaendiges HTML-Dokument, das sich so unveraendert uebernehmen laesst.
# Erzeugt werden die Karten von den bilder_04/erzeuge_*.py, die auch das
# zugehoerige SVG schreiben - aus demselben Lauf, damit beide Darstellungen
# dieselbe Loesung zeigen.
KARTE_RE = re.compile(r"^\{karte:([\w-]+)\}[ \t]*$", re.MULTILINE)
KARTEN_NAME = "karten"
KARTEN_VERZ = os.path.join(BILDER_VERZ, KARTEN_NAME)
def resolve_karte(text: str, fuer_html: bool) -> str:
"""Loest {karte:name}-Marken auf."""
def ersetze(m: re.Match) -> str:
name = m.group(1)
if not fuer_html:
# Wortlaut wie bei resolve_plotly(): Dieser Zweig bedient das PDF
# UND gesamtdokument.html - wer Letzteres liest, ist schon auf der
# Website, "auf der Website" waere dort also falsch.
return ("*Eine interaktive Karte dieser Touren steht auf den "
"Kapitelseiten der Website bereit.*")
pfad = os.path.join(KARTEN_VERZ, f"{name}.html")
if not os.path.exists(pfad):
print(f"WARNUNG: Karte fehlt: {pfad} - Marke entfernt.")
return ""
return ('```{=html}\n'
f'<iframe class="karte-figur" loading="lazy" '
f'src="{BILDER_NAME}/{KARTEN_NAME}/{name}.html" '
f'title="Interaktive Karte: {name}"></iframe>\n'
'```')
return KARTE_RE.sub(ersetze, text)
def resolve_plotly(text: str, fuer_html: bool) -> tuple[str, bool]: def resolve_plotly(text: str, fuer_html: bool) -> tuple[str, bool]:
"""Loest {plotly:name}-Marken auf. Liefert (Text, ob eine Figur eingebettet """Loest {plotly:name}-Marken auf. Liefert (Text, ob eine Figur eingebettet
@ -2138,6 +2195,7 @@ def baue_kapitel_seiten(html_verz: str, datei_seite: dict, labels: dict,
text = resolve_numbering_seite(text, eintrag["seite"], labels, label_seite) text = resolve_numbering_seite(text, eintrag["seite"], labels, label_seite)
text = resolve_index(text) text = resolve_index(text)
text, mit_plotly = resolve_plotly(text, fuer_html=True) text, mit_plotly = resolve_plotly(text, fuer_html=True)
text = resolve_karte(text, fuer_html=True)
text = markiere_karten(text) text = markiere_karten(text)
fragment = _pandoc_fragment(text, html_verz) fragment = _pandoc_fragment(text, html_verz)

View file

@ -1376,11 +1376,11 @@ Teilhistorie, die vollständig aussieht und es nicht ist. Volle Historie ginge n
7.1 leisten dasselbe gezielter, ohne Eingriff in jeden Abschnitt. 7.1 leisten dasselbe gezielter, ohne Eingriff in jeden Abschnitt.
* **`joblib`/`ray`** (V02/2) — `num_workers` und `ProcessPoolExecutor` decken den Bedarf; * **`joblib`/`ray`** (V02/2) — `num_workers` und `ProcessPoolExecutor` decken den Bedarf;
ein Cluster-Framework wäre eine Abhängigkeit ohne Beispiel, das sie braucht. ein Cluster-Framework wäre eine Abhängigkeit ohne Beispiel, das sie braucht.
* **Folium-Karten** (V03/3) — **blockiert durch eine offene Entscheidung**: Die VRP-Instanz * **Executive-PDF-Report** (V03/6) — ohne Streamlit-Apps fehlt der Ort, an dem er
ist ein synthetisches 100 × 100-km-Raster ohne Geokoordinaten. Eine echte Karte verlangt entstünde.
entweder eine neue Instanz (und damit neue abgedruckte Zahlen) oder eine willkürliche
Verankerung an einem realen Ort. **Folium-Karten** (V03/3) standen hier lange als **blockiert** — sie sind es nicht mehr,
* **Executive-PDF-Report** (V03/6) — ohne Streamlit-Apps fehlt der Ort, an dem er entstünde. siehe den Eintrag im nächsten Abschnitt.
### Offen, rein redaktionell ### Offen, rein redaktionell
@ -2258,6 +2258,51 @@ hat ihren Zweck erfüllt — aber einen Rückschritt könnte sie gar nicht mehr
Datei ist gelöscht, die Probe bliebe auch dann grün, wenn jemand `--delete` aus dem Datei ist gelöscht, die Probe bliebe auch dann grün, wenn jemand `--delete` aus dem
`rsync`-Aufruf nähme. Von den ursprünglich vierzehn Adressen bleiben dreizehn. `rsync`-Aufruf nähme. Von den ursprünglich vierzehn Adressen bleiben dreizehn.
### ✅ Folium-Karten — die Blockade war eine falsche Alternative
Der einzige Punkt, der in diesem Projekt je als *blockiert* geführt wurde, ist erledigt. Die
Begründung lautete: Die VRP-Instanz sei ein synthetisches 100 × 100-km-Raster ohne
Geokoordinaten, eine echte Karte verlange also **entweder** eine neue Instanz (und damit
zwei Dutzend neue abgedruckte Zahlen) **oder** eine willkürliche Verankerung.
**Es gibt einen dritten Weg, und er ist exakt.** Eine azimutal-äquidistante Projektion um den
Schwerpunkt des Rasters bildet die 17 Punkte so auf Länge und Breite ab, dass die
Großkreisdistanzen mit den Rasterdistanzen übereinstimmen:
| Verfahren | Median | Maximum |
| --- | --- | --- |
| festes `cos(Breite)` | 0,360 % | 2,003 % |
| `cos` der eigenen Breite | 0,351 % | 0,908 % |
| **azimutal-äquidistant** | **0,000 %** | **0,001 %** |
Ein Meter auf hundert Kilometer. Damit ist die Karte keine Näherung, für die man sich
entschuldigt, sondern eine zweite Darstellung derselben Instanz — **kein abgedruckter Wert
ändert sich**, die Gesamtstrecke bleibt bei 619 km, das SVG byte-identisch.
Zwei Befunde stützten die Entscheidung: „100 × 100 km Raster" steht **nur in zwei
Codekommentaren**, nirgends im Fließtext; und der didaktische Punkt der Bildunterschrift —
die sich kreuzenden Touren, die *kein* Fehler sind — wird auf einer Karte deutlicher.
**Umgesetzt** in `erzeuge_vrp_touren.py`, das jetzt SVG **und** Karte aus einem Lauf schreibt.
Ein zweites Skript hätte erneut gelöst, und die Routing-Bibliothek arbeitet heuristisch.
Neu im Build: die Marke `{karte:name}`, aufgelöst zu einem `<iframe>` (Leaflets Stylesheet
soll nicht in die Buchseite greifen), im PDF zum Verweissatz — wie bei `{plotly:}`. `--check`
bewacht jetzt **beide** Markenarten auf Existenz der Datei.
**Zwei Vorkehrungen, die ohne Messung nicht aufgefallen wären:**
* Folium vergibt jedem Element eine `uuid4`**jeder Lauf erzeugte eine andere Datei**.
`normalisiere()` ersetzt sie durch fortlaufende Nummern; drei Läufe liefern jetzt dieselbe
Prüfsumme.
* Folium bindet **zehn** CDN-Ressourcen ein (jQuery, Bootstrap, Glyphicons, FontAwesome,
awesome-markers …). Gebraucht werden **zwei**. Sie liegen jetzt in `web_04/assets/`, die
übrigen acht entfallen. Nachgeprüft: Die Karte benutzt ausschließlich `L.*`-Aufrufe —
17 × `circleMarker`, 4 × `polyline`, je einmal `map`, `tileLayer`, `control`, `CRS`. Ohne
Netz fehlen nur die Kacheln.
Gegenrechnung an vier Strecken: `Depot → K1` misst auf der Karte 27,20 km, die Distanzmatrix
sagt 27 — die Differenz stammt aus deren `int()`, nicht aus der Geographie.
--- ---
## 8. Commit-Historie des V04-Strangs ## 8. Commit-Historie des V04-Strangs

View file

@ -30,6 +30,7 @@ Benoetigt: numpy, matplotlib, ortools
from __future__ import annotations from __future__ import annotations
import os import os
import re
import matplotlib import matplotlib
matplotlib.use("Agg") matplotlib.use("Agg")
@ -178,6 +179,164 @@ def zeichne(koordinaten, touren) -> None:
plt.close(figur) plt.close(figur)
# --- Karte ------------------------------------------------------------------
#
# Dieselbe Loesung ein zweites Mal, diesmal auf einem echten Kartenhintergrund.
# Das war lange als "blockiert" vermerkt, weil die Instanz ein synthetisches
# 100 x 100-km-Raster ohne Geokoordinaten ist: Eine Karte schien entweder eine
# neue Instanz zu verlangen (und damit zwei Dutzend neue abgedruckte Zahlen)
# oder eine willkuerliche Verankerung.
#
# Das war eine falsche Alternative. Die azimutal-aequidistante Projektion um
# den Schwerpunkt des Rasters bildet die Punkte SO auf Laenge und Breite ab,
# dass die Grosskreisdistanzen mit den Rasterdistanzen uebereinstimmen - ueber
# alle 136 Punktpaare bis auf 0,001 %. Das Modell bleibt unberuehrt, kein
# abgedruckter Wert aendert sich, und die Karte zeigt trotzdem echte Lagen.
# pruefe_projektion() unten weist das bei jedem Lauf nach.
ERDRADIUS_KM = 6371.0088
ANKER_LAT, ANKER_LON = 51.00, 9.20 # Raum Kassel; zwei Zahlen genuegen,
# um die Instanz woanders hinzulegen
KARTEN = os.path.join(BILDER, "karten")
KARTENNAME = "kap_graphen_vrp_touren"
def projiziere(koordinaten):
"""Rasterkoordinaten (km) -> (Breite, Laenge), azimutal-aequidistant um den
Schwerpunkt. Formel: Zielpunkt aus Startpunkt, Azimut und Bogendistanz."""
mitte = koordinaten.mean(axis=0)
dx, dy = (koordinaten - mitte).T
bogen = np.hypot(dx, dy) / ERDRADIUS_KM # Winkeldistanz im Bogenmass
azimut = np.arctan2(dx, dy) # 0 = Nord, im Uhrzeigersinn
phi0 = np.radians(ANKER_LAT)
breite = np.arcsin(np.sin(phi0) * np.cos(bogen)
+ np.cos(phi0) * np.sin(bogen) * np.cos(azimut))
laenge = np.radians(ANKER_LON) + np.arctan2(
np.sin(azimut) * np.sin(bogen) * np.cos(phi0),
np.cos(bogen) - np.sin(phi0) * np.sin(breite))
return np.degrees(breite), np.degrees(laenge)
def pruefe_projektion(koordinaten, breite, laenge) -> float:
"""Vergleicht jede Grosskreisdistanz mit der Rasterdistanz und bricht ab,
wenn die Karte die Instanz verzerrt darstellen wuerde. Die Zahl steht im
Buch - sie wird hier bewiesen, nicht behauptet."""
def grosskreis(i, j):
p1, p2 = np.radians(breite[i]), np.radians(breite[j])
dl = np.radians(laenge[j] - laenge[i])
h = (np.sin((p2 - p1) / 2) ** 2
+ np.cos(p1) * np.cos(p2) * np.sin(dl / 2) ** 2)
return 2 * ERDRADIUS_KM * np.arcsin(np.sqrt(h))
schlimmster = 0.0
for i in range(len(koordinaten)):
for j in range(i + 1, len(koordinaten)):
raster = float(np.linalg.norm(koordinaten[i] - koordinaten[j]))
if raster > 0:
schlimmster = max(schlimmster,
abs(grosskreis(i, j) - raster) / raster * 100)
if schlimmster > 0.01:
raise SystemExit(
f"Karte: Die Projektion verzerrt die Instanz um bis zu "
f"{schlimmster:.3f} % - erwartet sind hoechstens 0,01 %. "
f"Anker oder Projektion pruefen.")
return schlimmster
# Von Foliums zehn CDN-Einbindungen braucht diese Karte genau zwei: leaflet.js
# und leaflet.css. jQuery, Bootstrap, Glyphicons, FontAwesome und
# awesome-markers gehoeren zu Foliums Standardmarkern - hier kommen nur
# CircleMarker, PolyLine und Tooltip vor, die reines Leaflet sind.
LEAFLET_JS = "https://cdn.jsdelivr.net/npm/leaflet@1.9.3/dist/leaflet.js"
LEAFLET_CSS = "https://cdn.jsdelivr.net/npm/leaflet@1.9.3/dist/leaflet.css"
EINBINDUNG_RE = re.compile(
r'[ \t]*<(?:script src|link rel="stylesheet" href)="(https?://[^"]+)"'
r'[^>]*>(?:</script>)?\n?')
KENNUNG_RE = re.compile(r"\b([a-z_]+)_([0-9a-f]{32})\b")
def normalisiere(html: str) -> str:
"""Macht Foliums Ausgabe byteweise reproduzierbar und netzunabhaengig.
Zwei Eingriffe:
1. Folium vergibt jedem Element eine uuid4 - jeder Lauf erzeugte sonst eine
andere Datei und einen unlesbaren git-diff. Dieselbe Sorgfalt wie
svg.hashsalt bei den matplotlib-Bildern.
2. Die beiden gebrauchten Leaflet-Dateien werden lokal eingebunden (sie
liegen in web_04/assets/ und landen beim Bau in OR_HTML_04/assets/),
die uebrigen acht Einbindungen entfallen. Ohne Netz fehlen dann nur die
Kartenkacheln - Marker und Touren bleiben sichtbar."""
reihenfolge: dict[str, str] = {}
def kennung(m: re.Match) -> str:
hexwert = m.group(2)
if hexwert not in reihenfolge:
reihenfolge[hexwert] = f"{len(reihenfolge) + 1:02d}"
return f"{m.group(1)}_{reihenfolge[hexwert]}"
html = KENNUNG_RE.sub(kennung, html)
def einbindung(m: re.Match) -> str:
url = m.group(1)
if url == LEAFLET_JS:
return ' <script src="../../assets/leaflet.js"></script>\n'
if url == LEAFLET_CSS:
return (' <link rel="stylesheet" '
'href="../../assets/leaflet.css"/>\n')
return ""
return EINBINDUNG_RE.sub(einbindung, html)
def zeichne_karte(koordinaten, touren, abweichung: float) -> None:
import folium
breite, laenge = projiziere(koordinaten)
karte = folium.Map(location=[float(breite.mean()), float(laenge.mean())],
zoom_start=9, tiles="OpenStreetMap",
control_scale=True)
for nummer, (tour, farbe) in enumerate(zip(touren, TOURFARBEN), start=1):
weg = [[float(breite[s]), float(laenge[s])] for s in tour["stationen"]]
folium.PolyLine(
weg, color=farbe, weight=3, opacity=0.85,
tooltip=(f"Fahrzeug {nummer}: {tour['strecke']} km, "
f"{tour['fracht']}/{tour['kapazitaet']} Einheiten")
).add_to(karte)
# Besuchsreihenfolge je Kunde - dieselbe Information, die im SVG als
# Linienzug steckt, hier als Zahl am Punkt.
reihenfolge = {}
for nummer, tour in enumerate(touren, start=1):
for stelle, ort in enumerate(tour["stationen"][1:-1], start=1):
reihenfolge[ort] = (nummer, stelle)
for ort in range(1, len(koordinaten)):
fahrzeug, stelle = reihenfolge.get(ort, (0, 0))
fenster = ZEITFENSTER[ort]
folium.CircleMarker(
[float(breite[ort]), float(laenge[ort])],
radius=4 + 1.4 * BEDARFE[ort],
color=TOURFARBEN[fahrzeug - 1] if fahrzeug else "#334155",
fill=True, fill_opacity=0.75, weight=2,
tooltip=(f"K{ort} — Bedarf {BEDARFE[ort]}, "
f"Zeitfenster {fenster[0]}{fenster[1]} min<br>"
f"Fahrzeug {fahrzeug}, Stopp {stelle}"),
).add_to(karte)
folium.CircleMarker(
[float(breite[0]), float(laenge[0])], radius=9, color="#be123c",
fill=True, fill_opacity=0.9, weight=3,
tooltip=f"Depot — Schicht 0{SCHICHTLAENGE} min",
).add_to(karte)
os.makedirs(KARTEN, exist_ok=True)
pfad = os.path.join(KARTEN, f"{KARTENNAME}.html")
with open(pfad, "w", encoding="utf-8") as datei:
datei.write(normalisiere(karte.get_root().render()))
print(f"geschrieben: {pfad} "
f"(Projektionsabweichung hoechstens {abweichung:.4f} %)")
if __name__ == "__main__": if __name__ == "__main__":
koordinaten, distanz = erzeuge_daten() koordinaten, distanz = erzeuge_daten()
touren = loese(distanz) touren = loese(distanz)
@ -189,3 +348,6 @@ if __name__ == "__main__":
f"{tour['fracht']}/{tour['kapazitaet']} | {stationen}") f"{tour['fracht']}/{tour['kapazitaet']} | {stationen}")
print(f"Gesamtstrecke: {gesamt} km") print(f"Gesamtstrecke: {gesamt} km")
zeichne(koordinaten, touren) zeichne(koordinaten, touren)
breite, laenge = projiziere(koordinaten)
zeichne_karte(koordinaten, touren,
pruefe_projektion(koordinaten, breite, laenge))

View file

@ -0,0 +1,430 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<script src="../../assets/leaflet.js"></script>
<link rel="stylesheet" href="../../assets/leaflet.css"/>
<meta name="viewport" content="width=device-width,
initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<style>
#map_01 {
position: relative;
width: 100.0%;
height: 100.0%;
left: 0.0%;
top: 0.0%;
}
.leaflet-container { font-size: 1rem; }
</style>
<style>html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
</style>
<style>#map {
position:absolute;
top:0;
bottom:0;
right:0;
left:0;
}
</style>
<script>
L_NO_TOUCH = false;
L_DISABLE_3D = false;
</script>
</head>
<body>
<div class="folium-map" id="map_01" ></div>
</body>
<script>
var map_01 = L.map(
"map_01",
{
center: [50.99937073954281, 9.200100716327679],
crs: L.CRS.EPSG3857,
...{
"zoom": 9,
"zoomControl": true,
"preferCanvas": false,
}
}
);
L.control.scale().addTo(map_01);
var tile_layer_02 = L.tileLayer(
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
{
"minZoom": 0,
"maxZoom": 19,
"maxNativeZoom": 19,
"noWrap": false,
"attribution": "\u0026copy; \u003ca href=\"https://www.openstreetmap.org/copyright\"\u003eOpenStreetMap\u003c/a\u003e contributors",
"subdomains": "abc",
"detectRetina": false,
"tms": false,
"opacity": 1,
}
);
tile_layer_02.addTo(map_01);
var poly_line_03 = L.polyline(
[[50.8902981044233, 9.493705194696913], [50.53528002580979, 9.056780383216372], [50.70020093721884, 9.023183616953922], [50.899334211646234, 8.572820141079644], [50.828627944796516, 8.85527539693994], [50.8902981044233, 9.493705194696913]],
{"bubblingMouseEvents": true, "color": "#4338ca", "dashArray": null, "dashOffset": null, "fill": false, "fillColor": "#4338ca", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "noClip": false, "opacity": 0.85, "smoothFactor": 1.0, "stroke": true, "weight": 3}
).addTo(map_01);
poly_line_03.bindTooltip(
`<div>
Fahrzeug 1: 172 km, 10/10 Einheiten
</div>`,
{
"sticky": true,
}
);
var poly_line_04 = L.polyline(
[[50.8902981044233, 9.493705194696913], [51.202571214486355, 9.47729598790725], [51.23585035464649, 9.309049094203546], [51.36580069667539, 9.454691406682949], [51.2977516796715, 9.779278159065392], [50.8902981044233, 9.493705194696913]],
{"bubblingMouseEvents": true, "color": "#0891b2", "dashArray": null, "dashOffset": null, "fill": false, "fillColor": "#0891b2", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "noClip": false, "opacity": 0.85, "smoothFactor": 1.0, "stroke": true, "weight": 3}
).addTo(map_01);
poly_line_04.bindTooltip(
`<div>
Fahrzeug 2: 135 km, 9/10 Einheiten
</div>`,
{
"sticky": true,
}
);
var poly_line_05 = L.polyline(
[[50.8902981044233, 9.493705194696913], [51.12239321211423, 9.61645605992352], [51.0634545325666, 9.57161203934019], [50.81449346507728, 9.470641609414168], [50.8902981044233, 9.493705194696913]],
{"bubblingMouseEvents": true, "color": "#b45309", "dashArray": null, "dashOffset": null, "fill": false, "fillColor": "#b45309", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "noClip": false, "opacity": 0.85, "smoothFactor": 1.0, "stroke": true, "weight": 3}
).addTo(map_01);
poly_line_05.bindTooltip(
`<div>
Fahrzeug 3: 70 km, 8/10 Einheiten
</div>`,
{
"sticky": true,
}
);
var poly_line_06 = L.polyline(
[[50.8902981044233, 9.493705194696913], [51.32909155311604, 8.916255495545233], [51.37139606353655, 8.517488901092882], [51.108758805458066, 8.607477788041049], [50.553365253206685, 9.181091101072626], [50.67063451777786, 9.498609802394965], [50.8902981044233, 9.493705194696913]],
{"bubblingMouseEvents": true, "color": "#059669", "dashArray": null, "dashOffset": null, "fill": false, "fillColor": "#059669", "fillOpacity": 0.2, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "noClip": false, "opacity": 0.85, "smoothFactor": 1.0, "stroke": true, "weight": 3}
).addTo(map_01);
poly_line_06.bindTooltip(
`<div>
Fahrzeug 4: 242 km, 10/10 Einheiten
</div>`,
{
"sticky": true,
}
);
var circle_marker_07 = L.circleMarker(
[51.12239321211423, 9.61645605992352],
{"bubblingMouseEvents": true, "color": "#b45309", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#b45309", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 6.8, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_07.bindTooltip(
`<div>
K1 — Bedarf 2, Zeitfenster 30120 min<br>Fahrzeug 3, Stopp 1
</div>`,
{
"sticky": true,
}
);
var circle_marker_08 = L.circleMarker(
[51.37139606353655, 8.517488901092882],
{"bubblingMouseEvents": true, "color": "#059669", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#059669", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 8.2, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_08.bindTooltip(
`<div>
K2 — Bedarf 3, Zeitfenster 60180 min<br>Fahrzeug 4, Stopp 2
</div>`,
{
"sticky": true,
}
);
var circle_marker_09 = L.circleMarker(
[51.202571214486355, 9.47729598790725],
{"bubblingMouseEvents": true, "color": "#0891b2", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#0891b2", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 5.4, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_09.bindTooltip(
`<div>
K3 — Bedarf 1, Zeitfenster 100240 min<br>Fahrzeug 2, Stopp 1
</div>`,
{
"sticky": true,
}
);
var circle_marker_10 = L.circleMarker(
[50.899334211646234, 8.572820141079644],
{"bubblingMouseEvents": true, "color": "#4338ca", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#4338ca", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 9.6, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_10.bindTooltip(
`<div>
K4 — Bedarf 4, Zeitfenster 150300 min<br>Fahrzeug 1, Stopp 3
</div>`,
{
"sticky": true,
}
);
var circle_marker_11 = L.circleMarker(
[51.32909155311604, 8.916255495545233],
{"bubblingMouseEvents": true, "color": "#059669", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#059669", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 6.8, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_11.bindTooltip(
`<div>
K5 — Bedarf 2, Zeitfenster 60180 min<br>Fahrzeug 4, Stopp 1
</div>`,
{
"sticky": true,
}
);
var circle_marker_12 = L.circleMarker(
[51.23585035464649, 9.309049094203546],
{"bubblingMouseEvents": true, "color": "#0891b2", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#0891b2", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 6.8, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_12.bindTooltip(
`<div>
K6 — Bedarf 2, Zeitfenster 120240 min<br>Fahrzeug 2, Stopp 2
</div>`,
{
"sticky": true,
}
);
var circle_marker_13 = L.circleMarker(
[50.70020093721884, 9.023183616953922],
{"bubblingMouseEvents": true, "color": "#4338ca", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#4338ca", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 8.2, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_13.bindTooltip(
`<div>
K7 — Bedarf 3, Zeitfenster 200360 min<br>Fahrzeug 1, Stopp 2
</div>`,
{
"sticky": true,
}
);
var circle_marker_14 = L.circleMarker(
[50.553365253206685, 9.181091101072626],
{"bubblingMouseEvents": true, "color": "#059669", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#059669", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 5.4, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_14.bindTooltip(
`<div>
K8 — Bedarf 1, Zeitfenster 300450 min<br>Fahrzeug 4, Stopp 4
</div>`,
{
"sticky": true,
}
);
var circle_marker_15 = L.circleMarker(
[51.0634545325666, 9.57161203934019],
{"bubblingMouseEvents": true, "color": "#b45309", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#b45309", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 6.8, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_15.bindTooltip(
`<div>
K9 — Bedarf 2, Zeitfenster 180300 min<br>Fahrzeug 3, Stopp 2
</div>`,
{
"sticky": true,
}
);
var circle_marker_16 = L.circleMarker(
[50.81449346507728, 9.470641609414168],
{"bubblingMouseEvents": true, "color": "#b45309", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#b45309", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 9.6, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_16.bindTooltip(
`<div>
K10 — Bedarf 4, Zeitfenster 240360 min<br>Fahrzeug 3, Stopp 3
</div>`,
{
"sticky": true,
}
);
var circle_marker_17 = L.circleMarker(
[51.2977516796715, 9.779278159065392],
{"bubblingMouseEvents": true, "color": "#0891b2", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#0891b2", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 8.2, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_17.bindTooltip(
`<div>
K11 — Bedarf 3, Zeitfenster 300480 min<br>Fahrzeug 2, Stopp 4
</div>`,
{
"sticky": true,
}
);
var circle_marker_18 = L.circleMarker(
[50.67063451777786, 9.498609802394965],
{"bubblingMouseEvents": true, "color": "#059669", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#059669", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 6.8, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_18.bindTooltip(
`<div>
K12 — Bedarf 2, Zeitfenster 360500 min<br>Fahrzeug 4, Stopp 5
</div>`,
{
"sticky": true,
}
);
var circle_marker_19 = L.circleMarker(
[50.53528002580979, 9.056780383216372],
{"bubblingMouseEvents": true, "color": "#4338ca", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#4338ca", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 5.4, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_19.bindTooltip(
`<div>
K13 — Bedarf 1, Zeitfenster 60200 min<br>Fahrzeug 1, Stopp 1
</div>`,
{
"sticky": true,
}
);
var circle_marker_20 = L.circleMarker(
[51.108758805458066, 8.607477788041049],
{"bubblingMouseEvents": true, "color": "#059669", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#059669", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 6.8, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_20.bindTooltip(
`<div>
K14 — Bedarf 2, Zeitfenster 120300 min<br>Fahrzeug 4, Stopp 3
</div>`,
{
"sticky": true,
}
);
var circle_marker_21 = L.circleMarker(
[51.36580069667539, 9.454691406682949],
{"bubblingMouseEvents": true, "color": "#0891b2", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#0891b2", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 8.2, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_21.bindTooltip(
`<div>
K15 — Bedarf 3, Zeitfenster 240400 min<br>Fahrzeug 2, Stopp 3
</div>`,
{
"sticky": true,
}
);
var circle_marker_22 = L.circleMarker(
[50.828627944796516, 8.85527539693994],
{"bubblingMouseEvents": true, "color": "#4338ca", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#4338ca", "fillOpacity": 0.75, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 6.8, "stroke": true, "weight": 2}
).addTo(map_01);
circle_marker_22.bindTooltip(
`<div>
K16 — Bedarf 2, Zeitfenster 300550 min<br>Fahrzeug 1, Stopp 4
</div>`,
{
"sticky": true,
}
);
var circle_marker_23 = L.circleMarker(
[50.8902981044233, 9.493705194696913],
{"bubblingMouseEvents": true, "color": "#be123c", "dashArray": null, "dashOffset": null, "fill": true, "fillColor": "#be123c", "fillOpacity": 0.9, "fillRule": "evenodd", "lineCap": "round", "lineJoin": "round", "opacity": 1.0, "radius": 9, "stroke": true, "weight": 3}
).addTo(map_01);
circle_marker_23.bindTooltip(
`<div>
Depot — Schicht 0600 min
</div>`,
{
"sticky": true,
}
);
</script>
</html>

View file

@ -85,6 +85,7 @@ api = [
figures = [ figures = [
"plotly>=6.0", "plotly>=6.0",
"graphviz>=0.20", "graphviz>=0.20",
"folium>=0.19", # die Leaflet-Karte der VRP-Touren
] ]
dev = [ dev = [

656
web_04/assets/leaflet.css Normal file
View file

@ -0,0 +1,656 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

6
web_04/assets/leaflet.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -282,6 +282,13 @@ article li { margin: .3em 0; }
border-radius: var(--radius); padding: .6em; overflow-x: auto; } border-radius: var(--radius); padding: .6em; overflow-x: auto; }
.plotly-figur .plotly-graph-div { min-width: 320px; } .plotly-figur .plotly-graph-div { min-width: 320px; }
/* Leaflet-Karte im iframe ({karte:...}). Der Rahmen kommt von hier, nicht vom
iframe selbst - dessen Standardrahmen passt zu keinem der beiden Themen. */
.karte-figur { display: block; width: 100%; height: 30rem; margin: 1.4em 0;
border: 1px solid var(--border); border-radius: var(--radius);
background: var(--surface); }
@media (max-width: 640px) { .karte-figur { height: 22rem; } }
/* Der Formel-Uebersetzer ist zweispaltig gedacht: links die Mathematik, /* Der Formel-Uebersetzer ist zweispaltig gedacht: links die Mathematik,
rechts der Klartext. Auf schmalen Displays wuerde die Formelspalte die rechts der Klartext. Auf schmalen Displays wuerde die Formelspalte die
Tabelle sonst aus dem Viewport druecken. */ Tabelle sonst aus dem Viewport druecken. */