Compare commits

..

No commits in common. "e6486d0fe10d1f843e56afd1149de726fd6e9804" and "943db56009a6aeeec34d697475a848c2860ba07d" have entirely different histories.

11 changed files with 711 additions and 544 deletions

114
README.md
View file

@ -10,11 +10,10 @@ This system is designed from the ground up to be modular, easily configurable, a
* **Performant Image Processing:** Uses `vips`, a high-performance and memory-efficient library, to scale even huge maps (tested up to 64k x 64k pixels) for the web. * **Performant Image Processing:** Uses `vips`, a high-performance and memory-efficient library, to scale even huge maps (tested up to 64k x 64k pixels) for the web.
* **Tiled Map Generation:** Uses `gdal2tiles.py` to create performant, zoomable map tiles for a smooth user experience. * **Tiled Map Generation:** Uses `gdal2tiles.py` to create performant, zoomable map tiles for a smooth user experience.
* **Dynamic Map Viewer:** Implements an interactive map viewer using **OpenLayers**, powered by the generated map tiles, including digital zoom beyond the highest resolution. * **Dynamic Map Viewer:** Implements an interactive map viewer using **OpenLayers**, powered by the generated map tiles, including digital zoom beyond the highest resolution.
* **Live Player & Area Display:** Dynamically loads player positions and protected areas, displaying them as interactive overlays on the live map. * **Live Player Tracking:** Dynamically fetches and displays player locations as markers on the live map, including custom-styled popups and permanent name labels.
* **Layer Control:** A menu on the map allows toggling the visibility of players, parent areas, and sub-areas (parcels).
* **Map Archive:** Automatically saves a daily snapshot of the map and makes it available through a toggle on the world detail page. * **Map Archive:** Automatically saves a daily snapshot of the map and makes it available through a toggle on the world detail page.
* **Template-Driven Site Generation:** Builds all static HTML pages from simple, customizable templates. * **Template-Driven Site Generation:** Builds all static HTML pages from simple, customizable templates.
* **Flexible Configuration:** Configuration is easy with a central global `config.sh` and a `web.conf` file for every one of your worlds. * **Flexible Configuration:** Configuration is easy with a central global config and a `web.conf`-file for every of your worlds.
* **Automation-Ready:** Designed for unattended execution via scheduling tools like `cron`. * **Automation-Ready:** Designed for unattended execution via scheduling tools like `cron`.
## 🔧 Prerequisites ## 🔧 Prerequisites
@ -22,18 +21,16 @@ This system is designed from the ground up to be modular, easily configurable, a
To run this system, the following software packages must be installed on your server: To run this system, the following software packages must be installed on your server:
* **bash:** The scripting language used for the entire project. * **bash:** The scripting language used for the entire project.
* **vips:** A high-performance image processing library. * **vips:** A high-performance image processing library that replaces `convert` (ImageMagick).
* *Debian/Ubuntu Install:* `sudo apt-get install libvips-tools` * *Debian/Ubuntu Install:* `sudo apt-get install libvips-tools`
* **ImageMagick:** Currently still required for the `identify` command (to read image dimensions). * **ImageMagick:** Currently still required for the `identify` command (to read image dimensions).
* *Debian/Ubuntu Install:* `sudo apt-get install imagemagick` * *Debian/Ubuntu Install:* `sudo apt-get install imagemagick`
* **GDAL/OGR:** Provides the `gdal2tiles.py` script for tile generation. * **GDAL/OGR:** Provides the `gdal2tiles.py` script for tile generation.
* *Debian/Ubuntu Install:* `sudo apt-get install gdal-bin python3-gdal` * *Debian/Ubuntu Install:* `sudo apt-get install gdal-bin python3-gdal`
* **SQLite3:** The command-line tool to query the game databases (`players.sqlite`, `auth.sqlite`). * **SQLite3:** The command-line tool for reading SQLite databases, required by `sync_players.sh`.
* *Debian/Ubuntu Install:* `sudo apt-get install sqlite3` * *Debian/Ubuntu Install:* `sudo apt-get install sqlite3`
* **bc:** The "basic calculator" command-line tool, required for calculations in scripts. * **bc:** The "basic calculator" command-line tool, required for calculations in `sync_players.sh`.
* *Debian/Ubuntu Install:* `sudo apt-get install bc` * *Debian/Ubuntu Install:* `sudo apt-get install bc`
* **jq:** A command-line JSON processor, used by `sync_areas.sh`.
* *Debian/Ubuntu Install:* `sudo apt-get install jq`
* **minetestmapper:** The executable used to render maps from world data. Must be placed within the project directory. * **minetestmapper:** The executable used to render maps from world data. Must be placed within the project directory.
* **iproute2:** Provides the `ss` command for `check_server_status.sh` (usually pre-installed on most systems). * **iproute2:** Provides the `ss` command for `check_server_status.sh` (usually pre-installed on most systems).
* **Web Server:** A web server like Nginx or Apache is needed to serve the generated static files. * **Web Server:** A web server like Nginx or Apache is needed to serve the generated static files.
@ -50,24 +47,34 @@ OR
Clone the Git repository to a base directory. Clone the Git repository to a base directory.
-```bash ```bash
git clone [https://git.geigernet.eu/rainer/luanti-web.git](https://git.geigernet.eu/rainer/luanti-web.git) /opt/luweb git clone https://git.geigernet.eu/rainer/luanti-web.git /opt/luweb
cd /opt/luweb cd /opt/luweb
# Make all scripts executable # Make all scripts executable
chmod +x generate_map.sh generate_site.sh check_server_status.sh check_dependencies.sh sync_players.sh sync_areas.sh chmod +x generate_map.sh generate_site.sh check_server_status.sh check_dependencies.sh sync_players.sh
-``` ```
### 2. Global Configuration ### 2. Global Configuration
The main configuration file is `config.sh`. You must edit this file to match your server's environment. The main configuration file is `config.sh`. You must edit this file to match your server's environment.
**Key variables in `config.sh`:**
* `BASE_SCRIPT_DIR`: The root directory of the project (e.g., `/opt/luweb`).
* `MINETESTMAPPER_WORLD_DATA_BASE_PATH`: The path to your Minetest/Luanti worlds' data directory.
* `WEB_ROOT_PATH`: The document root of your website where the generated files will be placed (e.g., `/var/www/your-domain.com/web`).
* `LOG_DIR_BASE`: The directory where log files will be written (e.g., `/var/log/luweb`).
### 3. Per-World Configuration ### 3. Per-World Configuration
The system is designed so that **only worlds with a `web.conf` file** will be displayed in the web frontend. This gives you full control over which worlds are publicly visible. To add a world, copy the template `site_generator/examples/web.conf.template` into the data directory of the respective world and adjust the values. The system is designed so that **only worlds with a `web.conf` file** will be displayed in the web frontend. This gives you full control over which worlds are publicly visible.
To add a world, copy the template `site_generator/examples/web.conf.template` into the data directory of the respective world (e.g., `<server-path>/worlds/my_world/web.conf`) and adjust the values.
## 📂 Directory Structure ## 📂 Directory Structure
-```md The system now uses a modular structure to improve maintainability:
```md
/opt/luweb/ /opt/luweb/
├── config.sh ├── config.sh
├── generate_map.sh ├── generate_map.sh
@ -75,43 +82,88 @@ The system is designed so that **only worlds with a `web.conf` file** will be di
├── check_server_status.sh ├── check_server_status.sh
├── check_dependencies.sh ├── check_dependencies.sh
├── sync_players.sh ├── sync_players.sh
├── sync_areas.sh
├── minetestmapper (executable) ├── minetestmapper (executable)
├── site_generator/ ├── site_generator/
│ ├── functions/ │ ├── functions/
│ │ ├── 01_utils.sh
│ │ ├── 02_init.sh
│ │ ├── 03_html_helpers.sh
│ │ └── generators/ │ │ └── generators/
│ │ └── ... │ │ ├── css_generator.sh
│ │ ├── main_orchestrator.sh
│ │ ├── static_pages_generator.sh
│ │ ├── world_detail_generator.sh
│ │ └── world_overview_generator.sh
│ ├── templates/ │ ├── templates/
│ │ ├── world_detail_page.template
│ │ └── ...
│ └── examples/ │ └── examples/
│ └── web.conf.template
├── web_content/ ├── web_content/
│ ├── images/
│ └── static/
└── worldmaps_output/ └── worldmaps_output/
└── <world_name>/ └── <world_name>/
├── map.png ├── map.png
└── map_info.txt └── map_info.txt
-``` ```
## 🚀 Usage & Automation (Cronjob) ## 🚀 Usage
The scripts are designed for automated execution. Set them up using `crontab -e`. ### 1. Map Generation
```bash
./generate_map.sh <world_key>
```
-```bash ### 2. Website Generation
# (Frequently) Update player and server status ```bash
* * * * * cd /opt/luweb && ./sync_players.sh >> /var/log/luweb/cron.log 2>&1 ./generate_site.sh
*/5 * * * * cd /opt/luweb && ./check_server_status.sh >> /var/log/luweb/cron.log 2>&1 ```
# (Hourly) Generate the base map and tiles ### 3. Status & Player Sync
0 * * * * cd /opt/luweb && ./generate_map.sh >> /var/log/luweb/cron.log 2>&1 ```bash
./check_server_status.sh
./sync_players.sh <world_key>
```
# (Infrequently) Sync area data and rebuild the static site ### 4. Automation (Cronjob)
45 */12 * * * cd /opt/luweb && ./sync_areas.sh >> /var/log/luweb/cron.log 2>&1
30 */12 * * * cd /opt/luweb && ./generate_site.sh >> /var/log/luweb/cron.log 2>&1 **Example for `crontab -e`:**
-``` ```bash
# Update player data every minute
* * * * * /opt/luweb/sync_players.sh world >> /var/log/luweb/cron.log 2>&1
# Check server online status every 5 minutes
*/5 * * * * /opt/luweb/check_server_status.sh >> /var/log/luweb/cron.log 2>&1
# Generate map and tiles once per hour
0 * * * * /opt/luweb/generate_map.sh world >> /var/log/luweb/cron.log 2>&1
# Re-build the static site twice a day
30 */12 * * * /opt/luweb/generate_site.sh >> /var/log/luweb/cron.log 2>&1
```
## 📄 License ## 📄 License
**MIT License** **MIT License**
Copyright (c) 2025 Rage87 - rage87@geigernet.eu Copyright (c) 2025 Rage87 - rage87@geigernet.eu
(License text as before) Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
## 👤 Authors The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.
## 👤 Autoren
* **Rage87** (Main-Developer) * **Rage87** (Main-Developer)

View file

@ -33,8 +33,8 @@ else
fi fi
# Checks für andere Programme, die im PATH sein sollten # Checks für andere Programme, die im PATH sein sollten
# HINZUGEFÜGT: sqlite3 und jq # KORREKTUR: 'convert' und 'identify' entfernt, 'vips' hinzugefügt
declare -a runtime_deps_in_path=("gdal2tiles.py" "vips" "ss" "bc" "sqlite3" "jq") declare -a runtime_deps_in_path=("gdal2tiles.py" "vips" "ss")
for dep in "${runtime_deps_in_path[@]}"; do for dep in "${runtime_deps_in_path[@]}"; do
if ! command -v "$dep" &> /dev/null; then if ! command -v "$dep" &> /dev/null; then
echo "[-] FEHLER: Das benötigte Programm '$dep' wurde nicht im System-Pfad gefunden." echo "[-] FEHLER: Das benötigte Programm '$dep' wurde nicht im System-Pfad gefunden."
@ -79,9 +79,6 @@ if [ "$missing_count" -gt 0 ]; then
echo " - gdal-bin: Enthält 'gdal2tiles.py'." echo " - gdal-bin: Enthält 'gdal2tiles.py'."
echo " - libvips-tools: Enthält 'vips' für die Bildbearbeitung." echo " - libvips-tools: Enthält 'vips' für die Bildbearbeitung."
echo " - iproute2: Enthält 'ss' (meist vorinstalliert)." echo " - iproute2: Enthält 'ss' (meist vorinstalliert)."
echo " - bc: Für mathematische Operationen in Skripten."
echo " - sqlite3: Zur Abfrage der Spieldatenbanken."
echo " - jq: Zur Verarbeitung von JSON in Skripten."
echo "" echo ""
echo " Build-Pakete (zum Kompilieren von minetestmapper):" echo " Build-Pakete (zum Kompilieren von minetestmapper):"
echo " - sudo apt-get install cmake libgd-dev libhiredis-dev libleveldb-dev libpq-dev libsqlite3-dev zlib1g-dev libzstd-dev" echo " - sudo apt-get install cmake libgd-dev libhiredis-dev libleveldb-dev libpq-dev libsqlite3-dev zlib1g-dev libzstd-dev"

View file

@ -1402,44 +1402,3 @@ mystreets:ramp_asphalt_side_solid_left_long 55 55 60 # Entspricht mystreets:asph
mystreets:ramp_asphalt_side_solid_right_long 55 55 60 # Entspricht mystreets:asphalt mystreets:ramp_asphalt_side_solid_right_long 55 55 60 # Entspricht mystreets:asphalt
mystreets:ramp_sidewalk_long 150 150 150 # Entspricht mystreets:sidewalk mystreets:ramp_sidewalk_long 150 150 150 # Entspricht mystreets:sidewalk
mystreets:stop_sign 200 0 0 # Stoppschild-Rot mystreets:stop_sign 200 0 0 # Stoppschild-Rot
# === NEUE EINTRÄGE VOM 23.06.2025 ===
# banner
banner:red_cyan_check_point 131 102 57 # Generische Holzfarbe für den Pfosten des Banners
# irrigation
irrigation:water_barrel_holding_1 131 102 57 # Holzfass, entspricht bestehendem water_barrel
irrigation:water_barrel_holding_2 131 102 57 # Holzfass, entspricht bestehendem water_barrel
# mesecons_detector
mesecons_detector:object_detector_off 110 110 110 # Entspricht mesecons_switch_off, steingrau
# mesecons_switch
mesecons_switch:mesecon_switch_on 255 200 0 # Leuchtendes Gelb für "An"-Zustand, wie bei Mesecon-Drähten
# mesecons
mesecons:wire_00010001_on 255 200 0
mesecons:wire_01000000_off 139 50 50
mesecons:wire_01000100_off 139 50 50
mesecons:wire_01010001_off 139 50 50
mesecons:wire_01010100_off 139 50 50
mesecons:wire_01100010_on 255 200 0
mesecons:wire_01110001_on 255 200 0
mesecons:wire_01110011_off 139 50 50
mesecons:wire_10011000_off 139 50 50
mesecons:wire_10100000_off 139 50 50
mesecons:wire_10101000_off 139 50 50
mesecons:wire_10110000_off 139 50 50
mesecons:wire_10110010_off 139 50 50
mesecons:wire_10111000_off 139 50 50
mesecons:wire_11000000_on 255 200 0
mesecons:wire_11010000_off 139 50 50
mesecons:wire_11100010_on 255 200 0
mesecons:wire_11111001_on 255 200 0
mesecons:wire_11111010_off 139 50 50
# tables_chairs
tables_chairs:outback_wood_bench 120 100 80 # Trockenes "Outback"-Holz, basierend auf naturalbiomes:outback_trunk
tables_chairs:outback_wood_bench_backrest 120 100 80 # Trockenes "Outback"-Holz

View file

@ -1,62 +1,30 @@
#!/bin/bash #!/bin/bash
# Lade globale Konfiguration # Lade globale Konfiguration
CONFIG_FILE_PATH="$(dirname "$0")/config.sh" GLOBAL_CONFIG_FILE="$(dirname "$0")/config.sh"
if [ -f "$CONFIG_FILE_PATH" ]; then if [ -f "$GLOBAL_CONFIG_FILE" ]; then
source "$CONFIG_FILE_PATH" source "$GLOBAL_CONFIG_FILE"
else else
echo "FEHLER: Globale Konfigurationsdatei config.sh nicht gefunden!" echo "FEHLER: Globale Konfigurationsdatei config.sh nicht gefunden!"
exit 1 exit 1
fi fi
# === Logging Funktion (früh definieren für Wrapper-Logik) ===
LOG_FILE_BASE="${LOG_DIR_BASE}/$(basename "$0" .sh)"
log_message() {
local key="${1:-main}"
local msg="$2"
local log_target="${LOG_FILE_BASE}.log"
[ "$key" != "main" ] && log_target="${LOG_FILE_BASE}_${key}.log"
local message_to_log; message_to_log="$(date '+%Y-%m-%d %H:%M:%S') - [${key}] - ${msg}"
echo "${message_to_log}" | tee -a "$log_target"
}
# --- Wrapper-Logik zur Verarbeitung aller Welten, wenn kein Argument übergeben wird ---
if [ -z "$1" ]; then
log_message "main" "Kein spezifischer Welt-Schlüssel angegeben. Verarbeite alle Welten mit web.conf..."
shopt -s nullglob
for world_dir in "${MINETESTMAPPER_WORLD_DATA_BASE_PATH}"/*/; do
if [ -f "${world_dir}web.conf" ]; then
world_key_to_process=$(basename "$world_dir")
log_message "main" "--- Starte Durchlauf für '${world_key_to_process}' ---"
# Rufe das Skript für die gefundene Welt rekursiv auf
bash "$0" "$world_key_to_process"
fi
done
shopt -u nullglob
log_message "main" "Alle Welten verarbeitet."
exit 0
fi
# #############################################################################
# Ab hier beginnt die Logik für eine EINZELNE Welt
# #############################################################################
# Prüfe Abhängigkeiten, bevor irgendetwas anderes passiert # Prüfe Abhängigkeiten, bevor irgendetwas anderes passiert
# Annahme: check_dependencies.sh wurde um 'vips' erweitert
/opt/luweb/check_dependencies.sh || exit 1 /opt/luweb/check_dependencies.sh || exit 1
WORLD_KEY=$1 # Welt-Schlüssel (Verzeichnisname) aus Argument oder Standardwert
WORLD_KEY="${1:-$DEFAULT_WORLD_NAME_KEY}"
# Pfad zum Verzeichnis der aktuellen Welt # Pfad zum Verzeichnis der aktuellen Welt
CURRENT_MINETEST_WORLD_DATA_PATH="${MINETESTMAPPER_WORLD_DATA_BASE_PATH}${WORLD_KEY}/" CURRENT_MINETEST_WORLD_DATA_PATH="${MINETESTMAPPER_WORLD_DATA_BASE_PATH}${WORLD_KEY}/"
if [ ! -d "$CURRENT_MINETEST_WORLD_DATA_PATH" ]; then if [ ! -d "$CURRENT_MINETEST_WORLD_DATA_PATH" ]; then
log_message "${WORLD_KEY}" "FEHLER: Das Welt-Datenverzeichnis '${CURRENT_MINETEST_WORLD_DATA_PATH}' wurde nicht gefunden!" echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - FEHLER: Das Welt-Datenverzeichnis '${CURRENT_MINETEST_WORLD_DATA_PATH}' wurde nicht gefunden!"
exit 1 exit 1
fi fi
if [ ! -f "${CURRENT_MINETEST_WORLD_DATA_PATH}world.mt" ]; then if [ ! -f "${CURRENT_MINETEST_WORLD_DATA_PATH}world.mt" ]; then
log_message "${WORLD_KEY}" "FEHLER: Die Datei 'world.mt' wurde im Verzeichnis '${CURRENT_MINETEST_WORLD_DATA_PATH}' nicht gefunden!" echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - FEHLER: Die Datei 'world.mt' wurde im Verzeichnis '${CURRENT_MINETEST_WORLD_DATA_PATH}' nicht gefunden!"
exit 1 exit 1
fi fi
@ -64,16 +32,18 @@ fi
MM_OPT_ZOOM_LEVEL="$DEFAULT_MM_OPT_ZOOM_LEVEL"; MM_OPT_MIN_Y="$DEFAULT_MM_OPT_MIN_Y" MM_OPT_ZOOM_LEVEL="$DEFAULT_MM_OPT_ZOOM_LEVEL"; MM_OPT_MIN_Y="$DEFAULT_MM_OPT_MIN_Y"
MM_OPT_ORIGINCOLOR="$DEFAULT_MM_OPT_ORIGINCOLOR"; MM_OPT_PLAYERCOLOR="$DEFAULT_MM_OPT_PLAYERCOLOR" MM_OPT_ORIGINCOLOR="$DEFAULT_MM_OPT_ORIGINCOLOR"; MM_OPT_PLAYERCOLOR="$DEFAULT_MM_OPT_PLAYERCOLOR"
MM_OPT_SCALECOLOR="$DEFAULT_MM_OPT_SCALECOLOR"; MM_OPT_BGCOLOR="$DEFAULT_MM_OPT_BGCOLOR" MM_OPT_SCALECOLOR="$DEFAULT_MM_OPT_SCALECOLOR"; MM_OPT_BGCOLOR="$DEFAULT_MM_OPT_BGCOLOR"
MM_CFG_DRAWALPHA="$DEFAULT_MM_CFG_DRAWALPHA"; MM_CFG_DRAWORIGIN="false" MM_CFG_DRAWALPHA="$DEFAULT_MM_CFG_DRAWALPHA"; MM_CFG_DRAWORIGIN="$DEFAULT_MM_CFG_DRAWORIGIN"
MM_CFG_DRAWPLAYERS="$DEFAULT_MM_CFG_DRAWPLAYERS"; MM_CFG_DRAWSCALE="false" MM_CFG_DRAWPLAYERS="$DEFAULT_MM_CFG_DRAWPLAYERS"; MM_CFG_DRAWSCALE="$DEFAULT_MM_CFG_DRAWSCALE"
TILES_SUBDIR_NAME="$DEFAULT_TILES_SUBDIR_NAME"; GDAL2TILES_ZOOM_LEVELS="$DEFAULT_GDAL2TILES_ZOOM_LEVELS" TILES_SUBDIR_NAME="$DEFAULT_TILES_SUBDIR_NAME"; GDAL2TILES_ZOOM_LEVELS="$DEFAULT_GDAL2TILES_ZOOM_LEVELS"
WEB_MAP_PNG_FILENAME="$DEFAULT_WEB_MAP_PNG_FILENAME"; RESIZED_MAX_DIMENSION="$DEFAULT_RESIZED_MAX_DIMENSION" WEB_MAP_PNG_FILENAME="$DEFAULT_WEB_MAP_PNG_FILENAME"; RESIZED_MAX_DIMENSION="$DEFAULT_RESIZED_MAX_DIMENSION"
ARCHIVE_SUBDIR_NAME="$DEFAULT_ARCHIVE_SUBDIR_NAME" ARCHIVE_SUBDIR_NAME="$DEFAULT_ARCHIVE_SUBDIR_NAME"
WORLD_WEB_CONFIG_FILE="${CURRENT_MINETEST_WORLD_DATA_PATH}web.conf" WORLD_WEB_CONFIG_FILE="${CURRENT_MINETEST_WORLD_DATA_PATH}web.conf"
if [ -f "$WORLD_WEB_CONFIG_FILE" ]; then if [ -f "$WORLD_WEB_CONFIG_FILE" ]; then
log_message "${WORLD_KEY}" "Lade Web-Konfiguration aus ${WORLD_WEB_CONFIG_FILE}" echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - Lade Web-Konfiguration aus ${WORLD_WEB_CONFIG_FILE}"
source "$WORLD_WEB_CONFIG_FILE" source "$WORLD_WEB_CONFIG_FILE"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - WARNUNG: Keine web.conf (${WORLD_WEB_CONFIG_FILE}) gefunden. Verwende globale/Default-Einstellungen."
fi fi
# === Abgeleitete Variablen === # === Abgeleitete Variablen ===
@ -92,107 +62,176 @@ TILES_FULL_OUTPUT_PATH="${WEB_CURRENT_WORLD_DIR}/${TILES_SUBDIR_NAME}"
WEB_MAP_PNG_FULL_PATH="${WEB_CURRENT_WORLD_DIR}/${WEB_MAP_PNG_FILENAME}" WEB_MAP_PNG_FULL_PATH="${WEB_CURRENT_WORLD_DIR}/${WEB_MAP_PNG_FILENAME}"
ARCHIVE_BASE_WEB_PATH="${WEB_CURRENT_WORLD_DIR}/${ARCHIVE_SUBDIR_NAME}" ARCHIVE_BASE_WEB_PATH="${WEB_CURRENT_WORLD_DIR}/${ARCHIVE_SUBDIR_NAME}"
# === Logging Funktion ===
log_message() {
local message_to_log; message_to_log="$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - $1"
echo "${message_to_log}" | tee -a "$LOG_FILE"
}
# === Funktion zur Archivbereinigung === # === Funktion zur Archivbereinigung ===
prune_archives() { prune_archives() {
log_message "${WORLD_KEY}" "Starte Archivbereinigung für Welt '${WORLD_KEY}' im Pfad '${ARCHIVE_BASE_WEB_PATH}'..." log_message "Starte Archivbereinigung für Welt '${WORLD_KEY}' im Pfad '${ARCHIVE_BASE_WEB_PATH}'..."
if [ ! -d "$ARCHIVE_BASE_WEB_PATH" ]; then log_message "${WORLD_KEY}" "Archiv-Basispfad ${ARCHIVE_BASE_WEB_PATH} nicht gefunden."; return; fi if [ ! -d "$ARCHIVE_BASE_WEB_PATH" ]; then log_message "Archiv-Basispfad ${ARCHIVE_BASE_WEB_PATH} nicht gefunden."; return; fi
local today_seconds=$(date +%s); local cutoff_date_14_days=$(date -d "today - 14 days" +%Y-%m-%d) local today_seconds=$(date +%s); local cutoff_date_14_days=$(date -d "today - 14 days" +%Y-%m-%d)
local cutoff_seconds_14_days=$(date -d "$cutoff_date_14_days" +%s) local cutoff_seconds_14_days=$(date -d "$cutoff_date_14_days" +%s)
log_message "${WORLD_KEY}" "Archivbereinigung: Behalte tägliche Bilder bis einschl. ${cutoff_date_14_days}. Ältere nur Montage." log_message "Archivbereinigung: Behalte tägliche Bilder bis einschl. ${cutoff_date_14_days}. Ältere nur Montage."
local images_processed=0; local images_deleted=0 local images_processed=0; local images_deleted=0
find "$ARCHIVE_BASE_WEB_PATH" -type f -name "*.png" | while IFS= read -r archive_file_path; do find "$ARCHIVE_BASE_WEB_PATH" -type f -name "*.png" | while IFS= read -r archive_file_path; do
((images_processed++)) images_processed=$((images_processed + 1))
if [[ "$archive_file_path" =~ /([0-9]{4})/([0-9]{2})/([0-9]{2})\.png$ ]]; then if [[ "$archive_file_path" =~ /([0-9]{4})/([0-9]{2})/([0-9]{2})\.png$ ]]; then
local year="${BASH_REMATCH[1]}"; local month="${BASH_REMATCH[2]}"; local day="${BASH_REMATCH[3]}" local year="${BASH_REMATCH[1]}"; local month="${BASH_REMATCH[2]}"; local day="${BASH_REMATCH[3]}"
local img_date_str="${year}-${month}-${day}"; local img_date_seconds; local day_of_week local img_date_str="${year}-${month}-${day}"; local img_date_seconds; local day_of_week
if ! date -d "$img_date_str" "+%s" >/dev/null 2>&1; then log_message "${WORLD_KEY}" "WARNUNG: Ungültiges Datum: '${img_date_str}' ('${archive_file_path}')."; continue; fi if ! date -d "$img_date_str" "+%s" >/dev/null 2>&1; then log_message "WARNUNG: Ungültiges Datum: '${img_date_str}' ('${archive_file_path}')."; continue; fi
img_date_seconds=$(date -d "$img_date_str" +%s) img_date_seconds=$(date -d "$img_date_str" +%s)
if [ "$img_date_seconds" -ge "$cutoff_seconds_14_days" ]; then log_message "${WORLD_KEY}" "BEHALTE (<=14 Tage): ${archive_file_path}" if [ "$img_date_seconds" -ge "$cutoff_seconds_14_days" ]; then log_message "BEHALTE (<=14 Tage): ${archive_file_path}"
else day_of_week=$(date -d "$img_date_str" +%u); if [ "$day_of_week" -eq 1 ]; then log_message "${WORLD_KEY}" "BEHALTE (>14 Tage, Montag): ${archive_file_path}"; else log_message "${WORLD_KEY}" "LÖSCHE (>14 Tage, kein Montag): ${archive_file_path}"; if rm -f "$archive_file_path"; then ((images_deleted++)); else log_message "${WORLD_KEY}" "FEHLER Löschen: ${archive_file_path}"; fi; fi; fi else day_of_week=$(date -d "$img_date_str" +%u); if [ "$day_of_week" -eq 1 ]; then log_message "BEHALTE (>14 Tage, Montag): ${archive_file_path}"; else log_message "LÖSCHE (>14 Tage, kein Montag): ${archive_file_path}"; if rm -f "$archive_file_path"; then images_deleted=$((images_deleted + 1)); else log_message "FEHLER Löschen: ${archive_file_path}"; fi; fi; fi
else log_message "${WORLD_KEY}" "WARNUNG: Pfad '${archive_file_path}' passt nicht zu JJJJ/MM/TT.png."; fi else log_message "WARNUNG: Pfad '${archive_file_path}' passt nicht zu JJJJ/MM/TT.png."; fi
done done
log_message "${WORLD_KEY}" "Archivbereinigung: ${images_processed} geprüft, ${images_deleted} gelöscht." log_message "Archivbereinigung: ${images_processed} geprüft, ${images_deleted} gelöscht."
log_message "${WORLD_KEY}" "Räume leere Archiv-Unterverzeichnisse auf..."; find "$ARCHIVE_BASE_WEB_PATH" -mindepth 1 -type d -empty -print -delete >> "$LOG_FILE" 2>&1 log_message "Räume leere Archiv-Unterverzeichnisse auf..."; find "$ARCHIVE_BASE_WEB_PATH" -mindepth 2 -maxdepth 2 -type d -empty -print -delete >> "$LOG_FILE" 2>&1
find "$ARCHIVE_BASE_WEB_PATH" -mindepth 1 -maxdepth 1 -type d -empty -print -delete >> "$LOG_FILE" 2>&1; log_message "Aufräumen leerer Archiv-Unterverzeichnisse abgeschlossen."
} }
# === Hauptlogik für eine einzelne Welt === # === Hauptlogik ===
exec 200>"$LOCK_FILE" exec 200>"$LOCK_FILE"
flock -n 200 || { log_message "${WORLD_KEY}" "Script ${SCRIPT_BASENAME}.sh ist bereits für diese Welt aktiv (Lock: ${LOCK_FILE}). Beende."; exit 1; } flock -n 200 || { echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] Script ${SCRIPT_BASENAME}.sh ist bereits aktiv (Lock: ${LOCK_FILE}). Beende." | tee -a "$LOG_FILE"; exit 1; }
trap 'rm -f "$LOCK_FILE"; log_message "${WORLD_KEY}" "Skript beendet."' EXIT trap 'rm -f "$LOCK_FILE"; log_message "Skript ${SCRIPT_BASENAME}.sh beendet."' EXIT
log_message "${WORLD_KEY}" "Skript gestartet." mkdir -p "$LOG_DIR_BASE"; log_message "Skript ${SCRIPT_BASENAME}.sh gestartet für Welt-Schlüssel: ${WORLD_KEY}"
mkdir -p "${RAW_MAP_OUTPUT_DIR_ABSOLUTE}" mkdir -p "${RAW_MAP_OUTPUT_DIR_ABSOLUTE}"
if [ ! -d "${RAW_MAP_OUTPUT_DIR_ABSOLUTE}" ]; then log_message "FEHLER: Rohkarten-Ausgabeverz. (${RAW_MAP_OUTPUT_DIR_ABSOLUTE}) nicht erstellt."; exit 1; fi
# 1. Generiere die map.png # 1. Generiere die map.png
log_message "${WORLD_KEY}" "Starte minetestmapper zur Kartengenerierung..." if [ ! -x "$MINETESTMAPPER_PATH" ]; then log_message "FEHLER: minetestmapper (${MINETESTMAPPER_PATH}) nicht ausführbar."; exit 1; fi
MM_ALL_OPTIONS_STR="--zoom ${MM_OPT_ZOOM_LEVEL}"; if [ "${MM_CFG_DRAWALPHA}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --drawalpha"; fi MM_ALL_OPTIONS_STR="--zoom ${MM_OPT_ZOOM_LEVEL}"; if [ "${MM_CFG_DRAWALPHA}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --drawalpha"; fi
if [ "${MM_CFG_DRAWORIGIN}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --draworigin"; fi if [ "${MM_CFG_DRAWORIGIN}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --draworigin"; fi
if [ "${MM_CFG_DRAWPLAYERS}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --drawplayers"; fi if [ "${MM_CFG_DRAWPLAYERS}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --drawplayers"; fi
if [ "${MM_CFG_DRAWSCALE}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --drawscale"; fi if [ "${MM_CFG_DRAWSCALE}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --drawscale"; fi
MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --origincolor '${MM_OPT_ORIGINCOLOR}' --playercolor '${MM_OPT_PLAYERCOLOR}'" MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --origincolor '${MM_OPT_ORIGINCOLOR}'"; MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --playercolor '${MM_OPT_PLAYERCOLOR}'"
MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --scalecolor '${MM_OPT_SCALECOLOR}' --bgcolor '${MM_OPT_BGCOLOR}'" MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --scalecolor '${MM_OPT_SCALECOLOR}'"; MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --bgcolor '${MM_OPT_BGCOLOR}'"
MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --min-y ${MM_OPT_MIN_Y}" MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --min-y ${MM_OPT_MIN_Y}"
MAP_GENERATION_COMMAND="'${MINETESTMAPPER_PATH}' -i '${CURRENT_MINETEST_WORLD_DATA_PATH}' -o '${RAW_MAP_ABSOLUTE_PATH}' ${MM_ALL_OPTIONS_STR}" MAP_GENERATION_COMMAND_TO_EVAL="'${MINETESTMAPPER_PATH}' -i '${CURRENT_MINETEST_WORLD_DATA_PATH}' -o '${RAW_MAP_ABSOLUTE_PATH}' ${MM_ALL_OPTIONS_STR}"
MAPPER_RUN_OUTPUT_CAPTURE_FILE=$(mktemp) log_message "Starte minetestmapper zur Kartengenerierung (Optionen: ${MM_ALL_OPTIONS_STR})."
(set -o pipefail; eval "${MAP_GENERATION_COMMAND}" 2>&1 | tee -a "$LOG_FILE" > "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"); MAPPER_EXIT_STATUS=$? MAPPER_RUN_OUTPUT_CAPTURE_FILE=$(mktemp); MAPPER_EXIT_STATUS=0
if [ ${MAPPER_EXIT_STATUS} -ne 0 ]; then log_message "${WORLD_KEY}" "FEHLER: minetestmapper (Status: ${MAPPER_EXIT_STATUS})."; rm -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"; exit 1; fi (set -o pipefail; eval "${MAP_GENERATION_COMMAND_TO_EVAL}" 2>&1 | tee -a "$LOG_FILE" > "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"); MAPPER_EXIT_STATUS=$?
if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then log_message "${WORLD_KEY}" "FEHLER: ${RAW_MAP_ABSOLUTE_PATH} nicht gefunden."; rm -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"; exit 1; fi if [ ${MAPPER_EXIT_STATUS} -ne 0 ]; then log_message "FEHLER: minetestmapper Kartengenerierung (Status: ${MAPPER_EXIT_STATUS})."; tail -n 15 "$MAPPER_RUN_OUTPUT_CAPTURE_FILE" | while IFS= read -r line; do log_message " ${line}"; done; rm -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"; exit 1; fi
log_message "${WORLD_KEY}" "map.png erfolgreich generiert." if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then log_message "FEHLER: ${RAW_MAP_ABSOLUTE_PATH} nicht gefunden (minetestmapper Status ${MAPPER_EXIT_STATUS})."; rm -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"; exit 1; fi
log_message "${RAW_MAP_FILENAME} erfolgreich generiert nach ${RAW_MAP_ABSOLUTE_PATH}."
# 2. Erstelle map_info.txt log_message "Erstelle map_info.txt..."
log_message "${WORLD_KEY}" "Erstelle map_info.txt..." MAP_DIMENSIONS=""; MAP_EXTENT=""
MAP_DIMENSIONS=$(identify -format "%wx%h" "$RAW_MAP_ABSOLUTE_PATH" 2>/dev/null) if ! command -v identify &> /dev/null; then
EXTENT_COMMAND="'${MINETESTMAPPER_PATH}' -i '${CURRENT_MINETEST_WORLD_DATA_PATH}' --extent ${MM_ALL_OPTIONS_STR}" log_message "WARNUNG: 'identify' (Teil von ImageMagick) nicht gefunden. map_dimension kann nicht ermittelt werden."
MAP_EXTENT=$(eval "${EXTENT_COMMAND}" 2>/dev/null | sed 's/Map extent: //')
if [ -n "$MAP_DIMENSIONS" ] && [ -n "$MAP_EXTENT" ]; then
{ echo "map_dimension=${MAP_DIMENSIONS}"; echo "map_extent=${MAP_EXTENT}"; } > "$MAP_INFO_FILE_ABSOLUTE_PATH"
log_message "${WORLD_KEY}" "map_info.txt erstellt: Dim=${MAP_DIMENSIONS}, Extent=${MAP_EXTENT}"
else else
log_message "${WORLD_KEY}" "FEHLER: map_info.txt konnte nicht erstellt werden." MAP_DIMENSIONS=$(identify -format "%wx%h" "$RAW_MAP_ABSOLUTE_PATH" 2>/dev/null)
fi fi
# 3. Verarbeite unknown_nodes.txt EXTENT_COMMAND_TO_EVAL="'${MINETESTMAPPER_PATH}' -i '${CURRENT_MINETEST_WORLD_DATA_PATH}' --extent ${MM_ALL_OPTIONS_STR}"
awk ' /Unknown nodes:/ {b=1;next} b&&NF==0 {b=0} b&&!/^[ \t]/ {b=0} b{n=$0;sub(/^[ \t]+/,"",n);sub(/[ \t]+$/,"",n);if(n~/:/&&n!="")print n} ' "$MAPPER_RUN_OUTPUT_CAPTURE_FILE" > "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new" log_message "Ermittle Map Extent mit: --extent"
if [ -s "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new" ]; then MAP_EXTENT_OUTPUT=$(eval "${EXTENT_COMMAND_TO_EVAL}" 2>/dev/null)
log_message "${WORLD_KEY}" "Neue 'Unknown nodes' gefunden. Füge zu bestehender Datei hinzu." if [ -n "$MAP_EXTENT_OUTPUT" ]; then
cat "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new" >> "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}" MAP_EXTENT=$(echo "$MAP_EXTENT_OUTPUT" | sed 's/Map extent: //')
sort -u "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}" -o "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}"
fi fi
rm -f "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new" "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"
# 4. Erzeuge Web-Vorschaukarte mit vips if [ -n "$MAP_DIMENSIONS" ] && [ -n "$MAP_EXTENT" ]; then
log_message "${WORLD_KEY}" "Erzeuge Web-Version von map.png (max ${RESIZED_MAX_DIMENSION}px) mit 'vips'..." {
echo "map_dimension=${MAP_DIMENSIONS}"
echo "map_extent=${MAP_EXTENT}"
} > "$MAP_INFO_FILE_ABSOLUTE_PATH"
log_message "map_info.txt erfolgreich erstellt: Dimension=${MAP_DIMENSIONS}, Extent=${MAP_EXTENT}"
else
log_message "FEHLER: map_info.txt konnte nicht erstellt werden, da Informationen fehlen!"
[ -z "$MAP_DIMENSIONS" ] && log_message "-> Bild-Dimensionen konnten nicht ermittelt werden."
[ -z "$MAP_EXTENT" ] && log_message "-> Karten-Extent konnte nicht ermittelt werden."
fi
if [ -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE" ]; then
TEMP_NEW_UNKNOWN_NODES_FILE="${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new_found"
awk ' /Unknown nodes:/ {b=1;next} b&&NF==0 {b=0} b&&!/^[ \t]/ {b=0} b{n=$0;sub(/^[ \t]+/,"",n);sub(/[ \t]+$/,"",n);if(n~/:/&&n!="")print n} ' "$MAPPER_RUN_OUTPUT_CAPTURE_FILE" > "$TEMP_NEW_UNKNOWN_NODES_FILE"
if [ -s "$TEMP_NEW_UNKNOWN_NODES_FILE" ]; then log_message "Neue 'Unknown nodes' gefunden."; touch "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}"; cat "$TEMP_NEW_UNKNOWN_NODES_FILE" >> "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}"; sort -u "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}" -o "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}"; log_message "$(wc -l < "$TEMP_NEW_UNKNOWN_NODES_FILE") 'Unknown nodes' verarbeitet.";
else log_message "Keine neuen 'Unknown nodes' gefunden."; fi; rm -f "$TEMP_NEW_UNKNOWN_NODES_FILE"
else log_message "WARNUNG: minetestmapper-Ausgabe nicht verarbeitbar."; fi
rm -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"
# === Web-Vorschaukarte (verkleinert) erstellen mit VIPS ===
log_message "Erzeuge Web-Version von ${RAW_MAP_FILENAME} (max ${RESIZED_MAX_DIMENSION}px) mit 'vips' nach ${WEB_MAP_PNG_FULL_PATH}..."
mkdir -p "$(dirname "$WEB_MAP_PNG_FULL_PATH")" mkdir -p "$(dirname "$WEB_MAP_PNG_FULL_PATH")"
(set -o pipefail; vips thumbnail "$RAW_MAP_ABSOLUTE_PATH" "$WEB_MAP_PNG_FULL_PATH" "${RESIZED_MAX_DIMENSION}" --height "${RESIZED_MAX_DIMENSION}" --size "down" 2>&1 | tee -a "$LOG_FILE") if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then
if [ $? -ne 0 ]; then log_message "${WORLD_KEY}" "FEHLER: Skalierung mit 'vips' fehlgeschlagen."; else log_message "${WORLD_KEY}" "Verkleinerte Web-map.png erstellt."; fi log_message "FEHLER: Quelldatei ${RAW_MAP_ABSOLUTE_PATH} für Web-Vorschau nicht gefunden!"
else
# KORREKTUR: --size "down" statt ">"
(set -o pipefail; vips thumbnail "$RAW_MAP_ABSOLUTE_PATH" "$WEB_MAP_PNG_FULL_PATH" "${RESIZED_MAX_DIMENSION}" --height "${RESIZED_MAX_DIMENSION}" --size "down" 2>&1 | tee -a "$LOG_FILE")
if [ $? -ne 0 ]; then
log_message "FEHLER: Skalierung mit 'vips' fehlgeschlagen."
else
log_message "Verkleinerte Web-map.png mit 'vips' erfolgreich erstellt."
fi
fi
# 5. Generiere Kacheln # === Tiles generieren ===
log_message "${WORLD_KEY}" "Generiere Kacheln (Zoom: ${GDAL2TILES_ZOOM_LEVELS})..." log_message "Generiere Kacheln (Zoom: ${GDAL2TILES_ZOOM_LEVELS}) nach ${TILES_FULL_OUTPUT_PATH}..."
TEMP_TILES_DIR="${TILES_FULL_OUTPUT_PATH}_temp_$(date +%s)"; rm -rf "$TEMP_TILES_DIR"; if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then log_message "FEHLER: Quelldatei ${RAW_MAP_ABSOLUTE_PATH} für Tiling nicht gefunden!"; else
(set -o pipefail; gdal2tiles.py --profile=raster --xyz --zoom="${GDAL2TILES_ZOOM_LEVELS}" -r near "${RAW_MAP_ABSOLUTE_PATH}" "${TEMP_TILES_DIR}" 2>&1 | tee -a "$LOG_FILE") TEMP_TILES_DIR="${TILES_FULL_OUTPUT_PATH}_temp_$(date +%s)"; rm -rf "$TEMP_TILES_DIR"; mkdir -p "$(dirname "$TILES_FULL_OUTPUT_PATH")"
if [ $? -ne 0 ]; then log_message "${WORLD_KEY}" "FEHLER: gdal2tiles.py fehlgeschlagen."; rm -rf "$TEMP_TILES_DIR"; exit 1; fi log_message "Führe gdal2tiles.py aus..."
rm -rf "$TILES_FULL_OUTPUT_PATH" # KORREKTUR: -r "near" statt "nearest"
if ! mv "$TEMP_TILES_DIR" "$TILES_FULL_OUTPUT_PATH"; then log_message "${WORLD_KEY}" "FEHLER: Verschieben der Kacheln fehlgeschlagen."; exit 1; fi (set -o pipefail; gdal2tiles.py --profile=raster --xyz --zoom="${GDAL2TILES_ZOOM_LEVELS}" -r near "${RAW_MAP_ABSOLUTE_PATH}" "${TEMP_TILES_DIR}" 2>&1 | tee -a "$LOG_FILE")
log_message "${WORLD_KEY}" "Kacheln erfolgreich generiert." if [ $? -ne 0 ]; then log_message "FEHLER: gdal2tiles.py fehlgeschlagen."; rm -rf "$TEMP_TILES_DIR"; exit 1; fi
log_message "Kacheln in ${TEMP_TILES_DIR} generiert."
log_message "Entferne altes Kachel-Verzeichnis: ${TILES_FULL_OUTPUT_PATH}"; rm -rf "$TILES_FULL_OUTPUT_PATH"
log_message "Verschiebe neue Kacheln nach ${TILES_FULL_OUTPUT_PATH}"
if ! mv "$TEMP_TILES_DIR" "$TILES_FULL_OUTPUT_PATH"; then log_message "FEHLER: Verschieben der Kacheln fehlgeschlagen."; exit 1; fi
log_message "Neue Kacheln erfolgreich verschoben."
fi
# 6. Archiv-Management # === Archivbereinigung ===
prune_archives prune_archives
# === Tägliches Archivbild ===
ARCHIVE_YEAR=$(date '+%Y'); ARCHIVE_MONTH=$(date '+%m'); ARCHIVE_DAY=$(date '+%d') ARCHIVE_YEAR=$(date '+%Y'); ARCHIVE_MONTH=$(date '+%m'); ARCHIVE_DAY=$(date '+%d')
ARCHIVE_DAILY_TARGET_DIR="${ARCHIVE_BASE_WEB_PATH}/${ARCHIVE_YEAR}/${ARCHIVE_MONTH}" ARCHIVE_DAILY_TARGET_DIR="${ARCHIVE_BASE_WEB_PATH}/${ARCHIVE_YEAR}/${ARCHIVE_MONTH}"
ARCHIVE_DAILY_FILE_PATH="${ARCHIVE_DAILY_TARGET_DIR}/${ARCHIVE_DAY}.png" ARCHIVE_DAILY_FILE_PATH="${ARCHIVE_DAILY_TARGET_DIR}/${ARCHIVE_DAY}.png"
log_message "Prüfe Notwendigkeit für Archivbild für ${ARCHIVE_YEAR}-${ARCHIVE_MONTH}-${ARCHIVE_DAY}..."
if [ ! -f "$ARCHIVE_DAILY_FILE_PATH" ]; then if [ ! -f "$ARCHIVE_DAILY_FILE_PATH" ]; then
log_message "${WORLD_KEY}" "Erzeuge Archivbild für ${ARCHIVE_DAILY_FILE_PATH}..." log_message "Archivbild ${ARCHIVE_DAILY_FILE_PATH} existiert noch nicht. Versuche Erstellung..."
mkdir -p "$ARCHIVE_DAILY_TARGET_DIR" if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then
(set -o pipefail; vips thumbnail "$RAW_MAP_ABSOLUTE_PATH" "$ARCHIVE_DAILY_FILE_PATH" "${RESIZED_MAX_DIMENSION}" --height "${RESIZED_MAX_DIMENSION}" --size "down" 2>&1 | tee -a "$LOG_FILE") log_message "FEHLER: Quelldatei (${RAW_MAP_ABSOLUTE_PATH}) nicht gefunden! Archiv nicht erstellt."
if [ $? -eq 0 ]; then log_message "${WORLD_KEY}" "Verkleinertes Archivbild erstellt."; else log_message "${WORLD_KEY}" "FEHLER: Archivbild nicht erstellt.";fi else
mkdir -p "$ARCHIVE_DAILY_TARGET_DIR"
if [ ! -d "$ARCHIVE_DAILY_TARGET_DIR" ]; then
log_message "FEHLER: Archiv-Zielverzeichnis ${ARCHIVE_DAILY_TARGET_DIR} nicht erstellt."
else
log_message "Erzeuge Archivbild (max ${RESIZED_MAX_DIMENSION}px) mit 'vips' für ${ARCHIVE_DAILY_FILE_PATH}..."
# KORREKTUR: --size "down" statt ">"
(set -o pipefail; vips thumbnail "$RAW_MAP_ABSOLUTE_PATH" "$ARCHIVE_DAILY_FILE_PATH" "${RESIZED_MAX_DIMENSION}" --height "${RESIZED_MAX_DIMENSION}" --size "down" 2>&1 | tee -a "$LOG_FILE")
if [ $? -eq 0 ]; then
log_message "Verkleinertes Archivbild erstellt."
if [ ! -s "$ARCHIVE_DAILY_FILE_PATH" ]; then log_message "WARNUNG: Archivbild ${ARCHIVE_DAILY_FILE_PATH} ist leer."; fi
else
log_message "FEHLER: Verkleinertes Archivbild nicht erstellt (vips)."
fi
fi
fi
else
log_message "Archivbild ${ARCHIVE_DAILY_FILE_PATH} existiert bereits."
fi fi
# 7. Finale Info-Dateien kopieren # === Status- und Info-Dateien im Webverzeichnis ===
log_message "${WORLD_KEY}" "Kopiere finale Info-Dateien ins Web-Verzeichnis..." log_message "Erstelle Status- und Info-Dateien im Webverzeichnis ${WEB_CURRENT_WORLD_DIR}..."
mkdir -p "$WEB_CURRENT_WORLD_DIR" mkdir -p "$WEB_CURRENT_WORLD_DIR"
echo "$(date '+%Y-%m-%d %H:%M:%S %Z')" > "${WEB_CURRENT_WORLD_DIR}/last_update.txt" echo "$(date '+%Y-%m-%d %H:%M:%S %Z')" > "${WEB_CURRENT_WORLD_DIR}/last_update.txt" && log_message "last_update.txt erstellt." || log_message "FEHLER: last_update.txt nicht erstellt."
cp "$UNKNOWN_NODES_FILE_ABSOLUTE_PATH" "${WEB_CURRENT_WORLD_DIR}/unknown_nodes.txt" if [ -f "$UNKNOWN_NODES_FILE_ABSOLUTE_PATH" ]; then
cp "$MAP_INFO_FILE_ABSOLUTE_PATH" "${WEB_CURRENT_WORLD_DIR}/map_info.txt" if cp "$UNKNOWN_NODES_FILE_ABSOLUTE_PATH" "${WEB_CURRENT_WORLD_DIR}/unknown_nodes.txt"; then log_message "unknown_nodes.txt nach Web kopiert."; else log_message "FEHLER: unknown_nodes.txt nicht nach Web kopiert."; fi
else log_message "WARNUNG: ${UNKNOWN_NODES_FILE_ABSOLUTE_PATH} für Web-Kopie nicht gefunden."; fi
if [ -f "$MAP_INFO_FILE_ABSOLUTE_PATH" ]; then
if cp "$MAP_INFO_FILE_ABSOLUTE_PATH" "${WEB_CURRENT_WORLD_DIR}/map_info.txt"; then
log_message "map_info.txt nach Web kopiert."
else
log_message "FEHLER: map_info.txt konnte nicht nach Web kopiert werden."
fi
else
log_message "WARNUNG: Quelldatei map_info.txt für Web-Kopie nicht gefunden."
fi
exit 0 exit 0

View file

@ -59,6 +59,15 @@ EOD
# OPTIONALE ÜBERSCHREIBUNGEN (Kommentare entfernen zum Aktivieren) # OPTIONALE ÜBERSCHREIBUNGEN (Kommentare entfernen zum Aktivieren)
# ================================================================ # ================================================================
# --- Leaflet Kartengrenzen ---
# Diese Werte aus der gdal2tiles Ausgabe (leaflet.html) entnehmen!
# Wichtig für die korrekte Positionierung der Karte.
#LEAFLET_BOUNDS_SOUTH="-85.0511"
#LEAFLET_BOUNDS_WEST="-170"
#LEAFLET_BOUNDS_NORTH="85.0511"
#LEAFLET_BOUNDS_EAST="170"
#LEAFLET_ZOOM_AFTER_FIT="1"
# --- Minetestmapper-Optionen für DIESE WELT --- # --- Minetestmapper-Optionen für DIESE WELT ---
#MM_OPT_ZOOM_LEVEL="3" #MM_OPT_ZOOM_LEVEL="3"
#MM_CFG_DRAWPLAYERS="false" #MM_CFG_DRAWPLAYERS="false"

View file

@ -30,6 +30,7 @@ generate_world_detail_page() {
local tiles_subdir_name; tiles_subdir_name=$(get_config_value_from_file "$web_conf_file" "TILES_SUBDIR_NAME" "$DEFAULT_TILES_SUBDIR_NAME") local tiles_subdir_name; tiles_subdir_name=$(get_config_value_from_file "$web_conf_file" "TILES_SUBDIR_NAME" "$DEFAULT_TILES_SUBDIR_NAME")
local web_tiles_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/${tiles_subdir_name}" local web_tiles_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/${tiles_subdir_name}"
local map_background_color; map_background_color=$(get_config_value_from_file "$web_conf_file" "MM_OPT_BGCOLOR" "$DEFAULT_MM_OPT_BGCOLOR") local map_background_color; map_background_color=$(get_config_value_from_file "$web_conf_file" "MM_OPT_BGCOLOR" "$DEFAULT_MM_OPT_BGCOLOR")
local WORLD_DISPLAY_NAME_PAGE; WORLD_DISPLAY_NAME_PAGE=$(get_config_value_from_file "$web_conf_file" "WORLD_DISPLAY_NAME") local WORLD_DISPLAY_NAME_PAGE; WORLD_DISPLAY_NAME_PAGE=$(get_config_value_from_file "$web_conf_file" "WORLD_DISPLAY_NAME")
@ -171,7 +172,6 @@ generate_world_detail_page() {
"RESOLUTIONS_JS_ARRAY" "$resolutions_array" \ "RESOLUTIONS_JS_ARRAY" "$resolutions_array" \
"web_tiles_rel_path" "$web_tiles_rel_path" \ "web_tiles_rel_path" "$web_tiles_rel_path" \
"web_map_info_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/map_info.txt" \ "web_map_info_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/map_info.txt" \
"web_areas_json_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/areas.json" \
"map_background_color" "$map_background_color" \ "map_background_color" "$map_background_color" \
"DEFAULT_PLAYER_SKIN_URL" "$DEFAULT_PLAYER_SKIN_URL" "DEFAULT_PLAYER_SKIN_URL" "$DEFAULT_PLAYER_SKIN_URL"
local WORLD_RADAR_HTML; WORLD_RADAR_HTML=$(<"$temp_radar_file") local WORLD_RADAR_HTML; WORLD_RADAR_HTML=$(<"$temp_radar_file")
@ -208,7 +208,6 @@ generate_world_detail_page() {
"web_last_update_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/last_update.txt" \ "web_last_update_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/last_update.txt" \
"web_players_txt_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/players.txt" \ "web_players_txt_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/players.txt" \
"web_weather_txt_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/weather.txt" \ "web_weather_txt_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/weather.txt" \
"web_areas_json_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/areas.json" \
"CACHE_BUSTER" "$CACHE_BUSTER" "CACHE_BUSTER" "$CACHE_BUSTER"
{ {

View file

@ -96,7 +96,7 @@ a.world-preview:hover {
.online-status-badge { font-size: 0.8em; padding: 3px 8px; border-radius: 10px; color: white !important; margin-left: 10px; } .online-status-badge { font-size: 0.8em; padding: 3px 8px; border-radius: 10px; color: white !important; margin-left: 10px; }
.online-status-badge.online { background-color: #3E8E41; } .online-status-badge.online { background-color: #3E8E41; }
.online-status-badge.offline { background-color: #C0392B; } .online-status-badge.offline { background-color: #C0392B; }
.online-status-badge.unknown { background-color: #808080; } .online-status-badge.unknown { background-color: #808080; } /* HINZUGEFÜGT */
/* Welt-Detailseite */ /* Welt-Detailseite */
.page-nav-buttons { text-align: right; margin-top: 10px; margin-bottom: 15px; } .page-nav-buttons { text-align: right; margin-top: 10px; margin-bottom: 15px; }
@ -114,17 +114,68 @@ a.world-preview:hover {
.map-sub-info .map-last-update { text-align: right; } .map-sub-info .map-last-update { text-align: right; }
.info-box { background-color: #3a3a3a; border: 1px solid #555; padding: 15px; margin-bottom: 20px; border-left: 5px solid #FFD700; border-radius: 3px;} .info-box { background-color: #3a3a3a; border: 1px solid #555; padding: 15px; margin-bottom: 20px; border-left: 5px solid #FFD700; border-radius: 3px;}
.info-box h3 { margin-top: 0; border-bottom: none; padding-bottom: 0.2em; } .info-box h3 { margin-top: 0; border-bottom: none; padding-bottom: 0.2em; }
/* Admin Sektion */
.admin-section h3 { margin-top: 2.5em !important; } .admin-section h3 { margin-top: 2.5em !important; }
.admin-grid { display: flex; flex-wrap: wrap; gap: 15px; } .admin-grid {
.admin-box { background-color: #303030; padding: 15px; border-radius: 5px; border: 1px solid #4a4a4a; flex-grow: 0; flex-shrink: 1; flex-basis: calc(50% - 8px); box-sizing: border-box; min-width: 280px; } display: flex;
flex-wrap: wrap;
gap: 15px;
}
.admin-box {
background-color: #303030;
padding: 15px;
border-radius: 5px;
border: 1px solid #4a4a4a;
flex-grow: 0;
flex-shrink: 1;
flex-basis: calc(50% - 8px);
box-sizing: border-box;
min-width: 280px;
}
.admin-contact-block { display: flex; align-items: center; } .admin-contact-block { display: flex; align-items: center; }
.admin-skin-icon { width: 48px; height: 48px; margin-right: 15px; border: 1px solid #666; border-radius: 3px; flex-shrink: 0;} .admin-skin-icon { width: 48px; height: 48px; margin-right: 15px; border: 1px solid #666; border-radius: 3px; flex-shrink: 0;}
.admin-text-details p { margin: 0.2em 0; } .admin-text-details p { margin: 0.2em 0; }
.admin-player-name { font-weight: bold; color: #FFD700; font-size: 1.1em; } .admin-player-name { font-weight: bold; color: #FFD700; font-size: 1.1em; }
.contact-links { margin-top: 10px; padding-top: 10px; border-top: 1px solid #555; display: flex; gap: 8px; flex-wrap: wrap; } .contact-links {
.contact-button { background-color: #555; color: #ddd; border: 1px solid #666; padding: 3px 8px; font-size: 0.85em; border-radius: 3px; text-decoration: none; position: relative; } margin-top: 10px;
.contact-button:hover { background-color: #666; color: #FFA500; text-decoration: none; } padding-top: 10px;
.contact-links a[data-title]:hover::after { content: attr(data-title); position: absolute; background-color: #222; color: #fff; padding: 3px 7px; border-radius: 3px; font-size: 0.7rem; white-space: nowrap; z-index: 100; left: 50%; bottom: 125%; transform: translateX(-50%); border: 1px solid #555; } border-top: 1px solid #555;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.contact-button {
background-color: #555;
color: #ddd;
border: 1px solid #666;
padding: 3px 8px;
font-size: 0.85em;
border-radius: 3px;
text-decoration: none;
position: relative;
}
.contact-button:hover {
background-color: #666;
color: #FFA500;
text-decoration: none;
}
.contact-links a[data-title]:hover::after {
content: attr(data-title);
position: absolute;
background-color: #222;
color: #fff;
padding: 3px 7px;
border-radius: 3px;
font-size: 0.7rem;
white-space: nowrap;
z-index: 100;
left: 50%;
bottom: 125%;
transform: translateX(-50%);
border: 1px solid #555;
}
.server-details p { margin: 0.5em 0; display: flex; align-items: center;} .server-details strong { min-width: 120px; display: inline-block; flex-shrink: 0;} .server-details p { margin: 0.5em 0; display: flex; align-items: center;} .server-details strong { min-width: 120px; display: inline-block; flex-shrink: 0;}
.status-text-colored.online { color: #7CFC00; font-weight: bold; } .status-text-colored.online { color: #7CFC00; font-weight: bold; }
.status-text-colored.offline { color: #FF4500; font-weight: bold; } .status-text-colored.offline { color: #FF4500; font-weight: bold; }
@ -133,7 +184,16 @@ a.world-preview:hover {
.copy-button:hover { background: #666; } .copy-button:hover { background: #666; }
.player-list-container { margin-top: 1em; } .player-list-container { margin-top: 1em; }
.player-list-grid { display: flex; flex-wrap: wrap; gap: 15px; box-sizing: border-box; } .player-list-grid { display: flex; flex-wrap: wrap; gap: 15px; box-sizing: border-box; }
.player-box { background-color: #303030; border: 1px solid #555; border-radius: 5px; padding: 10px; width: calc((100% - 30px) / 3); box-sizing: border-box; display: flex; flex-direction: column; } .player-box {
background-color: #303030;
border: 1px solid #555;
border-radius: 5px;
padding: 10px;
width: calc((100% - 30px) / 3);
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.player-box.has-server-priv { border-left: 5px solid #FFD700; } .player-box.has-server-priv { border-left: 5px solid #FFD700; }
.player-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .player-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.player-identity { display: flex; align-items: center; } .player-identity { display: flex; align-items: center; }
@ -141,12 +201,22 @@ a.world-preview:hover {
.player-icon { width: 40px; height: 40px; margin-right: 10px; border: 1px solid #666; border-radius: 3px; flex-shrink: 0;} .player-icon { width: 40px; height: 40px; margin-right: 10px; border: 1px solid #666; border-radius: 3px; flex-shrink: 0;}
.player-name-status { display: flex; align-items: center; } .player-name-status { display: flex; align-items: center; }
.player-name { font-weight: bold; margin-left: 5px; color: #FFD700; } .player-name { font-weight: bold; margin-left: 5px; color: #FFD700; }
.privilege-separator { height: 1px; background-color: #555; border: 0; margin: 8px 0; } .privilege-separator {
height: 1px;
background-color: #555;
border: 0;
margin: 8px 0;
}
.player-attributes { font-size: 0.9em; line-height: 1.5; } .player-attributes { font-size: 0.9em; line-height: 1.5; }
.player-attributes .icon { margin-right: 8px; cursor: help; font-size: 1.2em; } .player-attributes .icon { margin-right: 8px; cursor: help; font-size: 1.2em; }
.player-attributes .icon.privilege { opacity: 0.15; } .player-attributes .icon.privilege { opacity: 0.15; }
.player-attributes .icon.privilege.active { opacity: 1; } .player-attributes .icon.privilege.active { opacity: 1; }
.player-attributes .icon[data-title]:hover::after, .status-dot[data-title]:hover::after, .player-vitals .icon[data-title]:hover::after { content: attr(data-title); position: absolute; background-color: #222; color: #fff; padding: 3px 7px; border-radius: 3px; font-size: 0.85em; white-space: nowrap; z-index: 100; margin-left: 5px; transform: translateY(-28px); border: 1px solid #555; } .player-attributes .icon[data-title]:hover::after, .status-dot[data-title]:hover::after, .player-vitals .icon[data-title]:hover::after {
content: attr(data-title); position: absolute; background-color: #222; color: #fff;
padding: 3px 7px; border-radius: 3px; font-size: 0.85em; white-space: nowrap;
z-index: 100; margin-left: 5px; transform: translateY(-28px);
border: 1px solid #555;
}
footer { text-align: center; margin-top: 30px; padding: 20px; background-color: #222; color: #aaa; border-top: 2px solid #4A2E0A;} footer { text-align: center; margin-top: 30px; padding: 20px; background-color: #222; color: #aaa; border-top: 2px solid #4A2E0A;}
.scrollable-mod-list { max-height: 20em; overflow-y: auto; border: 1px solid #555; padding: 10px; background-color: #303030; margin-top: 0.5em; border-radius: 3px; } .scrollable-mod-list { max-height: 20em; overflow-y: auto; border: 1px solid #555; padding: 10px; background-color: #303030; margin-top: 0.5em; border-radius: 3px; }
ul.mod-list, ul.mod-list ul { list-style-type: none; padding-left: 20px; } ul.mod-list > li { margin-bottom: 0.5em; } ul.mod-list, ul.mod-list ul { list-style-type: none; padding-left: 20px; } ul.mod-list > li { margin-bottom: 0.5em; }
@ -166,50 +236,315 @@ ul.mod-list > li > ul > li { font-size: 0.9em; }
.archive-image-container img { max-width: 100%; border: 1px solid #666; background-color: #222; } .archive-image-container img { max-width: 100%; border: 1px solid #666; background-color: #222; }
.archive-image-container p { color: #aaa; } .archive-image-container p { color: #aaa; }
/* Stile für responsive Navigation & Burger-Menü */ /* Stile für responsive Navigation & Burger-Menü */
.responsive-nav { position: relative; margin-bottom: 15px; } .responsive-nav {
.burger-menu { display: none; width: 40px; height: 40px; background-color: #555; border: 1px solid #666; border-radius: 5px; cursor: pointer; flex-direction: column; justify-content: space-around; align-items: center; padding: 8px; box-sizing: border-box; position: absolute; top: 0; right: 0; z-index: 101; } position: relative;
.burger-menu:hover { background-color: #666; } margin-bottom: 15px;
.burger-menu span { display: block; width: 100%; height: 3px; background-color: #FFA500; border-radius: 3px; } }
.burger-menu {
display: none;
width: 40px;
height: 40px;
background-color: #555;
border: 1px solid #666;
border-radius: 5px;
cursor: pointer;
flex-direction: column;
justify-content: space-around;
align-items: center;
padding: 8px;
box-sizing: border-box;
position: absolute;
top: 0;
right: 0;
z-index: 101;
}
.burger-menu:hover {
background-color: #666;
}
.burger-menu span {
display: block;
width: 100%;
height: 3px;
background-color: #FFA500;
border-radius: 3px;
}
/* Stile für Live/Archiv Umschalter */ /* Stile für Live/Archiv Umschalter */
.section-header-flex { display: flex; justify-content: space-between; align-items: center; } .section-header-flex {
.map-toggle-controls { display: flex; gap: 5px; } display: flex;
.map-toggle-button { background-color: #555; color: #ddd; border: 1px solid #666; padding: 3px 8px; font-size: 0.85em; border-radius: 3px; cursor: pointer; } justify-content: space-between;
.map-toggle-button:hover { background-color: #666; } align-items: center;
.map-toggle-button.active { color: #FFA500; border-color: #FFA500; font-weight: bold; } }
.map-toggle-controls {
display: flex;
gap: 5px;
}
.map-toggle-button {
background-color: #555;
color: #ddd;
border: 1px solid #666;
padding: 3px 8px;
font-size: 0.85em;
border-radius: 3px;
cursor: pointer;
}
.map-toggle-button:hover {
background-color: #666;
}
.map-toggle-button.active {
color: #FFA500;
border-color: #FFA500;
font-weight: bold;
}
/* Spielerliste Filter */ /* Spielerliste Filter */
.filter-container { position: relative; } .filter-container {
.filter-dropdown-btn { background-color: #555; color: #ddd; border: 1px solid #666; padding: 3px 8px; font-size: 0.85em; border-radius: 3px; cursor: pointer; } position: relative;
.filter-dropdown-btn:hover { background-color: #666; } }
.filter-dropdown { display: none; position: absolute; top: 100%; right: 0; background-color: #3a3a3a; border: 1px solid #666; border-radius: 5px; padding: 10px; z-index: 100; min-width: 220px; box-shadow: 0 4px 8px rgba(0,0,0,0.5); } .filter-dropdown-btn {
.filter-dropdown.show { display: block; } background-color: #555;
.filter-option { display: flex; align-items: center; margin-bottom: 8px; cursor: pointer; font-size: 0.9em; font-weight: normal; color: #ddd; } color: #ddd;
.filter-option:last-child { margin-bottom: 0; } border: 1px solid #666;
.filter-option input[type="checkbox"] { margin-right: 8px; } padding: 3px 8px;
.filter-option .icon { font-size: 1.2em; width: 20px; text-align: center; margin-right: 8px; } font-size: 0.85em;
.filter-option .status-dot { margin-left: 0; margin-right: 8px; } border-radius: 3px;
cursor: pointer;
}
.filter-dropdown-btn:hover {
background-color: #666;
}
.filter-dropdown {
display: none;
position: absolute;
top: 100%;
right: 0;
background-color: #3a3a3a;
border: 1px solid #666;
border-radius: 5px;
padding: 10px;
z-index: 100;
min-width: 220px;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
}
.filter-dropdown.show {
display: block;
}
.filter-option {
display: flex;
align-items: center;
margin-bottom: 8px;
cursor: pointer;
font-size: 0.9em;
font-weight: normal;
color: #ddd;
}
.filter-option:last-child {
margin-bottom: 0;
}
.filter-option input[type="checkbox"] {
margin-right: 8px;
}
.filter-option .icon {
font-size: 1.2em;
width: 20px;
text-align: center;
margin-right: 8px;
}
.filter-option .status-dot {
margin-left: 0;
margin-right: 8px;
}
/* Stile für "Weiterlesen"-Funktion */ /* Stile für "Weiterlesen"-Funktion */
.collapsible-text { max-height: 250px; overflow: hidden; position: relative; transition: max-height 0.5s ease-in-out; } .collapsible-text {
.collapsible-text.expanded { max-height: 10000px; } max-height: 250px;
.collapsible-text::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 80px; background: linear-gradient(to bottom, rgba(68, 68, 68, 0), rgba(68, 68, 68, 1) 100%); pointer-events: none; transition: opacity 0.3s; } overflow: hidden;
.collapsible-text.expanded::after { opacity: 0; } position: relative;
.read-more-container { text-align: right; margin-top: 5px; } transition: max-height 0.5s ease-in-out;
a.read-more-link { font-size: 0.9em; font-weight: bold; } }
.collapsible-text.expanded {
max-height: 10000px;
}
.collapsible-text::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 80px;
background: linear-gradient(to bottom, rgba(68, 68, 68, 0), rgba(68, 68, 68, 1) 100%);
pointer-events: none;
transition: opacity 0.3s;
}
.collapsible-text.expanded::after {
opacity: 0;
}
.read-more-container {
text-align: right;
margin-top: 5px;
}
a.read-more-link {
font-size: 0.9em;
font-weight: bold;
}
/* Schließen-Button (X) */ /* Schließen-Button (X) */
.close-button-container { position: absolute; top: 15px; right: 15px; z-index: 10; } .close-button-container {
.close-button { display: inline-flex; justify-content: center; align-items: center; width: 30px; height: 30px; background-color: #555; color: #ddd; border: 1px solid #666; border-radius: 50%; text-decoration: none; font-size: 1em; line-height: 1; } position: absolute;
.close-button:hover { background-color: #C0392B; color: white; text-decoration: none; } top: 15px;
right: 15px;
z-index: 10;
}
.close-button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
background-color: #555;
color: #ddd;
border: 1px solid #666;
border-radius: 50%;
text-decoration: none;
font-size: 1em;
line-height: 1;
}
.close-button:hover {
background-color: #C0392B;
color: white;
text-decoration: none;
}
/* Stile für "Nach oben"-Button */ /* Stile für "Nach oben"-Button */
.scroll-to-top-btn { display: none; position: fixed; bottom: 20px; right: 20px; z-index: 999; background-color: #555; color: #FFA500; border: 1px solid #666; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 38px; cursor: pointer; transition: background-color 0.3s, opacity 0.3s; } .scroll-to-top-btn {
.scroll-to-top-btn:hover { background-color: #666; text-decoration: none; } display: none;
.scroll-to-top-btn.show { display: block; } position: fixed;
bottom: 20px;
right: 20px;
z-index: 999;
background-color: #555;
color: #FFA500;
border: 1px solid #666;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 38px;
cursor: pointer;
transition: background-color 0.3s, opacity 0.3s;
}
.scroll-to-top-btn:hover {
background-color: #666;
text-decoration: none;
}
.scroll-to-top-btn.show {
display: block;
}
@media (max-width: 768px) {
/* Responsive Stile für Burger-Menü */
.responsive-nav {
height: 50px;
}
.burger-menu {
display: flex;
}
.page-nav-buttons {
display: none;
position: absolute;
top: 45px;
right: 0;
width: 200px;
background-color: #3a3a3a;
border: 1px solid #666;
border-radius: 5px;
z-index: 100;
padding: 5px;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
}
.page-nav-buttons.menu-open {
display: flex;
flex-direction: column;
align-items: stretch;
}
.page-nav-buttons .button {
text-align: left;
margin: 5px;
width: auto;
}
.player-box { width: calc((100% - 15px) / 2); }
.page-nav-buttons .button { margin-top: 5px;}
.world-preview .world-preview-content {
flex-direction: column;
}
.world-preview img {
width: 100%;
max-width: 100%;
height: auto;
margin-right: 0;
margin-bottom: 15px;
}
.map-view-container {
height: auto;
aspect-ratio: 4 / 3;
max-height: 70vh;
}
}
@media (max-width: 480px) {
.player-box { width: 100%; }
.admin-box { flex-basis: 100%; }
}
/* Stile für OpenLayers Popups (Leaflet-Look-and-Feel) */ /* Stile für OpenLayers Popups (Leaflet-Look-and-Feel) */
.ol-popup { position: absolute; background-color: #303030; box-shadow: 0 1px 4px rgba(0,0,0,0.4); padding: 1px; border-radius: 5px; bottom: 12px; left: -50px; min-width: 280px; border: 1px solid #555; color: #ddd; } .ol-popup {
.ol-popup:after, .ol-popup:before { top: 100%; border: solid transparent; content: " "; height: 0; width: 0; position: absolute; pointer-events: none; } position: absolute;
.ol-popup:after { border-top-color: #303030; border-width: 10px; left: 48px; margin-left: -10px; } background-color: #303030;
.ol-popup:before { border-top-color: #555; border-width: 11px; left: 48px; margin-left: -11px; } box-shadow: 0 1px 4px rgba(0,0,0,0.4);
.ol-popup-closer { text-decoration: none; position: absolute; top: 2px; right: 8px; color: #ddd; font-size: 1.5em; font-weight: bold; } padding: 1px;
.ol-popup-closer:hover { color: #fff; } border-radius: 5px;
.ol-popup-content { font-size: 13px; line-height: 1.5; padding: 10px; margin: 0; } bottom: 12px;
left: -50px;
min-width: 280px;
border: 1px solid #555;
color: #ddd;
}
.ol-popup:after, .ol-popup:before {
top: 100%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.ol-popup:after {
border-top-color: #303030;
border-width: 10px;
left: 48px;
margin-left: -10px;
}
.ol-popup:before {
border-top-color: #555;
border-width: 11px;
left: 48px;
margin-left: -11px;
}
.ol-popup-closer {
text-decoration: none;
position: absolute;
top: 2px;
right: 8px;
color: #ddd;
font-size: 1.5em;
font-weight: bold;
}
.ol-popup-closer:hover {
color: #fff;
}
.ol-popup-content {
font-size: 13px;
line-height: 1.5;
padding: 10px;
margin: 0;
}
/* Stile für den Inhalt innerhalb des Popups */
.popup-player-box .player-header { display: flex; align-items: center; margin-bottom: 8px; } .popup-player-box .player-header { display: flex; align-items: center; margin-bottom: 8px; }
.popup-player-box .player-identity { display: flex; align-items: center; } .popup-player-box .player-identity { display: flex; align-items: center; }
.popup-player-box .player-icon { width: 40px; height: 40px; margin-right: 10px; border-radius: 3px; flex-shrink: 0;} .popup-player-box .player-icon { width: 40px; height: 40px; margin-right: 10px; border-radius: 3px; flex-shrink: 0;}
@ -219,68 +554,3 @@ a.read-more-link { font-size: 0.9em; font-weight: bold; }
.popup-player-box .popup-player-vitals { display: flex; justify-content: space-around; gap: 10px; align-items: center; margin-top: 8px; } .popup-player-box .popup-player-vitals { display: flex; justify-content: space-around; gap: 10px; align-items: center; margin-top: 8px; }
.popup-player-box .popup-player-vitals .vital { display: flex; align-items: center; gap: 5px; } .popup-player-box .popup-player-vitals .vital { display: flex; align-items: center; gap: 5px; }
.popup-player-box .popup-player-vitals .icon { font-size: 1.2em; } .popup-player-box .popup-player-vitals .icon { font-size: 1.2em; }
@media (max-width: 768px) {
.responsive-nav { height: 50px; }
.burger-menu { display: flex; }
.page-nav-buttons { display: none; position: absolute; top: 45px; right: 0; width: 200px; background-color: #3a3a3a; border: 1px solid #666; border-radius: 5px; z-index: 100; padding: 5px; box-shadow: 0 4px 8px rgba(0,0,0,0.5); }
.page-nav-buttons.menu-open { display: flex; flex-direction: column; align-items: stretch; }
.page-nav-buttons .button { text-align: left; margin: 5px; width: auto; }
.player-box { width: calc((100% - 15px) / 2); }
.world-preview .world-preview-content { flex-direction: column; }
.world-preview img { width: 100%; max-width: 100%; height: auto; margin-right: 0; margin-bottom: 15px; }
.map-view-container { height: auto; aspect-ratio: 4 / 3; max-height: 70vh; }
}
@media (max-width: 480px) {
.player-box { width: 100%; }
.admin-box { flex-basis: 100%; }
}
/* KORREKTUR: Stile für OpenLayers Layer Switcher */
.layer-switcher.shown {
background-color: #303030 !important;
}
.layer-switcher button {
font-size: 1.2em !important;
}
.layer-switcher button:hover, .layer-switcher button:focus {
background-color: #666 !important;
}
.layer-switcher .panel {
background-color: #303030 !important;
border: 1px solid #555 !important;
color: #ddd !important;
margin: 0;
}
.layer-switcher ul {
padding-left: 1em;
}
.layer-switcher label {
color: #ddd !important;
}
/* NEU: Styling für Checkboxen im Layer-Switcher */
.layer-switcher input[type=checkbox],
.layer-switcher input[type=radio] {
accent-color: #FFA500;
vertical-align: middle;
margin-right: 5px;
}
/* NEU: Vergrößerung der Standard-Karten-Buttons */
.ol-zoom .ol-zoom-in,
.ol-zoom .ol-zoom-out,
.ol-rotate .ol-rotate-reset,
.ol-full-screen button {
width: 25px !important;
height: 25px !important;
font-size: 1.5em;
line-height: 1;
padding: 0;
}

View file

@ -9,9 +9,6 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@main/dist/en/v7.0.0/legacy/ol.css" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@main/dist/en/v7.0.0/legacy/ol.css" type="text/css">
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@main/dist/en/v7.0.0/legacy/ol.js"></script> <script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@main/dist/en/v7.0.0/legacy/ol.js"></script>
<script src="https://unpkg.com/ol-layerswitcher@4.1.1"></script>
<link rel="stylesheet" href="https://unpkg.com/ol-layerswitcher@4.1.1/dist/ol-layerswitcher.css" />
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const scrollToTopBtn = document.getElementById('scrollToTopBtn'); const scrollToTopBtn = document.getElementById('scrollToTopBtn');

View file

@ -26,44 +26,32 @@
// Globale Referenzen // Globale Referenzen
window.olMap_%%current_world_key%% = null; window.olMap_%%current_world_key%% = null;
window.playerMarkerSource_%%current_world_key%% = new ol.source.Vector(); window.playerMarkerSource_%%current_world_key%% = new ol.source.Vector();
window.parentAreaLayerSource_%%current_world_key%% = new ol.source.Vector();
window.subAreaLayerSource_%%current_world_key%% = new ol.source.Vector();
window.mapData_%%current_world_key%% = {}; window.mapData_%%current_world_key%% = {};
// Konvertiert Minetest-Koordinaten in OpenLayers-Pixel-Koordinaten // Konvertiert Minetest-Koordinaten in OpenLayers-Pixel-Koordinaten
function convertMinetestToOpenLayers_%%current_world_key%%(posX, posZ) { function convertMinetestToOpenLayers_%%current_world_key%%(posX, posZ) {
const mapData = window.mapData_%%current_world_key%%; const mapData = window.mapData_%%current_world_key%%;
if (!mapData || !mapData.mapWidth) return null; if (!mapData || !mapData.mapWidth) return null;
const percentX = (posX - mapData.minX) / mapData.extentWidth; const percentX = (posX - mapData.minX) / mapData.extentWidth;
const percentZ = (posZ - mapData.minZ) / mapData.extentHeight; const percentZ = (posZ - mapData.minZ) / mapData.extentHeight;
const pixelX = percentX * mapData.mapWidth; const pixelX = percentX * mapData.mapWidth;
const pixelY = - (mapData.mapHeight - (percentZ * mapData.mapHeight)); const pixelY = - (mapData.mapHeight - (percentZ * mapData.mapHeight));
return [pixelX, pixelY]; return [pixelX, pixelY];
} }
function createPolygonFromPos_%%current_world_key%%(pos1, pos2) {
const p1 = convertMinetestToOpenLayers_%%current_world_key%%(pos1.x, pos1.z);
const p2 = convertMinetestToOpenLayers_%%current_world_key%%(pos2.x, pos2.z);
if (!p1 || !p2) return null;
const minX = Math.min(p1[0], p2[0]);
const minY = Math.min(p1[1], p2[1]);
const maxX = Math.max(p1[0], p2[0]);
const maxY = Math.max(p1[1], p2[1]);
const coordinates = [[ [minX, minY], [maxX, minY], [maxX, maxY], [minX, maxY], [minX, minY] ]];
return new ol.geom.Polygon(coordinates);
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const mapContainer = document.getElementById('ol-map-container-%%current_world_key%%'); const mapContainer = document.getElementById('ol-map-container-%%current_world_key%%');
const mapInfoPath = '/%%web_map_info_rel_path%%?v=' + new Date().getTime(); const mapInfoPath = '/%%web_map_info_rel_path%%?v=' + new Date().getTime();
const areasPath = '/%%web_areas_json_rel_path%%?v=' + new Date().getTime();
Promise.all([ fetch(mapInfoPath)
fetch(mapInfoPath).then(res => { if (!res.ok) throw new Error('map_info.txt'); return res.text(); }), .then(response => {
fetch(areasPath).then(res => { if (!res.ok) return {}; return res.json(); }) if (!response.ok) throw new Error('map_info.txt nicht gefunden (Status: ' + response.status + ')');
]).then(([mapInfoText, areaData]) => { return response.text();
})
.then(mapInfoText => {
const mapInfo = (text => { const mapInfo = (text => {
const data = {}; const data = {};
text.split('\n').forEach(line => { text.split('\n').forEach(line => {
@ -72,12 +60,14 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
return data; return data;
})(mapInfoText); })(mapInfoText);
if (!mapInfo.map_dimension || !mapInfo.map_extent) throw new Error('map_info.txt ist unvollständig.'); if (!mapInfo.map_dimension || !mapInfo.map_extent) throw new Error('map_info.txt ist unvollständig.');
const mapData = window.mapData_%%current_world_key%%; const mapData = window.mapData_%%current_world_key%%;
const mapDim = mapInfo.map_dimension.split('x'); const mapDim = mapInfo.map_dimension.split('x');
mapData.mapWidth = parseInt(mapDim[0], 10); mapData.mapWidth = parseInt(mapDim[0], 10);
mapData.mapHeight = parseInt(mapDim[1], 10); mapData.mapHeight = parseInt(mapDim[1], 10);
const mapExt = mapInfo.map_extent.split(/[+:]/); const mapExt = mapInfo.map_extent.split(/[+:]/);
mapData.minX = parseInt(mapExt[0], 10); mapData.minX = parseInt(mapExt[0], 10);
mapData.minZ = parseInt(mapExt[1], 10); mapData.minZ = parseInt(mapExt[1], 10);
@ -88,60 +78,62 @@ document.addEventListener('DOMContentLoaded', function() {
mapContainer.innerHTML = ''; mapContainer.innerHTML = '';
const extent = [0, -mapData.mapHeight, mapData.mapWidth, 0]; const extent = [0, -mapData.mapHeight, mapData.mapWidth, 0];
// KORREKTUR: Wir trennen die echten von den virtuellen Auflösungen
const sourceResolutions = %%RESOLUTIONS_JS_ARRAY%%; const sourceResolutions = %%RESOLUTIONS_JS_ARRAY%%;
// HINZUGEFÜGT: Logik für digitalen Zoom wiederhergestellt const viewResolutions = [...sourceResolutions]; // Kopie für die Ansicht
const maxNativeZoom = sourceResolutions.length -1; const digitalZoomLevels = 3;
const viewResolutions = [...sourceResolutions];
const digitalZoomLevels = 3;
let lastResolution = viewResolutions[viewResolutions.length - 1]; let lastResolution = viewResolutions[viewResolutions.length - 1];
for (let i = 0; i < digitalZoomLevels; i++) { for (let i = 0; i < digitalZoomLevels; i++) {
lastResolution = lastResolution / 2; lastResolution = lastResolution / 2;
viewResolutions.push(lastResolution); viewResolutions.push(lastResolution);
} }
const projection = new ol.proj.Projection({ code: 'pixel-map-%%current_world_key%%', units: 'pixels', extent: extent }); const projection = new ol.proj.Projection({
const tileGrid = new ol.tilegrid.TileGrid({ origin: ol.extent.getTopLeft(extent), resolutions: sourceResolutions }); code: 'pixel-map-%%current_world_key%%',
units: 'pixels',
extent: extent
});
const tileGrid = new ol.tilegrid.TileGrid({
origin: ol.extent.getTopLeft(extent),
resolutions: sourceResolutions, // Das TileGrid kennt NUR die echten Auflösungen
});
const tileSource = new ol.source.TileImage({ const tileSource = new ol.source.TileImage({
projection: projection, projection: projection,
tileGrid: tileGrid, tileGrid: tileGrid,
tileUrlFunction: function(tileCoord) { tileUrlFunction: function(tileCoord) {
if (!tileCoord) return ''; if (!tileCoord) return '';
let z = tileCoord[0]; let x = tileCoord[1]; let y = tileCoord[2]; // OpenLayers wählt jetzt selbst die beste verfügbare Kachel aus.
if (z > maxNativeZoom) { // Die URL-Funktion kann wieder einfach sein.
const zoomDiff = z - maxNativeZoom; return ('/%%web_tiles_rel_path%%/' + tileCoord[0] + '/' + tileCoord[1] + '/' + tileCoord[2] + '.png?v=%%CACHE_BUSTER%%');
const scale = Math.pow(2, zoomDiff);
x = Math.floor(x / scale);
y = Math.floor(y / scale);
z = maxNativeZoom;
}
return ('/%%web_tiles_rel_path%%/' + z + '/' + x + '/' + y + '.png?v=%%CACHE_BUSTER%%');
} }
}); });
const tileLayer = new ol.layer.Tile({ source: tileSource, title: 'Basiskarte', type: 'base' }); const tileLayer = new ol.layer.Tile({ source: tileSource });
const parentAreaStyle = new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'rgba(0, 100, 255, 0.8)', width: 2 }), fill: new ol.style.Fill({ color: 'rgba(0, 100, 255, 0.2)' }) }); const playerLayer = new ol.layer.Vector({ source: window.playerMarkerSource_%%current_world_key%%, style: f => f.get('style') });
const subAreaStyle = new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'rgba(0, 100, 255, 0.8)', width: 1.5, lineDash: [6, 6] }), fill: new ol.style.Fill({ color: 'transparent' }) });
const parentAreaLayer = new ol.layer.Vector({ source: window.parentAreaLayerSource_%%current_world_key%%, style: parentAreaStyle, title: 'Grundstücke' });
const subAreaLayer = new ol.layer.Vector({ source: window.subAreaLayerSource_%%current_world_key%%, style: subAreaStyle, title: 'Parzellen', visible: false });
const playerLayer = new ol.layer.Vector({ source: window.playerMarkerSource_%%current_world_key%%, style: f => f.get('style'), title: 'Spielerpositionen' });
const overlayLayers = new ol.layer.Group({ title: 'Overlays', layers: [subAreaLayer, parentAreaLayer, playerLayer] }); const view = new ol.View({
const view = new ol.View({ projection: projection, center: ol.extent.getCenter(extent), resolutions: viewResolutions }); projection: projection,
center: ol.extent.getCenter(extent),
resolutions: viewResolutions, // Die View kennt ALLE Auflösungen (echte + digitale)
});
const popupContainer = document.getElementById('popup-%%current_world_key%%'); const popupContainer = document.getElementById('popup-%%current_world_key%%');
const popupContent = document.getElementById('popup-content-%%current_world_key%%'); const popupContent = document.getElementById('popup-content-%%current_world_key%%');
const popupCloser = document.getElementById('popup-closer-%%current_world_key%%'); const popupCloser = document.getElementById('popup-closer-%%current_world_key%%');
const overlay = new ol.Overlay({ element: popupContainer, autoPan: { animation: { duration: 250 } } }); const overlay = new ol.Overlay({ element: popupContainer, autoPan: { animation: { duration: 250 } } });
popupCloser.onclick = () => { overlay.setPosition(undefined); popupCloser.blur(); return false; }; popupCloser.onclick = () => { overlay.setPosition(undefined); popupCloser.blur(); return false; };
const mapControls = [ new ol.control.Zoom(), new ol.control.Rotate(), new ol.control.Attribution({ collapsible: false }), new ol.control.FullScreen(), new ol.control.LayerSwitcher() ]; const mapControls = [ new ol.control.Zoom(), new ol.control.Rotate(), new ol.control.Attribution({ collapsible: false }), new ol.control.FullScreen() ];
window.olMap_%%current_world_key%% = new ol.Map({ window.olMap_%%current_world_key%% = new ol.Map({
controls: mapControls, controls: mapControls,
layers: [tileLayer, overlayLayers], layers: [tileLayer, playerLayer],
overlays: [overlay], overlays: [overlay],
target: mapContainer, target: mapContainer,
view: view view: view
@ -150,31 +142,17 @@ document.addEventListener('DOMContentLoaded', function() {
const map = window.olMap_%%current_world_key%%; const map = window.olMap_%%current_world_key%%;
map.getView().fit(extent, { size: map.getSize() }); map.getView().fit(extent, { size: map.getSize() });
for (const id in areaData) {
const area = areaData[id];
const polygon = createPolygonFromPos_%%current_world_key%%(area.pos1, area.pos2);
if (polygon) {
const feature = new ol.Feature({ geometry: polygon, areaData: area });
window.parentAreaLayerSource_%%current_world_key%%.addFeature(feature);
}
}
// KORREKTUR: Klick-Handler mit korrekter Priorisierung
map.on('click', function(evt) { map.on('click', function(evt) {
// Reset const feature = map.forEachFeatureAtPixel(evt.pixel, f => f);
overlay.setPosition(undefined); overlay.setPosition(undefined);
popupCloser.blur(); popupCloser.blur();
// Priorität 1: Spieler-Marker
const playerFeature = map.forEachFeatureAtPixel(evt.pixel, (f, l) => l === playerLayer ? f : undefined);
if (playerFeature) {
window.subAreaLayerSource_%%current_world_key%%.clear(); // Sub-Areas ausblenden, wenn ein Spieler geklickt wird
subAreaLayer.setVisible(false);
const playerData = playerFeature.get('playerData'); if (feature) {
const playerData = feature.get('playerData');
if (playerData) { if (playerData) {
const statusDotClass = playerFeature.get('statusDotClass'); const statusDotClass = feature.get('statusDotClass');
const lastLoginFormatted = playerFeature.get('lastLoginFormatted'); const lastLoginFormatted = feature.get('lastLoginFormatted');
const popupHTML = ` const popupHTML = `
<div class='popup-player-box'> <div class='popup-player-box'>
<div class="player-header"> <div class="player-header">
@ -194,39 +172,8 @@ document.addEventListener('DOMContentLoaded', function() {
</div> </div>
</div>`; </div>`;
popupContent.innerHTML = popupHTML; popupContent.innerHTML = popupHTML;
overlay.setPosition(playerFeature.getGeometry().getCoordinates()); overlay.setPosition(feature.getGeometry().getCoordinates());
} }
return; // Verarbeitung hier beenden
}
// Priorität 2: Hauptgrundstück (nur wenn kein Spieler geklickt wurde)
window.subAreaLayerSource_%%current_world_key%%.clear(); // Alte Sub-Areas immer entfernen
const areaFeature = map.forEachFeatureAtPixel(evt.pixel, (f, l) => l === parentAreaLayer ? f : undefined);
if (areaFeature) {
const areaData = areaFeature.get('areaData');
if (areaData.sub_areas && areaData.sub_areas.length > 0) {
const sub_features = [];
areaData.sub_areas.forEach(sub => {
const sub_polygon = createPolygonFromPos_%%current_world_key%%(sub.pos1, sub.pos2);
if(sub_polygon) sub_features.push(new ol.Feature({ geometry: sub_polygon }));
});
window.subAreaLayerSource_%%current_world_key%%.addFeatures(sub_features);
subAreaLayer.setVisible(true);
} else {
subAreaLayer.setVisible(false);
}
let popupHTML = `<strong>${areaData.name}</strong><br>Besitzer: ${areaData.owner}`;
if (areaData.sub_areas && areaData.sub_areas.length > 0) {
popupHTML += `<hr class="privilege-separator"><p><strong>Parzellen:</strong></p><ul>`;
areaData.sub_areas.forEach(sub => { popupHTML += `<li>${sub.name} (${sub.owner})</li>`; });
popupHTML += `</ul>`;
}
popupContent.innerHTML = popupHTML;
overlay.setPosition(evt.coordinate);
} else {
// Wenn nichts geklickt wurde, auch die sub-areas ausblenden
subAreaLayer.setVisible(false);
} }
}); });
@ -236,7 +183,7 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}) })
.catch(error => { .catch(error => {
console.error("Fehler beim Initialisieren der Karte oder der Grundstücksdaten:", error); console.error("Fehler beim Initialisieren der Karte:", error);
mapContainer.innerHTML = "<p><em>Karte konnte nicht initialisiert werden: " + error.message + "</em></p>"; mapContainer.innerHTML = "<p><em>Karte konnte nicht initialisiert werden: " + error.message + "</em></p>";
}); });
@ -244,9 +191,19 @@ document.addEventListener('DOMContentLoaded', function() {
const archiveBtn = document.getElementById('toggle-archive-btn-%%current_world_key%%'); const archiveBtn = document.getElementById('toggle-archive-btn-%%current_world_key%%');
const liveContainer = document.getElementById('live-map-container-%%current_world_key%%'); const liveContainer = document.getElementById('live-map-container-%%current_world_key%%');
const archiveContainer = document.getElementById('archive-view-container-%%current_world_key%%'); const archiveContainer = document.getElementById('archive-view-container-%%current_world_key%%');
if (liveBtn && archiveBtn && liveContainer && archiveContainer) { if (liveBtn && archiveBtn && liveContainer && archiveContainer) {
liveBtn.addEventListener('click', () => { liveContainer.style.display = 'block'; archiveContainer.style.display = 'none'; if (window.olMap_%%current_world_key%%) { setTimeout(() => window.olMap_%%current_world_key%%.updateSize(), 10); } }); liveBtn.addEventListener('click', () => {
archiveBtn.addEventListener('click', () => { archiveContainer.style.display = 'block'; liveContainer.style.display = 'none'; }); liveBtn.classList.add('active'); archiveBtn.classList.remove('active');
liveContainer.style.display = 'block'; archiveContainer.style.display = 'none';
if (window.olMap_%%current_world_key%%) {
setTimeout(() => window.olMap_%%current_world_key%%.updateSize(), 10);
}
});
archiveBtn.addEventListener('click', () => {
archiveBtn.classList.add('active'); liveBtn.classList.remove('active');
archiveContainer.style.display = 'block'; liveContainer.style.display = 'none';
});
} }
}); });
</script> </script>

View file

@ -1,99 +0,0 @@
#!/bin/bash
# Lade globale Konfiguration
CONFIG_FILE_PATH="$(dirname "$0")/config.sh"
if [ -f "$CONFIG_FILE_PATH" ]; then
source "$CONFIG_FILE_PATH"
else
echo "FEHLER: Globale config.sh nicht unter ${CONFIG_FILE_PATH} gefunden!"
exit 1
fi
# === Logging Funktion (früh definieren für Wrapper-Logik) ===
LOG_FILE_BASE="${LOG_DIR_BASE}/$(basename "$0" .sh)"
log_message() {
local key="${1:-main}"
local msg="$2"
local log_target="${LOG_FILE_BASE}.log"
[ "$key" != "main" ] && log_target="${LOG_FILE_BASE}_${key}.log"
local message_to_log; message_to_log="$(date '+%Y-%m-%d %H:%M:%S') - [${key}] - ${msg}"
echo "${message_to_log}" | tee -a "$log_target"
}
# --- Wrapper-Logik zur Verarbeitung aller Welten, wenn kein Argument übergeben wird ---
if [ -z "$1" ]; then
log_message "main" "Kein spezifischer Welt-Schlüssel angegeben. Verarbeite alle Welten mit web.conf..."
shopt -s nullglob
for world_dir in "${MINETESTMAPPER_WORLD_DATA_BASE_PATH}"/*/; do
if [ -f "${world_dir}web.conf" ]; then
world_key_to_process=$(basename "$world_dir")
log_message "main" "--- Starte Durchlauf für '${world_key_to_process}' ---"
# Rufe das Skript für die gefundene Welt rekursiv auf
bash "$0" "$world_key_to_process"
fi
done
shopt -u nullglob
log_message "main" "Alle Welten verarbeitet."
exit 0
fi
# #############################################################################
# Ab hier beginnt die Logik für eine EINZELNE Welt
# #############################################################################
# Prüfe Abhängigkeiten, bevor irgendetwas anderes passiert
/opt/luweb/check_dependencies.sh || exit 1
WORLD_KEY=$1
CURRENT_MINETEST_WORLD_DATA_PATH="${MINETESTMAPPER_WORLD_DATA_BASE_PATH}${WORLD_KEY}/"
# === Abgeleitete Variablen ===
SCRIPT_BASENAME=$(basename "$0" .sh)
LOG_FILE="${LOG_DIR_BASE}/${SCRIPT_BASENAME}_${WORLD_KEY}.log"
LOCK_FILE="${LOCK_FILE_BASE_DIR}/${SCRIPT_BASENAME}_${WORLD_KEY}.lock"
# Pfade zu den Quell- und Zieldateien
AREAS_DAT_SOURCE_PATH="${CURRENT_MINETEST_WORLD_DATA_PATH}areas.dat"
AREAS_JSON_TARGET_DIR="${WEB_ROOT_PATH}/${WEB_MAPS_BASE_SUBDIR}/${WORLD_KEY}"
AREAS_JSON_FILE_PATH="${AREAS_JSON_TARGET_DIR}/areas.json"
AREAS_JSON_TMP_FILE_PATH="${AREAS_JSON_FILE_PATH}.tmp"
# === Hauptlogik für eine einzelne Welt ===
exec 200>"$LOCK_FILE"
flock -n 200 || { log_message "${WORLD_KEY}" "Script ${SCRIPT_BASENAME}.sh ist bereits für diese Welt aktiv (Lock: ${LOCK_FILE}). Beende."; exit 1; }
trap 'rm -f "$LOCK_FILE"; log_message "${WORLD_KEY}" "Skript beendet."' EXIT
log_message "${WORLD_KEY}" "Script gestartet."
# Prüfen, ob die Quelldatei existiert
if [ ! -f "$AREAS_DAT_SOURCE_PATH" ]; then
log_message "${WORLD_KEY}" "INFO: Quelldatei ${AREAS_DAT_SOURCE_PATH} nicht gefunden. Erstelle leere areas.json."
mkdir -p "$AREAS_JSON_TARGET_DIR"
echo "{}" > "$AREAS_JSON_FILE_PATH"
exit 0
fi
log_message "${WORLD_KEY}" "Lese ${AREAS_DAT_SOURCE_PATH} und transformiere zu hierarchischem JSON..."
jq '
(map(select(.id != null)) | map(.sub_areas = []) | INDEX(.id | tostring)) as $parents
| (map(select(.parent != null)) | group_by(.parent)) as $children_grouped
| reduce $children_grouped[] as $group (
$parents;
.[($group[0].parent|tostring)].sub_areas = $group
)
' "$AREAS_DAT_SOURCE_PATH" > "$AREAS_JSON_TMP_FILE_PATH"
if [ $? -ne 0 ]; then
log_message "${WORLD_KEY}" "FEHLER: jq-Transformation fehlgeschlagen. Prüfen Sie die JSON-Struktur in areas.dat."
rm -f "$AREAS_JSON_TMP_FILE_PATH"
exit 1
fi
# Temporäre Datei an den Zielort verschieben
mkdir -p "$AREAS_JSON_TARGET_DIR"
mv "$AREAS_JSON_TMP_FILE_PATH" "$AREAS_JSON_FILE_PATH"
log_message "${WORLD_KEY}" "areas.json erfolgreich synchronisiert nach ${AREAS_JSON_FILE_PATH}"
exit 0

View file

@ -9,43 +9,11 @@ else
exit 1 exit 1
fi fi
# === Logging Funktion (früh definieren für Wrapper-Logik) ===
LOG_FILE_BASE="${LOG_DIR_BASE}/$(basename "$0" .sh)"
log_message() {
local key="${1:-main}"
local msg="$2"
local log_target="${LOG_FILE_BASE}.log"
[ "$key" != "main" ] && log_target="${LOG_FILE_BASE}_${key}.log"
local message_to_log; message_to_log="$(date '+%Y-%m-%d %H:%M:%S') - [${key}] - ${msg}"
echo "${message_to_log}" | tee -a "$log_target"
}
# --- Wrapper-Logik zur Verarbeitung aller Welten, wenn kein Argument übergeben wird ---
if [ -z "$1" ]; then
log_message "main" "Kein spezifischer Welt-Schlüssel angegeben. Verarbeite alle Welten mit web.conf..."
shopt -s nullglob
for world_dir in "${MINETESTMAPPER_WORLD_DATA_BASE_PATH}"/*/; do
if [ -f "${world_dir}web.conf" ]; then
world_key_to_process=$(basename "$world_dir")
log_message "main" "--- Starte Durchlauf für '${world_key_to_process}' ---"
# Rufe das Skript für die gefundene Welt rekursiv auf
bash "$0" "$world_key_to_process"
fi
done
shopt -u nullglob
log_message "main" "Alle Welten verarbeitet."
exit 0
fi
# #############################################################################
# Ab hier beginnt die Logik für eine EINZELNE Welt
# #############################################################################
# Prüfe Abhängigkeiten, bevor irgendetwas anderes passiert # Prüfe Abhängigkeiten, bevor irgendetwas anderes passiert
/opt/luweb/check_dependencies.sh || exit 1 /opt/luweb/check_dependencies.sh || exit 1
WORLD_KEY=$1 # Welt-Schlüssel (Verzeichnisname) aus Argument oder Standardwert
WORLD_KEY="${1:-$DEFAULT_WORLD_NAME_KEY}"
CURRENT_MINETEST_WORLD_DATA_PATH="${MINETESTMAPPER_WORLD_DATA_BASE_PATH}${WORLD_KEY}/" CURRENT_MINETEST_WORLD_DATA_PATH="${MINETESTMAPPER_WORLD_DATA_BASE_PATH}${WORLD_KEY}/"
# === Abgeleitete Variablen === # === Abgeleitete Variablen ===
@ -63,31 +31,44 @@ PLAYERS_JSON_TMP_FILE_PATH="${PLAYERS_JSON_FILE_PATH}.tmp"
# SQLite-Befehl # SQLite-Befehl
SQLITE_CMD="sqlite3 -readonly" SQLITE_CMD="sqlite3 -readonly"
# === Hauptlogik für eine einzelne Welt === # === Logging Funktion ===
exec 200>"$LOCK_FILE" log_message() {
flock -n 200 || { log_message "${WORLD_KEY}" "Script ${SCRIPT_BASENAME}.sh ist bereits für diese Welt aktiv (Lock: ${LOCK_FILE}). Beende."; exit 1; } local message_to_log; message_to_log="$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - $1"
trap 'rm -f "$LOCK_FILE"; log_message "${WORLD_KEY}" "Skript beendet."' EXIT echo "${message_to_log}" | tee -a "$LOG_FILE"
log_message "${WORLD_KEY}" "Script gestartet." }
# === Hauptlogik ===
exec 200>"$LOCK_FILE"
flock -n 200 || { log_message "Script ${SCRIPT_BASENAME}.sh ist bereits aktiv (Lock: ${LOCK_FILE}). Beende."; exit 1; }
trap 'rm -f "$LOCK_FILE"; log_message "Skript beendet."' EXIT
mkdir -p "$LOG_DIR_BASE"
log_message "Script gestartet für Welt: ${WORLD_KEY}"
# Prüfen, ob die Datenbanken existieren
if [ ! -f "$PLAYERS_DB_PATH" ] || [ ! -f "$AUTH_DB_PATH" ]; then if [ ! -f "$PLAYERS_DB_PATH" ] || [ ! -f "$AUTH_DB_PATH" ]; then
log_message "${WORLD_KEY}" "FEHLER: players.sqlite oder auth.sqlite nicht gefunden. Pfade prüfen:" log_message "FEHLER: players.sqlite oder auth.sqlite nicht gefunden. Pfade prüfen:"
log_message "${WORLD_KEY}" "-> ${PLAYERS_DB_PATH}" log_message "-> ${PLAYERS_DB_PATH}"
log_message "${WORLD_KEY}" "-> ${AUTH_DB_PATH}" log_message "-> ${AUTH_DB_PATH}"
exit 1 exit 1
fi fi
# Beginne mit dem Schreiben der temporären JSON-Datei
echo "{" > "$PLAYERS_JSON_TMP_FILE_PATH" echo "{" > "$PLAYERS_JSON_TMP_FILE_PATH"
first_entry=true first_entry=true
# Lese alle Spieler aus der auth-Datenbank (ID, Name, Letzter Login)
player_list_query="SELECT id, name, last_login FROM auth;" player_list_query="SELECT id, name, last_login FROM auth;"
$SQLITE_CMD "$AUTH_DB_PATH" "$player_list_query" | while IFS='|' read -r player_id name last_login; do $SQLITE_CMD "$AUTH_DB_PATH" "$player_list_query" | while IFS='|' read -r player_id name last_login; do
log_message "${WORLD_KEY}" "Verarbeite Spieler: ${name} (ID: ${player_id})" log_message "Verarbeite Spieler: ${name} (ID: ${player_id})"
player_data_query="SELECT posX, posY, posZ, hp, breath, pitch, yaw, creation_date FROM player WHERE name = '${name}';" player_data_query="SELECT posX, posY, posZ, hp, breath, pitch, yaw, creation_date FROM player WHERE name = '${name}';"
player_data=$($SQLITE_CMD "$PLAYERS_DB_PATH" "$player_data_query") player_data=$($SQLITE_CMD "$PLAYERS_DB_PATH" "$player_data_query")
if [ -z "$player_data" ]; then if [ -z "$player_data" ]; then
log_message "${WORLD_KEY}" "WARNUNG: Spieler '${name}' hat keine Positionsdaten in players.sqlite. Wird übersprungen." log_message "WARNUNG: Spieler '${name}' hat keine Positionsdaten in players.sqlite. Wird übersprungen."
continue continue
fi fi
@ -106,12 +87,14 @@ $SQLITE_CMD "$AUTH_DB_PATH" "$player_list_query" | while IFS='|' read -r player_
privs_query="SELECT privilege FROM user_privileges WHERE id = ${player_id};" privs_query="SELECT privilege FROM user_privileges WHERE id = ${player_id};"
privs_string=$($SQLITE_CMD "$AUTH_DB_PATH" "$privs_query" | tr '\n' ',' | sed 's/,$//') privs_string=$($SQLITE_CMD "$AUTH_DB_PATH" "$privs_query" | tr '\n' ',' | sed 's/,$//')
# Komma vor dem Eintrag hinzufügen, außer beim ersten
if [ "$first_entry" = true ]; then if [ "$first_entry" = true ]; then
first_entry=false first_entry=false
else else
echo "," >> "$PLAYERS_JSON_TMP_FILE_PATH" echo "," >> "$PLAYERS_JSON_TMP_FILE_PATH"
fi fi
# JSON-Objekt für den Spieler erstellen und in die Datei schreiben
printf ' "%s": {\n' "$player_id" >> "$PLAYERS_JSON_TMP_FILE_PATH" printf ' "%s": {\n' "$player_id" >> "$PLAYERS_JSON_TMP_FILE_PATH"
printf ' "name": "%s",\n' "$name" >> "$PLAYERS_JSON_TMP_FILE_PATH" printf ' "name": "%s",\n' "$name" >> "$PLAYERS_JSON_TMP_FILE_PATH"
printf ' "pitch": %d,\n' "$pitch_rounded" >> "$PLAYERS_JSON_TMP_FILE_PATH" printf ' "pitch": %d,\n' "$pitch_rounded" >> "$PLAYERS_JSON_TMP_FILE_PATH"
@ -124,17 +107,21 @@ $SQLITE_CMD "$AUTH_DB_PATH" "$player_list_query" | while IFS='|' read -r player_
printf ' "stamina": %d,\n' "$stamina_rounded" >> "$PLAYERS_JSON_TMP_FILE_PATH" printf ' "stamina": %d,\n' "$stamina_rounded" >> "$PLAYERS_JSON_TMP_FILE_PATH"
printf ' "creation_date": "%s",\n' "$(echo "$creation_date" | sed 's/ /T/')Z" >> "$PLAYERS_JSON_TMP_FILE_PATH" printf ' "creation_date": "%s",\n' "$(echo "$creation_date" | sed 's/ /T/')Z" >> "$PLAYERS_JSON_TMP_FILE_PATH"
printf ' "last_login": %s,\n' "$last_login" >> "$PLAYERS_JSON_TMP_FILE_PATH" printf ' "last_login": %s,\n' "$last_login" >> "$PLAYERS_JSON_TMP_FILE_PATH"
# KORREKTUR: Kein Komma nach dem letzten Element
printf ' "privilege": "%s"\n' "$privs_string" >> "$PLAYERS_JSON_TMP_FILE_PATH" printf ' "privilege": "%s"\n' "$privs_string" >> "$PLAYERS_JSON_TMP_FILE_PATH"
printf ' }' >> "$PLAYERS_JSON_TMP_FILE_PATH" printf ' }' >> "$PLAYERS_JSON_TMP_FILE_PATH"
done done
# JSON-Datei abschließen
# KORREKTUR: Die überflüssige schließende Klammer wird hier entfernt
echo "" >> "$PLAYERS_JSON_TMP_FILE_PATH" echo "" >> "$PLAYERS_JSON_TMP_FILE_PATH"
echo "}" >> "$PLAYERS_JSON_TMP_FILE_PATH" echo "}" >> "$PLAYERS_JSON_TMP_FILE_PATH"
# Temporäre Datei an den Zielort verschieben
mkdir -p "$PLAYERS_JSON_TARGET_DIR" mkdir -p "$PLAYERS_JSON_TARGET_DIR"
mv "$PLAYERS_JSON_TMP_FILE_PATH" "$PLAYERS_JSON_FILE_PATH" mv "$PLAYERS_JSON_TMP_FILE_PATH" "$PLAYERS_JSON_FILE_PATH"
log_message "${WORLD_KEY}" "players.txt erfolgreich synchronisiert nach ${PLAYERS_JSON_FILE_PATH}" log_message "players.txt erfolgreich synchronisiert nach ${PLAYERS_JSON_FILE_PATH}"
exit 0 exit 0