feat(map): Implement interactive area overlays

This major update introduces a complete system for visualizing protected areas on the OpenLayers map. It adds a new sync script to process area data and heavily modifies the frontend to support interactive, multi-layer display with custom popups.

Additionally, data-sync scripts were refactored to run globally on all configured worlds, simplifying cron automation.
This commit is contained in:
Rainer 2025-06-23 01:41:17 +02:00
parent fa94f0e23d
commit a225feef98
11 changed files with 568 additions and 687 deletions

116
README.md
View file

@ -7,51 +7,40 @@ This system is designed from the ground up to be modular, easily configurable, a
## ✨ Features ## ✨ Features
* **Automated Map Generation:** Leverages `minetestmapper` to create high-resolution PNG maps of your game worlds. * **Automated Map Generation:** Leverages `minetestmapper` to create high-resolution PNG maps of your game worlds.
* **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 [Leaflet.js](https://leafletjs.com/), powered by the generated map tiles.
* **Live Player Tracking:** Dynamically fetches and displays player locations as markers on the live map, including custom-styled popups and permanent name labels. * **Live Player Tracking:** Dynamically fetches and displays player locations as markers on the live map.
* **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 dropdown viewer 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 and a `web.conf`-file for every of your worlds. * **Flexible Configuration:** Configuration is easy with a central global config and a sub-config 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
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 that replaces `convert` (ImageMagick). * **ImageMagick:** Required for `identify` (to read image dimensions) and `convert` (to resize images).
* *Debian/Ubuntu Install:* `sudo apt-get install libvips-tools`
* **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 for reading SQLite databases, required by `sync_players.sh`. * **minetestmapper:** The executable used to render maps from world data. This must be placed within the project directory.
* *Debian/Ubuntu Install:* `sudo apt-get install sqlite3`
* **bc:** The "basic calculator" command-line tool, required for calculations in `sync_players.sh`.
* *Debian/Ubuntu Install:* `sudo apt-get install bc`
* **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).
* **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.
An included script (`check_dependencies.sh`) can automatically verify all important dependencies.
## ⚙️ Installation & Setup ## ⚙️ Installation & Setup
### 1. Project Files ### 1. Clone the Repository
Download the **latest build** from the [Releases-Page](https://git.geigernet.eu/rainer/luanti-web/releases) and extract it to a base directory on your server, such as `/opt/luweb/`. Download the **latest build** from the [Releases-Page](https://git.geigernet.eu/rainer/luanti-web/releases) and save it to your server's base directory, such as `/opt/luweb/`.
OR OR
Clone the Git repository to a base directory. Clone all project files to a base directory on your server.
```bash ```
git clone 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 chmod +x generate_map.sh generate_site.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
@ -61,47 +50,53 @@ The main configuration file is `config.sh`. You must edit this file to match you
**Key variables in `config.sh`:** **Key variables in `config.sh`:**
* `BASE_SCRIPT_DIR`: The root directory of the project (e.g., `/opt/luweb`). * `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. * `MINETESTMAPPER_WORLD_DATA_BASE_PATH`: The path to your Minetest/Luanti worlds' data directory (e.g., `<server-path>/worlds/`), docker compatible.
* `WEB_ROOT_PATH`: The document root of your website where the generated files will be placed (e.g., `/var/www/your-domain.com/web`). * `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`). * `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. For each world you want to feature on the website, a `web.conf` file must exist within that world's data directory (e.g., `<server-path>/worlds/my_world/web.conf`). This file allows you to override global defaults with world-specific settings.
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. A minimal `web.conf` could look like this and is automatically created for every detected world in your `MINETESTMAPPER_WORLD_DATA_BASE_PATH`:
```bash
# Display name for the world
WORLD_DISPLAY_NAME="My Creative World"
# Server connection details
SERVER_ADDRESS="your-server.com"
SERVER_PORT="30001"
# A short description for the world overview page
WORLD_SHORT_DESCRIPTION="A brief, catchy description of this world."
# A detailed HTML description for the world's detail page
WORLD_LONG_DESCRIPTION="<p>A longer description with <b>HTML</b> support.</p>"
```
## 📂 Directory Structure ## 📂 Directory Structure
The system now uses a modular structure to improve maintainability: The system expects the following directory structure:
```md ```md
/opt/luweb/ /opt/luweb/
├── config.sh ├── config.sh
├── generate_map.sh ├── generate_map.sh
├── generate_site.sh ├── generate_site.sh
├── check_server_status.sh
├── check_dependencies.sh
├── sync_players.sh
├── minetestmapper (executable) ├── minetestmapper (executable)
├── site_generator/ ├── site_generator/
│ ├── functions/
│ │ ├── 01_utils.sh
│ │ ├── 02_init.sh
│ │ ├── 03_html_helpers.sh
│ │ └── 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 │ │ ├── world_detail_page.template
│ │ ├── world_detail_archive.template
│ │ └── ... │ │ └── ...
│ └── examples/ │ └── examples/
│ └── web.conf.template │ └── web.conf.template
├── web_content/ ├── web_content/
│ ├── images/ │ ├── images/
│ │ └── players/
│ └── static/ │ └── static/
│ ├── startseite_content.html
│ └── ...
└── worldmaps_output/ └── worldmaps_output/
└── <world_name>/ └── <world_name>/
├── map.png ├── map.png
@ -110,37 +105,38 @@ The system now uses a modular structure to improve maintainability:
## 🚀 Usage ## 🚀 Usage
### 1. Map Generation The scripts are designed to be run from the command line, either manually or via automated jobs.
### 1. Map and Tile Generation
The `generate_map.sh` script creates the map, tiles, and archive images for a specific world. It must be called with the "world key" (the name of the world's directory) as an argument.
```bash ```bash
./generate_map.sh <world_key> # Generate map assets for the world located in the 'world' directory
./generate_map.sh world
``` ```
### 2. Website Generation ### 2. Website Generation
The `generate_site.sh` script builds the entire website (overview, detail pages, etc.). It scans all configured world directories and creates a detail page for each one.
```bash ```bash
# Generate the complete website
./generate_site.sh ./generate_site.sh
``` ```
### 3. Status & Player Sync ### 3. Automation (Cronjob)
```bash
./check_server_status.sh
./sync_players.sh <world_key>
```
### 4. Automation (Cronjob) For fully automatic operation, setting up cronjobs is recommended.
**Example for `crontab -e`:** **Example for `crontab -e`:**
```bash ```bash
# Update player data every minute # Generate map assets for the 'world' every 30 minutes
* * * * * /opt/luweb/sync_players.sh world >> /var/log/luweb/cron.log 2>&1 */30 * * * * /opt/luweb/generate_map.sh world >> /var/log/luweb/cron.log 2>&1
# Check server online status every 5 minutes # Re-build the website every 12 hours
*/5 * * * * /opt/luweb/check_server_status.sh >> /var/log/luweb/cron.log 2>&1 * */12 * * * /opt/luweb/generate_site.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
@ -165,5 +161,5 @@ 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 OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE. OR OTHER DEALINGS IN THE SOFTWARE.
## 👤 Autoren ## 👤 Authors
* **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
# KORREKTUR: 'convert' und 'identify' entfernt, 'vips' hinzugefügt # HINZUGEFÜGT: sqlite3 und jq
declare -a runtime_deps_in_path=("gdal2tiles.py" "vips" "ss") declare -a runtime_deps_in_path=("gdal2tiles.py" "vips" "ss" "bc" "sqlite3" "jq")
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,6 +79,9 @@ 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,3 +1402,44 @@ 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,30 +1,62 @@
#!/bin/bash #!/bin/bash
# Lade globale Konfiguration # Lade globale Konfiguration
GLOBAL_CONFIG_FILE="$(dirname "$0")/config.sh" CONFIG_FILE_PATH="$(dirname "$0")/config.sh"
if [ -f "$GLOBAL_CONFIG_FILE" ]; then if [ -f "$CONFIG_FILE_PATH" ]; then
source "$GLOBAL_CONFIG_FILE" source "$CONFIG_FILE_PATH"
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
# Welt-Schlüssel (Verzeichnisname) aus Argument oder Standardwert WORLD_KEY=$1
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
echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - FEHLER: Das Welt-Datenverzeichnis '${CURRENT_MINETEST_WORLD_DATA_PATH}' wurde nicht gefunden!" log_message "${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
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!" log_message "${WORLD_KEY}" "FEHLER: Die Datei 'world.mt' wurde im Verzeichnis '${CURRENT_MINETEST_WORLD_DATA_PATH}' nicht gefunden!"
exit 1 exit 1
fi fi
@ -32,18 +64,16 @@ 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="$DEFAULT_MM_CFG_DRAWORIGIN" MM_CFG_DRAWALPHA="$DEFAULT_MM_CFG_DRAWALPHA"; MM_CFG_DRAWORIGIN="false"
MM_CFG_DRAWPLAYERS="$DEFAULT_MM_CFG_DRAWPLAYERS"; MM_CFG_DRAWSCALE="$DEFAULT_MM_CFG_DRAWSCALE" MM_CFG_DRAWPLAYERS="$DEFAULT_MM_CFG_DRAWPLAYERS"; MM_CFG_DRAWSCALE="false"
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
echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - Lade Web-Konfiguration aus ${WORLD_WEB_CONFIG_FILE}" log_message "${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 ===
@ -62,176 +92,107 @@ 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 "Starte Archivbereinigung für Welt '${WORLD_KEY}' im Pfad '${ARCHIVE_BASE_WEB_PATH}'..." log_message "${WORLD_KEY}" "Starte Archivbereinigung für Welt '${WORLD_KEY}' im Pfad '${ARCHIVE_BASE_WEB_PATH}'..."
if [ ! -d "$ARCHIVE_BASE_WEB_PATH" ]; then log_message "Archiv-Basispfad ${ARCHIVE_BASE_WEB_PATH} nicht gefunden."; return; fi if [ ! -d "$ARCHIVE_BASE_WEB_PATH" ]; then log_message "${WORLD_KEY}" "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 "Archivbereinigung: Behalte tägliche Bilder bis einschl. ${cutoff_date_14_days}. Ältere nur Montage." log_message "${WORLD_KEY}" "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 + 1)) ((images_processed++))
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 "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 "${WORLD_KEY}" "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 "BEHALTE (<=14 Tage): ${archive_file_path}" if [ "$img_date_seconds" -ge "$cutoff_seconds_14_days" ]; then log_message "${WORLD_KEY}" "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 "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 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 log_message "WARNUNG: Pfad '${archive_file_path}' passt nicht zu JJJJ/MM/TT.png."; fi else log_message "${WORLD_KEY}" "WARNUNG: Pfad '${archive_file_path}' passt nicht zu JJJJ/MM/TT.png."; fi
done done
log_message "Archivbereinigung: ${images_processed} geprüft, ${images_deleted} gelöscht." log_message "${WORLD_KEY}" "Archivbereinigung: ${images_processed} geprüft, ${images_deleted} gelöscht."
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 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
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 === # === Hauptlogik für eine einzelne Welt ===
exec 200>"$LOCK_FILE" exec 200>"$LOCK_FILE"
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; } 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 "Skript ${SCRIPT_BASENAME}.sh beendet."' EXIT trap 'rm -f "$LOCK_FILE"; log_message "${WORLD_KEY}" "Skript beendet."' EXIT
mkdir -p "$LOG_DIR_BASE"; log_message "Skript ${SCRIPT_BASENAME}.sh gestartet für Welt-Schlüssel: ${WORLD_KEY}" log_message "${WORLD_KEY}" "Skript gestartet."
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
if [ ! -x "$MINETESTMAPPER_PATH" ]; then log_message "FEHLER: minetestmapper (${MINETESTMAPPER_PATH}) nicht ausführbar."; exit 1; fi log_message "${WORLD_KEY}" "Starte minetestmapper zur Kartengenerierung..."
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}'"; MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --playercolor '${MM_OPT_PLAYERCOLOR}'" MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --origincolor '${MM_OPT_ORIGINCOLOR}' --playercolor '${MM_OPT_PLAYERCOLOR}'"
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} --scalecolor '${MM_OPT_SCALECOLOR}' --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_TO_EVAL="'${MINETESTMAPPER_PATH}' -i '${CURRENT_MINETEST_WORLD_DATA_PATH}' -o '${RAW_MAP_ABSOLUTE_PATH}' ${MM_ALL_OPTIONS_STR}" MAP_GENERATION_COMMAND="'${MINETESTMAPPER_PATH}' -i '${CURRENT_MINETEST_WORLD_DATA_PATH}' -o '${RAW_MAP_ABSOLUTE_PATH}' ${MM_ALL_OPTIONS_STR}"
log_message "Starte minetestmapper zur Kartengenerierung (Optionen: ${MM_ALL_OPTIONS_STR})." MAPPER_RUN_OUTPUT_CAPTURE_FILE=$(mktemp)
MAPPER_RUN_OUTPUT_CAPTURE_FILE=$(mktemp); MAPPER_EXIT_STATUS=0 (set -o pipefail; eval "${MAP_GENERATION_COMMAND}" 2>&1 | tee -a "$LOG_FILE" > "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"); MAPPER_EXIT_STATUS=$?
(set -o pipefail; eval "${MAP_GENERATION_COMMAND_TO_EVAL}" 2>&1 | tee -a "$LOG_FILE" > "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"); MAPPER_EXIT_STATUS=$? 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
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 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 [ ! -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 "${WORLD_KEY}" "map.png erfolgreich generiert."
log_message "${RAW_MAP_FILENAME} erfolgreich generiert nach ${RAW_MAP_ABSOLUTE_PATH}."
log_message "Erstelle map_info.txt..."
MAP_DIMENSIONS=""; MAP_EXTENT=""
if ! command -v identify &> /dev/null; then
log_message "WARNUNG: 'identify' (Teil von ImageMagick) nicht gefunden. map_dimension kann nicht ermittelt werden."
else
MAP_DIMENSIONS=$(identify -format "%wx%h" "$RAW_MAP_ABSOLUTE_PATH" 2>/dev/null)
fi
EXTENT_COMMAND_TO_EVAL="'${MINETESTMAPPER_PATH}' -i '${CURRENT_MINETEST_WORLD_DATA_PATH}' --extent ${MM_ALL_OPTIONS_STR}"
log_message "Ermittle Map Extent mit: --extent"
MAP_EXTENT_OUTPUT=$(eval "${EXTENT_COMMAND_TO_EVAL}" 2>/dev/null)
if [ -n "$MAP_EXTENT_OUTPUT" ]; then
MAP_EXTENT=$(echo "$MAP_EXTENT_OUTPUT" | sed 's/Map extent: //')
fi
# 2. Erstelle map_info.txt
log_message "${WORLD_KEY}" "Erstelle map_info.txt..."
MAP_DIMENSIONS=$(identify -format "%wx%h" "$RAW_MAP_ABSOLUTE_PATH" 2>/dev/null)
EXTENT_COMMAND="'${MINETESTMAPPER_PATH}' -i '${CURRENT_MINETEST_WORLD_DATA_PATH}' --extent ${MM_ALL_OPTIONS_STR}"
MAP_EXTENT=$(eval "${EXTENT_COMMAND}" 2>/dev/null | sed 's/Map extent: //')
if [ -n "$MAP_DIMENSIONS" ] && [ -n "$MAP_EXTENT" ]; then if [ -n "$MAP_DIMENSIONS" ] && [ -n "$MAP_EXTENT" ]; then
{ { echo "map_dimension=${MAP_DIMENSIONS}"; echo "map_extent=${MAP_EXTENT}"; } > "$MAP_INFO_FILE_ABSOLUTE_PATH"
echo "map_dimension=${MAP_DIMENSIONS}" log_message "${WORLD_KEY}" "map_info.txt erstellt: Dim=${MAP_DIMENSIONS}, Extent=${MAP_EXTENT}"
echo "map_extent=${MAP_EXTENT}"
} > "$MAP_INFO_FILE_ABSOLUTE_PATH"
log_message "map_info.txt erfolgreich erstellt: Dimension=${MAP_DIMENSIONS}, Extent=${MAP_EXTENT}"
else else
log_message "FEHLER: map_info.txt konnte nicht erstellt werden, da Informationen fehlen!" log_message "${WORLD_KEY}" "FEHLER: map_info.txt konnte nicht erstellt werden."
[ -z "$MAP_DIMENSIONS" ] && log_message "-> Bild-Dimensionen konnten nicht ermittelt werden."
[ -z "$MAP_EXTENT" ] && log_message "-> Karten-Extent konnte nicht ermittelt werden."
fi fi
if [ -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE" ]; then # 3. Verarbeite unknown_nodes.txt
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" > "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new"
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 "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new" ]; then
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."; log_message "${WORLD_KEY}" "Neue 'Unknown nodes' gefunden. Füge zu bestehender Datei hinzu."
else log_message "Keine neuen 'Unknown nodes' gefunden."; fi; rm -f "$TEMP_NEW_UNKNOWN_NODES_FILE" cat "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new" >> "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}"
else log_message "WARNUNG: minetestmapper-Ausgabe nicht verarbeitbar."; fi sort -u "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}" -o "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}"
rm -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE" fi
rm -f "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new" "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"
# === Web-Vorschaukarte (verkleinert) erstellen mit VIPS === # 4. Erzeuge Web-Vorschaukarte mit vips
log_message "Erzeuge Web-Version von ${RAW_MAP_FILENAME} (max ${RESIZED_MAX_DIMENSION}px) mit 'vips' nach ${WEB_MAP_PNG_FULL_PATH}..." log_message "${WORLD_KEY}" "Erzeuge Web-Version von map.png (max ${RESIZED_MAX_DIMENSION}px) mit 'vips'..."
mkdir -p "$(dirname "$WEB_MAP_PNG_FULL_PATH")" mkdir -p "$(dirname "$WEB_MAP_PNG_FULL_PATH")"
if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then (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")
log_message "FEHLER: Quelldatei ${RAW_MAP_ABSOLUTE_PATH} für Web-Vorschau nicht gefunden!" if [ $? -ne 0 ]; then log_message "${WORLD_KEY}" "FEHLER: Skalierung mit 'vips' fehlgeschlagen."; else log_message "${WORLD_KEY}" "Verkleinerte Web-map.png erstellt."; fi
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
# === Tiles generieren === # 5. Generiere Kacheln
log_message "Generiere Kacheln (Zoom: ${GDAL2TILES_ZOOM_LEVELS}) nach ${TILES_FULL_OUTPUT_PATH}..." log_message "${WORLD_KEY}" "Generiere Kacheln (Zoom: ${GDAL2TILES_ZOOM_LEVELS})..."
if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then log_message "FEHLER: Quelldatei ${RAW_MAP_ABSOLUTE_PATH} für Tiling nicht gefunden!"; else TEMP_TILES_DIR="${TILES_FULL_OUTPUT_PATH}_temp_$(date +%s)"; rm -rf "$TEMP_TILES_DIR";
TEMP_TILES_DIR="${TILES_FULL_OUTPUT_PATH}_temp_$(date +%s)"; rm -rf "$TEMP_TILES_DIR"; mkdir -p "$(dirname "$TILES_FULL_OUTPUT_PATH")" (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 "Führe gdal2tiles.py aus..." if [ $? -ne 0 ]; then log_message "${WORLD_KEY}" "FEHLER: gdal2tiles.py fehlgeschlagen."; rm -rf "$TEMP_TILES_DIR"; exit 1; fi
# KORREKTUR: -r "near" statt "nearest" rm -rf "$TILES_FULL_OUTPUT_PATH"
(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") if ! mv "$TEMP_TILES_DIR" "$TILES_FULL_OUTPUT_PATH"; then log_message "${WORLD_KEY}" "FEHLER: Verschieben der Kacheln fehlgeschlagen."; exit 1; fi
if [ $? -ne 0 ]; then log_message "FEHLER: gdal2tiles.py fehlgeschlagen."; rm -rf "$TEMP_TILES_DIR"; exit 1; fi log_message "${WORLD_KEY}" "Kacheln erfolgreich generiert."
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
# === Archivbereinigung === # 6. Archiv-Management
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 "Archivbild ${ARCHIVE_DAILY_FILE_PATH} existiert noch nicht. Versuche Erstellung..." log_message "${WORLD_KEY}" "Erzeuge Archivbild für ${ARCHIVE_DAILY_FILE_PATH}..."
if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then mkdir -p "$ARCHIVE_DAILY_TARGET_DIR"
log_message "FEHLER: Quelldatei (${RAW_MAP_ABSOLUTE_PATH}) nicht gefunden! Archiv nicht erstellt." (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")
else if [ $? -eq 0 ]; then log_message "${WORLD_KEY}" "Verkleinertes Archivbild erstellt."; else log_message "${WORLD_KEY}" "FEHLER: Archivbild nicht erstellt.";fi
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
# === Status- und Info-Dateien im Webverzeichnis === # 7. Finale Info-Dateien kopieren
log_message "Erstelle Status- und Info-Dateien im Webverzeichnis ${WEB_CURRENT_WORLD_DIR}..." log_message "${WORLD_KEY}" "Kopiere finale Info-Dateien ins Web-Verzeichnis..."
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" && log_message "last_update.txt erstellt." || log_message "FEHLER: last_update.txt nicht erstellt." echo "$(date '+%Y-%m-%d %H:%M:%S %Z')" > "${WEB_CURRENT_WORLD_DIR}/last_update.txt"
if [ -f "$UNKNOWN_NODES_FILE_ABSOLUTE_PATH" ]; then cp "$UNKNOWN_NODES_FILE_ABSOLUTE_PATH" "${WEB_CURRENT_WORLD_DIR}/unknown_nodes.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 cp "$MAP_INFO_FILE_ABSOLUTE_PATH" "${WEB_CURRENT_WORLD_DIR}/map_info.txt"
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,15 +59,6 @@ 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,7 +30,6 @@ 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")
@ -172,6 +171,7 @@ 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,6 +208,7 @@ 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; } /* HINZUGEFÜGT */ .online-status-badge.unknown { background-color: #808080; }
/* 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,68 +114,17 @@ 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 { .admin-grid { display: flex; flex-wrap: wrap; gap: 15px; }
display: flex; .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; }
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 { .contact-links { margin-top: 10px; padding-top: 10px; border-top: 1px solid #555; display: flex; gap: 8px; flex-wrap: wrap; }
margin-top: 10px; .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; }
padding-top: 10px; .contact-button:hover { background-color: #666; color: #FFA500; text-decoration: none; }
border-top: 1px solid #555; .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; }
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; }
@ -184,16 +133,7 @@ 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 { .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; }
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; }
@ -201,22 +141,12 @@ 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 { .privilege-separator { height: 1px; background-color: #555; border: 0; margin: 8px 0; }
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 { .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; }
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; }
@ -236,315 +166,50 @@ 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 { .responsive-nav { position: relative; margin-bottom: 15px; }
position: relative; .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; }
margin-bottom: 15px; .burger-menu:hover { background-color: #666; }
} .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 { .section-header-flex { display: flex; justify-content: space-between; align-items: center; }
display: flex; .map-toggle-controls { display: flex; gap: 5px; }
justify-content: space-between; .map-toggle-button { background-color: #555; color: #ddd; border: 1px solid #666; padding: 3px 8px; font-size: 0.85em; border-radius: 3px; cursor: pointer; }
align-items: center; .map-toggle-button:hover { background-color: #666; }
} .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 { .filter-container { position: relative; }
position: relative; .filter-dropdown-btn { background-color: #555; color: #ddd; border: 1px solid #666; padding: 3px 8px; font-size: 0.85em; border-radius: 3px; cursor: pointer; }
} .filter-dropdown-btn:hover { background-color: #666; }
.filter-dropdown-btn { .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); }
background-color: #555; .filter-dropdown.show { display: block; }
color: #ddd; .filter-option { display: flex; align-items: center; margin-bottom: 8px; cursor: pointer; font-size: 0.9em; font-weight: normal; color: #ddd; }
border: 1px solid #666; .filter-option:last-child { margin-bottom: 0; }
padding: 3px 8px; .filter-option input[type="checkbox"] { margin-right: 8px; }
font-size: 0.85em; .filter-option .icon { font-size: 1.2em; width: 20px; text-align: center; margin-right: 8px; }
border-radius: 3px; .filter-option .status-dot { margin-left: 0; margin-right: 8px; }
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 { .collapsible-text { max-height: 250px; overflow: hidden; position: relative; transition: max-height 0.5s ease-in-out; }
max-height: 250px; .collapsible-text.expanded { max-height: 10000px; }
overflow: hidden; .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; }
position: relative; .collapsible-text.expanded::after { opacity: 0; }
transition: max-height 0.5s ease-in-out; .read-more-container { text-align: right; margin-top: 5px; }
} 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 { .close-button-container { position: absolute; top: 15px; right: 15px; z-index: 10; }
position: absolute; .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; }
top: 15px; .close-button:hover { background-color: #C0392B; color: white; text-decoration: none; }
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 { .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; }
display: none; .scroll-to-top-btn:hover { background-color: #666; text-decoration: none; }
position: fixed; .scroll-to-top-btn.show { display: block; }
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 { .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; }
position: absolute; .ol-popup:after, .ol-popup:before { top: 100%; border: solid transparent; content: " "; height: 0; width: 0; position: absolute; pointer-events: none; }
background-color: #303030; .ol-popup:after { border-top-color: #303030; border-width: 10px; left: 48px; margin-left: -10px; }
box-shadow: 0 1px 4px rgba(0,0,0,0.4); .ol-popup:before { border-top-color: #555; border-width: 11px; left: 48px; margin-left: -11px; }
padding: 1px; .ol-popup-closer { text-decoration: none; position: absolute; top: 2px; right: 8px; color: #ddd; font-size: 1.5em; font-weight: bold; }
border-radius: 5px; .ol-popup-closer:hover { color: #fff; }
bottom: 12px; .ol-popup-content { font-size: 13px; line-height: 1.5; padding: 10px; margin: 0; }
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;}
@ -554,3 +219,68 @@ a.read-more-link {
.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,6 +9,9 @@
<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,32 +26,44 @@
// 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();
fetch(mapInfoPath) Promise.all([
.then(response => { fetch(mapInfoPath).then(res => { if (!res.ok) throw new Error('map_info.txt'); return res.text(); }),
if (!response.ok) throw new Error('map_info.txt nicht gefunden (Status: ' + response.status + ')'); fetch(areasPath).then(res => { if (!res.ok) return {}; return res.json(); })
return response.text(); ]).then(([mapInfoText, areaData]) => {
})
.then(mapInfoText => {
const mapInfo = (text => { const mapInfo = (text => {
const data = {}; const data = {};
text.split('\n').forEach(line => { text.split('\n').forEach(line => {
@ -60,14 +72,12 @@ 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);
@ -78,62 +88,60 @@ 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%%;
const viewResolutions = [...sourceResolutions]; // Kopie für die Ansicht // HINZUGEFÜGT: Logik für digitalen Zoom wiederhergestellt
const digitalZoomLevels = 3; const maxNativeZoom = sourceResolutions.length -1;
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({ const projection = new ol.proj.Projection({ code: 'pixel-map-%%current_world_key%%', units: 'pixels', extent: extent });
code: 'pixel-map-%%current_world_key%%', const tileGrid = new ol.tilegrid.TileGrid({ origin: ol.extent.getTopLeft(extent), resolutions: sourceResolutions });
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 '';
// OpenLayers wählt jetzt selbst die beste verfügbare Kachel aus. let z = tileCoord[0]; let x = tileCoord[1]; let y = tileCoord[2];
// Die URL-Funktion kann wieder einfach sein. if (z > maxNativeZoom) {
return ('/%%web_tiles_rel_path%%/' + tileCoord[0] + '/' + tileCoord[1] + '/' + tileCoord[2] + '.png?v=%%CACHE_BUSTER%%'); const zoomDiff = z - maxNativeZoom;
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 }); const tileLayer = new ol.layer.Tile({ source: tileSource, title: 'Basiskarte', type: 'base' });
const playerLayer = new ol.layer.Vector({ source: window.playerMarkerSource_%%current_world_key%%, style: f => f.get('style') }); 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 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 view = new ol.View({ const overlayLayers = new ol.layer.Group({ title: 'Overlays', layers: [subAreaLayer, parentAreaLayer, playerLayer] });
projection: projection, const view = new ol.View({ projection: projection, center: ol.extent.getCenter(extent), resolutions: viewResolutions });
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() ]; const mapControls = [ new ol.control.Zoom(), new ol.control.Rotate(), new ol.control.Attribution({ collapsible: false }), new ol.control.FullScreen(), new ol.control.LayerSwitcher() ];
window.olMap_%%current_world_key%% = new ol.Map({ window.olMap_%%current_world_key%% = new ol.Map({
controls: mapControls, controls: mapControls,
layers: [tileLayer, playerLayer], layers: [tileLayer, overlayLayers],
overlays: [overlay], overlays: [overlay],
target: mapContainer, target: mapContainer,
view: view view: view
@ -142,17 +150,31 @@ 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) {
const feature = map.forEachFeatureAtPixel(evt.pixel, f => f); // Reset
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);
if (feature) { const playerData = playerFeature.get('playerData');
const playerData = feature.get('playerData');
if (playerData) { if (playerData) {
const statusDotClass = feature.get('statusDotClass'); const statusDotClass = playerFeature.get('statusDotClass');
const lastLoginFormatted = feature.get('lastLoginFormatted'); const lastLoginFormatted = playerFeature.get('lastLoginFormatted');
const popupHTML = ` const popupHTML = `
<div class='popup-player-box'> <div class='popup-player-box'>
<div class="player-header"> <div class="player-header">
@ -172,8 +194,39 @@ document.addEventListener('DOMContentLoaded', function() {
</div> </div>
</div>`; </div>`;
popupContent.innerHTML = popupHTML; popupContent.innerHTML = popupHTML;
overlay.setPosition(feature.getGeometry().getCoordinates()); overlay.setPosition(playerFeature.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);
} }
}); });
@ -183,7 +236,7 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}) })
.catch(error => { .catch(error => {
console.error("Fehler beim Initialisieren der Karte:", error); console.error("Fehler beim Initialisieren der Karte oder der Grundstücksdaten:", 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>";
}); });
@ -191,19 +244,9 @@ 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', () => { 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.classList.add('active'); archiveBtn.classList.remove('active'); archiveBtn.addEventListener('click', () => { archiveContainer.style.display = 'block'; liveContainer.style.display = 'none'; });
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>

99
sync_areas.sh Executable file
View file

@ -0,0 +1,99 @@
#!/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,11 +9,43 @@ 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
# Welt-Schlüssel (Verzeichnisname) aus Argument oder Standardwert WORLD_KEY=$1
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 ===
@ -31,44 +63,31 @@ PLAYERS_JSON_TMP_FILE_PATH="${PLAYERS_JSON_FILE_PATH}.tmp"
# SQLite-Befehl # SQLite-Befehl
SQLITE_CMD="sqlite3 -readonly" SQLITE_CMD="sqlite3 -readonly"
# === Logging Funktion === # === Hauptlogik für eine einzelne Welt ===
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"
}
# === Hauptlogik ===
exec 200>"$LOCK_FILE" exec 200>"$LOCK_FILE"
flock -n 200 || { log_message "Script ${SCRIPT_BASENAME}.sh ist bereits aktiv (Lock: ${LOCK_FILE}). Beende."; exit 1; } 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 "Skript beendet."' EXIT trap 'rm -f "$LOCK_FILE"; log_message "${WORLD_KEY}" "Skript beendet."' EXIT
log_message "${WORLD_KEY}" "Script gestartet."
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 "FEHLER: players.sqlite oder auth.sqlite nicht gefunden. Pfade prüfen:" log_message "${WORLD_KEY}" "FEHLER: players.sqlite oder auth.sqlite nicht gefunden. Pfade prüfen:"
log_message "-> ${PLAYERS_DB_PATH}" log_message "${WORLD_KEY}" "-> ${PLAYERS_DB_PATH}"
log_message "-> ${AUTH_DB_PATH}" log_message "${WORLD_KEY}" "-> ${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 "Verarbeite Spieler: ${name} (ID: ${player_id})" log_message "${WORLD_KEY}" "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 "WARNUNG: Spieler '${name}' hat keine Positionsdaten in players.sqlite. Wird übersprungen." log_message "${WORLD_KEY}" "WARNUNG: Spieler '${name}' hat keine Positionsdaten in players.sqlite. Wird übersprungen."
continue continue
fi fi
@ -87,14 +106,12 @@ $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"
@ -107,21 +124,17 @@ $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 "players.txt erfolgreich synchronisiert nach ${PLAYERS_JSON_FILE_PATH}" log_message "${WORLD_KEY}" "players.txt erfolgreich synchronisiert nach ${PLAYERS_JSON_FILE_PATH}"
exit 0 exit 0