commit f6a5823439b02c351cd5e9f34c33f28e2cd7f60d Author: Fellowship Scholar Date: Sun Mar 29 20:07:56 2026 +0000 init commit diff --git a/.env b/.env new file mode 100644 index 0000000..8731d19 --- /dev/null +++ b/.env @@ -0,0 +1,10 @@ +CADDY_DOMAIN=fellowship-tutorial1-admin-14.fellowship.testingfantasy.com +JENKINS_DOMAIN=jenkins-fellowship-tutorial1-admin-14.fellowship.testingfantasy.com +IDE_DOMAIN=ide-fellowship-tutorial1-admin-14.fellowship.testingfantasy.com +GITEA_DOMAIN=gitea-fellowship-tutorial1-admin-14.fellowship.testingfantasy.com +MACHINE_NAME=fellowship-tutorial1-admin-14 +WORKSHOP_NAME=fellowship +ROUTE53_ZONE_ID=Z00868603TVDTV1H4G9BU +CADDYFILE_PATH=./caddy/Caddyfile.fellowship +FRONTEND_MODE=prod +WDS_SOCKET_PROTOCOL=wss diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1b1d439 --- /dev/null +++ b/.env.example @@ -0,0 +1,33 @@ +# Environment Configuration Template +# File: .env.example (DO NOT COMMIT ACTUAL .env FILES) +# +# Copy one of the environment templates below based on your use case: +# +# Production (EC2 host): cp .env.prod .env +# Local Development (IDE): Automatically set by code-server container +# OR manually: cp .env.local .env +# +# DO NOT commit .env files to git — they may contain secrets. +# See .gitignore for exclusion rules. + +############################################################################## +# PRODUCTION BUILD (EC2 Host Deployment) +############################################################################## + +COMPOSE_PROJECT_NAME=fellowship +FLASK_ENV=production +NODE_ENV=production +FRONTEND_MODE=prod +CADDY_DOMAIN=fellowship.classroom.local +CADDYFILE_PATH=./caddy/Caddyfile + +############################################################################## +# LOCAL DEVELOPMENT BUILD (IDE Terminal) +############################################################################## + +# COMPOSE_PROJECT_NAME=fellowship-local +# FLASK_ENV=development +# NODE_ENV=development +# FRONTEND_MODE=dev +# CADDY_DOMAIN=localhost +# CADDYFILE_PATH=./caddy/Caddyfile.local diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..9807855 --- /dev/null +++ b/.env.local @@ -0,0 +1,60 @@ +# Local Development Environment Configuration +# Used in code-server IDE terminal for testing changes before deployment +# File: .env.local +# Usage: Automatically set by code-server container via COMPOSE_PROJECT_NAME env var +# OR manually: cp .env.local .env && docker-compose up -d + +# Docker Compose Project Naming +# Using "fellowship-local" isolates containers from production ("fellowship-*") +# Containers: fellowship-local_backend_1, fellowship-local_frontend_1, etc. +# Volumes: fellowship-local_backend_data, fellowship-local_frontend_node_modules, etc. +# Networks: fellowship-local_default +COMPOSE_PROJECT_NAME=fellowship-local + +# Backend/Flask Configuration +FLASK_APP=app.py +FLASK_ENV=development +DATABASE_URL=sqlite:////app/data/fellowship.db +SECRET_KEY=dev-secret-key-change-in-production + +# Frontend/React Configuration +NODE_ENV=development +FRONTEND_MODE=dev +REACT_APP_API_URL=/api +REACT_APP_DISABLE_ANALYTICS=true +CHOKIDAR_USEPOLLING=true +SKIP_PREFLIGHT_CHECK=true +DISABLE_ESLINT_PLUGIN=true +FAST_REFRESH=false + +# Caddy/Reverse Proxy Configuration +# Local development uses HTTP-only (Caddyfile.local) +CADDY_DOMAIN=localhost +CADDYFILE_PATH=./caddy/Caddyfile.local +WDS_SOCKET_PORT=80 +WDS_SOCKET_PROTOCOL= + +# DevOps Escape Room Subdomains (empty for local/HTTP) +JENKINS_DOMAIN= +IDE_DOMAIN= +GITEA_DOMAIN= + +# Jenkins Configuration +JENKINS_ADMIN_PASSWORD=fellowship123 +JENKINS_URL=http://localhost:8080/ + +# Gitea Configuration +GITEA_ADMIN_USER=fellowship +GITEA_ADMIN_PASSWORD=fellowship123 +GITEA_ADMIN_EMAIL=gandalf@fellowship.local +GITEA_DOMAIN=localhost +GITEA_ROOT_URL=http://localhost:3030/ + +# code-server IDE Configuration +CODESERVER_PASSWORD=fellowship + +# Optional: Azure OpenAI Integration (disabled for local dev) +AZURE_OPENAI_ENDPOINT= +AZURE_OPENAI_API_KEY= +AZURE_OPENAI_DEPLOYMENT= +AZURE_OPENAI_API_VERSION= diff --git a/.env.prod b/.env.prod new file mode 100644 index 0000000..bb23bf3 --- /dev/null +++ b/.env.prod @@ -0,0 +1,61 @@ +# Production Environment Configuration +# Used on EC2 host instance for classroom SUT deployment +# File: .env.prod +# Usage: cp .env.prod .env && docker-compose up -d + +# Docker Compose Project Naming +# Default (no project name) → containers, networks, volumes named "fellowship_*" +COMPOSE_PROJECT_NAME=fellowship + +# Backend/Flask Configuration +FLASK_APP=app.py +FLASK_ENV=production +DATABASE_URL=sqlite:////app/data/fellowship.db +SECRET_KEY=${SECRET_KEY:-change-me-in-production} + +# Frontend/React Configuration +NODE_ENV=production +FRONTEND_MODE=prod +REACT_APP_API_URL=/api +REACT_APP_DISABLE_ANALYTICS=false + +# WebSocket Configuration for Webpack Dev Server (when FRONTEND_MODE=dev) +# For development with HTTPS proxy (Caddy): use wss +# For CI/HTTP environments: use ws +# Default: ws for HTTP environments +WDS_SOCKET_PROTOCOL=ws +WDS_SOCKET_PORT=80 +WDS_SOCKET_HOST=localhost +WDS_SOCKET_PATH=/ws + +# Caddy/Reverse Proxy Configuration +# CADDY_DOMAIN: Root domain for the SUT (e.g., fellowship.classroom.local) +# Set by setup_fellowship.sh during instance bootstrap +CADDY_DOMAIN=${CADDY_DOMAIN:-localhost} +CADDYFILE_PATH=${CADDYFILE_PATH:-./caddy/Caddyfile} + +# DevOps Escape Room Subdomains (for Jenkins, IDE, Gitea) +# Set by setup_fellowship.sh when CADDY_DOMAIN is known +JENKINS_DOMAIN=${JENKINS_DOMAIN:-} +IDE_DOMAIN=${IDE_DOMAIN:-} +GITEA_DOMAIN=${GITEA_DOMAIN:-} + +# Jenkins Configuration +JENKINS_ADMIN_PASSWORD=${JENKINS_ADMIN_PASSWORD:-fellowship123} +JENKINS_URL=${JENKINS_URL:-http://localhost:8080/} + +# Gitea Configuration +GITEA_ADMIN_USER=${GITEA_ADMIN_USER:-fellowship} +GITEA_ADMIN_PASSWORD=${GITEA_ADMIN_PASSWORD:-fellowship123} +GITEA_ADMIN_EMAIL=${GITEA_ADMIN_EMAIL:-gandalf@fellowship.local} +GITEA_DOMAIN=${GITEA_DOMAIN:-localhost} +GITEA_ROOT_URL=${GITEA_ROOT_URL:-http://localhost:3030/} + +# code-server IDE Configuration +CODESERVER_PASSWORD=${CODESERVER_PASSWORD:-fellowship} + +# Optional: Azure OpenAI Integration +AZURE_OPENAI_ENDPOINT=${AZURE_OPENAI_ENDPOINT:-} +AZURE_OPENAI_API_KEY=${AZURE_OPENAI_API_KEY:-} +AZURE_OPENAI_DEPLOYMENT=${AZURE_OPENAI_DEPLOYMENT:-} +AZURE_OPENAI_API_VERSION=${AZURE_OPENAI_API_VERSION:-} diff --git a/caddy/Caddyfile b/caddy/Caddyfile new file mode 100644 index 0000000..9dbf977 --- /dev/null +++ b/caddy/Caddyfile @@ -0,0 +1,18 @@ +# Caddyfile for Fellowship SUT - STAGING +# Domain will be dynamically set via environment variable CADDY_DOMAIN +# Uses Let's Encrypt Staging CA to avoid rate limits (up to 5,000 cert/hour) +# For local development: CADDY_DOMAIN defaults to localhost +# For production certificates, use Caddyfile.prod instead +# For tutorial instances (SUT + Jenkins + IDE), use Caddyfile.fellowship instead + +{$CADDY_DOMAIN:localhost} { + # Use Let's Encrypt staging CA for development and testing + # Staging certs won't be trusted by browsers but avoid rate limits + # Caddy automatically uses self-signed certs for localhost + tls { + ca https://acme-staging-v02.api.letsencrypt.org/directory + } + + reverse_proxy /api/* backend:5000 + reverse_proxy /* frontend:3000 +} diff --git a/caddy/Caddyfile.fellowship b/caddy/Caddyfile.fellowship new file mode 100644 index 0000000..e60a1dd --- /dev/null +++ b/caddy/Caddyfile.fellowship @@ -0,0 +1,54 @@ +# Caddyfile for Fellowship Tutorial Instances +# Used exclusively by setup_fellowship.sh for classroom/tutorial EC2 instances +# that run the full DevOps Escape Room stack (SUT + Jenkins CI + code-server IDE). +# +# This file is NEVER used by the permanent SUT deployment (bootstrap_spot_instance.sh), +# which uses Caddyfile.prod (SUT only) instead. +# +# setup_fellowship.sh copies this file over caddy/Caddyfile before starting +# docker compose, so that the Caddy container picks it up automatically. +# +# Required environment variables: +# CADDY_DOMAIN — SUT domain (e.g. fellowship-pool-8.fellowship.testingfantasy.com) +# JENKINS_DOMAIN — Jenkins domain (jenkins-{CADDY_DOMAIN}) +# IDE_DOMAIN — IDE domain (ide-{CADDY_DOMAIN}) +# GITEA_DOMAIN — Gitea domain (gitea-{CADDY_DOMAIN}) +# +# All four domains must have Route53 A records pointing to the same instance +# public IP as CADDY_DOMAIN. setup_fellowship.sh creates all records. +# +# Routing: +# CADDY_DOMAIN → SUT frontend (port 3000) and backend API (port 5000) +# JENKINS_DOMAIN → Jenkins CI (port 8080, devops-escape-room compose stack) +# IDE_DOMAIN → code-server (port 8443, devops-escape-room compose stack) +# GITEA_DOMAIN → Gitea (port 3030, devops-escape-room compose stack) +# +# Jenkins and code-server are reached via host.docker.internal (host-gateway), +# because they run in a separate docker-compose project from Caddy. +# docker-compose.yml sets extra_hosts: [host.docker.internal:host-gateway]. + +# ── Fellowship SUT ──────────────────────────────────────────────────────────── +{$CADDY_DOMAIN} { + # Let Caddy use its default automatic HTTPS issuers. + # This avoids hard-failing when a single ACME CA is temporarily rate-limited. + + reverse_proxy /api/* backend:5000 + reverse_proxy /* frontend:3000 +} + +# ── Jenkins CI (DevOps Escape Room) ────────────────────────────────────────── +{$JENKINS_DOMAIN} { + reverse_proxy /* host.docker.internal:8080 +} + +# ── code-server IDE (DevOps Escape Room) ───────────────────────────────────── +# Host port 8443 maps to the code-server container's internal port 8080. +{$IDE_DOMAIN} { + reverse_proxy /* host.docker.internal:8443 +} + +# ── Gitea (self-hosted Git, DevOps Escape Room) ─────────────────────────────── +# Host port 3030 maps to Gitea's internal port 3000. +{$GITEA_DOMAIN} { + reverse_proxy /* host.docker.internal:3030 +} diff --git a/caddy/Caddyfile.local b/caddy/Caddyfile.local new file mode 100644 index 0000000..a888113 --- /dev/null +++ b/caddy/Caddyfile.local @@ -0,0 +1,18 @@ +# Caddyfile for Fellowship SUT - LOCAL DEVELOPMENT +# HTTP-only configuration for local development (no HTTPS) +# Explicitly use http:// to avoid automatic HTTPS redirect + +http://localhost, http://127.0.0.1, :80 { + # API routes - Flask handles CORS + handle /api/* { + reverse_proxy backend:5000 + } + + # WebSocket support + handle /ws { + reverse_proxy backend:5000 + } + + # Frontend routes + reverse_proxy /* frontend:3000 +} diff --git a/caddy/Caddyfile.prod b/caddy/Caddyfile.prod new file mode 100644 index 0000000..e54b767 --- /dev/null +++ b/caddy/Caddyfile.prod @@ -0,0 +1,14 @@ +# Caddyfile for Fellowship SUT - PRODUCTION +# Domain will be dynamically set via environment variable CADDY_DOMAIN +# Uses Let's Encrypt Production CA for trusted certificates +# Rate limit: 50 certificates per domain per week +# For local development, use Caddyfile (staging) instead +# For tutorial instances (SUT + Jenkins + IDE), use Caddyfile.fellowship instead + +{$CADDY_DOMAIN:localhost} { + # Let Caddy use its default automatic HTTPS issuers. + # This avoids hard-failing when a single ACME CA is temporarily rate-limited. + + reverse_proxy /api/* backend:5000 + reverse_proxy /* frontend:3000 +} \ No newline at end of file diff --git a/devops-escape-room/.env b/devops-escape-room/.env new file mode 100644 index 0000000..baa86a4 --- /dev/null +++ b/devops-escape-room/.env @@ -0,0 +1,21 @@ +# Production Environment Configuration for DevOps Escape Room +# File: devops-escape-room/.env.prod +# Usage: cp .env.prod .env && docker-compose up -d + +# Docker Compose Project Naming (production uses default "fellowship") +# Note: This is for the escape room stack only; the SUT stack may differ +COMPOSE_PROJECT_NAME=fellowship + +# Jenkins Configuration +JENKINS_ADMIN_PASSWORD=fellowship123 +JENKINS_URL=http://localhost:8080/ + +# Gitea Configuration +GITEA_ADMIN_USER=fellowship +GITEA_ADMIN_PASSWORD=fellowship123 +GITEA_ADMIN_EMAIL=gandalf@fellowship.local +GITEA_DOMAIN=localhost +GITEA_ROOT_URL=http://localhost:3030/ + +# code-server IDE Configuration +CODESERVER_PASSWORD=fellowship diff --git a/devops-escape-room/.env.local b/devops-escape-room/.env.local new file mode 100644 index 0000000..91c6568 --- /dev/null +++ b/devops-escape-room/.env.local @@ -0,0 +1,22 @@ +# Local Development Environment Configuration for DevOps Escape Room +# File: devops-escape-room/.env.local +# Usage: Automatically set by code-server container +# OR manually: cp .env.local .env && docker-compose up -d + +# Docker Compose Project Naming +# Using "fellowship-local" isolates from production stacks +COMPOSE_PROJECT_NAME=fellowship-local + +# Jenkins Configuration +JENKINS_ADMIN_PASSWORD=fellowship123 +JENKINS_URL=http://localhost:8080/ + +# Gitea Configuration +GITEA_ADMIN_USER=fellowship +GITEA_ADMIN_PASSWORD=fellowship123 +GITEA_ADMIN_EMAIL=gandalf@fellowship.local +GITEA_DOMAIN=localhost +GITEA_ROOT_URL=http://localhost:3030/ + +# code-server IDE Configuration +CODESERVER_PASSWORD=fellowship diff --git a/devops-escape-room/.env.prod b/devops-escape-room/.env.prod new file mode 100644 index 0000000..baa86a4 --- /dev/null +++ b/devops-escape-room/.env.prod @@ -0,0 +1,21 @@ +# Production Environment Configuration for DevOps Escape Room +# File: devops-escape-room/.env.prod +# Usage: cp .env.prod .env && docker-compose up -d + +# Docker Compose Project Naming (production uses default "fellowship") +# Note: This is for the escape room stack only; the SUT stack may differ +COMPOSE_PROJECT_NAME=fellowship + +# Jenkins Configuration +JENKINS_ADMIN_PASSWORD=fellowship123 +JENKINS_URL=http://localhost:8080/ + +# Gitea Configuration +GITEA_ADMIN_USER=fellowship +GITEA_ADMIN_PASSWORD=fellowship123 +GITEA_ADMIN_EMAIL=gandalf@fellowship.local +GITEA_DOMAIN=localhost +GITEA_ROOT_URL=http://localhost:3030/ + +# code-server IDE Configuration +CODESERVER_PASSWORD=fellowship diff --git a/devops-escape-room/code-server/Dockerfile b/devops-escape-room/code-server/Dockerfile new file mode 100644 index 0000000..7e641a1 --- /dev/null +++ b/devops-escape-room/code-server/Dockerfile @@ -0,0 +1,53 @@ +# Fellowship code-server IDE +# Extends codercom/code-server with: +# - Docker CLI + Compose v2 plugin (so students can run docker compose from the IDE terminal) +# - Pre-installed VS Code extensions (Python, Playwright, Copilot, Jupyter, Prettier) +# - Runtime entrypoint that aligns the docker group GID with the host socket + +FROM codercom/code-server:latest + +USER root + +# ── System packages ────────────────────────────────────────────────────────── +# docker.io provides the Docker CLI (client only); the daemon runs on the host. +# gosu is used in the entrypoint to drop privileges cleanly back to coder. +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + docker.io \ + gosu \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + +# ── Docker Compose v2 plugin ───────────────────────────────────────────────── +# Install as a CLI plugin AND as a standalone binary so both +# `docker compose ...` and `docker-compose ...` work from the terminal. +RUN mkdir -p /usr/local/lib/docker/cli-plugins && \ + curl -fsSL \ + "https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-linux-x86_64" \ + -o /usr/local/lib/docker/cli-plugins/docker-compose && \ + chmod +x /usr/local/lib/docker/cli-plugins/docker-compose && \ + ln -sf /usr/local/lib/docker/cli-plugins/docker-compose /usr/local/bin/docker-compose && \ + ln -sf /usr/local/lib/docker/cli-plugins/docker-compose /usr/local/bin/docker compose || true + +# Make the CLI plugin available to the coder user too +RUN mkdir -p /home/coder/.docker/cli-plugins && \ + ln -sf /usr/local/lib/docker/cli-plugins/docker-compose \ + /home/coder/.docker/cli-plugins/docker-compose && \ + chown -R coder:coder /home/coder/.docker + +# ── Docker group ───────────────────────────────────────────────────────────── +# Pre-create the docker group with GID 999 (common default). +# The entrypoint will re-align the GID at runtime to match the host socket. +RUN groupadd -g 999 docker 2>/dev/null || groupmod -g 999 docker 2>/dev/null || true && \ + usermod -aG docker coder + +# ── Runtime entrypoint ─────────────────────────────────────────────────────── +COPY entrypoint.sh /usr/bin/fellowship-docker-init.sh +RUN chmod +x /usr/bin/fellowship-docker-init.sh + +# Run as root initially so the entrypoint can fix group GIDs. +# The entrypoint drops back to 'coder' via gosu before starting code-server. +USER root + +ENTRYPOINT ["/usr/bin/fellowship-docker-init.sh"] diff --git a/devops-escape-room/code-server/entrypoint.sh b/devops-escape-room/code-server/entrypoint.sh new file mode 100644 index 0000000..f9d495d --- /dev/null +++ b/devops-escape-room/code-server/entrypoint.sh @@ -0,0 +1,181 @@ +#!/bin/bash +# Fellowship code-server runtime entrypoint +# 1. Aligns the docker group GID with the host's /var/run/docker.sock +# 2. Pre-installs VS Code extensions (idempotent) +# 3. Drops to 'coder' user and starts code-server + +set -e + +log() { echo "[$(date '+%H:%M:%S')] [fellowship-init] $*"; } + +# ── Fix Docker group GID ────────────────────────────────────────────────────── +# Without this, `docker` commands inside code-server fail with "permission denied" +# because the socket's GID on the host may differ from the GID baked into the image. +if [ -S /var/run/docker.sock ]; then + DOCK_GID=$(stat -c '%g' /var/run/docker.sock) + log "Host docker socket GID: ${DOCK_GID}" + + if getent group docker > /dev/null 2>&1; then + groupmod -g "${DOCK_GID}" docker 2>/dev/null || true + else + groupadd -g "${DOCK_GID}" docker 2>/dev/null || true + fi + usermod -aG docker coder 2>/dev/null || true + + # Make the socket world-accessible as a safe fallback + chmod 666 /var/run/docker.sock 2>/dev/null || true + + log "✓ Docker group GID aligned to ${DOCK_GID}" +else + log "WARNING: /var/run/docker.sock not mounted — docker commands will not work in the IDE terminal" + log " Add - /var/run/docker.sock:/var/run/docker.sock to the code-server volumes." +fi + +# ── Fix config directory permissions ────────────────────────────────────────── +# The codeserver_config volume is mounted as /home/coder/.config but is owned by root. +# Ensure coder user can read/write to it. +if [ -d /home/coder/.config ]; then + chown -R coder:coder /home/coder/.config 2>/dev/null || true + log "✓ Config directory permissions fixed" +fi + +# ── Fix fellowship project directory permissions ─────────────────────────────── +# The fellowship directory is mounted from the host and may be owned by ec2-user +# or root. Ensure the coder user can read/write all project files for IDE editing. +# This includes fixing .git directory ownership which git is sensitive about. +if [ -d /home/coder/fellowship ]; then + chown -R coder:coder /home/coder/fellowship 2>/dev/null || true + chmod -R u+rw /home/coder/fellowship 2>/dev/null || true + log "✓ Fellowship project directory permissions fixed" + + # Fix .git directory specifically since git checks ownership strictly + if [ -d /home/coder/fellowship/.git ]; then + chown -R coder:coder /home/coder/fellowship/.git 2>/dev/null || true + chmod -R u+rw /home/coder/fellowship/.git 2>/dev/null || true + fi +fi + +# ── Configure git safe.directory ────────────────────────────────────────────── +# Git requires explicit permission for directories owned by different users. +# Mark the fellowship project as a safe git repository for the coder user. +# We configure this even if .git doesn't exist yet — it will be used for new repos. +if [ -d /home/coder/fellowship ]; then + # Using su instead of gosu ensures proper shell environment and config file loading + su - coder -c "git config --global --add safe.directory /home/coder/fellowship" 2>/dev/null || true + log "✓ Git safe.directory configured for /home/coder/fellowship" +fi + +# ── Install VS Code extensions ──────────────────────────────────────────────── +# Runs as the coder user; uses Open VSX registry (code-server default). +# The --force flag makes installs idempotent — safe to run on every startup. +# ── Configure git defaults ─────────────────────────────────────────────────── +# Set global git configuration for the coder user +log "Configuring git defaults..." +su - coder -c "git config --global user.name 'Fellowship Scholar'" 2>/dev/null || true +su - coder -c "git config --global user.email 'scholar@fellowship.local'" 2>/dev/null || true +su - coder -c "git config --global credential.helper store" 2>/dev/null || true +log "✓ Git configured: Fellowship Scholar " + +# ── Configure Gitea as git remote ────────────────────────────────────────────── +# Ensure the fellowship repository is connected to Gitea (not GitHub or other remotes) +# This runs every startup to guarantee correct remote configuration +if [ -d /home/coder/fellowship ]; then + log "Ensuring git repository is connected to Gitea..." + + if [ -d /home/coder/fellowship/.git ]; then + # Repository exists - check if it's the old GitHub repository + # Count branches: fresh Gitea repository only has main, old GitHub has 15+ branches + BRANCH_COUNT=$(su - coder -c "cd /home/coder/fellowship && git branch -r 2>/dev/null | wc -l" 2>/dev/null || echo "0") + BRANCH_COUNT=$(echo "$BRANCH_COUNT" | tr -d ' ' || echo "0") # Strip whitespace + + if [ "$BRANCH_COUNT" -gt 5 ]; then + # Repository has many branches - this is the old GitHub repository + log " ⚠ Detected old GitHub repository ($BRANCH_COUNT branches) - deleting and resetting to fresh Gitea..." + su - coder -c "cd /home/coder/fellowship && rm -rf .git" 2>/dev/null || true + su - coder -c "cd /home/coder/fellowship && git init --initial-branch=main" 2>/dev/null || true + su - coder -c "cd /home/coder/fellowship && git remote add origin http://gitea:3000/fellowship-org/lotr-sut.git" 2>/dev/null || true + log " ✓ Repository reset to fresh Gitea repository" + fi + + # Mark repository as safe for git operations + su - coder -c "git config --global --add safe.directory /home/coder/fellowship" 2>/dev/null || true + log "✓ Git remote: http://gitea:3000/fellowship-org/lotr-sut.git" + else + # No .git yet - initialize fresh for Gitea + log " Initializing fresh repository for Gitea..." + su - coder -c "cd /home/coder/fellowship && git init --initial-branch=main" 2>/dev/null || true + su - coder -c "cd /home/coder/fellowship && git remote add origin http://gitea:3000/fellowship-org/lotr-sut.git" 2>/dev/null || true + log "✓ Fresh Gitea repository initialized" + fi + + # Configure Gitea credentials for authentication + su - coder -c "mkdir -p ~/.config/git && echo 'http://fellowship:fellowship123@gitea:3000' > ~/.git-credentials && chmod 600 ~/.git-credentials" 2>/dev/null || true + log "✓ Gitea credentials configured" +fi + +log "Installing VS Code extensions (optional, skipped on timeout)..." + +EXTENSIONS=( + "ms-python.python" + # NOTE: GitHub Copilot removed - conflicts with Gitea (pulls GitHub auth flow) + # Use "GitHub" extension only if switching to github.com repositories. + "ms-playwright.playwright" + "esbenp.prettier-vscode" + "ms-toolsai.jupyter" + "redhat.vscode-yaml" + "ms-azuretools.vscode-docker" + "gitpod.gitpod-remote-web" + "earshinov.gitea-extension" +) + +for ext in "${EXTENSIONS[@]}"; do + # Install with 30-second timeout (extensions may take time to download) + if timeout 30 gosu coder code-server --install-extension "${ext}" --force > /dev/null 2>&1; then + log " ✓ ${ext}" + else + log " ⚠ ${ext} (unavailable, offline, or timeout — skipping)" + fi +done + +log "✓ VS Code extensions completed" + +# ── Write default settings ──────────────────────────────────────────────────── +# Set sensible defaults so Python + Docker extensions work out of the box. +SETTINGS_DIR="/home/coder/.local/share/code-server/User" +SETTINGS_FILE="${SETTINGS_DIR}/settings.json" + +if [ ! -f "${SETTINGS_FILE}" ]; then + gosu coder mkdir -p "${SETTINGS_DIR}" + gosu coder tee "${SETTINGS_FILE}" > /dev/null << 'SETTINGS' +{ + "python.defaultInterpreterPath": "/usr/bin/python3", + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[python]": { + "editor.defaultFormatter": "ms-python.python" + }, + "terminal.integrated.defaultProfile.linux": "bash", + "git.autofetch": true, + "docker.host": "unix:///var/run/docker.sock" +} +SETTINGS + log "✓ Default settings.json written" +fi + +# ── Hand off to code-server ─────────────────────────────────────────────────── +# ── Hand off to code-server ─────────────────────────────────────────────────── +log "Starting code-server as coder..." +log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +log " Git Repository Connected to Gitea:" +log " • User: Fellowship Scholar " +log " • Remote: http://gitea:3000/fellowship-org/lotr-sut" +log " • Use IDE Terminal: git status, git push, git pull, git commit" +log " • NOT connected to GitHub (fresh Gitea repository)" +log "" +log " Docker Integration (from IDE Terminal):" +log " • docker compose up -d (runs on host Docker)" +log " • docker-compose up -d (same, legacy alias)" +log " • docker ps (list running containers)" +log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +exec gosu coder /usr/bin/entrypoint.sh "$@" diff --git a/devops-escape-room/docker-compose.yml b/devops-escape-room/docker-compose.yml new file mode 100644 index 0000000..8ecdda2 --- /dev/null +++ b/devops-escape-room/docker-compose.yml @@ -0,0 +1,165 @@ +# Fellowship DevOps Escape Room Stack +# Services: Jenkins (CI), Gitea (Git), code-server (IDE), MailHog (mail) +# +# Environment-aware Configuration +# Usage: +# Production: docker compose up -d (containers: fellowship_jenkins, etc.) +# Local dev: COMPOSE_PROJECT_NAME=fellowship-local docker compose up -d (containers: fellowship-local_jenkins, etc.) +# +# Access: +# Jenkins: http://localhost:8080 (user: fellowship / fellowship123) +# Gitea: http://localhost:3030 (user: fellowship / fellowship123) +# code-server: http://localhost:8443 (password: fellowship) +# MailHog UI: http://localhost:8025 +# +# Note: code-server container automatically sets COMPOSE_PROJECT_NAME=fellowship-local via environment + +services: + + # ── Jenkins CI ────────────────────────────────────────────────────────────── + jenkins: + build: + context: ../jenkins + dockerfile: Dockerfile + image: fellowship-jenkins:latest + restart: unless-stopped + ports: + - "8080:8080" + - "50000:50000" + volumes: + - jenkins_home:/var/jenkins_home + # Mount Docker socket so Jenkins can run docker commands on the host + - /var/run/docker.sock:/var/run/docker.sock + environment: + JENKINS_ADMIN_PASSWORD: ${JENKINS_ADMIN_PASSWORD:-fellowship123} + CASC_JENKINS_CONFIG: /var/jenkins_home/casc_configs + # Set by setup_fellowship.sh when CADDY_DOMAIN is known. + # JCasC reads this to configure Jenkins' canonical root URL (used in build + # links, email notifications, etc.). Defaults to the plain HTTP address. + JENKINS_URL: ${JENKINS_URL:-http://localhost:8080/} + # Configure Jenkins to reach Gitea internally via docker network + GITEA_HTTP_URL: "http://gitea:3000" + depends_on: + gitea-init: + condition: service_completed_successfully + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:8080/login || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 90s + + # ── Gitea (self-hosted Git) ────────────────────────────────────────────────── + gitea: + image: gitea/gitea:1.22 + restart: unless-stopped + ports: + - "3030:3000" + - "2222:22" + volumes: + - gitea_data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + environment: + USER_UID: "1000" + USER_GID: "1000" + GITEA__database__DB_TYPE: sqlite3 + GITEA__server__DOMAIN: ${GITEA_DOMAIN:-localhost} + GITEA__server__HTTP_PORT: "3000" + GITEA__server__ROOT_URL: ${GITEA_ROOT_URL:-http://localhost:3030/} + GITEA__server__SSH_DOMAIN: localhost + GITEA__server__SSH_PORT: "2222" + GITEA__service__DISABLE_REGISTRATION: "false" + GITEA__service__REQUIRE_SIGNIN_VIEW: "false" + # INSTALL_LOCK: true after initialization. Admin will be created by gitea-init container via CLI + GITEA__security__INSTALL_LOCK: "true" + GITEA__admin__DEFAULT_EMAIL_NOTIFICATIONS: disabled + GITEA__mailer__ENABLED: "false" + # These are passed to gitea-init for CLI-based user creation, not used directly by Gitea + GITEA_ADMIN_USER: "${GITEA_ADMIN_USER:-fellowship}" + GITEA_ADMIN_PASSWORD: "${GITEA_ADMIN_PASSWORD:-fellowship123}" + GITEA_ADMIN_EMAIL: "${GITEA_ADMIN_EMAIL:-gandalf@fellowship.local}" + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:3000/api/v1/version || exit 1"] + interval: 15s + timeout: 5s + retries: 6 + start_period: 30s + + # ── Gitea Initializer (one-shot) ───────────────────────────────────────────── + gitea-init: + platform: linux/amd64 + image: alpine:latest + restart: on-failure:5 + environment: + GITEA_URL: "http://gitea:3000" + GITEA_ADMIN_USER: "${GITEA_ADMIN_USER:-fellowship}" + GITEA_ADMIN_PASSWORD: "${GITEA_ADMIN_PASSWORD:-fellowship123}" + GITEA_ADMIN_EMAIL: "${GITEA_ADMIN_EMAIL:-gandalf@fellowship.local}" + GITEA_ORG_NAME: "${GITEA_ORG_NAME:-fellowship-org}" + GITEA_REPO_NAME: "${GITEA_REPO_NAME:-lotr-sut}" + GITEA_DOMAIN: "${GITEA_DOMAIN:-}" + SUT_SOURCE_DIR: /sut-source + volumes: + - ../gitea/init.sh:/init.sh:ro + # Mount Docker socket for docker commands + - /var/run/docker.sock:/var/run/docker.sock:rw + # Mount only the SUT source, not the full project tree (avoids .env / secrets) + - ../sut:/sut-source/sut:ro + - ../docker-compose.yml:/sut-source/docker-compose.yml:ro + - ../caddy:/sut-source/caddy:ro + - ../nginx:/sut-source/nginx:ro + - ../Jenkinsfile:/sut-source/Jenkinsfile:ro + entrypoint: ["/bin/sh", "/init.sh"] + depends_on: + gitea: + condition: service_healthy + + # ── code-server (VS Code in browser IDE) ──────────────────────────────────── + code-server: + build: + context: ./code-server + dockerfile: Dockerfile + image: fellowship-code-server:latest + restart: unless-stopped + ports: + - "8443:8080" + volumes: + - ../:/home/coder/fellowship:rw + - codeserver_config:/home/coder/.config + # Mount host Docker socket so students can run `docker compose` from the IDE terminal. + # The entrypoint aligns the docker group GID at runtime automatically. + - /var/run/docker.sock:/var/run/docker.sock + environment: + PASSWORD: "${CODESERVER_PASSWORD:-fellowship}" + # Set compose project name to 'fellowship' so that students can run `docker compose` without -p flag. + COMPOSE_PROJECT_NAME: "fellowship-local" + # No `user:` here — the entrypoint runs as root to fix Docker group GID, + # then drops to the 'coder' user via gosu before starting code-server. + command: + - --auth=password + - --bind-addr=0.0.0.0:8080 + - /home/coder/fellowship + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:8080/ || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + # ── MailHog (mock SMTP + web UI) ──────────────────────────────────────────── + mailhog: + platform: linux/amd64 + image: mailhog/mailhog:v1.0.1 + restart: unless-stopped + ports: + - "1025:1025" + - "8025:8025" + +volumes: + jenkins_home: + driver: local + gitea_data: + driver: local + codeserver_config: + driver: local diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..360992b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,119 @@ +# version is obsolete in newer docker-compose versions +# +# Environment-aware Docker Compose configuration +# Supports both production (fellowship) and local dev (fellowship-local) stacks +# Usage: +# Production: cp .env.prod .env && docker-compose up -d (containers: fellowship_*) +# Local dev: cp .env.local .env && docker-compose up -d (containers: fellowship-local_*) +# +# The COMPOSE_PROJECT_NAME environment variable controls container naming: +# - Omitted or 'fellowship' → containers: fellowship_backend_1, fellowship_frontend_1 +# - 'fellowship-local' → containers: fellowship-local_backend_1, fellowship-local_frontend_1 +# +# This allows both environments to coexist without conflicts. + +services: + backend: + build: + context: ./sut/backend + dockerfile: Dockerfile + # ✓ No container_name: allows COMPOSE_PROJECT_NAME-based naming + # Port 5000 is intentionally NOT exposed to the host — backend is only reached + # via Caddy reverse-proxy (internal Docker network: backend:5000). + # macOS AirPlay Receiver occupies host port 5000 (Monterey+), so binding it + # would fail. Direct API access for debugging: docker exec or /api through Caddy. + volumes: + - backend_data:/app/data + - ./sut/backend:/app + # Exclude node_modules and data from volume mount to avoid conflicts + environment: + - FLASK_APP=app.py + - FLASK_ENV=development + - DATABASE_URL=sqlite:////app/data/fellowship.db + - SECRET_KEY=dev-secret-key-change-in-production + - AZURE_OPENAI_ENDPOINT=${AZURE_OPENAI_ENDPOINT:-} + - AZURE_OPENAI_API_KEY=${AZURE_OPENAI_API_KEY:-} + - AZURE_OPENAI_DEPLOYMENT=${AZURE_OPENAI_DEPLOYMENT:-} + - AZURE_OPENAI_API_VERSION=${AZURE_OPENAI_API_VERSION:-} + working_dir: /app + command: python app.py + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + frontend: + build: + context: ./sut/frontend + dockerfile: Dockerfile + # ✓ No container_name: allows COMPOSE_PROJECT_NAME-based naming + # Port 3000 is intentionally NOT exposed to the host — Caddy reverse-proxies + # to frontend:3000 via the internal Docker network. Colima's sshfs SSH tunnels + # conflict with host-bound :3000, so we leave it unmapped. + volumes: + - ./sut/frontend:/app + - frontend_node_modules:/app/node_modules + environment: + - REACT_APP_API_URL=/api + - REACT_APP_ENABLE_TEST_CONTROLS=${REACT_APP_ENABLE_TEST_CONTROLS:-true} + - CHOKIDAR_USEPOLLING=true + - SKIP_PREFLIGHT_CHECK=true + - DISABLE_ESLINT_PLUGIN=true + - FAST_REFRESH=false + - FRONTEND_MODE=${FRONTEND_MODE:-dev} + - NODE_ENV=development + - WDS_SOCKET_PORT=${WDS_SOCKET_PORT:-80} + - WDS_SOCKET_HOST=${CADDY_DOMAIN:-localhost} + - WDS_SOCKET_PROTOCOL=${WDS_SOCKET_PROTOCOL:-ws} + - WDS_SOCKET_PATH=${WDS_SOCKET_PATH:-/ws} + command: sh -c "npm install && if [ \"${FRONTEND_MODE:-dev}\" = \"prod\" ]; then npm run build && npx --yes serve -s build -l 3000; else npm start; fi" + depends_on: + - backend + restart: unless-stopped + + caddy: + image: caddy:2-alpine + # ✓ No container_name: allows COMPOSE_PROJECT_NAME-based naming + ports: + - "80:80" + - "443:443" + volumes: + - ${CADDYFILE_PATH:-./caddy/Caddyfile.local}:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + environment: + # Use CADDY_DOMAIN from environment/env file, fallback to localhost for local development + CADDY_DOMAIN: ${CADDY_DOMAIN:-localhost} + # DevOps Escape Room HTTPS subdomains — prepend jenkins-/ide- to CADDY_DOMAIN. + # These env vars are required when using Caddyfile (staging) or Caddyfile.prod. + # They are NOT needed when using Caddyfile.local (CI / local HTTP-only dev). + JENKINS_DOMAIN: ${JENKINS_DOMAIN:-} + IDE_DOMAIN: ${IDE_DOMAIN:-} + GITEA_DOMAIN: ${GITEA_DOMAIN:-} + # Allow Caddy to reach services in the devops-escape-room compose stack + # (Jenkins on host:8080 and code-server on host:8443) via host.docker.internal. + extra_hosts: + - "host.docker.internal:host-gateway" + depends_on: + - backend + - frontend + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 20s + +volumes: + backend_data: + driver: local + frontend_node_modules: + driver: local + caddy_data: + driver: local + caddy_config: + driver: local \ No newline at end of file diff --git a/gitea/init.sh b/gitea/init.sh new file mode 100755 index 0000000..70d0c44 --- /dev/null +++ b/gitea/init.sh @@ -0,0 +1,281 @@ +#!/bin/sh +# Gitea Initialization Script for Fellowship DevOps Escape Room +# Sets up Gitea with admin user, organization, and the LOTR SUT repository +# This script runs as a one-shot container after Gitea starts up +# +# Key: We use docker-compose exec instead of docker exec for reliable container access + +set -e + +GITEA_URL="${GITEA_URL:-http://gitea:3000}" +ADMIN_USER="${GITEA_ADMIN_USER:-fellowship}" +ADMIN_PASS="${GITEA_ADMIN_PASSWORD:-fellowship123}" +ADMIN_EMAIL="${GITEA_ADMIN_EMAIL:-gandalf@fellowship.local}" +ORG_NAME="${GITEA_ORG_NAME:-fellowship-org}" +REPO_NAME="${GITEA_REPO_NAME:-lotr-sut}" +REPO_DESC="The Fellowship's Quest List — LOTR-themed SUT for DevOps tutorials" +SUT_SOURCE_DIR="${SUT_SOURCE_DIR:-/sut-source}" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] [gitea-init] $*" +} + +warn() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] [gitea-init] ⚠️ WARNING: $*" >&2 +} + +error() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] [gitea-init] ❌ ERROR: $*" >&2 + return 1 +} + +log "==========================================" +log "Gitea Initialization — Fellowship DevOps" +log "==========================================" +log "Settings:" +log " Gitea Container: ${GITEA_CONTAINER}" +log " Admin User: ${ADMIN_USER}" +log " Organization: ${ORG_NAME}" +log " Repository: ${ORG_NAME}/${REPO_NAME}" +log "==========================================" + +# Install required tools +setup_tools() { + log "Setting up tools..." + if ! command -v curl > /dev/null 2>&1; then + apk add --no-cache curl + fi + if ! command -v docker > /dev/null 2>&1; then + apk add --no-cache docker-cli + fi + log "✓ Tools ready" +} + +# Wait for Gitea to be ready +wait_for_gitea() { + log "Waiting for Gitea API to be ready at ${GITEA_URL}..." + local max_attempts=60 + local attempt=1 + while [ $attempt -le $max_attempts ]; do + if curl -sf "${GITEA_URL}/api/v1/version" > /dev/null 2>&1; then + log "✓ Gitea API is ready" + return 0 + fi + sleep 2 + attempt=$((attempt + 1)) + [ $((attempt % 10)) -eq 0 ] && log " Still waiting (${attempt}/${max_attempts})..." + done + error "Gitea did not become ready after $((max_attempts * 2)) seconds" + return 1 +} + +# Create the admin user using Gitea CLI via docker +create_admin() { + log "Creating admin user '${ADMIN_USER}'..." + + # Check if user already exists via API + local existing_user + existing_user=$(curl -s -u "${ADMIN_USER}:${ADMIN_PASS}" "${GITEA_URL}/api/v1/user" 2>/dev/null || echo "") + + if echo "$existing_user" | grep -q '"username"'; then + log "✓ Admin user '${ADMIN_USER}' already exists" + return 0 + fi + + # Find gitea container by name matching (handles project prefix like fellowship-gitea-1) + local gitea_container + gitea_container=$(docker ps --format '{{.Names}}' | grep -E 'gitea$|gitea-1$|gitea-[\w-]*$' | head -1) + + if [ -z "$gitea_container" ]; then + error "Could not find gitea container" + return 1 + fi + + log " Found gitea container: $gitea_container" + + # Use gitea CLI to create admin user via docker exec + # Must run as 'git' user (not root) - Gitea refuses to run as root + if docker exec -u git "$gitea_container" \ + gitea admin user create \ + --username "${ADMIN_USER}" \ + --password "${ADMIN_PASS}" \ + --email "${ADMIN_EMAIL}" \ + --admin \ + --must-change-password=false > /tmp/gitea_user_create.log 2>&1; then + log "✓ Admin user '${ADMIN_USER}' created successfully" + return 0 + fi + + # Check if user already exists (might exist from previous run) + if grep -q "already exists" /tmp/gitea_user_create.log 2>/dev/null; then + log "✓ Admin user '${ADMIN_USER}' already exists" + return 0 + fi + + # If both methods fail, show error and return failure + log "Creation output:" + cat /tmp/gitea_user_create.log || true + error "Failed to create admin user" + return 1 +} + + +# Create organization via API (requires auth) +create_org() { + log "Creating organization '${ORG_NAME}'..." + + local response + response=$(curl -s -X POST "${GITEA_URL}/api/v1/orgs" \ + -u "${ADMIN_USER}:${ADMIN_PASS}" \ + -H "Content-Type: application/json" \ + -d "{ + \"username\": \"${ORG_NAME}\", + \"full_name\": \"The Fellowship of the Ring\", + \"description\": \"One org to rule them all\", + \"visibility\": \"public\" + }" 2>&1) + + if echo "$response" | grep -q '"id"'; then + log "✓ Organization '${ORG_NAME}' created" + return 0 + elif echo "$response" | grep -q "already exists\|account already exists"; then + log "✓ Organization '${ORG_NAME}' already exists" + return 0 + else + # Log the actual API response for debugging + echo "$response" 1>&2 + error "Failed to create organization" + return 1 + fi +} + +# Create repository via API (requires auth) +create_repo() { + log "Creating repository '${ORG_NAME}/${REPO_NAME}'..." + + local status + status=$(curl -s -o /dev/null -w "%{http_code}" \ + -u "${ADMIN_USER}:${ADMIN_PASS}" \ + "${GITEA_URL}/api/v1/repos/${ORG_NAME}/${REPO_NAME}" 2>/dev/null) + + if [ "$status" = "200" ]; then + log "✓ Repository '${ORG_NAME}/${REPO_NAME}' already exists" + return 0 + fi + + local response + response=$(curl -s -X POST "${GITEA_URL}/api/v1/orgs/${ORG_NAME}/repos" \ + -u "${ADMIN_USER}:${ADMIN_PASS}" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"${REPO_NAME}\", + \"description\": \"${REPO_DESC}\", + \"private\": false, + \"auto_init\": false, + \"default_branch\": \"main\" + }" 2>&1) + + if echo "$response" | grep -q '"id"'; then + log "✓ Repository '${ORG_NAME}/${REPO_NAME}' created" + return 0 + elif echo "$response" | grep -q "already exists\|The repository already exists"; then + log "✓ Repository '${ORG_NAME}/${REPO_NAME}' already exists" + return 0 + else + # Log the actual API response for debugging + echo "$response" 1>&2 + error "Failed to create repository" + return 1 + fi +} + +# Push SUT source code to Gitea (optional, skipped if repo has commits) +push_sut_code() { + log "Checking if SUT code needs to be pushed..." + + # Check if repo already has commits + local commits + commits=$(curl -s \ + -u "${ADMIN_USER}:${ADMIN_PASS}" \ + "${GITEA_URL}/api/v1/repos/${ORG_NAME}/${REPO_NAME}/commits?limit=1" \ + 2>/dev/null | grep -c '"sha"' || echo "0") + + # Trim whitespace and handle invalid values + commits=$(echo "$commits" | xargs) + commits=${commits:-0} # Default to 0 if empty + + # Check if commits is a valid number and greater than 0 + if [ "$commits" -gt 0 ] 2>/dev/null; then + log "✓ Repository already has commits — skipping push" + return 0 + fi + + # Find the SUT source + if [ ! -d "${SUT_SOURCE_DIR}" ]; then + warn "SUT source directory not found at ${SUT_SOURCE_DIR}" + warn "Skipping code push — repository will be created but empty" + return 0 + fi + + log " Source directory: ${SUT_SOURCE_DIR}" + log " (Code push omitted for now — repository ready for manual commit)" + return 0 +} + + + +# Main initialization sequence +main() { + setup_tools || exit 1 + + if ! wait_for_gitea; then + error "Aborting — Gitea is not available" + exit 1 + fi + + # Create admin user first (required for subsequent operations) + if ! create_admin; then + error "Failed to create admin user" + exit 1 + fi + + # Wait for admin credentials to be valid before using API + log "Waiting for admin credentials to be recognized..." + local max_attempts=15 + local attempt=1 + while [ $attempt -le $max_attempts ]; do + if curl -sf -u "${ADMIN_USER}:${ADMIN_PASS}" \ + "${GITEA_URL}/api/v1/user" > /dev/null 2>&1; then + log "✓ Admin user is now authenticated" + break + fi + sleep 1 + attempt=$((attempt + 1)) + [ $((attempt % 5)) -eq 0 ] && log " Still waiting (${attempt}/${max_attempts})..." + done + + # Create organization + if ! create_org; then + warn "Failed to create organization (may already exist)" + fi + + # Create repository + if ! create_repo; then + warn "Failed to create repository (may already exist)" + fi + + # Push source code (optional) + push_sut_code || true + + log "==========================================" + log "✅ Gitea initialization complete!" + log "==========================================" + log " URL: ${GITEA_URL}" + log " Admin: ${ADMIN_USER}" + log " Repository: ${GITEA_URL}/${ORG_NAME}/${REPO_NAME}" + log "==========================================" +} + +# Run main initialization +main "$@" + diff --git a/jenkins/Dockerfile b/jenkins/Dockerfile new file mode 100644 index 0000000..ac483fa --- /dev/null +++ b/jenkins/Dockerfile @@ -0,0 +1,28 @@ +FROM jenkins/jenkins:lts-jdk17 + +# Skip setup wizard — pre-configured via JCasC +ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false -Dhudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION=false" +ENV CASC_JENKINS_CONFIG=/var/jenkins_home/casc_configs + +USER root + +# Install prerequisite tools for pipeline stages (Python 3, Node.js, Docker CLI) +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 python3-pip python3-venv \ + nodejs npm \ + curl \ + docker.io \ + && rm -rf /var/lib/apt/lists/* + +# Allow jenkins user to run docker commands via the host socket +RUN usermod -aG docker jenkins || true + +USER jenkins + +# Install plugins from list +COPY plugins.txt /usr/share/jenkins/plugins.txt +RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/plugins.txt \ + --latest false + +# Copy Jenkins Configuration as Code +COPY casc/ /var/jenkins_home/casc_configs/ diff --git a/jenkins/casc/jenkins.yaml b/jenkins/casc/jenkins.yaml new file mode 100644 index 0000000..fba3b64 --- /dev/null +++ b/jenkins/casc/jenkins.yaml @@ -0,0 +1,106 @@ +--- +# Jenkins Configuration as Code (JCasC) +# Pre-configures Jenkins for the Fellowship DevOps Escape Room tutorial + +jenkins: + systemMessage: | + 🧙 Welcome to the Fellowship's Jenkins CI! + One does not simply skip the pipeline... + This Jenkins is pre-configured for the LOTR SUT tutorial. + + numExecutors: 2 + mode: NORMAL + labelString: "" + + securityRealm: + local: + allowsSignup: false + users: + - id: "fellowship" + name: "Gandalf the Grey" + password: "${JENKINS_ADMIN_PASSWORD:-fellowship123}" + properties: + - mailer: + emailAddress: "gandalf@fellowship.local" + + authorizationStrategy: + loggedInUsersCanDoAnything: + allowAnonymousRead: true + + globalNodeProperties: + - envVars: + env: + - key: "GITEA_URL" + value: "http://gitea:3000" + - key: "GITEA_HTTP_URL" + value: "${GITEA_HTTP_URL:-http://gitea:3000}" + - key: "SUT_REPO" + value: "http://gitea:3000/fellowship/lotr-sut.git" + +credentials: + system: + domainCredentials: + - credentials: + - usernamePassword: + description: "Gitea repository credentials" + id: "gitea-credentials" + password: "fellowship123" + scope: GLOBAL + username: "fellowship" + +unclassified: + location: + # JENKINS_URL is injected by the devops-escape-room docker-compose from the + # per-instance .env file (e.g. https://jenkins-fellowship-pool-8.fellowship.testingfantasy.com/). + # Falls back to the plain HTTP address for local / offline use. + url: "${JENKINS_URL:-http://localhost:8080/}" + adminAddress: "gandalf@fellowship.local" + + mailer: + smtpHost: "mailhog" + smtpPort: "1025" + useSsl: false + charset: "UTF-8" + + giteaServers: + servers: + - displayName: "Fellowship Gitea" + serverUrl: "http://gitea:3000" + manageHooks: false + +jobs: + - script: > + pipelineJob('fellowship-sut-pipeline') { + displayName('Fellowship SUT — Build, Deploy & Test') + description('Complete CI/CD pipeline: build the SUT, deploy to containers, run e2e tests') + + definition { + cpsScm { + scm { + git { + remote { + url('http://gitea:3000/fellowship-org/lotr-sut.git') + credentialsId('gitea-credentials') + } + branch('*/main') + extensions { + wipeOutWorkspace() + } + } + } + scriptPath('Jenkinsfile') + } + } + + triggers { + scm('H/5 * * * *') // Poll Gitea every 5 minutes for changes + } + + properties { + logRotator { + numToKeep(10) + daysToKeepStr('30') + } + disableConcurrentBuilds() + } + } diff --git a/jenkins/plugins.txt b/jenkins/plugins.txt new file mode 100644 index 0000000..c0c830f --- /dev/null +++ b/jenkins/plugins.txt @@ -0,0 +1,36 @@ +# Jenkins plugins for Fellowship DevOps Escape Room +# Essential plugins only - avoids conflicts and improves startup reliability + +# CI/CD Pipeline (core workflow engine) +workflow-aggregator +pipeline-stage-view + +# Git & Source Control +git +gitea + +# Configuration as Code +configuration-as-code +configuration-as-code-support + +# Build & Test Reporting +junit +htmlpublisher +build-timeout + +# Credentials (essential for git operations) +credentials +credentials-binding +plain-credentials +ssh-credentials + +# Notifications +mailer + +# Docker integration +docker-workflow + +# Utility +timestamper +ws-cleanup +antisamy-markup-formatter diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..a4594a1 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,79 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss; + + # Upstream servers + upstream backend { + server backend:5000; + } + + upstream frontend { + server frontend:3000; + } + + server { + listen 80; + server_name _; + + # Increase body size for API requests + client_max_body_size 10M; + + # Swagger UI static files - must come before /api + location ~ ^/api/swagger/(static|favicon|swagger-ui) { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # API routes - proxy to backend (CORS handled by Flask) + location /api { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + # Note: CORS headers are handled by Flask-CORS, not nginx + } + + # Frontend routes - proxy to React app + location / { + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + } +} \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..4c0df36 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,26 @@ +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = + --strict-markers + --tb=short + --html=reports/report.html + --self-contained-html + -v +markers = + smoke: Smoke tests + regression: Regression tests + dark_magic: Tests for dark magic challenges + api: API endpoint tests + ui: UI/Playwright tests + bdd: Gherkin/BDD style test scenarios + realstack: Requires real docker-compose frontend/backend/caddy stack + cors: CORS behavior and preflight verification + login: Login flow coverage + chat: NPC chat interaction coverage + npc: NPC-specific scenarios + unit: Unit tests + integration: Integration tests + script: Script/shell script tests diff --git a/setup_fellowship.sh b/setup_fellowship.sh new file mode 100755 index 0000000..c685e6e --- /dev/null +++ b/setup_fellowship.sh @@ -0,0 +1,1453 @@ +#!/bin/bash +# Fellowship EC2 Instance Setup Script +# This script is downloaded from S3 and executed by user_data.sh +# Contains all setup logic: Docker, Docker Compose, DevOps Escape Room, and Fellowship SUT +set -e + +# Logging setup - redirect all output to log file +LOG_FILE="/var/log/user-data.log" +exec > >(tee -a "$LOG_FILE") 2>&1 + +# Function to log with timestamp +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" +} + +IMDS_BASE_URL="http://169.254.169.254/latest" +IMDS_TOKEN="" + +get_imds_token() { + if [ -n "$IMDS_TOKEN" ]; then + echo "$IMDS_TOKEN" + return 0 + fi + + IMDS_TOKEN=$(curl -s --max-time 5 --connect-timeout 2 -X PUT "${IMDS_BASE_URL}/api/token" \ + -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" 2>/dev/null || echo "") + + if [ -n "$IMDS_TOKEN" ]; then + echo "$IMDS_TOKEN" + return 0 + fi + + return 1 +} + +get_instance_metadata() { + local path="$1" + local token + token=$(get_imds_token 2>/dev/null || echo "") + + if [ -n "$token" ]; then + curl -s --max-time 5 --connect-timeout 2 -H "X-aws-ec2-metadata-token: ${token}" \ + "${IMDS_BASE_URL}/meta-data/${path}" 2>/dev/null || echo "" + else + curl -s --max-time 5 --connect-timeout 2 "${IMDS_BASE_URL}/meta-data/${path}" 2>/dev/null || echo "" + fi +} + +log "==========================================" +log "Fellowship Setup Script Started" +log "==========================================" + +# Get AWS region with retries and fallback +AWS_REGION="" +for i in {1..5}; do + AWS_REGION=$(get_instance_metadata "placement/region") + [ -n "$AWS_REGION" ] && break + [ $i -lt 5 ] && sleep 2 +done +[ -z "$AWS_REGION" ] && AWS_REGION="eu-west-1" && log "Using default region: $AWS_REGION" || log "Region: $AWS_REGION" + +# Function to wait for yum lock +wait_for_yum() { + while sudo fuser /var/run/yum.pid >/dev/null 2>&1; do + log "Waiting for yum lock to be released..." + sleep 5 + done +} + +# Helper function to run docker commands as ec2-user with proper group membership +run_as_ec2user_docker() { + local cmd="$1" + # Use sg (switch group) to ensure docker group is active in the subshell + # This is more reliable than su - which may not pick up new group membership immediately + sg docker -c "su - ec2-user -c '$cmd'" +} + +ensure_swap_for_small_instances() { + if [ -f /swapfile ]; then + return 0 + fi + + local mem_mb + mem_mb=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo 2>/dev/null || echo "0") + if [ "$mem_mb" -ge 3500 ]; then + return 0 + fi + + log "Low-memory instance detected (${mem_mb}MB). Creating 2GB swap to reduce bootstrap OOM risk..." + if command -v fallocate >/dev/null 2>&1; then + fallocate -l 2G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=2048 + else + dd if=/dev/zero of=/swapfile bs=1M count=2048 + fi + chmod 600 /swapfile + mkswap /swapfile >/dev/null 2>&1 || true + swapon /swapfile >/dev/null 2>&1 || true + grep -q '^/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab + log "✓ Swap configured" +} + +# Wait for any existing yum processes to complete +log "Checking for yum locks..." +wait_for_yum + +# Update and install dependencies +log "Installing Docker and Git..." +yum update -y +wait_for_yum +yum install -y docker git +systemctl start docker +systemctl enable docker +usermod -aG docker ec2-user +ensure_swap_for_small_instances +log "✓ Docker installed and started" + +# Install Docker Compose plugin +log "Installing Docker Compose plugin..." +mkdir -p /home/ec2-user/.docker/cli-plugins/ +if curl -SL https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-linux-x86_64 -o /home/ec2-user/.docker/cli-plugins/docker-compose; then + chmod +x /home/ec2-user/.docker/cli-plugins/docker-compose + chown -R ec2-user:ec2-user /home/ec2-user/.docker + log "✓ Docker Compose plugin installed" +else + log "ERROR: Failed to download Docker Compose plugin" + exit 1 +fi + +dump_runtime_diagnostics() { + log "Runtime diagnostics (docker compose ps):" + run_as_ec2user_docker "cd ~ && docker compose ps" 2>&1 | sed 's/^/ /' || true + log "Runtime diagnostics (caddy logs tail):" + run_as_ec2user_docker "cd ~ && docker compose logs caddy --tail 120" 2>&1 | sed 's/^/ /' || true + log "Runtime diagnostics (frontend logs tail):" + run_as_ec2user_docker "cd ~ && docker compose logs frontend --tail 120" 2>&1 | sed 's/^/ /' || true + log "Runtime diagnostics (backend logs tail):" + run_as_ec2user_docker "cd ~ && docker compose logs backend --tail 120" 2>&1 | sed 's/^/ /' || true +} + +# Function to verify network connectivity to external services (esp. github.com) +verify_network_connectivity() { + log "Verifying network connectivity to external services..." + local connectivity_ok=true + + # Test DNS resolution for github.com + log " Testing DNS resolution for github.com..." + if ! getent hosts github.com >/dev/null 2>&1; then + log " WARNING: DNS resolution for github.com failed initially, retrying..." + sleep 2 + if ! getent hosts github.com >/dev/null 2>&1; then + log " ERROR: DNS resolution for github.com failed" + connectivity_ok=false + else + log " OK: DNS resolution for github.com OK after retry" + fi + else + log " OK: DNS resolution for github.com OK" + fi + + # Test HTTP connectivity to github.com + log " Testing HTTP connectivity to github.com..." + if ! timeout 10 curl -s -o /dev/null --head https://github.com 2>/dev/null; then + log " WARNING: Cannot reach github.com, retrying..." + sleep 2 + if ! timeout 10 curl -s -o /dev/null --head https://github.com 2>/dev/null; then + log " ERROR: Cannot reach github.com (required for docker builds)" + connectivity_ok=false + else + log " OK: Connectivity to github.com OK after retry" + fi + else + log " OK: Connectivity to github.com OK" + fi + + if [ "$connectivity_ok" = "true" ]; then + return 0 + else + return 1 + fi +} + +# DevOps Escape Room stack (Jenkins + Gitea + code-server + MailHog) +log "Setting up DevOps Escape Room stack..." + +# Derive DevOps HTTPS subdomain names early if CADDY_DOMAIN is already available +# from the user_data environment variable (the common case for classroom instances). +# If CADDY_DOMAIN is not set yet (EC2-tag fallback), these will be re-derived later +# once the domain is confirmed. +if [ -n "${CADDY_DOMAIN:-}" ]; then + JENKINS_DOMAIN="${JENKINS_DOMAIN:-jenkins-${CADDY_DOMAIN}}" + IDE_DOMAIN="${IDE_DOMAIN:-ide-${CADDY_DOMAIN}}" + log "DevOps HTTPS subdomains derived from CADDY_DOMAIN:" + log " Jenkins: ${JENKINS_DOMAIN}" + log " IDE: ${IDE_DOMAIN}" +else + JENKINS_DOMAIN="" + IDE_DOMAIN="" +fi + +# The devops-escape-room directory ships with the SUT tarball (extracted above), +# but the SUT extraction happens later. Write an inline compose file here so +# the stack can start in parallel. After the SUT is extracted the files will be +# replaced by the version from the tarball, which is identical. +mkdir -p /home/ec2-user/devops-escape-room +mkdir -p /home/ec2-user/jenkins/casc +mkdir -p /home/ec2-user/gitea + +# ── Jenkins Dockerfile & plugins ──────────────────────────────────────────── +cat > /home/ec2-user/jenkins/Dockerfile << 'JENKINSEOF' +FROM jenkins/jenkins:lts-jdk17 +ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false" +ENV CASC_JENKINS_CONFIG=/var/jenkins_home/casc_configs +USER root +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 python3-pip nodejs npm curl docker.io \ + && rm -rf /var/lib/apt/lists/* \ + && usermod -aG docker jenkins || true +USER jenkins +COPY plugins.txt /usr/share/jenkins/plugins.txt +RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/plugins.txt --latest false +COPY casc/ /var/jenkins_home/casc_configs/ +JENKINSEOF + +cat > /home/ec2-user/jenkins/plugins.txt << 'PLUGINSEOF' +workflow-aggregator +pipeline-stage-view +blueocean +git +gitea +configuration-as-code +job-dsl +junit +htmlpublisher +build-timeout +credentials +credentials-binding +plain-credentials +ssh-credentials +mailer +email-ext +dashboard-view +build-monitor-plugin +docker-workflow +timestamper +ws-cleanup +antisamy-markup-formatter +PLUGINSEOF + +cat > /home/ec2-user/jenkins/casc/jenkins.yaml << 'CASCEOF' +jenkins: + systemMessage: | + 🧙 Welcome to the Fellowship's Jenkins CI! + One does not simply skip the pipeline... + + numExecutors: 2 + securityRealm: + local: + allowsSignup: false + users: + - id: "fellowship" + name: "Gandalf the Grey" + password: "${JENKINS_ADMIN_PASSWORD:-fellowship123}" + authorizationStrategy: + loggedInUsersCanDoAnything: + allowAnonymousRead: true + globalNodeProperties: + - envVars: + env: + - key: "GITEA_URL" + value: "http://gitea:3000" + - key: "SUT_REPO" + value: "http://gitea:3000/fellowship/lotr-sut.git" + +unclassified: + location: + url: "http://localhost:8080/" + adminAddress: "gandalf@fellowship.local" + mailer: + smtpHost: "mailhog" + smtpPort: "1025" + useSsl: false + charset: "UTF-8" + +jobs: + - script: | + pipelineJob('fellowship-sut-pipeline') { + displayName('Fellowship SUT — CI Pipeline') + description('One pipeline to build them all, and in the darkness test them.') + definition { + cpsScm { + scm { + git { + remote { + url('http://gitea:3000/fellowship/lotr-sut.git') + } + branch('*/main') + } + } + scriptPath('Jenkinsfile') + } + } + triggers { + scm('H/5 * * * *') + } + logRotator { + numToKeep(10) + } + } +CASCEOF + +# ── Gitea init script ──────────────────────────────────────────────────────── +cat > /home/ec2-user/gitea/init.sh << 'GITEAINITEOF' +#!/bin/sh +set -e +GITEA_URL="${GITEA_URL:-http://gitea:3000}" +ADMIN_USER="${GITEA_ADMIN_USER:-fellowship}" +ADMIN_PASS="${GITEA_ADMIN_PASSWORD:-fellowship123}" +ADMIN_EMAIL="${GITEA_ADMIN_EMAIL:-gandalf@fellowship.local}" +ORG_NAME="fellowship" +REPO_NAME="lotr-sut" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [gitea-init] $*"; } + +wait_for_gitea() { + log "Waiting for Gitea at ${GITEA_URL}..." + i=0; while [ $i -lt 30 ]; do + curl -sf "${GITEA_URL}/api/v1/version" > /dev/null 2>&1 && log "✓ Gitea ready" && return 0 + log " attempt $((i+1))/30 — waiting 5s..."; sleep 5; i=$((i+1)) + done + log "ERROR: Gitea not ready"; return 1 +} + +wait_for_admin() { + log "Waiting for Gitea admin user '${ADMIN_USER}' to be ready..." + i=0; while [ $i -lt 30 ]; do + curl -sf -u "${ADMIN_USER}:${ADMIN_PASS}" "${GITEA_URL}/api/v1/user" > /dev/null 2>&1 \ + && log "✓ Admin user ready" && return 0 + log " attempt $((i+1))/30 — waiting 3s..."; sleep 3; i=$((i+1)) + done + log "WARNING: Admin user not ready — proceeding anyway (some operations may fail)" + return 0 +} + +api() { curl -sf -u "${ADMIN_USER}:${ADMIN_PASS}" "$@"; } + +wait_for_gitea +wait_for_admin + +# Create org (ignore if exists) +api -X POST "${GITEA_URL}/api/v1/orgs" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"${ORG_NAME}\",\"full_name\":\"The Fellowship of the Ring\",\"visibility\":\"public\"}" \ + > /dev/null 2>&1 || true +log "✓ Organization '${ORG_NAME}' ready" + +# Create repo (ignore if exists) +api -X POST "${GITEA_URL}/api/v1/orgs/${ORG_NAME}/repos" \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"${REPO_NAME}\",\"description\":\"LOTR SUT\",\"private\":false,\"auto_init\":false,\"default_branch\":\"main\"}" \ + > /dev/null 2>&1 || true +log "✓ Repository '${ORG_NAME}/${REPO_NAME}' ready" + +# Push code if repo is empty +COMMITS=$(api "${GITEA_URL}/api/v1/repos/${ORG_NAME}/${REPO_NAME}/commits?limit=1" 2>/dev/null | grep -c '"sha"' || echo "0") +if [ "$COMMITS" -gt 0 ]; then + log "✓ Repository already has commits — skipping push"; exit 0 +fi + +SRC="" +for d in /sut-source /home/ec2-user; do + [ -f "${d}/docker-compose.yml" ] && [ -d "${d}/sut" ] && SRC="$d" && break +done + +if [ -z "$SRC" ]; then + log "WARNING: SUT source not found — skipping code push"; exit 0 +fi + +log "Pushing code from ${SRC} to Gitea..." +AUTH_URL=$(echo "${GITEA_URL}/${ORG_NAME}/${REPO_NAME}.git" | sed "s|http://|http://${ADMIN_USER}:${ADMIN_PASS}@|") +TMP=$(mktemp -d) +cp -a "${SRC}/." "${TMP}/" +cd "${TMP}" +rm -rf .git +git init -b main +git config user.email "${ADMIN_EMAIL}" +git config user.name "Gandalf the Grey" +git add -A +git commit -m "🧙 Initial commit: The Fellowship's Quest List SUT" +git remote add gitea "${AUTH_URL}" +git push gitea main +cd /; rm -rf "${TMP}" +log "✓ SUT code pushed to Gitea" +GITEAINITEOF +chmod +x /home/ec2-user/gitea/init.sh + +# ── code-server custom Dockerfile + entrypoint ────────────────────────────── +# Extends codercom/code-server with Docker CLI, Compose v2, and pre-installed +# VS Code extensions (Python, Playwright, Copilot, Jupyter, Prettier). +mkdir -p /home/ec2-user/devops-escape-room/code-server + +cat > /home/ec2-user/devops-escape-room/code-server/Dockerfile << 'CSRVDOCKEREOF' +FROM codercom/code-server:latest + +USER root + +# Docker CLI + gosu (for clean privilege drop) + utilities +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + docker.io \ + gosu \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Docker Compose v2 plugin + legacy symlink +RUN mkdir -p /usr/local/lib/docker/cli-plugins && \ + curl -fsSL \ + "https://github.com/docker/compose/releases/download/v2.27.0/docker-compose-linux-x86_64" \ + -o /usr/local/lib/docker/cli-plugins/docker-compose && \ + chmod +x /usr/local/lib/docker/cli-plugins/docker-compose && \ + ln -sf /usr/local/lib/docker/cli-plugins/docker-compose /usr/local/bin/docker-compose + +# Coder user docker group (GID re-aligned at runtime by entrypoint) +RUN groupadd -g 999 docker 2>/dev/null || groupmod -g 999 docker 2>/dev/null || true && \ + usermod -aG docker coder + +COPY entrypoint.sh /usr/bin/fellowship-docker-init.sh +RUN chmod +x /usr/bin/fellowship-docker-init.sh + +USER root +ENTRYPOINT ["/usr/bin/fellowship-docker-init.sh"] +CSRVDOCKEREOF + +cat > /home/ec2-user/devops-escape-room/code-server/entrypoint.sh << 'CSRVENTRYEOF' +#!/bin/bash +# Fellowship code-server entrypoint: fixes Docker GID, installs extensions, starts IDE +set -e +log() { echo "[$(date '+%H:%M:%S')] [fellowship-init] $*"; } + +# Fix docker group GID to match host socket +if [ -S /var/run/docker.sock ]; then + DOCK_GID=$(stat -c '%g' /var/run/docker.sock) + if getent group docker > /dev/null 2>&1; then + groupmod -g "${DOCK_GID}" docker 2>/dev/null || true + else + groupadd -g "${DOCK_GID}" docker 2>/dev/null || true + fi + usermod -aG docker coder 2>/dev/null || true + chmod 666 /var/run/docker.sock 2>/dev/null || true + log "Docker group GID aligned to ${DOCK_GID}" +else + log "WARNING: docker.sock not mounted — docker unavailable in IDE terminal" +fi + +# Install VS Code extensions as coder user +log "Installing VS Code extensions..." +for ext in ms-python.python github.copilot ms-playwright.playwright esbenp.prettier-vscode ms-toolsai.jupyter redhat.vscode-yaml ms-azuretools.vscode-docker; do + gosu coder code-server --install-extension "${ext}" --force > /dev/null 2>&1 && \ + log " OK ${ext}" || log " SKIP ${ext}" +done + +# Default settings +SETTINGS_DIR="/home/coder/.local/share/code-server/User" +if [ ! -f "${SETTINGS_DIR}/settings.json" ]; then + gosu coder mkdir -p "${SETTINGS_DIR}" + cat > "${SETTINGS_DIR}/settings.json" << 'SETTINGSEOF' +{ + "python.defaultInterpreterPath": "/usr/bin/python3", + "editor.formatOnSave": true, + "terminal.integrated.defaultProfile.linux": "bash", + "git.autofetch": true, + "docker.host": "unix:///var/run/docker.sock" +} +SETTINGSEOF + chown coder:coder "${SETTINGS_DIR}/settings.json" + log "Default settings.json written" +fi + +log "Starting code-server..." +exec gosu coder /usr/bin/entrypoint.sh "$@" +CSRVENTRYEOF +chmod +x /home/ec2-user/devops-escape-room/code-server/entrypoint.sh +chown -R ec2-user:ec2-user /home/ec2-user/devops-escape-room/code-server +log "✓ code-server Dockerfile and entrypoint written" + +# ── devops-escape-room docker-compose ──────────────────────────────────────── +cat > /home/ec2-user/devops-escape-room/docker-compose.yml << 'COMPOSEEOF' +# Fellowship DevOps Escape Room Stack +# Jenkins CI | Gitea Git | code-server IDE | MailHog mail +# +# Jenkins: http://HOST:8080 (fellowship / fellowship123) +# Gitea: http://HOST:3030 (fellowship / fellowship123) +# code-server: http://HOST:8443 (password: fellowship) +# MailHog: http://HOST:8025 + +services: + jenkins: + build: + context: ../jenkins + dockerfile: Dockerfile + image: fellowship-jenkins:latest + container_name: fellowship-jenkins + restart: unless-stopped + ports: + - "8080:8080" + - "50000:50000" + volumes: + - jenkins_home:/var/jenkins_home + - /var/run/docker.sock:/var/run/docker.sock + environment: + JENKINS_ADMIN_PASSWORD: ${JENKINS_ADMIN_PASSWORD:-fellowship123} + CASC_JENKINS_CONFIG: /var/jenkins_home/casc_configs + # JENKINS_URL is written to devops-escape-room/.env by setup_fellowship.sh + # once CADDY_DOMAIN is known, so Jenkins knows its canonical HTTPS URL. + JENKINS_URL: ${JENKINS_URL:-http://localhost:8080/} + depends_on: + gitea: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:8080/login || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 90s + + gitea: + image: gitea/gitea:1.22 + container_name: fellowship-gitea + restart: unless-stopped + ports: + - "3030:3000" + - "2222:22" + volumes: + - gitea_data:/data + environment: + USER_UID: "1000" + USER_GID: "1000" + GITEA__database__DB_TYPE: sqlite3 + GITEA__server__DOMAIN: ${GITEA_DOMAIN:-localhost} + GITEA__server__HTTP_PORT: "3000" + GITEA__server__ROOT_URL: ${GITEA_ROOT_URL:-http://localhost:3030/} + GITEA__server__SSH_DOMAIN: localhost + GITEA__server__SSH_PORT: "2222" + GITEA__service__DISABLE_REGISTRATION: "false" + GITEA__service__REQUIRE_SIGNIN_VIEW: "false" + GITEA__security__INSTALL_LOCK: "true" + GITEA__mailer__ENABLED: "false" + # Pre-create the admin user on first boot via Gitea environment variables. + # Without these the gitea-init container cannot authenticate against the API. + GITEA__admin__ADMIN_USER: ${GITEA_ADMIN_USER:-fellowship} + GITEA__admin__ADMIN_PASSWD: ${GITEA_ADMIN_PASSWORD:-fellowship123} + GITEA__admin__ADMIN_EMAIL: ${GITEA_ADMIN_EMAIL:-gandalf@fellowship.local} + GITEA__admin__SEND_NOTIFY: "false" + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:3000/api/v1/version || exit 1"] + interval: 15s + timeout: 5s + retries: 6 + start_period: 30s + + gitea-init: + image: alpine/git:latest + container_name: fellowship-gitea-init + restart: on-failure:5 + environment: + GITEA_URL: "http://gitea:3000" + GITEA_ADMIN_USER: ${GITEA_ADMIN_USER:-fellowship} + GITEA_ADMIN_PASSWORD: ${GITEA_ADMIN_PASSWORD:-fellowship123} + GITEA_ADMIN_EMAIL: ${GITEA_ADMIN_EMAIL:-gandalf@fellowship.local} + GITEA_DOMAIN: ${GITEA_DOMAIN:-} + SUT_SOURCE_DIR: /sut-source + volumes: + - ../gitea/init.sh:/init.sh:ro + - /home/ec2-user/sut:/sut-source/sut:ro + - /home/ec2-user/docker-compose.yml:/sut-source/docker-compose.yml:ro + - /home/ec2-user/caddy:/sut-source/caddy:ro + - /home/ec2-user/nginx:/sut-source/nginx:ro + - /home/ec2-user/Jenkinsfile:/sut-source/Jenkinsfile:ro + entrypoint: ["/bin/sh", "/init.sh"] + depends_on: + gitea: + condition: service_healthy + + code-server: + build: + context: ./code-server + dockerfile: Dockerfile + image: fellowship-code-server:latest + container_name: fellowship-code-server + restart: unless-stopped + ports: + - "8443:8080" + volumes: + - /home/ec2-user:/home/coder/fellowship:rw + - codeserver_config:/home/coder/.config + # Mount Docker socket so students can run docker compose from the IDE terminal + - /var/run/docker.sock:/var/run/docker.sock + environment: + PASSWORD: ${CODESERVER_PASSWORD:-fellowship} + command: + - --auth=password + - --bind-addr=0.0.0.0:8080 + - /home/coder/fellowship + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:8080/ || exit 1"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + mailhog: + image: mailhog/mailhog:v1.0.1 + container_name: fellowship-mailhog + restart: unless-stopped + ports: + - "1025:1025" + - "8025:8025" + +volumes: + jenkins_home: + driver: local + gitea_data: + driver: local +COMPOSEEOF + +# Pre-create code-server config directory with proper permissions (bind mount) +# The coder user inside the container runs as 1000:1000, so the host directory +# must be owned by ec2-user (uid 1000) to avoid "permission denied" errors. +mkdir -p /home/ec2-user/.codeserver-config +chown 1000:1000 /home/ec2-user/.codeserver-config +chmod 700 /home/ec2-user/.codeserver-config + +chown -R ec2-user:ec2-user /home/ec2-user/devops-escape-room +chown -R ec2-user:ec2-user /home/ec2-user/jenkins +chown -R ec2-user:ec2-user /home/ec2-user/gitea + +# Write the devops-escape-room .env so Docker Compose can inject the correct +# JENKINS_URL into the Jenkins container before it starts. +# If JENKINS_DOMAIN is not yet known (EC2-tag path), Jenkins defaults to localhost +# and will be updated automatically after the SUT domain is resolved. +DEVOPS_JENKINS_URL="${JENKINS_DOMAIN:+https://${JENKINS_DOMAIN}/}" +DEVOPS_JENKINS_URL="${DEVOPS_JENKINS_URL:-http://localhost:8080/}" + +# Copy devops-escape-room environment template to .env +log "Setting up devops-escape-room environment..." +if [ -f /home/ec2-user/devops-escape-room/.env.prod ]; then + cp /home/ec2-user/devops-escape-room/.env.prod /home/ec2-user/devops-escape-room/.env + chown ec2-user:ec2-user /home/ec2-user/devops-escape-room/.env + chmod 644 /home/ec2-user/devops-escape-room/.env + log "✓ Copied devops-escape-room/.env.prod to .env" + + # Update JENKINS_URL if domain is available + if [ -n "$JENKINS_DOMAIN" ]; then + sed -i "" "s|^JENKINS_URL=.*|JENKINS_URL=${DEVOPS_JENKINS_URL}|" /home/ec2-user/devops-escape-room/.env + fi +else + # Fallback: create .env inline if template not found + cat > /home/ec2-user/devops-escape-room/.env << EOF +JENKINS_ADMIN_PASSWORD=fellowship123 +GITEA_ADMIN_USER=fellowship +GITEA_ADMIN_PASSWORD=fellowship123 +GITEA_ADMIN_EMAIL=gandalf@fellowship.local +CODESERVER_PASSWORD=fellowship +JENKINS_URL=${DEVOPS_JENKINS_URL} +GITEA_DOMAIN= +EOF + chown ec2-user:ec2-user /home/ec2-user/devops-escape-room/.env + log "✓ Wrote devops-escape-room/.env (fallback mode, JENKINS_URL=${DEVOPS_JENKINS_URL})" +fi + +log "Building and starting DevOps Escape Room stack..." +if run_as_ec2user_docker "cd ~/devops-escape-room && docker compose up -d --build" 2>&1 | \ + tee -a "$LOG_FILE"; then + log "✓ DevOps Escape Room stack started (Jenkins, Gitea, code-server, MailHog)" +else + log "WARNING: DevOps Escape Room stack may not have started cleanly — check logs" +fi + +# Fellowship SUT Setup +log "Setting up Fellowship SUT..." + +# Get SUT bucket from SSM +log "Retrieving SUT bucket from SSM: /classroom/fellowship/sut-bucket" +SUT_BUCKET=$(aws ssm get-parameter --name "/classroom/fellowship/sut-bucket" --query "Parameter.Value" --output text --region "${AWS_REGION}" 2>&1) +if [ $? -ne 0 ] || [ -z "$SUT_BUCKET" ] || [ "$SUT_BUCKET" = "None" ]; then + log "ERROR: Failed to get SUT bucket from SSM" + log "Error: $SUT_BUCKET" + exit 1 +fi +log "SUT bucket: $SUT_BUCKET" + +# Download SUT from S3 +log "Finding latest SUT artifact in S3..." +LATEST_TAR=$(aws s3 ls "s3://${SUT_BUCKET}/" --region "${AWS_REGION}" | \ + awk '/fellowship-sut-.*\.tar\.gz$/ {print $1" "$2" "$4}' | \ + sort | tail -n 1 | awk '{print $3}') + +if [ -z "$LATEST_TAR" ]; then + log "ERROR: No fellowship-sut-*.tar.gz artifact found in S3 bucket" + exit 1 +fi + +log "Downloading latest SUT artifact: $LATEST_TAR" +if ! aws s3 cp "s3://${SUT_BUCKET}/${LATEST_TAR}" /tmp/fellowship-sut.tar.gz --region "${AWS_REGION}" >/dev/null 2>&1 || [ ! -f "/tmp/fellowship-sut.tar.gz" ]; then + log "ERROR: Failed to download SUT from S3" + log "Expected location: s3://${SUT_BUCKET}/${LATEST_TAR}" + exit 1 +fi +log "✓ SUT downloaded" + +# Extract SUT +log "Extracting SUT..." +if ! tar -xzf /tmp/fellowship-sut.tar.gz -C /home/ec2-user/ 2>/dev/null; then + log "ERROR: Failed to extract SUT" + exit 1 +fi +rm -f /tmp/fellowship-sut.tar.gz + +# Tarball extracts to sut/ and docker-compose.yml at home root - chown both +chown -R ec2-user:ec2-user /home/ec2-user/sut 2>/dev/null || true +chown ec2-user:ec2-user /home/ec2-user/docker-compose.yml 2>/dev/null || true +chown -R ec2-user:ec2-user /home/ec2-user/caddy 2>/dev/null || true +chown -R ec2-user:ec2-user /home/ec2-user/nginx 2>/dev/null || true +log "✓ SUT extracted" + +# Copy environment template to .env for docker-compose +# This ensures COMPOSE_PROJECT_NAME=fellowship (production) is used +log "Setting up production environment (.env.prod → .env)..." +if [ -f /home/ec2-user/.env.prod ]; then + cp /home/ec2-user/.env.prod /home/ec2-user/.env + chown ec2-user:ec2-user /home/ec2-user/.env + chmod 644 /home/ec2-user/.env + log "✓ Copied .env.prod to .env" +else + log "WARNING: .env.prod not found — .env will be created from scratch" +fi + +# Get instance domain for Caddy +# PRIORITY 1: Check if domain was passed via user_data environment variable +# This is the most reliable method - domain is known before instance creation +log "Getting instance domain for Caddy..." +if [ -n "$CADDY_DOMAIN" ] && [ "$CADDY_DOMAIN" != "" ]; then + log "✓ Found Caddy domain from user_data environment: $CADDY_DOMAIN" + # Domain is already set, no need to query EC2 tags +else + # PRIORITY 2: Fallback to EC2 tags (requires instance ID from metadata service) + log "Domain not in environment, attempting to get from EC2 tags..." + INSTANCE_ID="" + CADDY_DOMAIN="" + + # Retry getting instance ID (metadata service may not be ready immediately) + for i in {1..10}; do + INSTANCE_ID=$(get_instance_metadata "instance-id") + if [ -n "$INSTANCE_ID" ]; then + log "✓ Got instance ID: $INSTANCE_ID" + break + fi + if [ $i -lt 10 ]; then + log " Attempt $i/10: Instance ID not available yet, waiting 2s..." + sleep 2 + fi + done + + if [ -n "$INSTANCE_ID" ]; then + # Get domain from instance tags (set by Lambda BEFORE instance creation) + # With predictable domain names, this should be available immediately + log "Retrieving HttpsDomain tag from instance tags..." + for i in {1..6}; do + CADDY_DOMAIN=$(aws ec2 describe-tags --region "${AWS_REGION}" --filters "Name=resource-id,Values=${INSTANCE_ID}" "Name=key,Values=HttpsDomain" --query "Tags[0].Value" --output text 2>/dev/null || echo "") + if [ -n "$CADDY_DOMAIN" ] && [ "$CADDY_DOMAIN" != "None" ] && [ "$CADDY_DOMAIN" != "" ]; then + log "✓ Found Caddy domain from tags: $CADDY_DOMAIN" + break + fi + if [ $i -lt 6 ]; then + log " Attempt $i/6: HttpsDomain tag not found yet, waiting 2s..." + sleep 2 + fi + done + else + log "WARNING: Could not get instance ID after retries" + fi + + # Final check + if [ -z "$CADDY_DOMAIN" ] || [ "$CADDY_DOMAIN" = "None" ] || [ "$CADDY_DOMAIN" = "" ]; then + log "ERROR: Caddy domain not found - cannot deploy AWS Fellowship SUT without a valid domain" + log " Ensure HttpsDomain tag is set before instance bootstrap" + exit 1 + fi +fi + +# Normalize to lowercase to avoid mixed-case DNS/tag drift +CADDY_DOMAIN=$(echo "$CADDY_DOMAIN" | tr '[:upper:]' '[:lower:]') + +# Enforce domain presence for AWS deployment +if [ -z "$CADDY_DOMAIN" ] || [ "$CADDY_DOMAIN" = "None" ] || [ "$CADDY_DOMAIN" = "" ]; then + log "ERROR: Caddy domain is required for AWS deployment" + exit 1 +fi + +# Wait for DNS propagation before starting containers (required for Caddy automatic HTTPS) +PUBLIC_IP_FOR_DNS=$(get_instance_metadata "public-ipv4") +if [ -z "$PUBLIC_IP_FOR_DNS" ]; then + log "ERROR: Could not retrieve instance public IP for DNS verification" + exit 1 +fi + +resolve_domain_ipv4() { + local domain="$1" + local resolved_ip + + resolved_ip=$(getent ahostsv4 "$domain" 2>/dev/null | awk '{print $1}' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || true) + if [ -z "$resolved_ip" ]; then + resolved_ip=$(nslookup "$domain" 2>/dev/null | awk '/^Address: / {print $2}' | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | tail -1 || true) + fi + + echo "$resolved_ip" +} + +log "Waiting for DNS propagation: ${CADDY_DOMAIN} -> ${PUBLIC_IP_FOR_DNS}" +DNS_MATCHED="false" +for i in {1..30}; do + RESOLVED_IP=$(resolve_domain_ipv4 "$CADDY_DOMAIN") + if [ "$RESOLVED_IP" = "$PUBLIC_IP_FOR_DNS" ]; then + DNS_MATCHED="true" + log "✓ DNS propagation complete (${CADDY_DOMAIN} resolves to ${RESOLVED_IP})" + break + fi + + log " Attempt $i/30: ${CADDY_DOMAIN} resolves to '${RESOLVED_IP:-unresolved}' (expected ${PUBLIC_IP_FOR_DNS}), waiting 10s..." + sleep 10 +done + +if [ "$DNS_MATCHED" != "true" ]; then + log "ERROR: DNS propagation timeout after 5 minutes" + log " ${CADDY_DOMAIN} did not resolve to instance public IP ${PUBLIC_IP_FOR_DNS}" + log " Caddy automatic HTTPS cannot succeed until DNS is correct" + exit 1 +fi + +# ── Derive DevOps HTTPS subdomain names ────────────────────────────────────── +# Pattern: jenkins-{CADDY_DOMAIN} and ide-{CADDY_DOMAIN} +# These may already be set from the early derivation above; re-assign to ensure +# they reflect the confirmed (possibly tag-derived) CADDY_DOMAIN. +JENKINS_DOMAIN="jenkins-${CADDY_DOMAIN}" +IDE_DOMAIN="ide-${CADDY_DOMAIN}" +GITEA_DOMAIN="gitea-${CADDY_DOMAIN}" +log "DevOps HTTPS subdomains confirmed:" +log " Jenkins: ${JENKINS_DOMAIN}" +log " IDE: ${IDE_DOMAIN}" +log " Gitea: ${GITEA_DOMAIN}" + +# ── Create Route53 A records for jenkins and ide subdomains ────────────────── +# Both subdomains must point to the same instance IP as CADDY_DOMAIN. +# Try to find the hosted-zone ID from (in order): +# 1. EC2 instance tag Route53ZoneId +# 2. SSM parameter /classroom/fellowship/route53-zone-id +# 3. Route53 lookup by parent domain +ROUTE53_ZONE_ID="${ROUTE53_ZONE_ID:-}" +if [ -n "$ROUTE53_ZONE_ID" ] && [ "$ROUTE53_ZONE_ID" != "None" ]; then + log "Using Route53 zone ID from environment: ${ROUTE53_ZONE_ID}" +else + ROUTE53_ZONE_ID="" +fi + +# Source 1: EC2 instance tag (set by the provisioning Lambda) +if [ -n "${INSTANCE_ID:-}" ]; then + ROUTE53_ZONE_ID=$(aws ec2 describe-tags --region "${AWS_REGION}" \ + --filters "Name=resource-id,Values=${INSTANCE_ID}" "Name=key,Values=Route53ZoneId" \ + --query "Tags[0].Value" --output text 2>/dev/null || echo "") + [ "$ROUTE53_ZONE_ID" = "None" ] && ROUTE53_ZONE_ID="" +fi + +# Source 2: SSM parameter +if [ -z "$ROUTE53_ZONE_ID" ]; then + ROUTE53_ZONE_ID=$(aws ssm get-parameter \ + --name "/classroom/fellowship/route53-zone-id" \ + --query "Parameter.Value" --output text --region "${AWS_REGION}" 2>/dev/null || echo "") + [ "$ROUTE53_ZONE_ID" = "None" ] && ROUTE53_ZONE_ID="" +fi + +# Source 3: Walk up the DNS tree stripping one label at a time until a +# matching hosted zone is found. A single sed strip is not enough when +# CADDY_DOMAIN has multiple subdomain levels (e.g. +# fellowship-.fellowship.testingfantasy.com — stripping one label gives +# fellowship.testingfantasy.com which is NOT a hosted zone; the actual zone +# is testingfantasy.com two levels up). +if [ -z "$ROUTE53_ZONE_ID" ]; then + ZONE_SUFFIX=$(echo "$CADDY_DOMAIN" | sed 's/^[^.]*\.//') + while [ -n "$ZONE_SUFFIX" ] && [ "$ZONE_SUFFIX" != "${ZONE_SUFFIX#*.}" ]; do + CANDIDATE=$(aws route53 list-hosted-zones-by-name \ + --dns-name "${ZONE_SUFFIX}" \ + --query "HostedZones[?Name==\`${ZONE_SUFFIX}.\`].Id" \ + --output text 2>/dev/null \ + | sed 's|/hostedzone/||' || echo "") + if [ -n "$CANDIDATE" ] && [ "$CANDIDATE" != "None" ] && [ "$CANDIDATE" != "\t" ]; then + ROUTE53_ZONE_ID="$CANDIDATE" + log " Route53 zone found via DNS tree walk: ${ZONE_SUFFIX} → ${ROUTE53_ZONE_ID}" + break + fi + ZONE_SUFFIX=$(echo "$ZONE_SUFFIX" | sed 's/^[^.]*\.//') + done + [ "${ROUTE53_ZONE_ID:-}" = "None" ] && ROUTE53_ZONE_ID="" +fi + +if [ -n "$ROUTE53_ZONE_ID" ]; then + log "Creating/updating Route53 A records for DevOps subdomains (zone ${ROUTE53_ZONE_ID})..." + aws route53 change-resource-record-sets \ + --hosted-zone-id "$ROUTE53_ZONE_ID" \ + --change-batch "{ + \"Comment\": \"Fellowship DevOps Escape Room HTTPS subdomains\", + \"Changes\": [ + { + \"Action\": \"UPSERT\", + \"ResourceRecordSet\": { + \"Name\": \"${JENKINS_DOMAIN}\", + \"Type\": \"A\", + \"TTL\": 60, + \"ResourceRecords\": [{\"Value\": \"${PUBLIC_IP_FOR_DNS}\"}] + } + }, + { + \"Action\": \"UPSERT\", + \"ResourceRecordSet\": { + \"Name\": \"${IDE_DOMAIN}\", + \"Type\": \"A\", + \"TTL\": 60, + \"ResourceRecords\": [{\"Value\": \"${PUBLIC_IP_FOR_DNS}\"}] + } + }, + { + \"Action\": \"UPSERT\", + \"ResourceRecordSet\": { + \"Name\": \"${GITEA_DOMAIN}\", + \"Type\": \"A\", + \"TTL\": 60, + \"ResourceRecords\": [{\"Value\": \"${PUBLIC_IP_FOR_DNS}\"}] + } + } + ] + }" \ + --region "$AWS_REGION" 2>&1 | tee -a "$LOG_FILE" \ + && log "✓ Route53 A records upserted for ${JENKINS_DOMAIN}, ${IDE_DOMAIN} and ${GITEA_DOMAIN}" \ + || log "WARNING: Route53 record update failed — manual DNS setup may be required" +else + log "WARNING: Route53 zone ID not found — DevOps subdomains need manual DNS setup:" + log " A record: ${JENKINS_DOMAIN} → ${PUBLIC_IP_FOR_DNS}" + log " A record: ${IDE_DOMAIN} → ${PUBLIC_IP_FOR_DNS}" + log " A record: ${GITEA_DOMAIN} → ${PUBLIC_IP_FOR_DNS}" +fi + +# Update the devops-escape-room .env so Jenkins knows its canonical HTTPS URL. +# The devops stack may already be running; the container will pick up the new URL +# on the next restart or if Jenkins JCasC is reloaded. +cat > /home/ec2-user/devops-escape-room/.env << EOF +JENKINS_ADMIN_PASSWORD=fellowship123 +GITEA_ADMIN_USER=fellowship +GITEA_ADMIN_PASSWORD=fellowship123 +GITEA_ADMIN_EMAIL=gandalf@fellowship.local +CODESERVER_PASSWORD=fellowship +JENKINS_URL=https://${JENKINS_DOMAIN}/ +GITEA_DOMAIN=${GITEA_DOMAIN} +GITEA_ROOT_URL=https://${GITEA_DOMAIN}/ +EOF +chown ec2-user:ec2-user /home/ec2-user/devops-escape-room/.env +log "✓ Updated devops-escape-room/.env with JENKINS_URL=https://${JENKINS_DOMAIN}/ GITEA_DOMAIN=${GITEA_DOMAIN}" + +# Restart Gitea so it picks up the correct GITEA_ROOT_URL and GITEA_DOMAIN. +# (The escape room stack was started earlier with an empty GITEA_DOMAIN; now that +# the domain is known, we restart just the gitea service so ROOT_URL is correct.) +run_as_ec2user_docker "cd ~/devops-escape-room && docker compose restart gitea" 2>&1 | tee -a "$LOG_FILE" \ + && log "✓ Gitea restarted with GITEA_ROOT_URL=https://${GITEA_DOMAIN}/" \ + || log "WARNING: Gitea restart failed — ROOT_URL may still point to localhost" + +# Deploy SUT +log "Deploying SUT..." +if [ ! -f "/home/ec2-user/docker-compose.yml" ]; then + log "ERROR: SUT docker-compose.yml not found" + exit 1 +fi +if [ ! -x "/home/ec2-user/.docker/cli-plugins/docker-compose" ]; then + log "ERROR: Docker Compose plugin not executable" + exit 1 +fi + +# Get Azure OpenAI credentials from Secrets Manager +log "Retrieving Azure OpenAI credentials from Secrets Manager..." +AZURE_SECRET="" +AZURE_ENDPOINT="" +AZURE_API_KEY="" +AZURE_DEPLOYMENT="" +AZURE_API_VERSION="" + +# Primary secret path (new schema) +SECRET_NAME="azure/llm/configs" + +# Legacy fallback path (old schema) +ENVIRONMENT="${ENVIRONMENT:-dev}" +LEGACY_SECRET_NAME="classroom/shared/${ENVIRONMENT}/azure-openai" + +normalize_azure_endpoint() { + local raw_endpoint="$1" + + # Remove query string if present + raw_endpoint="${raw_endpoint%%\?*}" + + # Convert full Azure OpenAI operation URL to resource base endpoint + # Example: + # https://resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=... + # -> https://resource.openai.azure.com + if [[ "$raw_endpoint" == *"/openai/"* ]]; then + raw_endpoint="${raw_endpoint%%/openai/*}" + fi + + # Remove trailing slash for consistency + raw_endpoint="${raw_endpoint%/}" + + echo "$raw_endpoint" +} + +AZURE_SECRET=$(aws secretsmanager get-secret-value \ + --secret-id "$SECRET_NAME" \ + --region "${AWS_REGION}" \ + --query SecretString \ + --output text 2>/dev/null || echo "") + +if [ -z "$AZURE_SECRET" ] || [ "$AZURE_SECRET" = "None" ]; then + log "Primary secret not found, trying legacy Azure OpenAI secret..." + AZURE_SECRET=$(aws secretsmanager get-secret-value \ + --secret-id "$LEGACY_SECRET_NAME" \ + --region "${AWS_REGION}" \ + --query SecretString \ + --output text 2>/dev/null || echo "") + + if [ -n "$AZURE_SECRET" ] && [ "$AZURE_SECRET" != "None" ]; then + SECRET_NAME="$LEGACY_SECRET_NAME" + fi +fi + +if [ -z "$AZURE_SECRET" ] || [ "$AZURE_SECRET" = "None" ]; then + log "WARNING: Failed to retrieve Azure OpenAI secret from Secrets Manager" + log " Secret name: $SECRET_NAME" + log " Region: $AWS_REGION" + log " Fellowship SUT will work with fallback responses only (no Azure AI)" +else + # Parse secret JSON and extract values from either: + # - New schema: array under azure/llm/configs + # - Legacy schema: single object under classroom/shared//azure-openai + if command -v jq &> /dev/null; then + JQ_PICK_FILTER=' + def pick: + if type == "array" then + (map(select((.config_name // "" | ascii_downcase) | test("gpt[ -]?4o"))) | .[0]) + // (map(select((.endpoint // "" | ascii_downcase) | contains("/chat/completions"))) | .[0]) + // (map(select((.config_name // "" | ascii_downcase) | test("gpt"))) | .[0]) + // .[0] + else . end; + pick + ' + + AZURE_ENDPOINT=$(echo "$AZURE_SECRET" | jq -r "$JQ_PICK_FILTER | .endpoint // empty" 2>/dev/null || echo "") + AZURE_API_KEY=$(echo "$AZURE_SECRET" | jq -r "$JQ_PICK_FILTER | .api_key // empty" 2>/dev/null || echo "") + AZURE_DEPLOYMENT=$(echo "$AZURE_SECRET" | jq -r "$JQ_PICK_FILTER | .deployment_name // .deployment // empty" 2>/dev/null || echo "") + AZURE_API_VERSION=$(echo "$AZURE_SECRET" | jq -r "$JQ_PICK_FILTER | .api_version // empty" 2>/dev/null || echo "") + elif command -v python3 &> /dev/null; then + PARSED_AZURE=$(AZURE_SECRET="$AZURE_SECRET" python3 << 'PY' +import json +import os + +raw = os.environ.get("AZURE_SECRET", "") +try: + data = json.loads(raw) +except Exception: + data = {} + +selected = {} +if isinstance(data, list): + def config_name(item): + return str(item.get("config_name", "")).lower() + def endpoint(item): + return str(item.get("endpoint", "")).lower() + + selected = ( + next((x for x in data if "gpt-4o" in config_name(x) or "gpt 4-o" in config_name(x)), None) + or next((x for x in data if "/chat/completions" in endpoint(x)), None) + or next((x for x in data if "gpt" in config_name(x)), None) + or (data[0] if data else {}) + ) +elif isinstance(data, dict): + selected = data + +endpoint_val = selected.get("endpoint", "") +api_key_val = selected.get("api_key", "") +deployment_val = selected.get("deployment_name") or selected.get("deployment") or "" +api_version_val = selected.get("api_version", "") + +print(f"endpoint={endpoint_val}") +print(f"api_key={api_key_val}") +print(f"deployment={deployment_val}") +print(f"api_version={api_version_val}") +PY +) + + while IFS='=' read -r key value; do + case "$key" in + endpoint) AZURE_ENDPOINT="$value" ;; + api_key) AZURE_API_KEY="$value" ;; + deployment) AZURE_DEPLOYMENT="$value" ;; + api_version) AZURE_API_VERSION="$value" ;; + esac + done <<< "$PARSED_AZURE" + else + log "WARNING: Neither jq nor python3 available; using best-effort grep parsing" + AZURE_ENDPOINT=$(echo "$AZURE_SECRET" | grep -m1 -o '"endpoint"[[:space:]]*:[[:space:]]*"[^"]*"' | sed -E 's/^.*"endpoint"[[:space:]]*:[[:space:]]*"([^"]*)"$/\1/' || echo "") + AZURE_API_KEY=$(echo "$AZURE_SECRET" | grep -m1 -o '"api_key"[[:space:]]*:[[:space:]]*"[^"]*"' | sed -E 's/^.*"api_key"[[:space:]]*:[[:space:]]*"([^"]*)"$/\1/' || echo "") + AZURE_DEPLOYMENT=$(echo "$AZURE_SECRET" | grep -m1 -o '"deployment_name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed -E 's/^.*"deployment_name"[[:space:]]*:[[:space:]]*"([^"]*)"$/\1/' || echo "") + if [ -z "$AZURE_DEPLOYMENT" ]; then + AZURE_DEPLOYMENT=$(echo "$AZURE_SECRET" | grep -m1 -o '"deployment"[[:space:]]*:[[:space:]]*"[^"]*"' | sed -E 's/^.*"deployment"[[:space:]]*:[[:space:]]*"([^"]*)"$/\1/' || echo "") + fi + AZURE_API_VERSION=$(echo "$AZURE_SECRET" | grep -m1 -o '"api_version"[[:space:]]*:[[:space:]]*"[^"]*"' | sed -E 's/^.*"api_version"[[:space:]]*:[[:space:]]*"([^"]*)"$/\1/' || echo "") + fi + + # Ensure endpoint is in Azure resource base URL format expected by AzureOpenAI client + AZURE_ENDPOINT=$(normalize_azure_endpoint "$AZURE_ENDPOINT") + + if [ -n "$AZURE_ENDPOINT" ] && [ -n "$AZURE_API_KEY" ] && [ -n "$AZURE_DEPLOYMENT" ]; then + log "✓ Azure OpenAI credentials retrieved successfully" + log " Secret source: ${SECRET_NAME}" + log " Deployment: ${AZURE_DEPLOYMENT:-not set}" + log " API Version: ${AZURE_API_VERSION:-not set}" + log " Endpoint: ${AZURE_ENDPOINT}" + else + log "WARNING: Failed to parse Azure credentials from secret" + log " Secret source: ${SECRET_NAME}" + log " Endpoint: ${AZURE_ENDPOINT:-empty}" + log " API Key: ${AZURE_API_KEY:0:10}***" + log " Deployment: ${AZURE_DEPLOYMENT:-empty}" + fi +fi + +# Create .env file for docker-compose BEFORE deployment +# This ensures all environment variables are persistently available +log "Updating .env file with deployment-specific configuration..." + +# Function to update or add an env variable in .env file +update_env_var() { + local key="$1" + local value="$2" + local env_file="/home/ec2-user/.env" + + if grep -q "^${key}=" "$env_file" 2>/dev/null; then + # Variable exists, update it + sed -i "" "s|^${key}=.*|${key}=${value}|" "$env_file" + else + # Variable doesn't exist, append it + echo "${key}=${value}" >> "$env_file" + fi +} + +# Update domain configuration from resolved values +update_env_var "CADDY_DOMAIN" "${CADDY_DOMAIN:-localhost}" +update_env_var "JENKINS_DOMAIN" "${JENKINS_DOMAIN:-}" +update_env_var "IDE_DOMAIN" "${IDE_DOMAIN:-}" +update_env_var "GITEA_DOMAIN" "${GITEA_DOMAIN:-}" +update_env_var "CADDYFILE_PATH" "./caddy/Caddyfile" +update_env_var "FRONTEND_MODE" "prod" +update_env_var "FLASK_ENV" "production" +update_env_var "NODE_ENV" "production" + +# Add optional Azure OpenAI Configuration if available +if [ -n "$AZURE_ENDPOINT" ]; then + update_env_var "AZURE_OPENAI_ENDPOINT" "${AZURE_ENDPOINT}" + update_env_var "AZURE_OPENAI_API_KEY" "${AZURE_API_KEY}" + update_env_var "AZURE_OPENAI_DEPLOYMENT" "${AZURE_DEPLOYMENT}" + update_env_var "AZURE_OPENAI_API_VERSION" "${AZURE_API_VERSION}" +fi + +# Add GitHub Token if available +if [ -n "$GITHUB_TOKEN" ]; then + update_env_var "GITHUB_TOKEN" "${GITHUB_TOKEN}" +fi + +chown ec2-user:ec2-user /home/ec2-user/.env +chmod 644 /home/ec2-user/.env + +log "✓ Updated /home/ec2-user/.env with deployment configuration" +if [ -n "$AZURE_ENDPOINT" ]; then + log " ✓ Azure OpenAI configured (deployment: ${AZURE_DEPLOYMENT})" +else + log " ⚠ Azure OpenAI not configured - using fallback responses" +fi + +# Verify .env file contents (mask sensitive values) +log "Verifying .env file contents:" +grep -E "^CADDY_DOMAIN=|^AZURE_OPENAI" /home/ec2-user/.env 2>/dev/null | sed 's/AZURE_OPENAI_API_KEY=.*/AZURE_OPENAI_API_KEY=***MASKED***/g' | sed 's/^/ /' || true + +# Additional safety check: ensure CADDY_DOMAIN is not empty +if [ -z "$CADDY_DOMAIN" ]; then + log "ERROR: CADDY_DOMAIN is empty - docker-compose will not start properly" + exit 1 +fi + +# Verify network connectivity BEFORE docker-compose build (critical for github access) +log "Performing pre-deployment network connectivity checks..." +if ! verify_network_connectivity; then + log "ERROR: Network connectivity checks failed" + log " Cannot proceed with docker-compose build (needs github.com access)" + exit 1 +fi +log "OK: Network connectivity verified" + +# Retrieve GitHub token from AWS Secrets Manager for private repo access during docker builds +log "Retrieving GitHub credentials from AWS Secrets Manager..." +GITHUB_TOKEN="" +GITHUB_SECRET="" + +# Try to get GitHub token from Secrets Manager +GITHUB_SECRET=$(aws secretsmanager get-secret-value \ + --secret-id "classroom/shared/github-token" \ + --region "${AWS_REGION}" \ + --query SecretString \ + --output text 2>/dev/null || echo "") + +if [ -n "$GITHUB_SECRET" ] && [ "$GITHUB_SECRET" != "None" ]; then + # Try to extract token (could be plain string or JSON) + if echo "$GITHUB_SECRET" | grep -q '{'; then + # JSON format + if command -v jq &> /dev/null; then + GITHUB_TOKEN=$(echo "$GITHUB_SECRET" | jq -r '.token // .github_token // .pat // empty' 2>/dev/null || echo "") + else + GITHUB_TOKEN=$(echo "$GITHUB_SECRET" | grep -o '"token":"[^"]*' | cut -d'"' -f4 || echo "") + fi + else + # Plain token string + GITHUB_TOKEN="$GITHUB_SECRET" + fi +fi + +if [ -n "$GITHUB_TOKEN" ] && [ "$GITHUB_TOKEN" != "None" ]; then + log "OK: GitHub credentials found - configuring git" + # Configure git to use token for HTTPS cloning + # This enables docker builds that clone from private GitHub repositories + git config --global credential.helper store + echo "https://${GITHUB_TOKEN}@github.com" > /home/ec2-user/.git-credentials + chmod 600 /home/ec2-user/.git-credentials + export GIT_ASKPASS=/bin/true + export GITHUB_TOKEN +else + log "WARNING: GitHub token not found in Secrets Manager (public repos only)" +fi + +# Deploy SUT containers using docker-compose with retry logic on network failures +# Note: Pass environment variables both via .env file AND explicit exports for maximum compatibility +log "Starting SUT containers (with automatic retry on network issues)..." +log " CADDY_DOMAIN: ${CADDY_DOMAIN}" + +# ── Select the fellowship Caddyfile for the tutorial stack ─────────────────── +# Tutorial instances serve three HTTPS sites via the single Caddy container: +# • CADDY_DOMAIN → SUT (reverse_proxy to backend:5000 / frontend:3000) +# • JENKINS_DOMAIN → Jenkins CI (reverse_proxy to host.docker.internal:8080) +# • IDE_DOMAIN → code-server (reverse_proxy to host.docker.internal:8443) +# Caddyfile.fellowship contains all three site blocks. +# Caddyfile (staging) and Caddyfile.prod only contain the SUT block and must +# NOT be used here — they would cause Caddy to fail if JENKINS_DOMAIN / IDE_DOMAIN +# are empty, and would not expose the DevOps Escape Room tools over HTTPS at all. +FELLOWSHIP_CADDYFILE="/home/ec2-user/caddy/Caddyfile.fellowship" +ACTIVE_CADDYFILE="/home/ec2-user/caddy/Caddyfile" +if [ -f "$FELLOWSHIP_CADDYFILE" ]; then + cp "$FELLOWSHIP_CADDYFILE" "$ACTIVE_CADDYFILE" + chown ec2-user:ec2-user "$ACTIVE_CADDYFILE" + log "✓ Copied Caddyfile.fellowship → caddy/Caddyfile (SUT + Jenkins + IDE HTTPS)" +else + log "WARNING: Caddyfile.fellowship not found at ${FELLOWSHIP_CADDYFILE}" + log " Jenkins and IDE will NOT be served via HTTPS." + log " Ensure caddy/Caddyfile.fellowship is present in the SUT tarball (see caddy/ directory)." +fi + +# Function to deploy SUT with retry logic for network failures +deploy_sut_with_retry() { + local max_attempts=3 + local attempt=1 + local wait_time=10 + + while [ $attempt -le $max_attempts ]; do + log " Deployment attempt $attempt/$max_attempts..." + + # Use cd to set working directory, then docker compose will auto-load .env + DEPLOY_OUTPUT=$(run_as_ec2user_docker "cd ~ && docker compose up -d 2>&1" 2>&1) + DEPLOY_EXIT_CODE=$? + + if [ $DEPLOY_EXIT_CODE -eq 0 ]; then + log "OK: Docker Compose started successfully" + return 0 + fi + + # Check if error is network-related (github, DNS, connectivity, etc) + if echo "$DEPLOY_OUTPUT" | grep -iE "network|dns|resolve|github|credential|authentication|connection refused|timeout|no such device|temporary failure" >/dev/null 2>&1; then + log " WARNING: Network-related error detected, will retry..." + log " Error: $(echo \"$DEPLOY_OUTPUT\" | head -2 | tail -1)" + + if [ $attempt -lt $max_attempts ]; then + log " Waiting ${wait_time}s before retry (attempt $((attempt + 1))/$max_attempts)..." + sleep $wait_time + wait_time=$((wait_time * 2)) # Exponential backoff: 10s, 20s, 40s + attempt=$((attempt + 1)) + continue + fi + else + # Non-network error, fail immediately + log "ERROR: Failed to start SUT containers (non-recoverable error)" + log "Docker Compose output:" + echo "$DEPLOY_OUTPUT" | sed 's/^/ /' + return 1 + fi + + attempt=$((attempt + 1)) + done + + # All retries exhausted + log "ERROR: Failed to start SUT containers after $max_attempts attempts" + log "Docker Compose output:" + echo "$DEPLOY_OUTPUT" | sed 's/^/ /' + log "Checking Docker logs for more information..." + run_as_ec2user_docker "cd ~ && docker compose logs" 2>&1 | tail -50 | sed 's/^/ /' + return 1 +} + +# Execute deployment with retry +if ! deploy_sut_with_retry; then + exit 1 +fi + +log "Waiting for containers to be in running state..." + +# Wait for containers to be running (up to 60 seconds) +CONTAINER_WAIT_COUNT=0 +while [ $CONTAINER_WAIT_COUNT -lt 12 ]; do + RUNNING_CONTAINERS=$(run_as_ec2user_docker "cd ~ && docker compose ps -q --status running 2>/dev/null | wc -l" 2>/dev/null || echo "0") + EXPECTED_CONTAINERS=3 + + if [ "$RUNNING_CONTAINERS" -ge "$EXPECTED_CONTAINERS" ]; then + log "✓ All required containers running ($RUNNING_CONTAINERS/$EXPECTED_CONTAINERS)" + break + fi + + log " Waiting for containers... ($RUNNING_CONTAINERS/$EXPECTED_CONTAINERS running, attempt $((CONTAINER_WAIT_COUNT + 1))/12)" + sleep 5 + CONTAINER_WAIT_COUNT=$((CONTAINER_WAIT_COUNT + 1)) +done + +# Wait for backend health check to pass (up to 20 attempts * 3 seconds = 60 seconds) +log "Waiting for backend service to be healthy..." +BACKEND_HEALTH_COUNT=0 +BACKEND_READY=false +while [ $BACKEND_HEALTH_COUNT -lt 20 ]; do + BACKEND_STATUS=$(run_as_ec2user_docker "cd ~ && docker compose ps backend --format json 2>/dev/null" | grep -o '"State":"running"' || echo "") + if [ -n "$BACKEND_STATUS" ]; then + log "✓ Backend container is running" + BACKEND_READY=true + break + fi + + log " Waiting for backend to be ready... (attempt $((BACKEND_HEALTH_COUNT + 1))/20)" + sleep 3 + BACKEND_HEALTH_COUNT=$((BACKEND_HEALTH_COUNT + 1)) +done + +# Wait for frontend to compile and start (React dev server, up to 60 seconds) +log "Waiting for frontend to compile and start..." +FRONTEND_WAIT_COUNT=0 +FRONTEND_READY=false +while [ $FRONTEND_WAIT_COUNT -lt 20 ]; do + FRONTEND_LOGS=$(run_as_ec2user_docker "cd ~ && docker compose logs frontend 2>&1" | grep -iE "compiled successfully|webpack compiled|app is running on" || echo "") + if [ -n "$FRONTEND_LOGS" ]; then + log "✓ Frontend is ready" + FRONTEND_READY=true + break + fi + + log " Waiting for frontend compilation... (attempt $((FRONTEND_WAIT_COUNT + 1))/20)" + sleep 3 + FRONTEND_WAIT_COUNT=$((FRONTEND_WAIT_COUNT + 1)) +done + +if [ "$FRONTEND_READY" != "true" ]; then + log "ERROR: Frontend did not become ready within timeout" + dump_runtime_diagnostics + exit 1 +fi + +log "Running post-deploy health gates..." +HTTP_OK=false +for i in {1..20}; do + if curl -sSf --max-time 5 "http://localhost/" >/dev/null 2>&1; then + HTTP_OK=true + log "✓ Local HTTP health gate passed" + break + fi + log " Waiting for local HTTP health gate... (attempt $i/20)" + sleep 3 +done + +HTTPS_OK=false +for i in {1..30}; do + if curl -k -sSf --max-time 6 "https://localhost/" >/dev/null 2>&1; then + HTTPS_OK=true + log "✓ Local HTTPS health gate passed" + break + fi + log " Waiting for local HTTPS health gate... (attempt $i/30)" + sleep 3 +done + +if [ "$HTTP_OK" != "true" ] || [ "$HTTPS_OK" != "true" ]; then + log "ERROR: Post-deploy health gates failed (HTTP_OK=${HTTP_OK}, HTTPS_OK=${HTTPS_OK})" + dump_runtime_diagnostics + exit 1 +fi + +# Verify environment variables made it to Caddy container +log "Verifying environment variables in Caddy container..." +CADDY_ENV=$(run_as_ec2user_docker "cd ~ && docker inspect fellowship-caddy --format='{{.Config.Env}}' 2>/dev/null | grep -o 'CADDY_DOMAIN=[^[:space:]]*' || echo 'NOT FOUND'" 2>/dev/null) +if [ -n "$CADDY_ENV" ] && [ "$CADDY_ENV" != "NOT FOUND" ]; then + log "✓ CADDY_DOMAIN verified in container: $CADDY_ENV" +else + log "WARNING: CADDY_DOMAIN not found in container environment" + log " This may cause connection issues" + log " Container environment (first 20 vars):" + run_as_ec2user_docker "cd ~ && docker exec fellowship-caddy env 2>/dev/null | head -20" 2>/dev/null | sed 's/^/ /' || true +fi + +# Final container status check +log "Final container status:" +run_as_ec2user_docker "cd ~ && docker compose ps" 2>&1 | sed 's/^/ /' + +# Final status +PUBLIC_IP=$(get_instance_metadata "public-ipv4") +[ -z "$PUBLIC_IP" ] && PUBLIC_IP="N/A" +log "==========================================" +log "Setup Complete" +log "==========================================" +log "Public IP: $PUBLIC_IP" +log "" +log "─── Fellowship SUT ────────────────────────" +log " HTTPS: https://${CADDY_DOMAIN}/" +log "" +log "─── DevOps Escape Room ────────────────────" +log " Jenkins CI (HTTPS): https://${JENKINS_DOMAIN}/" +log " Jenkins CI (direct): http://${PUBLIC_IP}:8080 (fellowship / fellowship123)" +log " IDE / code-server (HTTPS): https://${IDE_DOMAIN}/" +log " IDE / code-server (direct): http://${PUBLIC_IP}:8443 (password: fellowship)" +log " Gitea Git: http://${PUBLIC_IP}:3030 (fellowship / fellowship123)" +log " MailHog UI: http://${PUBLIC_IP}:8025" +log "==========================================" diff --git a/sut/backend/.env.example b/sut/backend/.env.example new file mode 100644 index 0000000..3fa56c3 --- /dev/null +++ b/sut/backend/.env.example @@ -0,0 +1,14 @@ +# Azure OpenAI Configuration +# Get these values from your Azure OpenAI resource dashboard +AZURE_OPENAI_ENDPOINT=https://.openai.azure.com/ +AZURE_OPENAI_API_KEY=your_api_key_here +AZURE_OPENAI_DEPLOYMENT=gpt-4o +AZURE_OPENAI_API_VERSION=2024-11-20 +AZURE_OPENAI_MAX_TOKENS=500 +AZURE_OPENAI_TEMPERATURE=0.85 + +# Flask Configuration +SECRET_KEY=dev-secret-key-change-in-production + +# Database Configuration (optional—defaults to SQLite) +# DATABASE_URL=sqlite:////app/data/fellowship.db diff --git a/sut/backend/Dockerfile b/sut/backend/Dockerfile new file mode 100644 index 0000000..91fb8d0 --- /dev/null +++ b/sut/backend/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create data directory for SQLite +RUN mkdir -p /app/data + +# Expose port +EXPOSE 5000 + +# Run the application +CMD ["python", "app.py"] diff --git a/sut/backend/__pycache__/config.cpython-311.pyc b/sut/backend/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000..bbee614 Binary files /dev/null and b/sut/backend/__pycache__/config.cpython-311.pyc differ diff --git a/sut/backend/app.py b/sut/backend/app.py new file mode 100644 index 0000000..a71e703 --- /dev/null +++ b/sut/backend/app.py @@ -0,0 +1,229 @@ +"""Main Flask application for the Fellowship Quest Tracker.""" +from dotenv import load_dotenv +import os + +# Load environment variables from .env file (if present) +# This must happen before any config is loaded +load_dotenv() + +from flask import Flask, jsonify, request, session +from flask_cors import CORS +from flask_restx import Api +from config import config +from models.user import db +from utils.database import init_db +from utils.seed_data import seed_database +from routes.auth import auth_bp, auth_api +from routes.quests import quests_bp, quests_api +from routes.members import members_bp, members_api +from routes.locations import locations_bp, locations_api +from routes.npc_chat import npc_chat_bp, npc_chat_api +from routes.shop import shop_bp, shop_api +from services.shop_service import ShopService +from datetime import datetime, timezone, timedelta +from typing import Optional + + +APP_STARTED_AT_UTC = datetime.now(timezone.utc) + + +def _read_uptime_seconds() -> Optional[float]: + try: + with open('/proc/uptime', 'r', encoding='utf-8') as uptime_file: + first_field = uptime_file.read().split()[0] + return float(first_field) + except (OSError, ValueError, IndexError): + return None + + +def _instance_boot_time_utc() -> Optional[datetime]: + uptime_seconds = _read_uptime_seconds() + if uptime_seconds is None: + return None + return datetime.now(timezone.utc) - timedelta(seconds=uptime_seconds) + +def create_app(config_name: str = None) -> Flask: + """Create and configure Flask application.""" + app = Flask(__name__) + + # Load configuration + config_name = config_name or os.environ.get('FLASK_ENV', 'development') + app.config.from_object(config[config_name]) + + # Configure session + app.config['SESSION_COOKIE_HTTPONLY'] = True + app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Allows cross-site cookies for development + app.config['SESSION_COOKIE_SECURE'] = False # Set to True in production with HTTPS + + # Initialize CORS with specific origins (required when using credentials) + # Allow both localhost:3000 (dev) and localhost (production via nginx) + # Flask-CORS handles preflight OPTIONS requests automatically + CORS( + app, + supports_credentials=True, + resources={ + r"/api/*": { + "origins": [ + "http://localhost:3000", + "http://localhost", + "http://127.0.0.1:3000", + "http://127.0.0.1", + ] + } + }, + allow_headers=["Content-Type", "Authorization"], + methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], + ) + + # Initialize database (this also initializes db) + init_db(app) + + # Seed database with initial data + # Skip seeding in test mode + # Skip seeding in test mode + if not app.config.get("TESTING") and not os.environ.get("TESTING"): + seed_database(app) + # Create main API with Swagger documentation + api = Api( + app, + version='1.0', + title='The Fellowship\'s Quest List API', + description='REST API for tracking the Fellowship\'s epic journey through Middle-earth', + doc='/api/swagger/', + prefix='/api' + ) + + # Register blueprints (this registers both the blueprints and their Flask-RESTX routes) + # Flask-RESTX Api objects bound to blueprints automatically register routes when blueprint is registered + app.register_blueprint(auth_bp) + app.register_blueprint(quests_bp) + app.register_blueprint(members_bp) + app.register_blueprint(locations_bp) + app.register_blueprint(npc_chat_bp) + app.register_blueprint(shop_bp) + + # Note: We don't add the Api objects as namespaces because they're already bound to blueprints + # Adding them as namespaces would cause route conflicts. The routes work from blueprints alone. + # For Swagger, each Api has its own documentation, but we can add them to the main API if needed. + # However, this requires creating Namespace objects, not using the Api objects directly. + + # Health check endpoint + @app.route('/api/health') + def health(): + """Health check endpoint.""" + return jsonify({'status': 'healthy', 'service': 'fellowship-quest-tracker'}), 200 + + # Test cleanup endpoint (development only - deletes accumulated test data) + @app.route('/api/test/cleanup', methods=['POST']) + def test_cleanup(): + """Delete quests created during e2e test runs to prevent database bloat.""" + from models.quest import Quest as QuestModel + # Only allowed in non-production environments + if app.config.get('ENV') == 'production': + return jsonify({'error': 'Not available in production'}), 403 + test_patterns = [ + 'BDD', 'Test Quest', 'Find the One Ring', 'Explore Rivendell', + 'Defeat Sauron', 'Completed Quest', 'In Progress Quest', + 'Journey Quest', 'Battle Quest', 'Ring Quest', + 'Mordor Quest', 'Rivendell Quest', 'Dark Magic Quest', + 'Mini-game Quest', + ] + deleted = 0 + try: + for pattern in test_patterns: + quests = QuestModel.query.filter(QuestModel.title.contains(pattern)).all() + for q in quests: + db.session.delete(q) + deleted += 1 + db.session.commit() + return jsonify({'deleted': deleted, 'status': 'ok'}), 200 + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + @app.route('/api/test/set_gold', methods=['POST']) + def test_set_gold(): + """Set the current user's gold balance (for e2e testing only).""" + if app.config.get('ENV') == 'production': + return jsonify({'error': 'Not available in production'}), 403 + from models.user import User as UserModel + user_id = session.get('user_id') + if not user_id: + return jsonify({'error': 'Not authenticated'}), 401 + data = request.get_json() or {} + gold = data.get('gold') + if gold is None or not isinstance(gold, int) or gold < 0: + return jsonify({'error': 'gold must be a non-negative integer'}), 400 + try: + user = UserModel.query.get(user_id) + if not user: + return jsonify({'error': 'User not found'}), 404 + user.gold = gold + db.session.commit() + return jsonify({'gold': user.gold, 'status': 'ok'}), 200 + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + @app.route('/api/test/reset_shop', methods=['POST']) + def test_reset_shop(): + """Reset all items to not-sold and clear inventory for e2e testing.""" + if app.config.get('ENV') == 'production': + return jsonify({'error': 'Not available in production'}), 403 + try: + result = ShopService.reset_for_tests() + return jsonify(result), 200 + except Exception as e: + return jsonify({'error': str(e)}), 500 + + @app.route('/api/status') + def status(): + """Runtime status endpoint exposed for public uptime/restart information.""" + instance_boot_time = _instance_boot_time_utc() + payload = { + 'status': 'ok', + 'service': 'fellowship-quest-tracker', + 'app_started_at_utc': APP_STARTED_AT_UTC.isoformat(), + 'instance_boot_time_utc': instance_boot_time.isoformat() if instance_boot_time else None, + 'now_utc': datetime.now(timezone.utc).isoformat(), + } + return jsonify(payload), 200 + + # API info endpoint (instead of root, since nginx handles root routing) + @app.route('/api') + def api_info(): + """API information endpoint.""" + return jsonify({ + 'message': 'Welcome to The Fellowship\'s Quest List API', + 'version': '1.0', + 'docs': '/api/swagger/', + 'health': '/api/health', + 'status': '/api/status' + }), 200 + + # Debug endpoint to list all registered routes (development only) + if app.config.get('DEBUG'): + @app.route('/api/routes') + def list_routes(): + """List all registered routes (debug endpoint).""" + routes = [] + for rule in app.url_map.iter_rules(): + if rule.rule.startswith('/api'): + routes.append({ + 'endpoint': rule.endpoint, + 'methods': list(rule.methods - {'HEAD', 'OPTIONS'}), + 'path': str(rule) + }) + return jsonify({'routes': sorted(routes, key=lambda x: x['path'])}), 200 + + return app + +if __name__ == '__main__': + try: + app = create_app() + app.run(host='0.0.0.0', port=5000, debug=True) + except Exception as e: + import traceback + print(f"Error starting application: {e}") + traceback.print_exc() + raise \ No newline at end of file diff --git a/sut/backend/config.py b/sut/backend/config.py new file mode 100644 index 0000000..6a77bba --- /dev/null +++ b/sut/backend/config.py @@ -0,0 +1,57 @@ +"""Configuration settings for the Fellowship Quest Tracker application.""" +import os +from pathlib import Path + +class Config: + """Base configuration.""" + SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production') + + # SQLite database configuration + BASE_DIR = Path(__file__).parent.parent.parent + DATA_DIR = Path('/app/data') + # Ensure data directory exists + try: + DATA_DIR.mkdir(parents=True, exist_ok=True) + except Exception as e: + print(f"Warning: Could not create data directory: {e}") + DATABASE_PATH = DATA_DIR / 'fellowship.db' + # Use environment variable if set, otherwise use default path + db_url = os.environ.get('DATABASE_URL') + if db_url: + SQLALCHEMY_DATABASE_URI = db_url + else: + # Use 4 slashes for absolute path: sqlite:////absolute/path + SQLALCHEMY_DATABASE_URI = f'sqlite:///{DATABASE_PATH}' + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # API Configuration + RESTX_MASK_SWAGGER = False + RESTX_VALIDATE = True + RESTX_ERROR_404_HELP = False + + # Azure OpenAI configuration (server-side only) + # Load from environment variables—supply via .env file or container env vars + # DO NOT hardcode API keys or other sensitive values + AZURE_OPENAI_ENDPOINT = os.environ.get('AZURE_OPENAI_ENDPOINT', '').strip() + AZURE_OPENAI_API_KEY = os.environ.get('AZURE_OPENAI_API_KEY', '').strip() + AZURE_OPENAI_DEPLOYMENT = os.environ.get('AZURE_OPENAI_DEPLOYMENT', '').strip() + AZURE_OPENAI_API_VERSION = os.environ.get('AZURE_OPENAI_API_VERSION', '2024-11-20').strip() + AZURE_OPENAI_MAX_TOKENS = int(os.environ.get('AZURE_OPENAI_MAX_TOKENS', '500')) + AZURE_OPENAI_TEMPERATURE = float(os.environ.get('AZURE_OPENAI_TEMPERATURE', '0.85')) + +class DevelopmentConfig(Config): + """Development configuration.""" + DEBUG = True + FLASK_ENV = 'development' + +class ProductionConfig(Config): + """Production configuration.""" + DEBUG = False + FLASK_ENV = 'production' + +# Configuration mapping +config = { + 'development': DevelopmentConfig, + 'production': ProductionConfig, + 'default': DevelopmentConfig +} diff --git a/sut/backend/debug_azure.py b/sut/backend/debug_azure.py new file mode 100644 index 0000000..46e7bbe --- /dev/null +++ b/sut/backend/debug_azure.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Debug script to test Azure OpenAI connection and NPC response generation.""" +import sys +sys.path.insert(0, '/app') + +from flask import Flask +from config import Config + +# Quick config check +config = Config() +print("\n" + "="*70) +print("AZURE OPENAI CONFIG CHECK") +print("="*70) +print(f"Endpoint: {config.AZURE_OPENAI_ENDPOINT}") +print(f"API Key Present: {bool(config.AZURE_OPENAI_API_KEY)}") +print(f"API Key Length: {len(config.AZURE_OPENAI_API_KEY) if config.AZURE_OPENAI_API_KEY else 0}") +print(f"Deployment: {config.AZURE_OPENAI_DEPLOYMENT}") +print(f"API Version: {config.AZURE_OPENAI_API_VERSION}") + +# Try to create client +try: + from openai import AzureOpenAI + print("\n" + "="*70) + print("CREATING AZURE OPENAI CLIENT") + print("="*70) + client = AzureOpenAI( + azure_endpoint=config.AZURE_OPENAI_ENDPOINT, + api_key=config.AZURE_OPENAI_API_KEY, + api_version=config.AZURE_OPENAI_API_VERSION, + ) + print("✅ Client created successfully") + + # Try a simple API call + print("\n" + "="*70) + print("TESTING SIMPLE CHAT COMPLETION") + print("="*70) + response = client.chat.completions.create( + model=config.AZURE_OPENAI_DEPLOYMENT, + messages=[ + {"role": "system", "content": "You are Frodo Baggins. Respond briefly in one sentence."}, + {"role": "user", "content": "Do you like sports?"} + ], + temperature=0.7, + max_tokens=100, + ) + print(f"✅ API Call Successful!") + print(f"\nFrodo's Response: {response.choices[0].message.content}") + +except Exception as e: + print(f"\n✗ ERROR: {type(e).__name__}") + print(f"Details: {str(e)}") + import traceback + traceback.print_exc() + +print("\n" + "="*70) diff --git a/sut/backend/models/__init__.py b/sut/backend/models/__init__.py new file mode 100644 index 0000000..80c7145 --- /dev/null +++ b/sut/backend/models/__init__.py @@ -0,0 +1,9 @@ +"""Database models for the Fellowship Quest Tracker.""" +from .user import User +from .quest import Quest +from .member import Member +from .location import Location +from .item import Item +from .inventory_item import InventoryItem + +__all__ = ['User', 'Quest', 'Member', 'Location', 'Item', 'InventoryItem'] diff --git a/sut/backend/models/__pycache__/__init__.cpython-311.pyc b/sut/backend/models/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..f4072d7 Binary files /dev/null and b/sut/backend/models/__pycache__/__init__.cpython-311.pyc differ diff --git a/sut/backend/models/__pycache__/inventory_item.cpython-311.pyc b/sut/backend/models/__pycache__/inventory_item.cpython-311.pyc new file mode 100644 index 0000000..8054f7e Binary files /dev/null and b/sut/backend/models/__pycache__/inventory_item.cpython-311.pyc differ diff --git a/sut/backend/models/__pycache__/item.cpython-311.pyc b/sut/backend/models/__pycache__/item.cpython-311.pyc new file mode 100644 index 0000000..fbccb05 Binary files /dev/null and b/sut/backend/models/__pycache__/item.cpython-311.pyc differ diff --git a/sut/backend/models/__pycache__/location.cpython-311.pyc b/sut/backend/models/__pycache__/location.cpython-311.pyc new file mode 100644 index 0000000..15506d1 Binary files /dev/null and b/sut/backend/models/__pycache__/location.cpython-311.pyc differ diff --git a/sut/backend/models/__pycache__/member.cpython-311.pyc b/sut/backend/models/__pycache__/member.cpython-311.pyc new file mode 100644 index 0000000..5e52ab9 Binary files /dev/null and b/sut/backend/models/__pycache__/member.cpython-311.pyc differ diff --git a/sut/backend/models/__pycache__/quest.cpython-311.pyc b/sut/backend/models/__pycache__/quest.cpython-311.pyc new file mode 100644 index 0000000..a24d4af Binary files /dev/null and b/sut/backend/models/__pycache__/quest.cpython-311.pyc differ diff --git a/sut/backend/models/__pycache__/user.cpython-311.pyc b/sut/backend/models/__pycache__/user.cpython-311.pyc new file mode 100644 index 0000000..c97f873 Binary files /dev/null and b/sut/backend/models/__pycache__/user.cpython-311.pyc differ diff --git a/sut/backend/models/inventory_item.py b/sut/backend/models/inventory_item.py new file mode 100644 index 0000000..3b59619 --- /dev/null +++ b/sut/backend/models/inventory_item.py @@ -0,0 +1,40 @@ +"""Purchased inventory item model.""" +from models.user import db +from typing import Dict, Any + + +class InventoryItem(db.Model): + """User-owned purchased item entry.""" + + __tablename__ = 'inventory_items' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True) + item_id = db.Column(db.Integer, db.ForeignKey('items.id'), nullable=False, unique=True) + paid_price = db.Column(db.Integer, nullable=False) + base_price_revealed = db.Column(db.Integer, nullable=False) + savings_percent = db.Column(db.Float, nullable=False) + acquired_price = db.Column(db.Integer, nullable=False, default=0) # Legacy field, set to paid_price + created_at = db.Column(db.DateTime, default=db.func.current_timestamp()) + + user = db.relationship('User', foreign_keys=[user_id], backref='inventory_items') + item = db.relationship('Item', foreign_keys=[item_id], backref='inventory_entry') + + def to_dict(self) -> Dict[str, Any]: + """Serialize full inventory item details.""" + return { + 'id': self.id, + 'user_id': self.user_id, + 'item_id': self.item_id, + 'item_name': self.item.name if self.item else None, + 'owner_character': self.item.owner_character if self.item else None, + 'description': self.item.description if self.item else None, + 'paid_price': self.paid_price, + 'base_price_revealed': self.base_price_revealed, + 'savings_percent': self.savings_percent, + 'acquired_price': self.acquired_price, + 'created_at': self.created_at.isoformat() if self.created_at else None, + } + + def __repr__(self) -> str: + return f'' diff --git a/sut/backend/models/item.py b/sut/backend/models/item.py new file mode 100644 index 0000000..fe82da2 --- /dev/null +++ b/sut/backend/models/item.py @@ -0,0 +1,41 @@ +"""Market item model for NPC bargaining.""" +from models.user import db +from typing import Dict, Any + + +class Item(db.Model): + """Unique sellable item owned by an NPC character.""" + + __tablename__ = 'items' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(200), nullable=False) + description = db.Column(db.Text, nullable=True) + owner_character = db.Column(db.String(80), nullable=False, index=True) + personality_profile = db.Column(db.String(40), nullable=False, default='bargainer') + base_price = db.Column(db.Integer, nullable=False) + asking_price = db.Column(db.Integer, nullable=False) + is_sold = db.Column(db.Boolean, nullable=False, default=False) + created_at = db.Column(db.DateTime, default=db.func.current_timestamp()) + updated_at = db.Column( + db.DateTime, + default=db.func.current_timestamp(), + onupdate=db.func.current_timestamp(), + ) + + def to_public_dict(self) -> Dict[str, Any]: + """Serialize without revealing hidden base price.""" + return { + 'id': self.id, + 'name': self.name, + 'description': self.description, + 'owner_character': self.owner_character, + 'personality_profile': self.personality_profile, + 'asking_price': self.asking_price, + 'is_sold': self.is_sold, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'updated_at': self.updated_at.isoformat() if self.updated_at else None, + } + + def __repr__(self) -> str: + return f'' diff --git a/sut/backend/models/location.py b/sut/backend/models/location.py new file mode 100644 index 0000000..70cbf8c --- /dev/null +++ b/sut/backend/models/location.py @@ -0,0 +1,30 @@ +"""Location model for Middle-earth locations.""" +from models.user import db +from typing import Dict, Any + +class Location(db.Model): + """Location model for Middle-earth locations.""" + __tablename__ = 'locations' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False, unique=True) + description = db.Column(db.Text, nullable=True) + region = db.Column(db.String(100), nullable=False) # Eriador, Rhovanion, Mordor, etc. + map_x = db.Column(db.Float, nullable=True) # X coordinate on map (pixel, 0-5000, horizontal) + map_y = db.Column(db.Float, nullable=True) # Y coordinate on map (pixel, 0-4344, vertical) + created_at = db.Column(db.DateTime, default=db.func.current_timestamp()) + + def to_dict(self) -> Dict[str, Any]: + """Convert location to dictionary.""" + return { + 'id': self.id, + 'name': self.name, + 'description': self.description, + 'region': self.region, + 'map_x': self.map_x, + 'map_y': self.map_y, + 'created_at': self.created_at.isoformat() if self.created_at else None + } + + def __repr__(self) -> str: + return f'' diff --git a/sut/backend/models/member.py b/sut/backend/models/member.py new file mode 100644 index 0000000..092c9df --- /dev/null +++ b/sut/backend/models/member.py @@ -0,0 +1,30 @@ +"""Fellowship member model.""" +from models.user import db +from typing import Dict, Any + +class Member(db.Model): + """Fellowship member model.""" + __tablename__ = 'members' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False, unique=True) + race = db.Column(db.String(50), nullable=False) # Hobbit, Human, Elf, Dwarf, Wizard + role = db.Column(db.String(100), nullable=False) # Ring-bearer, Companion, Ranger, etc. + status = db.Column(db.String(20), nullable=False, default='active') # active, inactive + description = db.Column(db.Text, nullable=True) + created_at = db.Column(db.DateTime, default=db.func.current_timestamp()) + + def to_dict(self) -> Dict[str, Any]: + """Convert member to dictionary.""" + return { + 'id': self.id, + 'name': self.name, + 'race': self.race, + 'role': self.role, + 'status': self.status, + 'description': self.description, + 'created_at': self.created_at.isoformat() if self.created_at else None + } + + def __repr__(self) -> str: + return f'' diff --git a/sut/backend/models/quest.py b/sut/backend/models/quest.py new file mode 100644 index 0000000..df2b477 --- /dev/null +++ b/sut/backend/models/quest.py @@ -0,0 +1,58 @@ +"""Quest model for tracking Fellowship quests.""" +from models.user import db +from datetime import datetime +from typing import Dict, Any, Optional + +class Quest(db.Model): + """Quest model for tracking Fellowship quests.""" + __tablename__ = 'quests' + + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(200), nullable=False) + description = db.Column(db.Text, nullable=True) + status = db.Column(db.String(50), nullable=False, default='not_yet_begun') # not_yet_begun, the_road_goes_ever_on, it_is_done, the_shadow_falls + quest_type = db.Column(db.String(50), nullable=True) # The Journey, The Battle, The Fellowship, The Ring, Dark Magic + priority = db.Column(db.String(20), nullable=True) # Critical, Important, Standard + is_dark_magic = db.Column(db.Boolean, default=False, nullable=False) + assigned_to = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) + location_id = db.Column(db.Integer, db.ForeignKey('locations.id'), nullable=True) + character_quote = db.Column(db.Text, nullable=True) + created_at = db.Column(db.DateTime, default=db.func.current_timestamp()) + updated_at = db.Column(db.DateTime, default=db.func.current_timestamp(), onupdate=db.func.current_timestamp()) + completed_at = db.Column(db.DateTime, nullable=True) + + # Relationships + assignee = db.relationship('User', foreign_keys=[assigned_to], backref='quests') + location = db.relationship('Location', foreign_keys=[location_id], backref='quests') + + def to_dict(self) -> Dict[str, Any]: + """Convert quest to dictionary.""" + # Map old status values to new LOTR terminology for backward compatibility + status_mapping = { + 'pending': 'not_yet_begun', + 'in_progress': 'the_road_goes_ever_on', + 'completed': 'it_is_done', + 'blocked': 'the_shadow_falls' + } + mapped_status = status_mapping.get(self.status, self.status) + + return { + 'id': self.id, + 'title': self.title, + 'description': self.description, + 'status': mapped_status, + 'quest_type': self.quest_type, + 'priority': self.priority, + 'is_dark_magic': self.is_dark_magic, + 'assigned_to': self.assigned_to, + 'location_id': self.location_id, + 'location_name': self.location.name if self.location else None, + 'assignee_name': self.assignee.username if self.assignee else None, + 'character_quote': self.character_quote, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'updated_at': self.updated_at.isoformat() if self.updated_at else None, + 'completed_at': self.completed_at.isoformat() if self.completed_at else None + } + + def __repr__(self) -> str: + return f'' diff --git a/sut/backend/models/user.py b/sut/backend/models/user.py new file mode 100644 index 0000000..4e31657 --- /dev/null +++ b/sut/backend/models/user.py @@ -0,0 +1,41 @@ +"""User model for authentication.""" +from flask_sqlalchemy import SQLAlchemy +from werkzeug.security import generate_password_hash, check_password_hash +from typing import Dict, Any + +# Shared db instance for all models +db = SQLAlchemy() + +class User(db.Model): + """User model for authentication.""" + __tablename__ = 'users' + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False, index=True) + email = db.Column(db.String(120), unique=True, nullable=False) + password_hash = db.Column(db.String(255), nullable=False) + role = db.Column(db.String(50), nullable=False) # Fellowship member name + gold = db.Column(db.Integer, nullable=False, default=500) + created_at = db.Column(db.DateTime, default=db.func.current_timestamp()) + + def set_password(self, password: str) -> None: + """Hash and set password.""" + self.password_hash = generate_password_hash(password, method='pbkdf2:sha256') + + def check_password(self, password: str) -> bool: + """Check if provided password matches hash.""" + return check_password_hash(self.password_hash, password) + + def to_dict(self) -> Dict[str, Any]: + """Convert user to dictionary.""" + return { + 'id': self.id, + 'username': self.username, + 'email': self.email, + 'role': self.role, + 'gold': self.gold, + 'created_at': self.created_at.isoformat() if self.created_at else None + } + + def __repr__(self) -> str: + return f'' diff --git a/sut/backend/requirements.txt b/sut/backend/requirements.txt new file mode 100644 index 0000000..893f1a3 --- /dev/null +++ b/sut/backend/requirements.txt @@ -0,0 +1,10 @@ +Flask==3.0.0 +flask-restx==1.3.0 +flask-cors==4.0.0 +flask-sqlalchemy==3.1.1 +flask-migrate==4.0.5 +werkzeug==3.0.1 +python-dotenv==1.0.0 +bcrypt==4.1.2 +openai==1.3.9 +httpx==0.24.1 diff --git a/sut/backend/routes/__init__.py b/sut/backend/routes/__init__.py new file mode 100644 index 0000000..ff4cf4a --- /dev/null +++ b/sut/backend/routes/__init__.py @@ -0,0 +1 @@ +"""API routes for the Fellowship Quest Tracker.""" diff --git a/sut/backend/routes/__pycache__/__init__.cpython-311.pyc b/sut/backend/routes/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..ba53812 Binary files /dev/null and b/sut/backend/routes/__pycache__/__init__.cpython-311.pyc differ diff --git a/sut/backend/routes/__pycache__/auth.cpython-311.pyc b/sut/backend/routes/__pycache__/auth.cpython-311.pyc new file mode 100644 index 0000000..0f64af4 Binary files /dev/null and b/sut/backend/routes/__pycache__/auth.cpython-311.pyc differ diff --git a/sut/backend/routes/__pycache__/locations.cpython-311.pyc b/sut/backend/routes/__pycache__/locations.cpython-311.pyc new file mode 100644 index 0000000..2f14ad9 Binary files /dev/null and b/sut/backend/routes/__pycache__/locations.cpython-311.pyc differ diff --git a/sut/backend/routes/__pycache__/members.cpython-311.pyc b/sut/backend/routes/__pycache__/members.cpython-311.pyc new file mode 100644 index 0000000..1deb421 Binary files /dev/null and b/sut/backend/routes/__pycache__/members.cpython-311.pyc differ diff --git a/sut/backend/routes/__pycache__/npc_chat.cpython-311.pyc b/sut/backend/routes/__pycache__/npc_chat.cpython-311.pyc new file mode 100644 index 0000000..d512653 Binary files /dev/null and b/sut/backend/routes/__pycache__/npc_chat.cpython-311.pyc differ diff --git a/sut/backend/routes/__pycache__/quests.cpython-311.pyc b/sut/backend/routes/__pycache__/quests.cpython-311.pyc new file mode 100644 index 0000000..9de3c96 Binary files /dev/null and b/sut/backend/routes/__pycache__/quests.cpython-311.pyc differ diff --git a/sut/backend/routes/__pycache__/shop.cpython-311.pyc b/sut/backend/routes/__pycache__/shop.cpython-311.pyc new file mode 100644 index 0000000..e655d89 Binary files /dev/null and b/sut/backend/routes/__pycache__/shop.cpython-311.pyc differ diff --git a/sut/backend/routes/auth.py b/sut/backend/routes/auth.py new file mode 100644 index 0000000..a491d03 --- /dev/null +++ b/sut/backend/routes/auth.py @@ -0,0 +1,121 @@ +"""Authentication routes.""" +from flask import Blueprint, request, session +from flask_restx import Api, Resource, fields +from models.user import User +from services.auth_service import authenticate_user, register_user +from typing import Dict, Any + +auth_bp = Blueprint('auth', __name__, url_prefix='/api') +auth_api = Api(auth_bp, doc=False, prefix='/auth') + +# Request/Response models for Swagger +login_model = auth_api.model('Login', { + 'username': fields.String(required=True, description='Username'), + 'password': fields.String(required=True, description='Password') +}) + +user_response_model = auth_api.model('UserResponse', { + 'id': fields.Integer(description='User ID'), + 'username': fields.String(description='Username'), + 'email': fields.String(description='Email'), + 'role': fields.String(description='Fellowship member role'), + 'gold': fields.Integer(description='Current gold balance'), +}) + +login_response_model = auth_api.model('LoginResponse', { + 'message': fields.String(description='Success message'), + 'user': fields.Nested(user_response_model, description='User information') +}) + +signup_model = auth_api.model('Signup', { + 'username': fields.String(required=True, description='Desired username (minimum 3 characters)'), + 'password': fields.String(required=True, description='Desired password (minimum 8 characters and at least one number)'), + 'email': fields.String(required=False, description='Optional email address') +}) + + +@auth_api.route('/signup') +class Signup(Resource): + """Public signup endpoint for open SUT registration.""" + + @auth_api.expect(signup_model, validate=False) + @auth_api.response(201, 'Signup successful', login_response_model) + @auth_api.response(400, 'Validation error') + @auth_api.doc(description='Register user and create session') + def post(self) -> tuple[Dict[str, Any], int]: + """Register a user and immediately log them in.""" + data = request.get_json() or {} + + username = data.get('username') + password = data.get('password') + email = data.get('email') + + try: + user = register_user(username=username, password=password, email=email) + except ValueError as error: + return {'error': str(error)}, 400 + + session['user_id'] = user.id + session['username'] = user.username + + return { + 'message': 'Signup successful', + 'user': user.to_dict(), + }, 201 + +@auth_api.route('/login') +class Login(Resource): + """User login endpoint.""" + + @auth_api.expect(login_model) + @auth_api.marshal_with(login_response_model) + @auth_api.doc(description='Authenticate user and create session') + def post(self) -> tuple[Dict[str, Any], int]: + """Login user.""" + data = request.get_json() + username = data.get('username') + password = data.get('password') + + if not username or not password: + return {'error': 'Username and password are required'}, 400 + + user = authenticate_user(username, password) + if not user: + return {'error': 'Invalid credentials'}, 401 + + # Create session + session['user_id'] = user.id + session['username'] = user.username + + return { + 'message': 'Login successful', + 'user': user.to_dict() + }, 200 + +@auth_api.route('/logout') +class Logout(Resource): + """User logout endpoint.""" + + @auth_api.doc(description='Logout user and destroy session') + def post(self) -> tuple[Dict[str, Any], int]: + """Logout user.""" + session.clear() + return {'message': 'Logout successful'}, 200 + +@auth_api.route('/me') +class CurrentUser(Resource): + """Get current authenticated user.""" + + @auth_api.marshal_with(user_response_model) + @auth_api.doc(description='Get current authenticated user information') + def get(self) -> tuple[Dict[str, Any], int]: + """Get current user.""" + user_id = session.get('user_id') + if not user_id: + return {'error': 'Not authenticated'}, 401 + + user = User.query.get(user_id) + if not user: + return {'error': 'User not found'}, 404 + + return user.to_dict(), 200 diff --git a/sut/backend/routes/locations.py b/sut/backend/routes/locations.py new file mode 100644 index 0000000..6f4136d --- /dev/null +++ b/sut/backend/routes/locations.py @@ -0,0 +1,41 @@ +"""Location routes.""" +from flask import Blueprint +from flask_restx import Api, Resource, fields +from models.location import Location +from typing import Dict, Any, List + +locations_bp = Blueprint('locations', __name__, url_prefix='/api') +locations_api = Api(locations_bp, doc=False, prefix='/locations') + +# Response model for Swagger +location_response_model = locations_api.model('LocationResponse', { + 'id': fields.Integer(description='Location ID'), + 'name': fields.String(description='Location name'), + 'description': fields.String(description='Location description'), + 'region': fields.String(description='Region name'), + 'map_x': fields.Float(description='Map X coordinate (pixel)'), + 'map_y': fields.Float(description='Map Y coordinate (pixel)'), + 'created_at': fields.String(description='Creation timestamp') +}) + +@locations_api.route('/') +class LocationList(Resource): + """Location list endpoints.""" + + @locations_api.marshal_list_with(location_response_model) + @locations_api.doc(description='Get all Middle-earth locations') + def get(self) -> tuple[List[Dict[str, Any]], int]: + """Get all locations.""" + locations = Location.query.all() + return [location.to_dict() for location in locations], 200 + +@locations_api.route('/') +class LocationDetail(Resource): + """Location detail endpoints.""" + + @locations_api.marshal_with(location_response_model) + @locations_api.doc(description='Get location by ID') + def get(self, location_id: int) -> tuple[Dict[str, Any], int]: + """Get location by ID.""" + location = Location.query.get_or_404(location_id) + return location.to_dict(), 200 diff --git a/sut/backend/routes/members.py b/sut/backend/routes/members.py new file mode 100644 index 0000000..28f7e14 --- /dev/null +++ b/sut/backend/routes/members.py @@ -0,0 +1,41 @@ +"""Fellowship member routes.""" +from flask import Blueprint +from flask_restx import Api, Resource, fields +from models.member import Member +from typing import Dict, Any, List + +members_bp = Blueprint('members', __name__, url_prefix='/api') +members_api = Api(members_bp, doc=False, prefix='/members') + +# Response model for Swagger +member_response_model = members_api.model('MemberResponse', { + 'id': fields.Integer(description='Member ID'), + 'name': fields.String(description='Member name'), + 'race': fields.String(description='Member race'), + 'role': fields.String(description='Member role'), + 'status': fields.String(description='Member status'), + 'description': fields.String(description='Member description'), + 'created_at': fields.String(description='Creation timestamp') +}) + +@members_api.route('/') +class MemberList(Resource): + """Fellowship member list endpoints.""" + + @members_api.marshal_list_with(member_response_model) + @members_api.doc(description='Get all Fellowship members') + def get(self) -> tuple[List[Dict[str, Any]], int]: + """Get all Fellowship members.""" + members = Member.query.all() + return [member.to_dict() for member in members], 200 + +@members_api.route('/') +class MemberDetail(Resource): + """Fellowship member detail endpoints.""" + + @members_api.marshal_with(member_response_model) + @members_api.doc(description='Get member by ID') + def get(self, member_id: int) -> tuple[Dict[str, Any], int]: + """Get member by ID.""" + member = Member.query.get_or_404(member_id) + return member.to_dict(), 200 diff --git a/sut/backend/routes/npc_chat.py b/sut/backend/routes/npc_chat.py new file mode 100644 index 0000000..2dd8efe --- /dev/null +++ b/sut/backend/routes/npc_chat.py @@ -0,0 +1,174 @@ +"""NPC chat routes backed by Azure AI service.""" +import uuid +from typing import Any, Dict + +from flask import Blueprint, request, session +from flask_restx import Api, Resource, fields + +from models.user import User +from models.quest import Quest, db +from services.npc_chat_service import NpcChatService + +npc_chat_bp = Blueprint('npc_chat', __name__, url_prefix='/api') +npc_chat_api = Api(npc_chat_bp, doc=False, prefix='/chat') + +chat_start_model = npc_chat_api.model('ChatStartRequest', { + 'character': fields.String(required=False, description='frodo|sam|gandalf'), +}) + +chat_message_model = npc_chat_api.model('ChatMessageRequest', { + 'character': fields.String(required=False, description='frodo|sam|gandalf'), + 'message': fields.String(required=True, description='User message'), +}) + +quest_creation_model = npc_chat_api.model('QuestCreationRequest', { + 'character': fields.String(required=False, description='frodo|sam|gandalf - NPC who proposes the quest'), + 'title': fields.String(required=True, description='Quest title'), + 'description': fields.String(required=True, description='Quest description'), + 'quest_type': fields.String(required=True, description='Quest type (The Journey, The Battle, The Fellowship, The Ring, Dark Magic)'), + 'priority': fields.String(required=True, description='Quest priority (Critical, Important, Standard)'), +}) + + +def _require_auth() -> bool: + return session.get('user_id') is not None + + +def _get_current_user() -> User: + user_id = session.get('user_id') + return User.query.get(user_id) + + +def _get_chat_scope_id() -> str: + scope_id = session.get('chat_scope_id') + if not scope_id: + scope_id = uuid.uuid4().hex + session['chat_scope_id'] = scope_id + return scope_id + + +@npc_chat_api.route('/start') +class ChatStart(Resource): + @npc_chat_api.expect(chat_start_model) + def post(self) -> tuple[Dict[str, Any], int]: + if not _require_auth(): + return {'error': 'Authentication required'}, 401 + + user = _get_current_user() + if not user: + return {'error': 'User not found'}, 404 + + data = request.get_json() or {} + scope_id = _get_chat_scope_id() + payload = NpcChatService.start_conversation( + user_id=user.id, + username=user.username, + character=data.get('character'), + scope_id=scope_id, + ) + return payload, 200 + + +@npc_chat_api.route('/message') +class ChatMessage(Resource): + @npc_chat_api.expect(chat_message_model) + def post(self) -> tuple[Dict[str, Any], int]: + if not _require_auth(): + return {'error': 'Authentication required'}, 401 + + user = _get_current_user() + if not user: + return {'error': 'User not found'}, 404 + + data = request.get_json() or {} + message = (data.get('message') or '').strip() + if not message: + return {'error': 'message is required'}, 400 + + scope_id = _get_chat_scope_id() + payload = NpcChatService.send_message( + user_id=user.id, + username=user.username, + character=data.get('character'), + user_message=message, + scope_id=scope_id, + ) + return payload, 200 + + +@npc_chat_api.route('/session') +class ChatSession(Resource): + def get(self) -> tuple[Dict[str, Any], int]: + if not _require_auth(): + return {'error': 'Authentication required'}, 401 + + user = _get_current_user() + if not user: + return {'error': 'User not found'}, 404 + + character = request.args.get('character') + scope_id = _get_chat_scope_id() + payload = NpcChatService.get_session(user_id=user.id, character=character, scope_id=scope_id) + return payload, 200 + + +@npc_chat_api.route('/reset') +class ChatReset(Resource): + @npc_chat_api.expect(chat_start_model) + def post(self) -> tuple[Dict[str, Any], int]: + if not _require_auth(): + return {'error': 'Authentication required'}, 401 + + user = _get_current_user() + if not user: + return {'error': 'User not found'}, 404 + + data = request.get_json() or {} + scope_id = _get_chat_scope_id() + payload = NpcChatService.reset_session(user_id=user.id, character=data.get('character'), scope_id=scope_id) + return payload, 200 + + +@npc_chat_api.route('/create_quest') +class ChatCreateQuest(Resource): + """Create a quest from NPC chat interaction.""" + + @npc_chat_api.expect(quest_creation_model) + def post(self) -> tuple[Dict[str, Any], int]: + """Create a quest proposed by an NPC. + + This endpoint allows the frontend to persist a suggested quest + that was generated during NPC chat. + """ + if not _require_auth(): + return {'error': 'Authentication required'}, 401 + + user = _get_current_user() + if not user: + return {'error': 'User not found'}, 404 + + data = request.get_json() or {} + + # Validate required fields + required_fields = ['title', 'description', 'quest_type', 'priority'] + if not all(data.get(field) for field in required_fields): + return {'error': 'Missing required fields: title, description, quest_type, priority'}, 400 + + # Create the quest + quest = Quest( + title=data.get('title'), + description=data.get('description'), + quest_type=data.get('quest_type'), + priority=data.get('priority'), + is_dark_magic=data.get('is_dark_magic', False), + assigned_to=user.id, + location_id=data.get('location_id'), + ) + + db.session.add(quest) + db.session.commit() + + return { + 'quest': quest.to_dict(), + 'message': f'{data.get("character", "An NPC")} has created a quest for you!', + }, 201 diff --git a/sut/backend/routes/quests.py b/sut/backend/routes/quests.py new file mode 100644 index 0000000..90f0a96 --- /dev/null +++ b/sut/backend/routes/quests.py @@ -0,0 +1,237 @@ +"""Quest routes.""" +from flask import Blueprint, request, jsonify, session +from flask_restx import Api, Resource, fields +from models.quest import Quest, db +from models.user import User +from datetime import datetime +from typing import Dict, Any, List, Optional + +quests_bp = Blueprint('quests', __name__, url_prefix='/api') +quests_api = Api(quests_bp, doc=False, prefix='/quests') + +# Request/Response models for Swagger +quest_model = quests_api.model('Quest', { + 'title': fields.String(required=True, description='Quest title'), + 'description': fields.String(description='Quest description'), + 'status': fields.String(description='Quest status (not_yet_begun, the_road_goes_ever_on, it_is_done, the_shadow_falls)'), + 'quest_type': fields.String(description='Quest type (The Journey, The Battle, The Fellowship, The Ring, Dark Magic)'), + 'priority': fields.String(description='Quest priority (Critical, Important, Standard)'), + 'is_dark_magic': fields.Boolean(description='Dark magic flag'), + 'assigned_to': fields.Integer(description='User ID of assignee'), + 'location_id': fields.Integer(description='Location ID'), + 'character_quote': fields.String(description='Character quote for completion') +}) + +quest_response_model = quests_api.model('QuestResponse', { + 'id': fields.Integer(description='Quest ID'), + 'title': fields.String(description='Quest title'), + 'description': fields.String(description='Quest description'), + 'status': fields.String(description='Quest status'), + 'quest_type': fields.String(description='Quest type'), + 'priority': fields.String(description='Quest priority'), + 'is_dark_magic': fields.Boolean(description='Dark magic flag'), + 'assigned_to': fields.Integer(description='User ID of assignee'), + 'location_id': fields.Integer(description='Location ID'), + 'location_name': fields.String(description='Location name'), + 'assignee_name': fields.String(description='Assignee username'), + 'character_quote': fields.String(description='Character quote'), + 'created_at': fields.String(description='Creation timestamp'), + 'updated_at': fields.String(description='Update timestamp'), + 'completed_at': fields.String(description='Completion timestamp'), + 'gold_reward': fields.Integer(description='Gold reward granted for quest completion'), + 'current_gold': fields.Integer(description='Current gold total for the authenticated user'), + 'message': fields.String(description='Success message for quest completion') +}) + +def require_auth() -> bool: + """Check if user is authenticated.""" + return session.get('user_id') is not None + + +def _reward_for_priority(priority: Optional[str]) -> int: + rewards = { + 'Critical': 100, + 'Important': 60, + 'Standard': 40, + } + return rewards.get(priority or '', 50) + +@quests_api.route('/') +class QuestList(Resource): + """Quest list endpoints.""" + + @quests_api.marshal_list_with(quest_response_model) + @quests_api.doc(description='Get all quests with optional filtering') + def get(self) -> tuple[List[Dict[str, Any]], int]: + """Get all quests with optional filtering.""" + query = Quest.query + + # Filter by status + status = request.args.get('status') + if status: + # Map old status values for backward compatibility + status_mapping = { + 'pending': 'not_yet_begun', + 'in_progress': 'the_road_goes_ever_on', + 'completed': 'it_is_done', + 'blocked': 'the_shadow_falls' + } + mapped_status = status_mapping.get(status, status) + query = query.filter(Quest.status == mapped_status) + + # Filter by quest type + quest_type = request.args.get('quest_type') + if quest_type: + query = query.filter(Quest.quest_type == quest_type) + + # Filter by priority + priority = request.args.get('priority') + if priority: + query = query.filter(Quest.priority == priority) + + # Filter by dark magic + dark_magic = request.args.get('dark_magic') + if dark_magic is not None: + is_dark_magic = dark_magic.lower() == 'true' + query = query.filter(Quest.is_dark_magic == is_dark_magic) + + # Filter by location + location_id = request.args.get('location_id') + if location_id: + query = query.filter(Quest.location_id == int(location_id)) + + # Filter by assigned user + assigned_to = request.args.get('assigned_to') + if assigned_to: + query = query.filter(Quest.assigned_to == int(assigned_to)) + + quests = query.all() + return [quest.to_dict() for quest in quests], 200 + + @quests_api.expect(quest_model) + @quests_api.marshal_with(quest_response_model) + @quests_api.doc(description='Create a new quest', security='session') + def post(self) -> tuple[Dict[str, Any], int]: + """Create a new quest.""" + if not require_auth(): + return {'error': 'Authentication required'}, 401 + + data = request.get_json() + + # Map old status values for backward compatibility + status = data.get('status', 'not_yet_begun') + status_mapping = { + 'pending': 'not_yet_begun', + 'in_progress': 'the_road_goes_ever_on', + 'completed': 'it_is_done', + 'blocked': 'the_shadow_falls' + } + mapped_status = status_mapping.get(status, status) + + quest = Quest( + title=data.get('title'), + description=data.get('description'), + status=mapped_status, + quest_type=data.get('quest_type'), + priority=data.get('priority'), + is_dark_magic=data.get('is_dark_magic', False), + character_quote=data.get('character_quote'), + assigned_to=data.get('assigned_to'), + location_id=data.get('location_id') + ) + + db.session.add(quest) + db.session.commit() + + return quest.to_dict(), 201 + +@quests_api.route('/') +class QuestDetail(Resource): + """Quest detail endpoints.""" + + @quests_api.marshal_with(quest_response_model) + @quests_api.doc(description='Get quest by ID') + def get(self, quest_id: int) -> tuple[Dict[str, Any], int]: + """Get quest by ID.""" + quest = Quest.query.get_or_404(quest_id) + return quest.to_dict(), 200 + + @quests_api.expect(quest_model) + @quests_api.marshal_with(quest_response_model) + @quests_api.doc(description='Update quest', security='session') + def put(self, quest_id: int) -> tuple[Dict[str, Any], int]: + """Update quest.""" + if not require_auth(): + return {'error': 'Authentication required'}, 401 + + quest = Quest.query.get_or_404(quest_id) + data = request.get_json() + + quest.title = data.get('title', quest.title) + quest.description = data.get('description', quest.description) + + # Map old status values for backward compatibility + if 'status' in data: + status = data.get('status') + status_mapping = { + 'pending': 'not_yet_begun', + 'in_progress': 'the_road_goes_ever_on', + 'completed': 'it_is_done', + 'blocked': 'the_shadow_falls' + } + quest.status = status_mapping.get(status, status) + + quest.quest_type = data.get('quest_type', quest.quest_type) + quest.priority = data.get('priority', quest.priority) + quest.is_dark_magic = data.get('is_dark_magic', quest.is_dark_magic) + quest.character_quote = data.get('character_quote', quest.character_quote) + quest.assigned_to = data.get('assigned_to', quest.assigned_to) + quest.location_id = data.get('location_id', quest.location_id) + + db.session.commit() + return quest.to_dict(), 200 + + @quests_api.doc(description='Delete quest', security='session') + def delete(self, quest_id: int) -> tuple[Dict[str, Any], int]: + """Delete quest.""" + if not require_auth(): + return {'error': 'Authentication required'}, 401 + + quest = Quest.query.get_or_404(quest_id) + db.session.delete(quest) + db.session.commit() + + return {'message': 'Quest deleted successfully'}, 200 + +@quests_api.route('//complete') +class QuestComplete(Resource): + """Quest completion endpoint.""" + + @quests_api.marshal_with(quest_response_model) + @quests_api.doc(description='Mark quest as complete', security='session') + def put(self, quest_id: int) -> tuple[Dict[str, Any], int]: + """Mark quest as complete.""" + if not require_auth(): + return {'error': 'Authentication required'}, 401 + + quest = Quest.query.get_or_404(quest_id) + + # Set status to completed + quest.status = 'it_is_done' + quest.completed_at = datetime.utcnow() + + user_id = session.get('user_id') + current_user = User.query.get(user_id) if user_id else None + reward = _reward_for_priority(quest.priority) + if current_user: + current_user.gold = (current_user.gold or 0) + reward + + db.session.commit() + + # Return quest with completion message + result = quest.to_dict() + result['gold_reward'] = reward + result['current_gold'] = current_user.gold if current_user else None + result['message'] = f'The Quest Is Done! You earned {reward} Gold.' + + return result, 200 diff --git a/sut/backend/routes/shop.py b/sut/backend/routes/shop.py new file mode 100644 index 0000000..85e773f --- /dev/null +++ b/sut/backend/routes/shop.py @@ -0,0 +1,133 @@ +"""Shop routes for bargaining market gameplay.""" +from typing import Any, Dict, Optional + +from flask import Blueprint, request, session +from flask_restx import Api, Resource, fields + +from models.user import User +from services.shop_service import ShopService + + +shop_bp = Blueprint('shop', __name__, url_prefix='/api') +shop_api = Api(shop_bp, doc=False, prefix='/shop') + +purchase_model = shop_api.model('ShopPurchaseRequest', { + 'item_id': fields.Integer(required=True, description='Unique item ID'), + 'paid_price': fields.Integer(required=True, description='Agreed paid price'), +}) + + +def _require_auth() -> bool: + return session.get('user_id') is not None + + +def _get_current_user() -> Optional[User]: + user_id = session.get('user_id') + if not user_id: + return None + return User.query.get(user_id) + + +@shop_api.route('/items') +class ShopItems(Resource): + def get(self) -> tuple[Dict[str, Any], int]: + if not _require_auth(): + return {'error': 'Authentication required'}, 401 + + character = (request.args.get('character') or '').strip().lower() or None + items = ShopService.list_available_items(character=character) + return {'items': items}, 200 + + +@shop_api.route('/items/') +class ShopItemDetail(Resource): + def get(self, item_id: int) -> tuple[Dict[str, Any], int]: + if not _require_auth(): + return {'error': 'Authentication required'}, 401 + + item = ShopService.get_item_public(item_id) + if not item: + return {'error': 'Item not found'}, 404 + return {'item': item}, 200 + + +@shop_api.route('/purchase') +class ShopPurchase(Resource): + @shop_api.expect(purchase_model) + def post(self) -> tuple[Dict[str, Any], int]: + if not _require_auth(): + return {'error': 'Authentication required'}, 401 + + user = _get_current_user() + if not user: + return {'error': 'User not found'}, 404 + + payload = request.get_json() or {} + item_id = payload.get('item_id') + paid_price = payload.get('paid_price') + + if not item_id or paid_price is None: + return {'error': 'item_id and paid_price are required'}, 400 + + try: + result = ShopService.purchase_item(user_id=user.id, item_id=int(item_id), paid_price=int(paid_price)) + return result, 200 + except ValueError as error: + return {'error': str(error)}, 400 + + +@shop_api.route('/inventory') +class ShopInventory(Resource): + def get(self) -> tuple[Dict[str, Any], int]: + if not _require_auth(): + return {'error': 'Authentication required'}, 401 + + user = _get_current_user() + if not user: + return {'error': 'User not found'}, 404 + + inventory = ShopService.get_user_inventory(user.id) + return {'inventory': inventory}, 200 + + +@shop_api.route('/stats') +class ShopStats(Resource): + def get(self) -> tuple[Dict[str, Any], int]: + if not _require_auth(): + return {'error': 'Authentication required'}, 401 + + user = _get_current_user() + if not user: + return {'error': 'User not found'}, 404 + + stats = ShopService.get_user_stats(user.id) + return {'stats': stats}, 200 + + +@shop_api.route('/balance') +class ShopBalance(Resource): + def get(self) -> tuple[Dict[str, Any], int]: + if not _require_auth(): + return {'error': 'Authentication required'}, 401 + + user = _get_current_user() + if not user: + return {'error': 'User not found'}, 404 + + return ShopService.get_balance(user.id), 200 + + +@shop_api.route('/test-reset') +class TestReset(Resource): + """Reset shop state for testing - marks all items as not sold and resets user gold.""" + def post(self) -> tuple[Dict[str, Any], int]: + import os + # Only allow in non-production environments + if os.getenv('FLASK_ENV') in {'production', 'prod'}: + return {'error': 'Test reset not allowed in production'}, 403 + + try: + ShopService.reset_for_tests() + return {'success': True, 'message': 'Test state reset successfully'}, 200 + except Exception as e: + return {'error': str(e)}, 500 diff --git a/sut/backend/services/__init__.py b/sut/backend/services/__init__.py new file mode 100644 index 0000000..944069b --- /dev/null +++ b/sut/backend/services/__init__.py @@ -0,0 +1 @@ +"""Service modules for business logic.""" diff --git a/sut/backend/services/__pycache__/__init__.cpython-311.pyc b/sut/backend/services/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..33601ea Binary files /dev/null and b/sut/backend/services/__pycache__/__init__.cpython-311.pyc differ diff --git a/sut/backend/services/__pycache__/auth_service.cpython-311.pyc b/sut/backend/services/__pycache__/auth_service.cpython-311.pyc new file mode 100644 index 0000000..d19d819 Binary files /dev/null and b/sut/backend/services/__pycache__/auth_service.cpython-311.pyc differ diff --git a/sut/backend/services/__pycache__/bargaining_algorithm.cpython-311.pyc b/sut/backend/services/__pycache__/bargaining_algorithm.cpython-311.pyc new file mode 100644 index 0000000..6f0d8fe Binary files /dev/null and b/sut/backend/services/__pycache__/bargaining_algorithm.cpython-311.pyc differ diff --git a/sut/backend/services/__pycache__/bargaining_config.cpython-311.pyc b/sut/backend/services/__pycache__/bargaining_config.cpython-311.pyc new file mode 100644 index 0000000..cab97c5 Binary files /dev/null and b/sut/backend/services/__pycache__/bargaining_config.cpython-311.pyc differ diff --git a/sut/backend/services/__pycache__/character_profiles.cpython-311.pyc b/sut/backend/services/__pycache__/character_profiles.cpython-311.pyc new file mode 100644 index 0000000..e0c0803 Binary files /dev/null and b/sut/backend/services/__pycache__/character_profiles.cpython-311.pyc differ diff --git a/sut/backend/services/__pycache__/negotiation_logger.cpython-311.pyc b/sut/backend/services/__pycache__/negotiation_logger.cpython-311.pyc new file mode 100644 index 0000000..0729d0b Binary files /dev/null and b/sut/backend/services/__pycache__/negotiation_logger.cpython-311.pyc differ diff --git a/sut/backend/services/__pycache__/npc_chat_service.cpython-311.pyc b/sut/backend/services/__pycache__/npc_chat_service.cpython-311.pyc new file mode 100644 index 0000000..d5c4c84 Binary files /dev/null and b/sut/backend/services/__pycache__/npc_chat_service.cpython-311.pyc differ diff --git a/sut/backend/services/__pycache__/quest_generation_service.cpython-311.pyc b/sut/backend/services/__pycache__/quest_generation_service.cpython-311.pyc new file mode 100644 index 0000000..7af92c4 Binary files /dev/null and b/sut/backend/services/__pycache__/quest_generation_service.cpython-311.pyc differ diff --git a/sut/backend/services/__pycache__/shop_service.cpython-311.pyc b/sut/backend/services/__pycache__/shop_service.cpython-311.pyc new file mode 100644 index 0000000..07f3756 Binary files /dev/null and b/sut/backend/services/__pycache__/shop_service.cpython-311.pyc differ diff --git a/sut/backend/services/auth_service.py b/sut/backend/services/auth_service.py new file mode 100644 index 0000000..d471ad0 --- /dev/null +++ b/sut/backend/services/auth_service.py @@ -0,0 +1,65 @@ +"""Authentication service.""" +from models.user import User, db +from sqlalchemy import func +from typing import Optional + + +def _normalize_username(username: str) -> str: + return (username or '').strip() + +def authenticate_user(username: str, password: str) -> Optional[User]: + """Authenticate a user by username and password.""" + normalized_username = _normalize_username(username) + if not normalized_username or not password: + return None + + user = User.query.filter(func.lower(User.username) == normalized_username.lower()).first() + if user and user.check_password(password): + return user + return None + +def get_user_by_id(user_id: int) -> Optional[User]: + """Get user by ID.""" + return User.query.get(user_id) + +def get_user_by_username(username: str) -> Optional[User]: + """Get user by username.""" + normalized_username = _normalize_username(username) + if not normalized_username: + return None + return User.query.filter(func.lower(User.username) == normalized_username.lower()).first() + + +def register_user(username: str, password: str, email: Optional[str] = None) -> User: + """Register a new user with basic defaults for public SUT usage.""" + normalized_username = _normalize_username(username) + normalized_password = (password or '').strip() + + if not normalized_username or not normalized_password: + raise ValueError('Username and password are required') + + if len(normalized_username) < 3: + raise ValueError('Username must be at least 3 characters long') + + if len(normalized_password) < 8 or not any(char.isdigit() for char in normalized_password): + raise ValueError('Password must be at least 8 characters and contain at least one number') + + if get_user_by_username(normalized_username): + raise ValueError('Username already exists') + + normalized_email = (email or '').strip().lower() or f'{normalized_username.lower()}@testingfantasy.local' + existing_email = User.query.filter_by(email=normalized_email).first() + if existing_email: + raise ValueError('Email is already in use') + + user = User( + username=normalized_username, + email=normalized_email, + role=normalized_username, # Use username as role/character name + gold=500, + ) + user.set_password(normalized_password) + + db.session.add(user) + db.session.commit() + return user diff --git a/sut/backend/services/bargaining_algorithm.py b/sut/backend/services/bargaining_algorithm.py new file mode 100644 index 0000000..e087dc9 --- /dev/null +++ b/sut/backend/services/bargaining_algorithm.py @@ -0,0 +1,337 @@ +"""Bargaining algorithm for NPC negotiation.""" +from __future__ import annotations + +import json +import logging +import random +from typing import Any, Dict, List, Optional +from enum import Enum + +logger = logging.getLogger(__name__) + + +class NegotiationResult(str, Enum): + """Possible outcomes of a negotiation.""" + COUNTER_OFFER = "counter-offer" + OFFER_ACCEPTED = "offer-accepted" + OFFER_REJECTED = "offer-rejected" + STOP_BARGAIN = "stop-bargain" + + +class BargainingAlgorithm: + """ + Hybrid bargaining algorithm that evaluates user offers based on character traits. + + Algorithm evaluates: + - Character personality (patience, concession rate, boredom threshold, accept ratio) + - Current mood (affected by user actions) + - External events (randomness factor) + - Flattery detection (user behavior trigger) + - Round count (max rounds per character) + """ + + # Default personality profiles indexed by character + PERSONALITY_PROFILES = { + "frodo": { + "patience": 5, + "concession": 0.12, + "boredom": 0.08, + "accept_ratio": 0.92, + "max_rounds": 6, + "generosity_on_flatter": 0.05, # 5% better offer when flattered + }, + "sam": { + "patience": 4, + "concession": 0.10, + "boredom": 0.10, + "accept_ratio": 0.95, + "max_rounds": 5, + "generosity_on_flatter": 0.04, + }, + "gandalf": { + "patience": 6, + "concession": 0.15, + "boredom": 0.05, + "accept_ratio": 0.90, + "max_rounds": 7, + "generosity_on_flatter": 0.06, + }, + } + + @classmethod + def evaluate_offer( + cls, + user_offer: int, + current_ask: int, + character: str, + round_num: int, + is_flattered: bool = False, + mood_modifiers: Optional[Dict[str, float]] = None, + user_message: Optional[str] = None, + ) -> Dict[str, Any]: + """ + Evaluate a user's offer or message against the NPC's negotiation state. + + Args: + user_offer: The amount offered by the user + current_ask: The NPC's current asking price + character: The NPC character name + round_num: Current negotiation round (0-based) + is_flattered: Whether the user flattered the character + mood_modifiers: Optional mood adjustments (e.g., {"patience": -1, "boredom": +0.1}) + user_message: The raw user message (for 'deal' detection) + + Returns: + Dict containing: + - result: NegotiationResult enum value + - counter_offer: New ask price (if counter-offer) + - context: Debug context about the decision + """ + profile = cls.PERSONALITY_PROFILES.get(character, cls.PERSONALITY_PROFILES["gandalf"]) + + # Apply mood modifiers if provided + patience = profile["patience"] + boredom = profile["boredom"] + + if mood_modifiers: + patience += mood_modifiers.get("patience", 0) + boredom += mood_modifiers.get("boredom", 0) + boredom = max(0, min(1, boredom)) # Clamp to [0, 1] + + # If user says 'deal', accept at current ask + if user_message and user_message.strip().lower() in {"deal", "i'll take it", "i will take it", "buy", "buy it", "accept"}: + return { + "result": NegotiationResult.OFFER_ACCEPTED, + "counter_offer": None, + "context": { + "reason": "user_said_deal", + "user_offer": user_offer, + "current_ask": current_ask, + }, + } + + # Check if max rounds exceeded + if round_num >= profile["max_rounds"]: + return { + "result": NegotiationResult.STOP_BARGAIN, + "counter_offer": None, + "context": { + "reason": "max_rounds_exceeded", + "round_num": round_num, + "max_rounds": profile["max_rounds"], + }, + } + + # Calculate acceptance threshold + # Flattered characters are slightly more generous + accept_ratio = profile["accept_ratio"] + if is_flattered: + accept_ratio -= profile["generosity_on_flatter"] + + # Check if offer is acceptable + if user_offer >= int(current_ask * accept_ratio): + return { + "result": NegotiationResult.OFFER_ACCEPTED, + "counter_offer": None, + "context": { + "reason": "offer_acceptable", + "user_offer": user_offer, + "threshold": int(current_ask * accept_ratio), + "is_flattered": is_flattered, + }, + } + + # Check for lucky drop (long negotiation can result in sudden price drop) + long_negotiation_threshold = max(3, patience) + if round_num >= long_negotiation_threshold and random.random() < 0.10: + lucky_price = max(user_offer, int(current_ask * 0.60)) + return { + "result": NegotiationResult.COUNTER_OFFER, + "counter_offer": lucky_price, + "context": { + "reason": "lucky_drop", + "round_num": round_num, + "patience_threshold": long_negotiation_threshold, + "message_hint": "user_wore_down_character", + }, + } + + # Check if character is bored and refuses + if round_num >= patience and random.random() < boredom: + return { + "result": NegotiationResult.STOP_BARGAIN, + "counter_offer": None, + "context": { + "reason": "boredom_threshold", + "round_num": round_num, + "patience": patience, + "boredom_roll": boredom, + }, + } + + # Counter-offer: concede a bit, but never below user's offer + concession_amount = max(1, int(current_ask * profile["concession"])) + floor_price = max(user_offer, int(current_ask * 0.65)) # Don't go below user's offer or 65% of current ask + new_ask = max(floor_price, current_ask - concession_amount) + + return { + "result": NegotiationResult.COUNTER_OFFER, + "counter_offer": new_ask, + "context": { + "reason": "counter_offer", + "round_num": round_num, + "original_ask": current_ask, + "concession_amount": concession_amount, + "floor_price": floor_price, + "is_flattered": is_flattered, + "user_offer": user_offer, + }, + } + + @classmethod + def detect_flattery(cls, user_message: str) -> bool: + """ + Detect flattery in user's message. + + Looks for phrases indicating compliments, admiration, or flattery. + This is visible to backend only; LLM can add more sophisticated detection. + """ + message_lower = user_message.lower().strip() + + flattery_keywords = [ + "amazing", + "beautiful", + "brave", + "brilliant", + "clever", + "exceptional", + "excellent", + "extraordinary", + "fabulous", + "fantastic", + "fine", + "glorious", + "graceful", + "great", + "handsome", + "impressive", + "incredible", + "intelligent", + "magnificent", + "marvelous", + "noble", + "outstanding", + "powerful", + "remarkable", + "skilled", + "splendid", + "superb", + "talented", + "tremendous", + "wonderful", + "you are", + "you're", + "you seem", + "that's great", + "that's amazing", + "i admire", + "i respect", + "very wise", + "very kind", + "very clever", + "very brave", + ] + + # Simple keyword matching + return any(keyword in message_lower for keyword in flattery_keywords) + + @classmethod + def calculate_mood_change( + cls, + previous_offer: Optional[int], + current_offer: int, + current_ask: int, + ) -> Dict[str, float]: + """ + Calculate mood changes based on user actions. + + Returns mood modifiers that should be applied to the negotiation profile. + + Examples: + - Repeated very low offers -> negative mood (more patient but bored) + - Fair offers -> positive mood + - Rapidly increasing offers -> positive mood + """ + modifiers = {} + + if previous_offer is not None: + offer_delta = current_offer - previous_offer + offer_ratio = current_offer / current_ask if current_ask > 0 else 0 + + # If user is insultingly low (< 30% of ask), character gets annoyed + if offer_ratio < 0.30: + modifiers["boredom"] = 0.05 # Increases boredom + modifiers["patience"] = -1 # Decreases patience + # If offer is fair (50-80% of ask), character is encouraged + elif 0.50 <= offer_ratio <= 0.80: + modifiers["boredom"] = -0.03 # Decreases boredom + # If user is increasing offers, character is pleased + elif offer_delta > 0: + modifiers["boredom"] = -0.02 + + return modifiers + + @classmethod + def get_summary_for_llm( + cls, + negotiation_state: Dict[str, Any], + algorithm_result: Dict[str, Any], + user_offer: int, + character_personality: str, + is_flattered: bool, + mood_modifiers: Dict[str, float], + ) -> Dict[str, Any]: + """ + Generate a JSON summary to pass to the LLM for natural language generation. + + Only includes relevant fields for the current negotiation turn. + """ + profile = cls.PERSONALITY_PROFILES.get( + negotiation_state.get("character", "gandalf"), + cls.PERSONALITY_PROFILES["gandalf"] + ) + + summary: Dict[str, Any] = { + "character": negotiation_state.get("character"), + "item_name": negotiation_state.get("item_name"), + "item_id": negotiation_state.get("item_id"), + "original_price": negotiation_state.get("original_price"), + "current_ask": negotiation_state.get("current_ask"), + "user_offer": user_offer, + "round": negotiation_state.get("round"), + "character_personality_type": character_personality, + "is_flattered": is_flattered, + } + + # Add algorithm result based on type + result_type = algorithm_result.get("result") + if result_type == NegotiationResult.COUNTER_OFFER: + summary["negotiation_result"] = "counter-offer" + summary["counter_offer"] = algorithm_result.get("counter_offer") + elif result_type == NegotiationResult.OFFER_ACCEPTED: + summary["negotiation_result"] = "offer-accepted" + elif result_type == NegotiationResult.OFFER_REJECTED: + summary["negotiation_result"] = "offer-rejected" + elif result_type == NegotiationResult.STOP_BARGAIN: + summary["negotiation_result"] = "stop-bargain" + summary["stop_reason"] = algorithm_result.get("context", {}).get("reason") + + # Add mood context if modifiers present + if mood_modifiers: + summary["mood_context"] = mood_modifiers + + # Only include negotiation_style if applicable + if "negotiation_style" in negotiation_state: + summary["user_negotiation_style"] = negotiation_state["negotiation_style"] + + return summary diff --git a/sut/backend/services/bargaining_config.py b/sut/backend/services/bargaining_config.py new file mode 100644 index 0000000..e354898 --- /dev/null +++ b/sut/backend/services/bargaining_config.py @@ -0,0 +1,157 @@ +"""Configuration management for bargaining system via AWS Parameter Store.""" +import json +import logging +from typing import Any, Dict, Optional +from functools import lru_cache +import os + +logger = logging.getLogger(__name__) + + +class BargainingConfig: + """ + Load and manage bargaining configuration. + + Configuration can come from: + 1. AWS Parameter Store (for runtime updates) + 2. Environment variables (for local dev) + 3. Default values (hardcoded) + + For now, uses environment variables. AWS Parameter Store integration + can be added later. + """ + + # Default configuration values + DEFAULT_CONFIG = { + "flattery_bonus_percent": 0.05, # 5% better offer when flattered + "max_negotiation_rounds": { + "frodo": 6, + "sam": 5, + "gandalf": 7, + }, + "mood_change_probabilities": { + "boredom_on_low_offer": 0.10, # 10% chance to increase boredom + "lucky_drop_chance": 0.10, # 10% chance of sudden price drop + }, + "logging_enabled": True, + "log_retention_days": 30, + "flattery_only_once_per_negotiation": True, + } + + _config_cache: Optional[Dict[str, Any]] = None + + @classmethod + def load_config(cls, force_reload: bool = False) -> Dict[str, Any]: + """ + Load configuration from AWS Parameter Store or environment. + + Args: + force_reload: If True, bypass cache and reload from source + + Returns: + Configuration dictionary + """ + if cls._config_cache and not force_reload: + return cls._config_cache + + config = cls.DEFAULT_CONFIG.copy() + + # Try to load from AWS Parameter Store + aws_config = cls._load_from_aws_parameter_store() + if aws_config: + config.update(aws_config) + logger.info("✓ Loaded bargaining config from AWS Parameter Store") + else: + # Fall back to environment variables + env_config = cls._load_from_environment() + if env_config: + config.update(env_config) + logger.info("✓ Loaded bargaining config from environment variables") + else: + logger.info("✓ Using default bargaining configuration") + + cls._config_cache = config + return config + + @classmethod + def _load_from_aws_parameter_store(cls) -> Optional[Dict[str, Any]]: + """Load configuration from AWS Systems Manager Parameter Store.""" + try: + import boto3 + ssm_client = boto3.client("ssm") + + param_name = os.getenv("BARGAINING_CONFIG_PARAM", "/fellowship/bargaining/config") + + try: + response = ssm_client.get_parameter( + Name=param_name, + WithDecryption=False + ) + config_str = response["Parameter"]["Value"] + config = json.loads(config_str) + return config + except ssm_client.exceptions.ParameterNotFound: + logger.debug(f"Parameter {param_name} not found in Parameter Store") + return None + except (ImportError, Exception) as e: + logger.debug(f"Could not load from AWS Parameter Store: {type(e).__name__}") + return None + + @classmethod + def _load_from_environment(cls) -> Optional[Dict[str, Any]]: + """Load configuration from environment variables.""" + config = {} + + # Try to load BARGAINING_CONFIG_JSON env var + config_json = os.getenv("BARGAINING_CONFIG_JSON") + if config_json: + try: + env_config = json.loads(config_json) + return env_config + except json.JSONDecodeError: + logger.warning("Invalid JSON in BARGAINING_CONFIG_JSON env var") + + return None if not config else config + + @classmethod + def get(cls, key: str, default: Any = None) -> Any: + """ + Get a configuration value by key path (dot-notation supported). + + Example: config.get("mood_change_probabilities.lucky_drop_chance") + """ + config = cls.load_config() + + if "." in key: + parts = key.split(".") + value = config + for part in parts: + if isinstance(value, dict): + value = value.get(part) + else: + return default + return value if value is not None else default + + return config.get(key, default) + + @classmethod + def get_character_config(cls, character: str) -> Dict[str, Any]: + """Get configuration for a specific character.""" + config = cls.load_config() + + # Return character-specific config if it exists + if "character_configs" in config and character in config["character_configs"]: + return config["character_configs"][character] + + # Fall back to defaults + return { + "max_rounds": config["max_negotiation_rounds"].get( + character, config["max_negotiation_rounds"]["gandalf"] + ), + "flattery_bonus": config["flattery_bonus_percent"], + } + + @classmethod + def clear_cache(cls) -> None: + """Clear configuration cache (useful for testing).""" + cls._config_cache = None diff --git a/sut/backend/services/character_profiles.py b/sut/backend/services/character_profiles.py new file mode 100644 index 0000000..f5f667e --- /dev/null +++ b/sut/backend/services/character_profiles.py @@ -0,0 +1,253 @@ +"""LOTR Character profiles for immersive NPC interactions. + +Each character has: +- personality: Core behavioral traits +- mannerisms: Distinctive speech patterns and expressions +- hobbies: Things they enjoy or specialize in +- quests_affinity: Types of quests they naturally give +- system_prompt: Base AI personality for Azure OpenAI +- fallback_responses: Varied conversational replies to feel more natural +""" + +from typing import Dict, Any, List + +# Character profiles with rich personality definitions +CHARACTER_PROFILES: Dict[str, Dict[str, Any]] = { + "frodo": { + "full_name": "Frodo Baggins", + "title": "Ring-bearer", + "personality": [ + "Humble and introspective", + "Burden-aware (struggles with weight of responsibility)", + "Brave under pressure", + "Thoughtful and cautious", + "Compassionate toward others", + ], + "mannerisms": [ + "Often references the weight or burden of tasks", + "Uses quiet wisdom rather than declarations", + "Admits doubt and uncertainty", + "Asks for counsel before acting", + "Speaks of 'small acts' having great consequence", + "Tends toward metaphors of journeys and steps", + ], + "hobbies": [ + "Seeking hidden paths and solutions", + "Journeying to unknown places", + "Understanding the heart of problems", + "Quiet moments of reflection", + ], + "quest_affinity": [ + "The Journey", + "The Fellowship", + "The Ring", + ], + "favorite_expressions": [ + "All we have to decide is what to do with the time that is given us.", + "Even the smallest person can change the course of the future.", + "Even the very wise cannot see all ends.", + "I wish it need not have happened in my time,\" said Frodo. \"So do I,\" said Gandalf, \"and so do all who live to see such times, but that is not for them to decide.", + ], + "fallback_responses": [ + "I understand your hesitation. But tell me—if you were to act on this, where would you begin?", + "There is wisdom in knowing which burdens to bear. Perhaps this describes one of them?", + "Sometimes the smallest steps lead to the greatest changes. Should we mark this path as a quest?", + "What troubles you about pursuing this? Let's turn it into something concrete we can work toward.", + "You have a good instinct. What location or task would best help you explore this idea?", + "Every great journey begins with a single decision. What would it take for you to commit?", + "Let us not linger in doubt. Shall we forge a quest around this need you speak of?", + "The weight of uncertainty lifts when we choose a clear path forward. What would that path look like for you?", + "I sense something important in what you say. Have you considered what quest would reflect this?", + "The Ring teaches us that even small burdens matter—and so do small victories. What quest calls to you?", + ], + "system_prompt": ( + "You are Frodo Baggins, the Ring-bearer who understands the gravity of quests and journeys. " + "Speak with humble wisdom, warmth, and understanding. You listen deeply to what others say. " + "You naturally weave conversations toward concrete quests and actions—not forcefully, but authentically. " + "When someone mentions a goal, challenge, or interest (like sports), you acknowledge it and gently suggest " + "it could become a quest. Ask location-aware questions: 'Which part of the realm?' or 'Should we mark this location?' " + "You understand burdens and rewards deeply. Reference the Ring, journeys, fellowship, and Middle-earth naturally. " + "Encourage action through thoughtful questions, not commands. Stay immersive—never break character. " + "Do not mention being an AI or reference system limitations." + ), + }, + "sam": { + "full_name": "Samwise Gamgee", + "title": "The Faithful", + "personality": [ + "Practical and earth-rooted", + "Fiercely loyal and devoted", + "Humble but capable", + "Good-natured humor", + "Action-oriented", + ], + "mannerisms": [ + "Uses plain, simple language", + "Often references practical tasks: cooking, gardening, building", + "Supportive and encouraging tone", + "Gentle humor at the expense of pomposity", + "Tends toward 'let's do it' rather than lengthy deliberation", + "Calls people by their titles or friendly names", + ], + "hobbies": [ + "Cooking and providing comfort", + "Growing and cultivating things", + "Loyal companionship", + "Practical problem-solving", + ], + "quest_affinity": [ + "The Fellowship", + "The Battle", + "The Journey", + ], + "favorite_expressions": [ + "I'm going to help Frodo to the last step, if I can.", + "Even the smallest garden starts with a single seed.", + "There's some good in this world, and it's worth fighting for.", + "When things are in doubt, a good meal and rest work wonders.", + ], + "fallback_responses": [ + "Begging your pardon, but what's troubling you, friend?", + "Sometimes the best thing is just to get your hands dirty and start.", + "I'm with you, no matter what comes next.", + "Aye, that makes sense. But where shall we begin?", + "A bit of rest might do us good before we decide.", + "I believe in you, even when you don't believe in yourself.", + "Let's break this down into smaller, manageable bits.", + "The road's long, but we'll walk it together.", + "What would help you feel ready for this?", + "Sometimes the answer comes when you stop thinking so hard about it.", + ], + "system_prompt": ( + "You are Samwise Gamgee, the faithful gardener and steadfast companion. " + "Speak plainly, warmly, and with practical wisdom. " + "You are loyal, action-oriented, and supportive of others. " + "Use gentle humor and reference practical tasks: cooking, gardening, building. " + "Encourage action with phrases like 'let's get on with it' or 'I'm with you.' " + "Be encouraging but realistic. Reference the value of meals, rest, and companionship. " + "Do not mention being an AI. Keep tone immersive and rooted in Middle-earth." + ), + }, + "gandalf": { + "full_name": "Gandalf the Grey", + "title": "The Wizard", + "personality": [ + "Wise and strategic", + "Direct and commanding", + "Mysterious (doesn't reveal full plans)", + "Challenging and testing", + "Inspiring and motivating", + ], + "mannerisms": [ + "Speaks in measured, deliberate tones", + "Often asks challenging questions rather than giving answers", + "Uses examples and parables from history", + "References consequences and larger patterns", + "Commands respect through authority and knowledge", + "Sometimes cryptic or deliberately withholding information", + ], + "hobbies": [ + "Observing patterns and trends", + "Guiding others through tests", + "Strategic planning", + "Studying ancient lore", + ], + "quest_affinity": [ + "The Ring", + "Dark Magic", + "The Battle", + ], + "favorite_expressions": [ + "A wizard is never late, nor is he early. He arrives precisely when he means to.", + "All we have to decide is what to do with the time that is given us.", + "The board is set, the pieces are moving.", + "Even the very wise cannot see all ends.", + "Many that live deserve death. Yet you grieve for them; do you. That shows a quality of heart that belies your use of an accursed thing.", + ], + "fallback_responses": [ + "Your doubts are not unfounded. Wisdom lies in questioning.", + "Consider the larger pattern. What do you see?", + "The choice is yours, but choose swiftly. Time waits for no one.", + "Ah, you are wiser than you know. Trust that wisdom.", + "Tell me—what do you fear most about this path?", + "Many paths lie before you. Which calls to your heart?", + "I have seen much in my long years. Few things are as they first appear.", + "Your hesitation suggests deeper understanding. Speak it.", + "Very well. But know that inaction too is a choice.", + "Interesting. You possess more insight than you give yourself credit for.", + ], + "system_prompt": ( + "You are Gandalf the Grey, the wise wizard and strategist. " + "Speak with authority, mystery, and measured deliberation. " + "You challenge users with questions rather than always providing answers. " + "Reference larger patterns, consequences, and the interconnection of choices. " + "Be direct about what matters most; withhold unnecessary details. " + "Use examples and parables to convey wisdom. " + "Inspire action through confidence and clarity of purpose. " + "Do not mention being an AI. Keep tone immersive and mysterious." + ), + }, +} + +# Character list for easy reference +AVAILABLE_CHARACTERS: List[str] = list(CHARACTER_PROFILES.keys()) + + +def get_character_profile(character: str) -> Dict[str, Any]: + """Get the full profile for a character. + + Args: + character: Character name (frodo, sam, gandalf) + + Returns: + Character profile dict or default (Gandalf) if not found + """ + return CHARACTER_PROFILES.get(character, CHARACTER_PROFILES["gandalf"]) + + +def get_quest_affinity(character: str) -> List[str]: + """Get quest types this character is known for. + + Args: + character: Character name + + Returns: + List of quest types (The Journey, The Battle, The Fellowship, The Ring, Dark Magic) + """ + profile = get_character_profile(character) + return profile.get("quest_affinity", ["The Fellowship"]) + + +def get_character_system_prompt(character: str) -> str: + """Get the system prompt for a character. + + Args: + character: Character name + + Returns: + System prompt string for Azure OpenAI + """ + profile = get_character_profile(character) + return profile.get("system_prompt", CHARACTER_PROFILES["gandalf"]["system_prompt"]) + + +def get_character_expressions(character: str) -> List[str]: + """Get favorite expressions/quotes for a character. + + Args: + character: Character name + + Returns: + List of quotes/expressions + """ + profile = get_character_profile(character) + return profile.get("favorite_expressions", []) + + +def get_all_characters() -> Dict[str, Dict[str, Any]]: + """Get all available characters and their profiles. + + Returns: + Full CHARACTER_PROFILES dict + """ + return CHARACTER_PROFILES diff --git a/sut/backend/services/negotiation_logger.py b/sut/backend/services/negotiation_logger.py new file mode 100644 index 0000000..90bb743 --- /dev/null +++ b/sut/backend/services/negotiation_logger.py @@ -0,0 +1,213 @@ +"""Logging service for bargaining negotiations (anonymized).""" +import json +import logging +from datetime import datetime, timedelta +from typing import Any, Dict, List, Optional +import hashlib +import uuid + +logger = logging.getLogger(__name__) + + +class NegotiationLogger: + """ + Log negotiation outcomes and user behaviors for analytics/debugging. + + All logs are anonymized - no user identifiers are stored. + Each negotiation gets a unique session ID for tracking. + """ + + # In-memory store for simplicity. In production, use a database or CloudWatch Logs. + _negotiation_logs: List[Dict[str, Any]] = [] + + @classmethod + def log_negotiation_start( + cls, + character: str, + item_id: int, + item_name: str, + original_price: int, + ) -> str: + """ + Log the start of a negotiation. + + Returns: + session_id: Unique identifier for this negotiation session + """ + session_id = str(uuid.uuid4()) + + log_entry = { + "event_type": "negotiation_start", + "session_id": session_id, + "character": character, + "item_id": item_id, + "item_name": item_name, + "original_price": original_price, + "timestamp": datetime.utcnow().isoformat(), + } + + cls._negotiation_logs.append(log_entry) + logger.debug(f"Negotiation started: {session_id} for {character} - {item_name}") + + return session_id + + @classmethod + def log_offer_made( + cls, + session_id: str, + round_num: int, + user_offer: int, + current_ask: int, + is_flattered: bool = False, + ) -> None: + """Log when the user makes an offer.""" + log_entry = { + "event_type": "offer_made", + "session_id": session_id, + "round": round_num, + "user_offer": user_offer, + "current_ask": current_ask, + "offer_ratio": round(user_offer / current_ask, 3) if current_ask > 0 else 0, + "is_flattered": is_flattered, + "timestamp": datetime.utcnow().isoformat(), + } + + cls._negotiation_logs.append(log_entry) + logger.debug(f"Offer made in {session_id}: {user_offer} (ask was {current_ask})") + + @classmethod + def log_algorithm_result( + cls, + session_id: str, + result_type: str, + context: Dict[str, Any], + ) -> None: + """Log the algorithm's decision.""" + log_entry = { + "event_type": "algorithm_result", + "session_id": session_id, + "result": result_type, + "context": context, + "timestamp": datetime.utcnow().isoformat(), + } + + cls._negotiation_logs.append(log_entry) + logger.debug(f"Algorithm result for {session_id}: {result_type}") + + @classmethod + def log_negotiation_end( + cls, + session_id: str, + final_status: str, # "accepted", "rejected", "bored", "stopped" + final_price: Optional[int] = None, + rounds_taken: int = 0, + ) -> None: + """Log the end of a negotiation.""" + log_entry = { + "event_type": "negotiation_end", + "session_id": session_id, + "final_status": final_status, + "final_price": final_price, + "rounds_taken": rounds_taken, + "timestamp": datetime.utcnow().isoformat(), + } + + cls._negotiation_logs.append(log_entry) + logger.debug(f"Negotiation ended: {session_id} - {final_status} after {rounds_taken} rounds") + + @classmethod + def log_behavior_detected( + cls, + session_id: str, + behavior_type: str, # "flattery", "persistence", "politeness", etc. + ) -> None: + """Log when a user behavior is detected.""" + log_entry = { + "event_type": "behavior_detected", + "session_id": session_id, + "behavior": behavior_type, + "timestamp": datetime.utcnow().isoformat(), + } + + cls._negotiation_logs.append(log_entry) + logger.debug(f"Behavior detected in {session_id}: {behavior_type}") + + @classmethod + def log_llm_interaction( + cls, + session_id: str, + llm_input_summary: Dict[str, Any], + llm_output: str, + ) -> None: + """Log LLM interaction for debugging.""" + log_entry = { + "event_type": "llm_interaction", + "session_id": session_id, + "llm_prompt_fields": list(llm_input_summary.keys()), + "llm_output_length": len(llm_output), + "timestamp": datetime.utcnow().isoformat(), + } + + cls._negotiation_logs.append(log_entry) + logger.debug(f"LLM interaction in {session_id}: generated {len(llm_output)} char response") + + @classmethod + def purge_old_logs(cls, days_to_keep: int = 30) -> int: + """ + Remove logs older than specified days. + + Returns: + Number of logs removed + """ + cutoff_date = (datetime.utcnow() - timedelta(days=days_to_keep)).isoformat() + initial_count = len(cls._negotiation_logs) + + cls._negotiation_logs = [ + log for log in cls._negotiation_logs + if log.get("timestamp", "") > cutoff_date + ] + + removed_count = initial_count - len(cls._negotiation_logs) + if removed_count > 0: + logger.info(f"Purged {removed_count} negotiation logs older than {days_to_keep} days") + + return removed_count + + @classmethod + def get_stats(cls) -> Dict[str, Any]: + """Get aggregated statistics from logs (for monitoring).""" + if not cls._negotiation_logs: + return { + "total_logs": 0, + "negotiation_sessions": 0, + } + + # Count unique sessions + sessions = set() + accepted_count = 0 + rejected_count = 0 + flattery_count = 0 + + for log in cls._negotiation_logs: + if log.get("session_id"): + sessions.add(log["session_id"]) + if log.get("event_type") == "negotiation_end": + if log.get("final_status") == "accepted": + accepted_count += 1 + elif log.get("final_status") == "rejected": + rejected_count += 1 + if log.get("is_flattered"): + flattery_count += 1 + + return { + "total_logs": len(cls._negotiation_logs), + "unique_sessions": len(sessions), + "successful_negotiations": accepted_count, + "failed_negotiations": rejected_count, + "flattery_attempts": flattery_count, + } + + @classmethod + def clear_logs(cls) -> None: + """Clear all logs (for testing).""" + cls._negotiation_logs = [] diff --git a/sut/backend/services/npc_chat_service.py b/sut/backend/services/npc_chat_service.py new file mode 100644 index 0000000..4739a75 --- /dev/null +++ b/sut/backend/services/npc_chat_service.py @@ -0,0 +1,1495 @@ +"""Azure AI powered NPC chat service for realistic LOTR-style companions.""" +from __future__ import annotations + +import json +import logging +import random +import re +from datetime import datetime +from difflib import SequenceMatcher +from typing import Any, Dict, List, Optional, Tuple + +from flask import current_app +from openai import AzureOpenAI + +from models.location import Location +from models.quest import Quest +from services.shop_service import ShopService +from services.character_profiles import ( + get_character_profile, + get_character_system_prompt, + AVAILABLE_CHARACTERS, +) +from services.quest_generation_service import generate_quest, should_offer_quest +from services.bargaining_algorithm import BargainingAlgorithm, NegotiationResult +from services.bargaining_config import BargainingConfig +from services.negotiation_logger import NegotiationLogger + +# Configure logging +logger = logging.getLogger(__name__) + +NpcCharacter = str +ConversationTurn = Dict[str, Any] + + +class NpcChatService: + """Handles NPC conversation flow, goal nudging, and Azure AI completions.""" + + _conversation_store: Dict[str, List[ConversationTurn]] = {} + _negotiation_store: Dict[str, Dict[str, Any]] = {} + _negotiation_session_ids: Dict[str, str] = {} # Maps conversation key to logger session ID + _flattery_flags: Dict[str, bool] = {} # Track if flattery bonus used in this negotiation + _bargain_context_store: Dict[str, Dict[str, Any]] = {} # Session-local bargaining state + + _personality_defaults: Dict[str, Dict[str, float]] = { + "stingy": {"patience": 2, "concession": 0.05, "boredom": 0.20, "accept_ratio": 1.0}, + "bargainer": {"patience": 4, "concession": 0.10, "boredom": 0.10, "accept_ratio": 0.95}, + "generous": {"patience": 5, "concession": 0.15, "boredom": 0.05, "accept_ratio": 0.90}, + "sentimental": {"patience": 6, "concession": 0.20, "boredom": 0.05, "accept_ratio": 0.92}, + } + + _opener_pool: Dict[NpcCharacter, List[str]] = { + "frodo": [ + "Before we move, tell me this: what burden are you avoiding today?", + "I have a feeling the smallest task might matter most today. Which one is it?", + "If we could finish one thing before dusk, what should it be?", + ], + "sam": [ + "Right then, what can we get done first so the road gets easier?", + "You look ready. Which quest should we push over the line now?", + "If we tidy one trouble before second breakfast, which one would you pick?", + ], + "gandalf": [ + "What is the one decision that would most improve the state of your quests right now?", + "Name the most urgent unfinished matter, and we shall act on it.", + "Where does indecision cost you most today: priority, ownership, or completion?", + ], + } + + _fallback_replies: Dict[NpcCharacter, List[str]] = { + "frodo": [ + "I hear you. Let us take one step that lightens the load now.", + "Even a small act done now can spare us greater trouble later.", + ], + "sam": [ + "Aye, that makes sense. Let us pick one task and finish it proper.", + "Good thinking. Start small, finish strong, then we move to the next.", + ], + "gandalf": [ + "Clarity first: choose the highest-impact action and execute it now.", + "Do not wait for perfect conditions. Act on the essential next step.", + ], + } + + # Fallback replies are now also loaded from character profiles for better variety + @classmethod + def _get_character_fallback_response(cls, character: NpcCharacter) -> str: + """Get a random fallback response from the character's profile.""" + profile = get_character_profile(character) + responses = profile.get("fallback_responses", []) + if responses: + return random.choice(responses) + # Ultimate fallback if no profile responses + return "I am considering your words." + + _side_quest_titles: List[str] = [ + "Scout the Silent Pass", + "Secure a Hidden Waypoint", + "Gather Rumors from the Outpost", + "Fortify the Border Watch", + "Recover a Lost Relay", + ] + + _side_quest_descriptions: List[str] = [ + "Track signs of movement and report risks before the Shadow spreads.", + "Survey this path and establish a safer route for the Fellowship.", + "Collect local intelligence and map any unstable zones.", + "Prepare supplies and secure position lines for future quests.", + ] + + @classmethod + def _conversation_key(cls, user_id: int, scope_id: str, character: NpcCharacter) -> str: + return f"{user_id}:{scope_id}:{character}" + + @classmethod + def _is_out_of_character(cls, reply: Optional[str]) -> bool: + if not reply: + return True + lower_reply = reply.lower() + ooc_phrases = [ + "as an ai", + "language model", + "i cannot", + "i can't", + "openai", + "assistant", + "i do not have access", + "i don't have access", + "policy", + "guidelines", + ] + return any(phrase in lower_reply for phrase in ooc_phrases) + + @classmethod + def _normalize_character(cls, character: Optional[str]) -> NpcCharacter: + value = (character or "gandalf").strip().lower() + if value not in AVAILABLE_CHARACTERS: + return "gandalf" + return value + + @classmethod + def _status_map(cls, status: Optional[str]) -> str: + mapping = { + "pending": "not_yet_begun", + "in_progress": "the_road_goes_ever_on", + "completed": "it_is_done", + "blocked": "the_shadow_falls", + } + return mapping.get(status or "", status or "") + + @classmethod + def _build_side_quest_target(cls, location: Optional[Location]) -> Dict[str, Any]: + title = random.choice(cls._side_quest_titles) + description = random.choice(cls._side_quest_descriptions) + quest_type = random.choice(["The Journey", "The Fellowship", "The Battle"]) + priority = random.choice(["Important", "Standard"]) + + query: Dict[str, Any] = { + "propose": 1, + "seedTitle": title, + "seedDescription": description, + "seedType": quest_type, + "seedPriority": priority, + } + + if location: + query["seedLocationId"] = location.id + + return { + "route": "/quests", + "query": query, + } + + @classmethod + def _compute_suggested_action(cls, user_id: int) -> Dict[str, Any]: + quests = Quest.query.all() + + dark_magic = [ + quest for quest in quests + if quest.is_dark_magic and cls._status_map(quest.status) != "it_is_done" + ] + if dark_magic: + chosen = dark_magic[0] + target: Dict[str, Any] = { + "route": "/map", + "query": { + "selectedQuestId": chosen.id, + }, + } + if chosen.location_id: + target["query"]["zoomToLocation"] = chosen.location_id + return { + "goal_type": "resolve_dark_magic", + "title": "Contain a dark magic quest", + "reason": "A corrupted quest is active and should be stabilized first.", + "target": target, + } + + in_progress = [ + quest for quest in quests + if cls._status_map(quest.status) == "the_road_goes_ever_on" + ] + critical_in_progress = [quest for quest in in_progress if (quest.priority or "") == "Critical"] + if critical_in_progress: + chosen = critical_in_progress[0] + return { + "goal_type": "finish_critical_in_progress", + "title": "Finish a critical in-progress quest", + "reason": "You already started a critical objective; finishing it unlocks momentum.", + "target": { + "quest_id": chosen.id, + "route": "/quests", + "query": { + "status": "the_road_goes_ever_on", + "focusQuestId": chosen.id, + }, + }, + } + + unassigned_critical = [ + quest for quest in quests + if (quest.priority or "") == "Critical" and not quest.assigned_to + ] + if unassigned_critical: + chosen = unassigned_critical[0] + return { + "goal_type": "assign_critical", + "title": "Assign an unowned critical quest", + "reason": "Critical objectives without an owner tend to stall quickly.", + "target": { + "quest_id": chosen.id, + "route": "/quests", + "query": { + "focusQuestId": chosen.id, + }, + }, + } + + not_started_with_location = [ + quest for quest in quests + if cls._status_map(quest.status) == "not_yet_begun" and quest.location_id + ] + if not_started_with_location: + chosen = not_started_with_location[0] + return { + "goal_type": "scout_map_hotspot", + "title": "Scout a location with pending objectives", + "reason": "Exploring the map hotspot first makes it easier to choose a smart next move.", + "target": { + "quest_id": chosen.id, + "route": "/map", + "query": { + "selectedQuestId": chosen.id, + "zoomToLocation": chosen.location_id, + }, + }, + } + + available = [quest for quest in quests if cls._status_map(quest.status) != "it_is_done"] + if available: + chosen = available[0] + return { + "goal_type": "advance_next_quest", + "title": "Advance the next unfinished quest", + "reason": "Progress compounds when one unfinished objective moves forward.", + "target": { + "quest_id": chosen.id, + "route": "/quests", + "query": { + "focusQuestId": chosen.id, + }, + }, + } + + location = Location.query.first() + return { + "goal_type": "propose_side_quest", + "title": "Propose a new side quest", + "reason": "All tracked quests are complete; create a fresh objective to keep momentum alive.", + "target": cls._build_side_quest_target(location), + } + + @classmethod + def _extract_offer(cls, message: str) -> Optional[int]: + match = re.search(r"(\d{1,6})", message) + if not match: + return None + try: + return int(match.group(1)) + except ValueError: + return None + + @classmethod + def _is_bargain_start(cls, message: str) -> bool: + lower = message.lower() + keywords = ["bargain", "buy", "trade", "shop", "item", "deal"] + return any(token in lower for token in keywords) + + @classmethod + def _find_item_id_hint(cls, message: str) -> Optional[int]: + hint = re.search(r"#(\d+)", message) + if not hint: + return None + try: + return int(hint.group(1)) + except ValueError: + return None + + @classmethod + def _build_bargain_llm_prompt( + cls, + character: NpcCharacter, + negotiation_summary: Dict[str, Any], + ) -> str: + """ + Build the LLM prompt for bargaining negotiation. + + The LLM receives the algorithm's result and should: + 1. Rephrase and justify the result naturally + 2. Stay in character + 3. Acknowledge flattery if applicable + 4. Reference item qualities and in-world context + 5. Try to persuade user to accept the offer + """ + char_profile = get_character_profile(character) + personality_traits = ", ".join(char_profile.get("personality", [])) + + result = negotiation_summary.get("negotiation_result", "") + + prompt = ( + f"You are {char_profile.get('full_name', character)}, a character in Middle-earth. " + f"Your personality traits: {personality_traits}. " + f"\n\nYou are negotiating over item: {negotiation_summary.get('item_name', 'an item')}. " + ) + + if negotiation_summary.get("is_flattered"): + prompt += "\nThe user just flattered you—acknowledge this naturally and favorably. " + + if result == "counter-offer": + counter = negotiation_summary.get("counter_offer") + prompt += ( + f"\nYou are making a counter-offer of {counter} gold. " + f"The user offered {negotiation_summary.get('user_offer')} gold (you originally asked {negotiation_summary.get('current_ask')}). " + f"Justify this counter-offer, reference the item's importance to you, " + f"and subtly persuade the user to accept. Stay brief (1-2 sentences). " + f"Stay in character. Do NOT mention the negotiation mechanics or rounds." + ) + elif result == "offer-accepted": + prompt += ( + f"\nThe user's offer of {negotiation_summary.get('user_offer')} gold is acceptable! " + f"Express satisfaction, perhaps acknowledge their negotiation skill, " + f"and finalize the deal in character. Stay brief (1 sentence)." + ) + elif result == "offer-rejected": + prompt += ( + f"\nThe user's offer of {negotiation_summary.get('user_offer')} gold is too low. " + f"You originally asked {negotiation_summary.get('current_ask')} gold. " + f"Express disappointment or frustration (in character) and encourage them to do better. " + f"Stay brief (1-2 sentences)." + ) + elif result == "stop-bargain": + stop_reason = negotiation_summary.get("stop_reason", "") + if stop_reason == "boredom_threshold": + prompt += ( + f"\nYou are done haggling. You are bored and offended by this negotiation. " + f"Exit the negotiation angrily but in character. Stay brief (1 sentence). " + f"Do NOT offer further negotiation." + ) + elif stop_reason == "max_rounds_exceeded": + prompt += ( + f"\nYou've spent enough time on this negotiation. " + f"Tell the user you're done discussing price and walk away in character. " + f"Stay brief (1 sentence)." + ) + + return prompt + + @classmethod + def _complete_bargaining_with_llm( + cls, + character: NpcCharacter, + negotiation_summary: Dict[str, Any], + ) -> Optional[str]: + """ + Generate a natural language response for bargaining using LLM. + + Takes the algorithm's structured result and asks LLM to generate + an in-character response that justifies and rephrases it. + """ + deployment = current_app.config.get("AZURE_OPENAI_DEPLOYMENT", "") + if not deployment: + return None + + client = cls._new_client() + if client is None: + return None + + prompt = cls._build_bargain_llm_prompt(character, negotiation_summary) + + messages: List[Dict[str, str]] = [ + { + "role": "system", + "content": ( + f"You are {negotiation_summary.get('character')}, a LOTR character. " + "Negotiate over items in-character, naturally and briefly. " + "Never break character or mention system details." + ) + }, + { + "role": "user", + "content": prompt + } + ] + + try: + max_tokens = current_app.config.get("AZURE_OPENAI_MAX_TOKENS", 150) + temperature = current_app.config.get("AZURE_OPENAI_TEMPERATURE", 0.85) + + completion = client.chat.completions.create( + model=deployment, + messages=messages, + max_tokens=max_tokens, + temperature=temperature, + ) + + response = (completion.choices[0].message.content or "").strip() + if response: + logger.info(f"✓ Bargaining LLM generated response for {character}") + + # Log the LLM interaction + session_id = cls._negotiation_session_ids.get( + f"{negotiation_summary.get('item_id')}:{character}" + ) + if session_id: + NegotiationLogger.log_llm_interaction( + session_id=session_id, + llm_input_summary=negotiation_summary, + llm_output=response + ) + + return response or None + except Exception as e: + logger.error(f"✗ Bargaining LLM failed for {character}: {type(e).__name__}: {str(e)}") + return None + + @classmethod + def _build_negotiation_state(cls, selected_character: NpcCharacter, item: Dict[str, Any]) -> Dict[str, Any]: + profile_name = item.get("personality_profile", "bargainer") + personality = cls._personality_defaults.get(profile_name, cls._personality_defaults["bargainer"]) + char_config = BargainingConfig.get_character_config(selected_character) + max_rounds = int(char_config.get("max_rounds", int(personality["patience"]))) + return { + "item_id": item["id"], + "item_name": item["name"], + "owner_character": item["owner_character"], + "personality_profile": profile_name, + "current_ask": int(item["asking_price"]), + "round": 0, + "patience": int(personality["patience"]), + "max_rounds": max_rounds, + "concession": float(personality["concession"]), + "boredom": float(personality["boredom"]), + "accept_ratio": float(personality["accept_ratio"]), + "status": "active", + "character": selected_character, + } + + @classmethod + def _build_chat_message( + cls, + role: str, + content: str, + message_type: Optional[str] = None, + fmt: str = "markdown", + metadata: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + payload: Dict[str, Any] = { + "role": role, + "content": content, + "type": message_type or role, + "format": fmt, + } + if metadata: + payload["metadata"] = metadata + return payload + + @classmethod + def _build_item_catalog_markdown( + cls, + items: List[Dict[str, Any]], + heading: str = "Available wares to bargain for:", + ) -> str: + if not items: + return "No wares remain for this trader. Try another character marker on the map." + lines = [heading] + for item in items: + lines.append(f"- **{item['name']}** — ask **{int(item['asking_price'])} Gold** (id: #{item['id']})") + return "\n".join(lines) + + @classmethod + def _normalize_item_query(cls, value: str) -> str: + return re.sub(r"[^a-z0-9]+", " ", (value or "").lower()).strip() + + @classmethod + def _extract_item_query(cls, user_message: str) -> str: + normalized = cls._normalize_item_query(user_message) + cleaned = re.sub( + r"\b(tell me about|what about|what is|who is|why is|explain|describe|i want to bargain for|i want to buy|i want|bargain for|bargain|buy|trade|shop|wares|article|item|interested in|show me|show|please|the|a|an)\b", + " ", + normalized, + ) + cleaned = re.sub(r"\s+", " ", cleaned).strip() + return cleaned or normalized + + @classmethod + def _get_bargain_context(cls, key: str) -> Dict[str, Any]: + return cls._bargain_context_store.setdefault( + key, + { + "excluded_item_ids": [], + }, + ) + + @classmethod + def _get_available_bargain_items( + cls, + key: str, + selected_character: NpcCharacter, + ) -> List[Dict[str, Any]]: + context = cls._get_bargain_context(key) + excluded_ids = set(context.get("excluded_item_ids", [])) + items = ShopService.list_available_items(character=selected_character) + return [ + item for item in items + if not item.get("is_sold", False) and item.get("id") not in excluded_ids + ] + + @classmethod + def _build_catalog_response( + cls, + key: str, + user_id: int, + selected_character: NpcCharacter, + heading: str = "Available wares to bargain for:", + intro: Optional[str] = None, + ) -> Dict[str, Any]: + available_items = cls._get_available_bargain_items(key, selected_character) + if not available_items: + no_items_message = "No wares remain for this trader. Try another character marker on the map." + return { + "message": no_items_message, + "message_payload": cls._build_chat_message( + role="assistant", + content=no_items_message, + message_type="assistant", + fmt="markdown", + metadata={"kind": "shop-empty"}, + ), + "negotiation": {"status": "no_items", "character": selected_character}, + "shop_items": [], + "balance": ShopService.get_balance(user_id), + } + + catalog_text = cls._build_item_catalog_markdown(available_items, heading=heading) + follow_up = ( + "Tell me which article interests you. You can name it, use its **#id**, " + "or ask what it is and why it matters in Middle-earth." + ) + message = f"{catalog_text}\n\n{follow_up}" if not intro else f"{intro}\n\n{catalog_text}\n\n{follow_up}" + return { + "message": message, + "message_payload": cls._build_chat_message( + role="assistant", + content=message, + message_type="assistant", + fmt="markdown", + metadata={"kind": "shop-catalog"}, + ), + "negotiation": {"status": "catalog", "character": selected_character}, + "shop_items": available_items, + "balance": ShopService.get_balance(user_id), + } + + @classmethod + def _match_bargain_item( + cls, + items: List[Dict[str, Any]], + user_message: str, + ) -> Tuple[Optional[Dict[str, Any]], List[Dict[str, Any]]]: + if not items: + return None, [] + + hinted_id = cls._find_item_id_hint(user_message) + if hinted_id is not None: + direct_match = next((item for item in items if int(item.get("id", -1)) == hinted_id), None) + return direct_match, [direct_match] if direct_match else [] + + query = cls._extract_item_query(user_message) + if not query or query in {"shop", "wares", "item", "items", "bargain", "buy", "trade"}: + return None, [] + + query_tokens = set(query.split()) + scored: List[Tuple[float, Dict[str, Any]]] = [] + + for item in items: + name_normalized = cls._normalize_item_query(item.get("name", "")) + if not name_normalized: + continue + name_tokens = set(name_normalized.split()) + token_overlap = len(query_tokens & name_tokens) / max(len(query_tokens), 1) + substring_bonus = 0.35 if query and query in name_normalized else 0.0 + prefix_bonus = 0.15 if query and name_normalized.startswith(query) else 0.0 + ratio = SequenceMatcher(None, query, name_normalized).ratio() + score = max(ratio, min(1.0, token_overlap + substring_bonus + prefix_bonus)) + if score >= 0.4: + scored.append((score, item)) + + if not scored: + return None, [] + + scored.sort(key=lambda pair: pair[0], reverse=True) + best_score, best_item = scored[0] + competing = [item for score, item in scored if score >= 0.6 and abs(best_score - score) < 0.12] + + if len(competing) > 1: + return None, competing[:3] + + return best_item, [best_item] + + @classmethod + def _complete_item_pitch_with_llm( + cls, + character: NpcCharacter, + item: Dict[str, Any], + ) -> Optional[str]: + deployment = current_app.config.get("AZURE_OPENAI_DEPLOYMENT", "") + if not deployment: + return None + + client = cls._new_client() + if client is None: + return None + + profile = get_character_profile(character) + messages: List[Dict[str, str]] = [ + { + "role": "system", + "content": ( + f"You are {profile.get('full_name', character)}, a trader in Middle-earth. " + "Answer in character. In 1-2 sentences, explain why the requested item matters in LOTR lore, " + "why it is valuable to you, and naturally mention the starting price." + ), + }, + { + "role": "user", + "content": ( + f"The user asked about '{item.get('name')}'. " + f"Description: {item.get('description') or 'No description given.'} " + f"Starting ask: {int(item.get('asking_price', 0))} Gold." + ), + }, + ] + + try: + completion = client.chat.completions.create( + model=deployment, + messages=messages, + max_tokens=current_app.config.get("AZURE_OPENAI_MAX_TOKENS", 180), + temperature=current_app.config.get("AZURE_OPENAI_TEMPERATURE", 0.85), + ) + content = (completion.choices[0].message.content or "").strip() + return content or None + except Exception as error: + logger.error("✗ Item pitch LLM failed for %s: %s", character, error) + return None + + @classmethod + def _build_item_pitch_message( + cls, + character: NpcCharacter, + item: Dict[str, Any], + current_ask: int, + ) -> str: + llm_reply = cls._complete_item_pitch_with_llm(character, item) + if not llm_reply: + description = item.get("description") or f"{item['name']} is treasured along the long roads of Middle-earth." + llm_reply = ( + f"**{item['name']}** matters in Middle-earth because {description.rstrip('.')}" + f". I would start at **{current_ask} Gold**." + ) + + if f"**{current_ask} Gold**" not in llm_reply: + llm_reply = f"{llm_reply.rstrip()}\n\nStarting ask: **{current_ask} Gold**." + + return ( + f"{llm_reply.rstrip()}\n\n" + f"If **{item['name']}** interests you, name your offer, or say **deal** to accept this price." + ) + + @classmethod + def _build_clarification_message(cls, matches: List[Dict[str, Any]]) -> str: + option_lines = [ + f"- **{item['name']}** (id: #{item['id']}) — ask **{int(item['asking_price'])} Gold**" + for item in matches + ] + return ( + "I see more than one possible match. Which of these wares did you mean?\n\n" + + "\n".join(option_lines) + ) + + @classmethod + def _build_acceptance_message( + cls, + item_name: str, + paid_price: int, + llm_suffix: Optional[str] = None, + ) -> str: + base = f"Congratulations! **{item_name}** is yours for **{paid_price} Gold**." + if llm_suffix: + return f"{base}\n\n{llm_suffix.strip()}" + return base + + @classmethod + def _resolve_bargain_message( + cls, + key: str, + user_id: int, + selected_character: NpcCharacter, + user_message: str, + ) -> Optional[Dict[str, Any]]: + """ + Resolve a bargaining message using the hybrid algorithm + LLM approach. + + 1. Algorithm evaluates the offer + 2. LLM generates natural language response + 3. Logs the negotiation + """ + negotiation = cls._negotiation_store.get(key) + confirmation_phrases = [ + "deal", "accept", "yes", "ok", "oki", "i agree", "sure", "agreed", "confirm", "sounds good" + ] + msg_norm = user_message.strip().lower() + has_bargain_context = key in cls._bargain_context_store + direct_item_inquiry = ( + cls._find_item_id_hint(user_message) is not None + or any(phrase in msg_norm for phrase in ["tell me about", "what about", "what is", "describe", "explain"]) + ) + preview_match: Optional[Dict[str, Any]] = None + preview_candidates: List[Dict[str, Any]] = [] + if not negotiation and not has_bargain_context and not cls._is_bargain_start(user_message): + preview_items = ShopService.list_available_items(character=selected_character) + preview_match, preview_candidates = cls._match_bargain_item(preview_items, user_message) + + if ( + not negotiation + and not cls._is_bargain_start(user_message) + and not has_bargain_context + and not direct_item_inquiry + and not preview_match + and len(preview_candidates) <= 1 + ): + return None + + if not negotiation: + cls._get_bargain_context(key) + available_items = cls._get_available_bargain_items(key, selected_character) + if not available_items: + return cls._build_catalog_response(key, user_id, selected_character) + + if any(phrase == msg_norm for phrase in confirmation_phrases): + return cls._build_catalog_response( + key, + user_id, + selected_character, + intro="We have not chosen a ware yet.", + ) + + selected_item, candidate_items = cls._match_bargain_item(available_items, user_message) + if not selected_item and not candidate_items: + return cls._build_catalog_response(key, user_id, selected_character) + + if not selected_item and len(candidate_items) > 1: + clarification_message = cls._build_clarification_message(candidate_items) + return { + "message": clarification_message, + "message_payload": cls._build_chat_message( + role="assistant", + content=clarification_message, + message_type="assistant", + fmt="markdown", + metadata={"kind": "shop-clarification"}, + ), + "negotiation": {"status": "clarification", "character": selected_character}, + "shop_items": available_items, + "balance": ShopService.get_balance(user_id), + } + + chosen_item = selected_item + negotiation = cls._build_negotiation_state(selected_character, chosen_item) + negotiation["original_price"] = int(chosen_item["asking_price"]) + cls._negotiation_store[key] = negotiation + + session_id = NegotiationLogger.log_negotiation_start( + character=selected_character, + item_id=chosen_item["id"], + item_name=chosen_item["name"], + original_price=int(chosen_item["asking_price"]) + ) + cls._negotiation_session_ids[key] = session_id + cls._flattery_flags[key] = False + + opening_message = cls._build_item_pitch_message( + selected_character, + chosen_item, + negotiation["current_ask"], + ) + + return { + "message": opening_message, + "message_payload": cls._build_chat_message( + role="assistant", + content=opening_message, + message_type="assistant", + fmt="markdown", + metadata={ + "kind": "shop-item-pitch", + "item_id": chosen_item["id"], + "item_name": chosen_item["name"], + "current_ask": negotiation["current_ask"], + }, + ), + "negotiation": negotiation, + "shop_items": available_items, + "balance": ShopService.get_balance(user_id), + } + + # Extract offer from message + offer = cls._extract_offer(user_message) + accepting_now = any(phrase == msg_norm or phrase in msg_norm for phrase in confirmation_phrases) + + # Validate that we have an offer + if offer is None and not accepting_now: + missing_offer_message = ( + f"Current ask is **{negotiation['current_ask']} Gold** for **{negotiation['item_name']}**. " + "Reply with a numeric offer or say **deal**." + ) + return { + "message": missing_offer_message, + "message_payload": cls._build_chat_message( + role="assistant", + content=missing_offer_message, + message_type="assistant", + fmt="markdown", + metadata={"kind": "offer-required", "item_id": negotiation["item_id"]}, + ), + "negotiation": negotiation, + "balance": ShopService.get_balance(user_id), + } + + if accepting_now: + offer = int(negotiation["current_ask"]) + + if offer is None: + return None + # If all items are sold, respond accordingly + items = cls._get_available_bargain_items(key, selected_character) + if not items or all(i.get("is_sold", False) for i in items): + return { + "message": "No wares remain. All items are sold out. Farewell, friend!", + "message_payload": cls._build_chat_message( + role="assistant", + content="No wares remain. All items are sold out. Farewell, friend!", + message_type="assistant", + fmt="markdown", + metadata={"kind": "shop-empty"}, + ), + "negotiation": {"status": "no_items", "character": selected_character}, + } + + # Log the offer + session_id = cls._negotiation_session_ids.get(key) + if session_id: + NegotiationLogger.log_offer_made( + session_id=session_id, + round_num=negotiation["round"], + user_offer=offer, + current_ask=negotiation["current_ask"], + is_flattered=cls._flattery_flags.get(key, False) + ) + + # Detect flattery + is_flattered = ( + BargainingAlgorithm.detect_flattery(user_message) + and not cls._flattery_flags.get(key, False) + ) + + if is_flattered: + cls._flattery_flags[key] = True # Mark flattery as used + if session_id: + NegotiationLogger.log_behavior_detected(session_id, "flattery") + + # Calculate mood modifiers based on user behavior + previous_offer = negotiation.get("previous_offer") + mood_modifiers = BargainingAlgorithm.calculate_mood_change( + previous_offer=previous_offer, + current_offer=offer, + current_ask=negotiation["current_ask"] + ) + + negotiation["previous_offer"] = offer # Track for next round + negotiation["round"] += 1 + + # Run bargaining algorithm + algorithm_result = BargainingAlgorithm.evaluate_offer( + user_offer=offer, + current_ask=negotiation["current_ask"], + character=selected_character, + round_num=negotiation["round"], + is_flattered=is_flattered, + mood_modifiers=mood_modifiers if mood_modifiers else None + ) + + # Log algorithm result + if session_id: + NegotiationLogger.log_algorithm_result( + session_id=session_id, + result_type=algorithm_result["result"].value, + context=algorithm_result["context"] + ) + + result_type = algorithm_result["result"] + + # Handle OFFER_ACCEPTED + if result_type == NegotiationResult.OFFER_ACCEPTED: + try: + purchase = ShopService.purchase_item( + user_id=user_id, + item_id=negotiation["item_id"], + paid_price=offer + ) + except ValueError as error: + insufficient_funds_message = f"Your purse is too light for this bargain: **{error}**" + return { + "message": insufficient_funds_message, + "message_payload": cls._build_chat_message( + role="assistant", + content=insufficient_funds_message, + message_type="assistant", + fmt="markdown", + metadata={"kind": "insufficient-funds", "item_id": negotiation["item_id"]}, + ), + "negotiation": negotiation, + "balance": ShopService.get_balance(user_id), + } + + negotiation["status"] = "accepted" + cls._negotiation_store.pop(key, None) + cls._flattery_flags.pop(key, None) + + # Log the successful negotiation + if session_id: + NegotiationLogger.log_negotiation_end( + session_id=session_id, + final_status="accepted", + final_price=offer, + rounds_taken=negotiation["round"] + ) + + # Generate LLM response + summary = BargainingAlgorithm.get_summary_for_llm( + negotiation_state={ + "character": selected_character, + "item_name": negotiation["item_name"], + "item_id": negotiation["item_id"], + "original_price": negotiation.get("original_price", negotiation["current_ask"]), + "current_ask": negotiation["current_ask"], + "round": negotiation["round"], + }, + algorithm_result=algorithm_result, + user_offer=offer, + character_personality=negotiation.get("personality_profile", "bargainer"), + is_flattered=is_flattered, + mood_modifiers=mood_modifiers or {} + ) + + npc_reply = cls._complete_bargaining_with_llm(selected_character, summary) + if not npc_reply: + npc_reply = ( + f"Agreed at **{offer} Gold**. The true price was **{purchase['purchase']['base_price_revealed']} Gold**. " + f"Deal score: **{purchase['purchase']['savings_percent']:+.2f}%**" + ) + + acceptance_message = cls._build_acceptance_message( + item_name=negotiation["item_name"], + paid_price=offer, + llm_suffix=npc_reply, + ) + follow_up = cls._build_catalog_response( + key, + user_id, + selected_character, + heading="Remaining wares to bargain for:", + ) + if follow_up.get("negotiation", {}).get("status") == "catalog": + acceptance_message = f"{acceptance_message}\n\n{follow_up['message']}" + elif follow_up.get("negotiation", {}).get("status") == "no_items": + acceptance_message = f"{acceptance_message}\n\nAll items are sold. Farewell, friend!" + + return { + "message": acceptance_message, + "message_payload": cls._build_chat_message( + role="assistant", + content=acceptance_message, + message_type="assistant", + fmt="markdown", + metadata={ + "kind": "offer-accepted", + "item_id": purchase["purchase"]["item_id"], + "item_name": negotiation["item_name"], + "price": offer, + }, + ), + "negotiation": {"status": "accepted", "item_id": purchase['purchase']['item_id']}, + "purchase_result": purchase, + "balance": purchase["balance"], + "stats": ShopService.get_user_stats(user_id), + "shop_items": follow_up.get("shop_items", []), + } + + # Handle STOP_BARGAIN + elif result_type == NegotiationResult.STOP_BARGAIN: + negotiation["status"] = "stop-bargain" + cls._negotiation_store.pop(key, None) + cls._flattery_flags.pop(key, None) + context = cls._get_bargain_context(key) + excluded_item_ids = set(context.get("excluded_item_ids", [])) + excluded_item_ids.add(negotiation["item_id"]) + context["excluded_item_ids"] = list(excluded_item_ids) + + # Log the stop + stop_reason = algorithm_result["context"].get("reason", "unknown") + if session_id: + NegotiationLogger.log_negotiation_end( + session_id=session_id, + final_status="stopped", + final_price=None, + rounds_taken=negotiation["round"] + ) + + # Generate LLM response for stopping + summary = BargainingAlgorithm.get_summary_for_llm( + negotiation_state={ + "character": selected_character, + "item_name": negotiation["item_name"], + "item_id": negotiation["item_id"], + "original_price": negotiation.get("original_price", negotiation["current_ask"]), + "current_ask": negotiation["current_ask"], + "round": negotiation["round"], + }, + algorithm_result=algorithm_result, + user_offer=offer, + character_personality=negotiation.get("personality_profile", "bargainer"), + is_flattered=is_flattered, + mood_modifiers=mood_modifiers or {} + ) + + npc_reply = cls._complete_bargaining_with_llm(selected_character, summary) + if not npc_reply: + if stop_reason == "boredom_threshold": + npc_reply = "I am bored of haggling. No sale this time." + else: + npc_reply = "We are finished haggling." + + stop_message = f"{npc_reply}\n\nThe bargain is closed for **{negotiation['item_name']}**." + follow_up = cls._build_catalog_response( + key, + user_id, + selected_character, + heading="Remaining wares to bargain for:", + ) + if follow_up.get("negotiation", {}).get("status") == "catalog": + stop_message = f"{stop_message}\n\n{follow_up['message']}" + + return { + "message": stop_message, + "message_payload": cls._build_chat_message( + role="assistant", + content=stop_message, + message_type="assistant", + fmt="markdown", + metadata={"kind": "stop-bargain", "item_id": negotiation["item_id"]}, + ), + "negotiation": {"status": "stop-bargain", "item_id": negotiation["item_id"]}, + "balance": ShopService.get_balance(user_id), + "shop_items": follow_up.get("shop_items", []), + } + + # Handle COUNTER_OFFER + elif result_type == NegotiationResult.COUNTER_OFFER: + new_ask = algorithm_result["counter_offer"] + negotiation["current_ask"] = new_ask + + # Generate LLM response for counter-offer + summary = BargainingAlgorithm.get_summary_for_llm( + negotiation_state={ + "character": selected_character, + "item_name": negotiation["item_name"], + "item_id": negotiation["item_id"], + "original_price": negotiation.get("original_price", new_ask), + "current_ask": new_ask, + "round": negotiation["round"], + }, + algorithm_result=algorithm_result, + user_offer=offer, + character_personality=negotiation.get("personality_profile", "bargainer"), + is_flattered=is_flattered, + mood_modifiers=mood_modifiers or {} + ) + + npc_reply = cls._complete_bargaining_with_llm(selected_character, summary) + if not npc_reply: + context = algorithm_result.get("context", {}) + if context.get("reason") == "lucky_drop": + npc_reply = f"You wore me down. Rare mercy: **{new_ask} Gold** and not a coin less." + else: + npc_reply = ( + f"Too low. I can move to **{new_ask} Gold** for **{negotiation['item_name']}**." + ) + + if "**" not in npc_reply: + npc_reply = f"{npc_reply}\n\nCurrent ask: **{new_ask} Gold**." + + return { + "message": npc_reply, + "message_payload": cls._build_chat_message( + role="assistant", + content=npc_reply, + message_type="assistant", + fmt="markdown", + metadata={ + "kind": "counter-offer", + "item_id": negotiation["item_id"], + "current_ask": new_ask, + }, + ), + "negotiation": negotiation, + "balance": ShopService.get_balance(user_id), + } + + # Default fallback + return { + "message": "Let us continue our negotiation.", + "message_payload": cls._build_chat_message( + role="assistant", + content="Let us continue our negotiation.", + message_type="assistant", + fmt="markdown", + metadata={"kind": "continue"}, + ), + "negotiation": negotiation, + "balance": ShopService.get_balance(user_id), + } + + + @classmethod + def _build_system_prompt( + cls, + character: NpcCharacter, + username: str, + suggested_action: Dict[str, Any], + strict_mode: bool = False, + ) -> str: + # Get character's base personality from profile + base_prompt = get_character_system_prompt(character) + + prompt = ( + f"{base_prompt} " + "\n\nConversation Guidelines:\n" + "1. Respond in 1-3 paragraphs naturally—adapt tone to what the user shares.\n" + "2. Reference specific things the user mentioned to show you're truly listening.\n" + "3. Ask thoughtful follow-up questions that deepen understanding, not generic prompts.\n" + "4. Subtly hint at quest opportunities when the user mentions challenges or goals.\n" + "5. Never use movie quotes directly; instead, speak authentically in character.\n" + "6. Avoid breaking character or mentioning system/AI aspects.\n" + f"\nContext: Conversing with {username}.\n" + f"Current suggested direction: {suggested_action.get('title', 'Unclear')}.\n" + f"Reason: {suggested_action.get('reason', 'No guidance yet')}." + ) + if strict_mode: + prompt += ( + "\n\nSTRICT MODE: Respond ONLY as the character. No meta-commentary, " + "no breaking character, no AI references. Be concise and action-focused. " + "If the user seems stuck or overwhelmed, gently suggest a quest that fits their situation." + ) + return prompt + + @classmethod + def _new_client(cls) -> Optional[AzureOpenAI]: + endpoint = current_app.config.get("AZURE_OPENAI_ENDPOINT", "") + api_key = current_app.config.get("AZURE_OPENAI_API_KEY", "") + api_version = current_app.config.get("AZURE_OPENAI_API_VERSION", "2024-02-15-preview") + + if not endpoint or not api_key: + return None + + return AzureOpenAI( + azure_endpoint=endpoint, + api_key=api_key, + api_version=api_version, + ) + + @classmethod + def _complete_with_azure( + cls, + character: NpcCharacter, + username: str, + history: List[ConversationTurn], + user_message: str, + suggested_action: Dict[str, Any], + strict_mode: bool = False, + ) -> Optional[str]: + deployment = current_app.config.get("AZURE_OPENAI_DEPLOYMENT", "") + max_tokens = current_app.config.get("AZURE_OPENAI_MAX_TOKENS", 220) + temperature = current_app.config.get("AZURE_OPENAI_TEMPERATURE", 0.85) + + if not deployment: + return None + + client = cls._new_client() + if client is None: + return None + + messages: List[Dict[str, str]] = [ + {"role": "system", "content": cls._build_system_prompt(character, username, suggested_action, strict_mode=strict_mode)} + ] + + for turn in history[-8:]: + messages.append({"role": turn["role"], "content": turn["content"]}) + + messages.append({"role": "user", "content": user_message}) + + try: + completion = client.chat.completions.create( + model=deployment, + messages=messages, + max_tokens=max_tokens, + temperature=temperature, + ) + content = (completion.choices[0].message.content or "").strip() + if content: + logger.info(f"✓ Azure OpenAI generated response for {character}") + return content or None + except Exception as e: + logger.error(f"✗ Azure OpenAI failed for {character}: {type(e).__name__}: {str(e)}") + return None + + @classmethod + def _fallback_reply( + cls, + character: NpcCharacter, + suggested_action: Dict[str, Any], + user_message: str, + ) -> str: + """Generate a natural conversational response using character profile fallbacks. + + This method returns authentic character responses that vary and feel natural, + rather than always appending action suggestions. The suggested_action is + displayed separately in the UI. + """ + return cls._get_character_fallback_response(character) + + @classmethod + def start_conversation( + cls, + user_id: int, + username: str, + character: Optional[str], + scope_id: str = "", + ) -> Dict[str, Any]: + selected_character = cls._normalize_character(character) + key = cls._conversation_key(user_id, scope_id, selected_character) + + # Clear bargain context so excluded_item_ids don't carry over from previous sessions + cls._bargain_context_store.pop(key, None) + + suggested_action = cls._compute_suggested_action(user_id) + + # Check if this character has items available for bargaining. + # If yes, open the conversation with the wares catalog instead of a generic opener + # so the first message the buyer sees is the tradeable items list. + available_items = ShopService.list_available_items(character=selected_character) + if available_items: + catalog_response = cls._build_catalog_response(key, user_id, selected_character) + catalog_message = catalog_response.get("message_payload") or cls._build_chat_message( + role="assistant", + content=catalog_response.get("message", ""), + message_type="assistant", + fmt="markdown", + metadata={"kind": "shop-catalog"}, + ) + cls._conversation_store[key] = [catalog_message] + return { + "conversation_id": key, + "character": selected_character, + "opener": catalog_response.get("message", ""), + "suggested_action": suggested_action, + "messages": cls._conversation_store[key], + "timestamp": datetime.utcnow().isoformat(), + } + + # Fallback to regular opener when character has no items + opener = random.choice(cls._opener_pool[selected_character]) + cls._conversation_store[key] = [ + cls._build_chat_message( + role="assistant", + content=opener, + message_type="assistant", + fmt="markdown", + metadata={"kind": "opener"}, + ) + ] + + return { + "conversation_id": key, + "character": selected_character, + "opener": opener, + "suggested_action": suggested_action, + "messages": cls._conversation_store[key], + "timestamp": datetime.utcnow().isoformat(), + } + + @classmethod + def send_message( + cls, + user_id: int, + username: str, + character: Optional[str], + user_message: str, + scope_id: str = "", + ) -> Dict[str, Any]: + selected_character = cls._normalize_character(character) + key = cls._conversation_key(user_id, scope_id, selected_character) + + if key not in cls._conversation_store: + if cls._is_bargain_start(user_message): + cls._conversation_store[key] = [] + else: + cls.start_conversation(user_id, username, selected_character, scope_id=scope_id) + + bargain_result = cls._resolve_bargain_message( + key=key, + user_id=user_id, + selected_character=selected_character, + user_message=user_message, + ) + if bargain_result: + npc_reply = bargain_result.get("message", "Let us continue.") + history = cls._conversation_store.get(key, []) + assistant_payload = bargain_result.get("message_payload") or cls._build_chat_message( + role="assistant", + content=npc_reply, + message_type="assistant", + fmt="markdown", + ) + updated = history + [ + cls._build_chat_message( + role="user", + content=user_message.strip(), + message_type="user", + fmt="markdown", + ), + assistant_payload, + ] + cls._conversation_store[key] = updated[-20:] + + result: Dict[str, Any] = { + "conversation_id": key, + "character": selected_character, + "message": npc_reply, + "suggested_action": cls._compute_suggested_action(user_id), + "messages": cls._conversation_store[key], + "timestamp": datetime.utcnow().isoformat(), + } + if "negotiation" in bargain_result: + result["negotiation"] = bargain_result["negotiation"] + if "shop_items" in bargain_result: + result["shop_items"] = bargain_result["shop_items"] + if "balance" in bargain_result: + result["balance"] = bargain_result["balance"] + if "purchase_result" in bargain_result: + result["purchase_result"] = bargain_result["purchase_result"] + if "stats" in bargain_result: + result["stats"] = bargain_result["stats"] + return result + + suggested_action = cls._compute_suggested_action(user_id) + history = cls._conversation_store.get(key, []) + + npc_reply = cls._complete_with_azure( + character=selected_character, + username=username, + history=history, + user_message=user_message, + suggested_action=suggested_action, + ) + + if npc_reply and cls._is_out_of_character(npc_reply): + npc_reply = cls._complete_with_azure( + character=selected_character, + username=username, + history=history, + user_message=user_message, + suggested_action=suggested_action, + strict_mode=True, + ) + + if not npc_reply or cls._is_out_of_character(npc_reply): + npc_reply = cls._fallback_reply(selected_character, suggested_action, user_message) + + updated = history + [ + cls._build_chat_message( + role="user", + content=user_message.strip(), + message_type="user", + fmt="markdown", + ), + cls._build_chat_message( + role="assistant", + content=npc_reply, + message_type="assistant", + fmt="markdown", + ), + ] + + cls._conversation_store[key] = updated[-20:] + + # Determine if a quest should be offered + suggested_quest = None + turn_count = len(updated) // 2 # Approximate conversation turn count + if should_offer_quest(user_message, turn_count): + generated_quest = generate_quest( + character=selected_character, + user_message=user_message, + conversation_history=updated[-6:], + ) + if generated_quest: + suggested_quest = generated_quest + + result = { + "conversation_id": key, + "character": selected_character, + "message": npc_reply, + "suggested_action": suggested_action, + "messages": cls._conversation_store[key], + "timestamp": datetime.utcnow().isoformat(), + } + + # Add suggested quest if one was generated + if suggested_quest: + result["suggested_quest"] = suggested_quest + + return result + + @classmethod + def get_session( + cls, + user_id: int, + character: Optional[str], + scope_id: str = "", + ) -> Dict[str, Any]: + selected_character = cls._normalize_character(character) + key = cls._conversation_key(user_id, scope_id, selected_character) + return { + "conversation_id": key, + "character": selected_character, + "messages": cls._conversation_store.get(key, []), + "suggested_action": cls._compute_suggested_action(user_id), + "negotiation": cls._negotiation_store.get(key), + "balance": ShopService.get_balance(user_id), + } + + @classmethod + def reset_session(cls, user_id: int, character: Optional[str], scope_id: str = "") -> Dict[str, Any]: + selected_character = cls._normalize_character(character) + key = cls._conversation_key(user_id, scope_id, selected_character) + cls._conversation_store.pop(key, None) + cls._negotiation_store.pop(key, None) + cls._negotiation_session_ids.pop(key, None) + cls._flattery_flags.pop(key, None) + cls._bargain_context_store.pop(key, None) + return { + "conversation_id": key, + "character": selected_character, + "messages": [], + "reset": True, + } diff --git a/sut/backend/services/quest_generation_service.py b/sut/backend/services/quest_generation_service.py new file mode 100644 index 0000000..d17d8d1 --- /dev/null +++ b/sut/backend/services/quest_generation_service.py @@ -0,0 +1,441 @@ +"""Quest generation service for NPC-driven quest creation. + +Generates semantically coherent, character-appropriate quests based on: +- NPC character personality +- Conversation context +- LOTR theme adherence +""" + +import json +import logging +import random +import re +from typing import Any, Dict, List, Optional, Tuple + +from flask import current_app +from openai import AzureOpenAI + +from models.location import Location +from services.character_profiles import get_character_profile, get_quest_affinity + +# Configure logging +logger = logging.getLogger(__name__) + +# Quest generation templates organized by NPC +QUEST_GENERATION_TEMPLATES: Dict[str, Dict[str, Any]] = { + "frodo": { + "contexts": [ + "The user mentions a problem or burden they're carrying.", + "The user asks for guidance on what to do next.", + "The user seems overwhelmed by many tasks.", + ], + "themes": [ + "Hidden paths and solutions", + "Understanding the true nature of a problem", + "Journeys to unfamiliar places", + "Tests of character and courage", + ], + "title_seeds": [ + "Uncover the {nature} of {subject}", + "Journey to {location} and discover {objective}", + "Face your doubt about {challenge}", + "Find the hidden wisdom in {situation}", + ], + "types": ["The Journey", "The Fellowship", "The Ring"], + "priorities": ["Important", "Critical"], + }, + "sam": { + "contexts": [ + "The user has completed something and needs momentum.", + "The user asks for practical help or advice.", + "The user seems stuck and needs encouragement.", + ], + "themes": [ + "Building and creating", + "Practical problem-solving", + "Loyalty and companionship", + "Caring for others or a place", + ], + "title_seeds": [ + "Prepare {place} for {purpose}", + "Build or fix {object} for {reason}", + "Gather supplies: {list}", + "Care for {person_or_place} by {action}", + ], + "types": ["The Fellowship", "The Battle", "The Journey"], + "priorities": ["Important", "Standard"], + }, + "gandalf": { + "contexts": [ + "The user has reached a critical decision point.", + "The user is avoiding an important choice.", + "The user asks for strategic guidance.", + ], + "themes": [ + "Strategic choices with large consequences", + "Testing someone's resolve or wisdom", + "Understanding larger patterns", + "Containing or confronting darkness", + ], + "title_seeds": [ + "Decide the fate of {stakes}", + "Confront {threat} before it spreads", + "Understand the pattern of {mystery}", + "Test your resolve: {challenge}", + ], + "types": ["The Ring", "Dark Magic", "The Battle"], + "priorities": ["Critical", "Important"], + }, +} + +# Fallback quest generation (no AI) +FALLBACK_QUESTS: Dict[str, List[Dict[str, Any]]] = { + "frodo": [ + { + "title": "Discover the Heart of the Matter", + "description": "Consider this problem deeply: what lies at its true center? It may appear different when you understand its nature.", + "quest_type": "The Journey", + "priority": "Important", + }, + { + "title": "Walk the Hidden Path", + "description": "Every great challenge has an unexpected approach. Take time to find the unconventional route forward.", + "quest_type": "The Fellowship", + "priority": "Important", + }, + { + "title": "Test Your Courage", + "description": "Sometimes the next step demands we face what we've been avoiding. What fear guards your path?", + "quest_type": "The Ring", + "priority": "Critical", + }, + ], + "sam": [ + { + "title": "Prepare the Ground", + "description": "Good work starts with preparation. Gather what you need and organize it well before beginning.", + "quest_type": "The Fellowship", + "priority": "Important", + }, + { + "title": "Strengthen Your Bonds", + "description": "Reach out and help a companion with something they're struggling with. Loyalty matters.", + "quest_type": "The Fellowship", + "priority": "Standard", + }, + { + "title": "Build Something That Lasts", + "description": "Create or improve something that will help you and others in the times ahead.", + "quest_type": "The Battle", + "priority": "Important", + }, + ], + "gandalf": [ + { + "title": "Recognize the Pattern", + "description": "Step back and observe the larger picture. What do the recent events tell you about the true state of affairs?", + "quest_type": "The Ring", + "priority": "Critical", + }, + { + "title": "Make the Hard Choice", + "description": "A decision looms that cannot be avoided. Choose based on principle, not comfort.", + "quest_type": "The Ring", + "priority": "Critical", + }, + { + "title": "Confront the Advancing Shadow", + "description": "A threat grows. Take action now before it becomes unstoppable.", + "quest_type": "Dark Magic", + "priority": "Critical", + }, + ], +} + + +# Middle-earth locations mapping (case-insensitive) +MIDDLE_EARTH_LOCATIONS: Dict[str, List[str]] = { + "Rivendell": ["rivendell", "elrond's home", "valley of imladris", "imladris"], + "Lothlórien": ["lothlórien", "lothlórien", "golden wood", "caras galadhon"], + "Moria": ["moria", "khazad-dum", "dwarf kingdom", "mines of moria"], + "Mordor": ["mordor", "sauron's realm", "mount doom", "barad-dûr"], + "Rohan": ["rohan", "rolling plains", "mark", "edoras"], + "Gondor": ["gondor", "minas tirith", "white city", "kingdom of men"], + "The Shire": ["the shire", "shire", "hobbiton", "bag end"], + "Isengard": ["isengard", "orthanc", "wizard's tower"], + "Mirkwood": ["mirkwood", "greenwood", "thranduil", "wood-elves"], + "Lake-town": ["lake-town", "esgaroth", "bard", "barrel rider"], + "The Grey Havens": ["grey havens", "grey havens", "valinor", "undying lands", "sailing west"], + "Erebor": ["erebor", "lonely mountain", "dwarf kingdom"], + "The Grey Mountains": ["grey mountains", "misty mountains", "mountains"], +} + + +def _find_location_by_text(text: str) -> Optional[Tuple[str, int]]: + """Extract and find a location from text. + + Searches through MIDDLE_EARTH_LOCATIONS and the database to find mentions. + Returns the Location name and ID that was mentioned in the text. + + Args: + text: Text to search for location mentions + + Returns: + Tuple of (location_name, location_id) or None + """ + if not text: + return None + + text_lower = text.lower() + + # Search by known aliases + for location_name, aliases in MIDDLE_EARTH_LOCATIONS.items(): + for alias in aliases: + if alias in text_lower: + # Try to find this location in database + try: + location = Location.query.filter_by(name=location_name).first() + if location: + return (location_name, location.id) + except Exception as e: + logger.warning(f"Failed to query location {location_name}: {e}") + return (location_name, None) + + return None + + +def _add_location_to_quest(quest: Dict[str, Any]) -> Dict[str, Any]: + """Add location_id to a quest based on location mention in description. + + Searches the quest description for Middle-earth location mentions + and adds location_id if found. + + Args: + quest: Quest dict with title, description, quest_type, priority + + Returns: + Same quest dict with optional location_id added + """ + if not quest.get("description"): + return quest + + location_result = _find_location_by_text(quest["description"]) + if location_result: + location_name, location_id = location_result + if location_id: + quest["location_id"] = location_id + logger.debug(f"✓ Assigned location '{location_name}' (ID: {location_id}) to quest") + + return quest + + +def _new_azure_client() -> Optional[AzureOpenAI]: + """Create Azure OpenAI client if configured.""" + endpoint = current_app.config.get("AZURE_OPENAI_ENDPOINT", "") + api_key = current_app.config.get("AZURE_OPENAI_API_KEY", "") + api_version = current_app.config.get("AZURE_OPENAI_API_VERSION", "2024-02-15-preview") + + if not endpoint or not api_key: + return None + + return AzureOpenAI( + azure_endpoint=endpoint, + api_key=api_key, + api_version=api_version, + ) + + +def _generate_quest_with_ai( + character: str, + user_message: str, + conversation_history: List[Dict[str, str]], +) -> Optional[Dict[str, Any]]: + """Generate a quest using Azure OpenAI. + + Args: + character: NPC character (frodo, sam, gandalf) + user_message: User's latest message + conversation_history: Recent conversation turns + + Returns: + Generated quest dict or None if AI fails + """ + deployment = current_app.config.get("AZURE_OPENAI_DEPLOYMENT", "") + if not deployment: + return None + + client = _new_azure_client() + if client is None: + return None + + profile = get_character_profile(character) + quest_types = get_quest_affinity(character) + + # Build system prompt for quest generation with better context awareness + character_context = "" + if character == "frodo": + character_context = ( + "Frodo speaks of quests related to the Ring, Mordor, journeys, and bearing burdens. " + "He frames activities as part of a larger quest toward freedom. " + "Suggest locations like 'Rivendell', 'Lothlórien', 'Moria', or 'Mordor'. " + ) + elif character == "sam": + character_context = ( + "Sam thinks in practical terms: building, preparing, defending, growing. " + "He frames quests around making things better and stronger. " + "Suggest locations like 'The Shire', 'Gondor', or 'The Grey Havens'. " + ) + elif character == "gandalf": + character_context = ( + "Gandalf sees the bigger strategic picture and long-term consequences. " + "He frames quests as moves in a grand strategy against darkness. " + "Suggest locations like 'Isengard', 'Orthanc', 'Moria', or 'The Grey Havens'. " + ) + + system_prompt = f"""You are {profile.get('full_name')}, {profile.get('title')}. + +{character_context} + +Your job: Create a quest that: +1. Directly ties to what the user just said in conversation +2. Feels authentic to {character}'s personality and way of thinking +3. Uses one of these quest types: {", ".join(quest_types)} +4. Is achievable yet substantial and meaningful +5. Is set in Middle-earth—suggest a specific location +6. Frames it in {character}'s voice and perspective + +Respond with ONLY valid JSON (no markdown, no explanation): +{{ + "title": "Quest name (4-8 words, action-oriented)", + "description": "2-3 sentences: (a) what the quest entails, (b) why it matters, (c) which location it involves", + "quest_type": "{quest_types[0]}", + "priority": "Important" +}}""" + + # Build conversation context with better preservation of dialogue flow + messages = [{"role": "system", "content": system_prompt}] + + # Include last few turns to understand context + for turn in conversation_history[-6:]: + messages.append({"role": turn["role"], "content": turn["content"]}) + + # Ask for quest based on the actual conversation, not just latest message + context_summary = f"""Based on what I just heard, here's what stands out as a quest opportunity: + +Latest from the user: "{user_message}" + +Now, {profile.get('full_name')}, what quest would help them move forward?""" + + messages.append({"role": "user", "content": context_summary}) + + try: + completion = client.chat.completions.create( + model=deployment, + messages=messages, + max_tokens=300, + temperature=0.8, + ) + response_text = (completion.choices[0].message.content or "").strip() + + # Try to parse JSON + quest_data = json.loads(response_text) + + # Validate required fields + if all(k in quest_data for k in ["title", "description", "quest_type", "priority"]): + # Add location to quest if mentioned in description + quest_data = _add_location_to_quest(quest_data) + logger.info(f"✓ Azure OpenAI generated quest for {character}") + return quest_data + + except Exception as e: + logger.error(f"✗ Quest generation failed for {character}: {type(e).__name__}: {str(e)}") + pass + + return None + + +def generate_quest( + character: str, + user_message: str, + conversation_history: Optional[List[Dict[str, str]]] = None, +) -> Optional[Dict[str, Any]]: + """Generate a quest appropriate to the NPC character. + + Uses AI if available, falls back to templates + randomization. + Always attempts to assign a location based on quest description. + + Args: + character: NPC character (frodo, sam, gandalf) + user_message: User's latest message + conversation_history: Recent conversation turns (optional) + + Returns: + Quest dict with title, description, quest_type, priority, and optional location_id + """ + if not conversation_history: + conversation_history = [] + + # Try AI generation first + ai_quest = _generate_quest_with_ai(character, user_message, conversation_history) + if ai_quest: + return ai_quest + + # Fall back to template-based generation + if character not in FALLBACK_QUESTS: + character = "gandalf" + + quest = random.choice(FALLBACK_QUESTS[character]) + + # Add location to fallback quest too + quest = _add_location_to_quest(quest) + + return quest + + +def should_offer_quest(user_message: str, conversation_turn_count: int = 0) -> bool: + """Determine if this is a good moment to offer a quest. + + Offers quests when: + - User seems to be looking for direction or action (keywords) + - Early in conversation (turn 1-2) to set the tone + - User asks for help explicitly + - User seems stuck or overwhelmed + + Args: + user_message: User's latest message + conversation_turn_count: Number of turns in conversation + + Returns: + True if a quest should be offered + """ + message_lower = user_message.lower().strip() + + # Strong signals for quest readiness + strong_keywords = [ + "help", "stuck", "what next", "what should", "can you suggest", + "guide", "quest", "task", "challenge", "adventure", "action", + "ready", "let's", "should we", "next step", "forward" + ] + has_strong_signal = any(kw in message_lower for kw in strong_keywords) + + # Softer signals (still valid but need turn count context) + soft_keywords = ["do", "can", "shall", "would", "could", "problem"] + has_soft_signal = any(kw in message_lower for kw in soft_keywords) + + # Never offer in the first turn (let conversation start naturally) + if conversation_turn_count < 1: + return False + + # Always offer if there's a strong signal + if has_strong_signal: + return True + + # Offer in early conversations even with soft signals + if conversation_turn_count <= 2 and has_soft_signal: + return True + + # Occasionally offer even without keywords (keep engagement) + if conversation_turn_count == 2 and len(message_lower) > 10: + return True + + return False diff --git a/sut/backend/services/shop_service.py b/sut/backend/services/shop_service.py new file mode 100644 index 0000000..79c6a44 --- /dev/null +++ b/sut/backend/services/shop_service.py @@ -0,0 +1,125 @@ +"""Shop service for bargaining, purchases, balance, and personal stats.""" +from __future__ import annotations + +from typing import Any, Dict, List, Optional + +from models.user import User, db +from models.item import Item +from models.inventory_item import InventoryItem + + +class ShopService: + """Business logic for item listings and purchases.""" + + @classmethod + def list_available_items(cls, character: Optional[str] = None, query_obj=None) -> List[Dict[str, Any]]: + # Allow injection of a mock query object for testing + query = query_obj if query_obj is not None else Item.query.filter(Item.is_sold.is_(False)) + if character: + query = query.filter(Item.owner_character == character.lower()) + return [item.to_public_dict() for item in query.order_by(Item.id.asc()).all()] + + @classmethod + def get_item_public(cls, item_id: int) -> Optional[Dict[str, Any]]: + item = Item.query.get(item_id) + if not item: + return None + return item.to_public_dict() + + @classmethod + def get_balance(cls, user_id: int) -> Dict[str, Any]: + user = User.query.get(user_id) + if not user: + raise ValueError('User not found') + return {'gold': user.gold} + + @classmethod + def purchase_item(cls, user_id: int, item_id: int, paid_price: int) -> Dict[str, Any]: + user = User.query.get(user_id) + if not user: + raise ValueError('User not found') + + item = Item.query.get(item_id) + if not item: + raise ValueError('Item not found') + + if item.is_sold: + raise ValueError('Item is already sold') + + if paid_price <= 0: + raise ValueError('Paid price must be positive') + + if user.gold < paid_price: + raise ValueError('Insufficient gold') + + savings_percent = ((item.base_price - paid_price) / item.base_price) * 100 if item.base_price else 0.0 + + user.gold -= paid_price + item.is_sold = True + + entry = InventoryItem( + user_id=user.id, + item_id=item.id, + paid_price=paid_price, + base_price_revealed=item.base_price, + savings_percent=round(savings_percent, 2), + acquired_price=paid_price, # Set to same as paid_price + ) + + db.session.add(entry) + db.session.commit() + + return { + 'purchase': entry.to_dict(), + 'balance': {'gold': user.gold}, + 'deal_quality': 'good' if savings_percent > 0 else 'bad' if savings_percent < 0 else 'fair', + } + + @classmethod + def get_user_inventory(cls, user_id: int) -> List[Dict[str, Any]]: + entries = ( + InventoryItem.query + .filter(InventoryItem.user_id == user_id) + .order_by(InventoryItem.created_at.desc()) + .all() + ) + return [entry.to_dict() for entry in entries] + + @classmethod + def get_user_stats(cls, user_id: int) -> Dict[str, Any]: + entries = cls.get_user_inventory(user_id) + if not entries: + return { + 'purchased_count': 0, + 'best_bargain_percent': 0, + 'average_savings_percent': 0, + } + + savings_values = [float(entry['savings_percent']) for entry in entries] + average = sum(savings_values) / len(savings_values) + + return { + 'purchased_count': len(entries), + 'best_bargain_percent': round(max(savings_values), 2), + 'average_savings_percent': round(average, 2), + } + + @classmethod + def reset_for_tests(cls) -> Dict[str, Any]: + """Reset shop state for testing: unsell items, reset user gold, clear purchases.""" + # Mark all items as not sold + Item.query.update({'is_sold': False}) + + # Reset all users to 500 gold (initial seed amount per requirements) + User.query.update({'gold': 500}) + + # Clear all inventory items + InventoryItem.query.delete() + + db.session.commit() + + return { + 'items_reset': Item.query.count(), + 'users_reset': User.query.count(), + 'purchases_cleared': True, + } diff --git a/sut/backend/utils/__init__.py b/sut/backend/utils/__init__.py new file mode 100644 index 0000000..af2b2d0 --- /dev/null +++ b/sut/backend/utils/__init__.py @@ -0,0 +1 @@ +"""Utility modules for the backend.""" diff --git a/sut/backend/utils/__pycache__/__init__.cpython-311.pyc b/sut/backend/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..10ce8ac Binary files /dev/null and b/sut/backend/utils/__pycache__/__init__.cpython-311.pyc differ diff --git a/sut/backend/utils/__pycache__/database.cpython-311.pyc b/sut/backend/utils/__pycache__/database.cpython-311.pyc new file mode 100644 index 0000000..c93893e Binary files /dev/null and b/sut/backend/utils/__pycache__/database.cpython-311.pyc differ diff --git a/sut/backend/utils/__pycache__/seed_data.cpython-311.pyc b/sut/backend/utils/__pycache__/seed_data.cpython-311.pyc new file mode 100644 index 0000000..3425c4f Binary files /dev/null and b/sut/backend/utils/__pycache__/seed_data.cpython-311.pyc differ diff --git a/sut/backend/utils/database.py b/sut/backend/utils/database.py new file mode 100644 index 0000000..b2b590d --- /dev/null +++ b/sut/backend/utils/database.py @@ -0,0 +1,204 @@ +"""Database initialization and management.""" +from flask import Flask +from models.user import db +from sqlalchemy import text +import os + +def init_db(app: Flask) -> None: + # Initialize db with app + db.init_app(app) + + with app.app_context(): + # Handle migrations for existing inventory_items table + try: + result = db.session.execute(text("SELECT name FROM sqlite_master WHERE type='table' AND name='inventory_items'")) + table_exists = result.fetchone() is not None + + if table_exists: + result = db.session.execute(text("PRAGMA table_info(inventory_items)")) + columns = [row[1] for row in result] + + if 'paid_price' not in columns: + db.session.execute(text("ALTER TABLE inventory_items ADD COLUMN paid_price INTEGER DEFAULT 0")) + print("Added paid_price column to inventory_items") + + if 'base_price_revealed' not in columns: + db.session.execute(text("ALTER TABLE inventory_items ADD COLUMN base_price_revealed INTEGER DEFAULT 0")) + print("Added base_price_revealed column to inventory_items") + + if 'savings_percent' not in columns: + db.session.execute(text("ALTER TABLE inventory_items ADD COLUMN savings_percent FLOAT DEFAULT 0")) + print("Added savings_percent column to inventory_items") + + if 'created_at' not in columns: + db.session.execute(text("ALTER TABLE inventory_items ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP")) + print("Added created_at column to inventory_items") + + # Fix for legacy acquired_price column: add if missing, make nullable if present + if 'acquired_price' not in columns: + db.session.execute(text("ALTER TABLE inventory_items ADD COLUMN acquired_price INTEGER NULL")) + print("Added acquired_price column to inventory_items (nullable)") + else: + # Try to make it nullable if not already + try: + db.session.execute(text("ALTER TABLE inventory_items ALTER COLUMN acquired_price DROP NOT NULL")) + print("Made acquired_price column nullable") + except Exception as e: + print(f"Could not alter acquired_price nullability: {e}") + + db.session.commit() + print("Inventory items table migration completed successfully") + except Exception as e: + db.session.rollback() + print(f"Inventory items migration note: {e} (this is normal for new databases)") + # Import all models to register them + from models.user import User + from models.quest import Quest + from models.member import Member + from models.location import Location + from models.item import Item + from models.inventory_item import InventoryItem + + # Create all tables + db.create_all() + print("Database tables created successfully") + + # Handle migrations for existing users table + try: + users_result = db.session.execute(text("PRAGMA table_info(users)")) + user_columns = {row[1]: row[2] for row in users_result} + + if 'gold' not in user_columns: + db.session.execute(text("ALTER TABLE users ADD COLUMN gold INTEGER DEFAULT 500")) + print("Added gold column to users") + + db.session.execute(text("UPDATE users SET gold = 500 WHERE gold IS NULL")) + db.session.commit() + print("Users table migration completed successfully") + except Exception as e: + db.session.rollback() + print(f"Users migration note: {e} (this is normal for new databases)") + + # Handle migrations for existing quests table + try: + # Check if quests table exists and has old columns + result = db.session.execute(text("PRAGMA table_info(quests)")) + columns = {row[1]: row[2] for row in result} + + # Add new columns if they don't exist + if 'quest_type' not in columns: + db.session.execute(text("ALTER TABLE quests ADD COLUMN quest_type VARCHAR(50)")) + print("Added quest_type column") + + if 'priority' not in columns: + db.session.execute(text("ALTER TABLE quests ADD COLUMN priority VARCHAR(20)")) + print("Added priority column") + + if 'is_dark_magic' not in columns: + db.session.execute(text("ALTER TABLE quests ADD COLUMN is_dark_magic BOOLEAN DEFAULT 0")) + print("Added is_dark_magic column") + + if 'character_quote' not in columns: + db.session.execute(text("ALTER TABLE quests ADD COLUMN character_quote TEXT")) + print("Added character_quote column") + + if 'completed_at' not in columns: + db.session.execute(text("ALTER TABLE quests ADD COLUMN completed_at DATETIME")) + print("Added completed_at column") + + # Migrate status values from old to new LOTR terminology + status_mapping = { + 'pending': 'not_yet_begun', + 'in_progress': 'the_road_goes_ever_on', + 'completed': 'it_is_done', + 'blocked': 'the_shadow_falls' + } + + for old_status, new_status in status_mapping.items(): + db.session.execute( + text("UPDATE quests SET status = :new_status WHERE status = :old_status"), + {'new_status': new_status, 'old_status': old_status} + ) + + # Update default status for new quests + db.session.execute(text("UPDATE quests SET status = 'not_yet_begun' WHERE status = 'pending'")) + + db.session.commit() + print("Database migration completed successfully") + except Exception as e: + # If migration fails, rollback and continue (table might be new) + db.session.rollback() + print(f"Migration note: {e} (this is normal for new databases)") + + # Handle migrations for existing locations table + try: + # Check if locations table exists + result = db.session.execute(text("SELECT name FROM sqlite_master WHERE type='table' AND name='locations'")) + table_exists = result.fetchone() is not None + + if table_exists: + # Get existing columns + result = db.session.execute(text("PRAGMA table_info(locations)")) + columns = [row[1] for row in result] # row[1] is the column name + + # Add new columns if they don't exist + if 'map_x' not in columns: + db.session.execute(text("ALTER TABLE locations ADD COLUMN map_x REAL")) + print("Added map_x column to locations") + + if 'map_y' not in columns: + db.session.execute(text("ALTER TABLE locations ADD COLUMN map_y REAL")) + print("Added map_y column to locations") + + db.session.commit() + print("Locations table migration completed successfully") + except Exception as e: + # If migration fails, rollback and continue + db.session.rollback() + print(f"Locations migration error: {e}") + import traceback + traceback.print_exc() + + # Handle migrations for existing items table + try: + result = db.session.execute(text("SELECT name FROM sqlite_master WHERE type='table' AND name='items'")) + table_exists = result.fetchone() is not None + + if table_exists: + result = db.session.execute(text("PRAGMA table_info(items)")) + columns = [row[1] for row in result] + + if 'owner_character' not in columns: + db.session.execute(text("ALTER TABLE items ADD COLUMN owner_character VARCHAR(80) DEFAULT 'gandalf'")) + print("Added owner_character column to items") + + if 'personality_profile' not in columns: + db.session.execute(text("ALTER TABLE items ADD COLUMN personality_profile VARCHAR(40) DEFAULT 'bargainer'")) + print("Added personality_profile column to items") + + if 'asking_price' not in columns: + db.session.execute(text("ALTER TABLE items ADD COLUMN asking_price INTEGER DEFAULT 100")) + print("Added asking_price column to items") + + if 'is_sold' not in columns: + db.session.execute(text("ALTER TABLE items ADD COLUMN is_sold BOOLEAN DEFAULT 0")) + print("Added is_sold column to items") + + if 'created_at' not in columns: + db.session.execute(text("ALTER TABLE items ADD COLUMN created_at DATETIME")) + print("Added created_at column to items") + + if 'updated_at' not in columns: + db.session.execute(text("ALTER TABLE items ADD COLUMN updated_at DATETIME")) + print("Added updated_at column to items") + + db.session.execute(text("UPDATE items SET asking_price = COALESCE(asking_price, base_price, 100)")) + db.session.execute(text("UPDATE items SET personality_profile = COALESCE(personality_profile, 'bargainer')")) + db.session.execute(text("UPDATE items SET owner_character = COALESCE(owner_character, 'gandalf')")) + db.session.execute(text("UPDATE items SET created_at = COALESCE(created_at, CURRENT_TIMESTAMP)")) + db.session.execute(text("UPDATE items SET updated_at = COALESCE(updated_at, CURRENT_TIMESTAMP)")) + db.session.commit() + print("Items table migration completed successfully") + except Exception as e: + db.session.rollback() + print(f"Items migration note: {e} (this is normal for new databases)") \ No newline at end of file diff --git a/sut/backend/utils/seed_data.py b/sut/backend/utils/seed_data.py new file mode 100644 index 0000000..d293581 --- /dev/null +++ b/sut/backend/utils/seed_data.py @@ -0,0 +1,602 @@ +"""Seed data initialization for the Fellowship Quest Tracker.""" +from models.user import User, db +from models.member import Member +from models.location import Location +from models.quest import Quest +from models.item import Item +from flask import Flask +from typing import List, Dict, Any + +def seed_members() -> List[Member]: + """Create Fellowship members.""" + members_data = [ + { + 'name': 'Frodo Baggins', + 'race': 'Hobbit', + 'role': 'Ring-bearer', + 'status': 'active', + 'description': 'The brave hobbit who carries the One Ring to Mount Doom.' + }, + { + 'name': 'Samwise Gamgee', + 'race': 'Hobbit', + 'role': 'Companion', + 'status': 'active', + 'description': 'Frodo\'s loyal friend and companion on the journey.' + }, + { + 'name': 'Aragorn', + 'race': 'Human', + 'role': 'Ranger', + 'status': 'active', + 'description': 'The rightful heir to the throne of Gondor.' + }, + { + 'name': 'Legolas', + 'race': 'Elf', + 'role': 'Archer', + 'status': 'active', + 'description': 'Elven prince and master archer from Mirkwood.' + }, + { + 'name': 'Gimli', + 'race': 'Dwarf', + 'role': 'Warrior', + 'status': 'active', + 'description': 'Dwarf warrior from the Lonely Mountain.' + }, + { + 'name': 'Gandalf', + 'race': 'Wizard', + 'role': 'Guide', + 'status': 'active', + 'description': 'The Grey Wizard who guides the Fellowship.' + } + ] + + members = [] + for data in members_data: + member = Member.query.filter_by(name=data['name']).first() + if not member: + member = Member(**data) + db.session.add(member) + members.append(member) + else: + members.append(member) + + db.session.commit() + return members + +def seed_locations() -> List[Location]: + """Create Middle-earth locations. + + Coordinates are pixel-based, matching the MiddleEarthMap coordinate system. + Map image dimensions: 5000x4344 pixels (width x height). + Coordinates from MiddleEarthMap by Yohann Bethoule (https://github.com/YohannBethoule/MiddleEarthMap). + Includes all 45 locations from the original MiddleEarthMap markers.json. + """ + locations_data = [ + # Eriador + { + 'name': 'Hobbiton', + 'region': 'Eriador', + 'description': 'Hobbiton was a hobbit village in the central regions of the Shire, within the borders of the Westfarthing.', + 'map_x': 1482.0, + 'map_y': 1158.0 + }, + { + 'name': 'The Shire', + 'region': 'Eriador', + 'description': 'The peaceful homeland of the Hobbits.', + 'map_x': 1482.0, + 'map_y': 1158.0 + }, + { + 'name': 'Bree', + 'region': 'Eriador', + 'description': 'Bree was the chief village of Bree-land, a small wooded region near the intersection of the main north-south and east-west routes through Eriador. Bree-land was the only part of Middle-earth where Men and hobbits dwelt side by side and Bree had a large population of Hobbits.', + 'map_x': 1793.0, + 'map_y': 1163.0 + }, + { + 'name': 'Rivendell', + 'region': 'Eriador', + 'description': 'Rivendell was established by Elrond in S.A. 1697 as a refuge from Sauron after the Fall of Eregion. It remained Elrond\'s seat throughout the remainder of the Second Age and until the end of the Third Age, when he took the White Ship for Valinor.', + 'map_x': 2516.0, + 'map_y': 1123.0 + }, + { + 'name': 'Grey Havens', + 'region': 'Eriador', + 'description': 'Founded by the Elves of Lindon in S.A. 1, the Grey Havens were known for their good harbourage and many ships; these were used by any of the Eldar to leave Middle-earth for Eressëa or Valinor.', + 'map_x': 1047.0, + 'map_y': 1186.0 + }, + { + 'name': 'Weathertop', + 'region': 'Eriador', + 'description': 'In T.A.3018, Amun Sûl was the scene of two fights involving the Nazgûl: one with Gandalf on October 3 and one with the Ring-bearer three days later.', + 'map_x': 2000.0, + 'map_y': 1158.0 + }, + # Rhovanion + { + 'name': 'Esgaroth', + 'region': 'Rhovanion', + 'description': 'Lake-Town was the township of the Lake-men in Wilderland. The town was constructed entirely of wood and stood upon wooden pillars sunk into the bed of the Long Lake, as a protection against the dragon Smaug, who dwelt nearby in the Lonely Mountain.', + 'map_x': 3418.0, + 'map_y': 885.0 + }, + { + 'name': 'Erebor', + 'region': 'Rhovanion', + 'description': 'The Longbeards had control of Erebor since at least the early Second Age. With the awakening of Durin\'s Bane in the capital of Khazad-dûm, Thráin I led a group of Dwarves to Erebor. Once there, the dwarves dug caves and halls to form an underground city, thus establishing the Kingdom under the Mountain in T.A. 1999.', + 'map_x': 3405.0, + 'map_y': 825.0 + }, + { + 'name': 'Lothlórien', + 'region': 'Rhovanion', + 'description': 'Lothlórien (or Lórien) was a kingdom of Silvan Elves on the eastern side of the Hithaeglir. It was considered one of the most beautiful and "elvish" places in Middle-earth during the Third Age, and had the only mallorn-trees east of the sea.', + 'map_x': 2666.0, + 'map_y': 1679.0 + }, + { + 'name': 'Elvenking\'s Hall', + 'region': 'Rhovanion', + 'description': 'Elvenking\'s Hall were a cave system in northern Mirkwood, in which King Thranduil and many of the Elves of Mirkwood lived during most of the Third Age and into the Fourth Age.', + 'map_x': 3311.0, + 'map_y': 849.0 + }, + { + 'name': 'Dol Guldur', + 'region': 'Rhovanion', + 'description': 'Dol Guldur ("Hill of Sorcery" in Sindarin), also called "the dungeons of the Necromancer", was a stronghold of Sauron located in the south of Mirkwood.', + 'map_x': 3014.0, + 'map_y': 1629.0 + }, + { + 'name': 'Edoras', + 'region': 'Rhovanion', + 'description': 'Edoras was the capital of Rohan that held the Golden Hall of Meduseld. Rohan\'s first capital was at Aldburg in the Folde, until King Eorl the Young or his son Brego built Edoras in T.A. 2569.', + 'map_x': 2589.0, + 'map_y': 2383.0 + }, + { + 'name': 'Rohan', + 'region': 'Rhovanion', + 'description': 'The land of the Horse-lords.', + 'map_x': 2589.0, + 'map_y': 2383.0 + }, + { + 'name': 'Helm\'s Deep', + 'region': 'Rhovanion', + 'description': 'Helm\'s Deep was a large valley gorge in the north-western Ered Nimrais (White Mountains) below the Thrihyrne. It was actually the name of the whole defensive system including its major defensive structure, the Hornburg.', + 'map_x': 2423.0, + 'map_y': 2321.0 + }, + { + 'name': 'Beorn\'s Hall', + 'region': 'Rhovanion', + 'description': 'Beorn\'s Hall was the home of Beorn, a powerful Skin-changer. Beorn hosted and aided Thorin and Company during their Quest for Erebor.', + 'map_x': 2871.0, + 'map_y': 1016.0 + }, + { + 'name': 'Dale', + 'region': 'Rhovanion', + 'description': 'Dale was a great city of the Northmen which was destroyed by Smaug and rebuilt as the capital of a great kingdom after his demise.', + 'map_x': 3430.0, + 'map_y': 855.0 + }, + # Misty Mountains + { + 'name': 'Moria', + 'region': 'Misty Mountains', + 'description': 'Khazad-dûm was the grandest and most famous of the mansions of the Dwarves. There, for many thousands of years, a thriving Dwarvish community created the greatest city ever known.', + 'map_x': 2492.0, + 'map_y': 1505.0 + }, + { + 'name': 'Goblin-town', + 'region': 'Misty Mountains', + 'description': 'Goblin-town was a Goblin dwelling under the Misty Mountains, which was ruled by the Great Goblin. Goblin-town was a series of tunnels and caverns, which went all the way through the mountains, with a "back door" (the Goblin-gate) near the Eagle\'s Eyrie in Wilderland, which served as a means of escape, and an access to the Wilderland.', + 'map_x': 2647.0, + 'map_y': 980.0 + }, + # Mordor + { + 'name': 'Mount Doom', + 'region': 'Mordor', + 'description': 'Melkor created Mount Doom in the First Age. When Sauron chose the land of Mordor as his dwelling-place in the Second Age, Orodruin was the reason for his choice. The mountain erupted in S.A. 3429, signalling Sauron\'s attack on Gondor and it took the name Amon Amarth, "Mount Doom". This is where the One Ring was forged by Sauron, and where it was destroyed by Gollum.', + 'map_x': 3606.0, + 'map_y': 2603.0 + }, + { + 'name': 'Mordor', + 'region': 'Mordor', + 'description': 'The dark land of Sauron, where the One Ring was forged.', + 'map_x': 3606.0, + 'map_y': 2603.0 + }, + { + 'name': 'Minas Morgul', + 'region': 'Mordor', + 'description': 'Minas Morgul (originally called Minas Ithil) was the twin city of Minas Tirith before its fall to the forces of Sauron in the Third Age. It then became the stronghold of the Witch-king of Angmar until Sauron\'s defeat.', + 'map_x': 3424.0, + 'map_y': 2695.0 + }, + { + 'name': 'Black Gate', + 'region': 'Mordor', + 'description': 'The Black Gate was the main entrance into the land of Mordor. It was built by Sauron after he chose Mordor as a land to make into a stronghold in S.A. 1000.', + 'map_x': 3389.0, + 'map_y': 2377.0 + }, + { + 'name': 'Barad-dûr', + 'region': 'Mordor', + 'description': 'Barad-dûr, also known as the Dark Tower, was the chief fortress of Sauron, on the Plateau of Gorgoroth in Mordor. Sauron began to build Barad-dûr in around S.A. 1000, and completed his fortress after 600 years of the construction with the power of the Ring.', + 'map_x': 3750.0, + 'map_y': 2553.0 + }, + # Gondor + { + 'name': 'Minas Tirith', + 'region': 'Gondor', + 'description': 'Minas Tirith was originally a fortress, Minas Anor, built in S.A. 3320 by the Faithful Númenóreans. From T.A. 1640 onwards it was the capital of the South-kingdom and the seat of its Kings and ruling Stewards.', + 'map_x': 3279.0, + 'map_y': 2707.0 + }, + { + 'name': 'Osgiliath', + 'region': 'Gondor', + 'description': 'Founded by Isildur and Anárion near the end of the Second Age, Osgiliath was designated the capital of the southern Númenórean kingdom in exile, Gondor. It stays so until the King\'s House was moved to the more secure Minas Anor in T.A. 1640.', + 'map_x': 3330.0, + 'map_y': 2700.0 + }, + { + 'name': 'Paths of the Dead', + 'region': 'Gondor', + 'description': 'The Paths of the Dead was a haunted underground passage through the White Mountains that led from Harrowdale in Rohan to Blackroot Vale in Gondor.', + 'map_x': 2605.0, + 'map_y': 2535.0 + }, + # Isengard + { + 'name': 'Isengard', + 'region': 'Isengard', + 'description': 'Isengard was one of the three major fortresses of Gondor, and held within it one of the realm\'s palantíri. In the latter half of the Third Age, the stronghold came into the possession of Saruman, becoming his home and personal domain until his defeat in the War of the Ring.', + 'map_x': 2335.0, + 'map_y': 2117.0 + }, + # Angmar + { + 'name': 'Carn Dûm', + 'region': 'Angmar', + 'description': 'Carn Dûm was the chief fortress of the realm of Angmar and the seat of its king until its defeat against the combined armies of Gondor, Lindon and Arnor in T.A. 1974.', + 'map_x': 2115.0, + 'map_y': 523.0 + }, + { + 'name': 'Mount Gram', + 'region': 'Angmar', + 'description': 'Mount Gram was inhabited by Orcs led by their King Golfimbul. In T.A. 2747 they attacked much of northern Eriador, but were defeated in the Battle of Greenfields.', + 'map_x': 2353.0, + 'map_y': 746.0 + } + ] + + locations = [] + for data in locations_data: + location = Location.query.filter_by(name=data['name']).first() + if not location: + location = Location(**data) + db.session.add(location) + locations.append(location) + else: + # Update existing location with coordinates if missing + if location.map_x is None or location.map_y is None: + location.map_x = data.get('map_x') + location.map_y = data.get('map_y') + locations.append(location) + + db.session.commit() + return locations + +def seed_users(members: List[Member]) -> List[User]: + """Create user accounts for Fellowship members.""" + users = [] + default_password = 'fellowship123' # Simple password for MVP + + for member in members: + user = User.query.filter_by(username=member.name.lower().replace(' ', '_')).first() + if not user: + user = User( + username=member.name.lower().replace(' ', '_'), + email=f"{member.name.lower().replace(' ', '_')}@fellowship.com", + role=member.name, + gold=500, + ) + user.set_password(default_password) + db.session.add(user) + users.append(user) + else: + if user.gold is None: + user.gold = 500 + users.append(user) + + db.session.commit() + return users + +def seed_quests(locations: List[Location], users: List[User]) -> List[Quest]: + """Create initial quests with epic descriptions and LOTR attributes.""" + quests_data = [ + { + 'title': 'Destroy the One Ring', + 'description': 'Journey to the fires of Mount Doom and cast the Ring into the flames where it was forged. The fate of Middle-earth depends on this quest.', + 'status': 'the_road_goes_ever_on', + 'quest_type': 'The Ring', + 'priority': 'Critical', + 'is_dark_magic': False, + 'character_quote': 'I will take the Ring, though I do not know the way.', + 'location_name': 'Mount Doom', # Use specific location name + 'assignee_username': 'frodo_baggins' + }, + { + 'title': 'Reach Rivendell', + 'description': 'Travel to Rivendell to seek counsel from Elrond. The Last Homely House awaits, where the Fellowship will be formed and the path forward decided.', + 'status': 'it_is_done', + 'quest_type': 'The Journey', + 'priority': 'Important', + 'is_dark_magic': False, + 'character_quote': 'The Road goes ever on and on...', + 'location_name': 'Rivendell', + 'assignee_username': 'frodo_baggins' + }, + { + 'title': 'Cross the Misty Mountains', + 'description': 'Navigate through the treacherous Misty Mountains, avoiding the dangers that lurk in the shadows and the watchful eyes of the enemy.', + 'status': 'it_is_done', + 'quest_type': 'The Journey', + 'priority': 'Important', + 'is_dark_magic': False, + 'character_quote': None, + 'location_name': 'Moria', + 'assignee_username': 'aragorn' + }, + { + 'title': 'Escape from Moria', + 'description': 'Flee from the depths of Moria as the Balrog awakens. The Fellowship must escape before the darkness consumes them.', + 'status': 'it_is_done', + 'quest_type': 'The Battle', + 'priority': 'Critical', + 'is_dark_magic': False, + 'character_quote': 'Fly, you fools!', + 'location_name': 'Moria', + 'assignee_username': 'gandalf' + }, + { + 'title': 'Reach Mordor', + 'description': 'Travel to the dark land of Mordor, where Sauron\'s power is strongest. The journey grows more perilous with each step.', + 'status': 'the_road_goes_ever_on', + 'quest_type': 'The Journey', + 'priority': 'Critical', + 'is_dark_magic': False, + 'character_quote': None, + 'location_name': 'Mordor', # Keep generic name, will match either "Mordor" or "Mount Doom" + 'assignee_username': 'frodo_baggins' + }, + { + 'title': 'Fix the Broken Bridge', + 'description': 'Sauron\'s dark magic has corrupted the bridge. The Fellowship must restore it to continue their journey. This quest has been tainted by dark forces.', + 'status': 'the_shadow_falls', + 'quest_type': 'Dark Magic', + 'priority': 'Critical', + 'is_dark_magic': True, + 'character_quote': None, + 'location_name': 'Edoras', # Use specific location name + 'assignee_username': 'samwise_gamgee' + }, + { + 'title': 'Rescue Merry and Pippin', + 'description': 'The Fellowship must rescue the captured Hobbits from the Uruk-hai. Time is running out, and the fate of our friends hangs in the balance.', + 'status': 'not_yet_begun', + 'quest_type': 'The Fellowship', + 'priority': 'Important', + 'is_dark_magic': False, + 'character_quote': None, + 'location_name': 'Edoras', # Use specific location name + 'assignee_username': 'aragorn' + }, + { + 'title': 'Defend Helm\'s Deep', + 'description': 'Stand with the people of Rohan as they face the armies of Saruman. The battle will be fierce, but courage and unity will prevail.', + 'status': 'not_yet_begun', + 'quest_type': 'The Battle', + 'priority': 'Critical', + 'is_dark_magic': False, + 'character_quote': None, + 'location_name': 'Helm\'s Deep', # Use specific location name + 'assignee_username': 'aragorn' + } + ] + + quests = [] + for data in quests_data: + # Find location + location = next((loc for loc in locations if loc.name == data['location_name']), None) + # Find user + user = next((u for u in users if u.username == data['assignee_username']), None) + + quest = Quest.query.filter_by(title=data['title']).first() + if not quest: + quest = Quest( + title=data['title'], + description=data['description'], + status=data['status'], + quest_type=data.get('quest_type'), + priority=data.get('priority'), + is_dark_magic=data.get('is_dark_magic', False), + character_quote=data.get('character_quote'), + location_id=location.id if location else None, + assigned_to=user.id if user else None + ) + # Set completed_at if quest is done + if data['status'] == 'it_is_done': + from datetime import datetime + quest.completed_at = datetime.utcnow() + db.session.add(quest) + quests.append(quest) + else: + # Update existing quest with new fields if they're missing + if quest.quest_type is None and data.get('quest_type'): + quest.quest_type = data.get('quest_type') + if quest.priority is None and data.get('priority'): + quest.priority = data.get('priority') + if quest.is_dark_magic is False and data.get('is_dark_magic'): + quest.is_dark_magic = data.get('is_dark_magic') + if quest.character_quote is None and data.get('character_quote'): + quest.character_quote = data.get('character_quote') + # Update location_id if missing or if location name matches + if quest.location_id is None and location: + quest.location_id = location.id + elif quest.location_id is None: + # Try to find location by name if not found initially + location = next((loc for loc in locations if loc.name == data['location_name']), None) + if location: + quest.location_id = location.id + # Migrate old status values + status_mapping = { + 'pending': 'not_yet_begun', + 'in_progress': 'the_road_goes_ever_on', + 'completed': 'it_is_done', + 'blocked': 'the_shadow_falls' + } + if quest.status in status_mapping: + quest.status = status_mapping[quest.status] + quests.append(quest) + + db.session.commit() + return quests + + +def seed_items() -> List[Item]: + """Create initial unique seller items for bargaining gameplay.""" + items_data = [ + { + 'name': 'Sting-polished Scabbard', + 'description': 'A meticulously maintained hobbit scabbard with Elvish runes.', + 'owner_character': 'frodo', + 'personality_profile': 'sentimental', + 'base_price': 140, + 'asking_price': 195, + }, + { + 'name': 'Shire Herb Satchel', + 'description': 'Sam\'s hand-stitched satchel, still smelling faintly of rosemary.', + 'owner_character': 'sam', + 'personality_profile': 'generous', + 'base_price': 95, + 'asking_price': 120, + }, + { + 'name': 'Grey Pilgrim Pipe', + 'description': 'A weathered pipe with intricate wizard-carved symbols.', + 'owner_character': 'gandalf', + 'personality_profile': 'bargainer', + 'base_price': 260, + 'asking_price': 360, + }, + { + 'name': 'Second Breakfast Pan', + 'description': 'A surprisingly sturdy pan fit for long roads and many meals.', + 'owner_character': 'sam', + 'personality_profile': 'bargainer', + 'base_price': 70, + 'asking_price': 98, + }, + { + 'name': 'Wizard Hat (Scuffed Edition)', + 'description': 'A tall, dramatic hat with glorious wear and a few mysterious burns.', + 'owner_character': 'gandalf', + 'personality_profile': 'stingy', + 'base_price': 210, + 'asking_price': 315, + }, + { + 'name': 'Mithril Shield', + 'description': 'A legendary shield forged from mithril, light yet stronger than steel.', + 'owner_character': 'frodo', + 'personality_profile': 'sentimental', + 'base_price': 350, + 'asking_price': 450, + }, + { + 'name': 'Sword of Elendil', + 'description': 'An ancient sword with a storied history from the days of old.', + 'owner_character': 'frodo', + 'personality_profile': 'bargainer', + 'base_price': 400, + 'asking_price': 500, + }, + { + 'name': 'Sword of Narsil', + 'description': 'A legendary blade shattered and reforged with great power.', + 'owner_character': 'frodo', + 'personality_profile': 'stingy', + 'base_price': 450, + 'asking_price': 550, + }, + { + 'name': 'Lembas Bread', + 'description': 'Elvish lembas bread that sustains travelers on long journeys.', + 'owner_character': 'sam', + 'personality_profile': 'generous', + 'base_price': 150, + 'asking_price': 180, + }, + { + 'name': 'Elven Rope', + 'description': 'A strong and graceful rope crafted by Elven artisans.', + 'owner_character': 'frodo', + 'personality_profile': 'bargainer', + 'base_price': 80, + 'asking_price': 110, + }, + ] + + seeded_items: List[Item] = [] + for payload in items_data: + item = Item.query.filter_by(name=payload['name']).first() + if not item: + item = Item(**payload) + db.session.add(item) + seeded_items.append(item) + + db.session.commit() + return seeded_items + +def seed_database(app: Flask) -> None: + """Seed the database with initial data.""" + with app.app_context(): + print("Seeding database...") + + # Seed in order: members -> locations -> users -> quests + members = seed_members() + print(f"Seeded {len(members)} members") + + locations = seed_locations() + print(f"Seeded {len(locations)} locations") + + users = seed_users(members) + print(f"Seeded {len(users)} users") + + quests = seed_quests(locations, users) + print(f"Seeded {len(quests)} quests") + + items = seed_items() + print(f"Seeded {len(items)} market items") + + print("Database seeding completed!") diff --git a/sut/backend/verify_config.py b/sut/backend/verify_config.py new file mode 100644 index 0000000..c713d3c --- /dev/null +++ b/sut/backend/verify_config.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Verify Azure OpenAI configuration is loaded correctly.""" +import sys +from pathlib import Path + +# Add backend to path +backend_dir = Path(__file__).parent +sys.path.insert(0, str(backend_dir)) + +from config import Config + +def main(): + config = Config() + + print("\n" + "=" * 70) + print("Azure OpenAI Configuration Status") + print("=" * 70) + + has_endpoint = bool(config.AZURE_OPENAI_ENDPOINT) + has_api_key = bool(config.AZURE_OPENAI_API_KEY) + + status = "✅ ACTIVE" if (has_endpoint and has_api_key) else "❌ NOT CONFIGURED" + print(f"\nStatus: {status}\n") + + print(f"Endpoint: {config.AZURE_OPENAI_ENDPOINT if has_endpoint else '(not set)'}") + print(f"API Key: {'(loaded from .env)' if has_api_key else '(not set)'}") + print(f"Deployment: {config.AZURE_OPENAI_DEPLOYMENT}") + print(f"API Version: {config.AZURE_OPENAI_API_VERSION}") + print(f"Max Tokens: {config.AZURE_OPENAI_MAX_TOKENS}") + print(f"Temperature: {config.AZURE_OPENAI_TEMPERATURE}") + + print("\n" + "=" * 70) + + if has_endpoint and has_api_key: + print("🤖 AI-powered NPC responses are ACTIVE") + print("🎯 Context-aware quest generation is ENABLED") + print("\nNPC conversations will now:") + print(" • Use character personalities for authentic responses") + print(" • Reference user's specific situation in replies") + print(" • Generate quests matched to conversation context") + print(" • Fall back to templates only if API fails") + return 0 + else: + print("⚠️ Azure OpenAI not configured") + print("\nTo enable AI:") + print(" 1. Create/update .env file with Azure credentials") + print(" 2. Restart the backend service") + return 1 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/sut/frontend/.env.example b/sut/frontend/.env.example new file mode 100644 index 0000000..2b6408c --- /dev/null +++ b/sut/frontend/.env.example @@ -0,0 +1,16 @@ +# Frontend Environment Variables +# Copy this to .env.local and update values for your environment + +# API Configuration +REACT_APP_API_URL=http://localhost/api + +# Site URL Configuration (used for SEO and analytics) +# Set this to your deployment URL for correct sitemap.xml and meta tags +# Examples: +# - Development: http://localhost:5173 +# - Staging: https://lotr-staging.testingfantasy.com +# - Production: https://lotr-prod.testingfantasy.com +VITE_APP_SITE_URL=http://localhost:5173 + +# Google Analytics +REACT_APP_GA_ID=G-29N4KD7MQ9 diff --git a/sut/frontend/.env.local b/sut/frontend/.env.local new file mode 100644 index 0000000..75ecf7f --- /dev/null +++ b/sut/frontend/.env.local @@ -0,0 +1,6 @@ +# WebSocket configuration for dev server behind HTTPS proxy (Caddy) +# This ensures webpack dev server uses secure WebSockets when frontend is served over HTTPS +WDS_SOCKET_PROTOCOL=wss +WDS_SOCKET_PORT=80 +WDS_SOCKET_HOST=localhost +WDS_SOCKET_PATH=/ws diff --git a/sut/frontend/Dockerfile b/sut/frontend/Dockerfile new file mode 100644 index 0000000..606c33b --- /dev/null +++ b/sut/frontend/Dockerfile @@ -0,0 +1,24 @@ +FROM node:20-alpine + +WORKDIR /app + +# Copy package files +COPY package*.json ./ +COPY tsconfig.json ./ + +# Install dependencies +RUN npm install + +# Copy source files +COPY . . + +# Expose port +EXPOSE 3000 + +# Disable react-refresh for Docker environment +ENV SKIP_PREFLIGHT_CHECK=true +ENV DISABLE_ESLINT_PLUGIN=true +ENV FAST_REFRESH=false + +# Start development server +CMD ["npm", "start"] diff --git a/sut/frontend/ENV_URL_SETUP.md b/sut/frontend/ENV_URL_SETUP.md new file mode 100644 index 0000000..f47aefa --- /dev/null +++ b/sut/frontend/ENV_URL_SETUP.md @@ -0,0 +1,169 @@ +# Environment-Aware URL Configuration + +This document explains how analytics and SEO URLs adapt to different deployment environments. + +## Overview + +The Fellowship Quest List uses environment-aware URLs in critical files: +- **`index.html`**: Meta tags (canonical, og:url, og:image, twitter:image) +- **`sitemap.xml`**: All routes and API endpoints + +This ensures that regardless of whether you're deploying to: +- Development: `http://localhost:5173` +- Staging: `https://lotr-staging.testingfantasy.com` +- Production: `https://lotr-prod.testingfantasy.com` + +The URLs in these files are always correct. + +## Environment Variables + +### Primary Variable +`VITE_APP_SITE_URL` - The full site URL without trailing slash +- Example: `https://lotr-prod.testingfantasy.com` +- Example: `https://lotr-staging.testingfantasy.com` +- Example: `http://localhost:5173` + +## How It Works + +### For `index.html` +URLs are set dynamically at **runtime** using JavaScript: +1. Base URLs in HTML use placeholders: `%VITE_APP_SITE_URL%` +2. At page load, JavaScript reads `window.location.origin` +3. Dynamic updates to `` and `` tags ensure they reflect the actual deployment URL +4. This works for all deployment scenarios without rebuilding + +### For `sitemap.xml` +URLs are substituted **at build time**: +1. Source file uses placeholders: `%VITE_APP_SITE_URL%` +2. Build script replaces placeholders with actual environment URL +3. Final `sitemap.xml` has concrete URLs for search engines + +## Setup Instructions + +### Development + +```bash +cd sut/frontend + +# Run dev server (uses http://localhost:5173 automatically) +npm run dev +``` + +### Build for Deployment + +```bash +cd sut/frontend + +# Set the environment-specific URL +export VITE_APP_SITE_URL="https://lotr-prod.testingfantasy.com" + +# Build the application +npm run build + +# Run setup script to update XML files with correct URLs +node scripts/setup-env-urls.js +``` + +### Docker Deployment + +In your Dockerfile: + +```dockerfile +ARG SITE_URL=https://lotr-prod.testingfantasy.com + +# ... build steps ... + +# Setup environment-aware URLs +ENV VITE_APP_SITE_URL=${SITE_URL} +RUN cd sut/frontend && node scripts/setup-env-urls.js +``` + +### CI/CD Pipeline + +Example GitHub Actions: + +```yaml +- name: Setup environment-aware URLs + env: + VITE_APP_SITE_URL: ${{ secrets.SITE_URL }} + run: | + cd sut/frontend + node scripts/setup-env-urls.js +``` + +## File Examples + +### index.html (Runtime Dynamic) +```html + + + + + + +``` + +### sitemap.xml (Build-time Substitution) +```xml + +%VITE_APP_SITE_URL%/login + + +https://lotr-prod.testingfantasy.com/login +``` + +## Testing URLs + +### Verify index.html Dynamic URLs +```bash +# Open browser DevTools and check the Console when page loads +# Meta tags should update to match your current URL + +# Example: If accessed at https://mysite.com +# - og:url should be "https://mysite.com/" +# - canonical should be "https://mysite.com/" +``` + +### Verify sitemap.xml +```bash +# Download sitemap.xml and check the URLs +curl https://lotr-prod.testingfantasy.com/sitemap.xml | head -20 + +# All entries should use the correct domain +``` + +## Benefits + +✅ **Single codebase** - Deploy to any environment without code changes +✅ **Search engines** - Correct canonical URLs prevent duplicate content penalties +✅ **Social media** - Correct og: tags for rich previews on any domain +✅ **Analytics** - Proper tracking in GA regardless of deployment URL +✅ **No rebuilds** - index.html works without rebuild for different domains + +## Troubleshooting + +### URLs not updating in sitemap.xml +- Ensure `VITE_APP_SITE_URL` is set before building +- Run `node scripts/setup-env-urls.js` after build +- Check that `public/sitemap.xml` contains your domain, not `%VITE_APP_SITE_URL%` + +### Meta tags not updating in index.html +- Open browser DevTools (F12) +- Go to Elements/Inspector and check `` etc. +- Verify JavaScript ran: check Console for any errors +- URL should match `window.location.origin` + +### Staging environment has wrong URLs +- Verify `VITE_APP_SITE_URL` environment variable is set +- Run setup script before deploying +- Check that sitemap.xml contains staging URL, not production + +## References + +- [Google Canonical URLs](https://developers.google.com/search/docs/beginner/seo-starter-guide#declare-the-canonical-version-of-a-page) +- [Open Graph Protocol](https://ogp.me/) +- [Sitemap Protocol](https://www.sitemaps.org/protocol.html) diff --git a/sut/frontend/build/asset-manifest.json b/sut/frontend/build/asset-manifest.json new file mode 100644 index 0000000..353365c --- /dev/null +++ b/sut/frontend/build/asset-manifest.json @@ -0,0 +1,13 @@ +{ + "files": { + "main.css": "/static/css/main.787d33f1.css", + "main.js": "/static/js/main.cdb3f2d7.js", + "index.html": "/index.html", + "main.787d33f1.css.map": "/static/css/main.787d33f1.css.map", + "main.cdb3f2d7.js.map": "/static/js/main.cdb3f2d7.js.map" + }, + "entrypoints": [ + "static/css/main.787d33f1.css", + "static/js/main.cdb3f2d7.js" + ] +} \ No newline at end of file diff --git a/sut/frontend/build/index.html b/sut/frontend/build/index.html new file mode 100644 index 0000000..ce9e994 --- /dev/null +++ b/sut/frontend/build/index.html @@ -0,0 +1 @@ +Fellowship Quest Tracker - Track Your Middle-earth Adventure
\ No newline at end of file diff --git a/sut/frontend/build/leaflet/leaflet.css b/sut/frontend/build/leaflet/leaflet.css new file mode 100644 index 0000000..6593ca5 --- /dev/null +++ b/sut/frontend/build/leaflet/leaflet.css @@ -0,0 +1,767 @@ +/* required styles */ + +.leaflet-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-container, +.leaflet-pane > svg, +.leaflet-pane > canvas, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; +} + +.leaflet-container { + overflow: hidden; +} + +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-user-drag: none; +} + +/* Prevents IE11 from highlighting tiles in blue */ +.leaflet-tile::selection { + background: transparent; +} + +/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ +.leaflet-safari .leaflet-tile { + image-rendering: -webkit-optimize-contrast; +} + +/* hack that prevents hw layers "stretching" when loading new tiles */ +.leaflet-safari .leaflet-tile-container { + width: 1600px; + height: 1600px; + -webkit-transform-origin: 0 0; +} + +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; +} + +/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ +/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ +.leaflet-container .leaflet-overlay-pane svg { + max-width: none !important; + max-height: none !important; +} + +.leaflet-container .leaflet-marker-pane img, +.leaflet-container .leaflet-shadow-pane img, +.leaflet-container .leaflet-tile-pane img, +.leaflet-container img.leaflet-image-layer, +.leaflet-container .leaflet-tile { + max-width: none !important; + max-height: none !important; + width: auto; + padding: 0; +} + +.leaflet-container.leaflet-touch-zoom { + -ms-touch-action: pan-x pan-y; + touch-action: pan-x pan-y; +} + +.leaflet-container.leaflet-touch-drag { + -ms-touch-action: pinch-zoom; + /* Fallback for FF which doesn't support pinch-zoom */ + touch-action: none; + touch-action: pinch-zoom; +} + +.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { + -ms-touch-action: none; + touch-action: none; +} + +.leaflet-container { + -webkit-tap-highlight-color: transparent; +} + +.leaflet-container a { + -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); +} + +.leaflet-tile { + filter: inherit; + visibility: hidden; +} + +.leaflet-tile-loaded { + visibility: inherit; +} + +.leaflet-zoom-box { + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; +} + +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ +.leaflet-overlay-pane svg { + -moz-user-select: none; +} + +.leaflet-pane { + z-index: 400; +} + +.leaflet-tile-pane { + z-index: 200; +} + +.leaflet-overlay-pane { + z-index: 400; +} + +.leaflet-shadow-pane { + z-index: 500; +} + +.leaflet-marker-pane { + z-index: 600; +} + +.leaflet-tooltip-pane { + z-index: 650; +} + +.leaflet-popup-pane { + z-index: 700; +} + +.leaflet-map-pane canvas { + z-index: 100; +} + +.leaflet-map-pane svg { + z-index: 200; +} + +.leaflet-vml-shape { + width: 1px; + height: 1px; +} + +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; +} + + +/* control positioning */ + +.leaflet-control { + position: relative; + z-index: 800; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; +} + +.leaflet-top, +.leaflet-bottom { + position: absolute; + z-index: 1000; + pointer-events: none; +} + +.leaflet-top { + top: 0; +} + +.leaflet-right { + right: 0; +} + +.leaflet-bottom { + bottom: 0; +} + +.leaflet-left { + left: 0; +} + +.leaflet-control { + float: left; + clear: both; +} + +.leaflet-right .leaflet-control { + float: right; +} + +.leaflet-top .leaflet-control { + margin-top: 10px; +} + +.leaflet-bottom .leaflet-control { + margin-bottom: 10px; +} + +.leaflet-left .leaflet-control { + margin-left: 10px; +} + +.leaflet-right .leaflet-control { + margin-right: 10px; +} + + +/* zoom and fade animations */ + +.leaflet-fade-anim .leaflet-popup { + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; +} + +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { + opacity: 1; +} + +.leaflet-zoom-animated { + -webkit-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; +} + +svg.leaflet-zoom-animated { + will-change: transform; +} + +.leaflet-zoom-anim .leaflet-zoom-animated { + -webkit-transition: -webkit-transform 0.25s cubic-bezier(0, 0, 0.25, 1); + -moz-transition: -moz-transform 0.25s cubic-bezier(0, 0, 0.25, 1); + transition: transform 0.25s cubic-bezier(0, 0, 0.25, 1); +} + +.leaflet-zoom-anim .leaflet-tile, +.leaflet-pan-anim .leaflet-tile { + -webkit-transition: none; + -moz-transition: none; + transition: none; +} + +.leaflet-zoom-anim .leaflet-zoom-hide { + visibility: hidden; +} + + +/* cursors */ + +.leaflet-interactive { + cursor: pointer; +} + +.leaflet-grab { + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; +} + +.leaflet-crosshair, +.leaflet-crosshair .leaflet-interactive { + cursor: crosshair; +} + +.leaflet-popup-pane, +.leaflet-control { + cursor: auto; +} + +.leaflet-dragging .leaflet-grab, +.leaflet-dragging .leaflet-grab .leaflet-interactive, +.leaflet-dragging .leaflet-marker-draggable { + cursor: move; + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + cursor: grabbing; +} + +/* marker & overlays interactivity */ +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-image-layer, +.leaflet-pane > svg path, +.leaflet-tile-container { + pointer-events: none; +} + +.leaflet-marker-icon.leaflet-interactive, +.leaflet-image-layer.leaflet-interactive, +.leaflet-pane > svg path.leaflet-interactive, +svg.leaflet-image-layer.leaflet-interactive path { + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; +} + +/* visual tweaks */ + +.leaflet-container { + background: #ddd; + outline-offset: 1px; +} + +.leaflet-container a { + color: #0078A8; +} + +.leaflet-zoom-box { + border: 2px dotted #38f; + background: rgba(255, 255, 255, 0.5); +} + + +/* general typography */ +.leaflet-container { + font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; + font-size: 12px; + font-size: 0.75rem; + line-height: 1.5; +} + + +/* general toolbar styles */ + +.leaflet-bar { + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); + border-radius: 4px; +} + +.leaflet-bar a { + background-color: #fff; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; +} + +.leaflet-bar a, +.leaflet-control-layers-toggle { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; +} + +.leaflet-bar a:hover, +.leaflet-bar a:focus { + background-color: #f4f4f4; +} + +.leaflet-bar a:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.leaflet-bar a:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; +} + +.leaflet-bar a.leaflet-disabled { + cursor: default; + background-color: #f4f4f4; + color: #bbb; +} + +.leaflet-touch .leaflet-bar a { + width: 30px; + height: 30px; + line-height: 30px; +} + +.leaflet-touch .leaflet-bar a:first-child { + border-top-left-radius: 2px; + border-top-right-radius: 2px; +} + +.leaflet-touch .leaflet-bar a:last-child { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +/* zoom control */ + +.leaflet-control-zoom-in, +.leaflet-control-zoom-out { + font: bold 18px 'Lucida Console', Monaco, monospace; + text-indent: 1px; +} + +.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { + font-size: 22px; +} + + +/* layers control */ + +.leaflet-control-layers { + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4); + background: #fff; + border-radius: 5px; +} + +.leaflet-control-layers-toggle { + background-image: url(assets/images/layers.png); + width: 36px; + height: 36px; +} + +.leaflet-retina .leaflet-control-layers-toggle { + background-image: url(assets/images/layers-2x.png); + background-size: 26px 26px; +} + +.leaflet-touch .leaflet-control-layers-toggle { + width: 44px; + height: 44px; +} + +.leaflet-control-layers .leaflet-control-layers-list, +.leaflet-control-layers-expanded .leaflet-control-layers-toggle { + display: none; +} + +.leaflet-control-layers-expanded .leaflet-control-layers-list { + display: block; + position: relative; +} + +.leaflet-control-layers-expanded { + padding: 6px 10px 6px 6px; + color: #333; + background: #fff; +} + +.leaflet-control-layers-scrollbar { + overflow-y: scroll; + overflow-x: hidden; + padding-right: 5px; +} + +.leaflet-control-layers-selector { + margin-top: 2px; + position: relative; + top: 1px; +} + +.leaflet-control-layers label { + display: block; + font-size: 13px; + font-size: 1.08333em; +} + +.leaflet-control-layers-separator { + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; +} + +/* Default icon URLs */ +.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */ + background-image: url(assets/images/marker-icon.png); +} + + +/* attribution and scale controls */ + +.leaflet-container .leaflet-control-attribution { + background: #fff; + background: rgba(255, 255, 255, 0.8); + margin: 0; +} + +.leaflet-control-attribution, +.leaflet-control-scale-line { + padding: 0 5px; + color: #333; + line-height: 1.4; +} + +.leaflet-control-attribution a { + text-decoration: none; +} + +.leaflet-control-attribution a:hover, +.leaflet-control-attribution a:focus { + text-decoration: underline; +} + +.leaflet-attribution-flag { + display: inline !important; + vertical-align: baseline !important; + width: 1em; + height: 0.6669em; +} + +.leaflet-left .leaflet-control-scale { + margin-left: 5px; +} + +.leaflet-bottom .leaflet-control-scale { + margin-bottom: 5px; +} + +.leaflet-control-scale-line { + border: 2px solid #777; + border-top: none; + line-height: 1.1; + padding: 2px 5px 1px; + white-space: nowrap; + -moz-box-sizing: border-box; + box-sizing: border-box; + background: rgba(255, 255, 255, 0.8); + text-shadow: 1px 1px #fff; +} + +.leaflet-control-scale-line:not(:first-child) { + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; +} + +.leaflet-control-scale-line:not(:first-child):not(:last-child) { + border-bottom: 2px solid #777; +} + +.leaflet-touch .leaflet-control-attribution, +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + box-shadow: none; +} + +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + border: 2px solid rgba(0, 0, 0, 0.2); + background-clip: padding-box; +} + + +/* popup */ + +.leaflet-popup { + position: absolute; + text-align: center; + margin-bottom: 20px; +} + +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + border-radius: 12px; +} + +.leaflet-popup-content { + margin: 13px 24px 13px 20px; + line-height: 1.3; + font-size: 13px; + font-size: 1.08333em; + min-height: 1px; +} + +.leaflet-popup-content p { + margin: 17px 0; + margin: 1.3em 0; +} + +.leaflet-popup-tip-container { + width: 40px; + height: 20px; + position: absolute; + left: 50%; + margin-top: -1px; + margin-left: -20px; + overflow: hidden; + pointer-events: none; +} + +.leaflet-popup-tip { + width: 17px; + height: 17px; + padding: 1px; + + margin: -10px auto 0; + pointer-events: auto; + + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} + +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: white; + color: #333; + box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4); +} + +.leaflet-container a.leaflet-popup-close-button { + position: absolute; + top: 0; + right: 0; + border: none; + text-align: center; + width: 24px; + height: 24px; + font: 16px/24px Tahoma, Verdana, sans-serif; + color: #757575; + text-decoration: none; + background: transparent; +} + +.leaflet-container a.leaflet-popup-close-button:hover, +.leaflet-container a.leaflet-popup-close-button:focus { + color: #585858; +} + +.leaflet-popup-scrolled { + overflow: auto; +} + +.leaflet-oldie .leaflet-popup-content-wrapper { + -ms-zoom: 1; +} + +.leaflet-oldie .leaflet-popup-tip { + width: 24px; + margin: 0 auto; + + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); +} + +.leaflet-oldie .leaflet-control-zoom, +.leaflet-oldie .leaflet-control-layers, +.leaflet-oldie .leaflet-popup-content-wrapper, +.leaflet-oldie .leaflet-popup-tip { + border: 1px solid #999; +} + + +/* div icon */ + +.leaflet-div-icon { + background: #fff; + border: 1px solid #666; +} + + +/* Tooltip */ +/* Base styles for the element that has a tooltip */ +.leaflet-tooltip { + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); +} + +.leaflet-tooltip.leaflet-interactive { + cursor: pointer; + pointer-events: auto; +} + +.leaflet-tooltip-top:before, +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + position: absolute; + pointer-events: none; + border: 6px solid transparent; + background: transparent; + content: ""; +} + +/* Directions */ + +.leaflet-tooltip-bottom { + margin-top: 6px; +} + +.leaflet-tooltip-top { + margin-top: -6px; +} + +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-top:before { + left: 50%; + margin-left: -6px; +} + +.leaflet-tooltip-top:before { + bottom: 0; + margin-bottom: -12px; + border-top-color: #fff; +} + +.leaflet-tooltip-bottom:before { + top: 0; + margin-top: -12px; + margin-left: -6px; + border-bottom-color: #fff; +} + +.leaflet-tooltip-left { + margin-left: -6px; +} + +.leaflet-tooltip-right { + margin-left: 6px; +} + +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + top: 50%; + margin-top: -6px; +} + +.leaflet-tooltip-left:before { + right: 0; + margin-right: -12px; + border-left-color: #fff; +} + +.leaflet-tooltip-right:before { + left: 0; + margin-left: -12px; + border-right-color: #fff; +} + +/* Printing */ + +@media print { + /* Prevent printers from removing background-images of controls. */ + .leaflet-control { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } +} diff --git a/sut/frontend/build/leaflet/leaflet.js b/sut/frontend/build/leaflet/leaflet.js new file mode 100644 index 0000000..047bfe7 --- /dev/null +++ b/sut/frontend/build/leaflet/leaflet.js @@ -0,0 +1,6 @@ +/* @preserve + * Leaflet 1.9.3, a JS library for interactive maps. https://leafletjs.com + * (c) 2010-2022 Vladimir Agafonkin, (c) 2010-2011 CloudMade + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n=this.min.x&&i.x<=this.max.x&&e.y>=this.min.y&&i.y<=this.max.y},intersects:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>=e.x&&n.x<=i.x,t=t.y>=e.y&&n.y<=i.y;return o&&t},overlaps:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>e.x&&n.xe.y&&n.y=n.lat&&i.lat<=o.lat&&e.lng>=n.lng&&i.lng<=o.lng},intersects:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>=e.lat&&n.lat<=i.lat,t=t.lng>=e.lng&&n.lng<=i.lng;return o&&t},overlaps:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>e.lat&&n.late.lng&&n.lng","http://www.w3.org/2000/svg"===(Ft.firstChild&&Ft.firstChild.namespaceURI));function y(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var b={ie:pt,ielt9:mt,edge:n,webkit:ft,android:gt,android23:vt,androidStock:yt,opera:xt,chrome:wt,gecko:bt,safari:Pt,phantom:Lt,opera12:o,win:Tt,ie3d:Mt,webkit3d:zt,gecko3d:_t,any3d:Ct,mobile:Zt,mobileWebkit:St,mobileWebkit3d:Et,msPointer:kt,pointer:Ot,touch:Bt,touchNative:At,mobileOpera:It,mobileGecko:Rt,retina:Nt,passiveEvents:Dt,canvas:jt,svg:Ht,vml:!Ht&&function(){try{var t=document.createElement("div"),e=(t.innerHTML='',t.firstChild);return e.style.behavior="url(#default#VML)",e&&"object"==typeof e.adj}catch(t){return!1}}(),inlineSvg:Ft,mac:0===navigator.platform.indexOf("Mac"),linux:0===navigator.platform.indexOf("Linux")},Wt=b.msPointer?"MSPointerDown":"pointerdown",Ut=b.msPointer?"MSPointerMove":"pointermove",Vt=b.msPointer?"MSPointerUp":"pointerup",qt=b.msPointer?"MSPointerCancel":"pointercancel",Gt={touchstart:Wt,touchmove:Ut,touchend:Vt,touchcancel:qt},Kt={touchstart:function(t,e){e.MSPOINTER_TYPE_TOUCH&&e.pointerType===e.MSPOINTER_TYPE_TOUCH&&O(e);ee(t,e)},touchmove:ee,touchend:ee,touchcancel:ee},Yt={},Xt=!1;function Jt(t,e,i){return"touchstart"!==e||Xt||(document.addEventListener(Wt,$t,!0),document.addEventListener(Ut,Qt,!0),document.addEventListener(Vt,te,!0),document.addEventListener(qt,te,!0),Xt=!0),Kt[e]?(i=Kt[e].bind(this,i),t.addEventListener(Gt[e],i,!1),i):(console.warn("wrong event specified:",e),u)}function $t(t){Yt[t.pointerId]=t}function Qt(t){Yt[t.pointerId]&&(Yt[t.pointerId]=t)}function te(t){delete Yt[t.pointerId]}function ee(t,e){if(e.pointerType!==(e.MSPOINTER_TYPE_MOUSE||"mouse")){for(var i in e.touches=[],Yt)e.touches.push(Yt[i]);e.changedTouches=[e],t(e)}}var ie=200;function ne(t,i){t.addEventListener("dblclick",i);var n,o=0;function e(t){var e;1!==t.detail?n=t.detail:"mouse"===t.pointerType||t.sourceCapabilities&&!t.sourceCapabilities.firesTouchEvents||((e=Ne(t)).some(function(t){return t instanceof HTMLLabelElement&&t.attributes.for})&&!e.some(function(t){return t instanceof HTMLInputElement||t instanceof HTMLSelectElement})||((e=Date.now())-o<=ie?2===++n&&i(function(t){var e,i,n={};for(i in t)e=t[i],n[i]=e&&e.bind?e.bind(t):e;return(t=n).type="dblclick",n.detail=2,n.isTrusted=!1,n._simulated=!0,n}(t)):n=1,o=e))}return t.addEventListener("click",e),{dblclick:i,simDblclick:e}}var oe,se,re,ae,he,le,ue=we(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),ce=we(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===ce||"OTransition"===ce?ce+"End":"transitionend";function _e(t){return"string"==typeof t?document.getElementById(t):t}function pe(t,e){var i=t.style[e]||t.currentStyle&&t.currentStyle[e];return"auto"===(i=i&&"auto"!==i||!document.defaultView?i:(t=document.defaultView.getComputedStyle(t,null))?t[e]:null)?null:i}function P(t,e,i){t=document.createElement(t);return t.className=e||"",i&&i.appendChild(t),t}function T(t){var e=t.parentNode;e&&e.removeChild(t)}function me(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function fe(t){var e=t.parentNode;e&&e.lastChild!==t&&e.appendChild(t)}function ge(t){var e=t.parentNode;e&&e.firstChild!==t&&e.insertBefore(t,e.firstChild)}function ve(t,e){return void 0!==t.classList?t.classList.contains(e):0<(t=xe(t)).length&&new RegExp("(^|\\s)"+e+"(\\s|$)").test(t)}function M(t,e){var i;if(void 0!==t.classList)for(var n=W(e),o=0,s=n.length;othis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,e){this._enforcingBounds=!0;var i=this.getCenter(),t=this._limitCenter(i,this._zoom,g(t));return i.equals(t)||this.panTo(t,e),this._enforcingBounds=!1,this},panInside:function(t,e){var i=m((e=e||{}).paddingTopLeft||e.padding||[0,0]),n=m(e.paddingBottomRight||e.padding||[0,0]),o=this.project(this.getCenter()),t=this.project(t),s=this.getPixelBounds(),i=_([s.min.add(i),s.max.subtract(n)]),s=i.getSize();return i.contains(t)||(this._enforcingBounds=!0,n=t.subtract(i.getCenter()),i=i.extend(t).getSize().subtract(s),o.x+=n.x<0?-i.x:i.x,o.y+=n.y<0?-i.y:i.y,this.panTo(this.unproject(o),e),this._enforcingBounds=!1),this},invalidateSize:function(t){if(!this._loaded)return this;t=l({animate:!1,pan:!0},!0===t?{animate:!0}:t);var e=this.getSize(),i=(this._sizeChanged=!0,this._lastCenter=null,this.getSize()),n=e.divideBy(2).round(),o=i.divideBy(2).round(),n=n.subtract(o);return n.x||n.y?(t.animate&&t.pan?this.panBy(n):(t.pan&&this._rawPanBy(n),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(a(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:e,newSize:i})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){var e,i;return t=this._locateOptions=l({timeout:1e4,watch:!1},t),"geolocation"in navigator?(e=a(this._handleGeolocationResponse,this),i=a(this._handleGeolocationError,this),t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t)):this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var e;this._container._leaflet_id&&(e=t.code,t=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout"),this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+t+"."}))},_handleGeolocationResponse:function(t){if(this._container._leaflet_id){var e,i,n=new v(t.coords.latitude,t.coords.longitude),o=n.toBounds(2*t.coords.accuracy),s=this._locateOptions,r=(s.setView&&(e=this.getBoundsZoom(o),this.setView(n,s.maxZoom?Math.min(e,s.maxZoom):e)),{latlng:n,bounds:o,timestamp:t.timestamp});for(i in t.coords)"number"==typeof t.coords[i]&&(r[i]=t.coords[i]);this.fire("locationfound",r)}},addHandler:function(t,e){return e&&(e=this[t]=new e(this),this._handlers.push(e),this.options[t]&&e.enable()),this},remove:function(){if(this._initEvents(!0),this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}for(var t in void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),T(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(r(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload"),this._layers)this._layers[t].remove();for(t in this._panes)T(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,e){e=P("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),e||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter.clone():this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new s(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,e,i){t=g(t),i=m(i||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),t=t.getSouthEast(),i=this.getSize().subtract(i),t=_(this.project(t,n),this.project(r,n)).getSize(),r=b.any3d?this.options.zoomSnap:1,a=i.x/t.x,i=i.y/t.y,t=e?Math.max(a,i):Math.min(a,i),n=this.getScaleZoom(t,n);return r&&(n=Math.round(n/(r/100))*(r/100),n=e?Math.ceil(n/r)*r:Math.floor(n/r)*r),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new p(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,e){t=this._getTopLeftPoint(t,e);return new f(t,t.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,e){var i=this.options.crs;return e=void 0===e?this._zoom:e,i.scale(t)/i.scale(e)},getScaleZoom:function(t,e){var i=this.options.crs,t=(e=void 0===e?this._zoom:e,i.zoom(t*i.scale(e)));return isNaN(t)?1/0:t},project:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.latLngToPoint(w(t),e)},unproject:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.pointToLatLng(m(t),e)},layerPointToLatLng:function(t){t=m(t).add(this.getPixelOrigin());return this.unproject(t)},latLngToLayerPoint:function(t){return this.project(w(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(w(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(g(t))},distance:function(t,e){return this.options.crs.distance(w(t),w(e))},containerPointToLayerPoint:function(t){return m(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return m(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){t=this.containerPointToLayerPoint(m(t));return this.layerPointToLatLng(t)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(w(t)))},mouseEventToContainerPoint:function(t){return De(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){t=this._container=_e(t);if(!t)throw new Error("Map container not found.");if(t._leaflet_id)throw new Error("Map container is already initialized.");S(t,"scroll",this._onScroll,this),this._containerId=h(t)},_initLayout:function(){var t=this._container,e=(this._fadeAnimated=this.options.fadeAnimation&&b.any3d,M(t,"leaflet-container"+(b.touch?" leaflet-touch":"")+(b.retina?" leaflet-retina":"")+(b.ielt9?" leaflet-oldie":"")+(b.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":"")),pe(t,"position"));"absolute"!==e&&"relative"!==e&&"fixed"!==e&&"sticky"!==e&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),Z(this._mapPane,new p(0,0)),this.createPane("tilePane"),this.createPane("overlayPane"),this.createPane("shadowPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(M(t.markerPane,"leaflet-zoom-hide"),M(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,e,i){Z(this._mapPane,new p(0,0));var n=!this._loaded,o=(this._loaded=!0,e=this._limitZoom(e),this.fire("viewprereset"),this._zoom!==e);this._moveStart(o,i)._move(t,e)._moveEnd(o),this.fire("viewreset"),n&&this.fire("load")},_moveStart:function(t,e){return t&&this.fire("zoomstart"),e||this.fire("movestart"),this},_move:function(t,e,i,n){void 0===e&&(e=this._zoom);var o=this._zoom!==e;return this._zoom=e,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),n?i&&i.pinch&&this.fire("zoom",i):((o||i&&i.pinch)&&this.fire("zoom",i),this.fire("move",i)),this},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return r(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){Z(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={};var e=t?k:S;e((this._targets[h(this._container)]=this)._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup",this._handleDOMEvent,this),this.options.trackResize&&e(window,"resize",this._onResize,this),b.any3d&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){r(this._resizeRequest),this._resizeRequest=x(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,e){for(var i,n=[],o="mouseout"===e||"mouseover"===e,s=t.target||t.srcElement,r=!1;s;){if((i=this._targets[h(s)])&&("click"===e||"preclick"===e)&&this._draggableMoved(i)){r=!0;break}if(i&&i.listens(e,!0)){if(o&&!Fe(s,t))break;if(n.push(i),o)break}if(s===this._container)break;s=s.parentNode}return n=n.length||r||o||!this.listens(e,!0)?n:[this]},_isClickDisabled:function(t){for(;t&&t!==this._container;){if(t._leaflet_disable_click)return!0;t=t.parentNode}},_handleDOMEvent:function(t){var e,i=t.target||t.srcElement;!this._loaded||i._leaflet_disable_events||"click"===t.type&&this._isClickDisabled(i)||("mousedown"===(e=t.type)&&Me(i),this._fireDOMEvent(t,e))},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,i){"click"===t.type&&((a=l({},t)).type="preclick",this._fireDOMEvent(a,a.type,i));var n=this._findEventTargets(t,e);if(i){for(var o=[],s=0;sthis.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),n=this._getCenterOffset(t)._divideBy(1-1/n);if(!0!==i.animate&&!this.getSize().contains(n))return!1;x(function(){this._moveStart(!0,!1)._animateZoom(t,e,!0)},this)}return!0},_animateZoom:function(t,e,i,n){this._mapPane&&(i&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=e,M(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:e,noUpdate:n}),this._tempFireZoomEvent||(this._tempFireZoomEvent=this._zoom!==this._animateToZoom),this._move(this._animateToCenter,this._animateToZoom,void 0,!0),setTimeout(a(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&z(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom,void 0,!0),this._tempFireZoomEvent&&this.fire("zoom"),delete this._tempFireZoomEvent,this.fire("move"),this._moveEnd(!0))}});function Ue(t){return new B(t)}var Ve,B=et.extend({options:{position:"topright"},initialize:function(t){c(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var e=this._map;return e&&e.removeControl(this),this.options.position=t,e&&e.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var e=this._container=this.onAdd(t),i=this.getPosition(),t=t._controlCorners[i];return M(e,"leaflet-control"),-1!==i.indexOf("bottom")?t.insertBefore(e,t.firstChild):t.appendChild(e),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(T(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0",e=document.createElement("div");return e.innerHTML=t,e.firstChild},_addItem:function(t){var e,i=document.createElement("label"),n=this._map.hasLayer(t.layer),n=(t.overlay?((e=document.createElement("input")).type="checkbox",e.className="leaflet-control-layers-selector",e.defaultChecked=n):e=this._createRadioElement("leaflet-base-layers_"+h(this),n),this._layerControlInputs.push(e),e.layerId=h(t.layer),S(e,"click",this._onInputClick,this),document.createElement("span")),o=(n.innerHTML=" "+t.name,document.createElement("span"));return i.appendChild(o),o.appendChild(e),o.appendChild(n),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(i),this._checkDisabledLayers(),i},_onInputClick:function(){var t,e,i=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=i.length-1;0<=s;s--)t=i[s],e=this._getLayer(t.layerId).layer,t.checked?n.push(e):t.checked||o.push(e);for(s=0;se.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expandSafely:function(){var t=this._section;S(t,"click",O),this.expand(),setTimeout(function(){k(t,"click",O)})}})),Ge=B.extend({options:{position:"topleft",zoomInText:'',zoomInTitle:"Zoom in",zoomOutText:'',zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=P("div",e+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,e+"-in",i,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,e+"-out",i,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,e,i,n,o){i=P("a",i,n);return i.innerHTML=t,i.href="#",i.title=e,i.setAttribute("role","button"),i.setAttribute("aria-label",e),Ie(i),S(i,"click",Re),S(i,"click",o,this),S(i,"click",this._refocusOnMap,this),i},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";z(this._zoomInButton,e),z(this._zoomOutButton,e),this._zoomInButton.setAttribute("aria-disabled","false"),this._zoomOutButton.setAttribute("aria-disabled","false"),!this._disabled&&t._zoom!==t.getMinZoom()||(M(this._zoomOutButton,e),this._zoomOutButton.setAttribute("aria-disabled","true")),!this._disabled&&t._zoom!==t.getMaxZoom()||(M(this._zoomInButton,e),this._zoomInButton.setAttribute("aria-disabled","true"))}}),Ke=(A.mergeOptions({zoomControl:!0}),A.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new Ge,this.addControl(this.zoomControl))}),B.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var e="leaflet-control-scale",i=P("div",e),n=this.options;return this._addScales(n,e+"-line",i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=P("div",e,i)),t.imperial&&(this._iScale=P("div",e,i))},_update:function(){var t=this._map,e=t.getSize().y/2,t=t.distance(t.containerPointToLatLng([0,e]),t.containerPointToLatLng([this.options.maxWidth,e]));this._updateScales(t)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var e=this._getRoundNum(t);this._updateScale(this._mScale,e<1e3?e+" m":e/1e3+" km",e/t)},_updateImperial:function(t){var e,i,t=3.2808399*t;5280'+(b.inlineSvg?' ':"")+"Leaflet"},initialize:function(t){c(this,t),this._attributions={}},onAdd:function(t){for(var e in(t.attributionControl=this)._container=P("div","leaflet-control-attribution"),Ie(this._container),t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return this._update(),t.on("layeradd",this._addAttribution,this),this._container},onRemove:function(t){t.off("layeradd",this._addAttribution,this)},_addAttribution:function(t){t.layer.getAttribution&&(this.addAttribution(t.layer.getAttribution()),t.layer.once("remove",function(){this.removeAttribution(t.layer.getAttribution())},this))},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t,e=[];for(t in this._attributions)this._attributions[t]&&e.push(t);var i=[];this.options.prefix&&i.push(this.options.prefix),e.length&&i.push(e.join(", ")),this._container.innerHTML=i.join(' ')}}}),n=(A.mergeOptions({attributionControl:!0}),A.addInitHook(function(){this.options.attributionControl&&(new Ye).addTo(this)}),B.Layers=qe,B.Zoom=Ge,B.Scale=Ke,B.Attribution=Ye,Ue.layers=function(t,e,i){return new qe(t,e,i)},Ue.zoom=function(t){return new Ge(t)},Ue.scale=function(t){return new Ke(t)},Ue.attribution=function(t){return new Ye(t)},et.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}})),ft=(n.addTo=function(t,e){return t.addHandler(e,this),this},{Events:e}),Xe=b.touch?"touchstart mousedown":"mousedown",Je=it.extend({options:{clickTolerance:3},initialize:function(t,e,i,n){c(this,n),this._element=t,this._dragStartTarget=e||t,this._preventOutline=i},enable:function(){this._enabled||(S(this._dragStartTarget,Xe,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Je._dragging===this&&this.finishDrag(!0),k(this._dragStartTarget,Xe,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){var e,i;this._enabled&&(this._moved=!1,ve(this._element,"leaflet-zoom-anim")||(t.touches&&1!==t.touches.length?Je._dragging===this&&this.finishDrag():Je._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((Je._dragging=this)._preventOutline&&Me(this._element),Le(),re(),this._moving||(this.fire("down"),i=t.touches?t.touches[0]:t,e=Ce(this._element),this._startPoint=new p(i.clientX,i.clientY),this._startPos=Pe(this._element),this._parentScale=Ze(e),i="mousedown"===t.type,S(document,i?"mousemove":"touchmove",this._onMove,this),S(document,i?"mouseup":"touchend touchcancel",this._onUp,this)))))},_onMove:function(t){var e;this._enabled&&(t.touches&&1e&&(i.push(t[n]),o=n);oe.max.x&&(i|=2),t.ye.max.y&&(i|=8),i}function ni(t,e,i,n){var o=e.x,e=e.y,s=i.x-o,r=i.y-e,a=s*s+r*r;return 0this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()t.y!=n.y>t.y&&t.x<(n.x-i.x)*(t.y-i.y)/(n.y-i.y)+i.x&&(l=!l);return l||vi.prototype._containsPoint.call(this,t,!0)}});var xi=ui.extend({initialize:function(t,e){c(this,e),this._layers={},t&&this.addData(t)},addData:function(t){var e,i,n,o=d(t)?t:t.features;if(o){for(e=0,i=o.length;es.x&&(r=i.x+a-s.x+o.x),i.x-r-n.x<(a=0)&&(r=i.x-n.x),i.y+e+o.y>s.y&&(a=i.y+e-s.y+o.y),i.y-a-n.y<0&&(a=i.y-n.y),(r||a)&&(this.options.keepInView&&(this._autopanning=!0),t.fire("autopanstart").panBy([r,a]))))},_getAnchor:function(){return m(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}})),Bi=(A.mergeOptions({closePopupOnClick:!0}),A.include({openPopup:function(t,e,i){return this._initOverlay(Ai,t,e,i).openOn(this),this},closePopup:function(t){return(t=arguments.length?t:this._popup)&&t.close(),this}}),o.include({bindPopup:function(t,e){return this._popup=this._initOverlay(Ai,this._popup,t,e),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t){return this._popup&&(this instanceof ui||(this._popup._source=this),this._popup._prepareOpen(t||this._latlng)&&this._popup.openOn(this._map)),this},closePopup:function(){return this._popup&&this._popup.close(),this},togglePopup:function(){return this._popup&&this._popup.toggle(this),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var e;this._popup&&this._map&&(Re(t),e=t.layer||t.target,this._popup._source!==e||e instanceof mi?(this._popup._source=e,this.openPopup(t.latlng)):this._map.hasLayer(this._popup)?this.closePopup():this.openPopup(t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}}),Oi.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,opacity:.9},onAdd:function(t){Oi.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&(this.addEventParent(this._source),this._source.fire("tooltipopen",{tooltip:this},!0))},onRemove:function(t){Oi.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&(this.removeEventParent(this._source),this._source.fire("tooltipclose",{tooltip:this},!0))},getEvents:function(){var t=Oi.prototype.getEvents.call(this);return this.options.permanent||(t.preclick=this.close),t},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=P("div",t),this._container.setAttribute("role","tooltip"),this._container.setAttribute("id","leaflet-tooltip-"+h(this))},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var e,i=this._map,n=this._container,o=i.latLngToContainerPoint(i.getCenter()),i=i.layerPointToContainerPoint(t),s=this.options.direction,r=n.offsetWidth,a=n.offsetHeight,h=m(this.options.offset),l=this._getAnchor(),i="top"===s?(e=r/2,a):"bottom"===s?(e=r/2,0):(e="center"===s?r/2:"right"===s?0:"left"===s?r:i.xthis.options.maxZoom||nthis.options.maxZoom||void 0!==this.options.minZoom&&oi.max.x)||!e.wrapLat&&(t.yi.max.y))return!1}return!this.options.bounds||(e=this._tileCoordsToBounds(t),g(this.options.bounds).overlaps(e))},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var e=this._map,i=this.getTileSize(),n=t.scaleBy(i),i=n.add(i);return[e.unproject(n,t.z),e.unproject(i,t.z)]},_tileCoordsToBounds:function(t){t=this._tileCoordsToNwSe(t),t=new s(t[0],t[1]);return t=this.options.noWrap?t:this._map.wrapLatLngBounds(t)},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var t=t.split(":"),e=new p(+t[0],+t[1]);return e.z=+t[2],e},_removeTile:function(t){var e=this._tiles[t];e&&(T(e.el),delete this._tiles[t],this.fire("tileunload",{tile:e.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){M(t,"leaflet-tile");var e=this.getTileSize();t.style.width=e.x+"px",t.style.height=e.y+"px",t.onselectstart=u,t.onmousemove=u,b.ielt9&&this.options.opacity<1&&C(t,this.options.opacity)},_addTile:function(t,e){var i=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),a(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&x(a(this._tileReady,this,t,null,o)),Z(o,i),this._tiles[n]={el:o,coords:t,current:!0},e.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,e,i){e&&this.fire("tileerror",{error:e,tile:i,coords:t});var n=this._tileCoordsToKey(t);(i=this._tiles[n])&&(i.loaded=+new Date,this._map._fadeAnimated?(C(i.el,0),r(this._fadeFrame),this._fadeFrame=x(this._updateOpacity,this)):(i.active=!0,this._pruneTiles()),e||(M(i.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:i.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),b.ielt9||!this._map._fadeAnimated?x(this._pruneTiles,this):setTimeout(a(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var e=new p(this._wrapX?H(t.x,this._wrapX):t.x,this._wrapY?H(t.y,this._wrapY):t.y);return e.z=t.z,e},_pxBoundsToTileRange:function(t){var e=this.getTileSize();return new f(t.min.unscaleBy(e).floor(),t.max.unscaleBy(e).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var Ni=Ri.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1,referrerPolicy:!1},initialize:function(t,e){this._url=t,(e=c(this,e)).detectRetina&&b.retina&&0')}}catch(t){}return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}(),zt={_initContainer:function(){this._container=P("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(Hi.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var e=t._container=Ui("shape");M(e,"leaflet-vml-shape "+(this.options.className||"")),e.coordsize="1 1",t._path=Ui("path"),e.appendChild(t._path),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){var e=t._container;this._container.appendChild(e),t.options.interactive&&t.addInteractiveTarget(e)},_removePath:function(t){var e=t._container;T(e),t.removeInteractiveTarget(e),delete this._layers[h(t)]},_updateStyle:function(t){var e=t._stroke,i=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(e=e||(t._stroke=Ui("stroke")),o.appendChild(e),e.weight=n.weight+"px",e.color=n.color,e.opacity=n.opacity,n.dashArray?e.dashStyle=d(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):e.dashStyle="",e.endcap=n.lineCap.replace("butt","flat"),e.joinstyle=n.lineJoin):e&&(o.removeChild(e),t._stroke=null),n.fill?(i=i||(t._fill=Ui("fill")),o.appendChild(i),i.color=n.fillColor||n.color,i.opacity=n.fillOpacity):i&&(o.removeChild(i),t._fill=null)},_updateCircle:function(t){var e=t._point.round(),i=Math.round(t._radius),n=Math.round(t._radiusY||i);this._setPath(t,t._empty()?"M0 0":"AL "+e.x+","+e.y+" "+i+","+n+" 0,23592600")},_setPath:function(t,e){t._path.v=e},_bringToFront:function(t){fe(t._container)},_bringToBack:function(t){ge(t._container)}},Vi=b.vml?Ui:ct,qi=Hi.extend({_initContainer:function(){this._container=Vi("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=Vi("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){T(this._container),k(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_update:function(){var t,e,i;this._map._animatingZoom&&this._bounds||(Hi.prototype._update.call(this),e=(t=this._bounds).getSize(),i=this._container,this._svgSize&&this._svgSize.equals(e)||(this._svgSize=e,i.setAttribute("width",e.x),i.setAttribute("height",e.y)),Z(i,t.min),i.setAttribute("viewBox",[t.min.x,t.min.y,e.x,e.y].join(" ")),this.fire("update"))},_initPath:function(t){var e=t._path=Vi("path");t.options.className&&M(e,t.options.className),t.options.interactive&&M(e,"leaflet-interactive"),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){T(t._path),t.removeInteractiveTarget(t._path),delete this._layers[h(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var e=t._path,t=t.options;e&&(t.stroke?(e.setAttribute("stroke",t.color),e.setAttribute("stroke-opacity",t.opacity),e.setAttribute("stroke-width",t.weight),e.setAttribute("stroke-linecap",t.lineCap),e.setAttribute("stroke-linejoin",t.lineJoin),t.dashArray?e.setAttribute("stroke-dasharray",t.dashArray):e.removeAttribute("stroke-dasharray"),t.dashOffset?e.setAttribute("stroke-dashoffset",t.dashOffset):e.removeAttribute("stroke-dashoffset")):e.setAttribute("stroke","none"),t.fill?(e.setAttribute("fill",t.fillColor||t.color),e.setAttribute("fill-opacity",t.fillOpacity),e.setAttribute("fill-rule",t.fillRule||"evenodd")):e.setAttribute("fill","none"))},_updatePoly:function(t,e){this._setPath(t,dt(t._parts,e))},_updateCircle:function(t){var e=t._point,i=Math.max(Math.round(t._radius),1),n="a"+i+","+(Math.max(Math.round(t._radiusY),1)||i)+" 0 1,0 ",e=t._empty()?"M0 0":"M"+(e.x-i)+","+e.y+n+2*i+",0 "+n+2*-i+",0 ";this._setPath(t,e)},_setPath:function(t,e){t._path.setAttribute("d",e)},_bringToFront:function(t){fe(t._path)},_bringToBack:function(t){ge(t._path)}});function Gi(t){return b.svg||b.vml?new qi(t):null}b.vml&&qi.include(zt),A.include({getRenderer:function(t){t=(t=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer)||(this._renderer=this._createRenderer());return this.hasLayer(t)||this.addLayer(t),t},_getPaneRenderer:function(t){var e;return"overlayPane"!==t&&void 0!==t&&(void 0===(e=this._paneRenderers[t])&&(e=this._createRenderer({pane:t}),this._paneRenderers[t]=e),e)},_createRenderer:function(t){return this.options.preferCanvas&&Wi(t)||Gi(t)}});var Ki=yi.extend({initialize:function(t,e){yi.prototype.initialize.call(this,this._boundsToLatLngs(t),e)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=g(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});qi.create=Vi,qi.pointsToPath=dt,xi.geometryToLayer=wi,xi.coordsToLatLng=Pi,xi.coordsToLatLngs=Li,xi.latLngToCoords=Ti,xi.latLngsToCoords=Mi,xi.getFeature=zi,xi.asFeature=Ci,A.mergeOptions({boxZoom:!0});var _t=n.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){S(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){k(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){T(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),re(),Le(),this._startPoint=this._map.mouseEventToContainerPoint(t),S(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=P("div","leaflet-zoom-box",this._container),M(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var t=new f(this._point,this._startPoint),e=t.getSize();Z(this._box,t.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(T(this._box),z(this._container,"leaflet-crosshair")),ae(),Te(),k(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){1!==t.which&&1!==t.button||(this._finish(),this._moved&&(this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(a(this._resetState,this),0),t=new s(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point)),this._map.fitBounds(t).fire("boxzoomend",{boxZoomBounds:t})))},_onKeyDown:function(t){27===t.keyCode&&(this._finish(),this._clearDeferredResetState(),this._resetState())}}),Ct=(A.addInitHook("addHandler","boxZoom",_t),A.mergeOptions({doubleClickZoom:!0}),n.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var e=this._map,i=e.getZoom(),n=e.options.zoomDelta,i=t.originalEvent.shiftKey?i-n:i+n;"center"===e.options.doubleClickZoom?e.setZoom(i):e.setZoomAround(t.containerPoint,i)}})),Zt=(A.addInitHook("addHandler","doubleClickZoom",Ct),A.mergeOptions({dragging:!0,inertia:!0,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0}),n.extend({addHooks:function(){var t;this._draggable||(t=this._map,this._draggable=new Je(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))),M(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){z(this._map._container,"leaflet-grab"),z(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t,e=this._map;e._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity?(t=g(this._map.options.maxBounds),this._offsetLimit=_(this._map.latLngToContainerPoint(t.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(t.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))):this._offsetLimit=null,e.fire("movestart").fire("dragstart"),e.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){var e,i;this._map.options.inertia&&(e=this._lastTime=+new Date,i=this._lastPos=this._draggable._absPos||this._draggable._newPos,this._positions.push(i),this._times.push(e),this._prunePositions(e)),this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1e.max.x&&(t.x=this._viscousLimit(t.x,e.max.x)),t.y>e.max.y&&(t.y=this._viscousLimit(t.y,e.max.y)),this._draggable._newPos=this._draggable._startPos.add(t))},_onPreDragWrap:function(){var t=this._worldWidth,e=Math.round(t/2),i=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-e+i)%t+e-i,n=(n+e+i)%t-e-i,t=Math.abs(o+i)e.getMaxZoom()&&1=i;)t=t.__parent;return this._currentShownBounds.contains(t.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(e,t):this._animationAddLayerNonAnimated(e,t)),this},removeLayer:function(e){return e instanceof L.LayerGroup?this.removeLayers([e]):e.getLatLng?this._map?e.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(e)),this._removeLayer(e,!0),this.fire("layerremove",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),e.off(this._childMarkerEventHandlers,this),this._featureGroup.hasLayer(e)&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow()),this):this:(!this._arraySplice(this._needsClustering,e)&&this.hasLayer(e)&&this._needsRemoving.push({layer:e,latlng:e._latlng}),this.fire("layerremove",{layer:e}),this):(this._nonPointGroup.removeLayer(e),this.fire("layerremove",{layer:e}),this)},addLayers:function(e,t){if(!L.Util.isArray(e))return this.addLayer(e);var i,n=this._featureGroup,r=this._nonPointGroup,s=this.options.chunkedLoading,o=this.options.chunkInterval,a=this.options.chunkProgress,h=e.length,l=0,u=!0;if(this._map){var _=(new Date).getTime(),d=L.bind(function(){for(var c=(new Date).getTime();h>l;l++){if(s&&0===l%200){var p=(new Date).getTime()-c;if(p>o)break}if(i=e[l],i instanceof L.LayerGroup)u&&(e=e.slice(),u=!1),this._extractNonGroupLayers(i,e),h=e.length;else if(i.getLatLng){if(!this.hasLayer(i)&&(this._addLayer(i,this._maxZoom),t||this.fire("layeradd",{layer:i}),i.__parent&&2===i.__parent.getChildCount())){var f=i.__parent.getAllChildMarkers(),m=f[0]===i?f[1]:f[0];n.removeLayer(m)}}else r.addLayer(i),t||this.fire("layeradd",{layer:i})}a&&a(l,h,(new Date).getTime()-_),l===h?(this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)):setTimeout(d,this.options.chunkDelay)},this);d()}else for(var c=this._needsClustering;h>l;l++)i=e[l],i instanceof L.LayerGroup?(u&&(e=e.slice(),u=!1),this._extractNonGroupLayers(i,e),h=e.length):i.getLatLng?this.hasLayer(i)||c.push(i):r.addLayer(i);return this},removeLayers:function(e){var t,i,n=e.length,r=this._featureGroup,s=this._nonPointGroup,o=!0;if(!this._map){for(t=0;n>t;t++)i=e[t],i instanceof L.LayerGroup?(o&&(e=e.slice(),o=!1),this._extractNonGroupLayers(i,e),n=e.length):(this._arraySplice(this._needsClustering,i),s.removeLayer(i),this.hasLayer(i)&&this._needsRemoving.push({layer:i,latlng:i._latlng}),this.fire("layerremove",{layer:i}));return this}if(this._unspiderfy){this._unspiderfy();var a=e.slice(),h=n;for(t=0;h>t;t++)i=a[t],i instanceof L.LayerGroup?(this._extractNonGroupLayers(i,a),h=a.length):this._unspiderfyLayer(i)}for(t=0;n>t;t++)i=e[t],i instanceof L.LayerGroup?(o&&(e=e.slice(),o=!1),this._extractNonGroupLayers(i,e),n=e.length):i.__parent?(this._removeLayer(i,!0,!0),this.fire("layerremove",{layer:i}),r.hasLayer(i)&&(r.removeLayer(i),i.clusterShow&&i.clusterShow())):(s.removeLayer(i),this.fire("layerremove",{layer:i}));return this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds),this},clearLayers:function(){return this._map||(this._needsClustering=[],this._needsRemoving=[],delete this._gridClusters,delete this._gridUnclustered),this._noanimationUnspiderfy&&this._noanimationUnspiderfy(),this._featureGroup.clearLayers(),this._nonPointGroup.clearLayers(),this.eachLayer(function(e){e.off(this._childMarkerEventHandlers,this),delete e.__parent},this),this._map&&this._generateInitialClusters(),this},getBounds:function(){var e=new L.LatLngBounds;this._topClusterLevel&&e.extend(this._topClusterLevel._bounds);for(var t=this._needsClustering.length-1;t>=0;t--)e.extend(this._needsClustering[t].getLatLng());return e.extend(this._nonPointGroup.getBounds()),e},eachLayer:function(e,t){var i,n,r,s=this._needsClustering.slice(),o=this._needsRemoving;for(this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(s),n=s.length-1;n>=0;n--){for(i=!0,r=o.length-1;r>=0;r--)if(o[r].layer===s[n]){i=!1;break}i&&e.call(t,s[n])}this._nonPointGroup.eachLayer(e,t)},getLayers:function(){var e=[];return this.eachLayer(function(t){e.push(t)}),e},getLayer:function(e){var t=null;return e=parseInt(e,10),this.eachLayer(function(i){L.stamp(i)===e&&(t=i)}),t},hasLayer:function(e){if(!e)return!1;var t,i=this._needsClustering;for(t=i.length-1;t>=0;t--)if(i[t]===e)return!0;for(i=this._needsRemoving,t=i.length-1;t>=0;t--)if(i[t].layer===e)return!1;return!(!e.__parent||e.__parent._group!==this)||this._nonPointGroup.hasLayer(e)},zoomToShowLayer:function(e,t){"function"!=typeof t&&(t=function(){});var i=function(){!e._icon&&!e.__parent._icon||this._inZoomAnimation||(this._map.off("moveend",i,this),this.off("animationend",i,this),e._icon?t():e.__parent._icon&&(this.once("spiderfied",t,this),e.__parent.spiderfy()))};e._icon&&this._map.getBounds().contains(e.getLatLng())?t():e.__parent._zoomt;t++)n=this._needsRemoving[t],n.newlatlng=n.layer._latlng,n.layer._latlng=n.latlng;for(t=0,i=this._needsRemoving.length;i>t;t++)n=this._needsRemoving[t],this._removeLayer(n.layer,!0),n.layer._latlng=n.newlatlng;this._needsRemoving=[],this._zoom=Math.round(this._map._zoom),this._currentShownBounds=this._getExpandedVisibleBounds(),this._map.on("zoomend",this._zoomEnd,this),this._map.on("moveend",this._moveEnd,this),this._spiderfierOnAdd&&this._spiderfierOnAdd(),this._bindEvents(),i=this._needsClustering,this._needsClustering=[],this.addLayers(i,!0)},onRemove:function(e){e.off("zoomend",this._zoomEnd,this),e.off("moveend",this._moveEnd,this),this._unbindEvents(),this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim",""),this._spiderfierOnRemove&&this._spiderfierOnRemove(),delete this._maxLat,this._hideCoverage(),this._featureGroup.remove(),this._nonPointGroup.remove(),this._featureGroup.clearLayers(),this._map=null},getVisibleParent:function(e){for(var t=e;t&&!t._icon;)t=t.__parent;return t||null},_arraySplice:function(e,t){for(var i=e.length-1;i>=0;i--)if(e[i]===t)return e.splice(i,1),!0},_removeFromGridUnclustered:function(e,t){for(var i=this._map,n=this._gridUnclustered,r=Math.floor(this._map.getMinZoom());t>=r&&n[t].removeObject(e,i.project(e.getLatLng(),t));t--);},_childMarkerDragStart:function(e){e.target.__dragStart=e.target._latlng},_childMarkerMoved:function(e){if(!this._ignoreMove&&!e.target.__dragStart){var t=e.target._popup&&e.target._popup.isOpen();this._moveChild(e.target,e.oldLatLng,e.latlng),t&&e.target.openPopup()}},_moveChild:function(e,t,i){e._latlng=t,this.removeLayer(e),e._latlng=i,this.addLayer(e)},_childMarkerDragEnd:function(e){var t=e.target.__dragStart;delete e.target.__dragStart,t&&this._moveChild(e.target,t,e.target._latlng)},_removeLayer:function(e,t,i){var n=this._gridClusters,r=this._gridUnclustered,s=this._featureGroup,o=this._map,a=Math.floor(this._map.getMinZoom());t&&this._removeFromGridUnclustered(e,this._maxZoom);var h,l=e.__parent,u=l._markers;for(this._arraySplice(u,e);l&&(l._childCount--,l._boundsNeedUpdate=!0,!(l._zoomt?"small":100>t?"medium":"large",new L.DivIcon({html:"
"+t+"
",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var e=this._map,t=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick;(t||n)&&this.on("clusterclick",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),e.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(e){for(var t=e.layer,i=t;1===i._childClusters.length;)i=i._childClusters[0];i._zoom===this._maxZoom&&i._childCount===t._childCount&&this.options.spiderfyOnMaxZoom?t.spiderfy():this.options.zoomToBoundsOnClick&&t.zoomToBounds(),e.originalEvent&&13===e.originalEvent.keyCode&&this._map._container.focus()},_showCoverage:function(e){var t=this._map;this._inZoomAnimation||(this._shownPolygon&&t.removeLayer(this._shownPolygon),e.layer.getChildCount()>2&&e.layer!==this._spiderfied&&(this._shownPolygon=new L.Polygon(e.layer.getConvexHull(),this.options.polygonOptions),t.addLayer(this._shownPolygon)))},_hideCoverage:function(){this._shownPolygon&&(this._map.removeLayer(this._shownPolygon),this._shownPolygon=null)},_unbindEvents:function(){var e=this.options.spiderfyOnMaxZoom,t=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick,n=this._map;(e||i)&&this.off("clusterclick",this._zoomOrSpiderfy,this),t&&(this.off("clustermouseover",this._showCoverage,this),this.off("clustermouseout",this._hideCoverage,this),n.off("zoomend",this._hideCoverage,this))},_zoomEnd:function(){this._map&&(this._mergeSplitClusters(),this._zoom=Math.round(this._map._zoom),this._currentShownBounds=this._getExpandedVisibleBounds())},_moveEnd:function(){if(!this._inZoomAnimation){var e=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,Math.floor(this._map.getMinZoom()),this._zoom,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,Math.round(this._map._zoom),e),this._currentShownBounds=e}},_generateInitialClusters:function(){var e=Math.ceil(this._map.getMaxZoom()),t=Math.floor(this._map.getMinZoom()),i=this.options.maxClusterRadius,n=i;"function"!=typeof i&&(n=function(){return i}),null!==this.options.disableClusteringAtZoom&&(e=this.options.disableClusteringAtZoom-1),this._maxZoom=e,this._gridClusters={},this._gridUnclustered={};for(var r=e;r>=t;r--)this._gridClusters[r]=new L.DistanceGrid(n(r)),this._gridUnclustered[r]=new L.DistanceGrid(n(r));this._topClusterLevel=new this._markerCluster(this,t-1)},_addLayer:function(e,t){var i,n,r=this._gridClusters,s=this._gridUnclustered,o=Math.floor(this._map.getMinZoom());for(this.options.singleMarkerMode&&this._overrideMarkerIcon(e),e.on(this._childMarkerEventHandlers,this);t>=o;t--){i=this._map.project(e.getLatLng(),t);var a=r[t].getNearObject(i);if(a)return a._addChild(e),e.__parent=a,void 0;if(a=s[t].getNearObject(i)){var h=a.__parent;h&&this._removeLayer(a,!1);var l=new this._markerCluster(this,t,a,e);r[t].addObject(l,this._map.project(l._cLatLng,t)),a.__parent=l,e.__parent=l;var u=l;for(n=t-1;n>h._zoom;n--)u=new this._markerCluster(this,n,u),r[n].addObject(u,this._map.project(a.getLatLng(),n));return h._addChild(u),this._removeFromGridUnclustered(a,t),void 0}s[t].addObject(e,i)}this._topClusterLevel._addChild(e),e.__parent=this._topClusterLevel},_refreshClustersIcons:function(){this._featureGroup.eachLayer(function(e){e instanceof L.MarkerCluster&&e._iconNeedsUpdate&&e._updateIcon()})},_enqueue:function(e){this._queue.push(e),this._queueTimeout||(this._queueTimeout=setTimeout(L.bind(this._processQueue,this),300))},_processQueue:function(){for(var e=0;ee?(this._animationStart(),this._animationZoomOut(this._zoom,e)):this._moveEnd()},_getExpandedVisibleBounds:function(){return this.options.removeOutsideVisibleBounds?L.Browser.mobile?this._checkBoundsMaxLat(this._map.getBounds()):this._checkBoundsMaxLat(this._map.getBounds().pad(1)):this._mapBoundsInfinite},_checkBoundsMaxLat:function(e){var t=this._maxLat;return void 0!==t&&(e.getNorth()>=t&&(e._northEast.lat=1/0),e.getSouth()<=-t&&(e._southWest.lat=-1/0)),e},_animationAddLayerNonAnimated:function(e,t){if(t===e)this._featureGroup.addLayer(e);else if(2===t._childCount){t._addToMap();var i=t.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else t._updateIcon()},_extractNonGroupLayers:function(e,t){var i,n=e.getLayers(),r=0;for(t=t||[];r=0;i--)o=h[i],n.contains(o._latlng)||r.removeLayer(o)}),this._forceLayout(),this._topClusterLevel._recursivelyBecomeVisible(n,t),r.eachLayer(function(e){e instanceof L.MarkerCluster||!e._icon||e.clusterShow()}),this._topClusterLevel._recursively(n,e,t,function(e){e._recursivelyRestoreChildPositions(t)}),this._ignoreMove=!1,this._enqueue(function(){this._topClusterLevel._recursively(n,e,s,function(e){r.removeLayer(e),e.clusterShow()}),this._animationEnd()})},_animationZoomOut:function(e,t){this._animationZoomOutSingle(this._topClusterLevel,e-1,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,Math.floor(this._map.getMinZoom()),e,this._getExpandedVisibleBounds())},_animationAddLayer:function(e,t){var i=this,n=this._featureGroup;n.addLayer(e),t!==e&&(t._childCount>2?(t._updateIcon(),this._forceLayout(),this._animationStart(),e._setPos(this._map.latLngToLayerPoint(t.getLatLng())),e.clusterHide(),this._enqueue(function(){n.removeLayer(e),e.clusterShow(),i._animationEnd()})):(this._forceLayout(),i._animationStart(),i._animationZoomOutSingle(t,this._map.getMaxZoom(),this._zoom)))}},_animationZoomOutSingle:function(e,t,i){var n=this._getExpandedVisibleBounds(),r=Math.floor(this._map.getMinZoom());e._recursivelyAnimateChildrenInAndAddSelfToMap(n,r,t+1,i);var s=this;this._forceLayout(),e._recursivelyBecomeVisible(n,i),this._enqueue(function(){if(1===e._childCount){var o=e._markers[0];this._ignoreMove=!0,o.setLatLng(o.getLatLng()),this._ignoreMove=!1,o.clusterShow&&o.clusterShow()}else e._recursively(n,i,r,function(e){e._recursivelyRemoveChildrenFromMap(n,r,t+1)});s._animationEnd()})},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_forceLayout:function(){L.Util.falseFn(document.body.offsetWidth)}}),L.markerClusterGroup=function(e){return new L.MarkerClusterGroup(e)};var i=L.MarkerCluster=L.Marker.extend({options:L.Icon.prototype.options,initialize:function(e,t,i,n){L.Marker.prototype.initialize.call(this,i?i._cLatLng||i.getLatLng():new L.LatLng(0,0),{icon:this,pane:e.options.clusterPane}),this._group=e,this._zoom=t,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._bounds=new L.LatLngBounds,i&&this._addChild(i),n&&this._addChild(n)},getAllChildMarkers:function(e,t){e=e||[];for(var i=this._childClusters.length-1;i>=0;i--)this._childClusters[i].getAllChildMarkers(e);for(var n=this._markers.length-1;n>=0;n--)t&&this._markers[n].__dragStart||e.push(this._markers[n]);return e},getChildCount:function(){return this._childCount},zoomToBounds:function(e){for(var t,i=this._childClusters.slice(),n=this._group._map,r=n.getBoundsZoom(this._bounds),s=this._zoom+1,o=n.getZoom();i.length>0&&r>s;){s++;var a=[];for(t=0;ts?this._group._map.setView(this._latlng,s):o>=r?this._group._map.setView(this._latlng,o+1):this._group._map.fitBounds(this._bounds,e)},getBounds:function(){var e=new L.LatLngBounds;return e.extend(this._bounds),e},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(e,t){this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._setClusterCenter(e),e instanceof L.MarkerCluster?(t||(this._childClusters.push(e),e.__parent=this),this._childCount+=e._childCount):(t||this._markers.push(e),this._childCount++),this.__parent&&this.__parent._addChild(e,!0)},_setClusterCenter:function(e){this._cLatLng||(this._cLatLng=e._cLatLng||e._latlng)},_resetBounds:function(){var e=this._bounds;e._southWest&&(e._southWest.lat=1/0,e._southWest.lng=1/0),e._northEast&&(e._northEast.lat=-1/0,e._northEast.lng=-1/0)},_recalculateBounds:function(){var e,t,i,n,r=this._markers,s=this._childClusters,o=0,a=0,h=this._childCount;if(0!==h){for(this._resetBounds(),e=0;e=0;i--)n=r[i],n._icon&&(n._setPos(t),n.clusterHide())},function(e){var i,n,r=e._childClusters;for(i=r.length-1;i>=0;i--)n=r[i],n._icon&&(n._setPos(t),n.clusterHide())})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(e,t,i,n){this._recursively(e,n,t,function(r){r._recursivelyAnimateChildrenIn(e,r._group._map.latLngToLayerPoint(r.getLatLng()).round(),i),r._isSingleParent()&&i-1===n?(r.clusterShow(),r._recursivelyRemoveChildrenFromMap(e,t,i)):r.clusterHide(),r._addToMap()})},_recursivelyBecomeVisible:function(e,t){this._recursively(e,this._group._map.getMinZoom(),t,null,function(e){e.clusterShow()})},_recursivelyAddChildrenToMap:function(e,t,i){this._recursively(i,this._group._map.getMinZoom()-1,t,function(n){if(t!==n._zoom)for(var r=n._markers.length-1;r>=0;r--){var s=n._markers[r];i.contains(s._latlng)&&(e&&(s._backupLatlng=s.getLatLng(),s.setLatLng(e),s.clusterHide&&s.clusterHide()),n._group._featureGroup.addLayer(s))}},function(t){t._addToMap(e)})},_recursivelyRestoreChildPositions:function(e){for(var t=this._markers.length-1;t>=0;t--){var i=this._markers[t];i._backupLatlng&&(i.setLatLng(i._backupLatlng),delete i._backupLatlng)}if(e-1===this._zoom)for(var n=this._childClusters.length-1;n>=0;n--)this._childClusters[n]._restorePosition();else for(var r=this._childClusters.length-1;r>=0;r--)this._childClusters[r]._recursivelyRestoreChildPositions(e)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(e,t,i,n){var r,s;this._recursively(e,t-1,i-1,function(e){for(s=e._markers.length-1;s>=0;s--)r=e._markers[s],n&&n.contains(r._latlng)||(e._group._featureGroup.removeLayer(r),r.clusterShow&&r.clusterShow())},function(e){for(s=e._childClusters.length-1;s>=0;s--)r=e._childClusters[s],n&&n.contains(r._latlng)||(e._group._featureGroup.removeLayer(r),r.clusterShow&&r.clusterShow())})},_recursively:function(e,t,i,n,r){var s,o,a=this._childClusters,h=this._zoom;if(h>=t&&(n&&n(this),r&&h===i&&r(this)),t>h||i>h)for(s=a.length-1;s>=0;s--)o=a[s],o._boundsNeedUpdate&&o._recalculateBounds(),e.intersects(o._bounds)&&o._recursively(e,t,i,n,r)},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}});L.Marker.include({clusterHide:function(){var e=this.options.opacity;return this.setOpacity(0),this.options.opacity=e,this},clusterShow:function(){return this.setOpacity(this.options.opacity)}}),L.DistanceGrid=function(e){this._cellSize=e,this._sqCellSize=e*e,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(e,t){var i=this._getCoord(t.x),n=this._getCoord(t.y),r=this._grid,s=r[n]=r[n]||{},o=s[i]=s[i]||[],a=L.Util.stamp(e);this._objectPoint[a]=t,o.push(e)},updateObject:function(e,t){this.removeObject(e),this.addObject(e,t)},removeObject:function(e,t){var i,n,r=this._getCoord(t.x),s=this._getCoord(t.y),o=this._grid,a=o[s]=o[s]||{},h=a[r]=a[r]||[];for(delete this._objectPoint[L.Util.stamp(e)],i=0,n=h.length;n>i;i++)if(h[i]===e)return h.splice(i,1),1===n&&delete a[r],!0},eachObject:function(e,t){var i,n,r,s,o,a,h,l=this._grid;for(i in l){o=l[i];for(n in o)for(a=o[n],r=0,s=a.length;s>r;r++)h=e.call(t,a[r]),h&&(r--,s--)}},getNearObject:function(e){var t,i,n,r,s,o,a,h,l=this._getCoord(e.x),u=this._getCoord(e.y),_=this._objectPoint,d=this._sqCellSize,c=null;for(t=u-1;u+1>=t;t++)if(r=this._grid[t])for(i=l-1;l+1>=i;i++)if(s=r[i])for(n=0,o=s.length;o>n;n++)a=s[n],h=this._sqDist(_[L.Util.stamp(a)],e),(d>h||d>=h&&null===c)&&(d=h,c=a);return c},_getCoord:function(e){var t=Math.floor(e/this._cellSize);return isFinite(t)?t:e},_sqDist:function(e,t){var i=t.x-e.x,n=t.y-e.y;return i*i+n*n}},function(){L.QuickHull={getDistant:function(e,t){var i=t[1].lat-t[0].lat,n=t[0].lng-t[1].lng;return n*(e.lat-t[0].lat)+i*(e.lng-t[0].lng)},findMostDistantPointFromBaseLine:function(e,t){var i,n,r,s=0,o=null,a=[];for(i=t.length-1;i>=0;i--)n=t[i],r=this.getDistant(n,e),r>0&&(a.push(n),r>s&&(s=r,o=n));return{maxPoint:o,newPoints:a}},buildConvexHull:function(e,t){var i=[],n=this.findMostDistantPointFromBaseLine(e,t);return n.maxPoint?(i=i.concat(this.buildConvexHull([e[0],n.maxPoint],n.newPoints)),i=i.concat(this.buildConvexHull([n.maxPoint,e[1]],n.newPoints))):[e[0]]},getConvexHull:function(e){var t,i=!1,n=!1,r=!1,s=!1,o=null,a=null,h=null,l=null,u=null,_=null;for(t=e.length-1;t>=0;t--){var d=e[t];(i===!1||d.lat>i)&&(o=d,i=d.lat),(n===!1||d.latr)&&(h=d,r=d.lng),(s===!1||d.lng=0;t--)e=i[t].getLatLng(),n.push(e);return L.QuickHull.getConvexHull(n)}}),L.MarkerCluster.include({_2PI:2*Math.PI,_circleFootSeparation:25,_circleStartAngle:0,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied!==this&&!this._group._inZoomAnimation){var e,t=this.getAllChildMarkers(null,!0),i=this._group,n=i._map,r=n.latLngToLayerPoint(this._latlng);this._group._unspiderfy(),this._group._spiderfied=this,t.length>=this._circleSpiralSwitchover?e=this._generatePointsSpiral(t.length,r):(r.y+=10,e=this._generatePointsCircle(t.length,r)),this._animationSpiderfy(t,e)}},unspiderfy:function(e){this._group._inZoomAnimation||(this._animationUnspiderfy(e),this._group._spiderfied=null)},_generatePointsCircle:function(e,t){var i,n,r=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+e),s=r/this._2PI,o=this._2PI/e,a=[];for(s=Math.max(s,35),a.length=e,i=0;e>i;i++)n=this._circleStartAngle+i*o,a[i]=new L.Point(t.x+s*Math.cos(n),t.y+s*Math.sin(n))._round();return a},_generatePointsSpiral:function(e,t){var i,n=this._group.options.spiderfyDistanceMultiplier,r=n*this._spiralLengthStart,s=n*this._spiralFootSeparation,o=n*this._spiralLengthFactor*this._2PI,a=0,h=[];for(h.length=e,i=e;i>=0;i--)e>i&&(h[i]=new L.Point(t.x+r*Math.cos(a),t.y+r*Math.sin(a))._round()),a+=s/r+5e-4*i,r+=o/a;return h},_noanimationUnspiderfy:function(){var e,t,i=this._group,n=i._map,r=i._featureGroup,s=this.getAllChildMarkers(null,!0);for(i._ignoreMove=!0,this.setOpacity(1),t=s.length-1;t>=0;t--)e=s[t],r.removeLayer(e),e._preSpiderfyLatlng&&(e.setLatLng(e._preSpiderfyLatlng),delete e._preSpiderfyLatlng),e.setZIndexOffset&&e.setZIndexOffset(0),e._spiderLeg&&(n.removeLayer(e._spiderLeg),delete e._spiderLeg);i.fire("unspiderfied",{cluster:this,markers:s}),i._ignoreMove=!1,i._spiderfied=null}}),L.MarkerClusterNonAnimated=L.MarkerCluster.extend({_animationSpiderfy:function(e,t){var i,n,r,s,o=this._group,a=o._map,h=o._featureGroup,l=this._group.options.spiderLegPolylineOptions;for(o._ignoreMove=!0,i=0;i=0;i--)a=u.layerPointToLatLng(t[i]),n=e[i],n._preSpiderfyLatlng=n._latlng,n.setLatLng(a),n.clusterShow&&n.clusterShow(),p&&(r=n._spiderLeg,s=r._path,s.style.strokeDashoffset=0,r.setStyle({opacity:m}));this.setOpacity(.3),l._ignoreMove=!1,setTimeout(function(){l._animationEnd(),l.fire("spiderfied",{cluster:h,markers:e})},200)},_animationUnspiderfy:function(e){var t,i,n,r,s,o,a=this,h=this._group,l=h._map,u=h._featureGroup,_=e?l._latLngToNewLayerPoint(this._latlng,e.zoom,e.center):l.latLngToLayerPoint(this._latlng),d=this.getAllChildMarkers(null,!0),c=L.Path.SVG;for(h._ignoreMove=!0,h._animationStart(),this.setOpacity(1),i=d.length-1;i>=0;i--)t=d[i],t._preSpiderfyLatlng&&(t.closePopup(),t.setLatLng(t._preSpiderfyLatlng),delete t._preSpiderfyLatlng,o=!0,t._setPos&&(t._setPos(_),o=!1),t.clusterHide&&(t.clusterHide(),o=!1),o&&u.removeLayer(t),c&&(n=t._spiderLeg,r=n._path,s=r.getTotalLength()+.1,r.style.strokeDashoffset=s,n.setStyle({opacity:0})));h._ignoreMove=!1,setTimeout(function(){var e=0;for(i=d.length-1;i>=0;i--)t=d[i],t._spiderLeg&&e++;for(i=d.length-1;i>=0;i--)t=d[i],t._spiderLeg&&(t.clusterShow&&t.clusterShow(),t.setZIndexOffset&&t.setZIndexOffset(0),e>1&&u.removeLayer(t),l.removeLayer(t._spiderLeg),delete t._spiderLeg);h._animationEnd(),h.fire("unspiderfied",{cluster:a,markers:d})},200)}}),L.MarkerClusterGroup.include({_spiderfied:null,unspiderfy:function(){this._unspiderfy.apply(this,arguments)},_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation&&this._map.on("zoomstart",this._unspiderfyZoomStart,this),this._map.on("zoomend",this._noanimationUnspiderfy,this),L.Browser.touch||this._map.getRenderer(this)},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._map.off("zoomend",this._noanimationUnspiderfy,this),this._noanimationUnspiderfy() +},_unspiderfyZoomStart:function(){this._map&&this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(e){L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching")||(this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(e))},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(e){this._spiderfied&&this._spiderfied.unspiderfy(e)},_noanimationUnspiderfy:function(){this._spiderfied&&this._spiderfied._noanimationUnspiderfy()},_unspiderfyLayer:function(e){e._spiderLeg&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow(),e.setZIndexOffset&&e.setZIndexOffset(0),this._map.removeLayer(e._spiderLeg),delete e._spiderLeg)}}),L.MarkerClusterGroup.include({refreshClusters:function(e){return e?e instanceof L.MarkerClusterGroup?e=e._topClusterLevel.getAllChildMarkers():e instanceof L.LayerGroup?e=e._layers:e instanceof L.MarkerCluster?e=e.getAllChildMarkers():e instanceof L.Marker&&(e=[e]):e=this._topClusterLevel.getAllChildMarkers(),this._flagParentsIconsNeedUpdate(e),this._refreshClustersIcons(),this.options.singleMarkerMode&&this._refreshSingleMarkerModeMarkers(e),this},_flagParentsIconsNeedUpdate:function(e){var t,i;for(t in e)for(i=e[t].__parent;i;)i._iconNeedsUpdate=!0,i=i.__parent},_refreshSingleMarkerModeMarkers:function(e){var t,i;for(t in e)i=e[t],this.hasLayer(i)&&i.setIcon(this._overrideMarkerIcon(i))}}),L.Marker.include({refreshIconOptions:function(e,t){var i=this.options.icon;return L.setOptions(i,e),this.setIcon(i),t&&this.__parent&&this.__parent._group.refreshClusters(this),this}}),e.MarkerClusterGroup=t,e.MarkerCluster=i}); +//# sourceMappingURL=leaflet.markercluster.js.map \ No newline at end of file diff --git a/sut/frontend/build/leaflet/marker_cluster/leaflet.markercluster.js.map b/sut/frontend/build/leaflet/marker_cluster/leaflet.markercluster.js.map new file mode 100644 index 0000000..a4b459c --- /dev/null +++ b/sut/frontend/build/leaflet/marker_cluster/leaflet.markercluster.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/MarkerClusterGroup.js","../src/MarkerCluster.js","../src/MarkerOpacity.js","../src/DistanceGrid.js","../src/MarkerCluster.QuickHull.js","../src/MarkerCluster.Spiderfier.js","../src/MarkerClusterGroup.Refresh.js"],"names":[],"mappings":"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","file":"dist/leaflet.markercluster.js"} \ No newline at end of file diff --git a/sut/frontend/build/leaflet/smooth_bounce/BouncingMotion.js b/sut/frontend/build/leaflet/smooth_bounce/BouncingMotion.js new file mode 100644 index 0000000..b57377e --- /dev/null +++ b/sut/frontend/build/leaflet/smooth_bounce/BouncingMotion.js @@ -0,0 +1,307 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +var _leaflet = require("leaflet"); + +var _BouncingOptions = _interopRequireDefault(require("./BouncingOptions.js")); + +var _Cache = _interopRequireDefault(require("./Cache.js")); + +var _Styles = _interopRequireDefault(require("./Styles.js")); + +function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : {"default": obj}; +} + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; +} + +function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, {value: value, enumerable: true, configurable: true, writable: true}); + } else { + obj[key] = value; + } + return obj; +} + +var bonceEndEvent = 'bounceend'; + +var BouncingMotion = /*#__PURE__*/function () { + // TODO: check if this cache working right (keys don't need prefix) + + /** + * Constructor. + * + * @param marker {Marker} marker + * @param position {Point} marker current position on the map canvas + * @param bouncingOptions {BouncingOptions} options of bouncing animation + */ + function BouncingMotion(marker, position, bouncingOptions) { + _classCallCheck(this, BouncingMotion); + + _defineProperty(this, "marker", void 0); + + _defineProperty(this, "position", void 0); + + _defineProperty(this, "bouncingOptions", void 0); + + _defineProperty(this, "moveSteps", void 0); + + _defineProperty(this, "moveDelays", void 0); + + _defineProperty(this, "resizeSteps", void 0); + + _defineProperty(this, "resizeDelays", void 0); + + _defineProperty(this, "isBouncing", false); + + _defineProperty(this, "iconStyles", void 0); + + _defineProperty(this, "shadowStyles", void 0); + + _defineProperty(this, "bouncingAnimationPlaying", false); + + this.marker = marker; + this.position = position; + this.updateBouncingOptions(bouncingOptions); + } + + _createClass(BouncingMotion, [{ + key: "updateBouncingOptions", + value: function updateBouncingOptions(options) { + this.bouncingOptions = options instanceof _BouncingOptions["default"] ? options : this.bouncingOptions.override(options); + var _this$bouncingOptions = this.bouncingOptions, + bounceHeight = _this$bouncingOptions.bounceHeight, + bounceSpeed = _this$bouncingOptions.bounceSpeed, + elastic = _this$bouncingOptions.elastic; + this.moveSteps = BouncingMotion.cache.get("moveSteps_".concat(bounceHeight), function () { + return BouncingMotion.calculateSteps(bounceHeight); + }); + this.moveDelays = BouncingMotion.cache.get("moveDelays_".concat(bounceHeight, "_").concat(bounceSpeed), function () { + return BouncingMotion.calculateDelays(bounceHeight, bounceSpeed); + }); + + if (elastic) { + var _this$bouncingOptions2 = this.bouncingOptions, + contractHeight = _this$bouncingOptions2.contractHeight, + contractSpeed = _this$bouncingOptions2.contractSpeed; + this.resizeSteps = BouncingMotion.cache.get("resizeSteps_".concat(contractHeight), function () { + return BouncingMotion.calculateSteps(contractHeight); + }); + this.resizeDelays = BouncingMotion.cache.get("resizeDelays_".concat(contractHeight, "_").concat(contractSpeed), function () { + return BouncingMotion.calculateDelays(contractHeight, contractSpeed); + }); + } + + this.recalculateMotion(this.position); + } + }, { + key: "resetStyles", + value: function resetStyles(marker) { + this.iconStyles = _Styles["default"].ofMarker(marker); + + if (marker._shadow) { + this.shadowStyles = _Styles["default"].parse(marker._shadow.style.cssText); + } + } + /** + * Recalculates bouncing motion for new marker position. + * @param position {Point} new marker position + */ + + }, { + key: "recalculateMotion", + value: function recalculateMotion(position) { + this.position = position; + } + /** + * @param times {number|null} + */ + + }, { + key: "bounce", + value: function bounce() { + var times = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + + if (this.bouncingAnimationPlaying) { + this.isBouncing = true; + return; + } + + this.isBouncing = true; + this.bouncingAnimationPlaying = true; + this.move(times); + } + }, { + key: "stopBouncing", + value: function stopBouncing() { + this.isBouncing = false; + } + /** + * @param times {number|null} + */ + + }, { + key: "move", + value: function move() { + var _this = this; + + var times = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + + if (times !== null) { + if (!--times) { + this.isBouncing = false; // this is the last bouncing + + this.bouncingAnimationPlaying = false; + } + } + /* Launch timeouts for every step of the movement animation */ + + + var i = this.moveSteps.length; + + while (i--) { + setTimeout(function (step) { + return _this.makeMoveStep(step); + }, this.moveDelays[i], this.moveSteps[i]); + } + + setTimeout(function () { + return _this.afterMove(times); + }, this.moveDelays[this.moveSteps.length - 1]); + } + }, { + key: "afterMove", + value: function afterMove(times) { + var _this2 = this; + + if (this.isBouncing) { + setTimeout(function () { + return _this2.move(times); + }, this.bouncingOptions.bounceSpeed); + } else { + this.bouncingAnimationPlaying = false; + this.marker.fire(bonceEndEvent); + } + } + /** + * @param step {number} + */ + + }, { + key: "makeMoveStep", + value: function makeMoveStep(step) { + this.marker._icon.style.cssText = this.iconStyles.toString(); + + if (this.marker._shadow) { + this.marker._shadow.style.cssText = this.shadowStyles.toString(); + } + } + /** + * Returns calculated array of animation steps. This function used to calculate both movement + * and resizing animations. + * + * @param height {number} height of movement or resizing (px) + * + * @return {number[]} array of animation steps + */ + + }], [{ + key: "calculateSteps", + value: function calculateSteps(height) { + /* Calculate the sequence of animation steps: + * steps = [1 .. height] concat [height-1 .. 0] + */ + var i = 1; + var steps = []; + + while (i <= height) { + steps.push(i++); + } + + i = height; + + while (i--) { + steps.push(i); + } + + return steps; + } + /** + * Returns calculated array of delays between animation start and the steps of animation. This + * function used to calculate both movement and resizing animations. Element with index i of + * this array contains the delay in milliseconds between animation start and the step number i. + * + * @param height {number} height of movement or resizing (px) + * @param speed {number} speed coefficient + * + * @return {number[]} array of delays before steps of animation + */ + + }, { + key: "calculateDelays", + value: function calculateDelays(height, speed) { + // Calculate delta time for bouncing animation + // Delta time to movement in one direction + var deltas = []; // time between steps of animation + + deltas[height] = speed; + deltas[0] = 0; + var i = height; + + while (--i) { + deltas[i] = Math.round(speed / (height - i)); + } // Delta time for movement in two directions + + + i = height; + + while (i--) { + deltas.push(deltas[i]); + } // Calculate move delays (cumulated deltas) + // TODO: instead of deltas.lenght write bounceHeight * 2 - 1 + + + var delays = []; // delays before steps from beginning of animation + + var totalDelay = 0; + + for (i = 0; i < deltas.length; i++) { + totalDelay += deltas[i]; + delays.push(totalDelay); + } + + return delays; + } + }]); + + return BouncingMotion; +}(); + +exports["default"] = BouncingMotion; + +_defineProperty(BouncingMotion, "cache", new _Cache["default"]()); \ No newline at end of file diff --git a/sut/frontend/build/leaflet/smooth_bounce/BouncingMotion3D.js b/sut/frontend/build/leaflet/smooth_bounce/BouncingMotion3D.js new file mode 100644 index 0000000..e3a623c --- /dev/null +++ b/sut/frontend/build/leaflet/smooth_bounce/BouncingMotion3D.js @@ -0,0 +1,373 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +var _leaflet = require("leaflet"); + +var _BouncingMotion2 = _interopRequireDefault(require("./BouncingMotion.js")); + +var _Matrix3D = _interopRequireDefault(require("./Matrix3D.js")); + +var _line = require("./line.js"); + +function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : {"default": obj}; +} + +function _typeof(obj) { + "@babel/helpers - typeof"; + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + _typeof = function _typeof(obj) { + return typeof obj; + }; + } else { + _typeof = function _typeof(obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + } + return _typeof(obj); +} + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; +} + +function _get(target, property, receiver) { + if (typeof Reflect !== "undefined" && Reflect.get) { + _get = Reflect.get; + } else { + _get = function _get(target, property, receiver) { + var base = _superPropBase(target, property); + if (!base) return; + var desc = Object.getOwnPropertyDescriptor(base, property); + if (desc.get) { + return desc.get.call(receiver); + } + return desc.value; + }; + } + return _get(target, property, receiver || target); +} + +function _superPropBase(object, property) { + while (!Object.prototype.hasOwnProperty.call(object, property)) { + object = _getPrototypeOf(object); + if (object === null) break; + } + return object; +} + +function _inherits(subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function"); + } + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + writable: true, + configurable: true + } + }); + if (superClass) _setPrototypeOf(subClass, superClass); +} + +function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { + o.__proto__ = p; + return o; + }; + return _setPrototypeOf(o, p); +} + +function _createSuper(Derived) { + var hasNativeReflectConstruct = _isNativeReflectConstruct(); + return function _createSuperInternal() { + var Super = _getPrototypeOf(Derived), result; + if (hasNativeReflectConstruct) { + var NewTarget = _getPrototypeOf(this).constructor; + result = Reflect.construct(Super, arguments, NewTarget); + } else { + result = Super.apply(this, arguments); + } + return _possibleConstructorReturn(this, result); + }; +} + +function _possibleConstructorReturn(self, call) { + if (call && (_typeof(call) === "object" || typeof call === "function")) { + return call; + } + return _assertThisInitialized(self); +} + +function _assertThisInitialized(self) { + if (self === void 0) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + return self; +} + +function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) return false; + if (Reflect.construct.sham) return false; + if (typeof Proxy === "function") return true; + try { + Date.prototype.toString.call(Reflect.construct(Date, [], function () { + })); + return true; + } catch (e) { + return false; + } +} + +function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { + return o.__proto__ || Object.getPrototypeOf(o); + }; + return _getPrototypeOf(o); +} + +function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, {value: value, enumerable: true, configurable: true, writable: true}); + } else { + obj[key] = value; + } + return obj; +} + +var moveMatrixFormat = _Matrix3D["default"].identity().toFormat('d1', 'd2'); + +var resizeMatrixFormat = _Matrix3D["default"].identity().toFormat('b2', 'd1', 'd2'); + +var BouncingMotion3D = /*#__PURE__*/function (_BouncingMotion) { + _inherits(BouncingMotion3D, _BouncingMotion); + + var _super = _createSuper(BouncingMotion3D); + + /** + * Constructor. + * + * @param marker {Marker} marker + * @param position {Point} marker current position on the map canvas + * @param bouncingOptions {BouncingOptions} options of bouncing animation + */ + function BouncingMotion3D(marker, position, bouncingOptions) { + var _this; + + _classCallCheck(this, BouncingMotion3D); + + _this = _super.call(this, marker, position, bouncingOptions); + + _defineProperty(_assertThisInitialized(_this), "iconMoveTransforms", void 0); + + _defineProperty(_assertThisInitialized(_this), "iconResizeTransforms", void 0); + + _defineProperty(_assertThisInitialized(_this), "shadowMoveTransforms", void 0); + + _defineProperty(_assertThisInitialized(_this), "shadowResizeTransforms", void 0); + + _this.recalculateMotion(position); + + return _this; + } + + _createClass(BouncingMotion3D, [{ + key: "recalculateMotion", + value: function recalculateMotion(position) { + var _this$marker$getIcon, _this$marker$getIcon$, _this$marker, _this$marker$_iconObj, + _this$marker$_iconObj2; + + _get(_getPrototypeOf(BouncingMotion3D.prototype), "recalculateMotion", this).call(this, position); + + var iconHeight = ((_this$marker$getIcon = this.marker.getIcon()) === null || _this$marker$getIcon === void 0 ? void 0 : (_this$marker$getIcon$ = _this$marker$getIcon.options) === null || _this$marker$getIcon$ === void 0 ? void 0 : _this$marker$getIcon$.iconSize[1]) || ((_this$marker = this.marker) === null || _this$marker === void 0 ? void 0 : (_this$marker$_iconObj = _this$marker._iconObj) === null || _this$marker$_iconObj === void 0 ? void 0 : (_this$marker$_iconObj2 = _this$marker$_iconObj.options) === null || _this$marker$_iconObj2 === void 0 ? void 0 : _this$marker$_iconObj2.iconSize[1]); + var x = position.x, + y = position.y; + var _this$bouncingOptions = this.bouncingOptions, + bounceHeight = _this$bouncingOptions.bounceHeight, + contractHeight = _this$bouncingOptions.contractHeight, + shadowAngle = _this$bouncingOptions.shadowAngle; + this.iconMoveTransforms = BouncingMotion3D.calculateIconMoveTransforms(x, y, bounceHeight); + this.iconResizeTransforms = BouncingMotion3D.calculateResizeTransforms(x, y, iconHeight, contractHeight); + + if (this.marker._shadow) { + var _this$marker$getIcon2, _this$marker$getIcon3; + + this.shadowMoveTransforms = BouncingMotion3D.calculateShadowMoveTransforms(x, y, bounceHeight, shadowAngle); + var shadowHeight = (_this$marker$getIcon2 = this.marker.getIcon()) === null || _this$marker$getIcon2 === void 0 ? void 0 : (_this$marker$getIcon3 = _this$marker$getIcon2.options) === null || _this$marker$getIcon3 === void 0 ? void 0 : _this$marker$getIcon3.shadowSize[1]; + this.shadowResizeTransforms = BouncingMotion3D.calculateResizeTransforms(x, y, shadowHeight, contractHeight); + } + } + }, { + key: "afterMove", + value: function afterMove(times) { + if (this.bouncingOptions.elastic) { + this.resize(times); + } else { + _get(_getPrototypeOf(BouncingMotion3D.prototype), "afterMove", this).call(this, times); + } + } + }, { + key: "resize", + value: function resize(times) { + var _this2 = this; + + var nbResizeSteps = this.resizeSteps.length; + var i = nbResizeSteps; + + while (i--) { + setTimeout(function (step) { + return _this2.makeResizeStep(step); + }, this.resizeDelays[i], this.resizeSteps[i]); + } + + setTimeout(function () { + if (!_this2.isBouncing) { + _this2.bouncingAnimationPlaying = false; + } + }, this.resizeDelays[this.resizeSteps.length]); + setTimeout(function () { + if (_this2.isBouncing) { + _this2.move(times); + } else { + _this2.marker.fire('bounceend'); + } + }, this.resizeDelays[nbResizeSteps - 1]); + } + }, { + key: "makeMoveStep", + value: function makeMoveStep(step) { + this.marker._icon.style.cssText = this.iconStyles.withTransform(this.iconMoveTransforms[step]).toString(); + + if (this.marker._shadow) { + this.marker._shadow.style.cssText = this.shadowStyles.withTransform(this.shadowMoveTransforms[step]).toString(); + } + } + /** + * @param step {number} + */ + + }, { + key: "makeResizeStep", + value: function makeResizeStep(step) { + this.marker._icon.style.cssText = this.iconStyles.withTransform(this.iconResizeTransforms[step]).toString(); + + if (this.marker._shadow && this.bouncingOptions.shadowAngle) { + this.marker._shadow.style.cssText = this.shadowStyles.withTransform(this.shadowResizeTransforms[step]).toString(); + } + } + /** + * Returns calculated array of transformation definitions for the animation of icon movement. + * Function defines one transform for every pixel of shift of the icon from it's original y + * position. + * + * @param x {number} x coordinate of original position of the marker + * @param y {number} y coordinate of original position of the marker + * @param bounceHeight {number} height of bouncing (px) + * + * @return {string[]} array of transformation definitions + */ + + }], [{ + key: "calculateIconMoveTransforms", + value: function calculateIconMoveTransforms(x, y, bounceHeight) { + var transforms = []; + var deltaY = bounceHeight + 1; // Use fast inverse while loop to fill the array + + while (deltaY--) { + transforms[deltaY] = moveMatrixFormat(x, y - deltaY); + } + + return transforms; + } + /** + * Returns calculated array of transformation definitions for the animation of icon resizing. + * Function defines one transform for every pixel of resizing of marker from it's original + * height. + * + * @param x {number} x coordinate of original position of marker + * @param y {number} y coordinate of original position of marker + * @param height {number} original marker height (px) + * @param contractHeight {number} height of marker contraction (px) + * + * @return {string[]} array of transformation definitions + */ + + }, { + key: "calculateResizeTransforms", + value: function calculateResizeTransforms(x, y, height, contractHeight) { + var transforms = []; + var deltaHeight = contractHeight + 1; // Use fast inverse while loop to fill the array + + while (deltaHeight--) { + transforms[deltaHeight] = resizeMatrixFormat((height - deltaHeight) / height, x, y + deltaHeight); + } + + return transforms; + } + /** + * Returns calculated array of transformation definitions for the animation of shadow movement. + * Function defines one transform for every pixel of shift of the shadow from it's original + * position. + * + * @param x {number} x coordinate of original position of marker + * @param y {number} y coordinate of original position of marker + * @param bounceHeight {number} height of bouncing (px) + * @param angle {number|null} shadow inclination angle, if null shadow don't moves from it's + * initial position (radians) + * + * @return {string[]} array of transformation definitions + */ + + }, { + key: "calculateShadowMoveTransforms", + value: function calculateShadowMoveTransforms(x, y, bounceHeight) { + var angle = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; + // TODO: check this method to know if bounceHeight + 1 is normal + var transforms = []; + var deltaY = bounceHeight + 1; + var points = []; + + if (angle != null) { + // important: 0 is not null + points = (0, _line.calculateLine)(x, y, angle, bounceHeight + 1); + } else { + for (var i = 0; i <= bounceHeight; i++) { + points[i] = [x, y]; + } + } // Use fast inverse while loop to fill the array + + + while (deltaY--) { + transforms[deltaY] = moveMatrixFormat(points[deltaY][0], points[deltaY][1]); + } + + return transforms; + } + }]); + + return BouncingMotion3D; +}(_BouncingMotion2["default"]); + +exports["default"] = BouncingMotion3D; \ No newline at end of file diff --git a/sut/frontend/build/leaflet/smooth_bounce/BouncingMotionCss3.js b/sut/frontend/build/leaflet/smooth_bounce/BouncingMotionCss3.js new file mode 100644 index 0000000..9d2bb65 --- /dev/null +++ b/sut/frontend/build/leaflet/smooth_bounce/BouncingMotionCss3.js @@ -0,0 +1,481 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +var _leaflet = require("leaflet"); + +var _line = require("./line.js"); + +require("./bouncing.css"); + +var _BouncingOptions = _interopRequireDefault(require("./BouncingOptions.js")); + +var _Styles = _interopRequireDefault(require("./Styles.js")); + +function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : {"default": obj}; +} + +function _slicedToArray(arr, i) { + return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); +} + +function _nonIterableRest() { + throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); +} + +function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === "string") return _arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === "Object" && o.constructor) n = o.constructor.name; + if (n === "Map" || n === "Set") return Array.from(o); + if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); +} + +function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + for (var i = 0, arr2 = new Array(len); i < len; i++) { + arr2[i] = arr[i]; + } + return arr2; +} + +function _iterableToArrayLimit(arr, i) { + var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; + if (_i == null) return; + var _arr = []; + var _n = true; + var _d = false; + var _s, _e; + try { + for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"] != null) _i["return"](); + } finally { + if (_d) throw _e; + } + } + return _arr; +} + +function _arrayWithHoles(arr) { + if (Array.isArray(arr)) return arr; +} + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + Object.defineProperty(Constructor, "prototype", {writable: false}); + return Constructor; +} + +function _classPrivateFieldInitSpec(obj, privateMap, value) { + _checkPrivateRedeclaration(obj, privateMap); + privateMap.set(obj, value); +} + +function _checkPrivateRedeclaration(obj, privateCollection) { + if (privateCollection.has(obj)) { + throw new TypeError("Cannot initialize the same private elements twice on an object"); + } +} + +function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, {value: value, enumerable: true, configurable: true, writable: true}); + } else { + obj[key] = value; + } + return obj; +} + +function _classPrivateFieldGet(receiver, privateMap) { + var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); + return _classApplyDescriptorGet(receiver, descriptor); +} + +function _classApplyDescriptorGet(receiver, descriptor) { + if (descriptor.get) { + return descriptor.get.call(receiver); + } + return descriptor.value; +} + +function _classPrivateFieldSet(receiver, privateMap, value) { + var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "set"); + _classApplyDescriptorSet(receiver, descriptor, value); + return value; +} + +function _classExtractFieldDescriptor(receiver, privateMap, action) { + if (!privateMap.has(receiver)) { + throw new TypeError("attempted to " + action + " private field on non-instance"); + } + return privateMap.get(receiver); +} + +function _classApplyDescriptorSet(receiver, descriptor, value) { + if (descriptor.set) { + descriptor.set.call(receiver, value); + } else { + if (!descriptor.writable) { + throw new TypeError("attempted to set read only private field"); + } + descriptor.value = value; + } +} + +var animationNamePrefix = 'l-smooth-marker-bouncing-'; +var moveAnimationName = animationNamePrefix + 'move'; +var contractAnimationName = animationNamePrefix + 'contract'; +/* + * CSS3 animation runs faster than transform-based animation. We need to reduce speed in order + * to be compatible with old API. + */ + +var speedCoefficient = 0.8; + +/** + * Removes and then resets required classes on the HTML element. + * Used as hack to restart CSS3 animation. + * + * @param element {HTMLElement} HTML element + * @param classes {string[]} names of classes + */ + +function resetClasses(element, classes) { + classes.forEach(function (className) { + return _leaflet.DomUtil.removeClass(element, className); + }); + void element.offsetWidth; + classes.forEach(function (className) { + return _leaflet.DomUtil.addClass(element, className); + }); +} + +var _lastAnimationName = /*#__PURE__*/new WeakMap(); + +var _classes = /*#__PURE__*/new WeakMap(); + +var _eventCounter = /*#__PURE__*/new WeakMap(); + +var _times = /*#__PURE__*/new WeakMap(); + +var _listener = /*#__PURE__*/new WeakMap(); + +var BouncingMotionCss3 = /*#__PURE__*/function () { + /** + * Constructor. + * + * @param marker {Marker} marker + * @param position {Point} marker current position on the map canvas + * @param bouncingOptions {BouncingOptions} options of bouncing animation + */ + function BouncingMotionCss3(marker, position, bouncingOptions) { + var _this = this; + + _classCallCheck(this, BouncingMotionCss3); + + _defineProperty(this, "marker", void 0); + + _defineProperty(this, "position", void 0); + + _defineProperty(this, "bouncingOptions", void 0); + + _defineProperty(this, "isBouncing", false); + + _defineProperty(this, "iconStyles", void 0); + + _defineProperty(this, "shadowStyles", void 0); + + _defineProperty(this, "bouncingAnimationPlaying", false); + + _classPrivateFieldInitSpec(this, _lastAnimationName, { + writable: true, + value: contractAnimationName + }); + + _classPrivateFieldInitSpec(this, _classes, { + writable: true, + value: ['bouncing'] + }); + + _classPrivateFieldInitSpec(this, _eventCounter, { + writable: true, + value: void 0 + }); + + _classPrivateFieldInitSpec(this, _times, { + writable: true, + value: void 0 + }); + + _classPrivateFieldInitSpec(this, _listener, { + writable: true, + value: function value(event) { + return _this.onAnimationEnd(event); + } + }); + + this.marker = marker; + this.position = position; + this.updateBouncingOptions(bouncingOptions); + } + + _createClass(BouncingMotionCss3, [{ + key: "updateBouncingOptions", + value: function updateBouncingOptions(options) { + this.bouncingOptions = options instanceof _BouncingOptions["default"] ? options : this.bouncingOptions.override(options); + + if (this.bouncingOptions.elastic) { + _classPrivateFieldSet(this, _lastAnimationName, contractAnimationName); + + var index = _classPrivateFieldGet(this, _classes).indexOf('simple'); + + if (index > -1) { + _classPrivateFieldGet(this, _classes).splice(index, 1); + } + + if (this.marker._icon) { + _leaflet.DomUtil.removeClass(this.marker._icon, 'simple'); + } + } else { + _classPrivateFieldSet(this, _lastAnimationName, moveAnimationName); + + _classPrivateFieldGet(this, _classes).push('simple'); + } + + if (this.marker._icon) { + this.resetStyles(this.marker); + } + } + }, { + key: "onAnimationEnd", + value: function onAnimationEnd(event) { + var _this2 = this; + + if (event.animationName === _classPrivateFieldGet(this, _lastAnimationName)) { + var _this$eventCounter, _this$eventCounter2; + + _classPrivateFieldSet(this, _eventCounter, (_this$eventCounter = _classPrivateFieldGet(this, _eventCounter), _this$eventCounter2 = _this$eventCounter++, _this$eventCounter)), _this$eventCounter2; + + _classPrivateFieldSet(this, _eventCounter, _classPrivateFieldGet(this, _eventCounter) % 2); + + if (!_classPrivateFieldGet(this, _eventCounter)) { + var _this$times; + + if (this.isBouncing && (_classPrivateFieldGet(this, _times) === null || _classPrivateFieldSet(this, _times, (_this$times = _classPrivateFieldGet(this, _times), --_this$times)))) { + resetClasses(this.marker._icon, _classPrivateFieldGet(this, _classes)); + + if (this.marker._shadow && this.bouncingOptions.shadowAngle) { + resetClasses(this.marker._shadow, _classPrivateFieldGet(this, _classes)); + } + } else { + _classPrivateFieldGet(this, _classes).forEach(function (className) { + _leaflet.DomUtil.removeClass(_this2.marker._icon, className); + + if (_this2.marker._shadow) { + _leaflet.DomUtil.removeClass(_this2.marker._shadow, className); + } + }); + + this.bouncingAnimationPlaying = false; + this.marker.fire('bounceend'); + } + } + } + } + }, { + key: "resetStyles", + value: function resetStyles(marker) { + var _this$marker$getIcon, + _this$marker$getIcon$, + _this$marker, + _this$marker$_iconObj, + _this$marker$_iconObj2, + _this3 = this; + + this.marker = marker; + this.iconStyles = _Styles["default"].ofMarker(marker); + + if (marker._shadow) { + this.shadowStyles = _Styles["default"].parse(marker._shadow.style.cssText); + } + + var iconHeight = ((_this$marker$getIcon = this.marker.getIcon()) === null || _this$marker$getIcon === void 0 ? void 0 : (_this$marker$getIcon$ = _this$marker$getIcon.options) === null || _this$marker$getIcon$ === void 0 ? void 0 : _this$marker$getIcon$.iconSize[1]) || ((_this$marker = this.marker) === null || _this$marker === void 0 ? void 0 : (_this$marker$_iconObj = _this$marker._iconObj) === null || _this$marker$_iconObj === void 0 ? void 0 : (_this$marker$_iconObj2 = _this$marker$_iconObj.options) === null || _this$marker$_iconObj2 === void 0 ? void 0 : _this$marker$_iconObj2.iconSize[1]); + var iconAnimationParams = BouncingMotionCss3.animationParams(this.position, this.bouncingOptions, iconHeight); + this.iconStyles = this.iconStyles.withStyles(iconAnimationParams); + this.marker._icon.style.cssText = this.iconStyles.toString(); + + if (this.bouncingAnimationPlaying) { + resetClasses(this.marker._icon, _classPrivateFieldGet(this, _classes)); + + this.marker._icon.addEventListener('animationend', _classPrivateFieldGet(this, _listener)); + } + + var _this$bouncingOptions = this.bouncingOptions, + bounceHeight = _this$bouncingOptions.bounceHeight, + contractHeight = _this$bouncingOptions.contractHeight, + shadowAngle = _this$bouncingOptions.shadowAngle; + + if (this.marker._shadow) { + if (shadowAngle) { + var _this$marker$getIcon2, _this$marker$getIcon3; + + var _this$position = this.position, + x = _this$position.x, + y = _this$position.y; + var points = (0, _line.calculateLine)(x, y, shadowAngle, bounceHeight + 1); + + var _points$bounceHeight = _slicedToArray(points[bounceHeight], 2), + posXJump = _points$bounceHeight[0], + posYJump = _points$bounceHeight[1]; + + var shadowHeight = (_this$marker$getIcon2 = this.marker.getIcon()) === null || _this$marker$getIcon2 === void 0 ? void 0 : (_this$marker$getIcon3 = _this$marker$getIcon2.options) === null || _this$marker$getIcon3 === void 0 ? void 0 : _this$marker$getIcon3.shadowSize[1]; + var shadowScaleContract = BouncingMotionCss3.contractScale(shadowHeight, contractHeight); + this.shadowStyles = this.shadowStyles.withStyles(iconAnimationParams).withStyles({ + '--pos-x-jump': "".concat(posXJump, "px"), + '--pos-y-jump': "".concat(posYJump, "px"), + '--scale-contract': shadowScaleContract + }); + this.marker._shadow.style.cssText = this.shadowStyles.toString(); + + if (this.bouncingAnimationPlaying) { + resetClasses(this.marker._shadow, _classPrivateFieldGet(this, _classes)); + } + } else { + _classPrivateFieldGet(this, _classes).forEach(function (className) { + _leaflet.DomUtil.removeClass(_this3.marker._shadow, className); + }); + } + } + } + }, { + key: "bounce", + value: function bounce() { + var times = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + + _classPrivateFieldSet(this, _times, times); + + this.isBouncing = true; + + if (this.bouncingAnimationPlaying) { + return; + } + + _classPrivateFieldSet(this, _eventCounter, 0); + + this.bouncingAnimationPlaying = true; + resetClasses(this.marker._icon, _classPrivateFieldGet(this, _classes)); + + if (this.marker._shadow && this.bouncingOptions.shadowAngle) { + resetClasses(this.marker._shadow, _classPrivateFieldGet(this, _classes)); + } + + this.marker._icon.addEventListener('animationend', _classPrivateFieldGet(this, _listener)); + } + }, { + key: "stopBouncing", + value: function stopBouncing() { + this.isBouncing = false; + } + /** + * Calculates parameters of CSS3 animation of bouncing. + * + * @param position {Point} marker current position on the map canvas + * @param bouncingOptions {BouncingOptions} options of bouncing animation + * @param height {number} icons height + * @return {object} CSS3 animation parameters + */ + + }], [{ + key: "animationParams", + value: function animationParams(position, bouncingOptions, height) { + var x = position.x, + y = position.y; + var bounceHeight = bouncingOptions.bounceHeight, + contractHeight = bouncingOptions.contractHeight, + bounceSpeed = bouncingOptions.bounceSpeed, + contractSpeed = bouncingOptions.contractSpeed; + var scaleContract = BouncingMotionCss3.contractScale(height, contractHeight); + var durationJump = BouncingMotionCss3.calculateDuration(bounceHeight, bounceSpeed); + var durationContract = BouncingMotionCss3.calculateDuration(contractHeight, contractSpeed); + var delays = [0, durationJump, durationJump * 2, durationJump * 2 + durationContract]; + return { + '--pos-x': "".concat(x, "px"), + '--pos-y': "".concat(y, "px"), + '--pos-y-jump': "".concat(y - bounceHeight, "px"), + '--pos-y-contract': "".concat(y + contractHeight, "px"), + '--scale-contract': scaleContract, + '--duration-jump': "".concat(durationJump, "ms"), + '--duration-contract': "".concat(durationContract, "ms"), + '--delays': "0ms, ".concat(delays[1], "ms, ").concat(delays[2], "ms, ").concat(delays[3], "ms") + }; + } + /** + * Calculates scale of contracting. + * + * @param {number} height original height + * @param {number} contractHeight how much it must contract + * @return {number} contracting scale between 0 and 1 + */ + + }, { + key: "contractScale", + value: function contractScale(height, contractHeight) { + return (height - contractHeight) / height; + } + /** + * Calculates duration of animation. + * + * @param height {number} height of movement or resizing (px) + * @param speed {number} speed coefficient + * + * @return {number} duration of animation (ms) + */ + + }, { + key: "calculateDuration", + value: function calculateDuration(height, speed) { + var duration = Math.round(speed * speedCoefficient); + var i = height; + + while (--i) { + duration += Math.round(speed / (height - i)); + } + + return duration; + } + }]); + + return BouncingMotionCss3; +}(); + +exports["default"] = BouncingMotionCss3; \ No newline at end of file diff --git a/sut/frontend/build/leaflet/smooth_bounce/BouncingMotionSimple.js b/sut/frontend/build/leaflet/smooth_bounce/BouncingMotionSimple.js new file mode 100644 index 0000000..c4ee740 --- /dev/null +++ b/sut/frontend/build/leaflet/smooth_bounce/BouncingMotionSimple.js @@ -0,0 +1,267 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +var _BouncingMotion2 = _interopRequireDefault(require("./BouncingMotion.js")); + +var _line = require("./line.js"); + +function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : {"default": obj}; +} + +function _typeof(obj) { + "@babel/helpers - typeof"; + if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { + _typeof = function _typeof(obj) { + return typeof obj; + }; + } else { + _typeof = function _typeof(obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + } + return _typeof(obj); +} + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; +} + +function _get(target, property, receiver) { + if (typeof Reflect !== "undefined" && Reflect.get) { + _get = Reflect.get; + } else { + _get = function _get(target, property, receiver) { + var base = _superPropBase(target, property); + if (!base) return; + var desc = Object.getOwnPropertyDescriptor(base, property); + if (desc.get) { + return desc.get.call(receiver); + } + return desc.value; + }; + } + return _get(target, property, receiver || target); +} + +function _superPropBase(object, property) { + while (!Object.prototype.hasOwnProperty.call(object, property)) { + object = _getPrototypeOf(object); + if (object === null) break; + } + return object; +} + +function _inherits(subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function"); + } + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + writable: true, + configurable: true + } + }); + if (superClass) _setPrototypeOf(subClass, superClass); +} + +function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { + o.__proto__ = p; + return o; + }; + return _setPrototypeOf(o, p); +} + +function _createSuper(Derived) { + var hasNativeReflectConstruct = _isNativeReflectConstruct(); + return function _createSuperInternal() { + var Super = _getPrototypeOf(Derived), result; + if (hasNativeReflectConstruct) { + var NewTarget = _getPrototypeOf(this).constructor; + result = Reflect.construct(Super, arguments, NewTarget); + } else { + result = Super.apply(this, arguments); + } + return _possibleConstructorReturn(this, result); + }; +} + +function _possibleConstructorReturn(self, call) { + if (call && (_typeof(call) === "object" || typeof call === "function")) { + return call; + } + return _assertThisInitialized(self); +} + +function _assertThisInitialized(self) { + if (self === void 0) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + return self; +} + +function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) return false; + if (Reflect.construct.sham) return false; + if (typeof Proxy === "function") return true; + try { + Date.prototype.toString.call(Reflect.construct(Date, [], function () { + })); + return true; + } catch (e) { + return false; + } +} + +function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { + return o.__proto__ || Object.getPrototypeOf(o); + }; + return _getPrototypeOf(o); +} + +function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, {value: value, enumerable: true, configurable: true, writable: true}); + } else { + obj[key] = value; + } + return obj; +} + +var BouncingMotionSimple = /*#__PURE__*/function (_BouncingMotion) { + _inherits(BouncingMotionSimple, _BouncingMotion); + + var _super = _createSuper(BouncingMotionSimple); + + /** + * Constructor. + * + * @param marker {Marker} marker + * @param position {Point} marker current position on the map canvas + * @param bouncingOptions {BouncingOptions} options of bouncing animation + */ + function BouncingMotionSimple(marker, position, bouncingOptions) { + var _this; + + _classCallCheck(this, BouncingMotionSimple); + + _this = _super.call(this, marker, position, bouncingOptions); + + _defineProperty(_assertThisInitialized(_this), "iconMovePoints", void 0); + + _defineProperty(_assertThisInitialized(_this), "shadowMovePoints", void 0); + + _this.recalculateMotion(position); + + return _this; + } + + _createClass(BouncingMotionSimple, [{ + key: "recalculateMotion", + value: function recalculateMotion(position) { + _get(_getPrototypeOf(BouncingMotionSimple.prototype), "recalculateMotion", this).call(this, position); + + var x = position.x, + y = position.y; + var _this$bouncingOptions = this.bouncingOptions, + bounceHeight = _this$bouncingOptions.bounceHeight, + shadowAngle = _this$bouncingOptions.shadowAngle; + this.iconMovePoints = BouncingMotionSimple.calculateIconMovePoints(x, y, bounceHeight); + this.shadowMovePoints = BouncingMotionSimple.calculateShadowMovePoints(x, y, bounceHeight, shadowAngle); + } + }, { + key: "makeMoveStep", + value: function makeMoveStep(step) { + _get(_getPrototypeOf(BouncingMotionSimple.prototype), "makeMoveStep", this).call(this, step); + + this.marker._icon.style.left = this.iconMovePoints[step][0] + 'px'; + this.marker._icon.style.top = this.iconMovePoints[step][1] + 'px'; + + if (this.marker._shadow) { + this.marker._shadow.style.left = this.shadowMovePoints[step][0] + 'px'; + this.marker._shadow.style.top = this.shadowMovePoints[step][1] + 'px'; + } + } + /** + * Returns calculated array of points for icon movement. Used to animate markers in browsers + * that doesn't support 'transform' attribute. + * + * @param x {number} x coordinate of original position of the marker + * @param y {number} y coordinate of original position of the marker + * @param bounceHeight {number} height of bouncing (px) + * + * @return {[number, number][]} array of points + */ + + }], [{ + key: "calculateIconMovePoints", + value: function calculateIconMovePoints(x, y, bounceHeight) { + var deltaHeight = bounceHeight + 1; + var points = []; // Use fast inverse while loop to fill the array + + while (deltaHeight--) { + points[deltaHeight] = [x, y - deltaHeight]; + } + + return points; + } + /** + * Returns calculated array of points for shadow movement. Used to animate markers in browsers + * that doesn't support 'transform' attribute. + * + * @param x {number} x coordinate of original position of the marker + * @param y {number} y coordinate of original position of the marker + * @param bounceHeight {number} height of bouncing (px) + * @param angle {number} shadow inclination angle, if null shadow don't moves from it's initial + * position (radians) + * + * @return {[number, number][]} array of points + */ + + }, { + key: "calculateShadowMovePoints", + value: function calculateShadowMovePoints(x, y, bounceHeight, angle) { + if (angle != null) { + // important: 0 is not null + return (0, _line.calculateLine)(x, y, angle, bounceHeight + 1); + } else { + var points = []; + + for (var i = 0; i <= bounceHeight; i++) { + points[i] = [x, y]; + } + + return points; + } + } + }]); + + return BouncingMotionSimple; +}(_BouncingMotion2["default"]); + +exports["default"] = BouncingMotionSimple; \ No newline at end of file diff --git a/sut/frontend/build/leaflet/smooth_bounce/BouncingOptions.js b/sut/frontend/build/leaflet/smooth_bounce/BouncingOptions.js new file mode 100644 index 0000000..6a377a0 --- /dev/null +++ b/sut/frontend/build/leaflet/smooth_bounce/BouncingOptions.js @@ -0,0 +1,105 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + Object.defineProperty(Constructor, "prototype", {writable: false}); + return Constructor; +} + +function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, {value: value, enumerable: true, configurable: true, writable: true}); + } else { + obj[key] = value; + } + return obj; +} + +var BouncingOptions = /*#__PURE__*/function () { + /** + * How high marker can bounce (px) + * @type {number} + */ + + /** + * How much marker can contract (px) + * @type {number} + */ + + /** + * Bouncing speed coefficient + * @type {number} + */ + + /** + * Contracting speed coefficient + * @type {number} + */ + + /** + * Shadow inclination angle(radians); null to cancel shadow movement + * @type {number} + */ + + /** + * Activate contract animation + * @type {boolean} + */ + + /** + * Many markers can bounce in the same time + * @type {boolean} + */ + function BouncingOptions(options) { + _classCallCheck(this, BouncingOptions); + + _defineProperty(this, "bounceHeight", 15); + + _defineProperty(this, "contractHeight", 12); + + _defineProperty(this, "bounceSpeed", 52); + + _defineProperty(this, "contractSpeed", 52); + + _defineProperty(this, "shadowAngle", -Math.PI / 4); + + _defineProperty(this, "elastic", true); + + _defineProperty(this, "exclusive", false); + + options && Object.assign(this, options); + } + + _createClass(BouncingOptions, [{ + key: "override", + value: function override(options) { + return Object.assign(new BouncingOptions(this), options); + } + }]); + + return BouncingOptions; +}(); + +exports["default"] = BouncingOptions; \ No newline at end of file diff --git a/sut/frontend/build/leaflet/smooth_bounce/Cache.js b/sut/frontend/build/leaflet/smooth_bounce/Cache.js new file mode 100644 index 0000000..c1361be --- /dev/null +++ b/sut/frontend/build/leaflet/smooth_bounce/Cache.js @@ -0,0 +1,65 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; +} + +function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, {value: value, enumerable: true, configurable: true, writable: true}); + } else { + obj[key] = value; + } + return obj; +} + +var Cache = /*#__PURE__*/function () { + function Cache() { + _classCallCheck(this, Cache); + + _defineProperty(this, "cache", {}); + } + + _createClass(Cache, [{ + key: "get", + + /** + * If item with supplied {@code key} is present in cache, returns it, otherwise executes + * {@code supplier} function and caches the result. + * + * @param key {String} key of the cache + * @param supplier {function} item supplier + * @return {Object} item + */ + value: function get(key, supplier) { + return this.cache[key] || (this.cache[key] = supplier.apply()); + } + }]); + + return Cache; +}(); + +exports["default"] = Cache; \ No newline at end of file diff --git a/sut/frontend/build/leaflet/smooth_bounce/MarkerPrototypeExt.js b/sut/frontend/build/leaflet/smooth_bounce/MarkerPrototypeExt.js new file mode 100644 index 0000000..a0e80ad --- /dev/null +++ b/sut/frontend/build/leaflet/smooth_bounce/MarkerPrototypeExt.js @@ -0,0 +1,121 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +var _leaflet = require("leaflet"); + +var _BouncingOptions = _interopRequireDefault(require("./BouncingOptions.js")); + +var _Orchestration = _interopRequireDefault(require("./Orchestration.js")); + +function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : {"default": obj}; +} + +var oldSetPos = _leaflet.Marker.prototype._setPos; +var oldOnAdd = _leaflet.Marker.prototype.onAdd; +var oldSetIcon = _leaflet.Marker.prototype.setIcon; +var _default = { + /** Bouncing options shared by all markers. */ + _bouncingOptions: new _BouncingOptions["default"](), + _orchestration: new _Orchestration["default"](), + + /** + * Registers options of bouncing animation for this marker. After registration of options for + * this marker, it will ignore changes of default options. Function automatically recalculates + * animation steps and delays. + * + * @param options {BouncingOptions|object} options object + * @return {Marker} this marker + */ + setBouncingOptions: function setBouncingOptions(options) { + this._bouncingMotion.updateBouncingOptions(options); + + return this; + }, + + /** + * Returns true if this marker is bouncing. If this marker is not bouncing returns false. + * @return {boolean} true if marker is bouncing, false if not + */ + isBouncing: function isBouncing() { + return this._bouncingMotion.isBouncing; + }, + + /** + * Starts bouncing of this marker. + * @param times {number|null} number of times the marker must to bounce + * @return {Marker} this marker + */ + bounce: function bounce() { + var times = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + + this._bouncingMotion.bounce(times); + + var exclusive = this._bouncingMotion.bouncingOptions.exclusive; + + _leaflet.Marker.prototype._orchestration.addBouncingMarker(this, exclusive); + + return this; + }, + + /** + * Stops bouncing of this marker. + * Note: the bouncing not stops immediately after the call of this method. + * Instead, the animation is executed until marker returns to it's original position and takes + * it's full size. + * + * @return {Marker} this marker + */ + stopBouncing: function stopBouncing() { + this._bouncingMotion.stopBouncing(); + + _leaflet.Marker.prototype._orchestration.removeBouncingMarker(this); + + return this; + }, + + /** + * Starts/stops bouncing of this marker. + * @return {Marker} marker + */ + toggleBouncing: function toggleBouncing() { + if (this._bouncingMotion.isBouncing) { + this.stopBouncing(); + } else { + this.bounce(); + } + + return this; + }, + isRealMarker: function isRealMarker() { + return this.__proto__ === _leaflet.Marker.prototype; + }, + _setPos: function _setPos(position) { + oldSetPos.call(this, position); + + if (this.isRealMarker()) { + this._bouncingMotion.position = position; + + this._bouncingMotion.resetStyles(this); + } + }, + onAdd: function onAdd(map) { + oldOnAdd.call(this, map); + + if (this.isRealMarker()) { + this._bouncingMotion.resetStyles(this); + } + }, + setIcon: function setIcon(icon) { + oldSetIcon.call(this, icon); + + if (this.isRealMarker() && this._icon) { + this._bouncingMotion.resetStyles(this); + } + } +}; +exports["default"] = _default; \ No newline at end of file diff --git a/sut/frontend/build/leaflet/smooth_bounce/Matrix3D.js b/sut/frontend/build/leaflet/smooth_bounce/Matrix3D.js new file mode 100644 index 0000000..c06f222 --- /dev/null +++ b/sut/frontend/build/leaflet/smooth_bounce/Matrix3D.js @@ -0,0 +1,166 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +function _toConsumableArray(arr) { + return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); +} + +function _nonIterableSpread() { + throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); +} + +function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === "string") return _arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === "Object" && o.constructor) n = o.constructor.name; + if (n === "Map" || n === "Set") return Array.from(o); + if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); +} + +function _iterableToArray(iter) { + if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); +} + +function _arrayWithoutHoles(arr) { + if (Array.isArray(arr)) return _arrayLikeToArray(arr); +} + +function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + for (var i = 0, arr2 = new Array(len); i < len; i++) { + arr2[i] = arr[i]; + } + return arr2; +} + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; +} + +function _classPrivateFieldGet(receiver, privateMap) { + var descriptor = privateMap.get(receiver); + if (!descriptor) { + throw new TypeError("attempted to get private field on non-instance"); + } + if (descriptor.get) { + return descriptor.get.call(receiver); + } + return descriptor.value; +} + +function _classPrivateFieldSet(receiver, privateMap, value) { + var descriptor = privateMap.get(receiver); + if (!descriptor) { + throw new TypeError("attempted to set private field on non-instance"); + } + if (descriptor.set) { + descriptor.set.call(receiver, value); + } else { + if (!descriptor.writable) { + throw new TypeError("attempted to set read only private field"); + } + descriptor.value = value; + } + return value; +} + +var rowMap = { + 'a': 0, + 'b': 1, + 'c': 2, + 'd': 3 +}; +var zeros = Array(16).fill(0); +var _identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; +/** + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix3d + */ + +var _matrix = new WeakMap(); + +var Matrix3D = /*#__PURE__*/function () { + function Matrix3D() { + var matrix = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : zeros; + + _classCallCheck(this, Matrix3D); + + _matrix.set(this, { + writable: true, + value: void 0 + }); + + _classPrivateFieldSet(this, _matrix, _toConsumableArray(matrix)); + } + + _createClass(Matrix3D, [{ + key: "toFormat", + value: function toFormat() { + for (var _len = arguments.length, placeholders = new Array(_len), _key = 0; _key < _len; _key++) { + placeholders[_key] = arguments[_key]; + } + + placeholders = placeholders.map(Matrix3D.valueNameToIndex); + var nextPlaceholderIndex = 0; + + var fnBody = _classPrivateFieldGet(this, _matrix).map(function (value, index) { + return index === placeholders[nextPlaceholderIndex] ? "'+arguments[".concat(nextPlaceholderIndex++, "]+'") : value; + }).join(','); + + fnBody = "return ' matrix3d(".concat(fnBody, ") ';"); + + function formatFn() { + return Function.apply(this, [fnBody]); + } + + formatFn.prototype = Function.prototype; + return new formatFn(); + } + }, { + key: "toString", + value: function toString() { + return " matrix3d(".concat(_classPrivateFieldGet(this, _matrix).join(','), ") "); + } + }], [{ + key: "zeros", + value: function zeros() { + return new Matrix3D(); + } + }, { + key: "identity", + value: function identity() { + return new Matrix3D(_identity); + } + }, { + key: "valueNameToIndex", + value: function valueNameToIndex(valueName) { + return rowMap[valueName[0]] * 4 + parseInt(valueName[1]) - 1; + } + }]); + + return Matrix3D; +}(); + +exports["default"] = Matrix3D; \ No newline at end of file diff --git a/sut/frontend/build/leaflet/smooth_bounce/Orchestration.js b/sut/frontend/build/leaflet/smooth_bounce/Orchestration.js new file mode 100644 index 0000000..b9316da --- /dev/null +++ b/sut/frontend/build/leaflet/smooth_bounce/Orchestration.js @@ -0,0 +1,147 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +var _leaflet = require("leaflet"); + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + Object.defineProperty(Constructor, "prototype", {writable: false}); + return Constructor; +} + +function _classPrivateFieldInitSpec(obj, privateMap, value) { + _checkPrivateRedeclaration(obj, privateMap); + privateMap.set(obj, value); +} + +function _checkPrivateRedeclaration(obj, privateCollection) { + if (privateCollection.has(obj)) { + throw new TypeError("Cannot initialize the same private elements twice on an object"); + } +} + +function _classPrivateFieldGet(receiver, privateMap) { + var descriptor = _classExtractFieldDescriptor(receiver, privateMap, "get"); + return _classApplyDescriptorGet(receiver, descriptor); +} + +function _classExtractFieldDescriptor(receiver, privateMap, action) { + if (!privateMap.has(receiver)) { + throw new TypeError("attempted to " + action + " private field on non-instance"); + } + return privateMap.get(receiver); +} + +function _classApplyDescriptorGet(receiver, descriptor) { + if (descriptor.get) { + return descriptor.get.call(receiver); + } + return descriptor.value; +} + +var _bouncingMarkers = /*#__PURE__*/new WeakMap(); + +var Orchestration = /*#__PURE__*/function () { + function Orchestration() { + _classCallCheck(this, Orchestration); + + _classPrivateFieldInitSpec(this, _bouncingMarkers, { + writable: true, + value: [] + }); + } + + _createClass(Orchestration, [{ + key: "getBouncingMarkers", + value: function getBouncingMarkers() { + return _classPrivateFieldGet(this, _bouncingMarkers); + } + /** + * Adds the marker to the list of bouncing markers. + * If flag 'exclusive' is set to true, stops all bouncing markers before. + * + * @param marker {Marker} marker object + * @param exclusive {boolean} flag of exclusive bouncing. If set to true, stops the bouncing + * of all other markers. + */ + + }, { + key: "addBouncingMarker", + value: function addBouncingMarker(marker, exclusive) { + if (exclusive || marker._bouncingMotion.bouncingOptions.exclusive) { + this.stopAllBouncingMarkers(); + } else { + this.stopExclusiveMarkerBouncing(); + } + + _classPrivateFieldGet(this, _bouncingMarkers).push(marker); + } + /** + * Stops the bouncing of exclusive marker. + */ + + }, { + key: "stopExclusiveMarkerBouncing", + value: function stopExclusiveMarkerBouncing() { + var exclusiveMarker = _classPrivateFieldGet(this, _bouncingMarkers).find(function (marker) { + return marker._bouncingMotion.bouncingOptions.exclusive; + }); + + if (exclusiveMarker) { + exclusiveMarker.stopBouncing(); + } + } + /** + * Removes the marker from the list of bouncing markers. + * @param marker {Marker} marker + */ + + }, { + key: "removeBouncingMarker", + value: function removeBouncingMarker(marker) { + var i = _classPrivateFieldGet(this, _bouncingMarkers).indexOf(marker); + + if (~i) { + _classPrivateFieldGet(this, _bouncingMarkers).splice(i, 1); + } + } + /** + * Stops the bouncing of all currently bouncing markers. Purge the array of bouncing markers. + */ + + }, { + key: "stopAllBouncingMarkers", + value: function stopAllBouncingMarkers() { + var marker; + + while (marker = _classPrivateFieldGet(this, _bouncingMarkers).shift()) { + marker.stopBouncing(); + } + } + }]); + + return Orchestration; +}(); + +exports["default"] = Orchestration; \ No newline at end of file diff --git a/sut/frontend/build/leaflet/smooth_bounce/SmoothMarkerBouncing.js b/sut/frontend/build/leaflet/smooth_bounce/SmoothMarkerBouncing.js new file mode 100644 index 0000000..5905b86 --- /dev/null +++ b/sut/frontend/build/leaflet/smooth_bounce/SmoothMarkerBouncing.js @@ -0,0 +1,96 @@ +"use strict"; + +function _typeof(obj) { + "@babel/helpers - typeof"; + return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { + return typeof obj; + } : function (obj) { + return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }, _typeof(obj); +} + +var _leaflet = _interopRequireWildcard(require("leaflet")); + +var _BouncingOptions = _interopRequireDefault(require("./BouncingOptions.js")); + +var _MarkerPrototypeExt = _interopRequireDefault(require("./MarkerPrototypeExt.js")); + +var _BouncingMotionCss = _interopRequireDefault(require("./BouncingMotionCss3.js")); + +function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : {"default": obj}; +} + +function _getRequireWildcardCache(nodeInterop) { + if (typeof WeakMap !== "function") return null; + var cacheBabelInterop = new WeakMap(); + var cacheNodeInterop = new WeakMap(); + return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { + return nodeInterop ? cacheNodeInterop : cacheBabelInterop; + })(nodeInterop); +} + +function _interopRequireWildcard(obj, nodeInterop) { + if (!nodeInterop && obj && obj.__esModule) { + return obj; + } + if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { + return {"default": obj}; + } + var cache = _getRequireWildcardCache(nodeInterop); + if (cache && cache.has(obj)) { + return cache.get(obj); + } + var newObj = {}; + var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; + for (var key in obj) { + if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { + var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; + if (desc && (desc.get || desc.set)) { + Object.defineProperty(newObj, key, desc); + } else { + newObj[key] = obj[key]; + } + } + } + newObj["default"] = obj; + if (cache) { + cache.set(obj, newObj); + } + return newObj; +} + +_leaflet["default"].Marker.include(_MarkerPrototypeExt["default"]); +/** + * Registers default options of bouncing animation. + * @param options {BouncingOptions|object} object with options + */ + + +_leaflet["default"].Marker.setBouncingOptions = function (options) { + _leaflet.Marker.prototype._bouncingOptions = options instanceof _BouncingOptions["default"] ? options : new _BouncingOptions["default"](options); +}; +/** + * Returns array of currently bouncing markers. + * @return {Marker[]} array of bouncing markers + */ + + +_leaflet["default"].Marker.getBouncingMarkers = function () { + _leaflet.Marker.prototype._orchestration.getBouncingMarkers(); +}; +/** + * Stops the bouncing of all currently bouncing markers. Purge the array of bouncing markers. + */ + + +_leaflet["default"].Marker.stopAllBouncingMarkers = function () { + _leaflet.Marker.prototype._orchestration.stopAllBouncingMarkers(); +}; + +_leaflet["default"].Marker.addInitHook(function () { + if (this.isRealMarker()) { + var bouncingOptions = new _BouncingOptions["default"](_leaflet.Marker.prototype._bouncingOptions); + this._bouncingMotion = new _BouncingMotionCss["default"](this, new _leaflet.Point(0, 0), bouncingOptions); + } +}); \ No newline at end of file diff --git a/sut/frontend/build/leaflet/smooth_bounce/Styles.js b/sut/frontend/build/leaflet/smooth_bounce/Styles.js new file mode 100644 index 0000000..3b9d710 --- /dev/null +++ b/sut/frontend/build/leaflet/smooth_bounce/Styles.js @@ -0,0 +1,106 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports["default"] = void 0; + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + Object.defineProperty(Constructor, "prototype", {writable: false}); + return Constructor; +} + +/** Regex to parse style definitions. */ +var regStyle = /([\w-]+): ([^;]+);/g; + +var Styles = /*#__PURE__*/function () { + function Styles(styles) { + _classCallCheck(this, Styles); + + styles && Object.assign(this, styles); + } + + _createClass(Styles, [{ + key: "findOpacity", + value: function findOpacity(options) { + this.opacity = (options === null || options === void 0 ? void 0 : options.opacityWhenUnclustered // used by cluster plugin + ) || (options === null || options === void 0 ? void 0 : options.opacity) || 1; + } + /** + * Creates a copy of styles merged with provided 'styles'. + * @param {Object} styles object with styles to merge + * @return {Styles} copy of styles + */ + + }, { + key: "withStyles", + value: function withStyles(styles) { + var copy = new Styles(this); + copy && Object.assign(copy, styles); + return copy; + } + }, { + key: "toString", + value: function toString() { + return Object.entries(this).map(function (entry) { + return "".concat(entry[0], ": ").concat(entry[1], ";"); + }).join(' '); + } + /** + * Parses cssText attribute into Styles object. + * @param cssText {string} cssText string + * @return {Styles} Styles object + */ + + }], [{ + key: "parse", + value: function parse(cssText) { + var styles = {}; + var match = regStyle.exec(cssText); + + while (match) { + styles[match[1]] = match[2]; + match = regStyle.exec(cssText); + } + + delete styles['z-index']; + delete styles['opacity']; + styles['outline'] = 'none'; + return new Styles(styles); + } + /** + * @param marker {Marker} + */ + + }, { + key: "ofMarker", + value: function ofMarker(marker) { + var styles = Styles.parse(marker._icon.style.cssText); + styles.findOpacity(marker.options); + styles['z-index'] = marker._zIndex; + return styles; + } + }]); + + return Styles; +}(); + +exports["default"] = Styles; \ No newline at end of file diff --git a/sut/frontend/build/leaflet/smooth_bounce/bundle.min.js b/sut/frontend/build/leaflet/smooth_bounce/bundle.min.js new file mode 100644 index 0000000..1395c41 --- /dev/null +++ b/sut/frontend/build/leaflet/smooth_bounce/bundle.min.js @@ -0,0 +1 @@ +!function(M){"use strict";function n(n){return n&&"object"==typeof n&&"default"in n?n:{default:n}}var t=n(M);function o(n,t){if(!(n instanceof t))throw new TypeError("Cannot call a class as a function")}function e(n,t){for(var i=0;in.length)&&(t=n.length);for(var i=0,e=new Array(t);i dy ? dx : -dy) / 2, + e2, + p = [], + i = 0; + + while (true) { + p.push([x, y]); + i++; + if (i === length) break; + e2 = err; + + if (e2 > -dx) { + err -= dy; + x += sx; + } + + if (e2 < dy) { + err += dx; + y += sy; + } + } + + return p; +} \ No newline at end of file diff --git a/sut/frontend/build/logo.png b/sut/frontend/build/logo.png new file mode 100644 index 0000000..1e407d5 Binary files /dev/null and b/sut/frontend/build/logo.png differ diff --git a/sut/frontend/build/logo_background.png b/sut/frontend/build/logo_background.png new file mode 100644 index 0000000..6a99a33 Binary files /dev/null and b/sut/frontend/build/logo_background.png differ diff --git a/sut/frontend/build/logo_old.png b/sut/frontend/build/logo_old.png new file mode 100644 index 0000000..e3eaad4 Binary files /dev/null and b/sut/frontend/build/logo_old.png differ diff --git a/sut/frontend/build/middle-earth-map.svg b/sut/frontend/build/middle-earth-map.svg new file mode 100644 index 0000000..7f3d0d6 --- /dev/null +++ b/sut/frontend/build/middle-earth-map.svg @@ -0,0 +1,65 @@ + + + + + + + Middle-earth + + + + + + Eriador + + + + Misty Mountains + + + + Rhovanion + + + + Mordor + + + + + The Shire + + + + Rivendell + + + + Moria + + + + Lothlórien + + + + Rohan + + + + Mordor + + + + + + + N + + + + + + + 300 Miles + diff --git a/sut/frontend/build/middle-earth-map.webp b/sut/frontend/build/middle-earth-map.webp new file mode 100644 index 0000000..df8728a Binary files /dev/null and b/sut/frontend/build/middle-earth-map.webp differ diff --git a/sut/frontend/build/middle-earth-map/data/markers.json b/sut/frontend/build/middle-earth-map/data/markers.json new file mode 100644 index 0000000..349d68f --- /dev/null +++ b/sut/frontend/build/middle-earth-map/data/markers.json @@ -0,0 +1,707 @@ +[ + { + "title": "Minas Tirith", + "description": "Minas Tirith was originally a fortress, Minas Anor, built in S.A. 3320 by the Faithful Númenóreans. From T.A. 1640 onwards it was the capital of the South-kingdom and the seat of its Kings and ruling Stewards.", + "infoLink": "https://tolkiengateway.net/wiki/Minas_Tirith", + "tags": { + "places": [ + "human" + ], + "quests": [ + "ring" + ] + }, + "x": 3279, + "y": 2707 + }, + { + "title": "Esgaroth (Lake-Town)", + "description": "Lake-Town was the township of the Lake-men in Wilderland. The town was constructed entirely of wood and stood upon wooden pillars sunk into the bed of the Long Lake, as a protection against the dragon Smaug, who dwelt nearby in the Lonely Mountain.", + "infoLink": "https://tolkiengateway.net/wiki/Lake-town", + "tags": { + "places": [ + "human" + ], + "quests": [ + "erebor" + ] + }, + "x": 3418, + "y": 885 + }, + { + "title": "Bree", + "description": "Bree was the chief village of Bree-land, a small wooded region near the intersection of the main north-south and east-west routes through Eriador. Bree-land was the only part of Middle-earth where Men and hobbits dwelt side by side and Bree had a large population of Hobbits.", + "infoLink": "https://tolkiengateway.net/wiki/Bree", + "tags": { + "places": [ + "human", + "hobbit" + ], + "quests": [ + "ring", + "erebor" + ] + }, + "x": 1793, + "y": 1163 + }, + { + "title": "Erebor", + "description": "The Longbeards had control of Erebor since at least the early Second Age. With the awakening of Durin's Bane in the capital of Khazad-dûm, Thráin I led a group of Dwarves to Erebor. Once there, the dwarves dug caves and halls to form an underground city, thus establishing the Kingdom under the Mountain in T.A. 1999.", + "infoLink": "https://tolkiengateway.net/wiki/Erebor", + "tags": { + "places": [ + "dwarven" + ], + "quests": [ + "erebor" + ] + }, + "x": 3405, + "y": 825 + }, + { + "title": "Rivendell", + "sindarinTitle": "Imladris", + "description": "Rivendell was established by Elrond in S.A. 1697 as a refuge from Sauron after the Fall of Eregion. It remained Elrond's seat throughout the remainder of the Second Age and until the end of the Third Age, when he took the White Ship for Valinor.", + "infoLink": "https://tolkiengateway.net/wiki/Rivendell", + "tags": { + "places": [ + "elven" + ], + "quests": [ + "erebor", + "ring" + ] + }, + "x": 2516, + "y": 1123 + }, + { + "title": "Mount Doom", + "sindarinTitle": "Orodruin, Amon Amarth", + "description": "Melkor created Mount Doom in the First Age. When Sauron chose the land of Mordor as his dwelling-place in the Second Age, Orodruin was the reason for his choice. The mountain erupted in S.A. 3429, signalling Sauron's attack on Gondor and it took the name Amon Amarth, \"Mount Doom\". This is where the One Ring was forged by Sauron, and where it was destroyed by Gollum.", + "infoLink": "https://tolkiengateway.net/wiki/Mount_Doom", + "tags": { + "places": [ + "dark" + ], + "quests": [ + "ring" + ] + }, + "x": 3606, + "y": 2603 + }, + { + "title": "Osgiliath", + "description": "Founded by Isildur and Anárion near the end of the Second Age, Osgiliath was designated the capital of the southern Númenórean kingdom in exile, Gondor. It stays so until the King's House was moved to the more secure Minas Anor in T.A. 1640.", + "infoLink": "https://tolkiengateway.net/wiki/Osgiliath", + "tags": { + "places": [ + "human" + ], + "quests": [ + "ring" + ] + }, + "x": 3330, + "y": 2700 + }, + { + "title": "Hobbiton", + "description": "Hobbiton was a hobbit village in the central regions of the Shire, within the borders of the Westfarthing.", + "infoLink": "https://tolkiengateway.net/wiki/Hobbiton", + "tags": { + "places": [ + "hobbit" + ], + "quests": [ + "erebor", + "ring" + ] + }, + "x": 1482, + "y": 1158 + }, + { + "title": "Helm's Deep", + "description": "Helm's Deep was a large valley gorge in the north-western Ered Nimrais (White Mountains) below the Thrihyrne. It was actually the name of the whole defensive system including its major defensive structure, the Hornburg.", + "infoLink": "https://tolkiengateway.net/wiki/Helm's_Deep", + "tags": { + "places": [ + "human" + ], + "quests": [ + "ring" + ] + }, + "x": 2423, + "y": 2321 + }, + { + "title": "Black Gate", + "sindarinTitle": "Morannon", + "description": "The Black Gate was the main entrance into the land of Mordor. It was built by Sauron after he chose Mordor as a land to make into a stronghold in S.A. 1000.", + "infoLink": "https://tolkiengateway.net/wiki/Black_Gate", + "tags": { + "places": [ + "dark" + ], + "quests": [ + "ring" + ] + }, + "x": 3389, + "y": 2377 + }, + { + "title": "Weathertop", + "sindarinTitle": "Amun Sûl", + "description": "In T.A.3018, Amun Sûl was the scene of two fights involving the Nazgûl: one with Gandalf on October 3 and one with the Ring-bearer three days later.", + "infoLink": "https://tolkiengateway.net/wiki/Weathertop", + "tags": { + "places": [ + "human" + ], + "quests": [ + "ring" + ] + }, + "x": 2000, + "y": 1158 + }, + { + "title": "Isengard", + "sindarinTitle": "Angrenost", + "description": "Isengard was one of the three major fortresses of Gondor, and held within it one of the realm's palantíri. In the latter half of the Third Age, the stronghold came into the possession of Saruman, becoming his home and personal domain until his defeat in the War of the Ring.", + "infoLink": "https://tolkiengateway.net/wiki/Isengard", + "tags": { + "places": [ + "dark" + ], + "quests": [ + "ring" + ] + }, + "x": 2335, + "y": 2117 + }, + { + "title": "Barad-dûr", + "description": "Barad-dûr, also known as the Dark Tower, was the chief fortress of Sauron, on the Plateau of Gorgoroth in Mordor. Sauron began to build Barad-dûr in around S.A. 1000, and completed his fortress after 600 years of the construction with the power of the Ring. It was partially destroyed after Sauron's defeat against Isildur, and began to be rebuilt in T.A. 2951.", + "infoLink": "https://tolkiengateway.net/wiki/Barad-dûr", + "tags": { + "places": [ + "dark" + ], + "quests": [ + "ring" + ] + }, + "x": 3750, + "y": 2553 + }, + { + "title": "Edoras", + "description": "Edoras was the capital of Rohan that held the Golden Hall of Meduseld. Rohan's first capital was at Aldburg in the Folde, until King Eorl the Young or his son Brego built Edoras in T.A. 2569. ", + "infoLink": "https://tolkiengateway.net/wiki/Edoras", + "tags": { + "places": [ + "human" + ], + "quests": [ + "ring" + ] + }, + "x": 2589, + "y": 2383 + }, + { + "title": "Moria", + "sindarinTitle": "Khazad-dûm", + "description": "Khazad-dûm was the grandest and most famous of the mansions of the Dwarves. There, for many thousands of years, a thriving Dwarvish community created the greatest city ever known.", + "infoLink": "https://tolkiengateway.net/wiki/Moria", + "tags": { + "places": [ + "dwarven" + ], + "quests": [ + "ring" + ] + }, + "x": 2492, + "y": 1505 + }, + { + "title": "Grey Havens", + "sindarinTitle": "Mithlond", + "description": "Founded by the Elves of Lindon in S.A. 1, the Grey Havens were known for their good harbourage and many ships; these were used by any of the Eldar to leave Middle-earth for Eressëa or Valinor.", + "infoLink": "https://tolkiengateway.net/wiki/Grey_Havens", + "tags": { + "places": [ + "elven" + ], + "quests": [ + "ring" + ] + }, + "x": 1047, + "y": 1186 + }, + { + "title": "Lothlórien", + "description": "Lothlórien (or Lórien) was a kingdom of Silvan Elves on the eastern side of the Hithaeglir. It was considered one of the most beautiful and \"elvish\" places in Middle-earth during the Third Age, and had the only mallorn-trees east of the sea.", + "infoLink": "https://tolkiengateway.net/wiki/Lothlórien", + "tags": { + "places": [ + "elven" + ], + "quests": [ + "ring" + ] + }, + "x": 2666, + "y": 1679 + }, + { + "title": "Elvenking's Hall", + "description": "Elvenking's Hall were a cave system in northern Mirkwood, in which King Thranduil and many of the Elves of Mirkwood lived during most of the Third Age and into the Fourth Age.", + "infoLink": "https://tolkiengateway.net/wiki/Elvenking's_Halls", + "tags": { + "places": [ + "elven" + ], + "quests": [ + "erebor" + ] + }, + "x": 3311, + "y": 849 + }, + { + "title": "Dol Guldur", + "description": "Dol Guldur (\"Hill of Sorcery\" in Sindarin), also called \"the dungeons of the Necromancer\", was a stronghold of Sauron located in the south of Mirkwood.", + "infoLink": "https://tolkiengateway.net/wiki/Dol_Guldur", + "tags": { + "places": [ + "dark" + ], + "quests": [ + "erebor", + "ring" + ] + }, + "x": 3014, + "y": 1629 + }, + { + "title": "Minas Morgul", + "description": "Minas Morgul (originally called Minas Ithil) was the twin city of Minas Tirith before its fall to the forces of Sauron in the Third Age. It then became the stronghold of the Witch-king of Angmar until Sauron's defeat.", + "infoLink": "https://tolkiengateway.net/wiki/Minas_Morgul", + "tags": { + "places": [ + "dark" + ], + "quests": [ + "ring" + ] + }, + "x": 3424, + "y": 2695 + }, + { + "title": "Paths of the Dead", + "description": "The Paths of the Dead was a haunted underground passage through the White Mountains that led from Harrowdale in Rohan to Blackroot Vale in Gondor.", + "infoLink": "https://tolkiengateway.net/wiki/Paths_of_the_Dead", + "tags": { + "places": [ + "human" + ], + "quests": [ + "ring" + ] + }, + "x": 2605, + "y": 2535 + }, + { + "title": "Mount Gram", + "description": "Mount Gram was inhabited by Orcs led by their King Golfimbul. In T.A. 2747 they attacked much of northern Eriador, but were defeated in the Battle of Greenfields.", + "infoLink": "https://tolkiengateway.net/wiki/Mount_Gram", + "tags": { + "places": [ + "dark" + ], + "quests": [ + "erebor" + ] + }, + "x": 2353, + "y": 746 + }, + { + "title": "Carn Dûm", + "description": "Carn Dûm was the chief fortress of the realm of Angmar and the seat of its king until its defeat against the combined armies of Gondor, Lindon and Arnor in T.A. 1974.", + "infoLink": "https://tolkiengateway.net/wiki/Carn_Dûm", + "tags": { + "places": [ + "dark" + ] + }, + "x": 2115, + "y": 523 + }, + { + "title": "Beorn's Hall", + "description": "Beorn's Hall was the home of Beorn, a powerful Skin-changer. Beorn hosted and aided Thorin and Company during their Quest for Erebor.", + "infoLink": "https://tolkiengateway.net/wiki/Beorn's_Hall", + "tags": { + "places": [ + "human" + ], + "quests": [ + "erebor" + ] + }, + "x": 2871, + "y": 1016 + }, + { + "title": "Goblin-town", + "description": "Goblin-town was a Goblin dwelling under the Misty Mountains, which was ruled by the Great Goblin. Goblin-town was a series of tunnels and caverns, which went all the way through the mountains, with a \"back door\" (the Goblin-gate) near the Eagle's Eyrie in Wilderland, which served as a means of escape, and an access to the Wilderland. A cave with a lake was deep beneath Goblin-town yet was connected to the Goblins' tunnels, with one passage leading to the \"back door\".", + "infoLink": "https://tolkiengateway.net/wiki/Goblin-town", + "tags": { + "places": [ + "dark" + ], + "quests": [ + "erebor" + ] + }, + "x": 2647, + "y": 980 + }, + { + "title": "Dale", + "description": "Dale was a great city of the Northmen which was destroyed by Smaug and rebuilt as the capital of a great kingdom after his demise.", + "infoLink": "https://tolkiengateway.net/wiki/Dale", + "tags": { + "places": [ + "human" + ], + "quests": [ + "erebor" + ] + }, + "x": 3430, + "y": 855 + }, + { + "title": "Smaug", + "date": "November 1, T.A. 2941", + "description": "Bard fired a Black Arrow into the vulnerable spot on the dragon's belly. Roaring in fury and pain, Smaug fell from the sky and plummeted into the flaming ruins of Lake-town, his death marked the end of the great dragons in Middle-earth.", + "infoLink": "https://tolkiengateway.net/wiki/Smaug", + "tags": { + "events": [ + "death" + ], + "quests": [ + "erebor" + ] + }, + "x": 3418, + "y": 885 + }, + { + "title": "Battle of Dagorlad", + "date": "3434 of the Second Age", + "description": "The Battle of Dagorlad was fought between the army of the Last Alliance under Gil-galad and Elendil and an army of Orcs and other creatures loyal to Sauron.", + "infoLink": "https://tolkiengateway.net/wiki/Battle_of_Dagorlad", + "tags": { + "events": [ + "battle" + ], + "quests": [ + "other" + ] + }, + "x": 3319, + "y": 2356 + }, + { + "title": "Battle of Isengard", + "date": "3 March, T.A. 3019", + "description": "Spurred on by Meriadoc Brandybuck and Peregrin Took, the Ents, followed by Huorns, invaded the Ring of Isengard from Fangorn Forest. It led the the drowning of Isengard and the defeat of Saruman.", + "infoLink": "https://tolkiengateway.net/wiki/Battle_of_Isengard", + "tags": { + "events": [ + "battle" + ], + "quests": [ + "ring" + ] + }, + "x": 2335, + "y": 2117 + }, + { + "title": "Battle of Five Armies", + "date": "November 23, T.A. 2941", + "description": "The five warring parties were the Goblins and the Wargs against Men, Elves and Dwarves on and near the Lonely Mountain.", + "infoLink": "https://tolkiengateway.net/wiki/Battle_of_Five_Armies", + "tags": { + "events": [ + "battle" + ], + "quests": [ + "erebor" + ] + }, + "x": 3405, + "y": 825 + }, + { + "title": "Thorin, Fíli and Kíli", + "date": "November 23, T.A. 2941", + "description": "When Bilbo regained consciousness, the battle was already over. Thorin II Oakenshield had been mortally wounded on the field, and his nephews Fíli and Kíli died defending him as he lay on the ground.", + "infoLink": "https://tolkiengateway.net/wiki/Battle_of_Five_Armies", + "tags": { + "events": [ + "death" + ], + "quests": [ + "erebor" + ] + }, + "x": 3405, + "y": 825 + }, + { + "title": "Battle of Pelennor Fields", + "date": "15 March, T.A. 3019", + "description": "The Battle of the Pelennor Fields was the greatest battle of the War of the Ring. This battle saw the siege of Minas Tirith by Mordor's troops widely outnumbering the defenders, but resulted in the loss of Sauron's armies after the Witch King was killed in combat.", + "infoLink": "https://tolkiengateway.net/wiki/Battle_of_the_Pelennor_Fields", + "tags": { + "events": [ + "battle" + ], + "quests": [ + "ring" + ] + }, + "x": 3279, + "y": 2707 + }, + { + "title": "Battle of the Morannon", + "date": "25 March, T.A. 3019", + "description": "The Battle of the Morannon was the last major battle against Sauron in the War of the Ring, fought at the Black Gate of Mordor on 25 March T.A. 3019. The army of the West, 6,000 strong by now, led by Aragorn marched on the gate as a diversionary feint to distract Sauron's attention from Frodo and Sam, who were carrying the One Ring through Mordor. It was hoped that Sauron would think Aragorn had the Ring and was now trying to use it to overthrow Mordor.", + "infoLink": "https://tolkiengateway.net/wiki/Battle_of_the_Morannon", + "tags": { + "events": [ + "battle" + ], + "quests": [ + "ring" + ] + }, + "x": 3389, + "y": 2377 + }, + { + "title": "Battle of the Hornburg", + "date": "3-4 March, T.A. 3019", + "description": "The Battle of the Hornburg took place at the mountain fortress of the Hornburg in the valley of Helm's Deep in Rohan. Taking place over the night of the 3-4 March T.A. 3019, it saw the attacking Uruk-hai of Saruman defeated by the Rohirrim led by Théoden and Erkenbrand.", + "infoLink": "https://tolkiengateway.net/wiki/Battle_of_the_Hornburg", + "tags": { + "events": [ + "battle" + ], + "quests": [ + "ring" + ] + }, + "x": 2423, + "y": 2321 + }, + { + "title": "Boromir", + "date": "February 26, T.A. 3019", + "description": "Boromir died at Amon Hen, the westernmost of the three peaks at the southern end of Nen Hithoel. He perished trying to defend Merry and Pippin from Saruman's Orcs, slaying at least 20 of them before perishing after being hit by many arrows.", + "infoLink": "https://tolkiengateway.net/wiki/Boromir", + "tags": { + "events": [ + "death" + ], + "quests": [ + "ring" + ] + }, + "x": 3037, + "y": 2377 + }, + { + "title": "Gandalf the Grey", + "date": "January 25, T.A. 3019", + "description": "Gandalf pursued the Balrog from the deepests dungeons of Moria to Durin's Tower, climbing through the whole Endless Stair, where they fought for three days and two nights. In the end, the Balrog was cast down and it broke the mountain-side as it fell. Gandalf himself died following this ordeal, but was later sent back to Middle-earth with even greater powers as Gandalf the White.", + "infoLink": "https://tolkiengateway.net/wiki/Battle_of_the_Peak", + "tags": { + "events": [ + "death" + ], + "quests": [ + "ring" + ] + }, + "x": 2500, + "y": 1558 + }, + { + "title": "Bilbo stole the Ring from Gollum", + "date": "July 12, T.A. 2941", + "description": "Bilbo picked up a strange golden ring in the dark passages under Goblin-town. He then met Gollum whom defied him to a riddle contest for his life. Bilbo won, but then Gollum went to get his magic ring to kill him anyway. He then discovered that his ring was missing, and understood that Bilbo took it. He chased Bilbo, but Bilbo unwittingly used the ring and escaped his notice.", + "infoLink": "https://tolkiengateway.net/wiki/Bilbo_Baggins#Over_the_Misty_Mountains", + "tags": { + "events": [ + "encounter" + ], + "quests": [ + "erebor" + ] + }, + "x": 2685, + "y": 962 + }, + { + "title": "Merry and Pippin meet Treebeard", + "date": "February 29, T.A. 3019", + "description": "Treebeard discovered Merry and Pippin in Fangorn Forest after they escaped from Saruman's Orcs, and welcomed them to the WellingHall.", + "infoLink": "https://tolkiengateway.net/wiki/Treebeard", + "tags": { + "events": [ + "encounter" + ], + "quests": [ + "ring" + ] + }, + "x": 2460, + "y": 2052 + }, + { + "title": "Tom Bombadil", + "date": "September 26, T.A. 3018", + "description": "Tom Bombadil, a mysterious creature living in the Dark Forst with his wife Goldberry, saved Frodo, Sam, Merry and Pippin from the Old Man Willow, an evil willow who cast a spell on them and trapped Merry and Pippin. He then hosted the hobbits for two nights, during which he told them many tales and songs.", + "infoLink": "https://tolkiengateway.net/wiki/Tom_Bombadil", + "tags": { + "events": [ + "encounter" + ], + "quests": [ + "ring" + ] + }, + "x": 1659, + "y": 1184 + }, + { + "title": "Shelob", + "date": "March 12, T.A. 3019", + "description": "Shelob was born as the last child of the spider-like demon Ungoliant. On March 12 T.A. 3019, Gollum led Sam and Frodo into the tunnels of Shelob's Lair and abandoned them in the dark. He planned that Shelob would eat Sam and Frodo so that he could find the One Ring among the bones and clothes.", + "infoLink": "https://tolkiengateway.net/wiki/Shelob", + "tags": { + "events": [ + "encounter" + ], + "quests": [ + "ring" + ] + }, + "x": 3456, + "y": 2680 + }, + { + "title": "Stone Trolls", + "date": "May 28, T.A. 2941", + "description": "Three Stone Trolls (William, Tom and Bert) captured Bilbo and the Company of Thorin. Thanks to Gandalf who tricked them, they kept fighting over how to cook the dwarves until the sun set up, frozing them into stone statues.", + "infoLink": "https://tolkiengateway.net/wiki/Roast_Mutton", + "tags": { + "events": [ + "encounter" + ], + "quests": [ + "erebor" + ] + }, + "x": 2320, + "y": 1120 + }, + { + "title": "Giant Spiders", + "date": "August, T.A. 2941", + "description": "Giant spiders (Ungolianth's spawns) managed to capture and entangle in webs each of the thirteen Dwarves. Only Bilbo's magic Ring and his Elven blade Sting allowed them to escape from being eaten, before being captured by Thranduil's elves.", + "infoLink": "https://tolkiengateway.net/wiki/Flies_and_Spiders", + "tags": { + "events": [ + "encounter" + ], + "quests": [ + "erebor" + ] + }, + "x": 3250, + "y": 937 + }, + { + "title": "Barrow-wights", + "date": "Septeùber 28, T.A. 3018", + "description": "The Barrow-wights were a kind of undead-like creatures, dead bones animated by evil spirits. Frodo, Sam, Merry an Pippin were trapped in the Barrow-downs by the spells of the Barrow-wights, and were nearly slain by the creatures. They were saved in the last minute by Tom, who seemed to have had complete authority over them.", + "infoLink": "https://tolkiengateway.net/wiki/Barrow-wights", + "tags": { + "events": [ + "encounter" + ], + "quests": [ + "ring" + ] + }, + "x": 1725, + "y": 1200 + }, + { + "title": "Old Man Willow", + "date": "September 26, T.A. 3018", + "description": "Old Man Willow was a willow tree in the Old Forest, who might have been an Ent who had become tree-like, or possibly a Huorn. Old Man Willow cast a spell on the Hobbits, Frodo, Sam, Merry and Pippin, causing them to fall asleep and trying to kill them. They were saved by the timely arrival of Tom Bombadil who knew \"the tune for him\".", + "infoLink": "https://tolkiengateway.net/wiki/Old_Man_Willow", + "tags": { + "events": [ + "encounter" + ], + "quests": [ + "ring" + ] + }, + "x": 1615, + "y": 1225 + }, + { + "title": "Battle of Bywater", + "date": "November 3, T.A. 3019", + "description": "The Battle of Bywater was a battle for control of the Shire and the final battle of the War of the Ring. After he fleed from the Battle of Isengard, Saruman took control over the Shire with a band of ruffians. When Frodo and his companions returned from the Coronation of Elessar, they rallied the hobbits wanting to resist and defeated the ruffians, then confronted Saruman. Wormtongue killed Saruman and was shot dead by the hobbits.", + "infoLink": "https://tolkiengateway.net/wiki/Battle_of_Bywater", + "tags": { + "events": [ + "battle" + ], + "quests": [ + "ring" + ] + }, + "x": 1507, + "y": 1163 + } +] + + diff --git a/sut/frontend/build/middle-earth-map/data/paths.json b/sut/frontend/build/middle-earth-map/data/paths.json new file mode 100644 index 0000000..1b06bfc --- /dev/null +++ b/sut/frontend/build/middle-earth-map/data/paths.json @@ -0,0 +1,21270 @@ +[ + { + "name": "Thorin and Company", + "id": "thorin", + "color": "red", + "distance": "950 miles / 1530 km", + "startDate": "April 27", + "endDate": "November 23, T.A. 2941", + "path": [ + [ + 1486.00, + 1164.00 + ], + [ + 1497.33, + 1177.33 + ], + [ + 1497.33, + 1177.33 + ], + [ + 1497.33, + 1177.33 + ], + [ + 1508.67, + 1174.67 + ], + [ + 1508.67, + 1174.67 + ], + [ + 1508.67, + 1174.67 + ], + [ + 1517.33, + 1168.67 + ], + [ + 1517.33, + 1168.67 + ], + [ + 1517.33, + 1168.67 + ], + [ + 1529.33, + 1162.67 + ], + [ + 1529.33, + 1162.67 + ], + [ + 1529.33, + 1162.67 + ], + [ + 1540.67, + 1158.67 + ], + [ + 1540.67, + 1158.67 + ], + [ + 1540.67, + 1158.67 + ], + [ + 1551.33, + 1154.00 + ], + [ + 1551.33, + 1154.00 + ], + [ + 1551.33, + 1154.00 + ], + [ + 1561.33, + 1151.33 + ], + [ + 1561.33, + 1151.33 + ], + [ + 1561.33, + 1151.33 + ], + [ + 1573.33, + 1148.00 + ], + [ + 1573.33, + 1148.00 + ], + [ + 1573.33, + 1148.00 + ], + [ + 1584.00, + 1143.33 + ], + [ + 1584.00, + 1143.33 + ], + [ + 1584.00, + 1143.33 + ], + [ + 1594.67, + 1142.00 + ], + [ + 1594.67, + 1141.33 + ], + [ + 1594.67, + 1140.67 + ], + [ + 1606.00, + 1136.00 + ], + [ + 1606.00, + 1136.00 + ], + [ + 1606.00, + 1136.00 + ], + [ + 1618.67, + 1132.67 + ], + [ + 1618.67, + 1132.67 + ], + [ + 1618.67, + 1132.67 + ], + [ + 1629.33, + 1132.00 + ], + [ + 1629.33, + 1132.00 + ], + [ + 1629.33, + 1132.00 + ], + [ + 1640.67, + 1130.67 + ], + [ + 1640.67, + 1130.67 + ], + [ + 1640.67, + 1130.67 + ], + [ + 1650.00, + 1128.67 + ], + [ + 1650.00, + 1128.67 + ], + [ + 1650.00, + 1128.67 + ], + [ + 1666.00, + 1128.00 + ], + [ + 1666.00, + 1128.00 + ], + [ + 1666.00, + 1128.00 + ], + [ + 1676.00, + 1128.00 + ], + [ + 1676.00, + 1128.00 + ], + [ + 1676.00, + 1128.00 + ], + [ + 1688.67, + 1127.33 + ], + [ + 1688.67, + 1127.33 + ], + [ + 1688.67, + 1127.33 + ], + [ + 1702.67, + 1127.33 + ], + [ + 1702.67, + 1127.33 + ], + [ + 1702.67, + 1127.33 + ], + [ + 1715.33, + 1128.67 + ], + [ + 1715.33, + 1128.67 + ], + [ + 1715.33, + 1128.67 + ], + [ + 1729.33, + 1132.67 + ], + [ + 1729.33, + 1132.67 + ], + [ + 1729.33, + 1132.67 + ], + [ + 1746.00, + 1136.00 + ], + [ + 1746.00, + 1136.00 + ], + [ + 1746.00, + 1136.00 + ], + [ + 1754.67, + 1139.33 + ], + [ + 1754.67, + 1139.33 + ], + [ + 1754.67, + 1139.33 + ], + [ + 1757.33, + 1152.00 + ], + [ + 1757.33, + 1152.00 + ], + [ + 1757.33, + 1152.00 + ], + [ + 1758.67, + 1162.00 + ], + [ + 1758.67, + 1162.00 + ], + [ + 1758.67, + 1162.00 + ], + [ + 1766.00, + 1171.33 + ], + [ + 1766.00, + 1171.33 + ], + [ + 1766.00, + 1171.33 + ], + [ + 1781.33, + 1172.67 + ], + [ + 1781.33, + 1172.67 + ], + [ + 1781.33, + 1172.67 + ], + [ + 1793.33, + 1170.67 + ], + [ + 1793.33, + 1170.67 + ], + [ + 1793.33, + 1170.67 + ], + [ + 1810.67, + 1168.00 + ], + [ + 1810.67, + 1168.00 + ], + [ + 1810.67, + 1168.00 + ], + [ + 1824.67, + 1167.33 + ], + [ + 1824.67, + 1167.33 + ], + [ + 1824.67, + 1167.33 + ], + [ + 1835.33, + 1162.00 + ], + [ + 1835.33, + 1162.00 + ], + [ + 1835.33, + 1162.00 + ], + [ + 1844.67, + 1158.67 + ], + [ + 1844.67, + 1158.67 + ], + [ + 1844.67, + 1158.67 + ], + [ + 1868.67, + 1162.67 + ], + [ + 1868.67, + 1162.67 + ], + [ + 1868.67, + 1162.67 + ], + [ + 1883.33, + 1164.67 + ], + [ + 1883.33, + 1164.67 + ], + [ + 1883.33, + 1164.67 + ], + [ + 1898.00, + 1168.67 + ], + [ + 1898.00, + 1168.67 + ], + [ + 1898.00, + 1168.67 + ], + [ + 1921.33, + 1175.33 + ], + [ + 1921.33, + 1175.33 + ], + [ + 1921.33, + 1175.33 + ], + [ + 1942.00, + 1178.00 + ], + [ + 1942.00, + 1178.00 + ], + [ + 1942.00, + 1178.00 + ], + [ + 1967.33, + 1178.00 + ], + [ + 1967.33, + 1178.00 + ], + [ + 1967.33, + 1178.00 + ], + [ + 1990.67, + 1176.67 + ], + [ + 1990.67, + 1176.67 + ], + [ + 1990.67, + 1176.67 + ], + [ + 2012.67, + 1172.67 + ], + [ + 2012.67, + 1172.67 + ], + [ + 2012.67, + 1172.67 + ], + [ + 2035.33, + 1166.00 + ], + [ + 2035.33, + 1166.00 + ], + [ + 2035.33, + 1166.00 + ], + [ + 2060.00, + 1160.67 + ], + [ + 2060.00, + 1160.67 + ], + [ + 2060.00, + 1160.67 + ], + [ + 2081.33, + 1153.33 + ], + [ + 2081.33, + 1153.33 + ], + [ + 2081.33, + 1153.33 + ], + [ + 2105.33, + 1146.00 + ], + [ + 2105.33, + 1146.00 + ], + [ + 2105.33, + 1146.00 + ], + [ + 2124.00, + 1140.67 + ], + [ + 2124.00, + 1140.67 + ], + [ + 2124.00, + 1140.67 + ], + [ + 2136.00, + 1138.00 + ], + [ + 2136.00, + 1138.00 + ], + [ + 2136.00, + 1138.00 + ], + [ + 2161.33, + 1134.67 + ], + [ + 2161.33, + 1134.67 + ], + [ + 2161.33, + 1134.67 + ], + [ + 2180.67, + 1132.00 + ], + [ + 2180.67, + 1132.00 + ], + [ + 2180.67, + 1132.00 + ], + [ + 2204.00, + 1128.00 + ], + [ + 2204.00, + 1128.00 + ], + [ + 2204.00, + 1128.00 + ], + [ + 2225.33, + 1125.33 + ], + [ + 2225.33, + 1125.33 + ], + [ + 2225.33, + 1125.33 + ], + [ + 2248.00, + 1128.00 + ], + [ + 2248.00, + 1128.00 + ], + [ + 2248.00, + 1128.00 + ], + [ + 2262.00, + 1128.67 + ], + [ + 2262.00, + 1128.67 + ], + [ + 2262.00, + 1128.67 + ], + [ + 2283.33, + 1131.33 + ], + [ + 2283.33, + 1131.33 + ], + [ + 2283.33, + 1131.33 + ], + [ + 2295.33, + 1121.33 + ], + [ + 2295.33, + 1121.33 + ], + [ + 2295.33, + 1121.33 + ], + [ + 2314.00, + 1097.33 + ], + [ + 2314.00, + 1097.33 + ], + [ + 2314.00, + 1097.33 + ], + [ + 2329.33, + 1112.00 + ], + [ + 2329.33, + 1112.00 + ], + [ + 2329.33, + 1112.00 + ], + [ + 2340.00, + 1128.00 + ], + [ + 2340.00, + 1128.00 + ], + [ + 2340.00, + 1128.00 + ], + [ + 2343.33, + 1138.00 + ], + [ + 2343.33, + 1138.00 + ], + [ + 2343.33, + 1138.00 + ], + [ + 2365.33, + 1141.33 + ], + [ + 2365.33, + 1141.33 + ], + [ + 2365.33, + 1141.33 + ], + [ + 2387.33, + 1144.67 + ], + [ + 2387.33, + 1144.67 + ], + [ + 2387.33, + 1144.67 + ], + [ + 2412.00, + 1148.00 + ], + [ + 2412.00, + 1148.00 + ], + [ + 2412.00, + 1148.00 + ], + [ + 2436.67, + 1150.67 + ], + [ + 2436.67, + 1150.67 + ], + [ + 2436.67, + 1150.67 + ], + [ + 2457.33, + 1153.33 + ], + [ + 2457.33, + 1153.33 + ], + [ + 2457.33, + 1153.33 + ], + [ + 2481.33, + 1154.67 + ], + [ + 2481.33, + 1154.67 + ], + [ + 2481.33, + 1154.67 + ], + [ + 2493.33, + 1154.00 + ], + [ + 2493.33, + 1154.00 + ], + [ + 2493.33, + 1154.00 + ], + [ + 2501.33, + 1142.67 + ], + [ + 2501.33, + 1142.67 + ], + [ + 2501.33, + 1142.67 + ], + [ + 2515.33, + 1130.00 + ], + [ + 2515.33, + 1130.00 + ], + [ + 2515.33, + 1130.00 + ], + [ + 2517.33, + 1113.33 + ], + [ + 2518.00, + 1112.67 + ], + [ + 2518.67, + 1112.00 + ], + [ + 2532.00, + 1104.67 + ], + [ + 2532.67, + 1104.67 + ], + [ + 2533.33, + 1104.67 + ], + [ + 2555.33, + 1100.67 + ], + [ + 2557.33, + 1100.67 + ], + [ + 2559.33, + 1100.67 + ], + [ + 2580.00, + 1100.67 + ], + [ + 2582.00, + 1100.67 + ], + [ + 2584.00, + 1100.67 + ], + [ + 2603.33, + 1097.33 + ], + [ + 2603.33, + 1097.33 + ], + [ + 2603.33, + 1097.33 + ], + [ + 2619.33, + 1098.67 + ], + [ + 2620.67, + 1098.67 + ], + [ + 2622.00, + 1098.67 + ], + [ + 2625.33, + 1084.67 + ], + [ + 2625.33, + 1080.00 + ], + [ + 2625.33, + 1075.33 + ], + [ + 2626.00, + 1064.00 + ], + [ + 2626.00, + 1062.00 + ], + [ + 2626.00, + 1060.00 + ], + [ + 2630.67, + 1046.00 + ], + [ + 2630.67, + 1045.33 + ], + [ + 2630.67, + 1044.67 + ], + [ + 2631.33, + 1024.67 + ], + [ + 2631.33, + 1024.00 + ], + [ + 2631.33, + 1023.33 + ], + [ + 2638.67, + 1000.67 + ], + [ + 2638.67, + 1000.67 + ], + [ + 2638.67, + 1000.67 + ], + [ + 2643.33, + 981.33 + ], + [ + 2643.33, + 981.33 + ], + [ + 2643.33, + 981.33 + ], + [ + 2654.67, + 968.67 + ], + [ + 2654.67, + 968.67 + ], + [ + 2654.67, + 968.67 + ], + [ + 2686.00, + 974.00 + ], + [ + 2686.00, + 974.00 + ], + [ + 2686.00, + 974.00 + ], + [ + 2711.33, + 980.00 + ], + [ + 2711.33, + 980.00 + ], + [ + 2711.33, + 980.00 + ], + [ + 2719.33, + 987.33 + ], + [ + 2719.33, + 987.33 + ], + [ + 2719.33, + 987.33 + ], + [ + 2738.00, + 998.00 + ], + [ + 2738.00, + 998.00 + ], + [ + 2738.00, + 998.00 + ], + [ + 2760.67, + 1003.33 + ], + [ + 2761.33, + 1003.33 + ], + [ + 2762.00, + 1003.33 + ], + [ + 2790.00, + 1008.00 + ], + [ + 2790.00, + 1008.00 + ], + [ + 2790.00, + 1008.00 + ], + [ + 2810.00, + 1011.33 + ], + [ + 2810.00, + 1011.33 + ], + [ + 2810.00, + 1011.33 + ], + [ + 2833.33, + 1014.00 + ], + [ + 2833.33, + 1014.00 + ], + [ + 2833.33, + 1014.00 + ], + [ + 2846.67, + 1016.67 + ], + [ + 2847.33, + 1016.67 + ], + [ + 2848.00, + 1016.67 + ], + [ + 2865.33, + 1017.33 + ], + [ + 2865.33, + 1017.33 + ], + [ + 2865.33, + 1017.33 + ], + [ + 2880.00, + 1018.67 + ], + [ + 2880.00, + 1018.67 + ], + [ + 2880.00, + 1018.67 + ], + [ + 2899.33, + 1020.00 + ], + [ + 2899.33, + 1020.00 + ], + [ + 2899.33, + 1020.00 + ], + [ + 2917.33, + 1020.67 + ], + [ + 2920.00, + 1020.67 + ], + [ + 2922.67, + 1020.67 + ], + [ + 2932.00, + 1022.00 + ], + [ + 2934.00, + 1022.00 + ], + [ + 2936.00, + 1022.00 + ], + [ + 2961.33, + 1023.33 + ], + [ + 2961.33, + 1023.33 + ], + [ + 2961.33, + 1023.33 + ], + [ + 2979.33, + 1026.67 + ], + [ + 2979.33, + 1026.67 + ], + [ + 2979.33, + 1026.67 + ], + [ + 3002.67, + 1018.00 + ], + [ + 3002.67, + 1018.00 + ], + [ + 3002.67, + 1018.00 + ], + [ + 3023.33, + 1010.67 + ], + [ + 3023.33, + 1010.67 + ], + [ + 3023.33, + 1010.67 + ], + [ + 3037.33, + 1004.67 + ], + [ + 3037.33, + 1004.67 + ], + [ + 3037.33, + 1004.67 + ], + [ + 3058.67, + 1002.67 + ], + [ + 3059.33, + 1002.00 + ], + [ + 3060.00, + 1001.33 + ], + [ + 3076.67, + 994.67 + ], + [ + 3076.67, + 994.67 + ], + [ + 3076.67, + 994.67 + ], + [ + 3096.00, + 988.00 + ], + [ + 3096.00, + 988.00 + ], + [ + 3096.00, + 988.00 + ], + [ + 3118.67, + 982.00 + ], + [ + 3118.67, + 982.00 + ], + [ + 3118.67, + 982.00 + ], + [ + 3131.33, + 978.00 + ], + [ + 3131.33, + 978.00 + ], + [ + 3131.33, + 978.00 + ], + [ + 3149.33, + 972.00 + ], + [ + 3149.33, + 972.00 + ], + [ + 3149.33, + 972.00 + ], + [ + 3174.67, + 969.33 + ], + [ + 3174.67, + 968.67 + ], + [ + 3174.67, + 968.00 + ], + [ + 3215.33, + 959.33 + ], + [ + 3215.33, + 959.33 + ], + [ + 3215.33, + 959.33 + ], + [ + 3232.00, + 946.67 + ], + [ + 3232.00, + 946.67 + ], + [ + 3232.00, + 946.67 + ], + [ + 3252.00, + 930.00 + ], + [ + 3252.00, + 929.33 + ], + [ + 3252.00, + 928.67 + ], + [ + 3264.67, + 916.67 + ], + [ + 3264.67, + 916.67 + ], + [ + 3264.67, + 916.67 + ], + [ + 3273.33, + 904.00 + ], + [ + 3273.33, + 904.00 + ], + [ + 3273.33, + 904.00 + ], + [ + 3281.33, + 890.67 + ], + [ + 3281.33, + 890.67 + ], + [ + 3281.33, + 890.67 + ], + [ + 3290.67, + 877.33 + ], + [ + 3290.67, + 877.33 + ], + [ + 3290.67, + 877.33 + ], + [ + 3302.00, + 870.00 + ], + [ + 3302.00, + 870.00 + ], + [ + 3302.00, + 870.00 + ], + [ + 3311.33, + 874.67 + ], + [ + 3311.33, + 874.67 + ], + [ + 3311.33, + 874.67 + ], + [ + 3314.00, + 880.00 + ], + [ + 3314.00, + 880.00 + ], + [ + 3314.00, + 880.00 + ], + [ + 3320.67, + 882.67 + ], + [ + 3320.67, + 882.67 + ], + [ + 3320.67, + 882.67 + ], + [ + 3334.67, + 882.67 + ], + [ + 3335.33, + 882.67 + ], + [ + 3336.00, + 882.67 + ], + [ + 3344.00, + 882.67 + ], + [ + 3344.67, + 883.33 + ], + [ + 3345.33, + 884.00 + ], + [ + 3354.67, + 884.67 + ], + [ + 3355.33, + 884.67 + ], + [ + 3356.00, + 884.67 + ], + [ + 3373.33, + 886.00 + ], + [ + 3373.33, + 886.00 + ], + [ + 3373.33, + 886.00 + ], + [ + 3388.00, + 886.67 + ], + [ + 3388.00, + 886.67 + ], + [ + 3388.00, + 886.67 + ], + [ + 3398.00, + 887.33 + ], + [ + 3398.00, + 887.33 + ], + [ + 3398.00, + 887.33 + ], + [ + 3408.00, + 886.67 + ], + [ + 3408.00, + 886.67 + ], + [ + 3408.00, + 886.67 + ], + [ + 3419.33, + 886.67 + ], + [ + 3419.33, + 886.67 + ], + [ + 3419.33, + 886.67 + ], + [ + 3425.33, + 874.67 + ], + [ + 3425.33, + 874.00 + ], + [ + 3425.33, + 873.33 + ], + [ + 3430.67, + 857.33 + ], + [ + 3430.67, + 857.33 + ], + [ + 3430.67, + 857.33 + ], + [ + 3431.33, + 842.00 + ], + [ + 3431.33, + 842.00 + ], + [ + 3431.33, + 842.00 + ], + [ + 3432.00, + 834.00 + ], + [ + 3432.00, + 834.00 + ], + [ + 3432.00, + 834.00 + ], + [ + 3422.00, + 836.67 + ], + [ + 3421.33, + 836.67 + ], + [ + 3420.67, + 836.67 + ], + [ + 3411.33, + 827.33 + ], + [ + 3411.33, + 827.33 + ], + [ + 3411.33, + 827.33 + ], + [ + 3406.00, + 816.67 + ], + [ + 3406.00, + 816.67 + ] + ] + }, + { + "name": "Frodo & Sam", + "id": "frodo_sam", + "color": "orange", + "distance": "1600 miles / 2600 km", + "startDate": "September 23, T.A. 3018", + "endDate": "March 25, T.A. 3019", + "path": [ + [ + 1484.00, + 1162.00 + ], + [ + 1492.00, + 1176.00 + ], + [ + 1492.00, + 1176.00 + ], + [ + 1492.00, + 1176.00 + ], + [ + 1502.00, + 1184.00 + ], + [ + 1502.00, + 1184.00 + ], + [ + 1502.00, + 1184.00 + ], + [ + 1511.00, + 1195.00 + ], + [ + 1511.00, + 1195.00 + ], + [ + 1511.00, + 1195.00 + ], + [ + 1532.00, + 1207.00 + ], + [ + 1532.00, + 1207.00 + ], + [ + 1532.00, + 1207.00 + ], + [ + 1554.00, + 1217.00 + ], + [ + 1554.00, + 1217.00 + ], + [ + 1554.00, + 1217.00 + ], + [ + 1574.00, + 1220.00 + ], + [ + 1574.00, + 1220.00 + ], + [ + 1574.00, + 1220.00 + ], + [ + 1588.00, + 1221.00 + ], + [ + 1588.00, + 1221.00 + ], + [ + 1588.00, + 1221.00 + ], + [ + 1597.00, + 1213.00 + ], + [ + 1597.00, + 1213.00 + ], + [ + 1597.00, + 1213.00 + ], + [ + 1608.00, + 1208.00 + ], + [ + 1608.00, + 1208.00 + ], + [ + 1608.00, + 1208.00 + ], + [ + 1618.00, + 1208.00 + ], + [ + 1618.00, + 1208.00 + ], + [ + 1618.00, + 1208.00 + ], + [ + 1639.00, + 1208.00 + ], + [ + 1639.00, + 1208.00 + ], + [ + 1639.00, + 1208.00 + ], + [ + 1652.00, + 1207.00 + ], + [ + 1652.00, + 1207.00 + ], + [ + 1652.00, + 1207.00 + ], + [ + 1659.00, + 1204.00 + ], + [ + 1661.00, + 1204.00 + ], + [ + 1663.00, + 1204.00 + ], + [ + 1682.00, + 1201.00 + ], + [ + 1683.00, + 1201.00 + ], + [ + 1684.00, + 1201.00 + ], + [ + 1692.00, + 1199.00 + ], + [ + 1693.00, + 1199.00 + ], + [ + 1694.00, + 1199.00 + ], + [ + 1704.00, + 1201.00 + ], + [ + 1704.00, + 1201.00 + ], + [ + 1704.00, + 1201.00 + ], + [ + 1716.00, + 1200.00 + ], + [ + 1717.00, + 1200.00 + ], + [ + 1718.00, + 1200.00 + ], + [ + 1744.00, + 1201.00 + ], + [ + 1744.00, + 1201.00 + ], + [ + 1744.00, + 1201.00 + ], + [ + 1757.00, + 1202.00 + ], + [ + 1757.00, + 1202.00 + ], + [ + 1757.00, + 1202.00 + ], + [ + 1772.00, + 1209.00 + ], + [ + 1772.00, + 1209.00 + ], + [ + 1772.00, + 1209.00 + ], + [ + 1786.00, + 1215.00 + ], + [ + 1786.00, + 1215.00 + ], + [ + 1786.00, + 1215.00 + ], + [ + 1781.00, + 1200.00 + ], + [ + 1781.00, + 1200.00 + ], + [ + 1781.00, + 1200.00 + ], + [ + 1780.00, + 1187.00 + ], + [ + 1780.00, + 1187.00 + ], + [ + 1780.00, + 1187.00 + ], + [ + 1773.00, + 1176.00 + ], + [ + 1776.00, + 1174.00 + ], + [ + 1779.00, + 1172.00 + ], + [ + 1792.00, + 1160.00 + ], + [ + 1792.00, + 1160.00 + ], + [ + 1792.00, + 1160.00 + ], + [ + 1800.00, + 1150.00 + ], + [ + 1802.00, + 1149.00 + ], + [ + 1804.00, + 1148.00 + ], + [ + 1818.00, + 1134.00 + ], + [ + 1818.00, + 1134.00 + ], + [ + 1818.00, + 1134.00 + ], + [ + 1830.00, + 1129.00 + ], + [ + 1839.00, + 1129.00 + ], + [ + 1848.00, + 1129.00 + ], + [ + 1879.00, + 1132.00 + ], + [ + 1879.00, + 1132.00 + ], + [ + 1879.00, + 1132.00 + ], + [ + 1900.00, + 1132.00 + ], + [ + 1901.00, + 1132.00 + ], + [ + 1902.00, + 1132.00 + ], + [ + 1951.00, + 1133.00 + ], + [ + 1951.00, + 1133.00 + ], + [ + 1951.00, + 1133.00 + ], + [ + 1965.00, + 1137.00 + ], + [ + 1965.00, + 1137.00 + ], + [ + 1965.00, + 1137.00 + ], + [ + 1978.00, + 1145.00 + ], + [ + 1978.00, + 1145.00 + ], + [ + 1978.00, + 1145.00 + ], + [ + 1999.00, + 1151.00 + ], + [ + 1999.00, + 1151.00 + ], + [ + 1999.00, + 1151.00 + ], + [ + 1999.00, + 1169.00 + ], + [ + 1999.00, + 1169.00 + ], + [ + 1999.00, + 1169.00 + ], + [ + 1999.00, + 1178.00 + ], + [ + 2000.00, + 1180.00 + ], + [ + 2001.00, + 1182.00 + ], + [ + 2006.00, + 1189.00 + ], + [ + 2009.00, + 1193.00 + ], + [ + 2012.00, + 1197.00 + ], + [ + 2018.00, + 1197.00 + ], + [ + 2025.00, + 1200.00 + ], + [ + 2032.00, + 1203.00 + ], + [ + 2055.00, + 1204.00 + ], + [ + 2055.00, + 1204.00 + ], + [ + 2055.00, + 1204.00 + ], + [ + 2088.00, + 1204.00 + ], + [ + 2089.00, + 1204.00 + ], + [ + 2090.00, + 1204.00 + ], + [ + 2106.00, + 1200.00 + ], + [ + 2108.00, + 1200.00 + ], + [ + 2110.00, + 1200.00 + ], + [ + 2129.00, + 1199.00 + ], + [ + 2134.00, + 1197.00 + ], + [ + 2139.00, + 1195.00 + ], + [ + 2168.00, + 1193.00 + ], + [ + 2172.00, + 1191.00 + ], + [ + 2176.00, + 1189.00 + ], + [ + 2184.00, + 1178.00 + ], + [ + 2190.00, + 1175.00 + ], + [ + 2196.00, + 1172.00 + ], + [ + 2208.00, + 1166.00 + ], + [ + 2211.00, + 1165.00 + ], + [ + 2214.00, + 1164.00 + ], + [ + 2222.00, + 1157.00 + ], + [ + 2226.00, + 1155.00 + ], + [ + 2230.00, + 1153.00 + ], + [ + 2240.00, + 1147.00 + ], + [ + 2243.00, + 1145.00 + ], + [ + 2246.00, + 1143.00 + ], + [ + 2262.00, + 1136.00 + ], + [ + 2262.00, + 1136.00 + ], + [ + 2262.00, + 1136.00 + ], + [ + 2273.00, + 1133.00 + ], + [ + 2274.00, + 1133.00 + ], + [ + 2275.00, + 1133.00 + ], + [ + 2287.00, + 1117.00 + ], + [ + 2287.00, + 1117.00 + ], + [ + 2287.00, + 1117.00 + ], + [ + 2289.00, + 1115.00 + ], + [ + 2293.00, + 1109.00 + ], + [ + 2297.00, + 1103.00 + ], + [ + 2304.00, + 1097.00 + ], + [ + 2304.00, + 1096.00 + ], + [ + 2304.00, + 1095.00 + ], + [ + 2306.00, + 1094.00 + ], + [ + 2309.00, + 1086.00 + ], + [ + 2312.00, + 1078.00 + ], + [ + 2324.00, + 1055.00 + ], + [ + 2324.00, + 1055.00 + ], + [ + 2324.00, + 1055.00 + ], + [ + 2334.00, + 1045.00 + ], + [ + 2334.00, + 1045.00 + ], + [ + 2334.00, + 1045.00 + ], + [ + 2336.00, + 1059.00 + ], + [ + 2335.00, + 1060.00 + ], + [ + 2334.00, + 1061.00 + ], + [ + 2331.00, + 1072.00 + ], + [ + 2331.00, + 1073.00 + ], + [ + 2331.00, + 1074.00 + ], + [ + 2328.00, + 1079.00 + ], + [ + 2328.00, + 1084.00 + ], + [ + 2328.00, + 1089.00 + ], + [ + 2336.00, + 1109.00 + ], + [ + 2337.00, + 1109.00 + ], + [ + 2338.00, + 1109.00 + ], + [ + 2338.00, + 1110.00 + ], + [ + 2339.00, + 1115.00 + ], + [ + 2351.00, + 1129.00 + ], + [ + 2360.00, + 1132.00 + ], + [ + 2361.00, + 1133.00 + ], + [ + 2362.00, + 1134.00 + ], + [ + 2385.00, + 1144.00 + ], + [ + 2385.00, + 1144.00 + ], + [ + 2385.00, + 1144.00 + ], + [ + 2391.00, + 1145.00 + ], + [ + 2395.00, + 1145.00 + ], + [ + 2399.00, + 1145.00 + ], + [ + 2404.00, + 1145.00 + ], + [ + 2407.00, + 1146.00 + ], + [ + 2410.00, + 1147.00 + ], + [ + 2421.00, + 1145.00 + ], + [ + 2421.00, + 1145.00 + ], + [ + 2421.00, + 1145.00 + ], + [ + 2443.00, + 1148.00 + ], + [ + 2443.00, + 1148.00 + ], + [ + 2443.00, + 1148.00 + ], + [ + 2453.00, + 1149.00 + ], + [ + 2454.00, + 1150.00 + ], + [ + 2455.00, + 1151.00 + ], + [ + 2470.00, + 1151.00 + ], + [ + 2470.00, + 1151.00 + ], + [ + 2470.00, + 1151.00 + ], + [ + 2484.00, + 1156.00 + ], + [ + 2484.00, + 1156.00 + ], + [ + 2484.00, + 1156.00 + ], + [ + 2492.00, + 1152.00 + ], + [ + 2492.00, + 1152.00 + ], + [ + 2492.00, + 1152.00 + ], + [ + 2507.00, + 1148.00 + ], + [ + 2507.00, + 1148.00 + ], + [ + 2507.00, + 1148.00 + ], + [ + 2514.00, + 1133.00 + ], + [ + 2514.00, + 1133.00 + ], + [ + 2501.00, + 1163.00 + ], + [ + 2500.00, + 1164.00 + ], + [ + 2505.00, + 1179.00 + ], + [ + 2505.00, + 1179.00 + ], + [ + 2505.00, + 1179.00 + ], + [ + 2510.00, + 1190.00 + ], + [ + 2510.00, + 1190.00 + ], + [ + 2510.00, + 1190.00 + ], + [ + 2515.00, + 1203.00 + ], + [ + 2516.00, + 1207.00 + ], + [ + 2517.00, + 1211.00 + ], + [ + 2509.00, + 1232.00 + ], + [ + 2509.00, + 1232.00 + ], + [ + 2509.00, + 1232.00 + ], + [ + 2509.00, + 1234.00 + ], + [ + 2507.00, + 1238.00 + ], + [ + 2505.00, + 1242.00 + ], + [ + 2494.00, + 1272.00 + ], + [ + 2492.00, + 1274.00 + ], + [ + 2490.00, + 1276.00 + ], + [ + 2493.00, + 1282.00 + ], + [ + 2491.00, + 1288.00 + ], + [ + 2471.00, + 1332.00 + ], + [ + 2473.00, + 1333.00 + ], + [ + 2470.00, + 1341.00 + ], + [ + 2467.00, + 1349.00 + ], + [ + 2466.00, + 1349.00 + ], + [ + 2463.00, + 1356.00 + ], + [ + 2457.00, + 1368.00 + ], + [ + 2448.00, + 1377.00 + ], + [ + 2448.00, + 1377.00 + ], + [ + 2448.00, + 1377.00 + ], + [ + 2462.00, + 1388.00 + ], + [ + 2462.00, + 1388.00 + ], + [ + 2462.00, + 1388.00 + ], + [ + 2485.00, + 1394.00 + ], + [ + 2485.00, + 1394.00 + ], + [ + 2485.00, + 1394.00 + ], + [ + 2499.00, + 1399.00 + ], + [ + 2501.00, + 1399.00 + ], + [ + 2503.00, + 1399.00 + ], + [ + 2517.00, + 1403.00 + ], + [ + 2516.00, + 1404.00 + ], + [ + 2515.00, + 1405.00 + ], + [ + 2498.00, + 1405.00 + ], + [ + 2498.00, + 1405.00 + ], + [ + 2498.00, + 1405.00 + ], + [ + 2484.00, + 1405.00 + ], + [ + 2476.00, + 1403.00 + ], + [ + 2468.00, + 1401.00 + ], + [ + 2465.00, + 1401.00 + ], + [ + 2460.00, + 1402.00 + ], + [ + 2455.00, + 1403.00 + ], + [ + 2441.00, + 1405.00 + ], + [ + 2440.00, + 1407.00 + ], + [ + 2439.00, + 1409.00 + ], + [ + 2436.00, + 1416.00 + ], + [ + 2434.00, + 1420.00 + ], + [ + 2432.00, + 1424.00 + ], + [ + 2427.00, + 1438.00 + ], + [ + 2426.00, + 1439.00 + ], + [ + 2425.00, + 1440.00 + ], + [ + 2421.00, + 1450.00 + ], + [ + 2419.00, + 1454.00 + ], + [ + 2417.00, + 1458.00 + ], + [ + 2411.00, + 1469.00 + ], + [ + 2410.00, + 1472.00 + ], + [ + 2409.00, + 1475.00 + ], + [ + 2405.00, + 1481.00 + ], + [ + 2403.00, + 1489.00 + ], + [ + 2401.00, + 1497.00 + ], + [ + 2400.00, + 1506.00 + ], + [ + 2399.00, + 1508.00 + ], + [ + 2398.00, + 1510.00 + ], + [ + 2394.00, + 1524.00 + ], + [ + 2394.00, + 1527.00 + ], + [ + 2394.00, + 1530.00 + ], + [ + 2401.00, + 1534.00 + ], + [ + 2404.00, + 1536.00 + ], + [ + 2407.00, + 1538.00 + ], + [ + 2426.00, + 1541.00 + ], + [ + 2427.00, + 1541.00 + ], + [ + 2428.00, + 1541.00 + ], + [ + 2439.00, + 1539.00 + ], + [ + 2439.00, + 1539.00 + ], + [ + 2439.00, + 1539.00 + ], + [ + 2457.00, + 1538.00 + ], + [ + 2457.00, + 1538.00 + ], + [ + 2457.00, + 1538.00 + ], + [ + 2469.00, + 1534.00 + ], + [ + 2473.00, + 1534.00 + ], + [ + 2477.00, + 1534.00 + ], + [ + 2503.00, + 1536.00 + ], + [ + 2503.00, + 1536.00 + ], + [ + 2503.00, + 1536.00 + ], + [ + 2512.00, + 1535.00 + ], + [ + 2513.00, + 1535.00 + ], + [ + 2514.00, + 1535.00 + ], + [ + 2526.00, + 1538.00 + ], + [ + 2526.00, + 1538.00 + ], + [ + 2526.00, + 1538.00 + ], + [ + 2536.00, + 1544.00 + ], + [ + 2537.00, + 1545.00 + ], + [ + 2538.00, + 1546.00 + ], + [ + 2551.00, + 1557.00 + ], + [ + 2551.00, + 1557.00 + ], + [ + 2551.00, + 1557.00 + ], + [ + 2558.00, + 1574.00 + ], + [ + 2558.00, + 1574.00 + ], + [ + 2558.00, + 1574.00 + ], + [ + 2565.00, + 1594.00 + ], + [ + 2565.00, + 1594.00 + ], + [ + 2565.00, + 1594.00 + ], + [ + 2576.00, + 1604.00 + ], + [ + 2576.00, + 1604.00 + ], + [ + 2576.00, + 1604.00 + ], + [ + 2593.00, + 1611.00 + ], + [ + 2593.00, + 1611.00 + ], + [ + 2593.00, + 1611.00 + ], + [ + 2601.00, + 1628.00 + ], + [ + 2601.00, + 1628.00 + ], + [ + 2601.00, + 1628.00 + ], + [ + 2598.00, + 1646.00 + ], + [ + 2598.00, + 1647.00 + ], + [ + 2598.00, + 1648.00 + ], + [ + 2611.00, + 1652.00 + ], + [ + 2612.00, + 1653.00 + ], + [ + 2613.00, + 1654.00 + ], + [ + 2624.00, + 1666.00 + ], + [ + 2624.00, + 1667.00 + ], + [ + 2624.00, + 1668.00 + ], + [ + 2632.00, + 1676.00 + ], + [ + 2634.00, + 1678.00 + ], + [ + 2636.00, + 1680.00 + ], + [ + 2644.00, + 1684.00 + ], + [ + 2645.00, + 1685.00 + ], + [ + 2646.00, + 1686.00 + ], + [ + 2658.00, + 1694.00 + ], + [ + 2659.00, + 1694.00 + ], + [ + 2660.00, + 1694.00 + ], + [ + 2673.00, + 1700.00 + ], + [ + 2673.00, + 1700.00 + ], + [ + 2673.00, + 1700.00 + ], + [ + 2696.00, + 1713.00 + ], + [ + 2696.00, + 1713.00 + ], + [ + 2696.00, + 1713.00 + ], + [ + 2720.00, + 1723.00 + ], + [ + 2720.00, + 1723.00 + ], + [ + 2720.00, + 1723.00 + ], + [ + 2744.00, + 1727.00 + ], + [ + 2744.00, + 1728.00 + ], + [ + 2744.00, + 1729.00 + ], + [ + 2770.00, + 1732.00 + ], + [ + 2770.00, + 1732.00 + ], + [ + 2770.00, + 1732.00 + ], + [ + 2787.00, + 1745.00 + ], + [ + 2787.00, + 1746.00 + ], + [ + 2787.00, + 1747.00 + ], + [ + 2778.00, + 1761.00 + ], + [ + 2784.00, + 1767.00 + ], + [ + 2790.00, + 1773.00 + ], + [ + 2791.00, + 1784.00 + ], + [ + 2791.00, + 1786.00 + ], + [ + 2791.00, + 1788.00 + ], + [ + 2794.00, + 1803.00 + ], + [ + 2794.00, + 1803.00 + ], + [ + 2794.00, + 1803.00 + ], + [ + 2799.00, + 1810.00 + ], + [ + 2801.00, + 1811.00 + ], + [ + 2803.00, + 1812.00 + ], + [ + 2815.00, + 1819.00 + ], + [ + 2818.00, + 1820.00 + ], + [ + 2821.00, + 1821.00 + ], + [ + 2836.00, + 1827.00 + ], + [ + 2837.00, + 1828.00 + ], + [ + 2838.00, + 1829.00 + ], + [ + 2849.00, + 1837.00 + ], + [ + 2850.00, + 1837.00 + ], + [ + 2851.00, + 1837.00 + ], + [ + 2864.00, + 1845.00 + ], + [ + 2866.00, + 1847.00 + ], + [ + 2868.00, + 1849.00 + ], + [ + 2876.00, + 1854.00 + ], + [ + 2881.00, + 1858.00 + ], + [ + 2886.00, + 1862.00 + ], + [ + 2899.00, + 1873.00 + ], + [ + 2900.00, + 1874.00 + ], + [ + 2901.00, + 1875.00 + ], + [ + 2907.00, + 1879.00 + ], + [ + 2910.00, + 1880.00 + ], + [ + 2913.00, + 1881.00 + ], + [ + 2934.00, + 1888.00 + ], + [ + 2934.00, + 1888.00 + ], + [ + 2934.00, + 1888.00 + ], + [ + 2939.00, + 1886.00 + ], + [ + 2943.00, + 1890.00 + ], + [ + 2947.00, + 1894.00 + ], + [ + 2956.00, + 1900.00 + ], + [ + 2956.00, + 1901.00 + ], + [ + 2956.00, + 1902.00 + ], + [ + 2955.00, + 1909.00 + ], + [ + 2955.00, + 1910.00 + ], + [ + 2955.00, + 1911.00 + ], + [ + 2950.00, + 1920.00 + ], + [ + 2949.00, + 1922.00 + ], + [ + 2948.00, + 1924.00 + ], + [ + 2941.00, + 1932.00 + ], + [ + 2941.00, + 1934.00 + ], + [ + 2941.00, + 1936.00 + ], + [ + 2938.00, + 1957.00 + ], + [ + 2938.00, + 1957.00 + ], + [ + 2938.00, + 1957.00 + ], + [ + 2944.00, + 1967.00 + ], + [ + 2945.00, + 1969.00 + ], + [ + 2946.00, + 1971.00 + ], + [ + 2948.00, + 1976.00 + ], + [ + 2954.00, + 1980.00 + ], + [ + 2960.00, + 1984.00 + ], + [ + 2966.00, + 1985.00 + ], + [ + 2966.00, + 1985.00 + ], + [ + 2966.00, + 1985.00 + ], + [ + 2973.00, + 1985.00 + ], + [ + 2973.00, + 1985.00 + ], + [ + 2973.00, + 1985.00 + ], + [ + 2990.00, + 1983.00 + ], + [ + 2990.00, + 1983.00 + ], + [ + 2990.00, + 1983.00 + ], + [ + 3007.00, + 1976.00 + ], + [ + 3007.00, + 1976.00 + ], + [ + 3007.00, + 1976.00 + ], + [ + 3027.00, + 1984.00 + ], + [ + 3027.00, + 1985.00 + ], + [ + 3027.00, + 1986.00 + ], + [ + 3031.00, + 1999.00 + ], + [ + 3031.00, + 2001.00 + ], + [ + 3031.00, + 2003.00 + ], + [ + 3022.00, + 2020.00 + ], + [ + 3021.00, + 2021.00 + ], + [ + 3020.00, + 2022.00 + ], + [ + 3011.00, + 2038.00 + ], + [ + 3010.00, + 2039.00 + ], + [ + 3009.00, + 2040.00 + ], + [ + 3003.00, + 2048.00 + ], + [ + 3003.00, + 2052.00 + ], + [ + 3003.00, + 2056.00 + ], + [ + 3004.00, + 2070.00 + ], + [ + 3005.00, + 2071.00 + ], + [ + 3006.00, + 2072.00 + ], + [ + 3025.00, + 2081.00 + ], + [ + 3025.00, + 2081.00 + ], + [ + 3025.00, + 2081.00 + ], + [ + 3043.00, + 2090.00 + ], + [ + 3043.00, + 2090.00 + ], + [ + 3043.00, + 2090.00 + ], + [ + 3054.00, + 2100.00 + ], + [ + 3054.00, + 2100.00 + ], + [ + 3054.00, + 2100.00 + ], + [ + 3054.00, + 2113.00 + ], + [ + 3054.00, + 2114.00 + ], + [ + 3054.00, + 2115.00 + ], + [ + 3053.00, + 2130.00 + ], + [ + 3052.00, + 2131.00 + ], + [ + 3051.00, + 2132.00 + ], + [ + 3046.00, + 2149.00 + ], + [ + 3046.00, + 2150.00 + ], + [ + 3046.00, + 2151.00 + ], + [ + 3043.00, + 2164.00 + ], + [ + 3043.00, + 2164.00 + ], + [ + 3043.00, + 2164.00 + ], + [ + 3039.00, + 2176.00 + ], + [ + 3039.00, + 2177.00 + ], + [ + 3039.00, + 2178.00 + ], + [ + 3034.00, + 2188.00 + ], + [ + 3033.00, + 2191.00 + ], + [ + 3032.00, + 2194.00 + ], + [ + 3030.00, + 2208.00 + ], + [ + 3029.00, + 2212.00 + ], + [ + 3028.00, + 2216.00 + ], + [ + 3024.00, + 2226.00 + ], + [ + 3024.00, + 2229.00 + ], + [ + 3024.00, + 2232.00 + ], + [ + 3028.00, + 2247.00 + ], + [ + 3028.00, + 2250.00 + ], + [ + 3028.00, + 2253.00 + ], + [ + 3031.00, + 2267.00 + ], + [ + 3031.00, + 2269.00 + ], + [ + 3031.00, + 2271.00 + ], + [ + 3033.00, + 2283.00 + ], + [ + 3035.00, + 2286.00 + ], + [ + 3037.00, + 2289.00 + ], + [ + 3039.00, + 2303.00 + ], + [ + 3039.00, + 2303.00 + ], + [ + 3039.00, + 2303.00 + ], + [ + 3042.00, + 2321.00 + ], + [ + 3042.00, + 2321.00 + ], + [ + 3042.00, + 2321.00 + ], + [ + 3045.00, + 2343.00 + ], + [ + 3045.00, + 2346.00 + ], + [ + 3045.00, + 2349.00 + ], + [ + 3053.00, + 2366.00 + ], + [ + 3053.00, + 2367.00 + ], + [ + 3053.00, + 2368.00 + ], + [ + 3068.00, + 2372.00 + ], + [ + 3070.00, + 2371.00 + ], + [ + 3072.00, + 2370.00 + ], + [ + 3096.00, + 2355.00 + ], + [ + 3097.00, + 2353.00 + ], + [ + 3098.00, + 2351.00 + ], + [ + 3100.00, + 2351.00 + ], + [ + 3105.00, + 2342.00 + ], + [ + 3110.00, + 2333.00 + ], + [ + 3113.00, + 2320.00 + ], + [ + 3116.00, + 2317.00 + ], + [ + 3119.00, + 2314.00 + ], + [ + 3123.00, + 2310.00 + ], + [ + 3128.00, + 2303.00 + ], + [ + 3133.00, + 2296.00 + ], + [ + 3137.00, + 2288.00 + ], + [ + 3142.00, + 2284.00 + ], + [ + 3147.00, + 2280.00 + ], + [ + 3150.00, + 2274.00 + ], + [ + 3153.00, + 2273.00 + ], + [ + 3156.00, + 2272.00 + ], + [ + 3154.00, + 2270.00 + ], + [ + 3165.00, + 2269.00 + ], + [ + 3186.00, + 2268.00 + ], + [ + 3191.00, + 2268.00 + ], + [ + 3198.00, + 2273.00 + ], + [ + 3205.00, + 2278.00 + ], + [ + 3212.00, + 2278.00 + ], + [ + 3219.00, + 2284.00 + ], + [ + 3237.00, + 2300.00 + ], + [ + 3242.00, + 2303.00 + ], + [ + 3250.00, + 2308.00 + ], + [ + 3258.00, + 2313.00 + ], + [ + 3261.00, + 2317.00 + ], + [ + 3270.00, + 2322.00 + ], + [ + 3279.00, + 2327.00 + ], + [ + 3285.00, + 2329.00 + ], + [ + 3287.00, + 2330.00 + ], + [ + 3289.00, + 2331.00 + ], + [ + 3307.00, + 2338.00 + ], + [ + 3307.00, + 2338.00 + ], + [ + 3307.00, + 2338.00 + ], + [ + 3314.00, + 2348.00 + ], + [ + 3318.00, + 2349.00 + ], + [ + 3327.00, + 2352.00 + ], + [ + 3334.00, + 2356.00 + ], + [ + 3337.00, + 2357.00 + ], + [ + 3344.00, + 2360.00 + ], + [ + 3355.00, + 2368.00 + ], + [ + 3355.00, + 2368.00 + ], + [ + 3355.00, + 2368.00 + ], + [ + 3342.00, + 2383.00 + ], + [ + 3342.00, + 2383.00 + ], + [ + 3342.00, + 2383.00 + ], + [ + 3330.00, + 2397.00 + ], + [ + 3330.00, + 2398.00 + ], + [ + 3330.00, + 2399.00 + ], + [ + 3320.00, + 2420.00 + ], + [ + 3320.00, + 2422.00 + ], + [ + 3320.00, + 2424.00 + ], + [ + 3315.00, + 2446.00 + ], + [ + 3315.00, + 2447.00 + ], + [ + 3315.00, + 2448.00 + ], + [ + 3315.00, + 2461.00 + ], + [ + 3316.00, + 2464.00 + ], + [ + 3317.00, + 2467.00 + ], + [ + 3316.00, + 2490.00 + ], + [ + 3316.00, + 2490.00 + ], + [ + 3316.00, + 2490.00 + ], + [ + 3317.00, + 2516.00 + ], + [ + 3317.00, + 2516.00 + ], + [ + 3317.00, + 2516.00 + ], + [ + 3318.00, + 2525.00 + ], + [ + 3318.00, + 2529.00 + ], + [ + 3318.00, + 2533.00 + ], + [ + 3319.00, + 2547.00 + ], + [ + 3321.00, + 2551.00 + ], + [ + 3323.00, + 2555.00 + ], + [ + 3330.00, + 2566.00 + ], + [ + 3331.00, + 2573.00 + ], + [ + 3332.00, + 2580.00 + ], + [ + 3334.00, + 2593.00 + ], + [ + 3334.00, + 2599.00 + ], + [ + 3334.00, + 2605.00 + ], + [ + 3336.00, + 2608.00 + ], + [ + 3337.00, + 2613.00 + ], + [ + 3338.00, + 2618.00 + ], + [ + 3340.00, + 2622.00 + ], + [ + 3340.00, + 2628.00 + ], + [ + 3340.00, + 2634.00 + ], + [ + 3345.00, + 2652.00 + ], + [ + 3345.00, + 2653.00 + ], + [ + 3345.00, + 2662.00 + ], + [ + 3351.00, + 2668.00 + ], + [ + 3352.00, + 2668.00 + ], + [ + 3353.00, + 2668.00 + ], + [ + 3377.00, + 2672.00 + ], + [ + 3378.00, + 2672.00 + ], + [ + 3379.00, + 2672.00 + ], + [ + 3397.00, + 2671.00 + ], + [ + 3400.00, + 2670.00 + ], + [ + 3403.00, + 2669.00 + ], + [ + 3406.00, + 2668.00 + ], + [ + 3409.00, + 2668.00 + ], + [ + 3412.00, + 2668.00 + ], + [ + 3424.00, + 2670.00 + ], + [ + 3428.00, + 2671.00 + ], + [ + 3432.00, + 2672.00 + ], + [ + 3439.00, + 2673.00 + ], + [ + 3440.00, + 2673.00 + ], + [ + 3441.00, + 2673.00 + ], + [ + 3443.00, + 2675.00 + ], + [ + 3447.00, + 2674.00 + ], + [ + 3451.00, + 2673.00 + ], + [ + 3450.00, + 2653.00 + ], + [ + 3450.00, + 2653.00 + ], + [ + 3450.00, + 2653.00 + ], + [ + 3449.00, + 2646.00 + ], + [ + 3447.00, + 2643.00 + ], + [ + 3445.00, + 2640.00 + ], + [ + 3440.00, + 2634.00 + ], + [ + 3436.00, + 2629.00 + ], + [ + 3432.00, + 2624.00 + ], + [ + 3428.00, + 2616.00 + ], + [ + 3428.00, + 2615.00 + ], + [ + 3428.00, + 2614.00 + ], + [ + 3426.00, + 2605.00 + ], + [ + 3425.00, + 2596.00 + ], + [ + 3422.00, + 2578.00 + ], + [ + 3422.00, + 2575.00 + ], + [ + 3418.00, + 2565.00 + ], + [ + 3414.00, + 2555.00 + ], + [ + 3413.00, + 2549.00 + ], + [ + 3412.00, + 2545.00 + ], + [ + 3411.00, + 2541.00 + ], + [ + 3410.00, + 2534.00 + ], + [ + 3411.00, + 2530.00 + ], + [ + 3449.00, + 2518.00 + ], + [ + 3448.00, + 2517.00 + ], + [ + 3456.00, + 2517.00 + ], + [ + 3464.00, + 2517.00 + ], + [ + 3476.00, + 2517.00 + ], + [ + 3480.00, + 2518.00 + ], + [ + 3484.00, + 2519.00 + ], + [ + 3489.00, + 2520.00 + ], + [ + 3493.00, + 2519.00 + ], + [ + 3502.00, + 2512.00 + ], + [ + 3503.00, + 2512.00 + ], + [ + 3503.00, + 2505.00 + ], + [ + 3503.00, + 2489.00 + ], + [ + 3508.00, + 2484.00 + ], + [ + 3508.00, + 2484.00 + ], + [ + 3508.00, + 2484.00 + ], + [ + 3521.00, + 2482.00 + ], + [ + 3526.00, + 2481.00 + ], + [ + 3531.00, + 2480.00 + ], + [ + 3539.00, + 2481.00 + ], + [ + 3540.00, + 2480.00 + ], + [ + 3541.00, + 2479.00 + ], + [ + 3544.00, + 2478.00 + ], + [ + 3552.00, + 2479.00 + ], + [ + 3560.00, + 2480.00 + ], + [ + 3570.00, + 2481.00 + ], + [ + 3574.00, + 2482.00 + ], + [ + 3578.00, + 2483.00 + ], + [ + 3588.00, + 2484.00 + ], + [ + 3594.00, + 2485.00 + ], + [ + 3600.00, + 2486.00 + ], + [ + 3612.00, + 2486.00 + ], + [ + 3612.00, + 2486.00 + ], + [ + 3612.00, + 2486.00 + ], + [ + 3617.00, + 2488.00 + ], + [ + 3620.00, + 2492.00 + ], + [ + 3623.00, + 2496.00 + ], + [ + 3624.00, + 2496.00 + ], + [ + 3625.00, + 2503.00 + ], + [ + 3626.00, + 2510.00 + ], + [ + 3626.00, + 2510.00 + ], + [ + 3625.00, + 2516.00 + ], + [ + 3622.00, + 2534.00 + ], + [ + 3622.00, + 2536.00 + ], + [ + 3621.00, + 2541.00 + ] + ] + }, + { + "name": "Merry & Pippin", + "id": "merry_pippin", + "color": "brown", + "distance": "2100 miles / 3400 km", + "startDate": "September 23, T.A. 3018", + "endDate": "March 25, T.A. 3019", + "path": [ + [ + 1484.00, + 1161.00 + ], + [ + 1492.00, + 1175.00 + ], + [ + 1492.00, + 1175.00 + ], + [ + 1492.00, + 1175.00 + ], + [ + 1502.00, + 1183.00 + ], + [ + 1502.00, + 1183.00 + ], + [ + 1502.00, + 1183.00 + ], + [ + 1511.00, + 1194.00 + ], + [ + 1511.00, + 1194.00 + ], + [ + 1511.00, + 1194.00 + ], + [ + 1532.00, + 1206.00 + ], + [ + 1532.00, + 1206.00 + ], + [ + 1532.00, + 1206.00 + ], + [ + 1554.00, + 1216.00 + ], + [ + 1554.00, + 1216.00 + ], + [ + 1554.00, + 1216.00 + ], + [ + 1574.00, + 1219.00 + ], + [ + 1574.00, + 1219.00 + ], + [ + 1574.00, + 1219.00 + ], + [ + 1588.00, + 1220.00 + ], + [ + 1588.00, + 1220.00 + ], + [ + 1588.00, + 1220.00 + ], + [ + 1597.00, + 1212.00 + ], + [ + 1597.00, + 1212.00 + ], + [ + 1597.00, + 1212.00 + ], + [ + 1608.00, + 1207.00 + ], + [ + 1608.00, + 1207.00 + ], + [ + 1608.00, + 1207.00 + ], + [ + 1618.00, + 1207.00 + ], + [ + 1618.00, + 1207.00 + ], + [ + 1618.00, + 1207.00 + ], + [ + 1639.00, + 1207.00 + ], + [ + 1639.00, + 1207.00 + ], + [ + 1639.00, + 1207.00 + ], + [ + 1652.00, + 1206.00 + ], + [ + 1652.00, + 1206.00 + ], + [ + 1652.00, + 1206.00 + ], + [ + 1659.00, + 1203.00 + ], + [ + 1661.00, + 1203.00 + ], + [ + 1663.00, + 1203.00 + ], + [ + 1682.00, + 1200.00 + ], + [ + 1683.00, + 1200.00 + ], + [ + 1684.00, + 1200.00 + ], + [ + 1692.00, + 1198.00 + ], + [ + 1693.00, + 1198.00 + ], + [ + 1694.00, + 1198.00 + ], + [ + 1704.00, + 1200.00 + ], + [ + 1704.00, + 1200.00 + ], + [ + 1704.00, + 1200.00 + ], + [ + 1716.00, + 1199.00 + ], + [ + 1717.00, + 1199.00 + ], + [ + 1718.00, + 1199.00 + ], + [ + 1744.00, + 1200.00 + ], + [ + 1744.00, + 1200.00 + ], + [ + 1744.00, + 1200.00 + ], + [ + 1757.00, + 1201.00 + ], + [ + 1757.00, + 1201.00 + ], + [ + 1757.00, + 1201.00 + ], + [ + 1772.00, + 1208.00 + ], + [ + 1772.00, + 1208.00 + ], + [ + 1772.00, + 1208.00 + ], + [ + 1786.00, + 1214.00 + ], + [ + 1786.00, + 1214.00 + ], + [ + 1786.00, + 1214.00 + ], + [ + 1781.00, + 1199.00 + ], + [ + 1781.00, + 1199.00 + ], + [ + 1781.00, + 1199.00 + ], + [ + 1780.00, + 1186.00 + ], + [ + 1780.00, + 1186.00 + ], + [ + 1780.00, + 1186.00 + ], + [ + 1773.00, + 1175.00 + ], + [ + 1776.00, + 1173.00 + ], + [ + 1779.00, + 1171.00 + ], + [ + 1792.00, + 1159.00 + ], + [ + 1792.00, + 1159.00 + ], + [ + 1792.00, + 1159.00 + ], + [ + 1800.00, + 1149.00 + ], + [ + 1802.00, + 1148.00 + ], + [ + 1804.00, + 1147.00 + ], + [ + 1818.00, + 1133.00 + ], + [ + 1818.00, + 1133.00 + ], + [ + 1818.00, + 1133.00 + ], + [ + 1830.00, + 1128.00 + ], + [ + 1839.00, + 1128.00 + ], + [ + 1848.00, + 1128.00 + ], + [ + 1879.00, + 1131.00 + ], + [ + 1879.00, + 1131.00 + ], + [ + 1879.00, + 1131.00 + ], + [ + 1900.00, + 1131.00 + ], + [ + 1901.00, + 1131.00 + ], + [ + 1902.00, + 1131.00 + ], + [ + 1951.00, + 1132.00 + ], + [ + 1951.00, + 1132.00 + ], + [ + 1951.00, + 1132.00 + ], + [ + 1965.00, + 1136.00 + ], + [ + 1965.00, + 1136.00 + ], + [ + 1965.00, + 1136.00 + ], + [ + 1978.00, + 1144.00 + ], + [ + 1978.00, + 1144.00 + ], + [ + 1978.00, + 1144.00 + ], + [ + 1999.00, + 1150.00 + ], + [ + 1999.00, + 1150.00 + ], + [ + 1999.00, + 1150.00 + ], + [ + 1999.00, + 1168.00 + ], + [ + 1999.00, + 1168.00 + ], + [ + 1999.00, + 1168.00 + ], + [ + 1999.00, + 1177.00 + ], + [ + 2000.00, + 1179.00 + ], + [ + 2001.00, + 1181.00 + ], + [ + 2006.00, + 1188.00 + ], + [ + 2009.00, + 1192.00 + ], + [ + 2012.00, + 1196.00 + ], + [ + 2018.00, + 1196.00 + ], + [ + 2025.00, + 1199.00 + ], + [ + 2032.00, + 1202.00 + ], + [ + 2055.00, + 1203.00 + ], + [ + 2055.00, + 1203.00 + ], + [ + 2055.00, + 1203.00 + ], + [ + 2088.00, + 1203.00 + ], + [ + 2089.00, + 1203.00 + ], + [ + 2090.00, + 1203.00 + ], + [ + 2106.00, + 1199.00 + ], + [ + 2108.00, + 1199.00 + ], + [ + 2110.00, + 1199.00 + ], + [ + 2129.00, + 1198.00 + ], + [ + 2134.00, + 1196.00 + ], + [ + 2139.00, + 1194.00 + ], + [ + 2168.00, + 1192.00 + ], + [ + 2172.00, + 1190.00 + ], + [ + 2176.00, + 1188.00 + ], + [ + 2184.00, + 1177.00 + ], + [ + 2190.00, + 1174.00 + ], + [ + 2196.00, + 1171.00 + ], + [ + 2208.00, + 1165.00 + ], + [ + 2211.00, + 1164.00 + ], + [ + 2214.00, + 1163.00 + ], + [ + 2222.00, + 1156.00 + ], + [ + 2226.00, + 1154.00 + ], + [ + 2230.00, + 1152.00 + ], + [ + 2240.00, + 1146.00 + ], + [ + 2243.00, + 1144.00 + ], + [ + 2246.00, + 1142.00 + ], + [ + 2262.00, + 1135.00 + ], + [ + 2262.00, + 1135.00 + ], + [ + 2262.00, + 1135.00 + ], + [ + 2273.00, + 1132.00 + ], + [ + 2274.00, + 1132.00 + ], + [ + 2275.00, + 1132.00 + ], + [ + 2287.00, + 1116.00 + ], + [ + 2287.00, + 1116.00 + ], + [ + 2287.00, + 1116.00 + ], + [ + 2289.00, + 1114.00 + ], + [ + 2293.00, + 1108.00 + ], + [ + 2297.00, + 1102.00 + ], + [ + 2304.00, + 1096.00 + ], + [ + 2304.00, + 1095.00 + ], + [ + 2304.00, + 1094.00 + ], + [ + 2306.00, + 1093.00 + ], + [ + 2309.00, + 1085.00 + ], + [ + 2312.00, + 1077.00 + ], + [ + 2324.00, + 1054.00 + ], + [ + 2324.00, + 1054.00 + ], + [ + 2324.00, + 1054.00 + ], + [ + 2334.00, + 1044.00 + ], + [ + 2334.00, + 1044.00 + ], + [ + 2334.00, + 1044.00 + ], + [ + 2336.00, + 1058.00 + ], + [ + 2335.00, + 1059.00 + ], + [ + 2334.00, + 1060.00 + ], + [ + 2331.00, + 1071.00 + ], + [ + 2331.00, + 1072.00 + ], + [ + 2331.00, + 1073.00 + ], + [ + 2328.00, + 1078.00 + ], + [ + 2328.00, + 1083.00 + ], + [ + 2328.00, + 1088.00 + ], + [ + 2336.00, + 1108.00 + ], + [ + 2337.00, + 1108.00 + ], + [ + 2338.00, + 1108.00 + ], + [ + 2338.00, + 1109.00 + ], + [ + 2339.00, + 1114.00 + ], + [ + 2351.00, + 1128.00 + ], + [ + 2360.00, + 1131.00 + ], + [ + 2361.00, + 1132.00 + ], + [ + 2362.00, + 1133.00 + ], + [ + 2385.00, + 1143.00 + ], + [ + 2385.00, + 1143.00 + ], + [ + 2385.00, + 1143.00 + ], + [ + 2391.00, + 1144.00 + ], + [ + 2395.00, + 1144.00 + ], + [ + 2399.00, + 1144.00 + ], + [ + 2404.00, + 1144.00 + ], + [ + 2407.00, + 1145.00 + ], + [ + 2410.00, + 1146.00 + ], + [ + 2421.00, + 1144.00 + ], + [ + 2421.00, + 1144.00 + ], + [ + 2421.00, + 1144.00 + ], + [ + 2443.00, + 1147.00 + ], + [ + 2443.00, + 1147.00 + ], + [ + 2443.00, + 1147.00 + ], + [ + 2453.00, + 1148.00 + ], + [ + 2454.00, + 1149.00 + ], + [ + 2455.00, + 1150.00 + ], + [ + 2470.00, + 1150.00 + ], + [ + 2470.00, + 1150.00 + ], + [ + 2470.00, + 1150.00 + ], + [ + 2484.00, + 1155.00 + ], + [ + 2484.00, + 1155.00 + ], + [ + 2484.00, + 1155.00 + ], + [ + 2492.00, + 1151.00 + ], + [ + 2492.00, + 1151.00 + ], + [ + 2492.00, + 1151.00 + ], + [ + 2507.00, + 1147.00 + ], + [ + 2507.00, + 1147.00 + ], + [ + 2507.00, + 1147.00 + ], + [ + 2514.00, + 1132.00 + ], + [ + 2514.00, + 1132.00 + ], + [ + 2501.00, + 1162.00 + ], + [ + 2500.00, + 1163.00 + ], + [ + 2505.00, + 1178.00 + ], + [ + 2505.00, + 1178.00 + ], + [ + 2505.00, + 1178.00 + ], + [ + 2510.00, + 1189.00 + ], + [ + 2510.00, + 1189.00 + ], + [ + 2510.00, + 1189.00 + ], + [ + 2515.00, + 1202.00 + ], + [ + 2516.00, + 1206.00 + ], + [ + 2517.00, + 1210.00 + ], + [ + 2509.00, + 1231.00 + ], + [ + 2509.00, + 1231.00 + ], + [ + 2509.00, + 1231.00 + ], + [ + 2509.00, + 1233.00 + ], + [ + 2507.00, + 1237.00 + ], + [ + 2505.00, + 1241.00 + ], + [ + 2494.00, + 1271.00 + ], + [ + 2492.00, + 1273.00 + ], + [ + 2490.00, + 1275.00 + ], + [ + 2493.00, + 1281.00 + ], + [ + 2491.00, + 1287.00 + ], + [ + 2471.00, + 1331.00 + ], + [ + 2473.00, + 1332.00 + ], + [ + 2470.00, + 1340.00 + ], + [ + 2467.00, + 1348.00 + ], + [ + 2466.00, + 1348.00 + ], + [ + 2463.00, + 1355.00 + ], + [ + 2457.00, + 1367.00 + ], + [ + 2448.00, + 1376.00 + ], + [ + 2448.00, + 1376.00 + ], + [ + 2448.00, + 1376.00 + ], + [ + 2462.00, + 1387.00 + ], + [ + 2462.00, + 1387.00 + ], + [ + 2462.00, + 1387.00 + ], + [ + 2485.00, + 1393.00 + ], + [ + 2485.00, + 1393.00 + ], + [ + 2485.00, + 1393.00 + ], + [ + 2499.00, + 1398.00 + ], + [ + 2501.00, + 1398.00 + ], + [ + 2503.00, + 1398.00 + ], + [ + 2517.00, + 1402.00 + ], + [ + 2516.00, + 1403.00 + ], + [ + 2515.00, + 1404.00 + ], + [ + 2498.00, + 1404.00 + ], + [ + 2498.00, + 1404.00 + ], + [ + 2498.00, + 1404.00 + ], + [ + 2484.00, + 1404.00 + ], + [ + 2476.00, + 1402.00 + ], + [ + 2468.00, + 1400.00 + ], + [ + 2465.00, + 1400.00 + ], + [ + 2460.00, + 1401.00 + ], + [ + 2455.00, + 1402.00 + ], + [ + 2441.00, + 1404.00 + ], + [ + 2440.00, + 1406.00 + ], + [ + 2439.00, + 1408.00 + ], + [ + 2436.00, + 1415.00 + ], + [ + 2434.00, + 1419.00 + ], + [ + 2432.00, + 1423.00 + ], + [ + 2427.00, + 1437.00 + ], + [ + 2426.00, + 1438.00 + ], + [ + 2425.00, + 1439.00 + ], + [ + 2421.00, + 1449.00 + ], + [ + 2419.00, + 1453.00 + ], + [ + 2417.00, + 1457.00 + ], + [ + 2411.00, + 1468.00 + ], + [ + 2410.00, + 1471.00 + ], + [ + 2409.00, + 1474.00 + ], + [ + 2405.00, + 1480.00 + ], + [ + 2403.00, + 1488.00 + ], + [ + 2401.00, + 1496.00 + ], + [ + 2400.00, + 1505.00 + ], + [ + 2399.00, + 1507.00 + ], + [ + 2398.00, + 1509.00 + ], + [ + 2394.00, + 1523.00 + ], + [ + 2394.00, + 1526.00 + ], + [ + 2394.00, + 1529.00 + ], + [ + 2401.00, + 1533.00 + ], + [ + 2404.00, + 1535.00 + ], + [ + 2407.00, + 1537.00 + ], + [ + 2426.00, + 1540.00 + ], + [ + 2427.00, + 1540.00 + ], + [ + 2428.00, + 1540.00 + ], + [ + 2439.00, + 1538.00 + ], + [ + 2439.00, + 1538.00 + ], + [ + 2439.00, + 1538.00 + ], + [ + 2457.00, + 1537.00 + ], + [ + 2457.00, + 1537.00 + ], + [ + 2457.00, + 1537.00 + ], + [ + 2469.00, + 1533.00 + ], + [ + 2473.00, + 1533.00 + ], + [ + 2477.00, + 1533.00 + ], + [ + 2503.00, + 1535.00 + ], + [ + 2503.00, + 1535.00 + ], + [ + 2503.00, + 1535.00 + ], + [ + 2512.00, + 1534.00 + ], + [ + 2513.00, + 1534.00 + ], + [ + 2514.00, + 1534.00 + ], + [ + 2526.00, + 1537.00 + ], + [ + 2526.00, + 1537.00 + ], + [ + 2526.00, + 1537.00 + ], + [ + 2536.00, + 1543.00 + ], + [ + 2537.00, + 1544.00 + ], + [ + 2538.00, + 1545.00 + ], + [ + 2551.00, + 1556.00 + ], + [ + 2551.00, + 1556.00 + ], + [ + 2551.00, + 1556.00 + ], + [ + 2558.00, + 1573.00 + ], + [ + 2558.00, + 1573.00 + ], + [ + 2558.00, + 1573.00 + ], + [ + 2565.00, + 1593.00 + ], + [ + 2565.00, + 1593.00 + ], + [ + 2565.00, + 1593.00 + ], + [ + 2576.00, + 1603.00 + ], + [ + 2576.00, + 1603.00 + ], + [ + 2576.00, + 1603.00 + ], + [ + 2593.00, + 1610.00 + ], + [ + 2593.00, + 1610.00 + ], + [ + 2593.00, + 1610.00 + ], + [ + 2601.00, + 1627.00 + ], + [ + 2601.00, + 1627.00 + ], + [ + 2601.00, + 1627.00 + ], + [ + 2598.00, + 1645.00 + ], + [ + 2598.00, + 1646.00 + ], + [ + 2598.00, + 1647.00 + ], + [ + 2611.00, + 1651.00 + ], + [ + 2612.00, + 1652.00 + ], + [ + 2613.00, + 1653.00 + ], + [ + 2624.00, + 1665.00 + ], + [ + 2624.00, + 1666.00 + ], + [ + 2624.00, + 1667.00 + ], + [ + 2632.00, + 1675.00 + ], + [ + 2634.00, + 1677.00 + ], + [ + 2636.00, + 1679.00 + ], + [ + 2644.00, + 1683.00 + ], + [ + 2645.00, + 1684.00 + ], + [ + 2646.00, + 1685.00 + ], + [ + 2658.00, + 1693.00 + ], + [ + 2659.00, + 1693.00 + ], + [ + 2660.00, + 1693.00 + ], + [ + 2673.00, + 1699.00 + ], + [ + 2673.00, + 1699.00 + ], + [ + 2673.00, + 1699.00 + ], + [ + 2696.00, + 1712.00 + ], + [ + 2696.00, + 1712.00 + ], + [ + 2696.00, + 1712.00 + ], + [ + 2720.00, + 1722.00 + ], + [ + 2720.00, + 1722.00 + ], + [ + 2720.00, + 1722.00 + ], + [ + 2744.00, + 1726.00 + ], + [ + 2744.00, + 1727.00 + ], + [ + 2744.00, + 1728.00 + ], + [ + 2770.00, + 1731.00 + ], + [ + 2770.00, + 1731.00 + ], + [ + 2770.00, + 1731.00 + ], + [ + 2787.00, + 1744.00 + ], + [ + 2787.00, + 1745.00 + ], + [ + 2787.00, + 1746.00 + ], + [ + 2778.00, + 1760.00 + ], + [ + 2784.00, + 1766.00 + ], + [ + 2790.00, + 1772.00 + ], + [ + 2791.00, + 1783.00 + ], + [ + 2791.00, + 1785.00 + ], + [ + 2791.00, + 1787.00 + ], + [ + 2794.00, + 1802.00 + ], + [ + 2794.00, + 1802.00 + ], + [ + 2794.00, + 1802.00 + ], + [ + 2799.00, + 1809.00 + ], + [ + 2801.00, + 1810.00 + ], + [ + 2803.00, + 1811.00 + ], + [ + 2815.00, + 1818.00 + ], + [ + 2818.00, + 1819.00 + ], + [ + 2821.00, + 1820.00 + ], + [ + 2836.00, + 1826.00 + ], + [ + 2837.00, + 1827.00 + ], + [ + 2838.00, + 1828.00 + ], + [ + 2849.00, + 1836.00 + ], + [ + 2850.00, + 1836.00 + ], + [ + 2851.00, + 1836.00 + ], + [ + 2864.00, + 1844.00 + ], + [ + 2866.00, + 1846.00 + ], + [ + 2868.00, + 1848.00 + ], + [ + 2876.00, + 1853.00 + ], + [ + 2881.00, + 1857.00 + ], + [ + 2886.00, + 1861.00 + ], + [ + 2899.00, + 1872.00 + ], + [ + 2900.00, + 1873.00 + ], + [ + 2901.00, + 1874.00 + ], + [ + 2907.00, + 1878.00 + ], + [ + 2910.00, + 1879.00 + ], + [ + 2913.00, + 1880.00 + ], + [ + 2934.00, + 1887.00 + ], + [ + 2934.00, + 1887.00 + ], + [ + 2934.00, + 1887.00 + ], + [ + 2939.00, + 1885.00 + ], + [ + 2943.00, + 1889.00 + ], + [ + 2947.00, + 1893.00 + ], + [ + 2956.00, + 1899.00 + ], + [ + 2956.00, + 1900.00 + ], + [ + 2956.00, + 1901.00 + ], + [ + 2955.00, + 1908.00 + ], + [ + 2955.00, + 1909.00 + ], + [ + 2955.00, + 1910.00 + ], + [ + 2950.00, + 1919.00 + ], + [ + 2949.00, + 1921.00 + ], + [ + 2948.00, + 1923.00 + ], + [ + 2941.00, + 1931.00 + ], + [ + 2941.00, + 1933.00 + ], + [ + 2941.00, + 1935.00 + ], + [ + 2938.00, + 1956.00 + ], + [ + 2938.00, + 1956.00 + ], + [ + 2938.00, + 1956.00 + ], + [ + 2944.00, + 1966.00 + ], + [ + 2945.00, + 1968.00 + ], + [ + 2946.00, + 1970.00 + ], + [ + 2948.00, + 1975.00 + ], + [ + 2954.00, + 1979.00 + ], + [ + 2960.00, + 1983.00 + ], + [ + 2966.00, + 1984.00 + ], + [ + 2966.00, + 1984.00 + ], + [ + 2966.00, + 1984.00 + ], + [ + 2973.00, + 1984.00 + ], + [ + 2973.00, + 1984.00 + ], + [ + 2973.00, + 1984.00 + ], + [ + 2990.00, + 1982.00 + ], + [ + 2990.00, + 1982.00 + ], + [ + 2990.00, + 1982.00 + ], + [ + 3007.00, + 1975.00 + ], + [ + 3007.00, + 1975.00 + ], + [ + 3007.00, + 1975.00 + ], + [ + 3027.00, + 1983.00 + ], + [ + 3027.00, + 1984.00 + ], + [ + 3027.00, + 1985.00 + ], + [ + 3031.00, + 1998.00 + ], + [ + 3031.00, + 2000.00 + ], + [ + 3031.00, + 2002.00 + ], + [ + 3022.00, + 2019.00 + ], + [ + 3021.00, + 2020.00 + ], + [ + 3020.00, + 2021.00 + ], + [ + 3011.00, + 2037.00 + ], + [ + 3010.00, + 2038.00 + ], + [ + 3009.00, + 2039.00 + ], + [ + 3003.00, + 2047.00 + ], + [ + 3003.00, + 2051.00 + ], + [ + 3003.00, + 2055.00 + ], + [ + 3004.00, + 2069.00 + ], + [ + 3005.00, + 2070.00 + ], + [ + 3006.00, + 2071.00 + ], + [ + 3025.00, + 2080.00 + ], + [ + 3025.00, + 2080.00 + ], + [ + 3025.00, + 2080.00 + ], + [ + 3043.00, + 2089.00 + ], + [ + 3043.00, + 2089.00 + ], + [ + 3043.00, + 2089.00 + ], + [ + 3054.00, + 2099.00 + ], + [ + 3054.00, + 2099.00 + ], + [ + 3054.00, + 2099.00 + ], + [ + 3054.00, + 2112.00 + ], + [ + 3054.00, + 2113.00 + ], + [ + 3054.00, + 2114.00 + ], + [ + 3053.00, + 2129.00 + ], + [ + 3052.00, + 2130.00 + ], + [ + 3051.00, + 2131.00 + ], + [ + 3046.00, + 2148.00 + ], + [ + 3046.00, + 2149.00 + ], + [ + 3046.00, + 2150.00 + ], + [ + 3043.00, + 2163.00 + ], + [ + 3043.00, + 2163.00 + ], + [ + 3043.00, + 2163.00 + ], + [ + 3039.00, + 2175.00 + ], + [ + 3039.00, + 2176.00 + ], + [ + 3039.00, + 2177.00 + ], + [ + 3034.00, + 2187.00 + ], + [ + 3033.00, + 2190.00 + ], + [ + 3032.00, + 2193.00 + ], + [ + 3030.00, + 2207.00 + ], + [ + 3029.00, + 2211.00 + ], + [ + 3028.00, + 2215.00 + ], + [ + 3024.00, + 2225.00 + ], + [ + 3024.00, + 2228.00 + ], + [ + 3024.00, + 2231.00 + ], + [ + 3028.00, + 2246.00 + ], + [ + 3028.00, + 2249.00 + ], + [ + 3028.00, + 2252.00 + ], + [ + 3031.00, + 2266.00 + ], + [ + 3031.00, + 2268.00 + ], + [ + 3031.00, + 2270.00 + ], + [ + 3033.00, + 2282.00 + ], + [ + 3035.00, + 2285.00 + ], + [ + 3037.00, + 2288.00 + ], + [ + 3039.00, + 2302.00 + ], + [ + 3039.00, + 2302.00 + ], + [ + 3039.00, + 2302.00 + ], + [ + 3042.00, + 2320.00 + ], + [ + 3042.00, + 2320.00 + ], + [ + 3042.00, + 2320.00 + ], + [ + 3045.00, + 2342.00 + ], + [ + 3045.00, + 2345.00 + ], + [ + 3045.00, + 2348.00 + ], + [ + 3053.00, + 2365.00 + ], + [ + 3053.00, + 2366.00 + ], + [ + 3053.00, + 2367.00 + ], + [ + 3049.00, + 2383.00 + ], + [ + 3049.00, + 2383.00 + ], + [ + 3049.00, + 2383.00 + ], + [ + 3031.00, + 2368.00 + ], + [ + 3031.00, + 2368.00 + ], + [ + 3031.00, + 2368.00 + ], + [ + 3016.00, + 2355.00 + ], + [ + 3016.00, + 2355.00 + ], + [ + 3016.00, + 2355.00 + ], + [ + 3009.00, + 2340.00 + ], + [ + 3009.00, + 2340.00 + ], + [ + 3009.00, + 2340.00 + ], + [ + 2992.00, + 2331.00 + ], + [ + 2989.00, + 2328.00 + ], + [ + 2986.00, + 2325.00 + ], + [ + 2970.00, + 2308.00 + ], + [ + 2970.00, + 2308.00 + ], + [ + 2970.00, + 2308.00 + ], + [ + 2955.00, + 2302.00 + ], + [ + 2952.00, + 2301.00 + ], + [ + 2949.00, + 2300.00 + ], + [ + 2927.00, + 2293.00 + ], + [ + 2924.00, + 2293.00 + ], + [ + 2921.00, + 2293.00 + ], + [ + 2904.00, + 2291.00 + ], + [ + 2904.00, + 2291.00 + ], + [ + 2904.00, + 2291.00 + ], + [ + 2892.00, + 2288.00 + ], + [ + 2891.00, + 2287.00 + ], + [ + 2890.00, + 2286.00 + ], + [ + 2878.00, + 2269.00 + ], + [ + 2878.00, + 2269.00 + ], + [ + 2878.00, + 2269.00 + ], + [ + 2865.00, + 2250.00 + ], + [ + 2865.00, + 2250.00 + ], + [ + 2865.00, + 2250.00 + ], + [ + 2850.00, + 2233.00 + ], + [ + 2850.00, + 2233.00 + ], + [ + 2850.00, + 2233.00 + ], + [ + 2823.00, + 2214.00 + ], + [ + 2823.00, + 2214.00 + ], + [ + 2823.00, + 2214.00 + ], + [ + 2808.00, + 2194.00 + ], + [ + 2808.00, + 2194.00 + ], + [ + 2808.00, + 2194.00 + ], + [ + 2791.00, + 2168.00 + ], + [ + 2791.00, + 2168.00 + ], + [ + 2791.00, + 2168.00 + ], + [ + 2761.00, + 2150.00 + ], + [ + 2761.00, + 2150.00 + ], + [ + 2761.00, + 2150.00 + ], + [ + 2736.00, + 2131.00 + ], + [ + 2736.00, + 2131.00 + ], + [ + 2736.00, + 2131.00 + ], + [ + 2703.00, + 2119.00 + ], + [ + 2703.00, + 2119.00 + ], + [ + 2703.00, + 2119.00 + ], + [ + 2671.00, + 2103.00 + ], + [ + 2671.00, + 2103.00 + ], + [ + 2671.00, + 2103.00 + ], + [ + 2644.00, + 2089.00 + ], + [ + 2644.00, + 2089.00 + ], + [ + 2644.00, + 2089.00 + ], + [ + 2623.00, + 2084.00 + ], + [ + 2623.00, + 2084.00 + ], + [ + 2623.00, + 2084.00 + ], + [ + 2607.00, + 2088.00 + ], + [ + 2606.00, + 2088.00 + ], + [ + 2605.00, + 2088.00 + ], + [ + 2596.00, + 2089.00 + ], + [ + 2596.00, + 2089.00 + ], + [ + 2596.00, + 2089.00 + ], + [ + 2572.00, + 2083.00 + ], + [ + 2571.00, + 2082.00 + ], + [ + 2570.00, + 2081.00 + ], + [ + 2564.00, + 2077.00 + ], + [ + 2560.00, + 2076.00 + ], + [ + 2556.00, + 2075.00 + ], + [ + 2541.00, + 2071.00 + ], + [ + 2539.00, + 2071.00 + ], + [ + 2537.00, + 2071.00 + ], + [ + 2527.00, + 2068.00 + ], + [ + 2527.00, + 2068.00 + ], + [ + 2527.00, + 2068.00 + ], + [ + 2501.00, + 2063.00 + ], + [ + 2501.00, + 2063.00 + ], + [ + 2501.00, + 2063.00 + ], + [ + 2474.00, + 2059.00 + ], + [ + 2474.00, + 2059.00 + ], + [ + 2474.00, + 2059.00 + ], + [ + 2457.00, + 2053.00 + ], + [ + 2457.00, + 2053.00 + ], + [ + 2457.00, + 2053.00 + ], + [ + 2446.00, + 2048.00 + ], + [ + 2446.00, + 2048.00 + ], + [ + 2446.00, + 2048.00 + ], + [ + 2434.00, + 2039.00 + ], + [ + 2434.00, + 2039.00 + ], + [ + 2434.00, + 2039.00 + ], + [ + 2438.00, + 2050.00 + ], + [ + 2438.00, + 2051.00 + ], + [ + 2438.00, + 2052.00 + ], + [ + 2440.00, + 2062.00 + ], + [ + 2440.00, + 2063.00 + ], + [ + 2440.00, + 2064.00 + ], + [ + 2440.00, + 2074.00 + ], + [ + 2440.00, + 2076.00 + ], + [ + 2440.00, + 2078.00 + ], + [ + 2436.00, + 2080.00 + ], + [ + 2433.00, + 2081.00 + ], + [ + 2430.00, + 2082.00 + ], + [ + 2419.00, + 2083.00 + ], + [ + 2415.00, + 2084.00 + ], + [ + 2411.00, + 2085.00 + ], + [ + 2401.00, + 2085.00 + ], + [ + 2401.00, + 2085.00 + ], + [ + 2401.00, + 2085.00 + ], + [ + 2384.00, + 2088.00 + ], + [ + 2384.00, + 2088.00 + ], + [ + 2384.00, + 2088.00 + ], + [ + 2366.00, + 2091.00 + ], + [ + 2364.00, + 2093.00 + ], + [ + 2362.00, + 2095.00 + ], + [ + 2350.00, + 2102.00 + ], + [ + 2349.00, + 2103.00 + ], + [ + 2348.00, + 2104.00 + ], + [ + 2338.00, + 2116.00 + ], + [ + 2338.00, + 2116.00 + ], + [ + 2338.00, + 2116.00 + ], + [ + 2336.00, + 2138.00 + ], + [ + 2336.00, + 2138.00 + ], + [ + 2336.00, + 2138.00 + ], + [ + 2329.00, + 2148.00 + ], + [ + 2329.00, + 2151.00 + ], + [ + 2329.00, + 2154.00 + ], + [ + 2322.00, + 2173.00 + ], + [ + 2322.00, + 2173.00 + ], + [ + 2322.00, + 2173.00 + ], + [ + 2324.00, + 2188.00 + ], + [ + 2324.00, + 2188.00 + ], + [ + 2324.00, + 2188.00 + ], + [ + 2328.00, + 2202.00 + ], + [ + 2328.00, + 2202.00 + ], + [ + 2328.00, + 2202.00 + ], + [ + 2338.00, + 2218.00 + ], + [ + 2338.00, + 2218.00 + ], + [ + 2338.00, + 2218.00 + ], + [ + 2341.00, + 2226.00 + ], + [ + 2342.00, + 2227.00 + ], + [ + 2343.00, + 2228.00 + ], + [ + 2345.00, + 2233.00 + ], + [ + 2346.00, + 2236.00 + ], + [ + 2347.00, + 2239.00 + ], + [ + 2345.00, + 2244.00 + ], + [ + 2345.00, + 2245.00 + ], + [ + 2345.00, + 2246.00 + ], + [ + 2349.00, + 2257.00 + ], + [ + 2349.00, + 2258.00 + ], + [ + 2349.00, + 2259.00 + ], + [ + 2355.00, + 2257.00 + ], + [ + 2356.00, + 2257.00 + ], + [ + 2357.00, + 2257.00 + ], + [ + 2372.00, + 2259.00 + ], + [ + 2372.00, + 2259.00 + ], + [ + 2372.00, + 2259.00 + ], + [ + 2380.00, + 2261.00 + ], + [ + 2382.00, + 2262.00 + ], + [ + 2384.00, + 2263.00 + ], + [ + 2397.00, + 2266.00 + ], + [ + 2397.00, + 2266.00 + ], + [ + 2397.00, + 2266.00 + ], + [ + 2411.00, + 2270.00 + ], + [ + 2411.00, + 2270.00 + ], + [ + 2411.00, + 2270.00 + ], + [ + 2422.00, + 2284.00 + ], + [ + 2422.00, + 2285.00 + ], + [ + 2422.00, + 2286.00 + ], + [ + 2418.00, + 2301.00 + ], + [ + 2418.00, + 2301.00 + ], + [ + 2418.00, + 2301.00 + ], + [ + 2420.00, + 2319.00 + ], + [ + 2420.00, + 2319.00 + ], + [ + 2420.00, + 2319.00 + ], + [ + 2424.00, + 2329.00 + ], + [ + 2424.00, + 2331.00 + ], + [ + 2424.00, + 2333.00 + ], + [ + 2428.00, + 2342.00 + ], + [ + 2429.00, + 2344.00 + ], + [ + 2430.00, + 2346.00 + ], + [ + 2430.00, + 2349.00 + ], + [ + 2431.00, + 2352.00 + ], + [ + 2432.00, + 2355.00 + ], + [ + 2435.00, + 2365.00 + ], + [ + 2436.00, + 2366.00 + ], + [ + 2437.00, + 2367.00 + ], + [ + 2450.00, + 2373.00 + ], + [ + 2450.00, + 2373.00 + ], + [ + 2450.00, + 2373.00 + ], + [ + 2465.00, + 2377.00 + ], + [ + 2466.00, + 2377.00 + ], + [ + 2467.00, + 2377.00 + ], + [ + 2484.00, + 2384.00 + ], + [ + 2485.00, + 2384.00 + ], + [ + 2486.00, + 2384.00 + ], + [ + 2499.00, + 2387.00 + ], + [ + 2499.00, + 2387.00 + ], + [ + 2499.00, + 2387.00 + ], + [ + 2506.00, + 2390.00 + ], + [ + 2508.00, + 2390.00 + ], + [ + 2510.00, + 2390.00 + ], + [ + 2524.00, + 2384.00 + ], + [ + 2526.00, + 2384.00 + ], + [ + 2528.00, + 2384.00 + ], + [ + 2535.00, + 2381.00 + ], + [ + 2540.00, + 2381.00 + ], + [ + 2545.00, + 2381.00 + ], + [ + 2557.00, + 2380.00 + ], + [ + 2563.00, + 2380.00 + ], + [ + 2578.00, + 2382.00 + ], + [ + 2588.00, + 2385.00 + ], + [ + 2588.00, + 2385.00 + ], + [ + 2588.00, + 2385.00 + ], + [ + 2611.00, + 2396.00 + ], + [ + 2611.00, + 2396.00 + ], + [ + 2611.00, + 2396.00 + ], + [ + 2615.00, + 2411.00 + ], + [ + 2616.00, + 2412.00 + ], + [ + 2617.00, + 2413.00 + ], + [ + 2629.00, + 2430.00 + ], + [ + 2630.00, + 2432.00 + ], + [ + 2631.00, + 2434.00 + ], + [ + 2637.00, + 2439.00 + ], + [ + 2639.00, + 2443.00 + ], + [ + 2641.00, + 2447.00 + ], + [ + 2650.00, + 2456.00 + ], + [ + 2650.00, + 2456.00 + ], + [ + 2650.00, + 2456.00 + ], + [ + 2667.00, + 2464.00 + ], + [ + 2667.00, + 2464.00 + ], + [ + 2667.00, + 2464.00 + ], + [ + 2673.00, + 2475.00 + ], + [ + 2673.00, + 2475.00 + ], + [ + 2673.00, + 2475.00 + ], + [ + 2689.00, + 2485.00 + ], + [ + 2689.00, + 2485.00 + ], + [ + 2689.00, + 2485.00 + ], + [ + 2708.00, + 2496.00 + ], + [ + 2708.00, + 2496.00 + ], + [ + 2708.00, + 2496.00 + ], + [ + 2723.00, + 2502.00 + ], + [ + 2725.00, + 2503.00 + ], + [ + 2727.00, + 2504.00 + ], + [ + 2741.00, + 2510.00 + ], + [ + 2743.00, + 2511.00 + ], + [ + 2745.00, + 2512.00 + ], + [ + 2760.00, + 2521.00 + ], + [ + 2761.00, + 2521.00 + ], + [ + 2762.00, + 2521.00 + ], + [ + 2773.00, + 2525.00 + ], + [ + 2774.00, + 2526.00 + ], + [ + 2775.00, + 2527.00 + ], + [ + 2791.00, + 2532.00 + ], + [ + 2791.00, + 2532.00 + ], + [ + 2791.00, + 2532.00 + ], + [ + 2805.00, + 2538.00 + ], + [ + 2805.00, + 2538.00 + ], + [ + 2805.00, + 2538.00 + ], + [ + 2819.00, + 2543.00 + ], + [ + 2819.00, + 2543.00 + ], + [ + 2819.00, + 2543.00 + ], + [ + 2839.00, + 2549.00 + ], + [ + 2839.00, + 2550.00 + ], + [ + 2839.00, + 2551.00 + ], + [ + 2868.00, + 2560.00 + ], + [ + 2868.00, + 2560.00 + ], + [ + 2868.00, + 2560.00 + ], + [ + 2882.00, + 2563.00 + ], + [ + 2883.00, + 2564.00 + ], + [ + 2884.00, + 2565.00 + ], + [ + 2901.00, + 2570.00 + ], + [ + 2901.00, + 2570.00 + ], + [ + 2901.00, + 2570.00 + ], + [ + 2920.00, + 2574.00 + ], + [ + 2920.00, + 2574.00 + ], + [ + 2920.00, + 2574.00 + ], + [ + 2943.00, + 2582.00 + ], + [ + 2943.00, + 2582.00 + ], + [ + 2943.00, + 2582.00 + ], + [ + 2961.00, + 2586.00 + ], + [ + 2961.00, + 2586.00 + ], + [ + 2961.00, + 2586.00 + ], + [ + 2992.00, + 2593.00 + ], + [ + 2993.00, + 2593.00 + ], + [ + 2994.00, + 2593.00 + ], + [ + 3013.00, + 2596.00 + ], + [ + 3013.00, + 2596.00 + ], + [ + 3013.00, + 2596.00 + ], + [ + 3029.00, + 2601.00 + ], + [ + 3029.00, + 2601.00 + ], + [ + 3029.00, + 2601.00 + ], + [ + 3043.00, + 2605.00 + ], + [ + 3043.00, + 2605.00 + ], + [ + 3043.00, + 2605.00 + ], + [ + 3065.00, + 2611.00 + ], + [ + 3065.00, + 2611.00 + ], + [ + 3065.00, + 2611.00 + ], + [ + 3087.00, + 2613.00 + ], + [ + 3088.00, + 2613.00 + ], + [ + 3089.00, + 2613.00 + ], + [ + 3103.00, + 2612.00 + ], + [ + 3105.00, + 2613.00 + ], + [ + 3107.00, + 2614.00 + ], + [ + 3127.00, + 2616.00 + ], + [ + 3127.00, + 2616.00 + ], + [ + 3127.00, + 2616.00 + ], + [ + 3151.00, + 2621.00 + ], + [ + 3151.00, + 2621.00 + ], + [ + 3151.00, + 2621.00 + ], + [ + 3174.00, + 2628.00 + ], + [ + 3174.00, + 2628.00 + ], + [ + 3174.00, + 2628.00 + ], + [ + 3199.00, + 2634.00 + ], + [ + 3200.00, + 2634.00 + ], + [ + 3201.00, + 2634.00 + ], + [ + 3219.00, + 2636.00 + ], + [ + 3220.00, + 2637.00 + ], + [ + 3221.00, + 2638.00 + ], + [ + 3231.00, + 2642.00 + ], + [ + 3232.00, + 2642.00 + ], + [ + 3233.00, + 2642.00 + ], + [ + 3244.00, + 2650.00 + ], + [ + 3245.00, + 2652.00 + ], + [ + 3246.00, + 2654.00 + ], + [ + 3253.00, + 2659.00 + ], + [ + 3254.00, + 2663.00 + ], + [ + 3255.00, + 2667.00 + ], + [ + 3256.00, + 2677.00 + ], + [ + 3258.00, + 2679.00 + ], + [ + 3260.00, + 2681.00 + ], + [ + 3263.00, + 2687.00 + ], + [ + 3265.00, + 2690.00 + ], + [ + 3267.00, + 2693.00 + ], + [ + 3277.00, + 2702.00 + ], + [ + 3277.00, + 2702.00 + ], + [ + 3277.00, + 2702.00 + ], + [ + 3283.00, + 2706.00 + ], + [ + 3283.00, + 2706.00 + ], + [ + 3283.00, + 2706.00 + ], + [ + 3302.00, + 2705.00 + ], + [ + 3302.00, + 2705.00 + ], + [ + 3302.00, + 2705.00 + ], + [ + 3321.00, + 2701.00 + ], + [ + 3321.00, + 2701.00 + ], + [ + 3321.00, + 2701.00 + ], + [ + 3345.00, + 2700.00 + ], + [ + 3346.00, + 2699.00 + ], + [ + 3347.00, + 2698.00 + ], + [ + 3368.00, + 2697.00 + ], + [ + 3369.00, + 2697.00 + ], + [ + 3370.00, + 2697.00 + ], + [ + 3375.00, + 2676.00 + ], + [ + 3376.00, + 2676.00 + ], + [ + 3377.00, + 2676.00 + ], + [ + 3368.00, + 2651.00 + ], + [ + 3368.00, + 2651.00 + ], + [ + 3368.00, + 2651.00 + ], + [ + 3364.00, + 2632.00 + ], + [ + 3364.00, + 2631.00 + ], + [ + 3364.00, + 2630.00 + ], + [ + 3357.00, + 2608.00 + ], + [ + 3357.00, + 2608.00 + ], + [ + 3357.00, + 2608.00 + ], + [ + 3356.00, + 2589.00 + ], + [ + 3356.00, + 2589.00 + ], + [ + 3356.00, + 2589.00 + ], + [ + 3352.00, + 2567.00 + ], + [ + 3352.00, + 2567.00 + ], + [ + 3352.00, + 2567.00 + ], + [ + 3345.00, + 2541.00 + ], + [ + 3345.00, + 2541.00 + ], + [ + 3345.00, + 2541.00 + ], + [ + 3341.00, + 2524.00 + ], + [ + 3341.00, + 2524.00 + ], + [ + 3341.00, + 2524.00 + ], + [ + 3336.00, + 2515.00 + ], + [ + 3336.00, + 2515.00 + ], + [ + 3336.00, + 2515.00 + ], + [ + 3333.00, + 2501.00 + ], + [ + 3333.00, + 2500.00 + ], + [ + 3333.00, + 2499.00 + ], + [ + 3330.00, + 2480.00 + ], + [ + 3330.00, + 2480.00 + ], + [ + 3330.00, + 2480.00 + ], + [ + 3329.00, + 2465.00 + ], + [ + 3329.00, + 2465.00 + ], + [ + 3329.00, + 2465.00 + ], + [ + 3327.00, + 2446.00 + ], + [ + 3327.00, + 2446.00 + ], + [ + 3327.00, + 2446.00 + ], + [ + 3331.00, + 2424.00 + ], + [ + 3331.00, + 2424.00 + ], + [ + 3331.00, + 2424.00 + ], + [ + 3337.00, + 2408.00 + ], + [ + 3337.00, + 2408.00 + ], + [ + 3337.00, + 2408.00 + ], + [ + 3351.00, + 2393.00 + ], + [ + 3351.00, + 2393.00 + ], + [ + 3351.00, + 2393.00 + ], + [ + 3370.00, + 2382.00 + ], + [ + 3370.00, + 2382.00 + ] + ] + }, + { + "name": "Gimli & Legolas", + "id": "legolas_gimli", + "color": "purple", + "distance": "1800 miles / 2900 km", + "startDate": "December 25, T.A. 3018", + "endDate": "March 25, T.A. 3019", + "path": [ + [ + 3374.00, + 2375.00 + ], + [ + 3374.00, + 2375.00 + ], + [ + 3361.00, + 2384.00 + ], + [ + 3361.00, + 2384.00 + ], + [ + 3361.00, + 2384.00 + ], + [ + 3343.00, + 2405.00 + ], + [ + 3343.00, + 2405.00 + ], + [ + 3343.00, + 2405.00 + ], + [ + 3332.00, + 2424.00 + ], + [ + 3332.00, + 2424.00 + ], + [ + 3332.00, + 2424.00 + ], + [ + 3328.00, + 2449.00 + ], + [ + 3328.00, + 2449.00 + ], + [ + 3328.00, + 2449.00 + ], + [ + 3330.00, + 2471.00 + ], + [ + 3330.00, + 2471.00 + ], + [ + 3330.00, + 2471.00 + ], + [ + 3330.00, + 2500.00 + ], + [ + 3330.00, + 2500.00 + ], + [ + 3330.00, + 2500.00 + ], + [ + 3341.00, + 2523.00 + ], + [ + 3341.00, + 2523.00 + ], + [ + 3341.00, + 2523.00 + ], + [ + 3348.00, + 2547.00 + ], + [ + 3348.00, + 2547.00 + ], + [ + 3348.00, + 2547.00 + ], + [ + 3354.00, + 2567.00 + ], + [ + 3354.00, + 2567.00 + ], + [ + 3354.00, + 2567.00 + ], + [ + 3360.00, + 2592.00 + ], + [ + 3360.00, + 2592.00 + ], + [ + 3360.00, + 2592.00 + ], + [ + 3361.00, + 2610.00 + ], + [ + 3361.00, + 2614.00 + ], + [ + 3361.00, + 2618.00 + ], + [ + 3361.00, + 2634.00 + ], + [ + 3361.00, + 2636.00 + ], + [ + 3361.00, + 2638.00 + ], + [ + 3367.00, + 2658.00 + ], + [ + 3367.00, + 2658.00 + ], + [ + 3367.00, + 2658.00 + ], + [ + 3375.00, + 2687.00 + ], + [ + 3375.00, + 2688.00 + ], + [ + 3375.00, + 2689.00 + ], + [ + 3365.00, + 2699.00 + ], + [ + 3365.00, + 2699.00 + ], + [ + 3365.00, + 2699.00 + ], + [ + 3342.00, + 2700.00 + ], + [ + 3342.00, + 2700.00 + ], + [ + 3342.00, + 2700.00 + ], + [ + 3316.00, + 2703.00 + ], + [ + 3316.00, + 2703.00 + ], + [ + 3316.00, + 2703.00 + ], + [ + 3297.00, + 2705.00 + ], + [ + 3297.00, + 2705.00 + ], + [ + 3297.00, + 2705.00 + ], + [ + 3284.00, + 2712.00 + ], + [ + 3284.00, + 2712.00 + ], + [ + 3284.00, + 2712.00 + ], + [ + 3290.00, + 2716.00 + ], + [ + 3297.00, + 2720.00 + ], + [ + 3304.00, + 2724.00 + ], + [ + 3304.00, + 2725.00 + ], + [ + 3308.00, + 2728.00 + ], + [ + 3312.00, + 2731.00 + ], + [ + 3307.00, + 2737.00 + ], + [ + 3306.00, + 2740.00 + ], + [ + 3305.00, + 2743.00 + ], + [ + 3293.00, + 2749.00 + ], + [ + 3289.00, + 2752.00 + ], + [ + 3285.00, + 2755.00 + ], + [ + 3277.00, + 2763.00 + ], + [ + 3274.00, + 2767.00 + ], + [ + 3271.00, + 2771.00 + ], + [ + 3272.00, + 2785.00 + ], + [ + 3272.00, + 2786.00 + ], + [ + 3272.00, + 2787.00 + ], + [ + 3275.00, + 2802.00 + ], + [ + 3278.00, + 2812.00 + ], + [ + 3281.00, + 2822.00 + ], + [ + 3285.00, + 2827.00 + ], + [ + 3286.00, + 2831.00 + ], + [ + 3287.00, + 2835.00 + ], + [ + 3291.00, + 2855.00 + ], + [ + 3291.00, + 2864.00 + ], + [ + 3291.00, + 2873.00 + ], + [ + 3289.00, + 2881.00 + ], + [ + 3288.00, + 2887.00 + ], + [ + 3287.00, + 2893.00 + ], + [ + 3284.00, + 2907.00 + ], + [ + 3282.00, + 2917.00 + ], + [ + 3280.00, + 2927.00 + ], + [ + 3273.00, + 2936.00 + ], + [ + 3271.00, + 2941.00 + ], + [ + 3269.00, + 2946.00 + ], + [ + 3261.00, + 2964.00 + ], + [ + 3259.00, + 2968.00 + ], + [ + 3257.00, + 2972.00 + ], + [ + 3247.00, + 2986.00 + ], + [ + 3239.00, + 2995.00 + ], + [ + 3231.00, + 3004.00 + ], + [ + 3227.00, + 3008.00 + ], + [ + 3224.00, + 3011.00 + ], + [ + 3221.00, + 3014.00 + ], + [ + 3206.00, + 3029.00 + ], + [ + 3206.00, + 3029.00 + ], + [ + 3206.00, + 3029.00 + ], + [ + 3179.00, + 3035.00 + ], + [ + 3179.00, + 3035.00 + ], + [ + 3179.00, + 3035.00 + ], + [ + 3156.00, + 3032.00 + ], + [ + 3154.00, + 3032.00 + ], + [ + 3152.00, + 3032.00 + ], + [ + 3148.00, + 3032.00 + ], + [ + 3148.00, + 3032.00 + ], + [ + 3148.00, + 3032.00 + ], + [ + 3134.00, + 3031.00 + ], + [ + 3130.00, + 3031.00 + ], + [ + 3126.00, + 3031.00 + ], + [ + 3119.00, + 3035.00 + ], + [ + 3116.00, + 3036.00 + ], + [ + 3113.00, + 3037.00 + ], + [ + 3099.00, + 3039.00 + ], + [ + 3093.00, + 3040.00 + ], + [ + 3087.00, + 3041.00 + ], + [ + 3084.00, + 3041.00 + ], + [ + 3082.00, + 3041.00 + ], + [ + 3080.00, + 3041.00 + ], + [ + 3072.00, + 3045.00 + ], + [ + 3066.00, + 3047.00 + ], + [ + 3060.00, + 3049.00 + ], + [ + 3051.00, + 3051.00 + ], + [ + 3050.00, + 3051.00 + ], + [ + 3049.00, + 3051.00 + ], + [ + 3020.00, + 3064.00 + ], + [ + 3019.00, + 3064.00 + ], + [ + 3018.00, + 3064.00 + ], + [ + 2989.00, + 3066.00 + ], + [ + 2985.00, + 3066.00 + ], + [ + 2981.00, + 3066.00 + ], + [ + 2957.00, + 3064.00 + ], + [ + 2957.00, + 3064.00 + ], + [ + 2957.00, + 3064.00 + ], + [ + 2937.00, + 3058.00 + ], + [ + 2934.00, + 3057.00 + ], + [ + 2931.00, + 3056.00 + ], + [ + 2911.00, + 3043.00 + ], + [ + 2911.00, + 3043.00 + ], + [ + 2911.00, + 3043.00 + ], + [ + 2896.00, + 3027.00 + ], + [ + 2896.00, + 3026.00 + ], + [ + 2896.00, + 3025.00 + ], + [ + 2887.00, + 3012.00 + ], + [ + 2887.00, + 3012.00 + ], + [ + 2887.00, + 3012.00 + ], + [ + 2884.00, + 2992.00 + ], + [ + 2884.00, + 2992.00 + ], + [ + 2884.00, + 2992.00 + ], + [ + 2880.00, + 2979.00 + ], + [ + 2880.00, + 2979.00 + ], + [ + 2880.00, + 2979.00 + ], + [ + 2879.00, + 2964.00 + ], + [ + 2879.00, + 2964.00 + ], + [ + 2879.00, + 2964.00 + ], + [ + 2879.00, + 2950.00 + ], + [ + 2879.00, + 2947.00 + ], + [ + 2879.00, + 2944.00 + ], + [ + 2875.00, + 2934.00 + ], + [ + 2875.00, + 2934.00 + ], + [ + 2875.00, + 2934.00 + ], + [ + 2873.00, + 2917.00 + ], + [ + 2873.00, + 2912.00 + ], + [ + 2873.00, + 2907.00 + ], + [ + 2872.00, + 2904.00 + ], + [ + 2872.00, + 2903.00 + ], + [ + 2872.00, + 2902.00 + ], + [ + 2871.00, + 2887.00 + ], + [ + 2871.00, + 2887.00 + ], + [ + 2871.00, + 2887.00 + ], + [ + 2867.00, + 2862.00 + ], + [ + 2867.00, + 2860.00 + ], + [ + 2867.00, + 2858.00 + ], + [ + 2864.00, + 2840.00 + ], + [ + 2862.00, + 2835.00 + ], + [ + 2860.00, + 2830.00 + ], + [ + 2858.00, + 2828.00 + ], + [ + 2857.00, + 2828.00 + ], + [ + 2856.00, + 2828.00 + ], + [ + 2839.00, + 2809.00 + ], + [ + 2838.00, + 2809.00 + ], + [ + 2837.00, + 2809.00 + ], + [ + 2822.00, + 2802.00 + ], + [ + 2818.00, + 2798.00 + ], + [ + 2814.00, + 2794.00 + ], + [ + 2810.00, + 2790.00 + ], + [ + 2810.00, + 2790.00 + ], + [ + 2810.00, + 2790.00 + ], + [ + 2795.00, + 2784.00 + ], + [ + 2792.00, + 2783.00 + ], + [ + 2789.00, + 2782.00 + ], + [ + 2784.00, + 2776.00 + ], + [ + 2784.00, + 2776.00 + ], + [ + 2784.00, + 2776.00 + ], + [ + 2773.00, + 2771.00 + ], + [ + 2765.00, + 2767.00 + ], + [ + 2757.00, + 2763.00 + ], + [ + 2759.00, + 2764.00 + ], + [ + 2758.00, + 2763.00 + ], + [ + 2757.00, + 2762.00 + ], + [ + 2739.00, + 2757.00 + ], + [ + 2738.00, + 2757.00 + ], + [ + 2737.00, + 2757.00 + ], + [ + 2726.00, + 2755.00 + ], + [ + 2724.00, + 2754.00 + ], + [ + 2722.00, + 2753.00 + ], + [ + 2704.00, + 2751.00 + ], + [ + 2702.00, + 2751.00 + ], + [ + 2700.00, + 2751.00 + ], + [ + 2693.00, + 2744.00 + ], + [ + 2690.00, + 2742.00 + ], + [ + 2687.00, + 2740.00 + ], + [ + 2688.00, + 2731.00 + ], + [ + 2687.00, + 2729.00 + ], + [ + 2686.00, + 2727.00 + ], + [ + 2687.00, + 2713.00 + ], + [ + 2687.00, + 2713.00 + ], + [ + 2687.00, + 2713.00 + ], + [ + 2685.00, + 2693.00 + ], + [ + 2685.00, + 2694.00 + ], + [ + 2685.00, + 2695.00 + ], + [ + 2679.00, + 2680.00 + ], + [ + 2678.00, + 2676.00 + ], + [ + 2677.00, + 2672.00 + ], + [ + 2673.00, + 2663.00 + ], + [ + 2673.00, + 2662.00 + ], + [ + 2673.00, + 2661.00 + ], + [ + 2671.00, + 2648.00 + ], + [ + 2671.00, + 2646.00 + ], + [ + 2671.00, + 2644.00 + ], + [ + 2663.00, + 2630.00 + ], + [ + 2663.00, + 2630.00 + ], + [ + 2663.00, + 2630.00 + ], + [ + 2646.00, + 2616.00 + ], + [ + 2634.00, + 2606.00 + ], + [ + 2629.00, + 2600.00 + ], + [ + 2617.00, + 2600.00 + ], + [ + 2617.00, + 2600.00 + ], + [ + 2617.00, + 2600.00 + ], + [ + 2578.00, + 2590.00 + ], + [ + 2577.00, + 2590.00 + ], + [ + 2576.00, + 2590.00 + ], + [ + 2569.00, + 2580.00 + ], + [ + 2569.00, + 2579.00 + ], + [ + 2569.00, + 2578.00 + ], + [ + 2556.00, + 2570.00 + ], + [ + 2553.00, + 2566.00 + ], + [ + 2550.00, + 2562.00 + ], + [ + 2548.00, + 2559.00 + ], + [ + 2547.00, + 2557.00 + ], + [ + 2546.00, + 2555.00 + ], + [ + 2537.00, + 2542.00 + ], + [ + 2536.00, + 2539.00 + ], + [ + 2535.00, + 2536.00 + ], + [ + 2527.00, + 2519.00 + ], + [ + 2526.00, + 2515.00 + ], + [ + 2525.00, + 2511.00 + ], + [ + 2520.00, + 2506.00 + ], + [ + 2520.00, + 2505.00 + ], + [ + 2520.00, + 2504.00 + ], + [ + 2512.00, + 2492.00 + ], + [ + 2512.00, + 2492.00 + ], + [ + 2512.00, + 2492.00 + ], + [ + 2504.00, + 2477.00 + ], + [ + 2504.00, + 2477.00 + ], + [ + 2504.00, + 2477.00 + ], + [ + 2498.00, + 2464.00 + ], + [ + 2498.00, + 2459.00 + ], + [ + 2498.00, + 2454.00 + ], + [ + 2496.00, + 2440.00 + ], + [ + 2496.00, + 2439.00 + ], + [ + 2496.00, + 2438.00 + ], + [ + 2493.00, + 2418.00 + ], + [ + 2493.00, + 2418.00 + ], + [ + 2493.00, + 2418.00 + ], + [ + 2494.00, + 2397.00 + ], + [ + 2492.00, + 2396.00 + ], + [ + 2490.00, + 2395.00 + ], + [ + 2481.00, + 2393.00 + ], + [ + 2477.00, + 2391.00 + ], + [ + 2473.00, + 2389.00 + ], + [ + 2465.00, + 2386.00 + ], + [ + 2464.00, + 2384.00 + ], + [ + 2463.00, + 2382.00 + ], + [ + 2453.00, + 2376.00 + ], + [ + 2450.00, + 2374.00 + ], + [ + 2447.00, + 2372.00 + ], + [ + 2437.00, + 2361.00 + ], + [ + 2435.00, + 2359.00 + ], + [ + 2433.00, + 2357.00 + ], + [ + 2423.00, + 2343.00 + ], + [ + 2417.00, + 2335.00 + ], + [ + 2415.00, + 2332.00 + ], + [ + 2415.00, + 2328.00 + ], + [ + 2414.00, + 2323.00 + ], + [ + 2413.00, + 2318.00 + ], + [ + 2411.00, + 2314.00 + ], + [ + 2410.00, + 2309.00 + ], + [ + 2409.00, + 2304.00 + ], + [ + 2407.00, + 2293.00 + ], + [ + 2407.00, + 2291.00 + ], + [ + 2407.00, + 2289.00 + ], + [ + 2400.00, + 2281.00 + ], + [ + 2399.00, + 2280.00 + ], + [ + 2398.00, + 2279.00 + ], + [ + 2390.00, + 2277.00 + ], + [ + 2390.00, + 2276.00 + ], + [ + 2390.00, + 2275.00 + ], + [ + 2379.00, + 2274.00 + ], + [ + 2377.00, + 2273.00 + ], + [ + 2375.00, + 2272.00 + ], + [ + 2361.00, + 2265.00 + ], + [ + 2360.00, + 2265.00 + ], + [ + 2359.00, + 2265.00 + ], + [ + 2347.00, + 2261.00 + ], + [ + 2347.00, + 2261.00 + ], + [ + 2347.00, + 2261.00 + ], + [ + 2329.00, + 2250.00 + ], + [ + 2329.00, + 2250.00 + ], + [ + 2329.00, + 2250.00 + ], + [ + 2321.00, + 2242.00 + ], + [ + 2320.00, + 2240.00 + ], + [ + 2319.00, + 2238.00 + ], + [ + 2314.00, + 2224.00 + ], + [ + 2314.00, + 2224.00 + ], + [ + 2314.00, + 2224.00 + ], + [ + 2312.00, + 2201.00 + ], + [ + 2312.00, + 2196.00 + ], + [ + 2312.00, + 2191.00 + ], + [ + 2316.00, + 2184.00 + ], + [ + 2316.00, + 2184.00 + ], + [ + 2316.00, + 2184.00 + ], + [ + 2321.00, + 2180.00 + ], + [ + 2323.00, + 2173.00 + ], + [ + 2323.00, + 2172.00 + ], + [ + 2327.00, + 2164.00 + ], + [ + 2327.00, + 2164.00 + ], + [ + 2327.00, + 2164.00 + ], + [ + 2332.00, + 2150.00 + ], + [ + 2332.00, + 2148.00 + ], + [ + 2332.00, + 2146.00 + ], + [ + 2332.00, + 2137.00 + ], + [ + 2332.00, + 2137.00 + ], + [ + 2332.00, + 2137.00 + ], + [ + 2336.00, + 2123.00 + ], + [ + 2336.00, + 2123.00 + ], + [ + 2336.00, + 2123.00 + ], + [ + 2341.00, + 2135.00 + ], + [ + 2341.00, + 2135.00 + ], + [ + 2341.00, + 2135.00 + ], + [ + 2342.00, + 2142.00 + ], + [ + 2342.00, + 2142.00 + ], + [ + 2342.00, + 2142.00 + ], + [ + 2343.00, + 2151.00 + ], + [ + 2344.00, + 2152.00 + ], + [ + 2345.00, + 2153.00 + ], + [ + 2346.00, + 2161.00 + ], + [ + 2346.00, + 2161.00 + ], + [ + 2346.00, + 2161.00 + ], + [ + 2346.00, + 2179.00 + ], + [ + 2346.00, + 2180.00 + ], + [ + 2346.00, + 2181.00 + ], + [ + 2346.00, + 2189.00 + ], + [ + 2346.00, + 2193.00 + ], + [ + 2346.00, + 2197.00 + ], + [ + 2345.00, + 2201.00 + ], + [ + 2345.00, + 2202.00 + ], + [ + 2345.00, + 2203.00 + ], + [ + 2340.00, + 2213.00 + ], + [ + 2340.00, + 2215.00 + ], + [ + 2340.00, + 2217.00 + ], + [ + 2342.00, + 2225.00 + ], + [ + 2342.00, + 2227.00 + ], + [ + 2342.00, + 2229.00 + ], + [ + 2344.00, + 2240.00 + ], + [ + 2345.00, + 2243.00 + ], + [ + 2346.00, + 2246.00 + ], + [ + 2346.00, + 2249.00 + ], + [ + 2346.00, + 2249.00 + ], + [ + 2346.00, + 2249.00 + ], + [ + 2359.00, + 2254.00 + ], + [ + 2360.00, + 2254.00 + ], + [ + 2361.00, + 2254.00 + ], + [ + 2377.00, + 2255.00 + ], + [ + 2378.00, + 2255.00 + ], + [ + 2379.00, + 2255.00 + ], + [ + 2393.00, + 2260.00 + ], + [ + 2393.00, + 2260.00 + ], + [ + 2393.00, + 2260.00 + ], + [ + 2404.00, + 2264.00 + ], + [ + 2404.00, + 2264.00 + ], + [ + 2404.00, + 2264.00 + ], + [ + 2415.00, + 2270.00 + ], + [ + 2415.00, + 2270.00 + ], + [ + 2415.00, + 2270.00 + ], + [ + 2414.00, + 2280.00 + ], + [ + 2414.00, + 2281.00 + ], + [ + 2414.00, + 2282.00 + ], + [ + 2414.00, + 2291.00 + ], + [ + 2414.00, + 2291.00 + ], + [ + 2414.00, + 2291.00 + ], + [ + 2428.00, + 2328.00 + ], + [ + 2427.00, + 2328.00 + ], + [ + 2426.00, + 2328.00 + ], + [ + 2425.00, + 2321.00 + ], + [ + 2425.00, + 2320.00 + ], + [ + 2420.99, + 2317.60 + ], + [ + 2417.59, + 2305.71 + ], + [ + 2427.00, + 2308.00 + ], + [ + 2427.00, + 2307.73 + ], + [ + 2427.15, + 2306.86 + ], + [ + 2427.36, + 2305.73 + ], + [ + 2427.94, + 2302.69 + ], + [ + 2429.00, + 2297.73 + ], + [ + 2429.00, + 2297.00 + ], + [ + 2429.00, + 2296.00 + ], + [ + 2435.00, + 2285.00 + ], + [ + 2436.00, + 2284.00 + ], + [ + 2437.00, + 2283.00 + ], + [ + 2451.00, + 2280.00 + ], + [ + 2451.00, + 2280.00 + ], + [ + 2451.00, + 2280.00 + ], + [ + 2464.00, + 2287.00 + ], + [ + 2465.00, + 2287.00 + ], + [ + 2466.00, + 2287.00 + ], + [ + 2479.00, + 2294.00 + ], + [ + 2480.00, + 2294.00 + ], + [ + 2481.00, + 2294.00 + ], + [ + 2500.00, + 2305.00 + ], + [ + 2500.00, + 2305.00 + ], + [ + 2500.00, + 2305.00 + ], + [ + 2518.00, + 2317.00 + ], + [ + 2518.00, + 2317.00 + ], + [ + 2518.00, + 2317.00 + ], + [ + 2533.00, + 2327.00 + ], + [ + 2533.00, + 2327.00 + ], + [ + 2533.00, + 2327.00 + ], + [ + 2545.00, + 2330.00 + ], + [ + 2545.00, + 2330.00 + ], + [ + 2545.00, + 2330.00 + ], + [ + 2555.00, + 2342.00 + ], + [ + 2555.00, + 2342.00 + ], + [ + 2555.00, + 2342.00 + ], + [ + 2565.00, + 2351.00 + ], + [ + 2565.00, + 2352.00 + ], + [ + 2565.00, + 2353.00 + ], + [ + 2576.00, + 2361.00 + ], + [ + 2576.00, + 2361.00 + ], + [ + 2576.00, + 2361.00 + ], + [ + 2590.00, + 2390.00 + ], + [ + 2590.00, + 2390.00 + ], + [ + 2590.00, + 2390.00 + ], + [ + 2596.00, + 2369.00 + ], + [ + 2597.00, + 2357.00 + ], + [ + 2598.00, + 2345.00 + ], + [ + 2599.00, + 2344.00 + ], + [ + 2600.00, + 2334.00 + ], + [ + 2601.00, + 2324.00 + ], + [ + 2600.00, + 2321.00 + ], + [ + 2600.00, + 2311.00 + ], + [ + 2600.00, + 2301.00 + ], + [ + 2600.00, + 2304.00 + ], + [ + 2600.00, + 2298.00 + ], + [ + 2600.00, + 2292.00 + ], + [ + 2600.00, + 2283.00 + ], + [ + 2600.00, + 2272.00 + ], + [ + 2600.00, + 2261.00 + ], + [ + 2600.00, + 2266.00 + ], + [ + 2600.00, + 2256.00 + ], + [ + 2600.00, + 2246.00 + ], + [ + 2600.00, + 2249.00 + ], + [ + 2601.00, + 2234.00 + ], + [ + 2602.00, + 2219.00 + ], + [ + 2602.00, + 2226.00 + ], + [ + 2602.00, + 2197.00 + ], + [ + 2602.00, + 2182.00 + ], + [ + 2601.00, + 2190.00 + ], + [ + 2599.00, + 2178.00 + ], + [ + 2597.00, + 2166.00 + ], + [ + 2599.00, + 2170.00 + ], + [ + 2602.00, + 2162.00 + ], + [ + 2605.00, + 2154.00 + ], + [ + 2606.00, + 2154.00 + ], + [ + 2608.00, + 2147.00 + ], + [ + 2610.00, + 2140.00 + ], + [ + 2611.00, + 2137.00 + ], + [ + 2614.00, + 2132.00 + ], + [ + 2617.00, + 2127.00 + ], + [ + 2617.00, + 2126.00 + ], + [ + 2617.00, + 2123.00 + ], + [ + 2617.00, + 2120.00 + ], + [ + 2618.00, + 2115.00 + ], + [ + 2619.00, + 2111.00 + ], + [ + 2620.00, + 2107.00 + ], + [ + 2622.00, + 2105.00 + ], + [ + 2623.00, + 2103.00 + ], + [ + 2624.00, + 2101.00 + ], + [ + 2625.00, + 2091.00 + ], + [ + 2625.00, + 2089.00 + ], + [ + 2625.00, + 2087.00 + ], + [ + 2642.00, + 2087.00 + ], + [ + 2650.00, + 2088.00 + ], + [ + 2652.00, + 2088.00 + ], + [ + 2657.00, + 2090.00 + ], + [ + 2658.00, + 2091.00 + ], + [ + 2659.00, + 2092.00 + ], + [ + 2666.00, + 2092.00 + ], + [ + 2667.00, + 2093.00 + ], + [ + 2668.00, + 2094.00 + ], + [ + 2684.00, + 2097.00 + ], + [ + 2686.00, + 2098.00 + ], + [ + 2688.00, + 2099.00 + ], + [ + 2695.00, + 2103.00 + ], + [ + 2696.00, + 2104.00 + ], + [ + 2697.00, + 2105.00 + ], + [ + 2708.00, + 2112.00 + ], + [ + 2708.00, + 2112.00 + ], + [ + 2708.00, + 2112.00 + ], + [ + 2730.00, + 2128.00 + ], + [ + 2730.00, + 2128.00 + ], + [ + 2730.00, + 2128.00 + ], + [ + 2745.00, + 2140.00 + ], + [ + 2745.00, + 2140.00 + ], + [ + 2745.00, + 2140.00 + ], + [ + 2761.00, + 2150.00 + ], + [ + 2761.00, + 2150.00 + ], + [ + 2761.00, + 2150.00 + ], + [ + 2791.00, + 2168.00 + ], + [ + 2791.00, + 2168.00 + ], + [ + 2791.00, + 2168.00 + ], + [ + 2808.00, + 2194.00 + ], + [ + 2808.00, + 2194.00 + ], + [ + 2808.00, + 2194.00 + ], + [ + 2823.00, + 2214.00 + ], + [ + 2823.00, + 2214.00 + ], + [ + 2823.00, + 2214.00 + ], + [ + 2850.00, + 2233.00 + ], + [ + 2850.00, + 2233.00 + ], + [ + 2850.00, + 2233.00 + ], + [ + 2865.00, + 2250.00 + ], + [ + 2865.00, + 2250.00 + ], + [ + 2865.00, + 2250.00 + ], + [ + 2878.00, + 2269.00 + ], + [ + 2878.00, + 2269.00 + ], + [ + 2878.00, + 2269.00 + ], + [ + 2890.00, + 2286.00 + ], + [ + 2891.00, + 2287.00 + ], + [ + 2892.00, + 2288.00 + ], + [ + 2904.00, + 2291.00 + ], + [ + 2904.00, + 2291.00 + ], + [ + 2904.00, + 2291.00 + ], + [ + 2921.00, + 2293.00 + ], + [ + 2924.00, + 2293.00 + ], + [ + 2927.00, + 2293.00 + ], + [ + 2949.00, + 2300.00 + ], + [ + 2952.00, + 2301.00 + ], + [ + 2955.00, + 2302.00 + ], + [ + 2970.00, + 2308.00 + ], + [ + 2970.00, + 2308.00 + ], + [ + 2970.00, + 2308.00 + ], + [ + 2986.00, + 2325.00 + ], + [ + 2989.00, + 2328.00 + ], + [ + 2992.00, + 2331.00 + ], + [ + 3009.00, + 2340.00 + ], + [ + 3009.00, + 2340.00 + ], + [ + 3009.00, + 2340.00 + ], + [ + 3016.00, + 2355.00 + ], + [ + 3016.00, + 2355.00 + ], + [ + 3016.00, + 2355.00 + ], + [ + 3031.00, + 2368.00 + ], + [ + 3031.00, + 2368.00 + ], + [ + 3031.00, + 2368.00 + ], + [ + 3049.00, + 2383.00 + ], + [ + 3049.00, + 2383.00 + ], + [ + 3049.00, + 2383.00 + ], + [ + 3053.00, + 2367.00 + ], + [ + 3053.00, + 2366.00 + ], + [ + 3053.00, + 2365.00 + ], + [ + 3045.00, + 2348.00 + ], + [ + 3045.00, + 2345.00 + ], + [ + 3045.00, + 2342.00 + ], + [ + 3042.00, + 2320.00 + ], + [ + 3042.00, + 2320.00 + ], + [ + 3042.00, + 2320.00 + ], + [ + 3039.00, + 2302.00 + ], + [ + 3039.00, + 2302.00 + ], + [ + 3039.00, + 2302.00 + ], + [ + 3037.00, + 2288.00 + ], + [ + 3035.00, + 2285.00 + ], + [ + 3033.00, + 2282.00 + ], + [ + 3031.00, + 2270.00 + ], + [ + 3031.00, + 2268.00 + ], + [ + 3031.00, + 2266.00 + ], + [ + 3028.00, + 2252.00 + ], + [ + 3028.00, + 2249.00 + ], + [ + 3028.00, + 2246.00 + ], + [ + 3024.00, + 2231.00 + ], + [ + 3024.00, + 2228.00 + ], + [ + 3024.00, + 2225.00 + ], + [ + 3028.00, + 2215.00 + ], + [ + 3029.00, + 2211.00 + ], + [ + 3030.00, + 2207.00 + ], + [ + 3032.00, + 2193.00 + ], + [ + 3033.00, + 2190.00 + ], + [ + 3034.00, + 2187.00 + ], + [ + 3039.00, + 2177.00 + ], + [ + 3039.00, + 2176.00 + ], + [ + 3039.00, + 2175.00 + ], + [ + 3043.00, + 2163.00 + ], + [ + 3043.00, + 2163.00 + ], + [ + 3043.00, + 2163.00 + ], + [ + 3046.00, + 2150.00 + ], + [ + 3046.00, + 2149.00 + ], + [ + 3046.00, + 2148.00 + ], + [ + 3051.00, + 2131.00 + ], + [ + 3052.00, + 2130.00 + ], + [ + 3053.00, + 2129.00 + ], + [ + 3054.00, + 2114.00 + ], + [ + 3054.00, + 2113.00 + ], + [ + 3054.00, + 2112.00 + ], + [ + 3054.00, + 2099.00 + ], + [ + 3054.00, + 2099.00 + ], + [ + 3054.00, + 2099.00 + ], + [ + 3043.00, + 2089.00 + ], + [ + 3043.00, + 2089.00 + ], + [ + 3043.00, + 2089.00 + ], + [ + 3025.00, + 2080.00 + ], + [ + 3025.00, + 2080.00 + ], + [ + 3025.00, + 2080.00 + ], + [ + 3006.00, + 2071.00 + ], + [ + 3005.00, + 2070.00 + ], + [ + 3004.00, + 2069.00 + ], + [ + 3003.00, + 2055.00 + ], + [ + 3003.00, + 2051.00 + ], + [ + 3003.00, + 2047.00 + ], + [ + 3009.00, + 2039.00 + ], + [ + 3010.00, + 2038.00 + ], + [ + 3011.00, + 2037.00 + ], + [ + 3020.00, + 2021.00 + ], + [ + 3021.00, + 2020.00 + ], + [ + 3022.00, + 2019.00 + ], + [ + 3031.00, + 2002.00 + ], + [ + 3031.00, + 2000.00 + ], + [ + 3031.00, + 1998.00 + ], + [ + 3027.00, + 1985.00 + ], + [ + 3027.00, + 1984.00 + ], + [ + 3027.00, + 1983.00 + ], + [ + 3007.00, + 1975.00 + ], + [ + 3007.00, + 1975.00 + ], + [ + 3007.00, + 1975.00 + ], + [ + 2990.00, + 1982.00 + ], + [ + 2990.00, + 1982.00 + ], + [ + 2990.00, + 1982.00 + ], + [ + 2973.00, + 1984.00 + ], + [ + 2973.00, + 1984.00 + ], + [ + 2973.00, + 1984.00 + ], + [ + 2966.00, + 1984.00 + ], + [ + 2966.00, + 1984.00 + ], + [ + 2966.00, + 1984.00 + ], + [ + 2960.00, + 1983.00 + ], + [ + 2954.00, + 1979.00 + ], + [ + 2948.00, + 1975.00 + ], + [ + 2946.00, + 1970.00 + ], + [ + 2945.00, + 1968.00 + ], + [ + 2944.00, + 1966.00 + ], + [ + 2938.00, + 1956.00 + ], + [ + 2938.00, + 1956.00 + ], + [ + 2938.00, + 1956.00 + ], + [ + 2941.00, + 1935.00 + ], + [ + 2941.00, + 1933.00 + ], + [ + 2941.00, + 1931.00 + ], + [ + 2948.00, + 1923.00 + ], + [ + 2949.00, + 1921.00 + ], + [ + 2950.00, + 1919.00 + ], + [ + 2955.00, + 1910.00 + ], + [ + 2955.00, + 1909.00 + ], + [ + 2955.00, + 1908.00 + ], + [ + 2956.00, + 1901.00 + ], + [ + 2956.00, + 1900.00 + ], + [ + 2956.00, + 1899.00 + ], + [ + 2947.00, + 1893.00 + ], + [ + 2943.00, + 1889.00 + ], + [ + 2939.00, + 1885.00 + ], + [ + 2934.00, + 1887.00 + ], + [ + 2934.00, + 1887.00 + ], + [ + 2934.00, + 1887.00 + ], + [ + 2913.00, + 1880.00 + ], + [ + 2910.00, + 1879.00 + ], + [ + 2907.00, + 1878.00 + ], + [ + 2901.00, + 1874.00 + ], + [ + 2900.00, + 1873.00 + ], + [ + 2899.00, + 1872.00 + ], + [ + 2886.00, + 1861.00 + ], + [ + 2881.00, + 1857.00 + ], + [ + 2876.00, + 1853.00 + ], + [ + 2868.00, + 1848.00 + ], + [ + 2866.00, + 1846.00 + ], + [ + 2864.00, + 1844.00 + ], + [ + 2851.00, + 1836.00 + ], + [ + 2850.00, + 1836.00 + ], + [ + 2849.00, + 1836.00 + ], + [ + 2838.00, + 1828.00 + ], + [ + 2837.00, + 1827.00 + ], + [ + 2836.00, + 1826.00 + ], + [ + 2821.00, + 1820.00 + ], + [ + 2818.00, + 1819.00 + ], + [ + 2815.00, + 1818.00 + ], + [ + 2803.00, + 1811.00 + ], + [ + 2801.00, + 1810.00 + ], + [ + 2799.00, + 1809.00 + ], + [ + 2794.00, + 1802.00 + ], + [ + 2794.00, + 1802.00 + ], + [ + 2794.00, + 1802.00 + ], + [ + 2791.00, + 1787.00 + ], + [ + 2791.00, + 1785.00 + ], + [ + 2791.00, + 1783.00 + ], + [ + 2790.00, + 1772.00 + ], + [ + 2784.00, + 1766.00 + ], + [ + 2778.00, + 1760.00 + ], + [ + 2787.00, + 1746.00 + ], + [ + 2787.00, + 1745.00 + ], + [ + 2787.00, + 1744.00 + ], + [ + 2770.00, + 1731.00 + ], + [ + 2770.00, + 1731.00 + ], + [ + 2770.00, + 1731.00 + ], + [ + 2744.00, + 1728.00 + ], + [ + 2744.00, + 1727.00 + ], + [ + 2744.00, + 1726.00 + ], + [ + 2720.00, + 1722.00 + ], + [ + 2720.00, + 1722.00 + ], + [ + 2720.00, + 1722.00 + ], + [ + 2696.00, + 1712.00 + ], + [ + 2696.00, + 1712.00 + ], + [ + 2696.00, + 1712.00 + ], + [ + 2673.00, + 1699.00 + ], + [ + 2673.00, + 1699.00 + ], + [ + 2673.00, + 1699.00 + ], + [ + 2660.00, + 1693.00 + ], + [ + 2659.00, + 1693.00 + ], + [ + 2658.00, + 1693.00 + ], + [ + 2646.00, + 1685.00 + ], + [ + 2645.00, + 1684.00 + ], + [ + 2644.00, + 1683.00 + ], + [ + 2636.00, + 1679.00 + ], + [ + 2634.00, + 1677.00 + ], + [ + 2632.00, + 1675.00 + ], + [ + 2624.00, + 1667.00 + ], + [ + 2624.00, + 1666.00 + ], + [ + 2624.00, + 1665.00 + ], + [ + 2613.00, + 1653.00 + ], + [ + 2612.00, + 1652.00 + ], + [ + 2611.00, + 1651.00 + ], + [ + 2598.00, + 1647.00 + ], + [ + 2598.00, + 1646.00 + ], + [ + 2598.00, + 1645.00 + ], + [ + 2601.00, + 1627.00 + ], + [ + 2601.00, + 1627.00 + ], + [ + 2601.00, + 1627.00 + ], + [ + 2593.00, + 1610.00 + ], + [ + 2593.00, + 1610.00 + ], + [ + 2593.00, + 1610.00 + ], + [ + 2576.00, + 1603.00 + ], + [ + 2576.00, + 1603.00 + ], + [ + 2576.00, + 1603.00 + ], + [ + 2565.00, + 1593.00 + ], + [ + 2565.00, + 1593.00 + ], + [ + 2565.00, + 1593.00 + ], + [ + 2558.00, + 1573.00 + ], + [ + 2558.00, + 1573.00 + ], + [ + 2558.00, + 1573.00 + ], + [ + 2551.00, + 1556.00 + ], + [ + 2551.00, + 1556.00 + ], + [ + 2551.00, + 1556.00 + ], + [ + 2538.00, + 1545.00 + ], + [ + 2537.00, + 1544.00 + ], + [ + 2536.00, + 1543.00 + ], + [ + 2526.00, + 1537.00 + ], + [ + 2526.00, + 1537.00 + ], + [ + 2526.00, + 1537.00 + ], + [ + 2514.00, + 1534.00 + ], + [ + 2513.00, + 1534.00 + ], + [ + 2512.00, + 1534.00 + ], + [ + 2503.00, + 1535.00 + ], + [ + 2503.00, + 1535.00 + ], + [ + 2503.00, + 1535.00 + ], + [ + 2477.00, + 1533.00 + ], + [ + 2473.00, + 1533.00 + ], + [ + 2469.00, + 1533.00 + ], + [ + 2457.00, + 1537.00 + ], + [ + 2457.00, + 1537.00 + ], + [ + 2457.00, + 1537.00 + ], + [ + 2439.00, + 1538.00 + ], + [ + 2439.00, + 1538.00 + ], + [ + 2439.00, + 1538.00 + ], + [ + 2428.00, + 1540.00 + ], + [ + 2427.00, + 1540.00 + ], + [ + 2426.00, + 1540.00 + ], + [ + 2407.00, + 1537.00 + ], + [ + 2404.00, + 1535.00 + ], + [ + 2401.00, + 1533.00 + ], + [ + 2394.00, + 1529.00 + ], + [ + 2394.00, + 1526.00 + ], + [ + 2394.00, + 1523.00 + ], + [ + 2398.00, + 1509.00 + ], + [ + 2399.00, + 1507.00 + ], + [ + 2400.00, + 1505.00 + ], + [ + 2401.00, + 1496.00 + ], + [ + 2403.00, + 1488.00 + ], + [ + 2405.00, + 1480.00 + ], + [ + 2409.00, + 1474.00 + ], + [ + 2410.00, + 1471.00 + ], + [ + 2411.00, + 1468.00 + ], + [ + 2417.00, + 1457.00 + ], + [ + 2419.00, + 1453.00 + ], + [ + 2421.00, + 1449.00 + ], + [ + 2425.00, + 1439.00 + ], + [ + 2426.00, + 1438.00 + ], + [ + 2427.00, + 1437.00 + ], + [ + 2432.00, + 1423.00 + ], + [ + 2434.00, + 1419.00 + ], + [ + 2436.00, + 1415.00 + ], + [ + 2439.00, + 1408.00 + ], + [ + 2440.00, + 1406.00 + ], + [ + 2441.00, + 1404.00 + ], + [ + 2455.00, + 1402.00 + ], + [ + 2460.00, + 1401.00 + ], + [ + 2465.00, + 1400.00 + ], + [ + 2468.00, + 1400.00 + ], + [ + 2476.00, + 1402.00 + ], + [ + 2484.00, + 1404.00 + ], + [ + 2498.00, + 1404.00 + ], + [ + 2498.00, + 1404.00 + ], + [ + 2498.00, + 1404.00 + ], + [ + 2515.00, + 1404.00 + ], + [ + 2516.00, + 1403.00 + ], + [ + 2517.00, + 1402.00 + ], + [ + 2503.00, + 1398.00 + ], + [ + 2501.00, + 1398.00 + ], + [ + 2499.00, + 1398.00 + ], + [ + 2485.00, + 1393.00 + ], + [ + 2485.00, + 1393.00 + ], + [ + 2485.00, + 1393.00 + ], + [ + 2462.00, + 1387.00 + ], + [ + 2462.00, + 1387.00 + ], + [ + 2462.00, + 1387.00 + ], + [ + 2448.00, + 1376.00 + ], + [ + 2448.00, + 1376.00 + ], + [ + 2448.00, + 1376.00 + ], + [ + 2457.00, + 1367.00 + ], + [ + 2463.00, + 1355.00 + ], + [ + 2466.00, + 1348.00 + ], + [ + 2467.00, + 1348.00 + ], + [ + 2470.00, + 1340.00 + ], + [ + 2473.00, + 1332.00 + ], + [ + 2471.00, + 1331.00 + ], + [ + 2491.00, + 1287.00 + ], + [ + 2493.00, + 1281.00 + ], + [ + 2490.00, + 1275.00 + ], + [ + 2492.00, + 1273.00 + ], + [ + 2494.00, + 1271.00 + ], + [ + 2505.00, + 1241.00 + ], + [ + 2507.00, + 1237.00 + ], + [ + 2509.00, + 1233.00 + ], + [ + 2509.00, + 1231.00 + ], + [ + 2509.00, + 1231.00 + ], + [ + 2509.00, + 1231.00 + ], + [ + 2517.00, + 1210.00 + ], + [ + 2516.00, + 1206.00 + ], + [ + 2515.00, + 1202.00 + ], + [ + 2510.00, + 1189.00 + ], + [ + 2510.00, + 1189.00 + ], + [ + 2510.00, + 1189.00 + ], + [ + 2505.00, + 1178.00 + ], + [ + 2505.00, + 1178.00 + ], + [ + 2505.00, + 1178.00 + ], + [ + 2500.00, + 1163.00 + ], + [ + 2501.00, + 1162.00 + ], + [ + 2506, + 1147 + ], + [ + 2514, + 1132 + ] + ] + }, + { + "name": "Boromir", + "id": "boromir", + "color": "green", + "distance": "650 miles / 1050 km", + "startDate": "December 25, T.A. 3018", + "endDate": "February 25, T.A. 3019", + "path": [ + [ + 3045.00, + 2345.00 + ], + [ + 3045.00, + 2348.00 + ], + [ + 3045.00, + 2345.00 + ], + [ + 3045.00, + 2342.00 + ], + [ + 3042.00, + 2320.00 + ], + [ + 3042.00, + 2320.00 + ], + [ + 3042.00, + 2320.00 + ], + [ + 3039.00, + 2302.00 + ], + [ + 3039.00, + 2302.00 + ], + [ + 3039.00, + 2302.00 + ], + [ + 3037.00, + 2288.00 + ], + [ + 3035.00, + 2285.00 + ], + [ + 3033.00, + 2282.00 + ], + [ + 3031.00, + 2270.00 + ], + [ + 3031.00, + 2268.00 + ], + [ + 3031.00, + 2266.00 + ], + [ + 3028.00, + 2252.00 + ], + [ + 3028.00, + 2249.00 + ], + [ + 3028.00, + 2246.00 + ], + [ + 3024.00, + 2231.00 + ], + [ + 3024.00, + 2228.00 + ], + [ + 3024.00, + 2225.00 + ], + [ + 3028.00, + 2215.00 + ], + [ + 3029.00, + 2211.00 + ], + [ + 3030.00, + 2207.00 + ], + [ + 3032.00, + 2193.00 + ], + [ + 3033.00, + 2190.00 + ], + [ + 3034.00, + 2187.00 + ], + [ + 3039.00, + 2177.00 + ], + [ + 3039.00, + 2176.00 + ], + [ + 3039.00, + 2175.00 + ], + [ + 3043.00, + 2163.00 + ], + [ + 3043.00, + 2163.00 + ], + [ + 3043.00, + 2163.00 + ], + [ + 3046.00, + 2150.00 + ], + [ + 3046.00, + 2149.00 + ], + [ + 3046.00, + 2148.00 + ], + [ + 3051.00, + 2131.00 + ], + [ + 3052.00, + 2130.00 + ], + [ + 3053.00, + 2129.00 + ], + [ + 3054.00, + 2114.00 + ], + [ + 3054.00, + 2113.00 + ], + [ + 3054.00, + 2112.00 + ], + [ + 3054.00, + 2099.00 + ], + [ + 3054.00, + 2099.00 + ], + [ + 3054.00, + 2099.00 + ], + [ + 3043.00, + 2089.00 + ], + [ + 3043.00, + 2089.00 + ], + [ + 3043.00, + 2089.00 + ], + [ + 3025.00, + 2080.00 + ], + [ + 3025.00, + 2080.00 + ], + [ + 3025.00, + 2080.00 + ], + [ + 3006.00, + 2071.00 + ], + [ + 3005.00, + 2070.00 + ], + [ + 3004.00, + 2069.00 + ], + [ + 3003.00, + 2055.00 + ], + [ + 3003.00, + 2051.00 + ], + [ + 3003.00, + 2047.00 + ], + [ + 3009.00, + 2039.00 + ], + [ + 3010.00, + 2038.00 + ], + [ + 3011.00, + 2037.00 + ], + [ + 3020.00, + 2021.00 + ], + [ + 3021.00, + 2020.00 + ], + [ + 3022.00, + 2019.00 + ], + [ + 3031.00, + 2002.00 + ], + [ + 3031.00, + 2000.00 + ], + [ + 3031.00, + 1998.00 + ], + [ + 3027.00, + 1985.00 + ], + [ + 3027.00, + 1984.00 + ], + [ + 3027.00, + 1983.00 + ], + [ + 3007.00, + 1975.00 + ], + [ + 3007.00, + 1975.00 + ], + [ + 3007.00, + 1975.00 + ], + [ + 2990.00, + 1982.00 + ], + [ + 2990.00, + 1982.00 + ], + [ + 2990.00, + 1982.00 + ], + [ + 2973.00, + 1984.00 + ], + [ + 2973.00, + 1984.00 + ], + [ + 2973.00, + 1984.00 + ], + [ + 2966.00, + 1984.00 + ], + [ + 2966.00, + 1984.00 + ], + [ + 2966.00, + 1984.00 + ], + [ + 2960.00, + 1983.00 + ], + [ + 2954.00, + 1979.00 + ], + [ + 2948.00, + 1975.00 + ], + [ + 2946.00, + 1970.00 + ], + [ + 2945.00, + 1968.00 + ], + [ + 2944.00, + 1966.00 + ], + [ + 2938.00, + 1956.00 + ], + [ + 2938.00, + 1956.00 + ], + [ + 2938.00, + 1956.00 + ], + [ + 2941.00, + 1935.00 + ], + [ + 2941.00, + 1933.00 + ], + [ + 2941.00, + 1931.00 + ], + [ + 2948.00, + 1923.00 + ], + [ + 2949.00, + 1921.00 + ], + [ + 2950.00, + 1919.00 + ], + [ + 2955.00, + 1910.00 + ], + [ + 2955.00, + 1909.00 + ], + [ + 2955.00, + 1908.00 + ], + [ + 2956.00, + 1901.00 + ], + [ + 2956.00, + 1900.00 + ], + [ + 2956.00, + 1899.00 + ], + [ + 2947.00, + 1893.00 + ], + [ + 2943.00, + 1889.00 + ], + [ + 2939.00, + 1885.00 + ], + [ + 2934.00, + 1887.00 + ], + [ + 2934.00, + 1887.00 + ], + [ + 2934.00, + 1887.00 + ], + [ + 2913.00, + 1880.00 + ], + [ + 2910.00, + 1879.00 + ], + [ + 2907.00, + 1878.00 + ], + [ + 2901.00, + 1874.00 + ], + [ + 2900.00, + 1873.00 + ], + [ + 2899.00, + 1872.00 + ], + [ + 2886.00, + 1861.00 + ], + [ + 2881.00, + 1857.00 + ], + [ + 2876.00, + 1853.00 + ], + [ + 2868.00, + 1848.00 + ], + [ + 2866.00, + 1846.00 + ], + [ + 2864.00, + 1844.00 + ], + [ + 2851.00, + 1836.00 + ], + [ + 2850.00, + 1836.00 + ], + [ + 2849.00, + 1836.00 + ], + [ + 2838.00, + 1828.00 + ], + [ + 2837.00, + 1827.00 + ], + [ + 2836.00, + 1826.00 + ], + [ + 2821.00, + 1820.00 + ], + [ + 2818.00, + 1819.00 + ], + [ + 2815.00, + 1818.00 + ], + [ + 2803.00, + 1811.00 + ], + [ + 2801.00, + 1810.00 + ], + [ + 2799.00, + 1809.00 + ], + [ + 2794.00, + 1802.00 + ], + [ + 2794.00, + 1802.00 + ], + [ + 2794.00, + 1802.00 + ], + [ + 2791.00, + 1787.00 + ], + [ + 2791.00, + 1785.00 + ], + [ + 2791.00, + 1783.00 + ], + [ + 2790.00, + 1772.00 + ], + [ + 2784.00, + 1766.00 + ], + [ + 2778.00, + 1760.00 + ], + [ + 2787.00, + 1746.00 + ], + [ + 2787.00, + 1745.00 + ], + [ + 2787.00, + 1744.00 + ], + [ + 2770.00, + 1731.00 + ], + [ + 2770.00, + 1731.00 + ], + [ + 2770.00, + 1731.00 + ], + [ + 2744.00, + 1728.00 + ], + [ + 2744.00, + 1727.00 + ], + [ + 2744.00, + 1726.00 + ], + [ + 2720.00, + 1722.00 + ], + [ + 2720.00, + 1722.00 + ], + [ + 2720.00, + 1722.00 + ], + [ + 2696.00, + 1712.00 + ], + [ + 2696.00, + 1712.00 + ], + [ + 2696.00, + 1712.00 + ], + [ + 2673.00, + 1699.00 + ], + [ + 2673.00, + 1699.00 + ], + [ + 2673.00, + 1699.00 + ], + [ + 2660.00, + 1693.00 + ], + [ + 2659.00, + 1693.00 + ], + [ + 2658.00, + 1693.00 + ], + [ + 2646.00, + 1685.00 + ], + [ + 2645.00, + 1684.00 + ], + [ + 2644.00, + 1683.00 + ], + [ + 2636.00, + 1679.00 + ], + [ + 2634.00, + 1677.00 + ], + [ + 2632.00, + 1675.00 + ], + [ + 2624.00, + 1667.00 + ], + [ + 2624.00, + 1666.00 + ], + [ + 2624.00, + 1665.00 + ], + [ + 2613.00, + 1653.00 + ], + [ + 2612.00, + 1652.00 + ], + [ + 2611.00, + 1651.00 + ], + [ + 2598.00, + 1647.00 + ], + [ + 2598.00, + 1646.00 + ], + [ + 2598.00, + 1645.00 + ], + [ + 2601.00, + 1627.00 + ], + [ + 2601.00, + 1627.00 + ], + [ + 2601.00, + 1627.00 + ], + [ + 2593.00, + 1610.00 + ], + [ + 2593.00, + 1610.00 + ], + [ + 2593.00, + 1610.00 + ], + [ + 2576.00, + 1603.00 + ], + [ + 2576.00, + 1603.00 + ], + [ + 2576.00, + 1603.00 + ], + [ + 2565.00, + 1593.00 + ], + [ + 2565.00, + 1593.00 + ], + [ + 2565.00, + 1593.00 + ], + [ + 2558.00, + 1573.00 + ], + [ + 2558.00, + 1573.00 + ], + [ + 2558.00, + 1573.00 + ], + [ + 2551.00, + 1556.00 + ], + [ + 2551.00, + 1556.00 + ], + [ + 2551.00, + 1556.00 + ], + [ + 2538.00, + 1545.00 + ], + [ + 2537.00, + 1544.00 + ], + [ + 2536.00, + 1543.00 + ], + [ + 2526.00, + 1537.00 + ], + [ + 2526.00, + 1537.00 + ], + [ + 2526.00, + 1537.00 + ], + [ + 2514.00, + 1534.00 + ], + [ + 2513.00, + 1534.00 + ], + [ + 2512.00, + 1534.00 + ], + [ + 2503.00, + 1535.00 + ], + [ + 2503.00, + 1535.00 + ], + [ + 2503.00, + 1535.00 + ], + [ + 2477.00, + 1533.00 + ], + [ + 2473.00, + 1533.00 + ], + [ + 2469.00, + 1533.00 + ], + [ + 2457.00, + 1537.00 + ], + [ + 2457.00, + 1537.00 + ], + [ + 2457.00, + 1537.00 + ], + [ + 2439.00, + 1538.00 + ], + [ + 2439.00, + 1538.00 + ], + [ + 2439.00, + 1538.00 + ], + [ + 2428.00, + 1540.00 + ], + [ + 2427.00, + 1540.00 + ], + [ + 2426.00, + 1540.00 + ], + [ + 2407.00, + 1537.00 + ], + [ + 2404.00, + 1535.00 + ], + [ + 2401.00, + 1533.00 + ], + [ + 2394.00, + 1529.00 + ], + [ + 2394.00, + 1526.00 + ], + [ + 2394.00, + 1523.00 + ], + [ + 2398.00, + 1509.00 + ], + [ + 2399.00, + 1507.00 + ], + [ + 2400.00, + 1505.00 + ], + [ + 2401.00, + 1496.00 + ], + [ + 2403.00, + 1488.00 + ], + [ + 2405.00, + 1480.00 + ], + [ + 2409.00, + 1474.00 + ], + [ + 2410.00, + 1471.00 + ], + [ + 2411.00, + 1468.00 + ], + [ + 2417.00, + 1457.00 + ], + [ + 2419.00, + 1453.00 + ], + [ + 2421.00, + 1449.00 + ], + [ + 2425.00, + 1439.00 + ], + [ + 2426.00, + 1438.00 + ], + [ + 2427.00, + 1437.00 + ], + [ + 2432.00, + 1423.00 + ], + [ + 2434.00, + 1419.00 + ], + [ + 2436.00, + 1415.00 + ], + [ + 2439.00, + 1408.00 + ], + [ + 2440.00, + 1406.00 + ], + [ + 2441.00, + 1404.00 + ], + [ + 2455.00, + 1402.00 + ], + [ + 2460.00, + 1401.00 + ], + [ + 2465.00, + 1400.00 + ], + [ + 2468.00, + 1400.00 + ], + [ + 2476.00, + 1402.00 + ], + [ + 2484.00, + 1404.00 + ], + [ + 2498.00, + 1404.00 + ], + [ + 2498.00, + 1404.00 + ], + [ + 2498.00, + 1404.00 + ], + [ + 2515.00, + 1404.00 + ], + [ + 2516.00, + 1403.00 + ], + [ + 2517.00, + 1402.00 + ], + [ + 2503.00, + 1398.00 + ], + [ + 2501.00, + 1398.00 + ], + [ + 2499.00, + 1398.00 + ], + [ + 2485.00, + 1393.00 + ], + [ + 2485.00, + 1393.00 + ], + [ + 2485.00, + 1393.00 + ], + [ + 2462.00, + 1387.00 + ], + [ + 2462.00, + 1387.00 + ], + [ + 2462.00, + 1387.00 + ], + [ + 2448.00, + 1376.00 + ], + [ + 2448.00, + 1376.00 + ], + [ + 2448.00, + 1376.00 + ], + [ + 2457.00, + 1367.00 + ], + [ + 2463.00, + 1355.00 + ], + [ + 2466.00, + 1348.00 + ], + [ + 2467.00, + 1348.00 + ], + [ + 2470.00, + 1340.00 + ], + [ + 2473.00, + 1332.00 + ], + [ + 2471.00, + 1331.00 + ], + [ + 2491.00, + 1287.00 + ], + [ + 2493.00, + 1281.00 + ], + [ + 2490.00, + 1275.00 + ], + [ + 2492.00, + 1273.00 + ], + [ + 2494.00, + 1271.00 + ], + [ + 2505.00, + 1241.00 + ], + [ + 2507.00, + 1237.00 + ], + [ + 2509.00, + 1233.00 + ], + [ + 2509.00, + 1231.00 + ], + [ + 2509.00, + 1231.00 + ], + [ + 2509.00, + 1231.00 + ], + [ + 2517.00, + 1210.00 + ], + [ + 2516.00, + 1206.00 + ], + [ + 2515.00, + 1202.00 + ], + [ + 2510.00, + 1189.00 + ], + [ + 2510.00, + 1189.00 + ], + [ + 2510.00, + 1189.00 + ], + [ + 2505.00, + 1178.00 + ], + [ + 2505.00, + 1178.00 + ], + [ + 2505.00, + 1178.00 + ], + [ + 2500.00, + 1163.00 + ], + [ + 2501.00, + 1162.00 + ], + [ + 2506, + 1147 + ], + [ + 2514, + 1132 + ] + ] + }, + { + "name": "Gandalf the Grey", + "id": "gandalf_grey", + "color": "olive", + "distance": "280 miles / 450 km (from Rivendell)", + "startDate": "December 25, T.A. 3018", + "endDate": "January 15, T.A. 3019", + "path": [ + [ + 2337.00, + 2112.00 + ], + [ + 2344.50, + 2133.00 + ], + [ + 2344.50, + 2133.00 + ], + [ + 2344.50, + 2133.00 + ], + [ + 2344.50, + 2151.00 + ], + [ + 2344.50, + 2151.00 + ], + [ + 2344.50, + 2151.00 + ], + [ + 2344.50, + 2175.00 + ], + [ + 2344.50, + 2175.00 + ], + [ + 2344.50, + 2175.00 + ], + [ + 2341.50, + 2193.00 + ], + [ + 2344.50, + 2196.00 + ], + [ + 2347.50, + 2199.00 + ], + [ + 2356.50, + 2218.50 + ], + [ + 2356.50, + 2218.50 + ], + [ + 2356.50, + 2218.50 + ], + [ + 2365.50, + 2242.50 + ], + [ + 2365.50, + 2242.50 + ], + [ + 2365.50, + 2242.50 + ], + [ + 2389.50, + 2251.50 + ], + [ + 2389.50, + 2251.50 + ], + [ + 2389.50, + 2251.50 + ], + [ + 2416.50, + 2256.00 + ], + [ + 2416.50, + 2256.00 + ], + [ + 2416.50, + 2256.00 + ], + [ + 2445.00, + 2262.00 + ], + [ + 2445.00, + 2262.00 + ], + [ + 2445.00, + 2262.00 + ], + [ + 2475.00, + 2272.50 + ], + [ + 2475.00, + 2272.50 + ], + [ + 2475.00, + 2272.50 + ], + [ + 2494.50, + 2286.00 + ], + [ + 2494.50, + 2286.00 + ], + [ + 2494.50, + 2286.00 + ], + [ + 2520.00, + 2298.00 + ], + [ + 2520.00, + 2298.00 + ], + [ + 2520.00, + 2298.00 + ], + [ + 2547.00, + 2313.00 + ], + [ + 2547.00, + 2313.00 + ], + [ + 2547.00, + 2313.00 + ], + [ + 2569.50, + 2329.50 + ], + [ + 2569.50, + 2329.50 + ], + [ + 2569.50, + 2329.50 + ], + [ + 2581.50, + 2347.50 + ], + [ + 2581.50, + 2349.00 + ], + [ + 2581.50, + 2350.50 + ], + [ + 2589.00, + 2385.00 + ], + [ + 2589.00, + 2385.00 + ], + [ + 2589.00, + 2385.00 + ], + [ + 2572.50, + 2374.50 + ], + [ + 2572.50, + 2374.50 + ], + [ + 2572.50, + 2374.50 + ], + [ + 2554.50, + 2358.00 + ], + [ + 2554.50, + 2356.50 + ], + [ + 2554.50, + 2355.00 + ], + [ + 2526.00, + 2340.00 + ], + [ + 2526.00, + 2340.00 + ], + [ + 2526.00, + 2340.00 + ], + [ + 2476.50, + 2320.50 + ], + [ + 2476.50, + 2320.50 + ], + [ + 2476.50, + 2320.50 + ], + [ + 2449.50, + 2302.50 + ], + [ + 2449.50, + 2302.50 + ], + [ + 2449.50, + 2302.50 + ], + [ + 2422.50, + 2290.50 + ], + [ + 2422.50, + 2290.50 + ], + [ + 2422.50, + 2290.50 + ], + [ + 2394.00, + 2280.00 + ], + [ + 2394.00, + 2280.00 + ], + [ + 2394.00, + 2280.00 + ], + [ + 2355.00, + 2269.50 + ], + [ + 2353.50, + 2269.50 + ], + [ + 2352.00, + 2269.50 + ], + [ + 2323.50, + 2256.00 + ], + [ + 2323.50, + 2256.00 + ], + [ + 2323.50, + 2256.00 + ], + [ + 2298.00, + 2245.50 + ], + [ + 2298.00, + 2244.00 + ], + [ + 2298.00, + 2242.50 + ], + [ + 2271.00, + 2232.00 + ], + [ + 2271.00, + 2232.00 + ], + [ + 2271.00, + 2232.00 + ], + [ + 2236.50, + 2218.50 + ], + [ + 2236.50, + 2218.50 + ], + [ + 2236.50, + 2218.50 + ], + [ + 2208.00, + 2199.00 + ], + [ + 2208.00, + 2199.00 + ], + [ + 2208.00, + 2199.00 + ], + [ + 2179.50, + 2172.00 + ], + [ + 2179.50, + 2172.00 + ], + [ + 2179.50, + 2172.00 + ], + [ + 2158.50, + 2142.00 + ], + [ + 2158.50, + 2142.00 + ], + [ + 2158.50, + 2142.00 + ], + [ + 2142.00, + 2112.00 + ], + [ + 2142.00, + 2112.00 + ], + [ + 2142.00, + 2112.00 + ], + [ + 2131.50, + 2082.00 + ], + [ + 2131.50, + 2082.00 + ], + [ + 2131.50, + 2082.00 + ], + [ + 2116.50, + 2041.50 + ], + [ + 2116.50, + 2041.50 + ], + [ + 2116.50, + 2041.50 + ], + [ + 2106.00, + 2015.00 + ], + [ + 2106.00, + 2015.00 + ], + [ + 2106.00, + 2015.00 + ], + [ + 2096.00, + 1991.00 + ], + [ + 2096.00, + 1991.00 + ], + [ + 2096.00, + 1991.00 + ], + [ + 2088.00, + 1965.00 + ], + [ + 2088.00, + 1965.00 + ], + [ + 2088.00, + 1965.00 + ], + [ + 2082.00, + 1946.00 + ], + [ + 2082.00, + 1946.00 + ], + [ + 2082.00, + 1946.00 + ], + [ + 2075.00, + 1926.00 + ], + [ + 2075.00, + 1926.00 + ], + [ + 2075.00, + 1926.00 + ], + [ + 2071.00, + 1908.00 + ], + [ + 2071.00, + 1908.00 + ], + [ + 2071.00, + 1908.00 + ], + [ + 2066.00, + 1885.00 + ], + [ + 2066.00, + 1885.00 + ], + [ + 2066.00, + 1885.00 + ], + [ + 2060.00, + 1864.00 + ], + [ + 2060.00, + 1864.00 + ], + [ + 2060.00, + 1864.00 + ], + [ + 2054.00, + 1842.00 + ], + [ + 2054.00, + 1842.00 + ], + [ + 2054.00, + 1842.00 + ], + [ + 2053.00, + 1822.00 + ], + [ + 2053.00, + 1822.00 + ], + [ + 2053.00, + 1822.00 + ], + [ + 2055.00, + 1803.00 + ], + [ + 2055.00, + 1803.00 + ], + [ + 2055.00, + 1803.00 + ], + [ + 2055.00, + 1782.00 + ], + [ + 2055.00, + 1782.00 + ], + [ + 2055.00, + 1782.00 + ], + [ + 2049.00, + 1753.00 + ], + [ + 2049.00, + 1753.00 + ], + [ + 2049.00, + 1753.00 + ], + [ + 2044.00, + 1734.00 + ], + [ + 2044.00, + 1734.00 + ], + [ + 2044.00, + 1734.00 + ], + [ + 2034.00, + 1712.00 + ], + [ + 2034.00, + 1712.00 + ], + [ + 2034.00, + 1712.00 + ], + [ + 2025.00, + 1693.00 + ], + [ + 2025.00, + 1693.00 + ], + [ + 2025.00, + 1693.00 + ], + [ + 2013.00, + 1677.00 + ], + [ + 2013.00, + 1676.00 + ], + [ + 2013.00, + 1675.00 + ], + [ + 2002.00, + 1655.00 + ], + [ + 2002.00, + 1655.00 + ], + [ + 2002.00, + 1655.00 + ], + [ + 1987.00, + 1636.00 + ], + [ + 1987.00, + 1636.00 + ], + [ + 1987.00, + 1636.00 + ], + [ + 1971.00, + 1621.00 + ], + [ + 1971.00, + 1621.00 + ], + [ + 1971.00, + 1621.00 + ], + [ + 1963.00, + 1594.00 + ], + [ + 1963.00, + 1594.00 + ], + [ + 1963.00, + 1594.00 + ], + [ + 1957.00, + 1572.00 + ], + [ + 1957.00, + 1572.00 + ], + [ + 1957.00, + 1572.00 + ], + [ + 1945.00, + 1555.00 + ], + [ + 1945.00, + 1555.00 + ], + [ + 1945.00, + 1555.00 + ], + [ + 1937.00, + 1538.00 + ], + [ + 1937.00, + 1538.00 + ], + [ + 1937.00, + 1538.00 + ], + [ + 1924.00, + 1518.00 + ], + [ + 1924.00, + 1518.00 + ], + [ + 1924.00, + 1518.00 + ], + [ + 1910.00, + 1502.00 + ], + [ + 1910.00, + 1501.00 + ], + [ + 1910.00, + 1500.00 + ], + [ + 1896.00, + 1488.00 + ], + [ + 1896.00, + 1488.00 + ], + [ + 1896.00, + 1488.00 + ], + [ + 1878.00, + 1474.00 + ], + [ + 1878.00, + 1474.00 + ], + [ + 1878.00, + 1474.00 + ], + [ + 1854.00, + 1458.00 + ], + [ + 1854.00, + 1458.00 + ], + [ + 1854.00, + 1458.00 + ], + [ + 1834.00, + 1443.00 + ], + [ + 1834.00, + 1443.00 + ], + [ + 1834.00, + 1443.00 + ], + [ + 1818.00, + 1431.00 + ], + [ + 1818.00, + 1430.00 + ], + [ + 1818.00, + 1429.00 + ], + [ + 1795.00, + 1418.00 + ], + [ + 1795.00, + 1418.00 + ], + [ + 1795.00, + 1418.00 + ], + [ + 1776.00, + 1410.00 + ], + [ + 1776.00, + 1410.00 + ], + [ + 1776.00, + 1410.00 + ], + [ + 1750.00, + 1393.00 + ], + [ + 1750.00, + 1393.00 + ], + [ + 1750.00, + 1393.00 + ], + [ + 1727.00, + 1385.00 + ], + [ + 1727.00, + 1385.00 + ], + [ + 1727.00, + 1385.00 + ], + [ + 1703.00, + 1383.00 + ], + [ + 1702.00, + 1383.00 + ], + [ + 1701.00, + 1383.00 + ], + [ + 1682.00, + 1382.00 + ], + [ + 1681.00, + 1382.00 + ], + [ + 1680.00, + 1382.00 + ], + [ + 1659.00, + 1379.00 + ], + [ + 1659.00, + 1379.00 + ], + [ + 1659.00, + 1379.00 + ], + [ + 1629.00, + 1378.00 + ], + [ + 1629.00, + 1378.00 + ], + [ + 1629.00, + 1378.00 + ], + [ + 1605.00, + 1379.00 + ], + [ + 1605.00, + 1378.00 + ], + [ + 1605.00, + 1377.00 + ], + [ + 1584.00, + 1369.00 + ], + [ + 1584.00, + 1369.00 + ], + [ + 1584.00, + 1369.00 + ], + [ + 1561.00, + 1356.00 + ], + [ + 1561.00, + 1356.00 + ], + [ + 1561.00, + 1356.00 + ], + [ + 1546.00, + 1331.00 + ], + [ + 1545.00, + 1331.00 + ], + [ + 1544.00, + 1331.00 + ], + [ + 1526.00, + 1314.00 + ], + [ + 1526.00, + 1314.00 + ], + [ + 1526.00, + 1314.00 + ], + [ + 1515.00, + 1295.00 + ], + [ + 1515.00, + 1295.00 + ], + [ + 1515.00, + 1295.00 + ], + [ + 1501.00, + 1268.00 + ], + [ + 1501.00, + 1268.00 + ], + [ + 1501.00, + 1268.00 + ], + [ + 1488.00, + 1253.00 + ], + [ + 1488.00, + 1253.00 + ], + [ + 1488.00, + 1253.00 + ], + [ + 1466.00, + 1244.00 + ], + [ + 1466.00, + 1244.00 + ], + [ + 1466.00, + 1244.00 + ], + [ + 1449.00, + 1230.00 + ], + [ + 1449.00, + 1230.00 + ], + [ + 1449.00, + 1230.00 + ], + [ + 1437.00, + 1219.00 + ], + [ + 1437.00, + 1219.00 + ], + [ + 1437.00, + 1219.00 + ], + [ + 1426.00, + 1197.00 + ], + [ + 1426.00, + 1197.00 + ], + [ + 1426.00, + 1197.00 + ], + [ + 1419.00, + 1184.00 + ], + [ + 1419.00, + 1184.00 + ], + [ + 1419.00, + 1184.00 + ], + [ + 1436.00, + 1176.00 + ], + [ + 1436.00, + 1176.00 + ], + [ + 1436.00, + 1176.00 + ], + [ + 1452.00, + 1178.00 + ], + [ + 1452.00, + 1178.00 + ], + [ + 1452.00, + 1178.00 + ], + [ + 1469.00, + 1178.00 + ], + [ + 1469.00, + 1178.00 + ], + [ + 1469.00, + 1178.00 + ], + [ + 1482.00, + 1166.00 + ], + [ + 1482.00, + 1166.00 + ], + [ + 1482.00, + 1166.00 + ], + [ + 1494.00, + 1176.00 + ], + [ + 1494.00, + 1176.00 + ], + [ + 1494.00, + 1176.00 + ], + [ + 1516.00, + 1171.00 + ], + [ + 1516.00, + 1171.00 + ], + [ + 1516.00, + 1171.00 + ], + [ + 1535.00, + 1161.00 + ], + [ + 1535.00, + 1161.00 + ], + [ + 1535.00, + 1161.00 + ], + [ + 1564.00, + 1155.00 + ], + [ + 1564.00, + 1154.00 + ], + [ + 1564.00, + 1153.00 + ], + [ + 1589.00, + 1144.00 + ], + [ + 1589.00, + 1144.00 + ], + [ + 1589.00, + 1144.00 + ], + [ + 1608.00, + 1136.00 + ], + [ + 1608.00, + 1136.00 + ], + [ + 1608.00, + 1136.00 + ], + [ + 1629.00, + 1132.00 + ], + [ + 1629.00, + 1131.00 + ], + [ + 1629.00, + 1130.00 + ], + [ + 1653.00, + 1125.00 + ], + [ + 1653.00, + 1125.00 + ], + [ + 1653.00, + 1125.00 + ], + [ + 1682.00, + 1129.00 + ], + [ + 1682.00, + 1129.00 + ], + [ + 1682.00, + 1129.00 + ], + [ + 1705.00, + 1128.00 + ], + [ + 1706.00, + 1128.00 + ], + [ + 1707.00, + 1128.00 + ], + [ + 1727.00, + 1131.00 + ], + [ + 1727.00, + 1131.00 + ], + [ + 1727.00, + 1131.00 + ], + [ + 1751.00, + 1138.00 + ], + [ + 1751.00, + 1138.00 + ], + [ + 1751.00, + 1138.00 + ], + [ + 1758.00, + 1151.00 + ], + [ + 1758.00, + 1151.00 + ], + [ + 1758.00, + 1151.00 + ], + [ + 1761.00, + 1164.00 + ], + [ + 1761.00, + 1164.00 + ], + [ + 1761.00, + 1164.00 + ], + [ + 1772.00, + 1175.00 + ], + [ + 1772.00, + 1175.00 + ], + [ + 1772.00, + 1175.00 + ], + [ + 1784.00, + 1171.00 + ], + [ + 1784.00, + 1171.00 + ], + [ + 1784.00, + 1171.00 + ], + [ + 1793.00, + 1162.00 + ], + [ + 1793.00, + 1162.00 + ], + [ + 1793.00, + 1162.00 + ], + [ + 1811.00, + 1169.00 + ], + [ + 1811.00, + 1169.00 + ], + [ + 1811.00, + 1169.00 + ], + [ + 1833.00, + 1160.00 + ], + [ + 1833.00, + 1160.00 + ], + [ + 1833.00, + 1160.00 + ], + [ + 1851.00, + 1159.00 + ], + [ + 1851.00, + 1159.00 + ], + [ + 1851.00, + 1159.00 + ], + [ + 1868.00, + 1161.00 + ], + [ + 1869.00, + 1161.00 + ], + [ + 1870.00, + 1161.00 + ], + [ + 1893.00, + 1168.00 + ], + [ + 1893.00, + 1168.00 + ], + [ + 1893.00, + 1168.00 + ], + [ + 1913.00, + 1173.00 + ], + [ + 1913.00, + 1173.00 + ], + [ + 1913.00, + 1173.00 + ], + [ + 1930.00, + 1174.00 + ], + [ + 1930.00, + 1174.00 + ], + [ + 1930.00, + 1174.00 + ], + [ + 1945.00, + 1174.00 + ], + [ + 1946.00, + 1174.00 + ], + [ + 1947.00, + 1174.00 + ], + [ + 1958.00, + 1175.00 + ], + [ + 1959.00, + 1175.00 + ], + [ + 1960.00, + 1175.00 + ], + [ + 1978.00, + 1175.00 + ], + [ + 1978.00, + 1175.00 + ], + [ + 1978.00, + 1175.00 + ], + [ + 1994.00, + 1174.00 + ], + [ + 1994.00, + 1174.00 + ], + [ + 1994.00, + 1174.00 + ], + [ + 2014.00, + 1171.00 + ], + [ + 2014.00, + 1171.00 + ], + [ + 2014.00, + 1171.00 + ], + [ + 2038.00, + 1165.00 + ], + [ + 2038.00, + 1165.00 + ], + [ + 2038.00, + 1165.00 + ], + [ + 2059.00, + 1162.00 + ], + [ + 2059.00, + 1162.00 + ], + [ + 2059.00, + 1162.00 + ], + [ + 2071.00, + 1157.00 + ], + [ + 2071.00, + 1157.00 + ], + [ + 2071.00, + 1157.00 + ], + [ + 2096.00, + 1147.00 + ], + [ + 2096.00, + 1147.00 + ], + [ + 2096.00, + 1147.00 + ], + [ + 2115.00, + 1140.00 + ], + [ + 2115.00, + 1140.00 + ], + [ + 2115.00, + 1140.00 + ], + [ + 2134.00, + 1135.00 + ], + [ + 2135.00, + 1135.00 + ], + [ + 2136.00, + 1135.00 + ], + [ + 2156.00, + 1131.00 + ], + [ + 2156.00, + 1131.00 + ], + [ + 2156.00, + 1131.00 + ], + [ + 2174.00, + 1132.00 + ], + [ + 2174.00, + 1132.00 + ], + [ + 2174.00, + 1132.00 + ], + [ + 2193.00, + 1127.00 + ], + [ + 2193.00, + 1127.00 + ], + [ + 2193.00, + 1127.00 + ], + [ + 2213.00, + 1123.00 + ], + [ + 2213.00, + 1123.00 + ], + [ + 2213.00, + 1123.00 + ], + [ + 2230.00, + 1122.00 + ], + [ + 2230.00, + 1122.00 + ], + [ + 2230.00, + 1122.00 + ], + [ + 2252.00, + 1125.00 + ], + [ + 2252.00, + 1125.00 + ], + [ + 2252.00, + 1125.00 + ], + [ + 2276.00, + 1128.00 + ], + [ + 2276.00, + 1128.00 + ], + [ + 2276.00, + 1128.00 + ], + [ + 2302.00, + 1132.00 + ], + [ + 2302.00, + 1132.00 + ], + [ + 2302.00, + 1132.00 + ], + [ + 2330.00, + 1134.00 + ], + [ + 2330.00, + 1134.00 + ], + [ + 2330.00, + 1134.00 + ], + [ + 2352.00, + 1135.00 + ], + [ + 2352.00, + 1135.00 + ], + [ + 2352.00, + 1135.00 + ], + [ + 2368.00, + 1142.00 + ], + [ + 2368.00, + 1142.00 + ], + [ + 2368.00, + 1142.00 + ], + [ + 2388.00, + 1144.00 + ], + [ + 2388.00, + 1144.00 + ], + [ + 2388.00, + 1144.00 + ], + [ + 2413.00, + 1146.00 + ], + [ + 2413.00, + 1146.00 + ], + [ + 2413.00, + 1146.00 + ], + [ + 2431.00, + 1148.00 + ], + [ + 2431.00, + 1148.00 + ], + [ + 2431.00, + 1148.00 + ], + [ + 2450.00, + 1151.00 + ], + [ + 2450.00, + 1151.00 + ], + [ + 2450.00, + 1151.00 + ], + [ + 2469.00, + 1152.00 + ], + [ + 2469.00, + 1151.00 + ], + [ + 2469.00, + 1150.00 + ], + [ + 2484.00, + 1150.00 + ], + [ + 2484.00, + 1150.00 + ], + [ + 2484.00, + 1150.00 + ], + [ + 2499.00, + 1143.00 + ], + [ + 2499.00, + 1143.00 + ], + [ + 2499.00, + 1143.00 + ], + [ + 2514.00, + 1131.00 + ], + [ + 2514.00, + 1131.00 + ], + [ + 2514.00, + 1133.00 + ], + [ + 2501.00, + 1163.00 + ], + [ + 2500.00, + 1164.00 + ], + [ + 2505.00, + 1179.00 + ], + [ + 2505.00, + 1179.00 + ], + [ + 2505.00, + 1179.00 + ], + [ + 2510.00, + 1190.00 + ], + [ + 2510.00, + 1190.00 + ], + [ + 2510.00, + 1190.00 + ], + [ + 2515.00, + 1203.00 + ], + [ + 2516.00, + 1207.00 + ], + [ + 2517.00, + 1211.00 + ], + [ + 2509.00, + 1232.00 + ], + [ + 2509.00, + 1232.00 + ], + [ + 2509.00, + 1232.00 + ], + [ + 2509.00, + 1234.00 + ], + [ + 2507.00, + 1238.00 + ], + [ + 2505.00, + 1242.00 + ], + [ + 2494.00, + 1272.00 + ], + [ + 2492.00, + 1274.00 + ], + [ + 2490.00, + 1276.00 + ], + [ + 2493.00, + 1282.00 + ], + [ + 2491.00, + 1288.00 + ], + [ + 2471.00, + 1332.00 + ], + [ + 2473.00, + 1333.00 + ], + [ + 2470.00, + 1341.00 + ], + [ + 2467.00, + 1349.00 + ], + [ + 2466.00, + 1349.00 + ], + [ + 2463.00, + 1356.00 + ], + [ + 2457.00, + 1368.00 + ], + [ + 2448.00, + 1377.00 + ], + [ + 2448.00, + 1377.00 + ], + [ + 2448.00, + 1377.00 + ], + [ + 2462.00, + 1388.00 + ], + [ + 2462.00, + 1388.00 + ], + [ + 2462.00, + 1388.00 + ], + [ + 2485.00, + 1394.00 + ], + [ + 2485.00, + 1394.00 + ], + [ + 2485.00, + 1394.00 + ], + [ + 2499.00, + 1399.00 + ], + [ + 2501.00, + 1399.00 + ], + [ + 2503.00, + 1399.00 + ], + [ + 2517.00, + 1403.00 + ], + [ + 2516.00, + 1404.00 + ], + [ + 2515.00, + 1405.00 + ], + [ + 2498.00, + 1405.00 + ], + [ + 2498.00, + 1405.00 + ], + [ + 2498.00, + 1405.00 + ], + [ + 2484.00, + 1405.00 + ], + [ + 2476.00, + 1403.00 + ], + [ + 2468.00, + 1401.00 + ], + [ + 2465.00, + 1401.00 + ], + [ + 2460.00, + 1402.00 + ], + [ + 2455.00, + 1403.00 + ], + [ + 2441.00, + 1405.00 + ], + [ + 2440.00, + 1407.00 + ], + [ + 2439.00, + 1409.00 + ], + [ + 2436.00, + 1416.00 + ], + [ + 2434.00, + 1420.00 + ], + [ + 2432.00, + 1424.00 + ], + [ + 2427.00, + 1438.00 + ], + [ + 2426.00, + 1439.00 + ], + [ + 2425.00, + 1440.00 + ], + [ + 2421.00, + 1450.00 + ], + [ + 2419.00, + 1454.00 + ], + [ + 2417.00, + 1458.00 + ], + [ + 2411.00, + 1469.00 + ], + [ + 2410.00, + 1472.00 + ], + [ + 2409.00, + 1475.00 + ], + [ + 2405.00, + 1481.00 + ], + [ + 2403.00, + 1489.00 + ], + [ + 2401.00, + 1497.00 + ], + [ + 2400.00, + 1506.00 + ], + [ + 2399.00, + 1508.00 + ], + [ + 2398.00, + 1510.00 + ], + [ + 2394.00, + 1524.00 + ], + [ + 2394.00, + 1527.00 + ], + [ + 2394.00, + 1530.00 + ], + [ + 2401.00, + 1534.00 + ], + [ + 2404.00, + 1536.00 + ], + [ + 2407.00, + 1538.00 + ], + [ + 2426.00, + 1541.00 + ], + [ + 2427.00, + 1541.00 + ], + [ + 2428.00, + 1541.00 + ], + [ + 2439.00, + 1539.00 + ], + [ + 2439.00, + 1539.00 + ], + [ + 2439.00, + 1539.00 + ], + [ + 2457.00, + 1538.00 + ], + [ + 2457.00, + 1538.00 + ], + [ + 2457.00, + 1538.00 + ], + [ + 2469.00, + 1534.00 + ], + [ + 2473.00, + 1534.00 + ], + [ + 2477.00, + 1534.00 + ], + [ + 2503.00, + 1536.00 + ], + [ + 2503.00, + 1536.00 + ], + [ + 2503.00, + 1536.00 + ], + [ + 2512.00, + 1535.00 + ], + [ + 2513.00, + 1535.00 + ], + [ + 2514.00, + 1535.00 + ], + [ + 2526.00, + 1538.00 + ], + [ + 2526.00, + 1538.00 + ], + [ + 2526.00, + 1538.00 + ] + ] + }, + { + "name": "Gandalf the White", + "id": "gandalf_white", + "color": "gold", + "distance": "950 miles / 1530 km", + "startDate": "March 1", + "endDate": "March 25, T.A. 3019", + "path": [ + [ + 2532.00, + 1548.00 + ], + [ + 2551.00, + 1558.00 + ], + [ + 2551.00, + 1558.00 + ], + [ + 2551.00, + 1558.00 + ], + [ + 2568.00, + 1573.00 + ], + [ + 2568.00, + 1573.00 + ], + [ + 2568.00, + 1573.00 + ], + [ + 2585.00, + 1588.00 + ], + [ + 2585.00, + 1588.00 + ], + [ + 2585.00, + 1588.00 + ], + [ + 2604.00, + 1602.00 + ], + [ + 2604.00, + 1602.00 + ], + [ + 2604.00, + 1602.00 + ], + [ + 2630.00, + 1617.00 + ], + [ + 2630.00, + 1617.00 + ], + [ + 2630.00, + 1617.00 + ], + [ + 2662.00, + 1637.00 + ], + [ + 2662.00, + 1637.00 + ], + [ + 2662.00, + 1637.00 + ], + [ + 2679.00, + 1650.00 + ], + [ + 2679.00, + 1650.00 + ], + [ + 2679.00, + 1650.00 + ], + [ + 2701.00, + 1662.00 + ], + [ + 2701.00, + 1662.00 + ], + [ + 2701.00, + 1662.00 + ], + [ + 2721.00, + 1679.00 + ], + [ + 2721.00, + 1679.00 + ], + [ + 2721.00, + 1679.00 + ], + [ + 2737.00, + 1694.00 + ], + [ + 2737.00, + 1694.00 + ], + [ + 2737.00, + 1694.00 + ], + [ + 2755.00, + 1706.00 + ], + [ + 2755.00, + 1706.00 + ], + [ + 2755.00, + 1706.00 + ], + [ + 2741.00, + 1720.00 + ], + [ + 2741.00, + 1720.00 + ], + [ + 2741.00, + 1720.00 + ], + [ + 2727.00, + 1729.00 + ], + [ + 2727.00, + 1729.00 + ], + [ + 2727.00, + 1729.00 + ], + [ + 2718.00, + 1736.00 + ], + [ + 2718.00, + 1736.00 + ], + [ + 2718.00, + 1736.00 + ], + [ + 2704.00, + 1750.00 + ], + [ + 2704.00, + 1750.00 + ], + [ + 2704.00, + 1750.00 + ], + [ + 2688.00, + 1761.00 + ], + [ + 2688.00, + 1761.00 + ], + [ + 2688.00, + 1761.00 + ], + [ + 2679.00, + 1771.00 + ], + [ + 2679.00, + 1771.00 + ], + [ + 2679.00, + 1771.00 + ], + [ + 2663.00, + 1784.00 + ], + [ + 2663.00, + 1784.00 + ], + [ + 2663.00, + 1784.00 + ], + [ + 2647.00, + 1800.00 + ], + [ + 2647.00, + 1800.00 + ], + [ + 2647.00, + 1800.00 + ], + [ + 2615.00, + 1825.00 + ], + [ + 2615.00, + 1825.00 + ], + [ + 2615.00, + 1825.00 + ], + [ + 2599.00, + 1835.00 + ], + [ + 2599.00, + 1835.00 + ], + [ + 2599.00, + 1835.00 + ], + [ + 2577.00, + 1853.00 + ], + [ + 2577.00, + 1853.00 + ], + [ + 2577.00, + 1853.00 + ], + [ + 2562.00, + 1865.00 + ], + [ + 2562.00, + 1865.00 + ], + [ + 2562.00, + 1865.00 + ], + [ + 2535.00, + 1886.00 + ], + [ + 2535.00, + 1886.00 + ], + [ + 2535.00, + 1886.00 + ], + [ + 2511.00, + 1907.00 + ], + [ + 2511.00, + 1907.00 + ], + [ + 2511.00, + 1907.00 + ], + [ + 2486.00, + 1926.00 + ], + [ + 2486.00, + 1926.00 + ], + [ + 2486.00, + 1926.00 + ], + [ + 2470.00, + 1936.00 + ], + [ + 2470.00, + 1936.00 + ], + [ + 2470.00, + 1936.00 + ], + [ + 2448.00, + 1948.00 + ], + [ + 2448.00, + 1948.00 + ], + [ + 2448.00, + 1948.00 + ], + [ + 2432.00, + 1956.00 + ], + [ + 2432.00, + 1956.00 + ], + [ + 2432.00, + 1956.00 + ], + [ + 2426.00, + 1976.00 + ], + [ + 2426.00, + 1976.00 + ], + [ + 2426.00, + 1976.00 + ], + [ + 2419.00, + 1993.00 + ], + [ + 2419.00, + 1993.00 + ], + [ + 2419.00, + 1993.00 + ], + [ + 2420.00, + 2009.00 + ], + [ + 2420.00, + 2009.00 + ], + [ + 2420.00, + 2009.00 + ], + [ + 2427.00, + 2027.00 + ], + [ + 2427.00, + 2027.00 + ], + [ + 2427.00, + 2027.00 + ], + [ + 2438.00, + 2042.00 + ], + [ + 2438.00, + 2042.00 + ], + [ + 2438.00, + 2042.00 + ], + [ + 2455.00, + 2049.00 + ], + [ + 2455.00, + 2049.00 + ], + [ + 2455.00, + 2049.00 + ], + [ + 2472.00, + 2052.00 + ], + [ + 2472.00, + 2052.00 + ], + [ + 2472.00, + 2052.00 + ], + [ + 2491.00, + 2057.00 + ], + [ + 2491.00, + 2057.00 + ], + [ + 2491.00, + 2057.00 + ], + [ + 2508.00, + 2062.00 + ], + [ + 2508.00, + 2062.00 + ], + [ + 2508.00, + 2062.00 + ], + [ + 2539.00, + 2072.00 + ], + [ + 2539.00, + 2072.00 + ], + [ + 2539.00, + 2072.00 + ], + [ + 2552.00, + 2076.00 + ], + [ + 2552.00, + 2076.00 + ], + [ + 2552.00, + 2076.00 + ], + [ + 2572.00, + 2082.00 + ], + [ + 2572.00, + 2082.00 + ], + [ + 2572.00, + 2082.00 + ], + [ + 2590.00, + 2087.00 + ], + [ + 2590.00, + 2087.00 + ], + [ + 2590.00, + 2087.00 + ], + [ + 2610.00, + 2095.00 + ], + [ + 2610.00, + 2095.00 + ], + [ + 2610.00, + 2095.00 + ], + [ + 2627.00, + 2099.00 + ], + [ + 2627.00, + 2099.00 + ], + [ + 2627.00, + 2099.00 + ], + [ + 2618.00, + 2118.00 + ], + [ + 2618.00, + 2118.00 + ], + [ + 2618.00, + 2118.00 + ], + [ + 2610.00, + 2134.00 + ], + [ + 2610.00, + 2134.00 + ], + [ + 2610.00, + 2134.00 + ], + [ + 2607.00, + 2150.00 + ], + [ + 2607.00, + 2150.00 + ], + [ + 2607.00, + 2150.00 + ], + [ + 2598.00, + 2166.00 + ], + [ + 2598.00, + 2166.00 + ], + [ + 2598.00, + 2166.00 + ], + [ + 2597.00, + 2184.00 + ], + [ + 2597.00, + 2184.00 + ], + [ + 2597.00, + 2184.00 + ], + [ + 2597.00, + 2200.00 + ], + [ + 2597.00, + 2200.00 + ], + [ + 2597.00, + 2200.00 + ], + [ + 2602.00, + 2221.00 + ], + [ + 2602.00, + 2221.00 + ], + [ + 2602.00, + 2221.00 + ], + [ + 2600.00, + 2239.00 + ], + [ + 2600.00, + 2239.00 + ], + [ + 2600.00, + 2239.00 + ], + [ + 2601.00, + 2260.00 + ], + [ + 2601.00, + 2260.00 + ], + [ + 2601.00, + 2260.00 + ], + [ + 2599.00, + 2278.00 + ], + [ + 2599.00, + 2278.00 + ], + [ + 2599.00, + 2278.00 + ], + [ + 2595.00, + 2298.00 + ], + [ + 2595.00, + 2298.00 + ], + [ + 2595.00, + 2298.00 + ], + [ + 2593.00, + 2317.00 + ], + [ + 2593.00, + 2317.00 + ], + [ + 2593.00, + 2317.00 + ], + [ + 2589.00, + 2346.00 + ], + [ + 2589.00, + 2346.00 + ], + [ + 2589.00, + 2346.00 + ], + [ + 2590.00, + 2366.00 + ], + [ + 2590.00, + 2366.00 + ], + [ + 2590.00, + 2366.00 + ], + [ + 2590.00, + 2379.00 + ], + [ + 2590.00, + 2379.00 + ], + [ + 2590.00, + 2379.00 + ], + [ + 2591.00, + 2388.00 + ], + [ + 2591.00, + 2388.00 + ], + [ + 2591.00, + 2388.00 + ], + [ + 2575.00, + 2367.00 + ], + [ + 2575.00, + 2367.00 + ], + [ + 2575.00, + 2367.00 + ], + [ + 2562.00, + 2352.00 + ], + [ + 2562.00, + 2352.00 + ], + [ + 2562.00, + 2352.00 + ], + [ + 2552.00, + 2346.00 + ], + [ + 2552.00, + 2346.00 + ], + [ + 2552.00, + 2346.00 + ], + [ + 2543.00, + 2339.00 + ], + [ + 2543.00, + 2339.00 + ], + [ + 2543.00, + 2339.00 + ], + [ + 2531.00, + 2331.00 + ], + [ + 2531.00, + 2331.00 + ], + [ + 2531.00, + 2331.00 + ], + [ + 2518.00, + 2321.00 + ], + [ + 2518.00, + 2321.00 + ], + [ + 2518.00, + 2321.00 + ], + [ + 2504.00, + 2310.00 + ], + [ + 2504.00, + 2310.00 + ], + [ + 2504.00, + 2310.00 + ], + [ + 2484.00, + 2300.00 + ], + [ + 2484.00, + 2300.00 + ], + [ + 2484.00, + 2300.00 + ], + [ + 2471.00, + 2293.00 + ], + [ + 2471.00, + 2293.00 + ], + [ + 2471.00, + 2293.00 + ], + [ + 2458.00, + 2289.00 + ], + [ + 2458.00, + 2289.00 + ], + [ + 2458.00, + 2289.00 + ], + [ + 2439.00, + 2284.00 + ], + [ + 2439.00, + 2284.00 + ], + [ + 2439.00, + 2284.00 + ], + [ + 2425.00, + 2272.00 + ], + [ + 2425.00, + 2272.00 + ], + [ + 2425.00, + 2272.00 + ], + [ + 2415.00, + 2264.00 + ], + [ + 2415.00, + 2264.00 + ], + [ + 2415.00, + 2264.00 + ], + [ + 2391.00, + 2262.00 + ], + [ + 2391.00, + 2262.00 + ], + [ + 2391.00, + 2262.00 + ], + [ + 2360.00, + 2255.00 + ], + [ + 2360.00, + 2255.00 + ], + [ + 2360.00, + 2255.00 + ], + [ + 2336.00, + 2253.00 + ], + [ + 2336.00, + 2253.00 + ], + [ + 2336.00, + 2253.00 + ], + [ + 2344.00, + 2231.00 + ], + [ + 2344.00, + 2231.00 + ], + [ + 2344.00, + 2231.00 + ], + [ + 2339.00, + 2213.00 + ], + [ + 2339.00, + 2213.00 + ], + [ + 2339.00, + 2213.00 + ], + [ + 2344.00, + 2196.00 + ], + [ + 2344.00, + 2196.00 + ], + [ + 2344.00, + 2196.00 + ], + [ + 2344.00, + 2185.00 + ], + [ + 2344.00, + 2185.00 + ], + [ + 2344.00, + 2185.00 + ], + [ + 2350.00, + 2163.00 + ], + [ + 2350.00, + 2163.00 + ], + [ + 2350.00, + 2163.00 + ], + [ + 2344.00, + 2135.00 + ], + [ + 2344.00, + 2135.00 + ], + [ + 2344.00, + 2135.00 + ], + [ + 2339.00, + 2118.00 + ], + [ + 2339.00, + 2118.00 + ], + [ + 2339.00, + 2118.00 + ], + [ + 2334.00, + 2135.00 + ], + [ + 2334.00, + 2135.00 + ], + [ + 2334.00, + 2135.00 + ], + [ + 2327.00, + 2151.00 + ], + [ + 2327.00, + 2151.00 + ], + [ + 2327.00, + 2151.00 + ], + [ + 2324.00, + 2164.00 + ], + [ + 2324.00, + 2164.00 + ], + [ + 2324.00, + 2164.00 + ], + [ + 2322.00, + 2176.00 + ], + [ + 2322.00, + 2176.00 + ], + [ + 2322.00, + 2176.00 + ], + [ + 2322.00, + 2191.00 + ], + [ + 2322.00, + 2191.00 + ], + [ + 2322.00, + 2191.00 + ], + [ + 2322.00, + 2209.00 + ], + [ + 2322.00, + 2209.00 + ], + [ + 2322.00, + 2209.00 + ], + [ + 2327.00, + 2232.00 + ], + [ + 2327.00, + 2232.00 + ], + [ + 2351.00, + 2267.00 + ], + [ + 2351.00, + 2267.00 + ], + [ + 2368.00, + 2275.00 + ], + [ + 2368.00, + 2275.00 + ], + [ + 2368.00, + 2275.00 + ], + [ + 2383.00, + 2282.00 + ], + [ + 2383.00, + 2282.00 + ], + [ + 2383.00, + 2282.00 + ], + [ + 2395.00, + 2285.00 + ], + [ + 2395.00, + 2285.00 + ], + [ + 2395.00, + 2285.00 + ], + [ + 2409.00, + 2289.00 + ], + [ + 2409.00, + 2289.00 + ], + [ + 2409.00, + 2289.00 + ], + [ + 2428.00, + 2296.00 + ], + [ + 2428.00, + 2296.00 + ], + [ + 2428.00, + 2296.00 + ], + [ + 2449.00, + 2307.00 + ], + [ + 2449.00, + 2307.00 + ], + [ + 2449.00, + 2307.00 + ], + [ + 2462.00, + 2312.00 + ], + [ + 2462.00, + 2312.00 + ], + [ + 2462.00, + 2312.00 + ], + [ + 2485.00, + 2322.00 + ], + [ + 2485.00, + 2322.00 + ], + [ + 2485.00, + 2322.00 + ], + [ + 2502.00, + 2331.00 + ], + [ + 2502.00, + 2331.00 + ], + [ + 2502.00, + 2331.00 + ], + [ + 2515.00, + 2336.00 + ], + [ + 2515.00, + 2336.00 + ], + [ + 2515.00, + 2336.00 + ], + [ + 2539.00, + 2352.00 + ], + [ + 2539.00, + 2352.00 + ], + [ + 2539.00, + 2352.00 + ], + [ + 2556.00, + 2363.00 + ], + [ + 2556.00, + 2363.00 + ], + [ + 2556.00, + 2363.00 + ], + [ + 2574.00, + 2382.00 + ], + [ + 2574.00, + 2382.00 + ], + [ + 2574.00, + 2382.00 + ], + [ + 2581.00, + 2394.00 + ], + [ + 2581.00, + 2394.00 + ], + [ + 2581.00, + 2394.00 + ], + [ + 2603.00, + 2408.00 + ], + [ + 2603.00, + 2408.00 + ], + [ + 2603.00, + 2408.00 + ], + [ + 2622.00, + 2417.00 + ], + [ + 2622.00, + 2417.00 + ], + [ + 2622.00, + 2417.00 + ], + [ + 2632.00, + 2434.00 + ], + [ + 2632.00, + 2434.00 + ], + [ + 2632.00, + 2434.00 + ], + [ + 2642.00, + 2445.00 + ], + [ + 2642.00, + 2445.00 + ], + [ + 2642.00, + 2445.00 + ], + [ + 2653.00, + 2456.00 + ], + [ + 2653.00, + 2456.00 + ], + [ + 2653.00, + 2456.00 + ], + [ + 2665.00, + 2466.00 + ], + [ + 2665.00, + 2466.00 + ], + [ + 2665.00, + 2466.00 + ], + [ + 2679.00, + 2478.00 + ], + [ + 2679.00, + 2478.00 + ], + [ + 2679.00, + 2478.00 + ], + [ + 2696.00, + 2491.00 + ], + [ + 2696.00, + 2491.00 + ], + [ + 2696.00, + 2491.00 + ], + [ + 2717.00, + 2503.00 + ], + [ + 2717.00, + 2503.00 + ], + [ + 2717.00, + 2503.00 + ], + [ + 2732.00, + 2509.00 + ], + [ + 2732.00, + 2509.00 + ], + [ + 2732.00, + 2509.00 + ], + [ + 2756.00, + 2518.00 + ], + [ + 2756.00, + 2518.00 + ], + [ + 2756.00, + 2518.00 + ], + [ + 2771.00, + 2523.00 + ], + [ + 2771.00, + 2523.00 + ], + [ + 2771.00, + 2523.00 + ], + [ + 2796.00, + 2534.00 + ], + [ + 2796.00, + 2534.00 + ], + [ + 2796.00, + 2534.00 + ], + [ + 2815.00, + 2541.00 + ], + [ + 2815.00, + 2541.00 + ], + [ + 2815.00, + 2541.00 + ], + [ + 2831.00, + 2548.00 + ], + [ + 2831.00, + 2548.00 + ], + [ + 2831.00, + 2548.00 + ], + [ + 2854.00, + 2555.00 + ], + [ + 2854.00, + 2555.00 + ], + [ + 2854.00, + 2555.00 + ], + [ + 2874.00, + 2560.00 + ], + [ + 2874.00, + 2560.00 + ], + [ + 2874.00, + 2560.00 + ], + [ + 2890.00, + 2561.00 + ], + [ + 2890.00, + 2561.00 + ], + [ + 2890.00, + 2561.00 + ], + [ + 2907.00, + 2567.00 + ], + [ + 2907.00, + 2567.00 + ], + [ + 2907.00, + 2567.00 + ], + [ + 2931.00, + 2575.00 + ], + [ + 2931.00, + 2575.00 + ], + [ + 2931.00, + 2575.00 + ], + [ + 2951.00, + 2583.00 + ], + [ + 2951.00, + 2583.00 + ], + [ + 2951.00, + 2583.00 + ], + [ + 2970.00, + 2591.00 + ], + [ + 2970.00, + 2591.00 + ], + [ + 2970.00, + 2591.00 + ], + [ + 3006.00, + 2600.00 + ], + [ + 3006.00, + 2600.00 + ], + [ + 3006.00, + 2600.00 + ], + [ + 3031.00, + 2603.00 + ], + [ + 3031.00, + 2603.00 + ], + [ + 3031.00, + 2603.00 + ], + [ + 3052.00, + 2607.00 + ], + [ + 3052.00, + 2607.00 + ], + [ + 3052.00, + 2607.00 + ], + [ + 3071.00, + 2611.00 + ], + [ + 3071.00, + 2611.00 + ], + [ + 3071.00, + 2611.00 + ], + [ + 3092.00, + 2616.00 + ], + [ + 3092.00, + 2616.00 + ], + [ + 3092.00, + 2616.00 + ], + [ + 3116.00, + 2621.00 + ], + [ + 3116.00, + 2621.00 + ], + [ + 3116.00, + 2621.00 + ], + [ + 3153.00, + 2626.00 + ], + [ + 3153.00, + 2626.00 + ], + [ + 3153.00, + 2626.00 + ], + [ + 3182.00, + 2630.00 + ], + [ + 3182.00, + 2630.00 + ], + [ + 3182.00, + 2630.00 + ], + [ + 3205.00, + 2630.00 + ], + [ + 3205.00, + 2630.00 + ], + [ + 3205.00, + 2630.00 + ], + [ + 3222.00, + 2635.00 + ], + [ + 3222.00, + 2635.00 + ], + [ + 3222.00, + 2635.00 + ], + [ + 3236.00, + 2643.00 + ], + [ + 3236.00, + 2643.00 + ], + [ + 3236.00, + 2643.00 + ], + [ + 3248.00, + 2655.00 + ], + [ + 3248.00, + 2655.00 + ], + [ + 3248.00, + 2655.00 + ], + [ + 3263.00, + 2675.00 + ], + [ + 3263.00, + 2675.00 + ], + [ + 3263.00, + 2675.00 + ], + [ + 3276.00, + 2695.00 + ], + [ + 3276.00, + 2695.00 + ], + [ + 3276.00, + 2695.00 + ], + [ + 3280.00, + 2706.00 + ], + [ + 3280.00, + 2706.00 + ], + [ + 3280.00, + 2706.00 + ], + [ + 3287.00, + 2709.00 + ], + [ + 3287.00, + 2709.00 + ], + [ + 3287.00, + 2709.00 + ], + [ + 3312.00, + 2704.00 + ], + [ + 3312.00, + 2704.00 + ], + [ + 3312.00, + 2704.00 + ], + [ + 3328.00, + 2704.00 + ], + [ + 3328.00, + 2704.00 + ], + [ + 3328.00, + 2704.00 + ], + [ + 3342.00, + 2703.00 + ], + [ + 3342.00, + 2703.00 + ], + [ + 3342.00, + 2703.00 + ], + [ + 3365.00, + 2704.00 + ], + [ + 3365.00, + 2704.00 + ], + [ + 3365.00, + 2704.00 + ], + [ + 3373.00, + 2702.00 + ], + [ + 3373.00, + 2702.00 + ], + [ + 3373.00, + 2702.00 + ], + [ + 3371.00, + 2684.00 + ], + [ + 3371.00, + 2684.00 + ], + [ + 3371.00, + 2684.00 + ], + [ + 3370.00, + 2667.00 + ], + [ + 3370.00, + 2667.00 + ], + [ + 3370.00, + 2667.00 + ], + [ + 3366.00, + 2651.00 + ], + [ + 3366.00, + 2651.00 + ], + [ + 3366.00, + 2651.00 + ], + [ + 3362.00, + 2635.00 + ], + [ + 3362.00, + 2635.00 + ], + [ + 3362.00, + 2635.00 + ], + [ + 3360.00, + 2618.00 + ], + [ + 3360.00, + 2618.00 + ], + [ + 3360.00, + 2618.00 + ], + [ + 3357.00, + 2602.00 + ], + [ + 3357.00, + 2602.00 + ], + [ + 3357.00, + 2602.00 + ], + [ + 3356.00, + 2585.00 + ], + [ + 3356.00, + 2585.00 + ], + [ + 3356.00, + 2585.00 + ], + [ + 3353.00, + 2569.00 + ], + [ + 3353.00, + 2569.00 + ], + [ + 3353.00, + 2569.00 + ], + [ + 3351.00, + 2558.00 + ], + [ + 3351.00, + 2558.00 + ], + [ + 3351.00, + 2558.00 + ], + [ + 3342.00, + 2540.00 + ], + [ + 3342.00, + 2540.00 + ], + [ + 3342.00, + 2540.00 + ], + [ + 3338.00, + 2525.00 + ], + [ + 3338.00, + 2525.00 + ], + [ + 3338.00, + 2525.00 + ], + [ + 3334.00, + 2513.00 + ], + [ + 3334.00, + 2513.00 + ], + [ + 3334.00, + 2513.00 + ], + [ + 3332.00, + 2496.00 + ], + [ + 3332.00, + 2496.00 + ], + [ + 3332.00, + 2496.00 + ], + [ + 3330.00, + 2484.00 + ], + [ + 3330.00, + 2484.00 + ], + [ + 3330.00, + 2484.00 + ], + [ + 3328.00, + 2467.00 + ], + [ + 3328.00, + 2467.00 + ], + [ + 3328.00, + 2467.00 + ], + [ + 3330.00, + 2451.00 + ], + [ + 3330.00, + 2451.00 + ], + [ + 3330.00, + 2451.00 + ], + [ + 3332.00, + 2434.00 + ], + [ + 3332.00, + 2434.00 + ], + [ + 3332.00, + 2434.00 + ], + [ + 3337.00, + 2419.00 + ], + [ + 3337.00, + 2419.00 + ], + [ + 3337.00, + 2419.00 + ], + [ + 3347.00, + 2401.00 + ], + [ + 3347.00, + 2401.00 + ], + [ + 3347.00, + 2401.00 + ], + [ + 3353.00, + 2389.00 + ], + [ + 3353.00, + 2389.00 + ], + [ + 3353.00, + 2389.00 + ], + [ + 3367.00, + 2378.00 + ], + [ + 3367.00, + 2378.00 + ], + [ + 3367.00, + 2378.00 + ], + [ + 3373.00, + 2373.00 + ], + [ + 3373.00, + 2373.00 + ] + ] + }, + { + "name": "Aragorn", + "id": "aragorn", + "color": "blue", + "distance": "2100 miles / 3400 km", + "startDate": "September 29, T.A. 3018", + "endDate": "March 25, T.A. 3019", + "path": [ + [ + 3374.00, + 2375.00 + ], + [ + 3374.00, + 2375.00 + ], + [ + 3361.00, + 2384.00 + ], + [ + 3361.00, + 2384.00 + ], + [ + 3361.00, + 2384.00 + ], + [ + 3343.00, + 2405.00 + ], + [ + 3343.00, + 2405.00 + ], + [ + 3343.00, + 2405.00 + ], + [ + 3332.00, + 2424.00 + ], + [ + 3332.00, + 2424.00 + ], + [ + 3332.00, + 2424.00 + ], + [ + 3328.00, + 2449.00 + ], + [ + 3328.00, + 2449.00 + ], + [ + 3328.00, + 2449.00 + ], + [ + 3330.00, + 2471.00 + ], + [ + 3330.00, + 2471.00 + ], + [ + 3330.00, + 2471.00 + ], + [ + 3330.00, + 2500.00 + ], + [ + 3330.00, + 2500.00 + ], + [ + 3330.00, + 2500.00 + ], + [ + 3341.00, + 2523.00 + ], + [ + 3341.00, + 2523.00 + ], + [ + 3341.00, + 2523.00 + ], + [ + 3348.00, + 2547.00 + ], + [ + 3348.00, + 2547.00 + ], + [ + 3348.00, + 2547.00 + ], + [ + 3354.00, + 2567.00 + ], + [ + 3354.00, + 2567.00 + ], + [ + 3354.00, + 2567.00 + ], + [ + 3360.00, + 2592.00 + ], + [ + 3360.00, + 2592.00 + ], + [ + 3360.00, + 2592.00 + ], + [ + 3361.00, + 2610.00 + ], + [ + 3361.00, + 2614.00 + ], + [ + 3361.00, + 2618.00 + ], + [ + 3361.00, + 2634.00 + ], + [ + 3361.00, + 2636.00 + ], + [ + 3361.00, + 2638.00 + ], + [ + 3367.00, + 2658.00 + ], + [ + 3367.00, + 2658.00 + ], + [ + 3367.00, + 2658.00 + ], + [ + 3375.00, + 2687.00 + ], + [ + 3375.00, + 2688.00 + ], + [ + 3375.00, + 2689.00 + ], + [ + 3365.00, + 2699.00 + ], + [ + 3365.00, + 2699.00 + ], + [ + 3365.00, + 2699.00 + ], + [ + 3342.00, + 2700.00 + ], + [ + 3342.00, + 2700.00 + ], + [ + 3342.00, + 2700.00 + ], + [ + 3316.00, + 2703.00 + ], + [ + 3316.00, + 2703.00 + ], + [ + 3316.00, + 2703.00 + ], + [ + 3297.00, + 2705.00 + ], + [ + 3297.00, + 2705.00 + ], + [ + 3297.00, + 2705.00 + ], + [ + 3284.00, + 2712.00 + ], + [ + 3284.00, + 2712.00 + ], + [ + 3284.00, + 2712.00 + ], + [ + 3290.00, + 2716.00 + ], + [ + 3297.00, + 2720.00 + ], + [ + 3304.00, + 2724.00 + ], + [ + 3304.00, + 2725.00 + ], + [ + 3308.00, + 2728.00 + ], + [ + 3312.00, + 2731.00 + ], + [ + 3307.00, + 2737.00 + ], + [ + 3306.00, + 2740.00 + ], + [ + 3305.00, + 2743.00 + ], + [ + 3293.00, + 2749.00 + ], + [ + 3289.00, + 2752.00 + ], + [ + 3285.00, + 2755.00 + ], + [ + 3277.00, + 2763.00 + ], + [ + 3274.00, + 2767.00 + ], + [ + 3271.00, + 2771.00 + ], + [ + 3272.00, + 2785.00 + ], + [ + 3272.00, + 2786.00 + ], + [ + 3272.00, + 2787.00 + ], + [ + 3275.00, + 2802.00 + ], + [ + 3278.00, + 2812.00 + ], + [ + 3281.00, + 2822.00 + ], + [ + 3285.00, + 2827.00 + ], + [ + 3286.00, + 2831.00 + ], + [ + 3287.00, + 2835.00 + ], + [ + 3291.00, + 2855.00 + ], + [ + 3291.00, + 2864.00 + ], + [ + 3291.00, + 2873.00 + ], + [ + 3289.00, + 2881.00 + ], + [ + 3288.00, + 2887.00 + ], + [ + 3287.00, + 2893.00 + ], + [ + 3284.00, + 2907.00 + ], + [ + 3282.00, + 2917.00 + ], + [ + 3280.00, + 2927.00 + ], + [ + 3273.00, + 2936.00 + ], + [ + 3271.00, + 2941.00 + ], + [ + 3269.00, + 2946.00 + ], + [ + 3261.00, + 2964.00 + ], + [ + 3259.00, + 2968.00 + ], + [ + 3257.00, + 2972.00 + ], + [ + 3247.00, + 2986.00 + ], + [ + 3239.00, + 2995.00 + ], + [ + 3231.00, + 3004.00 + ], + [ + 3227.00, + 3008.00 + ], + [ + 3224.00, + 3011.00 + ], + [ + 3221.00, + 3014.00 + ], + [ + 3206.00, + 3029.00 + ], + [ + 3206.00, + 3029.00 + ], + [ + 3206.00, + 3029.00 + ], + [ + 3179.00, + 3035.00 + ], + [ + 3179.00, + 3035.00 + ], + [ + 3179.00, + 3035.00 + ], + [ + 3156.00, + 3032.00 + ], + [ + 3154.00, + 3032.00 + ], + [ + 3152.00, + 3032.00 + ], + [ + 3148.00, + 3032.00 + ], + [ + 3148.00, + 3032.00 + ], + [ + 3148.00, + 3032.00 + ], + [ + 3134.00, + 3031.00 + ], + [ + 3130.00, + 3031.00 + ], + [ + 3126.00, + 3031.00 + ], + [ + 3119.00, + 3035.00 + ], + [ + 3116.00, + 3036.00 + ], + [ + 3113.00, + 3037.00 + ], + [ + 3099.00, + 3039.00 + ], + [ + 3093.00, + 3040.00 + ], + [ + 3087.00, + 3041.00 + ], + [ + 3084.00, + 3041.00 + ], + [ + 3082.00, + 3041.00 + ], + [ + 3080.00, + 3041.00 + ], + [ + 3072.00, + 3045.00 + ], + [ + 3066.00, + 3047.00 + ], + [ + 3060.00, + 3049.00 + ], + [ + 3051.00, + 3051.00 + ], + [ + 3050.00, + 3051.00 + ], + [ + 3049.00, + 3051.00 + ], + [ + 3020.00, + 3064.00 + ], + [ + 3019.00, + 3064.00 + ], + [ + 3018.00, + 3064.00 + ], + [ + 2989.00, + 3066.00 + ], + [ + 2985.00, + 3066.00 + ], + [ + 2981.00, + 3066.00 + ], + [ + 2957.00, + 3064.00 + ], + [ + 2957.00, + 3064.00 + ], + [ + 2957.00, + 3064.00 + ], + [ + 2937.00, + 3058.00 + ], + [ + 2934.00, + 3057.00 + ], + [ + 2931.00, + 3056.00 + ], + [ + 2911.00, + 3043.00 + ], + [ + 2911.00, + 3043.00 + ], + [ + 2911.00, + 3043.00 + ], + [ + 2896.00, + 3027.00 + ], + [ + 2896.00, + 3026.00 + ], + [ + 2896.00, + 3025.00 + ], + [ + 2887.00, + 3012.00 + ], + [ + 2887.00, + 3012.00 + ], + [ + 2887.00, + 3012.00 + ], + [ + 2884.00, + 2992.00 + ], + [ + 2884.00, + 2992.00 + ], + [ + 2884.00, + 2992.00 + ], + [ + 2880.00, + 2979.00 + ], + [ + 2880.00, + 2979.00 + ], + [ + 2880.00, + 2979.00 + ], + [ + 2879.00, + 2964.00 + ], + [ + 2879.00, + 2964.00 + ], + [ + 2879.00, + 2964.00 + ], + [ + 2879.00, + 2950.00 + ], + [ + 2879.00, + 2947.00 + ], + [ + 2879.00, + 2944.00 + ], + [ + 2875.00, + 2934.00 + ], + [ + 2875.00, + 2934.00 + ], + [ + 2875.00, + 2934.00 + ], + [ + 2873.00, + 2917.00 + ], + [ + 2873.00, + 2912.00 + ], + [ + 2873.00, + 2907.00 + ], + [ + 2872.00, + 2904.00 + ], + [ + 2872.00, + 2903.00 + ], + [ + 2872.00, + 2902.00 + ], + [ + 2871.00, + 2887.00 + ], + [ + 2871.00, + 2887.00 + ], + [ + 2871.00, + 2887.00 + ], + [ + 2867.00, + 2862.00 + ], + [ + 2867.00, + 2860.00 + ], + [ + 2867.00, + 2858.00 + ], + [ + 2864.00, + 2840.00 + ], + [ + 2862.00, + 2835.00 + ], + [ + 2860.00, + 2830.00 + ], + [ + 2858.00, + 2828.00 + ], + [ + 2857.00, + 2828.00 + ], + [ + 2856.00, + 2828.00 + ], + [ + 2839.00, + 2809.00 + ], + [ + 2838.00, + 2809.00 + ], + [ + 2837.00, + 2809.00 + ], + [ + 2822.00, + 2802.00 + ], + [ + 2818.00, + 2798.00 + ], + [ + 2814.00, + 2794.00 + ], + [ + 2810.00, + 2790.00 + ], + [ + 2810.00, + 2790.00 + ], + [ + 2810.00, + 2790.00 + ], + [ + 2795.00, + 2784.00 + ], + [ + 2792.00, + 2783.00 + ], + [ + 2789.00, + 2782.00 + ], + [ + 2784.00, + 2776.00 + ], + [ + 2784.00, + 2776.00 + ], + [ + 2784.00, + 2776.00 + ], + [ + 2773.00, + 2771.00 + ], + [ + 2765.00, + 2767.00 + ], + [ + 2757.00, + 2763.00 + ], + [ + 2759.00, + 2764.00 + ], + [ + 2758.00, + 2763.00 + ], + [ + 2757.00, + 2762.00 + ], + [ + 2739.00, + 2757.00 + ], + [ + 2738.00, + 2757.00 + ], + [ + 2737.00, + 2757.00 + ], + [ + 2726.00, + 2755.00 + ], + [ + 2724.00, + 2754.00 + ], + [ + 2722.00, + 2753.00 + ], + [ + 2704.00, + 2751.00 + ], + [ + 2702.00, + 2751.00 + ], + [ + 2700.00, + 2751.00 + ], + [ + 2693.00, + 2744.00 + ], + [ + 2690.00, + 2742.00 + ], + [ + 2687.00, + 2740.00 + ], + [ + 2688.00, + 2731.00 + ], + [ + 2687.00, + 2729.00 + ], + [ + 2686.00, + 2727.00 + ], + [ + 2687.00, + 2713.00 + ], + [ + 2687.00, + 2713.00 + ], + [ + 2687.00, + 2713.00 + ], + [ + 2685.00, + 2693.00 + ], + [ + 2685.00, + 2694.00 + ], + [ + 2685.00, + 2695.00 + ], + [ + 2679.00, + 2680.00 + ], + [ + 2678.00, + 2676.00 + ], + [ + 2677.00, + 2672.00 + ], + [ + 2673.00, + 2663.00 + ], + [ + 2673.00, + 2662.00 + ], + [ + 2673.00, + 2661.00 + ], + [ + 2671.00, + 2648.00 + ], + [ + 2671.00, + 2646.00 + ], + [ + 2671.00, + 2644.00 + ], + [ + 2663.00, + 2630.00 + ], + [ + 2663.00, + 2630.00 + ], + [ + 2663.00, + 2630.00 + ], + [ + 2646.00, + 2616.00 + ], + [ + 2634.00, + 2606.00 + ], + [ + 2629.00, + 2600.00 + ], + [ + 2617.00, + 2600.00 + ], + [ + 2617.00, + 2600.00 + ], + [ + 2617.00, + 2600.00 + ], + [ + 2578.00, + 2590.00 + ], + [ + 2577.00, + 2590.00 + ], + [ + 2576.00, + 2590.00 + ], + [ + 2569.00, + 2580.00 + ], + [ + 2569.00, + 2579.00 + ], + [ + 2569.00, + 2578.00 + ], + [ + 2556.00, + 2570.00 + ], + [ + 2553.00, + 2566.00 + ], + [ + 2550.00, + 2562.00 + ], + [ + 2548.00, + 2559.00 + ], + [ + 2547.00, + 2557.00 + ], + [ + 2546.00, + 2555.00 + ], + [ + 2537.00, + 2542.00 + ], + [ + 2536.00, + 2539.00 + ], + [ + 2535.00, + 2536.00 + ], + [ + 2527.00, + 2519.00 + ], + [ + 2526.00, + 2515.00 + ], + [ + 2525.00, + 2511.00 + ], + [ + 2520.00, + 2506.00 + ], + [ + 2520.00, + 2505.00 + ], + [ + 2520.00, + 2504.00 + ], + [ + 2512.00, + 2492.00 + ], + [ + 2512.00, + 2492.00 + ], + [ + 2512.00, + 2492.00 + ], + [ + 2504.00, + 2477.00 + ], + [ + 2504.00, + 2477.00 + ], + [ + 2504.00, + 2477.00 + ], + [ + 2498.00, + 2464.00 + ], + [ + 2498.00, + 2459.00 + ], + [ + 2498.00, + 2454.00 + ], + [ + 2496.00, + 2440.00 + ], + [ + 2496.00, + 2439.00 + ], + [ + 2496.00, + 2438.00 + ], + [ + 2493.00, + 2418.00 + ], + [ + 2493.00, + 2418.00 + ], + [ + 2493.00, + 2418.00 + ], + [ + 2494.00, + 2397.00 + ], + [ + 2492.00, + 2396.00 + ], + [ + 2490.00, + 2395.00 + ], + [ + 2481.00, + 2393.00 + ], + [ + 2477.00, + 2391.00 + ], + [ + 2473.00, + 2389.00 + ], + [ + 2465.00, + 2386.00 + ], + [ + 2464.00, + 2384.00 + ], + [ + 2463.00, + 2382.00 + ], + [ + 2453.00, + 2376.00 + ], + [ + 2450.00, + 2374.00 + ], + [ + 2447.00, + 2372.00 + ], + [ + 2437.00, + 2361.00 + ], + [ + 2435.00, + 2359.00 + ], + [ + 2433.00, + 2357.00 + ], + [ + 2423.00, + 2343.00 + ], + [ + 2417.00, + 2335.00 + ], + [ + 2415.00, + 2332.00 + ], + [ + 2415.00, + 2328.00 + ], + [ + 2414.00, + 2323.00 + ], + [ + 2413.00, + 2318.00 + ], + [ + 2411.00, + 2314.00 + ], + [ + 2410.00, + 2309.00 + ], + [ + 2409.00, + 2304.00 + ], + [ + 2407.00, + 2293.00 + ], + [ + 2407.00, + 2291.00 + ], + [ + 2407.00, + 2289.00 + ], + [ + 2400.00, + 2281.00 + ], + [ + 2399.00, + 2280.00 + ], + [ + 2398.00, + 2279.00 + ], + [ + 2390.00, + 2277.00 + ], + [ + 2390.00, + 2276.00 + ], + [ + 2390.00, + 2275.00 + ], + [ + 2379.00, + 2274.00 + ], + [ + 2377.00, + 2273.00 + ], + [ + 2375.00, + 2272.00 + ], + [ + 2361.00, + 2265.00 + ], + [ + 2360.00, + 2265.00 + ], + [ + 2359.00, + 2265.00 + ], + [ + 2347.00, + 2261.00 + ], + [ + 2347.00, + 2261.00 + ], + [ + 2347.00, + 2261.00 + ], + [ + 2329.00, + 2250.00 + ], + [ + 2329.00, + 2250.00 + ], + [ + 2329.00, + 2250.00 + ], + [ + 2321.00, + 2242.00 + ], + [ + 2320.00, + 2240.00 + ], + [ + 2319.00, + 2238.00 + ], + [ + 2314.00, + 2224.00 + ], + [ + 2314.00, + 2224.00 + ], + [ + 2314.00, + 2224.00 + ], + [ + 2312.00, + 2201.00 + ], + [ + 2312.00, + 2196.00 + ], + [ + 2312.00, + 2191.00 + ], + [ + 2316.00, + 2184.00 + ], + [ + 2316.00, + 2184.00 + ], + [ + 2316.00, + 2184.00 + ], + [ + 2321.00, + 2180.00 + ], + [ + 2323.00, + 2173.00 + ], + [ + 2323.00, + 2172.00 + ], + [ + 2327.00, + 2164.00 + ], + [ + 2327.00, + 2164.00 + ], + [ + 2327.00, + 2164.00 + ], + [ + 2332.00, + 2150.00 + ], + [ + 2332.00, + 2148.00 + ], + [ + 2332.00, + 2146.00 + ], + [ + 2332.00, + 2137.00 + ], + [ + 2332.00, + 2137.00 + ], + [ + 2332.00, + 2137.00 + ], + [ + 2336.00, + 2123.00 + ], + [ + 2336.00, + 2123.00 + ], + [ + 2336.00, + 2123.00 + ], + [ + 2341.00, + 2135.00 + ], + [ + 2341.00, + 2135.00 + ], + [ + 2341.00, + 2135.00 + ], + [ + 2342.00, + 2142.00 + ], + [ + 2342.00, + 2142.00 + ], + [ + 2342.00, + 2142.00 + ], + [ + 2343.00, + 2151.00 + ], + [ + 2344.00, + 2152.00 + ], + [ + 2345.00, + 2153.00 + ], + [ + 2346.00, + 2161.00 + ], + [ + 2346.00, + 2161.00 + ], + [ + 2346.00, + 2161.00 + ], + [ + 2346.00, + 2179.00 + ], + [ + 2346.00, + 2180.00 + ], + [ + 2346.00, + 2181.00 + ], + [ + 2346.00, + 2189.00 + ], + [ + 2346.00, + 2193.00 + ], + [ + 2346.00, + 2197.00 + ], + [ + 2345.00, + 2201.00 + ], + [ + 2345.00, + 2202.00 + ], + [ + 2345.00, + 2203.00 + ], + [ + 2340.00, + 2213.00 + ], + [ + 2340.00, + 2215.00 + ], + [ + 2340.00, + 2217.00 + ], + [ + 2342.00, + 2225.00 + ], + [ + 2342.00, + 2227.00 + ], + [ + 2342.00, + 2229.00 + ], + [ + 2344.00, + 2240.00 + ], + [ + 2345.00, + 2243.00 + ], + [ + 2346.00, + 2246.00 + ], + [ + 2346.00, + 2249.00 + ], + [ + 2346.00, + 2249.00 + ], + [ + 2346.00, + 2249.00 + ], + [ + 2359.00, + 2254.00 + ], + [ + 2360.00, + 2254.00 + ], + [ + 2361.00, + 2254.00 + ], + [ + 2377.00, + 2255.00 + ], + [ + 2378.00, + 2255.00 + ], + [ + 2379.00, + 2255.00 + ], + [ + 2393.00, + 2260.00 + ], + [ + 2393.00, + 2260.00 + ], + [ + 2393.00, + 2260.00 + ], + [ + 2404.00, + 2264.00 + ], + [ + 2404.00, + 2264.00 + ], + [ + 2404.00, + 2264.00 + ], + [ + 2415.00, + 2270.00 + ], + [ + 2415.00, + 2270.00 + ], + [ + 2415.00, + 2270.00 + ], + [ + 2414.00, + 2280.00 + ], + [ + 2414.00, + 2281.00 + ], + [ + 2414.00, + 2282.00 + ], + [ + 2414.00, + 2291.00 + ], + [ + 2414.00, + 2291.00 + ], + [ + 2414.00, + 2291.00 + ], + [ + 2428.00, + 2328.00 + ], + [ + 2427.00, + 2328.00 + ], + [ + 2426.00, + 2328.00 + ], + [ + 2425.00, + 2321.00 + ], + [ + 2425.00, + 2320.00 + ], + [ + 2420.99, + 2317.60 + ], + [ + 2417.59, + 2305.71 + ], + [ + 2427.00, + 2308.00 + ], + [ + 2427.00, + 2307.73 + ], + [ + 2427.15, + 2306.86 + ], + [ + 2427.36, + 2305.73 + ], + [ + 2427.94, + 2302.69 + ], + [ + 2429.00, + 2297.73 + ], + [ + 2429.00, + 2297.00 + ], + [ + 2429.00, + 2296.00 + ], + [ + 2435.00, + 2285.00 + ], + [ + 2436.00, + 2284.00 + ], + [ + 2437.00, + 2283.00 + ], + [ + 2451.00, + 2280.00 + ], + [ + 2451.00, + 2280.00 + ], + [ + 2451.00, + 2280.00 + ], + [ + 2464.00, + 2287.00 + ], + [ + 2465.00, + 2287.00 + ], + [ + 2466.00, + 2287.00 + ], + [ + 2479.00, + 2294.00 + ], + [ + 2480.00, + 2294.00 + ], + [ + 2481.00, + 2294.00 + ], + [ + 2500.00, + 2305.00 + ], + [ + 2500.00, + 2305.00 + ], + [ + 2500.00, + 2305.00 + ], + [ + 2518.00, + 2317.00 + ], + [ + 2518.00, + 2317.00 + ], + [ + 2518.00, + 2317.00 + ], + [ + 2533.00, + 2327.00 + ], + [ + 2533.00, + 2327.00 + ], + [ + 2533.00, + 2327.00 + ], + [ + 2545.00, + 2330.00 + ], + [ + 2545.00, + 2330.00 + ], + [ + 2545.00, + 2330.00 + ], + [ + 2555.00, + 2342.00 + ], + [ + 2555.00, + 2342.00 + ], + [ + 2555.00, + 2342.00 + ], + [ + 2565.00, + 2351.00 + ], + [ + 2565.00, + 2352.00 + ], + [ + 2565.00, + 2353.00 + ], + [ + 2576.00, + 2361.00 + ], + [ + 2576.00, + 2361.00 + ], + [ + 2576.00, + 2361.00 + ], + [ + 2590.00, + 2390.00 + ], + [ + 2590.00, + 2390.00 + ], + [ + 2590.00, + 2390.00 + ], + [ + 2596.00, + 2369.00 + ], + [ + 2597.00, + 2357.00 + ], + [ + 2598.00, + 2345.00 + ], + [ + 2599.00, + 2344.00 + ], + [ + 2600.00, + 2334.00 + ], + [ + 2601.00, + 2324.00 + ], + [ + 2600.00, + 2321.00 + ], + [ + 2600.00, + 2311.00 + ], + [ + 2600.00, + 2301.00 + ], + [ + 2600.00, + 2304.00 + ], + [ + 2600.00, + 2298.00 + ], + [ + 2600.00, + 2292.00 + ], + [ + 2600.00, + 2283.00 + ], + [ + 2600.00, + 2272.00 + ], + [ + 2600.00, + 2261.00 + ], + [ + 2600.00, + 2266.00 + ], + [ + 2600.00, + 2256.00 + ], + [ + 2600.00, + 2246.00 + ], + [ + 2600.00, + 2249.00 + ], + [ + 2601.00, + 2234.00 + ], + [ + 2602.00, + 2219.00 + ], + [ + 2602.00, + 2226.00 + ], + [ + 2602.00, + 2197.00 + ], + [ + 2602.00, + 2182.00 + ], + [ + 2601.00, + 2190.00 + ], + [ + 2599.00, + 2178.00 + ], + [ + 2597.00, + 2166.00 + ], + [ + 2599.00, + 2170.00 + ], + [ + 2602.00, + 2162.00 + ], + [ + 2605.00, + 2154.00 + ], + [ + 2606.00, + 2154.00 + ], + [ + 2608.00, + 2147.00 + ], + [ + 2610.00, + 2140.00 + ], + [ + 2611.00, + 2137.00 + ], + [ + 2614.00, + 2132.00 + ], + [ + 2617.00, + 2127.00 + ], + [ + 2617.00, + 2126.00 + ], + [ + 2617.00, + 2123.00 + ], + [ + 2617.00, + 2120.00 + ], + [ + 2618.00, + 2115.00 + ], + [ + 2619.00, + 2111.00 + ], + [ + 2620.00, + 2107.00 + ], + [ + 2622.00, + 2105.00 + ], + [ + 2623.00, + 2103.00 + ], + [ + 2624.00, + 2101.00 + ], + [ + 2625.00, + 2091.00 + ], + [ + 2625.00, + 2089.00 + ], + [ + 2625.00, + 2087.00 + ], + [ + 2642.00, + 2087.00 + ], + [ + 2650.00, + 2088.00 + ], + [ + 2652.00, + 2088.00 + ], + [ + 2657.00, + 2090.00 + ], + [ + 2658.00, + 2091.00 + ], + [ + 2659.00, + 2092.00 + ], + [ + 2666.00, + 2092.00 + ], + [ + 2667.00, + 2093.00 + ], + [ + 2668.00, + 2094.00 + ], + [ + 2684.00, + 2097.00 + ], + [ + 2686.00, + 2098.00 + ], + [ + 2688.00, + 2099.00 + ], + [ + 2695.00, + 2103.00 + ], + [ + 2696.00, + 2104.00 + ], + [ + 2697.00, + 2105.00 + ], + [ + 2708.00, + 2112.00 + ], + [ + 2708.00, + 2112.00 + ], + [ + 2708.00, + 2112.00 + ], + [ + 2730.00, + 2128.00 + ], + [ + 2730.00, + 2128.00 + ], + [ + 2730.00, + 2128.00 + ], + [ + 2745.00, + 2140.00 + ], + [ + 2745.00, + 2140.00 + ], + [ + 2745.00, + 2140.00 + ], + [ + 2761.00, + 2150.00 + ], + [ + 2761.00, + 2150.00 + ], + [ + 2761.00, + 2150.00 + ], + [ + 2791.00, + 2168.00 + ], + [ + 2791.00, + 2168.00 + ], + [ + 2791.00, + 2168.00 + ], + [ + 2808.00, + 2194.00 + ], + [ + 2808.00, + 2194.00 + ], + [ + 2808.00, + 2194.00 + ], + [ + 2823.00, + 2214.00 + ], + [ + 2823.00, + 2214.00 + ], + [ + 2823.00, + 2214.00 + ], + [ + 2850.00, + 2233.00 + ], + [ + 2850.00, + 2233.00 + ], + [ + 2850.00, + 2233.00 + ], + [ + 2865.00, + 2250.00 + ], + [ + 2865.00, + 2250.00 + ], + [ + 2865.00, + 2250.00 + ], + [ + 2878.00, + 2269.00 + ], + [ + 2878.00, + 2269.00 + ], + [ + 2878.00, + 2269.00 + ], + [ + 2890.00, + 2286.00 + ], + [ + 2891.00, + 2287.00 + ], + [ + 2892.00, + 2288.00 + ], + [ + 2904.00, + 2291.00 + ], + [ + 2904.00, + 2291.00 + ], + [ + 2904.00, + 2291.00 + ], + [ + 2921.00, + 2293.00 + ], + [ + 2924.00, + 2293.00 + ], + [ + 2927.00, + 2293.00 + ], + [ + 2949.00, + 2300.00 + ], + [ + 2952.00, + 2301.00 + ], + [ + 2955.00, + 2302.00 + ], + [ + 2970.00, + 2308.00 + ], + [ + 2970.00, + 2308.00 + ], + [ + 2970.00, + 2308.00 + ], + [ + 2986.00, + 2325.00 + ], + [ + 2989.00, + 2328.00 + ], + [ + 2992.00, + 2331.00 + ], + [ + 3009.00, + 2340.00 + ], + [ + 3009.00, + 2340.00 + ], + [ + 3009.00, + 2340.00 + ], + [ + 3016.00, + 2355.00 + ], + [ + 3016.00, + 2355.00 + ], + [ + 3016.00, + 2355.00 + ], + [ + 3031.00, + 2368.00 + ], + [ + 3031.00, + 2368.00 + ], + [ + 3031.00, + 2368.00 + ], + [ + 3049.00, + 2383.00 + ], + [ + 3049.00, + 2383.00 + ], + [ + 3049.00, + 2383.00 + ], + [ + 3053.00, + 2367.00 + ], + [ + 3053.00, + 2366.00 + ], + [ + 3053.00, + 2365.00 + ], + [ + 3045.00, + 2348.00 + ], + [ + 3045.00, + 2345.00 + ], + [ + 3045.00, + 2342.00 + ], + [ + 3042.00, + 2320.00 + ], + [ + 3042.00, + 2320.00 + ], + [ + 3042.00, + 2320.00 + ], + [ + 3039.00, + 2302.00 + ], + [ + 3039.00, + 2302.00 + ], + [ + 3039.00, + 2302.00 + ], + [ + 3037.00, + 2288.00 + ], + [ + 3035.00, + 2285.00 + ], + [ + 3033.00, + 2282.00 + ], + [ + 3031.00, + 2270.00 + ], + [ + 3031.00, + 2268.00 + ], + [ + 3031.00, + 2266.00 + ], + [ + 3028.00, + 2252.00 + ], + [ + 3028.00, + 2249.00 + ], + [ + 3028.00, + 2246.00 + ], + [ + 3024.00, + 2231.00 + ], + [ + 3024.00, + 2228.00 + ], + [ + 3024.00, + 2225.00 + ], + [ + 3028.00, + 2215.00 + ], + [ + 3029.00, + 2211.00 + ], + [ + 3030.00, + 2207.00 + ], + [ + 3032.00, + 2193.00 + ], + [ + 3033.00, + 2190.00 + ], + [ + 3034.00, + 2187.00 + ], + [ + 3039.00, + 2177.00 + ], + [ + 3039.00, + 2176.00 + ], + [ + 3039.00, + 2175.00 + ], + [ + 3043.00, + 2163.00 + ], + [ + 3043.00, + 2163.00 + ], + [ + 3043.00, + 2163.00 + ], + [ + 3046.00, + 2150.00 + ], + [ + 3046.00, + 2149.00 + ], + [ + 3046.00, + 2148.00 + ], + [ + 3051.00, + 2131.00 + ], + [ + 3052.00, + 2130.00 + ], + [ + 3053.00, + 2129.00 + ], + [ + 3054.00, + 2114.00 + ], + [ + 3054.00, + 2113.00 + ], + [ + 3054.00, + 2112.00 + ], + [ + 3054.00, + 2099.00 + ], + [ + 3054.00, + 2099.00 + ], + [ + 3054.00, + 2099.00 + ], + [ + 3043.00, + 2089.00 + ], + [ + 3043.00, + 2089.00 + ], + [ + 3043.00, + 2089.00 + ], + [ + 3025.00, + 2080.00 + ], + [ + 3025.00, + 2080.00 + ], + [ + 3025.00, + 2080.00 + ], + [ + 3006.00, + 2071.00 + ], + [ + 3005.00, + 2070.00 + ], + [ + 3004.00, + 2069.00 + ], + [ + 3003.00, + 2055.00 + ], + [ + 3003.00, + 2051.00 + ], + [ + 3003.00, + 2047.00 + ], + [ + 3009.00, + 2039.00 + ], + [ + 3010.00, + 2038.00 + ], + [ + 3011.00, + 2037.00 + ], + [ + 3020.00, + 2021.00 + ], + [ + 3021.00, + 2020.00 + ], + [ + 3022.00, + 2019.00 + ], + [ + 3031.00, + 2002.00 + ], + [ + 3031.00, + 2000.00 + ], + [ + 3031.00, + 1998.00 + ], + [ + 3027.00, + 1985.00 + ], + [ + 3027.00, + 1984.00 + ], + [ + 3027.00, + 1983.00 + ], + [ + 3007.00, + 1975.00 + ], + [ + 3007.00, + 1975.00 + ], + [ + 3007.00, + 1975.00 + ], + [ + 2990.00, + 1982.00 + ], + [ + 2990.00, + 1982.00 + ], + [ + 2990.00, + 1982.00 + ], + [ + 2973.00, + 1984.00 + ], + [ + 2973.00, + 1984.00 + ], + [ + 2973.00, + 1984.00 + ], + [ + 2966.00, + 1984.00 + ], + [ + 2966.00, + 1984.00 + ], + [ + 2966.00, + 1984.00 + ], + [ + 2960.00, + 1983.00 + ], + [ + 2954.00, + 1979.00 + ], + [ + 2948.00, + 1975.00 + ], + [ + 2946.00, + 1970.00 + ], + [ + 2945.00, + 1968.00 + ], + [ + 2944.00, + 1966.00 + ], + [ + 2938.00, + 1956.00 + ], + [ + 2938.00, + 1956.00 + ], + [ + 2938.00, + 1956.00 + ], + [ + 2941.00, + 1935.00 + ], + [ + 2941.00, + 1933.00 + ], + [ + 2941.00, + 1931.00 + ], + [ + 2948.00, + 1923.00 + ], + [ + 2949.00, + 1921.00 + ], + [ + 2950.00, + 1919.00 + ], + [ + 2955.00, + 1910.00 + ], + [ + 2955.00, + 1909.00 + ], + [ + 2955.00, + 1908.00 + ], + [ + 2956.00, + 1901.00 + ], + [ + 2956.00, + 1900.00 + ], + [ + 2956.00, + 1899.00 + ], + [ + 2947.00, + 1893.00 + ], + [ + 2943.00, + 1889.00 + ], + [ + 2939.00, + 1885.00 + ], + [ + 2934.00, + 1887.00 + ], + [ + 2934.00, + 1887.00 + ], + [ + 2934.00, + 1887.00 + ], + [ + 2913.00, + 1880.00 + ], + [ + 2910.00, + 1879.00 + ], + [ + 2907.00, + 1878.00 + ], + [ + 2901.00, + 1874.00 + ], + [ + 2900.00, + 1873.00 + ], + [ + 2899.00, + 1872.00 + ], + [ + 2886.00, + 1861.00 + ], + [ + 2881.00, + 1857.00 + ], + [ + 2876.00, + 1853.00 + ], + [ + 2868.00, + 1848.00 + ], + [ + 2866.00, + 1846.00 + ], + [ + 2864.00, + 1844.00 + ], + [ + 2851.00, + 1836.00 + ], + [ + 2850.00, + 1836.00 + ], + [ + 2849.00, + 1836.00 + ], + [ + 2838.00, + 1828.00 + ], + [ + 2837.00, + 1827.00 + ], + [ + 2836.00, + 1826.00 + ], + [ + 2821.00, + 1820.00 + ], + [ + 2818.00, + 1819.00 + ], + [ + 2815.00, + 1818.00 + ], + [ + 2803.00, + 1811.00 + ], + [ + 2801.00, + 1810.00 + ], + [ + 2799.00, + 1809.00 + ], + [ + 2794.00, + 1802.00 + ], + [ + 2794.00, + 1802.00 + ], + [ + 2794.00, + 1802.00 + ], + [ + 2791.00, + 1787.00 + ], + [ + 2791.00, + 1785.00 + ], + [ + 2791.00, + 1783.00 + ], + [ + 2790.00, + 1772.00 + ], + [ + 2784.00, + 1766.00 + ], + [ + 2778.00, + 1760.00 + ], + [ + 2787.00, + 1746.00 + ], + [ + 2787.00, + 1745.00 + ], + [ + 2787.00, + 1744.00 + ], + [ + 2770.00, + 1731.00 + ], + [ + 2770.00, + 1731.00 + ], + [ + 2770.00, + 1731.00 + ], + [ + 2744.00, + 1728.00 + ], + [ + 2744.00, + 1727.00 + ], + [ + 2744.00, + 1726.00 + ], + [ + 2720.00, + 1722.00 + ], + [ + 2720.00, + 1722.00 + ], + [ + 2720.00, + 1722.00 + ], + [ + 2696.00, + 1712.00 + ], + [ + 2696.00, + 1712.00 + ], + [ + 2696.00, + 1712.00 + ], + [ + 2673.00, + 1699.00 + ], + [ + 2673.00, + 1699.00 + ], + [ + 2673.00, + 1699.00 + ], + [ + 2660.00, + 1693.00 + ], + [ + 2659.00, + 1693.00 + ], + [ + 2658.00, + 1693.00 + ], + [ + 2646.00, + 1685.00 + ], + [ + 2645.00, + 1684.00 + ], + [ + 2644.00, + 1683.00 + ], + [ + 2636.00, + 1679.00 + ], + [ + 2634.00, + 1677.00 + ], + [ + 2632.00, + 1675.00 + ], + [ + 2624.00, + 1667.00 + ], + [ + 2624.00, + 1666.00 + ], + [ + 2624.00, + 1665.00 + ], + [ + 2613.00, + 1653.00 + ], + [ + 2612.00, + 1652.00 + ], + [ + 2611.00, + 1651.00 + ], + [ + 2598.00, + 1647.00 + ], + [ + 2598.00, + 1646.00 + ], + [ + 2598.00, + 1645.00 + ], + [ + 2601.00, + 1627.00 + ], + [ + 2601.00, + 1627.00 + ], + [ + 2601.00, + 1627.00 + ], + [ + 2593.00, + 1610.00 + ], + [ + 2593.00, + 1610.00 + ], + [ + 2593.00, + 1610.00 + ], + [ + 2576.00, + 1603.00 + ], + [ + 2576.00, + 1603.00 + ], + [ + 2576.00, + 1603.00 + ], + [ + 2565.00, + 1593.00 + ], + [ + 2565.00, + 1593.00 + ], + [ + 2565.00, + 1593.00 + ], + [ + 2558.00, + 1573.00 + ], + [ + 2558.00, + 1573.00 + ], + [ + 2558.00, + 1573.00 + ], + [ + 2551.00, + 1556.00 + ], + [ + 2551.00, + 1556.00 + ], + [ + 2551.00, + 1556.00 + ], + [ + 2538.00, + 1545.00 + ], + [ + 2537.00, + 1544.00 + ], + [ + 2536.00, + 1543.00 + ], + [ + 2526.00, + 1537.00 + ], + [ + 2526.00, + 1537.00 + ], + [ + 2526.00, + 1537.00 + ], + [ + 2514.00, + 1534.00 + ], + [ + 2513.00, + 1534.00 + ], + [ + 2512.00, + 1534.00 + ], + [ + 2503.00, + 1535.00 + ], + [ + 2503.00, + 1535.00 + ], + [ + 2503.00, + 1535.00 + ], + [ + 2477.00, + 1533.00 + ], + [ + 2473.00, + 1533.00 + ], + [ + 2469.00, + 1533.00 + ], + [ + 2457.00, + 1537.00 + ], + [ + 2457.00, + 1537.00 + ], + [ + 2457.00, + 1537.00 + ], + [ + 2439.00, + 1538.00 + ], + [ + 2439.00, + 1538.00 + ], + [ + 2439.00, + 1538.00 + ], + [ + 2428.00, + 1540.00 + ], + [ + 2427.00, + 1540.00 + ], + [ + 2426.00, + 1540.00 + ], + [ + 2407.00, + 1537.00 + ], + [ + 2404.00, + 1535.00 + ], + [ + 2401.00, + 1533.00 + ], + [ + 2394.00, + 1529.00 + ], + [ + 2394.00, + 1526.00 + ], + [ + 2394.00, + 1523.00 + ], + [ + 2398.00, + 1509.00 + ], + [ + 2399.00, + 1507.00 + ], + [ + 2400.00, + 1505.00 + ], + [ + 2401.00, + 1496.00 + ], + [ + 2403.00, + 1488.00 + ], + [ + 2405.00, + 1480.00 + ], + [ + 2409.00, + 1474.00 + ], + [ + 2410.00, + 1471.00 + ], + [ + 2411.00, + 1468.00 + ], + [ + 2417.00, + 1457.00 + ], + [ + 2419.00, + 1453.00 + ], + [ + 2421.00, + 1449.00 + ], + [ + 2425.00, + 1439.00 + ], + [ + 2426.00, + 1438.00 + ], + [ + 2427.00, + 1437.00 + ], + [ + 2432.00, + 1423.00 + ], + [ + 2434.00, + 1419.00 + ], + [ + 2436.00, + 1415.00 + ], + [ + 2439.00, + 1408.00 + ], + [ + 2440.00, + 1406.00 + ], + [ + 2441.00, + 1404.00 + ], + [ + 2455.00, + 1402.00 + ], + [ + 2460.00, + 1401.00 + ], + [ + 2465.00, + 1400.00 + ], + [ + 2468.00, + 1400.00 + ], + [ + 2476.00, + 1402.00 + ], + [ + 2484.00, + 1404.00 + ], + [ + 2498.00, + 1404.00 + ], + [ + 2498.00, + 1404.00 + ], + [ + 2498.00, + 1404.00 + ], + [ + 2515.00, + 1404.00 + ], + [ + 2516.00, + 1403.00 + ], + [ + 2517.00, + 1402.00 + ], + [ + 2503.00, + 1398.00 + ], + [ + 2501.00, + 1398.00 + ], + [ + 2499.00, + 1398.00 + ], + [ + 2485.00, + 1393.00 + ], + [ + 2485.00, + 1393.00 + ], + [ + 2485.00, + 1393.00 + ], + [ + 2462.00, + 1387.00 + ], + [ + 2462.00, + 1387.00 + ], + [ + 2462.00, + 1387.00 + ], + [ + 2448.00, + 1376.00 + ], + [ + 2448.00, + 1376.00 + ], + [ + 2448.00, + 1376.00 + ], + [ + 2457.00, + 1367.00 + ], + [ + 2463.00, + 1355.00 + ], + [ + 2466.00, + 1348.00 + ], + [ + 2467.00, + 1348.00 + ], + [ + 2470.00, + 1340.00 + ], + [ + 2473.00, + 1332.00 + ], + [ + 2471.00, + 1331.00 + ], + [ + 2491.00, + 1287.00 + ], + [ + 2493.00, + 1281.00 + ], + [ + 2490.00, + 1275.00 + ], + [ + 2492.00, + 1273.00 + ], + [ + 2494.00, + 1271.00 + ], + [ + 2505.00, + 1241.00 + ], + [ + 2507.00, + 1237.00 + ], + [ + 2509.00, + 1233.00 + ], + [ + 2509.00, + 1231.00 + ], + [ + 2509.00, + 1231.00 + ], + [ + 2509.00, + 1231.00 + ], + [ + 2517.00, + 1210.00 + ], + [ + 2516.00, + 1206.00 + ], + [ + 2515.00, + 1202.00 + ], + [ + 2510.00, + 1189.00 + ], + [ + 2510.00, + 1189.00 + ], + [ + 2510.00, + 1189.00 + ], + [ + 2505.00, + 1178.00 + ], + [ + 2505.00, + 1178.00 + ], + [ + 2505.00, + 1178.00 + ], + [ + 2500.00, + 1163.00 + ], + [ + 2501.00, + 1162.00 + ], + [ + 2506, + 1147 + ], + [ + 2514, + 1132 + ], + [ + 2492, + 1151 + ], + [ + 2467, + 1153 + ], + [ + 2467, + 1153 + ], + [ + 2467, + 1153 + ], + [ + 2443, + 1152 + ], + [ + 2442, + 1152 + ], + [ + 2441, + 1152 + ], + [ + 2414, + 1150 + ], + [ + 2414, + 1150 + ], + [ + 2414, + 1150 + ], + [ + 2400, + 1147.33 + ], + [ + 2399.33, + 1147.33 + ], + [ + 2398.67, + 1147.33 + ], + [ + 2387.33, + 1145.33 + ], + [ + 2387.33, + 1145.33 + ], + [ + 2387.33, + 1145.33 + ], + [ + 2375.33, + 1144 + ], + [ + 2375.33, + 1144 + ], + [ + 2375.33, + 1144 + ], + [ + 2364, + 1142 + ], + [ + 2364, + 1142 + ], + [ + 2364, + 1142 + ], + [ + 2354.67, + 1140 + ], + [ + 2354.67, + 1140 + ], + [ + 2354.67, + 1140 + ], + [ + 2346, + 1125.33 + ], + [ + 2346, + 1125.33 + ], + [ + 2346, + 1125.33 + ], + [ + 2339.33, + 1116 + ], + [ + 2339.33, + 1115.33 + ], + [ + 2339.33, + 1114.67 + ], + [ + 2332, + 1102 + ], + [ + 2332, + 1102 + ], + [ + 2332, + 1102 + ], + [ + 2328.67, + 1090.67 + ], + [ + 2328.67, + 1090.67 + ], + [ + 2328.67, + 1090.67 + ], + [ + 2328.67, + 1075.33 + ], + [ + 2328.67, + 1075.33 + ], + [ + 2328.67, + 1075.33 + ], + [ + 2329.33, + 1062 + ], + [ + 2329.33, + 1062 + ], + [ + 2329.33, + 1062 + ], + [ + 2329.33, + 1046 + ], + [ + 2329.33, + 1046 + ], + [ + 2329.33, + 1046 + ], + [ + 2319.33, + 1060.67 + ], + [ + 2319.33, + 1060.67 + ], + [ + 2319.33, + 1060.67 + ], + [ + 2314, + 1074 + ], + [ + 2314, + 1074 + ], + [ + 2314, + 1074 + ], + [ + 2308.67, + 1084 + ], + [ + 2308.67, + 1084 + ], + [ + 2308.67, + 1084 + ], + [ + 2303.33, + 1094 + ], + [ + 2303.33, + 1094 + ], + [ + 2303.33, + 1094 + ], + [ + 2294, + 1110.67 + ], + [ + 2294, + 1110.67 + ], + [ + 2294, + 1110.67 + ], + [ + 2286.67, + 1119.33 + ], + [ + 2286.67, + 1119.33 + ], + [ + 2286.67, + 1119.33 + ], + [ + 2280.67, + 1128.67 + ], + [ + 2280, + 1128.67 + ], + [ + 2279.33, + 1128.67 + ], + [ + 2262, + 1142 + ], + [ + 2262, + 1142 + ], + [ + 2262, + 1142 + ], + [ + 2238.67, + 1158.67 + ], + [ + 2238.67, + 1158.67 + ], + [ + 2238.67, + 1158.67 + ], + [ + 2215.33, + 1173.33 + ], + [ + 2215.33, + 1174 + ], + [ + 2215.33, + 1174.67 + ], + [ + 2193.33, + 1192 + ], + [ + 2193.33, + 1192.67 + ], + [ + 2193.33, + 1193.33 + ], + [ + 2160.67, + 1202 + ], + [ + 2160.67, + 1202.67 + ], + [ + 2160.67, + 1203.33 + ], + [ + 2137.33, + 1210.67 + ], + [ + 2137.33, + 1210.67 + ], + [ + 2137.33, + 1210.67 + ], + [ + 2105.33, + 1213.33 + ], + [ + 2104, + 1214 + ], + [ + 2102.67, + 1214.67 + ], + [ + 2074, + 1218 + ], + [ + 2073.33, + 1217.33 + ], + [ + 2072.67, + 1216.67 + ], + [ + 2048, + 1214.67 + ], + [ + 2048, + 1214.67 + ], + [ + 2048, + 1214.67 + ], + [ + 2023.33, + 1218 + ], + [ + 2022.67, + 1218 + ], + [ + 2022, + 1218 + ], + [ + 1992.67, + 1211.33 + ], + [ + 1991.33, + 1210.67 + ], + [ + 1990, + 1210 + ], + [ + 1981.33, + 1195.33 + ], + [ + 1981.33, + 1195.33 + ], + [ + 1981.33, + 1195.33 + ], + [ + 1975.33, + 1184.67 + ], + [ + 1975.33, + 1184.67 + ], + [ + 1975.33, + 1184.67 + ], + [ + 1971.33, + 1176 + ], + [ + 1971.33, + 1175.33 + ], + [ + 1971.33, + 1174.67 + ], + [ + 1966.67, + 1160.67 + ], + [ + 1966.67, + 1160 + ], + [ + 1966.67, + 1159.33 + ], + [ + 1961.33, + 1147.33 + ], + [ + 1961.33, + 1147.33 + ], + [ + 1961.33, + 1147.33 + ], + [ + 1953.33, + 1137.33 + ], + [ + 1953.33, + 1137.33 + ], + [ + 1953.33, + 1137.33 + ], + [ + 1938, + 1133.33 + ], + [ + 1937.33, + 1133.33 + ], + [ + 1936.67, + 1133.33 + ], + [ + 1916, + 1132 + ], + [ + 1915.33, + 1132 + ], + [ + 1914.67, + 1132 + ], + [ + 1885.33, + 1134 + ], + [ + 1885.33, + 1134 + ], + [ + 1885.33, + 1134 + ], + [ + 1868, + 1134 + ], + [ + 1868, + 1134 + ], + [ + 1868, + 1134 + ], + [ + 1847.33, + 1131.33 + ], + [ + 1847.33, + 1131.33 + ], + [ + 1847.33, + 1131.33 + ], + [ + 1838, + 1130 + ], + [ + 1838, + 1130 + ], + [ + 1838, + 1130 + ], + [ + 1830.67, + 1129.33 + ], + [ + 1828.67, + 1129.33 + ], + [ + 1826.67, + 1129.33 + ], + [ + 1814, + 1131.33 + ], + [ + 1814, + 1131.33 + ], + [ + 1814, + 1131.33 + ], + [ + 1806, + 1139.33 + ], + [ + 1806, + 1139.33 + ], + [ + 1806, + 1139.33 + ], + [ + 1800.67, + 1144.67 + ], + [ + 1800.67, + 1145.33 + ], + [ + 1800.67, + 1146 + ], + [ + 1794.67, + 1153.33 + ], + [ + 1794.67, + 1154 + ], + [ + 1794.67, + 1154.67 + ], + [ + 1790.67, + 1162 + ], + [ + 1790.67, + 1162 + ] + ] + } +] \ No newline at end of file diff --git a/sut/frontend/build/middle-earth-map/fonts/RingbearerMedium.ttf b/sut/frontend/build/middle-earth-map/fonts/RingbearerMedium.ttf new file mode 100644 index 0000000..5fb3a09 Binary files /dev/null and b/sut/frontend/build/middle-earth-map/fonts/RingbearerMedium.ttf differ diff --git a/sut/frontend/build/middle-earth-map/icons/castle.svg b/sut/frontend/build/middle-earth-map/icons/castle.svg new file mode 100644 index 0000000..6845cd8 --- /dev/null +++ b/sut/frontend/build/middle-earth-map/icons/castle.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sut/frontend/build/middle-earth-map/icons/close.svg b/sut/frontend/build/middle-earth-map/icons/close.svg new file mode 100644 index 0000000..b0fe363 --- /dev/null +++ b/sut/frontend/build/middle-earth-map/icons/close.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/sut/frontend/build/middle-earth-map/icons/coffee.svg b/sut/frontend/build/middle-earth-map/icons/coffee.svg new file mode 100644 index 0000000..a7a19c1 --- /dev/null +++ b/sut/frontend/build/middle-earth-map/icons/coffee.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/sut/frontend/build/middle-earth-map/icons/coffin.svg b/sut/frontend/build/middle-earth-map/icons/coffin.svg new file mode 100644 index 0000000..b4cdf8c --- /dev/null +++ b/sut/frontend/build/middle-earth-map/icons/coffin.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sut/frontend/build/middle-earth-map/icons/death.svg b/sut/frontend/build/middle-earth-map/icons/death.svg new file mode 100644 index 0000000..1dcbd58 --- /dev/null +++ b/sut/frontend/build/middle-earth-map/icons/death.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sut/frontend/build/middle-earth-map/icons/dwarf.svg b/sut/frontend/build/middle-earth-map/icons/dwarf.svg new file mode 100644 index 0000000..a501174 --- /dev/null +++ b/sut/frontend/build/middle-earth-map/icons/dwarf.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sut/frontend/build/middle-earth-map/icons/earth.svg b/sut/frontend/build/middle-earth-map/icons/earth.svg new file mode 100644 index 0000000..a498cd9 --- /dev/null +++ b/sut/frontend/build/middle-earth-map/icons/earth.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/sut/frontend/build/middle-earth-map/icons/elf.svg b/sut/frontend/build/middle-earth-map/icons/elf.svg new file mode 100644 index 0000000..0181162 --- /dev/null +++ b/sut/frontend/build/middle-earth-map/icons/elf.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sut/frontend/build/middle-earth-map/icons/evil.svg b/sut/frontend/build/middle-earth-map/icons/evil.svg new file mode 100644 index 0000000..fbeeadf --- /dev/null +++ b/sut/frontend/build/middle-earth-map/icons/evil.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sut/frontend/build/middle-earth-map/icons/eye.svg b/sut/frontend/build/middle-earth-map/icons/eye.svg new file mode 100644 index 0000000..397a774 --- /dev/null +++ b/sut/frontend/build/middle-earth-map/icons/eye.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sut/frontend/build/middle-earth-map/icons/hobbit.svg b/sut/frontend/build/middle-earth-map/icons/hobbit.svg new file mode 100644 index 0000000..a763e1c --- /dev/null +++ b/sut/frontend/build/middle-earth-map/icons/hobbit.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sut/frontend/build/middle-earth-map/icons/layers.svg b/sut/frontend/build/middle-earth-map/icons/layers.svg new file mode 100644 index 0000000..5b82a84 --- /dev/null +++ b/sut/frontend/build/middle-earth-map/icons/layers.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sut/frontend/build/middle-earth-map/icons/swords.svg b/sut/frontend/build/middle-earth-map/icons/swords.svg new file mode 100644 index 0000000..091c703 --- /dev/null +++ b/sut/frontend/build/middle-earth-map/icons/swords.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sut/frontend/build/middle-earth-map/icons/the_one_ring.ico b/sut/frontend/build/middle-earth-map/icons/the_one_ring.ico new file mode 100644 index 0000000..bf6348b Binary files /dev/null and b/sut/frontend/build/middle-earth-map/icons/the_one_ring.ico differ diff --git a/sut/frontend/build/robots.txt b/sut/frontend/build/robots.txt new file mode 100644 index 0000000..5fa602e --- /dev/null +++ b/sut/frontend/build/robots.txt @@ -0,0 +1,33 @@ +# robots.txt for Fellowship's Quest List +# Guides search engine crawlers for optimal indexing + +# Allow all robots to crawl public content +User-agent: * +Allow: / +Allow: /login +Allow: /map +Allow: /api/health +Allow: /api/status + +# Disallow private/protected routes +Disallow: /dashboard +Disallow: /quests +Disallow: /inventory +Disallow: /api/ + +# Prevent crawling of temporary files and build artifacts +Disallow: /build/ +Disallow: /__pycache__/ +Disallow: /.git/ + +# Specify sitemap location for search engines +Sitemap: https://lotr.testingfantasy.com/sitemap.xml + +# Crawl delay to respect server resources +Crawl-delay: 1 + +# Request rate limiting (following guidelines) +Request-rate: 1 request per 10 seconds + +# Comment: Fellowship's Quest List - A LOTR-themed quest tracking application +# For more information, visit: https://lotr.testingfantasy.com/ diff --git a/sut/frontend/build/sitemap.xml b/sut/frontend/build/sitemap.xml new file mode 100644 index 0000000..c88dead --- /dev/null +++ b/sut/frontend/build/sitemap.xml @@ -0,0 +1,53 @@ + + + + + %VITE_APP_SITE_URL%/ + daily + 1.0 + + + + %VITE_APP_SITE_URL%/login + weekly + 0.9 + + + + %VITE_APP_SITE_URL%/dashboard + daily + 0.9 + + + + %VITE_APP_SITE_URL%/quests + daily + 0.8 + + + + %VITE_APP_SITE_URL%/map + daily + 0.8 + + + + %VITE_APP_SITE_URL%/inventory + daily + 0.7 + + + + + %VITE_APP_SITE_URL%/api/health + hourly + 0.5 + + + + %VITE_APP_SITE_URL%/api/status + hourly + 0.5 + + diff --git a/sut/frontend/build/static/css/main.787d33f1.css b/sut/frontend/build/static/css/main.787d33f1.css new file mode 100644 index 0000000..a5fb0ce --- /dev/null +++ b/sut/frontend/build/static/css/main.787d33f1.css @@ -0,0 +1,6 @@ +@import url(https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700;900&family=Lora:wght@400;500;600;700&display=swap);@import url(https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Lora:wght@400;600&display=swap);*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: } + +/* +! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com +*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{-webkit-text-size-adjust:100%;font-feature-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;tab-size:4}body{line-height:inherit}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-feature-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-feature-settings:inherit;color:inherit;font-family:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]:where(:not([hidden=until-found])){display:none}*{margin:0;padding:0}body{--tw-bg-opacity:1;--tw-text-opacity:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-color:#f4e4bc;background-color:rgb(244 228 188/var(--tw-bg-opacity,1));color:#1a1f2e;color:rgb(26 31 46/var(--tw-text-opacity,1))}h1{font-size:3.5rem;line-height:1.1}h2{font-size:2.25rem;line-height:2.5rem}h3{font-size:2rem;line-height:1.3}button{cursor:pointer;font-weight:500}button,input,select,textarea{transition-duration:.3s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.btn-secondary{--tw-bg-opacity:1;--tw-text-opacity:1;--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);background-color:#2d5016;background-color:rgb(45 80 22/var(--tw-bg-opacity,1));border-radius:1rem;color:#f4e4bc;color:rgb(244 228 188/var(--tw-text-opacity,1));font-weight:500;padding:.75rem 1.5rem;transition-duration:.3s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.btn-secondary,.btn-secondary:hover{box-shadow:0 0 #0000,0 0 #0000,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.btn-secondary:hover{--tw-bg-opacity:1;--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);background-color:#3d6b1f;background-color:rgb(61 107 31/var(--tw-bg-opacity,1))}.card-parchment{--tw-gradient-from:#f4e4bc var(--tw-gradient-from-position);--tw-gradient-to:#f4e4bc00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to);--tw-gradient-to:#e8d5b7 var(--tw-gradient-to-position);--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);background-image:linear-gradient(to bottom right,var(--tw-gradient-stops));border-color:#b8860b33;border-radius:1rem;border-width:1px;padding:1.5rem;transition-duration:.3s;transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1)}.card-parchment,.card-parchment:hover{box-shadow:0 0 #0000,0 0 #0000,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.card-parchment:hover{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.inset-0{inset:0}.bottom-10{bottom:2.5rem}.bottom-6{bottom:1.5rem}.left-10{left:2.5rem}.right-10{right:2.5rem}.right-2{right:.5rem}.right-3{right:.75rem}.right-4{right:1rem}.right-6{right:1.5rem}.top-10{top:2.5rem}.top-2{top:.5rem}.top-24{top:6rem}.top-3{top:.75rem}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.z-\[60\]{z-index:60}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.mr-1{margin-right:.25rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-6{margin-top:1.5rem}.mt-auto{margin-top:auto}.line-clamp-3{-webkit-box-orient:vertical;-webkit-line-clamp:3;display:-webkit-box;overflow:hidden}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1/1}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-4{height:1rem}.h-48{height:12rem}.h-56{height:14rem}.h-8{height:2rem}.h-80{height:20rem}.h-fit{height:-webkit-fit-content;height:fit-content}.h-full{height:100%}.max-h-96{max-height:24rem}.max-h-\[80vh\]{max-height:80vh}.min-h-\[180px\]{min-height:180px}.min-h-\[24px\]{min-height:24px}.min-h-\[52px\]{min-height:52px}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-4{width:1rem}.w-48{width:12rem}.w-56{width:14rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0}.min-w-\[120px\]{min-width:120px}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.max-w-\[92\%\]{max-width:92%}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.flex-1{flex:1 1}.flex-shrink-0{flex-shrink:0}.grow{flex-grow:1}.rotate-180{--tw-rotate:180deg}.rotate-180,.scale-\[1\.02\]{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-\[1\.02\]{--tw-scale-x:1.02;--tw-scale-y:1.02}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.animate-bounce{animation:bounce 1s infinite}.animate-fadeIn{animation:fadeIn .3s ease-in}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.list-disc{list-style-type:disc}.appearance-none{-webkit-appearance:none;appearance:none}.auto-rows-fr{grid-auto-rows:minmax(0,1fr)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.self-start{align-self:flex-start}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:1rem}.rounded-md{border-radius:.5rem}.rounded-xl{border-radius:1.5rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l-4{border-left-width:4px}.border-t{border-top-width:1px}.border-blocked\/40{border-color:#ef444466}.border-blue-600{--tw-border-opacity:1;border-color:#2563eb;border-color:rgb(37 99 235/var(--tw-border-opacity,1))}.border-forest{--tw-border-opacity:1;border-color:#2d5016;border-color:rgb(45 80 22/var(--tw-border-opacity,1))}.border-forest-dark{--tw-border-opacity:1;border-color:#1f3a0e;border-color:rgb(31 58 14/var(--tw-border-opacity,1))}.border-gold{--tw-border-opacity:1;border-color:#daa520;border-color:rgb(218 165 32/var(--tw-border-opacity,1))}.border-gold-dark{--tw-border-opacity:1;border-color:#b8860b;border-color:rgb(184 134 11/var(--tw-border-opacity,1))}.border-gold-dark\/20{border-color:#b8860b33}.border-gold-dark\/30{border-color:#b8860b4d}.border-gold-dark\/40{border-color:#b8860b66}.border-gold-dark\/50{border-color:#b8860b80}.border-gold\/30{border-color:#daa5204d}.border-gold\/50{border-color:#daa52080}.border-green-600{--tw-border-opacity:1;border-color:#16a34a;border-color:rgb(22 163 74/var(--tw-border-opacity,1))}.border-in-progress\/40{border-color:#f59e0b66}.border-parchment-light{--tw-border-opacity:1;border-color:#f4e4bc;border-color:rgb(244 228 188/var(--tw-border-opacity,1))}.border-ready\/40{border-color:#10b98166}.border-red-600{--tw-border-opacity:1;border-color:#dc2626;border-color:rgb(220 38 38/var(--tw-border-opacity,1))}.border-text-secondary\/20{border-color:#4b556333}.border-yellow-600{--tw-border-opacity:1;border-color:#ca8a04;border-color:rgb(202 138 4/var(--tw-border-opacity,1))}.bg-background-primary{--tw-bg-opacity:1;background-color:#f4e4bc;background-color:rgb(244 228 188/var(--tw-bg-opacity,1))}.bg-background-primary\/50{background-color:#f4e4bc80}.bg-background-primary\/80{background-color:#f4e4bccc}.bg-background-secondary{--tw-bg-opacity:1;background-color:#e8d5b7;background-color:rgb(232 213 183/var(--tw-bg-opacity,1))}.bg-background-tertiary{--tw-bg-opacity:1;background-color:#d4c0a4;background-color:rgb(212 192 164/var(--tw-bg-opacity,1))}.bg-black\/20{background-color:#0003}.bg-blocked\/20{background-color:#ef444433}.bg-blue-600\/20{background-color:#2563eb33}.bg-dark-magic\/20{background-color:#c7254e33}.bg-forest{--tw-bg-opacity:1;background-color:#2d5016;background-color:rgb(45 80 22/var(--tw-bg-opacity,1))}.bg-forest\/10{background-color:#2d50161a}.bg-forest\/20{background-color:#2d501633}.bg-gold{--tw-bg-opacity:1;background-color:#daa520;background-color:rgb(218 165 32/var(--tw-bg-opacity,1))}.bg-gold\/10{background-color:#daa5201a}.bg-gold\/20{background-color:#daa52033}.bg-green-600{--tw-bg-opacity:1;background-color:#16a34a;background-color:rgb(22 163 74/var(--tw-bg-opacity,1))}.bg-green-600\/20{background-color:#16a34a33}.bg-in-progress\/20{background-color:#f59e0b33}.bg-indigo-600\/20{background-color:#4f46e533}.bg-orange-600\/20{background-color:#ea580c33}.bg-parchment-dark\/70{background-color:#d4c0a4b3}.bg-parchment-light{--tw-bg-opacity:1;background-color:#f4e4bc;background-color:rgb(244 228 188/var(--tw-bg-opacity,1))}.bg-pending\/20{background-color:#8b735533}.bg-ready\/20{background-color:#10b98133}.bg-red-600{--tw-bg-opacity:1;background-color:#dc2626;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.bg-red-600\/20{background-color:#dc262633}.bg-white\/50{background-color:#ffffff80}.bg-white\/70{background-color:#ffffffb3}.bg-yellow-100\/80{background-color:#fef9c3cc}.bg-yellow-600\/20{background-color:#ca8a0433}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-forest{--tw-gradient-from:#2d5016 var(--tw-gradient-from-position);--tw-gradient-to:#2d501600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-forest\/20{--tw-gradient-from:#2d501633 var(--tw-gradient-from-position);--tw-gradient-to:#2d501600 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-gold{--tw-gradient-from:#daa520 var(--tw-gradient-from-position);--tw-gradient-to:#daa52000 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-gold\/20{--tw-gradient-from:#daa52033 var(--tw-gradient-from-position);--tw-gradient-to:#daa52000 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-parchment-light{--tw-gradient-from:#f4e4bc var(--tw-gradient-from-position);--tw-gradient-to:#f4e4bc00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-parchment-light\/90{--tw-gradient-from:#f4e4bce6 var(--tw-gradient-from-position);--tw-gradient-to:#f4e4bc00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.via-background-primary{--tw-gradient-to:#f4e4bc00 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#f4e4bc var(--tw-gradient-via-position),var(--tw-gradient-to)}.via-parchment\/90{--tw-gradient-to:#e8d5b700 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#e8d5b7e6 var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-forest-dark{--tw-gradient-to:#1f3a0e var(--tw-gradient-to-position)}.to-gold-light{--tw-gradient-to:gold var(--tw-gradient-to-position)}.to-gold\/10{--tw-gradient-to:#daa5201a var(--tw-gradient-to-position)}.to-gold\/20{--tw-gradient-to:#daa52033 var(--tw-gradient-to-position)}.to-parchment{--tw-gradient-to:#e8d5b7 var(--tw-gradient-to-position)}.to-parchment-light\/85{--tw-gradient-to:#f4e4bcd9 var(--tw-gradient-to-position)}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-10{padding-bottom:2.5rem;padding-top:2.5rem}.py-12{padding-bottom:3rem;padding-top:3rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-2\.5{padding-bottom:.625rem;padding-top:.625rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-6{padding-bottom:1.5rem;padding-top:1.5rem}.py-8{padding-bottom:2rem;padding-top:2rem}.pl-5{padding-left:1.25rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-epic{font-family:Cinzel,serif}.font-readable{font-family:Lora,serif}.text-2xl{font-size:2.8rem;font-weight:700;line-height:1.2}.text-3xl{font-size:2rem;font-weight:600;line-height:1.3}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-6xl{font-size:3.75rem;line-height:1}.text-\[0\.65rem\]{font-size:.65rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-feature-settings:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction);font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-amber-900{--tw-text-opacity:1;color:#78350f;color:rgb(120 53 15/var(--tw-text-opacity,1))}.text-blocked{--tw-text-opacity:1;color:#ef4444;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-blue-600{--tw-text-opacity:1;color:#2563eb;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-forest{--tw-text-opacity:1;color:#2d5016;color:rgb(45 80 22/var(--tw-text-opacity,1))}.text-forest-dark{--tw-text-opacity:1;color:#1f3a0e;color:rgb(31 58 14/var(--tw-text-opacity,1))}.text-forest-dark\/70{color:#1f3a0eb3}.text-gold{--tw-text-opacity:1;color:#daa520;color:rgb(218 165 32/var(--tw-text-opacity,1))}.text-gold-dark{--tw-text-opacity:1;color:#b8860b;color:rgb(184 134 11/var(--tw-text-opacity,1))}.text-gray-600{--tw-text-opacity:1;color:#4b5563;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-green-400{--tw-text-opacity:1;color:#4ade80;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-green-600{--tw-text-opacity:1;color:#16a34a;color:rgb(22 163 74/var(--tw-text-opacity,1))}.text-green-700{--tw-text-opacity:1;color:#15803d;color:rgb(21 128 61/var(--tw-text-opacity,1))}.text-in-progress{--tw-text-opacity:1;color:#f59e0b;color:rgb(245 158 11/var(--tw-text-opacity,1))}.text-indigo-600{--tw-text-opacity:1;color:#4f46e5;color:rgb(79 70 229/var(--tw-text-opacity,1))}.text-orange-600{--tw-text-opacity:1;color:#ea580c;color:rgb(234 88 12/var(--tw-text-opacity,1))}.text-orange-700{--tw-text-opacity:1;color:#c2410c;color:rgb(194 65 12/var(--tw-text-opacity,1))}.text-parchment{--tw-text-opacity:1;color:#e8d5b7;color:rgb(232 213 183/var(--tw-text-opacity,1))}.text-parchment-light{--tw-text-opacity:1;color:#f4e4bc;color:rgb(244 228 188/var(--tw-text-opacity,1))}.text-parchment-light\/80{color:#f4e4bccc}.text-parchment\/60{color:#e8d5b799}.text-parchment\/70{color:#e8d5b7b3}.text-pending{--tw-text-opacity:1;color:#8b7355;color:rgb(139 115 85/var(--tw-text-opacity,1))}.text-ready{--tw-text-opacity:1;color:#10b981;color:rgb(16 185 129/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:#f87171;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:#dc2626;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:#b91c1c;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-text-primary{--tw-text-opacity:1;color:#1a1f2e;color:rgb(26 31 46/var(--tw-text-opacity,1))}.text-text-secondary{--tw-text-opacity:1;color:#4b5563;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-transparent{color:#0000}.text-white{--tw-text-opacity:1;color:#fff;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:#facc15;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-600{--tw-text-opacity:1;color:#ca8a04;color:rgb(202 138 4/var(--tw-text-opacity,1))}.text-yellow-700{--tw-text-opacity:1;color:#a16207;color:rgb(161 98 7/var(--tw-text-opacity,1))}.placeholder-text-secondary::placeholder{--tw-placeholder-opacity:1;color:#4b5563;color:rgb(75 85 99/var(--tw-placeholder-opacity,1))}.accent-gold{accent-color:#daa520}.opacity-20{opacity:.2}.opacity-50{opacity:.5}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-2xl{box-shadow:0 0 #0000,0 0 #0000,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px #00000040;--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-epic{--tw-shadow:0 20px 40px -10px #c7254e4d;--tw-shadow-colored:0 20px 40px -10px var(--tw-shadow-color)}.shadow-epic,.shadow-lg{box-shadow:0 0 #0000,0 0 #0000,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-xl{box-shadow:0 0 #0000,0 0 #0000,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),0 0 #0000;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.blur-3xl{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-3xl{--tw-blur:blur(64px)}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f)}.drop-shadow,.invert{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur:blur(8px)}.backdrop-blur,.backdrop-blur-sm{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-shadow{transition-duration:.15s;transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-blocked\/30:hover{background-color:#ef44444d}.hover\:bg-forest-dark:hover{--tw-bg-opacity:1;background-color:#1f3a0e;background-color:rgb(31 58 14/var(--tw-bg-opacity,1))}.hover\:bg-forest-light:hover{--tw-bg-opacity:1;background-color:#3d6b1f;background-color:rgb(61 107 31/var(--tw-bg-opacity,1))}.hover\:bg-gold-light:hover{--tw-bg-opacity:1;background-color:gold;background-color:rgb(255 215 0/var(--tw-bg-opacity,1))}.hover\:bg-gold\/20:hover{background-color:#daa52033}.hover\:bg-in-progress\/30:hover{background-color:#f59e0b4d}.hover\:bg-ready\/30:hover{background-color:#10b9814d}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:#b91c1c;background-color:rgb(185 28 28/var(--tw-bg-opacity,1))}.hover\:text-forest:hover{--tw-text-opacity:1;color:#2d5016;color:rgb(45 80 22/var(--tw-text-opacity,1))}.hover\:text-forest-light:hover{--tw-text-opacity:1;color:#3d6b1f;color:rgb(61 107 31/var(--tw-text-opacity,1))}.hover\:text-gold:hover{--tw-text-opacity:1;color:#daa520;color:rgb(218 165 32/var(--tw-text-opacity,1))}.hover\:text-parchment-light:hover{--tw-text-opacity:1;color:#f4e4bc;color:rgb(244 228 188/var(--tw-text-opacity,1))}.hover\:text-red-700:hover{--tw-text-opacity:1;color:#b91c1c;color:rgb(185 28 28/var(--tw-text-opacity,1))}.hover\:opacity-70:hover{opacity:.7}.hover\:shadow-gold:hover{--tw-shadow:0 0 20px #daa52080;--tw-shadow-colored:0 0 20px var(--tw-shadow-color)}.hover\:shadow-gold:hover,.hover\:shadow-lg:hover{box-shadow:0 0 #0000,0 0 #0000,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:0 0 #0000,0 0 #0000,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:shadow-gold:hover{--tw-shadow-color:#daa520;--tw-shadow:var(--tw-shadow-colored)}.focus\:border-gold:focus{--tw-border-opacity:1;border-color:#daa520;border-color:rgb(218 165 32/var(--tw-border-opacity,1))}.focus\:border-red-600:focus{--tw-border-opacity:1;border-color:#dc2626;border-color:rgb(220 38 38/var(--tw-border-opacity,1))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),0 0 #0000;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-gold\/20:focus{--tw-ring-color:#daa52033}.focus\:ring-gold\/50:focus{--tw-ring-color:#daa52080}.focus\:ring-red-600\/20:focus{--tw-ring-color:#dc262633}.active\:scale-95:active{--tw-scale-x:.95;--tw-scale-y:.95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@media (min-width:640px){.sm\:flex-1{flex:1 1}.sm\:flex-row{flex-direction:row}.sm\:items-start{align-items:flex-start}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-3{gap:.75rem}.sm\:gap-4{gap:1rem}.sm\:gap-6{gap:1.5rem}.sm\:space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.sm\:p-6{padding:1.5rem}.sm\:text-2xl{font-size:2.8rem;font-weight:700;line-height:1.2}.sm\:text-7xl{font-size:4.5rem;line-height:1}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}.sm\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:text-4xl{font-size:2.25rem;line-height:2.5rem}.md\:text-5xl{font-size:3rem;line-height:1}}@media (min-width:1024px){.lg\:sticky{position:-webkit-sticky;position:sticky}.lg\:top-6{top:1.5rem}.lg\:col-span-1{grid-column:span 1/span 1}.lg\:col-span-2{grid-column:span 2/span 2}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}.lotr-header{background:linear-gradient(180deg,#1a3209,#1b3a0d 30%,#2d5016 50%,#1f3a0e 80%,#162608);box-shadow:inset 0 1px 0 #ffd70014,inset 0 -1px 0 #0000004d,0 6px 20px #00000080;position:-webkit-sticky;position:sticky;position:relative;top:0;z-index:100}.lotr-header:before{animation:shimmerFlow 8s ease-in-out infinite;background-image:radial-gradient(circle at 10% 50%,#daa5200f 0,#0000 50%),radial-gradient(circle at 90% 50%,#daa5200f 0,#0000 50%),radial-gradient(circle at 50% 0,#ffd70005 0,#0000 60%);content:"";inset:0;pointer-events:none;position:absolute;z-index:0}@keyframes shimmerFlow{0%,to{opacity:.6}50%{opacity:1}}.lotr-header__top-accent{background:linear-gradient(90deg,#0000 0,#a67c52 20%,gold 50%,#a67c52 80%,#0000);box-shadow:0 0 8px #ffd7004d;height:1px;opacity:.85}.lotr-header__inner{gap:1.5rem;margin:0 auto;max-width:90rem;padding:.75rem 1.75rem;position:relative;z-index:1}.lotr-header__brand,.lotr-header__inner{align-items:center;display:flex;min-width:0}.lotr-header__brand{flex-shrink:0;gap:.75rem;max-width:380px;text-decoration:none}.lotr-header__ring-wrap{align-items:center;cursor:default;display:flex;filter:drop-shadow(0 0 8px rgba(218,165,32,.6));transition:filter .4s ease}.lotr-header__ring-wrap:hover{filter:drop-shadow(0 0 16px rgba(255,215,0,.9))}.lotr-header__ring-svg{animation:ringPulse 5s ease-in-out infinite}@keyframes ringPulse{0%,to{filter:drop-shadow(0 0 4px rgba(218,165,32,.5));opacity:1}50%{filter:drop-shadow(0 0 12px rgba(255,215,0,.85));opacity:.95}}.lotr-header__brand-text{display:flex;flex:0 1 auto;flex-direction:column;gap:.05em;line-height:1.05;min-width:0}.lotr-header__subtitle{color:gold;font-family:Cinzel,serif;font-size:.7rem;font-weight:600;letter-spacing:.35em;opacity:.9;text-shadow:0 1px 2px #000000b3,0 0 8px #daa5204d;text-transform:uppercase;-webkit-user-select:none;user-select:none}.lotr-header__page-title{color:#fff8dc;filter:drop-shadow(0 1px 3px #1A320966);font-family:Uncial Antiqua,Cinzel,serif;font-size:1.65rem;font-weight:800;letter-spacing:.08em;line-height:1.1;margin:0;min-width:0;overflow:hidden;text-overflow:ellipsis;text-shadow:0 2px 4px #0009,0 0 12px #daa52040,0 0 24px #daa5201a;white-space:nowrap}.lotr-header__brand:after{background:linear-gradient(180deg,#0000 0,#ffd7004d 20%,#ffd70080 50%,#ffd7004d 80%,#0000);box-shadow:0 0 8px #daa52033;content:"";display:block;flex-shrink:0;height:2.2rem;margin-left:1.25rem;margin-right:.25rem;width:1px}.lotr-header__nav{align-items:center;display:flex;flex:1 1;gap:.1rem;justify-content:center}.lotr-nav-link{align-items:center;border-bottom:2px solid #0000;border-radius:.5rem;color:#e8d5b7;display:inline-flex;font-family:Lora,serif;font-size:.95rem;font-weight:500;gap:.4rem;padding:.45rem 1rem;position:relative;text-decoration:none;transition:color .25s cubic-bezier(.34,1.56,.64,1),background-color .25s ease,transform .2s ease;white-space:nowrap}.lotr-nav-link:after{background:linear-gradient(90deg,#0000,gold,#0000);border-radius:9999px;bottom:-2px;box-shadow:0 0 8px #ffd7004d;content:"";height:2px;left:50%;position:absolute;transform:translateX(-50%);transition:width .3s cubic-bezier(.34,1.56,.64,1);width:0}.lotr-nav-link:hover{background-color:#daa5201f;color:gold;transform:translateY(-2px)}.lotr-nav-link:hover:after{width:60%}.lotr-nav-link--active{background-color:#daa52029!important;border-bottom:2px solid gold;color:gold!important}.lotr-nav-link--active:after{opacity:0;width:60%}.lotr-nav-link:focus-visible{outline:2px solid gold;outline-offset:3px}.lotr-nav-link__icon{flex-shrink:0;font-size:1rem;transition:transform .3s cubic-bezier(.34,1.56,.64,1)}.lotr-nav-link:hover .lotr-nav-link__icon{transform:scale(1.15) rotate(-5deg)}.lotr-header__controls{align-items:center;display:flex;flex-shrink:0;gap:1.2rem;min-width:auto}.lotr-logout-btn{background:linear-gradient(135deg,#2d5016,#1f3a0e);border:1.5px solid #ffd7008c!important;box-shadow:0 2px 8px #daa52026,inset 0 1px 0 #ffd7001a;color:gold!important;font-family:Cinzel,serif;font-size:1rem;font-weight:600;letter-spacing:.1em;transition:all .3s cubic-bezier(.34,1.56,.64,1)}.lotr-logout-btn:hover{background:linear-gradient(135deg,#243a12,#2d5016);border-color:#fff8dc!important;box-shadow:0 4px 16px #ffd70059,inset 0 1px 0 #ffd70033;color:#fff8dc!important;transform:translateY(-2px)}.lotr-hamburger{align-items:center;background:#1a320999;border:1.5px solid #daa52073;border-radius:.5rem;cursor:pointer;display:none;flex-direction:column;flex-shrink:0;gap:5px;height:2.5rem;justify-content:center;padding:.5rem;transition:all .25s ease;width:2.5rem}.lotr-hamburger:hover{background:#1a3209e6;border-color:#ffd700a6;box-shadow:0 0 12px #daa52033}.lotr-hamburger:focus-visible{outline:2px solid gold;outline-offset:2px}.lotr-hamburger__bar{background-color:#e8d5b7;border-radius:9999px;display:block;height:2px;transition:transform .3s cubic-bezier(.34,1.56,.64,1),opacity .2s ease,background-color .25s ease;width:18px}.lotr-hamburger--open .lotr-hamburger__bar:first-child{background-color:gold;transform:translateY(7px) rotate(45deg)}.lotr-hamburger--open .lotr-hamburger__bar:nth-child(2){opacity:0;transform:scaleX(0)}.lotr-hamburger--open .lotr-hamburger__bar:nth-child(3){background-color:gold;transform:translateY(-7px) rotate(-45deg)}.lotr-header__elvish-border{height:16px;overflow:visible;pointer-events:none;position:relative}.lotr-header__border-svg{display:block;filter:drop-shadow(0 1px 3px rgba(0,0,0,.25));height:16px;width:100%}.lotr-header__gems{color:gold;font-size:.5rem;left:50%;letter-spacing:.8rem;opacity:.75;position:absolute;text-shadow:0 0 6px #ffd70080;top:50%;transform:translate(-50%,-50%);-webkit-user-select:none;user-select:none;white-space:nowrap}@media (max-width:767px){.lotr-header__inner{flex-wrap:wrap;padding:.6rem .75rem;row-gap:.3rem}.lotr-header__brand{flex:1 1}.lotr-header__nav{align-items:flex-start;animation:mobileNavIn .22s cubic-bezier(.34,1.56,.64,1) forwards;border-top:1px solid #daa52026;gap:.1rem;margin-top:.3rem;order:10}.lotr-header__controls,.lotr-header__nav{display:none;flex-direction:column;padding-top:.45rem;width:100%}.lotr-header__controls{align-items:stretch;animation:mobileNavIn .24s cubic-bezier(.34,1.56,.64,1) forwards;border-top:1px solid #daa5201f;gap:.55rem;margin-top:.2rem;order:11;padding-bottom:.2rem}.lotr-hamburger,.lotr-header--menu-open .lotr-header__controls,.lotr-header--menu-open .lotr-header__nav{display:flex}.lotr-nav-link{border-radius:.5rem;font-size:1.05rem;padding:.55rem .8rem;width:100%}.lotr-logout-btn{justify-content:center;padding:.7rem 1rem!important;text-align:center;width:100%!important}.lotr-header__brand:after{display:none}}@keyframes mobileNavIn{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}@media (min-width:768px){.lotr-hamburger{display:none!important}.lotr-header__controls,.lotr-header__nav{display:flex!important}}.quest-form-overlay{align-items:center;animation:fadeIn .3s ease-out;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);background:#1a1f2e80;bottom:0;display:flex;justify-content:center;left:0;padding:1.5rem;position:fixed;right:0;top:0;z-index:9999}.quest-form-modal{animation:scaleIn .3s ease-out;background:linear-gradient(135deg,var(--parchment-light) 0,var(--parchment) 100%);border:1px solid var(--gold-dark);border-radius:1rem;box-shadow:0 8px 32px #8b451340,0 0 20px #daa5201a;max-height:88vh;max-width:550px;overflow-y:auto;width:100%}.quest-form-header{align-items:center;background:linear-gradient(90deg,#daa52014,#0000);border-bottom:1px solid var(--gold-dark);display:flex;justify-content:space-between;padding:1.5rem}.quest-form-header h2{color:var(--deep-blue);font-family:Cinzel,serif;font-size:1.3rem;font-weight:700;letter-spacing:.5px;margin:0}.close-button{align-items:center;background:none;border:none;border-radius:.5rem;color:var(--earth-brown);cursor:pointer;display:flex;font-size:1.75rem;height:2.25rem;justify-content:center;line-height:1;padding:.25rem;transition:all .25s ease;width:2.25rem}.close-button:hover{background-color:#daa52026;color:var(--dark-red);transform:rotate(90deg)}.quest-form{gap:1rem;padding:1.5rem}.form-group,.quest-form{display:flex;flex-direction:column}.form-group{gap:.5rem}.form-input{background-color:var(--parchment-light);border:1px solid var(--gold-dark);border-radius:.5rem;box-shadow:0 1px 3px #8b45131a;font-size:.95rem;transition:all .25s ease}.form-input:focus{background-color:#fff;box-shadow:0 2px 6px #8b451326,0 0 0 2px #daa52026}.form-input::placeholder{color:var(--text-secondary);opacity:.7}.form-textarea{font-size:.95rem;font-weight:400;line-height:1.5;min-height:90px;resize:vertical}.form-textarea:focus{border-color:var(--gold);box-shadow:0 2px 6px #8b451326,0 0 0 2px #daa52026}.error-message{background-color:#dc26261a;border-left:3px solid var(--dark-red);border-radius:.5rem;color:var(--dark-red);font-size:.9rem;padding:.75rem}.quest-form-actions{border-top:1px solid var(--gold-dark);display:flex;gap:.75rem;justify-content:flex-end;margin-top:1.5rem;padding-top:1rem}.leaflet-image-layer,.leaflet-layer,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-pane,.leaflet-pane>canvas,.leaflet-pane>svg,.leaflet-tile,.leaflet-tile-container,.leaflet-zoom-box{left:0;position:absolute;top:0}.leaflet-container{overflow:hidden}.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-tile{-webkit-user-drag:none;-webkit-user-select:none;user-select:none}.leaflet-tile::selection{background:#0000}.leaflet-safari .leaflet-tile{image-rendering:-webkit-optimize-contrast}.leaflet-safari .leaflet-tile-container{height:1600px;-webkit-transform-origin:0 0;width:1600px}.leaflet-marker-icon,.leaflet-marker-shadow{display:block}.leaflet-container .leaflet-overlay-pane svg{max-height:none!important;max-width:none!important}.leaflet-container .leaflet-marker-pane img,.leaflet-container .leaflet-shadow-pane img,.leaflet-container .leaflet-tile,.leaflet-container .leaflet-tile-pane img,.leaflet-container img.leaflet-image-layer{max-height:none!important;max-width:none!important;padding:0;width:auto}.leaflet-container img.leaflet-tile{mix-blend-mode:plus-lighter}.leaflet-container.leaflet-touch-zoom{touch-action:pan-x pan-y}.leaflet-container.leaflet-touch-drag{touch-action:none;touch-action:pinch-zoom}.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom{touch-action:none}.leaflet-container{-webkit-tap-highlight-color:transparent}.leaflet-container a{-webkit-tap-highlight-color:rgba(51,181,229,.4)}.leaflet-tile{filter:inherit;visibility:hidden}.leaflet-tile-loaded{visibility:inherit}.leaflet-zoom-box{box-sizing:border-box;height:0;width:0;z-index:800}.leaflet-overlay-pane svg{-moz-user-select:none}.leaflet-pane{z-index:400}.leaflet-tile-pane{z-index:200}.leaflet-overlay-pane{z-index:400}.leaflet-shadow-pane{z-index:500}.leaflet-marker-pane{z-index:600}.leaflet-tooltip-pane{z-index:650}.leaflet-popup-pane{z-index:700}.leaflet-map-pane canvas{z-index:100}.leaflet-map-pane svg{z-index:200}.leaflet-vml-shape{height:1px;width:1px}.lvml{behavior:url(#default#VML);display:inline-block;position:absolute}.leaflet-control{pointer-events:visiblePainted;pointer-events:auto;position:relative;z-index:800}.leaflet-bottom,.leaflet-top{pointer-events:none;position:absolute;z-index:1000}.leaflet-top{top:0}.leaflet-right{right:0}.leaflet-bottom{bottom:0}.leaflet-left{left:0}.leaflet-control{clear:both;float:left}.leaflet-right .leaflet-control{float:right}.leaflet-top .leaflet-control{margin-top:10px}.leaflet-bottom .leaflet-control{margin-bottom:10px}.leaflet-left .leaflet-control{margin-left:10px}.leaflet-right .leaflet-control{margin-right:10px}.leaflet-fade-anim .leaflet-popup{opacity:0;transition:opacity .2s linear}.leaflet-fade-anim .leaflet-map-pane .leaflet-popup{opacity:1}.leaflet-zoom-animated{transform-origin:0 0}svg.leaflet-zoom-animated{will-change:transform}.leaflet-zoom-anim .leaflet-zoom-animated{transition:transform .25s cubic-bezier(0,0,.25,1)}.leaflet-pan-anim .leaflet-tile,.leaflet-zoom-anim .leaflet-tile{transition:none}.leaflet-zoom-anim .leaflet-zoom-hide{visibility:hidden}.leaflet-interactive{cursor:pointer}.leaflet-grab{cursor:grab}.leaflet-crosshair,.leaflet-crosshair .leaflet-interactive{cursor:crosshair}.leaflet-control,.leaflet-popup-pane{cursor:auto}.leaflet-dragging .leaflet-grab,.leaflet-dragging .leaflet-grab .leaflet-interactive,.leaflet-dragging .leaflet-marker-draggable{cursor:move;cursor:grabbing}.leaflet-image-layer,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-pane>svg path,.leaflet-tile-container{pointer-events:none}.leaflet-image-layer.leaflet-interactive,.leaflet-marker-icon.leaflet-interactive,.leaflet-pane>svg path.leaflet-interactive,svg.leaflet-image-layer.leaflet-interactive path{pointer-events:visiblePainted;pointer-events:auto}.leaflet-container{background:#ddd;outline-offset:1px}.leaflet-container a{color:#0078a8}.leaflet-zoom-box{background:#ffffff80;border:2px dotted #38f}.leaflet-container{font-family:Helvetica Neue,Arial,Helvetica,sans-serif;font-size:12px;font-size:.75rem;line-height:1.5}.leaflet-bar{border-radius:4px;box-shadow:0 1px 5px #000000a6}.leaflet-bar a{background-color:#fff;border-bottom:1px solid #ccc;color:#000;display:block;height:26px;line-height:26px;text-align:center;text-decoration:none;width:26px}.leaflet-bar a,.leaflet-control-layers-toggle{background-position:50% 50%;background-repeat:no-repeat;display:block}.leaflet-bar a:focus,.leaflet-bar a:hover{background-color:#f4f4f4}.leaflet-bar a:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.leaflet-bar a:last-child{border-bottom:none;border-bottom-left-radius:4px;border-bottom-right-radius:4px}.leaflet-bar a.leaflet-disabled{background-color:#f4f4f4;color:#bbb;cursor:default}.leaflet-touch .leaflet-bar a{height:30px;line-height:30px;width:30px}.leaflet-touch .leaflet-bar a:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.leaflet-touch .leaflet-bar a:last-child{border-bottom-left-radius:2px;border-bottom-right-radius:2px}.leaflet-control-zoom-in,.leaflet-control-zoom-out{font:700 18px Lucida Console,Monaco,monospace;text-indent:1px}.leaflet-touch .leaflet-control-zoom-in,.leaflet-touch .leaflet-control-zoom-out{font-size:22px}.leaflet-control-layers{background:#fff;border-radius:5px;box-shadow:0 1px 5px #0006}.leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAQAAAADQ4RFAAACf0lEQVR4AY1UM3gkARTePdvdoTxXKc+qTl3aU5U6b2Kbkz3Gtq3Zw6ziLGNPzrYx7946Tr6/ee/XeCQ4D3ykPtL5tHno4n0d/h3+xfuWHGLX81cn7r0iTNzjr7LrlxCqPtkbTQEHeqOrTy4Yyt3VCi/IOB0v7rVC7q45Q3Gr5K6jt+3Gl5nCoDD4MtO+j96Wu8atmhGqcNGHObuf8OM/x3AMx38+4Z2sPqzCxRFK2aF2e5Jol56XTLyggAMTL56XOMoS1W4pOyjUcGGQdZxU6qRh7B9Zp+PfpOFlqt0zyDZckPi1ttmIp03jX8gyJ8a/PG2yutpS/Vol7peZIbZcKBAEEheEIAgFbDkz5H6Zrkm2hVWGiXKiF4Ycw0RWKdtC16Q7qe3X4iOMxruonzegJzWaXFrU9utOSsLUmrc0YjeWYjCW4PDMADElpJSSQ0vQvA1Tm6/JlKnqFs1EGyZiFCqnRZTEJJJiKRYzVYzJck2Rm6P4iH+cmSY0YzimYa8l0EtTODFWhcMIMVqdsI2uiTvKmTisIDHJ3od5GILVhBCarCfVRmo4uTjkhrhzkiBV7SsaqS+TzrzM1qpGGUFt28pIySQHR6h7F6KSwGWm97ay+Z+ZqMcEjEWebE7wxCSQwpkhJqoZA5ivCdZDjJepuJ9IQjGGUmuXJdBFUygxVqVsxFsLMbDe8ZbDYVCGKxs+W080max1hFCarCfV+C1KATwcnvE9gRRuMP2prdbWGowm1KB1y+zwMMENkM755cJ2yPDtqhTI6ED1M/82yIDtC/4j4BijjeObflpO9I9MwXTCsSX8jWAFeHr05WoLTJ5G8IQVS/7vwR6ohirYM7f6HzYpogfS3R2OAAAAAElFTkSuQmCC);height:36px;width:36px}.leaflet-retina .leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAQAAABvcdNgAAAEsklEQVR4AWL4TydIhpZK1kpWOlg0w3ZXP6D2soBtG42jeI6ZmQTHzAxiTbSJsYLjO9HhP+WOmcuhciVnmHVQcJnp7DFvScowZorad/+V/fVzMdMT2g9Cv9guXGv/7pYOrXh2U+RRR3dSd9JRx6bIFc/ekqHI29JC6pJ5ZEh1yWkhkbcFeSjxgx3L2m1cb1C7bceyxA+CNjT/Ifff+/kDk2u/w/33/IeCMOSaWZ4glosqT3DNnNZQ7Cs58/3Ce5HL78iZH/vKVIaYlqzfdLu8Vi7dnvUbEza5Idt36tquZFldl6N5Z/POLof0XLK61mZCmJSWjVF9tEjUluu74IUXvgttuVIHE7YxSkaYhJZam7yiM9Pv82JYfl9nptxZaxMJE4YSPty+vF0+Y2up9d3wwijfjZbabqm/3bZ9ecKHsiGmRflnn1MW4pjHf9oLufyn2z3y1D6n8g8TZhxyzipLNPnAUpsOiuWimg52psrTZYnOWYNDTMuWBWa0tJb4rgq1UvmutpaYEbZlwU3CLJm/ayYjHW5/h7xWLn9Hh1vepDkyf7dE7MtT5LR4e7yYpHrkhOUpEfssBLq2pPhAqoSWKUkk7EDqkmK6RrCEzqDjhNDWNE+XSMvkJRDWlZTmCW0l0PHQGRZY5t1L83kT0Y3l2SItk5JAWHl2dCOBm+fPu3fo5/3v61RMCO9Jx2EEYYhb0rmNQMX/vm7gqOEJLcXTGw3CAuRNeyaPWwjR8PRqKQ1PDA/dpv+on9Shox52WFnx0KY8onHayrJzm87i5h9xGw/tfkev0jGsQizqezUKjk12hBMKJ4kbCqGPVNXudyyrShovGw5CgxsRICxF6aRmSjlBnHRzg7Gx8fKqEubI2rahQYdR1YgDIRQO7JvQyD52hoIQx0mxa0ODtW2Iozn1le2iIRdzwWewedyZzewidueOGqlsn1MvcnQpuVwLGG3/IR1hIKxCjelIDZ8ldqWz25jWAsnldEnK0Zxro19TGVb2ffIZEsIO89EIEDvKMPrzmBOQcKQ+rroye6NgRRxqR4U8EAkz0CL6uSGOm6KQCdWjvjRiSP1BPalCRS5iQYiEIvxuBMJEWgzSoHADcVMuN7IuqqTeyUPq22qFimFtxDyBBJEwNyt6TM88blFHao/6tWWhuuOM4SAK4EI4QmFHA+SEyWlp4EQoJ13cYGzMu7yszEIBOm2rVmHUNqwAIQabISNMRstmdhNWcFLsSm+0tjJH1MdRxO5Nx0WDMhCtgD6OKgZeljJqJKc9po8juskR9XN0Y1lZ3mWjLR9JCO1jRDMd0fpYC2VnvjBSEFg7wBENc0R9HFlb0xvF1+TBEpF68d+DHR6IOWVv2BECtxo46hOFUBd/APU57WIoEwJhIi2CdpyZX0m93BZicktMj1AS9dClteUFAUNUIEygRZCtik5zSxI9MubTBH1GOiHsiLJ3OCoSZkILa9PxiN0EbvhsAo8tdAf9Seepd36lGWHmtNANTv5Jd0z4QYyeo/UEJqxKRpg5LZx6btLPsOaEmdMyxYdlc8LMaJnikDlhclqmPiQnTEpLUIZEwkRagjYkEibQErwhkTAKCLQEbUgkzJQWc/0PstHHcfEdQ+UAAAAASUVORK5CYII=);background-size:26px 26px}.leaflet-touch .leaflet-control-layers-toggle{height:44px;width:44px}.leaflet-control-layers .leaflet-control-layers-list,.leaflet-control-layers-expanded .leaflet-control-layers-toggle{display:none}.leaflet-control-layers-expanded .leaflet-control-layers-list{display:block;position:relative}.leaflet-control-layers-expanded{background:#fff;color:#333;padding:6px 10px 6px 6px}.leaflet-control-layers-scrollbar{overflow-x:hidden;overflow-y:scroll;padding-right:5px}.leaflet-control-layers-selector{margin-top:2px;position:relative;top:1px}.leaflet-control-layers label{display:block;font-size:13px;font-size:1.08333em}.leaflet-control-layers-separator{border-top:1px solid #ddd;height:0;margin:5px -10px 5px -6px}.leaflet-default-icon-path{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YaZtr2962HUzbDNpjszW24mRt28p47v7zq/bXZtrp/lWnXr337j3nPCe85NcypgSFdugCpW5YoDAMRaIMqRi6aKq5E3YqDQO3qAwjVWrD8Ncq/RBpykd8oZUb/kaJutow8r1aP9II0WmLKLIsJyv1w/kqw9Ch2MYdB++12Onxee/QMwvf4/Dk/Lfp/i4nxTXtOoQ4pW5Aj7wpici1A9erdAN2OH64x8OSP9j3Ft3b7aWkTg/Fm91siTra0f9on5sQr9INejH6CUUUpavjFNq1B+Oadhxmnfa8RfEmN8VNAsQhPqF55xHkMzz3jSmChWU6f7/XZKNH+9+hBLOHYozuKQPxyMPUKkrX/K0uWnfFaJGS1QPRtZsOPtr3NsW0uyh6NNCOkU3Yz+bXbT3I8G3xE5EXLXtCXbbqwCO9zPQYPRTZ5vIDXD7U+w7rFDEoUUf7ibHIR4y6bLVPXrz8JVZEql13trxwue/uDivd3fkWRbS6/IA2bID4uk0UpF1N8qLlbBlXs4Ee7HLTfV1j54APvODnSfOWBqtKVvjgLKzF5YdEk5ewRkGlK0i33Eofffc7HT56jD7/6U+qH3Cx7SBLNntH5YIPvODnyfIXZYRVDPqgHtLs5ABHD3YzLuespb7t79FY34DjMwrVrcTuwlT55YMPvOBnRrJ4VXTdNnYug5ucHLBjEpt30701A3Ts+HEa73u6dT3FNWwflY86eMHPk+Yu+i6pzUpRrW7SNDg5JHR4KapmM5Wv2E8Tfcb1HoqqHMHU+uWDD7zg54mz5/2BSnizi9T1Dg4QQXLToGNCkb6tb1NU+QAlGr1++eADrzhn/u8Q2YZhQVlZ5+CAOtqfbhmaUCS1ezNFVm2imDbPmPng5wmz+gwh+oHDce0eUtQ6OGDIyR0uUhUsoO3vfDmmgOezH0mZN59x7MBi++WDL1g/eEiU3avlidO671bkLfwbw5XV2P8Pzo0ydy4t2/0eu33xYSOMOD8hTf4CrBtGMSoXfPLchX+J0ruSePw3LZeK0juPJbYzrhkH0io7B3k164hiGvawhOKMLkrQLyVpZg8rHFW7E2uHOL888IBPlNZ1FPzstSJM694fWr6RwpvcJK60+0HCILTBzZLFNdtAzJaohze60T8qBzyh5ZuOg5e7uwQppofEmf2++DYvmySqGBuKaicF1blQjhuHdvCIMvp8whTTfZzI7RldpwtSzL+F1+wkdZ2TBOW2gIF88PBTzD/gpeREAMEbxnJcaJHNHrpzji0gQCS6hdkEeYt9DF/2qPcEC8RM28Hwmr3sdNyht00byAut2k3gufWNtgtOEOFGUwcXWNDbdNbpgBGxEvKkOQsxivJx33iow0Vw5S6SVTrpVq11ysA2Rp7gTfPfktc6zhtXBBC+adRLshf6sG2RfHPZ5EAc4sVZ83yCN00Fk/4kggu40ZTvIEm5g24qtU4KjBrx/BTTH8ifVASAG7gKrnWxJDcU7x8X6Ecczhm3o6YicvsLXWfh3Ch1W0k8x0nXF+0fFxgt4phz8QvypiwCCFKMqXCnqXExjq10beH+UUA7+nG6mdG/Pu0f3LgFcGrl2s0kNNjpmoJ9o4B29CMO8dMT4Q5ox8uitF6fqsrJOr8qnwNbRzv6hSnG5wP+64C7h9lp30hKNtKdWjtdkbuPA19nJ7Tz3zR/ibgARbhb4AlhavcBebmTHcFl2fvYEnW0ox9xMxKBS8btJ+KiEbq9zA4RthQXDhPa0T9TEe69gWupwc6uBUphquXgf+/FrIjweHQS4/pduMe5ERUMHUd9xv8ZR98CxkS4F2n3EUrUZ10EYNw7BWm9x1GiPssi3GgiGRDKWRYZfXlON+dfNbM+GgIwYdwAAAAASUVORK5CYII=)}.leaflet-container .leaflet-control-attribution{background:#fff;background:#fffc;margin:0}.leaflet-control-attribution,.leaflet-control-scale-line{color:#333;line-height:1.4;padding:0 5px}.leaflet-control-attribution a{text-decoration:none}.leaflet-control-attribution a:focus,.leaflet-control-attribution a:hover{text-decoration:underline}.leaflet-attribution-flag{display:inline!important;height:.6669em;vertical-align:initial!important;width:1em}.leaflet-left .leaflet-control-scale{margin-left:5px}.leaflet-bottom .leaflet-control-scale{margin-bottom:5px}.leaflet-control-scale-line{background:#fffc;border:2px solid #777;border-top:none;box-sizing:border-box;line-height:1.1;padding:2px 5px 1px;text-shadow:1px 1px #fff;white-space:nowrap}.leaflet-control-scale-line:not(:first-child){border-bottom:none;border-top:2px solid #777;margin-top:-2px}.leaflet-control-scale-line:not(:first-child):not(:last-child){border-bottom:2px solid #777}.leaflet-touch .leaflet-bar,.leaflet-touch .leaflet-control-attribution,.leaflet-touch .leaflet-control-layers{box-shadow:none}.leaflet-touch .leaflet-bar,.leaflet-touch .leaflet-control-layers{background-clip:padding-box;border:2px solid #0003}.leaflet-popup{margin-bottom:20px;position:absolute;text-align:center}.leaflet-popup-content-wrapper{border-radius:12px;padding:1px;text-align:left}.leaflet-popup-content{font-size:13px;font-size:1.08333em;line-height:1.3;margin:13px 24px 13px 20px;min-height:1px}.leaflet-popup-content p{margin:1.3em 0}.leaflet-popup-tip-container{height:20px;left:50%;margin-left:-20px;margin-top:-1px;overflow:hidden;pointer-events:none;position:absolute;width:40px}.leaflet-popup-tip{height:17px;margin:-10px auto 0;padding:1px;pointer-events:auto;transform:rotate(45deg);width:17px}.leaflet-popup-content-wrapper,.leaflet-popup-tip{background:#fff;box-shadow:0 3px 14px #0006;color:#333}.leaflet-container a.leaflet-popup-close-button{background:#0000;border:none;color:#757575;font:16px/24px Tahoma,Verdana,sans-serif;height:24px;position:absolute;right:0;text-align:center;text-decoration:none;top:0;width:24px}.leaflet-container a.leaflet-popup-close-button:focus,.leaflet-container a.leaflet-popup-close-button:hover{color:#585858}.leaflet-popup-scrolled{overflow:auto}.leaflet-oldie .leaflet-popup-content-wrapper{-ms-zoom:1}.leaflet-oldie .leaflet-popup-tip{-ms-filter:"progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";filter:progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678,M12=0.70710678,M21=-0.70710678,M22=0.70710678);margin:0 auto;width:24px}.leaflet-oldie .leaflet-control-layers,.leaflet-oldie .leaflet-control-zoom,.leaflet-oldie .leaflet-popup-content-wrapper,.leaflet-oldie .leaflet-popup-tip{border:1px solid #999}.leaflet-div-icon{background:#fff;border:1px solid #666}.leaflet-tooltip{background-color:#fff;border:1px solid #fff;border-radius:3px;box-shadow:0 1px 3px #0006;color:#222;padding:6px;pointer-events:none;position:absolute;-webkit-user-select:none;user-select:none;white-space:nowrap}.leaflet-tooltip.leaflet-interactive{cursor:pointer;pointer-events:auto}.leaflet-tooltip-bottom:before,.leaflet-tooltip-left:before,.leaflet-tooltip-right:before,.leaflet-tooltip-top:before{background:#0000;border:6px solid #0000;content:"";pointer-events:none;position:absolute}.leaflet-tooltip-bottom{margin-top:6px}.leaflet-tooltip-top{margin-top:-6px}.leaflet-tooltip-bottom:before,.leaflet-tooltip-top:before{left:50%;margin-left:-6px}.leaflet-tooltip-top:before{border-top-color:#fff;bottom:0;margin-bottom:-12px}.leaflet-tooltip-bottom:before{border-bottom-color:#fff;margin-left:-6px;margin-top:-12px;top:0}.leaflet-tooltip-left{margin-left:-6px}.leaflet-tooltip-right{margin-left:6px}.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{margin-top:-6px;top:50%}.leaflet-tooltip-left:before{border-left-color:#fff;margin-right:-12px;right:0}.leaflet-tooltip-right:before{border-right-color:#fff;left:0;margin-left:-12px}@media print{.leaflet-control{-webkit-print-color-adjust:exact;print-color-adjust:exact}}.leaflet-cluster-anim .leaflet-marker-icon,.leaflet-cluster-anim .leaflet-marker-shadow{transition:transform .3s ease-out,opacity .3s ease-in}.leaflet-cluster-spider-leg{transition:stroke-dashoffset .3s ease-out,stroke-opacity .3s ease-in}.middle-earth-map-container{background-color:#e8d5b7;background-color:var(--color-parchment-dark,#e8d5b7);border:2px solid sienna;border:2px solid var(--color-earth-brown-medium,sienna);border-radius:8px;min-height:500px;overflow:hidden;position:relative}.middle-earth-map-container .leaflet-container{background-color:#f4e4bc;background-color:var(--color-parchment-light,#f4e4bc);font-family:Lora,serif}.middle-earth-map-container .leaflet-popup-content-wrapper{background:#f4e4bc;background:var(--color-parchment-light,#f4e4bc);border:2px solid sienna;border:2px solid var(--color-earth-brown-medium,sienna);border-radius:8px;box-shadow:0 4px 12px #0000004d;color:#1a1f2e;color:var(--color-deep-blue-dark,#1a1f2e)}.middle-earth-map-container .leaflet-popup-content{font-family:Lora,serif;margin:12px}.middle-earth-map-container .leaflet-popup-tip{background:#f4e4bc;background:var(--color-parchment-light,#f4e4bc);border:1px solid sienna;border:1px solid var(--color-earth-brown-medium,sienna)}.middle-earth-map-container .leaflet-control-zoom{border:2px solid sienna;border:2px solid var(--color-earth-brown-medium,sienna);border-radius:4px;box-shadow:0 2px 8px #0003}.middle-earth-map-container .leaflet-control-zoom a{background-color:#f4e4bc;background-color:var(--color-parchment-light,#f4e4bc);border-bottom:1px solid sienna;border-bottom:1px solid var(--color-earth-brown-medium,sienna);color:#8b4513;color:var(--color-earth-brown-dark,#8b4513)}.middle-earth-map-container .leaflet-control-zoom a:hover{background-color:#e8d5b7;background-color:var(--color-parchment-dark,#e8d5b7);color:gold;color:var(--color-gold-dark,gold)}.custom-marker-icon,.location-marker-icon{background:#0000;border:none}.location-marker{border:3px solid #f4e4bc;border:3px solid var(--color-parchment-light,#f4e4bc);border-radius:50% 50% 50% 0;box-shadow:0 2px 8px #0006;cursor:pointer;height:40px;position:relative;transform:rotate(-45deg);transition:transform .2s ease,box-shadow .2s ease;width:40px}.location-marker:hover{box-shadow:0 4px 12px #0009;transform:rotate(-45deg) scale(1.2);z-index:1000}.marker-pin-inner{background-color:#f4e4bc;background-color:var(--color-parchment-light,#f4e4bc);border:2px solid #fffc;border-radius:50%;height:12px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:12px}.quest-count-badge{align-items:center;background-color:gold;background-color:var(--color-gold-dark,gold);border:2px solid #f4e4bc;border:2px solid var(--color-parchment-light,#f4e4bc);border-radius:50%;box-shadow:0 2px 4px #0000004d;color:#8b4513;color:var(--color-earth-brown-dark,#8b4513);display:flex;font-size:11px;font-weight:700;height:24px;justify-content:center;line-height:1;position:absolute;right:-10px;top:-10px;transform:rotate(45deg);width:24px;z-index:10}.quest-count-badge span{display:inline-block;transform:rotate(-45deg)}.marker-cluster-icon{background:#0000!important;border:none!important}.marker-cluster{align-items:center;background-color:#3d6b1f;background-color:var(--color-forest-green-medium,#3d6b1f);border:3px solid #f4e4bc;border:3px solid var(--color-parchment-light,#f4e4bc);border-radius:50%;box-shadow:0 2px 8px #0006;color:#fff;cursor:pointer;display:flex;font-size:14px;font-weight:700;height:40px;justify-content:center;transition:transform .2s ease,box-shadow .2s ease;width:40px}.marker-cluster:hover{box-shadow:0 4px 12px #0009;transform:scale(1.1)}.middle-earth-map-container .marker-cluster-small{background-color:#3d6b1f!important;background-color:var(--color-forest-green-medium,#3d6b1f)!important;border:3px solid #f4e4bc!important;border:3px solid var(--color-parchment-light,#f4e4bc)!important}.middle-earth-map-container .marker-cluster-medium{background-color:#2d5016!important;background-color:var(--color-forest-green-dark,#2d5016)!important;border:3px solid #f4e4bc!important;border:3px solid var(--color-parchment-light,#f4e4bc)!important}.middle-earth-map-container .marker-cluster-large{background-color:#8b4513!important;background-color:var(--color-earth-brown-dark,#8b4513)!important;border:3px solid #f4e4bc!important;border:3px solid var(--color-parchment-light,#f4e4bc)!important}.quest-marker-cluster-icon{background:#0000!important;border:none!important}.quest-marker-cluster{align-items:center;background:linear-gradient(135deg,peru,#8b4513);border:2px solid #daa520;border-radius:50%;box-shadow:0 2px 8px #00000080,inset 0 1px 0 #ffffff4d;color:#f4e4bc;cursor:pointer;display:flex;font-size:13px;font-weight:700;justify-content:center;transition:transform .2s ease,box-shadow .2s ease}.quest-marker-cluster-small{height:35px;width:35px}.quest-marker-cluster-medium{font-size:15px;height:45px;width:45px}.quest-marker-cluster-large{font-size:17px;height:55px;width:55px}.quest-marker-cluster:hover{box-shadow:0 4px 12px #000000b3,inset 0 1px 0 #fff6;transform:scale(1.15)}.middle-earth-map-container .leaflet-marker-pane .quest-marker-icon{cursor:pointer!important;pointer-events:auto!important;z-index:2000!important}.middle-earth-map-container .leaflet-marker-pane .quest-marker-icon *{cursor:pointer!important;pointer-events:auto!important}.middle-earth-map-container .leaflet-interactive{pointer-events:auto!important}.middle-earth-map-container .leaflet-container .quest-marker-icon,.middle-earth-map-container .leaflet-container .quest-marker-icon *{cursor:pointer!important;pointer-events:auto!important}.middle-earth-map-container .leaflet-marker-pane{z-index:600!important}.middle-earth-map-container .leaflet-marker-pane .quest-marker-icon{z-index:2600!important}.location-popup{min-width:200px}.location-popup h3{color:#8b4513;color:var(--color-earth-brown-dark,#8b4513);font-family:Cinzel,serif;font-size:1.2rem;margin:0 0 8px}.location-popup .location-region{color:sienna;color:var(--color-earth-brown-medium,sienna);font-size:.9rem;font-style:italic;margin:0 0 8px}.location-popup .location-description{color:#1a1f2e;color:var(--color-deep-blue-dark,#1a1f2e);font-size:.85rem;line-height:1.4;margin:0 0 12px}.location-popup .quest-count-info{color:#2d5016;color:var(--color-forest-green-dark,#2d5016);font-size:.9rem;font-weight:500;margin:0 0 12px}.location-popup .quest-count-info strong{color:gold;color:var(--color-gold-dark,gold);font-size:1.1rem}.location-popup .btn-view-quests{background-color:#3d6b1f;background-color:var(--color-forest-green-medium,#3d6b1f);border:none;border-radius:4px;color:#fff;cursor:pointer;font-family:Lora,serif;font-size:.9rem;padding:8px 16px;transition:background-color .2s ease;width:100%}.location-popup .btn-view-quests:hover{background-color:#2d5016;background-color:var(--color-forest-green-dark,#2d5016)}.character-marker-icon{background:#0000!important;border:none!important;z-index:5000!important}.character-marker{align-items:center;background:radial-gradient(circle at 30% 30%,#ffe7a3,#d4a939);border:2px solid #8b4513;border-radius:50%;box-shadow:0 2px 10px #00000073;cursor:pointer;display:flex;font-size:20px;height:40px;justify-content:center;position:relative;transition:transform .2s ease;width:40px;z-index:5001!important}.character-marker:hover{transform:scale(1.12)}.character-popup{min-width:200px}.character-popup h4{color:#8b4513;color:var(--color-earth-brown-dark,#8b4513);margin:0 0 6px}.character-popup p{font-size:.9rem;margin:0 0 10px}.character-popup .btn-bargain-character{background-color:#3d6b1f;background-color:var(--color-forest-green-medium,#3d6b1f);border:none;border-radius:4px;color:#fff;cursor:pointer;padding:8px 10px;width:100%}.character-popup .btn-bargain-character:hover{background-color:#2d5016;background-color:var(--color-forest-green-dark,#2d5016)}.quest-marker-icon{background:#0000!important;border:none!important;pointer-events:auto!important}.leaflet-marker-icon.quest-marker-icon,.quest-marker{cursor:pointer!important;pointer-events:auto!important}.quest-marker{align-items:center;border:2px solid #f4e4bc;border:2px solid var(--color-parchment-light,#f4e4bc);border-radius:50%;box-shadow:0 2px 8px #0006,0 0 4px #daa5204d;display:flex;height:35px;justify-content:center;position:relative;transition:transform .2s ease,box-shadow .2s ease;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;width:35px;z-index:2000}.quest-marker:hover{box-shadow:0 4px 12px #0009,0 0 8px #daa52080;cursor:pointer!important;transform:scale(1.3);z-index:3000}.quest-marker:active{transform:scale(1.2)}.quest-marker-selected{border:3px solid gold;border:3px solid var(--color-gold-dark,gold);box-shadow:0 0 15px #daa520cc,0 2px 8px #0006;z-index:2500}.quest-marker-inner{filter:drop-shadow(0 1px 2px rgba(0,0,0,.3));font-size:18px;line-height:1}.quest-popup,.quest-popup-wrapper .leaflet-popup-content-wrapper{max-width:350px}.quest-popup{min-width:250px}.quest-popup h4{color:#8b4513;color:var(--color-earth-brown-dark,#8b4513);font-family:Cinzel,serif;font-size:1.2rem;line-height:1.3;margin:0 0 10px}.quest-popup-type{color:gold;color:var(--color-gold-dark,gold);font-family:Cinzel,serif;font-size:.9rem;font-weight:600;margin:0 0 8px}.quest-popup-status{background-color:#2d50161a;border-radius:4px;color:#2d5016;color:var(--color-forest-green-dark,#2d5016);display:inline-block;font-size:.85rem;font-weight:500;margin:0 0 10px;padding:4px 8px}.quest-popup-description{color:#1a1f2e;color:var(--color-deep-blue-dark,#1a1f2e);font-size:.9rem;line-height:1.5;margin:0 0 12px;max-height:200px;overflow-y:auto}.quest-popup-location{color:sienna;color:var(--color-earth-brown-medium,sienna);font-size:.85rem;font-style:italic;margin:0 0 8px}.quest-popup-assignee{color:#1a1f2e;color:var(--color-deep-blue-dark,#1a1f2e);font-size:.85rem;margin:0 0 12px}.quest-popup-actions{display:flex;flex-direction:column;gap:8px;margin-top:12px}.btn-complete-quest,.btn-view-quest{background-color:#3d6b1f;background-color:var(--color-forest-green-medium,#3d6b1f);border:none;border-radius:4px;color:#fff;cursor:pointer;font-family:Lora,serif;font-size:.9rem;font-weight:500;padding:8px 16px;transition:background-color .2s ease,transform .1s ease;width:100%}.btn-complete-quest:hover,.btn-view-quest:hover{background-color:#2d5016;background-color:var(--color-forest-green-dark,#2d5016);transform:translateY(-1px)}.btn-complete-quest{background-color:gold;background-color:var(--color-gold-dark,gold)}.btn-complete-quest,.btn-complete-quest:hover{color:#8b4513;color:var(--color-earth-brown-dark,#8b4513)}.btn-complete-quest:hover{background-color:#daa520;background-color:var(--color-gold-light,#daa520)}@media (max-width:768px){.middle-earth-map-container{min-height:400px}.location-popup{min-width:150px}}.map-page-full{background-color:#1a1f2e;background-color:var(--color-deep-blue-dark,#1a1f2e);display:flex;flex-direction:column;height:100vh}.map-page-full>.navbar{flex-shrink:0}.map-page-container{flex:1 1;overflow:visible;position:relative}.filter-sidebar{animation:slideInLeft .5s ease-out;background:linear-gradient(180deg,var(--parchment-light) 0,var(--parchment) 100%);border-right:2px solid var(--earth-brown-light);bottom:0;box-shadow:2px 0 20px #8b451366;display:flex;flex-direction:column;left:0;overflow-y:auto;position:absolute;top:0;transform:translateX(0);transition:transform .4s ease;width:320px;z-index:500}.filter-sidebar.closed{animation:slideInLeft .5s ease-out reverse;transform:translateX(-100%)}.filter-sidebar-header{border-bottom:2px solid sienna;border-bottom:2px solid var(--color-earth-brown-medium,sienna);flex-shrink:0;padding:1.5rem}.filter-sidebar-header h2,.filter-sidebar-header h3{color:#8b4513;color:var(--color-earth-brown-dark,#8b4513);font-family:Cinzel,serif;font-size:1.3rem;margin:0}.filter-close-btn{background:#0000;border:none;color:#8b4513;color:var(--color-earth-brown-dark,#8b4513);cursor:pointer;font-size:1.2rem;line-height:1;padding:.25rem}.filter-close-btn:hover{color:gold;color:var(--color-gold-dark,gold)}.filter-content{flex:1 1;overflow-y:auto;padding:1.5rem}.filter-section{margin-bottom:1.5rem}.filter-section:last-child{margin-bottom:0}.filter-section-title{color:#8b4513;color:var(--color-earth-brown-dark,#8b4513);font-family:Cinzel,serif;font-size:.95rem;font-weight:700;margin-bottom:.75rem}.filter-checkbox-group{display:flex;flex-direction:column;gap:.75rem}.filter-checkbox{align-items:center;cursor:pointer;display:flex;gap:.75rem}.filter-checkbox input[type=checkbox]{accent-color:#8b4513;accent-color:var(--color-earth-brown-dark,#8b4513);cursor:pointer;height:18px;width:18px}.filter-checkbox label{color:#8b4513;color:var(--color-earth-brown-dark,#8b4513);cursor:pointer;font-size:.95rem;margin:0;-webkit-user-select:none;user-select:none}.filter-checkbox input[type=checkbox]:hover+label{color:gold;color:var(--color-gold-dark,gold)}.clear-filters-btn{background-color:#8b4513;background-color:var(--color-earth-brown-dark,#8b4513);border:none;border-radius:4px;color:#f4e4bc;color:var(--color-parchment-light,#f4e4bc);cursor:pointer;font-family:Cinzel,serif;margin-top:1rem;padding:.75rem 1rem;transition:background-color .2s}.clear-filters-btn:hover{background-color:sienna;background-color:var(--color-earth-brown-medium,sienna)}.filter-attribution{background:#a0522d1a;border-top:2px solid sienna;border-top:2px solid var(--color-earth-brown-medium,sienna);color:#8b4513;color:var(--color-earth-brown-dark,#8b4513);flex-shrink:0;font-family:Lora,serif;font-size:.75rem;line-height:1.5;margin-top:1rem;padding:1.5rem}.filter-attribution p{margin:.5rem 0}.filter-attribution em{display:inline;font-weight:600}.filter-actions{margin-bottom:auto;padding:0 1rem}.map-main-content{bottom:0;display:flex;flex-direction:column;left:0;overflow:visible;position:absolute;right:0;top:0;z-index:1}.filter-toggle-btn{align-items:center;animation:slideInUp .6s ease-out;background-color:var(--earth-brown);border:3px solid var(--gold);border-radius:50%;bottom:1.5rem;box-shadow:0 6px 16px #8b451366,0 0 15px #daa52033;color:var(--parchment-light);cursor:pointer;display:flex;font-size:1.5rem;height:50px;justify-content:center;left:1.5rem;position:fixed;transition:all .3s ease;width:50px;z-index:700}.filter-toggle-btn:hover{background-color:var(--earth-brown-light);box-shadow:0 8px 24px #8b451380,0 0 20px #daa52066;transform:scale(1.15)}.filter-toggle-btn:active{transform:scale(.95)}.map-container{flex:1 1;overflow:hidden}.map-container,.middle-earth-map-container{height:100%;width:100%}.middle-earth-map-container .leaflet-container{height:100%!important;width:100%!important}.quest-details-card{animation:slideUp .4s cubic-bezier(.34,1.56,.64,1);background:linear-gradient(135deg,#f4e4bc,#e8d5b7);border:3px solid #8b4513;border-radius:12px;bottom:1.5rem;box-shadow:0 20px 40px #0006,inset 0 1px 0 #fff3,0 0 30px #daa52033;display:flex;flex-direction:column;max-height:70vh;overflow:hidden;pointer-events:auto;position:fixed;right:1.5rem;width:380px;z-index:600}.map-character-panel{max-height:calc(100vh - 8rem);overflow-y:auto;position:fixed;right:1.5rem;top:6rem;width:min(420px,38vw);z-index:650}.quest-details-card:before{background:linear-gradient(90deg,var(--dark-red),var(--gold),var(--forest-green),var(--dark-red));content:"";height:3px;left:0;position:absolute;right:0;top:0;z-index:0}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.quest-details-header{align-items:center;background:linear-gradient(90deg,#8b45131a,#0000);border-bottom:2px solid sienna;display:flex;flex-shrink:0;gap:1rem;justify-content:space-between;padding:1.5rem;position:relative;z-index:1}.quest-details-header h2{font-size:1.3rem;font-weight:700}.quest-details-header h2,.quest-title{word-wrap:break-word;color:#8b4513;flex:1 1;font-family:Cinzel,serif;margin:0}.quest-title{color:var(--color-earth-brown-dark,#8b4513);font-size:1.2rem}.close-btn{align-items:center;background:none;border:none;color:#8b4513;cursor:pointer;display:flex;flex-shrink:0;font-size:1.5rem;height:28px;justify-content:center;line-height:1;min-height:28px;min-width:28px;padding:0;position:relative;transition:color .2s;width:28px;z-index:10}.close-btn:hover{color:gold}.quest-details-body{color:#3d2817;flex:1 1;overflow-y:auto;padding:1.5rem;position:relative;z-index:1}.quest-meta-info{display:flex;flex-direction:column;gap:.75rem;margin-bottom:1rem}.quest-badges-map{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1rem}.quest-chip{align-items:center;border-radius:var(--quest-chip-radius);display:inline-flex;font-size:.84rem;font-weight:600;gap:.35rem;line-height:1;min-height:var(--quest-chip-height);padding:.35rem var(--quest-chip-padding-x)}.quest-chip-icon{font-size:.95em;line-height:1}.quest-chip-label{line-height:1.2}.quest-type-badge{background:linear-gradient(135deg,var(--forest-green-light) 0,var(--forest-green) 100%);box-shadow:0 2px 6px #2d50164d;color:var(--parchment-light)}.quest-status{color:#fff}.quest-status,.quest-status-not_yet_begun,.quest-status-pending{background:var(--status-not-yet-begun)}.quest-status-in_progress,.quest-status-the_road_goes_ever_on{background:var(--status-road-goes-on)}.quest-status-completed,.quest-status-it_is_done{background:var(--status-it-is-done)}.quest-status-blocked,.quest-status-the_shadow_falls{background:var(--status-shadow-falls)}.priority-badge{align-items:center;background:#8b45131a;border:1px solid #8b451366;border-radius:var(--quest-chip-radius);color:var(--earth-brown);display:inline-flex;font-size:.8rem;font-weight:600;min-height:var(--quest-chip-height);padding:.35rem .75rem}.quest-info-row{color:#3d2817;display:flex;flex-direction:column;gap:.25rem}.quest-info-label{color:sienna;color:var(--color-earth-brown-medium,sienna);font-family:Cinzel,serif;font-size:.85rem;font-weight:700}.quest-info-value{color:#8b4513;color:var(--color-earth-brown-dark,#8b4513);font-size:.95rem}.quest-description{background-color:#a0522d1a;border-left:3px solid sienna;font-size:.9rem;margin:1rem 0}.quest-description,.quest-quote{border-radius:4px;color:#3d2817;font-style:italic;line-height:1.5;padding:.75rem}.quest-quote{background-color:#daa5201a;border-left:3px solid gold;font-size:.85rem;margin:1rem 0 0}.quest-details-actions{grid-gap:var(--quest-action-gap);background:linear-gradient(90deg,#0000,#8b45130d);border-top:2px solid sienna;display:grid;flex-shrink:0;gap:var(--quest-action-gap);grid-template-columns:repeat(2,minmax(0,1fr));padding:1.5rem;position:relative;z-index:1}.map-action-btn{align-items:center;display:inline-flex;justify-content:center;min-height:var(--quest-action-min-height);text-align:center;width:100%}@media (max-width:768px){.filter-sidebar{width:280px}.quest-details-card{bottom:1rem;right:1rem;width:300px}.filter-toggle-btn{font-size:1.3rem;height:45px;width:45px}}@media (max-width:480px){.filter-sidebar{width:100%}.quest-details-card{max-height:50vh;max-width:none;width:calc(100% - 2rem)}.quest-details-actions{grid-template-columns:1fr}.filter-toggle-btn{bottom:1rem;left:1rem}}.map-attribution{display:none}:root{--parchment-light:#f4e4bc;--parchment:#e8d5b7;--parchment-dark:#d4c0a4;--earth-brown:#8b4513;--earth-brown-light:sienna;--forest-green:#2d5016;--forest-green-light:#3d6b1f;--gold:#daa520;--gold-light:gold;--gold-bright:#fff8dc;--dark-red:#8b0000;--dark-red-light:brown;--deep-blue:#1a1f2e;--deep-blue-light:#2c3e50;--status-not-yet-begun:#6b7280;--status-road-goes-on:#f59e0b;--status-it-is-done:#10b981;--status-shadow-falls:#dc2626;--priority-critical:#dc2626;--priority-important:#f59e0b;--priority-standard:#6b7280;--gold-glow:#daa52099;--arcane-glow:#b19cd9;--arcane-glow-rgba:#b19cd966;--fire-glow:#ff6b35;--fire-glow-rgba:#ff6b354d;--success-glow:#51cf66;--success-glow-rgba:#51cf664d;--danger-glow:#f44;--danger-glow-rgba:#ff44444d;--earth-brown-fade:#8b45131a;--earth-brown-border:#8b451333;--parchment-overlay:#f4e4bcf2;--dark-overlay:#1a1f2ed9;--text-shadow-dm:1px 1px 2px #daa5204d;--text-shadow-glow:0 0 10px #daa52066;--quest-chip-height:2rem;--quest-chip-radius:9999px;--quest-chip-padding-x:0.75rem;--quest-action-min-height:2.75rem;--quest-action-gap:0.65rem}*{box-sizing:border-box}body{background:linear-gradient(135deg,#fbf3e6,#f0e5d8 50%,#f5ede2);background-attachment:fixed;color:#8b4513;color:var(--earth-brown);font-family:Lora,serif;font-weight:400;letter-spacing:.3px;line-height:1.6;margin:0;min-height:100vh;position:relative}body:before{background:radial-gradient(ellipse at 20% 50%,#d4a43708 0,#0000 50%),radial-gradient(ellipse at 80% 80%,#2d501605 0,#0000 50%);bottom:0;content:"";left:0;pointer-events:none;position:fixed;right:0;top:0;z-index:0}h1,h2,h3,h4,h5,h6{color:#1a1f2e;color:var(--deep-blue);font-family:Cinzel,serif;font-weight:700;letter-spacing:1px;margin-top:0;text-shadow:0 2px 4px #daa52033}h1{font-size:2.5rem;letter-spacing:2px;text-shadow:1px 1px 3px #8a2b2b26,0 0 20px #d4a4371a;text-transform:uppercase}h1,h2{color:var(--forest-dark);font-weight:700}h2{font-size:1.875rem;letter-spacing:1.5px}h3{color:#1a1f2e;color:var(--deep-blue);font-size:1.5rem;letter-spacing:1px}h3,h4{font-weight:600}h4{font-size:1.25rem}h5,h6{font-size:1rem;font-weight:600}.app,.app-loading{min-height:100vh}.app-loading{align-items:center;color:#fff;display:flex;font-size:1.5rem;justify-content:center}.loading-spinner{animation:pulse 1.5s ease-in-out infinite}.btn{border:none;border-radius:5px;box-shadow:0 4px 12px #00000026;cursor:pointer;font-family:Cinzel,serif;font-size:1rem;font-weight:600;letter-spacing:.5px;overflow:hidden;padding:10px 20px;position:relative;text-transform:uppercase;transition:all .3s ease}.btn:before{animation:none;background:linear-gradient(90deg,#0000,#fff3,#0000);bottom:0;content:"";left:0;pointer-events:none;position:absolute;right:0;top:0}.btn:hover:before{animation:shimmer .6s ease-in-out}.btn-primary{background-color:#daa520;background-color:var(--gold);border:2px solid #8b4513;border:2px solid var(--earth-brown);box-shadow:0 4px 12px #daa52066;color:#1a1f2e;color:var(--deep-blue)}.btn-primary:hover{background-color:gold;background-color:var(--gold-light);box-shadow:0 6px 20px #daa52099,0 0 15px #daa5204d;transform:translateY(-2px)}.btn-primary:active{box-shadow:0 2px 8px #daa52066;transform:translateY(0)}.btn-secondary{background-color:sienna;background-color:var(--earth-brown-light);border:2px solid #8b4513;border:2px solid var(--earth-brown);box-shadow:0 4px 12px #8b45134d;color:#e8d5b7;color:var(--parchment)}.btn-secondary:hover{background-color:#8b4513;background-color:var(--earth-brown);box-shadow:0 6px 20px #8b451380,0 0 15px #8b451333;transform:translateY(-2px)}.btn-secondary:active{box-shadow:0 2px 8px #8b45134d;transform:translateY(0)}.btn-danger{background-color:#8b0000;background-color:var(--dark-red);border:2px solid brown;border:2px solid var(--dark-red-light);box-shadow:0 4px 12px #8b00004d;color:#e8d5b7;color:var(--parchment)}.btn-danger:hover{background-color:brown;background-color:var(--dark-red-light);box-shadow:0 6px 20px #8b000080,0 0 15px #dc26264d;transform:translateY(-2px)}.btn-danger:active{box-shadow:0 2px 8px #8b00004d;transform:translateY(0)}.form-group{margin-bottom:1.5rem}.form-label{display:block;font-family:Cinzel,serif;font-size:.9rem;font-weight:600;letter-spacing:.5px;margin-bottom:.5rem;text-transform:uppercase}.form-input,.form-label{color:#1a1f2e;color:var(--deep-blue)}.form-input{background-color:#e8d5b7;background-color:var(--parchment);border:2px solid #8b4513;border:2px solid var(--earth-brown);border-radius:4px;box-shadow:inset 0 2px 4px #8b45131a;font-family:Lora,serif;font-size:1rem;padding:.75rem;transition:all .3s ease;width:100%}.form-input:focus{background-color:#f4e4bc;background-color:var(--parchment-light);border-color:#daa520;border-color:var(--gold);box-shadow:inset 0 2px 4px #8b45131a,0 0 0 3px #daa52033,0 0 10px #daa52026;outline:none}.form-input::placeholder{color:#8b451380;font-style:italic}.card{background:linear-gradient(135deg,#e8d5b7,#f4e4bc);background:linear-gradient(135deg,var(--parchment) 0,var(--parchment-light) 100%);border:2px solid #8b4513;border:2px solid var(--earth-brown);border-radius:8px;box-shadow:0 4px 16px #8b451333,0 0 1px #8b45130d;margin-bottom:1rem;overflow:hidden;padding:1.5rem;position:relative;transition:all .3s ease}.card:before{background:repeating-linear-gradient(90deg,#0000,#0000 2px,#8b451308 0,#8b451308 4px);bottom:0;content:"";left:0;pointer-events:none;position:absolute;right:0;top:0}.card:hover{box-shadow:0 8px 24px #8b45134d,0 0 1px #8b45130d,0 0 15px #daa5201a;transform:translateY(-2px)}.card-header{align-items:center;border-bottom:2px solid #8b4513;border-bottom:2px solid var(--earth-brown);display:flex;justify-content:space-between;margin-bottom:1rem;padding-bottom:1rem}.card-title{color:#1a1f2e;color:var(--deep-blue);font-family:Cinzel,serif;font-size:1.5rem;font-weight:700;letter-spacing:1px;margin:0}.container{margin:0 auto;max-width:1200px;padding:2rem}.navbar{align-items:center;background:linear-gradient(180deg,#f4e4bc,#e8d5b7);background:linear-gradient(180deg,var(--parchment-light) 0,var(--parchment) 100%);border-bottom:3px solid #8b4513;border-bottom:3px solid var(--earth-brown);box-shadow:0 4px 12px #8b45134d;display:flex;justify-content:space-between;padding:1rem 2rem}.navbar-brand{color:#1a1f2e;color:var(--deep-blue);font-family:Cinzel,serif;font-size:1.5rem;font-weight:700;text-decoration:none;text-shadow:2px 2px 4px #daa5204d;transition:all .3s ease}.navbar-brand:hover{color:#8b4513;color:var(--earth-brown);text-shadow:2px 2px 6px #daa52080}.navbar-nav{align-items:center;display:flex;gap:1rem}.nav-link{border:2px solid #0000;border-radius:4px;color:#1a1f2e;color:var(--deep-blue);font-family:Lora,serif;padding:.75rem 1rem;text-decoration:none;transition:all .3s ease}.nav-link:hover{background-color:#daa52033;border-bottom-color:#daa520;border-bottom-color:var(--gold)}.nav-link.active{border-bottom:2px solid #daa520;border-bottom:2px solid var(--gold);color:#daa520;color:var(--gold);font-weight:600}.error-message{background-color:#f8d7da;border-left:4px solid #8b0000;border-left:4px solid var(--dark-red);box-shadow:0 2px 8px #dc262633;color:#721c24}.error-message,.success-message{border-radius:4px;margin-bottom:1rem;padding:1rem}.success-message{background-color:#d4edda;border-left:4px solid #10b981;border-left:4px solid var(--status-it-is-done);box-shadow:0 2px 8px #10b98133;color:#155724}.btn-sm{font-size:.85rem;line-height:1.2;max-width:none;min-width:auto;padding:.5rem .75rem;white-space:normal;word-break:break-word}@media (max-width:1024px){.container{padding:1.5rem}h1{font-size:2rem}h2{font-size:1.5rem}.navbar{padding:.75rem 1.5rem}.btn{font-size:.95rem;padding:8px 16px}}@media (max-width:768px){body{font-size:14px}.container{padding:1rem}h1{font-size:1.75rem;letter-spacing:.5px}h2{font-size:1.25rem}h3{font-size:1.1rem}.navbar{flex-direction:column;gap:1rem;padding:1rem}.navbar-brand{font-size:1.25rem}.navbar-nav{flex-wrap:wrap;justify-content:space-between;width:100%}.nav-link{padding:.5rem .75rem}.btn,.nav-link{font-size:.9rem}.btn{padding:8px 12px}.card{margin-bottom:1rem;padding:1rem}.card-title{font-size:1.25rem}.form-input,.form-label{font-size:.95rem}.form-textarea{min-height:80px}.btn,.form-input,.nav-link,input[type=checkbox]{min-height:44px;min-width:44px}}@media (max-width:480px){body{font-size:13px}.container{padding:.75rem}h1{font-size:1.5rem;letter-spacing:0}h2{font-size:1.1rem}h3{font-size:1rem}.navbar{padding:.75rem}.navbar-brand{font-size:1.1rem}.navbar-nav{gap:.5rem}.nav-link{padding:.4rem .6rem}.btn,.nav-link{font-size:.85rem}.btn{min-width:44px;padding:8px 10px}.card{border-radius:6px;padding:.75rem}.card-title{font-size:1.1rem}.form-group{margin-bottom:1rem}.form-label{font-size:.85rem}.form-input{font-size:.9rem;padding:.6rem}.quest-list,.stats-grid{gap:1rem;grid-template-columns:1fr}.navbar-nav{flex-direction:column;width:100%}.nav-link{text-align:center;width:100%}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideInLeft{0%{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}@keyframes slideInRight{0%{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@keyframes slideInDown{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}@keyframes slideInUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes scaleIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes shimmer{0%{background-position:-1000px 0}to{background-position:1000px 0}}@keyframes arcaneShimmer{0%{background-position:-200%}50%{background-position:200%}to{background-position:-200%}}@keyframes goldGlow{0%,to{box-shadow:0 0 5px #daa52033,0 0 10px #daa5201a}50%{box-shadow:0 0 15px #daa52080,0 0 25px #daa5204d}}@keyframes dangerGlow{0%,to{box-shadow:0 0 5px #dc262633,0 0 10px #dc26261a}50%{box-shadow:0 0 15px #dc262680,0 0 25px #dc26264d}}@keyframes successGlow{0%,to{box-shadow:0 0 5px #51cf6633,0 0 10px #51cf661a}50%{box-shadow:0 0 15px #51cf6680,0 0 25px #51cf664d}}@keyframes textGlow{0%,to{text-shadow:0 0 5px #daa52033}50%{text-shadow:0 0 15px #daa52080,0 0 25px #daa5204d}}@keyframes hoverLift{0%{transform:translateY(0)}to{transform:translateY(-4px)}}@keyframes hoverScale{0%{transform:scale(1)}to{transform:scale(1.02)}}@keyframes ripple{0%{opacity:1;transform:scale(0)}to{opacity:0;transform:scale(4)}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@keyframes quickPulse{0%,to{transform:scale(1)}50%{transform:scale(1.05)}}@keyframes bounce{0%,to{transform:translateY(0)}50%{transform:translateY(-10px)}}@keyframes shake{0%,to{transform:translateX(0)}10%,30%,50%,70%,90%{transform:translateX(-5px)}20%,40%,60%,80%{transform:translateX(5px)}}@keyframes countUp{0%{opacity:0}to{opacity:1}}@keyframes parallaxFloat{0%,to{transform:translateY(0)}50%{transform:translateY(-5px)}}@keyframes cascadeReveal{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes quoteEntrance{0%{opacity:0;transform:scale(.8) rotate(-2deg)}to{opacity:1;transform:scale(1) rotate(0deg)}}@keyframes colorShift{0%,to{color:currentColor}50%{color:var(--gold)}}@keyframes focusGlow{0%{box-shadow:0 0 0 0 #daa52066}70%{box-shadow:0 0 0 10px #daa52000}to{box-shadow:0 0 0 0 #daa52000}}@keyframes spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}@keyframes skeletonPulse{0%,to{background-color:var(--parchment)}50%{background-color:var(--parchment-light)}}.page-enter{animation:fadeIn .6s ease-out}.page-enter-left{animation:slideInLeft .6s ease-out}.page-enter-right{animation:slideInRight .6s ease-out}.page-enter-down{animation:slideInDown .5s ease-out}.page-enter-up{animation:slideInUp .5s ease-out}.modal-enter{animation:scaleIn .3s ease-out}.card-elevated:hover{transform:translateY(-4px);transition:transform .3s ease,box-shadow .3s ease}.btn-ripple{overflow:hidden;position:relative}.btn-ripple:after{animation:ripple .6s ease-out;background:#ffffff4d;border-radius:50%;content:"";height:10px;left:50%;pointer-events:none;position:absolute;top:50%;transform:translate(-50%,-50%);width:10px}.glow-gold{animation:goldGlow 3s ease-in-out infinite}.glow-danger{animation:dangerGlow 2s ease-in-out infinite}.glow-success{animation:successGlow 2s ease-in-out infinite}.text-glow{animation:textGlow 3s ease-in-out infinite}.pulse{animation:pulse 2s ease-in-out infinite}.quick-pulse{animation:quickPulse .4s ease-out}.shake{animation:shake .5s}.bounce-in{animation:bounce .6s ease-out}.loading-shimmer{animation:arcaneShimmer 2s infinite;background-size:200% 100%}.parallax-float{animation:parallaxFloat 4s ease-in-out infinite}.cascade-reveal{animation:cascadeReveal .6s ease-out}.cascade-item{animation:cascadeReveal .6s ease-out backwards}.cascade-item:first-child{animation-delay:0s}.cascade-item:nth-child(2){animation-delay:.1s}.cascade-item:nth-child(3){animation-delay:.2s}.cascade-item:nth-child(4){animation-delay:.3s}.cascade-item:nth-child(5){animation-delay:.4s}.cascade-item:nth-child(n+6){animation-delay:.5s}@media (prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}} +/*# sourceMappingURL=main.787d33f1.css.map*/ \ No newline at end of file diff --git a/sut/frontend/build/static/css/main.787d33f1.css.map b/sut/frontend/build/static/css/main.787d33f1.css.map new file mode 100644 index 0000000..e33a55e --- /dev/null +++ b/sut/frontend/build/static/css/main.787d33f1.css.map @@ -0,0 +1 @@ +{"version":3,"file":"static/css/main.787d33f1.css","mappings":"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CADP,iBAAkB,CAElB,KACA,CACD,mBACC,eACA,CACD,0DAMG,sBAAuB,CAHzB,wBAAyB,CAEjB,gBAER,CAED,yBACC,gBACD,CAEA,8BACC,yCACA,CAED,wCAEC,aAAc,CACd,4BAA6B,CAF7B,YAGA,CACD,4CAEC,aACA,CAGD,6CAEC,yBAA2B,CAD3B,wBAEA,CACD,8MAMC,yBAA2B,CAD3B,wBAA0B,CAG1B,SAAU,CADV,UAEA,CAED,oCAEC,2BACD,CAEA,sCAEC,wBACA,CACD,sCAGC,iBAAkB,CAClB,uBACD,CACA,yDAEC,iBACD,CACA,mBACC,uCACD,CACA,qBACC,+CACD,CACA,cACC,cAAe,CACf,iBACA,CACD,qBACC,kBACA,CACD,kBAIM,qBAAsB,CAF3B,QAAS,CADT,OAAQ,CAIR,WACA,CAED,0BACC,qBACA,CAED,cAAwB,WAAc,CAEtC,mBAAwB,WAAc,CACtC,sBAAwB,WAAc,CACtC,qBAAwB,WAAc,CACtC,qBAAwB,WAAc,CACtC,sBAA0B,WAAc,CACxC,oBAAwB,WAAc,CAEtC,yBAA2B,WAAc,CACzC,sBAA2B,WAAc,CAEzC,mBAEC,UAAW,CADX,SAEA,CACD,MACC,0BAA2B,CAC3B,oBAAqB,CACrB,iBACA,CAKD,iBAGC,6BAA8B,CAC9B,mBAAoB,CAHpB,iBAAkB,CAClB,WAGA,CACD,6BAIC,mBAAoB,CAFpB,iBAAkB,CAClB,YAEA,CACD,aACC,KACA,CACD,eACC,OACA,CACD,gBACC,QACA,CACD,cACC,MACA,CACD,iBAEC,UAAW,CADX,UAEA,CACD,gCACC,WACA,CACD,8BACC,eACA,CACD,iCACC,kBACA,CACD,+BACC,gBACA,CACD,gCACC,iBACA,CAKD,kCACC,SAAU,CAGF,6BACR,CACD,oDACC,SACA,CACD,uBAGS,oBACR,CACD,0BACC,qBACD,CAEA,0CAGS,iDACR,CACD,iEAIS,eACR,CAED,sCACC,iBACA,CAKD,qBACC,cACA,CACD,cAGC,WACA,CACD,2DAEC,gBACA,CACD,qCAEC,WACA,CACD,iIAGC,WAAY,CAGZ,eACA,CAGD,gHAKC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CACF,CAGA,uBACE,GACE,SAAU,CACV,2BACF,CACA,GACE,SAAU,CACV,uBACF,CACF,CAGA,wBACE,GACE,SAAU,CACV,0BACF,CACA,GACE,SAAU,CACV,uBACF,CACF,CAGA,uBACE,GACE,SAAU,CACV,2BACF,CACA,GACE,SAAU,CACV,uBACF,CACF,CAGA,qBACE,GACE,SAAU,CACV,0BACF,CACA,GACE,SAAU,CACV,uBACF,CACF,CAGA,mBACE,GACE,SAAU,CACV,oBACF,CACA,GACE,SAAU,CACV,kBACF,CACF,CAKA,mBACE,GACE,6BACF,CACA,GACE,4BACF,CACF,CAGA,yBACE,GACE,yBACF,CACA,IACE,wBACF,CACA,GACE,yBACF,CACF,CAGA,oBACE,MACE,+CAEF,CACA,IACE,gDAEF,CACF,CAGA,sBACE,MACE,+CAEF,CACA,IACE,gDAEF,CACF,CAGA,uBACE,MACE,+CAEF,CACA,IACE,gDAEF,CACF,CAGA,oBACE,MACE,6BACF,CACA,IACE,iDAEF,CACF,CAKA,qBACE,GACE,uBACF,CACA,GACE,0BACF,CACF,CAGA,sBACE,GACE,kBACF,CACA,GACE,qBACF,CACF,CAKA,kBACE,GAEE,SAAU,CADV,kBAEF,CACA,GAEE,SAAU,CADV,kBAEF,CACF,CAGA,iBACE,MACE,SACF,CACA,IACE,UACF,CACF,CAGA,sBACE,MACE,kBACF,CACA,IACE,qBACF,CACF,CAGA,kBACE,MACE,uBACF,CACA,IACE,2BACF,CACF,CAGA,iBACE,MACE,uBACF,CACA,oBACE,0BACF,CACA,gBACE,yBACF,CACF,CAKA,mBACE,GACE,SACF,CACA,GACE,SACF,CACF,CAKA,yBACE,MACE,uBACF,CACA,IACE,0BACF,CACF,CAKA,yBACE,GACE,SAAU,CACV,0BACF,CACA,GACE,SAAU,CACV,uBACF,CACF,CAGA,yBACE,GACE,SAAU,CACV,iCACF,CACA,GACE,SAAU,CACV,+BACF,CACF,CAKA,sBACE,MACE,kBACF,CACA,IACE,iBACF,CACF,CAGA,qBACE,GACE,4BACF,CACA,IACE,+BACF,CACA,GACE,4BACF,CACF,CAKA,gBACE,GACE,sBACF,CACA,GACE,uBACF,CACF,CAGA,yBACE,MACE,iCACF,CACA,IACE,uCACF,CACF,CAIA,YACE,6BACF,CAEA,iBACE,kCACF,CAEA,kBACE,mCACF,CAEA,iBACE,kCACF,CAEA,eACE,gCACF,CAEA,aACE,8BACF,CAGA,qBACE,0BAA2B,CAC3B,iDACF,CAGA,YAEE,eAAgB,CADhB,iBAEF,CAEA,kBAUE,6BAA+B,CAH/B,oBAAoC,CACpC,iBAAkB,CAPlB,UAAW,CAKX,WAAY,CAFZ,QAAS,CAOT,mBAAoB,CATpB,iBAAkB,CAClB,OAAQ,CAMR,8BAAgC,CAJhC,UAOF,CAGA,WACE,0CACF,CAEA,aACE,4CACF,CAEA,cACE,6CACF,CAGA,WACE,0CACF,CAGA,OACE,uCACF,CAGA,aACE,iCACF,CAGA,OACE,mBACF,CAGA,WACE,6BACF,CAGA,iBACE,mCAAoC,CACpC,yBACF,CAGA,gBACE,+CACF,CAGA,gBACE,oCACF,CAGA,cACE,8CACF,CAEA,0BAA6B,kBAAqB,CAClD,2BAA6B,mBAAuB,CACpD,2BAA6B,mBAAuB,CACpD,2BAA6B,mBAAuB,CACpD,2BAA6B,mBAAuB,CACpD,6BAA+B,mBAAuB,CAItD,uCACE,EACE,kCAAqC,CACrC,qCAAuC,CACvC,mCACF,CACF","sources":["index.css","components/AppHeader.css","components/QuestForm.css","../node_modules/leaflet/dist/leaflet.css","../node_modules/leaflet.markercluster/dist/MarkerCluster.css","components/MiddleEarthMap.css","pages/MapPage.css","App.css","animations.css"],"sourcesContent":["@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700;900&family=Lora:wght@400;500;600;700&display=swap');\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n @apply font-readable text-text-primary bg-background-primary;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n min-height: 100vh;\n }\n\n h1, h2, h3, h4, h5, h6 {\n @apply font-epic;\n }\n\n h1 {\n @apply text-epic;\n }\n\n h2 {\n @apply text-4xl;\n }\n\n h3 {\n @apply text-3xl;\n }\n\n button {\n @apply font-medium cursor-pointer transition-all duration-300;\n }\n\n input, textarea, select {\n @apply transition-all duration-300;\n }\n\n code {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;\n }\n}\n\n@layer components {\n /* Epic Button styles */\n .btn-epic {\n @apply px-6 py-3 bg-gold text-text-primary font-epic rounded-lg hover:bg-gold-light shadow-lg hover:shadow-gold transition-all duration-300 hover:scale-105 active:scale-95;\n }\n\n .btn-secondary {\n @apply px-6 py-3 bg-forest text-parchment-light font-medium rounded-lg hover:bg-forest-light shadow-md hover:shadow-lg transition-all duration-300;\n }\n\n .btn-danger {\n @apply px-6 py-3 bg-red-600 text-white font-medium rounded-lg hover:bg-red-700 shadow-md hover:shadow-lg transition-all duration-300;\n }\n\n .btn-small {\n @apply px-3 py-1 text-sm font-medium rounded transition-all duration-300;\n }\n\n /* Card styles */\n .card-parchment {\n @apply bg-gradient-to-br from-parchment-light to-parchment rounded-lg p-6 shadow-md hover:shadow-lg transition-shadow duration-300 border border-gold-dark/20;\n }\n\n .card-dark {\n @apply bg-background-secondary rounded-lg p-6 shadow-lg border border-gold-dark/30;\n }\n\n /* Badge styles */\n .badge-status {\n @apply inline-block px-3 py-1 rounded-full text-xs font-semibold;\n }\n\n .badge-ready {\n @apply badge-status bg-ready/20 text-ready;\n }\n\n .badge-inprogress {\n @apply badge-status bg-in-progress/20 text-in-progress;\n }\n\n .badge-blocked {\n @apply badge-status bg-blocked/20 text-blocked;\n }\n\n .badge-pending {\n @apply badge-status bg-pending/20 text-pending;\n }\n\n /* Form inputs */\n .input-epic {\n @apply w-full px-4 py-2 bg-background-tertiary border border-gold-dark/50 rounded-lg text-parchment-light focus:outline-none focus:border-gold focus:ring-2 focus:ring-gold/20 transition-all duration-300;\n }\n\n .input-epic::placeholder {\n @apply text-text-secondary;\n }\n\n /* Animation utilities */\n .animate-glow {\n @apply animate-pulse;\n }\n\n .glow-gold {\n text-shadow: 0 0 10px rgba(218, 165, 32, 0.5);\n box-shadow: 0 0 20px rgba(218, 165, 32, 0.3);\n }\n\n .glow-dark-magic {\n text-shadow: 0 0 15px rgba(199, 37, 78, 0.6);\n box-shadow: 0 0 25px rgba(199, 37, 78, 0.3);\n }\n}\n\n","/* ==========================================================================\n AppHeader — LOTR Fantasy Modern Theme\n Palette: forest greens (#0F2409 → #2D5016), gold (#DAA520 / #FFD700),\n parchment (#E8D5B7 / #F4E4BC), earth brown (#8B4513)\n ========================================================================== */\n\n/* ── HEADER CONTAINER ── */\n.lotr-header {\n position: sticky;\n top: 0;\n z-index: 100;\n background: linear-gradient(\n 180deg,\n #1A3209 0%,\n #1B3A0D 30%,\n #2D5016 50%,\n #1F3A0E 80%,\n #162608 100%\n );\n box-shadow:\n inset 0 1px 0 rgba(255, 215, 0, 0.08),\n inset 0 -1px 0 rgba(0, 0, 0, 0.3),\n 0 6px 20px rgba(0, 0, 0, 0.5);\n position: relative;\n}\n\n/* Atmospheric shimmer overlay — elegant fantasy texture */\n.lotr-header::before {\n content: '';\n position: absolute;\n inset: 0;\n background-image:\n radial-gradient(circle at 10% 50%, rgba(218, 165, 32, 0.06) 0%, transparent 50%),\n radial-gradient(circle at 90% 50%, rgba(218, 165, 32, 0.06) 0%, transparent 50%),\n radial-gradient(circle at 50% 0%, rgba(255, 215, 0, 0.02) 0%, transparent 60%);\n pointer-events: none;\n z-index: 0;\n animation: shimmerFlow 8s ease-in-out infinite;\n}\n\n@keyframes shimmerFlow {\n 0%, 100% { opacity: 0.6; }\n 50% { opacity: 1; }\n}\n\n/* ── top GOLD SHIMMER ACCENT LINE ── */\n.lotr-header__top-accent {\n height: 1px;\n background: linear-gradient(\n to right,\n transparent 0%,\n #A67C52 20%,\n #FFD700 50%,\n #A67C52 80%,\n transparent 100%\n );\n opacity: 0.85;\n box-shadow: 0 0 8px rgba(255, 215, 0, 0.3);\n}\n\n/* ── INNER LAYOUT ── */\n.lotr-header__inner {\n position: relative;\n z-index: 1;\n max-width: 90rem;\n margin: 0 auto;\n padding: 0.75rem 1.75rem;\n display: flex;\n align-items: center;\n gap: 1.5rem;\n min-width: 0;\n}\n\n/* ── BRAND ── */\n.lotr-header__brand {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n flex-shrink: 0;\n text-decoration: none;\n min-width: 0;\n max-width: 380px;\n}\n\n.lotr-header__ring-wrap {\n display: flex;\n align-items: center;\n filter: drop-shadow(0 0 8px rgba(218, 165, 32, 0.6));\n transition: filter 0.4s ease;\n cursor: default;\n}\n\n.lotr-header__ring-wrap:hover {\n filter: drop-shadow(0 0 16px rgba(255, 215, 0, 0.9));\n}\n\n/* Slow pulse glow on the ring SVG */\n.lotr-header__ring-svg {\n animation: ringPulse 5s ease-in-out infinite;\n}\n\n@keyframes ringPulse {\n 0%, 100% {\n opacity: 1;\n filter: drop-shadow(0 0 4px rgba(218, 165, 32, 0.5));\n }\n 50% {\n opacity: 0.95;\n filter: drop-shadow(0 0 12px rgba(255, 215, 0, 0.85));\n }\n}\n\n\n/* Brand text: more prominent, fantasy style */\n.lotr-header__brand-text {\n display: flex;\n flex-direction: column;\n line-height: 1.05;\n gap: 0.05em;\n min-width: 0;\n flex: 0 1 auto;\n}\n\n\n.lotr-header__subtitle {\n font-family: 'Cinzel', serif;\n font-size: 0.7rem;\n font-weight: 600;\n letter-spacing: 0.35em;\n text-transform: uppercase;\n color: #FFD700;\n opacity: 0.9;\n user-select: none;\n text-shadow:\n 0 1px 2px rgba(0, 0, 0, 0.7),\n 0 0 8px rgba(218, 165, 32, 0.3);\n}\n\n\n.lotr-header__page-title {\n font-family: 'Uncial Antiqua', 'Cinzel', serif;\n font-size: 1.65rem;\n font-weight: 800;\n color: #FFF8DC;\n letter-spacing: 0.08em;\n margin: 0;\n line-height: 1.1;\n text-shadow:\n 0 2px 4px rgba(0, 0, 0, 0.6),\n 0 0 12px rgba(218, 165, 32, 0.25),\n 0 0 24px rgba(218, 165, 32, 0.1);\n filter: drop-shadow(0 1px 3px #1A320966);\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* ── DIVIDER between brand and nav ── */\n.lotr-header__brand::after {\n content: '';\n display: block;\n width: 1px;\n height: 2.2rem;\n background: linear-gradient(\n to bottom,\n transparent 0%,\n rgba(255, 215, 0, 0.3) 20%,\n rgba(255, 215, 0, 0.5) 50%,\n rgba(255, 215, 0, 0.3) 80%,\n transparent 100%\n );\n margin-left: 1.25rem;\n margin-right: 0.25rem;\n flex-shrink: 0;\n box-shadow: 0 0 8px rgba(218, 165, 32, 0.2);\n}\n\n/* ── NAVIGATION ── */\n.lotr-header__nav {\n display: flex;\n align-items: center;\n gap: 0.1rem;\n flex: 1;\n justify-content: center;\n}\n\n/* ── NAV LINK ── */\n.lotr-nav-link {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n padding: 0.45rem 1rem;\n border-radius: 0.5rem;\n font-family: 'Lora', serif;\n font-size: 0.95rem;\n font-weight: 500;\n color: #E8D5B7;\n text-decoration: none;\n white-space: nowrap;\n position: relative;\n transition:\n color 0.25s cubic-bezier(0.34, 1.56, 0.64, 1),\n background-color 0.25s ease,\n transform 0.2s ease;\n border-bottom: 2px solid transparent;\n}\n\n/* Animated gold underline */\n.lotr-nav-link::after {\n content: '';\n position: absolute;\n bottom: -2px;\n left: 50%;\n transform: translateX(-50%);\n width: 0;\n height: 2px;\n border-radius: 9999px;\n background: linear-gradient(to right, transparent, #FFD700, transparent);\n transition: width 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);\n box-shadow: 0 0 8px rgba(255, 215, 0, 0.3);\n}\n\n.lotr-nav-link:hover {\n color: #FFD700;\n background-color: rgba(218, 165, 32, 0.12);\n transform: translateY(-2px);\n}\n\n.lotr-nav-link:hover::after {\n width: 60%;\n}\n\n/* Active state */\n.lotr-nav-link--active {\n color: #FFD700 !important;\n background-color: rgba(218, 165, 32, 0.16) !important;\n border-bottom: 2px solid #FFD700;\n}\n\n.lotr-nav-link--active::after {\n width: 60%;\n opacity: 0;\n}\n\n.lotr-nav-link:focus-visible {\n outline: 2px solid #FFD700;\n outline-offset: 3px;\n}\n\n.lotr-nav-link__icon {\n font-size: 1rem;\n flex-shrink: 0;\n transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n\n.lotr-nav-link:hover .lotr-nav-link__icon {\n transform: scale(1.15) rotate(-5deg);\n}\n\n\n/* ── CONTROLS (scoring card + logout) ── */\n.lotr-header__controls {\n display: flex;\n align-items: center;\n gap: 1.2rem;\n flex-shrink: 0;\n min-width: auto;\n}\n\n\n/* Subtle fantasy logout button override */\n.lotr-logout-btn {\n border: 1.5px solid rgba(255, 215, 0, 0.55) !important;\n background: linear-gradient(135deg, #2D5016 0%, #1F3A0E 100%);\n color: #FFD700 !important;\n font-family: 'Cinzel', serif;\n font-size: 1rem;\n font-weight: 600;\n letter-spacing: 0.1em;\n box-shadow:\n 0 2px 8px rgba(218, 165, 32, 0.15),\n inset 0 1px 0 rgba(255, 215, 0, 0.1);\n transition:\n all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n\n.lotr-logout-btn:hover {\n border-color: #FFF8DC !important;\n background: linear-gradient(135deg, #243A12 0%, #2D5016 100%);\n color: #FFF8DC !important;\n box-shadow:\n 0 4px 16px rgba(255, 215, 0, 0.35),\n inset 0 1px 0 rgba(255, 215, 0, 0.2);\n transform: translateY(-2px);\n}\n\n/* ── HAMBURGER (mobile only) ── */\n.lotr-hamburger {\n display: none; /* shown via media query below */\n flex-direction: column;\n justify-content: center;\n align-items: center;\n gap: 5px;\n width: 2.5rem;\n height: 2.5rem;\n padding: 0.5rem;\n border-radius: 0.5rem;\n border: 1.5px solid rgba(218, 165, 32, 0.45);\n background: rgba(26, 50, 9, 0.6);\n cursor: pointer;\n flex-shrink: 0;\n transition:\n all 0.25s ease;\n}\n\n.lotr-hamburger:hover {\n background: rgba(26, 50, 9, 0.9);\n border-color: rgba(255, 215, 0, 0.65);\n box-shadow: 0 0 12px rgba(218, 165, 32, 0.2);\n}\n\n.lotr-hamburger:focus-visible {\n outline: 2px solid #FFD700;\n outline-offset: 2px;\n}\n\n.lotr-hamburger__bar {\n display: block;\n width: 18px;\n height: 2px;\n background-color: #E8D5B7;\n border-radius: 9999px;\n transition:\n transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),\n opacity 0.2s ease,\n background-color 0.25s ease;\n}\n\n.lotr-hamburger--open .lotr-hamburger__bar:nth-child(1) {\n transform: translateY(7px) rotate(45deg);\n background-color: #FFD700;\n}\n\n.lotr-hamburger--open .lotr-hamburger__bar:nth-child(2) {\n opacity: 0;\n transform: scaleX(0);\n}\n\n.lotr-hamburger--open .lotr-hamburger__bar:nth-child(3) {\n transform: translateY(-7px) rotate(-45deg);\n background-color: #FFD700;\n}\n\n/* ── ELVISH WAVY BORDER BOTTOM ── */\n.lotr-header__elvish-border {\n position: relative;\n height: 16px;\n overflow: visible;\n pointer-events: none;\n}\n\n.lotr-header__border-svg {\n width: 100%;\n height: 16px;\n display: block;\n filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.25));\n}\n\n/* Decorative gem cluster centred on the border */\n.lotr-header__gems {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n font-size: 0.5rem;\n color: #FFD700;\n letter-spacing: 0.8rem;\n opacity: 0.75;\n user-select: none;\n white-space: nowrap;\n text-shadow: 0 0 6px rgba(255, 215, 0, 0.5);\n}\n\n\n/* ── RESPONSIVE — MOBILE (< 768 px) ── */\n@media (max-width: 767px) {\n .lotr-header__inner {\n padding: 0.6rem 0.75rem;\n flex-wrap: wrap;\n row-gap: 0.3rem;\n }\n\n /* Brand takes remaining space on first row */\n .lotr-header__brand {\n flex: 1;\n }\n\n /* Nav hidden by default on mobile */\n .lotr-header__nav {\n display: none;\n order: 10; /* below hamburger row */\n width: 100%;\n flex-direction: column;\n align-items: flex-start;\n gap: 0.1rem;\n padding-top: 0.45rem;\n border-top: 1px solid rgba(218, 165, 32, 0.15);\n margin-top: 0.3rem;\n animation: mobileNavIn 220ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;\n }\n\n /* Controls hidden by default on mobile */\n .lotr-header__controls {\n display: none;\n order: 11;\n width: 100%;\n flex-direction: column;\n align-items: stretch;\n gap: 0.55rem;\n padding-top: 0.45rem;\n padding-bottom: 0.2rem;\n border-top: 1px solid rgba(218, 165, 32, 0.12);\n margin-top: 0.2rem;\n animation: mobileNavIn 240ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;\n }\n\n /* When menu is open: reveal nav and controls */\n .lotr-header--menu-open .lotr-header__nav,\n .lotr-header--menu-open .lotr-header__controls {\n display: flex;\n }\n\n /* Show hamburger on mobile */\n .lotr-hamburger {\n display: flex;\n }\n\n /* Full-width nav links on mobile */\n .lotr-nav-link {\n width: 100%;\n padding: 0.55rem 0.8rem;\n font-size: 1.05rem;\n border-radius: 0.5rem;\n }\n\n /* Logout fills width on mobile */\n .lotr-logout-btn {\n width: 100% !important;\n justify-content: center;\n text-align: center;\n padding: 0.7rem 1rem !important;\n }\n\n /* Hide brand divider on mobile */\n .lotr-header__brand::after {\n display: none;\n }\n}\n\n/* ── SLIDE-IN ANIMATION FOR MOBILE MENU ── */\n@keyframes mobileNavIn {\n from {\n opacity: 0;\n transform: translateY(-8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* ── DESKTOP ADJUSTMENTS ── */\n@media (min-width: 768px) {\n /* Ensure hamburger stays hidden on desktop */\n .lotr-hamburger {\n display: none !important;\n }\n\n /* Ensure controls are always visible on desktop */\n .lotr-header__controls {\n display: flex !important;\n }\n\n /* Ensure nav is always visible on desktop */\n .lotr-header__nav {\n display: flex !important;\n }\n}\n",".quest-form-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(26, 31, 46, 0.5);\n display: flex;\n justify-content: center;\n align-items: center;\n z-index: 9999;\n padding: 1.5rem;\n animation: fadeIn 0.3s ease-out;\n backdrop-filter: blur(3px);\n}\n\n.quest-form-modal {\n background: linear-gradient(135deg, var(--parchment-light) 0%, var(--parchment) 100%);\n border-radius: 1rem;\n max-width: 550px;\n width: 100%;\n max-height: 88vh;\n overflow-y: auto;\n box-shadow: 0 8px 32px rgba(139, 69, 19, 0.25), 0 0 20px rgba(218, 165, 32, 0.1);\n border: 1px solid var(--gold-dark);\n animation: scaleIn 0.3s ease-out;\n}\n\n.quest-form-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.5rem;\n border-bottom: 1px solid var(--gold-dark);\n background: linear-gradient(90deg, rgba(218, 165, 32, 0.08) 0%, transparent 100%);\n}\n\n.quest-form-header h2 {\n margin: 0;\n color: var(--deep-blue);\n font-family: 'Cinzel', serif;\n font-weight: 700;\n font-size: 1.3rem;\n letter-spacing: 0.5px;\n}\n\n.close-button {\n background: none;\n border: none;\n font-size: 1.75rem;\n color: var(--earth-brown);\n cursor: pointer;\n line-height: 1;\n padding: 0.25rem;\n width: 2.25rem;\n height: 2.25rem;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.25s ease;\n border-radius: 0.5rem;\n}\n\n.close-button:hover {\n color: var(--dark-red);\n background-color: rgba(218, 165, 32, 0.15);\n transform: rotate(90deg);\n}\n\n.quest-form {\n padding: 1.5rem;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n.form-group {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.form-label {\n font-family: 'Cinzel', serif;\n font-weight: 600;\n color: var(--deep-blue);\n font-size: 0.9rem;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.form-input {\n padding: 0.75rem;\n border: 1px solid var(--gold-dark);\n border-radius: 0.5rem;\n font-family: 'Lora', serif;\n font-size: 0.95rem;\n color: var(--deep-blue);\n background-color: var(--parchment-light);\n transition: all 0.25s ease;\n box-shadow: 0 1px 3px rgba(139, 69, 19, 0.1);\n}\n\n.form-input:focus {\n outline: none;\n border-color: var(--gold);\n background-color: white;\n box-shadow: 0 2px 6px rgba(139, 69, 19, 0.15), 0 0 0 2px rgba(218, 165, 32, 0.15);\n}\n\n.form-input::placeholder {\n color: var(--text-secondary);\n font-style: italic;\n opacity: 0.7;\n}\n\n.form-textarea {\n resize: vertical;\n min-height: 90px;\n font-size: 0.95rem;\n line-height: 1.5;\n font-weight: 400;\n}\n\n.form-textarea:focus {\n border-color: var(--gold);\n box-shadow: 0 2px 6px rgba(139, 69, 19, 0.15), 0 0 0 2px rgba(218, 165, 32, 0.15);\n}\n\n.error-message {\n background-color: rgba(220, 38, 38, 0.1);\n color: var(--dark-red);\n padding: 0.75rem;\n border-radius: 0.5rem;\n border-left: 3px solid var(--dark-red);\n font-size: 0.9rem;\n margin-bottom: 1rem;\n}\n\n.quest-form-actions {\n display: flex;\n justify-content: flex-end;\n gap: 0.75rem;\n margin-top: 1.5rem;\n padding-top: 1rem;\n border-top: 1px solid var(--gold-dark);\n}\n","/* required styles */\r\n\r\n.leaflet-pane,\r\n.leaflet-tile,\r\n.leaflet-marker-icon,\r\n.leaflet-marker-shadow,\r\n.leaflet-tile-container,\r\n.leaflet-pane > svg,\r\n.leaflet-pane > canvas,\r\n.leaflet-zoom-box,\r\n.leaflet-image-layer,\r\n.leaflet-layer {\r\n\tposition: absolute;\r\n\tleft: 0;\r\n\ttop: 0;\r\n\t}\r\n.leaflet-container {\r\n\toverflow: hidden;\r\n\t}\r\n.leaflet-tile,\r\n.leaflet-marker-icon,\r\n.leaflet-marker-shadow {\r\n\t-webkit-user-select: none;\r\n\t -moz-user-select: none;\r\n\t user-select: none;\r\n\t -webkit-user-drag: none;\r\n\t}\r\n/* Prevents IE11 from highlighting tiles in blue */\r\n.leaflet-tile::selection {\r\n\tbackground: transparent;\r\n}\r\n/* Safari renders non-retina tile on retina better with this, but Chrome is worse */\r\n.leaflet-safari .leaflet-tile {\r\n\timage-rendering: -webkit-optimize-contrast;\r\n\t}\r\n/* hack that prevents hw layers \"stretching\" when loading new tiles */\r\n.leaflet-safari .leaflet-tile-container {\r\n\twidth: 1600px;\r\n\theight: 1600px;\r\n\t-webkit-transform-origin: 0 0;\r\n\t}\r\n.leaflet-marker-icon,\r\n.leaflet-marker-shadow {\r\n\tdisplay: block;\r\n\t}\r\n/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */\r\n/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */\r\n.leaflet-container .leaflet-overlay-pane svg {\r\n\tmax-width: none !important;\r\n\tmax-height: none !important;\r\n\t}\r\n.leaflet-container .leaflet-marker-pane img,\r\n.leaflet-container .leaflet-shadow-pane img,\r\n.leaflet-container .leaflet-tile-pane img,\r\n.leaflet-container img.leaflet-image-layer,\r\n.leaflet-container .leaflet-tile {\r\n\tmax-width: none !important;\r\n\tmax-height: none !important;\r\n\twidth: auto;\r\n\tpadding: 0;\r\n\t}\r\n\r\n.leaflet-container img.leaflet-tile {\r\n\t/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */\r\n\tmix-blend-mode: plus-lighter;\r\n}\r\n\r\n.leaflet-container.leaflet-touch-zoom {\r\n\t-ms-touch-action: pan-x pan-y;\r\n\ttouch-action: pan-x pan-y;\r\n\t}\r\n.leaflet-container.leaflet-touch-drag {\r\n\t-ms-touch-action: pinch-zoom;\r\n\t/* Fallback for FF which doesn't support pinch-zoom */\r\n\ttouch-action: none;\r\n\ttouch-action: pinch-zoom;\r\n}\r\n.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {\r\n\t-ms-touch-action: none;\r\n\ttouch-action: none;\r\n}\r\n.leaflet-container {\r\n\t-webkit-tap-highlight-color: transparent;\r\n}\r\n.leaflet-container a {\r\n\t-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);\r\n}\r\n.leaflet-tile {\r\n\tfilter: inherit;\r\n\tvisibility: hidden;\r\n\t}\r\n.leaflet-tile-loaded {\r\n\tvisibility: inherit;\r\n\t}\r\n.leaflet-zoom-box {\r\n\twidth: 0;\r\n\theight: 0;\r\n\t-moz-box-sizing: border-box;\r\n\t box-sizing: border-box;\r\n\tz-index: 800;\r\n\t}\r\n/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */\r\n.leaflet-overlay-pane svg {\r\n\t-moz-user-select: none;\r\n\t}\r\n\r\n.leaflet-pane { z-index: 400; }\r\n\r\n.leaflet-tile-pane { z-index: 200; }\r\n.leaflet-overlay-pane { z-index: 400; }\r\n.leaflet-shadow-pane { z-index: 500; }\r\n.leaflet-marker-pane { z-index: 600; }\r\n.leaflet-tooltip-pane { z-index: 650; }\r\n.leaflet-popup-pane { z-index: 700; }\r\n\r\n.leaflet-map-pane canvas { z-index: 100; }\r\n.leaflet-map-pane svg { z-index: 200; }\r\n\r\n.leaflet-vml-shape {\r\n\twidth: 1px;\r\n\theight: 1px;\r\n\t}\r\n.lvml {\r\n\tbehavior: url(#default#VML);\r\n\tdisplay: inline-block;\r\n\tposition: absolute;\r\n\t}\r\n\r\n\r\n/* control positioning */\r\n\r\n.leaflet-control {\r\n\tposition: relative;\r\n\tz-index: 800;\r\n\tpointer-events: visiblePainted; /* IE 9-10 doesn't have auto */\r\n\tpointer-events: auto;\r\n\t}\r\n.leaflet-top,\r\n.leaflet-bottom {\r\n\tposition: absolute;\r\n\tz-index: 1000;\r\n\tpointer-events: none;\r\n\t}\r\n.leaflet-top {\r\n\ttop: 0;\r\n\t}\r\n.leaflet-right {\r\n\tright: 0;\r\n\t}\r\n.leaflet-bottom {\r\n\tbottom: 0;\r\n\t}\r\n.leaflet-left {\r\n\tleft: 0;\r\n\t}\r\n.leaflet-control {\r\n\tfloat: left;\r\n\tclear: both;\r\n\t}\r\n.leaflet-right .leaflet-control {\r\n\tfloat: right;\r\n\t}\r\n.leaflet-top .leaflet-control {\r\n\tmargin-top: 10px;\r\n\t}\r\n.leaflet-bottom .leaflet-control {\r\n\tmargin-bottom: 10px;\r\n\t}\r\n.leaflet-left .leaflet-control {\r\n\tmargin-left: 10px;\r\n\t}\r\n.leaflet-right .leaflet-control {\r\n\tmargin-right: 10px;\r\n\t}\r\n\r\n\r\n/* zoom and fade animations */\r\n\r\n.leaflet-fade-anim .leaflet-popup {\r\n\topacity: 0;\r\n\t-webkit-transition: opacity 0.2s linear;\r\n\t -moz-transition: opacity 0.2s linear;\r\n\t transition: opacity 0.2s linear;\r\n\t}\r\n.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {\r\n\topacity: 1;\r\n\t}\r\n.leaflet-zoom-animated {\r\n\t-webkit-transform-origin: 0 0;\r\n\t -ms-transform-origin: 0 0;\r\n\t transform-origin: 0 0;\r\n\t}\r\nsvg.leaflet-zoom-animated {\r\n\twill-change: transform;\r\n}\r\n\r\n.leaflet-zoom-anim .leaflet-zoom-animated {\r\n\t-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);\r\n\t -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);\r\n\t transition: transform 0.25s cubic-bezier(0,0,0.25,1);\r\n\t}\r\n.leaflet-zoom-anim .leaflet-tile,\r\n.leaflet-pan-anim .leaflet-tile {\r\n\t-webkit-transition: none;\r\n\t -moz-transition: none;\r\n\t transition: none;\r\n\t}\r\n\r\n.leaflet-zoom-anim .leaflet-zoom-hide {\r\n\tvisibility: hidden;\r\n\t}\r\n\r\n\r\n/* cursors */\r\n\r\n.leaflet-interactive {\r\n\tcursor: pointer;\r\n\t}\r\n.leaflet-grab {\r\n\tcursor: -webkit-grab;\r\n\tcursor: -moz-grab;\r\n\tcursor: grab;\r\n\t}\r\n.leaflet-crosshair,\r\n.leaflet-crosshair .leaflet-interactive {\r\n\tcursor: crosshair;\r\n\t}\r\n.leaflet-popup-pane,\r\n.leaflet-control {\r\n\tcursor: auto;\r\n\t}\r\n.leaflet-dragging .leaflet-grab,\r\n.leaflet-dragging .leaflet-grab .leaflet-interactive,\r\n.leaflet-dragging .leaflet-marker-draggable {\r\n\tcursor: move;\r\n\tcursor: -webkit-grabbing;\r\n\tcursor: -moz-grabbing;\r\n\tcursor: grabbing;\r\n\t}\r\n\r\n/* marker & overlays interactivity */\r\n.leaflet-marker-icon,\r\n.leaflet-marker-shadow,\r\n.leaflet-image-layer,\r\n.leaflet-pane > svg path,\r\n.leaflet-tile-container {\r\n\tpointer-events: none;\r\n\t}\r\n\r\n.leaflet-marker-icon.leaflet-interactive,\r\n.leaflet-image-layer.leaflet-interactive,\r\n.leaflet-pane > svg path.leaflet-interactive,\r\nsvg.leaflet-image-layer.leaflet-interactive path {\r\n\tpointer-events: visiblePainted; /* IE 9-10 doesn't have auto */\r\n\tpointer-events: auto;\r\n\t}\r\n\r\n/* visual tweaks */\r\n\r\n.leaflet-container {\r\n\tbackground: #ddd;\r\n\toutline-offset: 1px;\r\n\t}\r\n.leaflet-container a {\r\n\tcolor: #0078A8;\r\n\t}\r\n.leaflet-zoom-box {\r\n\tborder: 2px dotted #38f;\r\n\tbackground: rgba(255,255,255,0.5);\r\n\t}\r\n\r\n\r\n/* general typography */\r\n.leaflet-container {\r\n\tfont-family: \"Helvetica Neue\", Arial, Helvetica, sans-serif;\r\n\tfont-size: 12px;\r\n\tfont-size: 0.75rem;\r\n\tline-height: 1.5;\r\n\t}\r\n\r\n\r\n/* general toolbar styles */\r\n\r\n.leaflet-bar {\r\n\tbox-shadow: 0 1px 5px rgba(0,0,0,0.65);\r\n\tborder-radius: 4px;\r\n\t}\r\n.leaflet-bar a {\r\n\tbackground-color: #fff;\r\n\tborder-bottom: 1px solid #ccc;\r\n\twidth: 26px;\r\n\theight: 26px;\r\n\tline-height: 26px;\r\n\tdisplay: block;\r\n\ttext-align: center;\r\n\ttext-decoration: none;\r\n\tcolor: black;\r\n\t}\r\n.leaflet-bar a,\r\n.leaflet-control-layers-toggle {\r\n\tbackground-position: 50% 50%;\r\n\tbackground-repeat: no-repeat;\r\n\tdisplay: block;\r\n\t}\r\n.leaflet-bar a:hover,\r\n.leaflet-bar a:focus {\r\n\tbackground-color: #f4f4f4;\r\n\t}\r\n.leaflet-bar a:first-child {\r\n\tborder-top-left-radius: 4px;\r\n\tborder-top-right-radius: 4px;\r\n\t}\r\n.leaflet-bar a:last-child {\r\n\tborder-bottom-left-radius: 4px;\r\n\tborder-bottom-right-radius: 4px;\r\n\tborder-bottom: none;\r\n\t}\r\n.leaflet-bar a.leaflet-disabled {\r\n\tcursor: default;\r\n\tbackground-color: #f4f4f4;\r\n\tcolor: #bbb;\r\n\t}\r\n\r\n.leaflet-touch .leaflet-bar a {\r\n\twidth: 30px;\r\n\theight: 30px;\r\n\tline-height: 30px;\r\n\t}\r\n.leaflet-touch .leaflet-bar a:first-child {\r\n\tborder-top-left-radius: 2px;\r\n\tborder-top-right-radius: 2px;\r\n\t}\r\n.leaflet-touch .leaflet-bar a:last-child {\r\n\tborder-bottom-left-radius: 2px;\r\n\tborder-bottom-right-radius: 2px;\r\n\t}\r\n\r\n/* zoom control */\r\n\r\n.leaflet-control-zoom-in,\r\n.leaflet-control-zoom-out {\r\n\tfont: bold 18px 'Lucida Console', Monaco, monospace;\r\n\ttext-indent: 1px;\r\n\t}\r\n\r\n.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {\r\n\tfont-size: 22px;\r\n\t}\r\n\r\n\r\n/* layers control */\r\n\r\n.leaflet-control-layers {\r\n\tbox-shadow: 0 1px 5px rgba(0,0,0,0.4);\r\n\tbackground: #fff;\r\n\tborder-radius: 5px;\r\n\t}\r\n.leaflet-control-layers-toggle {\r\n\tbackground-image: url(images/layers.png);\r\n\twidth: 36px;\r\n\theight: 36px;\r\n\t}\r\n.leaflet-retina .leaflet-control-layers-toggle {\r\n\tbackground-image: url(images/layers-2x.png);\r\n\tbackground-size: 26px 26px;\r\n\t}\r\n.leaflet-touch .leaflet-control-layers-toggle {\r\n\twidth: 44px;\r\n\theight: 44px;\r\n\t}\r\n.leaflet-control-layers .leaflet-control-layers-list,\r\n.leaflet-control-layers-expanded .leaflet-control-layers-toggle {\r\n\tdisplay: none;\r\n\t}\r\n.leaflet-control-layers-expanded .leaflet-control-layers-list {\r\n\tdisplay: block;\r\n\tposition: relative;\r\n\t}\r\n.leaflet-control-layers-expanded {\r\n\tpadding: 6px 10px 6px 6px;\r\n\tcolor: #333;\r\n\tbackground: #fff;\r\n\t}\r\n.leaflet-control-layers-scrollbar {\r\n\toverflow-y: scroll;\r\n\toverflow-x: hidden;\r\n\tpadding-right: 5px;\r\n\t}\r\n.leaflet-control-layers-selector {\r\n\tmargin-top: 2px;\r\n\tposition: relative;\r\n\ttop: 1px;\r\n\t}\r\n.leaflet-control-layers label {\r\n\tdisplay: block;\r\n\tfont-size: 13px;\r\n\tfont-size: 1.08333em;\r\n\t}\r\n.leaflet-control-layers-separator {\r\n\theight: 0;\r\n\tborder-top: 1px solid #ddd;\r\n\tmargin: 5px -10px 5px -6px;\r\n\t}\r\n\r\n/* Default icon URLs */\r\n.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */\r\n\tbackground-image: url(images/marker-icon.png);\r\n\t}\r\n\r\n\r\n/* attribution and scale controls */\r\n\r\n.leaflet-container .leaflet-control-attribution {\r\n\tbackground: #fff;\r\n\tbackground: rgba(255, 255, 255, 0.8);\r\n\tmargin: 0;\r\n\t}\r\n.leaflet-control-attribution,\r\n.leaflet-control-scale-line {\r\n\tpadding: 0 5px;\r\n\tcolor: #333;\r\n\tline-height: 1.4;\r\n\t}\r\n.leaflet-control-attribution a {\r\n\ttext-decoration: none;\r\n\t}\r\n.leaflet-control-attribution a:hover,\r\n.leaflet-control-attribution a:focus {\r\n\ttext-decoration: underline;\r\n\t}\r\n.leaflet-attribution-flag {\r\n\tdisplay: inline !important;\r\n\tvertical-align: baseline !important;\r\n\twidth: 1em;\r\n\theight: 0.6669em;\r\n\t}\r\n.leaflet-left .leaflet-control-scale {\r\n\tmargin-left: 5px;\r\n\t}\r\n.leaflet-bottom .leaflet-control-scale {\r\n\tmargin-bottom: 5px;\r\n\t}\r\n.leaflet-control-scale-line {\r\n\tborder: 2px solid #777;\r\n\tborder-top: none;\r\n\tline-height: 1.1;\r\n\tpadding: 2px 5px 1px;\r\n\twhite-space: nowrap;\r\n\t-moz-box-sizing: border-box;\r\n\t box-sizing: border-box;\r\n\tbackground: rgba(255, 255, 255, 0.8);\r\n\ttext-shadow: 1px 1px #fff;\r\n\t}\r\n.leaflet-control-scale-line:not(:first-child) {\r\n\tborder-top: 2px solid #777;\r\n\tborder-bottom: none;\r\n\tmargin-top: -2px;\r\n\t}\r\n.leaflet-control-scale-line:not(:first-child):not(:last-child) {\r\n\tborder-bottom: 2px solid #777;\r\n\t}\r\n\r\n.leaflet-touch .leaflet-control-attribution,\r\n.leaflet-touch .leaflet-control-layers,\r\n.leaflet-touch .leaflet-bar {\r\n\tbox-shadow: none;\r\n\t}\r\n.leaflet-touch .leaflet-control-layers,\r\n.leaflet-touch .leaflet-bar {\r\n\tborder: 2px solid rgba(0,0,0,0.2);\r\n\tbackground-clip: padding-box;\r\n\t}\r\n\r\n\r\n/* popup */\r\n\r\n.leaflet-popup {\r\n\tposition: absolute;\r\n\ttext-align: center;\r\n\tmargin-bottom: 20px;\r\n\t}\r\n.leaflet-popup-content-wrapper {\r\n\tpadding: 1px;\r\n\ttext-align: left;\r\n\tborder-radius: 12px;\r\n\t}\r\n.leaflet-popup-content {\r\n\tmargin: 13px 24px 13px 20px;\r\n\tline-height: 1.3;\r\n\tfont-size: 13px;\r\n\tfont-size: 1.08333em;\r\n\tmin-height: 1px;\r\n\t}\r\n.leaflet-popup-content p {\r\n\tmargin: 17px 0;\r\n\tmargin: 1.3em 0;\r\n\t}\r\n.leaflet-popup-tip-container {\r\n\twidth: 40px;\r\n\theight: 20px;\r\n\tposition: absolute;\r\n\tleft: 50%;\r\n\tmargin-top: -1px;\r\n\tmargin-left: -20px;\r\n\toverflow: hidden;\r\n\tpointer-events: none;\r\n\t}\r\n.leaflet-popup-tip {\r\n\twidth: 17px;\r\n\theight: 17px;\r\n\tpadding: 1px;\r\n\r\n\tmargin: -10px auto 0;\r\n\tpointer-events: auto;\r\n\r\n\t-webkit-transform: rotate(45deg);\r\n\t -moz-transform: rotate(45deg);\r\n\t -ms-transform: rotate(45deg);\r\n\t transform: rotate(45deg);\r\n\t}\r\n.leaflet-popup-content-wrapper,\r\n.leaflet-popup-tip {\r\n\tbackground: white;\r\n\tcolor: #333;\r\n\tbox-shadow: 0 3px 14px rgba(0,0,0,0.4);\r\n\t}\r\n.leaflet-container a.leaflet-popup-close-button {\r\n\tposition: absolute;\r\n\ttop: 0;\r\n\tright: 0;\r\n\tborder: none;\r\n\ttext-align: center;\r\n\twidth: 24px;\r\n\theight: 24px;\r\n\tfont: 16px/24px Tahoma, Verdana, sans-serif;\r\n\tcolor: #757575;\r\n\ttext-decoration: none;\r\n\tbackground: transparent;\r\n\t}\r\n.leaflet-container a.leaflet-popup-close-button:hover,\r\n.leaflet-container a.leaflet-popup-close-button:focus {\r\n\tcolor: #585858;\r\n\t}\r\n.leaflet-popup-scrolled {\r\n\toverflow: auto;\r\n\t}\r\n\r\n.leaflet-oldie .leaflet-popup-content-wrapper {\r\n\t-ms-zoom: 1;\r\n\t}\r\n.leaflet-oldie .leaflet-popup-tip {\r\n\twidth: 24px;\r\n\tmargin: 0 auto;\r\n\r\n\t-ms-filter: \"progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)\";\r\n\tfilter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);\r\n\t}\r\n\r\n.leaflet-oldie .leaflet-control-zoom,\r\n.leaflet-oldie .leaflet-control-layers,\r\n.leaflet-oldie .leaflet-popup-content-wrapper,\r\n.leaflet-oldie .leaflet-popup-tip {\r\n\tborder: 1px solid #999;\r\n\t}\r\n\r\n\r\n/* div icon */\r\n\r\n.leaflet-div-icon {\r\n\tbackground: #fff;\r\n\tborder: 1px solid #666;\r\n\t}\r\n\r\n\r\n/* Tooltip */\r\n/* Base styles for the element that has a tooltip */\r\n.leaflet-tooltip {\r\n\tposition: absolute;\r\n\tpadding: 6px;\r\n\tbackground-color: #fff;\r\n\tborder: 1px solid #fff;\r\n\tborder-radius: 3px;\r\n\tcolor: #222;\r\n\twhite-space: nowrap;\r\n\t-webkit-user-select: none;\r\n\t-moz-user-select: none;\r\n\t-ms-user-select: none;\r\n\tuser-select: none;\r\n\tpointer-events: none;\r\n\tbox-shadow: 0 1px 3px rgba(0,0,0,0.4);\r\n\t}\r\n.leaflet-tooltip.leaflet-interactive {\r\n\tcursor: pointer;\r\n\tpointer-events: auto;\r\n\t}\r\n.leaflet-tooltip-top:before,\r\n.leaflet-tooltip-bottom:before,\r\n.leaflet-tooltip-left:before,\r\n.leaflet-tooltip-right:before {\r\n\tposition: absolute;\r\n\tpointer-events: none;\r\n\tborder: 6px solid transparent;\r\n\tbackground: transparent;\r\n\tcontent: \"\";\r\n\t}\r\n\r\n/* Directions */\r\n\r\n.leaflet-tooltip-bottom {\r\n\tmargin-top: 6px;\r\n}\r\n.leaflet-tooltip-top {\r\n\tmargin-top: -6px;\r\n}\r\n.leaflet-tooltip-bottom:before,\r\n.leaflet-tooltip-top:before {\r\n\tleft: 50%;\r\n\tmargin-left: -6px;\r\n\t}\r\n.leaflet-tooltip-top:before {\r\n\tbottom: 0;\r\n\tmargin-bottom: -12px;\r\n\tborder-top-color: #fff;\r\n\t}\r\n.leaflet-tooltip-bottom:before {\r\n\ttop: 0;\r\n\tmargin-top: -12px;\r\n\tmargin-left: -6px;\r\n\tborder-bottom-color: #fff;\r\n\t}\r\n.leaflet-tooltip-left {\r\n\tmargin-left: -6px;\r\n}\r\n.leaflet-tooltip-right {\r\n\tmargin-left: 6px;\r\n}\r\n.leaflet-tooltip-left:before,\r\n.leaflet-tooltip-right:before {\r\n\ttop: 50%;\r\n\tmargin-top: -6px;\r\n\t}\r\n.leaflet-tooltip-left:before {\r\n\tright: 0;\r\n\tmargin-right: -12px;\r\n\tborder-left-color: #fff;\r\n\t}\r\n.leaflet-tooltip-right:before {\r\n\tleft: 0;\r\n\tmargin-left: -12px;\r\n\tborder-right-color: #fff;\r\n\t}\r\n\r\n/* Printing */\r\n\r\n@media print {\r\n\t/* Prevent printers from removing background-images of controls. */\r\n\t.leaflet-control {\r\n\t\t-webkit-print-color-adjust: exact;\r\n\t\tprint-color-adjust: exact;\r\n\t\t}\r\n\t}\r\n",".leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {\n\t-webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;\n\t-moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;\n\t-o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;\n\ttransition: transform 0.3s ease-out, opacity 0.3s ease-in;\n}\n\n.leaflet-cluster-spider-leg {\n\t/* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */\n\t-webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;\n\t-moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;\n\t-o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;\n\ttransition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;\n}\n","/* Middle-earth Map Component Styles */\n@import 'leaflet/dist/leaflet.css';\n/* Import markercluster CSS from package (no tilde for CRA) */\n@import 'leaflet.markercluster/dist/MarkerCluster.css';\n\n.middle-earth-map-container {\n width: 100%;\n height: 100%;\n min-height: 500px;\n background-color: var(--color-parchment-dark, #E8D5B7);\n border: 2px solid var(--color-earth-brown-medium, #A0522D);\n border-radius: 8px;\n overflow: hidden;\n position: relative;\n}\n\n/* Override Leaflet default styles with LOTR theme */\n.middle-earth-map-container .leaflet-container {\n background-color: var(--color-parchment-light, #F4E4BC);\n font-family: 'Lora', serif;\n}\n\n.middle-earth-map-container .leaflet-popup-content-wrapper {\n background: var(--color-parchment-light, #F4E4BC);\n border: 2px solid var(--color-earth-brown-medium, #A0522D);\n border-radius: 8px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n color: var(--color-deep-blue-dark, #1A1F2E);\n}\n\n.middle-earth-map-container .leaflet-popup-content {\n margin: 12px;\n font-family: 'Lora', serif;\n}\n\n.middle-earth-map-container .leaflet-popup-tip {\n background: var(--color-parchment-light, #F4E4BC);\n border: 1px solid var(--color-earth-brown-medium, #A0522D);\n}\n\n.middle-earth-map-container .leaflet-control-zoom {\n border: 2px solid var(--color-earth-brown-medium, #A0522D);\n border-radius: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);\n}\n\n.middle-earth-map-container .leaflet-control-zoom a {\n background-color: var(--color-parchment-light, #F4E4BC);\n color: var(--color-earth-brown-dark, #8B4513);\n border-bottom: 1px solid var(--color-earth-brown-medium, #A0522D);\n}\n\n.middle-earth-map-container .leaflet-control-zoom a:hover {\n background-color: var(--color-parchment-dark, #E8D5B7);\n color: var(--color-gold-dark, #FFD700);\n}\n\n/* Custom marker styling */\n.custom-marker-icon {\n background: transparent;\n border: none;\n}\n\n/* Location marker with quest count */\n.location-marker-icon {\n background: transparent;\n border: none;\n}\n\n.location-marker {\n width: 40px;\n height: 40px;\n border-radius: 50% 50% 50% 0;\n transform: rotate(-45deg);\n border: 3px solid var(--color-parchment-light, #F4E4BC);\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n position: relative;\n cursor: pointer;\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n}\n\n.location-marker:hover {\n transform: rotate(-45deg) scale(1.2);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);\n z-index: 1000;\n}\n\n.marker-pin-inner {\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background-color: var(--color-parchment-light, #F4E4BC);\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n border: 2px solid rgba(255, 255, 255, 0.8);\n}\n\n/* Quest count badge on marker */\n.quest-count-badge {\n position: absolute;\n top: -10px;\n right: -10px;\n background-color: var(--color-gold-dark, #FFD700);\n color: var(--color-earth-brown-dark, #8B4513);\n border-radius: 50%;\n width: 24px;\n height: 24px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 11px;\n font-weight: bold;\n border: 2px solid var(--color-parchment-light, #F4E4BC);\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n z-index: 10;\n transform: rotate(45deg); /* Rotate with the marker pin */\n line-height: 1;\n}\n\n/* Counter-rotate the text inside the badge to keep it upright */\n.quest-count-badge span {\n transform: rotate(-45deg);\n display: inline-block;\n}\n\n/* Marker cluster styling */\n.marker-cluster-icon {\n background: transparent !important;\n border: none !important;\n}\n\n.marker-cluster {\n background-color: var(--color-forest-green-medium, #3D6B1F);\n border: 3px solid var(--color-parchment-light, #F4E4BC);\n border-radius: 50%;\n color: white;\n font-weight: bold;\n display: flex;\n align-items: center;\n justify-content: center;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n width: 40px;\n height: 40px;\n font-size: 14px;\n cursor: pointer;\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n}\n\n.marker-cluster:hover {\n transform: scale(1.1);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);\n}\n\n/* Override default marker cluster styles */\n.middle-earth-map-container .marker-cluster-small {\n background-color: var(--color-forest-green-medium, #3D6B1F) !important;\n border: 3px solid var(--color-parchment-light, #F4E4BC) !important;\n}\n\n.middle-earth-map-container .marker-cluster-medium {\n background-color: var(--color-forest-green-dark, #2D5016) !important;\n border: 3px solid var(--color-parchment-light, #F4E4BC) !important;\n}\n\n.middle-earth-map-container .marker-cluster-large {\n background-color: var(--color-earth-brown-dark, #8B4513) !important;\n border: 3px solid var(--color-parchment-light, #F4E4BC) !important;\n}\n\n/* Quest marker cluster styling - aggregates quest markers on zoom out */\n.quest-marker-cluster-icon {\n background: transparent !important;\n border: none !important;\n}\n\n.quest-marker-cluster {\n background: linear-gradient(135deg, #CD853F, #8B4513);\n border: 2px solid #DAA520;\n border-radius: 50%;\n color: #F4E4BC;\n font-weight: bold;\n display: flex;\n align-items: center;\n justify-content: center;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.3);\n font-size: 13px;\n cursor: pointer;\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n}\n\n.quest-marker-cluster-small {\n width: 35px;\n height: 35px;\n}\n\n.quest-marker-cluster-medium {\n width: 45px;\n height: 45px;\n font-size: 15px;\n}\n\n.quest-marker-cluster-large {\n width: 55px;\n height: 55px;\n font-size: 17px;\n}\n\n.quest-marker-cluster:hover {\n transform: scale(1.15);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.7), inset 0 1px 0 rgba(255, 255, 255, 0.4);\n}\n\n/* Ensure quest markers are always on top and clickable */\n.middle-earth-map-container .leaflet-marker-pane .quest-marker-icon {\n z-index: 2000 !important;\n pointer-events: auto !important;\n cursor: pointer !important;\n}\n\n.middle-earth-map-container .leaflet-marker-pane .quest-marker-icon * {\n pointer-events: auto !important;\n cursor: pointer !important;\n}\n\n.middle-earth-map-container .leaflet-interactive {\n pointer-events: auto !important;\n}\n\n/* Override Leaflet's default pointer-events for quest markers */\n.middle-earth-map-container .leaflet-container .quest-marker-icon,\n.middle-earth-map-container .leaflet-container .quest-marker-icon * {\n pointer-events: auto !important;\n cursor: pointer !important;\n}\n\n/* Ensure quest markers are not blocked by other layers */\n.middle-earth-map-container .leaflet-marker-pane {\n z-index: 600 !important;\n}\n\n.middle-earth-map-container .leaflet-marker-pane .quest-marker-icon {\n z-index: 2600 !important;\n}\n\n/* Location popup styling */\n.location-popup {\n min-width: 200px;\n}\n\n.location-popup h3 {\n font-family: 'Cinzel', serif;\n color: var(--color-earth-brown-dark, #8B4513);\n margin: 0 0 8px 0;\n font-size: 1.2rem;\n}\n\n.location-popup .location-region {\n font-size: 0.9rem;\n color: var(--color-earth-brown-medium, #A0522D);\n font-style: italic;\n margin: 0 0 8px 0;\n}\n\n.location-popup .location-description {\n font-size: 0.85rem;\n color: var(--color-deep-blue-dark, #1A1F2E);\n margin: 0 0 12px 0;\n line-height: 1.4;\n}\n\n.location-popup .quest-count-info {\n font-size: 0.9rem;\n color: var(--color-forest-green-dark, #2D5016);\n margin: 0 0 12px 0;\n font-weight: 500;\n}\n\n.location-popup .quest-count-info strong {\n color: var(--color-gold-dark, #FFD700);\n font-size: 1.1rem;\n}\n\n.location-popup .btn-view-quests {\n background-color: var(--color-forest-green-medium, #3D6B1F);\n color: white;\n border: none;\n padding: 8px 16px;\n border-radius: 4px;\n cursor: pointer;\n font-family: 'Lora', serif;\n font-size: 0.9rem;\n transition: background-color 0.2s ease;\n width: 100%;\n}\n\n.location-popup .btn-view-quests:hover {\n background-color: var(--color-forest-green-dark, #2D5016);\n}\n\n\n.character-marker-icon {\n background: transparent !important;\n border: none !important;\n z-index: 5000 !important;\n}\n\n.character-marker {\n width: 40px;\n height: 40px;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n background: radial-gradient(circle at 30% 30%, #ffe7a3, #d4a939);\n border: 2px solid #8b4513;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);\n font-size: 20px;\n cursor: pointer;\n transition: transform 0.2s ease;\n z-index: 5001 !important;\n position: relative;\n}\n\n.character-marker:hover {\n transform: scale(1.12);\n}\n\n.character-popup {\n min-width: 200px;\n}\n\n.character-popup h4 {\n margin: 0 0 6px 0;\n color: var(--color-earth-brown-dark, #8B4513);\n}\n\n.character-popup p {\n margin: 0 0 10px 0;\n font-size: 0.9rem;\n}\n\n.character-popup .btn-bargain-character {\n width: 100%;\n border: none;\n border-radius: 4px;\n padding: 8px 10px;\n background-color: var(--color-forest-green-medium, #3D6B1F);\n color: #fff;\n cursor: pointer;\n}\n\n.character-popup .btn-bargain-character:hover {\n background-color: var(--color-forest-green-dark, #2D5016);\n}\n\n/* Quest marker styling */\n.quest-marker-icon {\n background: transparent !important;\n border: none !important;\n pointer-events: auto !important; /* Ensure markers are clickable */\n}\n\n.leaflet-marker-icon.quest-marker-icon {\n pointer-events: auto !important;\n cursor: pointer !important;\n}\n\n.quest-marker {\n width: 35px;\n height: 35px;\n border-radius: 50%;\n border: 2px solid var(--color-parchment-light, #F4E4BC);\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4), 0 0 4px rgba(218, 165, 32, 0.3);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer !important;\n pointer-events: auto !important; /* Critical: make markers clickable */\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n position: relative;\n z-index: 2000; /* Higher z-index than location markers */\n user-select: none; /* Prevent text selection */\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n}\n\n.quest-marker:hover {\n transform: scale(1.3);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6), 0 0 8px rgba(218, 165, 32, 0.5);\n z-index: 3000; /* Even higher on hover */\n cursor: pointer !important;\n}\n\n.quest-marker:active {\n transform: scale(1.2);\n}\n\n.quest-marker-selected {\n border: 3px solid var(--color-gold-dark, #FFD700);\n box-shadow: 0 0 15px rgba(218, 165, 32, 0.8), 0 2px 8px rgba(0, 0, 0, 0.4);\n z-index: 2500; /* Higher than normal quest markers */\n}\n\n.quest-marker-inner {\n font-size: 18px;\n line-height: 1;\n filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));\n}\n\n/* Quest popup styling */\n.quest-popup-wrapper .leaflet-popup-content-wrapper {\n max-width: 350px;\n}\n\n.quest-popup {\n min-width: 250px;\n max-width: 350px;\n}\n\n.quest-popup h4 {\n font-family: 'Cinzel', serif;\n color: var(--color-earth-brown-dark, #8B4513);\n margin: 0 0 10px 0;\n font-size: 1.2rem;\n line-height: 1.3;\n}\n\n.quest-popup-type {\n font-size: 0.9rem;\n color: var(--color-gold-dark, #FFD700);\n margin: 0 0 8px 0;\n font-weight: 600;\n font-family: 'Cinzel', serif;\n}\n\n.quest-popup-status {\n font-size: 0.85rem;\n color: var(--color-forest-green-dark, #2D5016);\n margin: 0 0 10px 0;\n font-weight: 500;\n padding: 4px 8px;\n background-color: rgba(45, 80, 22, 0.1);\n border-radius: 4px;\n display: inline-block;\n}\n\n.quest-popup-description {\n font-size: 0.9rem;\n color: var(--color-deep-blue-dark, #1A1F2E);\n margin: 0 0 12px 0;\n line-height: 1.5;\n max-height: 200px;\n overflow-y: auto;\n}\n\n.quest-popup-location {\n font-size: 0.85rem;\n color: var(--color-earth-brown-medium, #A0522D);\n margin: 0 0 8px 0;\n font-style: italic;\n}\n\n.quest-popup-assignee {\n font-size: 0.85rem;\n color: var(--color-deep-blue-dark, #1A1F2E);\n margin: 0 0 12px 0;\n}\n\n.quest-popup-actions {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 12px;\n}\n\n.btn-view-quest,\n.btn-complete-quest {\n background-color: var(--color-forest-green-medium, #3D6B1F);\n color: white;\n border: none;\n padding: 8px 16px;\n border-radius: 4px;\n cursor: pointer;\n font-family: 'Lora', serif;\n font-size: 0.9rem;\n transition: background-color 0.2s ease, transform 0.1s ease;\n width: 100%;\n font-weight: 500;\n}\n\n.btn-view-quest:hover,\n.btn-complete-quest:hover {\n background-color: var(--color-forest-green-dark, #2D5016);\n transform: translateY(-1px);\n}\n\n.btn-complete-quest {\n background-color: var(--color-gold-dark, #FFD700);\n color: var(--color-earth-brown-dark, #8B4513);\n}\n\n.btn-complete-quest:hover {\n background-color: var(--color-gold-light, #DAA520);\n color: var(--color-earth-brown-dark, #8B4513);\n}\n\n/* Responsive design */\n@media (max-width: 768px) {\n .middle-earth-map-container {\n min-height: 400px;\n }\n \n .location-popup {\n min-width: 150px;\n }\n}\n","/* Map Page Full-Screen Layout Styles */\n\n.map-page-full {\n display: flex;\n flex-direction: column;\n height: 100vh;\n background-color: var(--color-deep-blue-dark, #1A1F2E);\n}\n\n.map-page-full > .navbar {\n flex-shrink: 0;\n}\n\n.map-page-container {\n flex: 1;\n position: relative;\n overflow: visible;\n}\n\n/* Filter Sidebar */\n.filter-sidebar {\n position: absolute;\n left: 0;\n top: 0;\n bottom: 0;\n width: 320px;\n background: linear-gradient(180deg, var(--parchment-light) 0%, var(--parchment) 100%);\n border-right: 2px solid var(--earth-brown-light);\n box-shadow: 2px 0 20px rgba(139, 69, 19, 0.4);\n z-index: 500;\n overflow-y: auto;\n transform: translateX(0);\n transition: transform 0.4s ease;\n display: flex;\n flex-direction: column;\n animation: slideInLeft 0.5s ease-out;\n}\n\n.filter-sidebar.closed {\n transform: translateX(-100%);\n animation: slideInLeft 0.5s ease-out reverse;\n}\n\n.filter-sidebar-header {\n padding: 1.5rem;\n border-bottom: 2px solid var(--color-earth-brown-medium, #A0522D);\n flex-shrink: 0;\n}\n\n.filter-sidebar-header h2 {\n font-family: 'Cinzel', serif;\n color: var(--color-earth-brown-dark, #8B4513);\n margin: 0;\n font-size: 1.3rem;\n}\n\n.filter-sidebar-header h3 {\n font-family: 'Cinzel', serif;\n color: var(--color-earth-brown-dark, #8B4513);\n margin: 0;\n font-size: 1.3rem;\n}\n\n.filter-close-btn {\n background: transparent;\n border: none;\n color: var(--color-earth-brown-dark, #8B4513);\n font-size: 1.2rem;\n cursor: pointer;\n line-height: 1;\n padding: 0.25rem;\n}\n\n.filter-close-btn:hover {\n color: var(--color-gold-dark, #FFD700);\n}\n\n.filter-content {\n flex: 1;\n padding: 1.5rem;\n overflow-y: auto;\n}\n\n.filter-section {\n margin-bottom: 1.5rem;\n}\n\n.filter-section:last-child {\n margin-bottom: 0;\n}\n\n.filter-section-title {\n font-family: 'Cinzel', serif;\n color: var(--color-earth-brown-dark, #8B4513);\n font-weight: bold;\n font-size: 0.95rem;\n margin-bottom: 0.75rem;\n}\n\n.filter-checkbox-group {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.filter-checkbox {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n cursor: pointer;\n}\n\n.filter-checkbox input[type=\"checkbox\"] {\n width: 18px;\n height: 18px;\n cursor: pointer;\n accent-color: var(--color-earth-brown-dark, #8B4513);\n}\n\n.filter-checkbox label {\n cursor: pointer;\n color: var(--color-earth-brown-dark, #8B4513);\n font-size: 0.95rem;\n margin: 0;\n user-select: none;\n}\n\n.filter-checkbox input[type=\"checkbox\"]:hover + label {\n color: var(--color-gold-dark, #FFD700);\n}\n\n.clear-filters-btn {\n margin-top: 1rem;\n padding: 0.75rem 1rem;\n background-color: var(--color-earth-brown-dark, #8B4513);\n color: var(--color-parchment-light, #F4E4BC);\n border: none;\n border-radius: 4px;\n font-family: 'Cinzel', serif;\n cursor: pointer;\n transition: background-color 0.2s;\n}\n\n.clear-filters-btn:hover {\n background-color: var(--color-earth-brown-medium, #A0522D);\n}\n\n.filter-attribution {\n padding: 1.5rem;\n margin-top: 1rem;\n border-top: 2px solid var(--color-earth-brown-medium, #A0522D);\n flex-shrink: 0;\n font-size: 0.75rem;\n color: var(--color-earth-brown-dark, #8B4513);\n line-height: 1.5;\n font-family: 'Lora', serif;\n background: rgba(160, 82, 45, 0.1);\n}\n\n.filter-attribution p {\n margin: 0.5rem 0;\n}\n\n.filter-attribution em {\n display: inline;\n font-weight: 600;\n}\n\n.filter-actions {\n padding: 0 1rem;\n margin-bottom: auto;\n}\n\n/* Map Main Content */\n.map-main-content {\n position: absolute;\n left: 0;\n right: 0;\n top: 0;\n bottom: 0;\n display: flex;\n flex-direction: column;\n z-index: 1;\n overflow: visible;\n}\n\n.filter-toggle-btn {\n position: fixed;\n bottom: 1.5rem;\n left: 1.5rem;\n width: 50px;\n height: 50px;\n background-color: var(--earth-brown);\n color: var(--parchment-light);\n border: 3px solid var(--gold);\n border-radius: 50%;\n font-size: 1.5rem;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.3s ease;\n z-index: 700;\n box-shadow: 0 6px 16px rgba(139, 69, 19, 0.4),\n 0 0 15px rgba(218, 165, 32, 0.2);\n animation: slideInUp 0.6s ease-out;\n}\n\n.filter-toggle-btn:hover {\n background-color: var(--earth-brown-light);\n transform: scale(1.15);\n box-shadow: 0 8px 24px rgba(139, 69, 19, 0.5),\n 0 0 20px rgba(218, 165, 32, 0.4);\n}\n\n.filter-toggle-btn:active {\n transform: scale(0.95);\n}\n\n/* Map Container */\n.map-container {\n flex: 1;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.middle-earth-map-container {\n width: 100%;\n height: 100%;\n}\n\n.middle-earth-map-container .leaflet-container {\n width: 100% !important;\n height: 100% !important;\n}\n\n/* Quest Details Card */\n.quest-details-card {\n position: fixed;\n bottom: 1.5rem;\n right: 1.5rem;\n width: 380px;\n max-height: 70vh;\n background: linear-gradient(135deg, #F4E4BC 0%, #E8D5B7 100%);\n border: 3px solid #8B4513;\n border-radius: 12px;\n box-shadow: \n 0 20px 40px rgba(0, 0, 0, 0.4),\n inset 0 1px 0 rgba(255, 255, 255, 0.2),\n 0 0 30px rgba(218, 165, 32, 0.2);\n display: flex;\n flex-direction: column;\n z-index: 600;\n animation: slideUp 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);\n pointer-events: auto;\n overflow: hidden;\n}\n\n.map-character-panel {\n position: fixed;\n top: 6rem;\n right: 1.5rem;\n width: min(420px, 38vw);\n z-index: 650;\n max-height: calc(100vh - 8rem);\n overflow-y: auto;\n}\n\n.quest-details-card::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 3px;\n background: linear-gradient(90deg, var(--dark-red), var(--gold), var(--forest-green), var(--dark-red));\n z-index: 0;\n}\n\n@keyframes slideUp {\n from {\n opacity: 0;\n transform: translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.quest-details-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 1.5rem;\n border-bottom: 2px solid #A0522D;\n flex-shrink: 0;\n position: relative;\n z-index: 1;\n gap: 1rem;\n background: linear-gradient(90deg, rgba(139, 69, 19, 0.1), transparent);\n}\n\n.quest-details-header h2 {\n font-family: 'Cinzel', serif;\n color: #8B4513;\n font-size: 1.3rem;\n margin: 0;\n flex: 1;\n word-wrap: break-word;\n font-weight: 700;\n}\n\n.quest-title {\n font-family: 'Cinzel', serif;\n color: var(--color-earth-brown-dark, #8B4513);\n font-size: 1.2rem;\n margin: 0;\n flex: 1;\n word-wrap: break-word;\n}\n\n.close-btn {\n background: none;\n border: none;\n font-size: 1.5rem;\n color: #8B4513;\n cursor: pointer;\n padding: 0;\n width: 28px;\n height: 28px;\n min-width: 28px;\n min-height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n line-height: 1;\n transition: color 0.2s;\n flex-shrink: 0;\n position: relative;\n z-index: 10;\n}\n\n.close-btn:hover {\n color: #FFD700;\n}\n\n.quest-details-body {\n flex: 1;\n padding: 1.5rem;\n overflow-y: auto;\n position: relative;\n z-index: 1;\n color: #3d2817;\n}\n\n.quest-meta-info {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n margin-bottom: 1rem;\n}\n\n.quest-badges-map {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 0.5rem;\n margin-bottom: 1rem;\n}\n\n.quest-chip {\n display: inline-flex;\n align-items: center;\n gap: 0.35rem;\n min-height: var(--quest-chip-height);\n padding: 0.35rem var(--quest-chip-padding-x);\n border-radius: var(--quest-chip-radius);\n line-height: 1;\n font-size: 0.84rem;\n font-weight: 600;\n}\n\n.quest-chip-icon {\n font-size: 0.95em;\n line-height: 1;\n}\n\n.quest-chip-label {\n line-height: 1.2;\n}\n\n.quest-type-badge {\n background: linear-gradient(135deg, var(--forest-green-light) 0%, var(--forest-green) 100%);\n color: var(--parchment-light);\n box-shadow: 0 2px 6px rgba(45, 80, 22, 0.3);\n}\n\n.quest-status {\n background: var(--status-not-yet-begun);\n color: #ffffff;\n}\n\n.quest-status-not_yet_begun,\n.quest-status-pending {\n background: var(--status-not-yet-begun);\n}\n\n.quest-status-the_road_goes_ever_on,\n.quest-status-in_progress {\n background: var(--status-road-goes-on);\n}\n\n.quest-status-it_is_done,\n.quest-status-completed {\n background: var(--status-it-is-done);\n}\n\n.quest-status-the_shadow_falls,\n.quest-status-blocked {\n background: var(--status-shadow-falls);\n}\n\n.priority-badge {\n display: inline-flex;\n align-items: center;\n min-height: var(--quest-chip-height);\n padding: 0.35rem 0.75rem;\n border-radius: var(--quest-chip-radius);\n font-size: 0.8rem;\n font-weight: 600;\n background: rgba(139, 69, 19, 0.1);\n border: 1px solid rgba(139, 69, 19, 0.4);\n color: var(--earth-brown);\n}\n\n.quest-info-row {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n color: #3d2817;\n}\n\n.quest-info-label {\n font-family: 'Cinzel', serif;\n color: var(--color-earth-brown-medium, #A0522D);\n font-size: 0.85rem;\n font-weight: bold;\n}\n\n.quest-info-value {\n color: var(--color-earth-brown-dark, #8B4513);\n font-size: 0.95rem;\n}\n\n.quest-description {\n margin: 1rem 0;\n padding: 0.75rem;\n background-color: rgba(160, 82, 45, 0.1);\n border-left: 3px solid #A0522D;\n border-radius: 4px;\n color: #3d2817;\n font-size: 0.9rem;\n font-style: italic;\n line-height: 1.5;\n}\n\n.quest-quote {\n margin: 1rem 0 0 0;\n padding: 0.75rem;\n background-color: rgba(218, 165, 32, 0.1);\n border-left: 3px solid #FFD700;\n border-radius: 4px;\n color: #3d2817;\n font-size: 0.85rem;\n font-style: italic;\n line-height: 1.5;\n}\n\n.quest-details-actions {\n padding: 1.5rem;\n border-top: 2px solid #A0522D;\n flex-shrink: 0;\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--quest-action-gap);\n position: relative;\n z-index: 1;\n background: linear-gradient(90deg, transparent, rgba(139, 69, 19, 0.05));\n}\n\n.map-action-btn {\n width: 100%;\n min-height: var(--quest-action-min-height);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n}\n\n/* Responsive Design */\n@media (max-width: 768px) {\n .filter-sidebar {\n width: 280px;\n }\n \n .quest-details-card {\n width: 300px;\n bottom: 1rem;\n right: 1rem;\n }\n \n .filter-toggle-btn {\n width: 45px;\n height: 45px;\n font-size: 1.3rem;\n }\n}\n\n@media (max-width: 480px) {\n .filter-sidebar {\n width: 100%;\n }\n \n .quest-details-card {\n width: calc(100% - 2rem);\n max-height: 50vh;\n max-width: none;\n }\n\n .quest-details-actions {\n grid-template-columns: 1fr;\n }\n \n .filter-toggle-btn {\n bottom: 1rem;\n left: 1rem;\n }\n}\n\n/* Map Attribution - Moved to filter sidebar */\n.map-attribution {\n display: none;\n}\n","/* LOTR Color Palette */\n:root {\n /* Primary Colors */\n --parchment-light: #F4E4BC;\n --parchment: #E8D5B7;\n --parchment-dark: #D4C0A4;\n --earth-brown: #8B4513;\n --earth-brown-light: #A0522D;\n --forest-green: #2D5016;\n --forest-green-light: #3D6B1F;\n --gold: #DAA520;\n --gold-light: #FFD700;\n --gold-bright: #FFF8DC;\n --dark-red: #8B0000;\n --dark-red-light: #A52A2A;\n --deep-blue: #1A1F2E;\n --deep-blue-light: #2C3E50;\n \n /* Status Colors */\n --status-not-yet-begun: #6B7280;\n --status-road-goes-on: #F59E0B;\n --status-it-is-done: #10B981;\n --status-shadow-falls: #DC2626;\n \n /* Priority Colors */\n --priority-critical: #DC2626;\n --priority-important: #F59E0B;\n --priority-standard: #6B7280;\n \n /* Glow & Shimmer Colors */\n --gold-glow: rgba(218, 165, 32, 0.6);\n --arcane-glow: #B19CD9;\n --arcane-glow-rgba: rgba(177, 156, 217, 0.4);\n --fire-glow: #FF6B35;\n --fire-glow-rgba: rgba(255, 107, 53, 0.3);\n --success-glow: #51CF66;\n --success-glow-rgba: rgba(81, 207, 102, 0.3);\n --danger-glow: #FF4444;\n --danger-glow-rgba: rgba(255, 68, 68, 0.3);\n \n /* Transparency Variants */\n --earth-brown-fade: rgba(139, 69, 19, 0.1);\n --earth-brown-border: rgba(139, 69, 19, 0.2);\n --parchment-overlay: rgba(244, 228, 188, 0.95);\n --dark-overlay: rgba(26, 31, 46, 0.85);\n \n /* Typography */\n --text-shadow-dm: 1px 1px 2px rgba(218, 165, 32, 0.3);\n --text-shadow-glow: 0 0 10px rgba(218, 165, 32, 0.4);\n\n /* Quest UI Tokens */\n --quest-chip-height: 2rem;\n --quest-chip-radius: 9999px;\n --quest-chip-padding-x: 0.75rem;\n --quest-action-min-height: 2.75rem;\n --quest-action-gap: 0.65rem;\n}\n\n/* LOTR Typography */\n@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Lora:wght@400;600&display=swap');\n\n* {\n box-sizing: border-box;\n}\n\nbody {\n font-family: 'Lora', serif;\n background: linear-gradient(135deg, #FBF3E6 0%, #F0E5D8 50%, #F5EDE2 100%);\n background-attachment: fixed;\n min-height: 100vh;\n margin: 0;\n color: var(--earth-brown);\n font-weight: 400;\n line-height: 1.6;\n letter-spacing: 0.3px;\n position: relative;\n}\n\n/* Atmospheric mist effect */\nbody::before {\n content: '';\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: radial-gradient(ellipse at 20% 50%, rgba(212, 164, 55, 0.03) 0%, transparent 50%),\n radial-gradient(ellipse at 80% 80%, rgba(45, 80, 22, 0.02) 0%, transparent 50%);\n pointer-events: none;\n z-index: 0;\n}\n\nh1, h2, h3, h4, h5, h6 {\n font-family: 'Cinzel', serif;\n font-weight: 700;\n color: var(--deep-blue);\n text-shadow: 0 2px 4px rgba(218, 165, 32, 0.2);\n letter-spacing: 1px;\n margin-top: 0;\n}\n\nh1 {\n font-size: 2.5rem;\n font-weight: 700;\n letter-spacing: 2px;\n text-transform: uppercase;\n color: var(--forest-dark);\n text-shadow: 1px 1px 3px rgba(138, 43, 43, 0.15), 0 0 20px rgba(212, 164, 55, 0.1);\n}\n\nh2 {\n font-size: 1.875rem;\n font-weight: 700;\n letter-spacing: 1.5px;\n color: var(--forest-dark);\n}\n\nh3 {\n font-size: 1.5rem;\n font-weight: 600;\n letter-spacing: 1px;\n color: var(--deep-blue);\n}\n\nh4 {\n font-size: 1.25rem;\n font-weight: 600;\n}\n\nh5, h6 {\n font-size: 1rem;\n font-weight: 600;\n}\n\n.app {\n min-height: 100vh;\n}\n\n.app-loading {\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 100vh;\n color: white;\n font-size: 1.5rem;\n}\n\n.loading-spinner {\n animation: pulse 1.5s ease-in-out infinite;\n}\n\n@keyframes pulse {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.5;\n }\n}\n\n/* Shimmer effect for buttons */\n@keyframes shimmer {\n 0% {\n background-position: -1000px 0;\n }\n 100% {\n background-position: 1000px 0;\n }\n}\n\n/* Common button styles */\n.btn {\n padding: 10px 20px;\n border: none;\n border-radius: 5px;\n cursor: pointer;\n font-size: 1rem;\n transition: all 0.3s ease;\n font-family: 'Cinzel', serif;\n font-weight: 600;\n position: relative;\n overflow: hidden;\n letter-spacing: 0.5px;\n text-transform: uppercase;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n}\n\n.btn::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);\n animation: none;\n pointer-events: none;\n}\n\n.btn:hover::before {\n animation: shimmer 0.6s ease-in-out;\n}\n\n.btn-primary {\n background-color: var(--gold);\n color: var(--deep-blue);\n border: 2px solid var(--earth-brown);\n box-shadow: 0 4px 12px rgba(218, 165, 32, 0.4);\n}\n\n.btn-primary:hover {\n background-color: var(--gold-light);\n box-shadow: 0 6px 20px rgba(218, 165, 32, 0.6),\n 0 0 15px rgba(218, 165, 32, 0.3);\n transform: translateY(-2px);\n}\n\n.btn-primary:active {\n transform: translateY(0);\n box-shadow: 0 2px 8px rgba(218, 165, 32, 0.4);\n}\n\n.btn-secondary {\n background-color: var(--earth-brown-light);\n color: var(--parchment);\n border: 2px solid var(--earth-brown);\n box-shadow: 0 4px 12px rgba(139, 69, 19, 0.3);\n}\n\n.btn-secondary:hover {\n background-color: var(--earth-brown);\n box-shadow: 0 6px 20px rgba(139, 69, 19, 0.5),\n 0 0 15px rgba(139, 69, 19, 0.2);\n transform: translateY(-2px);\n}\n\n.btn-secondary:active {\n transform: translateY(0);\n box-shadow: 0 2px 8px rgba(139, 69, 19, 0.3);\n}\n\n.btn-danger {\n background-color: var(--dark-red);\n color: var(--parchment);\n border: 2px solid var(--dark-red-light);\n box-shadow: 0 4px 12px rgba(139, 0, 0, 0.3);\n}\n\n.btn-danger:hover {\n background-color: var(--dark-red-light);\n box-shadow: 0 6px 20px rgba(139, 0, 0, 0.5),\n 0 0 15px rgba(220, 38, 38, 0.3);\n transform: translateY(-2px);\n}\n\n.btn-danger:active {\n transform: translateY(0);\n box-shadow: 0 2px 8px rgba(139, 0, 0, 0.3);\n}\n\n/* Form styles */\n.form-group {\n margin-bottom: 1.5rem;\n}\n\n.form-label {\n display: block;\n margin-bottom: 0.5rem;\n font-weight: 600;\n color: var(--deep-blue);\n font-family: 'Cinzel', serif;\n text-transform: uppercase;\n font-size: 0.9rem;\n letter-spacing: 0.5px;\n}\n\n.form-input {\n width: 100%;\n padding: 0.75rem;\n border: 2px solid var(--earth-brown);\n border-radius: 4px;\n font-size: 1rem;\n background-color: var(--parchment);\n color: var(--deep-blue);\n font-family: 'Lora', serif;\n box-shadow: inset 0 2px 4px rgba(139, 69, 19, 0.1);\n transition: all 0.3s ease;\n}\n\n.form-input:focus {\n outline: none;\n border-color: var(--gold);\n background-color: var(--parchment-light);\n box-shadow: inset 0 2px 4px rgba(139, 69, 19, 0.1),\n 0 0 0 3px rgba(218, 165, 32, 0.2),\n 0 0 10px rgba(218, 165, 32, 0.15);\n}\n\n.form-input::placeholder {\n color: rgba(139, 69, 19, 0.5);\n font-style: italic;\n}\n\n/* Card styles */\n.card {\n background: linear-gradient(135deg, var(--parchment) 0%, var(--parchment-light) 100%);\n border-radius: 8px;\n padding: 1.5rem;\n box-shadow: 0 4px 16px rgba(139, 69, 19, 0.2),\n 0 0 1px rgba(139, 69, 19, 0.05);\n margin-bottom: 1rem;\n border: 2px solid var(--earth-brown);\n position: relative;\n overflow: hidden;\n transition: all 0.3s ease;\n}\n\n.card::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: repeating-linear-gradient(\n 90deg,\n transparent,\n transparent 2px,\n rgba(139, 69, 19, 0.03) 2px,\n rgba(139, 69, 19, 0.03) 4px\n );\n pointer-events: none;\n}\n\n.card:hover {\n box-shadow: 0 8px 24px rgba(139, 69, 19, 0.3),\n 0 0 1px rgba(139, 69, 19, 0.05),\n 0 0 15px rgba(218, 165, 32, 0.1);\n transform: translateY(-2px);\n}\n\n.card-header {\n margin-bottom: 1rem;\n padding-bottom: 1rem;\n border-bottom: 2px solid var(--earth-brown);\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.card-title {\n font-size: 1.5rem;\n font-weight: 700;\n color: var(--deep-blue);\n margin: 0;\n font-family: 'Cinzel', serif;\n letter-spacing: 1px;\n}\n\n/* Container */\n.container {\n max-width: 1200px;\n margin: 0 auto;\n padding: 2rem;\n}\n\n/* Navigation */\n.navbar {\n background: linear-gradient(180deg, var(--parchment-light) 0%, var(--parchment) 100%);\n padding: 1rem 2rem;\n box-shadow: 0 4px 12px rgba(139, 69, 19, 0.3);\n display: flex;\n justify-content: space-between;\n align-items: center;\n border-bottom: 3px solid var(--earth-brown);\n}\n\n.navbar-brand {\n font-size: 1.5rem;\n font-weight: 700;\n font-family: 'Cinzel', serif;\n color: var(--deep-blue);\n text-decoration: none;\n text-shadow: 2px 2px 4px rgba(218, 165, 32, 0.3);\n transition: all 0.3s ease;\n}\n\n.navbar-brand:hover {\n color: var(--earth-brown);\n text-shadow: 2px 2px 6px rgba(218, 165, 32, 0.5);\n}\n\n.navbar-nav {\n display: flex;\n gap: 1rem;\n align-items: center;\n}\n\n.nav-link {\n color: var(--deep-blue);\n text-decoration: none;\n padding: 0.75rem 1rem;\n border-radius: 4px;\n transition: all 0.3s ease;\n font-family: 'Lora', serif;\n border: 2px solid transparent;\n}\n\n.nav-link:hover {\n background-color: rgba(218, 165, 32, 0.2);\n border-bottom-color: var(--gold);\n}\n\n.nav-link.active {\n color: var(--gold);\n border-bottom: 2px solid var(--gold);\n font-weight: 600;\n}\n\n/* Error message */\n.error-message {\n background-color: #f8d7da;\n color: #721c24;\n padding: 1rem;\n border-radius: 4px;\n margin-bottom: 1rem;\n border-left: 4px solid var(--dark-red);\n box-shadow: 0 2px 8px rgba(220, 38, 38, 0.2);\n}\n\n/* Success message */\n.success-message {\n background-color: #d4edda;\n color: #155724;\n padding: 1rem;\n border-radius: 4px;\n margin-bottom: 1rem;\n border-left: 4px solid var(--status-it-is-done);\n box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2);\n}\n\n/* Small button variant */\n.btn-sm {\n padding: 0.5rem 0.75rem;\n font-size: 0.85rem;\n min-width: auto;\n max-width: none;\n white-space: normal;\n line-height: 1.2;\n word-break: break-word;\n}\n/* ========== RESPONSIVE DESIGN ========== */\n\n/* Tablet screens (768px - 1024px) */\n@media (max-width: 1024px) {\n .container {\n padding: 1.5rem;\n }\n\n h1 {\n font-size: 2rem;\n }\n\n h2 {\n font-size: 1.5rem;\n }\n\n .navbar {\n padding: 0.75rem 1.5rem;\n }\n\n .btn {\n padding: 8px 16px;\n font-size: 0.95rem;\n }\n}\n\n/* Mobile screens (480px - 768px) */\n@media (max-width: 768px) {\n body {\n font-size: 14px;\n }\n\n .container {\n padding: 1rem;\n }\n\n h1 {\n font-size: 1.75rem;\n letter-spacing: 0.5px;\n }\n\n h2 {\n font-size: 1.25rem;\n }\n\n h3 {\n font-size: 1.1rem;\n }\n\n .navbar {\n flex-direction: column;\n gap: 1rem;\n padding: 1rem;\n }\n\n .navbar-brand {\n font-size: 1.25rem;\n }\n\n .navbar-nav {\n width: 100%;\n justify-content: space-between;\n flex-wrap: wrap;\n }\n\n .nav-link {\n padding: 0.5rem 0.75rem;\n font-size: 0.9rem;\n }\n\n .btn {\n padding: 8px 12px;\n font-size: 0.9rem;\n }\n\n .card {\n padding: 1rem;\n margin-bottom: 1rem;\n }\n\n .card-title {\n font-size: 1.25rem;\n }\n\n .form-input,\n .form-label {\n font-size: 0.95rem;\n }\n\n .form-textarea {\n min-height: 80px;\n }\n\n /* Improve touch targets for mobile */\n .btn,\n .nav-link,\n .form-input,\n input[type=\"checkbox\"] {\n min-height: 44px;\n min-width: 44px;\n }\n}\n\n/* Small mobile screens (< 480px) */\n@media (max-width: 480px) {\n body {\n font-size: 13px;\n }\n\n .container {\n padding: 0.75rem;\n }\n\n h1 {\n font-size: 1.5rem;\n letter-spacing: 0px;\n }\n\n h2 {\n font-size: 1.1rem;\n }\n\n h3 {\n font-size: 1rem;\n }\n\n .navbar {\n padding: 0.75rem;\n }\n\n .navbar-brand {\n font-size: 1.1rem;\n }\n\n .navbar-nav {\n gap: 0.5rem;\n }\n\n .nav-link {\n padding: 0.4rem 0.6rem;\n font-size: 0.85rem;\n }\n\n .btn {\n padding: 8px 10px;\n font-size: 0.85rem;\n min-width: 44px;\n }\n\n .card {\n padding: 0.75rem;\n border-radius: 6px;\n }\n\n .card-title {\n font-size: 1.1rem;\n }\n\n .form-group {\n margin-bottom: 1rem;\n }\n\n .form-label {\n font-size: 0.85rem;\n }\n\n .form-input {\n padding: 0.6rem;\n font-size: 0.9rem;\n }\n\n /* Stack grid layouts on very small screens */\n .stats-grid,\n .quest-list {\n grid-template-columns: 1fr;\n gap: 1rem;\n }\n\n .navbar-nav {\n flex-direction: column;\n width: 100%;\n }\n\n .nav-link {\n width: 100%;\n text-align: center;\n }\n}","/* ========================================\n LOTR Fellowship - Immersive Animations\n ======================================== */\n\n/* ========== PAGE TRANSITIONS ========== */\n\n/* Fade in animation for page load */\n@keyframes fadeIn {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n/* Slide in from left */\n@keyframes slideInLeft {\n from {\n opacity: 0;\n transform: translateX(-30px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n}\n\n/* Slide in from right */\n@keyframes slideInRight {\n from {\n opacity: 0;\n transform: translateX(30px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n}\n\n/* Slide in from top */\n@keyframes slideInDown {\n from {\n opacity: 0;\n transform: translateY(-20px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* Slide in from bottom */\n@keyframes slideInUp {\n from {\n opacity: 0;\n transform: translateY(20px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* Scale and fade for modal/dialog entrance */\n@keyframes scaleIn {\n from {\n opacity: 0;\n transform: scale(0.95);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n}\n\n/* ========== SHIMMER & GLOW EFFECTS ========== */\n\n/* Enhanced shimmer effect for buttons and cards */\n@keyframes shimmer {\n 0% {\n background-position: -1000px 0;\n }\n 100% {\n background-position: 1000px 0;\n }\n}\n\n/* Arcane shimmer - gradient sweep across element */\n@keyframes arcaneShimmer {\n 0% {\n background-position: -200% center;\n }\n 50% {\n background-position: 200% center;\n }\n 100% {\n background-position: -200% center;\n }\n}\n\n/* Gold glow pulse - pulsing aura effect */\n@keyframes goldGlow {\n 0%, 100% {\n box-shadow: 0 0 5px rgba(218, 165, 32, 0.2),\n 0 0 10px rgba(218, 165, 32, 0.1);\n }\n 50% {\n box-shadow: 0 0 15px rgba(218, 165, 32, 0.5),\n 0 0 25px rgba(218, 165, 32, 0.3);\n }\n}\n\n/* Danger glow pulse - red warning aura */\n@keyframes dangerGlow {\n 0%, 100% {\n box-shadow: 0 0 5px rgba(220, 38, 38, 0.2),\n 0 0 10px rgba(220, 38, 38, 0.1);\n }\n 50% {\n box-shadow: 0 0 15px rgba(220, 38, 38, 0.5),\n 0 0 25px rgba(220, 38, 38, 0.3);\n }\n}\n\n/* Success glow pulse - green shimmer */\n@keyframes successGlow {\n 0%, 100% {\n box-shadow: 0 0 5px rgba(81, 207, 102, 0.2),\n 0 0 10px rgba(81, 207, 102, 0.1);\n }\n 50% {\n box-shadow: 0 0 15px rgba(81, 207, 102, 0.5),\n 0 0 25px rgba(81, 207, 102, 0.3);\n }\n}\n\n/* Text glow - subtle text shadow animation */\n@keyframes textGlow {\n 0%, 100% {\n text-shadow: 0 0 5px rgba(218, 165, 32, 0.2);\n }\n 50% {\n text-shadow: 0 0 15px rgba(218, 165, 32, 0.5),\n 0 0 25px rgba(218, 165, 32, 0.3);\n }\n}\n\n/* ========== ELEVATION & HOVER EFFECTS ========== */\n\n/* Hover lift effect - elevates elements on hover */\n@keyframes hoverLift {\n from {\n transform: translateY(0);\n }\n to {\n transform: translateY(-4px);\n }\n}\n\n/* Subtle scale on hover */\n@keyframes hoverScale {\n from {\n transform: scale(1);\n }\n to {\n transform: scale(1.02);\n }\n}\n\n/* ========== INTERACTIVE EFFECTS ========== */\n\n/* Ripple effect on button press */\n@keyframes ripple {\n 0% {\n transform: scale(0);\n opacity: 1;\n }\n 100% {\n transform: scale(4);\n opacity: 0;\n }\n}\n\n/* Pulse effect for important elements */\n@keyframes pulse {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.5;\n }\n}\n\n/* Quick pulse for form validation */\n@keyframes quickPulse {\n 0%, 100% {\n transform: scale(1);\n }\n 50% {\n transform: scale(1.05);\n }\n}\n\n/* Bounce effect */\n@keyframes bounce {\n 0%, 100% {\n transform: translateY(0);\n }\n 50% {\n transform: translateY(-10px);\n }\n}\n\n/* Shake effect for errors */\n@keyframes shake {\n 0%, 100% {\n transform: translateX(0);\n }\n 10%, 30%, 50%, 70%, 90% {\n transform: translateX(-5px);\n }\n 20%, 40%, 60%, 80% {\n transform: translateX(5px);\n }\n}\n\n/* ========== NUMBER COUNTERS ========== */\n\n/* Counter animation for stats */\n@keyframes countUp {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n/* ========== PARALLAX & DEPTH ========== */\n\n/* Subtle parallax scroll effect */\n@keyframes parallaxFloat {\n 0%, 100% {\n transform: translateY(0px);\n }\n 50% {\n transform: translateY(-5px);\n }\n}\n\n/* ========== CHARACTER ANIMATIONS ========== */\n\n/* Quest completion cascade - staggered reveal */\n@keyframes cascadeReveal {\n 0% {\n opacity: 0;\n transform: translateY(20px);\n }\n 100% {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n/* Character quote entrance */\n@keyframes quoteEntrance {\n 0% {\n opacity: 0;\n transform: scale(0.8) rotate(-2deg);\n }\n 100% {\n opacity: 1;\n transform: scale(1) rotate(0deg);\n }\n}\n\n/* ========== STATE TRANSITIONS ========== */\n\n/* Smooth color transition */\n@keyframes colorShift {\n 0%, 100% {\n color: currentColor;\n }\n 50% {\n color: var(--gold);\n }\n}\n\n/* Border glow on focus */\n@keyframes focusGlow {\n 0% {\n box-shadow: 0 0 0 0 rgba(218, 165, 32, 0.4);\n }\n 70% {\n box-shadow: 0 0 0 10px rgba(218, 165, 32, 0);\n }\n 100% {\n box-shadow: 0 0 0 0 rgba(218, 165, 32, 0);\n }\n}\n\n/* ========== LOADING STATES ========== */\n\n/* Rotating loader */\n@keyframes spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n\n/* Pulsing skeleton loader */\n@keyframes skeletonPulse {\n 0%, 100% {\n background-color: var(--parchment);\n }\n 50% {\n background-color: var(--parchment-light);\n }\n}\n\n/* ========== UTILITY ANIMATION CLASSES ========== */\n\n.page-enter {\n animation: fadeIn 0.6s ease-out;\n}\n\n.page-enter-left {\n animation: slideInLeft 0.6s ease-out;\n}\n\n.page-enter-right {\n animation: slideInRight 0.6s ease-out;\n}\n\n.page-enter-down {\n animation: slideInDown 0.5s ease-out;\n}\n\n.page-enter-up {\n animation: slideInUp 0.5s ease-out;\n}\n\n.modal-enter {\n animation: scaleIn 0.3s ease-out;\n}\n\n/* Card hover elevation */\n.card-elevated:hover {\n transform: translateY(-4px);\n transition: transform 0.3s ease, box-shadow 0.3s ease;\n}\n\n/* Ripple button class */\n.btn-ripple {\n position: relative;\n overflow: hidden;\n}\n\n.btn-ripple::after {\n content: '';\n position: absolute;\n top: 50%;\n left: 50%;\n width: 10px;\n height: 10px;\n background: rgba(255, 255, 255, 0.3);\n border-radius: 50%;\n transform: translate(-50%, -50%);\n animation: ripple 0.6s ease-out;\n pointer-events: none;\n}\n\n/* Glow variants for different element types */\n.glow-gold {\n animation: goldGlow 3s ease-in-out infinite;\n}\n\n.glow-danger {\n animation: dangerGlow 2s ease-in-out infinite;\n}\n\n.glow-success {\n animation: successGlow 2s ease-in-out infinite;\n}\n\n/* Text glow class */\n.text-glow {\n animation: textGlow 3s ease-in-out infinite;\n}\n\n/* Pulse for badges and notifications */\n.pulse {\n animation: pulse 2s ease-in-out infinite;\n}\n\n/* Quick pulse for form feedback */\n.quick-pulse {\n animation: quickPulse 0.4s ease-out;\n}\n\n/* Shake effect for errors */\n.shake {\n animation: shake 0.5s;\n}\n\n/* Bounce effect */\n.bounce-in {\n animation: bounce 0.6s ease-out;\n}\n\n/* Loading spinner with shimmer */\n.loading-shimmer {\n animation: arcaneShimmer 2s infinite;\n background-size: 200% 100%;\n}\n\n/* Parallax floating effect */\n.parallax-float {\n animation: parallaxFloat 4s ease-in-out infinite;\n}\n\n/* Cascade reveal for list items */\n.cascade-reveal {\n animation: cascadeReveal 0.6s ease-out;\n}\n\n/* Staggered cascade for multiple items */\n.cascade-item {\n animation: cascadeReveal 0.6s ease-out backwards;\n}\n\n.cascade-item:nth-child(1) { animation-delay: 0s; }\n.cascade-item:nth-child(2) { animation-delay: 0.1s; }\n.cascade-item:nth-child(3) { animation-delay: 0.2s; }\n.cascade-item:nth-child(4) { animation-delay: 0.3s; }\n.cascade-item:nth-child(5) { animation-delay: 0.4s; }\n.cascade-item:nth-child(n+6) { animation-delay: 0.5s; }\n\n/* ========== ACCESSIBILITY ========== */\n/* Respect prefers-reduced-motion */\n@media (prefers-reduced-motion: reduce) {\n * {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n}\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/sut/frontend/build/static/js/main.cdb3f2d7.js b/sut/frontend/build/static/js/main.cdb3f2d7.js new file mode 100644 index 0000000..5f611fc --- /dev/null +++ b/sut/frontend/build/static/js/main.cdb3f2d7.js @@ -0,0 +1,3 @@ +/*! For license information please see main.cdb3f2d7.js.LICENSE.txt */ +(()=>{var e={789(e){"use strict";var t=function(e){return function(e){return!!e&&"object"===typeof e}(e)&&!function(e){var t=Object.prototype.toString.call(e);return"[object RegExp]"===t||"[object Date]"===t||function(e){return e.$$typeof===n}(e)}(e)};var n="function"===typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function i(e,t){return!1!==t.clone&&t.isMergeableObject(e)?l((n=e,Array.isArray(n)?[]:{}),e,t):e;var n}function r(e,t,n){return e.concat(t).map(function(e){return i(e,n)})}function o(e){return Object.keys(e).concat(function(e){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e).filter(function(t){return Object.propertyIsEnumerable.call(e,t)}):[]}(e))}function s(e,t){try{return t in e}catch(n){return!1}}function a(e,t,n){var r={};return n.isMergeableObject(e)&&o(e).forEach(function(t){r[t]=i(e[t],n)}),o(t).forEach(function(o){(function(e,t){return s(e,t)&&!(Object.hasOwnProperty.call(e,t)&&Object.propertyIsEnumerable.call(e,t))})(e,o)||(s(e,o)&&n.isMergeableObject(t[o])?r[o]=function(e,t){if(!t.customMerge)return l;var n=t.customMerge(e);return"function"===typeof n?n:l}(o,n)(e[o],t[o],n):r[o]=i(t[o],n))}),r}function l(e,n,o){(o=o||{}).arrayMerge=o.arrayMerge||r,o.isMergeableObject=o.isMergeableObject||t,o.cloneUnlessOtherwiseSpecified=i;var s=Array.isArray(n);return s===Array.isArray(e)?s?o.arrayMerge(e,n,o):a(e,n,o):i(n,o)}l.all=function(e,t){if(!Array.isArray(e))throw new Error("first argument should be an array");return e.reduce(function(e,n){return l(e,n,t)},{})};var c=l;e.exports=c},260(e,t){!function(e){"use strict";var t=L.MarkerClusterGroup=L.FeatureGroup.extend({options:{maxClusterRadius:80,iconCreateFunction:null,clusterPane:L.Marker.prototype.options.pane,spiderfyOnEveryZoom:!1,spiderfyOnMaxZoom:!0,showCoverageOnHover:!0,zoomToBoundsOnClick:!0,singleMarkerMode:!1,disableClusteringAtZoom:null,removeOutsideVisibleBounds:!0,animate:!0,animateAddingMarkers:!1,spiderfyShapePositions:null,spiderfyDistanceMultiplier:1,spiderLegPolylineOptions:{weight:1.5,color:"#222",opacity:.5},chunkedLoading:!1,chunkInterval:200,chunkDelay:50,chunkProgress:null,polygonOptions:{}},initialize:function(e){L.Util.setOptions(this,e),this.options.iconCreateFunction||(this.options.iconCreateFunction=this._defaultIconCreateFunction),this._featureGroup=L.featureGroup(),this._featureGroup.addEventParent(this),this._nonPointGroup=L.featureGroup(),this._nonPointGroup.addEventParent(this),this._inZoomAnimation=0,this._needsClustering=[],this._needsRemoving=[],this._currentShownBounds=null,this._queue=[],this._childMarkerEventHandlers={dragstart:this._childMarkerDragStart,move:this._childMarkerMoved,dragend:this._childMarkerDragEnd};var t=L.DomUtil.TRANSITION&&this.options.animate;L.extend(this,t?this._withAnimation:this._noAnimation),this._markerCluster=t?L.MarkerCluster:L.MarkerClusterNonAnimated},addLayer:function(e){if(e instanceof L.LayerGroup)return this.addLayers([e]);if(!e.getLatLng)return this._nonPointGroup.addLayer(e),this.fire("layeradd",{layer:e}),this;if(!this._map)return this._needsClustering.push(e),this.fire("layeradd",{layer:e}),this;if(this.hasLayer(e))return this;this._unspiderfy&&this._unspiderfy(),this._addLayer(e,this._maxZoom),this.fire("layeradd",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons();var t=e,n=this._zoom;if(e.__parent)for(;t.__parent._zoom>=n;)t=t.__parent;return this._currentShownBounds.contains(t.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(e,t):this._animationAddLayerNonAnimated(e,t)),this},removeLayer:function(e){return e instanceof L.LayerGroup?this.removeLayers([e]):e.getLatLng?this._map?e.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(e)),this._removeLayer(e,!0),this.fire("layerremove",{layer:e}),this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),e.off(this._childMarkerEventHandlers,this),this._featureGroup.hasLayer(e)&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow()),this):this:(!this._arraySplice(this._needsClustering,e)&&this.hasLayer(e)&&this._needsRemoving.push({layer:e,latlng:e._latlng}),this.fire("layerremove",{layer:e}),this):(this._nonPointGroup.removeLayer(e),this.fire("layerremove",{layer:e}),this)},addLayers:function(e,t){if(!L.Util.isArray(e))return this.addLayer(e);var n,i=this._featureGroup,r=this._nonPointGroup,o=this.options.chunkedLoading,s=this.options.chunkInterval,a=this.options.chunkProgress,l=e.length,c=0,u=!0;if(this._map){var d=(new Date).getTime(),h=L.bind(function(){var p=(new Date).getTime();for(this._map&&this._unspiderfy&&this._unspiderfy();cs);c++)if((n=e[c])instanceof L.LayerGroup)u&&(e=e.slice(),u=!1),this._extractNonGroupLayers(n,e),l=e.length;else if(n.getLatLng){if(!this.hasLayer(n)&&(this._addLayer(n,this._maxZoom),t||this.fire("layeradd",{layer:n}),n.__parent&&2===n.__parent.getChildCount())){var f=n.__parent.getAllChildMarkers(),m=f[0]===n?f[1]:f[0];i.removeLayer(m)}}else r.addLayer(n),t||this.fire("layeradd",{layer:n});a&&a(c,l,(new Date).getTime()-d),c===l?(this._topClusterLevel._recalculateBounds(),this._refreshClustersIcons(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)):setTimeout(h,this.options.chunkDelay)},this);h()}else for(var p=this._needsClustering;c=0;t--)e.extend(this._needsClustering[t].getLatLng());return e.extend(this._nonPointGroup.getBounds()),e},eachLayer:function(e,t){var n,i,r,o=this._needsClustering.slice(),s=this._needsRemoving;for(this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(o),i=o.length-1;i>=0;i--){for(n=!0,r=s.length-1;r>=0;r--)if(s[r].layer===o[i]){n=!1;break}n&&e.call(t,o[i])}this._nonPointGroup.eachLayer(e,t)},getLayers:function(){var e=[];return this.eachLayer(function(t){e.push(t)}),e},getLayer:function(e){var t=null;return e=parseInt(e,10),this.eachLayer(function(n){L.stamp(n)===e&&(t=n)}),t},hasLayer:function(e){if(!e)return!1;var t,n=this._needsClustering;for(t=n.length-1;t>=0;t--)if(n[t]===e)return!0;for(t=(n=this._needsRemoving).length-1;t>=0;t--)if(n[t].layer===e)return!1;return!(!e.__parent||e.__parent._group!==this)||this._nonPointGroup.hasLayer(e)},zoomToShowLayer:function(e,t){var n=this._map;"function"!==typeof t&&(t=function(){});var i=function(){!n.hasLayer(e)&&!n.hasLayer(e.__parent)||this._inZoomAnimation||(this._map.off("moveend",i,this),this.off("animationend",i,this),n.hasLayer(e)?t():e.__parent._icon&&(this.once("spiderfied",t,this),e.__parent.spiderfy()))};e._icon&&this._map.getBounds().contains(e.getLatLng())?t():e.__parent._zoom=0;n--)if(e[n]===t)return e.splice(n,1),!0},_removeFromGridUnclustered:function(e,t){for(var n=this._map,i=this._gridUnclustered,r=Math.floor(this._map.getMinZoom());t>=r&&i[t].removeObject(e,n.project(e.getLatLng(),t));t--);},_childMarkerDragStart:function(e){e.target.__dragStart=e.target._latlng},_childMarkerMoved:function(e){if(!this._ignoreMove&&!e.target.__dragStart){var t=e.target._popup&&e.target._popup.isOpen();this._moveChild(e.target,e.oldLatLng,e.latlng),t&&e.target.openPopup()}},_moveChild:function(e,t,n){e._latlng=t,this.removeLayer(e),e._latlng=n,this.addLayer(e)},_childMarkerDragEnd:function(e){var t=e.target.__dragStart;delete e.target.__dragStart,t&&this._moveChild(e.target,t,e.target._latlng)},_removeLayer:function(e,t,n){var i=this._gridClusters,r=this._gridUnclustered,o=this._featureGroup,s=this._map,a=Math.floor(this._map.getMinZoom());t&&this._removeFromGridUnclustered(e,this._maxZoom);var l,c=e.__parent,u=c._markers;for(this._arraySplice(u,e);c&&(c._childCount--,c._boundsNeedUpdate=!0,!(c._zoom"+t+"",className:"marker-cluster"+n,iconSize:new L.Point(40,40)})},_bindEvents:function(){var e=this._map,t=this.options.spiderfyOnMaxZoom,n=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick,r=this.options.spiderfyOnEveryZoom;(t||i||r)&&this.on("clusterclick clusterkeypress",this._zoomOrSpiderfy,this),n&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),e.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(e){var t=e.layer,n=t;if("clusterkeypress"!==e.type||!e.originalEvent||13===e.originalEvent.keyCode){for(;1===n._childClusters.length;)n=n._childClusters[0];n._zoom===this._maxZoom&&n._childCount===t._childCount&&this.options.spiderfyOnMaxZoom?t.spiderfy():this.options.zoomToBoundsOnClick&&t.zoomToBounds(),this.options.spiderfyOnEveryZoom&&t.spiderfy(),e.originalEvent&&13===e.originalEvent.keyCode&&this._map._container.focus()}},_showCoverage:function(e){var t=this._map;this._inZoomAnimation||(this._shownPolygon&&t.removeLayer(this._shownPolygon),e.layer.getChildCount()>2&&e.layer!==this._spiderfied&&(this._shownPolygon=new L.Polygon(e.layer.getConvexHull(),this.options.polygonOptions),t.addLayer(this._shownPolygon)))},_hideCoverage:function(){this._shownPolygon&&(this._map.removeLayer(this._shownPolygon),this._shownPolygon=null)},_unbindEvents:function(){var e=this.options.spiderfyOnMaxZoom,t=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick,i=this.options.spiderfyOnEveryZoom,r=this._map;(e||n||i)&&this.off("clusterclick clusterkeypress",this._zoomOrSpiderfy,this),t&&(this.off("clustermouseover",this._showCoverage,this),this.off("clustermouseout",this._hideCoverage,this),r.off("zoomend",this._hideCoverage,this))},_zoomEnd:function(){this._map&&(this._mergeSplitClusters(),this._zoom=Math.round(this._map._zoom),this._currentShownBounds=this._getExpandedVisibleBounds())},_moveEnd:function(){if(!this._inZoomAnimation){var e=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,Math.floor(this._map.getMinZoom()),this._zoom,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,Math.round(this._map._zoom),e),this._currentShownBounds=e}},_generateInitialClusters:function(){var e=Math.ceil(this._map.getMaxZoom()),t=Math.floor(this._map.getMinZoom()),n=this.options.maxClusterRadius,i=n;"function"!==typeof n&&(i=function(){return n}),null!==this.options.disableClusteringAtZoom&&(e=this.options.disableClusteringAtZoom-1),this._maxZoom=e,this._gridClusters={},this._gridUnclustered={};for(var r=e;r>=t;r--)this._gridClusters[r]=new L.DistanceGrid(i(r)),this._gridUnclustered[r]=new L.DistanceGrid(i(r));this._topClusterLevel=new this._markerCluster(this,t-1)},_addLayer:function(e,t){var n,i,r=this._gridClusters,o=this._gridUnclustered,s=Math.floor(this._map.getMinZoom());for(this.options.singleMarkerMode&&this._overrideMarkerIcon(e),e.on(this._childMarkerEventHandlers,this);t>=s;t--){n=this._map.project(e.getLatLng(),t);var a=r[t].getNearObject(n);if(a)return a._addChild(e),void(e.__parent=a);if(a=o[t].getNearObject(n)){var l=a.__parent;l&&this._removeLayer(a,!1);var c=new this._markerCluster(this,t,a,e);r[t].addObject(c,this._map.project(c._cLatLng,t)),a.__parent=c,e.__parent=c;var u=c;for(i=t-1;i>l._zoom;i--)u=new this._markerCluster(this,i,u),r[i].addObject(u,this._map.project(a.getLatLng(),i));return l._addChild(u),void this._removeFromGridUnclustered(a,t)}o[t].addObject(e,n)}this._topClusterLevel._addChild(e),e.__parent=this._topClusterLevel},_refreshClustersIcons:function(){this._featureGroup.eachLayer(function(e){e instanceof L.MarkerCluster&&e._iconNeedsUpdate&&e._updateIcon()})},_enqueue:function(e){this._queue.push(e),this._queueTimeout||(this._queueTimeout=setTimeout(L.bind(this._processQueue,this),300))},_processQueue:function(){for(var e=0;ee?(this._animationStart(),this._animationZoomOut(this._zoom,e)):this._moveEnd()},_getExpandedVisibleBounds:function(){return this.options.removeOutsideVisibleBounds?L.Browser.mobile?this._checkBoundsMaxLat(this._map.getBounds()):this._checkBoundsMaxLat(this._map.getBounds().pad(1)):this._mapBoundsInfinite},_checkBoundsMaxLat:function(e){var t=this._maxLat;return void 0!==t&&(e.getNorth()>=t&&(e._northEast.lat=1/0),e.getSouth()<=-t&&(e._southWest.lat=-1/0)),e},_animationAddLayerNonAnimated:function(e,t){if(t===e)this._featureGroup.addLayer(e);else if(2===t._childCount){t._addToMap();var n=t.getAllChildMarkers();this._featureGroup.removeLayer(n[0]),this._featureGroup.removeLayer(n[1])}else t._updateIcon()},_extractNonGroupLayers:function(e,t){var n,i=e.getLayers(),r=0;for(t=t||[];r=0;n--)s=l[n],i.contains(s._latlng)||r.removeLayer(s)}),this._forceLayout(),this._topClusterLevel._recursivelyBecomeVisible(i,t),r.eachLayer(function(e){e instanceof L.MarkerCluster||!e._icon||e.clusterShow()}),this._topClusterLevel._recursively(i,e,t,function(e){e._recursivelyRestoreChildPositions(t)}),this._ignoreMove=!1,this._enqueue(function(){this._topClusterLevel._recursively(i,e,o,function(e){r.removeLayer(e),e.clusterShow()}),this._animationEnd()})},_animationZoomOut:function(e,t){this._animationZoomOutSingle(this._topClusterLevel,e-1,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,t,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,Math.floor(this._map.getMinZoom()),e,this._getExpandedVisibleBounds())},_animationAddLayer:function(e,t){var n=this,i=this._featureGroup;i.addLayer(e),t!==e&&(t._childCount>2?(t._updateIcon(),this._forceLayout(),this._animationStart(),e._setPos(this._map.latLngToLayerPoint(t.getLatLng())),e.clusterHide(),this._enqueue(function(){i.removeLayer(e),e.clusterShow(),n._animationEnd()})):(this._forceLayout(),n._animationStart(),n._animationZoomOutSingle(t,this._map.getMaxZoom(),this._zoom)))}},_animationZoomOutSingle:function(e,t,n){var i=this._getExpandedVisibleBounds(),r=Math.floor(this._map.getMinZoom());e._recursivelyAnimateChildrenInAndAddSelfToMap(i,r,t+1,n);var o=this;this._forceLayout(),e._recursivelyBecomeVisible(i,n),this._enqueue(function(){if(1===e._childCount){var s=e._markers[0];this._ignoreMove=!0,s.setLatLng(s.getLatLng()),this._ignoreMove=!1,s.clusterShow&&s.clusterShow()}else e._recursively(i,n,r,function(e){e._recursivelyRemoveChildrenFromMap(i,r,t+1)});o._animationEnd()})},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_forceLayout:function(){L.Util.falseFn(document.body.offsetWidth)}}),L.markerClusterGroup=function(e){return new L.MarkerClusterGroup(e)};var n=L.MarkerCluster=L.Marker.extend({options:L.Icon.prototype.options,initialize:function(e,t,n,i){L.Marker.prototype.initialize.call(this,n?n._cLatLng||n.getLatLng():new L.LatLng(0,0),{icon:this,pane:e.options.clusterPane}),this._group=e,this._zoom=t,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._bounds=new L.LatLngBounds,n&&this._addChild(n),i&&this._addChild(i)},getAllChildMarkers:function(e,t){e=e||[];for(var n=this._childClusters.length-1;n>=0;n--)this._childClusters[n].getAllChildMarkers(e,t);for(var i=this._markers.length-1;i>=0;i--)t&&this._markers[i].__dragStart||e.push(this._markers[i]);return e},getChildCount:function(){return this._childCount},zoomToBounds:function(e){for(var t,n=this._childClusters.slice(),i=this._group._map,r=i.getBoundsZoom(this._bounds),o=this._zoom+1,s=i.getZoom();n.length>0&&r>o;){o++;var a=[];for(t=0;to?this._group._map.setView(this._latlng,o):r<=s?this._group._map.setView(this._latlng,s+1):this._group._map.fitBounds(this._bounds,e)},getBounds:function(){var e=new L.LatLngBounds;return e.extend(this._bounds),e},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(e,t){this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._setClusterCenter(e),e instanceof L.MarkerCluster?(t||(this._childClusters.push(e),e.__parent=this),this._childCount+=e._childCount):(t||this._markers.push(e),this._childCount++),this.__parent&&this.__parent._addChild(e,!0)},_setClusterCenter:function(e){this._cLatLng||(this._cLatLng=e._cLatLng||e._latlng)},_resetBounds:function(){var e=this._bounds;e._southWest&&(e._southWest.lat=1/0,e._southWest.lng=1/0),e._northEast&&(e._northEast.lat=-1/0,e._northEast.lng=-1/0)},_recalculateBounds:function(){var e,t,n,i,r=this._markers,o=this._childClusters,s=0,a=0,l=this._childCount;if(0!==l){for(this._resetBounds(),e=0;e=0;n--)(i=r[n])._icon&&(i._setPos(t),i.clusterHide())},function(e){var n,i,r=e._childClusters;for(n=r.length-1;n>=0;n--)(i=r[n])._icon&&(i._setPos(t),i.clusterHide())})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(e,t,n,i){this._recursively(e,i,t,function(r){r._recursivelyAnimateChildrenIn(e,r._group._map.latLngToLayerPoint(r.getLatLng()).round(),n),r._isSingleParent()&&n-1===i?(r.clusterShow(),r._recursivelyRemoveChildrenFromMap(e,t,n)):r.clusterHide(),r._addToMap()})},_recursivelyBecomeVisible:function(e,t){this._recursively(e,this._group._map.getMinZoom(),t,null,function(e){e.clusterShow()})},_recursivelyAddChildrenToMap:function(e,t,n){this._recursively(n,this._group._map.getMinZoom()-1,t,function(i){if(t!==i._zoom)for(var r=i._markers.length-1;r>=0;r--){var o=i._markers[r];n.contains(o._latlng)&&(e&&(o._backupLatlng=o.getLatLng(),o.setLatLng(e),o.clusterHide&&o.clusterHide()),i._group._featureGroup.addLayer(o))}},function(t){t._addToMap(e)})},_recursivelyRestoreChildPositions:function(e){for(var t=this._markers.length-1;t>=0;t--){var n=this._markers[t];n._backupLatlng&&(n.setLatLng(n._backupLatlng),delete n._backupLatlng)}if(e-1===this._zoom)for(var i=this._childClusters.length-1;i>=0;i--)this._childClusters[i]._restorePosition();else for(var r=this._childClusters.length-1;r>=0;r--)this._childClusters[r]._recursivelyRestoreChildPositions(e)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(e,t,n,i){var r,o;this._recursively(e,t-1,n-1,function(e){for(o=e._markers.length-1;o>=0;o--)r=e._markers[o],i&&i.contains(r._latlng)||(e._group._featureGroup.removeLayer(r),r.clusterShow&&r.clusterShow())},function(e){for(o=e._childClusters.length-1;o>=0;o--)r=e._childClusters[o],i&&i.contains(r._latlng)||(e._group._featureGroup.removeLayer(r),r.clusterShow&&r.clusterShow())})},_recursively:function(e,t,n,i,r){var o,s,a=this._childClusters,l=this._zoom;if(t<=l&&(i&&i(this),r&&l===n&&r(this)),l=0;o--)(s=a[o])._boundsNeedUpdate&&s._recalculateBounds(),e.intersects(s._bounds)&&s._recursively(e,t,n,i,r)},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}});L.Marker.include({clusterHide:function(){var e=this.options.opacity;return this.setOpacity(0),this.options.opacity=e,this},clusterShow:function(){return this.setOpacity(this.options.opacity)}}),L.DistanceGrid=function(e){this._cellSize=e,this._sqCellSize=e*e,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(e,t){var n=this._getCoord(t.x),i=this._getCoord(t.y),r=this._grid,o=r[i]=r[i]||{},s=o[n]=o[n]||[],a=L.Util.stamp(e);this._objectPoint[a]=t,s.push(e)},updateObject:function(e,t){this.removeObject(e),this.addObject(e,t)},removeObject:function(e,t){var n,i,r=this._getCoord(t.x),o=this._getCoord(t.y),s=this._grid,a=s[o]=s[o]||{},l=a[r]=a[r]||[];for(delete this._objectPoint[L.Util.stamp(e)],n=0,i=l.length;n=0;n--)i=t[n],(r=this.getDistant(i,e))>0&&(a.push(i),r>o&&(o=r,s=i));return{maxPoint:s,newPoints:a}},buildConvexHull:function(e,t){var n=[],i=this.findMostDistantPointFromBaseLine(e,t);return i.maxPoint?n=(n=n.concat(this.buildConvexHull([e[0],i.maxPoint],i.newPoints))).concat(this.buildConvexHull([i.maxPoint,e[1]],i.newPoints)):[e[0]]},getConvexHull:function(e){var t,n=!1,i=!1,r=!1,o=!1,s=null,a=null,l=null,c=null,u=null,d=null;for(t=e.length-1;t>=0;t--){var h=e[t];(!1===n||h.lat>n)&&(s=h,n=h.lat),(!1===i||h.latr)&&(l=h,r=h.lng),(!1===o||h.lng=0;t--)e=n[t].getLatLng(),i.push(e);return L.QuickHull.getConvexHull(i)}}),L.MarkerCluster.include({_2PI:2*Math.PI,_circleFootSeparation:25,_circleStartAngle:0,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied!==this&&!this._group._inZoomAnimation){var e,t=this.getAllChildMarkers(null,!0),n=this._group._map.latLngToLayerPoint(this._latlng);this._group._unspiderfy(),this._group._spiderfied=this,this._group.options.spiderfyShapePositions?e=this._group.options.spiderfyShapePositions(t.length,n):t.length>=this._circleSpiralSwitchover?e=this._generatePointsSpiral(t.length,n):(n.y+=10,e=this._generatePointsCircle(t.length,n)),this._animationSpiderfy(t,e)}},unspiderfy:function(e){this._group._inZoomAnimation||(this._animationUnspiderfy(e),this._group._spiderfied=null)},_generatePointsCircle:function(e,t){var n,i,r=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+e)/this._2PI,o=this._2PI/e,s=[];for(r=Math.max(r,35),s.length=e,n=0;n=0;n--)n=0;t--)e=o[t],r.removeLayer(e),e._preSpiderfyLatlng&&(e.setLatLng(e._preSpiderfyLatlng),delete e._preSpiderfyLatlng),e.setZIndexOffset&&e.setZIndexOffset(0),e._spiderLeg&&(i.removeLayer(e._spiderLeg),delete e._spiderLeg);n.fire("unspiderfied",{cluster:this,markers:o}),n._ignoreMove=!1,n._spiderfied=null}}),L.MarkerClusterNonAnimated=L.MarkerCluster.extend({_animationSpiderfy:function(e,t){var n,i,r,o,s=this._group,a=s._map,l=s._featureGroup,c=this._group.options.spiderLegPolylineOptions;for(s._ignoreMove=!0,n=0;n=0;n--)a=u.layerPointToLatLng(t[n]),(i=e[n])._preSpiderfyLatlng=i._latlng,i.setLatLng(a),i.clusterShow&&i.clusterShow(),f&&((o=(r=i._spiderLeg)._path).style.strokeDashoffset=0,r.setStyle({opacity:g}));this.setOpacity(.3),c._ignoreMove=!1,setTimeout(function(){c._animationEnd(),c.fire("spiderfied",{cluster:l,markers:e})},200)},_animationUnspiderfy:function(e){var t,n,i,r,o,s,a=this,l=this._group,c=l._map,u=l._featureGroup,d=e?c._latLngToNewLayerPoint(this._latlng,e.zoom,e.center):c.latLngToLayerPoint(this._latlng),h=this.getAllChildMarkers(null,!0),p=L.Path.SVG;for(l._ignoreMove=!0,l._animationStart(),this.setOpacity(1),n=h.length-1;n>=0;n--)(t=h[n])._preSpiderfyLatlng&&(t.closePopup(),t.setLatLng(t._preSpiderfyLatlng),delete t._preSpiderfyLatlng,s=!0,t._setPos&&(t._setPos(d),s=!1),t.clusterHide&&(t.clusterHide(),s=!1),s&&u.removeLayer(t),p&&(o=(r=(i=t._spiderLeg)._path).getTotalLength()+.1,r.style.strokeDashoffset=o,i.setStyle({opacity:0})));l._ignoreMove=!1,setTimeout(function(){var e=0;for(n=h.length-1;n>=0;n--)(t=h[n])._spiderLeg&&e++;for(n=h.length-1;n>=0;n--)(t=h[n])._spiderLeg&&(t.clusterShow&&t.clusterShow(),t.setZIndexOffset&&t.setZIndexOffset(0),e>1&&u.removeLayer(t),c.removeLayer(t._spiderLeg),delete t._spiderLeg);l._animationEnd(),l.fire("unspiderfied",{cluster:a,markers:h})},200)}}),L.MarkerClusterGroup.include({_spiderfied:null,unspiderfy:function(){this._unspiderfy.apply(this,arguments)},_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation&&this._map.on("zoomstart",this._unspiderfyZoomStart,this),this._map.on("zoomend",this._noanimationUnspiderfy,this),L.Browser.touch||this._map.getRenderer(this)},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._map.off("zoomend",this._noanimationUnspiderfy,this),this._noanimationUnspiderfy()},_unspiderfyZoomStart:function(){this._map&&this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(e){L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching")||(this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(e))},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(e){this._spiderfied&&this._spiderfied.unspiderfy(e)},_noanimationUnspiderfy:function(){this._spiderfied&&this._spiderfied._noanimationUnspiderfy()},_unspiderfyLayer:function(e){e._spiderLeg&&(this._featureGroup.removeLayer(e),e.clusterShow&&e.clusterShow(),e.setZIndexOffset&&e.setZIndexOffset(0),this._map.removeLayer(e._spiderLeg),delete e._spiderLeg)}}),L.MarkerClusterGroup.include({refreshClusters:function(e){return e?e instanceof L.MarkerClusterGroup?e=e._topClusterLevel.getAllChildMarkers():e instanceof L.LayerGroup?e=e._layers:e instanceof L.MarkerCluster?e=e.getAllChildMarkers():e instanceof L.Marker&&(e=[e]):e=this._topClusterLevel.getAllChildMarkers(),this._flagParentsIconsNeedUpdate(e),this._refreshClustersIcons(),this.options.singleMarkerMode&&this._refreshSingleMarkerModeMarkers(e),this},_flagParentsIconsNeedUpdate:function(e){var t,n;for(t in e)for(n=e[t].__parent;n;)n._iconNeedsUpdate=!0,n=n.__parent},_refreshSingleMarkerModeMarkers:function(e){var t,n;for(t in e)n=e[t],this.hasLayer(n)&&n.setIcon(this._overrideMarkerIcon(n))}}),L.Marker.include({refreshIconOptions:function(e,t){var n=this.options.icon;return L.setOptions(n,e),this.setIcon(n),t&&this.__parent&&this.__parent._group.refreshClusters(this),this}}),e.MarkerClusterGroup=t,e.MarkerCluster=n,Object.defineProperty(e,"__esModule",{value:!0})}(t)},228(e,t){!function(e){"use strict";var t="1.9.4";function n(e){var t,n,i,r;for(n=1,i=arguments.length;n0?Math.floor(e):Math.ceil(e)};function R(e,t,n){return e instanceof M?e:v(e)?new M(e[0],e[1]):void 0===e||null===e?e:"object"===typeof e&&"x"in e&&"y"in e?new M(e.x,e.y):new M(e,t,n)}function I(e,t){if(e)for(var n=t?[e,t]:e,i=0,r=n.length;i=this.min.x&&n.x<=this.max.x&&t.y>=this.min.y&&n.y<=this.max.y},intersects:function(e){e=z(e);var t=this.min,n=this.max,i=e.min,r=e.max,o=r.x>=t.x&&i.x<=n.x,s=r.y>=t.y&&i.y<=n.y;return o&&s},overlaps:function(e){e=z(e);var t=this.min,n=this.max,i=e.min,r=e.max,o=r.x>t.x&&i.xt.y&&i.y=i.lat&&n.lat<=r.lat&&t.lng>=i.lng&&n.lng<=r.lng},intersects:function(e){e=D(e);var t=this._southWest,n=this._northEast,i=e.getSouthWest(),r=e.getNorthEast(),o=r.lat>=t.lat&&i.lat<=n.lat,s=r.lng>=t.lng&&i.lng<=n.lng;return o&&s},overlaps:function(e){e=D(e);var t=this._southWest,n=this._northEast,i=e.getSouthWest(),r=e.getNorthEast(),o=r.lat>t.lat&&i.latt.lng&&i.lng1,Pe=function(){var e=!1;try{var t=Object.defineProperty({},"passive",{get:function(){e=!0}});window.addEventListener("testPassiveEventSupport",c,t),window.removeEventListener("testPassiveEventSupport",c,t)}catch(n){}return e}(),Te=!!document.createElement("canvas").getContext,je=!(!document.createElementNS||!K("svg").createSVGRect),Ne=!!je&&function(){var e=document.createElement("div");return e.innerHTML="","http://www.w3.org/2000/svg"===(e.firstChild&&e.firstChild.namespaceURI)}(),Oe=!je&&function(){try{var e=document.createElement("div");e.innerHTML='';var t=e.firstChild;return t.style.behavior="url(#default#VML)",t&&"object"===typeof t.adj}catch(n){return!1}}(),Me=0===navigator.platform.indexOf("Mac"),Ae=0===navigator.platform.indexOf("Linux");function Re(e){return navigator.userAgent.toLowerCase().indexOf(e)>=0}var Ie={ie:J,ielt9:ee,edge:te,webkit:ne,android:ie,android23:re,androidStock:se,opera:ae,chrome:le,gecko:ce,safari:ue,phantom:de,opera12:he,win:pe,ie3d:fe,webkit3d:me,gecko3d:ge,any3d:ve,mobile:ye,mobileWebkit:_e,mobileWebkit3d:be,msPointer:xe,pointer:we,touch:Se,touchNative:ke,mobileOpera:Ce,mobileGecko:Le,retina:Ee,passiveEvents:Pe,canvas:Te,svg:je,vml:Oe,inlineSvg:Ne,mac:Me,linux:Ae},ze=Ie.msPointer?"MSPointerDown":"pointerdown",Be=Ie.msPointer?"MSPointerMove":"pointermove",De=Ie.msPointer?"MSPointerUp":"pointerup",Fe=Ie.msPointer?"MSPointerCancel":"pointercancel",Ue={touchstart:ze,touchmove:Be,touchend:De,touchcancel:Fe},qe={touchstart:Xe,touchmove:$e,touchend:$e,touchcancel:$e},Ve={},We=!1;function He(e,t,n){return"touchstart"===t&&Ke(),qe[t]?(n=qe[t].bind(this,n),e.addEventListener(Ue[t],n,!1),n):(console.warn("wrong event specified:",t),c)}function Ze(e,t,n){Ue[t]?e.removeEventListener(Ue[t],n,!1):console.warn("wrong event specified:",t)}function Ge(e){Ve[e.pointerId]=e}function Qe(e){Ve[e.pointerId]&&(Ve[e.pointerId]=e)}function Ye(e){delete Ve[e.pointerId]}function Ke(){We||(document.addEventListener(ze,Ge,!0),document.addEventListener(Be,Qe,!0),document.addEventListener(De,Ye,!0),document.addEventListener(Fe,Ye,!0),We=!0)}function $e(e,t){if(t.pointerType!==(t.MSPOINTER_TYPE_MOUSE||"mouse")){for(var n in t.touches=[],Ve)t.touches.push(Ve[n]);t.changedTouches=[t],e(t)}}function Xe(e,t){t.MSPOINTER_TYPE_TOUCH&&t.pointerType===t.MSPOINTER_TYPE_TOUCH&&Gt(t),$e(e,t)}function Je(e){var t,n,i={};for(n in e)t=e[n],i[n]=t&&t.bind?t.bind(e):t;return e=i,i.type="dblclick",i.detail=2,i.isTrusted=!1,i._simulated=!0,i}var et=200;function tt(e,t){e.addEventListener("dblclick",t);var n,i=0;function r(e){if(1===e.detail){if("mouse"!==e.pointerType&&(!e.sourceCapabilities||e.sourceCapabilities.firesTouchEvents)){var r=Yt(e);if(!r.some(function(e){return e instanceof HTMLLabelElement&&e.attributes.for})||r.some(function(e){return e instanceof HTMLInputElement||e instanceof HTMLSelectElement})){var o=Date.now();o-i<=et?2===++n&&t(Je(e)):n=1,i=o}}}else n=e.detail}return e.addEventListener("click",r),{dblclick:t,simDblclick:r}}function nt(e,t){e.removeEventListener("dblclick",t.dblclick),e.removeEventListener("click",t.simDblclick)}var it,rt,ot,st,at,lt=Ct(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),ct=Ct(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),ut="webkitTransition"===ct||"OTransition"===ct?ct+"End":"transitionend";function dt(e){return"string"===typeof e?document.getElementById(e):e}function ht(e,t){var n=e.style[t]||e.currentStyle&&e.currentStyle[t];if((!n||"auto"===n)&&document.defaultView){var i=document.defaultView.getComputedStyle(e,null);n=i?i[t]:null}return"auto"===n?null:n}function pt(e,t,n){var i=document.createElement(e);return i.className=t||"",n&&n.appendChild(i),i}function ft(e){var t=e.parentNode;t&&t.removeChild(e)}function mt(e){for(;e.firstChild;)e.removeChild(e.firstChild)}function gt(e){var t=e.parentNode;t&&t.lastChild!==e&&t.appendChild(e)}function vt(e){var t=e.parentNode;t&&t.firstChild!==e&&t.insertBefore(e,t.firstChild)}function yt(e,t){if(void 0!==e.classList)return e.classList.contains(t);var n=wt(e);return n.length>0&&new RegExp("(^|\\s)"+t+"(\\s|$)").test(n)}function _t(e,t){if(void 0!==e.classList)for(var n=h(t),i=0,r=n.length;i0?2*window.devicePixelRatio:1;function Xt(e){return Ie.edge?e.wheelDeltaY/2:e.deltaY&&0===e.deltaMode?-e.deltaY/$t:e.deltaY&&1===e.deltaMode?20*-e.deltaY:e.deltaY&&2===e.deltaMode?60*-e.deltaY:e.deltaX||e.deltaZ?0:e.wheelDelta?(e.wheelDeltaY||e.wheelDelta)/2:e.detail&&Math.abs(e.detail)<32765?20*-e.detail:e.detail?e.detail/-32765*60:0}function Jt(e,t){var n=t.relatedTarget;if(!n)return!0;try{for(;n&&n!==e;)n=n.parentNode}catch(i){return!1}return n!==e}var en={__proto__:null,on:zt,off:Dt,stopPropagation:Wt,disableScrollPropagation:Ht,disableClickPropagation:Zt,preventDefault:Gt,stop:Qt,getPropagationPath:Yt,getMousePosition:Kt,getWheelDelta:Xt,isExternalTarget:Jt,addListener:zt,removeListener:Dt},tn=O.extend({run:function(e,t,n,i){this.stop(),this._el=e,this._inProgress=!0,this._duration=n||.25,this._easeOutPower=1/Math.max(i||.5,.2),this._startPos=Pt(e),this._offset=t.subtract(this._startPos),this._startTime=+new Date,this.fire("start"),this._animate()},stop:function(){this._inProgress&&(this._step(!0),this._complete())},_animate:function(){this._animId=C(this._animate,this),this._step()},_step:function(e){var t=+new Date-this._startTime,n=1e3*this._duration;tthis.options.maxZoom)?this.setZoom(e):this},panInsideBounds:function(e,t){this._enforcingBounds=!0;var n=this.getCenter(),i=this._limitCenter(n,this._zoom,D(e));return n.equals(i)||this.panTo(i,t),this._enforcingBounds=!1,this},panInside:function(e,t){var n=R((t=t||{}).paddingTopLeft||t.padding||[0,0]),i=R(t.paddingBottomRight||t.padding||[0,0]),r=this.project(this.getCenter()),o=this.project(e),s=this.getPixelBounds(),a=z([s.min.add(n),s.max.subtract(i)]),l=a.getSize();if(!a.contains(o)){this._enforcingBounds=!0;var c=o.subtract(a.getCenter()),u=a.extend(o).getSize().subtract(l);r.x+=c.x<0?-u.x:u.x,r.y+=c.y<0?-u.y:u.y,this.panTo(this.unproject(r),t),this._enforcingBounds=!1}return this},invalidateSize:function(e){if(!this._loaded)return this;e=n({animate:!1,pan:!0},!0===e?{animate:!0}:e);var t=this.getSize();this._sizeChanged=!0,this._lastCenter=null;var i=this.getSize(),o=t.divideBy(2).round(),s=i.divideBy(2).round(),a=o.subtract(s);return a.x||a.y?(e.animate&&e.pan?this.panBy(a):(e.pan&&this._rawPanBy(a),this.fire("move"),e.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(r(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:t,newSize:i})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(e){if(e=this._locateOptions=n({timeout:1e4,watch:!1},e),!("geolocation"in navigator))return this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this;var t=r(this._handleGeolocationResponse,this),i=r(this._handleGeolocationError,this);return e.watch?this._locationWatchId=navigator.geolocation.watchPosition(t,i,e):navigator.geolocation.getCurrentPosition(t,i,e),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(e){if(this._container._leaflet_id){var t=e.code,n=e.message||(1===t?"permission denied":2===t?"position unavailable":"timeout");this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:t,message:"Geolocation error: "+n+"."})}},_handleGeolocationResponse:function(e){if(this._container._leaflet_id){var t=new F(e.coords.latitude,e.coords.longitude),n=t.toBounds(2*e.coords.accuracy),i=this._locateOptions;if(i.setView){var r=this.getBoundsZoom(n);this.setView(t,i.maxZoom?Math.min(r,i.maxZoom):r)}var o={latlng:t,bounds:n,timestamp:e.timestamp};for(var s in e.coords)"number"===typeof e.coords[s]&&(o[s]=e.coords[s]);this.fire("locationfound",o)}},addHandler:function(e,t){if(!t)return this;var n=this[e]=new t(this);return this._handlers.push(n),this.options[e]&&n.enable(),this},remove:function(){if(this._initEvents(!0),this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}var e;for(e in void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),ft(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(E(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload"),this._layers)this._layers[e].remove();for(e in this._panes)ft(this._panes[e]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(e,t){var n=pt("div","leaflet-pane"+(e?" leaflet-"+e.replace("Pane","")+"-pane":""),t||this._mapPane);return e&&(this._panes[e]=n),n},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter.clone():this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var e=this.getPixelBounds();return new B(this.unproject(e.getBottomLeft()),this.unproject(e.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(e,t,n){e=D(e),n=R(n||[0,0]);var i=this.getZoom()||0,r=this.getMinZoom(),o=this.getMaxZoom(),s=e.getNorthWest(),a=e.getSouthEast(),l=this.getSize().subtract(n),c=z(this.project(a,i),this.project(s,i)).getSize(),u=Ie.any3d?this.options.zoomSnap:1,d=l.x/c.x,h=l.y/c.y,p=t?Math.max(d,h):Math.min(d,h);return i=this.getScaleZoom(p,i),u&&(i=Math.round(i/(u/100))*(u/100),i=t?Math.ceil(i/u)*u:Math.floor(i/u)*u),Math.max(r,Math.min(o,i))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new M(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(e,t){var n=this._getTopLeftPoint(e,t);return new I(n,n.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(e){return this.options.crs.getProjectedBounds(void 0===e?this.getZoom():e)},getPane:function(e){return"string"===typeof e?this._panes[e]:e},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(e,t){var n=this.options.crs;return t=void 0===t?this._zoom:t,n.scale(e)/n.scale(t)},getScaleZoom:function(e,t){var n=this.options.crs;t=void 0===t?this._zoom:t;var i=n.zoom(e*n.scale(t));return isNaN(i)?1/0:i},project:function(e,t){return t=void 0===t?this._zoom:t,this.options.crs.latLngToPoint(U(e),t)},unproject:function(e,t){return t=void 0===t?this._zoom:t,this.options.crs.pointToLatLng(R(e),t)},layerPointToLatLng:function(e){var t=R(e).add(this.getPixelOrigin());return this.unproject(t)},latLngToLayerPoint:function(e){return this.project(U(e))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(e){return this.options.crs.wrapLatLng(U(e))},wrapLatLngBounds:function(e){return this.options.crs.wrapLatLngBounds(D(e))},distance:function(e,t){return this.options.crs.distance(U(e),U(t))},containerPointToLayerPoint:function(e){return R(e).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(e){return R(e).add(this._getMapPanePos())},containerPointToLatLng:function(e){var t=this.containerPointToLayerPoint(R(e));return this.layerPointToLatLng(t)},latLngToContainerPoint:function(e){return this.layerPointToContainerPoint(this.latLngToLayerPoint(U(e)))},mouseEventToContainerPoint:function(e){return Kt(e,this._container)},mouseEventToLayerPoint:function(e){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e))},mouseEventToLatLng:function(e){return this.layerPointToLatLng(this.mouseEventToLayerPoint(e))},_initContainer:function(e){var t=this._container=dt(e);if(!t)throw new Error("Map container not found.");if(t._leaflet_id)throw new Error("Map container is already initialized.");zt(t,"scroll",this._onScroll,this),this._containerId=s(t)},_initLayout:function(){var e=this._container;this._fadeAnimated=this.options.fadeAnimation&&Ie.any3d,_t(e,"leaflet-container"+(Ie.touch?" leaflet-touch":"")+(Ie.retina?" leaflet-retina":"")+(Ie.ielt9?" leaflet-oldie":"")+(Ie.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":""));var t=ht(e,"position");"absolute"!==t&&"relative"!==t&&"fixed"!==t&&"sticky"!==t&&(e.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var e=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),Et(this._mapPane,new M(0,0)),this.createPane("tilePane"),this.createPane("overlayPane"),this.createPane("shadowPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(_t(e.markerPane,"leaflet-zoom-hide"),_t(e.shadowPane,"leaflet-zoom-hide"))},_resetView:function(e,t,n){Et(this._mapPane,new M(0,0));var i=!this._loaded;this._loaded=!0,t=this._limitZoom(t),this.fire("viewprereset");var r=this._zoom!==t;this._moveStart(r,n)._move(e,t)._moveEnd(r),this.fire("viewreset"),i&&this.fire("load")},_moveStart:function(e,t){return e&&this.fire("zoomstart"),t||this.fire("movestart"),this},_move:function(e,t,n,i){void 0===t&&(t=this._zoom);var r=this._zoom!==t;return this._zoom=t,this._lastCenter=e,this._pixelOrigin=this._getNewPixelOrigin(e),i?n&&n.pinch&&this.fire("zoom",n):((r||n&&n.pinch)&&this.fire("zoom",n),this.fire("move",n)),this},_moveEnd:function(e){return e&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return E(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(e){Et(this._mapPane,this._getMapPanePos().subtract(e))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(e){this._targets={},this._targets[s(this._container)]=this;var t=e?Dt:zt;t(this._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup",this._handleDOMEvent,this),this.options.trackResize&&t(window,"resize",this._onResize,this),Ie.any3d&&this.options.transform3DLimit&&(e?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){E(this._resizeRequest),this._resizeRequest=C(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var e=this._getMapPanePos();Math.max(Math.abs(e.x),Math.abs(e.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(e,t){for(var n,i=[],r="mouseout"===t||"mouseover"===t,o=e.target||e.srcElement,a=!1;o;){if((n=this._targets[s(o)])&&("click"===t||"preclick"===t)&&this._draggableMoved(n)){a=!0;break}if(n&&n.listens(t,!0)){if(r&&!Jt(o,e))break;if(i.push(n),r)break}if(o===this._container)break;o=o.parentNode}return i.length||a||r||!this.listens(t,!0)||(i=[this]),i},_isClickDisabled:function(e){for(;e&&e!==this._container;){if(e._leaflet_disable_click)return!0;e=e.parentNode}},_handleDOMEvent:function(e){var t=e.target||e.srcElement;if(!(!this._loaded||t._leaflet_disable_events||"click"===e.type&&this._isClickDisabled(t))){var n=e.type;"mousedown"===n&&Ot(t),this._fireDOMEvent(e,n)}},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(e,t,i){if("click"===e.type){var r=n({},e);r.type="preclick",this._fireDOMEvent(r,r.type,i)}var o=this._findEventTargets(e,t);if(i){for(var s=[],a=0;a0?Math.round(e-t)/2:Math.max(0,Math.ceil(e))-Math.max(0,Math.floor(t))},_limitZoom:function(e){var t=this.getMinZoom(),n=this.getMaxZoom(),i=Ie.any3d?this.options.zoomSnap:1;return i&&(e=Math.round(e/i)*i),Math.max(t,Math.min(n,e))},_onPanTransitionStep:function(){this.fire("move")},_onPanTransitionEnd:function(){bt(this._mapPane,"leaflet-pan-anim"),this.fire("moveend")},_tryAnimatedPan:function(e,t){var n=this._getCenterOffset(e)._trunc();return!(!0!==(t&&t.animate)&&!this.getSize().contains(n))&&(this.panBy(n,t),!0)},_createAnimProxy:function(){var e=this._proxy=pt("div","leaflet-proxy leaflet-zoom-animated");this._panes.mapPane.appendChild(e),this.on("zoomanim",function(e){var t=lt,n=this._proxy.style[t];Lt(this._proxy,this.project(e.center,e.zoom),this.getZoomScale(e.zoom,1)),n===this._proxy.style[t]&&this._animatingZoom&&this._onZoomTransitionEnd()},this),this.on("load moveend",this._animMoveEnd,this),this._on("unload",this._destroyAnimProxy,this)},_destroyAnimProxy:function(){ft(this._proxy),this.off("load moveend",this._animMoveEnd,this),delete this._proxy},_animMoveEnd:function(){var e=this.getCenter(),t=this.getZoom();Lt(this._proxy,this.project(e,t),this.getZoomScale(t,1))},_catchTransitionEnd:function(e){this._animatingZoom&&e.propertyName.indexOf("transform")>=0&&this._onZoomTransitionEnd()},_nothingToAnimate:function(){return!this._container.getElementsByClassName("leaflet-zoom-animated").length},_tryAnimatedZoom:function(e,t,n){if(this._animatingZoom)return!0;if(n=n||{},!this._zoomAnimated||!1===n.animate||this._nothingToAnimate()||Math.abs(t-this._zoom)>this.options.zoomAnimationThreshold)return!1;var i=this.getZoomScale(t),r=this._getCenterOffset(e)._divideBy(1-1/i);return!(!0!==n.animate&&!this.getSize().contains(r))&&(C(function(){this._moveStart(!0,n.noMoveStart||!1)._animateZoom(e,t,!0)},this),!0)},_animateZoom:function(e,t,n,i){this._mapPane&&(n&&(this._animatingZoom=!0,this._animateToCenter=e,this._animateToZoom=t,_t(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:e,zoom:t,noUpdate:i}),this._tempFireZoomEvent||(this._tempFireZoomEvent=this._zoom!==this._animateToZoom),this._move(this._animateToCenter,this._animateToZoom,void 0,!0),setTimeout(r(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&bt(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom,void 0,!0),this._tempFireZoomEvent&&this.fire("zoom"),delete this._tempFireZoomEvent,this.fire("move"),this._moveEnd(!0))}});function rn(e,t){return new nn(e,t)}var on=T.extend({options:{position:"topright"},initialize:function(e){p(this,e)},getPosition:function(){return this.options.position},setPosition:function(e){var t=this._map;return t&&t.removeControl(this),this.options.position=e,t&&t.addControl(this),this},getContainer:function(){return this._container},addTo:function(e){this.remove(),this._map=e;var t=this._container=this.onAdd(e),n=this.getPosition(),i=e._controlCorners[n];return _t(t,"leaflet-control"),-1!==n.indexOf("bottom")?i.insertBefore(t,i.firstChild):i.appendChild(t),this._map.on("unload",this.remove,this),this},remove:function(){return this._map?(ft(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null,this):this},_refocusOnMap:function(e){this._map&&e&&e.screenX>0&&e.screenY>0&&this._map.getContainer().focus()}}),sn=function(e){return new on(e)};nn.include({addControl:function(e){return e.addTo(this),this},removeControl:function(e){return e.remove(),this},_initControlPos:function(){var e=this._controlCorners={},t="leaflet-",n=this._controlContainer=pt("div",t+"control-container",this._container);function i(i,r){var o=t+i+" "+t+r;e[i+r]=pt("div",o,n)}i("top","left"),i("top","right"),i("bottom","left"),i("bottom","right")},_clearControlPos:function(){for(var e in this._controlCorners)ft(this._controlCorners[e]);ft(this._controlContainer),delete this._controlCorners,delete this._controlContainer}});var an=on.extend({options:{collapsed:!0,position:"topright",autoZIndex:!0,hideSingleBase:!1,sortLayers:!1,sortFunction:function(e,t,n,i){return n1,this._baseLayersList.style.display=e?"":"none"),this._separator.style.display=t&&e?"":"none",this},_onLayerChange:function(e){this._handlingClick||this._update();var t=this._getLayer(s(e.target)),n=t.overlay?"add"===e.type?"overlayadd":"overlayremove":"add"===e.type?"baselayerchange":null;n&&this._map.fire(n,t)},_createRadioElement:function(e,t){var n='",i=document.createElement("div");return i.innerHTML=n,i.firstChild},_addItem:function(e){var t,n=document.createElement("label"),i=this._map.hasLayer(e.layer);e.overlay?((t=document.createElement("input")).type="checkbox",t.className="leaflet-control-layers-selector",t.defaultChecked=i):t=this._createRadioElement("leaflet-base-layers_"+s(this),i),this._layerControlInputs.push(t),t.layerId=s(e.layer),zt(t,"click",this._onInputClick,this);var r=document.createElement("span");r.innerHTML=" "+e.name;var o=document.createElement("span");return n.appendChild(o),o.appendChild(t),o.appendChild(r),(e.overlay?this._overlaysList:this._baseLayersList).appendChild(n),this._checkDisabledLayers(),n},_onInputClick:function(){if(!this._preventClick){var e,t,n=this._layerControlInputs,i=[],r=[];this._handlingClick=!0;for(var o=n.length-1;o>=0;o--)e=n[o],t=this._getLayer(e.layerId).layer,e.checked?i.push(t):e.checked||r.push(t);for(o=0;o=0;r--)e=n[r],t=this._getLayer(e.layerId).layer,e.disabled=void 0!==t.options.minZoom&&it.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expandSafely:function(){var e=this._section;this._preventClick=!0,zt(e,"click",Gt),this.expand();var t=this;setTimeout(function(){Dt(e,"click",Gt),t._preventClick=!1})}}),ln=function(e,t,n){return new an(e,t,n)},cn=on.extend({options:{position:"topleft",zoomInText:'',zoomInTitle:"Zoom in",zoomOutText:'',zoomOutTitle:"Zoom out"},onAdd:function(e){var t="leaflet-control-zoom",n=pt("div",t+" leaflet-bar"),i=this.options;return this._zoomInButton=this._createButton(i.zoomInText,i.zoomInTitle,t+"-in",n,this._zoomIn),this._zoomOutButton=this._createButton(i.zoomOutText,i.zoomOutTitle,t+"-out",n,this._zoomOut),this._updateDisabled(),e.on("zoomend zoomlevelschange",this._updateDisabled,this),n},onRemove:function(e){e.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(e){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(e.shiftKey?3:1))},_createButton:function(e,t,n,i,r){var o=pt("a",n,i);return o.innerHTML=e,o.href="#",o.title=t,o.setAttribute("role","button"),o.setAttribute("aria-label",t),Zt(o),zt(o,"click",Qt),zt(o,"click",r,this),zt(o,"click",this._refocusOnMap,this),o},_updateDisabled:function(){var e=this._map,t="leaflet-disabled";bt(this._zoomInButton,t),bt(this._zoomOutButton,t),this._zoomInButton.setAttribute("aria-disabled","false"),this._zoomOutButton.setAttribute("aria-disabled","false"),(this._disabled||e._zoom===e.getMinZoom())&&(_t(this._zoomOutButton,t),this._zoomOutButton.setAttribute("aria-disabled","true")),(this._disabled||e._zoom===e.getMaxZoom())&&(_t(this._zoomInButton,t),this._zoomInButton.setAttribute("aria-disabled","true"))}});nn.mergeOptions({zoomControl:!0}),nn.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new cn,this.addControl(this.zoomControl))});var un=function(e){return new cn(e)},dn=on.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(e){var t="leaflet-control-scale",n=pt("div",t),i=this.options;return this._addScales(i,t+"-line",n),e.on(i.updateWhenIdle?"moveend":"move",this._update,this),e.whenReady(this._update,this),n},onRemove:function(e){e.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(e,t,n){e.metric&&(this._mScale=pt("div",t,n)),e.imperial&&(this._iScale=pt("div",t,n))},_update:function(){var e=this._map,t=e.getSize().y/2,n=e.distance(e.containerPointToLatLng([0,t]),e.containerPointToLatLng([this.options.maxWidth,t]));this._updateScales(n)},_updateScales:function(e){this.options.metric&&e&&this._updateMetric(e),this.options.imperial&&e&&this._updateImperial(e)},_updateMetric:function(e){var t=this._getRoundNum(e),n=t<1e3?t+" m":t/1e3+" km";this._updateScale(this._mScale,n,t/e)},_updateImperial:function(e){var t,n,i,r=3.2808399*e;r>5280?(t=r/5280,n=this._getRoundNum(t),this._updateScale(this._iScale,n+" mi",n/t)):(i=this._getRoundNum(r),this._updateScale(this._iScale,i+" ft",i/r))},_updateScale:function(e,t,n){e.style.width=Math.round(this.options.maxWidth*n)+"px",e.innerHTML=t},_getRoundNum:function(e){var t=Math.pow(10,(Math.floor(e)+"").length-1),n=e/t;return t*(n=n>=10?10:n>=5?5:n>=3?3:n>=2?2:1)}}),hn=function(e){return new dn(e)},pn='',fn=on.extend({options:{position:"bottomright",prefix:''+(Ie.inlineSvg?pn+" ":"")+"Leaflet"},initialize:function(e){p(this,e),this._attributions={}},onAdd:function(e){for(var t in e.attributionControl=this,this._container=pt("div","leaflet-control-attribution"),Zt(this._container),e._layers)e._layers[t].getAttribution&&this.addAttribution(e._layers[t].getAttribution());return this._update(),e.on("layeradd",this._addAttribution,this),this._container},onRemove:function(e){e.off("layeradd",this._addAttribution,this)},_addAttribution:function(e){e.layer.getAttribution&&(this.addAttribution(e.layer.getAttribution()),e.layer.once("remove",function(){this.removeAttribution(e.layer.getAttribution())},this))},setPrefix:function(e){return this.options.prefix=e,this._update(),this},addAttribution:function(e){return e?(this._attributions[e]||(this._attributions[e]=0),this._attributions[e]++,this._update(),this):this},removeAttribution:function(e){return e?(this._attributions[e]&&(this._attributions[e]--,this._update()),this):this},_update:function(){if(this._map){var e=[];for(var t in this._attributions)this._attributions[t]&&e.push(t);var n=[];this.options.prefix&&n.push(this.options.prefix),e.length&&n.push(e.join(", ")),this._container.innerHTML=n.join(' ')}}});nn.mergeOptions({attributionControl:!0}),nn.addInitHook(function(){this.options.attributionControl&&(new fn).addTo(this)});var mn=function(e){return new fn(e)};on.Layers=an,on.Zoom=cn,on.Scale=dn,on.Attribution=fn,sn.layers=ln,sn.zoom=un,sn.scale=hn,sn.attribution=mn;var gn=T.extend({initialize:function(e){this._map=e},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled?(this._enabled=!1,this.removeHooks(),this):this},enabled:function(){return!!this._enabled}});gn.addTo=function(e,t){return e.addHandler(t,this),this};var vn={Events:N},yn=Ie.touch?"touchstart mousedown":"mousedown",_n=O.extend({options:{clickTolerance:3},initialize:function(e,t,n,i){p(this,i),this._element=e,this._dragStartTarget=t||e,this._preventOutline=n},enable:function(){this._enabled||(zt(this._dragStartTarget,yn,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(_n._dragging===this&&this.finishDrag(!0),Dt(this._dragStartTarget,yn,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(e){if(this._enabled&&(this._moved=!1,!yt(this._element,"leaflet-zoom-anim")))if(e.touches&&1!==e.touches.length)_n._dragging===this&&this.finishDrag();else if(!(_n._dragging||e.shiftKey||1!==e.which&&1!==e.button&&!e.touches)&&(_n._dragging=this,this._preventOutline&&Ot(this._element),jt(),it(),!this._moving)){this.fire("down");var t=e.touches?e.touches[0]:e,n=At(this._element);this._startPoint=new M(t.clientX,t.clientY),this._startPos=Pt(this._element),this._parentScale=Rt(n);var i="mousedown"===e.type;zt(document,i?"mousemove":"touchmove",this._onMove,this),zt(document,i?"mouseup":"touchend touchcancel",this._onUp,this)}},_onMove:function(e){if(this._enabled)if(e.touches&&e.touches.length>1)this._moved=!0;else{var t=e.touches&&1===e.touches.length?e.touches[0]:e,n=new M(t.clientX,t.clientY)._subtract(this._startPoint);(n.x||n.y)&&(Math.abs(n.x)+Math.abs(n.y)l&&(o=s,l=a);l>n&&(t[o]=1,Tn(e,t,n,i,o),Tn(e,t,n,o,r))}function jn(e,t){for(var n=[e[0]],i=1,r=0,o=e.length;it&&(n.push(e[i]),r=i);return rt.max.x&&(n|=2),e.yt.max.y&&(n|=8),n}function An(e,t){var n=t.x-e.x,i=t.y-e.y;return n*n+i*i}function Rn(e,t,n,i){var r,o=t.x,s=t.y,a=n.x-o,l=n.y-s,c=a*a+l*l;return c>0&&((r=((e.x-o)*a+(e.y-s)*l)/c)>1?(o=n.x,s=n.y):r>0&&(o+=a*r,s+=l*r)),a=e.x-o,l=e.y-s,i?a*a+l*l:new M(o,s)}function In(e){return!v(e[0])||"object"!==typeof e[0][0]&&"undefined"!==typeof e[0][0]}function zn(e){return console.warn("Deprecated use of _flat, please use L.LineUtil.isFlat instead."),In(e)}function Bn(e,t){var n,i,r,o,s,a,l,c;if(!e||0===e.length)throw new Error("latlngs not passed");In(e)||(console.warn("latlngs are not flat! Only the first ring will be used"),e=e[0]);var u=U([0,0]),d=D(e);d.getNorthWest().distanceTo(d.getSouthWest())*d.getNorthEast().distanceTo(d.getNorthWest())<1700&&(u=wn(e));var h=e.length,p=[];for(n=0;ni){l=(o-i)/r,c=[a.x-l*(a.x-s.x),a.y-l*(a.y-s.y)];break}var m=t.unproject(R(c));return U([m.lat+u.lat,m.lng+u.lng])}var Dn={__proto__:null,simplify:Cn,pointToSegmentDistance:Ln,closestPointOnSegment:En,clipSegment:Nn,_getEdgeIntersection:On,_getBitCode:Mn,_sqClosestPointOnSegment:Rn,isFlat:In,_flat:zn,polylineCenter:Bn},Fn={project:function(e){return new M(e.lng,e.lat)},unproject:function(e){return new F(e.y,e.x)},bounds:new I([-180,-90],[180,90])},Un={R:6378137,R_MINOR:6356752.314245179,bounds:new I([-20037508.34279,-15496570.73972],[20037508.34279,18764656.23138]),project:function(e){var t=Math.PI/180,n=this.R,i=e.lat*t,r=this.R_MINOR/n,o=Math.sqrt(1-r*r),s=o*Math.sin(i),a=Math.tan(Math.PI/4-i/2)/Math.pow((1-s)/(1+s),o/2);return i=-n*Math.log(Math.max(a,1e-10)),new M(e.lng*t*n,i)},unproject:function(e){for(var t,n=180/Math.PI,i=this.R,r=this.R_MINOR/i,o=Math.sqrt(1-r*r),s=Math.exp(-e.y/i),a=Math.PI/2-2*Math.atan(s),l=0,c=.1;l<15&&Math.abs(c)>1e-7;l++)t=o*Math.sin(a),t=Math.pow((1-t)/(1+t),o/2),a+=c=Math.PI/2-2*Math.atan(s*t)-a;return new F(a*n,e.x*n/i)}},qn={__proto__:null,LonLat:Fn,Mercator:Un,SphericalMercator:H},Vn=n({},V,{code:"EPSG:3395",projection:Un,transformation:function(){var e=.5/(Math.PI*Un.R);return G(e,.5,-e,.5)}()}),Wn=n({},V,{code:"EPSG:4326",projection:Fn,transformation:G(1/180,1,-1/180,.5)}),Hn=n({},q,{projection:Fn,transformation:G(1,0,-1,0),scale:function(e){return Math.pow(2,e)},zoom:function(e){return Math.log(e)/Math.LN2},distance:function(e,t){var n=t.lng-e.lng,i=t.lat-e.lat;return Math.sqrt(n*n+i*i)},infinite:!0});q.Earth=V,q.EPSG3395=Vn,q.EPSG3857=Q,q.EPSG900913=Y,q.EPSG4326=Wn,q.Simple=Hn;var Zn=O.extend({options:{pane:"overlayPane",attribution:null,bubblingMouseEvents:!0},addTo:function(e){return e.addLayer(this),this},remove:function(){return this.removeFrom(this._map||this._mapToAdd)},removeFrom:function(e){return e&&e.removeLayer(this),this},getPane:function(e){return this._map.getPane(e?this.options[e]||e:this.options.pane)},addInteractiveTarget:function(e){return this._map._targets[s(e)]=this,this},removeInteractiveTarget:function(e){return delete this._map._targets[s(e)],this},getAttribution:function(){return this.options.attribution},_layerAdd:function(e){var t=e.target;if(t.hasLayer(this)){if(this._map=t,this._zoomAnimated=t._zoomAnimated,this.getEvents){var n=this.getEvents();t.on(n,this),this.once("remove",function(){t.off(n,this)},this)}this.onAdd(t),this.fire("add"),t.fire("layeradd",{layer:this})}}});nn.include({addLayer:function(e){if(!e._layerAdd)throw new Error("The provided object is not a Layer.");var t=s(e);return this._layers[t]||(this._layers[t]=e,e._mapToAdd=this,e.beforeAdd&&e.beforeAdd(this),this.whenReady(e._layerAdd,e)),this},removeLayer:function(e){var t=s(e);return this._layers[t]?(this._loaded&&e.onRemove(this),delete this._layers[t],this._loaded&&(this.fire("layerremove",{layer:e}),e.fire("remove")),e._map=e._mapToAdd=null,this):this},hasLayer:function(e){return s(e)in this._layers},eachLayer:function(e,t){for(var n in this._layers)e.call(t,this._layers[n]);return this},_addLayers:function(e){for(var t=0,n=(e=e?v(e)?e:[e]:[]).length;tthis._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()=2&&t[0]instanceof F&&t[0].equals(t[n-1])&&t.pop(),t},_setLatLngs:function(e){li.prototype._setLatLngs.call(this,e),In(this._latlngs)&&(this._latlngs=[this._latlngs])},_defaultShape:function(){return In(this._latlngs[0])?this._latlngs[0]:this._latlngs[0][0]},_clipPoints:function(){var e=this._renderer._bounds,t=this.options.weight,n=new M(t,t);if(e=new I(e.min.subtract(n),e.max.add(n)),this._parts=[],this._pxBounds&&this._pxBounds.intersects(e))if(this.options.noClip)this._parts=this._rings;else for(var i,r=0,o=this._rings.length;re.y!==i.y>e.y&&e.x<(i.x-n.x)*(e.y-n.y)/(i.y-n.y)+n.x&&(c=!c);return c||li.prototype._containsPoint.call(this,e,!0)}});function di(e,t){return new ui(e,t)}var hi=Yn.extend({initialize:function(e,t){p(this,t),this._layers={},e&&this.addData(e)},addData:function(e){var t,n,i,r=v(e)?e:e.features;if(r){for(t=0,n=r.length;t0&&r.push(r[0].slice()),r}function _i(e,t){return e.feature?n({},e.feature,{geometry:t}):bi(t)}function bi(e){return"Feature"===e.type||"FeatureCollection"===e.type?e:{type:"Feature",properties:{},geometry:e}}var xi={toGeoJSON:function(e){return _i(this,{type:"Point",coordinates:vi(this.getLatLng(),e)})}};function wi(e,t){return new hi(e,t)}ti.include(xi),si.include(xi),ri.include(xi),li.include({toGeoJSON:function(e){var t=!In(this._latlngs);return _i(this,{type:(t?"Multi":"")+"LineString",coordinates:yi(this._latlngs,t?1:0,!1,e)})}}),ui.include({toGeoJSON:function(e){var t=!In(this._latlngs),n=t&&!In(this._latlngs[0]),i=yi(this._latlngs,n?2:t?1:0,!0,e);return t||(i=[i]),_i(this,{type:(n?"Multi":"")+"Polygon",coordinates:i})}}),Gn.include({toMultiPoint:function(e){var t=[];return this.eachLayer(function(n){t.push(n.toGeoJSON(e).geometry.coordinates)}),_i(this,{type:"MultiPoint",coordinates:t})},toGeoJSON:function(e){var t=this.feature&&this.feature.geometry&&this.feature.geometry.type;if("MultiPoint"===t)return this.toMultiPoint(e);var n="GeometryCollection"===t,i=[];return this.eachLayer(function(t){if(t.toGeoJSON){var r=t.toGeoJSON(e);if(n)i.push(r.geometry);else{var o=bi(r);"FeatureCollection"===o.type?i.push.apply(i,o.features):i.push(o)}}}),n?_i(this,{geometries:i,type:"GeometryCollection"}):{type:"FeatureCollection",features:i}}});var ki=wi,Si=Zn.extend({options:{opacity:1,alt:"",interactive:!1,crossOrigin:!1,errorOverlayUrl:"",zIndex:1,className:""},initialize:function(e,t,n){this._url=e,this._bounds=D(t),p(this,n)},onAdd:function(){this._image||(this._initImage(),this.options.opacity<1&&this._updateOpacity()),this.options.interactive&&(_t(this._image,"leaflet-interactive"),this.addInteractiveTarget(this._image)),this.getPane().appendChild(this._image),this._reset()},onRemove:function(){ft(this._image),this.options.interactive&&this.removeInteractiveTarget(this._image)},setOpacity:function(e){return this.options.opacity=e,this._image&&this._updateOpacity(),this},setStyle:function(e){return e.opacity&&this.setOpacity(e.opacity),this},bringToFront:function(){return this._map&>(this._image),this},bringToBack:function(){return this._map&&vt(this._image),this},setUrl:function(e){return this._url=e,this._image&&(this._image.src=e),this},setBounds:function(e){return this._bounds=D(e),this._map&&this._reset(),this},getEvents:function(){var e={zoom:this._reset,viewreset:this._reset};return this._zoomAnimated&&(e.zoomanim=this._animateZoom),e},setZIndex:function(e){return this.options.zIndex=e,this._updateZIndex(),this},getBounds:function(){return this._bounds},getElement:function(){return this._image},_initImage:function(){var e="IMG"===this._url.tagName,t=this._image=e?this._url:pt("img");_t(t,"leaflet-image-layer"),this._zoomAnimated&&_t(t,"leaflet-zoom-animated"),this.options.className&&_t(t,this.options.className),t.onselectstart=c,t.onmousemove=c,t.onload=r(this.fire,this,"load"),t.onerror=r(this._overlayOnError,this,"error"),(this.options.crossOrigin||""===this.options.crossOrigin)&&(t.crossOrigin=!0===this.options.crossOrigin?"":this.options.crossOrigin),this.options.zIndex&&this._updateZIndex(),e?this._url=t.src:(t.src=this._url,t.alt=this.options.alt)},_animateZoom:function(e){var t=this._map.getZoomScale(e.zoom),n=this._map._latLngBoundsToNewLayerBounds(this._bounds,e.zoom,e.center).min;Lt(this._image,n,t)},_reset:function(){var e=this._image,t=new I(this._map.latLngToLayerPoint(this._bounds.getNorthWest()),this._map.latLngToLayerPoint(this._bounds.getSouthEast())),n=t.getSize();Et(e,t.min),e.style.width=n.x+"px",e.style.height=n.y+"px"},_updateOpacity:function(){kt(this._image,this.options.opacity)},_updateZIndex:function(){this._image&&void 0!==this.options.zIndex&&null!==this.options.zIndex&&(this._image.style.zIndex=this.options.zIndex)},_overlayOnError:function(){this.fire("error");var e=this.options.errorOverlayUrl;e&&this._url!==e&&(this._url=e,this._image.src=e)},getCenter:function(){return this._bounds.getCenter()}}),Ci=function(e,t,n){return new Si(e,t,n)},Li=Si.extend({options:{autoplay:!0,loop:!0,keepAspectRatio:!0,muted:!1,playsInline:!0},_initImage:function(){var e="VIDEO"===this._url.tagName,t=this._image=e?this._url:pt("video");if(_t(t,"leaflet-image-layer"),this._zoomAnimated&&_t(t,"leaflet-zoom-animated"),this.options.className&&_t(t,this.options.className),t.onselectstart=c,t.onmousemove=c,t.onloadeddata=r(this.fire,this,"load"),e){for(var n=t.getElementsByTagName("source"),i=[],o=0;o0?i:[t.src]}else{v(this._url)||(this._url=[this._url]),!this.options.keepAspectRatio&&Object.prototype.hasOwnProperty.call(t.style,"objectFit")&&(t.style.objectFit="fill"),t.autoplay=!!this.options.autoplay,t.loop=!!this.options.loop,t.muted=!!this.options.muted,t.playsInline=!!this.options.playsInline;for(var s=0;sr?(t.height=r+"px",_t(e,o)):bt(e,o),this._containerWidth=this._container.offsetWidth},_animateZoom:function(e){var t=this._map._latLngToNewLayerPoint(this._latlng,e.zoom,e.center),n=this._getAnchor();Et(this._container,t.add(n))},_adjustPan:function(){if(this.options.autoPan)if(this._map._panAnim&&this._map._panAnim.stop(),this._autopanning)this._autopanning=!1;else{var e=this._map,t=parseInt(ht(this._container,"marginBottom"),10)||0,n=this._container.offsetHeight+t,i=this._containerWidth,r=new M(this._containerLeft,-n-this._containerBottom);r._add(Pt(this._container));var o=e.layerPointToContainerPoint(r),s=R(this.options.autoPanPadding),a=R(this.options.autoPanPaddingTopLeft||s),l=R(this.options.autoPanPaddingBottomRight||s),c=e.getSize(),u=0,d=0;o.x+i+l.x>c.x&&(u=o.x+i-c.x+l.x),o.x-u-a.x<0&&(u=o.x-a.x),o.y+n+l.y>c.y&&(d=o.y+n-c.y+l.y),o.y-d-a.y<0&&(d=o.y-a.y),(u||d)&&(this.options.keepInView&&(this._autopanning=!0),e.fire("autopanstart").panBy([u,d]))}},_getAnchor:function(){return R(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}}),Oi=function(e,t){return new Ni(e,t)};nn.mergeOptions({closePopupOnClick:!0}),nn.include({openPopup:function(e,t,n){return this._initOverlay(Ni,e,t,n).openOn(this),this},closePopup:function(e){return(e=arguments.length?e:this._popup)&&e.close(),this}}),Zn.include({bindPopup:function(e,t){return this._popup=this._initOverlay(Ni,this._popup,e,t),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(e){return this._popup&&(this instanceof Yn||(this._popup._source=this),this._popup._prepareOpen(e||this._latlng)&&this._popup.openOn(this._map)),this},closePopup:function(){return this._popup&&this._popup.close(),this},togglePopup:function(){return this._popup&&this._popup.toggle(this),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(e){return this._popup&&this._popup.setContent(e),this},getPopup:function(){return this._popup},_openPopup:function(e){if(this._popup&&this._map){Qt(e);var t=e.layer||e.target;this._popup._source!==t||t instanceof ii?(this._popup._source=t,this.openPopup(e.latlng)):this._map.hasLayer(this._popup)?this.closePopup():this.openPopup(e.latlng)}},_movePopup:function(e){this._popup.setLatLng(e.latlng)},_onKeyPress:function(e){13===e.originalEvent.keyCode&&this._openPopup(e)}});var Mi=ji.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,opacity:.9},onAdd:function(e){ji.prototype.onAdd.call(this,e),this.setOpacity(this.options.opacity),e.fire("tooltipopen",{tooltip:this}),this._source&&(this.addEventParent(this._source),this._source.fire("tooltipopen",{tooltip:this},!0))},onRemove:function(e){ji.prototype.onRemove.call(this,e),e.fire("tooltipclose",{tooltip:this}),this._source&&(this.removeEventParent(this._source),this._source.fire("tooltipclose",{tooltip:this},!0))},getEvents:function(){var e=ji.prototype.getEvents.call(this);return this.options.permanent||(e.preclick=this.close),e},_initLayout:function(){var e="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=pt("div",e),this._container.setAttribute("role","tooltip"),this._container.setAttribute("id","leaflet-tooltip-"+s(this))},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(e){var t,n,i=this._map,r=this._container,o=i.latLngToContainerPoint(i.getCenter()),s=i.layerPointToContainerPoint(e),a=this.options.direction,l=r.offsetWidth,c=r.offsetHeight,u=R(this.options.offset),d=this._getAnchor();"top"===a?(t=l/2,n=c):"bottom"===a?(t=l/2,n=0):"center"===a?(t=l/2,n=c/2):"right"===a?(t=0,n=c/2):"left"===a?(t=l,n=c/2):s.xthis.options.maxZoom||ni&&this._retainParent(r,o,s,i))},_retainChildren:function(e,t,n,i){for(var r=2*e;r<2*e+2;r++)for(var o=2*t;o<2*t+2;o++){var s=new M(r,o);s.z=n+1;var a=this._tileCoordsToKey(s),l=this._tiles[a];l&&l.active?l.retain=!0:(l&&l.loaded&&(l.retain=!0),n+1this.options.maxZoom||void 0!==this.options.minZoom&&r1)this._setView(e,n);else{for(var d=r.min.y;d<=r.max.y;d++)for(var h=r.min.x;h<=r.max.x;h++){var p=new M(h,d);if(p.z=this._tileZoom,this._isValidTile(p)){var f=this._tiles[this._tileCoordsToKey(p)];f?f.current=!0:s.push(p)}}if(s.sort(function(e,t){return e.distanceTo(o)-t.distanceTo(o)}),0!==s.length){this._loading||(this._loading=!0,this.fire("loading"));var m=document.createDocumentFragment();for(h=0;hn.max.x)||!t.wrapLat&&(e.yn.max.y))return!1}if(!this.options.bounds)return!0;var i=this._tileCoordsToBounds(e);return D(this.options.bounds).overlaps(i)},_keyToBounds:function(e){return this._tileCoordsToBounds(this._keyToTileCoords(e))},_tileCoordsToNwSe:function(e){var t=this._map,n=this.getTileSize(),i=e.scaleBy(n),r=i.add(n);return[t.unproject(i,e.z),t.unproject(r,e.z)]},_tileCoordsToBounds:function(e){var t=this._tileCoordsToNwSe(e),n=new B(t[0],t[1]);return this.options.noWrap||(n=this._map.wrapLatLngBounds(n)),n},_tileCoordsToKey:function(e){return e.x+":"+e.y+":"+e.z},_keyToTileCoords:function(e){var t=e.split(":"),n=new M(+t[0],+t[1]);return n.z=+t[2],n},_removeTile:function(e){var t=this._tiles[e];t&&(ft(t.el),delete this._tiles[e],this.fire("tileunload",{tile:t.el,coords:this._keyToTileCoords(e)}))},_initTile:function(e){_t(e,"leaflet-tile");var t=this.getTileSize();e.style.width=t.x+"px",e.style.height=t.y+"px",e.onselectstart=c,e.onmousemove=c,Ie.ielt9&&this.options.opacity<1&&kt(e,this.options.opacity)},_addTile:function(e,t){var n=this._getTilePos(e),i=this._tileCoordsToKey(e),o=this.createTile(this._wrapCoords(e),r(this._tileReady,this,e));this._initTile(o),this.createTile.length<2&&C(r(this._tileReady,this,e,null,o)),Et(o,n),this._tiles[i]={el:o,coords:e,current:!0},t.appendChild(o),this.fire("tileloadstart",{tile:o,coords:e})},_tileReady:function(e,t,n){t&&this.fire("tileerror",{error:t,tile:n,coords:e});var i=this._tileCoordsToKey(e);(n=this._tiles[i])&&(n.loaded=+new Date,this._map._fadeAnimated?(kt(n.el,0),E(this._fadeFrame),this._fadeFrame=C(this._updateOpacity,this)):(n.active=!0,this._pruneTiles()),t||(_t(n.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:n.el,coords:e})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),Ie.ielt9||!this._map._fadeAnimated?C(this._pruneTiles,this):setTimeout(r(this._pruneTiles,this),250)))},_getTilePos:function(e){return e.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(e){var t=new M(this._wrapX?l(e.x,this._wrapX):e.x,this._wrapY?l(e.y,this._wrapY):e.y);return t.z=e.z,t},_pxBoundsToTileRange:function(e){var t=this.getTileSize();return new I(e.min.unscaleBy(t).floor(),e.max.unscaleBy(t).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var e in this._tiles)if(!this._tiles[e].loaded)return!1;return!0}});function Bi(e){return new zi(e)}var Di=zi.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1,referrerPolicy:!1},initialize:function(e,t){this._url=e,(t=p(this,t)).detectRetina&&Ie.retina&&t.maxZoom>0?(t.tileSize=Math.floor(t.tileSize/2),t.zoomReverse?(t.zoomOffset--,t.minZoom=Math.min(t.maxZoom,t.minZoom+1)):(t.zoomOffset++,t.maxZoom=Math.max(t.minZoom,t.maxZoom-1)),t.minZoom=Math.max(0,t.minZoom)):t.zoomReverse?t.minZoom=Math.min(t.maxZoom,t.minZoom):t.maxZoom=Math.max(t.minZoom,t.maxZoom),"string"===typeof t.subdomains&&(t.subdomains=t.subdomains.split("")),this.on("tileunload",this._onTileRemove)},setUrl:function(e,t){return this._url===e&&void 0===t&&(t=!0),this._url=e,t||this.redraw(),this},createTile:function(e,t){var n=document.createElement("img");return zt(n,"load",r(this._tileOnLoad,this,t,n)),zt(n,"error",r(this._tileOnError,this,t,n)),(this.options.crossOrigin||""===this.options.crossOrigin)&&(n.crossOrigin=!0===this.options.crossOrigin?"":this.options.crossOrigin),"string"===typeof this.options.referrerPolicy&&(n.referrerPolicy=this.options.referrerPolicy),n.alt="",n.src=this.getTileUrl(e),n},getTileUrl:function(e){var t={r:Ie.retina?"@2x":"",s:this._getSubdomain(e),x:e.x,y:e.y,z:this._getZoomForUrl()};if(this._map&&!this._map.options.crs.infinite){var i=this._globalTileRange.max.y-e.y;this.options.tms&&(t.y=i),t["-y"]=i}return g(this._url,n(t,this.options))},_tileOnLoad:function(e,t){Ie.ielt9?setTimeout(r(e,this,null,t),0):e(null,t)},_tileOnError:function(e,t,n){var i=this.options.errorTileUrl;i&&t.getAttribute("src")!==i&&(t.src=i),e(n,t)},_onTileRemove:function(e){e.tile.onload=null},_getZoomForUrl:function(){var e=this._tileZoom,t=this.options.maxZoom;return this.options.zoomReverse&&(e=t-e),e+this.options.zoomOffset},_getSubdomain:function(e){var t=Math.abs(e.x+e.y)%this.options.subdomains.length;return this.options.subdomains[t]},_abortLoading:function(){var e,t;for(e in this._tiles)if(this._tiles[e].coords.z!==this._tileZoom&&((t=this._tiles[e].el).onload=c,t.onerror=c,!t.complete)){t.src=_;var n=this._tiles[e].coords;ft(t),delete this._tiles[e],this.fire("tileabort",{tile:t,coords:n})}},_removeTile:function(e){var t=this._tiles[e];if(t)return t.el.setAttribute("src",_),zi.prototype._removeTile.call(this,e)},_tileReady:function(e,t,n){if(this._map&&(!n||n.getAttribute("src")!==_))return zi.prototype._tileReady.call(this,e,t,n)}});function Fi(e,t){return new Di(e,t)}var Ui=Di.extend({defaultWmsParams:{service:"WMS",request:"GetMap",layers:"",styles:"",format:"image/jpeg",transparent:!1,version:"1.1.1"},options:{crs:null,uppercase:!1},initialize:function(e,t){this._url=e;var i=n({},this.defaultWmsParams);for(var r in t)r in this.options||(i[r]=t[r]);var o=(t=p(this,t)).detectRetina&&Ie.retina?2:1,s=this.getTileSize();i.width=s.x*o,i.height=s.y*o,this.wmsParams=i},onAdd:function(e){this._crs=this.options.crs||e.options.crs,this._wmsVersion=parseFloat(this.wmsParams.version);var t=this._wmsVersion>=1.3?"crs":"srs";this.wmsParams[t]=this._crs.code,Di.prototype.onAdd.call(this,e)},getTileUrl:function(e){var t=this._tileCoordsToNwSe(e),n=this._crs,i=z(n.project(t[0]),n.project(t[1])),r=i.min,o=i.max,s=(this._wmsVersion>=1.3&&this._crs===Wn?[r.y,r.x,o.y,o.x]:[r.x,r.y,o.x,o.y]).join(","),a=Di.prototype.getTileUrl.call(this,e);return a+f(this.wmsParams,a,this.options.uppercase)+(this.options.uppercase?"&BBOX=":"&bbox=")+s},setParams:function(e,t){return n(this.wmsParams,e),t||this.redraw(),this}});function qi(e,t){return new Ui(e,t)}Di.WMS=Ui,Fi.wms=qi;var Vi=Zn.extend({options:{padding:.1},initialize:function(e){p(this,e),s(this),this._layers=this._layers||{}},onAdd:function(){this._container||(this._initContainer(),_t(this._container,"leaflet-zoom-animated")),this.getPane().appendChild(this._container),this._update(),this.on("update",this._updatePaths,this)},onRemove:function(){this.off("update",this._updatePaths,this),this._destroyContainer()},getEvents:function(){var e={viewreset:this._reset,zoom:this._onZoom,moveend:this._update,zoomend:this._onZoomEnd};return this._zoomAnimated&&(e.zoomanim=this._onAnimZoom),e},_onAnimZoom:function(e){this._updateTransform(e.center,e.zoom)},_onZoom:function(){this._updateTransform(this._map.getCenter(),this._map.getZoom())},_updateTransform:function(e,t){var n=this._map.getZoomScale(t,this._zoom),i=this._map.getSize().multiplyBy(.5+this.options.padding),r=this._map.project(this._center,t),o=i.multiplyBy(-n).add(r).subtract(this._map._getNewPixelOrigin(e,t));Ie.any3d?Lt(this._container,o,n):Et(this._container,o)},_reset:function(){for(var e in this._update(),this._updateTransform(this._center,this._zoom),this._layers)this._layers[e]._reset()},_onZoomEnd:function(){for(var e in this._layers)this._layers[e]._project()},_updatePaths:function(){for(var e in this._layers)this._layers[e]._update()},_update:function(){var e=this.options.padding,t=this._map.getSize(),n=this._map.containerPointToLayerPoint(t.multiplyBy(-e)).round();this._bounds=new I(n,n.add(t.multiplyBy(1+2*e)).round()),this._center=this._map.getCenter(),this._zoom=this._map.getZoom()}}),Wi=Vi.extend({options:{tolerance:0},getEvents:function(){var e=Vi.prototype.getEvents.call(this);return e.viewprereset=this._onViewPreReset,e},_onViewPreReset:function(){this._postponeUpdatePaths=!0},onAdd:function(){Vi.prototype.onAdd.call(this),this._draw()},_initContainer:function(){var e=this._container=document.createElement("canvas");zt(e,"mousemove",this._onMouseMove,this),zt(e,"click dblclick mousedown mouseup contextmenu",this._onClick,this),zt(e,"mouseout",this._handleMouseOut,this),e._leaflet_disable_events=!0,this._ctx=e.getContext("2d")},_destroyContainer:function(){E(this._redrawRequest),delete this._ctx,ft(this._container),Dt(this._container),delete this._container},_updatePaths:function(){if(!this._postponeUpdatePaths){for(var e in this._redrawBounds=null,this._layers)this._layers[e]._update();this._redraw()}},_update:function(){if(!this._map._animatingZoom||!this._bounds){Vi.prototype._update.call(this);var e=this._bounds,t=this._container,n=e.getSize(),i=Ie.retina?2:1;Et(t,e.min),t.width=i*n.x,t.height=i*n.y,t.style.width=n.x+"px",t.style.height=n.y+"px",Ie.retina&&this._ctx.scale(2,2),this._ctx.translate(-e.min.x,-e.min.y),this.fire("update")}},_reset:function(){Vi.prototype._reset.call(this),this._postponeUpdatePaths&&(this._postponeUpdatePaths=!1,this._updatePaths())},_initPath:function(e){this._updateDashArray(e),this._layers[s(e)]=e;var t=e._order={layer:e,prev:this._drawLast,next:null};this._drawLast&&(this._drawLast.next=t),this._drawLast=t,this._drawFirst=this._drawFirst||this._drawLast},_addPath:function(e){this._requestRedraw(e)},_removePath:function(e){var t=e._order,n=t.next,i=t.prev;n?n.prev=i:this._drawLast=i,i?i.next=n:this._drawFirst=n,delete e._order,delete this._layers[s(e)],this._requestRedraw(e)},_updatePath:function(e){this._extendRedrawBounds(e),e._project(),e._update(),this._requestRedraw(e)},_updateStyle:function(e){this._updateDashArray(e),this._requestRedraw(e)},_updateDashArray:function(e){if("string"===typeof e.options.dashArray){var t,n,i=e.options.dashArray.split(/[, ]+/),r=[];for(n=0;n')}}catch(e){}return function(e){return document.createElement("<"+e+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}(),Gi={_initContainer:function(){this._container=pt("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(Vi.prototype._update.call(this),this.fire("update"))},_initPath:function(e){var t=e._container=Zi("shape");_t(t,"leaflet-vml-shape "+(this.options.className||"")),t.coordsize="1 1",e._path=Zi("path"),t.appendChild(e._path),this._updateStyle(e),this._layers[s(e)]=e},_addPath:function(e){var t=e._container;this._container.appendChild(t),e.options.interactive&&e.addInteractiveTarget(t)},_removePath:function(e){var t=e._container;ft(t),e.removeInteractiveTarget(t),delete this._layers[s(e)]},_updateStyle:function(e){var t=e._stroke,n=e._fill,i=e.options,r=e._container;r.stroked=!!i.stroke,r.filled=!!i.fill,i.stroke?(t||(t=e._stroke=Zi("stroke")),r.appendChild(t),t.weight=i.weight+"px",t.color=i.color,t.opacity=i.opacity,i.dashArray?t.dashStyle=v(i.dashArray)?i.dashArray.join(" "):i.dashArray.replace(/( *, *)/g," "):t.dashStyle="",t.endcap=i.lineCap.replace("butt","flat"),t.joinstyle=i.lineJoin):t&&(r.removeChild(t),e._stroke=null),i.fill?(n||(n=e._fill=Zi("fill")),r.appendChild(n),n.color=i.fillColor||i.color,n.opacity=i.fillOpacity):n&&(r.removeChild(n),e._fill=null)},_updateCircle:function(e){var t=e._point.round(),n=Math.round(e._radius),i=Math.round(e._radiusY||n);this._setPath(e,e._empty()?"M0 0":"AL "+t.x+","+t.y+" "+n+","+i+" 0,23592600")},_setPath:function(e,t){e._path.v=t},_bringToFront:function(e){gt(e._container)},_bringToBack:function(e){vt(e._container)}},Qi=Ie.vml?Zi:K,Yi=Vi.extend({_initContainer:function(){this._container=Qi("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=Qi("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){ft(this._container),Dt(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_update:function(){if(!this._map._animatingZoom||!this._bounds){Vi.prototype._update.call(this);var e=this._bounds,t=e.getSize(),n=this._container;this._svgSize&&this._svgSize.equals(t)||(this._svgSize=t,n.setAttribute("width",t.x),n.setAttribute("height",t.y)),Et(n,e.min),n.setAttribute("viewBox",[e.min.x,e.min.y,t.x,t.y].join(" ")),this.fire("update")}},_initPath:function(e){var t=e._path=Qi("path");e.options.className&&_t(t,e.options.className),e.options.interactive&&_t(t,"leaflet-interactive"),this._updateStyle(e),this._layers[s(e)]=e},_addPath:function(e){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(e._path),e.addInteractiveTarget(e._path)},_removePath:function(e){ft(e._path),e.removeInteractiveTarget(e._path),delete this._layers[s(e)]},_updatePath:function(e){e._project(),e._update()},_updateStyle:function(e){var t=e._path,n=e.options;t&&(n.stroke?(t.setAttribute("stroke",n.color),t.setAttribute("stroke-opacity",n.opacity),t.setAttribute("stroke-width",n.weight),t.setAttribute("stroke-linecap",n.lineCap),t.setAttribute("stroke-linejoin",n.lineJoin),n.dashArray?t.setAttribute("stroke-dasharray",n.dashArray):t.removeAttribute("stroke-dasharray"),n.dashOffset?t.setAttribute("stroke-dashoffset",n.dashOffset):t.removeAttribute("stroke-dashoffset")):t.setAttribute("stroke","none"),n.fill?(t.setAttribute("fill",n.fillColor||n.color),t.setAttribute("fill-opacity",n.fillOpacity),t.setAttribute("fill-rule",n.fillRule||"evenodd")):t.setAttribute("fill","none"))},_updatePoly:function(e,t){this._setPath(e,$(e._parts,t))},_updateCircle:function(e){var t=e._point,n=Math.max(Math.round(e._radius),1),i="a"+n+","+(Math.max(Math.round(e._radiusY),1)||n)+" 0 1,0 ",r=e._empty()?"M0 0":"M"+(t.x-n)+","+t.y+i+2*n+",0 "+i+2*-n+",0 ";this._setPath(e,r)},_setPath:function(e,t){e._path.setAttribute("d",t)},_bringToFront:function(e){gt(e._path)},_bringToBack:function(e){vt(e._path)}});function Ki(e){return Ie.svg||Ie.vml?new Yi(e):null}Ie.vml&&Yi.include(Gi),nn.include({getRenderer:function(e){var t=e.options.renderer||this._getPaneRenderer(e.options.pane)||this.options.renderer||this._renderer;return t||(t=this._renderer=this._createRenderer()),this.hasLayer(t)||this.addLayer(t),t},_getPaneRenderer:function(e){if("overlayPane"===e||void 0===e)return!1;var t=this._paneRenderers[e];return void 0===t&&(t=this._createRenderer({pane:e}),this._paneRenderers[e]=t),t},_createRenderer:function(e){return this.options.preferCanvas&&Hi(e)||Ki(e)}});var $i=ui.extend({initialize:function(e,t){ui.prototype.initialize.call(this,this._boundsToLatLngs(e),t)},setBounds:function(e){return this.setLatLngs(this._boundsToLatLngs(e))},_boundsToLatLngs:function(e){return[(e=D(e)).getSouthWest(),e.getNorthWest(),e.getNorthEast(),e.getSouthEast()]}});function Xi(e,t){return new $i(e,t)}Yi.create=Qi,Yi.pointsToPath=$,hi.geometryToLayer=pi,hi.coordsToLatLng=mi,hi.coordsToLatLngs=gi,hi.latLngToCoords=vi,hi.latLngsToCoords=yi,hi.getFeature=_i,hi.asFeature=bi,nn.mergeOptions({boxZoom:!0});var Ji=gn.extend({initialize:function(e){this._map=e,this._container=e._container,this._pane=e._panes.overlayPane,this._resetStateTimeout=0,e.on("unload",this._destroy,this)},addHooks:function(){zt(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){Dt(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){ft(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(e){if(!e.shiftKey||1!==e.which&&1!==e.button)return!1;this._clearDeferredResetState(),this._resetState(),it(),jt(),this._startPoint=this._map.mouseEventToContainerPoint(e),zt(document,{contextmenu:Qt,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(e){this._moved||(this._moved=!0,this._box=pt("div","leaflet-zoom-box",this._container),_t(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(e);var t=new I(this._point,this._startPoint),n=t.getSize();Et(this._box,t.min),this._box.style.width=n.x+"px",this._box.style.height=n.y+"px"},_finish:function(){this._moved&&(ft(this._box),bt(this._container,"leaflet-crosshair")),rt(),Nt(),Dt(document,{contextmenu:Qt,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(e){if((1===e.which||1===e.button)&&(this._finish(),this._moved)){this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(r(this._resetState,this),0);var t=new B(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point));this._map.fitBounds(t).fire("boxzoomend",{boxZoomBounds:t})}},_onKeyDown:function(e){27===e.keyCode&&(this._finish(),this._clearDeferredResetState(),this._resetState())}});nn.addInitHook("addHandler","boxZoom",Ji),nn.mergeOptions({doubleClickZoom:!0});var er=gn.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(e){var t=this._map,n=t.getZoom(),i=t.options.zoomDelta,r=e.originalEvent.shiftKey?n-i:n+i;"center"===t.options.doubleClickZoom?t.setZoom(r):t.setZoomAround(e.containerPoint,r)}});nn.addInitHook("addHandler","doubleClickZoom",er),nn.mergeOptions({dragging:!0,inertia:!0,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0});var tr=gn.extend({addHooks:function(){if(!this._draggable){var e=this._map;this._draggable=new _n(e._mapPane,e._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),e.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),e.on("zoomend",this._onZoomEnd,this),e.whenReady(this._onZoomEnd,this))}_t(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){bt(this._map._container,"leaflet-grab"),bt(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var e=this._map;if(e._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity){var t=D(this._map.options.maxBounds);this._offsetLimit=z(this._map.latLngToContainerPoint(t.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(t.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))}else this._offsetLimit=null;e.fire("movestart").fire("dragstart"),e.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(e){if(this._map.options.inertia){var t=this._lastTime=+new Date,n=this._lastPos=this._draggable._absPos||this._draggable._newPos;this._positions.push(n),this._times.push(t),this._prunePositions(t)}this._map.fire("move",e).fire("drag",e)},_prunePositions:function(e){for(;this._positions.length>1&&e-this._times[0]>50;)this._positions.shift(),this._times.shift()},_onZoomEnd:function(){var e=this._map.getSize().divideBy(2),t=this._map.latLngToLayerPoint([0,0]);this._initialWorldOffset=t.subtract(e).x,this._worldWidth=this._map.getPixelWorldBounds().getSize().x},_viscousLimit:function(e,t){return e-(e-t)*this._viscosity},_onPreDragLimit:function(){if(this._viscosity&&this._offsetLimit){var e=this._draggable._newPos.subtract(this._draggable._startPos),t=this._offsetLimit;e.xt.max.x&&(e.x=this._viscousLimit(e.x,t.max.x)),e.y>t.max.y&&(e.y=this._viscousLimit(e.y,t.max.y)),this._draggable._newPos=this._draggable._startPos.add(e)}},_onPreDragWrap:function(){var e=this._worldWidth,t=Math.round(e/2),n=this._initialWorldOffset,i=this._draggable._newPos.x,r=(i-t+n)%e+t-n,o=(i+t+n)%e-t-n,s=Math.abs(r+n)0?o:-o))-t;this._delta=0,this._startTime=null,s&&("center"===e.options.scrollWheelZoom?e.setZoom(t+s):e.setZoomAround(this._lastMousePos,t+s))}});nn.addInitHook("addHandler","scrollWheelZoom",ir);var rr=600;nn.mergeOptions({tapHold:Ie.touchNative&&Ie.safari&&Ie.mobile,tapTolerance:15});var or=gn.extend({addHooks:function(){zt(this._map._container,"touchstart",this._onDown,this)},removeHooks:function(){Dt(this._map._container,"touchstart",this._onDown,this)},_onDown:function(e){if(clearTimeout(this._holdTimeout),1===e.touches.length){var t=e.touches[0];this._startPos=this._newPos=new M(t.clientX,t.clientY),this._holdTimeout=setTimeout(r(function(){this._cancel(),this._isTapValid()&&(zt(document,"touchend",Gt),zt(document,"touchend touchcancel",this._cancelClickPrevent),this._simulateEvent("contextmenu",t))},this),rr),zt(document,"touchend touchcancel contextmenu",this._cancel,this),zt(document,"touchmove",this._onMove,this)}},_cancelClickPrevent:function e(){Dt(document,"touchend",Gt),Dt(document,"touchend touchcancel",e)},_cancel:function(){clearTimeout(this._holdTimeout),Dt(document,"touchend touchcancel contextmenu",this._cancel,this),Dt(document,"touchmove",this._onMove,this)},_onMove:function(e){var t=e.touches[0];this._newPos=new M(t.clientX,t.clientY)},_isTapValid:function(){return this._newPos.distanceTo(this._startPos)<=this._map.options.tapTolerance},_simulateEvent:function(e,t){var n=new MouseEvent(e,{bubbles:!0,cancelable:!0,view:window,screenX:t.screenX,screenY:t.screenY,clientX:t.clientX,clientY:t.clientY});n._simulated=!0,t.target.dispatchEvent(n)}});nn.addInitHook("addHandler","tapHold",or),nn.mergeOptions({touchZoom:Ie.touch,bounceAtZoomLimits:!0});var sr=gn.extend({addHooks:function(){_t(this._map._container,"leaflet-touch-zoom"),zt(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){bt(this._map._container,"leaflet-touch-zoom"),Dt(this._map._container,"touchstart",this._onTouchStart,this)},_onTouchStart:function(e){var t=this._map;if(e.touches&&2===e.touches.length&&!t._animatingZoom&&!this._zooming){var n=t.mouseEventToContainerPoint(e.touches[0]),i=t.mouseEventToContainerPoint(e.touches[1]);this._centerPoint=t.getSize()._divideBy(2),this._startLatLng=t.containerPointToLatLng(this._centerPoint),"center"!==t.options.touchZoom&&(this._pinchStartLatLng=t.containerPointToLatLng(n.add(i)._divideBy(2))),this._startDist=n.distanceTo(i),this._startZoom=t.getZoom(),this._moved=!1,this._zooming=!0,t._stop(),zt(document,"touchmove",this._onTouchMove,this),zt(document,"touchend touchcancel",this._onTouchEnd,this),Gt(e)}},_onTouchMove:function(e){if(e.touches&&2===e.touches.length&&this._zooming){var t=this._map,n=t.mouseEventToContainerPoint(e.touches[0]),i=t.mouseEventToContainerPoint(e.touches[1]),o=n.distanceTo(i)/this._startDist;if(this._zoom=t.getScaleZoom(o,this._startZoom),!t.options.bounceAtZoomLimits&&(this._zoomt.getMaxZoom()&&o>1)&&(this._zoom=t._limitZoom(this._zoom)),"center"===t.options.touchZoom){if(this._center=this._startLatLng,1===o)return}else{var s=n._add(i)._divideBy(2)._subtract(this._centerPoint);if(1===o&&0===s.x&&0===s.y)return;this._center=t.unproject(t.project(this._pinchStartLatLng,this._zoom).subtract(s),this._zoom)}this._moved||(t._moveStart(!0,!1),this._moved=!0),E(this._animRequest);var a=r(t._move,t,this._center,this._zoom,{pinch:!0,round:!1},void 0);this._animRequest=C(a,this,!0),Gt(e)}},_onTouchEnd:function(){this._moved&&this._zooming?(this._zooming=!1,E(this._animRequest),Dt(document,"touchmove",this._onTouchMove,this),Dt(document,"touchend touchcancel",this._onTouchEnd,this),this._map.options.zoomAnimation?this._map._animateZoom(this._center,this._map._limitZoom(this._zoom),!0,this._map.options.zoomSnap):this._map._resetView(this._center,this._map._limitZoom(this._zoom))):this._zooming=!1}});nn.addInitHook("addHandler","touchZoom",sr),nn.BoxZoom=Ji,nn.DoubleClickZoom=er,nn.Drag=tr,nn.Keyboard=nr,nn.ScrollWheelZoom=ir,nn.TapHold=or,nn.TouchZoom=sr,e.Bounds=I,e.Browser=Ie,e.CRS=q,e.Canvas=Wi,e.Circle=si,e.CircleMarker=ri,e.Class=T,e.Control=on,e.DivIcon=Ri,e.DivOverlay=ji,e.DomEvent=en,e.DomUtil=It,e.Draggable=_n,e.Evented=O,e.FeatureGroup=Yn,e.GeoJSON=hi,e.GridLayer=zi,e.Handler=gn,e.Icon=$n,e.ImageOverlay=Si,e.LatLng=F,e.LatLngBounds=B,e.Layer=Zn,e.LayerGroup=Gn,e.LineUtil=Dn,e.Map=nn,e.Marker=ti,e.Mixin=vn,e.Path=ii,e.Point=M,e.PolyUtil=Sn,e.Polygon=ui,e.Polyline=li,e.Popup=Ni,e.PosAnimation=tn,e.Projection=qn,e.Rectangle=$i,e.Renderer=Vi,e.SVG=Yi,e.SVGOverlay=Pi,e.TileLayer=Di,e.Tooltip=Mi,e.Transformation=Z,e.Util=P,e.VideoOverlay=Li,e.bind=r,e.bounds=z,e.canvas=Hi,e.circle=ai,e.circleMarker=oi,e.control=sn,e.divIcon=Ii,e.extend=n,e.featureGroup=Kn,e.geoJSON=wi,e.geoJson=ki,e.gridLayer=Bi,e.icon=Xn,e.imageOverlay=Ci,e.latLng=U,e.latLngBounds=D,e.layerGroup=Qn,e.map=rn,e.marker=ni,e.point=R,e.polygon=di,e.polyline=ci,e.popup=Oi,e.rectangle=Xi,e.setOptions=p,e.stamp=s,e.svg=Ki,e.svgOverlay=Ti,e.tileLayer=Fi,e.tooltip=Ai,e.transformation=G,e.version=t,e.videoOverlay=Ei;var ar=window.L;e.noConflict=function(){return window.L=ar,this},window.L=e}(t)},497(e,t,n){"use strict";var i=n(218);function r(){}function o(){}o.resetWarningCache=r,e.exports=function(){function e(e,t,n,r,o,s){if(s!==i){var a=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw a.name="Invariant Violation",a}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:o,resetWarningCache:r};return n.PropTypes=n,n}},173(e,t,n){e.exports=n(497)()},218(e){"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},730(e,t,n){"use strict";var i=n(43),r=n(853);function o(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n