Repository: errorcode26/test Branch: main Commit: f7944e60f6a3 Files: 65 Total size: 874.1 KB Directory structure: gitextract_tb8zvos5/ ├── IndiaLIMS.spec ├── LIMS.spec ├── README.md ├── app.py ├── build_exe.py ├── clean_db.py ├── config.py ├── core/ │ ├── __init__.py │ ├── database.py │ ├── logic.py │ └── security.py ├── gis_processor.py ├── inno_setup.iss ├── render.yaml ├── report_generator.py ├── requirements-dev.txt ├── requirements-windows.txt ├── requirements.txt ├── routes/ │ ├── __init__.py │ ├── auth.py │ ├── documents.py │ ├── feedback.py │ ├── gis.py │ ├── pages.py │ ├── records.py │ ├── users.py │ └── utils.py ├── run_server.py ├── scripts/ │ ├── inspect_recovery.py │ ├── recover_superadmin_auto.py │ └── recovery_credentials_2026-04-25T050759_400298.txt ├── static/ │ ├── css/ │ │ └── style.css │ ├── data/ │ │ └── india-boundary.geojson │ └── js/ │ ├── api.js │ ├── auth.js │ ├── map.js │ ├── map_OLD_BAK.js │ └── modules/ │ ├── admin.js │ ├── dashboard.js │ ├── forms.js │ ├── gis.js │ ├── map_engine.js │ ├── records.js │ ├── state.js │ └── utils.js ├── templates/ │ ├── admin_dashboard.html │ ├── login.html │ └── public_viewer_v2.html ├── tests/ │ ├── Testing_Report_20260425_211328.md │ ├── Testing_Report_20260426_013232.md │ ├── Testing_Report_20260426_095300.md │ ├── Testing_Report_20260426_125456.md │ ├── Testing_Report_20260426_132557.md │ ├── Testing_Report_20260426_132640.md │ ├── check_users_roles.py │ ├── generate_test_report.py │ ├── robust_role_test.py │ ├── test_advanced_gis.py │ ├── test_logins.py │ ├── test_output/ │ │ └── Village_Ledger_Amingaon.xlsx │ ├── test_record_soft_delete.py │ ├── test_recovery_flow.py │ ├── test_report_gen.py │ └── test_restore_flow.py └── utils.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: IndiaLIMS.spec ================================================ # -*- mode: python ; coding: utf-8 -*- a = Analysis( ['c:\\Users\\user\\Desktop\\zz\\app.py'], pathex=[], binaries=[], datas=[('c:\\Users\\user\\Desktop\\zz\\templates', 'templates'), ('c:\\Users\\user\\Desktop\\zz\\static', 'static'), ('c:\\Users\\user\\Desktop\\zz\\.env', '.')], hiddenimports=['flask', 'werkzeug', 'werkzeug.security', 'jinja2', 'shapely', 'shapely.geometry', 'shapely.validation', 'fpdf', 'pandas', 'openpyxl', 'qrcode', 'webview', 'pymongo', 'dnspython', 'certifi', 'dotenv'], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[], noarchive=False, optimize=0, ) pyz = PYZ(a.pure) exe = EXE( pyz, a.scripts, a.binaries, a.datas, [], name='IndiaLIMS', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=False, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, icon=['c:\\Users\\user\\Desktop\\zz\\logo.ico'], ) ================================================ FILE: LIMS.spec ================================================ # -*- mode: python ; coding: utf-8 -*- a = Analysis( ['c:\\Users\\user\\Desktop\\zz\\app.py'], pathex=[], binaries=[], datas=[('c:\\Users\\user\\Desktop\\zz\\templates', 'templates'), ('c:\\Users\\user\\Desktop\\zz\\static', 'static'), ('c:\\Users\\user\\Desktop\\zz\\.env', '.')], hiddenimports=['flask', 'werkzeug', 'werkzeug.security', 'jinja2', 'shapely', 'shapely.geometry', 'shapely.validation', 'fpdf', 'pandas', 'openpyxl', 'qrcode', 'webview', 'pymongo', 'dnspython', 'certifi', 'dotenv'], hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=[], noarchive=False, optimize=0, ) pyz = PYZ(a.pure) exe = EXE( pyz, a.scripts, a.binaries, a.datas, [], name='LIMS', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=False, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, icon=['c:\\Users\\user\\Desktop\\zz\\logo.ico'], ) ================================================ FILE: README.md ================================================ nothing ================================================ FILE: app.py ================================================ import os import sys import time import socket import threading import uuid from datetime import datetime, timedelta import json from flask import Flask, jsonify from werkzeug.security import generate_password_hash from config import SECRET_KEY, DEBUG, HOST, PORT, MONGO_URI from routes import ( pages_bp, auth_bp, records_bp, users_bp, gis_bp, documents_bp, feedback_bp, utils_bp ) from core import DATA_DIR, load_users, save_users, users_collection def create_app(): app = Flask(__name__) app.secret_key = SECRET_KEY app.permanent_session_lifetime = timedelta(days=30) # Register Blueprints app.register_blueprint(pages_bp) app.register_blueprint(auth_bp) app.register_blueprint(records_bp) app.register_blueprint(users_bp) app.register_blueprint(gis_bp) app.register_blueprint(documents_bp) app.register_blueprint(feedback_bp) app.register_blueprint(utils_bp) @app.route('/ping', methods=['GET']) def ping(): return jsonify({"status": "alive"}), 200 @app.after_request def add_header(response): response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0' response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '0' return response return app app = create_app() def bootstrap_admin(): """Bootstrap default admin if no superadmin exists with the standard ID.""" try: if users_collection is not None: # Check for the specific bootstrap ID to ensure test consistency bootstrap_user = users_collection.find_one({"user_id": "bootstrap-admin-01"}) if not bootstrap_user: default_admin_user = os.environ.get("DEFAULT_ADMIN_USER", "admin") default_admin_password = os.environ.get("DEFAULT_ADMIN_PASSWORD", "password123") users = load_users() new_sa = { "user_id": "bootstrap-admin-01", "username": default_admin_user, "password_hash": generate_password_hash(default_admin_password), "role": "superadmin", "full_name": "System Administrator", "email": "admin@indialims.edu", "phone": "+91-0000000000", "designation": "System Administrator", "department": "Land Records", "office_location": "System Default", "is_active": True, "is_recovery": True, # Grant recovery rights for testing "created_at": datetime.now().isoformat() + "Z", "last_login": datetime.now().isoformat() + "Z" } # If 'admin' username is taken by a non-bootstrap user, use a unique one if any(u.get('username') == default_admin_user for u in users): new_sa['username'] = f"admin_root_{uuid.uuid4().hex[:4]}" users.append(new_sa) save_users(users) print(f"[BOOTSTRAP] System Administrator '{new_sa['username']}' created.") except Exception as e: print(f"Bootstrap error: {e}") # If the users collection is empty or missing expected test accounts, seed from user_id_password.json try: existing = load_users() if not existing: cred_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'user_id_password.json') if os.path.exists(cred_file): with open(cred_file, 'r', encoding='utf-8') as f: try: creds = json.load(f) except Exception: creds = [] seeded = [] for idx, c in enumerate(creds): uname = c.get('username') or c.get('user_id') pwd = c.get('password') if not uname or not pwd: continue user_id = f"user-{uuid.uuid4().hex[:8]}" role = 'admin' is_recovery = False if idx == 0 or uname == os.environ.get('DEFAULT_ADMIN_USER', 'admin'): role = 'superadmin' user_id = 'bootstrap-admin-01' if isinstance(uname, str) and uname.startswith('recovery_sa_'): is_recovery = True role = 'superadmin' seeded_user = { 'user_id': user_id, 'username': uname, 'password_hash': generate_password_hash(pwd), 'role': role, 'is_recovery': is_recovery, 'created_at': datetime.now().isoformat() + 'Z' } seeded.append(seeded_user) if seeded: save_users(seeded) print(f"[BOOTSTRAP] Seeded {len(seeded)} user(s) from {cred_file}") except Exception: pass # Ensure a bootstrap superadmin exists when the app is imported (useful for tests) try: bootstrap_admin() except Exception: # Non-fatal: if DB isn't reachable at import time, tests or runtime will still try again when running __main__ pass if __name__ == "__main__": # Redirect logs safely log_path = os.path.join(os.environ.get('APPDATA', os.path.dirname(os.path.abspath(__file__))), "LIMS.log") if sys.stdout is None or getattr(sys.stdout, "closed", True): sys.stdout = open(log_path, "w", encoding="utf-8") if sys.stderr is None or getattr(sys.stderr, "closed", True): sys.stderr = open(log_path, "a", encoding="utf-8") os.makedirs(DATA_DIR, exist_ok=True) bootstrap_admin() print("\n" + "=" * 60) print(" LIMS - Modular Version") print(f" Server starting on http://{HOST}:{PORT}") print("=" * 60 + "\n") try: import webview def get_free_port(start_port): for port in range(start_port, 65535): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: try: s.bind((HOST, port)) return port except OSError: continue return start_port actual_port = get_free_port(PORT) def start_server(): app.run(host=HOST, port=actual_port, debug=False, use_reloader=False) threading.Thread(target=start_server, daemon=True).start() time.sleep(2) display_host = "127.0.0.1" if HOST == "0.0.0.0" else HOST webview.settings['ALLOW_DOWNLOADS'] = True webview.create_window("LIMS", url=f"http://{display_host}:{actual_port}/login", width=1400, height=900) webview.start() except ImportError: app.run(host=HOST, port=PORT, debug=DEBUG) ================================================ FILE: build_exe.py ================================================ """ build_exe.py - PyInstaller & PyWebView Compilation Script for LIMS Compiles the application into a standalone Windows .exe using PyWebView as the GUI wrapper and PyInstaller for bundling. Usage: python build_exe.py # Build the .exe python build_exe.py --run # Run in PyWebView window (for testing) """ import os import sys import subprocess import threading import time from utils import resource_path def start_flask(): """Start the Flask server in a background thread.""" from app import app app.run(host="127.0.0.1", port=5000, debug=False, use_reloader=False) def run_pywebview(): """Run the application in a PyWebView window (for testing).""" try: import webview except ImportError: print("ERROR: pywebview is not installed. Install with: pip install pywebview") sys.exit(1) # Start Flask in background thread flask_thread = threading.Thread(target=start_flask, daemon=True) flask_thread.start() # Wait for Flask to be ready print("Waiting for Flask server to start...") time.sleep(2) # Enable downloads in PyWebView import webview webview.settings['ALLOW_DOWNLOADS'] = True # Create PyWebView window window = webview.create_window( title="LIMS - Land Information Management System", url="http://127.0.0.1:5000/login", width=1400, height=900, min_size=(1024, 700), resizable=True, frameless=False, easy_drag=True ) webview.start(debug=False) print("Application closed.") def build_exe(): """Build the standalone .exe using PyInstaller.""" try: import PyInstaller # noqa: F401 except ImportError: print("ERROR: PyInstaller is not installed. Install with: pip install pyinstaller") sys.exit(1) project_dir = os.path.dirname(os.path.abspath(__file__)) # PyInstaller command pyinstaller_args = [ sys.executable, '-m', 'PyInstaller', '--name=LIMS', '--onefile', '--windowed', # No console window on Windows '--clean', ] # Add icon if available icon_path = os.path.join(project_dir, 'logo.ico') if os.path.exists(icon_path): pyinstaller_args.append('--icon=' + icon_path) print('Using icon: ' + icon_path) else: print('Warning: Icon file not found at ' + icon_path + ". Please place a 'logo.ico' in the project root to include an icon.") pyinstaller_args.extend([ # Add data files (templates, static) '--add-data=' + os.path.join(project_dir, 'templates') + os.pathsep + 'templates', '--add-data=' + os.path.join(project_dir, 'static') + os.pathsep + 'static', '--add-data=' + os.path.join(project_dir, '.env') + os.pathsep + '.', # Hidden imports that PyInstaller might miss '--hidden-import=flask', '--hidden-import=werkzeug', '--hidden-import=werkzeug.security', '--hidden-import=jinja2', '--hidden-import=shapely', '--hidden-import=shapely.geometry', '--hidden-import=shapely.validation', '--hidden-import=fpdf', '--hidden-import=pandas', '--hidden-import=openpyxl', '--hidden-import=qrcode', '--hidden-import=webview', '--hidden-import=pymongo', '--hidden-import=dnspython', '--hidden-import=certifi', '--hidden-import=dotenv', # Main entry point os.path.join(project_dir, 'app.py') ]) print('=' * 60) print(' Building LIMS .exe with PyInstaller') print('=' * 60) print('\nCommand: ' + ' '.join(pyinstaller_args) + '\n') result = subprocess.run(pyinstaller_args, cwd=project_dir) if result.returncode == 0: print('\n' + '=' * 60) print(' BUILD SUCCESSFUL!') print(' Executable: ' + os.path.join(project_dir, 'dist', 'LIMS.exe')) print('=' * 60) else: print('\n' + '=' * 60) print(' BUILD FAILED!') print('=' * 60) sys.exit(1) if __name__ == '__main__': if len(sys.argv) > 1 and sys.argv[1] == '--run': # Run in PyWebView window for testing run_pywebview() else: # Build the .exe build_exe() ================================================ FILE: clean_db.py ================================================ from config import MONGO_URI from pymongo import MongoClient import certifi db = MongoClient(MONGO_URI, tlsCAFile=certifi.where()).get_database('indialims') db.records.delete_many({}) db.users.delete_many({"user_id": {"$ne": "bootstrap-admin-01"}}) print('Cleaned') ================================================ FILE: config.py ================================================ """ config.py - Application Configuration for India LIMS Centralized settings for the application. """ import os from dotenv import load_dotenv from utils import resource_path # Load variables from .env file env_path = resource_path(".env") load_dotenv(env_path) # --- Application Settings --- # Use environment variable, or persist a generated key to file so sessions # survive server restarts. def _get_or_create_secret_key(): env_key = os.environ.get("LIMS_SECRET_KEY") if env_key: return env_key key_file = os.path.join(BASE_DIR, ".secret_key") if os.path.exists(key_file): with open(key_file, "r") as f: return f.read().strip() new_key = os.urandom(32).hex() with open(key_file, "w") as f: f.write(new_key) return new_key BASE_DIR = os.path.dirname(os.path.abspath(__file__)) SECRET_KEY = _get_or_create_secret_key() DEBUG = os.environ.get("LIMS_DEBUG", "false").lower() == "true" # Render provides 'PORT' env var, standard local uses 'LIMS_PORT' PORT = int(os.environ.get("PORT", os.environ.get("LIMS_PORT", 5000))) # On Render/Production, bind to all interfaces; locally use 127.0.0.1 for safety RENDER = os.environ.get("RENDER") HOST = os.environ.get("LIMS_HOST", "0.0.0.0" if RENDER else "127.0.0.1") # --- Database Configuration --- # The MongoDB Atlas connection string MONGO_URI = os.environ.get("MONGO_URI") if not MONGO_URI: print("WARNING: MONGO_URI is not set in the environment or .env file.") # --- Data Paths (relative to project root) --- # Note: app.py overrides these for PyInstaller using resource_path() DATA_DIR = os.path.join(BASE_DIR, "data") RECORDS_FILE = os.path.join(DATA_DIR, "records.json") USERS_FILE = os.path.join(DATA_DIR, "users.json") FEEDBACK_FILE = os.path.join(DATA_DIR, "feedback.json") # --- Land Use Configuration --- LAND_USE_OPTIONS = [ "Agricultural", "Residential", "Commercial", "Industrial", "Government", "Forest", "Wasteland" ] # --- Land Use Color Map --- LAND_USE_COLORS = { "Agricultural": "#22c55e", "Residential": "#3b82f6", "Commercial": "#f59e0b", "Industrial": "#8b5cf6", "Government": "#ef4444", "Forest": "#065f46", "Wasteland": "#9ca3af" } # --- Mutation Types --- MUTATION_TYPES = [ "Sale Deed", "Inheritance", "Gift Deed", "Partition", "Court Order" ] # --- Pagination --- DEFAULT_PAGE_SIZE = 50 MAX_PAGE_SIZE = 200 # --- Dashboard Limits --- DASHBOARD_TOP_DISTRICTS = 6 DASHBOARD_RECENT_MUTATIONS = 6 ================================================ FILE: core/__init__.py ================================================ from .database import ( load_users, save_users, load_records, save_records, load_feedback, save_feedback, _log_audit, audit_collection, users_collection, records_collection, feedback_collection, DATA_DIR ) from .security import ( admin_required, viewer_or_admin_required, role_required, generate_captcha, verify_captcha_logic ) from .logic import ( _mask_owner_for_viewer, _strip_b64_from_list, _apply_filters_to_records, _generate_ulpin, _update_nested, _calculate_estimated_value ) ================================================ FILE: core/database.py ================================================ import os import json import uuid from datetime import datetime from pymongo import MongoClient, ReplaceOne import certifi from config import MONGO_URI from utils import resource_path # Override data paths for PyInstaller DATA_DIR = resource_path("data") RECORDS_FILE = os.path.join(DATA_DIR, "records.json") USERS_FILE = os.path.join(DATA_DIR, "users.json") FEEDBACK_FILE = os.path.join(DATA_DIR, "feedback.json") # Database Setup (MongoDB) try: mongo_client = MongoClient(MONGO_URI, tlsCAFile=certifi.where(), serverSelectionTimeoutMS=5000) db = mongo_client.get_database("indialims") users_collection = db.users records_collection = db.records feedback_collection = db.feedback audit_collection = db.audit print("Successfully connected to MongoDB Cluster.") except Exception as e: print(f"MongoDB connection error: {e}") mongo_client = None db = None users_collection = None records_collection = None feedback_collection = None audit_collection = None # --- Data Access Helpers --- def load_users(): if users_collection is None: return [] return list(users_collection.find({}, {"_id": 0})) def save_users(users): if users_collection is None: return if not users: users_collection.delete_many({}) return requests = [ReplaceOne({"user_id": u["user_id"]}, u, upsert=True) for u in users] users_collection.bulk_write(requests) users_collection.delete_many({"user_id": {"$nin": [u["user_id"] for u in users]}}) def load_records(): if records_collection is None: return [] records = list(records_collection.find({})) for r in records: if "_id" in r: r["_id"] = str(r["_id"]) return records def save_records(records): if records_collection is None: return if not records: records_collection.delete_many({}) return requests = [ReplaceOne({"_id": r["_id"]}, r, upsert=True) for r in records] records_collection.bulk_write(requests) records_collection.delete_many({"_id": {"$nin": [r["_id"] for r in records]}}) def load_feedback(): if feedback_collection is None: return [] return list(feedback_collection.find({}, {"_id": 0})) def save_feedback(feedback_data): if feedback_collection is None: return if not feedback_data: feedback_collection.delete_many({}) return requests = [ReplaceOne({"id": f["id"]}, f, upsert=True) for f in feedback_data] feedback_collection.bulk_write(requests) feedback_collection.delete_many({"id": {"$nin": [f["id"] for f in feedback_data]}}) def _log_audit(action, performed_by, record_id=None, details=None): """Write a simple audit entry to the audit collection/file.""" try: entry = { "action": action, "performed_by": performed_by, "record_id": record_id, "details": details or {}, "timestamp": datetime.now().isoformat() + "Z" } if audit_collection is not None: audit_collection.insert_one(entry) else: audit_file = os.path.join(DATA_DIR, "audit.json") try: if os.path.exists(audit_file): with open(audit_file, "r", encoding="utf-8") as f: existing = json.load(f) else: existing = [] except Exception: existing = [] existing.append(entry) with open(audit_file, "w", encoding="utf-8") as f: json.dump(existing, f, indent=2, ensure_ascii=False) except Exception: pass def get_audit_collection(): return audit_collection def get_data_dir(): return DATA_DIR ================================================ FILE: core/logic.py ================================================ import random def _mask_owner_for_viewer(record): if "owner" not in record: return record record = record.copy() record["owner"] = record["owner"].copy() record["owner"].pop("aadhaar", None) record["owner"].pop("phone", None) name = record["owner"].get("name", "") parts = name.split() if len(parts) > 1: record["owner"]["name"] = parts[0] + " " + parts[1][0] + "." record["owner"].pop("proof_doc_b64", None) for mut in record.get("mutation_history", []): mut.pop("proof_doc_b64", None) return record def _strip_b64_from_list(records): clean_records = [] for r in records: r_copy = r.copy() if "owner" in r_copy: r_copy["owner"] = r_copy["owner"].copy() r_copy["owner"].pop("proof_doc_b64", None) if "mutation_history" in r_copy: r_copy["mutation_history"] = [m.copy() for m in r_copy["mutation_history"]] for m in r_copy["mutation_history"]: m.pop("proof_doc_b64", None) clean_records.append(r_copy) return clean_records def _apply_filters_to_records(records, params): state = (params.get("state") or "").strip().lower() district = (params.get("district") or "").strip().lower() village = (params.get("village") or "").strip().lower() land_use = (params.get("land_use") or "").strip() search = (params.get("search") or "").strip().lower() if not any([state, district, village, land_use, search]): return records filtered = [] for rec in records: loc = rec.get("location", {}) attrs = rec.get("attributes", {}) if state and loc.get("state", "").lower() != state: continue if district and loc.get("district", "").lower() != district: continue if village and loc.get("village", "").lower() != village: continue if land_use and attrs.get("land_use") != land_use: continue if search: search_text = " ".join([ rec.get("khasra_no", ""), rec.get("ulpin", ""), loc.get("village", ""), loc.get("district", ""), rec.get("owner", {}).get("name", "") ]).lower() if search not in search_text: continue filtered.append(rec) return filtered def _generate_ulpin(): return str(random.randint(10000000000000, 99999999999999)) def _calculate_estimated_value(area_ha, circle_rate_inr, land_use): """ Calculate estimated value using a land-use multiplier for realism. """ multipliers = { 'Commercial': 2.5, 'Industrial': 1.8, 'Residential': 1.5, 'Agricultural': 1.0, 'Government': 1.2, 'Forest': 0.8, 'Wasteland': 0.5 } multiplier = multipliers.get(land_use, 1.0) return float(area_ha) * float(circle_rate_inr) * multiplier def _update_nested(record, parent_key, child_key, value): if parent_key not in record: record[parent_key] = {} record[parent_key][child_key] = value return value ================================================ FILE: core/security.py ================================================ import string import random from functools import wraps from flask import session, jsonify from itsdangerous import URLSafeTimedSerializer from config import SECRET_KEY def admin_required(f): @wraps(f) def decorated_function(*args, **kwargs): role = (session.get("role") or "").lower() if role not in ("admin", "superadmin"): return jsonify({"error": "Unauthorized. Admin access required."}), 403 return f(*args, **kwargs) return decorated_function def viewer_or_admin_required(f): @wraps(f) def decorated_function(*args, **kwargs): role = (session.get("role") or "").lower() if role not in ("admin", "superadmin", "viewer", "officer"): return jsonify({"error": "Unauthorized. Please log in or pass CAPTCHA."}), 401 return f(*args, **kwargs) return decorated_function def role_required(*allowed_roles): allowed = tuple(r.lower() for r in allowed_roles) def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): role = (session.get("role") or "").lower() if role not in allowed: return jsonify({"error": "Unauthorized. Insufficient role."}), 403 return f(*args, **kwargs) return decorated_function return decorator def generate_captcha(): chars = string.ascii_letters captcha_text = ''.join(random.choice(chars) for _ in range(6)) serializer = URLSafeTimedSerializer(SECRET_KEY) token = serializer.dumps(captcha_text, salt='captcha-salt') return captcha_text, token def verify_captcha_logic(token, user_answer): serializer = URLSafeTimedSerializer(SECRET_KEY) try: expected = serializer.loads(token, salt='captcha-salt', max_age=300) return user_answer == expected except Exception: return False ================================================ FILE: gis_processor.py ================================================ """ gis_processor.py - Spatial Calculation Module for India LIMS Uses Shapely for all GIS heavy lifting: area calculation, validation, and spatial operations. """ import math import os import json from shapely.geometry import Polygon, mapping, shape from shapely.validation import make_valid _india_boundary_shape = None def get_india_boundary_shape(): """Load and cache the India boundary GeoJSON. Loads once at startup.""" global _india_boundary_shape if _india_boundary_shape is None: try: base_dir = os.path.dirname(os.path.abspath(__file__)) geojson_path = os.path.join(base_dir, 'static', 'data', 'india-boundary.geojson') with open(geojson_path, 'r', encoding='utf-8') as f: data = json.load(f) if 'features' in data and len(data['features']) > 0: _india_boundary_shape = shape(data['features'][0]['geometry']) else: _india_boundary_shape = shape(data) except Exception as e: print(f"Warning: India boundary not loaded: {e}") return _india_boundary_shape # Preload at module import time try: get_india_boundary_shape() except Exception: pass # Will load on first request if fails at startup # --- Constants for Unit Conversion --- # 1 Hectare = 2.47105 Acres # 1 Hectare = 100.00065 Guntha (standardized) # 1 Hectare = 107639.104 Sq. Ft. HECTARE_TO_ACRE = 2.47105 HECTARE_TO_GUNTHA = 100.00065 HECTARE_TO_SQFT = 107639.104 # Bigha varies significantly by state. Using Madhya Pradesh standard: # 1 Bigha ≈ 0.2529 Hectares in MP HECTARE_TO_BIGHA_MP = 3.9537 # Assam (Northeast India) Land Units: # 1 Assam Bigha = 14,400 sq ft = 1,337.804 sq meters ≈ 0.13378 Ha # 1 Lecha = 144 sq ft = 1/100 Assam Bigha HECTARE_TO_BIGHA_ASSAM = 7.4752 # 1 Ha = 7.4752 Assam Bigha HECTARE_TO_LECHA_ASSAM = 747.52 # 1 Ha = 747.52 Lecha # WGS84 Authalic Radius (equal-area sphere radius for area calculations) WGS84_AUTHALIC_RADIUS = 6371007.180918 # meters def calculate_area(geometry_dict): """ Calculate area of a polygon from a GeoJSON geometry dict. Returns area in Hectares along with local unit equivalents. Uses geodesic area calculation (WGS84 ellipsoid) for high accuracy. This is the proper method for land survey calculations. Args: geometry_dict: GeoJSON geometry object with type 'Polygon' and coordinates. Returns: dict with area in hectares, acres, guntha, sqft, and bigha. """ try: geom = shape(geometry_dict) if not geom.is_valid: geom = make_valid(geom) # Calculate geodesic area in square meters using WGS84 ellipsoid area_sq_meters = _geodesic_area_m2(geom) # Convert to hectares (1 hectare = 10,000 sq meters) area_ha = area_sq_meters / 10000.0 return { "area_ha": round(area_ha, 4), "area_acres": round(area_ha * HECTARE_TO_ACRE, 4), "area_guntha": round(area_ha * HECTARE_TO_GUNTHA, 2), "area_sqft": round(area_ha * HECTARE_TO_SQFT, 2), "area_bigha_mp": round(area_ha * HECTARE_TO_BIGHA_MP, 4), "area_bigha_assam": round(area_ha * HECTARE_TO_BIGHA_ASSAM, 2), "area_lecha_assam": round(area_ha * HECTARE_TO_LECHA_ASSAM, 0), "unit": "hectares" } except Exception as e: return {"error": f"Spatial calculation failed: {str(e)}", "area_ha": 0} def _geodesic_area_m2(geom): """ Calculate the geodesic area of a polygon on a sphere (WGS84 authalic radius). Uses the spherical excess formula: Area = R² * |Σ(Δλ * (2 + sin(φ1) + sin(φ2)))| / 2 This algorithm is based on "Some algorithms for polygons on a sphere" by Robert G. Chamberlain and William H. Duquette (NASA JPL). Accuracy: Within 0.1% for typical land parcels in India. For survey-grade accuracy (< 0.01%), use pyproj with proper UTM zones. Args: geom: A Shapely geometry object (Polygon). Returns: float: Area in square meters. """ # WGS84 authalic radius (equal-area sphere radius) # This ensures area calculations are consistent with the WGS84 ellipsoid R = WGS84_AUTHALIC_RADIUS # 6371007.180918 meters coords = list(geom.exterior.coords) if len(coords) < 4: return 0.0 # Calculate signed area using the spherical trapezoid method # Formula: A = R² * Σ[(λ2 - λ1) * (2 + sin(φ1) + sin(φ2))] / 2 # where λ = longitude (radians), φ = latitude (radians) signed_area = 0.0 for i in range(len(coords) - 1): lng1, lat1 = coords[i] lng2, lat2 = coords[i + 1] # Convert to radians lng1_rad = math.radians(lng1) lat1_rad = math.radians(lat1) lng2_rad = math.radians(lng2) lat2_rad = math.radians(lat2) # Add contribution from this edge signed_area += (lng2_rad - lng1_rad) * (2.0 + math.sin(lat1_rad) + math.sin(lat2_rad)) # Multiply by R²/2 to get area area = abs(signed_area) * (R ** 2) / 2.0 # Handle polygons with holes (subtract hole areas) for interior in geom.interiors: hole_coords = list(interior.coords) hole_area = 0.0 for i in range(len(hole_coords) - 1): lng1, lat1 = hole_coords[i] lng2, lat2 = hole_coords[i + 1] lng1_rad = math.radians(lng1) lat1_rad = math.radians(lat1) lng2_rad = math.radians(lng2) lat2_rad = math.radians(lat2) hole_area += (lng2_rad - lng1_rad) * (2.0 + math.sin(lat1_rad) + math.sin(lat2_rad)) area -= abs(hole_area) * (R ** 2) / 2.0 return max(0.0, area) # Ensure non-negative def validate_polygon(geometry_dict): """ Validate a GeoJSON polygon geometry. Checks for: valid structure, sufficient vertices, no self-intersection, and ensures the polygon is within India's bounding box. Args: geometry_dict: GeoJSON geometry object. Returns: dict with 'valid' (bool) and 'errors' (list of strings). """ errors = [] # Check structure if not isinstance(geometry_dict, dict): return {"valid": False, "errors": ["Geometry must be a dictionary."]} if geometry_dict.get("type") != "Polygon": errors.append("Geometry type must be 'Polygon'.") coords = geometry_dict.get("coordinates", []) if not coords or not coords[0]: errors.append("Polygon must have at least one ring with coordinates.") return {"valid": False, "errors": errors} ring = coords[0] # Minimum 4 points for a closed polygon (triangle + closing point) if len(ring) < 4: errors.append(f"Polygon ring must have at least 4 points (got {len(ring)}). A valid polygon requires at least 3 distinct vertices plus the closing point.") # Check that ring is closed (first point == last point) if ring[0] != ring[-1]: errors.append("Polygon ring must be closed (first coordinate must equal last coordinate).") # Try to create Shapely geometry and check validity try: geom = shape(geometry_dict) if not geom.is_valid: # Get specific reason from shapely.validation import explain_validity reason = explain_validity(geom) errors.append(f"Invalid polygon geometry: {reason}") # Check against actual official India GeoJSON boundary india_shape = get_india_boundary_shape() if india_shape is not None: # Check if the polygon is strictly within the Indian boundary # Using buffer to allow small edge/coastal tolerance (0.01 deg is ~1km) if not india_shape.buffer(0.01).contains(geom): errors.append("Polygon is located outside the borders of India. Data creation is restricted to Indian territories only.") except Exception as e: errors.append(f"Could not parse geometry or bounds: {str(e)}") return {"valid": False, "errors": errors} # Fallback/Backward compatibility checking # India bbox: lat ~6.5 to ~37.5, lng ~68.0 to ~97.5 for point in ring: lng, lat = point[0], point[1] if not (6.5 <= lat <= 37.5 and 68.0 <= lng <= 97.5): errors.append( f"Coordinate ({lng}, {lat}) is outside India's general boundaries. " "All coordinates must be within India (Lat: 6.5-37.5, Lng: 68.0-97.5)." ) break return {"valid": len(errors) == 0, "errors": errors} def check_overlap(new_geometry_dict, existing_records): """ Check if a new polygon overlaps with any existing land record polygons. Args: new_geometry_dict: GeoJSON geometry for the new parcel. existing_records: List of existing record dicts, each with 'geometry' key. Returns: dict with 'overlaps' (bool) and 'conflicting_records' (list of record IDs). """ try: new_geom = shape(new_geometry_dict) if not new_geom.is_valid: new_geom = make_valid(new_geom) conflicting = [] for record in existing_records: try: existing_geom = shape(record["geometry"]) if not existing_geom.is_valid: existing_geom = make_valid(existing_geom) if new_geom.intersects(existing_geom): # Check for actual area overlap, not just touching boundaries intersection = new_geom.intersection(existing_geom) if intersection.area > 1e-7: # Precise threshold for real-world land parcels conflicting.append({ "record_id": record.get("_id", "unknown"), "khasra_no": record.get("khasra_no", "unknown"), "overlap_area_ha": round(intersection.area, 6) }) except Exception: continue return { "overlaps": len(conflicting) > 0, "conflicting_records": conflicting } except Exception as e: return {"error": f"Overlap check failed: {str(e)}", "overlaps": False, "conflicting_records": []} def get_centroid(geometry_dict): """ Calculate the centroid of a polygon. Args: geometry_dict: GeoJSON geometry object. Returns: dict with 'lat' and 'lng', or error dict. """ try: geom = shape(geometry_dict) if not geom.is_valid: geom = make_valid(geom) centroid = geom.centroid return {"lat": round(centroid.y, 6), "lng": round(centroid.x, 6)} except Exception as e: return {"error": f"Centroid calculation failed: {str(e)}"} def geojson_to_wkt(geometry_dict): """ Convert a GeoJSON geometry to Well-Known Text (WKT) format. Useful for interoperability with other GIS systems. Args: geometry_dict: GeoJSON geometry object. Returns: str: WKT representation of the geometry. """ try: geom = shape(geometry_dict) return geom.wkt except Exception as e: return f"ERROR: {str(e)}" def calculate_perimeter(geometry_dict): """ Calculate the perimeter of a polygon in meters. Args: geometry_dict: GeoJSON geometry object. Returns: dict with perimeter in meters and kilometers. """ try: geom = shape(geometry_dict) if not geom.is_valid: geom = make_valid(geom) def haversine_distance(p1, p2): """True spherical distance between two points in meters.""" lon1, lat1 = math.radians(p1[0]), math.radians(p1[1]) lon2, lat2 = math.radians(p2[0]), math.radians(p2[1]) dlon, dlat = lon2 - lon1, lat2 - lat1 a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2 c = 2 * math.asin(math.sqrt(a)) return c * WGS84_AUTHALIC_RADIUS coords = list(geom.exterior.coords) perimeter_m = 0.0 for i in range(len(coords) - 1): perimeter_m += haversine_distance(coords[i], coords[i + 1]) # Add perimeters of any holes for interior in geom.interiors: hole_coords = list(interior.coords) for i in range(len(hole_coords) - 1): perimeter_m += haversine_distance(hole_coords[i], hole_coords[i + 1]) return { "perimeter_m": round(perimeter_m, 2), "perimeter_km": round(perimeter_m / 1000, 4) } except Exception as e: return {"error": f"Perimeter calculation failed: {str(e)}"} ================================================ FILE: inno_setup.iss ================================================ ; Script generated by the Inno Setup Script Wizard. ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "LIMS" #define MyAppVersion "1.0" #define MyAppPublisher "Premithiews" #define MyAppExeName "LIMS.exe" [Setup] ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) AppId={{5A1B2C3D-4E5F-6A7B-8C9D-0E1F2A3B4C5D} AppName={#MyAppName} AppVersion={#MyAppVersion} AppPublisher={#MyAppPublisher} DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes ; Uncomment the following line to run in non administrative install mode (install for current user only.) ;PrivilegesRequired=lowest OutputDir=installer_output OutputBaseFilename=LIMS_Setup Compression=lzma SolidCompression=yes WizardStyle=modern [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked [Files] ; The actual executable Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent ================================================ FILE: render.yaml ================================================ services: - type: web name: lims-website env: python buildCommand: pip install -r requirements.txt startCommand: PYTHONPATH=. gunicorn app:app envVars: - key: PYTHON_VERSION value: 3.11 - key: LIMS_MODE value: production ================================================ FILE: report_generator.py ================================================ """ report_generator.py - Document Generation Module for India LIMS Generates single-page PDF Property Cards and Excel Village Ledgers. """ import os import io import json import uuid import struct import base64 import tempfile from datetime import datetime from utils import resource_path try: from fpdf import FPDF except ImportError: FPDF = None try: import pandas as pd except ImportError: pd = None try: import qrcode except ImportError: qrcode = None # ── Unit Conversion Constants ───────────────────────────────────────────────── HA_TO_ACRE = 2.47105 HA_TO_BIGHA_ASSAM = 7.4752 # 1 Assam Bigha = 14,400 sq ft HA_TO_LECHA_ASSAM = 747.52 # 1 Lecha = 1/100 Assam Bigha def _fmt_inr(value): """Format a number as Indian Rupees with commas.""" try: v = int(float(value)) s = str(v) if len(s) > 3: last3 = s[-3:] rest = s[:-3] parts = [] while len(rest) > 2: parts.append(rest[-2:]) rest = rest[:-2] if rest: parts.append(rest) parts.reverse() return ','.join(parts) + ',' + last3 return s except Exception: return str(value) def generate_property_card_pdf(record, map_image_base64=None): """ Generate a clean, single-page A4 PDF Property Card. Layout (all on one page): • Header with QR code • 2-column property info table • Prominent map section (90mm) • Polygon coordinates table • Footer """ if FPDF is None: raise ImportError("fpdf2 is required. Install with: pip install fpdf2") pdf = PropertyCardPDF() pdf.set_auto_page_break(auto=False) # We handle layout manually pdf.add_page() # ── Extract Data ───────────────────────────────────────────────────────── loc = record.get("location", {}) attrs = record.get("attributes", {}) owner = record.get("owner", {}) mutations = record.get("mutation_history", []) geometry = record.get("geometry", {}) area_ha = float(attrs.get("area_ha", 0) or 0) area_acres = round(area_ha * HA_TO_ACRE, 2) area_bigha = round(area_ha * HA_TO_BIGHA_ASSAM, 2) area_lecha = int(round(area_ha * HA_TO_LECHA_ASSAM)) try: circle_rate = float(attrs.get("circle_rate_inr", 0) or 0) land_use = attrs.get("land_use", "Agricultural") multipliers = { 'Commercial': 2.5, 'Industrial': 1.8, 'Residential': 1.5, 'Agricultural': 1.0, 'Government': 1.2, 'Forest': 0.8, 'Wasteland': 0.5 } multiplier = multipliers.get(land_use, 1.0) estimated_value = area_ha * circle_rate * multiplier except Exception: circle_rate = 0 estimated_value = 0 state = loc.get("state", "N/A") district = loc.get("district", "N/A") village = loc.get("village", "N/A") # ── QR Code (top-right) ────────────────────────────────────────────────── qr_path = None if qrcode is not None: try: qr = qrcode.QRCode(version=1, box_size=4, border=1) qr.add_data(record.get("ulpin", "N/A")) qr.make(fit=True) qr_img = qr.make_image(fill_color="black", back_color="white") buf = io.BytesIO() qr_img.save(buf, format="PNG") buf.seek(0) qr_path = os.path.join(tempfile.gettempdir(), f"qr_{uuid.uuid4().hex[:8]}.png") with open(qr_path, "wb") as f: f.write(buf.getvalue()) pdf.image(qr_path, x=183, y=10, w=18, h=18) except Exception: pass # ── Header ─────────────────────────────────────────────────────────────── pdf.set_y(10) pdf.set_font("Helvetica", "B", 14) pdf.cell(170, 7, "LIMS - Property Card (Khasra Patta)", new_x="LMARGIN", new_y="NEXT", align="C") pdf.set_font("Helvetica", "", 8) pdf.cell(170, 4, "Land Information Management System | Academic Prototype", new_x="LMARGIN", new_y="NEXT", align="C") pdf.set_font("Helvetica", "B", 9) pdf.cell(170, 5, f"{village}, {district}, {state}", new_x="LMARGIN", new_y="NEXT", align="C") pdf.ln(1) pdf.set_draw_color(30, 64, 150) pdf.set_line_width(0.6) y_div = pdf.get_y() pdf.line(10, y_div, 200, y_div) pdf.ln(3) # ── Two-Column Info Table ──────────────────────────────────────────────── Y_TABLE = pdf.get_y() RH = 6.5 # row height mm LW = 38 # label cell width VW = 54 # value cell width (total col = 92mm) GAP = 6 # gap between cols def draw_row(x, y, label, value, fill=True): pdf.set_xy(x, y) pdf.set_font("Helvetica", "B", 7.5) pdf.set_fill_color(230, 237, 255) pdf.cell(LW, RH, f" {label}", border=1, fill=fill) pdf.set_font("Helvetica", "", 7.5) pdf.set_fill_color(255, 255, 255) val_str = str(value)[:42] pdf.cell(VW, RH, f" {val_str}", border=1, fill=True, new_x="RIGHT", new_y="TOP") left = [ ("ULPIN", record.get("ulpin", "N/A")), ("Khasra No.", record.get("khasra_no", "N/A")), ("Khata No.", record.get("khata_no", "N/A")), ("Land Use", attrs.get("land_use", "N/A")), ("State", state), ("District", district), ("Village/Ward", village), ("Share (%)", f"{owner.get('share_pct', 'N/A')}%"), ] right = [ ("Area (Ha)", f"{area_ha} Ha"), ("Area (Acres)", f"{area_acres} Ac"), ("Area (Bigha)", f"{area_bigha} Bigha"), ("Circle Rate", f"Rs. {_fmt_inr(int(circle_rate))}/Ha" if circle_rate else "N/A"), ("Est. Value", f"Rs. {_fmt_inr(int(estimated_value))}" if estimated_value else "N/A"), ("Centroid", f"{attrs.get('centroid', {}).get('lat', 'N/A')}, {attrs.get('centroid', {}).get('lng', 'N/A')}"), ("Perimeter", f"{int(attrs.get('perimeter_m', 0))} meters"), ("Owner", owner.get("name", "N/A")), ] for i, (lbl, val) in enumerate(left): draw_row(10, Y_TABLE + i * RH, lbl, val) for i, (lbl, val) in enumerate(right): draw_row(10 + LW + VW + GAP, Y_TABLE + i * RH, lbl, val) # Extra row for Mutations (full width below the two columns) y_mut = Y_TABLE + max(len(left), len(right)) * RH draw_row(10, y_mut, "Mutation History", f"{len(mutations)} transaction(s) recorded on this parcel") pdf.ln(0) y_after_table = y_mut + RH + 3 # ── Divider ─────────────────────────────────────────────────────────────── pdf.set_draw_color(30, 64, 150) pdf.set_line_width(0.4) pdf.line(10, y_after_table, 200, y_after_table) # ── Map Section ─────────────────────────────────────────────────────────── MAP_LABEL_Y = y_after_table + 2 MAP_Y = MAP_LABEL_Y + 6 MAP_H = 118 # mm — generous height now that coords table is removed pdf.set_font("Helvetica", "B", 9) pdf.set_xy(10, MAP_LABEL_Y) pdf.cell(0, 5, "PARCEL MAP", new_x="LMARGIN", new_y="NEXT") if map_image_base64: try: if "," in map_image_base64: map_image_base64 = map_image_base64.split(",")[1] img_data = base64.b64decode(map_image_base64) tmp_map = os.path.join(tempfile.gettempdir(), f"map_{record.get('ulpin','x')}_{uuid.uuid4().hex[:6]}.png") with open(tmp_map, "wb") as f: f.write(img_data) # Read actual PNG dimensions for proportional placement img_w, img_h = 800, 450 # fallback defaults try: with open(tmp_map, "rb") as f: f.read(8) chunk = f.read(17) if len(chunk) == 17: img_w = struct.unpack(">I", chunk[8:12])[0] img_h = struct.unpack(">I", chunk[12:16])[0] except Exception: pass # Scale to fill 190mm width, but cap at MAP_H height max_w = 190.0 scale = min(max_w / img_w, MAP_H / (img_h * (210 / img_w)) if img_w else 1) draw_w = min(max_w, img_w * (max_w / img_w)) draw_h = img_h * (draw_w / img_w) if draw_h > MAP_H: draw_h = MAP_H draw_w = img_w * (draw_h / img_h) x_pos = (210 - draw_w) / 2 pdf.image(tmp_map, x=x_pos, y=MAP_Y, w=draw_w, h=draw_h) try: os.remove(tmp_map) except Exception: pass except Exception as e: pdf.set_font("Helvetica", "I", 9) pdf.set_xy(10, MAP_Y + 5) pdf.cell(0, 6, f"Map not available: {str(e)[:60]}", new_x="LMARGIN", new_y="NEXT") else: pdf.set_font("Helvetica", "I", 9) pdf.set_xy(10, MAP_Y + 5) pdf.cell(0, 6, "Map image not captured.", new_x="LMARGIN", new_y="NEXT") y_after_map = MAP_Y + MAP_H + 3 # ── Page 2: Mutation Ledger (History) ────────────────────────────────── if mutations: pdf.add_page() pdf.set_y(15) pdf.set_font("Helvetica", "B", 12) pdf.cell(0, 7, "Ownership Mutation Ledger (History of Transactions)", new_x="LMARGIN", new_y="NEXT", align="C") pdf.set_font("Helvetica", "I", 8) pdf.cell(0, 4, f"Detailed record of ownership changes for ULPIN: {record.get('ulpin', 'N/A')}", new_x="LMARGIN", new_y="NEXT", align="C") pdf.ln(5) # Table Header pdf.set_font("Helvetica", "B", 8) pdf.set_fill_color(240, 240, 240) col_widths = [35, 60, 30, 30, 35] headers = ["Date", "Previous Owner", "Type", "Share", "Reference"] for i, h in enumerate(headers): pdf.cell(col_widths[i], 8, h, border=1, fill=True, align="C") pdf.ln(8) # Table Rows pdf.set_font("Helvetica", "", 7.5) for mut in mutations: pdf.cell(col_widths[0], 7, f" {mut.get('mutation_date', 'N/A')}", border=1) pdf.cell(col_widths[1], 7, f" {mut.get('previous_owner', 'N/A')[:38]}", border=1) pdf.cell(col_widths[2], 7, f" {mut.get('mutation_type', 'Sale Deed')}", border=1) pdf.cell(col_widths[3], 7, f" {mut.get('previous_share_pct', 100)}%", border=1, align="C") pdf.cell(col_widths[4], 7, f" {mut.get('mutation_ref', 'N/A')[:22]}", border=1) pdf.ln(7) pdf.ln(10) pdf.set_font("Helvetica", "I", 8) pdf.multi_cell(0, 5, "Note: This ledger is an archived record of past ownership. The first page of this document reflects the current state of the land record as per the digital database.") # ── Footer ─────────────────────────────────────────────────────────────── pdf.set_y(281) # 297 - 16mm from bottom pdf.set_draw_color(30, 64, 150) pdf.set_line_width(0.3) pdf.line(10, pdf.get_y(), 200, pdf.get_y()) pdf.ln(2) gen_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") pdf.set_font("Helvetica", "I", 7) doc_id = f"PC-{record.get('ulpin','N/A')}-{datetime.now().strftime('%Y%m%d%H%M')}" pdf.cell(0, 4, f"Generated: {gen_time} | Document ID: {doc_id} | LIMS Academic Prototype", new_x="LMARGIN", new_y="NEXT", align="C") pdf.set_font("Helvetica", "B", 7) pdf.cell(0, 4, "Computer-generated document. Scan QR code for digital verification.", new_x="LMARGIN", new_y="NEXT", align="C") # Cleanup QR if qr_path: try: os.remove(qr_path) except Exception: pass # ── Output ──────────────────────────────────────────────────────────────── try: return bytes(pdf.output()) except Exception: output = pdf.output() if isinstance(output, (bytes, bytearray)): return bytes(output) return output.encode("latin-1") if isinstance(output, str) else bytes(output) class PropertyCardPDF(FPDF): """Custom FPDF with thin blue top border.""" def header(self): self.set_draw_color(30, 64, 150) self.set_line_width(1.2) self.line(5, 5, 205, 5) self.set_line_width(0.3) self.set_draw_color(0, 0, 0) def footer(self): pass # Footer handled manually above def generate_village_excel(records, village_name="All Villages"): """ Generate a formatted Excel village ledger from land records. Includes Bigha and Lecha columns. """ if pd is None: raise ImportError("pandas and openpyxl are required.") flat_rows = [] for rec in records: loc = rec.get("location", {}) attrs = rec.get("attributes", {}) owner = rec.get("owner", {}) muts = rec.get("mutation_history", []) last_mut = muts[-1] if muts else {} area_ha = float(attrs.get("area_ha", 0) or 0) circle_rate = float(attrs.get("circle_rate_inr", 0) or 0) land_use = attrs.get("land_use", "Agricultural") multipliers = { 'Commercial': 2.5, 'Industrial': 1.8, 'Residential': 1.5, 'Agricultural': 1.0, 'Government': 1.2, 'Forest': 0.8, 'Wasteland': 0.5 } multiplier = multipliers.get(land_use, 1.0) estimated_value = area_ha * circle_rate * multiplier flat_rows.append({ "ULPIN": rec.get("ulpin", ""), "Khasra No.": rec.get("khasra_no", ""), "Khata No.": rec.get("khata_no", ""), "State": loc.get("state", ""), "District": loc.get("district", ""), "Village": loc.get("village", ""), "Area (Ha)": area_ha, "Area (Bigha - Assam)": round(area_ha * HA_TO_BIGHA_ASSAM, 2), "Area (Lecha - Assam)": int(round(area_ha * HA_TO_LECHA_ASSAM)), "Area (Acres)": round(area_ha * HA_TO_ACRE, 2), "Land Use": attrs.get("land_use", ""), "Circle Rate (INR/Ha)": circle_rate, "Multiplier": multiplier, "Estimated Value (INR)": estimated_value, "Owner Name": owner.get("name", ""), "Share (%)": owner.get("share_pct", 0), "Aadhaar (Masked)": owner.get("aadhaar_mask", ""), "Total Mutations": len(muts), "Last Mutation Date": last_mut.get("mutation_date", ""), "Last Mutation Type": last_mut.get("mutation_type", ""), "Record ID": rec.get("_id", ""), }) df = pd.DataFrame(flat_rows) output = io.BytesIO() with pd.ExcelWriter(output, engine="openpyxl") as writer: df.to_excel(writer, index=False, sheet_name="Village Ledger") try: ws = writer.sheets["Village Ledger"] for idx, col in enumerate(df.columns): max_len = max( df[col].astype(str).map(len).max() if len(df) > 0 else 0, len(col) ) col_letter = chr(65 + idx) if idx < 26 else chr(64 + idx // 26) + chr(65 + idx % 26) ws.column_dimensions[col_letter].width = min(max_len + 3, 40) except Exception: pass output.seek(0) return output.getvalue() if __name__ == "__main__": sample = { "_id": "test-001", "ulpin": "18011010001001", "khasra_no": "42/B", "khata_no": "KH-07", "location": {"state": "Assam", "district": "Kamrup Metropolitan", "village": "Guwahati Ward 12"}, "attributes": {"area_ha": 1.34, "land_use": "Agricultural", "circle_rate_inr": 85000}, "owner": {"name": "Ramesh Kumar", "share_pct": 100, "aadhaar_mask": "XXXX-XXXX-7890"}, "geometry": {"type": "Polygon", "coordinates": [[[91.76, 26.12], [91.765, 26.12], [91.765, 26.125], [91.76, 26.125], [91.76, 26.12]]]}, "mutation_history": [] } b = generate_property_card_pdf(sample) print(f"PDF generated: {len(b)} bytes") ================================================ FILE: requirements-dev.txt ================================================ pytest>=7.0 flask>=3.0 pymongo>=4.0 certifi python-dotenv ================================================ FILE: requirements-windows.txt ================================================ # Base Web Requirements -r requirements.txt # Windows Desktop Specific pywebview==4.4.1 pythonnet==3.0.3 pyinstaller==6.3.0 pillow==10.1.0 ================================================ FILE: requirements.txt ================================================ Flask==3.0.0 pymongo[srv]==4.6.1 python-dotenv==1.0.0 reportlab==4.0.8 pandas openpyxl==3.1.2 gunicorn==21.2.0 werkzeug==3.0.1 jinja2==3.1.2 itsdangerous==2.1.2 click==8.1.7 blinker==1.7.0 certifi==2023.11.17 fpdf2==2.7.7 qrcode==7.4.2 ================================================ FILE: routes/__init__.py ================================================ from .pages import pages_bp from .auth import auth_bp from .records import records_bp from .users import users_bp from .gis import gis_bp from .documents import documents_bp from .feedback import feedback_bp from .utils import utils_bp ================================================ FILE: routes/auth.py ================================================ from datetime import datetime from flask import Blueprint, request, jsonify, session from werkzeug.security import check_password_hash from core import generate_captcha, verify_captcha_logic, load_users, save_users auth_bp = Blueprint('auth', __name__) @auth_bp.route("/api/captcha", methods=["GET"]) def get_captcha(): question, token = generate_captcha() return jsonify({"question": question, "token": token}) @auth_bp.route("/api/verify-captcha", methods=["POST"]) def verify_captcha(): data = request.get_json() or {} user_answer = str(data.get("answer", "")).strip() token = str(data.get("token", "")).strip() if verify_captcha_logic(token, user_answer): session.permanent = True session["role"] = "viewer" session["username"] = "Viewer" return jsonify({"success": True, "redirect": "/viewer"}) else: new_question, new_token = generate_captcha() return jsonify({"success": False, "message": "Incorrect answer or expired. Please try again.", "new_question": new_question, "new_token": new_token}), 400 @auth_bp.route("/api/login", methods=["POST"]) def admin_login(): data = request.get_json() or {} username = data.get("username", "").strip() password = data.get("password", "") if not username or not password: return jsonify({"error": "Username and password are required."}), 400 users = load_users() user = next((u for u in users if u["username"] == username), None) if user and check_password_hash(user["password_hash"], password): session.permanent = True role = (user.get("role") or "Officer").lower() session["role"] = role session["username"] = username session["admin_id"] = user.get("user_id", "") user["last_login"] = datetime.now().isoformat() + "Z" save_users(users) redirect_url = "/admin" if role in ("admin", "superadmin") else "/viewer" return jsonify({"success": True, "redirect": redirect_url}) else: return jsonify({"error": "Invalid username or password."}), 401 @auth_bp.route("/api/logout", methods=["POST"]) def logout(): session.clear() return jsonify({"success": True, "redirect": "/login"}) @auth_bp.route("/api/session-info", methods=["GET"]) def session_info(): return jsonify({ "role": session.get("role", None), "username": session.get("username", None), "is_authenticated": session.get("role") is not None }) @auth_bp.route("/api/forgot", methods=["GET", "POST"]) def forgot_password(): """Handle password recovery instructions.""" return jsonify({ "success": True, "instructions": "To recover your password, please contact the District Revenue Officer or System Administrator at support@india-lims.gov.in. Provide your Employee ID and Office Location for verification." }) ================================================ FILE: routes/documents.py ================================================ import io from datetime import datetime from flask import Blueprint, request, jsonify, send_file from core import load_records, viewer_or_admin_required, admin_required documents_bp = Blueprint('documents', __name__) @documents_bp.route("/api/print-card/", methods=["GET", "POST"]) @viewer_or_admin_required def print_property_card(ulpin): records = load_records() record = next((r for r in records if r.get("ulpin") == ulpin), None) if not record: return jsonify({"error": "Not found."}), 404 map_image = None if request.method == "POST": map_image = (request.get_json() or {}).get("map_image") try: from report_generator import generate_property_card_pdf pdf_bytes = generate_property_card_pdf(record, map_image_base64=map_image) return send_file(io.BytesIO(pdf_bytes), mimetype="application/pdf", as_attachment=True, download_name=f"Card_{ulpin}.pdf") except Exception as e: return jsonify({"error": str(e)}), 500 @documents_bp.route("/api/export-village", methods=["GET"]) @admin_required def export_village_ledger(): records = load_records() village = request.args.get("village", "").strip() if village: records = [r for r in records if r.get("location", {}).get("village", "").lower() == village.lower()] if not records: return jsonify({"error": "No records."}), 404 try: from report_generator import generate_village_excel excel_bytes = generate_village_excel(records, village_name=village or "All") return send_file(io.BytesIO(excel_bytes), mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", as_attachment=True, download_name="Ledger.xlsx") except Exception as e: return jsonify({"error": str(e)}), 500 ================================================ FILE: routes/feedback.py ================================================ import uuid import os import json from datetime import datetime from flask import Blueprint, request, jsonify, session from core import ( load_feedback, save_feedback, load_records, admin_required, viewer_or_admin_required, _apply_filters_to_records, audit_collection, DATA_DIR, _calculate_estimated_value ) feedback_bp = Blueprint('feedback', __name__) @feedback_bp.route("/api/feedback", methods=["GET"]) @admin_required def get_feedback(): """Admin-only: Fetch all feedback submissions.""" return jsonify(load_feedback()) @feedback_bp.route("/api/feedback", methods=["POST"]) @viewer_or_admin_required def submit_feedback(): """Submit feedback or issue reports.""" data = request.get_json() or {} email = data.get("email", "").strip() message = data.get("message", "").strip() if not email or not message: return jsonify({"error": "Required fields missing."}), 400 entry = { "id": str(uuid.uuid4()), "email": email, "type": data.get("type", "General"), "message": message, "timestamp": datetime.now().isoformat(), "status": "New" } fb = load_feedback() fb.append(entry) save_feedback(fb) return jsonify({"success": True}) @feedback_bp.route("/api/dashboard", methods=["GET"]) @admin_required def dashboard_analytics(): """Compute heavy aggregation for the admin dashboard charts and KPIs.""" records = load_records() params = {k: request.args.get(k, "") for k in ["state", "district", "village", "land_use", "search"]} filtered = _apply_filters_to_records(records, params) total_area = 0.0 total_value = 0.0 total_mutations = 0 land_use_stats = {} district_stats = {} top_parcel = None top_parcel_value = -1 for rec in filtered: loc = rec.get("location", {}) attrs = rec.get("attributes", {}) area = float(attrs.get("area_ha", 0) or 0) rate = float(attrs.get("circle_rate_inr", 0) or 0) lu = attrs.get("land_use", "Agricultural") value = _calculate_estimated_value(area, rate, lu) total_area += area total_value += value muts = rec.get("mutation_history", []) or [] total_mutations += len(muts) # Land use distribution lu = attrs.get("land_use", "Unknown") if lu not in land_use_stats: land_use_stats[lu] = {"count": 0, "area": 0.0} land_use_stats[lu]["count"] += 1 land_use_stats[lu]["area"] += area # District stats d = loc.get("district", "Unknown") if d not in district_stats: district_stats[d] = {"count": 0, "area": 0.0, "value": 0.0} district_stats[d]["count"] += 1 district_stats[d]["area"] += area district_stats[d]["value"] += value if value > top_parcel_value: top_parcel_value = value top_parcel = { "khasra_no": rec.get("khasra_no"), "ulpin": rec.get("ulpin"), "village": loc.get("village"), "district": d, "land_use": attrs.get("land_use", "N/A"), "area_ha": round(area, 2), "estimated_value": round(value, 0) } sorted_districts = sorted(district_stats.items(), key=lambda x: x[1]["value"], reverse=True)[:6] all_mutations = [] for rec in filtered: for m in rec.get("mutation_history", []) or []: all_mutations.append({ "khasra_no": rec.get("khasra_no"), "district": rec.get("location", {}).get("district"), "previous_owner": m.get("previous_owner"), "mutation_date": m.get("mutation_date"), "mutation_type": m.get("mutation_type", "Mutation") }) all_mutations.sort(key=lambda x: str(x.get("mutation_date", "")), reverse=True) return jsonify({ "kpis": { "total_parcels": len(filtered), "total_area": round(total_area, 2), "estimated_value": round(total_value, 0), "total_mutations": total_mutations }, "land_use_distribution": land_use_stats, "district_overview": [{"name": d, **s} for d, s in sorted_districts], "top_parcel": top_parcel, "recent_mutations": all_mutations[:6] }) @feedback_bp.route('/api/audit', methods=['GET']) @admin_required def list_audit(): """Fetch system audit logs.""" limit = min(100, max(1, int(request.args.get('limit', 50)))) if audit_collection is not None: entries = list(audit_collection.find({}, {'_id': 0}).sort('timestamp', -1).limit(limit)) else: audit_file = os.path.join(DATA_DIR, 'audit.json') if os.path.exists(audit_file): with open(audit_file, 'r', encoding='utf-8') as f: entries = json.load(f) entries = sorted(entries, key=lambda x: x.get('timestamp', ''), reverse=True)[:limit] else: entries = [] return jsonify(entries) @feedback_bp.route("/api/feedback/", methods=["DELETE"]) @admin_required def delete_feedback(feedback_id): fb = load_feedback() new_fb = [entry for entry in fb if entry.get("id") != feedback_id] if len(new_fb) == len(fb): return jsonify({"error": "Feedback not found."}), 404 save_feedback(new_fb) return jsonify({"success": True}) @feedback_bp.route("/api/feedback//status", methods=["PUT"]) @admin_required def update_feedback_status(feedback_id): """Mark feedback as reviewed or resolved.""" data = request.get_json() or {} new_status = data.get("status", "Reviewed") fb = load_feedback() entry = next((e for e in fb if e.get("id") == feedback_id), None) if not entry: return jsonify({"error": "Feedback not found."}), 404 entry["status"] = new_status entry["reviewed_at"] = datetime.now().isoformat() entry["reviewed_by"] = session.get("username", "admin") save_feedback(fb) return jsonify({"success": True, "status": new_status}) ================================================ FILE: routes/gis.py ================================================ import os import json from flask import Blueprint, request, jsonify, current_app from urllib.parse import urlencode from urllib.request import Request, urlopen from core import role_required gis_bp = Blueprint('gis', __name__) @gis_bp.route("/api/boundary", methods=["GET"]) def get_india_boundary(): geojson_path = os.path.join(current_app.root_path, "static", "data", "india-boundary.geojson") try: with open(geojson_path, "r", encoding="utf-8") as f: data = json.load(f) return jsonify(data) except FileNotFoundError: return jsonify({"error": "Boundary data not found."}), 404 @gis_bp.route("/api/calculate-area", methods=["POST"]) @role_required("admin", "superadmin", "officer") def api_calculate_area(): data = request.get_json() or {} geometry = data.get("geometry") if not geometry: return jsonify({"error": "Geometry required."}), 400 from gis_processor import calculate_area, calculate_perimeter, get_centroid return jsonify({ "area": calculate_area(geometry), "perimeter": calculate_perimeter(geometry), "centroid": get_centroid(geometry) }) @gis_bp.route("/api/validate-geometry", methods=["POST"]) @role_required("admin", "superadmin", "officer") def api_validate_geometry(): """Validate a GeoJSON polygon geometry.""" data = request.get_json() or {} geometry = data.get("geometry") if not geometry: return jsonify({"error": "Geometry is required."}), 400 from gis_processor import validate_polygon return jsonify(validate_polygon(geometry)) @gis_bp.route("/api/location-from-coords", methods=["GET"]) @role_required("admin", "superadmin", "officer") def location_from_coordinates(): lat = request.args.get("lat", type=float) lng = request.args.get("lng", type=float) if lat is None or lng is None: return jsonify({"error": "lat and lng required."}), 400 query = urlencode({"lat": f"{lat:.6f}", "lon": f"{lng:.6f}", "format": "jsonv2", "addressdetails": 1}) url = f"https://nominatim.openstreetmap.org/reverse?{query}" try: req = Request(url, headers={"User-Agent": "LIMS/1.0"}) with urlopen(req, timeout=8) as response: data = json.loads(response.read().decode("utf-8")) addr = data.get("address", {}) # Robust detection for Indian administrative levels state = addr.get("state", "") # District can be in several fields district = addr.get("state_district") or addr.get("district") or addr.get("county") or addr.get("city") or "" # Clean up district names (e.g. "Indore District" -> "Indore") district = district.replace(" District", "").replace(" Zila", "").replace(" Dist.", "").strip() # Village/Ward/Locality can be in many fields in India village = ( addr.get("village") or addr.get("suburb") or addr.get("neighbourhood") or addr.get("hamlet") or addr.get("town") or addr.get("city_district") or addr.get("locality") or addr.get("residential") or "" ) return jsonify({ "success": True, "state": state, "district": district, "village": village, "display_name": data.get("display_name", "") }) except Exception as e: return jsonify({"error": str(e)}), 502 @gis_bp.route("/api/location-catalog", methods=["GET"]) def get_location_catalog(): """Return the master hierarchy of States, Districts, and Villages.""" # This would typically come from a DB, but we'll use a robust static catalog for India catalog = { "Madhya Pradesh": { "Indore": ["Bicholi Mardana", "Kanadia", "Hatod", "Rau", "Mhow"], "Bhopal": ["Bairagarh", "Huzur", "Berasia", "Misrod", "Arera"], "Jabalpur": ["Panagar", "Sihora", "Patan", "Shahpura"], "Gwalior": ["Dabra", "Bhitarwar", "Chinore"], "Ujjain": ["Nagda", "Mahidpur", "Tarana", "Khachrod"] }, "Maharashtra": { "Mumbai": ["Colaba", "Dadar", "Andheri", "Borivali", "Kurla"], "Pune": ["Haveli", "Khed", "Shirur", "Baramati", "Indapur"], "Nagpur": ["Kamptee", "Ramtek", "Katol", "Saoner"], "Nashik": ["Malegaon", "Sinnar", "Yeola", "Igatpuri"] }, "Uttar Pradesh": { "Lucknow": ["Bakshi Ka Talab", "Malihabad", "Mohanlalganj"], "Kanpur": ["Bilhaur", "Ghatampur"], "Varanasi": ["Pindra", "Rajatalab"], "Agra": ["Etmadpur", "Fatehabad", "Kheragarh"] }, "Delhi": { "New Delhi": ["Connaught Place", "Chanakyapuri"], "South Delhi": ["Saket", "Hauz Khas", "Mehrauli"], "North Delhi": ["Model Town", "Narela"], "East Delhi": ["Preet Vihar", "Mayur Vihar"] } } return jsonify(catalog) ================================================ FILE: routes/pages.py ================================================ from flask import Blueprint, render_template, redirect, url_for, session, request, jsonify from core import generate_captcha pages_bp = Blueprint('pages', __name__) @pages_bp.route("/") def index(): return redirect(url_for("pages.login_page")) @pages_bp.route("/login") def login_page(): role = (session.get("role") or "").lower() if role in ("admin", "superadmin"): return redirect(url_for("pages.admin_dashboard")) elif role == "viewer": return redirect(url_for("pages.viewer_page")) captcha_question, captcha_token = generate_captcha() return render_template("login.html", captcha_question=captcha_question, captcha_token=captcha_token) @pages_bp.route("/admin") def admin_dashboard(): role = (session.get("role") or "").lower() if role not in ("admin", "superadmin"): return redirect(url_for("pages.login_page")) return render_template("admin_dashboard.html", username=session.get("username", "Admin")) @pages_bp.route("/viewer") def viewer_page(): role = (session.get("role") or "").lower() if role not in ("admin", "superadmin", "viewer"): return redirect(url_for("pages.login_page")) return render_template("public_viewer_v2.html") ================================================ FILE: routes/records.py ================================================ import uuid import random from datetime import datetime from flask import Blueprint, request, jsonify, session from core import ( load_records, save_records, viewer_or_admin_required, role_required, _strip_b64_from_list, _mask_owner_for_viewer, _log_audit, _generate_ulpin, _update_nested, _apply_filters_to_records ) records_bp = Blueprint('records', __name__) def generate_ulpin(state_name, district_name): """Generate a unique 14-digit ULPIN.""" state_code = str(abs(hash(state_name)) % 90 + 10) dist_code = str(abs(hash(district_name)) % 90 + 10) parcel_code = ''.join([str(random.randint(0, 9)) for _ in range(10)]) return f"{state_code}{dist_code}{parcel_code}" @records_bp.route("/api/records", methods=["GET"]) @viewer_or_admin_required def get_records(): """Fetch all land records with role-based masking and filtering.""" records = load_records() records = _strip_b64_from_list(records) role = (session.get("role") or "").lower() # Exclude soft-deleted records for non-admins if role not in ("admin", "superadmin"): records = [r for r in records if not r.get("deleted")] # Mask owner details for public viewers if role == "viewer": records = [_mask_owner_for_viewer(rec) for rec in records] return jsonify(records) @records_bp.route("/api/records/", methods=["GET"]) @viewer_or_admin_required def get_record(record_id): """Fetch a single land record by its ID.""" records = load_records() record = next((r for r in records if r["_id"] == record_id), None) if not record: return jsonify({"error": "Record not found."}), 404 role = (session.get("role") or "").lower() if record.get("deleted") and role not in ("admin", "superadmin"): return jsonify({"error": "Record not found."}), 404 if role == "viewer": record = _mask_owner_for_viewer(record) return jsonify(record) @records_bp.route("/api/records/search", methods=["GET"]) @viewer_or_admin_required def search_records(): """Search records using a global text query.""" query = request.args.get("q", "").strip().lower() if not query: return jsonify({"error": "Search query parameter 'q' is required."}), 400 records = load_records() results = _apply_filters_to_records(records, {"search": query}) role = (session.get("role") or "").lower() if role not in ("admin", "superadmin"): results = [r for r in results if not r.get("deleted")] if role == "viewer": results = [_mask_owner_for_viewer(rec) for rec in results] return jsonify(results) @records_bp.route("/api/records/filter", methods=["GET"]) @viewer_or_admin_required def filter_records(): """Advanced filtering of records by location and attributes.""" records = load_records() params = {k: request.args.get(k, "") for k in ["state", "district", "village", "land_use", "search"]} filtered = _apply_filters_to_records(records, params) role = (session.get("role") or "").lower() if role not in ("admin", "superadmin"): filtered = [r for r in filtered if not r.get("deleted")] filtered = _strip_b64_from_list(filtered) return jsonify(filtered) @records_bp.route("/api/location-catalog", methods=["GET"]) @viewer_or_admin_required def location_catalog(): """Return the hierarchy of state > district > village for dropdowns.""" records = load_records() catalog = {} for rec in records: loc = rec.get("location", {}) state, dist, vill = loc.get("state"), loc.get("district"), loc.get("village") if not all([state, dist, vill]): continue if state not in catalog: catalog[state] = {} if dist not in catalog[state]: catalog[state][dist] = set() catalog[state][dist].add(vill) # Format for JSON result = {} for s in sorted(catalog.keys()): result[s] = {d: sorted(list(v)) for d, v in sorted(catalog[s].items())} return jsonify(result) @records_bp.route("/api/records", methods=["POST"]) @role_required("admin", "superadmin", "officer") def create_record(): """Create a new land record with geometry validation.""" data = request.get_json() or {} required_fields = ["khasra_no", "khata_no", "location", "geometry", "land_use", "owner_name"] missing = [f for f in required_fields if not data.get(f)] if missing: return jsonify({"error": f"Missing fields: {', '.join(missing)}"}), 400 username = session.get("username", "admin") # 1. Geometry Validation & Metrics geometry = data.get("geometry") if not geometry: return jsonify({"error": "Geometry is required."}), 400 from gis_processor import validate_polygon, calculate_area, check_overlap, get_centroid, calculate_perimeter validation = validate_polygon(geometry) if not validation["valid"]: return jsonify({"error": "Invalid Geometry", "details": validation["errors"]}), 400 # 2. Overlap Check records = load_records() overlap = check_overlap(geometry, [r for r in records if not r.get("deleted")]) if overlap["overlaps"]: return jsonify({ "error": "Spatial Overlap Detected", "conflicting_records": overlap["conflicting_records"] }), 409 # 3. Auto-fill Data area_data = calculate_area(geometry) centroid = get_centroid(geometry) loc = data.get("location", {}) state = loc.get("state", "Unknown") district = loc.get("district", "Unknown") ulpin = data.get("ulpin") if not ulpin or len(str(ulpin)) < 10: ulpin = generate_ulpin(state, district) # Ensure uniqueness while any(r.get("ulpin") == ulpin for r in records): ulpin = generate_ulpin(state, district) new_record = { "_id": str(uuid.uuid4()), "ulpin": ulpin, "khasra_no": data.get("khasra_no", "N/A"), "khata_no": data.get("khata_no", "N/A"), "location": { "state": state, "district": district, "village": loc.get("village", "Unknown") }, "owner": { "name": data.get("owner_name", "N/A"), "share_pct": data.get("share_pct", 100), "aadhaar_mask": data.get("aadhaar_mask", "XXXX-XXXX-XXXX"), "proof_doc_b64": data.get("owner_proof_doc_b64") }, "attributes": { "area_ha": area_data.get("area_ha", 0), "land_use": data.get("land_use", "Other"), "circle_rate_inr": data.get("circle_rate_inr", 0), "centroid": centroid, "perimeter_m": calculate_perimeter(geometry).get("perimeter_m", 0) }, "geometry": geometry, "mutation_history": [], "deleted": False } records.append(new_record) save_records(records) _log_audit('create', username, new_record["_id"], {'ulpin': ulpin, 'khasra_no': new_record["khasra_no"]}) return jsonify({"success": True, "record": new_record}), 201 @records_bp.route("/api/records/", methods=["PUT"]) @role_required("admin", "superadmin", "officer") def update_record(record_id): """Update a record or perform an ownership mutation.""" data = request.get_json() or {} records = load_records() record = next((r for r in records if r["_id"] == record_id), None) if not record: return jsonify({"error": "Record not found."}), 404 from gis_processor import validate_polygon, calculate_area, get_centroid, calculate_perimeter # --- Scenario A: Ownership Mutation --- if data.get("mutation") and data.get("new_owner_name"): old_owner = record.get("owner", {}) mutation_entry = { "previous_owner": old_owner.get("name", "Unknown"), "previous_share_pct": old_owner.get("share_pct", 0), "previous_aadhaar": old_owner.get("aadhaar_mask", "XXXX-XXXX-XXXX"), "previous_proof_doc": old_owner.get("proof_doc_b64"), "mutation_date": data.get("mutation_date", datetime.now().strftime("%Y-%m-%d")), "mutation_type": data.get("mutation_type", "Sale Deed"), "mutation_ref": data.get("mutation_ref", f"MUT-{datetime.now().strftime('%Y')}-{random.randint(10000, 99999)}"), "proof_doc_b64": data.get("mutation_proof_doc_b64") } if "mutation_history" not in record: record["mutation_history"] = [] record["mutation_history"].append(mutation_entry) record["owner"] = { "name": data["new_owner_name"], "share_pct": data.get("new_share_pct", 100), "aadhaar_mask": data.get("new_aadhaar_mask", "XXXX-XXXX-XXXX") } # Mutations can also update location/geometry if provided if "location" in data: record["location"] = data["location"] if "geometry" in data: val = validate_polygon(data["geometry"]) if val["valid"]: record["geometry"] = data["geometry"] record["attributes"]["area_ha"] = calculate_area(data["geometry"]).get("area_ha", 0) record["attributes"]["centroid"] = get_centroid(data["geometry"]) record["attributes"]["perimeter_m"] = calculate_perimeter(data["geometry"]).get("perimeter_m", 0) # --- Scenario B: Regular Field Updates --- else: for field in ["khasra_no", "khata_no"]: if field in data: record[field] = data[field] if "land_use" in data: _update_nested(record, "attributes", "land_use", data["land_use"]) if "circle_rate_inr" in data: _update_nested(record, "attributes", "circle_rate_inr", data["circle_rate_inr"]) if "share_pct" in data: _update_nested(record, "owner", "share_pct", data["share_pct"]) if "aadhaar_mask" in data: _update_nested(record, "owner", "aadhaar_mask", data["aadhaar_mask"]) if "location" in data: record["location"] = data["location"] if "owner_proof_doc_b64" in data: _update_nested(record, "owner", "proof_doc_b64", data["owner_proof_doc_b64"]) if "geometry" in data: val = validate_polygon(data["geometry"]) if not val["valid"]: return jsonify({"error": "Invalid geometry.", "details": val["errors"]}), 400 record["geometry"] = data["geometry"] # Check for overlaps with other records from gis_processor import check_overlap others = [r for r in records if r["_id"] != record_id and not r.get("deleted")] overlap_result = check_overlap(data["geometry"], others) if overlap_result.get("overlaps"): return jsonify({"error": "Parcel overlap detected.", "conflicting_records": overlap_result["conflicting_records"]}), 409 record["attributes"]["area_ha"] = calculate_area(data["geometry"]).get("area_ha", 0) record["attributes"]["centroid"] = get_centroid(data["geometry"]) record["attributes"]["perimeter_m"] = calculate_perimeter(data["geometry"]).get("perimeter_m", 0) save_records(records) username = session.get("username", "admin") _log_audit('update', username, record_id, {'ulpin': record.get('ulpin'), 'khasra_no': record.get('khasra_no')}) return jsonify({"success": True, "record": record}) @records_bp.route("/api/records/", methods=["DELETE"]) @role_required("admin", "superadmin", "officer") def delete_record(record_id): """ Safely delete a record. - If active: Soft-delete (move to trash). - If already in trash: Hard-delete (permanent) if user is Admin/Superadmin. """ records = load_records() record_index = next((i for i, r in enumerate(records) if r["_id"] == record_id), None) if record_index is None: return jsonify({"error": "Record not found."}), 404 record = records[record_index] role = (session.get("role") or "").lower() username = session.get("username", "admin") if record.get("deleted"): # Record is already in trash. Only Admins can permanently remove it. if role in ("admin", "superadmin"): records.pop(record_index) save_records(records) _log_audit('hard_delete', username, record_id, {'khasra_no': record.get('khasra_no')}) return jsonify({"success": True, "message": "Record permanently deleted from database."}) else: return jsonify({"error": "Only administrators can permanently delete records."}), 403 else: # Soft delete the record record["deleted"] = True record["deleted_at"] = datetime.now().isoformat() + "Z" record["deleted_by"] = username save_records(records) _log_audit('soft_delete', username, record_id, {'khasra_no': record.get('khasra_no')}) return jsonify({"success": True, "message": "Record moved to trash."}) @records_bp.route("/api/records//restore", methods=["POST"]) @role_required("admin", "superadmin") def restore_record(record_id): """Restore a soft-deleted record.""" records = load_records() record = next((r for r in records if r.get("_id") == record_id), None) if not record: return jsonify({"error": "Not found."}), 404 record["deleted"] = False record.pop("deleted_by", None) record.pop("deleted_at", None) save_records(records) return jsonify({"success": True}) import io import base64 from fpdf import FPDF import qrcode from flask import send_file @records_bp.route("/api/records//card", methods=["POST"]) @viewer_or_admin_required def generate_property_card(ulpin): """Generate a PDF Property Card for a given ULPIN.""" records = load_records() record = next((r for r in records if r.get("ulpin") == ulpin), None) if not record: return jsonify({"error": "Record not found."}), 404 data = request.get_json() or {} map_image_b64 = data.get("map_image") # PDF Configuration pdf = FPDF() pdf.add_page() pdf.set_font("helvetica", "B", 18) # Header pdf.set_text_color(234, 88, 12) # Orange-600 pdf.cell(0, 10, "GOVERNMENT OF INDIA", ln=True, align="C") pdf.set_font("helvetica", "B", 14) pdf.set_text_color(31, 41, 55) # Gray-800 pdf.cell(0, 8, "BHOOMI-LIMS PROPERTY CARD", ln=True, align="C") pdf.ln(5) # Horizontal Line pdf.set_draw_color(209, 213, 219) pdf.line(10, pdf.get_y(), 200, pdf.get_y()) pdf.ln(10) # Main Info Grid pdf.set_font("helvetica", "B", 10) pdf.set_fill_color(249, 250, 251) col_width = 45 def add_info_row(label, value): pdf.set_font("helvetica", "B", 9) pdf.set_text_color(107, 114, 128) pdf.cell(col_width, 8, f"{label}:", border=0) pdf.set_font("helvetica", "", 10) pdf.set_text_color(0, 0, 0) pdf.cell(0, 8, str(value), ln=True) add_info_row("ULPIN", record.get("ulpin", "N/A")) add_info_row("Khasra No", record.get("khasra_no", "N/A")) add_info_row("Khata No", record.get("khata_no", "N/A")) add_info_row("Area (Hectares)", f"{record.get('attributes', {}).get('area_ha', 0):.4f}") add_info_row("Village", record.get("location", {}).get("village", "N/A")) add_info_row("District", record.get("location", {}).get("district", "N/A")) add_info_row("State", record.get("location", {}).get("state", "N/A")) # Owner Info pdf.ln(5) pdf.set_font("helvetica", "B", 11) pdf.cell(0, 10, "OWNERSHIP DETAILS", ln=True) pdf.set_font("helvetica", "", 10) owner = record.get("owner", {}) add_info_row("Primary Owner", owner.get("name", "N/A")) add_info_row("Share Percentage", f"{owner.get('share_pct', 100)}%") # Map Image if map_image_b64: try: img_data = base64.b64decode(map_image_b64.split(",")[1]) img_io = io.BytesIO(img_data) # Position at bottom right or below text y_pos = pdf.get_y() + 10 if y_pos > 180: # Start new page if no space pdf.add_page() y_pos = 20 pdf.image(img_io, x=10, y=y_pos, w=120) pdf.set_y(y_pos + 70) except Exception as e: print(f"PDF Map Error: {e}") # QR Code for Verification qr_data = f"https://lims-india.gov.in/verify/{ulpin}" qr = qrcode.QRCode(version=1, box_size=10, border=1) qr.add_data(qr_data) qr.make(fit=True) qr_img = qr.make_image(fill_color="black", back_color="white") qr_io = io.BytesIO() qr_img.save(qr_io, format="PNG") qr_io.seek(0) # Place QR code in top right pdf.image(qr_io, x=165, y=30, w=30) pdf.set_font("helvetica", "I", 7) pdf.set_xy(165, 60) pdf.cell(30, 5, "Scan to Verify", align="C") # Mutation History Table pdf.set_xy(10, pdf.get_y() + 10) pdf.set_font("helvetica", "B", 11) pdf.cell(0, 10, "MUTATION HISTORY", ln=True) mutations = record.get("mutation_history", []) if not mutations: pdf.set_font("helvetica", "I", 9) pdf.cell(0, 8, "No prior mutations recorded.", ln=True) else: pdf.set_font("helvetica", "B", 8) pdf.set_fill_color(243, 244, 246) pdf.cell(30, 8, "Date", 1, 0, "C", True) pdf.cell(30, 8, "Type", 1, 0, "C", True) pdf.cell(80, 8, "Previous Owner", 1, 0, "C", True) pdf.cell(50, 8, "Reference", 1, 1, "C", True) pdf.set_font("helvetica", "", 8) for m in mutations: pdf.cell(30, 7, str(m.get("mutation_date", "N/A")), 1) pdf.cell(30, 7, str(m.get("mutation_type", "N/A")), 1) pdf.cell(80, 7, str(m.get("previous_owner", "N/A")), 1) pdf.cell(50, 7, str(m.get("mutation_ref", "N/A")), 1, 1) # Footer pdf.set_y(-25) pdf.set_font("helvetica", "I", 8) pdf.set_text_color(156, 163, 175) pdf.cell(0, 10, f"Document Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", align="C") pdf.ln(5) pdf.cell(0, 10, "This is a computer-generated document and does not require a physical signature.", align="C") # Return PDF pdf_output = pdf.output() return send_file( io.BytesIO(pdf_output), mimetype="application/pdf", as_attachment=True, download_name=f"Property_Card_{ulpin}.pdf" ) ================================================ FILE: routes/users.py ================================================ import uuid from datetime import datetime from flask import Blueprint, request, jsonify, session from werkzeug.security import generate_password_hash, check_password_hash from core import load_users, save_users, admin_required, viewer_or_admin_required users_bp = Blueprint('users', __name__) def _get_current_user(users): current_username = session.get("username", "") return next((u for u in users if u.get("username") == current_username), None) @users_bp.route("/api/profile", methods=["GET"]) @viewer_or_admin_required def get_profile(): users = load_users() user = _get_current_user(users) if not user: return jsonify({"error": "Profile not found."}), 404 profile = {k: v for k, v in user.items() if k != "password_hash"} return jsonify(profile) @users_bp.route("/api/profile", methods=["PUT"]) @viewer_or_admin_required def update_profile(): data = request.get_json() or {} users = load_users() user = _get_current_user(users) if not user: return jsonify({"error": "Profile not found."}), 404 allowed_fields = ["full_name", "email", "phone", "designation", "department", "office_location"] for field in allowed_fields: if field in data: user[field] = data[field] if data.get("current_password") and data.get("new_password"): if not check_password_hash(user.get("password_hash", ""), data["current_password"]): return jsonify({"error": "Current password is incorrect."}), 403 user["password_hash"] = generate_password_hash(data["new_password"]) save_users(users) profile = {k: v for k, v in user.items() if k != "password_hash"} return jsonify({"success": True, "profile": profile}) @users_bp.route("/api/users", methods=["GET"]) @admin_required def list_users(): """List all users (admin only). Recovery accounts are hidden.""" users = load_users() # Ghost Mode: Recovery accounts are hidden from standard admins current_user = _get_current_user(users) is_rec = current_user.get("is_recovery") if current_user else False result = [{k: v for k, v in u.items() if k != "password_hash"} for u in users if is_rec or not u.get("is_recovery")] return jsonify(result) @users_bp.route("/api/users", methods=["POST"]) @admin_required def create_user(): data = request.get_json() or {} role = (data.get("role") or "officer").strip().lower() current_role = (session.get("role") or "").lower() users = load_users() current_user = _get_current_user(users) # 1. SECURITY CHECK FIRST if role == "superadmin" and current_role != "superadmin": return jsonify({"error": "Unauthorized. Only SuperAdmins can create other SuperAdmins."}), 403 # Define role hierarchy if current_role == "superadmin": valid_roles = ["superadmin", "admin", "officer", "viewer"] elif current_role == "admin": valid_roles = ["admin", "officer", "viewer"] else: valid_roles = [] if role not in valid_roles: return jsonify({ "error": f"Unauthorized role assignment. Your current role '{current_role}' is not permitted to create a '{role}' account.", "allowed_roles_for_you": valid_roles }), 403 # 2. VALIDATION CHECKS required = ["username", "password", "full_name"] missing = [f for f in required if not data.get(f)] if missing: return jsonify({"error": f"Missing: {', '.join(missing)}"}), 400 if any(u.get("username") == data["username"] for u in users): return jsonify({"error": "Username exists."}), 409 # 3. EXECUTION new_user = { "user_id": str(uuid.uuid4()), "username": data["username"], "password_hash": generate_password_hash(data["password"]), "role": role, "full_name": data["full_name"], "email": data.get("email", ""), "phone": data.get("phone", ""), "designation": data.get("designation", ""), "department": data.get("department", ""), "office_location": data.get("office_location", ""), "is_active": data.get("is_active", True), "is_recovery": data.get("is_recovery", False) if current_user and current_user.get("is_recovery") else False, "created_at": datetime.now().isoformat() + "Z", "last_login": None } users.append(new_user) save_users(users) return jsonify({"success": True, "user": {k:v for k,v in new_user.items() if k != "password_hash"}}), 201 @users_bp.route("/api/users/", methods=["GET"]) @admin_required def get_user(user_id): """Get a specific user's profile (admin only). Recovery accounts are invisible.""" users = load_users() user = next((u for u in users if u.get("user_id") == user_id), None) # Hide the existence of recovery accounts from standard admins current_user = _get_current_user(users) is_rec = current_user.get("is_recovery") if current_user else False if not user or (user.get("is_recovery") and not is_rec): return jsonify({"error": "Not found."}), 404 return jsonify({k: v for k, v in user.items() if k != "password_hash"}) @users_bp.route("/api/users/", methods=["PUT"]) @admin_required def update_user(user_id): """Update any user's profile (admin only).""" data = request.get_json() or {} users = load_users() target_user = next((u for u in users if u.get("user_id") == user_id), None) # Standard admins can't even "see" that a recovery account exists to update it current_user = _get_current_user(users) is_rec = current_user.get("is_recovery") if current_user else False if not target_user or (target_user.get("is_recovery") and not is_rec): return jsonify({"error": "Not found."}), 404 current_user = _get_current_user(users) current_role = (session.get("role") or "").lower() # 1. SECURITY CHECK target_role = target_user.get("role", "").lower() # Only superadmins can modify other superadmins if target_role == "superadmin" and current_role != "superadmin": return jsonify({"error": "Unauthorized. Only SuperAdmins can modify SuperAdmin accounts."}), 403 # Admins cannot modify superadmins, but can modify anyone else if current_role == "admin" and target_role == "superadmin": return jsonify({"error": "Unauthorized. Admins cannot modify SuperAdmin accounts."}), 403 if "role" in data: new_role = data["role"].lower() if current_role == "superadmin": valid_roles = ["superadmin", "admin", "officer", "viewer"] elif current_role == "admin": valid_roles = ["admin", "officer", "viewer"] else: valid_roles = [] if new_role not in valid_roles: return jsonify({"error": f"Invalid role assignment: '{new_role}' is not allowed for your role."}), 403 target_user["role"] = new_role # 2. EXECUTION allowed_fields = ["full_name", "email", "phone", "designation", "department", "office_location", "is_active"] for field in allowed_fields: if field in data: target_user[field] = data[field] if "is_recovery" in data and current_user and current_user.get("is_recovery"): target_user["is_recovery"] = data["is_recovery"] if data.get("new_password"): target_user["password_hash"] = generate_password_hash(data["new_password"]) save_users(users) return jsonify({"success": True, "user": {k:v for k,v in target_user.items() if k != "password_hash"}}) @users_bp.route("/api/users/", methods=["DELETE"]) @admin_required def delete_user(user_id): """Delete a user (admin only).""" users = load_users() target_user = next((u for u in users if u.get("user_id") == user_id), None) # Hide the existence of recovery accounts from standard admins current_user = _get_current_user(users) is_rec = current_user.get("is_recovery") if current_user else False if not target_user or (target_user.get("is_recovery") and not is_rec): return jsonify({"error": "Not found."}), 404 current_user = _get_current_user(users) current_role = (session.get("role") or "").lower() if target_user.get("username") == session.get("username"): return jsonify({"error": "Cannot delete self."}), 403 target_role = target_user.get("role", "").lower() if target_role == "superadmin": if current_role != "superadmin": return jsonify({"error": "Unauthorized. Only SuperAdmins can delete other SuperAdmins."}), 403 if current_role != "superadmin" and target_role == "admin": return jsonify({"error": "Unauthorized to delete admins."}), 403 # 2. EXECUTION users = [u for u in users if u.get("user_id") != user_id] save_users(users) return jsonify({"success": True}) ================================================ FILE: routes/utils.py ================================================ from flask import Blueprint, jsonify from config import LAND_USE_OPTIONS, LAND_USE_COLORS, MUTATION_TYPES utils_bp = Blueprint('utils', __name__) @utils_bp.route("/api/config", methods=["GET"]) def app_config(): return jsonify({ "land_use_options": LAND_USE_OPTIONS, "land_use_colors": LAND_USE_COLORS, "mutation_types": MUTATION_TYPES }) ================================================ FILE: run_server.py ================================================ from app import create_app import os if __name__ == '__main__': app = create_app() # Disable debug mode and reloader to avoid issues in background app.run(host='127.0.0.1', port=5000, debug=False, use_reloader=False) ================================================ FILE: scripts/inspect_recovery.py ================================================ #!/usr/bin/env python3 """Inspect superadmin and recovery accounts in the database.""" from dotenv import load_dotenv import os load_dotenv() from pymongo import MongoClient try: from config import MONGO_URI except Exception: MONGO_URI = os.environ.get('MONGO_URI') def main(): uri = MONGO_URI or os.environ.get('MONGO_URI') if not uri: raise SystemExit('MONGO_URI not found in config or environment') client = MongoClient(uri) db = client.get_database('indialims') users = list(db.users.find({}, {'password_hash': 0})) print('Total users:', len(users)) print('\nSuperadmin users:') for u in users: if u.get('role') == 'superadmin': print('-', u.get('username'), '| is_recovery=', u.get('is_recovery', False)) print('\nRecovery-flagged users:') for u in users: if u.get('is_recovery'): print('-', u.get('username'), '| role=', u.get('role')) if __name__ == '__main__': main() ================================================ FILE: scripts/recover_superadmin_auto.py ================================================ #!/usr/bin/env python3 """Server-only recovery script to upsert or reset a superadmin account. Run this from the server/host with access to the MongoDB instance (SSH). It prints the generated password once — copy it securely and rotate after first login. Supports `--dry-run` to show actions without modifying the database. """ import secrets import argparse import os from datetime import datetime from dotenv import load_dotenv # Load .env into environment so MONGO_URI is available when running the script load_dotenv() def random_password(length=20): alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+" return ''.join(secrets.choice(alphabet) for _ in range(length)) def upsert_superadmin(username=None, password=None, dry_run=False): now = datetime.utcnow().isoformat() + "Z" username = username or f"recovery_sa_{now.replace(':','').replace('-','').replace('T','_')}" password = password or random_password() if dry_run: print("[DRY-RUN] Would ensure a superadmin exists or be updated with the following:") print("USERNAME:", username) print("PASSWORD:", password) print("[DRY-RUN] No database connections or writes were performed.") return username, password, False # Perform actual DB operations # Import heavy dependencies only when performing real DB operations so # `--dry-run` remains usable in minimal environments. from werkzeug.security import generate_password_hash from pymongo import MongoClient # Load MONGO_URI from project config if available, otherwise from env try: from config import MONGO_URI except Exception: MONGO_URI = os.environ.get("MONGO_URI") if not MONGO_URI: raise RuntimeError("MONGO_URI is not set in config.py or environment.") client = MongoClient(MONGO_URI) db = client.get_database("indialims") users = db.users sa = users.find_one({"role": "superadmin"}) if sa: users.update_one({"_id": sa["_id"]}, {"$set": { "username": username, "password_hash": generate_password_hash(password), "is_active": True, "last_login": now, "is_recovery": True }}) created = False else: users.insert_one({ "user_id": f"recovery-sa-{now}", "username": username, "password_hash": generate_password_hash(password), "role": "superadmin", "full_name": "Recovery SuperAdmin", "email": "", "phone": "", "is_active": True, "is_recovery": True, "created_at": now, "last_login": now }) created = True print("SuperAdmin created" if created else "SuperAdmin updated") print("USERNAME:", username) print("PASSWORD:", password) print("IMPORTANT: copy the password now and rotate on first login.") return username, password, created if __name__ == "__main__": parser = argparse.ArgumentParser(description="Upsert/reset superadmin (server-only).") parser.add_argument("username", nargs="?", help="Optional username to set") parser.add_argument("password", nargs="?", help="Optional password to set") parser.add_argument("--dry-run", action="store_true", help="Show actions without writing to DB") parser.add_argument("--dump", action="store_true", help="Dump credentials to a file in the scripts folder") parser.add_argument("--dump-file", help="Optional path for dumped credentials (overrides default)") args = parser.parse_args() username, password, created = upsert_superadmin(args.username, args.password, dry_run=args.dry_run) # If requested, write the last generated credentials to a file for operator convenience. # Note: `upsert_superadmin` prints credentials; to capture them here we re-run a quiet # generation when --dump is requested but do not modify DB again. if args.dump and not args.dry_run: # Use the exact credentials returned from the upsert operation # (so the dump matches what was written to the DB) dump_username = username dump_password = password # If user provided --dump-file use it; otherwise place in scripts folder dump_path = args.dump_file if not dump_path: scripts_dir = os.path.dirname(__file__) safe_now = datetime.utcnow().isoformat().replace(':', '').replace('.', '_') dump_path = os.path.join(scripts_dir, f"recovery_credentials_{safe_now}.txt") with open(dump_path, 'w', encoding='utf-8') as f: f.write(f"# Recovery credentials generated: {datetime.utcnow().isoformat()}Z\n") f.write(f"USERNAME: {dump_username}\n") f.write(f"PASSWORD: {dump_password}\n") f.write("# IMPORTANT: rotate this password immediately after first login.\n") print(f"Credentials dumped to: {dump_path}") ================================================ FILE: scripts/recovery_credentials_2026-04-25T050759_400298.txt ================================================ SuperAdmin updated USERNAME: recovery_sa_20260425_154817.302101Z PASSWORD: WwI9v*V_J=MpH_SHi3+^ IMPORTANT: copy the password now and rotate on first login. ================================================ FILE: static/css/style.css ================================================ /* * style.css - Custom Styling for India LIMS * Tailwind CSS is loaded via CDN; this file adds Leaflet & custom overrides. */ /* ─── Base Overrides ──────────────────────────────────────────────────────── */ * { box-sizing: border-box; } body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; margin: 0; padding: 0; } /* ─── Leaflet Map Overrides ───────────────────────────────────────────────── */ #map, #add-record-map, #view-record-map { width: 100% !important; height: 100% !important; z-index: 1; } /* Ensure Leaflet controls don't conflict with our UI */ .leaflet-control-container { z-index: 400; } .leaflet-control-zoom { border: none !important; box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important; border-radius: 8px !important; overflow: hidden; } .leaflet-control-zoom a { width: 36px !important; height: 36px !important; line-height: 36px !important; font-size: 18px !important; color: #374151 !important; background: white !important; border-bottom: 1px solid #e5e7eb !important; } .leaflet-control-zoom a:hover { background: #f3f4f6 !important; color: #ea580c !important; } /* ─── Leaflet-Geoman Toolbar Styling ──────────────────────────────────────── */ .leaflet-pm-toolbar { z-index: 500 !important; } .pm-toolbar { border-radius: 8px !important; box-shadow: 0 2px 10px rgba(0,0,0,0.15) !important; border: none !important; overflow: hidden; } .pm-toolbar .button-container .pm-btn { width: 40px !important; height: 40px !important; background: white !important; border: none !important; border-bottom: 1px solid #e5e7eb !important; transition: background 0.15s; } .pm-toolbar .button-container .pm-btn:hover { background: #fff7ed !important; } .pm-toolbar .button-container .pm-btn.active { background: #fed7aa !important; color: #ea580c !important; } .pm-toolbar .button-container:last-child .pm-btn { border-bottom: none !important; } /* ─── Map Popup Styling ───────────────────────────────────────────────────── */ .leaflet-popup-content-wrapper { border-radius: 10px !important; box-shadow: 0 4px 20px rgba(0,0,0,0.12) !important; padding: 0 !important; } .leaflet-popup-content { margin: 0 !important; font-family: 'Segoe UI', system-ui, sans-serif; font-size: 13px; min-width: 220px; } .leaflet-popup-tip { box-shadow: 0 4px 20px rgba(0,0,0,0.08) !important; } /* ─── Parcel Tooltip Styling ──────────────────────────────────────────────── */ .parcel-tooltip { background: white !important; border: none !important; border-radius: 10px !important; box-shadow: 0 6px 20px rgba(0,0,0,0.15) !important; padding: 0 !important; font-family: 'Segoe UI', system-ui, sans-serif !important; font-size: 13px !important; color: #374151 !important; max-width: 300px !important; min-width: 250px !important; } .parcel-tooltip::before { border-top-color: white !important; } .parcel-tooltip .tooltip-content { padding: 14px 16px; } .parcel-tooltip .tooltip-title { font-size: 15px; font-weight: 700; color: #1f2937; margin-bottom: 10px; border-bottom: 2px solid #ea580c; padding-bottom: 8px; } .parcel-tooltip .tooltip-row { display: flex; justify-content: space-between; margin-bottom: 6px; line-height: 1.6; } .parcel-tooltip .tooltip-label { font-weight: 600; color: #6b7280; margin-right: 10px; min-width: 80px; } .parcel-tooltip .tooltip-value { font-weight: 500; color: #1f2937; text-align: right; } .parcel-tooltip .tooltip-divider { height: 1px; background: #e5e7eb; margin: 10px 0; } .parcel-tooltip .tooltip-hint { margin-top: 10px; padding-top: 8px; border-top: 1px solid #e5e7eb; font-size: 12px; color: #ea580c; font-weight: 600; text-align: center; } /* ─── Custom Parcel Styling ───────────────────────────────────────────────── */ .parcel-polygon { stroke: #ea580c; stroke-width: 2.5; fill-opacity: 0.2; stroke-opacity: 0.9; } .parcel-polygon:hover { fill-opacity: 0.35; stroke-width: 3.5; } .parcel-polygon-selected { stroke: #dc2626; stroke-width: 3.5; fill-opacity: 0.35; stroke-opacity: 1; } /* Land use color coding */ .parcel-agricultural { fill: #22c55e; } .parcel-residential { fill: #3b82f6; } .parcel-commercial { fill: #f59e0b; } .parcel-industrial { fill: #8b5cf6; } .parcel-government { fill: #ef4444; } .parcel-forest { fill: #065f46; } .parcel-wasteland { fill: #9ca3af; } /* ─── Map Layer Switcher ─────────────────────────────────────────────────── */ input[type="radio"][name="basemap"], input[type="radio"][name="add-basemap"], input[type="radio"][name="view-basemap"] { -webkit-appearance: none; appearance: none; width: 14px; height: 14px; border: 2px solid #d1d5db; border-radius: 50%; background: white; cursor: pointer; position: relative; flex-shrink: 0; } input[type="radio"][name="basemap"]:checked, input[type="radio"][name="add-basemap"]:checked, input[type="radio"][name="view-basemap"]:checked { border-color: #ea580c; border-width: 2px; } input[type="radio"][name="basemap"]:checked::after, input[type="radio"][name="add-basemap"]:checked::after, input[type="radio"][name="view-basemap"]:checked::after { content: ''; position: absolute; top: 2px; left: 2px; width: 6px; height: 6px; border-radius: 50%; background: #ea580c; } input[type="radio"][name="basemap"]:hover, input[type="radio"][name="add-basemap"]:hover, input[type="radio"][name="view-basemap"]:hover { border-color: #ea580c; } /* ─── Public Viewer Map Layer Switcher (green theme) ─────────────────────── */ .viewer-radio { -webkit-appearance: none; appearance: none; width: 14px; height: 14px; border: 2px solid #d1d5db; border-radius: 50%; background: white; cursor: pointer; position: relative; flex-shrink: 0; } .viewer-radio:checked { border-color: #16a34a; border-width: 2px; } .viewer-radio:checked::after { content: ''; position: absolute; top: 2px; left: 2px; width: 6px; height: 6px; border-radius: 50%; background: #16a34a; } .viewer-radio:hover { border-color: #16a34a; } /* ─── Public Viewer Filter Selects (green theme) ────────────────────────── */ #filter-state:focus, #filter-district:focus, #filter-village:focus, #filter-land-use:focus { --tw-ring-color: #16a34a; border-color: #16a34a; } /* ─── Main Tab Navigation (Left Sidebar) ─────────────────────────────────── */ .main-tab-btn { width: 100%; border: 0; background: transparent; color: #6b7280; font-size: 0.9rem; font-weight: 500; padding: 0.875rem 1rem; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 0.75rem; position: relative; border-left: 4px solid transparent; border-radius: 0.5rem; margin-bottom: 0.25rem; } .main-tab-btn:hover { color: #374151; background: #f9fafb; } .main-tab-btn.active { color: #ea580c; background: #fff7ed; border-left-color: #ea580c; font-weight: 600; } .main-tab-btn svg { flex-shrink: 0; } .main-tab-btn span { flex: 1; text-align: left; } /* Logout button styling */ #btn-logout { margin-top: 0.25rem; border-radius: 0.5rem; } #btn-logout:hover { background: #fef2f2; border-left-color: #dc2626; color: #dc2626; } .main-tab-panel { animation: fadeIn 0.25s ease-in; } /* ─── Sidebar Styling (Legacy Support) ────────────────────────────────────── */ .sidebar-tab.active { color: #ea580c; border-bottom-color: #ea580c; } .sidebar-tab { transition: color 0.15s, border-color 0.15s; border-bottom: 2px solid transparent; cursor: pointer; background: none; border-top: none; border-left: none; border-right: none; } .sidebar-tab:hover { color: #374151; } /* ─── Form Sub-Tabs ───────────────────────────────────────────────────────── */ .form-tab-btn { flex: 1; border: 0; background: transparent; color: #6b7280; font-size: 0.875rem; font-weight: 500; padding: 0.75rem 1rem; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center; gap: 0.5rem; border-bottom: 2px solid transparent; } .form-tab-btn:hover { color: #374151; background: #f3f4f6; } .form-tab-btn.active { color: #ea580c; background: #fff7ed; border-bottom-color: #ea580c; } .form-tab-panel { animation: fadeIn 0.2s ease-in; } /* ─── Draw Settings Panel ─────────────────────────────────────────────────── */ .draw-settings-btn { border: 0; border-radius: 0.375rem; font-size: 0.75rem; font-weight: 600; padding: 0.5rem 0.75rem; transition: background 0.15s; } #map-autofill-status { min-height: 1rem; } /* ─── Dashboard KPI Cards ─────────────────────────────────────────────────── */ .dashboard-kpi-card { border: 1px solid #e5e7eb; background: #f9fafb; border-radius: 0.5rem; padding: 0.55rem 0.7rem; } .dashboard-kpi-label { font-size: 0.68rem; color: #6b7280; text-transform: uppercase; letter-spacing: 0.04em; } .dashboard-kpi-value { font-size: 1rem; font-weight: 700; color: #1f2937; margin-top: 0.15rem; } .dashboard-row { border: 1px solid #e5e7eb; border-radius: 0.5rem; background: #ffffff; padding: 0.5rem 0.65rem; } .dashboard-row-bar { height: 0.4rem; background: #ffedd5; border-radius: 9999px; overflow: hidden; margin-top: 0.35rem; } .dashboard-row-bar-fill { height: 100%; background: #ea580c; } /* ─── Dashboard Content Improvements ──────────────────────────────────────── */ #main-tab-dashboard .bg-white.rounded-lg.shadow-md { transition: box-shadow 0.2s ease; } #main-tab-dashboard .bg-white.rounded-lg.shadow-md:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } #dashboard-land-use > div, #dashboard-districts > div { transition: transform 0.15s ease; } #dashboard-land-use > div:hover, #dashboard-districts > div:hover { transform: translateX(2px); } /* ─── Records List Styling ────────────────────────────────────────────────── */ .records-header { position: sticky; top: 0; z-index: 5; background: #ffffff; border-bottom: 1px solid #e5e7eb; padding: 0.65rem 0.75rem; display: flex; align-items: center; justify-content: space-between; } .records-view-tabs { display: inline-flex; border: 1px solid #e5e7eb; border-radius: 0.5rem; overflow: hidden; margin-bottom: 0; width: auto; min-width: 180px; flex-shrink: 0; } .records-view-tab { border: 0; background: #ffffff; color: #4b5563; font-size: 0.75rem; font-weight: 600; padding: 0.5rem 0.875rem; cursor: pointer; transition: background 0.15s, color 0.15s; flex: 1; white-space: nowrap; min-width: 90px; } .records-view-tab:hover { background: #f3f4f6; } .records-view-tab.active { background: #fff7ed; color: #c2410c; } .record-table-row { border: 1px solid #e5e7eb; border-radius: 0.5rem; background: #ffffff; padding: 0.55rem 0.65rem; margin-bottom: 0.5rem; transition: all 0.15s; cursor: pointer; } .record-table-row:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } .record-table-row.active { border-color: #fdba74; background: #fff7ed; } .view-record-btn-table { cursor: pointer; transition: all 0.15s; font-weight: 500; } .view-record-btn-table:hover { background: #ea580c !important; transform: translateY(-1px); box-shadow: 0 2px 4px rgba(234, 88, 12, 0.3); } /* ─── Record Card in Sidebar ──────────────────────────────────────────────── */ .record-card { padding: 12px 16px; cursor: pointer; transition: background 0.15s; border-left: 3px solid transparent; border-radius: 0.5rem; margin-bottom: 0.5rem; background: #ffffff; border: 1px solid #e5e7eb; } .record-card:hover { background: #f9fafb; border-left-color: #ea580c; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } .record-card.active { background: #fff7ed; border-left-color: #ea580c; box-shadow: 0 2px 8px rgba(234, 88, 12, 0.1); } .view-record-btn { cursor: pointer; transition: all 0.15s; } .view-record-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 4px rgba(234, 88, 12, 0.3); } .record-card .land-use-badge { display: inline-block; padding: 2px 10px; border-radius: 9999px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; } .badge-agricultural { background: #dcfce7; color: #166534; } .badge-residential { background: #dbeafe; color: #1e40af; } .badge-commercial { background: #fef3c7; color: #92400e; } .badge-industrial { background: #ede9fe; color: #5b21b6; } .badge-government { background: #fee2e2; color: #991b1b; } .badge-forest { background: #d1fae5; color: #065f46; } .badge-wasteland { background: #f3f4f6; color: #4b5563; } /* Soft-deleted badge */ .badge-deleted { background: #fee2e2; color: #9f1239; padding: 2px 8px; border-radius: 9999px; font-size: 11px; font-weight: 700; text-transform: none; } /* Muted appearance for deleted records in lists */ .record-card.deleted { opacity: 0.65; filter: grayscale(0.08); } .record-table-row.deleted { opacity: 0.6; filter: grayscale(0.08); } /* ─── Form Field Group Styling ────────────────────────────────────────────── */ fieldset { border-color: #e5e7eb; } fieldset legend { font-size: 0.75rem; font-weight: 600; color: #6b7280; padding: 0 0.5rem; } /* ─── Toast Notification ──────────────────────────────────────────────────── */ #toast { position: fixed; bottom: 20px; right: 20px; z-index: 10000; transform: translateY(20px); opacity: 0; transition: all 0.3s ease; max-width: 400px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } #toast.show { display: block !important; transform: translateY(0); opacity: 1; } #toast .toast-inner { padding: 12px 20px; border-radius: 8px; color: white; font-size: 14px; font-weight: 500; } #toast.success .toast-inner { background: #166534; } #toast.error .toast-inner { background: #991b1b; } #toast.info .toast-inner { background: #1e40af; } #toast.warning .toast-inner { background: #92400e; } /* ─── Form Input Improvements ─────────────────────────────────────────────── */ input[type="text"], input[type="number"], input[type="email"], input[type="date"], input[type="password"], select, textarea { transition: all 0.2s ease; } input[type="text"]:focus, input[type="number"]:focus, input[type="email"]:focus, input[type="date"]:focus, input[type="password"]:focus, select:focus, textarea:focus { outline: none; } input[readonly] { cursor: not-allowed; } /* ─── Login Page Styling ──────────────────────────────────────────────────── */ #captcha-question { font-variant-numeric: tabular-nums; letter-spacing: 2px; user-select: none; } /* ─── Scrollbar Styling ───────────────────────────────────────────────────── */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: #f1f1f1; } ::-webkit-scrollbar-thumb { background: #c4c4c4; border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background: #a0a0a0; } /* ─── Loading Spinner ─────────────────────────────────────────────────────── */ .spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid rgba(255,255,255,0.3); border-radius: 50%; border-top-color: #fff; animation: spin 0.6s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* ─── Print Styles (for Property Cards) ───────────────────────────────────── */ @media print { body * { visibility: hidden; } #print-area, #print-area * { visibility: visible; } #print-area { position: absolute; left: 0; top: 0; } } /* ─── Responsive Adjustments ──────────────────────────────────────────────── */ @media (max-width: 1024px) { /* Adjust dashboard grid for tablets */ #main-tab-dashboard .grid { gap: 1rem; } /* Reduce padding in records toolbar */ .bg-white.border-b.px-4.py-3 { padding-left: 0.75rem; padding-right: 0.75rem; } /* Adjust search input width */ #search-input { width: 12rem !important; } /* Hide department column on tablets */ #users-table-body td:nth-child(4), #users-table-body th:nth-child(4) { display: none; } } @media (max-width: 1024px) and (min-width: 768px) { /* Make sidebar slightly smaller on tablet only */ .w-56 { width: 64px !important; } .main-tab-btn span, #btn-logout span { display: none; } .main-tab-btn { padding: 0.75rem; justify-content: center; } #btn-logout { padding: 0.75rem; justify-content: center; } } @media (max-width: 768px) { /* Stack dashboard cards on mobile */ #main-tab-dashboard .grid { grid-template-columns: 1fr !important; } /* Hide some filters on mobile */ .records-header .records-view-tabs { display: none; } /* Hide contact and department columns on mobile */ #users-table-body td:nth-child(3), #users-table-body th:nth-child(3), #users-table-body td:nth-child(4), #users-table-body th:nth-child(4) { display: none; } } @media (max-width: 767px) { /* Ensure map takes full viewport on mobile */ #map { height: calc(100vh - 60px) !important; } /* Parcel panel on mobile - full width with top border */ #parcel-panel { max-width: 100vw !important; border-radius: 0 !important; } /* Make filter sidebar touch-friendly */ #filter-sidebar { -webkit-overflow-scrolling: touch; } /* Larger touch targets for filter inputs */ #filter-state, #filter-district, #filter-village, #filter-land-use, #search-input { min-height: 44px; font-size: 16px !important; /* Prevents iOS zoom on focus */ } #btn-apply-filters, #btn-clear-filters { min-height: 44px; } /* Ensure overlay is clickable */ #filter-overlay { z-index: 35; } } ================================================ FILE: static/data/india-boundary.geojson ================================================ {"type": "FeatureCollection", "features": [{"type": "Feature", "properties": {"name": "India", "ISO3166-1-Alpha-3": "IND", "ISO3166-1-Alpha-2": "IN"}, "geometry": {"type": "MultiPolygon", "coordinates": [[[[77.800346, 35.495406], [77.815332, 35.47334], [77.834246, 35.452152], [77.857087, 35.436598], [77.883132, 35.431068], [77.912691, 35.44099], [77.957856, 35.482073], [77.985865, 35.494165], [78.044259, 35.491633], [78.055628, 35.452876], [78.038058, 35.398099], [78.009843, 35.347456], [78.001161, 35.268908], [78.036404, 35.194235], [78.130042, 35.055432], [78.13924, 35.018949], [78.148129, 34.94314], [78.162185, 34.908826], [78.202492, 34.865728], [78.211381, 34.848262], [78.273186, 34.658867], [78.296027, 34.624658], [78.335301, 34.594375], [78.379432, 34.578666], [78.607325, 34.546471], [78.664583, 34.526421], [78.750676, 34.471282], [78.801732, 34.415006], [78.839249, 34.396894], [78.885861, 34.385758], [78.922138, 34.372296], [78.95118, 34.349171], [78.95694, 34.339961], [78.976192, 34.309173], [78.98539, 34.286358], [78.988491, 34.263129], [78.985494, 34.239901], [78.976192, 34.217163], [78.903225, 34.158123], [78.806073, 34.122983], [78.730109, 34.079265], [78.721117, 33.994386], [78.770313, 33.872352], [78.787263, 33.808428], [78.794084, 33.74391], [78.781372, 33.552785], [78.800905, 33.494236], [78.824263, 33.461059], [78.915948, 33.387656], [78.917694, 33.386258], [78.964986, 33.344254], [78.993449, 33.330782], [79.023041, 33.31798], [79.053278, 33.307873], [79.075048, 33.295743], [79.098028, 33.287655], [79.125038, 33.290203], [79.150213, 33.292555], [79.260061, 33.291776], [79.340307, 33.275132], [79.420084, 33.275169], [79.456096, 33.250399], [79.423475, 33.157825], [79.37317, 33.11214], [79.381063, 33.061768], [79.399273, 33.026878], [79.339787, 33.013257], [79.333586, 32.994292], [79.378462, 32.978881], [79.414089, 32.972053], [79.471089, 32.908959], [79.564093, 32.793928], [79.613913, 32.765035], [79.620673, 32.728725], [79.57617, 32.670646], [79.531201, 32.648317], [79.515741, 32.60812], [79.472113, 32.565472], [79.443497, 32.534773], [79.40682, 32.529683], [79.379477, 32.57645], [79.320466, 32.592267], [79.287412, 32.532579], [79.235608, 32.504296], [79.215247, 32.507914], [79.191166, 32.50427], [79.1614, 32.496467], [79.132875, 32.48546], [79.112928, 32.472386], [79.102902, 32.449855], [79.09174, 32.390556], [79.075307, 32.37079], [79.066316, 32.370067], [79.039651, 32.375984], [79.028178, 32.377095], [79.014742, 32.373994], [78.989834, 32.364098], [78.976192, 32.361618], [78.943429, 32.346373], [78.911286, 32.354745], [78.881521, 32.376268], [78.855682, 32.400452], [78.77145, 32.461663], [78.754707, 32.493806], [78.73693, 32.545792], [78.734966, 32.560649], [78.740651, 32.578684], [78.749126, 32.595867], [78.750986, 32.612791], [78.736827, 32.629844], [78.713262, 32.637518], [78.694452, 32.629663], [78.664893, 32.599019], [78.647736, 32.585971], [78.630166, 32.578013], [78.589859, 32.569925], [78.447542, 32.566256], [78.408165, 32.558376], [78.384807, 32.547549], [78.380983, 32.528041], [78.409198, 32.476623], [78.448782, 32.426782], [78.452193, 32.416782], [78.448162, 32.410658], [78.441444, 32.404948], [78.43638, 32.396163], [78.434519, 32.383761], [78.435966, 32.377973], [78.43886, 32.373296], [78.462735, 32.281803], [78.461391, 32.260151], [78.456327, 32.242116], [78.457981, 32.229662], [78.468541, 32.226668], [78.469017, 32.226533], [78.476481, 32.224417], [78.513068, 32.20757], [78.534978, 32.181344], [78.551825, 32.15039], [78.649287, 32.036444], [78.678329, 32.016755], [78.722151, 31.994586], [78.733829, 31.984664], [78.744578, 31.9642], [78.741994, 31.945338], [78.720187, 31.902912], [78.714192, 31.883016], [78.706648, 31.840797], [78.700343, 31.821676], [78.674091, 31.78695], [78.670887, 31.770517], [78.695589, 31.746694], [78.695175, 31.737961], [78.692592, 31.7284], [78.693212, 31.71853], [78.698069, 31.711037], [78.710472, 31.69817], [78.71843, 31.6806], [78.724011, 31.673313], [78.731349, 31.668042], [78.739824, 31.666027], [78.758117, 31.666595], [78.795324, 31.633781], [78.818889, 31.607374], [78.814031, 31.595023], [78.796978, 31.578849], [78.699516, 31.510016], [78.695382, 31.488105], [78.733003, 31.467744], [78.762561, 31.44511], [78.760081, 31.412037], [78.745508, 31.373538], [78.73848, 31.334988], [78.740857, 31.317728], [78.745095, 31.308116], [78.753156, 31.301966], [78.784472, 31.288272], [78.796048, 31.288117], [78.821989, 31.293698], [78.848654, 31.290908], [78.861987, 31.291476], [78.886972, 31.284171], [78.921008, 31.269685], [78.940282, 31.301576], [78.959552, 31.325117], [78.988801, 31.343256], [79.015529, 31.383397], [79.049937, 31.422953], [79.07021, 31.461381], [79.07799, 31.461664], [79.131397, 31.438433], [79.157693, 31.414926], [79.179218, 31.393796], [79.192442, 31.369789], [79.207628, 31.351373], [79.238319, 31.329656], [79.239239, 31.311724], [79.25411, 31.299006], [79.262024, 31.278513], [79.24843, 31.256704], [79.25286, 31.245437], [79.278316, 31.240044], [79.288321, 31.225474], [79.298078, 31.203242], [79.303209, 31.183104], [79.302162, 31.16345], [79.319417, 31.14228], [79.367048, 31.111559], [79.372343, 31.094072], [79.382593, 31.078335], [79.388363, 31.069474], [79.394978, 31.036711], [79.401592, 31.023637], [79.414408, 31.020382], [79.430738, 31.023172], [79.446344, 31.023792], [79.46164, 31.021105], [79.47642, 31.014025], [79.480761, 31.010046], [79.484275, 31.005447], [79.486858, 31.000228], [79.488202, 30.994492], [79.497607, 30.981211], [79.512697, 30.966173], [79.529646, 30.953564], [79.544219, 30.947414], [79.565923, 30.942454], [79.577809, 30.938371], [79.589488, 30.940231], [79.630312, 30.962504], [79.648192, 30.96576], [79.6885, 30.967413], [79.739039, 30.979247], [79.76064, 30.977232], [79.833504, 30.961522], [79.85035, 30.954494], [79.862856, 30.941523], [79.880426, 30.912843], [79.903164, 30.890209], [79.914635, 30.883819], [79.91937, 30.881182], [79.934893, 30.872535], [80.005586, 30.847266], [80.028634, 30.830781], [80.044447, 30.8067], [80.062534, 30.784789], [80.092196, 30.774505], [80.109249, 30.778122], [80.140669, 30.793212], [80.157102, 30.79316], [80.169297, 30.785254], [80.196066, 30.748098], [80.224591, 30.734094], [80.231412, 30.724947], [80.207021, 30.686138], [80.19989, 30.680712], [80.191725, 30.679679], [80.18201, 30.680041], [80.172915, 30.679162], [80.16661, 30.674408], [80.16692, 30.661902], [80.177255, 30.649862], [80.190588, 30.637976], [80.19989, 30.625884], [80.2033, 30.60635], [80.197823, 30.591364], [80.188418, 30.576998], [80.179839, 30.559479], [80.21622, 30.566818], [80.252806, 30.565009], [80.325877, 30.546509], [80.424992, 30.497829], [80.475429, 30.480466], [80.508295, 30.462328], [80.525141, 30.458607], [80.545088, 30.461036], [80.559971, 30.465118], [80.575267, 30.466255], [80.596351, 30.459796], [80.694743, 30.411737], [80.723062, 30.392048], [80.735361, 30.378199], [80.754998, 30.345797], [80.767194, 30.331431], [80.781663, 30.320993], [80.866981, 30.288488], [80.943772, 30.270143], [80.976018, 30.255209], [80.996275, 30.22689], [81.003303, 30.212731], [80.996017, 30.196969], [80.988162, 30.196556], [80.920827, 30.17666], [80.903309, 30.180433], [80.893647, 30.198104], [80.883879, 30.210405], [80.883684, 30.21028], [80.867911, 30.200173], [80.850393, 30.181931], [80.83644, 30.170046], [80.849669, 30.143381], [80.829825, 30.117129], [80.769674, 30.077287], [80.755721, 30.064574], [80.725749, 30.022768], [80.715827, 30.013311], [80.680171, 29.992072], [80.654126, 29.970575], [80.641413, 29.963444], [80.622293, 29.958173], [80.586223, 29.954142], [80.57134, 29.946855], [80.562865, 29.929699], [80.549533, 29.89368], [80.527312, 29.862416], [80.476152, 29.80614], [80.454861, 29.790586], [80.395227, 29.776582], [80.368975, 29.757926], [80.354299, 29.730279], [80.354402, 29.70488], [80.363911, 29.679714], [80.377553, 29.652946], [80.386752, 29.626823], [80.385201, 29.604835], [80.373316, 29.58419], [80.350785, 29.562073], [80.345101, 29.558352], [80.332698, 29.552461], [80.327221, 29.548585], [80.323603, 29.54166], [80.32505, 29.535511], [80.327634, 29.52993], [80.327221, 29.52484], [80.320089, 29.515822], [80.311511, 29.508122], [80.29146, 29.494738], [80.29115, 29.494609], [80.282055, 29.4843], [80.273064, 29.478667], [80.266242, 29.47213], [80.263452, 29.459237], [80.257457, 29.450064], [80.228312, 29.441718], [80.217666, 29.434639], [80.213739, 29.416888], [80.220974, 29.400119], [80.242161, 29.367408], [80.249499, 29.326997], [80.254977, 29.316635], [80.263555, 29.315318], [80.272754, 29.315705], [80.280092, 29.31015], [80.282779, 29.291314], [80.278748, 29.268085], [80.258181, 29.202456], [80.248672, 29.204394], [80.236063, 29.213076], [80.218803, 29.211086], [80.213739, 29.196565], [80.230482, 29.154733], [80.233066, 29.139308], [80.220664, 29.126079], [80.20144, 29.121169], [80.180563, 29.121324], [80.16351, 29.123366], [80.132607, 29.110214], [80.113073, 29.072206], [80.104702, 29.027609], [80.107906, 28.994769], [80.107906, 28.994588], [80.099431, 28.977276], [80.085168, 28.967484], [80.068425, 28.959939], [80.052922, 28.949113], [80.04052, 28.932809], [80.033905, 28.915936], [80.031115, 28.897798], [80.030288, 28.87767], [80.036386, 28.837026], [80.054782, 28.824185], [80.073054, 28.820925], [80.081861, 28.819353], [80.11421, 28.802584], [80.147593, 28.76331], [80.162063, 28.753285], [80.181183, 28.74729], [80.216426, 28.741942], [80.233273, 28.732718], [80.238957, 28.726258], [80.249396, 28.710213], [80.257044, 28.702745], [80.265209, 28.699283], [80.283399, 28.695356], [80.291047, 28.689671], [80.318332, 28.640113], [80.329701, 28.627479], [80.349751, 28.620218], [80.369078, 28.622647], [80.388302, 28.627246], [80.408352, 28.626342], [80.426543, 28.616807], [80.468814, 28.571849], [80.488451, 28.562444], [80.493412, 28.575647], [80.484937, 28.639287], [80.487624, 28.656469], [80.497546, 28.670137], [80.517596, 28.680033], [80.534444, 28.679555], [80.555837, 28.678948], [80.559041, 28.673031], [80.557387, 28.66435], [80.556871, 28.655022], [80.563589, 28.647038], [80.581365, 28.638951], [80.598522, 28.633783], [80.635625, 28.627866], [80.648338, 28.619753], [80.668182, 28.586344], [80.67862, 28.574717], [80.695777, 28.567508], [80.727093, 28.559679], [80.743526, 28.549964], [80.781663, 28.514746], [80.798406, 28.505703], [80.81701, 28.502551], [80.857989, 28.502447], [80.872252, 28.496866], [80.880365, 28.48188], [80.881864, 28.466842], [80.88667, 28.452786], [80.905273, 28.440849], [80.921344, 28.436198], [80.941188, 28.432684], [80.960515, 28.432271], [80.976018, 28.436922], [80.9878, 28.430979], [80.991314, 28.420256], [80.993175, 28.407828], [81.000409, 28.397002], [81.047066, 28.389088], [81.146344, 28.372249], [81.169701, 28.361319], [81.190372, 28.338116], [81.210732, 28.278637], [81.224323, 28.250783], [81.282356, 28.191588], [81.295947, 28.167455], [81.280754, 28.161977], [81.276775, 28.153787], [81.283079, 28.146087], [81.299409, 28.142521], [81.296205, 28.12831], [81.307057, 28.123814], [81.323697, 28.126243], [81.338115, 28.132728], [81.347262, 28.144433], [81.351447, 28.15681], [81.357907, 28.165982], [81.373203, 28.167843], [81.396044, 28.160634], [81.417335, 28.147017], [81.435267, 28.129783], [81.448237, 28.111412], [81.45294, 28.097046], [81.454025, 28.086607], [81.458211, 28.07728], [81.473455, 28.066402], [81.561925, 28.025991], [81.582183, 28.013278], [81.595102, 27.994881], [81.59536, 27.994778], [81.615049, 27.981239], [81.665433, 27.9708], [81.688946, 27.963204], [81.710392, 27.94752], [81.750131, 27.909667], [81.800154, 27.884087], [81.827852, 27.865639], [81.855654, 27.850937], [81.883198, 27.849051], [81.906091, 27.863107], [81.946398, 27.905352], [81.975905, 27.916953], [82.02722, 27.912406], [82.050212, 27.905586], [82.051611, 27.905171], [82.071662, 27.89003], [82.090369, 27.872331], [82.107422, 27.863572], [82.151037, 27.848275], [82.270409, 27.760477], [82.347924, 27.726009], [82.401926, 27.677175], [82.440683, 27.666426], [82.526156, 27.675211], [82.652143, 27.70415], [82.673891, 27.696458], [82.679687, 27.694409], [82.69705, 27.669397], [82.709246, 27.630924], [82.718857, 27.556123], [82.72971, 27.518166], [82.752137, 27.494964], [82.876264, 27.487548], [82.901275, 27.480365], [82.947164, 27.457317], [83.010468, 27.443391], [83.132786, 27.444217], [83.169631, 27.431195], [83.21924, 27.393781], [83.231591, 27.381224], [83.243115, 27.362155], [83.249419, 27.348099], [83.259496, 27.338126], [83.282441, 27.330943], [83.304868, 27.331925], [83.32435, 27.341691], [83.341145, 27.356936], [83.362746, 27.385616], [83.369567, 27.398174], [83.370962, 27.410214], [83.355821, 27.428663], [83.353857, 27.44029], [83.355925, 27.452486], [83.360989, 27.462201], [83.387034, 27.470469], [83.480981, 27.469746], [83.590432, 27.45662], [83.663399, 27.43228], [83.801995, 27.365928], [83.847988, 27.351045], [83.85334, 27.346194], [83.854602, 27.34505], [83.861733, 27.354507], [83.867521, 27.358383], [83.877391, 27.361742], [83.87765, 27.369907], [83.873671, 27.380087], [83.87119, 27.38944], [83.842717, 27.418069], [83.834087, 27.434037], [83.853517, 27.440962], [83.899871, 27.443856], [83.922043, 27.449696], [83.923022, 27.449954], [83.935941, 27.446078], [83.975835, 27.439722], [84.007668, 27.440807], [84.028907, 27.453726], [84.079601, 27.509536], [84.099548, 27.516874], [84.11717, 27.513283], [84.121563, 27.494964], [84.130968, 27.486411], [84.141716, 27.480753], [84.165746, 27.472174], [84.175099, 27.462976], [84.185848, 27.438636], [84.195253, 27.436053], [84.225536, 27.440393], [84.239023, 27.431143], [84.248893, 27.412436], [84.267703, 27.388562], [84.289408, 27.376108], [84.577039, 27.329031], [84.606546, 27.310479], [84.631919, 27.277044], [84.648197, 27.240716], [84.657654, 27.203405], [84.659824, 27.165113], [84.654398, 27.125374], [84.644528, 27.103721], [84.630369, 27.080829], [84.62148, 27.057988], [84.627268, 27.03649], [84.640239, 27.028377], [84.760697, 26.999025], [84.771962, 26.99918], [84.785501, 27.008482], [84.801934, 27.013753], [84.817489, 27.0106], [84.828134, 26.994943], [84.828289, 26.994839], [84.851751, 26.982127], [84.901567, 26.967192], [84.924046, 26.955668], [84.938567, 26.9366], [84.944251, 26.915723], [84.952726, 26.897636], [84.975774, 26.886887], [84.988176, 26.883787], [85.000837, 26.882236], [85.018045, 26.874433], [85.016857, 26.85924], [85.018562, 26.845804], [85.043987, 26.843737], [85.100211, 26.863529], [85.122845, 26.8657], [85.162016, 26.851024], [85.16553, 26.820793], [85.165685, 26.786273], [85.194934, 26.758885], [85.28728, 26.736974], [85.302369, 26.737336], [85.337044, 26.746792], [85.353219, 26.757593], [85.355352, 26.759912], [85.369238, 26.775008], [85.385878, 26.788444], [85.40319, 26.787617], [85.421535, 26.782656], [85.439622, 26.78772], [85.475692, 26.805238], [85.519462, 26.826426], [85.598578, 26.854383], [85.609379, 26.851024], [85.687772, 26.811853], [85.701828, 26.796608], [85.709631, 26.76245], [85.702241, 26.68845], [85.712887, 26.653206], [85.727046, 26.6376], [85.780996, 26.599204], [85.789988, 26.597034], [85.800065, 26.600703], [85.809728, 26.603028], [85.817635, 26.596724], [85.819547, 26.588301], [85.819753, 26.579516], [85.821614, 26.571713], [85.828538, 26.566131], [85.844765, 26.568509], [85.866366, 26.579929], [85.934682, 26.632897], [85.952148, 26.642044], [85.975713, 26.64437], [86.01137, 26.654447], [86.041704, 26.645455], [86.110692, 26.606749], [86.115859, 26.602563], [86.122061, 26.60029], [86.144798, 26.60153], [86.146355, 26.601356], [86.152653, 26.600651], [86.167794, 26.596621], [86.17446, 26.593313], [86.179266, 26.588611], [86.185364, 26.584425], [86.195855, 26.582668], [86.202883, 26.58458], [86.225155, 26.597396], [86.263086, 26.609075], [86.284428, 26.61202], [86.301378, 26.609023], [86.308613, 26.60215], [86.309026, 26.587991], [86.316002, 26.580963], [86.323185, 26.580084], [86.344838, 26.582616], [86.353674, 26.582616], [86.383647, 26.572849], [86.444832, 26.543084], [86.475476, 26.531973], [86.494906, 26.527839], [86.510616, 26.520139], [86.52431, 26.509081], [86.537746, 26.49487], [86.537953, 26.49487], [86.5579, 26.484018], [86.625337, 26.456319], [86.695204, 26.418234], [86.713601, 26.414565], [86.724091, 26.421954], [86.730706, 26.433788], [86.738199, 26.44371], [86.751893, 26.445519], [86.78724, 26.433065], [86.796438, 26.431514], [86.821863, 26.438129], [86.846357, 26.45265], [86.865684, 26.472442], [86.875968, 26.494921], [86.907129, 26.51151], [86.935153, 26.520289], [86.972448, 26.531973], [86.985212, 26.540655], [87.015443, 26.568974], [87.029912, 26.579774], [87.041333, 26.580187], [87.044433, 26.561171], [87.045002, 26.544272], [87.056474, 26.494921], [87.056681, 26.49487], [87.066706, 26.465621], [87.083294, 26.432083], [87.10629, 26.404746], [87.135022, 26.394204], [87.188455, 26.399578], [87.2191, 26.408105], [87.229435, 26.398752], [87.236308, 26.3833], [87.2453, 26.370226], [87.258425, 26.36294], [87.30049, 26.34599], [87.314029, 26.343768], [87.326225, 26.353328], [87.344932, 26.389346], [87.356404, 26.403712], [87.384206, 26.418647], [87.416452, 26.426967], [87.449628, 26.428621], [87.480324, 26.423401], [87.552051, 26.386711], [87.586984, 26.378029], [87.623984, 26.392912], [87.648745, 26.409993], [87.659641, 26.41751], [87.68119, 26.424228], [87.697572, 26.41627], [87.710904, 26.405676], [87.727544, 26.403764], [87.74222, 26.410482], [87.749455, 26.425727], [87.756173, 26.446914], [87.768988, 26.451461], [87.785938, 26.445984], [87.804439, 26.437354], [87.821595, 26.437509], [87.852084, 26.46066], [87.869809, 26.464639], [87.870688, 26.460712], [87.894769, 26.442935], [87.897301, 26.443452], [87.902004, 26.435287], [87.908515, 26.41782], [87.914768, 26.40826], [87.928979, 26.396426], [87.961225, 26.378959], [87.975488, 26.366557], [88.006648, 26.369864], [88.044321, 26.405676], [88.074189, 26.453942], [88.082251, 26.49487], [88.07915, 26.507375], [88.079822, 26.517556], [88.087136, 26.5391], [88.101681, 26.581944], [88.14664, 26.661216], [88.163383, 26.705141], [88.167517, 26.725037], [88.169067, 26.744002], [88.167827, 26.762915], [88.159042, 26.802551], [88.155321, 26.845598], [88.151394, 26.862806], [88.142816, 26.878412], [88.12044, 26.90885], [88.11181, 26.924301], [88.096824, 26.959337], [88.076877, 26.99179], [88.055948, 27.018042], [88.042822, 27.028946], [88.027371, 27.035353], [88.009491, 27.045637], [87.9913, 27.081501], [87.975488, 27.095143], [87.970733, 27.10274], [87.969183, 27.110801], [87.970837, 27.119224], [87.985461, 27.14837], [87.989337, 27.218391], [88.004663, 27.249163], [88.029024, 27.298076], [88.035122, 27.322571], [88.032538, 27.333888], [88.020136, 27.35337], [88.015795, 27.364222], [88.014762, 27.378072], [88.017242, 27.389234], [88.02582, 27.412075], [88.029593, 27.418741], [88.034812, 27.423805], [88.039308, 27.42949], [88.041633, 27.438068], [88.039773, 27.442357], [88.030781, 27.450729], [88.028094, 27.455121], [88.02334, 27.474655], [88.0221, 27.484241], [88.02334, 27.494912], [88.048765, 27.545348], [88.11088, 27.639503], [88.134858, 27.723167], [88.149327, 27.748772], [88.159662, 27.77412], [88.154598, 27.815151], [88.166587, 27.833599], [88.164623, 27.845356], [88.156407, 27.851273], [88.143023, 27.855717], [88.118218, 27.860885], [88.104885, 27.879721], [88.097496, 27.904008], [88.099924, 27.928322], [88.115737, 27.947262], [88.126693, 27.95044], [88.151187, 27.94721], [88.16297, 27.9469], [88.174855, 27.949768], [88.197851, 27.958295], [88.37877, 27.982634], [88.399751, 27.994726], [88.399854, 27.994726], [88.399906, 27.994881], [88.455768, 28.03152], [88.475405, 28.036248], [88.50207, 28.028885], [88.51716, 28.039582], [88.530802, 28.059012], [88.552817, 28.078055], [88.594003, 28.106606], [88.610488, 28.105831], [88.631675, 28.083352], [88.652035, 28.069399], [88.71012, 28.061983], [88.735648, 28.055265], [88.780141, 28.028342], [88.802931, 28.011005], [88.817503, 27.994881], [88.81771, 27.994881], [88.81771, 27.994726], [88.819674, 27.977337], [88.809752, 27.944497], [88.810682, 27.927857], [88.818899, 27.915196], [88.842773, 27.892407], [88.851558, 27.877163], [88.855072, 27.859179], [88.853884, 27.843676], [88.805669, 27.655109], [88.783087, 27.622036], [88.767377, 27.586198], [88.748154, 27.560128], [88.741022, 27.54571], [88.741642, 27.53168], [88.757559, 27.511448], [88.757197, 27.494964], [88.754355, 27.464216], [88.759781, 27.435071], [88.773578, 27.408509], [88.795386, 27.385926], [88.808202, 27.378382], [88.819829, 27.373627], [88.830836, 27.367323], [88.852023, 27.342363], [88.864632, 27.33239], [88.892331, 27.315543], [88.884631, 27.286346], [88.876116, 27.280486], [88.861428, 27.270378], [88.801174, 27.256425], [88.775852, 27.240871], [88.754355, 27.212604], [88.738438, 27.179789], [88.730067, 27.150954], [88.733082, 27.148972], [88.742572, 27.142737], [88.805256, 27.113023], [88.827632, 27.097882], [88.840809, 27.075248], [88.845615, 27.049565], [88.845615, 26.994943], [88.845615, 26.994839], [88.851713, 26.945437], [88.867113, 26.964247], [88.88644, 26.978975], [88.906645, 26.981093], [88.924474, 26.961663], [88.926954, 26.949416], [88.925404, 26.939029], [88.9253, 26.929365], [88.932328, 26.919237], [88.942767, 26.913707], [88.954549, 26.912622], [88.965815, 26.915464], [88.975323, 26.921717], [88.996614, 26.922751], [89.021574, 26.912674], [89.04488, 26.897119], [89.060693, 26.881461], [89.074335, 26.856398], [89.082448, 26.836037], [89.096504, 26.821413], [89.128079, 26.813455], [89.184613, 26.810561], [89.212828, 26.813041], [89.240837, 26.819759], [89.252619, 26.826787], [89.262954, 26.836348], [89.273599, 26.843892], [89.286364, 26.844978], [89.300161, 26.844409], [89.341657, 26.854331], [89.368895, 26.837359], [89.407338, 26.813403], [89.442891, 26.797022], [89.50542, 26.803688], [89.546503, 26.797539], [89.586087, 26.784051], [89.611357, 26.765964], [89.613475, 26.748549], [89.597507, 26.720954], [89.609806, 26.712221], [89.628307, 26.712531], [89.66324, 26.725502], [89.685151, 26.724571], [89.738739, 26.703281], [89.75467, 26.700989], [89.760288, 26.70018], [89.800079, 26.70049], [89.804058, 26.699767], [89.81367, 26.696149], [89.817287, 26.696201], [89.822093, 26.701007], [89.825245, 26.707673], [89.826796, 26.713513], [89.826744, 26.715683], [89.827261, 26.717905], [89.827674, 26.722453], [89.829741, 26.72762], [89.834961, 26.731548], [89.839043, 26.731238], [89.847001, 26.72483], [89.850774, 26.723176], [89.858701, 26.722057], [89.85992, 26.721884], [89.880281, 26.716252], [89.890409, 26.714856], [89.912113, 26.716717], [89.975262, 26.731858], [90.089157, 26.741728], [90.127191, 26.751392], [90.152202, 26.771752], [90.17742, 26.832058], [90.210907, 26.851489], [90.229148, 26.852884], [90.265994, 26.851592], [90.284442, 26.85707], [90.300875, 26.868284], [90.314208, 26.880376], [90.328574, 26.890659], [90.348831, 26.896654], [90.382627, 26.891796], [90.475232, 26.83242], [90.587783, 26.780072], [90.717077, 26.767049], [90.944493, 26.77887], [91.007395, 26.782139], [91.012781, 26.784348], [91.062069, 26.804567], [91.091938, 26.804773], [91.127078, 26.800898], [91.198185, 26.802344], [91.232498, 26.795161], [91.261954, 26.779038], [91.27601, 26.774077], [91.296577, 26.774594], [91.313217, 26.778677], [91.330063, 26.785498], [91.345566, 26.794696], [91.358382, 26.806169], [91.370061, 26.824669], [91.378122, 26.842807], [91.388457, 26.858465], [91.406338, 26.869524], [91.420187, 26.871539], [91.460598, 26.869369], [91.475067, 26.865545], [91.484472, 26.852729], [91.507417, 26.808029], [91.520646, 26.79759], [91.539353, 26.79883], [91.57563, 26.810148], [91.593716, 26.810768], [91.637951, 26.798985], [91.653764, 26.797952], [91.702133, 26.803481], [91.731589, 26.816194], [91.795254, 26.853504], [91.825123, 26.858465], [91.843934, 26.84937], [91.849515, 26.834746], [91.852408, 26.818519], [91.863054, 26.804773], [91.87866, 26.803016], [91.886928, 26.814437], [91.885895, 26.83087], [91.874009, 26.844306], [91.895507, 26.853504], [91.895507, 26.868284], [91.89313, 26.881358], [91.908219, 26.885182], [91.925686, 26.878671], [91.957415, 26.854279], [91.975088, 26.846631], [92.03586, 26.854848], [92.072757, 26.887766], [92.080371, 26.921571], [92.083919, 26.937323], [92.066969, 26.994839], [92.066969, 26.994943], [92.049709, 27.026827], [91.999893, 27.071527], [91.987697, 27.104031], [91.987904, 27.122273], [91.990798, 27.140773], [91.995862, 27.158033], [92.002994, 27.172606], [92.005788, 27.176112], [92.008471, 27.179479], [92.021494, 27.19183], [92.027075, 27.19984], [92.029969, 27.20847], [92.033173, 27.227435], [92.03679, 27.236478], [92.050639, 27.251413], [92.081852, 27.275132], [92.088776, 27.29234], [92.084849, 27.304226], [92.023598, 27.44678], [91.975088, 27.472433], [91.963306, 27.468919], [91.900591, 27.45973], [91.856578, 27.466239], [91.816232, 27.461357], [91.779555, 27.456475], [91.727092, 27.459942], [91.727041, 27.460005], [91.704924, 27.468764], [91.680223, 27.472846], [91.657382, 27.479151], [91.641672, 27.494964], [91.632887, 27.511655], [91.604465, 27.532145], [91.59506, 27.546382], [91.573252, 27.619711], [91.579867, 27.657977], [91.626686, 27.716423], [91.634957, 27.746571], [91.63577, 27.782525], [91.605336, 27.810658], [91.566023, 27.821074], [91.564355, 27.851372], [91.628339, 27.852694], [91.680131, 27.849823], [91.729871, 27.804136], [91.826589, 27.807627], [91.856373, 27.764539], [91.864914, 27.729988], [91.908736, 27.7319], [91.952247, 27.72482], [91.968001, 27.746278], [91.988898, 27.769398], [92.064878, 27.761721], [92.10616, 27.786081], [92.126294, 27.812722], [92.239568, 27.865277], [92.24918, 27.862642], [92.258275, 27.848198], [92.275225, 27.811689], [92.288764, 27.793085], [92.303854, 27.78616], [92.31729, 27.803679], [92.329175, 27.832928], [92.334653, 27.831274], [92.339821, 27.815564], [92.350053, 27.802542], [92.367933, 27.803834], [92.375581, 27.821119], [92.381058, 27.842462], [92.392841, 27.856027], [92.404416, 27.853831], [92.417335, 27.828948], [92.427567, 27.821171], [92.439039, 27.82329], [92.475109, 27.846596], [92.518151, 27.839468], [92.574977, 27.847806], [92.592912, 27.868193], [92.636468, 27.89537], [92.683674, 27.91045], [92.71077, 27.949702], [92.736392, 27.985909], [92.701349, 28.025241], [92.701349, 28.037825], [92.689257, 28.048315], [92.65422, 28.052087], [92.638924, 28.057487], [92.63727, 28.071983], [92.65484, 28.105831], [92.678611, 28.133064], [92.70786, 28.155414], [92.770079, 28.192182], [92.779174, 28.195903], [92.782894, 28.190942], [92.785375, 28.183294], [92.790232, 28.178901], [92.805322, 28.178023], [92.819895, 28.179263], [92.834777, 28.183526], [92.85028, 28.19195], [92.86537, 28.205205], [92.889554, 28.235823], [92.903817, 28.249698], [92.922007, 28.258638], [92.960351, 28.270214], [92.974924, 28.282306], [93.046399, 28.302097], [93.129828, 28.325962], [93.207825, 28.340539], [93.218073, 28.394647], [93.186286, 28.431995], [93.218838, 28.457436], [93.259068, 28.496023], [93.286972, 28.520517], [93.302843, 28.556921], [93.31717, 28.603463], [93.355432, 28.624065], [93.446213, 28.671894], [93.551736, 28.678948], [93.607133, 28.672204], [93.62553, 28.67236], [93.64341, 28.68024], [93.664908, 28.690239], [93.705525, 28.691893], [93.722475, 28.696647], [93.781744, 28.680504], [93.86308, 28.704871], [93.927786, 28.682752], [93.961093, 28.729943], [93.981591, 28.768131], [93.993136, 28.801887], [93.992019, 28.844649], [94.01145, 28.85302], [94.026849, 28.864182], [94.079474, 28.883072], [94.135396, 28.897788], [94.173658, 28.930164], [94.250183, 28.933107], [94.291389, 28.977256], [94.347311, 29.024349], [94.309048, 29.059668], [94.270786, 29.09793], [94.287505, 29.147938], [94.323782, 29.146026], [94.335771, 29.149617], [94.346313, 29.159074], [94.36254, 29.185326], [94.372875, 29.196307], [94.396439, 29.207081], [94.422381, 29.210492], [94.474987, 29.210802], [94.498242, 29.21522], [94.514778, 29.22106], [94.528628, 29.23124], [94.582681, 29.302915], [94.599941, 29.316635], [94.63043, 29.319452], [94.668464, 29.30661], [94.704224, 29.284699], [94.756417, 29.230517], [94.767683, 29.213851], [94.761482, 29.174706], [94.776571, 29.166696], [94.798792, 29.166438], [94.815432, 29.168867], [94.854086, 29.170004], [94.891293, 29.160495], [94.989065, 29.153984], [95.047666, 29.140806], [95.099032, 29.11378], [95.116912, 29.108095], [95.191533, 29.09683], [95.199698, 29.094246], [95.207863, 29.089595], [95.212824, 29.081869], [95.214064, 29.073084], [95.216338, 29.064997], [95.224916, 29.059364], [95.281553, 29.052672], [95.367406, 29.036496], [95.429824, 29.046525], [95.487577, 29.068994], [95.511307, 29.131789], [95.521435, 29.137861], [95.522675, 29.161193], [95.511927, 29.197547], [95.515337, 29.209433], [95.550167, 29.212482], [95.552028, 29.22013], [95.550684, 29.230284], [95.553785, 29.239043], [95.56443, 29.245606], [95.572905, 29.24726], [95.58169, 29.247467], [95.592335, 29.249689], [95.646697, 29.22835], [95.717165, 29.218284], [95.747365, 29.273651], [95.744781, 29.340432], [95.776407, 29.345548], [95.79594, 29.352886], [95.863134, 29.323985], [95.953735, 29.359219], [96.014135, 29.364252], [96.074536, 29.369286], [96.141966, 29.368467], [96.150957, 29.354075], [96.166047, 29.303174], [96.176279, 29.286456], [96.205424, 29.256949], [96.235293, 29.241214], [96.26785, 29.24173], [96.304126, 29.261212], [96.323247, 29.27501], [96.337096, 29.279713], [96.349912, 29.274235], [96.366241, 29.257233], [96.366862, 29.244211], [96.342367, 29.210647], [96.327484, 29.180442], [96.316425, 29.171864], [96.210075, 29.145767], [96.193642, 29.136931], [96.183514, 29.123779], [96.174625, 29.108845], [96.175954, 29.017354], [96.195323, 28.941106], [96.248588, 28.945343], [96.294655, 28.992324], [96.366568, 29.036537], [96.457605, 28.994588], [96.451198, 28.981385], [96.452128, 28.970378], [96.460086, 28.96335], [96.474659, 28.962058], [96.492229, 28.948002], [96.500394, 28.929243], [96.510832, 28.885525], [96.523751, 28.864415], [96.576668, 28.808527], [96.592584, 28.757884], [96.59801, 28.70991], [96.501044, 28.687631], [96.502375, 28.644476], [96.409019, 28.558929], [96.336459, 28.479667], [96.301852, 28.420711], [96.369644, 28.378116], [96.441127, 28.395416], [96.495226, 28.42147], [96.512382, 28.428808], [96.561518, 28.447293], [96.604426, 28.444534], [96.716924, 28.436539], [96.897889, 28.354704], [96.933959, 28.336618], [96.965791, 28.330468], [96.997417, 28.337083], [97.032971, 28.35703], [97.078446, 28.375143], [97.11555, 28.366564], [97.182729, 28.314991], [97.193271, 28.3114], [97.20278, 28.311555], [97.210531, 28.308092], [97.220763, 28.283391], [97.229651, 28.274606], [97.285255, 28.235668], [97.323496, 28.217478], [97.328663, 28.190012], [97.298898, 28.12924], [97.292903, 28.095082], [97.306546, 28.069657], [97.355122, 28.023045], [97.362253, 27.994881], [97.356775, 27.980567], [97.340032, 27.951809], [97.335691, 27.935609], [97.336311, 27.913181], [97.338172, 27.901554], [97.333521, 27.894164], [97.314917, 27.884604], [97.292903, 27.87755], [97.288666, 27.884346], [97.288046, 27.897756], [97.276367, 27.910339], [97.260657, 27.912044], [97.241227, 27.907858], [97.222933, 27.899719], [97.210944, 27.889513], [97.198025, 27.873261], [97.182109, 27.857267], [97.131776, 27.816856], [97.123198, 27.812154], [97.112449, 27.809208], [97.106041, 27.805539], [97.098186, 27.791018], [97.092192, 27.785489], [97.072348, 27.779184], [97.06532, 27.773991], [97.062013, 27.763345], [97.060359, 27.750452], [97.056742, 27.746783], [97.050541, 27.746783], [97.036175, 27.74292], [97.004032, 27.734277], [96.990906, 27.726629], [96.957006, 27.688079], [96.93954, 27.674023], [96.899749, 27.649476], [96.883523, 27.636325], [96.87019, 27.61909], [96.862129, 27.599298], [96.862025, 27.578369], [96.866159, 27.568448], [96.877321, 27.553074], [96.880319, 27.54385], [96.881042, 27.534393], [96.884659, 27.515841], [96.888484, 27.507185], [96.890241, 27.492483], [96.877011, 27.463079], [96.881352, 27.444114], [96.905123, 27.408406], [97.067904, 27.2171], [97.095499, 27.191261], [97.101287, 27.183303], [97.1017, 27.165268], [97.104388, 27.156018], [97.111726, 27.148835], [97.120821, 27.143926], [97.129296, 27.13788], [97.134773, 27.127338], [97.133016, 27.119586], [97.122888, 27.093696], [97.119167, 27.087288], [97.106041, 27.082586], [97.09798, 27.085893], [97.089918, 27.092198], [97.077206, 27.096487], [97.066974, 27.095246], [97.058827, 27.092076], [97.047854, 27.087805], [97.038035, 27.08703], [97.02677, 27.091474], [97.018915, 27.098709], [97.011887, 27.107029], [97.002998, 27.114522], [96.865332, 27.171624], [96.842698, 27.188522], [96.841768, 27.204387], [96.851276, 27.221182], [96.859648, 27.240716], [96.853757, 27.248725], [96.821718, 27.273065], [96.809935, 27.285777], [96.789161, 27.317972], [96.776346, 27.330633], [96.758879, 27.341743], [96.724152, 27.356264], [96.705239, 27.36076], [96.687255, 27.361742], [96.675887, 27.357659], [96.659247, 27.341433], [96.649015, 27.336214], [96.636406, 27.335542], [96.629275, 27.338694], [96.623177, 27.343655], [96.614185, 27.348409], [96.5869, 27.353577], [96.576048, 27.345412], [96.56747, 27.328307], [96.547316, 27.306448], [96.531813, 27.298542], [96.510936, 27.292495], [96.490058, 27.290738], [96.474659, 27.295389], [96.407789, 27.29818], [96.142586, 27.25751], [96.074166, 27.229864], [96.013188, 27.190796], [95.974844, 27.142892], [95.938361, 27.079899], [95.91645, 27.051373], [95.888855, 27.027034], [95.861466, 27.014218], [95.804932, 27.004761], [95.777957, 26.995149], [95.734962, 26.947297], [95.709227, 26.907454], [95.699305, 26.896189], [95.686076, 26.89128], [95.639981, 26.886422], [95.624271, 26.880738], [95.613316, 26.867922], [95.603394, 26.844254], [95.591302, 26.823739], [95.574765, 26.816142], [95.554405, 26.816607], [95.531357, 26.820276], [95.489809, 26.810716], [95.462731, 26.777178], [95.439993, 26.735372], [95.410641, 26.701627], [95.392658, 26.691757], [95.31597, 26.669071], [95.278969, 26.652948], [95.260573, 26.647625], [95.246933, 26.648904], [95.239075, 26.649641], [95.223262, 26.670363], [95.204865, 26.667572], [95.195564, 26.660751], [95.181611, 26.641682], [95.173239, 26.633828], [95.163421, 26.629745], [95.141407, 26.624991], [95.132622, 26.620805], [95.122803, 26.611607], [95.119082, 26.604217], [95.115879, 26.57564], [95.116189, 26.569335], [95.114948, 26.562566], [95.109574, 26.5513], [95.099239, 26.536107], [95.069887, 26.506239], [95.054384, 26.494921], [95.040638, 26.475594], [95.042601, 26.459678], [95.061722, 26.426605], [95.066062, 26.407433], [95.064202, 26.392602], [95.050456, 26.359064], [95.043635, 26.327696], [95.041775, 26.287596], [95.046322, 26.24796], [95.058724, 26.217677], [95.066373, 26.211011], [95.074537, 26.208324], [95.082806, 26.206619], [95.090764, 26.202691], [95.094484, 26.195043], [95.08973, 26.187602], [95.082392, 26.179798], [95.078361, 26.171324], [95.08818, 26.104093], [95.101719, 26.093395], [95.137479, 26.082957], [95.150088, 26.070968], [95.151225, 26.049936], [95.139546, 26.029937], [95.10823, 25.995107], [95.065856, 25.953817], [95.049423, 25.943741], [95.012009, 25.931442], [95.00064, 25.922036], [94.994026, 25.900436], [94.993199, 25.880954], [95.013456, 25.777704], [95.015316, 25.755122], [95.008805, 25.737138], [94.999917, 25.731557], [94.978626, 25.729077], [94.969014, 25.725614], [94.961469, 25.717966], [94.871656, 25.598025], [94.868038, 25.594718], [94.857496, 25.589344], [94.853466, 25.586036], [94.852949, 25.580765], [94.86101, 25.571412], [94.860287, 25.567174], [94.850778, 25.562782], [94.831038, 25.562472], [94.822356, 25.559113], [94.80799, 25.542628], [94.782359, 25.504594], [94.764375, 25.491675], [94.745772, 25.486611], [94.711149, 25.482787], [94.692028, 25.476327], [94.659472, 25.455502], [94.65036, 25.44665], [94.630533, 25.42739], [94.608003, 25.394627], [94.550125, 25.245282], [94.553329, 25.204147], [94.576893, 25.173607], [94.612964, 25.161359], [94.653271, 25.154125], [94.689135, 25.138518], [94.706808, 25.109941], [94.713836, 25.068264], [94.708565, 25.02589], [94.689755, 24.99509], [94.673528, 24.974807], [94.654821, 24.889154], [94.639112, 24.862799], [94.599011, 24.813474], [94.593223, 24.783786], [94.593223, 24.765931], [94.589502, 24.74725], [94.581544, 24.73012], [94.569452, 24.716399], [94.555706, 24.708855], [94.543407, 24.70733], [94.531005, 24.707485], [94.516329, 24.704953], [94.507957, 24.689011], [94.48708, 24.620462], [94.474987, 24.597389], [94.430236, 24.570904], [94.418453, 24.560311], [94.410805, 24.545945], [94.397059, 24.495069], [94.381866, 24.48631], [94.370394, 24.477732], [94.363676, 24.46582], [94.361196, 24.425409], [94.352204, 24.413937], [94.340009, 24.406005], [94.328743, 24.394481], [94.32833, 24.388487], [94.33174, 24.371098], [94.330707, 24.362855], [94.323679, 24.354664], [94.302802, 24.341022], [94.29536, 24.332599], [94.289986, 24.31676], [94.286885, 24.283067], [94.282751, 24.266427], [94.233969, 24.160335], [94.222186, 24.122637], [94.218466, 24.08667], [94.214125, 24.069178], [94.203686, 24.057757], [94.199139, 24.048094], [94.193868, 24.009673], [94.19025, 23.995281], [94.144878, 23.938876], [94.13506, 23.919006], [94.133613, 23.899162], [94.13506, 23.877303], [94.131856, 23.856969], [94.128859, 23.853905], [94.11687, 23.841647], [94.099713, 23.842628], [94.085347, 23.857511], [94.071084, 23.876115], [94.055065, 23.887949], [93.997911, 23.916965], [93.97507, 23.920918], [93.94055, 23.928876], [93.912334, 23.939005], [93.883189, 23.94531], [93.803711, 23.936059], [93.782833, 23.945465], [93.743042, 23.995281], [93.711106, 24.005461], [93.660464, 24.011223], [93.612404, 24.009052], [93.588013, 23.995229], [93.585636, 23.987297], [93.581812, 23.979597], [93.576954, 23.973008], [93.571063, 23.968021], [93.561348, 23.965696], [93.55525, 23.970993], [93.549979, 23.978021], [93.543468, 23.980889], [93.526828, 23.975385], [93.495719, 23.959133], [93.474842, 23.957505], [93.456652, 23.95996], [93.449004, 23.968926], [93.445593, 23.981612], [93.439185, 23.995229], [93.406526, 24.025847], [93.3965, 24.037526], [93.368285, 24.07848], [93.348545, 24.088893], [93.32219, 24.080056], [93.309684, 24.063984], [93.302966, 24.041143], [93.302559, 24.035775], [93.301106, 24.016571], [93.302966, 23.995281], [93.308134, 23.977504], [93.316092, 23.961097], [93.337589, 23.928411], [93.345961, 23.918696], [93.351852, 23.913916], [93.355986, 23.906991], [93.35919, 23.890946], [93.35826, 23.855263], [93.374796, 23.739017], [93.406319, 23.719122], [93.412313, 23.708709], [93.425956, 23.690803], [93.430297, 23.680339], [93.432364, 23.647317], [93.400221, 23.454022], [93.399808, 23.426091], [93.405905, 23.36767], [93.407766, 23.356379], [93.407249, 23.347258], [93.403735, 23.338757], [93.384201, 23.313126], [93.377897, 23.296822], [93.37645, 23.279485], [93.380791, 23.237265], [93.372729, 23.169827], [93.373039, 23.154221], [93.37521, 23.142154], [93.3749, 23.129726], [93.333352, 23.052289], [93.318262, 23.03366], [93.298832, 23.01795], [93.272787, 23.00591], [93.246535, 23.004488], [93.225555, 23.020379], [93.212119, 23.039887], [93.196512, 23.053839], [93.177082, 23.058439], [93.152381, 23.049938], [93.140082, 23.031903], [93.138221, 23.005264], [93.143182, 22.977513], [93.151141, 22.956223], [93.171088, 22.920437], [93.177392, 22.900205], [93.175532, 22.881705], [93.161062, 22.860983], [93.125302, 22.821554], [93.115897, 22.798403], [93.112383, 22.799953], [93.079517, 22.77272], [93.070835, 22.69288], [93.073936, 22.671899], [93.081894, 22.65283], [93.104425, 22.615933], [93.110523, 22.596348], [93.109076, 22.574696], [93.098224, 22.535266], [93.099154, 22.51196], [93.109593, 22.479042], [93.117861, 22.462868], [93.153518, 22.427521], [93.16592, 22.386593], [93.174292, 22.269391], [93.173672, 22.259418], [93.169021, 22.246912], [93.161269, 22.242519], [93.151451, 22.240969], [93.129747, 22.231616], [93.124786, 22.232287], [93.123545, 22.230686], [93.123132, 22.218387], [93.141735, 22.187277], [93.119515, 22.180973], [93.104322, 22.187174], [93.090576, 22.197148], [93.072489, 22.20247], [93.045824, 22.206863], [93.036419, 22.206243], [93.024533, 22.201902], [93.022363, 22.196217], [93.023913, 22.188828], [93.02257, 22.120976], [93.025464, 22.112915], [93.00748, 22.105939], [92.995181, 22.103251], [92.985466, 22.095965], [92.967172, 22.062685], [92.965312, 22.04956], [92.968413, 22.036175], [92.974924, 22.023308], [92.977921, 22.016487], [92.979575, 22.009355], [92.979885, 22.002276], [92.978955, 21.995351], [92.977094, 21.993026], [92.974924, 21.990907], [92.968103, 21.987238], [92.961695, 21.986876], [92.955597, 21.989615], [92.950326, 21.995351], [92.937407, 22.013024], [92.920974, 22.021809], [92.906194, 22.017314], [92.89865, 21.995403], [92.889244, 21.966722], [92.877462, 21.957007], [92.867024, 21.966516], [92.862063, 21.995403], [92.853898, 22.024135], [92.836121, 22.046511], [92.771629, 22.104182], [92.708067, 22.147952], [92.683779, 22.154308], [92.67396, 22.147073], [92.671067, 22.133896], [92.674684, 22.105577], [92.67396, 22.095242], [92.669516, 22.092658], [92.664555, 22.093846], [92.662075, 22.094777], [92.656184, 22.075294], [92.65608, 22.060567], [92.658354, 22.046562], [92.658354, 22.032816], [92.650913, 22.019174], [92.639854, 22.013386], [92.627555, 22.012353], [92.615256, 22.008787], [92.603887, 21.995403], [92.597996, 21.989305], [92.591278, 21.984241], [92.583837, 21.980313], [92.575879, 21.977574], [92.575155, 21.986566], [92.573088, 21.995403], [92.559962, 22.061342], [92.537328, 22.128625], [92.549317, 22.138495], [92.5642, 22.13834], [92.575568, 22.143301], [92.577015, 22.168674], [92.518931, 22.495372], [92.517381, 22.51258], [92.504565, 22.543793], [92.500844, 22.560536], [92.502911, 22.618621], [92.495883, 22.695618], [92.491543, 22.71107], [92.480691, 22.726159], [92.453715, 22.747967], [92.442037, 22.762927], [92.435009, 22.793287], [92.426327, 22.87106], [92.411858, 22.887932], [92.378681, 22.900438], [92.359871, 22.926612], [92.352223, 22.960331], [92.35305, 23.029913], [92.327625, 23.171429], [92.328555, 23.211194], [92.332689, 23.226258], [92.350673, 23.260984], [92.357494, 23.278942], [92.356874, 23.289122], [92.352223, 23.298837], [92.347055, 23.315426], [92.346745, 23.325296], [92.349536, 23.344959], [92.348606, 23.354105], [92.344885, 23.359118], [92.331656, 23.368652], [92.326695, 23.375267], [92.325351, 23.382166], [92.326901, 23.396015], [92.325351, 23.403508], [92.31977, 23.412164], [92.306748, 23.423869], [92.301477, 23.431801], [92.296826, 23.44658], [92.291038, 23.495285], [92.252797, 23.609309], [92.250007, 23.642615], [92.261686, 23.685119], [92.259737, 23.702641], [92.259309, 23.706487], [92.238741, 23.716848], [92.222102, 23.707779], [92.209389, 23.661606], [92.1943, 23.648144], [92.180761, 23.665378], [92.170632, 23.703645], [92.150788, 23.731653], [92.108414, 23.718243], [92.092187, 23.701345], [92.059631, 23.659487], [92.042371, 23.64556], [92.019013, 23.640057], [92.00351, 23.647421], [91.961652, 23.691139], [91.949147, 23.709768], [91.936641, 23.72354], [91.922688, 23.722972], [91.916487, 23.709872], [91.91442, 23.688323], [91.915971, 23.65016], [91.942325, 23.53443], [91.940775, 23.495182], [91.918141, 23.459939], [91.905118, 23.446632], [91.889099, 23.435935], [91.833805, 23.413895], [91.817682, 23.400924], [91.791637, 23.368342], [91.762595, 23.321549], [91.743681, 23.272327], [91.749159, 23.232407], [91.763938, 23.200626], [91.779028, 23.131767], [91.791947, 23.101149], [91.795564, 23.089496], [91.791327, 23.080375], [91.776237, 23.06526], [91.773757, 23.064898], [91.764248, 23.065647], [91.760941, 23.065492], [91.75598, 23.061539], [91.755567, 23.056837], [91.75629, 23.05216], [91.75505, 23.048233], [91.738307, 23.024668], [91.731279, 23.01733], [91.714432, 23.003119], [91.706991, 22.995368], [91.694692, 22.988133], [91.667614, 22.982629], [91.654074, 22.978056], [91.646943, 22.976583], [91.639915, 22.978004], [91.632784, 22.980304], [91.625756, 22.981544], [91.616661, 22.978056], [91.61201, 22.969839], [91.608289, 22.960331], [91.602398, 22.952967], [91.586068, 22.944466], [91.582968, 22.947903], [91.583071, 22.957799], [91.576766, 22.968806], [91.563124, 22.974981], [91.549791, 22.977229], [91.536562, 22.981854], [91.523643, 22.995213], [91.519406, 23.005625], [91.518165, 23.035081], [91.515582, 23.038802], [91.506073, 23.041179], [91.503283, 23.044408], [91.498115, 23.069962], [91.475067, 23.143085], [91.470416, 23.153291], [91.463595, 23.184116], [91.461218, 23.184426], [91.446438, 23.197887], [91.445405, 23.203029], [91.446748, 23.208791], [91.446438, 23.215535], [91.439927, 23.223519], [91.439617, 23.22264], [91.436207, 23.215716], [91.436517, 23.214863], [91.428145, 23.229513], [91.413572, 23.248634], [91.394349, 23.262664], [91.372128, 23.261889], [91.360966, 23.247523], [91.355695, 23.224139], [91.355901, 23.179646], [91.374608, 23.095723], [91.370887, 23.062779], [91.337401, 23.071074], [91.313527, 23.101046], [91.300504, 23.142309], [91.27725, 23.303256], [91.272909, 23.313488], [91.267535, 23.321549], [91.265778, 23.329352], [91.272909, 23.338499], [91.282934, 23.347646], [91.287275, 23.355552], [91.283968, 23.361443], [91.270945, 23.364596], [91.25813, 23.373639], [91.247898, 23.393767], [91.234462, 23.436271], [91.229707, 23.461179], [91.22733, 23.468775], [91.220819, 23.477896], [91.203352, 23.487921], [91.195084, 23.495182], [91.185886, 23.511563], [91.177514, 23.544275], [91.169866, 23.561974], [91.140824, 23.6121], [91.136276, 23.629515], [91.139687, 23.65357], [91.152296, 23.655095], [91.166456, 23.654087], [91.175241, 23.670313], [91.168936, 23.685377], [91.1524, 23.69101], [91.136587, 23.69889], [91.132349, 23.720646], [91.141754, 23.739896], [91.157567, 23.744986], [91.176584, 23.746536], [91.194981, 23.755166], [91.205213, 23.771573], [91.225263, 23.826144], [91.228157, 23.84498], [91.224023, 23.857485], [91.212758, 23.88074], [91.211931, 23.89446], [91.216272, 23.908387], [91.250378, 23.964042], [91.26092, 23.977271], [91.274149, 23.988227], [91.291616, 23.994816], [91.304948, 23.99386], [91.308433, 23.992501], [91.316214, 23.989467], [91.327066, 23.987865], [91.339055, 23.995229], [91.349287, 24.036053], [91.350837, 24.074242], [91.363033, 24.099848], [91.404581, 24.103129], [91.480442, 24.088014], [91.517545, 24.085198], [91.55868, 24.089668], [91.581831, 24.096101], [91.596507, 24.10499], [91.606015, 24.118865], [91.613767, 24.140104], [91.624515, 24.203459], [91.629063, 24.215319], [91.639502, 24.211702], [91.657175, 24.171368], [91.666167, 24.155917], [91.68415, 24.145478], [91.703684, 24.142946], [91.72115, 24.148992], [91.732623, 24.164314], [91.732002, 24.181678], [91.725801, 24.202064], [91.723011, 24.220977], [91.732933, 24.234284], [91.796598, 24.221598], [91.807037, 24.221133], [91.809724, 24.214544], [91.811377, 24.192039], [91.813755, 24.183124], [91.844244, 24.155323], [91.864501, 24.150594], [91.87711, 24.158036], [91.884345, 24.175554], [91.905945, 24.260639], [91.906875, 24.280354], [91.896437, 24.320119], [91.899847, 24.33805], [91.921862, 24.34451], [91.930957, 24.336965], [91.953074, 24.322987], [91.972194, 24.316062], [91.971368, 24.329679], [91.948837, 24.363527], [91.949974, 24.375387], [92.033379, 24.36885], [92.062732, 24.37102], [92.08826, 24.38164], [92.107587, 24.405979], [92.111204, 24.434298], [92.105933, 24.49525], [92.110894, 24.514293], [92.123193, 24.525378], [92.138593, 24.533723], [92.14998, 24.542183], [92.153165, 24.544549], [92.163604, 24.5592], [92.168875, 24.574315], [92.173422, 24.608396], [92.182931, 24.647256], [92.233781, 24.777533], [92.236468, 24.793036], [92.236881, 24.80859], [92.235021, 24.824765], [92.234928, 24.824991], [92.221792, 24.856908], [92.221759, 24.857047], [92.217658, 24.874581], [92.223032, 24.891376], [92.252177, 24.902977], [92.290245, 24.891351], [92.290418, 24.891298], [92.358146, 24.853749], [92.358321, 24.853652], [92.359321, 24.846428], [92.359354, 24.846185], [92.363507, 24.84112], [92.363592, 24.841017], [92.37093, 24.837917], [92.380748, 24.836702], [92.442037, 24.855409], [92.477693, 24.86391], [92.491543, 24.883728], [92.49454, 24.894864], [92.491336, 24.912279], [92.491267, 24.912424], [92.483791, 24.928221], [92.474386, 24.93667], [92.474272, 24.936715], [92.449788, 24.946437], [92.450028, 24.946636], [92.458056, 24.953284], [92.457828, 24.953558], [92.454026, 24.958116], [92.453769, 24.958231], [92.444517, 24.962379], [92.444446, 24.962427], [92.412478, 24.984083], [92.41246, 24.984361], [92.412271, 24.987391], [92.41214, 24.987434], [92.41186, 24.987527], [92.394701, 24.99323], [92.38695, 24.99509], [92.386926, 24.995202], [92.386876, 24.995444], [92.384937, 25.004773], [92.381058, 25.023435], [92.356254, 25.037], [92.328142, 25.048007], [92.312122, 25.0686], [92.303544, 25.074285], [92.251557, 25.080796], [92.233677, 25.084956], [92.220138, 25.090382], [92.208046, 25.098107], [92.194093, 25.109166], [92.17921, 25.126684], [92.172699, 25.131955], [92.164741, 25.133557], [92.150995, 25.129733], [92.145517, 25.131697], [92.138593, 25.1364], [92.124123, 25.136968], [92.116165, 25.139449], [92.110377, 25.144875], [92.100455, 25.159086], [92.092911, 25.164718], [92.066039, 25.174537], [92.033999, 25.181668], [92.001753, 25.18296], [91.975088, 25.175364], [91.956278, 25.169783], [91.900571, 25.177586], [91.793704, 25.165338], [91.745335, 25.169369], [91.730142, 25.167457], [91.723734, 25.161928], [91.71774, 25.149836], [91.710712, 25.144668], [91.701823, 25.147148], [91.688904, 25.152988], [91.677432, 25.152626], [91.672264, 25.136503], [91.662033, 25.127201], [91.638675, 25.124049], [91.613147, 25.125134], [91.596197, 25.128907], [91.580177, 25.140895], [91.573459, 25.152264], [91.566121, 25.159964], [91.548758, 25.161101], [91.540179, 25.157639], [91.523436, 25.145185], [91.511654, 25.142497], [91.500905, 25.141154], [91.480752, 25.135263], [91.471036, 25.133919], [91.431866, 25.13795], [91.283968, 25.178981], [91.235702, 25.201925], [91.224953, 25.201615], [91.203146, 25.191332], [91.190847, 25.189471], [91.135346, 25.191228], [90.975149, 25.167819], [90.94342, 25.157432], [90.934821, 25.15636], [90.82198, 25.142291], [90.793972, 25.145391], [90.779399, 25.151954], [90.766893, 25.160533], [90.753974, 25.167561], [90.737955, 25.169421], [90.732684, 25.166889], [90.722038, 25.156605], [90.716354, 25.153556], [90.707672, 25.152988], [90.670568, 25.158776], [90.648968, 25.167819], [90.637599, 25.171075], [90.62344, 25.171436], [90.583339, 25.162031], [90.50138, 25.168801], [90.399784, 25.149009], [90.364644, 25.149991], [90.286923, 25.180066], [90.130085, 25.211589], [89.908134, 25.296907], [89.870204, 25.295563], [89.834599, 25.282437], [89.819044, 25.284969], [89.807365, 25.304503], [89.798529, 25.340056], [89.795015, 25.374163], [89.799234, 25.402893], [89.800699, 25.412869], [89.808568, 25.439507], [89.82323, 25.489143], [89.82509, 25.564487], [89.834392, 25.634767], [89.824212, 25.674093], [89.801526, 25.724684], [89.783129, 25.814446], [89.786953, 25.839147], [89.830051, 25.90798], [89.834392, 25.931752], [89.826434, 25.937488], [89.810724, 25.93878], [89.792017, 25.949115], [89.811551, 25.955213], [89.825607, 25.965703], [89.828656, 25.979397], [89.815272, 25.995107], [89.814135, 25.99614], [89.812688, 25.996347], [89.811034, 25.996037], [89.809329, 25.99521], [89.809174, 25.995107], [89.808864, 25.995107], [89.802043, 25.988647], [89.795531, 25.98596], [89.789434, 25.987976], [89.783956, 25.995107], [89.776411, 26.008233], [89.755844, 26.032417], [89.750211, 26.041564], [89.748713, 26.059031], [89.751141, 26.071536], [89.749849, 26.083887], [89.736724, 26.100579], [89.728766, 26.114169], [89.724321, 26.130654], [89.718482, 26.145537], [89.706545, 26.154167], [89.697656, 26.154787], [89.677709, 26.153133], [89.670371, 26.154115], [89.658175, 26.159231], [89.655798, 26.161298], [89.657245, 26.165381], [89.657829, 26.187174], [89.658382, 26.207859], [89.653008, 26.22269], [89.634921, 26.225842], [89.61332, 26.219383], [89.606396, 26.211011], [89.608463, 26.180677], [89.615284, 26.169101], [89.614406, 26.164192], [89.593993, 26.169722], [89.586759, 26.169205], [89.579937, 26.166104], [89.573426, 26.16073], [89.562781, 26.142591], [89.569395, 26.130086], [89.585777, 26.122903], [89.604225, 26.120732], [89.592546, 26.113963], [89.57787, 26.107245], [89.566915, 26.098977], [89.567018, 26.087091], [89.57694, 26.082647], [89.590789, 26.085024], [89.603864, 26.084662], [89.611357, 26.072208], [89.602882, 26.055258], [89.559163, 26.022237], [89.550689, 25.995055], [89.551825, 25.981516], [89.549087, 25.968855], [89.541697, 25.959709], [89.528416, 25.957125], [89.518184, 25.962086], [89.509451, 25.972628], [89.495343, 25.995055], [89.490641, 25.997329], [89.485628, 25.998724], [89.480615, 25.999241], [89.426407, 25.995055], [89.426355, 25.994952], [89.421187, 25.993195], [89.416123, 25.992626], [89.411266, 25.993195], [89.406615, 25.995055], [89.391422, 26.015467], [89.379743, 26.015312], [89.367754, 26.005442], [89.351476, 25.996864], [89.335301, 25.998569], [89.315147, 26.006786], [89.296906, 26.018155], [89.272979, 26.043476], [89.237633, 26.0581], [89.229261, 26.067196], [89.22306, 26.086574], [89.212621, 26.098977], [89.197842, 26.10771], [89.131593, 26.133755], [89.11919, 26.146622], [89.111697, 26.164502], [89.103377, 26.22517], [89.095884, 26.241035], [89.075885, 26.271059], [89.069426, 26.287389], [89.068857, 26.314261], [89.08033, 26.315604], [89.094799, 26.309506], [89.103532, 26.313692], [89.102912, 26.327335], [89.095729, 26.334518], [89.071855, 26.340616], [89.055215, 26.347023], [89.054078, 26.353948], [89.057282, 26.36263], [89.052631, 26.374619], [89.043743, 26.381078], [89.02178, 26.386246], [89.010877, 26.390173], [89.001781, 26.39715], [88.986537, 26.412704], [88.975323, 26.417872], [88.959303, 26.428569], [88.950157, 26.43694], [88.94101, 26.439421], [88.924577, 26.432135], [88.911968, 26.421231], [88.899462, 26.404488], [88.891246, 26.385781], [88.891866, 26.368779], [88.912433, 26.348935], [88.941217, 26.340099], [88.966021, 26.328472], [88.975323, 26.299946], [88.981731, 26.291833], [88.989689, 26.289404], [88.998629, 26.291523], [89.007983, 26.297156], [89.004365, 26.27571], [89.0116, 26.269199], [89.023537, 26.268837], [89.034338, 26.265788], [89.039712, 26.247185], [89.019455, 26.234472], [88.975323, 26.224344], [88.946849, 26.232922], [88.902718, 26.272919], [88.875794, 26.277364], [88.855641, 26.265116], [88.840654, 26.232043], [88.824841, 26.226566], [88.807995, 26.233439], [88.798486, 26.24765], [88.792027, 26.2646], [88.784017, 26.279947], [88.747947, 26.292505], [88.663404, 26.264444], [88.645834, 26.27602], [88.653276, 26.2831], [88.690121, 26.299843], [88.702988, 26.309661], [88.712238, 26.328265], [88.709706, 26.340616], [88.69627, 26.350279], [88.673378, 26.360718], [88.667435, 26.368159], [88.66852, 26.385729], [88.661079, 26.388881], [88.652242, 26.391517], [88.649865, 26.39839], [88.651932, 26.407071], [88.656273, 26.415133], [88.633018, 26.420094], [88.622373, 26.424538], [88.611521, 26.430843], [88.603563, 26.440196], [88.597878, 26.451461], [88.591161, 26.461228], [88.579172, 26.466034], [88.557984, 26.465724], [88.549768, 26.466654], [88.526823, 26.475388], [88.506825, 26.487635], [88.49866, 26.49487], [88.487911, 26.505773], [88.475405, 26.514972], [88.462383, 26.528821], [88.446467, 26.535539], [88.411016, 26.545099], [88.397891, 26.55869], [88.394997, 26.578379], [88.395927, 26.599566], [88.394377, 26.618015], [88.385282, 26.623544], [88.372518, 26.611607], [88.36027, 26.593107], [88.352829, 26.578689], [88.332365, 26.514662], [88.319342, 26.494921], [88.313865, 26.481537], [88.315105, 26.465052], [88.322546, 26.451772], [88.335621, 26.448206], [88.34022, 26.454149], [88.34301, 26.477196], [88.346524, 26.484018], [88.353552, 26.484586], [88.37691, 26.475336], [88.434581, 26.465156], [88.461143, 26.45389], [88.475405, 26.432031], [88.495456, 26.378081], [88.497626, 26.352708], [88.475405, 26.355808], [88.467912, 26.354051], [88.45799, 26.353018], [88.448637, 26.353431], [88.442022, 26.356118], [88.431687, 26.362216], [88.426003, 26.357927], [88.421197, 26.348729], [88.413807, 26.340202], [88.366781, 26.313434], [88.351692, 26.30098], [88.336602, 26.281808], [88.332933, 26.267235], [88.334535, 26.230803], [88.326267, 26.21592], [88.308697, 26.206102], [88.250303, 26.188015], [88.229787, 26.184036], [88.218884, 26.18016], [88.211959, 26.173752], [88.205654, 26.166414], [88.196921, 26.159645], [88.163796, 26.140473], [88.155632, 26.128484], [88.144469, 26.095876], [88.141369, 26.090502], [88.139922, 26.085334], [88.141162, 26.075464], [88.14478, 26.070503], [88.150981, 26.066989], [88.156407, 26.061408], [88.157905, 26.050452], [88.151032, 26.031332], [88.125866, 26.0134], [88.106539, 25.979966], [88.088039, 25.923225], [88.082664, 25.915939], [88.077393, 25.912786], [88.074396, 25.908135], [88.082044, 25.863849], [88.088969, 25.848656], [88.087574, 25.839974], [88.087574, 25.830983], [88.103335, 25.814808], [88.108038, 25.806281], [88.114911, 25.787264], [88.127726, 25.774914], [88.146433, 25.77574], [88.166225, 25.783647], [88.18271, 25.792432], [88.227462, 25.804266], [88.259915, 25.789021], [88.320273, 25.725253], [88.392981, 25.680759], [88.41944, 25.653784], [88.423781, 25.592237], [88.437785, 25.579835], [88.475405, 25.562575], [88.485586, 25.542628], [88.512767, 25.523714], [88.520467, 25.509297], [88.530286, 25.500202], [88.550956, 25.496481], [88.593589, 25.495086], [88.613175, 25.486404], [88.645421, 25.467336], [88.66635, 25.462891], [88.67808, 25.465889], [88.687072, 25.473898], [88.69565, 25.483614], [88.706192, 25.491675], [88.71415, 25.492812], [88.723142, 25.491417], [88.732651, 25.491003], [88.742572, 25.495189], [88.741952, 25.512759], [88.772338, 25.501959], [88.780968, 25.495189], [88.793112, 25.48196], [88.805824, 25.471676], [88.814919, 25.458602], [88.816315, 25.437208], [88.799313, 25.401706], [88.799623, 25.395609], [88.810372, 25.388426], [88.813266, 25.380054], [88.814041, 25.370391], [88.81833, 25.359642], [88.841998, 25.332822], [88.849026, 25.327189], [88.859258, 25.324709], [88.880187, 25.323468], [88.88613, 25.319076], [88.902873, 25.303418], [88.926489, 25.300472], [88.975323, 25.302643], [88.982713, 25.294478], [88.984935, 25.284969], [88.982351, 25.274892], [88.975323, 25.265229], [88.937754, 25.240166], [88.929073, 25.229417], [88.927988, 25.21593], [88.930778, 25.202649], [88.927884, 25.192727], [88.909642, 25.189161], [88.924163, 25.167871], [88.898635, 25.169318], [88.830216, 25.191332], [88.823601, 25.194846], [88.81709, 25.196809], [88.809132, 25.195259], [88.802517, 25.188128], [88.792337, 25.166579], [88.784224, 25.160946], [88.724537, 25.166475], [88.664593, 25.186888], [88.634259, 25.192262], [88.599119, 25.19314], [88.590127, 25.190143], [88.577725, 25.177482], [88.570697, 25.173245], [88.55938, 25.170764], [88.554677, 25.171126], [88.550026, 25.173142], [88.523154, 25.179808], [88.491838, 25.191125], [88.475405, 25.195104], [88.441299, 25.18973], [88.43148, 25.173038], [88.434168, 25.116659], [88.425796, 25.051056], [88.414117, 25.021523], [88.391018, 24.99509], [88.387865, 24.968761], [88.37567, 24.94561], [88.340892, 24.90414], [88.322753, 24.874684], [88.313606, 24.868302], [88.296812, 24.869698], [88.25485, 24.879955], [88.242758, 24.880601], [88.24188, 24.898766], [88.232836, 24.918273], [88.21878, 24.935146], [88.20245, 24.9453], [88.18302, 24.94698], [88.16452, 24.941114], [88.114911, 24.916413], [88.114911, 24.912021], [88.120337, 24.906775], [88.124936, 24.898145], [88.138062, 24.863496], [88.140955, 24.844583], [88.13124, 24.831767], [88.117391, 24.819261], [88.107779, 24.801097], [88.094808, 24.803112], [88.088245, 24.796136], [88.085352, 24.784018], [88.084008, 24.770453], [88.07698, 24.761177], [88.064268, 24.759162], [88.053209, 24.754976], [88.048455, 24.711206], [88.035432, 24.693817], [88.027164, 24.67573], [88.02179, 24.645603], [88.04091, 24.640435], [88.05724, 24.634105], [88.067265, 24.613667], [88.081838, 24.599869], [88.08499, 24.593875], [88.08499, 24.541733], [88.086023, 24.53672], [88.090002, 24.533], [88.098787, 24.522484], [88.107934, 24.507963], [88.10995, 24.500986], [88.138785, 24.49525], [88.401301, 24.369418], [88.475405, 24.315468], [88.49835, 24.310972], [88.5631, 24.308207], [88.612451, 24.292885], [88.634517, 24.294125], [88.649503, 24.315364], [88.660149, 24.338257], [88.681801, 24.33389], [88.714564, 24.315416], [88.737508, 24.287097], [88.743038, 24.247539], [88.74495, 24.243379], [88.747482, 24.226972], [88.749807, 24.223251], [88.746397, 24.214079], [88.734821, 24.198421], [88.731617, 24.189842], [88.731456, 24.189771], [88.715907, 24.182918], [88.69379, 24.175321], [88.683765, 24.166536], [88.67808, 24.154289], [88.678287, 24.144651], [88.68299, 24.124291], [88.681801, 24.112664], [88.675393, 24.091838], [88.674101, 24.08264], [88.67901, 24.071219], [88.697821, 24.056259], [88.700921, 24.04365], [88.725002, 24.038379], [88.725468, 24.028896], [88.71384, 24.014685], [88.701593, 23.995281], [88.708363, 23.98355], [88.714357, 23.964404], [88.717148, 23.943139], [88.71415, 23.925156], [88.704435, 23.913709], [88.666505, 23.8795], [88.653121, 23.869784], [88.633949, 23.866141], [88.613175, 23.867847], [88.593589, 23.866916], [88.577621, 23.85516], [88.574366, 23.845626], [88.575451, 23.836556], [88.577621, 23.827616], [88.577518, 23.818521], [88.57235, 23.806894], [88.557674, 23.785681], [88.552713, 23.774984], [88.552507, 23.765424], [88.559741, 23.750283], [88.561188, 23.741007], [88.540104, 23.649953], [88.541551, 23.638765], [88.550956, 23.634424], [88.56124, 23.632486], [88.566356, 23.62874], [88.564392, 23.616208], [88.561085, 23.608612], [88.561705, 23.602023], [88.57142, 23.592334], [88.581549, 23.588329], [88.601289, 23.590344], [88.609247, 23.588794], [88.624233, 23.573859], [88.647643, 23.535076], [88.664489, 23.518307], [88.69379, 23.499807], [88.704642, 23.495182], [88.719421, 23.468104], [88.731927, 23.472625], [88.743709, 23.489317], [88.756835, 23.498799], [88.770168, 23.488826], [88.767894, 23.467277], [88.719266, 23.348292], [88.69503, 23.312609], [88.685935, 23.293308], [88.686607, 23.271604], [88.69658, 23.252354], [88.710016, 23.241218], [88.775697, 23.221633], [88.786187, 23.221529], [88.794197, 23.224966], [88.800967, 23.230754], [88.809545, 23.236051], [88.822671, 23.237963], [88.839621, 23.23481], [88.909642, 23.212434], [88.918066, 23.208584], [88.926954, 23.206517], [88.954239, 23.207422], [88.959975, 23.202693], [88.95982, 23.194451], [88.954704, 23.183806], [88.945868, 23.174349], [88.919099, 23.153859], [88.864787, 23.100271], [88.8513, 23.07513], [88.848716, 23.023841], [88.839414, 22.995368], [88.838794, 22.975808], [88.842773, 22.964568], [88.861738, 22.942399], [88.872177, 22.926767], [88.893468, 22.879638], [88.905147, 22.866435], [88.933103, 22.858115], [88.944524, 22.848012], [88.946798, 22.834525], [88.945144, 22.815094], [88.941217, 22.795664], [88.936876, 22.782331], [88.926231, 22.767035], [88.914965, 22.7544], [88.908144, 22.740784], [88.910573, 22.722593], [88.92251, 22.693706], [88.92592, 22.680839], [88.930003, 22.658618], [88.942354, 22.637482], [88.952017, 22.611903], [88.95641, 22.584979], [88.950312, 22.561983], [88.927988, 22.548134], [88.959045, 22.536817], [88.970982, 22.527928], [88.979974, 22.490928], [88.998371, 22.451241], [89.002505, 22.427986], [89.000748, 22.420131], [88.99217, 22.406954], [88.989482, 22.39667], [88.989586, 22.386903], [89.001523, 22.333677], [89.006949, 22.321429], [89.023537, 22.294816], [89.00912, 22.285618], [89.018628, 22.264689], [89.062295, 22.211565], [89.070718, 22.194822], [89.076196, 22.17503], [89.078159, 22.150845], [89.060693, 22.130485], [89.060395, 22.129869], [89.060313, 22.130601], [89.043712, 22.137397], [89.048595, 22.122952], [89.045665, 22.112047], [89.039887, 22.100898], [89.036876, 22.085598], [89.04005, 22.069525], [89.054047, 22.043647], [89.057384, 22.031236], [89.065603, 21.967108], [89.060883, 21.938463], [89.036876, 21.925116], [89.023123, 21.931098], [89.013031, 21.929429], [89.008067, 21.920559], [89.009532, 21.904608], [88.993012, 21.912665], [88.96046, 21.934312], [88.944672, 21.938788], [88.937836, 21.948717], [88.934581, 21.969306], [88.928722, 21.986558], [88.91391, 21.986558], [88.90504, 21.969428], [88.910818, 21.946479], [88.924083, 21.926337], [88.937836, 21.91767], [88.951915, 21.913886], [88.968272, 21.904731], [88.99586, 21.884182], [89.01588, 21.853339], [89.030935, 21.770453], [89.043712, 21.733344], [89.047862, 21.728013], [89.067882, 21.709133], [89.082774, 21.678209], [89.085216, 21.674709], [89.088227, 21.633734], [89.082693, 21.617336], [89.06422, 21.609198], [89.053233, 21.609809], [89.044688, 21.613511], [89.038829, 21.619452], [89.036876, 21.626899], [89.033946, 21.633246], [89.027599, 21.624579], [89.021658, 21.611802], [89.019786, 21.606106], [89.000255, 21.60342], [88.975759, 21.615912], [88.933849, 21.643948], [88.896983, 21.650133], [88.891612, 21.65351], [88.886078, 21.661811], [88.881602, 21.672024], [88.872081, 21.74901], [88.866059, 21.766832], [88.858653, 21.766832], [88.855724, 21.748847], [88.841563, 21.716254], [88.838227, 21.701972], [88.842947, 21.684149], [88.859874, 21.652574], [88.858653, 21.636542], [88.844249, 21.622219], [88.823985, 21.617255], [88.805837, 21.62287], [88.797862, 21.640204], [88.814789, 21.653876], [88.81837, 21.65762], [88.818533, 21.665839], [88.816661, 21.675035], [88.810883, 21.691718], [88.804942, 21.671861], [88.800466, 21.661851], [88.7942, 21.65762], [88.784028, 21.655951], [88.779063, 21.65119], [88.77711, 21.643541], [88.776866, 21.63345], [88.781749, 21.614244], [88.791026, 21.598863], [88.796641, 21.584418], [88.790538, 21.568264], [88.762462, 21.555854], [88.727875, 21.559882], [88.70753, 21.578315], [88.722179, 21.609198], [88.687999, 21.678127], [88.684906, 21.697211], [88.703624, 21.810614], [88.698009, 21.825507], [88.694509, 21.83983], [88.714692, 21.897854], [88.71697, 21.951972], [88.722179, 21.966132], [88.733165, 21.9772], [88.746837, 21.987128], [88.758311, 21.999457], [88.763031, 22.017279], [88.754649, 22.037584], [88.717052, 22.058051], [88.708507, 22.072211], [88.714854, 22.086982], [88.730154, 22.103583], [88.763031, 22.130561], [88.743907, 22.12934], [88.726573, 22.121161], [88.712169, 22.109036], [88.701671, 22.095852], [88.694835, 22.081529], [88.692882, 22.065863], [88.699474, 22.053209], [88.718516, 22.048041], [88.730724, 22.037177], [88.723318, 22.013088], [88.701671, 21.97602], [88.698416, 21.962592], [88.689952, 21.94599], [88.678722, 21.936672], [88.667003, 21.945014], [88.656261, 21.941067], [88.642589, 21.940904], [88.63087, 21.945461], [88.625987, 21.955512], [88.627452, 21.970404], [88.631684, 21.979804], [88.646332, 22.00023], [88.653494, 22.018012], [88.652599, 22.031806], [88.648774, 22.046047], [88.64503, 22.075141], [88.639496, 22.097235], [88.639496, 22.109524], [88.656993, 22.151068], [88.674001, 22.162055], [88.683116, 22.184963], [88.679942, 22.204413], [88.660167, 22.205024], [88.661143, 22.198961], [88.656016, 22.183254], [88.649669, 22.169094], [88.646332, 22.167792], [88.644298, 22.162299], [88.634939, 22.144965], [88.63087, 22.1258], [88.622081, 22.116034], [88.619151, 22.109524], [88.618337, 22.102973], [88.619965, 22.078315], [88.625499, 22.059272], [88.625987, 22.048041], [88.620372, 22.029975], [88.60255, 22.006415], [88.598643, 21.990302], [88.600271, 21.947211], [88.598399, 21.929348], [88.567068, 21.850816], [88.564626, 21.832343], [88.56544, 21.771186], [88.564626, 21.766832], [88.581309, 21.769436], [88.604177, 21.778754], [88.624034, 21.778022], [88.632823, 21.750067], [88.630219, 21.731757], [88.623383, 21.710273], [88.612641, 21.697496], [88.598643, 21.70539], [88.591807, 21.70539], [88.59962, 21.637274], [88.595063, 21.612128], [88.578136, 21.62344], [88.578136, 21.571682], [88.572276, 21.557929], [88.55893, 21.555569], [88.54477, 21.561998], [88.536469, 21.5751], [88.530284, 21.564399], [88.529633, 21.553371], [88.534434, 21.542914], [88.543956, 21.534084], [88.51295, 21.524644], [88.493907, 21.534817], [88.487966, 21.556545], [88.495616, 21.581855], [88.502126, 21.589342], [88.5088, 21.594387], [88.514008, 21.600816], [88.516124, 21.612616], [88.516612, 21.62641], [88.518809, 21.639146], [88.52296, 21.649888], [88.529633, 21.65762], [88.509532, 21.712307], [88.504893, 21.741523], [88.512706, 21.763414], [88.532888, 21.795315], [88.531016, 21.824123], [88.51588, 21.850816], [88.495616, 21.876695], [88.514659, 21.916205], [88.514985, 21.937201], [88.495616, 21.951809], [88.499848, 21.933661], [88.492361, 21.922024], [88.481212, 21.909898], [88.474457, 21.890326], [88.477061, 21.871812], [88.492035, 21.837144], [88.495616, 21.818671], [88.4935, 21.805162], [88.488292, 21.803656], [88.481456, 21.805162], [88.474457, 21.800971], [88.468272, 21.777248], [88.457693, 21.758857], [88.45338, 21.741767], [88.452159, 21.722805], [88.454763, 21.70539], [88.468598, 21.677151], [88.472179, 21.663275], [88.468272, 21.643948], [88.456309, 21.62401], [88.441905, 21.616156], [88.426117, 21.611029], [88.409679, 21.59927], [88.393077, 21.600246], [88.383556, 21.631415], [88.382823, 21.671332], [88.392589, 21.698554], [88.380626, 21.696194], [88.364513, 21.686957], [88.355724, 21.684963], [88.346202, 21.688625], [88.340668, 21.697496], [88.336436, 21.708726], [88.331228, 21.719062], [88.326345, 21.711086], [88.324718, 21.701361], [88.327403, 21.691107], [88.345714, 21.6706], [88.349457, 21.660956], [88.348806, 21.650295], [88.34547, 21.636542], [88.33546, 21.61579], [88.321788, 21.605658], [88.309744, 21.610175], [88.304454, 21.63345], [88.302013, 21.745185], [88.298188, 21.764106], [88.289806, 21.780219], [88.273123, 21.797919], [88.261567, 21.798651], [88.264171, 21.775295], [88.295421, 21.676581], [88.297048, 21.653876], [88.294444, 21.642808], [88.28419, 21.621243], [88.283376, 21.609198], [88.287608, 21.600287], [88.300629, 21.589342], [88.304454, 21.581855], [88.305837, 21.571682], [88.303722, 21.568793], [88.297862, 21.566555], [88.28712, 21.558295], [88.278982, 21.555162], [88.266124, 21.55386], [88.254405, 21.554674], [88.249278, 21.558295], [88.245942, 21.579901], [88.237478, 21.601711], [88.224376, 21.613023], [88.208263, 21.602973], [88.189464, 21.641791], [88.181895, 21.665229], [88.180919, 21.684963], [88.19044, 21.704088], [88.203461, 21.724351], [88.21046, 21.74726], [88.201427, 21.774319], [88.209727, 21.78856], [88.203787, 21.805406], [88.154063, 21.888088], [88.156423, 21.897854], [88.159434, 21.906155], [88.146739, 21.959215], [88.149913, 21.976386], [88.156261, 21.990871], [88.164317, 22.003119], [88.206798, 22.053371], [88.222423, 22.077948], [88.212738, 22.100043], [88.20338, 22.15176], [88.198009, 22.167792], [88.165294, 22.185004], [88.118419, 22.200181], [88.081309, 22.217353], [88.077322, 22.240424], [88.059744, 22.229478], [88.043468, 22.224758], [88.027029, 22.227851], [88.009044, 22.240424], [87.985118, 22.265937], [87.972667, 22.283189], [87.967459, 22.298489], [87.964203, 22.319281], [87.950694, 22.356187], [87.94752, 22.373847], [87.946137, 22.394924], [87.94044, 22.413031], [87.927989, 22.424221], [87.906586, 22.42475], [87.906586, 22.418606], [87.922862, 22.397773], [87.935802, 22.35517], [87.94752, 22.277655], [87.95281, 22.262437], [87.965343, 22.244818], [87.991954, 22.215888], [88.008474, 22.204901], [88.025401, 22.200914], [88.057384, 22.198879], [88.09547, 22.19123], [88.132579, 22.179145], [88.160981, 22.156562], [88.173513, 22.116889], [88.164317, 22.089789], [88.140636, 22.060126], [88.111176, 22.034857], [88.084727, 22.020697], [88.048595, 22.020738], [88.043142, 22.017279], [88.045177, 22.006537], [88.049001, 21.995429], [88.050792, 21.985012], [88.046886, 21.97602], [88.0324, 21.95718], [87.983653, 21.854193], [87.97047, 21.836493], [87.957774, 21.828925], [87.948009, 21.825385], [87.909028, 21.798814], [87.902192, 21.79682], [87.899181, 21.794827], [87.895681, 21.789374], [87.894786, 21.778754], [87.892914, 21.774319], [87.867198, 21.7508], [87.833995, 21.729315], [87.755707, 21.691718], [87.703787, 21.655504], [87.683849, 21.650133], [87.6421, 21.652086], [87.621349, 21.650621], [87.477387, 21.613593], [87.468761, 21.611396], [87.404959, 21.586656], [87.341807, 21.562405], [87.293468, 21.553656], [87.256114, 21.561998], [87.245942, 21.556586], [87.235199, 21.554511], [87.211436, 21.554592], [87.200938, 21.551988], [87.113048, 21.507961], [87.063731, 21.475002], [86.917654, 21.327379], [86.866954, 21.256049], [86.83725, 21.174709], [86.844005, 21.082221], [86.952159, 20.849107], [86.965668, 20.805976], [86.953868, 20.780585], [86.93629, 20.787502], [86.916515, 20.788072], [86.878754, 20.780585], [86.889334, 20.764472], [86.910655, 20.756415], [86.957205, 20.753241], [86.979991, 20.755561], [86.986583, 20.751858], [86.994884, 20.738959], [87.000011, 20.728217], [87.001801, 20.71898], [86.998709, 20.711127], [86.988617, 20.70425], [86.975759, 20.710679], [86.961192, 20.712836], [86.948253, 20.709052], [86.940196, 20.697984], [86.951915, 20.697008], [86.961192, 20.695054], [86.968516, 20.691148], [86.974294, 20.684394], [86.9817, 20.68537], [86.986501, 20.684394], [86.990245, 20.679999], [86.994884, 20.670722], [87.019379, 20.680365], [87.033376, 20.690172], [87.043224, 20.697984], [87.027517, 20.679674], [87.003591, 20.65705], [86.976248, 20.637885], [86.923188, 20.621731], [86.774181, 20.511949], [86.751475, 20.489163], [86.733897, 20.462226], [86.72462, 20.431952], [86.72047, 20.368476], [86.741954, 20.379543], [86.756358, 20.393744], [86.769786, 20.400702], [86.788748, 20.390123], [86.788259, 20.402818], [86.789806, 20.4147], [86.796153, 20.437323], [86.809825, 20.415107], [86.7942, 20.37816], [86.768321, 20.343451], [86.751475, 20.32807], [86.739268, 20.324205], [86.719737, 20.31509], [86.701671, 20.303778], [86.693696, 20.293931], [86.701671, 20.284654], [86.719737, 20.291734], [86.754649, 20.314439], [86.717459, 20.286933], [86.628184, 20.235256], [86.583832, 20.218207], [86.53419, 20.206122], [86.511485, 20.197455], [86.495128, 20.184068], [86.490408, 20.174058], [86.478852, 20.12934], [86.468516, 20.10928], [86.467133, 20.094672], [86.41505, 20.036322], [86.391449, 20.020209], [86.396739, 20.009955], [86.404145, 20.002631], [86.414073, 19.998847], [86.42628, 19.999172], [86.397227, 19.982856], [86.348399, 20.008857], [86.275401, 20.067369], [86.231293, 20.064887], [86.209158, 20.066636], [86.199718, 20.077948], [86.195974, 20.080308], [86.173025, 20.1022], [86.168468, 20.113186], [86.163097, 20.134833], [86.158702, 20.143134], [86.152517, 20.143134], [86.14503, 20.122016], [86.154307, 20.089993], [86.158702, 20.081041], [86.167735, 20.072943], [86.187755, 20.063666], [86.213715, 20.042426], [86.230235, 20.035305], [86.268565, 20.026435], [86.273448, 20.02619], [86.284679, 20.027493], [86.289073, 20.026435], [86.293224, 20.02143], [86.294119, 20.015692], [86.2942, 20.010403], [86.295909, 20.006578], [86.302908, 19.999091], [86.308116, 19.992092], [86.314952, 19.986884], [86.326671, 19.984849], [86.341563, 19.979885], [86.356456, 19.970404], [86.372406, 19.964993], [86.268565, 19.910346], [86.111583, 19.855699], [86.089041, 19.844224], [86.07838, 19.843573], [86.066742, 19.851996], [86.056651, 19.856879], [86.04656, 19.852525], [86.037934, 19.845282], [86.032074, 19.841498], [86.01352, 19.839057], [85.888845, 19.802965], [85.846365, 19.795844], [85.808604, 19.773383], [85.788829, 19.765692], [85.778087, 19.764635], [85.744395, 19.765692], [85.734386, 19.763617], [85.714854, 19.754218], [85.684581, 19.747748], [85.624278, 19.718573], [85.625987, 19.722886], [85.635997, 19.72663], [85.63795, 19.731635], [85.614268, 19.727932], [85.549164, 19.690619], [85.480479, 19.666327], [85.456798, 19.663316], [85.44988, 19.666693], [85.44752, 19.674628], [85.446788, 19.690619], [85.443126, 19.69245], [85.43686, 19.691352], [85.430431, 19.691718], [85.42628, 19.697455], [85.427013, 19.704088], [85.431651, 19.704901], [85.43686, 19.704657], [85.439301, 19.707994], [85.450857, 19.716295], [85.47755, 19.723863], [85.521983, 19.739081], [85.525564, 19.734036], [85.523448, 19.728705], [85.518891, 19.721584], [85.514985, 19.711127], [85.558849, 19.738105], [85.576508, 19.745836], [85.567638, 19.7508], [85.563243, 19.756903], [85.564138, 19.76439], [85.570323, 19.773179], [85.567149, 19.791653], [85.570323, 19.875637], [85.55714, 19.881781], [85.514985, 19.889309], [85.476817, 19.901028], [85.463552, 19.902899], [85.440115, 19.896226], [85.41627, 19.879788], [85.374522, 19.837795], [85.319591, 19.793647], [85.30421, 19.79267], [85.290294, 19.789944], [85.278087, 19.78559], [85.268077, 19.780015], [85.246837, 19.762641], [85.220958, 19.736029], [85.20338, 19.707261], [85.206554, 19.683783], [85.189952, 19.670966], [85.175141, 19.636908], [85.165538, 19.6258], [85.150157, 19.613471], [85.140147, 19.596584], [85.139334, 19.579779], [85.151378, 19.567776], [85.151378, 19.560289], [85.139171, 19.557074], [85.122895, 19.549018], [85.108897, 19.538967], [85.102875, 19.529608], [85.104503, 19.514879], [85.108735, 19.506781], [85.115489, 19.507473], [85.124034, 19.519355], [85.128754, 19.509508], [85.130219, 19.498725], [85.128673, 19.487982], [85.124034, 19.478339], [85.135753, 19.481594], [85.141775, 19.487128], [85.145763, 19.493476], [85.151378, 19.498847], [85.162446, 19.503485], [85.171153, 19.504462], [85.178559, 19.504299], [85.185557, 19.505683], [85.202891, 19.517157], [85.201915, 19.527086], [85.1963, 19.538479], [85.199229, 19.554104], [85.206309, 19.548], [85.214529, 19.547268], [85.223643, 19.549954], [85.233246, 19.554104], [85.203787, 19.570502], [85.193044, 19.57392], [85.193044, 19.580797], [85.212901, 19.581122], [85.223643, 19.590237], [85.232595, 19.601549], [85.247569, 19.60871], [85.247569, 19.614936], [85.241873, 19.617011], [85.237315, 19.620551], [85.234386, 19.625678], [85.233246, 19.632554], [85.236339, 19.635321], [85.247813, 19.653144], [85.247569, 19.65648], [85.260753, 19.656073], [85.309093, 19.642239], [85.350597, 19.677639], [85.36378, 19.678046], [85.37436, 19.674709], [85.383067, 19.676093], [85.391449, 19.690619], [85.399181, 19.679145], [85.403168, 19.671291], [85.404959, 19.661933], [85.405284, 19.645941], [85.402192, 19.643988], [85.388682, 19.638251], [85.384613, 19.635972], [85.382172, 19.63109], [85.380382, 19.619615], [85.37794, 19.614936], [85.368419, 19.6081], [85.352306, 19.599433], [85.336762, 19.596096], [85.330089, 19.604967], [85.326427, 19.618801], [85.31837, 19.619534], [85.311046, 19.6105], [85.309093, 19.595038], [85.303477, 19.59748], [85.293793, 19.599555], [85.288422, 19.601264], [85.302501, 19.586656], [85.305837, 19.580146], [85.302908, 19.57392], [85.330577, 19.576402], [85.576508, 19.690619], [85.540294, 19.669582], [85.46339, 19.631049], [85.380219, 19.593817], [85.336681, 19.575181], [85.285411, 19.536282], [85.240571, 19.518052], [85.180431, 19.475002], [85.069509, 19.372138], [84.99586, 19.32331], [84.872081, 19.219875], [84.791026, 19.120592], [84.777761, 19.116197], [84.775157, 19.115302], [84.764415, 19.12344], [84.750662, 19.138373], [84.73878, 19.14765], [84.733653, 19.139146], [84.731212, 19.128811], [84.720958, 19.114244], [84.719981, 19.10163], [84.72283, 19.096015], [84.735362, 19.083197], [84.740489, 19.073676], [84.756847, 19.097642], [84.760997, 19.10163], [84.767833, 19.101793], [84.775238, 19.100491], [84.78004, 19.097073], [84.778331, 19.09101], [84.75115, 19.0539], [84.741059, 19.028144], [84.699474, 18.977484], [84.679535, 18.945705], [84.66505, 18.930121], [84.63087, 18.915717], [84.547862, 18.796088], [84.54184, 18.775784], [84.448253, 18.68065], [84.437185, 18.662787], [84.432628, 18.645697], [84.423676, 18.636461], [84.370453, 18.601304], [84.349457, 18.56981], [84.332856, 18.553697], [84.312266, 18.546698], [84.306326, 18.543687], [84.288585, 18.529771], [84.281749, 18.52558], [84.272716, 18.524319], [84.250011, 18.524237], [84.246918, 18.52558], [84.237641, 18.510932], [84.24936, 18.501654], [84.271007, 18.50019], [84.297699, 18.516303], [84.326182, 18.537421], [84.349946, 18.552924], [84.331228, 18.538642], [84.287934, 18.505764], [84.240082, 18.460761], [84.230479, 18.446234], [84.188324, 18.418036], [84.17449, 18.399848], [84.152843, 18.383612], [84.143891, 18.374823], [84.138927, 18.364163], [84.136485, 18.353583], [84.132823, 18.343411], [84.124034, 18.333808], [84.113129, 18.298285], [84.077403, 18.271552], [83.775157, 18.139594], [83.613943, 18.045152], [83.582286, 18.01911], [83.576182, 18.009752], [83.562836, 17.9831], [83.55836, 17.977525], [83.556651, 17.973456], [83.53891, 17.956285], [83.534434, 17.953315], [83.526134, 17.942532], [83.486583, 17.916897], [83.472911, 17.902493], [83.465587, 17.902493], [83.462901, 17.908515], [83.451915, 17.922919], [83.448985, 17.906887], [83.452403, 17.886217], [83.452973, 17.868476], [83.431163, 17.854234], [83.425304, 17.838609], [83.417735, 17.80622], [83.402354, 17.784491], [83.341319, 17.717434], [83.319102, 17.703192], [83.313243, 17.701158], [83.304373, 17.692206], [83.298595, 17.690131], [83.292735, 17.691148], [83.289561, 17.693264], [83.287446, 17.69538], [83.284679, 17.696357], [83.282237, 17.699286], [83.280284, 17.705959], [83.276378, 17.71308], [83.26824, 17.717475], [83.246918, 17.716254], [83.242035, 17.706529], [83.249278, 17.69538], [83.271983, 17.688137], [83.292491, 17.677476], [83.298595, 17.672797], [83.299083, 17.664293], [83.289236, 17.65762], [83.26824, 17.648586], [83.255138, 17.638129], [83.246918, 17.634223], [83.212901, 17.634914], [83.233897, 17.607733], [83.238943, 17.592963], [83.222911, 17.586493], [83.209239, 17.583686], [83.196137, 17.576809], [83.171397, 17.559801], [83.088227, 17.530707], [83.053233, 17.506496], [83.019867, 17.490912], [82.99643, 17.473456], [82.975841, 17.453681], [82.969574, 17.4501], [82.95281, 17.44595], [82.945486, 17.443101], [82.896821, 17.409329], [82.888682, 17.410142], [82.883311, 17.422065], [82.869151, 17.414008], [82.854259, 17.402818], [82.837657, 17.392768], [82.796397, 17.383043], [82.758149, 17.359198], [82.718516, 17.348619], [82.474864, 17.205959], [82.451915, 17.18244], [82.435069, 17.153795], [82.422862, 17.13996], [82.387706, 17.126939], [82.370372, 17.110093], [82.342052, 17.073188], [82.311697, 17.046617], [82.299327, 17.030341], [82.291677, 16.99844], [82.252696, 16.929185], [82.250743, 16.907864], [82.255138, 16.885321], [82.267914, 16.867499], [82.29835, 16.858466], [82.303559, 16.854193], [82.308116, 16.849555], [82.31422, 16.846625], [82.319591, 16.847235], [82.327159, 16.849433], [82.333344, 16.852118], [82.334646, 16.854071], [82.352061, 16.845404], [82.362478, 16.859076], [82.361176, 16.891343], [82.357432, 16.924872], [82.348888, 16.956488], [82.361176, 16.936021], [82.366547, 16.908637], [82.367442, 16.860297], [82.365571, 16.811591], [82.352061, 16.761298], [82.345958, 16.706122], [82.338145, 16.678412], [82.31422, 16.62816], [82.312348, 16.619859], [82.313813, 16.612982], [82.312836, 16.60814], [82.303966, 16.606431], [82.299327, 16.610093], [82.293793, 16.625637], [82.286876, 16.62816], [82.280935, 16.620836], [82.285492, 16.606513], [82.296072, 16.59101], [82.307384, 16.579779], [82.259044, 16.565497], [82.203461, 16.512681], [82.188243, 16.504055], [82.173025, 16.500922], [82.122895, 16.477362], [82.090668, 16.454779], [82.068614, 16.444281], [82.037364, 16.447659], [82.022716, 16.435736], [82.007823, 16.419582], [81.992686, 16.408433], [81.9817, 16.407864], [81.967947, 16.408922], [81.956228, 16.407172], [81.94809, 16.391547], [81.940766, 16.388373], [81.899913, 16.38581], [81.880544, 16.381659], [81.861664, 16.375393], [81.783214, 16.338568], [81.76295, 16.322577], [81.717621, 16.312201], [81.711599, 16.312201], [81.696056, 16.314927], [81.676931, 16.327379], [81.656261, 16.337348], [81.636404, 16.332709], [81.620942, 16.339504], [81.584972, 16.342108], [81.568126, 16.346381], [81.573904, 16.358344], [81.569184, 16.36758], [81.556977, 16.373033], [81.540212, 16.373684], [81.549083, 16.352362], [81.535981, 16.350043], [81.49586, 16.360663], [81.475352, 16.358629], [81.416677, 16.340155], [81.421153, 16.350979], [81.426117, 16.35871], [81.428233, 16.365668], [81.424083, 16.373684], [81.411388, 16.382514], [81.398936, 16.382636], [81.386974, 16.376776], [81.375743, 16.367499], [81.35963, 16.374335], [81.345225, 16.373603], [81.331554, 16.369778], [81.317393, 16.367499], [81.259613, 16.332709], [81.248546, 16.320136], [81.249848, 16.311021], [81.256033, 16.301825], [81.259613, 16.288642], [81.254161, 16.271308], [81.230317, 16.243598], [81.218516, 16.199449], [81.190278, 16.138861], [81.180024, 16.086737], [81.161957, 16.057074], [81.156016, 16.03852], [81.155447, 15.9855], [81.14975, 15.969631], [81.135102, 15.966498], [81.088227, 15.92121], [81.043305, 15.89411], [81.026866, 15.880845], [80.998871, 15.846747], [80.998383, 15.834703], [81.00294, 15.825263], [81.0088, 15.817816], [81.012543, 15.811957], [81.019216, 15.792385], [81.019054, 15.784166], [81.012543, 15.777818], [81.004731, 15.77851], [80.99586, 15.784329], [80.988292, 15.791205], [80.979015, 15.803412], [80.949474, 15.827867], [80.940684, 15.832994], [80.921641, 15.838609], [80.910004, 15.852688], [80.904307, 15.871283], [80.90268, 15.890815], [80.910167, 15.965888], [80.908946, 15.983466], [80.906016, 15.999823], [80.896495, 16.031073], [80.892263, 16.020941], [80.896495, 16.010565], [80.889008, 16.00373], [80.885753, 16.008694], [80.882823, 16.011461], [80.875336, 16.017401], [80.891287, 15.978258], [80.895763, 15.936021], [80.890147, 15.89525], [80.855724, 15.822699], [80.830251, 15.747626], [80.820079, 15.726874], [80.80714, 15.716376], [80.80836, 15.803941], [80.802745, 15.842434], [80.776134, 15.884263], [80.760102, 15.892971], [80.736339, 15.89761], [80.69044, 15.900702], [80.682465, 15.903266], [80.67628, 15.907172], [80.670177, 15.907701], [80.663097, 15.900702], [80.660411, 15.893948], [80.662852, 15.88935], [80.618419, 15.887681], [80.600108, 15.882229], [80.584321, 15.872545], [80.571137, 15.86872], [80.56072, 15.880845], [80.553884, 15.880845], [80.541677, 15.868069], [80.52589, 15.85692], [80.491873, 15.83926], [80.482677, 15.836412], [80.475597, 15.835924], [80.469086, 15.834662], [80.461436, 15.829657], [80.445486, 15.814399], [80.43629, 15.808295], [80.393321, 15.797024], [80.35963, 15.775824], [80.279633, 15.70067], [80.263194, 15.674547], [80.219493, 15.56745], [80.217459, 15.548163], [80.201345, 15.518297], [80.197602, 15.503079], [80.211192, 15.496649], [80.20045, 15.482164], [80.183849, 15.452297], [80.172862, 15.4383], [80.14031, 15.406399], [80.126801, 15.388129], [80.121267, 15.370022], [80.090587, 15.304877], [80.09254, 15.296576], [80.082367, 15.204088], [80.052989, 15.092597], [80.054535, 15.014146], [80.061534, 14.974677], [80.073416, 14.942043], [80.073009, 14.920559], [80.105154, 14.763861], [80.108165, 14.714342], [80.110688, 14.704779], [80.116954, 14.698147], [80.128184, 14.688463], [80.151866, 14.672797], [80.159841, 14.662665], [80.166352, 14.62816], [80.173595, 14.616523], [80.178477, 14.60578], [80.175955, 14.592963], [80.169688, 14.585842], [80.160899, 14.57982], [80.141856, 14.571234], [80.149181, 14.565009], [80.159353, 14.565741], [80.183279, 14.571112], [80.190196, 14.571234], [80.196951, 14.565009], [80.196056, 14.559027], [80.192393, 14.553209], [80.190196, 14.547593], [80.188243, 14.525702], [80.175955, 14.468736], [80.178722, 14.38939], [80.174978, 14.34455], [80.159434, 14.324774], [80.137055, 14.269924], [80.128184, 14.256537], [80.097911, 14.254299], [80.087169, 14.250312], [80.071788, 14.232245], [80.070079, 14.229193], [80.055024, 14.221584], [80.046641, 14.20661], [80.050629, 14.196112], [80.073497, 14.20185], [80.090994, 14.216742], [80.106619, 14.234036], [80.121104, 14.242011], [80.13559, 14.229193], [80.123383, 14.193508], [80.125824, 14.148505], [80.149669, 14.027737], [80.16391, 13.987982], [80.237153, 13.839667], [80.249278, 13.800279], [80.245453, 13.755601], [80.227387, 13.720526], [80.220958, 13.696967], [80.227712, 13.677069], [80.238048, 13.660224], [80.259044, 13.591742], [80.288097, 13.527086], [80.309581, 13.479315], [80.313731, 13.440904], [80.306895, 13.440904], [80.277761, 13.512519], [80.245453, 13.591742], [80.197276, 13.643622], [80.183604, 13.679185], [80.152517, 13.71894], [80.141856, 13.728949], [80.141775, 13.649115], [80.13559, 13.619086], [80.121267, 13.627265], [80.117686, 13.638861], [80.121267, 13.663764], [80.117849, 13.674506], [80.109874, 13.680732], [80.100841, 13.686265], [80.094005, 13.69477], [80.093598, 13.671332], [80.091319, 13.659125], [80.075531, 13.648383], [80.052989, 13.619086], [80.056488, 13.59516], [80.069509, 13.574408], [80.101329, 13.537095], [80.112071, 13.511542], [80.119395, 13.500678], [80.131847, 13.496161], [80.153087, 13.494208], [80.175955, 13.489325], [80.219737, 13.487779], [80.227712, 13.485582], [80.235118, 13.478095], [80.243419, 13.472154], [80.253429, 13.468655], [80.260183, 13.468451], [80.26588, 13.468248], [80.26238, 13.451239], [80.27475, 13.440009], [80.293142, 13.429145], [80.306895, 13.413031], [80.312266, 13.423163], [80.313731, 13.427232], [80.326996, 13.430365], [80.333995, 13.413031], [80.344005, 13.375434], [80.34783, 13.349066], [80.345551, 13.321234], [80.336274, 13.270941], [80.334158, 13.242906], [80.323741, 13.246894], [80.319835, 13.249172], [80.318044, 13.230902], [80.313731, 13.215033], [80.318207, 13.216986], [80.334158, 13.221177], [80.332774, 13.198147], [80.324229, 13.180365], [80.313813, 13.164293], [80.306895, 13.146064], [80.30714, 13.135647], [80.313731, 13.10578], [80.307953, 13.096015], [80.289724, 13.076728], [80.285818, 13.067572], [80.282725, 13.029364], [80.269054, 12.957261], [80.263194, 12.835354], [80.25172, 12.762519], [80.233653, 12.711005], [80.228689, 12.674994], [80.222179, 12.65998], [80.203868, 12.632148], [80.196462, 12.613715], [80.185069, 12.567125], [80.18336, 12.54682], [80.179698, 12.53262], [80.156016, 12.474514], [80.121267, 12.416815], [80.117035, 12.400946], [80.095958, 12.36225], [80.087169, 12.35102], [80.063731, 12.33747], [80.059825, 12.334215], [80.057465, 12.331244], [80.025727, 12.276516], [80.005138, 12.256049], [79.952159, 12.229926], [79.943858, 12.217841], [79.953136, 12.213568], [79.973806, 12.222398], [80.005138, 12.241767], [79.947602, 12.153632], [79.933604, 12.138739], [79.927501, 12.129869], [79.896983, 12.068752], [79.870128, 12.037421], [79.861827, 12.02204], [79.860525, 12.011542], [79.861827, 11.977973], [79.85963, 11.972724], [79.850352, 11.959133], [79.849539, 11.955064], [79.844981, 11.933254], [79.820323, 11.878648], [79.803233, 11.796088], [79.792735, 11.788072], [79.785899, 11.769192], [79.7671, 11.673651], [79.763438, 11.634263], [79.754161, 11.596991], [79.751964, 11.576972], [79.757335, 11.536078], [79.780772, 11.465074], [79.785981, 11.426174], [79.792979, 11.426174], [79.797374, 11.434882], [79.799164, 11.442206], [79.797618, 11.44831], [79.792979, 11.453518], [79.809337, 11.451361], [79.816661, 11.443061], [79.821951, 11.391059], [79.819347, 11.381496], [79.810313, 11.378892], [79.782725, 11.378892], [79.776052, 11.380357], [79.769298, 11.380927], [79.7588, 11.378363], [79.749848, 11.373684], [79.740408, 11.366441], [79.684337, 11.312201], [79.67628, 11.296454], [79.683116, 11.296454], [79.6963, 11.303412], [79.742035, 11.335598], [79.751964, 11.340522], [79.759288, 11.358344], [79.776378, 11.362982], [79.796886, 11.359036], [79.823741, 11.346666], [79.829926, 11.346096], [79.833018, 11.341986], [79.834972, 11.263861], [79.855317, 11.159857], [79.852224, 11.006985], [79.84669, 10.979844], [79.840668, 10.950141], [79.847341, 10.809638], [79.849864, 10.755194], [79.850271, 10.592108], [79.85963, 10.551459], [79.86378, 10.37873], [79.8567, 10.29328], [79.851329, 10.282213], [79.834321, 10.278754], [79.814626, 10.272406], [79.795095, 10.269232], [79.778575, 10.275377], [79.78004, 10.296617], [79.757091, 10.309068], [79.633149, 10.328599], [79.621267, 10.324164], [79.614106, 10.309556], [79.630382, 10.310492], [79.646495, 10.309638], [79.661794, 10.307074], [79.67628, 10.30272], [79.684581, 10.298733], [79.694672, 10.291653], [79.703624, 10.289049], [79.730805, 10.289049], [79.758962, 10.282213], [79.773692, 10.273668], [79.772472, 10.261705], [79.759776, 10.258246], [79.580089, 10.295885], [79.563487, 10.297105], [79.556651, 10.298651], [79.552745, 10.30272], [79.551768, 10.3133], [79.556895, 10.316067], [79.565196, 10.315741], [79.573904, 10.316962], [79.604015, 10.329088], [79.607107, 10.336005], [79.586925, 10.343695], [79.567719, 10.335883], [79.528575, 10.338324], [79.532237, 10.330024], [79.532237, 10.323188], [79.511485, 10.316596], [79.459727, 10.308295], [79.437266, 10.309556], [79.411957, 10.32099], [79.398123, 10.324652], [79.385509, 10.320136], [79.36378, 10.301907], [79.304942, 10.268134], [79.286143, 10.252834], [79.272227, 10.234442], [79.270274, 10.223334], [79.270681, 10.20954], [79.269216, 10.197821], [79.261974, 10.192857], [79.248302, 10.189195], [79.240001, 10.179755], [79.236339, 10.166571], [79.237478, 10.151923], [79.244314, 10.084947], [79.254161, 10.054267], [79.279063, 10.035834], [79.240733, 10.010565], [79.200369, 9.970282], [79.141856, 9.891181], [79.121349, 9.850246], [79.103526, 9.830146], [79.100841, 9.826077], [79.094086, 9.802314], [79.07781, 9.782172], [79.038829, 9.747219], [78.980805, 9.667182], [78.979828, 9.658637], [78.980235, 9.649115], [78.977306, 9.637926], [78.97283, 9.631293], [78.962087, 9.622707], [78.956798, 9.617499], [78.94752, 9.602851], [78.938324, 9.583686], [78.931488, 9.561672], [78.925304, 9.513617], [78.913259, 9.47427], [78.915294, 9.45303], [78.933442, 9.416449], [78.959727, 9.384955], [79.039887, 9.314683], [79.074718, 9.299872], [79.115571, 9.291246], [79.25172, 9.28852], [79.273774, 9.296698], [79.29713, 9.312649], [79.320079, 9.32331], [79.341075, 9.315863], [79.345714, 9.306545], [79.34254, 9.299018], [79.336681, 9.290473], [79.333669, 9.278266], [79.335704, 9.267646], [79.340587, 9.260932], [79.345225, 9.255927], [79.347341, 9.250312], [79.353526, 9.240912], [79.367931, 9.227607], [79.438243, 9.176337], [79.453624, 9.159003], [79.440278, 9.151353], [79.424001, 9.159084], [79.361013, 9.213446], [79.292735, 9.250312], [79.232432, 9.25788], [79.21697, 9.267401], [79.20102, 9.266506], [79.184093, 9.271796], [79.165375, 9.274237], [79.129405, 9.256334], [79.098643, 9.257229], [79.087169, 9.253811], [79.060802, 9.264594], [79.025727, 9.273749], [78.988943, 9.278266], [78.956798, 9.274848], [78.948985, 9.270494], [78.928966, 9.253811], [78.918468, 9.251614], [78.911306, 9.252143], [78.90447, 9.253567], [78.895518, 9.253811], [78.861013, 9.248969], [78.843761, 9.243964], [78.815196, 9.22842], [78.77711, 9.219387], [78.761567, 9.209703], [78.751964, 9.200588], [78.740733, 9.192816], [78.727794, 9.187445], [78.67628, 9.184149], [78.661306, 9.177191], [78.655121, 9.161566], [78.648936, 9.151435], [78.634776, 9.144599], [78.618826, 9.14053], [78.48585, 9.124823], [78.44988, 9.116604], [78.41212, 9.09984], [78.377452, 9.07746], [78.239757, 8.965562], [78.217621, 8.938951], [78.182791, 8.877102], [78.177908, 8.86107], [78.17449, 8.79442], [78.170421, 8.771877], [78.162364, 8.753485], [78.175792, 8.763902], [78.188731, 8.765448], [78.20045, 8.76968], [78.210216, 8.788275], [78.21518, 8.765448], [78.2046, 8.753323], [78.186534, 8.74494], [78.1692, 8.733629], [78.164806, 8.726508], [78.154959, 8.698879], [78.140473, 8.680609], [78.136729, 8.669867], [78.142589, 8.657904], [78.114594, 8.657904], [78.135509, 8.6258], [78.142345, 8.593085], [78.128266, 8.488023], [78.123302, 8.477525], [78.114594, 8.466132], [78.087169, 8.440497], [78.079845, 8.431952], [78.062755, 8.373196], [78.055919, 8.363715], [78.038829, 8.360907], [77.999766, 8.344916], [77.806163, 8.228664], [77.793468, 8.215318], [77.768321, 8.181545], [77.752452, 8.173814], [77.643809, 8.155707], [77.595225, 8.138983], [77.561046, 8.114569], [77.565278, 8.082506], [77.510916, 8.075995], [77.449718, 8.08747], [77.315603, 8.127753], [77.308604, 8.130927], [77.294688, 8.14704], [77.286794, 8.15351], [77.26295, 8.168402], [77.238943, 8.175116], [77.225271, 8.184394], [77.128754, 8.271796], [77.110362, 8.285183], [76.99586, 8.368109], [76.962576, 8.40469], [76.880138, 8.520697], [76.826508, 8.56981], [76.813731, 8.601264], [76.74171, 8.684516], [76.731293, 8.707953], [76.695649, 8.739814], [76.66033, 8.79975], [76.644542, 8.818996], [76.558604, 8.890692], [76.543956, 8.912502], [76.548106, 8.924221], [76.559093, 8.925238], [76.573253, 8.904446], [76.591156, 8.908149], [76.609874, 8.917955], [76.620616, 8.925442], [76.60727, 8.925767], [76.593761, 8.921047], [76.581554, 8.919908], [76.572276, 8.931627], [76.573985, 8.942572], [76.582286, 8.956366], [76.593272, 8.968207], [76.603282, 8.973212], [76.657563, 8.966376], [76.660818, 8.969875], [76.65211, 8.977281], [76.640391, 8.984442], [76.634288, 8.986884], [76.641938, 8.99136], [76.656505, 8.995063], [76.668468, 8.999823], [76.668468, 9.007392], [76.657888, 9.010688], [76.644054, 9.00788], [76.632091, 9.002346], [76.62672, 8.997463], [76.620616, 8.987128], [76.606456, 8.989407], [76.582774, 9.000556], [76.576834, 8.994127], [76.565929, 8.963284], [76.558604, 8.952094], [76.551117, 8.966376], [76.558116, 8.973049], [76.561534, 8.97899], [76.56544, 8.99433], [76.549653, 8.984442], [76.538829, 8.972602], [76.534434, 8.957831], [76.538097, 8.939114], [76.528087, 8.946967], [76.521495, 8.96133], [76.517914, 8.977444], [76.516856, 8.990627], [76.513845, 9.002143], [76.500173, 9.022284], [76.489268, 9.055569], [76.46046, 9.114936], [76.462413, 9.138332], [76.464529, 9.132514], [76.466645, 9.128974], [76.468272, 9.124579], [76.469086, 9.116604], [76.476573, 9.116604], [76.476248, 9.131252], [76.482758, 9.17182], [76.473318, 9.164252], [76.470958, 9.160142], [76.469086, 9.151353], [76.462413, 9.151353], [76.44337, 9.212144], [76.428233, 9.245347], [76.414561, 9.246975], [76.416026, 9.236314], [76.446788, 9.159613], [76.448741, 9.145168], [76.442556, 9.145168], [76.362966, 9.325181], [76.311778, 9.492987], [76.291026, 9.633205], [76.29713, 9.658433], [76.277354, 9.809272], [76.253184, 9.883734], [76.246755, 9.893744], [76.240245, 9.939399], [76.236339, 9.952704], [76.251475, 9.963121], [76.269542, 9.950995], [76.284679, 9.928534], [76.291026, 9.908596], [76.285655, 9.904364], [76.262218, 9.911566], [76.256847, 9.908596], [76.25766, 9.895819], [76.26059, 9.884711], [76.266856, 9.876044], [76.277354, 9.870754], [76.278575, 9.876899], [76.28419, 9.891181], [76.289073, 9.877143], [76.288259, 9.861233], [76.284434, 9.848212], [76.280772, 9.842841], [76.289806, 9.843817], [76.299978, 9.847805], [76.304698, 9.856269], [76.29713, 9.870754], [76.308604, 9.879625], [76.313813, 9.861314], [76.338878, 9.699408], [76.346202, 9.699408], [76.347341, 9.726874], [76.337576, 9.789252], [76.346202, 9.816107], [76.323985, 9.846177], [76.320079, 9.86164], [76.332693, 9.870754], [76.337413, 9.856187], [76.353038, 9.82982], [76.359223, 9.816107], [76.361095, 9.80386], [76.358653, 9.781806], [76.359223, 9.768297], [76.368012, 9.715562], [76.377126, 9.695502], [76.394054, 9.678941], [76.392426, 9.662828], [76.386567, 9.649848], [76.379161, 9.637763], [76.372895, 9.624335], [76.353282, 9.535346], [76.357432, 9.520697], [76.37672, 9.514472], [76.425792, 9.505276], [76.435069, 9.507636], [76.458995, 9.497301], [76.477061, 9.50373], [76.503266, 9.53498], [76.484386, 9.543769], [76.466563, 9.556383], [76.448497, 9.563422], [76.428233, 9.555406], [76.417247, 9.561428], [76.406993, 9.569078], [76.419688, 9.594184], [76.419607, 9.614488], [76.41505, 9.636176], [76.414561, 9.682685], [76.410004, 9.700263], [76.402599, 9.710598], [76.394054, 9.706244], [76.3921, 9.716946], [76.394786, 9.735175], [76.394054, 9.747219], [76.390798, 9.756985], [76.382091, 9.774726], [76.380382, 9.785102], [76.384044, 9.802639], [76.391287, 9.812201], [76.396495, 9.822659], [76.394054, 9.842841], [76.38559, 9.861518], [76.36378, 9.899237], [76.359223, 9.915432], [76.320811, 9.953315], [76.311534, 9.966946], [76.294281, 9.951321], [76.278168, 9.977932], [76.269298, 10.016262], [76.273936, 10.035834], [76.255056, 10.047675], [76.245128, 10.1022], [76.236339, 10.103461], [76.227712, 10.109565], [76.222911, 10.117621], [76.215343, 10.138251], [76.20753, 10.117133], [76.213552, 10.098822], [76.223481, 10.082099], [76.229015, 10.065904], [76.230968, 10.025824], [76.235037, 10.005072], [76.242686, 9.987454], [76.228201, 9.999945], [76.21461, 10.024482], [76.182384, 10.105658], [76.181163, 10.121161], [76.184906, 10.13581], [76.184581, 10.144924], [76.177745, 10.155341], [76.177501, 10.17064], [76.224132, 10.196926], [76.236339, 10.210273], [76.237315, 10.216986], [76.24171, 10.223944], [76.242686, 10.231024], [76.240245, 10.238959], [76.234386, 10.237942], [76.227794, 10.23428], [76.22283, 10.234442], [76.21339, 10.253241], [76.206309, 10.262885], [76.194835, 10.268622], [76.207856, 10.234565], [76.209483, 10.21601], [76.201671, 10.199693], [76.184093, 10.189358], [76.170584, 10.193752], [76.160492, 10.206244], [76.153168, 10.22016], [76.138194, 10.261054], [76.120616, 10.343573], [76.10613, 10.38467], [76.070079, 10.432807], [76.065115, 10.449856], [76.063324, 10.47016], [76.052419, 10.513983], [76.044607, 10.529283], [76.018728, 10.553412], [76.008067, 10.567084], [75.999685, 10.605536], [75.939138, 10.72606], [75.934744, 10.74433], [75.93214, 10.750963], [75.926931, 10.757717], [75.922048, 10.76557], [75.921072, 10.775702], [75.924083, 10.780707], [75.936534, 10.790717], [75.94158, 10.79621], [75.911143, 10.809231], [75.894542, 10.849758], [75.880138, 10.932685], [75.832774, 11.097846], [75.742931, 11.310126], [75.737804, 11.319485], [75.733653, 11.325181], [75.730805, 11.331732], [75.729259, 11.343655], [75.739513, 11.350735], [75.742442, 11.35871], [75.735525, 11.371527], [75.722179, 11.367499], [75.711192, 11.381781], [75.69516, 11.419989], [75.677094, 11.4501], [75.664806, 11.462633], [75.650157, 11.46776], [75.630138, 11.468736], [75.616547, 11.473619], [75.608897, 11.484931], [75.606293, 11.505316], [75.600841, 11.523383], [75.581879, 11.549791], [75.585297, 11.563381], [75.530772, 11.693671], [75.506114, 11.72724], [75.379731, 11.864976], [75.372569, 11.864732], [75.355805, 11.857652], [75.35255, 11.861558], [75.349376, 11.871975], [75.324555, 11.899115], [75.307628, 11.932196], [75.326508, 11.935858], [75.400238, 11.920233], [75.400238, 11.926459], [75.386567, 11.935126], [75.382823, 11.945502], [75.385997, 11.958319], [75.393403, 11.97484], [75.380219, 11.968695], [75.361339, 11.951239], [75.349132, 11.947577], [75.331716, 11.94953], [75.316091, 11.955634], [75.302257, 11.966051], [75.290375, 11.981106], [75.278819, 12.003241], [75.281098, 12.010647], [75.314708, 12.008368], [75.329763, 12.012641], [75.324962, 12.02265], [75.305593, 12.041775], [75.30421, 12.043158], [75.264334, 12.013332], [75.260509, 11.997789], [75.276703, 11.967434], [75.245616, 12.002916], [75.239268, 12.008368], [75.233246, 12.010484], [75.228201, 12.015082], [75.224457, 12.019355], [75.222179, 12.02204], [75.216482, 12.021064], [75.210623, 12.016995], [75.207042, 12.01203], [75.208507, 12.008368], [75.193696, 12.022284], [75.18629, 12.041571], [75.180512, 12.077297], [75.143809, 12.161322], [75.13087, 12.207831], [75.139659, 12.241767], [75.139659, 12.248603], [75.12672, 12.245836], [75.11964, 12.239], [75.113943, 12.23017], [75.105479, 12.221259], [75.10613, 12.23371], [75.104747, 12.24608], [75.098643, 12.269029], [75.082856, 12.305894], [75.018403, 12.41474], [75.010997, 12.435777], [75.009939, 12.443793], [75.006602, 12.449449], [74.99936, 12.454901], [74.992035, 12.461737], [74.98878, 12.471381], [74.985362, 12.49136], [74.976736, 12.507025], [74.955251, 12.537258], [74.879649, 12.725491], [74.870128, 12.742865], [74.855724, 12.759426], [74.854259, 12.765286], [74.854747, 12.787014], [74.852306, 12.797309], [74.844005, 12.80801], [74.834972, 12.815823], [74.827647, 12.824652], [74.824962, 12.838202], [74.85141, 12.825507], [74.859141, 12.824612], [74.866954, 12.830024], [74.881114, 12.848049], [74.889822, 12.851874], [74.901134, 12.85342], [74.917491, 12.85814], [74.932872, 12.866441], [74.940929, 12.878607], [74.894786, 12.871161], [74.850597, 12.857164], [74.817556, 12.861762], [74.770274, 13.052436], [74.783376, 13.084662], [74.774099, 13.093411], [74.769216, 13.099555], [74.76295, 13.118801], [74.761892, 13.143134], [74.759776, 13.155219], [74.75294, 13.160386], [74.74879, 13.170844], [74.737804, 13.221381], [74.733409, 13.262763], [74.715099, 13.331041], [74.707205, 13.348375], [74.696137, 13.365953], [74.689464, 13.381578], [74.694591, 13.393134], [74.687511, 13.410346], [74.68336, 13.442816], [74.680919, 13.468248], [74.690766, 13.439114], [74.693696, 13.415269], [74.708263, 13.413031], [74.701915, 13.420356], [74.702403, 13.442613], [74.701427, 13.461371], [74.677094, 13.52676], [74.677989, 13.54914], [74.671397, 13.603949], [74.673513, 13.619086], [74.669932, 13.622016], [74.667979, 13.625149], [74.667166, 13.628607], [74.666677, 13.632758], [74.672862, 13.628892], [74.677094, 13.630316], [74.679698, 13.636461], [74.680919, 13.646959], [74.687185, 13.646959], [74.696788, 13.640448], [74.709972, 13.64057], [74.723806, 13.645575], [74.735606, 13.653795], [74.720551, 13.652289], [74.712901, 13.656195], [74.701427, 13.673733], [74.688324, 13.68594], [74.688975, 13.690863], [74.701427, 13.69477], [74.687755, 13.711819], [74.676036, 13.701077], [74.660492, 13.660061], [74.646983, 13.692084], [74.623057, 13.774156], [74.614594, 13.835517], [74.595958, 13.871487], [74.583995, 13.903876], [74.550792, 13.945502], [74.536388, 13.969875], [74.504731, 14.01024], [74.495372, 14.030585], [74.494802, 14.040717], [74.496267, 14.06151], [74.495372, 14.071519], [74.491384, 14.080634], [74.479177, 14.095649], [74.474864, 14.105699], [74.473155, 14.161689], [74.467459, 14.181383], [74.438731, 14.22012], [74.429047, 14.240424], [74.43336, 14.263373], [74.447032, 14.253974], [74.471039, 14.248684], [74.494965, 14.247463], [74.508962, 14.250312], [74.500011, 14.257961], [74.436371, 14.27969], [74.427745, 14.28384], [74.415375, 14.301093], [74.412608, 14.344794], [74.403575, 14.362982], [74.396983, 14.372748], [74.394379, 14.384914], [74.394216, 14.410793], [74.391856, 14.419013], [74.386729, 14.424221], [74.381602, 14.43122], [74.374278, 14.467271], [74.353852, 14.505072], [74.351899, 14.530219], [74.367035, 14.512356], [74.377452, 14.484605], [74.388845, 14.462714], [74.407237, 14.461982], [74.397634, 14.475409], [74.401134, 14.479722], [74.427745, 14.476264], [74.385427, 14.524115], [74.372406, 14.530219], [74.374685, 14.533759], [74.377126, 14.541002], [74.379242, 14.544582], [74.368337, 14.549709], [74.364106, 14.54914], [74.358735, 14.544582], [74.355479, 14.557074], [74.355805, 14.562405], [74.358735, 14.571234], [74.342947, 14.563666], [74.340994, 14.551174], [74.342947, 14.536851], [74.339041, 14.523993], [74.32488, 14.517239], [74.309825, 14.519436], [74.300548, 14.527411], [74.303559, 14.537665], [74.295909, 14.565009], [74.295177, 14.587307], [74.307384, 14.601874], [74.339041, 14.605943], [74.339041, 14.612779], [74.323904, 14.618598], [74.312266, 14.615465], [74.301117, 14.60932], [74.28712, 14.605943], [74.27589, 14.610175], [74.268077, 14.61994], [74.266856, 14.631293], [74.276215, 14.640082], [74.269216, 14.658922], [74.263194, 14.701606], [74.25294, 14.71894], [74.23406, 14.734076], [74.22047, 14.738105], [74.208751, 14.731838], [74.194347, 14.715806], [74.180675, 14.742174], [74.173839, 14.749945], [74.169444, 14.750881], [74.157725, 14.748969], [74.153331, 14.749945], [74.146495, 14.761868], [74.145844, 14.763617], [74.111339, 14.780341], [74.096446, 14.793362], [74.091319, 14.811428], [74.109141, 14.802639], [74.114024, 14.808417], [74.114268, 14.82095], [74.118663, 14.831855], [74.128754, 14.83747], [74.140473, 14.838772], [74.151541, 14.83511], [74.159516, 14.82567], [74.169688, 14.844468], [74.178966, 14.856269], [74.190684, 14.863105], [74.208018, 14.866645], [74.21811, 14.865383], [74.226899, 14.862738], [74.233084, 14.864569], [74.235199, 14.876532], [74.230724, 14.880113], [74.220876, 14.882066], [74.211192, 14.887356], [74.208018, 14.900824], [74.19752, 14.890855], [74.166189, 14.876532], [74.159516, 14.869778], [74.154063, 14.862128], [74.14088, 14.852688], [74.125011, 14.845852], [74.111827, 14.846177], [74.103038, 14.85692], [74.091156, 14.8876], [74.081065, 14.893988], [74.077159, 14.898383], [74.035981, 14.921291], [74.030284, 14.935207], [74.031016, 14.949774], [74.035981, 14.979641], [74.031261, 14.995307], [74.020193, 15.001776], [74.006847, 15.004869], [73.995779, 15.010688], [73.972992, 15.056871], [73.964366, 15.064643], [73.913097, 15.078925], [73.922537, 15.107856], [73.928966, 15.120592], [73.937022, 15.130439], [73.958263, 15.143134], [73.962169, 15.152045], [73.954112, 15.16828], [73.947276, 15.160834], [73.891287, 15.343207], [73.882579, 15.355292], [73.869477, 15.359565], [73.848399, 15.360093], [73.827403, 15.36518], [73.810395, 15.37759], [73.782888, 15.407904], [73.839366, 15.404039], [73.848399, 15.404486], [73.860118, 15.407864], [73.871918, 15.401842], [73.884939, 15.398017], [73.899587, 15.407904], [73.93336, 15.395413], [73.945323, 15.385688], [73.954112, 15.366278], [73.958507, 15.367174], [73.961681, 15.368232], [73.964203, 15.370185], [73.967784, 15.373765], [73.959972, 15.380683], [73.937022, 15.411322], [73.927582, 15.417385], [73.885916, 15.435207], [73.879161, 15.435777], [73.865082, 15.434312], [73.858572, 15.435207], [73.854177, 15.4383], [73.846853, 15.447089], [73.841563, 15.448879], [73.806163, 15.452216], [73.796397, 15.448879], [73.795177, 15.460761], [73.800466, 15.475002], [73.808849, 15.488023], [73.817556, 15.496649], [73.828136, 15.50141], [73.872244, 15.510932], [73.863129, 15.516343], [73.859548, 15.521145], [73.860525, 15.525865], [73.865977, 15.531399], [73.865977, 15.537584], [73.796397, 15.500393], [73.786388, 15.492174], [73.766287, 15.49726], [73.751231, 15.513129], [73.756114, 15.537584], [73.741222, 15.559963], [73.736827, 15.571723], [73.735118, 15.586005], [73.737071, 15.598538], [73.74171, 15.604193], [73.746593, 15.608385], [73.748709, 15.616767], [73.760427, 15.632025], [73.787364, 15.641791], [73.838145, 15.654283], [73.838145, 15.661119], [73.801036, 15.661851], [73.783865, 15.659125], [73.776622, 15.650865], [73.76824, 15.646918], [73.733246, 15.625067], [73.728282, 15.619574], [73.718272, 15.627143], [73.707042, 15.643704], [73.697927, 15.662828], [73.690766, 15.693793], [73.685883, 15.704291], [73.686778, 15.713121], [73.700857, 15.723212], [73.689871, 15.724473], [73.672374, 15.726508], [73.656016, 15.730292], [73.646251, 15.736884], [73.643565, 15.749091], [73.646251, 15.792141], [73.638845, 15.817206], [73.604666, 15.880845], [73.587576, 15.902899], [73.528982, 15.949164], [73.510102, 15.967597], [73.490977, 15.993476], [73.487315, 16.014228], [73.515961, 16.017401], [73.515961, 16.024848], [73.499278, 16.024319], [73.483897, 16.028225], [73.472504, 16.036363], [73.46811, 16.048489], [73.464854, 16.051663], [73.45753, 16.054185], [73.450369, 16.058905], [73.447032, 16.068915], [73.452973, 16.074164], [73.45753, 16.079535], [73.460704, 16.08633], [73.460704, 16.14468], [73.46339, 16.159573], [73.470225, 16.168891], [73.479259, 16.177232], [73.487966, 16.189358], [73.465017, 16.182278], [73.45281, 16.166571], [73.448009, 16.143297], [73.447032, 16.113593], [73.440196, 16.162014], [73.440196, 16.202338], [73.435232, 16.217841], [73.430837, 16.218736], [73.425955, 16.215155], [73.419688, 16.216702], [73.413585, 16.223944], [73.410411, 16.230455], [73.406098, 16.2508], [73.378673, 16.332709], [73.377778, 16.337958], [73.378673, 16.35692], [73.375662, 16.360256], [73.358246, 16.367499], [73.359386, 16.380601], [73.364431, 16.389309], [73.373057, 16.395657], [73.384939, 16.401597], [73.367442, 16.408637], [73.352061, 16.418118], [73.341319, 16.432074], [73.333995, 16.468166], [73.319184, 16.509752], [73.313813, 16.518297], [73.305837, 16.52265], [73.302989, 16.533026], [73.303722, 16.555569], [73.307302, 16.563178], [73.314952, 16.557603], [73.321951, 16.548489], [73.323497, 16.544989], [73.344005, 16.523586], [73.355724, 16.519436], [73.378673, 16.518297], [73.369477, 16.52147], [73.362315, 16.525458], [73.356212, 16.530748], [73.350841, 16.538153], [73.35434, 16.543158], [73.365001, 16.554389], [73.371267, 16.559272], [73.354015, 16.559638], [73.336436, 16.565619], [73.322765, 16.576158], [73.317231, 16.590277], [73.316091, 16.598293], [73.314626, 16.599433], [73.316091, 16.600043], [73.323497, 16.606431], [73.326996, 16.607123], [73.358246, 16.620673], [73.358409, 16.603949], [73.363048, 16.596829], [73.368663, 16.599026], [73.371267, 16.610175], [73.366466, 16.628079], [73.354259, 16.63467], [73.338878, 16.631659], [73.323497, 16.620673], [73.312185, 16.644721], [73.310557, 16.651435], [73.312511, 16.658393], [73.321462, 16.670844], [73.323497, 16.679104], [73.321625, 16.69245], [73.31658, 16.699897], [73.300304, 16.713202], [73.289561, 16.74022], [73.295258, 16.768052], [73.304454, 16.793524], [73.303722, 16.813137], [73.296886, 16.80565], [73.286632, 16.820868], [73.274669, 16.848822], [73.269379, 16.875637], [73.279552, 16.887641], [73.279063, 16.891913], [73.269379, 16.929185], [73.270518, 16.945014], [73.274913, 16.958645], [73.289399, 16.983832], [73.26238, 16.996975], [73.254568, 17.006741], [73.262706, 17.018541], [73.251231, 17.032172], [73.251475, 17.042426], [73.258962, 17.044094], [73.269379, 17.031643], [73.277354, 17.054674], [73.275727, 17.078599], [73.267426, 17.101386], [73.255138, 17.120998], [73.261974, 17.134955], [73.26059, 17.150539], [73.255138, 17.165188], [73.236176, 17.199205], [73.236013, 17.220608], [73.218028, 17.253241], [73.214854, 17.268744], [73.211192, 17.275092], [73.193126, 17.294013], [73.187022, 17.305976], [73.203461, 17.298733], [73.217133, 17.286851], [73.231212, 17.279283], [73.249034, 17.284857], [73.234711, 17.302314], [73.207367, 17.353746], [73.195974, 17.369371], [73.170584, 17.389716], [73.166515, 17.402167], [73.165294, 17.413031], [73.177094, 17.426663], [73.185395, 17.441881], [73.17335, 17.457424], [73.180675, 17.477688], [73.165538, 17.503404], [73.131684, 17.539374], [73.125987, 17.556545], [73.12794, 17.564683], [73.136404, 17.567206], [73.149425, 17.567288], [73.163422, 17.569485], [73.200694, 17.586493], [73.185883, 17.592515], [73.154063, 17.595608], [73.139171, 17.600775], [73.128184, 17.611558], [73.121349, 17.62641], [73.120291, 17.642035], [73.12615, 17.655422], [73.118907, 17.663479], [73.110525, 17.675727], [73.105479, 17.68891], [73.108165, 17.699774], [73.117361, 17.711249], [73.12322, 17.723334], [73.123302, 17.735785], [73.115001, 17.748196], [73.109874, 17.758287], [73.106619, 17.785549], [73.101329, 17.796332], [73.08546, 17.813666], [73.079438, 17.824612], [73.073741, 17.857123], [73.065196, 17.878363], [73.018565, 17.951728], [73.0171, 17.972113], [73.035411, 17.991848], [73.018891, 17.992499], [73.008637, 18.00023], [73.005219, 18.012356], [73.0088, 18.025946], [73.019542, 18.035875], [73.032481, 18.041083], [73.042247, 18.048977], [73.043468, 18.066962], [73.031993, 18.06391], [73.022716, 18.057685], [73.015147, 18.04914], [73.0088, 18.039008], [73.00115, 18.052069], [72.987315, 18.066555], [72.980968, 18.080552], [72.983165, 18.081732], [72.986664, 18.087714], [72.988129, 18.096015], [72.98463, 18.104193], [72.978363, 18.111884], [72.976085, 18.118638], [72.967296, 18.19599], [72.954845, 18.217515], [72.926931, 18.223944], [72.935232, 18.244778], [72.939952, 18.251288], [72.934825, 18.25788], [72.933279, 18.266588], [72.935069, 18.276313], [72.939952, 18.286038], [72.953136, 18.281317], [72.95753, 18.274604], [72.959239, 18.265855], [72.964529, 18.25495], [72.972667, 18.244696], [72.976736, 18.24258], [72.98227, 18.244208], [72.995128, 18.245063], [73.012462, 18.24315], [73.0324, 18.23786], [73.050955, 18.227484], [73.063487, 18.210354], [73.047618, 18.201158], [73.047618, 18.188788], [73.057465, 18.185614], [73.071544, 18.204088], [73.073985, 18.224189], [73.065278, 18.257554], [73.071544, 18.273017], [73.056977, 18.280951], [73.042979, 18.282375], [73.012462, 18.279202], [72.994884, 18.283596], [72.977387, 18.295071], [72.968028, 18.310614], [72.974783, 18.327582], [72.958751, 18.328111], [72.950857, 18.332953], [72.933604, 18.354926], [72.927908, 18.357571], [72.922537, 18.356635], [72.917328, 18.356391], [72.912608, 18.361151], [72.909841, 18.368069], [72.910167, 18.372504], [72.911632, 18.377346], [72.912608, 18.385647], [72.910167, 18.399237], [72.904307, 18.408515], [72.897227, 18.417304], [72.891449, 18.429389], [72.889659, 18.441311], [72.891449, 18.480902], [72.895763, 18.502997], [72.907237, 18.523586], [72.924653, 18.535631], [72.946788, 18.532416], [72.961111, 18.517727], [72.990245, 18.469957], [73.0088, 18.450507], [73.014985, 18.469957], [73.004405, 18.480048], [72.98878, 18.489936], [72.980968, 18.508857], [72.977875, 18.524115], [72.970388, 18.541002], [72.960297, 18.554674], [72.950206, 18.56037], [72.921153, 18.554877], [72.910167, 18.558987], [72.905772, 18.577135], [72.882172, 18.635484], [72.864513, 18.648912], [72.855317, 18.681138], [72.851736, 18.719672], [72.852387, 18.769924], [72.856944, 18.7862], [72.865571, 18.799384], [72.879161, 18.807359], [72.887055, 18.807034], [72.902029, 18.799506], [72.912608, 18.799994], [72.926931, 18.826646], [72.938975, 18.816962], [72.974783, 18.779486], [72.966319, 18.76142], [72.969574, 18.746975], [72.980642, 18.736762], [72.995128, 18.731024], [72.988048, 18.753852], [72.99171, 18.779364], [73.002452, 18.801459], [73.016287, 18.813625], [72.987966, 18.824937], [72.981293, 18.832709], [72.974783, 18.848334], [72.976085, 18.854397], [72.979747, 18.863349], [72.9817, 18.871487], [72.977794, 18.874986], [72.972179, 18.874254], [72.96697, 18.872138], [72.963145, 18.868964], [72.961681, 18.865139], [72.946137, 18.858303], [72.91627, 18.87051], [72.896821, 18.894477], [72.912608, 18.923489], [72.928071, 18.911363], [72.934581, 18.920803], [72.936778, 18.947414], [72.949229, 18.951483], [72.96339, 18.947984], [72.977224, 18.948432], [72.988292, 18.964423], [72.953624, 18.964423], [72.953624, 18.971259], [72.968272, 18.972602], [72.992035, 18.982489], [73.042166, 18.995754], [73.046641, 18.995185], [73.056814, 19.015326], [73.039806, 19.017524], [73.011892, 19.009426], [72.988292, 18.998603], [72.983572, 19.010443], [72.987804, 19.036933], [72.980968, 19.046373], [72.991873, 19.067206], [72.985688, 19.074286], [72.973806, 19.078559], [72.967296, 19.09101], [72.974783, 19.163072], [72.967296, 19.163072], [72.950043, 19.127916], [72.935557, 19.115058], [72.93393, 19.080308], [72.926931, 19.06684], [72.94337, 19.055121], [72.934255, 19.034084], [72.891449, 18.991767], [72.891449, 19.00137], [72.885265, 19.025865], [72.869314, 19.01142], [72.851085, 18.987128], [72.836274, 18.959133], [72.827647, 18.918362], [72.8213, 18.903306], [72.812511, 18.897447], [72.802745, 18.909817], [72.802013, 18.920478], [72.805349, 18.930976], [72.807384, 18.942816], [72.802745, 18.957587], [72.795909, 18.957587], [72.792328, 18.951239], [72.788259, 18.946519], [72.775401, 18.93651], [72.770356, 18.948188], [72.776052, 18.959703], [72.795909, 18.984931], [72.802419, 18.999661], [72.804942, 19.011054], [72.802745, 19.040188], [72.812999, 19.035549], [72.817068, 19.032701], [72.821788, 19.041978], [72.824229, 19.053534], [72.8213, 19.063178], [72.809581, 19.06684], [72.821951, 19.097113], [72.82309, 19.108466], [72.821056, 19.119208], [72.811778, 19.137356], [72.809581, 19.146308], [72.812185, 19.153713], [72.822439, 19.163764], [72.82309, 19.169257], [72.818044, 19.17536], [72.811209, 19.175523], [72.805186, 19.17182], [72.799571, 19.156887], [72.792166, 19.150458], [72.784923, 19.150377], [72.781586, 19.159654], [72.783376, 19.168647], [72.802501, 19.211615], [72.819835, 19.240627], [72.830089, 19.252427], [72.815115, 19.253648], [72.802094, 19.23786], [72.789399, 19.217353], [72.775401, 19.204657], [72.772227, 19.265123], [72.775645, 19.299018], [72.785411, 19.31391], [72.839366, 19.319078], [72.867035, 19.316799], [72.885509, 19.296047], [72.900564, 19.290758], [72.930349, 19.286566], [72.946544, 19.289862], [72.959727, 19.294827], [72.972667, 19.293687], [72.988292, 19.27912], [73.010509, 19.224311], [73.026622, 19.205512], [73.049653, 19.217719], [73.03004, 19.227729], [73.028005, 19.244086], [73.029145, 19.259345], [73.019298, 19.266059], [73.012462, 19.272447], [73.001801, 19.286566], [72.987804, 19.300686], [72.970958, 19.307034], [72.916759, 19.303371], [72.898936, 19.307034], [72.883556, 19.316392], [72.855317, 19.339504], [72.836925, 19.348049], [72.818533, 19.330268], [72.802094, 19.331936], [72.787608, 19.345201], [72.763438, 19.380194], [72.759776, 19.388983], [72.74822, 19.444241], [72.746837, 19.467963], [72.754405, 19.481757], [72.769216, 19.490465], [72.879161, 19.532375], [72.8567, 19.541327], [72.835948, 19.536689], [72.815929, 19.52851], [72.795909, 19.526801], [72.787852, 19.53026], [72.781261, 19.536566], [72.777029, 19.545559], [72.775401, 19.557196], [72.770681, 19.566148], [72.76002, 19.573065], [72.741954, 19.580797], [72.734548, 19.580797], [72.738292, 19.570258], [72.740733, 19.560736], [72.739757, 19.55268], [72.734548, 19.546617], [72.749278, 19.534247], [72.754893, 19.527045], [72.755626, 19.519355], [72.749197, 19.5126], [72.741547, 19.516018], [72.722504, 19.536282], [72.710785, 19.587592], [72.718028, 19.606676], [72.709646, 19.64997], [72.686697, 19.724799], [72.694102, 19.720201], [72.699962, 19.714179], [72.7046, 19.706529], [72.707774, 19.697455], [72.72641, 19.705797], [72.72641, 19.715237], [72.718598, 19.727932], [72.714041, 19.745836], [72.717296, 19.751532], [72.724376, 19.752672], [72.731293, 19.754584], [72.734548, 19.762641], [72.73406, 19.771918], [72.732758, 19.779731], [72.728282, 19.793647], [72.690115, 19.748969], [72.677908, 19.746568], [72.675792, 19.764146], [72.679861, 19.800482], [72.672862, 19.812812], [72.650076, 19.841986], [72.649181, 19.851996], [72.65919, 19.866441], [72.664236, 19.881578], [72.666189, 19.916571], [72.662771, 19.941799], [72.665212, 19.952948], [72.676443, 19.957506], [72.684337, 19.964016], [72.694509, 19.979315], [72.70338, 19.997626], [72.707774, 20.012763], [72.707774, 20.071112], [72.710297, 20.082913], [72.728282, 20.129462], [72.734548, 20.173529], [72.737641, 20.184272], [72.752289, 20.203762], [72.755626, 20.215155], [72.753591, 20.218207], [72.749034, 20.218329], [72.744395, 20.219387], [72.741954, 20.225043], [72.741954, 20.245551], [72.743012, 20.247748], [72.74822, 20.280341], [72.756602, 20.295966], [72.767833, 20.311957], [72.777517, 20.330064], [72.781586, 20.352281], [72.786794, 20.35224], [72.798188, 20.35932], [72.810313, 20.368842], [72.817068, 20.375922], [72.820811, 20.386135], [72.82309, 20.416815], [72.829438, 20.437934], [72.840017, 20.458726], [72.879161, 20.512437], [72.886485, 20.531562], [72.890391, 20.55268], [72.891449, 20.575141], [72.890636, 20.587063], [72.886241, 20.608466], [72.885265, 20.619208], [72.888438, 20.628485], [72.902599, 20.641913], [72.905772, 20.653306], [72.909434, 20.698676], [72.90797, 20.718329], [72.898936, 20.732164], [72.940278, 20.759426], [72.946788, 20.759467], [72.944998, 20.770006], [72.936371, 20.776272], [72.926443, 20.780992], [72.919444, 20.787421], [72.919119, 20.7956], [72.922862, 20.80268], [72.924978, 20.810614], [72.919444, 20.821519], [72.900238, 20.79914], [72.885753, 20.807115], [72.880138, 20.829901], [72.888438, 20.851955], [72.900401, 20.868109], [72.893809, 20.870063], [72.881196, 20.865668], [72.875336, 20.862494], [72.836925, 20.835842], [72.845876, 20.851793], [72.854259, 20.887152], [72.864919, 20.897284], [72.836681, 20.929918], [72.856456, 20.947984], [72.886485, 20.962104], [72.888438, 20.982611], [72.875987, 20.98135], [72.85377, 20.974921], [72.832693, 20.972357], [72.82309, 20.982611], [72.8213, 20.991604], [72.816091, 20.99901], [72.80836, 21.003974], [72.799327, 21.005845], [72.787608, 21.006903], [72.781993, 21.010159], [72.771983, 21.023871], [72.766856, 21.03856], [72.783946, 21.040432], [72.82309, 21.03384], [72.844981, 21.038316], [72.849864, 21.048], [72.815929, 21.121487], [72.809581, 21.130032], [72.799571, 21.128811], [72.755626, 21.109565], [72.752778, 21.113715], [72.746349, 21.118354], [72.741954, 21.123236], [72.737641, 21.117377], [72.735606, 21.110745], [72.734548, 21.092475], [72.730154, 21.087836], [72.700369, 21.074774], [72.705821, 21.10635], [72.720551, 21.129706], [72.728689, 21.148098], [72.714041, 21.16413], [72.706554, 21.157701], [72.686697, 21.158149], [72.676443, 21.153632], [72.672699, 21.145738], [72.671886, 21.134426], [72.673025, 21.112616], [72.671235, 21.099799], [72.666352, 21.097398], [72.658702, 21.097602], [72.637869, 21.085395], [72.629649, 21.087958], [72.615001, 21.105862], [72.615489, 21.119818], [72.630382, 21.135972], [72.693533, 21.178453], [72.711762, 21.194037], [72.721039, 21.198798], [72.734548, 21.19892], [72.723643, 21.209133], [72.687673, 21.220445], [72.679861, 21.22899], [72.673188, 21.239081], [72.659353, 21.244696], [72.646983, 21.253607], [72.645763, 21.273383], [72.625173, 21.259345], [72.618419, 21.252916], [72.60613, 21.266588], [72.602061, 21.277655], [72.597911, 21.307563], [72.588634, 21.331977], [72.586762, 21.341213], [72.590505, 21.348538], [72.603201, 21.352729], [72.63852, 21.357164], [72.645763, 21.362779], [72.638682, 21.371243], [72.623383, 21.370266], [72.594086, 21.362779], [72.578868, 21.367011], [72.567638, 21.37641], [72.564626, 21.38581], [72.573741, 21.390082], [72.635102, 21.407457], [72.672862, 21.428127], [72.676443, 21.431627], [72.728282, 21.465806], [72.747081, 21.452867], [72.752615, 21.463568], [72.748546, 21.479641], [72.738292, 21.482856], [72.683279, 21.465806], [72.66505, 21.457221], [72.612478, 21.421861], [72.583669, 21.417426], [72.589854, 21.432766], [72.61964, 21.487372], [72.635102, 21.499335], [72.651052, 21.507025], [72.690115, 21.540676], [72.703949, 21.548407], [72.726817, 21.553168], [72.75115, 21.565131], [72.773204, 21.580512], [72.789073, 21.595526], [72.810395, 21.635077], [72.82309, 21.643948], [72.833995, 21.644477], [72.858735, 21.6376], [72.871593, 21.636542], [72.889659, 21.64053], [72.935557, 21.658352], [72.965343, 21.678778], [73.022472, 21.698554], [73.084972, 21.729682], [73.108165, 21.733344], [73.116954, 21.73725], [73.126964, 21.746772], [73.132009, 21.757799], [73.12615, 21.766832], [73.118826, 21.764716], [73.083995, 21.746405], [73.046641, 21.733344], [73.041026, 21.73017], [73.022472, 21.71601], [73.012462, 21.712836], [72.991466, 21.708441], [72.950206, 21.689154], [72.926931, 21.684963], [72.882823, 21.69302], [72.858165, 21.694973], [72.840343, 21.688381], [72.819184, 21.675035], [72.798106, 21.673529], [72.755626, 21.684963], [72.733246, 21.687486], [72.676443, 21.678127], [72.653005, 21.680569], [72.634939, 21.684638], [72.616059, 21.685452], [72.570486, 21.670478], [72.550304, 21.666693], [72.534679, 21.672919], [72.528494, 21.695136], [72.532074, 21.713853], [72.550141, 21.751939], [72.556326, 21.774319], [72.562755, 21.839423], [72.566905, 21.849351], [72.58546, 21.858873], [72.598806, 21.881008], [72.618419, 21.925116], [72.634044, 21.937486], [72.65211, 21.941067], [72.690115, 21.938788], [72.702322, 21.947577], [72.706798, 21.966132], [72.714041, 21.982978], [72.734548, 21.986558], [72.734548, 21.994045], [72.72641, 21.995754], [72.7199, 21.996324], [72.713715, 21.995795], [72.707774, 21.994045], [72.700938, 21.988918], [72.69752, 21.982123], [72.695649, 21.975898], [72.693533, 21.972317], [72.623057, 21.96662], [72.611583, 21.955512], [72.602712, 21.94359], [72.581798, 21.928168], [72.558604, 21.915269], [72.542735, 21.910834], [72.533376, 21.920315], [72.507091, 21.962307], [72.50172, 21.97602], [72.504568, 21.994208], [72.517751, 22.032131], [72.522227, 22.054267], [72.521495, 22.066148], [72.519542, 22.077541], [72.519542, 22.088446], [72.53004, 22.107489], [72.533376, 22.118557], [72.535899, 22.14053], [72.542166, 22.156724], [72.557384, 22.176663], [72.57545, 22.194525], [72.590505, 22.205024], [72.614106, 22.211737], [72.634451, 22.210395], [72.652354, 22.202338], [72.669607, 22.18891], [72.681895, 22.181586], [72.691661, 22.181952], [72.699718, 22.184963], [72.707774, 22.185777], [72.719412, 22.182196], [72.729259, 22.177436], [72.74822, 22.164049], [72.790212, 22.217231], [72.812266, 22.234117], [72.847423, 22.240424], [72.865082, 22.233466], [72.881114, 22.220404], [72.897227, 22.213039], [72.916026, 22.22309], [72.92213, 22.236518], [72.922699, 22.254584], [72.917491, 22.27143], [72.905772, 22.281399], [72.899669, 22.269192], [72.886078, 22.262397], [72.869477, 22.262397], [72.854259, 22.270819], [72.838634, 22.276557], [72.818207, 22.273424], [72.798676, 22.26557], [72.785411, 22.256903], [72.756602, 22.24372], [72.726573, 22.248928], [72.659434, 22.273912], [72.646495, 22.273912], [72.635265, 22.271633], [72.625011, 22.271389], [72.606619, 22.28384], [72.596365, 22.286933], [72.573741, 22.288235], [72.553071, 22.282457], [72.521169, 22.253119], [72.50172, 22.240424], [72.479015, 22.233385], [72.466156, 22.232571], [72.457042, 22.236721], [72.454845, 22.245307], [72.46697, 22.255845], [72.463878, 22.263983], [72.448741, 22.266832], [72.432302, 22.255845], [72.413748, 22.246324], [72.391856, 22.253485], [72.386241, 22.266506], [72.381033, 22.289944], [72.380382, 22.312405], [72.388194, 22.322333], [72.404959, 22.332953], [72.408051, 22.353461], [72.397227, 22.366767], [72.371349, 22.355902], [72.361013, 22.343451], [72.34962, 22.324368], [72.340505, 22.303209], [72.336599, 22.284817], [72.328298, 22.260728], [72.309093, 22.257229], [72.288341, 22.261298], [72.275157, 22.260321], [72.263194, 22.276353], [72.265961, 22.28734], [72.264659, 22.295233], [72.240408, 22.301907], [72.223399, 22.303168], [72.208507, 22.300442], [72.200938, 22.291449], [72.206309, 22.273912], [72.161794, 22.280951], [72.148774, 22.277899], [72.166026, 22.260321], [72.190684, 22.251288], [72.209239, 22.259426], [72.240408, 22.288235], [72.246104, 22.27558], [72.243175, 22.265937], [72.237315, 22.256293], [72.234141, 22.243557], [72.239106, 22.238918], [72.268321, 22.226752], [72.306488, 22.22956], [72.317231, 22.184882], [72.307628, 22.079006], [72.298025, 22.061672], [72.295665, 22.051174], [72.291759, 22.053412], [72.283458, 22.043606], [72.276134, 22.031643], [72.275157, 22.027533], [72.25766, 22.012152], [72.24936, 22.008287], [72.237315, 22.007025], [72.220958, 22.010972], [72.216482, 22.007799], [72.214366, 21.994045], [72.206309, 21.994045], [72.19988, 22.006985], [72.19516, 22.003648], [72.191173, 21.993638], [72.186371, 21.986558], [72.182465, 21.982733], [72.173106, 21.975653], [72.163422, 21.973212], [72.15919, 21.98314], [72.151703, 21.993883], [72.134288, 22.004584], [72.11378, 22.010565], [72.097016, 22.007025], [72.119314, 21.993638], [72.128103, 21.983547], [72.124848, 21.972317], [72.114919, 21.969387], [72.076508, 21.979722], [72.08253, 21.973375], [72.104503, 21.959215], [72.104828, 21.951483], [72.102061, 21.945054], [72.096202, 21.94302], [72.087413, 21.948432], [72.073253, 21.954495], [72.055186, 21.953518], [72.038422, 21.947577], [72.028656, 21.938788], [72.087576, 21.919664], [72.104503, 21.91767], [72.1421, 21.924994], [72.156505, 21.925849], [72.145356, 21.91767], [72.145356, 21.910834], [72.15919, 21.904608], [72.152354, 21.897854], [72.16977, 21.890448], [72.167328, 21.879136], [72.160004, 21.865912], [72.162608, 21.852851], [72.169444, 21.841376], [72.161876, 21.839179], [72.13087, 21.846259], [72.123302, 21.853746], [72.111339, 21.869859], [72.098481, 21.88052], [72.077647, 21.894436], [72.057953, 21.898749], [72.049327, 21.880439], [72.052908, 21.876532], [72.061534, 21.873114], [72.071788, 21.870795], [72.080333, 21.869859], [72.086436, 21.865627], [72.104503, 21.835761], [72.086762, 21.830268], [72.062836, 21.834377], [72.004161, 21.857733], [71.997325, 21.854926], [71.99464, 21.839504], [71.994151, 21.812201], [71.996104, 21.799506], [72.001475, 21.787909], [72.005382, 21.792955], [72.009613, 21.7956], [72.021821, 21.800971], [72.021495, 21.79442], [72.022309, 21.780911], [72.021821, 21.774319], [72.054942, 21.78559], [72.073904, 21.788723], [72.087413, 21.784491], [72.103201, 21.775214], [72.117442, 21.776597], [72.166026, 21.793931], [72.171072, 21.790961], [72.172699, 21.777737], [72.177013, 21.766669], [72.195811, 21.757799], [72.200043, 21.750067], [72.202403, 21.748114], [72.214366, 21.712836], [72.219005, 21.706], [72.223806, 21.700588], [72.230235, 21.69599], [72.240408, 21.691718], [72.251801, 21.690904], [72.263031, 21.692328], [72.271739, 21.690904], [72.278331, 21.671942], [72.285411, 21.665025], [72.292491, 21.659491], [72.295665, 21.653876], [72.296641, 21.642157], [72.298513, 21.633979], [72.298839, 21.626288], [72.295665, 21.616034], [72.292003, 21.611762], [72.279145, 21.600531], [72.275157, 21.595526], [72.242198, 21.48078], [72.2199, 21.444729], [72.16684, 21.392076], [72.15919, 21.373277], [72.151622, 21.360175], [72.116059, 21.32274], [72.104503, 21.315009], [72.110037, 21.304836], [72.111501, 21.296088], [72.107758, 21.289984], [72.097016, 21.287665], [72.10255, 21.2744], [72.099376, 21.26496], [72.093598, 21.258205], [72.090831, 21.252916], [72.093516, 21.243557], [72.104503, 21.222154], [72.104747, 21.205064], [72.090831, 21.191474], [72.084809, 21.188707], [72.07545, 21.185696], [72.066661, 21.181342], [72.062836, 21.174709], [72.055024, 21.168362], [71.990733, 21.136379], [71.972423, 21.13227], [71.92628, 21.130032], [71.806163, 21.062079], [71.788585, 21.047512], [71.76295, 21.032294], [71.743663, 21.027574], [71.730235, 21.030707], [71.717621, 21.03205], [71.699743, 21.021306], [71.68336, 21.011461], [71.639985, 20.996812], [71.583507, 20.958238], [71.549571, 20.951239], [71.557302, 20.96601], [71.562348, 20.972968], [71.569347, 20.979193], [71.551117, 20.973578], [71.536794, 20.964301], [71.493419, 20.929145], [71.484386, 20.917141], [71.480724, 20.900377], [71.46225, 20.885321], [71.378103, 20.859442], [71.318614, 20.849311], [71.140636, 20.765692], [71.116954, 20.766913], [71.110118, 20.759467], [71.09547, 20.763739], [71.083263, 20.756537], [71.072602, 20.745673], [71.062348, 20.738959], [71.044444, 20.740546], [71.024181, 20.744534], [71.00766, 20.741604], [71.005951, 20.736558], [71.000824, 20.722154], [70.982432, 20.71015], [70.972992, 20.709621], [70.902531, 20.70552], [70.84018, 20.70189], [70.819672, 20.702948], [70.801606, 20.709052], [70.785167, 20.722154], [70.774587, 20.727362], [70.763194, 20.724555], [70.751638, 20.719631], [70.740245, 20.718492], [70.730805, 20.723212], [70.698578, 20.746405], [70.562174, 20.799767], [70.541515, 20.807847], [70.484141, 20.84394], [70.463064, 20.848863], [70.451427, 20.855902], [70.417003, 20.890448], [70.398774, 20.90351], [70.336681, 20.927069], [70.299978, 20.958564], [70.26352, 20.980414], [70.152192, 21.078518], [70.054445, 21.153155], [69.988332, 21.2334], [69.968019, 21.258055], [69.966156, 21.263414], [69.958263, 21.273342], [69.911632, 21.315009], [69.844981, 21.389594], [69.814708, 21.433661], [69.808604, 21.465806], [69.788748, 21.469306], [69.771495, 21.47842], [69.756358, 21.490424], [69.629731, 21.616034], [69.568207, 21.650133], [69.552745, 21.665188], [69.519786, 21.704779], [69.49936, 21.720201], [69.478526, 21.752631], [69.4463, 21.766303], [69.427419, 21.781236], [69.397716, 21.815253], [69.391775, 21.83218], [69.395193, 21.861314], [69.39088, 21.876695], [69.367849, 21.856106], [69.355479, 21.853176], [69.336192, 21.862454], [69.243907, 21.94595], [69.21225, 21.967475], [69.198416, 21.979722], [69.150645, 22.041205], [69.119314, 22.062079], [69.103038, 22.076361], [69.092052, 22.099595], [69.048188, 22.157904], [69.012543, 22.185614], [68.994151, 22.203925], [68.980479, 22.235541], [68.953298, 22.276028], [68.944591, 22.295071], [68.944021, 22.318264], [68.951427, 22.334866], [68.960623, 22.349514], [68.965017, 22.366767], [68.967621, 22.387193], [68.974376, 22.404731], [68.983246, 22.419379], [68.992931, 22.431586], [69.007498, 22.443183], [69.047537, 22.464016], [69.073578, 22.481879], [69.081391, 22.480536], [69.085216, 22.472886], [69.082286, 22.45954], [69.074474, 22.449449], [69.056, 22.44184], [69.048188, 22.431586], [69.062266, 22.407701], [69.071788, 22.399482], [69.088552, 22.396877], [69.097423, 22.398871], [69.11378, 22.407864], [69.122732, 22.411078], [69.132009, 22.410468], [69.139822, 22.407131], [69.146983, 22.404853], [69.154633, 22.407701], [69.168793, 22.42064], [69.178559, 22.427639], [69.188243, 22.426418], [69.202159, 22.414862], [69.20574, 22.398668], [69.18214, 22.366441], [69.185313, 22.349677], [69.169688, 22.327948], [69.173839, 22.308295], [69.19044, 22.292304], [69.212087, 22.281399], [69.212087, 22.273912], [69.286469, 22.284613], [69.335704, 22.306138], [69.346446, 22.324897], [69.359874, 22.329169], [69.435395, 22.33039], [69.468923, 22.337836], [69.479666, 22.342231], [69.485362, 22.347561], [69.500011, 22.366767], [69.502696, 22.37226], [69.508556, 22.374254], [69.515147, 22.37519], [69.519867, 22.377631], [69.525645, 22.392076], [69.522797, 22.403876], [69.516938, 22.416083], [69.513682, 22.431586], [69.515961, 22.448147], [69.521983, 22.44892], [69.530772, 22.446112], [69.541026, 22.452094], [69.547862, 22.452094], [69.559825, 22.411526], [69.561697, 22.391588], [69.554698, 22.377631], [69.569021, 22.370551], [69.601085, 22.363837], [69.616059, 22.355902], [69.617442, 22.365058], [69.619884, 22.371039], [69.629731, 22.383775], [69.640391, 22.375963], [69.647227, 22.380439], [69.652354, 22.389879], [69.657725, 22.396877], [69.661306, 22.400214], [69.665212, 22.404975], [69.669444, 22.409247], [69.674653, 22.411078], [69.680837, 22.409613], [69.694509, 22.403957], [69.699392, 22.404283], [69.708995, 22.418891], [69.725352, 22.464545], [69.733409, 22.480048], [69.742198, 22.472968], [69.788748, 22.423163], [69.794932, 22.418606], [69.806977, 22.420966], [69.812999, 22.429511], [69.816905, 22.440985], [69.822276, 22.452094], [69.833263, 22.463813], [69.841807, 22.466376], [69.851085, 22.465237], [69.86378, 22.465766], [69.884776, 22.473578], [69.905284, 22.486396], [69.962087, 22.535346], [69.972992, 22.541449], [69.973888, 22.539984], [69.996267, 22.541449], [70.002696, 22.543158], [70.020844, 22.555121], [70.059093, 22.558417], [70.144705, 22.547797], [70.178477, 22.561957], [70.189708, 22.577786], [70.207286, 22.614976], [70.234874, 22.648871], [70.267263, 22.705959], [70.319102, 22.778266], [70.323253, 22.787665], [70.321544, 22.811713], [70.32252, 22.822659], [70.32545, 22.826321], [70.342947, 22.843166], [70.375255, 22.902655], [70.387706, 22.912014], [70.404633, 22.919501], [70.43686, 22.954901], [70.45338, 22.96662], [70.462738, 22.966539], [70.474294, 22.964057], [70.486339, 22.96369], [70.497569, 22.970038], [70.504731, 22.97956], [70.51238, 22.995347], [70.517914, 23.003892], [70.527192, 23.023586], [70.528494, 23.047065], [70.523692, 23.07038], [70.514903, 23.089545], [70.50294, 23.104397], [70.484223, 23.122138], [70.467133, 23.129706], [70.459646, 23.114], [70.450938, 23.105658], [70.410655, 23.086005], [70.398774, 23.069037], [70.398936, 23.059068], [70.401052, 23.045396], [70.40447, 23.033352], [70.410981, 23.023179], [70.401866, 22.999254], [70.402029, 22.99022], [70.403819, 22.981879], [70.403494, 22.967841], [70.401622, 22.953843], [70.398774, 22.945502], [70.392426, 22.940863], [70.384532, 22.93887], [70.363536, 22.938707], [70.357677, 22.936713], [70.352387, 22.933051], [70.34669, 22.931301], [70.339529, 22.935289], [70.334483, 22.940131], [70.330414, 22.943305], [70.325694, 22.945014], [70.319102, 22.945502], [70.314626, 22.943508], [70.308767, 22.939765], [70.303477, 22.938137], [70.300059, 22.944281], [70.29363, 22.951402], [70.291759, 22.95303], [70.269298, 22.958564], [70.258311, 22.971869], [70.249685, 22.987454], [70.233084, 23.000149], [70.231944, 22.974921], [70.222016, 22.959174], [70.204112, 22.952216], [70.178477, 22.95303], [70.171723, 22.951972], [70.167735, 22.949123], [70.163097, 22.948961], [70.154307, 22.956041], [70.150727, 22.961005], [70.149669, 22.964301], [70.147472, 22.966051], [70.140636, 22.96662], [70.12908, 22.969306], [70.125011, 22.975979], [70.126231, 22.984809], [70.130626, 22.993964], [70.109548, 22.959174], [70.120453, 22.952582], [70.132335, 22.941067], [70.135427, 22.929999], [70.120128, 22.925035], [70.061778, 22.918199], [69.996918, 22.893785], [69.940684, 22.886135], [69.884288, 22.867825], [69.852875, 22.863593], [69.833181, 22.858466], [69.806814, 22.845282], [69.780121, 22.82746], [69.760102, 22.808336], [69.728852, 22.754828], [69.715831, 22.746894], [69.694347, 22.749701], [69.653819, 22.760484], [69.585704, 22.761176], [69.575694, 22.76435], [69.556977, 22.77851], [69.544444, 22.781684], [69.510753, 22.780504], [69.500011, 22.781684], [69.489431, 22.784735], [69.482595, 22.788479], [69.477306, 22.79206], [69.472179, 22.794664], [69.424653, 22.804267], [69.405935, 22.813666], [69.372569, 22.818996], [69.338145, 22.833075], [69.23878, 22.841946], [69.201915, 22.85224], [69.13559, 22.888373], [69.039887, 22.950507], [68.866222, 23.019965], [68.691254, 23.13052], [68.660655, 23.154242], [68.654063, 23.157782], [68.650645, 23.162055], [68.651134, 23.171454], [68.649669, 23.180854], [68.622569, 23.191555], [68.609141, 23.206773], [68.599946, 23.225043], [68.595714, 23.240383], [68.598318, 23.236965], [68.609386, 23.226711], [68.620128, 23.245429], [68.62322, 23.256171], [68.623057, 23.267646], [68.605317, 23.259589], [68.57781, 23.255276], [68.552257, 23.259711], [68.541026, 23.278225], [68.561046, 23.316107], [68.568533, 23.318427], [68.599132, 23.322252], [68.606944, 23.325588], [68.603038, 23.332831], [68.592052, 23.340074], [68.57838, 23.343411], [68.571056, 23.346381], [68.566661, 23.352973], [68.563731, 23.359931], [68.561046, 23.363918], [68.555186, 23.363959], [68.548513, 23.361518], [68.543142, 23.358588], [68.541026, 23.357001], [68.52947, 23.363105], [68.520681, 23.369208], [68.515147, 23.377021], [68.513194, 23.388088], [68.515147, 23.399319], [68.520763, 23.407538], [68.529552, 23.411933], [68.541026, 23.411689], [68.536388, 23.421332], [68.530121, 23.425686], [68.522146, 23.42475], [68.513194, 23.418524], [68.501475, 23.423285], [68.49643, 23.417873], [68.490489, 23.409329], [68.475352, 23.404853], [68.462576, 23.409125], [68.458832, 23.419827], [68.461111, 23.433173], [68.465505, 23.445787], [68.451915, 23.43773], [68.44044, 23.434272], [68.430919, 23.436591], [68.423676, 23.445787], [68.432953, 23.465888], [68.447439, 23.487454], [68.465587, 23.505276], [68.48585, 23.514106], [68.481293, 23.525133], [68.475597, 23.524115], [68.468028, 23.519843], [68.458669, 23.52147], [68.454112, 23.526272], [68.444021, 23.541205], [68.437348, 23.548814], [68.441417, 23.53205], [68.447439, 23.516343], [68.446951, 23.502997], [68.431163, 23.493598], [68.411632, 23.494696], [68.406098, 23.508734], [68.410818, 23.548814], [68.404959, 23.597602], [68.408865, 23.621324], [68.427501, 23.631415], [68.471528, 23.618638], [68.491466, 23.618842], [68.492686, 23.6376], [68.486176, 23.641425], [68.476899, 23.63935], [68.468761, 23.640815], [68.465505, 23.65526], [68.469412, 23.659247], [68.488943, 23.664781], [68.504731, 23.674221], [68.523936, 23.67593], [68.533539, 23.679185], [68.538829, 23.684027], [68.554698, 23.706488], [68.583507, 23.735297], [68.601736, 23.748684], [68.637869, 23.761542], [68.654552, 23.799058], [68.670746, 23.815741], [68.680186, 23.817776], [68.715587, 23.815741], [68.725597, 23.818915], [68.745128, 23.833075], [68.756521, 23.836249], [68.768809, 23.841132], [68.821625, 23.878404], [68.805675, 23.884426], [68.78004, 23.874091], [68.766938, 23.878404], [68.740245, 23.853461], [68.717784, 23.84101], [68.691905, 23.836656], [68.654063, 23.836249], [68.633474, 23.833075], [68.623057, 23.824408], [68.609386, 23.795233], [68.598888, 23.781887], [68.587738, 23.775702], [68.554698, 23.768541], [68.538341, 23.761298], [68.52475, 23.752021], [68.51002, 23.744086], [68.47283, 23.737494], [68.449718, 23.723334], [68.427745, 23.714301], [68.410818, 23.700507], [68.34197, 23.623969], [68.351573, 23.617825], [68.353038, 23.610663], [68.348888, 23.60342], [68.34197, 23.596625], [68.327322, 23.5869], [68.317638, 23.586127], [68.308116, 23.589057], [68.294119, 23.590399], [68.230805, 23.58808], [68.181407, 23.594062], [68.1574, 23.600816], [68.143403, 23.612698], [68.150156, 23.631372], [68.157069, 23.632131], [68.167229, 23.63173], [68.174327, 23.631415], [68.17437, 23.631448], [68.17543, 23.631406], [68.178674, 23.633718], [68.181895, 23.647854], [68.184753, 23.652181], [68.192673, 23.65628], [68.199663, 23.659597], [68.209321, 23.662746], [68.220787, 23.65843], [68.220789, 23.658427], [68.232578, 23.644912], [68.232669, 23.645015], [68.232677, 23.645006], [68.245128, 23.659125], [68.258931, 23.660481], [68.27589, 23.665839], [68.280447, 23.685981], [68.258962, 23.674709], [68.240977, 23.674018], [68.237165, 23.675876], [68.224864, 23.681871], [68.208751, 23.696234], [68.19752, 23.714545], [68.195811, 23.733466], [68.197927, 23.753404], [68.197765, 23.774807], [68.187723, 23.764264], [68.177736, 23.753962], [68.169275, 23.748231], [68.163748, 23.757717], [68.166026, 23.773179], [68.175629, 23.798245], [68.179857, 23.827937], [68.182465, 23.835761], [68.183035, 23.842108], [68.183517, 23.8433], [68.207288, 23.877019], [68.21504, 23.881748], [68.23292, 23.889111], [68.239948, 23.893116], [68.243049, 23.893426], [68.252867, 23.891695], [68.256794, 23.891928], [68.26036, 23.895545], [68.257931, 23.899473], [68.253746, 23.90278], [68.251937, 23.904821], [68.253074, 23.911229], [68.25266, 23.920091], [68.254934, 23.929109], [68.263926, 23.93593], [68.274003, 23.937687], [68.277672, 23.933501], [68.280049, 23.925724], [68.286198, 23.916371], [68.300409, 23.910505], [68.314052, 23.915906], [68.325989, 23.927791], [68.335033, 23.940995], [68.333689, 23.948048], [68.329865, 23.958151], [68.330433, 23.966574], [68.342629, 23.968564], [68.348882, 23.964611], [68.352396, 23.956549], [68.353843, 23.94717], [68.353636, 23.939289], [68.385469, 23.960425], [68.431564, 23.967143], [68.547836, 23.966316], [68.646435, 23.965696], [68.724466, 23.965179], [68.725086, 24.10406], [68.725551, 24.208911], [68.725913, 24.289216], [68.747307, 24.331177], [68.79919, 24.329085], [68.813815, 24.30844], [68.819654, 24.250304], [68.838774, 24.236455], [68.848903, 24.244025], [68.880116, 24.297355], [68.883268, 24.305365], [68.885077, 24.313556], [68.890296, 24.319473], [68.904197, 24.320584], [68.913189, 24.317251], [68.921974, 24.310507], [68.929415, 24.302368], [68.94869, 24.270277], [68.962643, 24.257228], [68.980781, 24.255394], [69.007808, 24.264567], [69.048477, 24.285237], [69.067701, 24.288441], [69.091886, 24.281904], [69.148213, 24.25666], [69.166713, 24.253172], [69.206246, 24.258572], [69.280815, 24.283739], [69.563071, 24.276762], [69.59232, 24.264618], [69.670558, 24.188706], [69.714587, 24.168578], [69.769054, 24.162557], [69.972039, 24.165219], [70.015964, 24.174055], [70.052137, 24.202064], [70.062989, 24.220306], [70.087174, 24.28255], [70.097871, 24.298828], [70.109705, 24.3049], [70.144586, 24.307897], [70.202205, 24.325571], [70.222773, 24.326707], [70.242565, 24.330609], [70.2791, 24.355078], [70.29853, 24.363424], [70.353204, 24.366266], [70.370774, 24.372364], [70.416353, 24.401948], [70.520946, 24.424919], [70.562907, 24.424092], [70.575206, 24.400036], [70.569211, 24.389882], [70.551952, 24.379366], [70.546474, 24.373139], [70.545647, 24.362183], [70.555362, 24.327018], [70.56022, 24.287253], [70.567661, 24.272809], [70.584714, 24.2579], [70.621508, 24.241157], [70.755453, 24.231442], [70.776021, 24.236687], [70.813589, 24.254464], [70.834105, 24.261285], [70.851468, 24.264877], [70.857566, 24.271672], [70.844543, 24.288286], [70.840926, 24.30583], [70.856532, 24.323814], [70.917717, 24.361718], [70.936321, 24.367196], [70.955338, 24.365904], [70.977145, 24.357248], [70.996679, 24.356602], [71.007118, 24.363966], [71.014352, 24.375206], [71.025101, 24.38629], [71.073057, 24.402103], [71.082772, 24.411508], [71.07502, 24.436417], [71.040087, 24.446778], [71.00009, 24.452901], [70.977145, 24.464942], [70.974355, 24.472202], [70.973114, 24.479644], [70.973218, 24.48724], [70.974768, 24.494966], [70.977145, 24.515068], [70.980504, 24.521889], [70.981899, 24.52853], [70.980866, 24.534731], [70.977145, 24.54039], [70.957767, 24.556022], [70.954821, 24.584521], [70.962779, 24.615785], [70.977145, 24.639686], [71.043343, 24.669064], [71.063858, 24.682577], [71.056632, 24.69282], [71.036935, 24.72074], [71.00319, 24.808203], [70.943039, 24.894063], [70.915237, 24.946618], [70.893326, 25.001886], [70.859891, 25.139449], [70.848574, 25.163323], [70.831418, 25.183322], [70.768372, 25.233086], [70.734886, 25.267348], [70.723104, 25.287295], [70.718556, 25.310808], [70.710495, 25.335871], [70.670394, 25.375558], [70.654478, 25.39659], [70.646623, 25.431369], [70.652824, 25.545884], [70.657475, 25.63363], [70.653857, 25.674455], [70.632308, 25.701378], [70.592259, 25.708819], [70.554432, 25.698794], [70.516657, 25.68386], [70.477227, 25.676315], [70.360129, 25.673473], [70.303698, 25.684583], [70.264579, 25.697296], [70.249231, 25.707683], [70.234452, 25.730989], [70.213988, 25.786334], [70.195591, 25.806953], [70.15425, 25.839406], [70.114976, 25.881729], [70.083246, 25.929943], [70.064643, 25.980327], [70.064281, 25.99552], [70.072911, 26.047455], [70.073893, 26.083112], [70.078285, 26.099648], [70.132029, 26.18047], [70.146912, 26.217419], [70.151614, 26.254058], [70.144018, 26.294313], [70.142467, 26.313692], [70.147945, 26.333226], [70.157144, 26.354], [70.160503, 26.371311], [70.156989, 26.410947], [70.162931, 26.493319], [70.158074, 26.530113], [70.1296, 26.562514], [70.093685, 26.580394], [70.056064, 26.589076], [69.815562, 26.580291], [69.772258, 26.59507], [69.700221, 26.653], [69.659396, 26.677701], [69.50416, 26.735165], [69.472844, 26.766584], [69.465093, 26.80777], [69.48597, 26.926833], [69.507571, 27.050081], [69.534443, 27.125581], [69.575577, 27.188419], [69.666114, 27.270016], [69.730606, 27.310324], [69.848015, 27.410369], [69.908063, 27.497289], [69.993536, 27.571083], [70.016894, 27.60059], [70.090688, 27.79355], [70.101953, 27.81174], [70.189028, 27.89179], [70.199002, 27.90096], [70.267731, 27.945272], [70.323852, 28.000437], [70.341939, 28.01147], [70.359509, 28.016327], [70.398266, 28.02165], [70.43692, 28.035344], [70.456247, 28.039788], [70.477227, 28.037256], [70.506786, 28.028962], [70.534898, 28.015965], [70.559806, 27.998447], [70.592879, 27.964496], [70.621198, 27.944135], [70.633187, 27.93163], [70.64161, 27.911269], [70.637631, 27.874217], [70.640732, 27.854787], [70.671738, 27.791095], [70.710391, 27.74115], [70.761758, 27.709783], [70.831573, 27.701463], [70.91379, 27.717818], [71.027581, 27.768048], [71.150675, 27.822411], [71.226329, 27.845356], [71.31144, 27.861711], [71.397791, 27.868378], [71.477115, 27.862435], [71.560727, 27.868533], [71.70139, 27.906773], [71.860864, 27.950207], [71.874403, 27.95969], [71.879984, 27.974909], [71.89156, 28.097097], [71.896934, 28.115546], [71.908303, 28.135571], [71.988195, 28.228097], [72.110978, 28.317627], [72.149942, 28.353774], [72.17764, 28.397053], [72.197794, 28.44488], [72.256499, 28.645591], [72.28027, 28.687165], [72.354581, 28.767186], [72.382176, 28.784006], [72.52563, 28.84992], [72.658541, 28.911001], [72.77254, 28.96335], [72.901524, 29.022622], [72.918164, 29.032854], [72.930359, 29.047685], [72.962915, 29.116829], [72.98865, 29.15463], [73.050869, 29.228217], [73.1289, 29.360302], [73.177993, 29.443449], [73.232873, 29.536596], [73.284756, 29.683745], [73.327441, 29.80521], [73.370332, 29.927322], [73.385215, 29.942308], [73.557815, 30.012536], [73.739716, 30.048451], [73.778266, 30.067313], [73.944354, 30.188288], [73.946831, 30.204077], [73.948902, 30.217278], [73.934639, 30.261048], [73.911901, 30.303784], [73.891644, 30.329778], [73.842345, 30.35298], [73.845239, 30.35696], [73.85175, 30.372669], [73.867666, 30.387449], [73.887614, 30.396544], [73.906114, 30.394838], [73.904253, 30.401453], [73.902083, 30.415612], [73.899913, 30.422124], [73.916656, 30.417163], [73.929265, 30.419075], [73.939083, 30.426051], [73.98983, 30.487804], [73.996031, 30.500982], [74.004402, 30.50894], [74.04409, 30.518655], [74.057526, 30.531367], [74.061453, 30.555604], [74.058249, 30.574466], [74.057112, 30.591571], [74.067448, 30.610536], [74.082227, 30.625884], [74.09587, 30.637149], [74.111476, 30.646244], [74.132663, 30.655494], [74.155711, 30.659577], [74.163669, 30.664899], [74.16677, 30.67911], [74.170077, 30.687172], [74.18465, 30.696835], [74.191161, 30.717041], [74.198809, 30.724999], [74.215139, 30.737453], [74.235706, 30.764118], [74.244078, 30.771249], [74.259167, 30.781016], [74.266712, 30.788354], [74.265782, 30.795434], [74.256687, 30.815278], [74.260201, 30.819412], [74.279941, 30.824683], [74.300198, 30.83817], [74.31136, 30.855947], [74.304022, 30.874034], [74.31074, 30.884473], [74.319215, 30.893413], [74.329757, 30.899614], [74.34247, 30.901939], [74.353632, 30.901112], [74.379677, 30.894498], [74.401277, 30.893103], [74.409132, 30.901009], [74.41337, 30.915065], [74.424325, 30.932325], [74.441585, 30.946174], [74.458535, 30.953409], [74.477242, 30.956096], [74.499463, 30.95651], [74.519306, 30.962452], [74.534809, 30.97625], [74.548762, 30.992166], [74.564058, 31.004362], [74.564265, 31.025032], [74.579458, 31.042706], [74.600955, 31.056607], [74.658833, 31.083789], [74.650254, 31.093142], [74.631961, 31.107508], [74.616975, 31.11526], [74.596924, 31.115776], [74.553103, 31.108955], [74.536773, 31.115156], [74.523544, 31.129315], [74.514862, 31.141821], [74.509591, 31.156084], [74.506387, 31.175411], [74.509074, 31.195668], [74.525094, 31.235407], [74.525301, 31.280211], [74.532225, 31.303207], [74.571603, 31.389248], [74.583695, 31.40656], [74.600025, 31.42413], [74.61191, 31.440201], [74.617181, 31.458908], [74.614598, 31.477821], [74.584522, 31.517509], [74.555583, 31.612232], [74.499463, 31.699875], [74.489437, 31.711192], [74.492641, 31.714551], [74.503287, 31.730571], [74.505457, 31.732018], [74.513312, 31.735118], [74.516206, 31.737392], [74.518273, 31.741836], [74.521167, 31.753773], [74.523027, 31.758476], [74.530778, 31.767829], [74.535119, 31.771137], [74.53698, 31.775736], [74.537393, 31.788913], [74.540804, 31.810876], [74.550415, 31.826999], [74.565608, 31.840538], [74.641056, 31.890561], [74.657179, 31.895677], [74.669995, 31.904152], [74.698417, 31.950247], [74.763012, 31.938982], [74.783683, 31.942858], [74.802493, 31.968851], [74.811278, 32.003732], [74.828641, 32.025437], [74.873186, 32.011691], [74.889103, 32.028899], [74.908843, 32.031896], [74.931374, 32.029829], [74.955662, 32.032206], [74.967858, 32.037891], [74.974059, 32.04497], [74.978503, 32.053393], [74.986151, 32.063212], [74.995556, 32.067398], [75.020154, 32.065847], [75.030179, 32.066416], [75.022635, 32.094683], [75.040825, 32.09799], [75.102113, 32.078095], [75.110278, 32.07763], [75.11896, 32.08104], [75.129812, 32.090239], [75.13839, 32.093494], [75.161334, 32.087965], [75.17415, 32.086828], [75.173116, 32.109721], [75.195751, 32.128634], [75.226447, 32.14207], [75.249908, 32.148892], [75.263344, 32.150907], [75.294659, 32.148892], [75.29807, 32.159795], [75.308405, 32.193902], [75.31657, 32.210748], [75.34861, 32.242012], [75.359048, 32.261675], [75.35295, 32.284387], [75.326906, 32.312473], [75.298403, 32.332427], [75.230167, 32.380195], [75.196164, 32.3973], [75.125988, 32.411744], [75.092191, 32.429339], [75.054881, 32.455539], [75.023668, 32.466262], [74.990699, 32.463368], [74.948427, 32.44877], [74.906983, 32.445178], [74.83815, 32.474866], [74.799393, 32.47267], [74.763529, 32.462671], [74.724978, 32.46081], [74.689322, 32.471352], [74.662553, 32.498379], [74.632581, 32.568168], [74.629584, 32.588089], [74.636922, 32.606254], [74.647981, 32.623617], [74.656042, 32.64067], [74.657282, 32.660592], [74.650151, 32.701235], [74.649738, 32.723146], [74.656972, 32.745341], [74.682294, 32.787328], [74.689115, 32.808825], [74.685291, 32.831227], [74.671752, 32.840581], [74.653562, 32.837428], [74.635888, 32.82208], [74.626276, 32.80265], [74.622039, 32.785416], [74.614081, 32.770404], [74.59341, 32.757872], [74.573773, 32.75255], [74.52096, 32.745289], [74.500909, 32.746013], [74.484476, 32.753532], [74.455331, 32.778595], [74.438691, 32.785984], [74.418951, 32.784615], [74.386394, 32.767768], [74.367688, 32.763531], [74.34619, 32.76689], [74.329344, 32.776424], [74.316321, 32.791075], [74.306503, 32.809911], [74.315939, 32.82175], [74.316989, 32.823067], [74.322936, 32.830529], [74.333788, 32.849262], [74.336682, 32.870139], [74.324486, 32.921402], [74.322006, 32.972045], [74.311257, 32.994292], [74.283972, 33.008813], [74.190851, 33.022094], [74.153851, 33.040181], [74.098453, 33.104828], [74.065794, 33.132604], [74.029414, 33.154153], [74.002335, 33.177692], [73.988486, 33.20862], [73.991793, 33.252571], [74.001715, 33.270167], [74.017838, 33.279494], [74.055355, 33.292026], [74.071995, 33.302284], [74.085121, 33.314324], [74.095973, 33.328484], [74.105481, 33.345201], [74.136281, 33.418013], [74.157778, 33.494236], [74.141552, 33.549374], [74.088118, 33.58529], [74.024246, 33.614254], [73.976704, 33.648387], [73.968332, 33.662081], [73.963061, 33.676757], [73.960787, 33.692286], [73.963061, 33.72156], [73.966162, 33.734531], [73.970709, 33.747063], [73.976704, 33.758922], [74.011327, 33.81047], [74.034581, 33.828608], [74.142275, 33.84424], [74.177518, 33.857547], [74.212555, 33.878424], [74.23953, 33.901007], [74.262474, 33.929946], [74.272293, 33.96214], [74.259684, 33.994386], [74.228368, 34.012886], [74.192091, 34.011853], [74.154264, 34.004101], [74.118401, 34.002525], [74.065277, 34.018338], [74.04502, 34.019423], [73.976704, 34.004592], [73.945801, 34.009786], [73.916242, 34.02795], [73.893711, 34.054305], [73.88441, 34.0842], [73.893298, 34.11474], [73.916862, 34.135256], [73.976704, 34.161559], [73.995411, 34.176442], [73.998304, 34.196803], [73.990553, 34.218507], [73.976704, 34.23755], [73.95438, 34.287185], [73.93774, 34.304212], [73.907457, 34.306796], [73.879035, 34.30429], [73.8533, 34.307416], [73.829116, 34.315348], [73.804208, 34.327492], [73.78271, 34.346819], [73.774856, 34.370952], [73.779817, 34.396067], [73.79625, 34.418546], [73.806792, 34.425161], [73.831596, 34.4364], [73.840381, 34.44441], [73.845549, 34.456477], [73.846996, 34.494175], [73.846996, 34.49433], [73.863119, 34.517067], [73.910971, 34.544973], [73.925957, 34.564971], [73.925441, 34.593393], [73.917483, 34.619283], [73.919343, 34.642538], [73.948592, 34.662743], [73.962234, 34.668169], [74.120881, 34.690855], [74.146823, 34.701862], [74.221753, 34.747751], [74.285832, 34.768887], [74.348877, 34.773434], [74.412129, 34.764494], [74.66493, 34.688323], [75.017777, 34.629722], [75.111001, 34.633649], [75.17632, 34.64538], [75.212907, 34.644967], [75.224544, 34.638622], [75.236885, 34.631892], [75.251355, 34.613082], [75.267478, 34.598303], [75.306958, 34.574015], [75.348093, 34.55722], [75.611643, 34.49836], [75.655568, 34.496965], [75.713652, 34.508515], [75.734839, 34.508618], [75.777111, 34.503812], [75.795818, 34.507921], [75.816178, 34.521667], [75.873952, 34.571379], [75.939271, 34.612049], [75.966246, 34.619077], [75.973378, 34.622332], [75.98671, 34.636027], [76.007484, 34.66512], [76.023401, 34.677161], [76.058334, 34.683414], [76.121792, 34.660883], [76.154969, 34.661658], [76.262042, 34.684654], [76.399915, 34.750748], [76.438156, 34.762892], [76.476499, 34.757776], [76.519908, 34.731111], [76.535824, 34.726202], [76.553187, 34.72584], [76.63959, 34.741136], [76.652613, 34.746976], [76.743873, 34.819271], [76.753278, 34.83834], [76.749558, 34.892083], [76.757619, 34.915131], [76.782011, 34.930892], [76.817047, 34.940762], [76.852601, 34.943346], [76.879576, 34.937093], [76.905931, 34.923141], [76.921123, 34.920712], [76.933836, 34.928877], [76.952956, 34.94655], [77.013108, 34.986393], [77.02861, 35.003394], [77.035225, 35.034503], [77.02706, 35.062099], [77.023133, 35.086335], [77.042426, 35.107014], [77.048971, 35.110442], [77.424659, 35.302924], [77.800346, 35.495406]]], [[[80.968272, 15.795233], [80.983165, 15.780748], [80.988536, 15.770738], [80.984874, 15.761786], [80.97169, 15.750474], [80.942638, 15.741034], [80.942556, 15.741075], [80.917979, 15.755113], [80.8685, 15.811957], [80.879161, 15.840074], [80.888357, 15.846747], [80.937266, 15.818915], [80.968272, 15.795233]]], [[[73.646332, 10.084703], [73.644054, 10.076972], [73.640961, 10.069322], [73.637055, 10.062567], [73.632579, 10.057278], [73.631684, 10.067776], [73.633311, 10.077867], [73.637462, 10.087063], [73.643891, 10.09455], [73.643972, 10.094468], [73.645193, 10.09219], [73.646332, 10.084703]]], [[[73.662283, 10.144599], [73.662201, 10.144517], [73.66033, 10.142524], [73.659353, 10.13996], [73.657888, 10.139065], [73.654633, 10.141506], [73.65561, 10.144761], [73.657481, 10.145087], [73.659679, 10.144355], [73.662283, 10.144599]]], [[[72.64975, 10.56684], [72.63852, 10.559394], [72.633067, 10.556545], [72.627533, 10.554796], [72.627452, 10.554755], [72.632091, 10.560004], [72.648611, 10.573961], [72.648692, 10.57392], [72.650564, 10.572577], [72.650401, 10.571194], [72.649587, 10.569322], [72.64975, 10.56684]]], [[[72.548839, 10.779446], [72.548757, 10.779324], [72.541189, 10.772895], [72.540863, 10.776516], [72.542491, 10.777899], [72.545421, 10.778306], [72.548839, 10.779446]]], [[[73.671397, 10.829739], [73.681244, 10.82807], [73.684418, 10.827541], [73.67628, 10.824368], [73.672862, 10.824693], [73.671886, 10.827094], [73.671397, 10.829657], [73.671397, 10.829739]]], [[[72.750011, 11.119086], [72.747569, 11.115424], [72.746755, 11.111965], [72.744965, 11.109524], [72.739513, 11.108832], [72.739513, 11.113959], [72.742035, 11.115953], [72.745779, 11.116889], [72.750011, 11.119086]]], [[[73.013194, 11.502183], [73.010916, 11.490139], [73.0088, 11.484687], [73.005626, 11.479641], [73.004405, 11.488837], [73.004405, 11.496487], [73.007091, 11.501288], [73.013194, 11.502183]]], [[[72.716156, 11.694159], [72.716075, 11.694078], [72.708669, 11.689154], [72.705251, 11.687323], [72.701101, 11.686428], [72.700938, 11.686428], [72.705089, 11.692328], [72.709239, 11.694159], [72.716156, 11.694159]]], [[[93.872813, 12.268866], [93.85377, 12.260565], [93.851899, 12.271552], [93.860606, 12.281195], [93.872813, 12.268866]]], [[[94.280935, 13.443549], [94.290782, 13.422919], [94.274913, 13.420315], [94.261892, 13.429267], [94.280935, 13.443549]]], [[[93.920258, 7.024319], [93.928722, 7.012356], [93.929861, 6.959052], [93.921072, 6.933824], [93.893565, 6.876776], [93.889415, 6.841783], [93.892833, 6.834052], [93.899262, 6.827135], [93.904145, 6.819241], [93.903087, 6.808295], [93.898448, 6.804185], [93.889903, 6.800971], [93.880056, 6.800442], [93.871755, 6.804511], [93.859386, 6.804267], [93.847911, 6.785305], [93.834239, 6.745551], [93.812673, 6.753974], [93.809337, 6.769029], [93.813243, 6.788031], [93.813731, 6.808295], [93.805431, 6.816718], [93.792166, 6.825873], [93.784434, 6.836412], [93.793224, 6.849189], [93.783702, 6.857978], [93.775727, 6.867377], [93.773123, 6.877997], [93.779552, 6.890204], [93.770763, 6.893866], [93.766124, 6.901109], [93.764496, 6.911526], [93.76531, 6.924302], [93.748057, 6.92357], [93.737478, 6.943264], [93.72877, 6.970526], [93.71754, 6.992621], [93.703624, 7.006293], [93.684337, 7.020901], [93.664561, 7.026516], [93.649181, 7.013088], [93.649669, 7.119615], [93.653575, 7.131496], [93.676443, 7.181871], [93.685395, 7.184516], [93.70753, 7.184394], [93.717296, 7.18769], [93.72462, 7.194973], [93.730154, 7.202216], [93.73463, 7.205512], [93.739919, 7.207099], [93.76531, 7.219143], [93.772146, 7.211737], [93.797618, 7.234605], [93.813975, 7.241848], [93.830821, 7.235907], [93.855317, 7.214179], [93.865001, 7.20067], [93.870372, 7.180569], [93.87672, 7.16356], [93.880626, 7.144273], [93.883149, 7.104722], [93.887055, 7.089301], [93.903087, 7.060858], [93.912934, 7.035102], [93.920258, 7.024319]]], [[[93.731212, 7.369371], [93.734386, 7.350816], [93.728526, 7.337104], [93.715505, 7.325832], [93.696951, 7.314765], [93.67921, 7.298326], [93.644216, 7.257514], [93.628184, 7.253323], [93.617931, 7.259955], [93.616384, 7.267279], [93.617442, 7.276068], [93.615082, 7.287421], [93.61085, 7.294501], [93.594005, 7.314765], [93.606944, 7.326402], [93.610362, 7.347317], [93.61671, 7.367255], [93.638682, 7.376166], [93.667247, 7.382758], [93.677989, 7.3994], [93.683767, 7.421698], [93.696951, 7.445054], [93.696951, 7.417792], [93.699474, 7.398749], [93.709483, 7.388739], [93.72169, 7.381171], [93.731212, 7.369371]]], [[[93.429454, 7.955268], [93.433116, 7.954291], [93.459809, 7.910793], [93.46046, 7.899848], [93.457367, 7.880194], [93.454438, 7.869289], [93.447927, 7.871405], [93.43686, 7.883287], [93.421235, 7.891791], [93.41505, 7.892239], [93.40211, 7.890082], [93.389171, 7.88581], [93.378429, 7.880764], [93.367849, 7.878607], [93.354259, 7.883287], [93.344981, 7.891343], [93.340343, 7.901272], [93.341075, 7.912584], [93.348155, 7.924872], [93.340668, 7.931708], [93.332693, 7.924954], [93.323009, 7.924384], [93.31365, 7.929185], [93.306651, 7.938544], [93.305024, 7.949937], [93.313324, 7.989732], [93.322602, 8.002102], [93.344412, 8.012112], [93.370291, 8.01732], [93.391368, 8.01557], [93.394786, 8.006741], [93.396169, 7.997219], [93.396007, 7.976386], [93.401215, 7.967963], [93.424164, 7.959866], [93.429454, 7.955268]]], [[[93.565929, 7.999335], [93.570323, 7.989], [93.574229, 7.968817], [93.573985, 7.924872], [93.518321, 7.965806], [93.510753, 7.98017], [93.511485, 7.99551], [93.520763, 8.004381], [93.539317, 7.999335], [93.540294, 8.016181], [93.54835, 8.020087], [93.558604, 8.013617], [93.565929, 7.999335]]], [[[93.531912, 8.034735], [93.528005, 8.032416], [93.514903, 8.027655], [93.511974, 8.024156], [93.509776, 8.018541], [93.494965, 7.996283], [93.484386, 7.991889], [93.479259, 8.002916], [93.477306, 8.030992], [93.480805, 8.040513], [93.488536, 8.044379], [93.495616, 8.049384], [93.497813, 8.061998], [93.505219, 8.061998], [93.514822, 8.05622], [93.516938, 8.0633], [93.511485, 8.074856], [93.497813, 8.082506], [93.492198, 8.063625], [93.479747, 8.063218], [93.466319, 8.074408], [93.457367, 8.089993], [93.446625, 8.154364], [93.446544, 8.164496], [93.454845, 8.170111], [93.465099, 8.183295], [93.473643, 8.198188], [93.477306, 8.209133], [93.48585, 8.22012], [93.504649, 8.226996], [93.523448, 8.225816], [93.531668, 8.213202], [93.531016, 8.193752], [93.521495, 8.179104], [93.510509, 8.166571], [93.505219, 8.14704], [93.508556, 8.128811], [93.525157, 8.095038], [93.537608, 8.057318], [93.537364, 8.047797], [93.531912, 8.034735]]], [[[73.077159, 8.315253], [73.081798, 8.315823], [73.083018, 8.314358], [73.083018, 8.311672], [73.083995, 8.308498], [73.066091, 8.270453], [73.056651, 8.259996], [73.044688, 8.252427], [73.032725, 8.249498], [73.024099, 8.255439], [73.022472, 8.274359], [73.029307, 8.274319], [73.029307, 8.274115], [73.028982, 8.263414], [73.029307, 8.259996], [73.04835, 8.267564], [73.063162, 8.279486], [73.073009, 8.295396], [73.077159, 8.315253]]], [[[93.134532, 8.24018], [93.160818, 8.228746], [93.173025, 8.221259], [93.17628, 8.212877], [93.167328, 8.204535], [93.153494, 8.205512], [93.124685, 8.212877], [93.10255, 8.221503], [93.081554, 8.242377], [93.065766, 8.268134], [93.059581, 8.291409], [93.06129, 8.320746], [93.064301, 8.336127], [93.069835, 8.346625], [93.083751, 8.355902], [93.095063, 8.35517], [93.10141, 8.345771], [93.100759, 8.299262], [93.104015, 8.27558], [93.114106, 8.256415], [93.134532, 8.24018]]], [[[93.607677, 8.548041], [93.621755, 8.520087], [93.626801, 8.505845], [93.628184, 8.486558], [93.625173, 8.473619], [93.618337, 8.453925], [93.609711, 8.438422], [93.60141, 8.438178], [93.600352, 8.450344], [93.607107, 8.49079], [93.607677, 8.507066], [93.595551, 8.541734], [93.594981, 8.55801], [93.607677, 8.569159], [93.607677, 8.548041]]], [[[92.811373, 9.23117], [92.833849, 9.202359], [92.831981, 9.145025], [92.799565, 9.110154], [92.731212, 9.130845], [92.720225, 9.147121], [92.711681, 9.175116], [92.708507, 9.201606], [92.713878, 9.213446], [92.729259, 9.217841], [92.745128, 9.229071], [92.754405, 9.244452], [92.750987, 9.261217], [92.763031, 9.264146], [92.772716, 9.259223], [92.795272, 9.240818], [92.811373, 9.23117]]], [[[92.585092, 10.771269], [92.586986, 10.736294], [92.598059, 10.709302], [92.595908, 10.678364], [92.574492, 10.655484], [92.56124, 10.642555], [92.551056, 10.634603], [92.539835, 10.620669], [92.54176, 10.59172], [92.555964, 10.588675], [92.574213, 10.580624], [92.546675, 10.547781], [92.535423, 10.524861], [92.513027, 10.507969], [92.479558, 10.516073], [92.463372, 10.531103], [92.444139, 10.545143], [92.421788, 10.546203], [92.406466, 10.529266], [92.384111, 10.529328], [92.378057, 10.541329], [92.402557, 10.575206], [92.413855, 10.606117], [92.417003, 10.629059], [92.408917, 10.647055], [92.390674, 10.663085], [92.379486, 10.672122], [92.378586, 10.718071], [92.378671, 10.750037], [92.37568, 10.775031], [92.376721, 10.78402], [92.385878, 10.781981], [92.407265, 10.789891], [92.41542, 10.798863], [92.422598, 10.822836], [92.428966, 10.850816], [92.482796, 10.885622], [92.527592, 10.895466], [92.54888, 10.874373], [92.56506, 10.849308], [92.567981, 10.815301], [92.582116, 10.790261], [92.585092, 10.771269]]], [[[92.676524, 11.412543], [92.685069, 11.399359], [92.696788, 11.391791], [92.69988, 11.384426], [92.682791, 11.371527], [92.673188, 11.367336], [92.646007, 11.359076], [92.63502, 11.357896], [92.61964, 11.360745], [92.61085, 11.366156], [92.60377, 11.372504], [92.593516, 11.378363], [92.598969, 11.401313], [92.600841, 11.406317], [92.608165, 11.411322], [92.623871, 11.412502], [92.631196, 11.416246], [92.637218, 11.430162], [92.635265, 11.447008], [92.627615, 11.477362], [92.63559, 11.508043], [92.654145, 11.508124], [92.675059, 11.490424], [92.689708, 11.46776], [92.6963, 11.454413], [92.689708, 11.442532], [92.679698, 11.429389], [92.676524, 11.412543]]], [[[92.266775, 11.589545], [92.278087, 11.583808], [92.286876, 11.563178], [92.285004, 11.544013], [92.273204, 11.531073], [92.252126, 11.529242], [92.234548, 11.540351], [92.230235, 11.577338], [92.217296, 11.591254], [92.217296, 11.59809], [92.228689, 11.597724], [92.254893, 11.593817], [92.266775, 11.589545]]], [[[93.042491, 11.961168], [93.048676, 11.952786], [93.066417, 11.899115], [93.049327, 11.896145], [93.035411, 11.911933], [93.026541, 11.931627], [93.024913, 11.940131], [93.015391, 11.94599], [93.001313, 11.949205], [92.990571, 11.954413], [92.980235, 11.96369], [92.974457, 11.972235], [92.96754, 11.978583], [92.953136, 11.981106], [92.947032, 11.989691], [92.957205, 12.008694], [92.972179, 12.027737], [92.980724, 12.036322], [93.014659, 12.037177], [93.025401, 12.031562], [93.018565, 12.015815], [93.040375, 11.969062], [93.042491, 11.961168]]], [[[93.103282, 12.190497], [93.10377, 12.187934], [93.103852, 12.185289], [93.103282, 12.18008], [93.096202, 12.158759], [93.094981, 12.15176], [93.102306, 12.119534], [93.103282, 12.102037], [93.094249, 12.090969], [93.08546, 12.095852], [93.074229, 12.110338], [93.064301, 12.127143], [93.059581, 12.138739], [93.059337, 12.153551], [93.061534, 12.172309], [93.066254, 12.189846], [93.073253, 12.200832], [93.082042, 12.204779], [93.091075, 12.20425], [93.098481, 12.199408], [93.103282, 12.190497]]], [[[92.733084, 12.97248], [92.737478, 12.961737], [92.732188, 12.945705], [92.729991, 12.895209], [92.724376, 12.872382], [92.717621, 12.862209], [92.701508, 12.844428], [92.697113, 12.838202], [92.695079, 12.8251], [92.696544, 12.8133], [92.694672, 12.803778], [92.682791, 12.797309], [92.670258, 12.867662], [92.672862, 12.878607], [92.677745, 12.890367], [92.684906, 12.945054], [92.682791, 12.961737], [92.695079, 12.973456], [92.702403, 12.983059], [92.708344, 12.988674], [92.71697, 12.988471], [92.722504, 12.985785], [92.727061, 12.982001], [92.730724, 12.977484], [92.733084, 12.97248]]], [[[93.052745, 13.283881], [93.075694, 13.26911], [93.079438, 13.259833], [93.056895, 13.224799], [93.052582, 13.196967], [93.052745, 13.133043], [93.048106, 13.103705], [93.036794, 13.062161], [93.019542, 13.033637], [92.998057, 13.043687], [92.990571, 13.043687], [92.986827, 13.033677], [92.97934, 13.029527], [92.970876, 13.030748], [92.964041, 13.036851], [92.961436, 13.048163], [92.966563, 13.07217], [92.964041, 13.084662], [92.956554, 13.084662], [92.956554, 13.064154], [92.937022, 13.067084], [92.928884, 13.050198], [92.928396, 13.027493], [92.932628, 13.013007], [92.950694, 12.993638], [92.956228, 12.981676], [92.952647, 12.970771], [92.942882, 12.954291], [92.936046, 12.954291], [92.934744, 12.973049], [92.927419, 12.981024], [92.916515, 12.982652], [92.905284, 12.982245], [92.902599, 12.974026], [92.880707, 12.930406], [92.87908, 12.921942], [92.876313, 12.916653], [92.876801, 12.911526], [92.884532, 12.903469], [92.896332, 12.899726], [92.905528, 12.906073], [92.915538, 12.919582], [92.92449, 12.914374], [92.928233, 12.905341], [92.929861, 12.896674], [92.932628, 12.89289], [92.940684, 12.889309], [92.964041, 12.864936], [92.960704, 12.847154], [92.965831, 12.825629], [92.968028, 12.803168], [92.956554, 12.783026], [92.973155, 12.76435], [92.984386, 12.745103], [92.990489, 12.724311], [92.990571, 12.701117], [92.976817, 12.654934], [92.972911, 12.628119], [92.989024, 12.592108], [92.991547, 12.567084], [92.990571, 12.519232], [92.9817, 12.506049], [92.964854, 12.491034], [92.955903, 12.477444], [92.970225, 12.468329], [92.962413, 12.454413], [92.949718, 12.446967], [92.93865, 12.438137], [92.936046, 12.419908], [92.92921, 12.419908], [92.923839, 12.430976], [92.915863, 12.440863], [92.906016, 12.448961], [92.89503, 12.454657], [92.897146, 12.449937], [92.901866, 12.433539], [92.873302, 12.431383], [92.84669, 12.433539], [92.84669, 12.427395], [92.853526, 12.416815], [92.863617, 12.385484], [92.871104, 12.378892], [92.881847, 12.375922], [92.894054, 12.368109], [92.902517, 12.357123], [92.901866, 12.344794], [92.91505, 12.338365], [92.912771, 12.333645], [92.903168, 12.329088], [92.89503, 12.323676], [92.886404, 12.315741], [92.881114, 12.314602], [92.879242, 12.312974], [92.880707, 12.30386], [92.885753, 12.295559], [92.893403, 12.290351], [92.900157, 12.283189], [92.901866, 12.269029], [92.896251, 12.261705], [92.886404, 12.253363], [92.879893, 12.243598], [92.884532, 12.231838], [92.888682, 12.225002], [92.892589, 12.214545], [92.894216, 12.205024], [92.891612, 12.200832], [92.872569, 12.201483], [92.874522, 12.183743], [92.864024, 12.171536], [92.842947, 12.156195], [92.827403, 12.140123], [92.833669, 12.125678], [92.833669, 12.118232], [92.825938, 12.115668], [92.806407, 12.10456], [92.80893, 12.095771], [92.799327, 12.089545], [92.783946, 12.085883], [92.768809, 12.084703], [92.757498, 12.07746], [92.767263, 12.061225], [92.792654, 12.036322], [92.781016, 12.037787], [92.771251, 12.036078], [92.763682, 12.030951], [92.758556, 12.02204], [92.765391, 12.02204], [92.745616, 11.996649], [92.740571, 11.983629], [92.737478, 11.961168], [92.737478, 11.878648], [92.744884, 11.878648], [92.749034, 11.912258], [92.753754, 11.931586], [92.761974, 11.940131], [92.776378, 11.939276], [92.787934, 11.934516], [92.795909, 11.922187], [92.79949, 11.899115], [92.796886, 11.865139], [92.76002, 11.708686], [92.754731, 11.701117], [92.734548, 11.699286], [92.722911, 11.693793], [92.689952, 11.660061], [92.68393, 11.649888], [92.682628, 11.636949], [92.682791, 11.61518], [92.687022, 11.611558], [92.696788, 11.611233], [92.706391, 11.614488], [92.710704, 11.621731], [92.710704, 11.659573], [92.715343, 11.65762], [92.726736, 11.65469], [92.731212, 11.652737], [92.730235, 11.671454], [92.736013, 11.68244], [92.746104, 11.683743], [92.758556, 11.673245], [92.761485, 11.663642], [92.765636, 11.629584], [92.765391, 11.618598], [92.760509, 11.610338], [92.747569, 11.597846], [92.744884, 11.587592], [92.744884, 11.557115], [92.735118, 11.523871], [92.726736, 11.506171], [92.71697, 11.494452], [92.713145, 11.50731], [92.707042, 11.511054], [92.698985, 11.511786], [92.689708, 11.51557], [92.658946, 11.546291], [92.654796, 11.555894], [92.65561, 11.565985], [92.654633, 11.573798], [92.645274, 11.576972], [92.633556, 11.582587], [92.625662, 11.595404], [92.621593, 11.609361], [92.621349, 11.618598], [92.62794, 11.625149], [92.63738, 11.627143], [92.645681, 11.629828], [92.648692, 11.638495], [92.642833, 11.64525], [92.631114, 11.647447], [92.61964, 11.651313], [92.614513, 11.662991], [92.613455, 11.674221], [92.608165, 11.693264], [92.607107, 11.704535], [92.60377, 11.718899], [92.595958, 11.72016], [92.586762, 11.71601], [92.579845, 11.714179], [92.563243, 11.725572], [92.560313, 11.74136], [92.566661, 11.7862], [92.566661, 11.827135], [92.563243, 11.83393], [92.554942, 11.837836], [92.545665, 11.840806], [92.534353, 11.846869], [92.530935, 11.845893], [92.528087, 11.845893], [92.525157, 11.851304], [92.524913, 11.857489], [92.527354, 11.862006], [92.530528, 11.864651], [92.531993, 11.864976], [92.539724, 11.894436], [92.562999, 11.930162], [92.570567, 11.932603], [92.577159, 11.927395], [92.592133, 11.905748], [92.594737, 11.898749], [92.600271, 11.895494], [92.614513, 11.899115], [92.607677, 11.882799], [92.61085, 11.87051], [92.617686, 11.8664], [92.621349, 11.874945], [92.624522, 11.930162], [92.629405, 11.943752], [92.627289, 11.95893], [92.623057, 11.97427], [92.621349, 11.987942], [92.625011, 12.003119], [92.63738, 12.025295], [92.641856, 12.036322], [92.640147, 12.136135], [92.645274, 12.156073], [92.668224, 12.200995], [92.681814, 12.219631], [92.689708, 12.207668], [92.697113, 12.207668], [92.703868, 12.225165], [92.713064, 12.232571], [92.720958, 12.226549], [92.724376, 12.20425], [92.719086, 12.184516], [92.719493, 12.176256], [92.727794, 12.172838], [92.733735, 12.174018], [92.736339, 12.176581], [92.737804, 12.179145], [92.741059, 12.180325], [92.760997, 12.179877], [92.765391, 12.180325], [92.784028, 12.189358], [92.788829, 12.195543], [92.792654, 12.207668], [92.7824, 12.248603], [92.775564, 12.250637], [92.767751, 12.255439], [92.761241, 12.261176], [92.758556, 12.265937], [92.7588, 12.276557], [92.760427, 12.276842], [92.764496, 12.274319], [92.781016, 12.279242], [92.789399, 12.279486], [92.7942, 12.283189], [92.792654, 12.296373], [92.786957, 12.306301], [92.778494, 12.310126], [92.754731, 12.310045], [92.733653, 12.318061], [92.722341, 12.337795], [92.717784, 12.362494], [92.72169, 12.599433], [92.726817, 12.612982], [92.744802, 12.63349], [92.750173, 12.644273], [92.754731, 12.649563], [92.76002, 12.651109], [92.775238, 12.65176], [92.778982, 12.652655], [92.783214, 12.664984], [92.777354, 12.669338], [92.766287, 12.671536], [92.754731, 12.677191], [92.747732, 12.692694], [92.744884, 12.762519], [92.737478, 12.807196], [92.740896, 12.819648], [92.748871, 12.828111], [92.758149, 12.835679], [92.765391, 12.84512], [92.785655, 12.840033], [92.800304, 12.857815], [92.809337, 12.884508], [92.813243, 12.90648], [92.813243, 12.958075], [92.828136, 12.985175], [92.834727, 13.002021], [92.830251, 13.009508], [92.807953, 13.020087], [92.809255, 13.044745], [92.8296, 13.103461], [92.848399, 13.136705], [92.853038, 13.142727], [92.854177, 13.146064], [92.851085, 13.152045], [92.837657, 13.16706], [92.833669, 13.174018], [92.844737, 13.186998], [92.842052, 13.20775], [92.842784, 13.226996], [92.864024, 13.2355], [92.863943, 13.244696], [92.849864, 13.289008], [92.854177, 13.304389], [92.854177, 13.310614], [92.841156, 13.324897], [92.847423, 13.344916], [92.886892, 13.393459], [92.891368, 13.400946], [92.89503, 13.413031], [92.895681, 13.422838], [92.894542, 13.447252], [92.89503, 13.454576], [92.901866, 13.468248], [92.903331, 13.46776], [92.915538, 13.475735], [92.937999, 13.50137], [92.942882, 13.509223], [92.945486, 13.519721], [92.945486, 13.536933], [92.949718, 13.544623], [92.958507, 13.550238], [92.977224, 13.553778], [92.984386, 13.557603], [92.99171, 13.550035], [92.997325, 13.546698], [93.003184, 13.547065], [93.011729, 13.550767], [93.006114, 13.556627], [93.004568, 13.562201], [93.006521, 13.568793], [93.011729, 13.578111], [93.016287, 13.567369], [93.022797, 13.566107], [93.031586, 13.569078], [93.042491, 13.571234], [93.049978, 13.566392], [93.066417, 13.537095], [93.055024, 13.503079], [93.053722, 13.471422], [93.059581, 13.406806], [93.064138, 13.408637], [93.07545, 13.411119], [93.080089, 13.413031], [93.07838, 13.397121], [93.057872, 13.38288], [93.052745, 13.368883], [93.046886, 13.357408], [93.033458, 13.348578], [93.017345, 13.340481], [93.004242, 13.331041], [93.003673, 13.348619], [92.995128, 13.357367], [92.984386, 13.356431], [92.977061, 13.345364], [92.97934, 13.329779], [92.990489, 13.317816], [93.004568, 13.311835], [93.025564, 13.316148], [93.035655, 13.306952], [93.052745, 13.283881]]], [[[93.039073, 13.667467], [93.042817, 13.662055], [93.052745, 13.640204], [93.019542, 13.6494], [93.000499, 13.657701], [92.990571, 13.667467], [93.019542, 13.680121], [93.033214, 13.680243], [93.039073, 13.667467]]], [[[88.872895, 21.595526], [88.88852, 21.589545], [88.902029, 21.580268], [88.911306, 21.566881], [88.91391, 21.548407], [88.90797, 21.530707], [88.895763, 21.528469], [88.879731, 21.529975], [88.862478, 21.523586], [88.838715, 21.531928], [88.830821, 21.577379], [88.841319, 21.614], [88.872895, 21.595526]]], [[[88.050548, 21.70539], [88.090017, 21.808661], [88.119314, 21.859524], [88.146739, 21.869859], [88.167003, 21.765448], [88.163422, 21.705471], [88.132579, 21.684963], [88.144379, 21.657782], [88.146007, 21.643744], [88.139415, 21.630316], [88.130056, 21.625922], [88.1185, 21.627672], [88.091563, 21.636542], [88.063487, 21.636664], [88.051768, 21.641588], [88.043142, 21.65762], [88.042247, 21.668158], [88.043956, 21.680854], [88.050548, 21.70539]]], [[[88.636241, 21.921373], [88.650157, 21.927436], [88.663259, 21.921291], [88.670584, 21.907416], [88.667003, 21.890326], [88.655284, 21.866604], [88.649181, 21.842353], [88.646332, 21.787909], [88.636241, 21.79267], [88.59962, 21.803656], [88.591807, 21.804389], [88.590505, 21.812893], [88.584972, 21.826606], [88.584321, 21.835761], [88.586274, 21.843004], [88.598643, 21.869859], [88.616466, 21.897935], [88.636241, 21.921373]]], [[[88.080903, 21.849351], [88.072276, 21.856757], [88.071056, 21.873603], [88.073904, 21.892157], [88.077322, 21.904608], [88.084727, 21.921576], [88.094249, 21.937893], [88.105724, 21.947577], [88.118907, 21.945014], [88.132579, 21.920478], [88.125011, 21.888658], [88.104991, 21.861151], [88.080903, 21.849351]]], [[[72.172699, 10.809231], [72.172618, 10.829983], [72.179047, 10.8487], [72.185151, 10.858303], [72.189301, 10.864936], [72.195649, 10.872707], [72.200043, 10.878079], [72.200043, 10.864447], [72.188162, 10.836127], [72.172699, 10.809231]]], [[[72.789073, 11.261664], [72.789236, 11.261664], [72.795909, 11.261664], [72.795584, 11.257229], [72.793956, 11.254299], [72.791515, 11.251654], [72.789073, 11.248033], [72.787934, 11.232245], [72.784923, 11.216376], [72.775483, 11.186754], [72.775401, 11.186591], [72.771169, 11.201117], [72.770356, 11.20364], [72.774181, 11.222886], [72.789073, 11.261664]]]]}}]} ================================================ FILE: static/js/api.js ================================================ /** * api.js - Backend API Communication Module for India LIMS * All fetch calls to the Flask REST API are centralized here. */ const API_BASE = ''; // Same origin; empty string for relative paths // Shared fetch options to include session cookies const FETCH_OPTS = { credentials: 'include' }; // --- Authentication Endpoints --- async function verifyCaptcha(answer, token) { const res = await fetch(`${API_BASE}/api/verify-captcha`, { ...FETCH_OPTS, method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ answer: answer, token: token }) }); return res.json(); } async function getCaptcha() { const res = await fetch(`${API_BASE}/api/captcha`, { ...FETCH_OPTS, method: 'GET' }); return res.json(); } async function adminLogin(username, password) { const res = await fetch(`${API_BASE}/api/login`, { ...FETCH_OPTS, method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); return res.json(); } async function logout() { const res = await fetch(`${API_BASE}/api/logout`, { ...FETCH_OPTS, method: 'POST', headers: { 'Content-Type': 'application/json' } }); return res.json(); } async function getSessionInfo() { const res = await fetch(`${API_BASE}/api/session-info`, { ...FETCH_OPTS, method: 'GET' }); return res.json(); } async function forgotPassword() { const res = await fetch(`${API_BASE}/api/forgot`, { ...FETCH_OPTS, method: 'GET' }); return res.json(); } // --- Records Endpoints --- async function fetchRecords() { const res = await fetch(`${API_BASE}/api/records`, { ...FETCH_OPTS, method: 'GET' }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Failed to fetch records'); } return res.json(); } async function fetchRecord(recordId) { const res = await fetch(`${API_BASE}/api/records/${recordId}`, { ...FETCH_OPTS, method: 'GET' }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Record not found'); } return res.json(); } async function searchRecords(query) { const res = await fetch(`${API_BASE}/api/records/search?q=${encodeURIComponent(query)}`, { ...FETCH_OPTS, method: 'GET' }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Search failed'); } return res.json(); } async function fetchLocationCatalog() { const res = await fetch(`${API_BASE}/api/location-catalog`, FETCH_OPTS); if (!res.ok) throw new Error('Failed to fetch location catalog'); return await res.json(); } async function createRecord(recordData) { const res = await fetch(`${API_BASE}/api/records`, { ...FETCH_OPTS, method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(recordData) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Failed to create record'); } return res.json(); } async function updateRecord(recordId, updateData) { const res = await fetch(`${API_BASE}/api/records/${recordId}`, { ...FETCH_OPTS, method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updateData) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Failed to update record'); } return res.json(); } async function deleteRecord(recordId) { const res = await fetch(`${API_BASE}/api/records/${recordId}`, { ...FETCH_OPTS, method: 'DELETE' }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Failed to delete record'); } return res.json(); } async function restoreRecord(recordId) { const res = await fetch(`${API_BASE}/api/records/${recordId}/restore`, { ...FETCH_OPTS, method: 'POST' }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Failed to restore record'); } return res.json(); } // --- GIS Processing Endpoints --- async function calculateArea(geometry) { const res = await fetch(`${API_BASE}/api/calculate-area`, { ...FETCH_OPTS, method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ geometry }) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Area calculation failed'); } return res.json(); } async function validateGeometry(geometry) { const res = await fetch(`${API_BASE}/api/validate-geometry`, { ...FETCH_OPTS, method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ geometry }) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Validation failed'); } return res.json(); } async function fetchLocationFromCoordinates(lat, lng) { const res = await fetch(`${API_BASE}/api/location-from-coords?lat=${encodeURIComponent(lat)}&lng=${encodeURIComponent(lng)}`, { ...FETCH_OPTS, method: 'GET' }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Reverse geocoding failed'); } return res.json(); } // --- Server-Side Filtering & Analytics (Python-heavy) --- /** * Server-side filtering of records. * @param {Object} filters - { state, district, village, land_use, search } * @returns {Promise} - Filtered records */ async function fetchFilteredRecords(filters = {}) { const params = new URLSearchParams(); if (filters.state) params.set('state', filters.state); if (filters.district) params.set('district', filters.district); if (filters.village) params.set('village', filters.village); if (filters.land_use) params.set('land_use', filters.land_use); if (filters.search) params.set('search', filters.search); const res = await fetch(`${API_BASE}/api/records/filter?${params}`, { ...FETCH_OPTS, method: 'GET' }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Filter failed'); } return res.json(); } /** * Pre-computed dashboard analytics from server. * @param {Object} filters - Same as fetchFilteredRecords * @returns {Promise} - { kpis, land_use_distribution, district_overview, top_parcel, recent_mutations } */ async function fetchDashboardAnalytics(filters = {}) { const params = new URLSearchParams(); if (filters.state) params.set('state', filters.state); if (filters.district) params.set('district', filters.district); if (filters.village) params.set('village', filters.village); if (filters.land_use) params.set('land_use', filters.land_use); if (filters.search) params.set('search', filters.search); const res = await fetch(`${API_BASE}/api/dashboard?${params}`, { ...FETCH_OPTS, method: 'GET' }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Dashboard analytics failed'); } return res.json(); } /** * Get application config (land use colors, options, etc.). * @returns {Promise} - { land_use_options, land_use_colors, mutation_types } */ async function fetchAppConfig() { const res = await fetch(`${API_BASE}/api/config`, { ...FETCH_OPTS, method: 'GET' }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Failed to fetch app config'); } return res.json(); } async function fetchAudit(limit = 50) { const res = await fetch(`${API_BASE}/api/audit?limit=${encodeURIComponent(limit)}`, { ...FETCH_OPTS, method: 'GET' }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Failed to fetch audit log'); } return res.json(); } // --- Document Generation Endpoints --- function getPropertyCardUrl(ulpin) { return `${API_BASE}/api/print-card/${ulpin}`; } function getVillageExcelUrl(village) { const params = village ? `?village=${encodeURIComponent(village)}` : ''; return `${API_BASE}/api/export-village${params}`; } function downloadFile(url, filename) { const a = document.createElement('a'); a.href = url; a.download = filename || 'download'; document.body.appendChild(a); a.click(); document.body.removeChild(a); } // --- Utility --- function showToast(message, type = 'info', duration = 3000) { const toast = document.getElementById('toast'); const msgEl = document.getElementById('toast-msg'); const iconEl = document.getElementById('toast-icon'); if (!toast || !msgEl) return; const icons = { success: '', error: '', info: '', warning: '' }; if (iconEl) iconEl.innerHTML = icons[type] || icons.info; msgEl.textContent = message; toast.classList.add('show'); toast.className = toast.className.replace(/success|error|info|warning/g, '').trim(); toast.classList.add(type); setTimeout(() => { toast.classList.remove('show'); }, duration); } // --- Real-Time Clock --- function updateRealTimeClock() { const timeEl = document.getElementById('rtc-time'); const dateEl = document.getElementById('rtc-date'); if (!timeEl || !dateEl) return; const now = new Date(); // Time formatted as HH:MM:SS AM/PM const timeString = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true }); // Date formatted as DD MMM YYYY (e.g. 14 Apr 2026) const dateString = now.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }).replace(/ /g, ' '); timeEl.textContent = timeString; dateEl.textContent = dateString; } // Initialize clock if elements exist document.addEventListener('DOMContentLoaded', () => { if (document.getElementById('rtc-time')) { updateRealTimeClock(); setInterval(updateRealTimeClock, 1000); } }); ================================================ FILE: static/js/auth.js ================================================ /** * auth.js - Login and CAPTCHA Handling for India LIMS * Handles the login page forms: CAPTCHA verification and Admin login. */ document.addEventListener('DOMContentLoaded', function() { // --- Only run session check on login page --- const isLoginPage = window.location.pathname === '/login' || window.location.pathname === '/'; if (isLoginPage) { checkSessionAndRedirect(); } // --- Tab Navigation Handling --- const tabPublic = document.getElementById('tab-public'); const tabAdmin = document.getElementById('tab-admin'); const panelPublic = document.getElementById('panel-public'); const panelAdmin = document.getElementById('panel-admin'); if (tabPublic && tabAdmin && panelPublic && panelAdmin) { tabPublic.addEventListener('click', () => { panelPublic.classList.remove('hidden'); panelAdmin.classList.add('hidden'); // Active Public Tab tabPublic.classList.add('text-emerald-700', 'border-emerald-500', 'bg-emerald-50/50'); tabPublic.classList.remove('text-gray-400', 'border-transparent', 'hover:text-orange-600', 'hover:bg-orange-50/30'); // Inactive Admin Tab tabAdmin.classList.add('text-gray-400', 'border-transparent', 'hover:text-orange-600', 'hover:bg-orange-50/30'); tabAdmin.classList.remove('text-orange-700', 'border-orange-500', 'bg-orange-50/50'); }); tabAdmin.addEventListener('click', () => { panelAdmin.classList.remove('hidden'); panelPublic.classList.add('hidden'); // Active Admin Tab tabAdmin.classList.add('text-orange-700', 'border-orange-500', 'bg-orange-50/50'); tabAdmin.classList.remove('text-gray-400', 'border-transparent', 'hover:text-orange-600', 'hover:bg-orange-50/30'); // Inactive Public Tab tabPublic.classList.add('text-gray-400', 'border-transparent', 'hover:text-emerald-600', 'hover:bg-emerald-50/30'); tabPublic.classList.remove('text-emerald-700', 'border-emerald-500', 'bg-emerald-50/50'); }); } // --- CAPTCHA Form Handling --- const captchaForm = document.getElementById('captcha-form'); const captchaAnswer = document.getElementById('captcha-answer'); const captchaToken = document.getElementById('captcha-token'); const captchaError = document.getElementById('captcha-error'); const captchaQuestionEl = document.getElementById('captcha-question'); if (captchaForm) { captchaForm.addEventListener('submit', async function(e) { e.preventDefault(); const answer = captchaAnswer.value.trim(); const token = captchaToken.value.trim(); if (!answer) { showCaptchaError('Please enter the answer.'); return; } // Disable button while processing const submitBtn = captchaForm.querySelector('button[type="submit"]'); const originalText = submitBtn.innerHTML; submitBtn.disabled = true; submitBtn.innerHTML = ' Verifying...'; try { const result = await verifyCaptcha(answer, token); if (result.success) { // Replace current history entry so back button doesn't return to login window.location.replace(result.redirect || '/viewer'); } else { showCaptchaError(result.message || 'Incorrect answer. Please try again.'); // Update CAPTCHA question if a new one was provided if (result.new_question && captchaQuestionEl) { captchaQuestionEl.textContent = result.new_question; } if (result.new_token && captchaToken) { captchaToken.value = result.new_token; } captchaAnswer.value = ''; captchaAnswer.focus(); } } catch (err) { showCaptchaError('An error occurred. Please try again.'); } finally { submitBtn.disabled = false; submitBtn.innerHTML = originalText; } }); } const refreshCaptchaBtn = document.getElementById('refresh-captcha'); if (refreshCaptchaBtn && captchaQuestionEl && captchaToken) { refreshCaptchaBtn.addEventListener('click', async function() { try { // Optionally show a spinning state const originalIcon = refreshCaptchaBtn.innerHTML; refreshCaptchaBtn.innerHTML = ' Regenerating...'; refreshCaptchaBtn.disabled = true; const result = await getCaptcha(); if (result.question && result.token) { captchaQuestionEl.textContent = result.question; captchaToken.value = result.token; captchaAnswer.value = ''; captchaAnswer.focus(); } else { showCaptchaError('Failed to get new CAPTCHA.'); } refreshCaptchaBtn.innerHTML = originalIcon; refreshCaptchaBtn.disabled = false; } catch (err) { showCaptchaError('Error refreshing CAPTCHA.'); refreshCaptchaBtn.disabled = false; } }); } function showCaptchaError(msg) { if (captchaError) { captchaError.textContent = msg; captchaError.classList.remove('hidden'); setTimeout(() => captchaError.classList.add('hidden'), 5000); } } // --- Admin Login Form Handling --- const loginForm = document.getElementById('login-form'); const usernameInput = document.getElementById('username'); const passwordInput = document.getElementById('password'); const loginError = document.getElementById('login-error'); if (loginForm) { loginForm.addEventListener('submit', async function(e) { e.preventDefault(); const username = usernameInput.value.trim(); const password = passwordInput.value; if (!username || !password) { showLoginError('Please enter both username and password.'); return; } // Disable button while processing const submitBtn = loginForm.querySelector('button[type="submit"]'); const originalText = submitBtn.innerHTML; submitBtn.disabled = true; submitBtn.innerHTML = ' Signing in...'; try { const result = await adminLogin(username, password); if (result.success) { // Redirect to admin dashboard window.location.replace(result.redirect || '/admin'); } else { showLoginError(result.error || 'Invalid credentials.'); passwordInput.value = ''; passwordInput.focus(); } } catch (err) { showLoginError('An error occurred during login. Please try again.'); } finally { submitBtn.disabled = false; submitBtn.innerHTML = originalText; } }); } const forgotBtn = document.getElementById('btn-forgot-password'); if (forgotBtn) { forgotBtn.addEventListener('click', async () => { try { const data = await forgotPassword(); // We use showConfirmModal just as a styled alert here showConfirmModal(data.instructions || 'Please contact your administrator.', null); } catch (err) { showLoginError('Could not fetch recovery instructions.'); } }); } function showLoginError(msg) { if (loginError) { loginError.textContent = msg; loginError.classList.remove('hidden'); setTimeout(() => loginError.classList.add('hidden'), 5000); } } // --- Logout Button (present on admin & viewer pages) --- // Logout logic should be handled by specific pages to avoid conflicts }); // --- Session Check Helper --- async function checkSessionAndRedirect() { // Check sessionStorage cache first (5-minute TTL) const CACHE_KEY = 'lims_session_cache'; const CACHE_TTL = 5 * 60 * 1000; // 5 minutes try { const cached = sessionStorage.getItem(CACHE_KEY); if (cached) { const { timestamp, sessionInfo } = JSON.parse(cached); if (Date.now() - timestamp < CACHE_TTL) { // Use cached session if (sessionInfo.is_authenticated) { if (sessionInfo.role === 'admin') { window.location.replace('/admin'); } else if (sessionInfo.role === 'viewer') { window.location.replace('/viewer'); } } return; } } } catch (err) { // Cache invalid, continue to API call } try { const sessionInfo = await getSessionInfo(); // Update cache try { sessionStorage.setItem(CACHE_KEY, JSON.stringify({ timestamp: Date.now(), sessionInfo })); } catch (err) { // sessionStorage not available, ignore } if (sessionInfo.is_authenticated) { // User is already logged in, redirect to their dashboard if (sessionInfo.role === 'admin') { window.location.replace('/admin'); } else if (sessionInfo.role === 'viewer') { window.location.replace('/viewer'); } } } catch (err) { // Session check failed, stay on login page console.log('No active session found'); } } ================================================ FILE: static/js/map.js ================================================ /** * map.js - Entry point for India LIMS client-side logic. * Loads modules and initializes the application. */ // --- Global State is now in modules/state.js --- // --- Global Event Listeners & Initialization --- document.addEventListener('DOMContentLoaded', async function() { try { // 1. Identify mode const mapEl = document.getElementById('map'); const adminMode = mapEl ? mapEl.dataset.mode === 'admin' : false; // 2. Initialize Core Map if (mapEl) { initMap(adminMode); } // 3. Admin-only initializations if (adminMode) { isAdmin = true; // Load essential data try { const catalog = await fetchLocationCatalog(); locationCatalog = catalog; console.log('Location catalog loaded:', Object.keys(catalog).length, 'states'); } catch (err) { console.warn('Could not load location catalog:', err); } loadProfile(); initializeLocationFilters(); initializeRecordFilters(); // Tab switching logic setupTabSwitching(); switchMainTab('dashboard'); // Search handler const searchBtn = document.getElementById('btn-search'); if (searchBtn) { searchBtn.addEventListener('click', performAdminSearch); } // Form submission const recordForm = document.getElementById('record-form'); if (recordForm) { recordForm.addEventListener('submit', function(e) { e.preventDefault(); handleFormSubmit(); }); } // Reset form button const resetBtn = document.getElementById('btn-reset-form'); if (resetBtn) resetBtn.addEventListener('click', resetForm); // Logout is handled by admin.js with confirmation // Refresh feedback const refreshFeedbackBtn = document.getElementById('btn-refresh-feedback'); if (refreshFeedbackBtn) { refreshFeedbackBtn.addEventListener('click', loadFeedback); } } } catch (e) { console.error('Initialization error:', e); } }); // --- Tab Switching Logic --- function setupTabSwitching() { // Main Tabs (Dashboard, Map, Records, Add Record, etc.) document.querySelectorAll('.main-tab-btn').forEach(btn => { btn.addEventListener('click', function() { const tabId = this.dataset.tab; if (tabId) switchMainTab(tabId); }); }); // Form Tabs (Location, Parcel, Owner, Mutation) document.querySelectorAll('.form-tab-btn').forEach(btn => { btn.addEventListener('click', function() { const tabId = this.dataset.formTab || this.dataset.tab; if (tabId) switchFormTab(tabId); }); }); } function switchMainTab(tabId) { // Hide all panels document.querySelectorAll('.main-tab-panel').forEach(panel => { panel.classList.add('hidden'); }); // Show target panel const target = document.getElementById('main-tab-' + tabId); if (target) { target.classList.remove('hidden'); } // Update button states document.querySelectorAll('.main-tab-btn').forEach(btn => { const active = btn.dataset.tab === tabId; btn.classList.toggle('active', active); // Tailwind active classes if (active) { btn.classList.add('bg-orange-50', 'text-orange-700', 'border-orange-500'); btn.classList.remove('text-gray-500', 'border-transparent'); } else { btn.classList.remove('bg-orange-50', 'text-orange-700', 'border-orange-500'); btn.classList.add('text-gray-500', 'border-transparent'); } }); // Special handling for maps when switching tabs if (tabId === 'map' && map) { setTimeout(() => map.invalidateSize(), 100); } else if (tabId === 'add-record') { if (!addRecordMap) initAddRecordMap(); else setTimeout(() => addRecordMap.invalidateSize(), 100); } else if (tabId === 'users') { loadUsers(); } else if (tabId === 'audit') { fetch(`${API_BASE}/api/audit`, { credentials: 'include' }) .then(r => r.json()) .then(data => showAuditModal(data)); } else if (tabId === 'feedback') { loadFeedback(); } } function switchFormTab(tabId) { document.querySelectorAll('.form-tab-panel').forEach(content => { content.classList.add('hidden'); }); const target = document.getElementById('form-tab-' + tabId); if (target) { target.classList.remove('hidden'); } document.querySelectorAll('.form-tab-btn').forEach(btn => { const active = (btn.dataset.formTab || btn.dataset.tab) === tabId; btn.classList.toggle('active', active); if (active) { btn.classList.add('border-orange-500', 'text-orange-600'); btn.classList.remove('border-transparent', 'text-gray-500'); } else { btn.classList.remove('border-orange-500', 'text-orange-600'); btn.classList.add('border-transparent', 'text-gray-500'); } }); if (tabId === 'parcel' && addRecordMap) { setTimeout(() => addRecordMap.invalidateSize(), 100); } } async function performAdminSearch() { applyAdminFilters(true); } async function viewRecordDetails(recordId) { selectedRecordId = recordId; selectedRecord = null; // Will be populated try { const record = await fetchRecord(recordId); selectedRecord = record; // Switch to view record tab switchMainTab('view-record'); // Populate details const loc = record.location || {}; const attrs = record.attributes || {}; const owner = record.owner || {}; const mutations = record.mutation_history || []; document.getElementById('view-khasra').textContent = record.khasra_no || 'N/A'; document.getElementById('view-ulpin').textContent = 'ULPIN: ' + (record.ulpin || 'N/A'); document.getElementById('view-land-use-badge').textContent = attrs.land_use || 'Unknown'; document.getElementById('view-land-use-badge').className = 'land-use-badge badge-' + (attrs.land_use || '').toLowerCase(); document.getElementById('view-state').textContent = loc.state || 'N/A'; document.getElementById('view-district').textContent = loc.district || 'N/A'; document.getElementById('view-village').textContent = loc.village || 'N/A'; document.getElementById('view-khata').textContent = record.khata_no || 'N/A'; document.getElementById('view-area').textContent = attrs.area_ha ? attrs.area_ha + ' Ha' : 'N/A'; document.getElementById('view-rate').textContent = attrs.circle_rate_inr ? 'Rs. ' + Number(attrs.circle_rate_inr).toLocaleString() + '/ha' : 'N/A'; const value = calculateValuation(attrs.area_ha, attrs.circle_rate_inr, attrs.land_use || ''); if (value > 0) { document.getElementById('view-value').textContent = 'Rs. ' + formatInr(value); } else { document.getElementById('view-value').textContent = 'Rs. 0'; } document.getElementById('view-owner').textContent = owner.name || 'N/A'; document.getElementById('view-share').textContent = owner.share_pct ? owner.share_pct + '%' : 'N/A'; document.getElementById('view-aadhaar').textContent = owner.aadhaar_mask || 'N/A'; // Owner document logic const ownerDocContainer = document.getElementById('view-owner-doc-container'); const ownerDocLink = document.getElementById('view-owner-doc-link'); if (owner.proof_doc_b64) { ownerDocContainer.classList.remove('hidden'); ownerDocLink.href = owner.proof_doc_b64; } else { ownerDocContainer.classList.add('hidden'); } // Mutation history const mutationsEl = document.getElementById('view-mutations'); if (mutations.length > 0) { mutationsEl.innerHTML = mutations.map(m => { const docLinkHTML = m.proof_doc_b64 ? ` Download Proof` : ''; return `
${m.previous_owner} ${m.mutation_type}
Share: ${m.previous_share_pct}%Aadhaar: ${m.previous_aadhaar_mask || 'N/A'}
Date: ${m.mutation_date}Ref: ${m.mutation_ref || 'N/A'}
${docLinkHTML}
`; }).join(''); } else { mutationsEl.innerHTML = '

No historical mutations found for this parcel.

'; } // Initialize small map after tab switch setTimeout(() => { if (typeof initViewRecordMap === 'function') { initViewRecordMap(record); } }, 200); // Wire buttons (if not already wired) const backBtn = document.getElementById('btn-back-to-records'); if (backBtn) backBtn.onclick = () => switchMainTab('records'); const editBtn = document.getElementById('btn-view-edit'); if (editBtn) editBtn.onclick = () => { if (typeof editRecord === 'function') editRecord(record._id); }; const printBtn = document.getElementById('btn-view-print'); if (printBtn) printBtn.onclick = () => { if (typeof printCard === 'function') printCard(record.ulpin); }; const delBtn = document.getElementById('btn-view-delete'); if (delBtn) delBtn.onclick = () => { if (typeof confirmDelete === 'function') confirmDelete(record._id, record.khasra_no); }; } catch (_err) { if (typeof showToast === 'function') { showToast('Failed to load record details.', 'error'); } } } ================================================ FILE: static/js/map_OLD_BAK.js ================================================ /** * map.js - Leaflet map and parcel workflow logic for India LIMS. * Handles map initialization, polygon drawing, record rendering, * location filtering, and admin form interactions. */ let map = null; let viewRecordMap = null; // Small map in view record tab let addRecordMap = null; // Map in add record tab let addRecordDrawnItems = null; // Drawing layer for add record let baseLayers = {}; let currentBaseLayer = null; let parcelLayers = []; let drawnItems = null; let currentSketchLayer = null; let isAdmin = false; let selectedRecordId = null; let selectedRecord = null; let reverseGeocodeTimer = null; let lastGeocodeKey = ''; let lastAutoLocation = { state: '', district: '', village: '' }; let lastGpsDetectedLocation = { state: '', district: '', village: '' }; let allRecordsCache = []; let filteredRecordsCache = []; let recordsViewMode = 'cards'; const DEFAULT_SNAP_DISTANCE = 20; let locationCatalog = {}; function showConfirmModal(message, onConfirm) { const overlay = document.createElement('div'); overlay.className = 'fixed inset-0 bg-black bg-opacity-50 z-[9999] flex items-center justify-center p-4'; overlay.style.backdropFilter = 'blur(2px)'; const modal = document.createElement('div'); modal.className = 'bg-white rounded-lg shadow-xl max-w-sm w-full overflow-hidden fade-in'; const content = document.createElement('div'); content.className = 'p-6'; const iconContainer = document.createElement('div'); iconContainer.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4'; iconContainer.innerHTML = ''; const textContainer = document.createElement('div'); textContainer.className = 'text-center'; const title = document.createElement('h3'); title.className = 'text-lg leading-6 font-medium text-gray-900 mb-2'; title.textContent = 'Confirm Action'; const messageEl = document.createElement('p'); messageEl.className = 'text-sm text-gray-500 whitespace-pre-line'; messageEl.textContent = message; textContainer.appendChild(title); textContainer.appendChild(messageEl); content.appendChild(iconContainer); content.appendChild(textContainer); const buttonsContainer = document.createElement('div'); buttonsContainer.className = 'bg-gray-50 px-4 py-3 sm:px-6 flex flex-row-reverse gap-2'; const confirmBtn = document.createElement('button'); confirmBtn.className = 'w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none sm:w-auto sm:text-sm transition-colors cursor-pointer'; confirmBtn.textContent = 'Confirm'; const cancelBtn = document.createElement('button'); cancelBtn.className = 'w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:w-auto sm:text-sm transition-colors cursor-pointer'; cancelBtn.textContent = 'Cancel'; buttonsContainer.appendChild(confirmBtn); buttonsContainer.appendChild(cancelBtn); modal.appendChild(content); modal.appendChild(buttonsContainer); overlay.appendChild(modal); document.body.appendChild(overlay); const close = () => document.body.removeChild(overlay); confirmBtn.addEventListener('click', () => { close(); if (onConfirm) onConfirm(); }); cancelBtn.addEventListener('click', close); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); } function sortedValues(values) { return [...new Set(values)].sort((a, b) => a.localeCompare(b)); } function asNumber(value) { const parsed = parseFloat(value); return Number.isFinite(parsed) ? parsed : 0; } function formatInr(value) { return Math.round(value).toLocaleString('en-IN'); } function escapeHtml(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // Utility: simple debounce implementation function debounce(fn, wait = 200) { let t = null; return function(...args) { const ctx = this; clearTimeout(t); t = setTimeout(() => fn.apply(ctx, args), wait); }; } function ensureLocationInCatalog(state, district, village) { if (!state || !district || !village) return; if (!locationCatalog[state]) { locationCatalog[state] = {}; } if (!locationCatalog[state][district]) { locationCatalog[state][district] = []; } if (!locationCatalog[state][district].includes(village)) { locationCatalog[state][district].push(village); locationCatalog[state][district] = sortedValues(locationCatalog[state][district]); } } function syncLocationCatalogWithRecords(records) { records.forEach(rec => { const loc = rec.location || {}; ensureLocationInCatalog(loc.state, loc.district, loc.village); }); } function populateStateFilter(records) { const stateFilterEl = document.getElementById('state-filter'); if (!stateFilterEl) return; const selected = stateFilterEl.value || ''; const states = sortedValues(records .map(rec => (rec.location && rec.location.state) || '') .filter(Boolean)); stateFilterEl.innerHTML = ''; states.forEach(state => { const option = document.createElement('option'); option.value = state; option.textContent = state; stateFilterEl.appendChild(option); }); if (selected && states.includes(selected)) { stateFilterEl.value = selected; } } function populateDistrictFilter(records) { const districtFilterEl = document.getElementById('district-filter'); if (!districtFilterEl) return; const selected = districtFilterEl.value || ''; const districts = sortedValues(records .map(rec => (rec.location && rec.location.district) || '') .filter(Boolean)); districtFilterEl.innerHTML = ''; districts.forEach(district => { const option = document.createElement('option'); option.value = district; option.textContent = district; districtFilterEl.appendChild(option); }); if (selected && districts.includes(selected)) { districtFilterEl.value = selected; } } function getAdminFilterState() { const query = (document.getElementById('search-input') || {}).value || ''; const landUse = (document.getElementById('land-use-filter') || {}).value || ''; const district = (document.getElementById('district-filter') || {}).value || ''; const state = (document.getElementById('state-filter') || {}).value || ''; return { query: query.trim().toLowerCase(), landUse, district, state }; } function filterRecordsByState(records, filterState) { return records.filter(rec => { const attrs = rec.attributes || {}; const loc = rec.location || {}; const owner = rec.owner || {}; const searchText = [ rec.khasra_no, rec.ulpin, rec.khata_no, loc.village, loc.district, loc.state, owner.name, attrs.land_use ].join(' ').toLowerCase(); const queryMatch = !filterState.query || searchText.includes(filterState.query); const landUseMatch = !filterState.landUse || attrs.land_use === filterState.landUse; const stateMatch = !filterState.state || loc.state === filterState.state; const districtMatch = !filterState.district || loc.district === filterState.district; return queryMatch && landUseMatch && stateMatch && districtMatch; }); } function renderKpiCards(records) { const totalParcelsEl = document.getElementById('kpi-total-parcels'); const totalAreaEl = document.getElementById('kpi-total-area'); const estimatedValueEl = document.getElementById('kpi-estimated-value'); const mutationEl = document.getElementById('kpi-mutations'); if (!totalParcelsEl || !totalAreaEl || !estimatedValueEl || !mutationEl) return; let totalArea = 0; let totalValue = 0; let totalMutations = 0; records.forEach(rec => { const attrs = rec.attributes || {}; const area = asNumber(attrs.area_ha); const rate = asNumber(attrs.circle_rate_inr); totalArea += area; totalValue += area * rate; totalMutations += (rec.mutation_history || []).length; }); totalParcelsEl.textContent = String(records.length); totalAreaEl.textContent = totalArea.toFixed(2); estimatedValueEl.textContent = formatInr(totalValue); mutationEl.textContent = String(totalMutations); } function buildDonutChart(slices, colors) { // slices: [{label, value, extra}] const total = slices.reduce((s, x) => s + x.value, 0); if (total === 0) return '

No data

'; const R = 40, CX = 50, CY = 50, stroke = 18; let cumAngle = -90; const arcs = slices.map((s, i) => { const pct = s.value / total; const angle = pct * 360; const r1 = (cumAngle * Math.PI) / 180; const r2 = ((cumAngle + angle) * Math.PI) / 180; const x1 = CX + R * Math.cos(r1), y1 = CY + R * Math.sin(r1); const x2 = CX + R * Math.cos(r2), y2 = CY + R * Math.sin(r2); const large = angle > 180 ? 1 : 0; const d = `M ${x1} ${y1} A ${R} ${R} 0 ${large} 1 ${x2} ${y2}`; cumAngle += angle; return ``; }).join(''); return `
${arcs} ${total} parcels
${slices.map((s, i) => `
${escapeHtml(s.label)} ${s.value} (${(s.value/total*100).toFixed(0)}%)
`).join('')}
`; } function buildRankedList(items, colors) { // items: [{label, value, sublabel}] const max = Math.max(...items.map(x => x.value), 1); return items.map((item, i) => { const pct = Math.max((item.value / max) * 100, 4); const color = colors[i % colors.length]; return `
${escapeHtml(item.label)} ${item.sublabel}
`; }).join(''); } function renderLandUseDistribution(records) { const target = document.getElementById('dashboard-land-use'); if (!target) return; if (!records.length) { target.innerHTML = '

No records yet.

'; return; } const metrics = {}; records.forEach(rec => { const lu = (rec.attributes && rec.attributes.land_use) || 'Unknown'; if (!metrics[lu]) metrics[lu] = { count: 0, area: 0 }; metrics[lu].count++; metrics[lu].area += asNumber(rec.attributes && rec.attributes.area_ha); }); const COLORS = ['#f97316','#3b82f6','#22c55e','#a855f7','#ec4899','#14b8a6','#f59e0b','#ef4444']; const slices = Object.entries(metrics).sort((a,b)=>b[1].count-a[1].count) .map(([label, v]) => ({ label, value: v.count, extra: v.area.toFixed(1)+' Ha' })); target.innerHTML = buildDonutChart(slices, COLORS); } function renderDistrictOverview(records) { const target = document.getElementById('dashboard-districts'); if (!target) return; if (!records.length) { target.innerHTML = '

No district data.

'; return; } const districtMap = {}; records.forEach(rec => { const d = (rec.location && rec.location.district) || 'Unknown'; if (!districtMap[d]) districtMap[d] = { count: 0, area: 0, value: 0 }; const area = asNumber(rec.attributes && rec.attributes.area_ha); districtMap[d].count++; districtMap[d].area += area; districtMap[d].value += area * asNumber(rec.attributes && rec.attributes.circle_rate_inr); }); const COLORS = ['#6366f1','#f97316','#10b981','#f59e0b','#f43f5e','#a855f7']; const items = Object.entries(districtMap).sort((a,b)=>b[1].count-a[1].count).slice(0,6) .map(([label, v]) => ({ label, value: v.count, sublabel: `${v.count} parcels · ${v.area.toFixed(1)} Ha` })); target.innerHTML = buildRankedList(items, COLORS); } function renderTopValueParcel(records) { const target = document.getElementById('dashboard-top-parcel'); if (!target) return; if (!records.length) { target.textContent = 'No parcel data yet.'; return; } let topRecord = null; let topValue = -1; records.forEach(rec => { const area = asNumber(rec.attributes && rec.attributes.area_ha); const rate = asNumber(rec.attributes && rec.attributes.circle_rate_inr); const value = area * rate; if (value > topValue) { topValue = value; topRecord = rec; } }); if (!topRecord) { target.textContent = 'No parcel data yet.'; return; } const loc = topRecord.location || {}; const attrs = topRecord.attributes || {}; target.innerHTML = `
${escapeHtml(topRecord.khasra_no || 'N/A')} (${escapeHtml(topRecord.ulpin || 'N/A')})
${escapeHtml(loc.village || 'N/A')}, ${escapeHtml(loc.district || 'N/A')} | ${escapeHtml(attrs.land_use || 'N/A')}
Area: ${asNumber(attrs.area_ha).toFixed(2)} Ha | Estimated: Rs. ${formatInr(topValue)}
`; } function renderRecentMutations(records) { const target = document.getElementById('dashboard-mutations'); if (!target) return; const entries = []; records.forEach(rec => { (rec.mutation_history || []).forEach(item => { entries.push({ khasraNo: rec.khasra_no || 'N/A', district: (rec.location && rec.location.district) || 'N/A', previousOwner: item.previous_owner || 'N/A', mutationType: item.mutation_type || 'N/A', mutationDate: item.mutation_date || 'N/A', mutationRef: item.mutation_ref || 'N/A' }); }); }); entries.sort((a, b) => String(b.mutationDate).localeCompare(String(a.mutationDate))); if (!entries.length) { target.innerHTML = '
No mutation history available for selected filters.
'; return; } target.innerHTML = entries.slice(0, 6).map(item => `
${escapeHtml(item.khasraNo)} | ${escapeHtml(item.mutationType)}
${escapeHtml(item.previousOwner)} -> ${escapeHtml(item.mutationDate)}
${escapeHtml(item.district)} | Ref: ${escapeHtml(item.mutationRef)}
`).join(''); } function renderDashboardAnalytics(filteredRecords) { renderKpiCards(filteredRecords); renderLandUseDistribution(filteredRecords); renderDistrictOverview(filteredRecords); renderTopValueParcel(filteredRecords); renderRecentMutations(filteredRecords); } function applyAdminFilters(notify) { if (!isAdmin) return; const filterState = getAdminFilterState(); // Use server-side filtering fetchFilteredRecords({ state: filterState.query ? '' : filterState.state, district: filterState.query ? '' : filterState.district, village: filterState.query ? '' : filterState.village, land_use: filterState.landUse, search: filterState.query }).then(filtered => { filteredRecordsCache = filtered; // Update map clearMapLayers(); addRecordsToMap(filtered); // Update records list renderRecordsList(filtered); // Update dashboard from server fetchDashboardAnalytics({ state: filterState.state, land_use: filterState.landUse, district: filterState.district, search: filterState.query }).then(analytics => { renderKpiCardsFromServer(analytics.kpis); renderLandUseDistributionFromServer(analytics.land_use_distribution); renderDistrictOverviewFromServer(analytics.district_overview); renderTopValueParcelFromServer(analytics.top_parcel); renderRecentMutationsFromServer(analytics.recent_mutations); }).catch(err => { console.error('Dashboard analytics failed:', err); }); if (notify) { showToast(`Showing ${filtered.length} record(s).`, 'info'); } }).catch(err => { console.error('Server-side filtering failed:', err); // Fallback to client-side const filtered = filterRecordsByState(allRecordsCache, filterState); filteredRecordsCache = filtered; clearMapLayers(); addRecordsToMap(filtered); renderRecordsList(filtered); renderDashboardAnalytics(filtered); if (notify) showToast(`Showing ${filtered.length} record(s).`, 'info'); }); } // Server-side rendering helpers function renderKpiCardsFromServer(kpis) { const totalParcelsEl = document.getElementById('kpi-total-parcels'); const totalAreaEl = document.getElementById('kpi-total-area'); const estimatedValueEl = document.getElementById('kpi-estimated-value'); const mutationEl = document.getElementById('kpi-mutations'); if (totalParcelsEl) totalParcelsEl.textContent = String(kpis.total_parcels || 0); if (totalAreaEl) totalAreaEl.textContent = (kpis.total_area || 0).toFixed(2); if (estimatedValueEl) estimatedValueEl.textContent = formatInr(kpis.estimated_value || 0); if (mutationEl) mutationEl.textContent = String(kpis.total_mutations || 0); } function renderLandUseDistributionFromServer(stats) { const target = document.getElementById('dashboard-land-use'); if (!target) return; const entries = Object.entries(stats || {}); if (!entries.length) { target.innerHTML = '

No records yet.

'; return; } const COLORS = ['#f97316','#3b82f6','#22c55e','#a855f7','#ec4899','#14b8a6','#f59e0b','#ef4444']; const slices = entries.sort((a,b)=>b[1].count-a[1].count) .map(([label, s]) => ({ label, value: s.count, extra: s.area.toFixed(1)+' Ha' })); target.innerHTML = buildDonutChart(slices, COLORS); } function renderDistrictOverviewFromServer(districts) { const target = document.getElementById('dashboard-districts'); if (!target) return; if (!districts || !districts.length) { target.innerHTML = '

No district data.

'; return; } const COLORS = ['#6366f1','#f97316','#10b981','#f59e0b','#f43f5e','#a855f7']; const items = districts.map(d => ({ label: d.name, value: d.count, sublabel: `${d.count} parcels · ${d.area.toFixed(1)} Ha` })); target.innerHTML = buildRankedList(items, COLORS); } function renderTopValueParcelFromServer(parcel) { const target = document.getElementById('dashboard-top-parcel'); if (!target) return; if (!parcel) { target.textContent = 'No parcel data yet.'; return; } target.innerHTML = `
${escapeHtml(parcel.khasra_no)} (${escapeHtml(parcel.ulpin)})
${escapeHtml(parcel.village)}, ${escapeHtml(parcel.district)} | ${escapeHtml(parcel.land_use)}
Area: ${parcel.area_ha} Ha | Estimated: Rs. ${formatInr(parcel.estimated_value)}
`; } function renderRecentMutationsFromServer(mutations) { const target = document.getElementById('dashboard-mutations'); if (!target) return; if (!mutations || !mutations.length) { target.innerHTML = '
No mutation history available for selected filters.
'; return; } target.innerHTML = mutations.slice(0, 6).map(m => `
${escapeHtml(m.khasra_no)} | ${escapeHtml(m.mutation_type)}
${escapeHtml(m.previous_owner)} -> ${escapeHtml(m.mutation_date)}
`).join(''); } function getFormElements() { return { stateEl: document.getElementById('form-state'), districtEl: document.getElementById('form-district'), villageEl: document.getElementById('form-village'), manualOverrideEl: document.getElementById('location-manual-override'), manualFieldsEl: document.getElementById('location-manual-fields'), stateManualEl: document.getElementById('form-state-manual'), districtManualEl: document.getElementById('form-district-manual'), villageManualEl: document.getElementById('form-village-manual'), sourceEl: document.getElementById('form-location-source') }; } function populateSelect(selectEl, values, placeholder, selectedValue) { if (!selectEl) return; const current = selectedValue || ''; selectEl.innerHTML = ''; const placeholderOption = document.createElement('option'); placeholderOption.value = ''; placeholderOption.textContent = placeholder; selectEl.appendChild(placeholderOption); values.forEach(value => { const option = document.createElement('option'); option.value = value; option.textContent = value; selectEl.appendChild(option); }); if (current && !values.includes(current)) { const customOption = document.createElement('option'); customOption.value = current; customOption.textContent = current; selectEl.appendChild(customOption); } selectEl.value = current; } function refreshStateOptions(selectedState) { const { stateEl } = getFormElements(); if (!stateEl) return; const states = sortedValues(Object.keys(locationCatalog)); populateSelect(stateEl, states, 'Select State', selectedState || ''); } function refreshDistrictOptions(selectedDistrict) { const { stateEl, districtEl } = getFormElements(); if (!stateEl || !districtEl) return; const state = stateEl.value; const districts = state && locationCatalog[state] ? sortedValues(Object.keys(locationCatalog[state])) : []; populateSelect(districtEl, districts, 'Select District', selectedDistrict || ''); } function refreshVillageOptions(selectedVillage) { const { stateEl, districtEl, villageEl } = getFormElements(); if (!stateEl || !districtEl || !villageEl) return; const state = stateEl.value; const district = districtEl.value; const villages = state && district && locationCatalog[state] && locationCatalog[state][district] ? sortedValues(locationCatalog[state][district]) : []; populateSelect(villageEl, villages, 'Select Village / Ward', selectedVillage || ''); } function setLocationValues(state, district, village) { ensureLocationInCatalog(state, district, village); refreshStateOptions(state || ''); refreshDistrictOptions(district || ''); refreshVillageOptions(village || ''); const { stateManualEl, districtManualEl, villageManualEl } = getFormElements(); if (stateManualEl && !stateManualEl.value.trim()) { stateManualEl.value = state || ''; } if (districtManualEl && !districtManualEl.value.trim()) { districtManualEl.value = district || ''; } if (villageManualEl && !villageManualEl.value.trim()) { villageManualEl.value = village || ''; } } function setLocationSource(text) { const { sourceEl } = getFormElements(); if (sourceEl) { sourceEl.textContent = text; } } function isManualLocationOverrideEnabled() { const { manualOverrideEl } = getFormElements(); return !!(manualOverrideEl && manualOverrideEl.checked); } function toggleManualLocationOverride(enabled) { const { stateEl, districtEl, villageEl, manualFieldsEl, stateManualEl, districtManualEl, villageManualEl } = getFormElements(); if (!stateEl || !districtEl || !villageEl || !manualFieldsEl || !stateManualEl || !districtManualEl || !villageManualEl) { return; } manualFieldsEl.classList.toggle('hidden', !enabled); stateEl.disabled = enabled; districtEl.disabled = enabled; villageEl.disabled = enabled; stateEl.required = !enabled; districtEl.required = !enabled; villageEl.required = !enabled; stateManualEl.required = enabled; districtManualEl.required = enabled; villageManualEl.required = enabled; if (enabled) { stateManualEl.value = stateEl.value || stateManualEl.value; districtManualEl.value = districtEl.value || districtManualEl.value; villageManualEl.value = villageEl.value || villageManualEl.value; setLocationSource('Source: manual override enabled'); } else { setLocationSource('Source: map auto-fill enabled'); } } function getEffectiveLocationValues() { const { stateEl, districtEl, villageEl, stateManualEl, districtManualEl, villageManualEl } = getFormElements(); if (isManualLocationOverrideEnabled()) { return { state: (stateManualEl ? stateManualEl.value : '').trim(), district: (districtManualEl ? districtManualEl.value : '').trim(), village: (villageManualEl ? villageManualEl.value : '').trim() }; } return { state: stateEl ? stateEl.value : '', district: districtEl ? districtEl.value : '', village: villageEl ? villageEl.value : '' }; } function initializeLocationFilters() { const { stateEl, districtEl, villageEl, manualOverrideEl, stateManualEl, districtManualEl, villageManualEl } = getFormElements(); if (!stateEl || !districtEl || !villageEl) return; locationCatalog = {}; refreshStateOptions(''); refreshDistrictOptions(''); refreshVillageOptions(''); stateEl.addEventListener('change', function() { lastAutoLocation = { state: '', district: '', village: '' }; refreshDistrictOptions(''); refreshVillageOptions(''); }); districtEl.addEventListener('change', function() { lastAutoLocation = { state: '', district: '', village: '' }; refreshVillageOptions(''); }); if (manualOverrideEl) { manualOverrideEl.addEventListener('change', function() { toggleManualLocationOverride(this.checked); }); } if (stateManualEl && districtManualEl && villageManualEl) { const onManualEntry = function() { if (!isManualLocationOverrideEnabled()) return; lastAutoLocation = { state: stateManualEl.value.trim(), district: districtManualEl.value.trim(), village: villageManualEl.value.trim() }; }; stateManualEl.addEventListener('input', onManualEntry); districtManualEl.addEventListener('input', onManualEntry); villageManualEl.addEventListener('input', onManualEntry); } toggleManualLocationOverride(false); } function updateAutofillStatus(message, isError) { const statusEl = document.getElementById('map-autofill-status'); if (!statusEl) return; statusEl.textContent = message || ''; statusEl.classList.toggle('text-red-600', !!isError); statusEl.classList.toggle('text-gray-500', !isError); } function isMapAutofillEnabled() { const enabledEl = document.getElementById('map-autofill-enabled'); return enabledEl ? enabledEl.checked : false; } function shouldApplyLocationUpdate(nextLocation, forceUpdate) { const { stateEl, districtEl, villageEl } = getFormElements(); if (!stateEl || !districtEl || !villageEl) return false; if (isManualLocationOverrideEnabled()) return false; if (forceUpdate) return true; const current = { state: stateEl.value || '', district: districtEl.value || '', village: villageEl.value || '' }; const currentIsEmpty = !current.state && !current.district && !current.village; const currentIsPreviousAuto = current.state === (lastAutoLocation.state || '') && current.district === (lastAutoLocation.district || '') && current.village === (lastAutoLocation.village || ''); const nextLooksUseful = nextLocation.state || nextLocation.district || nextLocation.village; return !!nextLooksUseful && (currentIsEmpty || currentIsPreviousAuto); } function applyResolvedLocation(locationData, forceUpdate) { const nextLocation = { state: locationData.state || '', district: locationData.district || '', village: locationData.village || '' }; if (!shouldApplyLocationUpdate(nextLocation, forceUpdate)) { updateAutofillStatus('Map location detected. Location fields kept as manually selected.', false); return; } setLocationValues(nextLocation.state, nextLocation.district, nextLocation.village); lastAutoLocation = nextLocation; setLocationSource(`Source: ${locationData.display_name || 'Map reverse geocoding'}`); updateAutofillStatus(`Auto-filled: ${nextLocation.district || 'District N/A'}, ${nextLocation.state || 'State N/A'}`, false); } function scheduleMapLocationLookup(lat, lng, forceUpdate) { if (!isAdmin || !isMapAutofillEnabled()) return; if (typeof lat !== 'number' || typeof lng !== 'number') return; const geocodeKey = `${lat.toFixed(4)},${lng.toFixed(4)}`; if (!forceUpdate && geocodeKey === lastGeocodeKey) return; lastGeocodeKey = geocodeKey; if (reverseGeocodeTimer) { clearTimeout(reverseGeocodeTimer); } reverseGeocodeTimer = setTimeout(async function() { updateAutofillStatus('Detecting state and district from map...', false); try { const locationData = await fetchLocationFromCoordinates(lat, lng); applyResolvedLocation(locationData, forceUpdate); } catch (err) { updateAutofillStatus(err.message || 'Map location detection failed.', true); } }, 650); } function initializeFormTabs() { const tabButtons = document.querySelectorAll('.form-tab-btn'); if (!tabButtons.length) return; tabButtons.forEach(btn => { btn.addEventListener('click', function() { switchFormTab(this.dataset.formTab); }); }); switchFormTab('location'); } function switchFormTab(tabName) { const tabButtons = document.querySelectorAll('.form-tab-btn'); const panels = document.querySelectorAll('.form-tab-panel'); tabButtons.forEach(btn => { const isActive = btn.dataset.formTab === tabName; btn.classList.toggle('active', isActive); }); panels.forEach(panel => { panel.classList.toggle('hidden', panel.id !== `form-tab-${tabName}`); }); } function setMutationMode(enabled) { const mutationSection = document.getElementById('mutation-section'); const mutationTabBtn = document.getElementById('form-mutation-tab-btn'); if (!mutationSection || !mutationTabBtn) return; mutationSection.classList.toggle('hidden', !enabled); mutationTabBtn.classList.toggle('hidden', !enabled); } function updateSnapDistanceLabel() { const slider = document.getElementById('snap-distance'); const valueLabel = document.getElementById('snap-distance-value'); if (!slider || !valueLabel) return; valueLabel.textContent = `${slider.value}px`; } function getDrawSettings() { const snapEnabledEl = document.getElementById('snap-enabled'); const snapDistanceEl = document.getElementById('snap-distance'); return { snappable: snapEnabledEl ? snapEnabledEl.checked : true, snapDistance: snapDistanceEl ? parseInt(snapDistanceEl.value, 10) || DEFAULT_SNAP_DISTANCE : DEFAULT_SNAP_DISTANCE, continueDrawing: false, allowSelfIntersection: false }; } function clearMetricsUI() { const areaInput = document.getElementById('form-area'); const areaAuto = document.getElementById('area-auto'); const areaEquivalents = document.getElementById('area-equivalents'); const geometryMetrics = document.getElementById('geometry-metrics'); const perimeterEl = document.getElementById('metric-perimeter'); const centroidEl = document.getElementById('metric-centroid'); if (areaInput) areaInput.value = ''; if (areaAuto) areaAuto.textContent = ''; if (areaEquivalents) { areaEquivalents.classList.add('hidden'); areaEquivalents.innerHTML = ''; } if (geometryMetrics) { geometryMetrics.classList.add('hidden'); } if (perimeterEl) perimeterEl.textContent = '--'; if (centroidEl) centroidEl.textContent = '--'; } function clearGeometrySelection(alsoDisableDraw) { const geometryInput = document.getElementById('form-geometry'); if (geometryInput) { geometryInput.value = ''; } clearMetricsUI(); if (drawnItems) { drawnItems.clearLayers(); } currentSketchLayer = null; if (alsoDisableDraw && map) { map.pm.disableDraw(); } } function startPolygonDraw() { if (!isAdmin || !map) return; const drawSettings = getDrawSettings(); map.pm.disableDraw(); map.pm.enableDraw('Polygon', drawSettings); showToast('Drawing started. Click points on map and double-click to finish.', 'info', 5000); } function finishPolygonDraw() { if (!isAdmin || !map) return; map.pm.disableDraw(); showToast('Drawing mode closed.', 'info'); } function cancelPolygonSelection() { if (!isAdmin) return; clearGeometrySelection(true); showToast('Polygon selection canceled.', 'warning'); } function clearPolygonSelection() { if (!isAdmin) return; clearGeometrySelection(false); showToast('Selection cleared.', 'info'); } function initializeDrawSettingsPanel() { const panel = document.getElementById('draw-settings-panel'); if (!panel) return; const startBtn = document.getElementById('btn-start-draw'); const finishBtn = document.getElementById('btn-finish-draw'); const cancelBtn = document.getElementById('btn-cancel-selection'); const clearBtn = document.getElementById('btn-clear-selection'); const snapDistance = document.getElementById('snap-distance'); const mapAutofillEnabled = document.getElementById('map-autofill-enabled'); if (startBtn) { startBtn.addEventListener('click', startPolygonDraw); } if (finishBtn) { finishBtn.addEventListener('click', finishPolygonDraw); } if (cancelBtn) { cancelBtn.addEventListener('click', cancelPolygonSelection); } if (clearBtn) { clearBtn.addEventListener('click', clearPolygonSelection); } if (snapDistance) { snapDistance.addEventListener('input', updateSnapDistanceLabel); updateSnapDistanceLabel(); } if (mapAutofillEnabled) { mapAutofillEnabled.addEventListener('change', function() { if (this.checked && map) { const center = map.getCenter(); scheduleMapLocationLookup(center.lat, center.lng, false); updateAutofillStatus('Auto-fill enabled. Move map to update location.', false); } else { updateAutofillStatus('Auto-fill paused.', false); } }); } updateAutofillStatus('Move map or draw parcel to auto-detect location.', false); } async function updateGeometryMetrics(geometry, options) { const shouldRefreshMetrics = options && options.shouldRefreshMetrics; if (!geometry) return; const geometryInput = document.getElementById('form-geometry'); if (geometryInput) { geometryInput.value = JSON.stringify(geometry); } if (!shouldRefreshMetrics) { return; } try { const result = await calculateArea(geometry); if (!result.area) { return; } const area = result.area; const perimeter = result.perimeter || {}; const centroid = result.centroid || {}; const areaInput = document.getElementById('form-area'); const areaAuto = document.getElementById('area-auto'); const areaEquivalents = document.getElementById('area-equivalents'); const geometryMetrics = document.getElementById('geometry-metrics'); const perimeterEl = document.getElementById('metric-perimeter'); const centroidEl = document.getElementById('metric-centroid'); if (areaInput) { areaInput.value = area.area_ha; } if (areaAuto) { areaAuto.textContent = '(auto)'; } if (areaEquivalents) { areaEquivalents.classList.remove('hidden'); areaEquivalents.innerHTML = ` ${area.area_acres} Acres ${area.area_guntha} Guntha ${area.area_bigha_mp} Bigha `; } if (geometryMetrics) { geometryMetrics.classList.remove('hidden'); } if (perimeterEl) { perimeterEl.textContent = perimeter.perimeter_m ? `${perimeter.perimeter_m} m (${perimeter.perimeter_km || 0} km)` : '--'; } if (centroidEl) { centroidEl.textContent = typeof centroid.lat === 'number' && typeof centroid.lng === 'number' ? `${centroid.lat.toFixed(6)}, ${centroid.lng.toFixed(6)}` : '--'; } if (typeof centroid.lat === 'number' && typeof centroid.lng === 'number') { scheduleMapLocationLookup(centroid.lat, centroid.lng, true); } } catch (err) { console.error('Area calculation failed:', err); showToast('Area metrics could not be calculated.', 'warning'); } } function switchBaseLayer(layerName) { if (!baseLayers[layerName] || layerName === currentBaseLayer) return; map.removeLayer(baseLayers[currentBaseLayer]); baseLayers[layerName].addTo(map); currentBaseLayer = layerName; } // Add India boundary mask to dim everything outside India async function addIndiaMask(mapInstance) { try { const response = await fetch('/api/boundary'); if (!response.ok) return; const indiaGeoJSON = await response.json(); // Create a mask: large rectangle with India cut out as a hole const worldBounds = [ [-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90] ]; let maskCoordinates = [worldBounds]; const geometry = indiaGeoJSON.features[0].geometry; if (geometry.type === 'MultiPolygon') { geometry.coordinates.forEach(polygon => { maskCoordinates.push(polygon[0]); // Add exterior ring of each polygon as a hole }); } else if (geometry.type === 'Polygon') { maskCoordinates.push(geometry.coordinates[0]); } // Create a GeoJSON with the mask (world minus India) const maskGeoJSON = { type: 'Feature', geometry: { type: 'Polygon', coordinates: maskCoordinates // Exterior ring is world, holes are India's parts } }; // Add the mask layer const maskLayer = L.geoJSON(maskGeoJSON, { style: { color: '#1f2937', weight: 2, fillColor: '#1f2937', fillOpacity: 0.7 }, interactive: false // Allow clicks to pass through }); maskLayer.addTo(mapInstance); // Add India border outline for clarity const indiaBorder = L.geoJSON(indiaGeoJSON, { style: { color: '#f97316', // Orange border weight: 3, fillColor: 'transparent', fillOpacity: 0, opacity: 0.9 }, interactive: false }); indiaBorder.addTo(mapInstance); } catch (err) { console.warn('Failed to load India boundary mask:', err); } } function initMap(adminMode) { isAdmin = adminMode || false; // Expanded India bounding box with generous margins const indiaBounds = L.latLngBounds( L.latLng(4.0, 60.0), // Southwest (extended into ocean) L.latLng(40.0, 105.0) // Northeast (extended into China/Myanmar) ); map = L.map('map', { center: [23.5, 77.5], zoom: 7, minZoom: 4, maxZoom: 18, maxBounds: indiaBounds, maxBoundsViscosity: 1.0, zoomControl: true, attributionControl: true }); baseLayers.osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 19, crossOrigin: true }); baseLayers.google = L.tileLayer('https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', { attribution: '© Google Maps', maxZoom: 20, crossOrigin: true }); baseLayers.esri = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: '© Esri', maxZoom: 18, crossOrigin: true }); baseLayers.osm.addTo(map); currentBaseLayer = 'osm'; document.querySelectorAll('input[name="basemap"]').forEach(radio => { radio.addEventListener('change', function() { switchBaseLayer(this.value); }); }); // Add India boundary mask addIndiaMask(map); drawnItems = new L.FeatureGroup(); map.addLayer(drawnItems); if (isAdmin) { // For Map View tab, disable all drawing controls map.pm.addControls({ position: 'topleft', drawPolygon: false, drawMarker: false, drawCircleMarker: false, drawPolyline: false, drawRectangle: false, drawCircle: false, editMode: false, dragMode: false, cutPolygon: false, removalMode: false, rotateMode: false }); // Hide Geoman toolbar by default (only shown in Add Record tab) setTimeout(() => { const geomanToolbar = document.querySelector('.leaflet-pm-toolbar'); if (geomanToolbar) { geomanToolbar.style.display = 'none'; } }, 100); map.pm.setPathOptions({ color: '#ea580c', fillColor: '#fed7aa', fillOpacity: 0.3, weight: 3 }); map.on('pm:create', async function(e) { const layer = e.layer; drawnItems.clearLayers(); drawnItems.addLayer(layer); currentSketchLayer = layer; map.pm.disableDraw(); const geometry = layer.toGeoJSON().geometry; await updateGeometryMetrics(geometry, { shouldRefreshMetrics: true }); switchMainTab('add-record'); switchFormTab('parcel'); const drawInstruction = document.getElementById('draw-instruction'); if (drawInstruction) { drawInstruction.style.display = 'none'; } showToast('Polygon ready. Review parcel details and save.', 'success'); }); map.on('pm:edit', async function(e) { const layer = e.layer; if (!layer) return; currentSketchLayer = layer; const geometry = layer.toGeoJSON().geometry; await updateGeometryMetrics(geometry, { shouldRefreshMetrics: true }); }); map.on('pm:remove', function() { clearGeometrySelection(false); }); } map.on('mousemove', function(e) { const coordsEl = document.getElementById('cursor-coords'); if (coordsEl) { coordsEl.textContent = `Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}`; } }); if (isAdmin) { map.on('moveend', function() { const center = map.getCenter(); scheduleMapLocationLookup(center.lat, center.lng, false); }); map.on('click', function(e) { if (map.pm && typeof map.pm.globalDrawModeEnabled === 'function' && map.pm.globalDrawModeEnabled()) { return; } scheduleMapLocationLookup(e.latlng.lat, e.latlng.lng, true); }); } loadRecordsOnMap(); } async function loadRecordsOnMap() { try { const records = await fetchRecords(); allRecordsCache = records; if (isAdmin) { syncLocationCatalogWithRecords(records); populateStateFilter(records); populateDistrictFilter(records); const { stateEl, districtEl, villageEl } = getFormElements(); const selectedState = stateEl ? stateEl.value : ''; const selectedDistrict = districtEl ? districtEl.value : ''; const selectedVillage = villageEl ? villageEl.value : ''; refreshStateOptions(selectedState); refreshDistrictOptions(selectedDistrict); refreshVillageOptions(selectedVillage); applyAdminFilters(false); return; } clearMapLayers(); addRecordsToMap(records); } catch (err) { console.error('Failed to load records:', err); showToast('Failed to load land records.', 'error'); } } function clearMapLayers() { parcelLayers.forEach(layer => { map.removeLayer(layer); }); parcelLayers = []; } function addRecordsToMap(records) { records.forEach(record => { if (!record.geometry || record.geometry.type !== 'Polygon') return; const landUse = (record.attributes && record.attributes.land_use) || 'agricultural'; const color = getLandUseColor(landUse); const geoJsonLayer = L.geoJSON(record.geometry, { style: function() { return { color: color, fillColor: color, fillOpacity: 0.25, weight: 2.5, opacity: 0.9 }; }, onEachFeature: function(_feature, layer) { layer.on('click', function() { onParcelClick(record, layer); }); layer.on('mouseover', function() { this.setStyle({ fillOpacity: 0.45, weight: 3.5 }); }); layer.on('mouseout', function() { this.setStyle({ fillOpacity: 0.25, weight: 2.5 }); }); const attrs = record.attributes || {}; const owner = record.owner || {}; const loc = record.location || {}; layer.bindTooltip( `
${record.khasra_no || 'N/A'}
ULPIN: ${record.ulpin || 'N/A'}
Land Use: ${landUse}
Area: ${attrs.area_ha || '?'} Ha
Owner: ${owner.name || 'N/A'}
Location: ${loc.village || '?'}, ${loc.district || '?'}
Click to view details →
`, { sticky: true, className: 'parcel-tooltip', direction: 'top', offset: [0, -10] } ); } }); geoJsonLayer.addTo(map); geoJsonLayer._recordId = record._id; parcelLayers.push(geoJsonLayer); }); if (isAdmin) { renderRecordsList(records); } } function getLandUseColor(landUse) { const colors = { Agricultural: '#22c55e', Residential: '#3b82f6', Commercial: '#f59e0b', Industrial: '#8b5cf6', Government: '#ef4444', Forest: '#065f46', Wasteland: '#9ca3af' }; return colors[landUse] || '#6b7280'; } function onParcelClick(record) { if (isAdmin) { showAdminDetails(record); flyToRecord(record); } else if (typeof showViewerInfo === 'function') { showViewerInfo(record); } } function flyToRecord(record) { if (!record.geometry) return; const geoJsonLayer = L.geoJSON(record.geometry); const bounds = geoJsonLayer.getBounds(); map.fitBounds(bounds, { padding: [60, 60], maxZoom: 16 }); } function switchRecordsView(mode) { const cardsContainer = document.getElementById('records-list-cards'); const tableContainer = document.getElementById('records-list-table'); if (!cardsContainer || !tableContainer) return; recordsViewMode = mode === 'table' ? 'table' : 'cards'; cardsContainer.classList.toggle('hidden', recordsViewMode !== 'cards'); tableContainer.classList.toggle('hidden', recordsViewMode !== 'table'); document.querySelectorAll('.records-view-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.view === recordsViewMode); }); } function renderRecordsList(records) { const cardsEl = document.getElementById('records-list-cards'); const tableEl = document.getElementById('records-list-table'); const noRecordsEl = document.getElementById('no-records'); const countLabel = document.getElementById('records-count-label'); if (!cardsEl || !tableEl) return; if (countLabel) { countLabel.textContent = String(records.length); } if (records.length === 0) { cardsEl.innerHTML = ''; tableEl.innerHTML = ''; if (noRecordsEl) noRecordsEl.classList.remove('hidden'); return; } if (noRecordsEl) noRecordsEl.classList.add('hidden'); cardsEl.innerHTML = records.map(rec => { const attrs = rec.attributes || {}; const owner = rec.owner || {}; const loc = rec.location || {}; const landUse = attrs.land_use || 'Unknown'; const badgeClass = 'badge-' + landUse.toLowerCase(); return `
${rec.khasra_no || 'N/A'} ${rec.deleted ? `Deleted` : ''}
${landUse}
ULPIN: ${rec.ulpin || 'N/A'}
${owner.name || 'No Owner'} | ${attrs.area_ha || '?'} Ha
${loc.village || ''}, ${loc.district || ''}
${loc.state || ''} • ${loc.district || ''}
`; }).join(''); // Attach click handlers for view buttons cardsEl.querySelectorAll('.view-record-btn').forEach((btn, index) => { btn.addEventListener('click', function(e) { e.stopPropagation(); const card = this.closest('.record-card'); if (card) { viewRecordDetails(records[index]._id); } }); }); tableEl.innerHTML = records.map((rec, index) => { const attrs = rec.attributes || {}; const loc = rec.location || {}; const landUse = attrs.land_use || 'Unknown'; const recordValue = asNumber(attrs.area_ha) * asNumber(attrs.circle_rate_inr); return `

${escapeHtml(rec.khasra_no || 'N/A')} ${rec.deleted ? 'Deleted' : ''}

${escapeHtml(rec.ulpin || 'N/A')} | ${escapeHtml(loc.district || 'N/A')}

${escapeHtml(landUse)} ${asNumber(attrs.area_ha).toFixed(2)} Ha Rs. ${formatInr(recordValue)}
`; }).join(''); // Attach click handlers for table view buttons tableEl.querySelectorAll('.view-record-btn-table').forEach((btn, index) => { btn.addEventListener('click', function(e) { e.stopPropagation(); const row = this.closest('.record-table-row'); if (row) { viewRecordDetails(records[index]._id); } }); }); switchRecordsView(recordsViewMode); } async function selectRecord(recordId) { selectedRecordId = recordId; document.querySelectorAll('.record-card').forEach(el => { el.classList.toggle('active', el.dataset.id === recordId); }); document.querySelectorAll('.record-table-row').forEach(el => { el.classList.toggle('active', el.dataset.id === recordId); }); try { const record = await fetchRecord(recordId); flyToRecord(record); showAdminDetails(record); } catch (_err) { showToast('Failed to load record details.', 'error'); } } // View record details in full screen tab async function viewRecordDetails(recordId) { selectedRecordId = recordId; selectedRecord = null; // Will be populated try { const record = await fetchRecord(recordId); selectedRecord = record; // Switch to view record tab switchMainTab('view-record'); // Populate details const loc = record.location || {}; const attrs = record.attributes || {}; const owner = record.owner || {}; const mutations = record.mutation_history || []; document.getElementById('view-khasra').textContent = record.khasra_no || 'N/A'; document.getElementById('view-ulpin').textContent = 'ULPIN: ' + (record.ulpin || 'N/A'); document.getElementById('view-land-use-badge').textContent = attrs.land_use || 'Unknown'; document.getElementById('view-land-use-badge').className = 'land-use-badge badge-' + (attrs.land_use || '').toLowerCase(); document.getElementById('view-state').textContent = loc.state || 'N/A'; document.getElementById('view-district').textContent = loc.district || 'N/A'; document.getElementById('view-village').textContent = loc.village || 'N/A'; document.getElementById('view-khata').textContent = record.khata_no || 'N/A'; document.getElementById('view-area').textContent = attrs.area_ha ? attrs.area_ha + ' Ha' : 'N/A'; document.getElementById('view-rate').textContent = attrs.circle_rate_inr ? 'Rs. ' + Number(attrs.circle_rate_inr).toLocaleString() + '/ha' : 'N/A'; const value = asNumber(attrs.area_ha) * asNumber(attrs.circle_rate_inr); document.getElementById('view-value').textContent = 'Rs. ' + formatInr(value); document.getElementById('view-owner').textContent = owner.name || 'N/A'; document.getElementById('view-share').textContent = owner.share_pct ? owner.share_pct + '%' : 'N/A'; document.getElementById('view-aadhaar').textContent = owner.aadhaar_mask || 'N/A'; // Owner document logic const ownerDocContainer = document.getElementById('view-owner-doc-container'); const ownerDocLink = document.getElementById('view-owner-doc-link'); if (owner.proof_doc_b64) { ownerDocContainer.classList.remove('hidden'); ownerDocLink.href = owner.proof_doc_b64; } else { ownerDocContainer.classList.add('hidden'); } // Mutation history const mutationsEl = document.getElementById('view-mutations'); if (mutations.length > 0) { mutationsEl.innerHTML = mutations.map(m => { const docLinkHTML = m.proof_doc_b64 ? ` Download Proof` : ''; return `
${m.previous_owner} (${m.previous_share_pct}%) ${m.mutation_type}

Date: ${m.mutation_date} | Ref: ${m.mutation_ref || 'N/A'}

${docLinkHTML}
`; }).join(''); } else { mutationsEl.innerHTML = '

No mutation history

'; } // Initialize small map after tab switch setTimeout(() => initViewRecordMap(record), 200); } catch (_err) { showToast('Failed to load record details.', 'error'); } } // Initialize small map for view record tab function initViewRecordMap(record) { if (!document.getElementById('view-record-map')) return; // Destroy existing map if any if (viewRecordMap) { viewRecordMap.remove(); viewRecordMap = null; } // Expanded India bounding box with generous margins const indiaBounds = L.latLngBounds( L.latLng(4.0, 60.0), // Southwest (extended into ocean) L.latLng(40.0, 105.0) // Northeast (extended into China/Myanmar) ); viewRecordMap = L.map('view-record-map', { center: [23.5, 77.5], zoom: 7, minZoom: 4, maxZoom: 18, maxBounds: indiaBounds, maxBoundsViscosity: 1.0, zoomControl: true }); // Add basemap layers for view record map const viewBaseLayers = {}; viewBaseLayers.osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 19, crossOrigin: true }); viewBaseLayers.google = L.tileLayer('https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', { attribution: '© Google Maps', maxZoom: 20, crossOrigin: true }); viewBaseLayers.esri = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: '© Esri', maxZoom: 18, crossOrigin: true }); viewBaseLayers.osm.addTo(viewRecordMap); let currentViewBaseLayer = 'osm'; // Handle layer switching for view record map document.querySelectorAll('input[name="view-basemap"]').forEach(radio => { radio.addEventListener('change', function() { if (viewBaseLayers[this.value] && this.value !== currentViewBaseLayer) { viewRecordMap.removeLayer(viewBaseLayers[currentViewBaseLayer]); viewBaseLayers[this.value].addTo(viewRecordMap); currentViewBaseLayer = this.value; } }); }); // Coordinate display viewRecordMap.on('mousemove', function(e) { const coordsEl = document.getElementById('view-cursor-coords'); if (coordsEl) { coordsEl.textContent = `Lat: ${e.latlng.lat.toFixed(5)}, Lng: ${e.latlng.lng.toFixed(5)}`; } }); // Add parcel geometry if (record.geometry) { const landUse = (record.attributes && record.attributes.land_use) || ''; const color = getLandUseColor(landUse); const geoJsonLayer = L.geoJSON(record.geometry, { style: { color: color || '#ea580c', fillColor: color || '#ea580c', fillOpacity: 0.35, weight: 3 } }); geoJsonLayer.addTo(viewRecordMap); const bounds = geoJsonLayer.getBounds(); viewRecordMap.fitBounds(bounds, { padding: [40, 40] }); } setTimeout(() => { viewRecordMap.invalidateSize(); // Add India mask to view record map addIndiaMask(viewRecordMap); }, 200); } // Render and show audit modal function showAuditModal(entries) { const container = document.getElementById('audit-entries'); if (!container) return; if (!Array.isArray(entries)) entries = []; // Prepare internal state for client-side filtering/pagination container.__auditEntries = entries.slice(); container.__auditPage = 1; container.__auditPageSize = 10; function getActionBadge(action) { const a = (action || '').toLowerCase(); if (a.includes('delete')) return 'Delete'; if (a.includes('create') || a.includes('add')) return 'Create'; if (a.includes('update') || a.includes('edit')) return 'Update'; if (a.includes('restore')) return 'Restore'; return `${escapeHtml(action)}`; } function renderAuditList(page = 1, pageSize = 10, filter = {}) { const all = container.__auditEntries || []; let filtered = all.filter(e => { if (filter.q) { const q = filter.q.toLowerCase(); const hay = [e.action, e.performed_by, e.user, e.record_id, JSON.stringify(e.details || {})].join(' ').toLowerCase(); if (!hay.includes(q)) return false; } if (filter.action && filter.action !== 'all') { if (!String(e.action || '').toLowerCase().includes(filter.action)) return false; } if (filter.user && filter.user !== 'all') { if (!String((e.performed_by||e.user||'')).toLowerCase().includes(filter.user)) return false; } return true; }); const total = filtered.length; const pages = Math.max(1, Math.ceil(total / pageSize)); if (page > pages) page = pages; container.__auditPage = page; container.__auditPageSize = pageSize; const start = (page - 1) * pageSize; const pageItems = filtered.slice(start, start + pageSize); const controlsHtml = `
${total} results
Timestamp Action User Record ID Details
`; container.innerHTML = controlsHtml; // populate user filter options const users = Array.from(new Set(all.map(a => (a.performed_by||a.user||'').toLowerCase()).filter(Boolean))).sort(); const userFilter = document.getElementById('audit-user-filter'); users.forEach(u => { const opt = document.createElement('option'); opt.value = u; opt.textContent = u; if (filter.user === u) opt.selected = true; userFilter.appendChild(opt); }); const listEl = document.getElementById('audit-list'); if (pageItems.length === 0) { listEl.innerHTML = 'No logs found matching your criteria.'; } else { pageItems.forEach(e => { const ts = e.timestamp || e.time || e.created_at || ''; const action = e.action || 'unknown'; const by = e.performed_by || e.user || 'system'; const rid = e.record_id || '-'; let detailsStr = ''; if (e.details) { if (typeof e.details === 'string') { detailsStr = e.details; } else { // Extract meaningful details for table const parts = []; if (e.details.khasra_no) parts.push(`Khasra: ${e.details.khasra_no}`); if (e.details.ulpin) parts.push(`ULPIN: ${e.details.ulpin}`); if (e.details.changes) { const numChanges = Object.keys(e.details.changes).length; parts.push(`${numChanges} field(s) changed`); } if (parts.length > 0) { detailsStr = parts.join(' | '); } else { detailsStr = JSON.stringify(e.details).slice(0, 100); } } } const detailsRaw = e.details ? (typeof e.details === 'string' ? e.details : JSON.stringify(e.details, null, 2)) : ''; let dateStr = ts; try { const d = new Date(ts); dateStr = d.toLocaleString(); } catch (_) {} const tr = document.createElement('tr'); tr.className = 'hover:bg-gray-50 transition-colors'; tr.innerHTML = ` ${escapeHtml(dateStr)} ${getActionBadge(action)} ${escapeHtml(by)} ${escapeHtml(rid)}
${escapeHtml(detailsStr || 'No details')}
`; listEl.appendChild(tr); }); } // pager const pager = document.getElementById('audit-pager'); const prevDisabled = page <= 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'; const nextDisabled = page >= pages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'; pager.innerHTML = `
Page ${page} of ${pages}
`; const getFilterState = () => ({ q: document.getElementById('audit-search').value, action: document.getElementById('audit-action-filter').value, user: document.getElementById('audit-user-filter').value }); // wire up controls const searchInput = document.getElementById('audit-search'); searchInput.addEventListener('input', debounce(() => { renderAuditList(1, container.__auditPageSize, getFilterState()); }, 300)); // Ensure cursor is at the end of the text when re-rendering with search if (document.activeElement.id === 'audit-search') { setTimeout(() => { const input = document.getElementById('audit-search'); if (input) { input.focus(); const len = input.value.length; input.setSelectionRange(len, len); } }, 0); } document.getElementById('audit-action-filter').addEventListener('change', () => { renderAuditList(1, container.__auditPageSize, getFilterState()); }); document.getElementById('audit-user-filter').addEventListener('change', () => { renderAuditList(1, container.__auditPageSize, getFilterState()); }); document.getElementById('audit-page-size').addEventListener('change', () => { const ps = parseInt(document.getElementById('audit-page-size').value, 10) || 10; renderAuditList(1, ps, getFilterState()); }); document.getElementById('audit-prev').addEventListener('click', () => { if (container.__auditPage > 1) renderAuditList(container.__auditPage - 1, container.__auditPageSize, getFilterState()); }); document.getElementById('audit-next').addEventListener('click', () => { if (container.__auditPage < pages) renderAuditList(container.__auditPage + 1, container.__auditPageSize, getFilterState()); }); // download document.getElementById('audit-download').addEventListener('click', () => { try { // If filtered, download filtered data, otherwise all const dataToDownload = filtered.length > 0 ? filtered : container.__auditEntries; const blob = new Blob([JSON.stringify(dataToDownload, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `audit_log_${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } catch (e) { console.error('Audit download failed', e); showToast('Failed to download audit JSON.', 'error'); } }); } // initialize renderAuditList(1, container.__auditPageSize, {}); } function showAdminDetails(record) { selectedRecordId = record._id; selectedRecord = record; // Save for print/delete buttons // Show details in the map overlay panel instead of sidebar const detailsPanel = document.getElementById('map-details-panel'); const detailsActions = document.getElementById('map-details-actions'); if (detailsPanel) { detailsPanel.classList.remove('hidden'); } if (detailsActions) { detailsActions.classList.remove('hidden'); } const content = document.getElementById('map-details-content'); if (!content) return; const loc = record.location || {}; const attrs = record.attributes || {}; const owner = record.owner || {}; const mutations = record.mutation_history || []; let mutationHtml = ''; if (mutations.length > 0) { mutationHtml = `

MUTATION HISTORY

${mutations.map(m => `
${m.previous_owner} (${m.previous_share_pct}%)
${m.mutation_type} on ${m.mutation_date}
Ref: ${m.mutation_ref || 'N/A'}
`).join('')}
`; } content.innerHTML = `

${record.khasra_no || 'N/A'}

${record.deleted ? `Soft-deleted` : ''}
${attrs.land_use || 'N/A'}
ULPIN ${record.ulpin || 'N/A'} Khata No. ${record.khata_no || 'N/A'} Area ${attrs.area_ha || 'N/A'} Ha Circle Rate Rs. ${(attrs.circle_rate_inr || 0).toLocaleString()}/ha Village ${loc.village || 'N/A'} District ${loc.district || 'N/A'} State ${loc.state || 'N/A'}

OWNER

Name ${owner.name || 'N/A'} Share ${owner.share_pct || 'N/A'}% Aadhaar ${owner.aadhaar_mask || 'N/A'}
${mutationHtml} ${record.deleted ? `

Deletion details

Deleted by: ${record.deleted_by || 'Unknown'}

Deleted at: ${record.deleted_at || 'Unknown'}

` : ''}
`; // Attach edit button handler const editBtn = document.getElementById('btn-map-edit'); if (editBtn) { editBtn.addEventListener('click', function() { editRecord(record._id); }); } // Rebuild details action buttons depending on deleted state // detailsActions reference already obtained above if (detailsActions) { // If record is soft-deleted, show Restore and Hard Delete buttons prominently if (record.deleted) { detailsActions.innerHTML = ` `; const restoreBtn = document.getElementById('btn-map-restore'); if (restoreBtn) { restoreBtn.addEventListener('click', function() { if (!record || !record._id) return showToast('No record selected.', 'error'); showConfirmModal(`Restore record "${record.khasra_no || ''}"?`, async () => { try { await restoreRecord(record._id); showToast('Record restored.', 'success'); selectedRecordId = null; selectedRecord = null; // Refresh map/list loadRecordsOnMap(); const panel = document.getElementById('map-details-panel'); if (panel) panel.classList.add('hidden'); } catch (err) { showToast('Restore failed: ' + err.message, 'error'); } }); }); } const hardDeleteBtn = document.getElementById('btn-map-hard-delete'); if (hardDeleteBtn) { hardDeleteBtn.addEventListener('click', function() { if (!record || !record._id) return showToast('No record selected.', 'error'); showConfirmModal(`Permanently delete record "${record.khasra_no || ''}"? This cannot be undone.`, () => { deleteRecord(record._id).then(() => { showToast('Record permanently deleted.', 'success'); selectedRecordId = null; selectedRecord = null; loadRecordsOnMap(); const panel = document.getElementById('map-details-panel'); if (panel) panel.classList.add('hidden'); }).catch(err => showToast('Delete failed: ' + err.message, 'error')); }); }); } } else { // Restore default actions: Edit / Print / Delete detailsActions.innerHTML = ` `; // Reattach handlers for newly created buttons const mapPrintBtn = document.getElementById('btn-map-print'); if (mapPrintBtn) mapPrintBtn.addEventListener('click', function() { if (selectedRecord && selectedRecord.ulpin) printCard(selectedRecord.ulpin); else showToast('No ULPIN available.', 'error'); }); const mapDeleteBtn = document.getElementById('btn-map-delete'); if (mapDeleteBtn) mapDeleteBtn.addEventListener('click', function() { if (selectedRecord) confirmDelete(selectedRecord._id, selectedRecord.khasra_no || ''); else showToast('No record selected.', 'error'); }); const mapEditBtn2 = document.getElementById('btn-map-edit'); if (mapEditBtn2) mapEditBtn2.addEventListener('click', function() { if (selectedRecord) editRecord(selectedRecord._id); }); } } } async function editRecord(recordId) { try { const record = await fetchRecord(recordId); switchMainTab('add-record'); // Wait for tab to show await new Promise(resolve => setTimeout(resolve, 200)); const { manualOverrideEl } = getFormElements(); if (manualOverrideEl) { manualOverrideEl.checked = false; } toggleManualLocationOverride(false); document.getElementById('form-record-id').value = record._id; document.getElementById('form-title').textContent = 'Edit Record: ' + (record.khasra_no || ''); document.getElementById('form-khasra').value = record.khasra_no || ''; document.getElementById('form-khata').value = record.khata_no || ''; document.getElementById('form-ulpin').value = record.ulpin || ''; document.getElementById('form-land-use').value = (record.attributes && record.attributes.land_use) || ''; document.getElementById('form-area').value = (record.attributes && record.attributes.area_ha) || ''; document.getElementById('form-circle-rate').value = (record.attributes && record.attributes.circle_rate_inr) || 0; document.getElementById('form-owner-name').value = (record.owner && record.owner.name) || ''; document.getElementById('form-share').value = (record.owner && record.owner.share_pct) || 100; document.getElementById('form-aadhaar').value = (record.owner && record.owner.aadhaar_mask) || ''; const loc = record.location || {}; setLocationValues(loc.state || '', loc.district || '', loc.village || ''); setLocationSource('Source: loaded from existing record'); const geometry = record.geometry || null; if (geometry) { // Update geometry metrics await updateGeometryMetricsForAddRecord(geometry); // Clear existing layers on add record map if (addRecordDrawnItems) { addRecordDrawnItems.clearLayers(); } // Add existing geometry to add record map const editableLayer = L.geoJSON(geometry, { style: { color: '#ea580c', fillColor: '#fed7aa', fillOpacity: 0.4, weight: 3 } }); editableLayer.eachLayer(layer => { if (addRecordDrawnItems) { addRecordDrawnItems.addLayer(layer); } currentSketchLayer = layer; }); // Enable editing on the layer if (addRecordMap) { // Fly to the parcel const bounds = editableLayer.getBounds(); addRecordMap.fitBounds(bounds, { padding: [50, 50], maxZoom: 16 }); // Enable edit mode setTimeout(() => { editableLayer.eachLayer(layer => { if (layer.pm) { layer.pm.enable(); } }); }, 300); } } setMutationMode(true); document.getElementById('form-submit-btn').textContent = 'Update Record'; const drawInstruction = document.getElementById('draw-instruction'); if (drawInstruction) { drawInstruction.style.display = 'none'; } const drawStatus = document.getElementById('draw-status'); if (drawStatus) { drawStatus.innerHTML = 'Editing existing record. You can modify the polygon on the map.'; drawStatus.className = 'bg-blue-50 border-l-4 border-blue-500 rounded-r-lg p-3 text-sm text-blue-800'; } switchFormTab('location'); } catch (_err) { showToast('Failed to load record for editing.', 'error'); } } async function capturePolygonMapForPdf(geometry) { return new Promise((resolve) => { const W = 800, H = 450; const wrap = document.createElement('div'); // Must be in-viewport for tiles to load and html-to-image to work // Use opacity near-zero so user never sees it wrap.style.cssText = `position:fixed;left:0;top:0;width:${W}px;height:${H}px;z-index:99999;opacity:0.01;pointer-events:none;`; document.body.appendChild(wrap); const pdfMap = L.map(wrap, { zoomControl: false, attributionControl: false }); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(pdfMap); if (geometry && geometry.coordinates && geometry.coordinates[0]) { const latlngs = geometry.coordinates[0].map(c => [c[1], c[0]]); const poly = L.polygon(latlngs, { color: '#dc2626', weight: 3, fillColor: '#fca5a5', fillOpacity: 0.35 }).addTo(pdfMap); pdfMap.fitBounds(poly.getBounds(), { padding: [55, 55] }); } else { pdfMap.setView([23.5, 77.5], 5); } setTimeout(async () => { try { // Briefly make fully visible for capture wrap.style.opacity = '1'; const dataUrl = await window.htmlToImage.toPng(wrap, { pixelRatio: 1.5, width: W, height: H }); resolve(dataUrl); } catch (e) { console.warn('PDF map capture error:', e); resolve(null); } finally { try { pdfMap.remove(); } catch (_) {} try { document.body.removeChild(wrap); } catch (_) {} } }, 2500); }); } async function printCard(ulpin) { if (!ulpin) { showToast('No ULPIN available for this record.', 'error'); return; } showToast('Generating PDF — capturing map, please wait...', 'info'); // Get geometry from selected record or cache const record = selectedRecord || (allRecordsCache || []).find(r => r.ulpin === ulpin); const geometry = record && record.geometry; let mapImageBase64 = null; if (geometry && typeof window.htmlToImage !== 'undefined') { mapImageBase64 = await capturePolygonMapForPdf(geometry); } try { const res = await fetch(getPropertyCardUrl(ulpin), { method: 'POST', headers: { 'Content-Type': 'application/json', ...FETCH_OPTS.headers }, body: JSON.stringify({ map_image: mapImageBase64 }) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Failed to generate property card'); } const blob = await res.blob(); const url = window.URL.createObjectURL(blob); downloadFile(url, `Property_Card_${ulpin}.pdf`); window.URL.revokeObjectURL(url); } catch (err) { showToast(err.message, 'error'); } } function confirmDelete(recordId, khasraNo) { showConfirmModal(`Are you sure you want to delete record "${khasraNo}"?\n\nThis action cannot be undone.`, () => { deleteRecord(recordId).then(() => { showToast(`Record "${khasraNo}" deleted successfully.`, 'success'); selectedRecordId = null; selectedRecord = null; // Hide details panel const detailsPanel = document.getElementById('map-details-panel'); const detailsActions = document.getElementById('map-details-actions'); if (detailsPanel) detailsPanel.classList.add('hidden'); if (detailsActions) detailsActions.classList.add('hidden'); loadRecordsOnMap(); switchMainTab('records'); }).catch(err => { showToast(`Delete failed: ${err.message}`, 'error'); }); }); } // Initialize Add Record map (split view) function initAddRecordMap() { if (!document.getElementById('add-record-map')) return; // Expanded India bounding box with generous margins const indiaBounds = L.latLngBounds( L.latLng(4.0, 60.0), // Southwest (extended into ocean) L.latLng(40.0, 105.0) // Northeast (extended into China/Myanmar) ); addRecordMap = L.map('add-record-map', { center: [23.5, 77.5], zoom: 7, minZoom: 4, maxZoom: 18, maxBounds: indiaBounds, maxBoundsViscosity: 1.0, zoomControl: true }); // Add basemap layers for add record map const addBaseLayers = {}; addBaseLayers.osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 19 }); addBaseLayers.google = L.tileLayer('https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', { attribution: '© Google Maps', maxZoom: 20 }); addBaseLayers.esri = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: '© Esri', maxZoom: 18 }); addBaseLayers.osm.addTo(addRecordMap); let currentAddBaseLayer = 'osm'; // Handle layer switching for add record map document.querySelectorAll('input[name="add-basemap"]').forEach(radio => { radio.addEventListener('change', function() { if (addBaseLayers[this.value] && this.value !== currentAddBaseLayer) { addRecordMap.removeLayer(addBaseLayers[currentAddBaseLayer]); addBaseLayers[this.value].addTo(addRecordMap); currentAddBaseLayer = this.value; } }); }); addRecordDrawnItems = new L.FeatureGroup(); addRecordMap.addLayer(addRecordDrawnItems); // Add India mask to add record map addIndiaMask(addRecordMap); // Debug: verify PM is initialized console.log('Add Record Map PM initialized:', !!addRecordMap.pm); // Handle polygon creation on add record map addRecordMap.on('pm:create', async function(e) { console.log('pm:create fired on add-record-map', e); const layer = e.layer; addRecordDrawnItems.clearLayers(); addRecordDrawnItems.addLayer(layer); currentSketchLayer = layer; // Make polygon editable if (layer.pm) { layer.pm.enable(); } const geometry = layer.toGeoJSON().geometry; // Set geometry IMMEDIATELY const geometryInput = document.getElementById('form-geometry'); if (geometryInput) { geometryInput.value = JSON.stringify(geometry); console.log('Geometry set to:', geometryInput.value.substring(0, 50) + '...'); } await updateGeometryMetricsForAddRecord(geometry); // Auto-fill location from drawn parcel centroid const centroid = L.geoJSON(geometry).getBounds().getCenter(); lookupLocationForAddRecord(centroid.lat, centroid.lng); document.getElementById('draw-status').innerHTML = 'Polygon ready! Location auto-filled. Fill in the details below.'; document.getElementById('draw-status').className = 'bg-green-50 border-l-4 border-green-500 rounded-r-lg p-3 text-sm text-green-800'; showToast('Polygon drawn and saved. Fill in parcel details and save.', 'success'); }); // Handle edits to existing polygons addRecordMap.on('pm:edit', async function(e) { const layer = e.layer; if (!layer) return; currentSketchLayer = layer; const geometry = layer.toGeoJSON().geometry; await updateGeometryMetricsForAddRecord(geometry); }); // Handle removal addRecordMap.on('pm:remove', function() { document.getElementById('form-geometry').value = ''; clearMetricsUI(); document.getElementById('draw-status').innerHTML = 'Waiting for map drawing... Draw a parcel on the map to continue.'; document.getElementById('draw-status').className = 'bg-blue-50 border-l-4 border-blue-500 rounded-r-lg p-3 text-sm text-blue-800'; }); // Auto-fill location on map move addRecordMap.on('moveend', function() { const center = addRecordMap.getCenter(); lookupLocationForAddRecord(center.lat, center.lng); }); // Add draw button handler - start drawing polygon const startDrawBtn = document.getElementById('btn-start-draw-new'); if (startDrawBtn) { startDrawBtn.addEventListener('click', function() { // Clear any existing shapes first addRecordDrawnItems.clearLayers(); document.getElementById('form-geometry').value = ''; clearMetricsUI(); // Start drawing mode addRecordMap.pm.enableDraw('Polygon', { snappable: true, snapDistance: 20, continueDrawing: false, allowSelfIntersection: false, finishOn: 'dblclick' }); document.getElementById('draw-status').innerHTML = 'Drawing mode active. Click to add points. Double-click to finish.'; document.getElementById('draw-status').className = 'bg-yellow-50 border-l-4 border-yellow-500 rounded-r-lg p-3 text-sm text-yellow-800'; showToast('Click points on the map to draw. Double-click to finish the polygon.', 'info', 5000); }); } // Finish drawing button const finishDrawBtn = document.getElementById('btn-finish-draw-new'); if (finishDrawBtn) { finishDrawBtn.addEventListener('click', function() { addRecordMap.pm.disableDraw(); showToast('Drawing mode closed.', 'info'); }); } // Cancel drawing button const cancelDrawBtn = document.getElementById('btn-cancel-draw-new'); if (cancelDrawBtn) { cancelDrawBtn.addEventListener('click', function() { addRecordMap.pm.disableDraw(); addRecordDrawnItems.clearLayers(); currentSketchLayer = null; document.getElementById('form-geometry').value = ''; clearMetricsUI(); document.getElementById('draw-status').innerHTML = 'Waiting for map drawing... Draw a parcel on the map to continue.'; document.getElementById('draw-status').className = 'bg-blue-50 border-l-4 border-blue-500 rounded-r-lg p-3 text-sm text-blue-800'; showToast('Drawing cancelled.', 'warning'); }); } // Clear selection button const clearDrawBtn = document.getElementById('btn-clear-draw-new'); if (clearDrawBtn) { clearDrawBtn.addEventListener('click', function() { addRecordMap.pm.disableDraw(); addRecordDrawnItems.clearLayers(); currentSketchLayer = null; document.getElementById('form-geometry').value = ''; clearMetricsUI(); document.getElementById('draw-status').innerHTML = 'Waiting for map drawing... Draw a parcel on the map to continue.'; document.getElementById('draw-status').className = 'bg-blue-50 border-l-4 border-blue-500 rounded-r-lg p-3 text-sm text-blue-800'; showToast('Drawing cleared.', 'info'); }); } setTimeout(() => addRecordMap.invalidateSize(), 200); } // Lookup location for Add Record map let addRecordLookupTimer = null; function lookupLocationForAddRecord(lat, lng) { if (typeof lat !== 'number' || typeof lng !== 'number') return; // Check if manual override is enabled const manualOverride = document.getElementById('location-manual-override'); if (manualOverride && manualOverride.checked) return; if (addRecordLookupTimer) { clearTimeout(addRecordLookupTimer); } // Show "Detecting..." in the GPS banner const gpsBanner = document.getElementById('gps-detected-banner'); if (gpsBanner) { gpsBanner.classList.remove('hidden'); gpsBanner.innerHTML = `
Detecting location from GPS coordinates...
`; } addRecordLookupTimer = setTimeout(async function() { try { const locationData = await fetchLocationFromCoordinates(lat, lng); if (locationData && locationData.state && locationData.district && locationData.village) { // Store what GPS detected lastGpsDetectedLocation = { state: locationData.state, district: locationData.district, village: locationData.village }; // Update form dropdowns if not in manual override mode const manualOverrideNow = document.getElementById('location-manual-override'); if (!manualOverrideNow || !manualOverrideNow.checked) { setLocationValues(locationData.state, locationData.district, locationData.village); document.getElementById('form-location-source').textContent = `Source: ${locationData.display_name || 'Map reverse geocoding'}`; } // Update GPS banner with confirmed location if (gpsBanner) { gpsBanner.innerHTML = `

GPS Detected Location

${escapeHtml(locationData.state)}${escapeHtml(locationData.district)} › ${escapeHtml(locationData.village)}

`; } } else { lastGpsDetectedLocation = { state: '', district: '', village: '' }; if (gpsBanner) gpsBanner.classList.add('hidden'); } } catch (err) { console.error('Location lookup failed:', err); if (gpsBanner) gpsBanner.classList.add('hidden'); } }, 800); } async function updateGeometryMetricsForAddRecord(geometry) { const geometryInput = document.getElementById('form-geometry'); // Set geometry IMMEDIATELY so form can save if (geometryInput) geometryInput.value = JSON.stringify(geometry); // Now fetch area metrics try { const result = await calculateArea(geometry); // API returns: { area: { area_ha, area_acres, ... }, perimeter: {...}, centroid: {...} } const areaData = result.area || result; const perimeterData = result.perimeter || {}; const centroidData = result.centroid || {}; const areaInput = document.getElementById('form-area'); const areaAuto = document.getElementById('area-auto'); const areaEquivalents = document.getElementById('area-equivalents'); const geometryMetrics = document.getElementById('geometry-metrics'); const perimeterEl = document.getElementById('metric-perimeter'); const centroidEl = document.getElementById('metric-centroid'); if (areaInput) areaInput.value = areaData.area_ha || ''; if (areaAuto) areaAuto.textContent = `(Auto-calculated)`; if (areaEquivalents) { areaEquivalents.classList.remove('hidden'); const bigha = areaData.area_bigha_assam || (areaData.area_ha * 7.4752).toFixed(2); const lecha = areaData.area_lecha_assam || Math.round(areaData.area_ha * 747.52); areaEquivalents.innerHTML = `
${areaData.area_ha || '?'} Ha
${areaData.area_acres || '?'} Acres
${areaData.area_guntha || '?'} Guntha
${bigha} Bigha
${lecha} Lecha
`; } if (geometryMetrics) { geometryMetrics.classList.remove('hidden'); if (perimeterEl) perimeterEl.textContent = perimeterData.perimeter ? perimeterData.perimeter + ' m' : '--'; if (centroidEl) centroidEl.textContent = centroidData.lat ? centroidData.lat.toFixed(4) + ', ' + (centroidData.lng || centroidData.lon).toFixed(4) : '--'; } } catch (err) { console.error('Failed to calculate area:', err); showToast('Area calculation failed but geometry is saved. You can still save the record.', 'warning'); } } function switchSidebarTab(tabName) { // Legacy function - redirect to new main tab switching switchMainTab(tabName); } function switchMainTab(tabName) { const dashboardPanel = document.getElementById('main-tab-dashboard'); const recordsPanel = document.getElementById('main-tab-records'); const mapPanel = document.getElementById('main-tab-map'); const addRecordPanel = document.getElementById('main-tab-add-record'); const viewRecordPanel = document.getElementById('main-tab-view-record'); const profilePanel = document.getElementById('main-tab-profile'); const usersPanel = document.getElementById('main-tab-users'); const reportsPanel = document.getElementById('main-tab-reports'); const auditPanel = document.getElementById('main-tab-audit'); document.querySelectorAll('.main-tab-btn').forEach(tab => { const isActive = tab.dataset.tab === tabName; tab.classList.toggle('active', isActive); if (isActive) tab.classList.add('bg-green-50', 'text-green-700'); else tab.classList.remove('bg-green-50', 'text-green-700'); }); if (dashboardPanel) dashboardPanel.classList.toggle('hidden', tabName !== 'dashboard'); if (recordsPanel) recordsPanel.classList.toggle('hidden', tabName !== 'records'); if (mapPanel) mapPanel.classList.toggle('hidden', tabName !== 'map'); if (addRecordPanel) addRecordPanel.classList.toggle('hidden', tabName !== 'add-record'); if (viewRecordPanel) viewRecordPanel.classList.toggle('hidden', tabName !== 'view-record'); if (profilePanel) profilePanel.classList.toggle('hidden', tabName !== 'profile'); if (usersPanel) usersPanel.classList.toggle('hidden', tabName !== 'users'); if (reportsPanel) reportsPanel.classList.toggle('hidden', tabName !== 'reports'); if (auditPanel) auditPanel.classList.toggle('hidden', tabName !== 'audit'); // Show/hide Geoman toolbar based on tab const geomanToolbar = document.querySelector('.leaflet-pm-toolbar'); if (geomanToolbar) { // Only show toolbar in Add Record tab (for drawing) if (tabName === 'add-record') { geomanToolbar.style.display = 'block'; } else { geomanToolbar.style.display = 'none'; } } // Load users when switching to users tab if (tabName === 'users') { loadUsers(); } // Load feedback when switching to reports tab if (tabName === 'reports') { loadFeedback(); } // Load audit when switching to audit tab if (tabName === 'audit') { fetchAudit(100).then(entries => showAuditModal(entries)).catch(err => showToast('Failed to load audit: ' + err.message, 'error')); } // Invalidate map size when switching to map tabs if (tabName === 'map' && map) { setTimeout(() => map.invalidateSize(), 100); } if (tabName === 'add-record' && addRecordMap) { setTimeout(() => addRecordMap.invalidateSize(), 100); } if (tabName === 'view-record' && viewRecordMap) { setTimeout(() => viewRecordMap.invalidateSize(), 100); } } document.addEventListener('DOMContentLoaded', function() { console.log('map.js loaded — initializing admin dashboard handlers'); try { // Basic setup const refreshBtn = document.getElementById('btn-refresh-feedback'); if (refreshBtn) { refreshBtn.addEventListener('click', loadFeedback); } // Check if we're on the admin dashboard if (!document.getElementById('map')) return; // --- Prevent back button from exiting to login page --- // Push a state when page loads so back button stays within the app history.pushState({ page: 'admin', tab: 'records' }, '', window.location.href); // Intercept back button window.addEventListener('popstate', function(e) { if (e.state && e.state.page === 'admin') { history.pushState({ page: 'admin', tab: 'records' }, '', window.location.href); switchMainTab('records'); } }); initializeFormTabs(); initializeLocationFilters(); initMap(true); initializeDrawSettingsPanel(); initAddRecordMap(); // Initialize main tab buttons document.querySelectorAll('.main-tab-btn').forEach(tab => { tab.addEventListener('click', function() { switchMainTab(this.dataset.tab); }); }); // Start with dashboard tab active switchMainTab('dashboard'); // Back to records button const backBtn = document.getElementById('btn-back-to-records'); if (backBtn) { backBtn.addEventListener('click', function() { switchMainTab('records'); }); } // (removed redundant auditBtn listener) // (rest of handler unchanged) — reattach the original listeners // View record tab action buttons const viewEditBtn = document.getElementById('btn-view-edit'); if (viewEditBtn) { viewEditBtn.addEventListener('click', function() { if (selectedRecord) editRecord(selectedRecord._id); }); } const viewPrintBtn = document.getElementById('btn-view-print'); if (viewPrintBtn) { viewPrintBtn.addEventListener('click', function() { if (selectedRecord && selectedRecord.ulpin) printCard(selectedRecord.ulpin); else showToast('No ULPIN available.', 'error'); }); } const viewDeleteBtn = document.getElementById('btn-view-delete'); if (viewDeleteBtn) { viewDeleteBtn.addEventListener('click', function() { if (selectedRecord) confirmDelete(selectedRecord._id, selectedRecord.khasra_no || ''); else showToast('No record selected.', 'error'); }); } // Close map details button const closeMapDetails = document.getElementById('close-map-details'); if (closeMapDetails) { closeMapDetails.addEventListener('click', function() { const detailsPanel = document.getElementById('map-details-panel'); if (detailsPanel) detailsPanel.classList.add('hidden'); const detailsActions = document.getElementById('map-details-actions'); if (detailsActions) detailsActions.classList.add('hidden'); selectedRecordId = null; selectedRecord = null; }); } // Map details print/delete buttons const printBtn = document.getElementById('btn-map-print'); if (printBtn) { printBtn.addEventListener('click', function() { if (selectedRecord && selectedRecord.ulpin) printCard(selectedRecord.ulpin); else showToast('No ULPIN available.', 'error'); }); } const mapDeleteBtn = document.getElementById('btn-map-delete'); if (mapDeleteBtn) { mapDeleteBtn.addEventListener('click', function() { if (selectedRecord) confirmDelete(selectedRecord._id, selectedRecord.khasra_no || ''); else showToast('No record selected.', 'error'); }); } const mapEditBtn = document.getElementById('btn-map-edit'); if (mapEditBtn) { mapEditBtn.addEventListener('click', function() { if (selectedRecord) editRecord(selectedRecord._id); }); } document.querySelectorAll('.records-view-tab').forEach(tab => { tab.addEventListener('click', function() { switchRecordsView(this.dataset.view); }); }); const addRecordBtn = document.getElementById('btn-add-record-new'); if (addRecordBtn) { addRecordBtn.addEventListener('click', function() { resetForm(); switchMainTab('add-record'); switchFormTab('location'); }); } const exportBtn = document.getElementById('btn-export-excel'); if (exportBtn) { exportBtn.addEventListener('click', function() { const url = getVillageExcelUrl(); downloadFile(url, 'Village_Ledger.xlsx'); showToast('Generating Excel ledger...', 'info'); }); } const searchInput = document.getElementById('search-input'); if (searchInput) { let searchDebounceTimer = null; searchInput.addEventListener('input', function() { if (searchDebounceTimer) clearTimeout(searchDebounceTimer); searchDebounceTimer = setTimeout(() => { applyAdminFilters(false); }, 300); }); searchInput.addEventListener('keypress', function(e) { if (e.key === 'Enter') { if (searchDebounceTimer) clearTimeout(searchDebounceTimer); performAdminSearch(); } }); } const landUseFilter = document.getElementById('land-use-filter'); if (landUseFilter) { landUseFilter.addEventListener('change', function() { applyAdminFilters(false); }); } const districtFilter = document.getElementById('district-filter'); if (districtFilter) { districtFilter.addEventListener('change', function() { applyAdminFilters(false); }); } const stateFilter = document.getElementById('state-filter'); if (stateFilter) { stateFilter.addEventListener('change', function() { applyAdminFilters(false); }); } const clearFiltersBtn = document.getElementById('btn-clear-filters'); if (clearFiltersBtn) { clearFiltersBtn.addEventListener('click', function() { if (searchInput) searchInput.value = ''; if (landUseFilter) landUseFilter.value = ''; if (districtFilter) districtFilter.value = ''; if (stateFilter) stateFilter.value = ''; applyAdminFilters(true); }); } const recordForm = document.getElementById('record-form'); if (recordForm) { recordForm.addEventListener('submit', async function(e) { e.preventDefault(); await handleFormSubmit(); }); } const cancelBtn = document.getElementById('form-cancel-btn'); if (cancelBtn) { cancelBtn.addEventListener('click', function() { resetForm(); switchMainTab('records'); }); } const logoutBtn = document.getElementById('btn-logout'); if (logoutBtn) { logoutBtn.addEventListener('click', async function() { try { await logout(); } catch (_err) {} window.location.href = '/login'; }); } // --- Profile Tab Event Handlers --- let profileLoaded = false; const profileTabBtn = document.querySelector('.main-tab-btn[data-tab="profile"]'); if (profileTabBtn) { profileTabBtn.addEventListener('click', function() { if (!profileLoaded) { loadProfile(); loadUsers(); getSessionUsername(); profileLoaded = true; } }); } const editProfileBtn = document.getElementById('btn-edit-profile'); const editProfileBtnInline = document.getElementById('btn-edit-profile-inline'); function showProfileEditForm() { document.getElementById('edit-profile-form').classList.remove('hidden'); document.getElementById('profile-view').classList.add('hidden'); if (editProfileBtn) editProfileBtn.classList.add('hidden'); if (currentProfile) { document.getElementById('edit-full-name').value = currentProfile.full_name || ''; document.getElementById('edit-email').value = currentProfile.email || ''; document.getElementById('edit-phone').value = currentProfile.phone || ''; document.getElementById('edit-designation').value = currentProfile.designation || ''; document.getElementById('edit-department').value = currentProfile.department || ''; document.getElementById('edit-office').value = currentProfile.office_location || ''; } } if (editProfileBtn) editProfileBtn.addEventListener('click', showProfileEditForm); if (editProfileBtnInline) editProfileBtnInline.addEventListener('click', showProfileEditForm); const cancelEditProfileBtn = document.getElementById('btn-cancel-edit-profile'); if (cancelEditProfileBtn) { cancelEditProfileBtn.addEventListener('click', function() { document.getElementById('edit-profile-form').classList.add('hidden'); document.getElementById('profile-view').classList.remove('hidden'); if (editProfileBtn) editProfileBtn.classList.remove('hidden'); }); } const profileForm = document.getElementById('profile-form'); if (profileForm) { profileForm.addEventListener('submit', async function(e) { e.preventDefault(); const updateData = { full_name: document.getElementById('edit-full-name').value.trim(), email: document.getElementById('edit-email').value.trim(), phone: document.getElementById('edit-phone').value.trim(), designation: document.getElementById('edit-designation').value.trim(), department: document.getElementById('edit-department').value.trim(), office_location: document.getElementById('edit-office').value.trim() }; const currentPass = document.getElementById('edit-current-password').value; const newPass = document.getElementById('edit-new-password').value; if (currentPass && newPass) { updateData.current_password = currentPass; updateData.new_password = newPass; } try { const res = await fetch(`${API_BASE}/api/profile`, { credentials: 'include', method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updateData) }); const data = await res.json(); if (!res.ok) throw new Error(data.error); showToast('Profile updated successfully.', 'success'); currentProfile = data.profile; displayProfile(currentProfile); document.getElementById('edit-profile-form').classList.add('hidden'); document.getElementById('profile-view').classList.remove('hidden'); if (editProfileBtn) editProfileBtn.classList.remove('hidden'); } catch (err) { showToast(`Failed to update profile: ${err.message}`, 'error'); } }); } const createUserBtn = document.getElementById('btn-create-user'); if (createUserBtn) createUserBtn.addEventListener('click', function() { document.getElementById('create-user-form').classList.remove('hidden'); }); const cancelCreateUserBtn = document.getElementById('btn-cancel-create-user'); const cancelCreateUserBtn2 = document.getElementById('btn-cancel-create-user-2'); function hideCreateUserForm() { document.getElementById('create-user-form').classList.add('hidden'); document.getElementById('user-create-form').reset(); } if (cancelCreateUserBtn) cancelCreateUserBtn.addEventListener('click', hideCreateUserForm); if (cancelCreateUserBtn2) cancelCreateUserBtn2.addEventListener('click', hideCreateUserForm); const userCreateForm = document.getElementById('user-create-form'); if (userCreateForm) { userCreateForm.addEventListener('submit', async function(e) { e.preventDefault(); const userData = { username: document.getElementById('create-username').value.trim(), password: document.getElementById('create-password').value, full_name: document.getElementById('create-full-name').value.trim(), email: document.getElementById('create-email').value.trim(), phone: document.getElementById('create-phone').value.trim(), designation: document.getElementById('create-designation').value.trim(), department: document.getElementById('create-department').value.trim(), role: document.getElementById('create-role').value, office_location: document.getElementById('create-office').value.trim(), is_active: document.getElementById('create-active').checked }; try { const res = await fetch(`${API_BASE}/api/users`, { credentials: 'include', method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }); const data = await res.json(); if (!res.ok) throw new Error(data.error); showToast(`User "${userData.username}" created successfully.`, 'success'); document.getElementById('create-user-form').classList.add('hidden'); userCreateForm.reset(); loadUsers(); } catch (err) { showToast(`Failed to create user: ${err.message}`, 'error'); } }); } } catch (e) { console.error('map.js initialization error:', e); try { showToast('Client script error — check browser console', 'error'); } catch (_) {} } }); async function performAdminSearch() { applyAdminFilters(true); if (filteredRecordsCache.length === 1) { const record = filteredRecordsCache[0]; // If on map tab, fly to record const mapPanel = document.getElementById('main-tab-map'); if (mapPanel && !mapPanel.classList.contains('hidden')) { flyToRecord(record); } } } function fileToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result); reader.onerror = error => reject(error); }); } async function handleFormSubmit() { const recordId = document.getElementById('form-record-id').value; const geometryStr = document.getElementById('form-geometry').value; const locationValues = { state: document.getElementById('form-state-manual').value || document.getElementById('form-state').value, district: document.getElementById('form-district-manual').value || document.getElementById('form-district').value, village: document.getElementById('form-village-manual').value || document.getElementById('form-village').value }; if (!locationValues.state || !locationValues.district || !locationValues.village) { showToast('Location (State, District, Village) is required.', 'error'); switchFormTab('location'); return; } // GPS Mismatch Warning: Only check if we have GPS data AND manual override is active const manualOverrideEl = document.getElementById('location-manual-override'); const isManualOverride = manualOverrideEl && manualOverrideEl.checked; const hasGpsData = lastGpsDetectedLocation.state && lastGpsDetectedLocation.district; if (isManualOverride && hasGpsData) { const stateMatch = locationValues.state.trim().toLowerCase() === lastGpsDetectedLocation.state.trim().toLowerCase(); const districtMatch = locationValues.district.trim().toLowerCase() === lastGpsDetectedLocation.district.trim().toLowerCase(); if (!stateMatch || !districtMatch) { // Show a blocking mismatch warning modal const proceed = await new Promise(resolve => { const overlay = document.createElement('div'); overlay.className = 'fixed inset-0 bg-black bg-opacity-60 z-[9999] flex items-center justify-center p-4'; overlay.style.backdropFilter = 'blur(3px)'; overlay.innerHTML = `

Location Mismatch Warning

The location you entered does not match the GPS coordinates of the parcel you drew on the map.

📡 GPS Detected

${escapeHtml(lastGpsDetectedLocation.state)}

${escapeHtml(lastGpsDetectedLocation.district)}

✏️ You Entered

${escapeHtml(locationValues.state)}

${escapeHtml(locationValues.district)}

Are you sure you want to continue with the manually entered location? This may cause incorrect revenue records.

`; document.body.appendChild(overlay); overlay.querySelector('#mismatch-cancel').addEventListener('click', () => { // Auto-fill with GPS-detected location const manualOvEl = document.getElementById('location-manual-override'); if (manualOvEl) manualOvEl.checked = false; toggleManualLocationOverride(false); setLocationValues(lastGpsDetectedLocation.state, lastGpsDetectedLocation.district, lastGpsDetectedLocation.village); document.body.removeChild(overlay); resolve(false); }); overlay.querySelector('#mismatch-force').addEventListener('click', () => { document.body.removeChild(overlay); resolve(true); }); }); if (!proceed) return; // User chose to fix location — abort save } } // Validate parcel fields const khasra = document.getElementById('form-khasra').value.trim(); const khata = document.getElementById('form-khata').value.trim(); const landUse = document.getElementById('form-land-use').value; if (!khasra) { showToast('Khasra No. is required.', 'error'); switchFormTab('parcel'); return; } if (!khata) { showToast('Khata No. is required.', 'error'); switchFormTab('parcel'); return; } if (!landUse) { showToast('Land Use is required.', 'error'); switchFormTab('parcel'); return; } // Validate geometry if (!geometryStr) { showToast('Please draw a polygon on the map first.', 'error'); switchFormTab('parcel'); return; } // Validate owner (only for new records) if (!recordId) { const ownerName = document.getElementById('form-owner-name').value.trim(); if (!ownerName) { showToast('Owner Name is required.', 'error'); switchFormTab('owner'); return; } } ensureLocationInCatalog(locationValues.state, locationValues.district, locationValues.village); let geometry; try { geometry = JSON.parse(geometryStr); } catch (_err) { showToast('Invalid geometry data.', 'error'); return; } const submitBtn = document.getElementById('form-submit-btn'); const originalText = submitBtn.textContent; submitBtn.disabled = true; submitBtn.innerHTML = ' Saving...'; try { let ownerDocB64 = undefined; const ownerDocFile = document.getElementById('form-owner-doc')?.files[0]; if (ownerDocFile) ownerDocB64 = await fileToBase64(ownerDocFile); let mutationDocB64 = undefined; const mutationDocFile = document.getElementById('form-mutation-doc')?.files[0]; if (mutationDocFile) mutationDocB64 = await fileToBase64(mutationDocFile); if (recordId) { const updateData = { khasra_no: document.getElementById('form-khasra').value, khata_no: document.getElementById('form-khata').value, land_use: document.getElementById('form-land-use').value, circle_rate_inr: parseFloat(document.getElementById('form-circle-rate').value) || 0, share_pct: parseFloat(document.getElementById('form-share').value) || 100, aadhaar_mask: document.getElementById('form-aadhaar').value, geometry: geometry, location: locationValues, owner_proof_doc_b64: ownerDocB64 }; const newOwner = document.getElementById('form-new-owner').value.trim(); if (newOwner) { updateData.mutation = true; updateData.new_owner_name = newOwner; updateData.new_share_pct = parseFloat(document.getElementById('form-new-share').value) || 100; updateData.mutation_type = document.getElementById('form-mutation-type').value; updateData.mutation_date = document.getElementById('form-mutation-date').value || new Date().toISOString().split('T')[0]; updateData.mutation_proof_doc_b64 = mutationDocB64; } await updateRecord(recordId, updateData); showToast('Record updated successfully.', 'success'); } else { const recordData = { khasra_no: document.getElementById('form-khasra').value, khata_no: document.getElementById('form-khata').value, ulpin: document.getElementById('form-ulpin').value || undefined, land_use: document.getElementById('form-land-use').value, circle_rate_inr: parseFloat(document.getElementById('form-circle-rate').value) || 0, owner_name: document.getElementById('form-owner-name').value, share_pct: parseFloat(document.getElementById('form-share').value) || 100, aadhaar_mask: document.getElementById('form-aadhaar').value || 'XXXX-XXXX-XXXX', geometry: geometry, location: locationValues, owner_proof_doc_b64: ownerDocB64 }; const result = await createRecord(recordData); if (result.area_details && result.area_details.area) { showToast(`Record created. Area: ${result.area_details.area.area_ha} Ha (${result.area_details.area.area_acres} Acres)`, 'success', 5000); } else { showToast('Record created successfully.', 'success'); } } resetForm(); switchMainTab('records'); loadRecordsOnMap(); } catch (err) { showToast(`Error: ${err.message}`, 'error'); } finally { submitBtn.disabled = false; submitBtn.textContent = originalText; } } function resetForm() { const form = document.getElementById('record-form'); if (form) { form.reset(); } const { manualOverrideEl, stateManualEl, districtManualEl, villageManualEl } = getFormElements(); if (manualOverrideEl) { manualOverrideEl.checked = false; } if (stateManualEl) stateManualEl.value = ''; if (districtManualEl) districtManualEl.value = ''; if (villageManualEl) villageManualEl.value = ''; toggleManualLocationOverride(false); document.getElementById('form-record-id').value = ''; document.getElementById('form-title').textContent = 'Add New Land Record'; setMutationMode(false); switchFormTab('location'); refreshStateOptions(''); refreshDistrictOptions(''); refreshVillageOptions(''); lastAutoLocation = { state: '', district: '', village: '' }; lastGeocodeKey = ''; updateAutofillStatus('Move map or draw parcel to auto-detect location.', false); document.getElementById('form-submit-btn').textContent = 'Save Record'; const drawInstruction = document.getElementById('draw-instruction'); if (drawInstruction) { drawInstruction.style.display = ''; } // Clear both maps clearGeometrySelection(true); if (addRecordDrawnItems) { addRecordDrawnItems.clearLayers(); } if (addRecordMap) { addRecordMap.pm.disableDraw(); } currentSketchLayer = null; document.getElementById('form-geometry').value = ''; clearMetricsUI(); const drawStatus = document.getElementById('draw-status'); if (drawStatus) { drawStatus.innerHTML = 'Waiting for map drawing... Draw a parcel on the map to continue.'; drawStatus.className = 'bg-blue-50 border-l-4 border-blue-500 rounded-r-lg p-3 text-sm text-blue-800'; } } // --- Profile & User Management --- let currentProfile = null; let allUsers = []; async function loadProfile() { try { currentProfile = await fetch(`${API_BASE}/api/profile`, { credentials: 'include' }).then(r => r.json()); displayProfile(currentProfile); } catch (err) { console.error('Failed to load profile:', err); } } function displayProfile(profile) { // Original profile card document.getElementById('profile-display-name').textContent = profile.full_name || 'N/A'; document.getElementById('profile-display-role').textContent = profile.role || 'N/A'; document.getElementById('profile-username').textContent = profile.username || 'N/A'; document.getElementById('profile-email').textContent = profile.email || 'N/A'; document.getElementById('profile-phone').textContent = profile.phone || 'N/A'; document.getElementById('profile-designation').textContent = profile.designation || 'N/A'; document.getElementById('profile-department').textContent = profile.department || 'N/A'; document.getElementById('profile-office').textContent = profile.office_location || 'N/A'; document.getElementById('profile-last-login').textContent = profile.last_login ? new Date(profile.last_login).toLocaleString() : 'Never'; const badge = document.getElementById('profile-status-badge'); if (badge) { badge.textContent = profile.is_active ? 'Active' : 'Inactive'; badge.className = `inline-block mt-2 px-3 py-1 text-xs font-semibold rounded-full ${profile.is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`; } // New profile view fields document.getElementById('profile-view-username').textContent = profile.username || 'N/A'; document.getElementById('profile-view-name').textContent = profile.full_name || 'N/A'; document.getElementById('profile-view-email').textContent = profile.email || 'N/A'; document.getElementById('profile-view-phone').textContent = profile.phone || 'N/A'; document.getElementById('profile-view-designation').textContent = profile.designation || 'N/A'; document.getElementById('profile-view-department').textContent = profile.department || 'N/A'; document.getElementById('profile-view-office').textContent = profile.office_location || 'N/A'; document.getElementById('profile-view-last-login').textContent = profile.last_login ? new Date(profile.last_login).toLocaleString() : 'Never'; } async function loadFeedback() { try { const tbody = document.getElementById('feedback-table-body'); if (tbody) tbody.innerHTML = 'Loading...'; const res = await fetch(`${API_BASE}/api/feedback`, { credentials: 'include' }); const feedbackList = await res.json(); if (tbody) { tbody.innerHTML = ''; if (feedbackList.length === 0) { tbody.innerHTML = 'No feedback forms right now.'; return; } // Sort by timestamp descending feedbackList.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); feedbackList.forEach(fb => { const tr = document.createElement('tr'); tr.className = 'hover:bg-gray-50 transition cursor-pointer'; const d = new Date(fb.timestamp); const dateStr = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); tr.innerHTML = ` ${dateStr} ${escapeHtml(fb.email)} ${escapeHtml(fb.type)} ${escapeHtml(fb.message)} `; tbody.appendChild(tr); }); } } catch (err) { console.error('Failed to load feedback:', err); } } function deleteFeedback(feedbackId) { showConfirmModal("Are you sure you want to delete this feedback report?\n\nThis action is permanent and cannot be recovered.", async () => { try { const res = await fetch(`${API_BASE}/api/feedback/${feedbackId}`, { credentials: 'include', method: 'DELETE' }); const data = await res.json(); if (!res.ok) throw new Error(data.error); showToast("Feedback deleted successfully.", "success"); loadFeedback(); // Refresh the table } catch (err) { showToast(`Failed to delete feedback: ${err.message}`, "error"); } }); } async function loadUsers() { try { if (!sessionUsername) { await getSessionUsername(); } allUsers = await fetch(`${API_BASE}/api/users`, { credentials: 'include' }).then(r => r.json()); renderUsersTable(); } catch (err) { console.error('Failed to load users:', err); } } function renderUsersTable() { const tbody = document.getElementById('users-table-body'); const noUsers = document.getElementById('no-users'); if (!tbody) return; if (!allUsers.length) { tbody.innerHTML = ''; noUsers.classList.remove('hidden'); return; } noUsers.classList.add('hidden'); const roleColors = { SuperAdmin: 'bg-purple-100 text-purple-800', Admin: 'bg-orange-100 text-orange-800', Officer: 'bg-blue-100 text-blue-800', Viewer: 'bg-green-100 text-green-800' }; const currentUsername = sessionUsername; // Update stats document.getElementById('stat-total-users').textContent = allUsers.length; document.getElementById('stat-active-users').textContent = allUsers.filter(u => u.is_active).length; document.getElementById('stat-admin-users').textContent = allUsers.filter(u => ['Admin', 'SuperAdmin'].includes(u.role)).length; document.getElementById('stat-viewer-users').textContent = allUsers.filter(u => u.role === 'Viewer').length; tbody.innerHTML = allUsers.map(user => { const isCurrentUser = user.username === currentUsername; return `
${(user.full_name || user.username || 'U')[0].toUpperCase()}
${escapeHtml(user.full_name || 'N/A')}
@${escapeHtml(user.username)}
${escapeHtml(user.role)} ${user.email ? `
${escapeHtml(user.email)}
` : '
--
'} ${user.phone ? `
${escapeHtml(user.phone)}
` : ''} ${user.department ? `
${escapeHtml(user.department)}
` : '
--
'} ${user.designation ? `
${escapeHtml(user.designation)}
` : ''} ${user.is_active ? 'Active' : 'Inactive'}
${!isCurrentUser ? `` : 'You'}
`; }).join(''); } let sessionUsername = ''; async function getSessionUsername() { try { const info = await getSessionInfo(); sessionUsername = info.username || ''; } catch (err) { sessionUsername = ''; } } async function editUser(userId) { const user = allUsers.find(u => u.user_id === userId); if (!user) return; // Populate the edit modal document.getElementById('edit-user-id').value = user.user_id; document.getElementById('edit-username').value = user.username; document.getElementById('edit-role').value = user.role; document.getElementById('edit-fullname').value = user.full_name || ''; document.getElementById('edit-email').value = user.email || ''; document.getElementById('edit-phone-user').value = user.phone || ''; document.getElementById('edit-designation-user').value = user.designation || ''; document.getElementById('edit-department-user').value = user.department || ''; document.getElementById('edit-office-user').value = user.office_location || ''; document.getElementById('edit-is-active').checked = user.is_active !== false; document.getElementById('edit-new-password').value = ''; // Show the modal document.getElementById('edit-user-modal').classList.remove('hidden'); } // Close edit user modal function closeEditUserModal() { document.getElementById('edit-user-modal').classList.add('hidden'); document.getElementById('user-edit-form').reset(); } // Edit user modal event listeners document.addEventListener('DOMContentLoaded', function() { const closeBtn = document.getElementById('btn-close-edit-user'); const cancelBtn = document.getElementById('btn-cancel-edit-user'); if (closeBtn) closeBtn.addEventListener('click', closeEditUserModal); if (cancelBtn) cancelBtn.addEventListener('click', closeEditUserModal); // Close on backdrop click const modal = document.getElementById('edit-user-modal'); if (modal) { modal.addEventListener('click', function(e) { if (e.target === modal) { closeEditUserModal(); } }); } // Submit edit user form const editUserForm = document.getElementById('user-edit-form'); if (editUserForm) { editUserForm.addEventListener('submit', async function(e) { e.preventDefault(); const userId = document.getElementById('edit-user-id').value; const updateData = { full_name: document.getElementById('edit-fullname').value.trim(), email: document.getElementById('edit-email').value.trim(), phone: document.getElementById('edit-phone-user').value.trim(), designation: document.getElementById('edit-designation-user').value.trim(), department: document.getElementById('edit-department-user').value.trim(), office_location: document.getElementById('edit-office-user').value.trim(), role: document.getElementById('edit-role').value, is_active: document.getElementById('edit-is-active').checked }; const newPassword = document.getElementById('edit-new-password').value; if (newPassword) { updateData.new_password = newPassword; } try { const res = await fetch(`${API_BASE}/api/users/${userId}`, { credentials: 'include', method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updateData) }); const data = await res.json(); if (!res.ok) throw new Error(data.error); showToast('User updated successfully.', 'success'); closeEditUserModal(); loadUsers(); } catch (err) { showToast(`Failed to update user: ${err.message}`, 'error'); } }); } }); function deleteUser(userId, username) { showConfirmModal(`Are you sure you want to delete user "${username}"?`, async () => { try { await fetch(`${API_BASE}/api/users/${userId}`, { credentials: 'include', method: 'DELETE' }).then(async r => { const data = await r.json(); if (!r.ok) throw new Error(data.error); return data; }); showToast(`User "${username}" deleted.`, 'success'); loadUsers(); } catch (err) { showToast(`Failed to delete user: ${err.message}`, 'error'); } }); } ================================================ FILE: static/js/modules/admin.js ================================================ /** * admin.js - Admin UI, User Management, and Audit Logs logic */ const roleColors = { 'superadmin': 'bg-red-100 text-red-800', 'admin': 'bg-purple-100 text-purple-800', 'officer': 'bg-blue-100 text-blue-800', 'viewer': 'bg-gray-100 text-gray-800' }; function showAuditModal(entries) { const container = document.getElementById('audit-entries'); if (!container) return; if (!Array.isArray(entries)) entries = []; container.__auditEntries = entries.slice(); container.__auditPage = 1; container.__auditPageSize = 10; function getActionBadge(action) { const a = (action || '').toLowerCase(); if (a.includes('delete')) return 'Delete'; if (a.includes('create') || a.includes('add')) return 'Create'; if (a.includes('update') || a.includes('edit')) return 'Update'; if (a.includes('restore')) return 'Restore'; return `${escapeHtml(action)}`; } function renderAuditList(page = 1, pageSize = 10, filter = {}) { const all = container.__auditEntries || []; let filtered = all.filter(e => { if (filter.q) { const q = filter.q.toLowerCase(); const hay = [e.action, e.performed_by, e.user, e.record_id, JSON.stringify(e.details || {})].join(' ').toLowerCase(); if (!hay.includes(q)) return false; } if (filter.action && filter.action !== 'all') { if (!String(e.action || '').toLowerCase().includes(filter.action)) return false; } if (filter.user && filter.user !== 'all') { if (!String((e.performed_by||e.user||'')).toLowerCase().includes(filter.user)) return false; } return true; }); const total = filtered.length; const pages = Math.max(1, Math.ceil(total / pageSize)); if (page > pages) page = pages; container.__auditPage = page; container.__auditPageSize = pageSize; const start = (page - 1) * pageSize; const pageItems = filtered.slice(start, start + pageSize); container.innerHTML = `
${total} results
Timestamp Action User Record ID Details
`; const users = Array.from(new Set(all.map(a => (a.performed_by||a.user||'').toLowerCase()).filter(Boolean))).sort(); const userFilter = document.getElementById('audit-user-filter'); users.forEach(u => { const opt = document.createElement('option'); opt.value = u; opt.textContent = u; if (filter.user === u) opt.selected = true; userFilter.appendChild(opt); }); const listEl = document.getElementById('audit-list'); if (pageItems.length === 0) { listEl.innerHTML = 'No logs found.'; } else { pageItems.forEach(e => { const ts = e.timestamp || e.time || e.created_at || ''; const action = e.action || 'unknown'; const by = e.performed_by || e.user || 'system'; const rid = e.record_id || '-'; let detailsStr = ''; if (e.details) { if (typeof e.details === 'string') detailsStr = e.details; else { const parts = []; if (e.details.khasra_no) parts.push(`Khasra: ${e.details.khasra_no}`); if (e.details.ulpin) parts.push(`ULPIN: ${e.details.ulpin}`); if (e.details.changes) parts.push(`${Object.keys(e.details.changes).length} field(s) changed`); detailsStr = parts.length > 0 ? parts.join(' | ') : JSON.stringify(e.details).slice(0, 100); } } const tr = document.createElement('tr'); tr.className = 'hover:bg-gray-50 transition-colors'; tr.innerHTML = `${new Date(ts).toLocaleString()}${getActionBadge(action)}${escapeHtml(by)}${escapeHtml(rid)}
${escapeHtml(detailsStr || 'No details')}
`; listEl.appendChild(tr); }); } const pager = document.getElementById('audit-pager'); if (pager) { pager.innerHTML = `
Page ${page} of ${pages}
`; } const getFilters = () => ({ q: document.getElementById('audit-search').value, action: document.getElementById('audit-action-filter').value, user: document.getElementById('audit-user-filter').value }); // Event Listeners (ensure we don't stack them) const refresh = () => renderAuditList(1, container.__auditPageSize, getFilters()); document.getElementById('audit-search').oninput = debounce(refresh, 300); document.getElementById('audit-action-filter').onchange = refresh; document.getElementById('audit-user-filter').onchange = refresh; document.getElementById('audit-page-size').onchange = (e) => { renderAuditList(1, parseInt(e.target.value, 10), getFilters()); }; document.getElementById('audit-prev').onclick = () => { if (container.__auditPage > 1) renderAuditList(container.__auditPage - 1, container.__auditPageSize, getFilters()); }; document.getElementById('audit-next').onclick = () => { if (container.__auditPage < pages) renderAuditList(container.__auditPage + 1, container.__auditPageSize, getFilters()); }; document.getElementById('audit-download').onclick = () => { const dataToExport = filtered.length > 0 ? filtered : all; const blob = new Blob([JSON.stringify(dataToExport, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `audit_logs_${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); showToast('Audit logs exported.', 'success'); }; } renderAuditList(1, 10, {}); } function showAdminDetails(record) { selectedRecordId = record._id; selectedRecord = record; const detailsPanel = document.getElementById('map-details-panel'); const detailsActions = document.getElementById('map-details-actions'); const content = document.getElementById('map-details-content'); if (detailsPanel) detailsPanel.classList.remove('hidden'); if (detailsActions) detailsActions.classList.remove('hidden'); if (!content) return; const loc = record.location || {}; const attrs = record.attributes || {}; const owner = record.owner || {}; const mutations = record.mutation_history || []; content.innerHTML = `

${record.khasra_no || 'N/A'}

${record.deleted ? `Deleted` : ''}
${attrs.land_use || 'N/A'}
Role${(record.role || 'Officer').toUpperCase()} ULPIN${record.ulpin || 'N/A'} Area${attrs.area_ha || 'N/A'} Ha Village${loc.village || 'N/A'} District${loc.district || 'N/A'}

OWNER

Name${owner.name || 'N/A'} Share${owner.share_pct || 'N/A'}%
`; document.getElementById('btn-map-edit-init').addEventListener('click', () => editRecord(record._id)); if (detailsActions) { if (record.deleted) { detailsActions.innerHTML = ``; document.getElementById('btn-map-restore').addEventListener('click', () => { showConfirmModal(`Restore record?`, async () => { await restoreRecord(record._id); loadRecordsOnMap(); detailsPanel.classList.add('hidden'); }); }); document.getElementById('btn-map-hard-delete').addEventListener('click', () => { showConfirmModal(`Permanently delete?`, async () => { await deleteRecord(record._id); loadRecordsOnMap(); detailsPanel.classList.add('hidden'); }); }); } else { detailsActions.innerHTML = ``; document.getElementById('btn-map-print').addEventListener('click', () => printCard(record.ulpin)); document.getElementById('btn-map-delete').addEventListener('click', () => confirmDelete(record._id, record.khasra_no)); } } } async function capturePolygonMapForPdf(geometry) { return new Promise((resolve) => { const W = 800, H = 450; const wrap = document.createElement('div'); wrap.style.cssText = `position:fixed;left:0;top:0;width:${W}px;height:${H}px;z-index:99999;opacity:0.01;pointer-events:none;`; document.body.appendChild(wrap); const pdfMap = L.map(wrap, { zoomControl: false, attributionControl: false }); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(pdfMap); if (geometry && geometry.coordinates && geometry.coordinates[0]) { const poly = L.polygon(geometry.coordinates[0].map(c => [c[1], c[0]]), { color: '#dc2626', weight: 3, fillColor: '#fca5a5', fillOpacity: 0.35 }).addTo(pdfMap); pdfMap.fitBounds(poly.getBounds(), { padding: [55, 55] }); } else pdfMap.setView([23.5, 77.5], 5); setTimeout(async () => { try { wrap.style.opacity = '1'; resolve(await window.htmlToImage.toPng(wrap, { pixelRatio: 1.5, width: W, height: H })); } catch (e) { resolve(null); } finally { pdfMap.remove(); document.body.removeChild(wrap); } }, 2500); }); } async function printCard(ulpin) { if (!ulpin) return showToast('No ULPIN available.', 'error'); showToast('Generating PDF — capturing map...', 'info'); const record = selectedRecord || (allRecordsCache || []).find(r => r.ulpin === ulpin); const mapImageBase64 = record && record.geometry ? await capturePolygonMapForPdf(record.geometry) : null; try { const res = await fetch(getPropertyCardUrl(ulpin), { method: 'POST', headers: { 'Content-Type': 'application/json', ...FETCH_OPTS.headers }, body: JSON.stringify({ map_image: mapImageBase64 }) }); const blob = await res.blob(); const url = window.URL.createObjectURL(blob); downloadFile(url, `Property_Card_${ulpin}.pdf`); window.URL.revokeObjectURL(url); } catch (err) { showToast(err.message, 'error'); } } function confirmDelete(recordId, khasraNo) { showConfirmModal(`Delete record "${khasraNo}"?`, () => { deleteRecord(recordId).then(() => { showToast(`Deleted.`, 'success'); loadRecordsOnMap(); switchMainTab('records'); document.getElementById('map-details-panel').classList.add('hidden'); }).catch(err => showToast(`Delete failed: ${err.message}`, 'error')); }); } async function loadProfile() { try { currentProfile = await fetch(`${API_BASE}/api/profile`, { credentials: 'include' }).then(r => r.json()); displayProfile(currentProfile); } catch (err) { console.error('Failed to load profile:', err); } } function displayProfile(profile) { // Original profile card const profileName = document.getElementById('profile-display-name'); const profileRole = document.getElementById('profile-display-role'); const profileUser = document.getElementById('profile-username'); const profileEmail = document.getElementById('profile-email'); const profilePhone = document.getElementById('profile-phone'); const profileDesig = document.getElementById('profile-designation'); const profileDept = document.getElementById('profile-department'); const profileOffice = document.getElementById('profile-office'); const profileLastLogin = document.getElementById('profile-last-login'); if (profileName) profileName.textContent = profile.full_name || 'N/A'; if (profileRole) profileRole.textContent = profile.role || 'N/A'; if (profileUser) profileUser.textContent = profile.username || 'N/A'; if (profileEmail) profileEmail.textContent = profile.email || 'N/A'; if (profilePhone) profilePhone.textContent = profile.phone || 'N/A'; if (profileDesig) profileDesig.textContent = profile.designation || 'N/A'; if (profileDept) profileDept.textContent = profile.department || 'N/A'; if (profileOffice) profileOffice.textContent = profile.office_location || 'N/A'; if (profileLastLogin) profileLastLogin.textContent = profile.last_login ? new Date(profile.last_login).toLocaleString() : 'Never'; const badge = document.getElementById('profile-status-badge'); if (badge) { badge.textContent = profile.is_active ? 'Active' : 'Inactive'; badge.className = `inline-block mt-2 px-3 py-1 text-xs font-semibold rounded-full ${profile.is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`; } // Update Top Navbar Badge const navRoleBadge = document.querySelector('nav .bg-white\\/30') || document.querySelector('.nav-role-badge'); if (navRoleBadge) { const displayRole = (profile.role || 'Admin').toUpperCase(); navRoleBadge.textContent = displayRole; if (displayRole === 'SUPERADMIN') { navRoleBadge.className = 'text-[10px] sm:text-xs bg-red-500 text-white px-2 py-0.5 rounded-full font-bold animate-pulse'; } else { navRoleBadge.className = 'text-[10px] sm:text-xs bg-white/30 text-white px-2 py-0.5 rounded-full font-semibold'; } } // Role-based visibility for sidebar tabs const role = (profile.role || '').toLowerCase(); const isFullAdmin = (role === 'admin' || role === 'superadmin'); isAdmin = isFullAdmin; // Update global state const usersTabBtn = document.getElementById('btn-users'); const auditTabBtn = document.getElementById('btn-audit'); if (usersTabBtn) { usersTabBtn.classList.toggle('hidden', !isFullAdmin); } if (auditTabBtn) { auditTabBtn.classList.toggle('hidden', !isFullAdmin); } // New profile view fields const viewUser = document.getElementById('profile-view-username'); const viewName = document.getElementById('profile-view-name'); const viewEmail = document.getElementById('profile-view-email'); const viewPhone = document.getElementById('profile-view-phone'); const viewDesig = document.getElementById('profile-view-designation'); const viewDept = document.getElementById('profile-view-department'); const viewOffice = document.getElementById('profile-view-office'); const viewLastLogin = document.getElementById('profile-view-last-login'); if (viewUser) viewUser.textContent = profile.username || 'N/A'; if (viewName) viewName.textContent = profile.full_name || 'N/A'; if (viewEmail) viewEmail.textContent = profile.email || 'N/A'; if (viewPhone) viewPhone.textContent = profile.phone || 'N/A'; if (viewDesig) viewDesig.textContent = profile.designation || 'N/A'; if (viewDept) viewDept.textContent = profile.department || 'N/A'; if (viewOffice) viewOffice.textContent = profile.office_location || 'N/A'; if (viewLastLogin) viewLastLogin.textContent = profile.last_login ? new Date(profile.last_login).toLocaleString() : 'Never'; // Toggle Recovery Containers based on status const createRec = document.getElementById('create-recovery-container'); const editRec = document.getElementById('edit-recovery-container'); if (profile.is_recovery) { if (createRec) createRec.classList.remove('hidden'); if (editRec) editRec.classList.remove('hidden'); } else { if (createRec) createRec.classList.add('hidden'); if (editRec) editRec.classList.add('hidden'); } } // deleteFeedback is defined as a global function later in this file window.toggleFeedbackStatus = async (id, currentStatus) => { const newStatus = currentStatus === 'New' ? 'Reviewed' : 'New'; try { const res = await fetch(`${API_BASE}/api/feedback/${id}/status`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: newStatus }), credentials: 'include' }); if (!res.ok) throw new Error('Update failed'); showToast(`Feedback marked as ${newStatus}.`, 'success'); loadFeedback(); } catch (err) { showToast(err.message, 'error'); } }; async function loadFeedback() { try { const tbody = document.getElementById('feedback-table-body'); if (!tbody) return; tbody.innerHTML = 'Loading...'; const res = await fetch(`${API_BASE}/api/feedback`, { credentials: 'include' }); const feedbackList = await res.json(); tbody.innerHTML = ''; if (feedbackList.length === 0) { tbody.innerHTML = 'No feedback submissions found.'; return; } // Sort by timestamp descending feedbackList.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); feedbackList.forEach(fb => { const tr = document.createElement('tr'); tr.className = 'hover:bg-gray-50 transition'; const d = new Date(fb.timestamp); const dateStr = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); const isNew = fb.status === 'New'; const statusClass = isNew ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'; tr.innerHTML = ` ${dateStr}
${escapeHtml(fb.email)}
${escapeHtml(fb.type)} ${escapeHtml(fb.message)} ${fb.status || 'New'}
`; tbody.appendChild(tr); }); } catch (err) { console.error('Failed to load feedback:', err); } } window.deleteFeedback = function(feedbackId) { showConfirmModal("Are you sure you want to delete this feedback report?\n\nThis action is permanent and cannot be recovered.", async () => { try { const res = await fetch(`${API_BASE}/api/feedback/${feedbackId}`, { credentials: 'include', method: 'DELETE' }); const data = await res.json(); if (!res.ok) throw new Error(data.error); showToast("Feedback deleted successfully.", "success"); loadFeedback(); // Refresh the table } catch (err) { showToast(`Failed to delete feedback: ${err.message}`, "error"); } }); } async function loadUsers() { try { if (!sessionUsername) { await getSessionUsername(); } allUsers = await fetch(`${API_BASE}/api/users`, { credentials: 'include' }).then(r => r.json()); renderUsersTable(); } catch (err) { console.error('Failed to load users:', err); } } function renderUsersTable() { const tbody = document.getElementById('users-table-body'); const noUsers = document.getElementById('no-users'); if (!tbody) return; if (!allUsers.length) { tbody.innerHTML = ''; if (noUsers) noUsers.classList.remove('hidden'); return; } if (noUsers) noUsers.classList.add('hidden'); const roleColors = { superadmin: 'bg-purple-100 text-purple-800', admin: 'bg-orange-100 text-orange-800', officer: 'bg-blue-100 text-blue-800', viewer: 'bg-green-100 text-green-800' }; const currentUsername = sessionUsername; // Update stats const totalUsersEl = document.getElementById('stat-total-users'); const activeUsersEl = document.getElementById('stat-active-users'); const adminUsersEl = document.getElementById('stat-admin-users'); const viewerUsersEl = document.getElementById('stat-viewer-users'); if (totalUsersEl) totalUsersEl.textContent = allUsers.length; if (activeUsersEl) activeUsersEl.textContent = allUsers.filter(u => u.is_active).length; if (adminUsersEl) adminUsersEl.textContent = allUsers.filter(u => ['Admin', 'SuperAdmin'].includes(u.role)).length; if (viewerUsersEl) viewerUsersEl.textContent = allUsers.filter(u => u.role === 'Viewer').length; tbody.innerHTML = allUsers.map(user => { const isCurrentUser = user.username === currentUsername; const roleKey = (user.role || '').toLowerCase(); const colorClass = roleColors[roleKey] || 'bg-gray-100 text-gray-800'; return `
${(user.full_name || user.username || 'U')[0].toUpperCase()}
${escapeHtml(user.full_name || 'N/A')}
@${escapeHtml(user.username)}
${escapeHtml(user.role)} ${user.email ? `
${escapeHtml(user.email)}
` : '
--
'} ${user.phone ? `
${escapeHtml(user.phone)}
` : ''} ${user.department ? `
${escapeHtml(user.department)}
` : '
--
'} ${user.designation ? `
${escapeHtml(user.designation)}
` : ''} ${user.is_active ? 'Active' : 'Inactive'}
${!isCurrentUser ? `` : 'You'}
`; }).join(''); } async function getSessionUsername() { try { const info = await getSessionInfo(); sessionUsername = info.username || ''; } catch (err) { sessionUsername = ''; } } async function editUser(userId) { const user = allUsers.find(u => u.user_id === userId); if (!user) return; // Populate the edit modal const editId = document.getElementById('edit-user-id'); const editUser = document.getElementById('edit-username'); const editRole = document.getElementById('edit-role'); const editFullname = document.getElementById('edit-fullname'); const editEmail = document.getElementById('edit-email'); const editPhone = document.getElementById('edit-phone-user'); const editDesig = document.getElementById('edit-designation-user'); const editDept = document.getElementById('edit-department-user'); const editOffice = document.getElementById('edit-office-user'); const editActive = document.getElementById('edit-is-active'); const editPass = document.getElementById('edit-new-password'); if (editId) editId.value = user.user_id; if (editUser) editUser.value = user.username; // Dynamic Role Filtering for Edit Modal if (editRole) { const myRole = (currentProfile.role || '').toLowerCase(); const targetRole = (user.role || '').toLowerCase(); // Populate options based on hierarchy let roles = ['Officer', 'Viewer']; if (myRole === 'superadmin') roles = ['SuperAdmin', 'Admin', 'Officer', 'Viewer']; else if (myRole === 'admin') roles = ['Admin', 'Officer', 'Viewer']; editRole.innerHTML = roles.map(r => ``).join(''); editRole.value = user.role; // Extra safety: If editing a SuperAdmin as a non-SuperAdmin, disable the role dropdown if (targetRole === 'superadmin' && myRole !== 'superadmin') { editRole.disabled = true; } else { editRole.disabled = false; } } if (editFullname) editFullname.value = user.full_name || ''; if (editEmail) editEmail.value = user.email || ''; if (editPhone) editPhone.value = user.phone || ''; if (editDesig) editDesig.value = user.designation || ''; if (editDept) editDept.value = user.department || ''; if (editOffice) editOffice.value = user.office_location || ''; if (editActive) editActive.checked = user.is_active !== false; const editRec = document.getElementById('edit-is-recovery'); if (editRec) editRec.checked = user.is_recovery === true; if (editPass) editPass.value = ''; // Show the modal const modal = document.getElementById('edit-user-modal'); if (modal) modal.classList.remove('hidden'); } function closeEditUserModal() { const modal = document.getElementById('edit-user-modal'); const form = document.getElementById('user-edit-form'); if (modal) modal.classList.add('hidden'); if (form) form.reset(); } function deleteUser(userId, username) { showConfirmModal(`Are you sure you want to delete user "${username}"?`, async () => { try { await fetch(`${API_BASE}/api/users/${userId}`, { credentials: 'include', method: 'DELETE' }).then(async r => { const data = await r.json(); if (!r.ok) throw new Error(data.error); return data; }); showToast(`User "${username}" deleted.`, 'success'); loadUsers(); } catch (err) { showToast(`Failed to delete user: ${err.message}`, 'error'); } }); } // Edit user modal event listeners (added to admin.js logic) document.addEventListener('DOMContentLoaded', function() { // Initialize Dashboard Data loadProfile(); loadFeedback(); loadUsers(); const closeBtn = document.getElementById('btn-close-edit-user'); const cancelBtn = document.getElementById('btn-cancel-edit-user'); if (closeBtn) closeBtn.addEventListener('click', closeEditUserModal); if (cancelBtn) cancelBtn.addEventListener('click', closeEditUserModal); const modal = document.getElementById('edit-user-modal'); if (modal) { modal.addEventListener('click', function(e) { if (e.target === modal) closeEditUserModal(); }); } const editUserForm = document.getElementById('user-edit-form'); if (editUserForm) { editUserForm.addEventListener('submit', async function(e) { e.preventDefault(); const userId = document.getElementById('edit-user-id').value; const updateData = { full_name: document.getElementById('edit-fullname').value.trim(), email: document.getElementById('edit-email').value.trim(), phone: document.getElementById('edit-phone-user').value.trim(), designation: document.getElementById('edit-designation-user').value.trim(), department: document.getElementById('edit-department-user').value.trim(), office_location: document.getElementById('edit-office-user').value.trim(), role: document.getElementById('edit-role').value, is_active: document.getElementById('edit-is-active').checked, is_recovery: document.getElementById('edit-is-recovery') ? document.getElementById('edit-is-recovery').checked : false }; const newPassword = document.getElementById('edit-new-password').value; if (newPassword) updateData.new_password = newPassword; try { const res = await fetch(`${API_BASE}/api/users/${userId}`, { credentials: 'include', method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updateData) }); const data = await res.json(); if (!res.ok) throw new Error(data.error); showToast('User updated successfully.', 'success'); closeEditUserModal(); loadUsers(); } catch (err) { showToast(`Failed to update user: ${err.message}`, 'error'); } }); } // Create User Form Handler const btnCreateUser = document.getElementById('btn-create-user'); const createUserForm = document.getElementById('create-user-form'); const userCreateForm = document.getElementById('user-create-form'); const cancelCreateUserBtn = document.getElementById('btn-cancel-create-user'); const cancelCreateUserBtn2 = document.getElementById('btn-cancel-create-user-2'); function hideCreateUserForm() { if (createUserForm) createUserForm.classList.add('hidden'); if (userCreateForm) userCreateForm.reset(); } if (btnCreateUser && createUserForm) { btnCreateUser.addEventListener('click', () => { createUserForm.classList.remove('hidden'); createUserForm.scrollIntoView({ behavior: 'smooth' }); // Dynamic Role Filtering for Create Modal const createRoleEl = document.getElementById('create-role'); if (createRoleEl) { const myRole = (currentProfile.role || '').toLowerCase(); let roles = ['Officer', 'Viewer']; if (myRole === 'superadmin') roles = ['SuperAdmin', 'Admin', 'Officer', 'Viewer']; else if (myRole === 'admin') roles = ['Admin', 'Officer', 'Viewer']; createRoleEl.innerHTML = roles.map(r => ``).join(''); } }); } if (cancelCreateUserBtn) cancelCreateUserBtn.addEventListener('click', hideCreateUserForm); if (cancelCreateUserBtn2) cancelCreateUserBtn2.addEventListener('click', hideCreateUserForm); if (userCreateForm) { userCreateForm.addEventListener('submit', async function(e) { e.preventDefault(); const userData = { username: document.getElementById('create-username').value.trim(), password: document.getElementById('create-password').value, full_name: document.getElementById('create-full-name').value.trim(), email: document.getElementById('create-email').value.trim(), phone: document.getElementById('create-phone').value.trim(), designation: document.getElementById('create-designation').value.trim(), department: document.getElementById('create-department').value.trim(), role: document.getElementById('create-role').value, office_location: document.getElementById('create-office').value.trim(), is_active: document.getElementById('create-active').checked, is_recovery: document.getElementById('create-is-recovery') ? document.getElementById('create-is-recovery').checked : false }; try { const res = await fetch(`${API_BASE}/api/users`, { credentials: 'include', method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }); const data = await res.json(); if (!res.ok) throw new Error(data.error); showToast(`User "${userData.username}" created successfully.`, 'success'); hideCreateUserForm(); loadUsers(); } catch (err) { showToast(`Failed to create user: ${err.message}`, 'error'); } }); } // Profile Edit Logic const btnEditProfile = document.getElementById('btn-edit-profile-inline'); const btnCancelEditProfile = document.getElementById('btn-cancel-edit-profile'); const profileView = document.getElementById('profile-view'); const profileEditForm = document.getElementById('edit-profile-form'); const profileForm = document.getElementById('profile-form'); if (btnEditProfile) { btnEditProfile.addEventListener('click', () => { if (currentProfile) { document.getElementById('edit-full-name').value = currentProfile.full_name || ''; document.getElementById('edit-email').value = currentProfile.email || ''; document.getElementById('edit-phone').value = currentProfile.phone || ''; document.getElementById('edit-designation').value = currentProfile.designation || ''; document.getElementById('edit-department').value = currentProfile.department || ''; document.getElementById('edit-office').value = currentProfile.office_location || ''; // Reset password fields document.getElementById('edit-current-password').value = ''; document.getElementById('edit-new-password').value = ''; } if (profileView) profileView.classList.add('hidden'); if (profileEditForm) profileEditForm.classList.remove('hidden'); }); } if (btnCancelEditProfile) { btnCancelEditProfile.addEventListener('click', () => { if (profileView) profileView.classList.remove('hidden'); if (profileEditForm) profileEditForm.classList.add('hidden'); }); } if (profileForm) { profileForm.addEventListener('submit', async (e) => { e.preventDefault(); const updateData = { full_name: document.getElementById('edit-full-name').value.trim(), email: document.getElementById('edit-email').value.trim(), phone: document.getElementById('edit-phone').value.trim(), designation: document.getElementById('edit-designation').value.trim(), department: document.getElementById('edit-department').value.trim(), office_location: document.getElementById('edit-office').value.trim() }; const currentPass = document.getElementById('edit-current-password').value; const newPass = document.getElementById('edit-new-password').value; if (newPass) { if (!currentPass) { showToast('Current password is required to set a new password.', 'error'); return; } updateData.current_password = currentPass; updateData.new_password = newPass; } try { const res = await fetch(`${API_BASE}/api/profile`, { credentials: 'include', method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updateData) }); const data = await res.json(); if (!res.ok) throw new Error(data.error); showToast('Profile updated successfully.', 'success'); // Update global state and UI currentProfile = data.profile; displayProfile(currentProfile); // Switch back to view mode if (profileView) profileView.classList.remove('hidden'); if (profileEditForm) profileEditForm.classList.add('hidden'); } catch (err) { showToast(`Update failed: ${err.message}`, 'error'); } }); } // Refresh Buttons const btnRefreshDashboard = document.getElementById('btn-refresh-dashboard'); if (btnRefreshDashboard) { btnRefreshDashboard.addEventListener('click', async () => { btnRefreshDashboard.classList.add('animate-spin'); try { await loadRecordsOnMap(); showToast('Dashboard data refreshed.', 'success'); } finally { setTimeout(() => btnRefreshDashboard.classList.remove('animate-spin'), 600); } }); } const btnRefreshFeedback = document.getElementById('btn-refresh-feedback'); if (btnRefreshFeedback) { btnRefreshFeedback.addEventListener('click', async () => { btnRefreshFeedback.classList.add('animate-spin'); try { await loadFeedback(); showToast('Feedback list updated.', 'success'); } finally { setTimeout(() => btnRefreshFeedback.classList.remove('animate-spin'), 600); } }); } const btnExportExcel = document.getElementById('btn-export-excel'); if (btnExportExcel) { btnExportExcel.addEventListener('click', () => { const village = document.getElementById('village-filter')?.value || ''; const url = getVillageExcelUrl(village); showToast('Preparing village ledger (Excel)...', 'info'); window.location.href = url; }); } const btnRefreshRecords = document.getElementById('btn-refresh-records'); if (btnRefreshRecords) { btnRefreshRecords.addEventListener('click', async () => { btnRefreshRecords.classList.add('animate-spin'); try { await loadRecordsOnMap(); showToast('Records refreshed.', 'success'); } finally { setTimeout(() => btnRefreshRecords.classList.remove('animate-spin'), 600); } }); } // Header Actions const btnLogout = document.getElementById('btn-logout'); if (btnLogout) { btnLogout.addEventListener('click', () => { showConfirmModal('Are you sure you want to log out?', async () => { const data = await logout(); window.location.href = data.redirect || '/login'; }); }); } // Theme toggle is now handled globally in admin_dashboard.html }); ================================================ FILE: static/js/modules/dashboard.js ================================================ /** * dashboard.js - Dashboard rendering and analytics logic */ function renderKpiCards(records) { const totalParcelsEl = document.getElementById('kpi-total-parcels'); const totalAreaEl = document.getElementById('kpi-total-area'); const estimatedValueEl = document.getElementById('kpi-estimated-value'); const mutationEl = document.getElementById('kpi-mutations'); if (!totalParcelsEl || !totalAreaEl || !estimatedValueEl || !mutationEl) return; let totalArea = 0; let totalValue = 0; let totalMutations = 0; records.forEach(rec => { const attrs = rec.attributes || {}; const area = asNumber(attrs.area_ha); const rate = asNumber(attrs.circle_rate_inr); totalArea += area; totalValue += area * rate; totalMutations += (rec.mutation_history || []).length; }); totalParcelsEl.textContent = String(records.length); totalAreaEl.textContent = totalArea.toFixed(2); estimatedValueEl.textContent = formatInr(totalValue); mutationEl.textContent = String(totalMutations); } function buildDonutChart(slices, colors) { const total = slices.reduce((s, x) => s + x.value, 0); if (total === 0) return '

No data

'; const R = 40, CX = 50, CY = 50, stroke = 18; let cumAngle = -90; const arcs = slices.map((s, i) => { const pct = s.value / total; const angle = pct * 360; const r1 = (cumAngle * Math.PI) / 180; const r2 = ((cumAngle + angle) * Math.PI) / 180; const x1 = CX + R * Math.cos(r1), y1 = CY + R * Math.sin(r1); const x2 = CX + R * Math.cos(r2), y2 = CY + R * Math.sin(r2); const large = angle > 180 ? 1 : 0; const d = `M ${x1} ${y1} A ${R} ${R} 0 ${large} 1 ${x2} ${y2}`; cumAngle += angle; return ``; }).join(''); return `
${arcs} ${total} parcels
${slices.map((s, i) => `
${escapeHtml(s.label)} ${s.value} (${(s.value/total*100).toFixed(0)}%)
`).join('')}
`; } function buildRankedList(items, colors) { const max = Math.max(...items.map(x => x.value), 1); return items.map((item, i) => { const pct = Math.max((item.value / max) * 100, 4); const color = colors[i % colors.length]; return `
${escapeHtml(item.label)} ${item.sublabel}
`; }).join(''); } function renderLandUseDistribution(records) { const target = document.getElementById('dashboard-land-use'); if (!target) return; if (!records.length) { target.innerHTML = '

No records yet.

'; return; } const metrics = {}; records.forEach(rec => { const lu = (rec.attributes && rec.attributes.land_use) || 'Unknown'; if (!metrics[lu]) metrics[lu] = { count: 0, area: 0 }; metrics[lu].count++; metrics[lu].area += asNumber(rec.attributes && rec.attributes.area_ha); }); const COLORS = ['#f97316','#3b82f6','#22c55e','#a855f7','#ec4899','#14b8a6','#f59e0b','#ef4444']; const slices = Object.entries(metrics).sort((a,b)=>b[1].count-a[1].count) .map(([label, v]) => ({ label, value: v.count, extra: v.area.toFixed(1)+' Ha' })); target.innerHTML = buildDonutChart(slices, COLORS); } function renderDistrictOverview(records) { const target = document.getElementById('dashboard-districts'); if (!target) return; if (!records.length) { target.innerHTML = '

No district data.

'; return; } const districtMap = {}; records.forEach(rec => { const d = (rec.location && rec.location.district) || 'Unknown'; if (!districtMap[d]) districtMap[d] = { count: 0, area: 0, value: 0 }; const area = asNumber(rec.attributes && rec.attributes.area_ha); districtMap[d].count++; districtMap[d].area += area; districtMap[d].value += area * asNumber(rec.attributes && rec.attributes.circle_rate_inr); }); const COLORS = ['#6366f1','#f97316','#10b981','#f59e0b','#f43f5e','#a855f7']; const items = Object.entries(districtMap).sort((a,b)=>b[1].count-a[1].count).slice(0,6) .map(([label, v]) => ({ label, value: v.count, sublabel: `${v.count} parcels · ${v.area.toFixed(1)} Ha` })); target.innerHTML = buildRankedList(items, COLORS); } function renderTopValueParcel(records) { const target = document.getElementById('dashboard-top-parcel'); if (!target) return; if (!records.length) { target.textContent = 'No parcel data yet.'; return; } let topRecord = null; let topValue = -1; records.forEach(rec => { const area = asNumber(rec.attributes && rec.attributes.area_ha); const rate = asNumber(rec.attributes && rec.attributes.circle_rate_inr); const value = area * rate; if (value > topValue) { topValue = value; topRecord = rec; } }); if (!topRecord) { target.textContent = 'No parcel data yet.'; return; } const loc = topRecord.location || {}; const attrs = topRecord.attributes || {}; target.innerHTML = `
${escapeHtml(topRecord.khasra_no || 'N/A')} (${escapeHtml(topRecord.ulpin || 'N/A')})
${escapeHtml(loc.village || 'N/A')}, ${escapeHtml(loc.district || 'N/A')} | ${escapeHtml(attrs.land_use || 'N/A')}
Area: ${asNumber(attrs.area_ha).toFixed(2)} Ha | Estimated: Rs. ${formatInr(topValue)}
`; } function renderRecentMutations(records) { const target = document.getElementById('dashboard-mutations'); if (!target) return; const entries = []; records.forEach(rec => { (rec.mutation_history || []).forEach(item => { entries.push({ khasraNo: rec.khasra_no || 'N/A', district: (rec.location && rec.location.district) || 'N/A', previousOwner: item.previous_owner || 'N/A', mutationType: item.mutation_type || 'N/A', mutationDate: item.mutation_date || 'N/A', mutationRef: item.mutation_ref || 'N/A' }); }); }); entries.sort((a, b) => String(b.mutationDate).localeCompare(String(a.mutationDate))); if (!entries.length) { target.innerHTML = '
No mutation history available for selected filters.
'; return; } target.innerHTML = entries.slice(0, 6).map(item => `
${escapeHtml(item.khasraNo)} | ${escapeHtml(item.mutationType)}
${escapeHtml(item.previousOwner)} -> ${escapeHtml(item.mutationDate)}
${escapeHtml(item.district)} | Ref: ${escapeHtml(item.mutationRef)}
`).join(''); } function renderDashboardAnalytics(filteredRecords) { renderKpiCards(filteredRecords); renderLandUseDistribution(filteredRecords); renderDistrictOverview(filteredRecords); renderTopValueParcel(filteredRecords); renderRecentMutations(filteredRecords); } // Server-side rendering helpers function renderKpiCardsFromServer(kpis) { const totalParcelsEl = document.getElementById('kpi-total-parcels'); const totalAreaEl = document.getElementById('kpi-total-area'); const estimatedValueEl = document.getElementById('kpi-estimated-value'); const mutationEl = document.getElementById('kpi-mutations'); if (totalParcelsEl) totalParcelsEl.textContent = String(kpis.total_parcels || 0); if (totalAreaEl) totalAreaEl.textContent = (kpis.total_area || 0).toFixed(2); if (estimatedValueEl) estimatedValueEl.textContent = formatInr(kpis.estimated_value || 0); if (mutationEl) mutationEl.textContent = String(kpis.total_mutations || 0); } function renderLandUseDistributionFromServer(stats) { const target = document.getElementById('dashboard-land-use'); if (!target) return; const entries = Object.entries(stats || {}); if (!entries.length) { target.innerHTML = '

No records yet.

'; return; } const COLORS = ['#f97316','#3b82f6','#22c55e','#a855f7','#ec4899','#14b8a6','#f59e0b','#ef4444']; const slices = entries.sort((a,b)=>b[1].count-a[1].count) .map(([label, s]) => ({ label, value: s.count, extra: s.area.toFixed(1)+' Ha' })); target.innerHTML = buildDonutChart(slices, COLORS); } function renderDistrictOverviewFromServer(districts) { const target = document.getElementById('dashboard-districts'); if (!target) return; if (!districts || !districts.length) { target.innerHTML = '

No district data.

'; return; } const COLORS = ['#6366f1','#f97316','#10b981','#f59e0b','#f43f5e','#a855f7']; const items = districts.map(d => ({ label: d.name, value: d.count, sublabel: `${d.count} parcels · ${d.area.toFixed(1)} Ha` })); target.innerHTML = buildRankedList(items, COLORS); } function renderTopValueParcelFromServer(parcel) { const target = document.getElementById('dashboard-top-parcel'); if (!target) return; if (!parcel) { target.textContent = 'No parcel data yet.'; return; } target.innerHTML = `
${escapeHtml(parcel.khasra_no)} (${escapeHtml(parcel.ulpin)})
${escapeHtml(parcel.village)}, ${escapeHtml(parcel.district)} | ${escapeHtml(parcel.land_use)}
Area: ${parcel.area_ha} Ha | Estimated: Rs. ${formatInr(parcel.estimated_value)}
`; } function renderRecentMutationsFromServer(mutations) { const target = document.getElementById('dashboard-mutations'); if (!target) return; if (!mutations || !mutations.length) { target.innerHTML = '
No mutation history available for selected filters.
'; return; } target.innerHTML = mutations.slice(0, 6).map(m => `
${escapeHtml(m.khasra_no)} | ${escapeHtml(m.mutation_type)}
${escapeHtml(m.previous_owner)} -> ${escapeHtml(m.mutation_date)}
`).join(''); } ================================================ FILE: static/js/modules/forms.js ================================================ /** * forms.js - Form handling and location auto-fill logic */ function getFormElements() { return { stateEl: document.getElementById('form-state'), districtEl: document.getElementById('form-district'), villageEl: document.getElementById('form-village'), manualOverrideEl: document.getElementById('location-manual-override'), manualFieldsEl: document.getElementById('location-manual-fields'), stateManualEl: document.getElementById('form-state-manual'), districtManualEl: document.getElementById('form-district-manual'), villageManualEl: document.getElementById('form-village-manual'), sourceEl: document.getElementById('form-location-source') }; } function populateSelect(selectEl, values, placeholder, selectedValue) { if (!selectEl) return; const current = selectedValue || ''; selectEl.innerHTML = ''; const placeholderOption = document.createElement('option'); placeholderOption.value = ''; placeholderOption.textContent = placeholder; selectEl.appendChild(placeholderOption); values.forEach(value => { const option = document.createElement('option'); option.value = value; option.textContent = value; selectEl.appendChild(option); }); if (current && !values.includes(current)) { const customOption = document.createElement('option'); customOption.value = current; customOption.textContent = current; selectEl.appendChild(customOption); } selectEl.value = current; } function refreshStateOptions(selectedState) { const { stateEl } = getFormElements(); if (!stateEl) return; const states = sortedValues(Object.keys(locationCatalog)); populateSelect(stateEl, states, 'Select State', selectedState || ''); } function refreshDistrictOptions(selectedDistrict) { const { stateEl, districtEl } = getFormElements(); if (!stateEl || !districtEl) return; const state = stateEl.value; const districts = state && locationCatalog[state] ? sortedValues(Object.keys(locationCatalog[state])) : []; populateSelect(districtEl, districts, 'Select District', selectedDistrict || ''); } function refreshVillageOptions(selectedVillage) { const { stateEl, districtEl, villageEl } = getFormElements(); if (!stateEl || !districtEl || !villageEl) return; const state = stateEl.value; const district = districtEl.value; const villages = state && district && locationCatalog[state] && locationCatalog[state][district] ? sortedValues(locationCatalog[state][district]) : []; populateSelect(villageEl, villages, 'Select Village / Ward', selectedVillage || ''); } function setLocationValues(state, district, village) { ensureLocationInCatalog(state, district, village); refreshStateOptions(state || ''); refreshDistrictOptions(district || ''); refreshVillageOptions(village || ''); const { stateManualEl, districtManualEl, villageManualEl } = getFormElements(); if (stateManualEl && !stateManualEl.value.trim()) { stateManualEl.value = state || ''; } if (districtManualEl && !districtManualEl.value.trim()) { districtManualEl.value = district || ''; } if (villageManualEl && !villageManualEl.value.trim()) { villageManualEl.value = village || ''; } } function setLocationSource(text) { const { sourceEl } = getFormElements(); if (sourceEl) { sourceEl.textContent = text; } } function isManualLocationOverrideEnabled() { const { manualOverrideEl } = getFormElements(); return !!(manualOverrideEl && manualOverrideEl.checked); } function toggleManualLocationOverride(enabled) { const { stateEl, districtEl, villageEl, manualFieldsEl, stateManualEl, districtManualEl, villageManualEl } = getFormElements(); if (!stateEl || !districtEl || !villageEl || !manualFieldsEl || !stateManualEl || !districtManualEl || !villageManualEl) { return; } manualFieldsEl.classList.toggle('hidden', !enabled); stateEl.disabled = enabled; districtEl.disabled = enabled; villageEl.disabled = enabled; stateEl.required = !enabled; districtEl.required = !enabled; villageEl.required = !enabled; stateManualEl.required = enabled; districtManualEl.required = enabled; villageManualEl.required = enabled; if (enabled) { stateManualEl.value = stateEl.value || stateManualEl.value; districtManualEl.value = districtEl.value || districtManualEl.value; villageManualEl.value = villageEl.value || villageManualEl.value; setLocationSource('Source: manual override enabled'); } else { setLocationSource('Source: map auto-fill enabled'); } } function getEffectiveLocationValues() { const { stateEl, districtEl, villageEl, stateManualEl, districtManualEl, villageManualEl } = getFormElements(); if (isManualLocationOverrideEnabled()) { return { state: (stateManualEl ? stateManualEl.value : '').trim(), district: (districtManualEl ? districtManualEl.value : '').trim(), village: (villageManualEl ? villageManualEl.value : '').trim() }; } return { state: stateEl ? stateEl.value : '', district: districtEl ? districtEl.value : '', village: villageEl ? villageEl.value : '' }; } function initializeLocationFilters() { const { stateEl, districtEl, villageEl, manualOverrideEl, stateManualEl, districtManualEl, villageManualEl } = getFormElements(); if (!stateEl || !districtEl || !villageEl) return; // Reset catalog (will be repopulated from records) locationCatalog = {}; refreshStateOptions(''); refreshDistrictOptions(''); refreshVillageOptions(''); stateEl.addEventListener('change', function() { lastAutoLocation = { state: '', district: '', village: '' }; refreshDistrictOptions(''); refreshVillageOptions(''); }); districtEl.addEventListener('change', function() { lastAutoLocation = { state: '', district: '', village: '' }; refreshVillageOptions(''); }); if (manualOverrideEl) { manualOverrideEl.addEventListener('change', function() { toggleManualLocationOverride(this.checked); }); } if (stateManualEl && districtManualEl && villageManualEl) { const onManualEntry = function() { if (!isManualLocationOverrideEnabled()) return; lastAutoLocation = { state: stateManualEl.value.trim(), district: districtManualEl.value.trim(), village: villageManualEl.value.trim() }; }; stateManualEl.addEventListener('input', onManualEntry); districtManualEl.addEventListener('input', onManualEntry); villageManualEl.addEventListener('input', onManualEntry); } toggleManualLocationOverride(false); ['form-area', 'form-circle-rate', 'form-land-use'].forEach(id => { const el = document.getElementById(id); if (el) { el.addEventListener('input', () => window.updateLiveValuation()); el.addEventListener('change', () => window.updateLiveValuation()); } }); } // Live Valuation Logic window.updateLiveValuation = function() { const areaInput = document.getElementById('form-area'); const rateInput = document.getElementById('form-circle-rate'); const luInput = document.getElementById('form-land-use'); if (!areaInput || !rateInput || !luInput) return; const area = asNumber(areaInput.value); const rate = asNumber(rateInput.value); const landUse = luInput.value; const total = calculateValuation(area, rate, landUse); const valEl = document.getElementById('form-live-value'); const multEl = document.getElementById('form-live-multiplier'); if (valEl) valEl.textContent = 'Rs. ' + formatInr(total); if (multEl) { const multipliers = { 'Commercial': 2.5, 'Industrial': 1.8, 'Residential': 1.5, 'Agricultural': 1.0, 'Government': 1.2, 'Forest': 0.8, 'Wasteland': 0.5 }; const multiplier = multipliers[landUse] || 1.0; multEl.textContent = multiplier.toFixed(1) + 'x'; } }; function isMapAutofillEnabled() { const enabledEl = document.getElementById('map-autofill-enabled'); return enabledEl ? enabledEl.checked : true; } function updateGpsBanner(locationData) { const gpsBanner = document.getElementById('gps-detected-banner'); if (!gpsBanner) return; if (!locationData) { gpsBanner.classList.add('hidden'); return; } gpsBanner.classList.remove('hidden'); if (locationData.loading) { gpsBanner.innerHTML = `
Detecting location from GPS coordinates...
`; return; } if (locationData.state && locationData.district) { const villageDisplay = locationData.village ? `${escapeHtml(locationData.village)}, ` : ''; gpsBanner.innerHTML = `

GPS Detected Location

${escapeHtml(locationData.state)}${escapeHtml(locationData.district)} › ${escapeHtml(locationData.village || 'N/A')}

`; const syncBtn = document.getElementById('btn-sync-gps'); if (syncBtn) { syncBtn.addEventListener('click', () => { applyResolvedLocation(locationData, true); showToast('Form updated with GPS location.', 'success'); }); } } else { gpsBanner.classList.add('hidden'); } } function shouldApplyLocationUpdate(nextLocation, forceUpdate) { const { stateEl, districtEl, villageEl } = getFormElements(); if (!stateEl || !districtEl || !villageEl) return false; if (isManualLocationOverrideEnabled()) return false; if (forceUpdate) return true; const current = { state: stateEl.value || '', district: districtEl.value || '', village: villageEl.value || '' }; const currentIsEmpty = !current.state && !current.district; // Loosened check: if state and district match previous auto, we can still update village // OR if the new location is in a completely different state/district, we should probably update if it was auto-filled const currentIsPreviousAuto = current.state === (lastAutoLocation.state || '') && current.district === (lastAutoLocation.district || ''); const nextLooksUseful = nextLocation.state || nextLocation.district; return !!nextLooksUseful && (currentIsEmpty || currentIsPreviousAuto); } function applyResolvedLocation(locationData, forceUpdate) { const nextLocation = { state: locationData.state || '', district: locationData.district || '', village: locationData.village || '' }; // Update the GPS detected state for mismatch validation if (nextLocation.state && nextLocation.district) { lastGpsDetectedLocation = { ...nextLocation }; updateGpsBanner(nextLocation); } else { lastGpsDetectedLocation = { state: '', district: '', village: '' }; updateGpsBanner(null); } if (!shouldApplyLocationUpdate(nextLocation, forceUpdate)) { updateAutofillStatus('Map location detected. Location fields kept as manually selected.', false); return; } setLocationValues(nextLocation.state, nextLocation.district, nextLocation.village); lastAutoLocation = nextLocation; setLocationSource(`Source: ${locationData.display_name || 'Map reverse geocoding'}`); const displayVillage = nextLocation.village ? `${nextLocation.village}, ` : ''; updateAutofillStatus(`Auto-filled: ${displayVillage}${nextLocation.district || 'District N/A'}, ${nextLocation.state || 'State N/A'}`, false); } function scheduleMapLocationLookup(lat, lng, forceUpdate) { if (!isAdmin) return; // Stop tracker if manual override is enabled if (isManualLocationOverrideEnabled()) { updateGpsBanner(null); return; } if (typeof lat !== 'number' || typeof lng !== 'number') return; const geocodeKey = `${lat.toFixed(4)},${lng.toFixed(4)}`; if (!forceUpdate && geocodeKey === lastGeocodeKey) return; lastGeocodeKey = geocodeKey; if (reverseGeocodeTimer) { clearTimeout(reverseGeocodeTimer); } // Show loading state in GPS banner updateGpsBanner({ loading: true }); reverseGeocodeTimer = setTimeout(async function() { updateAutofillStatus('Detecting state and district from map...', false); try { const locationData = await fetchLocationFromCoordinates(lat, lng); applyResolvedLocation(locationData, forceUpdate); } catch (err) { updateAutofillStatus(err.message || 'Map location detection failed.', true); updateGpsBanner(null); } }, 650); } function setMutationMode(enabled) { const mutationSection = document.getElementById('mutation-section'); const mutationTabBtn = document.getElementById('form-mutation-tab-btn'); if (!mutationSection || !mutationTabBtn) return; mutationSection.classList.toggle('hidden', !enabled); mutationTabBtn.classList.toggle('hidden', !enabled); } async function editRecord(recordId) { try { const record = await fetchRecord(recordId); switchMainTab('add-record'); // Wait for tab to show await new Promise(resolve => setTimeout(resolve, 200)); const { manualOverrideEl } = getFormElements(); if (manualOverrideEl) { manualOverrideEl.checked = false; } toggleManualLocationOverride(false); document.getElementById('form-record-id').value = record._id; document.getElementById('form-title').textContent = 'Edit Record: ' + (record.khasra_no || ''); document.getElementById('form-khasra').value = record.khasra_no || ''; document.getElementById('form-khata').value = record.khata_no || ''; document.getElementById('form-ulpin').value = record.ulpin || ''; document.getElementById('form-land-use').value = (record.attributes && record.attributes.land_use) || ''; document.getElementById('form-area').value = (record.attributes && record.attributes.area_ha) || ''; document.getElementById('form-circle-rate').value = (record.attributes && record.attributes.circle_rate_inr) || 0; document.getElementById('form-owner-name').value = (record.owner && record.owner.name) || ''; document.getElementById('form-share').value = (record.owner && record.owner.share_pct) || 100; document.getElementById('form-aadhaar').value = (record.owner && record.owner.aadhaar_mask) || ''; const loc = record.location || {}; setLocationValues(loc.state || '', loc.district || '', loc.village || ''); setLocationSource('Source: loaded from existing record'); const geometry = record.geometry || null; if (geometry) { await updateGeometryMetricsForAddRecord(geometry); if (addRecordDrawnItems) addRecordDrawnItems.clearLayers(); const editableLayer = L.geoJSON(geometry, { style: { color: '#ea580c', fillColor: '#fed7aa', fillOpacity: 0.4, weight: 3 } }); editableLayer.eachLayer(layer => { if (addRecordDrawnItems) addRecordDrawnItems.addLayer(layer); currentSketchLayer = layer; }); if (addRecordMap) { const bounds = editableLayer.getBounds(); addRecordMap.fitBounds(bounds, { padding: [50, 50], maxZoom: 16 }); setTimeout(() => { editableLayer.eachLayer(layer => { if (layer.pm) layer.pm.enable(); }); }, 300); } } setMutationMode(true); document.getElementById('form-submit-btn').textContent = 'Update Record'; const drawInstruction = document.getElementById('draw-instruction'); if (drawInstruction) drawInstruction.style.display = 'none'; const drawStatus = document.getElementById('draw-status'); if (drawStatus) { drawStatus.innerHTML = 'Editing existing record. You can modify the polygon on the map.'; drawStatus.className = 'bg-blue-50 border-l-4 border-blue-500 rounded-r-lg p-3 text-sm text-blue-800'; } switchFormTab('location'); } catch (_err) { showToast('Failed to load record for editing.', 'error'); } } async function handleFormSubmit() { const recordId = document.getElementById('form-record-id').value; const geometryStr = document.getElementById('form-geometry').value; const locationValues = { state: document.getElementById('form-state-manual').value || document.getElementById('form-state').value, district: document.getElementById('form-district-manual').value || document.getElementById('form-district').value, village: document.getElementById('form-village-manual').value || document.getElementById('form-village').value }; if (!locationValues.state || !locationValues.district || !locationValues.village) { showToast('Location (State, District, Village) is required.', 'error'); switchFormTab('location'); return; } const manualOverrideEl = document.getElementById('location-manual-override'); const isManualOverride = manualOverrideEl && manualOverrideEl.checked; const hasGpsData = lastGpsDetectedLocation.state && lastGpsDetectedLocation.district; if (isManualOverride && hasGpsData) { const stateMatch = locationValues.state.trim().toLowerCase() === lastGpsDetectedLocation.state.trim().toLowerCase(); const districtMatch = locationValues.district.trim().toLowerCase() === lastGpsDetectedLocation.district.trim().toLowerCase(); if (!stateMatch || !districtMatch) { const proceed = await new Promise(resolve => { const overlay = document.createElement('div'); overlay.className = 'fixed inset-0 bg-black bg-opacity-60 z-[9999] flex items-center justify-center p-4'; overlay.style.backdropFilter = 'blur(3px)'; overlay.innerHTML = `

Location Mismatch Warning

The location you entered does not match the GPS coordinates of the parcel you drew on the map.

📡 GPS Detected

${escapeHtml(lastGpsDetectedLocation.state)}

${escapeHtml(lastGpsDetectedLocation.district)}

✏️ You Entered

${escapeHtml(locationValues.state)}

${escapeHtml(locationValues.district)}

Are you sure you want to continue with the manually entered location?

`; document.body.appendChild(overlay); overlay.querySelector('#mismatch-cancel').addEventListener('click', () => { const manualOvEl = document.getElementById('location-manual-override'); if (manualOvEl) manualOvEl.checked = false; toggleManualLocationOverride(false); setLocationValues(lastGpsDetectedLocation.state, lastGpsDetectedLocation.district, lastGpsDetectedLocation.village); document.body.removeChild(overlay); resolve(false); }); overlay.querySelector('#mismatch-force').addEventListener('click', () => { document.body.removeChild(overlay); resolve(true); }); }); if (!proceed) return; } } const khasra = document.getElementById('form-khasra').value.trim(); const khata = document.getElementById('form-khata').value.trim(); const landUse = document.getElementById('form-land-use').value; if (!khasra) { showToast('Khasra No. is required.', 'error'); switchFormTab('parcel'); return; } if (!khata) { showToast('Khata No. is required.', 'error'); switchFormTab('parcel'); return; } if (!landUse) { showToast('Land Use is required.', 'error'); switchFormTab('parcel'); return; } if (!geometryStr) { showToast('Please draw a parcel on the map first.', 'error'); switchFormTab('parcel'); return; } if (!recordId) { const ownerName = document.getElementById('form-owner-name').value.trim(); if (!ownerName) { showToast('Owner Name is required.', 'error'); switchFormTab('owner'); return; } } let geometry; try { geometry = JSON.parse(geometryStr); } catch (_err) { showToast('Invalid geometry data.', 'error'); return; } const submitBtn = document.getElementById('form-submit-btn'); const originalText = submitBtn.textContent; submitBtn.disabled = true; submitBtn.innerHTML = 'Saving...'; try { let ownerDocB64 = undefined; const ownerDocFile = document.getElementById('form-owner-doc')?.files[0]; if (ownerDocFile) ownerDocB64 = await fileToBase64(ownerDocFile); let mutationDocB64 = undefined; const mutationDocFile = document.getElementById('form-mutation-doc')?.files[0]; if (mutationDocFile) mutationDocB64 = await fileToBase64(mutationDocFile); if (recordId) { const updateData = { khasra_no: document.getElementById('form-khasra').value, khata_no: document.getElementById('form-khata').value, land_use: document.getElementById('form-land-use').value, circle_rate_inr: parseFloat(document.getElementById('form-circle-rate').value) || 0, share_pct: parseFloat(document.getElementById('form-share').value) || 100, aadhaar_mask: document.getElementById('form-aadhaar').value, geometry: geometry, location: locationValues, owner_proof_doc_b64: ownerDocB64 }; const newOwner = document.getElementById('form-new-owner').value.trim(); if (newOwner) { updateData.mutation = true; updateData.new_owner_name = newOwner; updateData.new_share_pct = parseFloat(document.getElementById('form-new-share').value) || 100; updateData.mutation_type = document.getElementById('form-mutation-type').value; updateData.mutation_date = document.getElementById('form-mutation-date').value || new Date().toISOString().split('T')[0]; updateData.mutation_proof_doc_b64 = mutationDocB64; } await updateRecord(recordId, updateData); showToast('Record updated successfully.', 'success'); } else { const recordData = { khasra_no: document.getElementById('form-khasra').value, khata_no: document.getElementById('form-khata').value, ulpin: document.getElementById('form-ulpin').value || undefined, land_use: document.getElementById('form-land-use').value, circle_rate_inr: parseFloat(document.getElementById('form-circle-rate').value) || 0, owner_name: document.getElementById('form-owner-name').value, share_pct: parseFloat(document.getElementById('form-share').value) || 100, aadhaar_mask: document.getElementById('form-aadhaar').value || 'XXXX-XXXX-XXXX', geometry: geometry, location: locationValues, owner_proof_doc_b64: ownerDocB64 }; const result = await createRecord(recordData); showToast('Record created successfully.', 'success'); } resetForm(); switchMainTab('records'); loadRecordsOnMap(); } catch (err) { showToast(`Error: ${err.message}`, 'error'); } finally { submitBtn.disabled = false; submitBtn.textContent = originalText; } } function resetForm() { const form = document.getElementById('record-form'); if (form) form.reset(); document.getElementById('form-record-id').value = ''; document.getElementById('form-title').textContent = 'Add New Record'; document.getElementById('form-submit-btn').textContent = 'Create Record'; document.getElementById('form-geometry').value = ''; setMutationMode(false); clearGeometrySelection(true); const { manualOverrideEl } = getFormElements(); if (manualOverrideEl) manualOverrideEl.checked = false; toggleManualLocationOverride(false); const drawInstruction = document.getElementById('draw-instruction'); if (drawInstruction) drawInstruction.style.display = 'block'; const drawStatus = document.getElementById('draw-status'); if (drawStatus) { drawStatus.innerHTML = 'Waiting for map drawing... Draw a parcel on the map to continue.'; drawStatus.className = 'bg-blue-50 border-l-4 border-blue-500 rounded-r-lg p-3 text-sm text-blue-800'; } lastGpsDetectedLocation = { state: '', district: '', village: '' }; updateGpsBanner(null); switchFormTab('location'); } /** * Ensures a location exists in the local catalog so it can be selected in dropdowns. * Used primarily when map auto-detection finds a location not yet in our database. */ function ensureLocationInCatalog(state, district, village) { if (!state) return; if (!locationCatalog) locationCatalog = {}; if (!locationCatalog[state]) locationCatalog[state] = {}; if (district) { if (!locationCatalog[state][district]) locationCatalog[state][district] = []; if (village && !locationCatalog[state][district].includes(village)) { locationCatalog[state][district].push(village); locationCatalog[state][district].sort(); } } } ================================================ FILE: static/js/modules/gis.js ================================================ /** * gis.js - GIS-related UI updates (area, perimeter, centroid) */ async function updateGeometryMetrics(geometry, options) { const shouldRefreshMetrics = options && options.shouldRefreshMetrics; if (!geometry) return; const geometryInput = document.getElementById('form-geometry'); if (geometryInput) { geometryInput.value = JSON.stringify(geometry); } if (!shouldRefreshMetrics) { return; } try { const result = await calculateArea(geometry); if (!result.area) { return; } const area = result.area; const perimeter = result.perimeter || {}; const centroid = result.centroid || {}; const areaInput = document.getElementById('form-area'); const areaAuto = document.getElementById('area-auto'); const areaEquivalents = document.getElementById('area-equivalents'); const geometryMetrics = document.getElementById('geometry-metrics'); const perimeterEl = document.getElementById('metric-perimeter'); const centroidEl = document.getElementById('metric-centroid'); if (areaInput) { areaInput.value = area.area_ha; } if (areaAuto) { areaAuto.textContent = '(auto)'; } if (areaEquivalents) { areaEquivalents.classList.remove('hidden'); areaEquivalents.innerHTML = ` ${area.area_acres} Acres ${area.area_guntha} Guntha ${area.area_bigha_mp} Bigha `; } if (geometryMetrics) { geometryMetrics.classList.remove('hidden'); } if (perimeterEl) { perimeterEl.textContent = `${Math.round(perimeter.perimeter_m || 0)} m`; } if (centroidEl) { centroidEl.textContent = `${centroid.lat.toFixed(5)}, ${centroid.lng.toFixed(5)}`; } } catch (err) { console.error('Failed to update geometry metrics:', err); } } async function updateGeometryMetricsForAddRecord(geometry) { const geometryInput = document.getElementById('form-geometry'); // Set geometry IMMEDIATELY so form can save if (geometryInput) geometryInput.value = JSON.stringify(geometry); // Now fetch area metrics try { const result = await calculateArea(geometry); // API returns: { area: { area_ha, area_acres, ... }, perimeter: {...}, centroid: {...} } const areaData = result.area || result; const perimeterData = result.perimeter || {}; const centroidData = result.centroid || {}; const areaInput = document.getElementById('form-area'); const areaAuto = document.getElementById('area-auto'); const areaEquivalents = document.getElementById('area-equivalents'); const geometryMetrics = document.getElementById('geometry-metrics'); const perimeterEl = document.getElementById('metric-perimeter'); const centroidEl = document.getElementById('metric-centroid'); if (areaInput) areaInput.value = areaData.area_ha || ''; if (areaAuto) areaAuto.textContent = `(Auto-calculated)`; if (areaEquivalents) { areaEquivalents.classList.remove('hidden'); const bigha = areaData.area_bigha_assam || (areaData.area_ha * 7.4752).toFixed(2); const lecha = areaData.area_lecha_assam || Math.round(areaData.area_ha * 747.52); areaEquivalents.innerHTML = `
${areaData.area_ha || '?'} Ha
${areaData.area_acres || '?'} Acres
${areaData.area_guntha || '?'} Guntha
${bigha} Bigha
${lecha} Lecha
`; } if (geometryMetrics) geometryMetrics.classList.remove('hidden'); if (perimeterEl) perimeterEl.textContent = perimeterData.perimeter_m ? `${Math.round(perimeterData.perimeter_m)} m` : 'N/A'; if (centroidEl) centroidEl.textContent = centroidData.lat ? `${centroidData.lat.toFixed(5)}, ${centroidData.lng.toFixed(5)}` : 'N/A'; const drawStatus = document.getElementById('draw-status'); if (drawStatus) { drawStatus.innerHTML = `Parcel geometry captured. Area: ${areaData.area_ha} Ha. Ready to save.`; drawStatus.className = 'bg-green-50 border-l-4 border-green-500 rounded-r-lg p-3 text-sm text-green-800'; } // Trigger live valuation update in forms.js if (typeof updateLiveValuation === 'function') { updateLiveValuation(); } } catch (err) { console.error('Failed to fetch geometry metrics:', err); updateAutofillStatus('Failed to calculate area.', true); } } function clearMetricsUI() { const areaInput = document.getElementById('form-area'); const areaAuto = document.getElementById('area-auto'); const areaEquivalents = document.getElementById('area-equivalents'); const geometryMetrics = document.getElementById('geometry-metrics'); if (areaInput) areaInput.value = ''; if (areaAuto) areaAuto.textContent = '(Draw on map)'; if (areaEquivalents) { areaEquivalents.classList.add('hidden'); areaEquivalents.innerHTML = ''; } if (geometryMetrics) { geometryMetrics.classList.add('hidden'); } } function clearGeometrySelection(clearAddRecordMap) { if (drawnItems) { drawnItems.clearLayers(); } if (clearAddRecordMap && addRecordDrawnItems) { addRecordDrawnItems.clearLayers(); } currentSketchLayer = null; const geomInput = document.getElementById('form-geometry'); if (geomInput) geomInput.value = ''; clearMetricsUI(); } ================================================ FILE: static/js/modules/map_engine.js ================================================ /** * map_engine.js - Leaflet map initialization, layers, and drawing logic */ function switchBaseLayer(layerName) { if (!baseLayers[layerName] || layerName === currentBaseLayer) return; map.removeLayer(baseLayers[currentBaseLayer]); baseLayers[layerName].addTo(map); currentBaseLayer = layerName; } async function addIndiaMask(mapInstance) { try { const response = await fetch('/api/boundary'); if (!response.ok) return; const indiaGeoJSON = await response.json(); const worldBounds = [[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]]; let maskCoordinates = [worldBounds]; const geometry = indiaGeoJSON.features[0].geometry; if (geometry.type === 'MultiPolygon') { geometry.coordinates.forEach(polygon => { maskCoordinates.push(polygon[0]); }); } else if (geometry.type === 'Polygon') { maskCoordinates.push(geometry.coordinates[0]); } const maskGeoJSON = { type: 'Feature', geometry: { type: 'Polygon', coordinates: maskCoordinates } }; const maskLayer = L.geoJSON(maskGeoJSON, { style: { color: '#1f2937', weight: 2, fillColor: '#1f2937', fillOpacity: 0.7 }, interactive: false }); maskLayer.addTo(mapInstance); const indiaBorder = L.geoJSON(indiaGeoJSON, { style: { color: '#f97316', weight: 3, fillColor: 'transparent', fillOpacity: 0, opacity: 0.9 }, interactive: false }); indiaBorder.addTo(mapInstance); } catch (err) { console.warn('Failed to load India boundary mask:', err); } } function initMap(adminMode) { isAdmin = adminMode || false; const indiaBounds = L.latLngBounds(L.latLng(4.0, 60.0), L.latLng(40.0, 105.0)); map = L.map('map', { center: [23.5, 77.5], zoom: 7, minZoom: 4, maxZoom: 18, maxBounds: indiaBounds, maxBoundsViscosity: 1.0, zoomControl: true, attributionControl: true }); baseLayers.osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 19, crossOrigin: true }); baseLayers.google = L.tileLayer('https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', { attribution: '© Google Maps', maxZoom: 20, crossOrigin: true }); baseLayers.esri = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: '© Esri', maxZoom: 18, crossOrigin: true }); baseLayers.osm.addTo(map); currentBaseLayer = 'osm'; document.querySelectorAll('input[name="basemap"]').forEach(radio => { radio.addEventListener('change', function() { switchBaseLayer(this.value); }); }); addIndiaMask(map); drawnItems = new L.FeatureGroup(); map.addLayer(drawnItems); if (isAdmin) { // map.pm.addControls is removed to use custom draw tools box map.pm.setPathOptions({ color: '#ea580c', fillColor: '#fed7aa', fillOpacity: 0.3, weight: 3 }); map.on('pm:create', async function(e) { const layer = e.layer; drawnItems.clearLayers(); drawnItems.addLayer(layer); currentSketchLayer = layer; map.pm.disableDraw(); const geometry = layer.toGeoJSON().geometry; await updateGeometryMetrics(geometry, { shouldRefreshMetrics: true }); switchMainTab('add-record'); switchFormTab('parcel'); const inst = document.getElementById('draw-instruction'); if (inst) inst.style.display = 'none'; showToast('Polygon ready. Review parcel details and save.', 'success'); }); map.on('pm:edit', async function(e) { const layer = e.layer; if (!layer) return; currentSketchLayer = layer; const geometry = layer.toGeoJSON().geometry; await updateGeometryMetrics(geometry, { shouldRefreshMetrics: true }); }); map.on('pm:remove', function() { clearGeometrySelection(false); }); } map.on('mousemove', function(e) { const coordsEl = document.getElementById('cursor-coords'); if (coordsEl) coordsEl.textContent = `Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}`; }); if (isAdmin) { map.on('moveend', function() { scheduleMapLocationLookup(map.getCenter().lat, map.getCenter().lng, false); }); map.on('click', function(e) { if (map.pm && typeof map.pm.globalDrawModeEnabled === 'function' && map.pm.globalDrawModeEnabled()) return; scheduleMapLocationLookup(e.latlng.lat, e.latlng.lng, true); }); } loadRecordsOnMap(); } async function loadRecordsOnMap() { try { const records = await fetchRecords(); allRecordsCache = records; if (isAdmin) { syncLocationCatalogWithRecords(records); populateStateFilter(records); populateDistrictFilter(records); const { stateEl, districtEl, villageEl } = getFormElements(); refreshStateOptions(stateEl ? stateEl.value : ''); refreshDistrictOptions(districtEl ? districtEl.value : ''); refreshVillageOptions(villageEl ? villageEl.value : ''); applyAdminFilters(false); return; } clearMapLayers(); addRecordsToMap(records); } catch (err) { console.error('Failed to load records:', err); showToast('Failed to load land records.', 'error'); } } function clearMapLayers() { parcelLayers.forEach(layer => { map.removeLayer(layer); }); parcelLayers = []; } function addRecordsToMap(records) { records.forEach(record => { if (!record.geometry || record.geometry.type !== 'Polygon') return; const landUse = (record.attributes && record.attributes.land_use) || 'Agricultural'; const color = getLandUseColor(landUse); const geoJsonLayer = L.geoJSON(record.geometry, { style: { color: color, fillColor: color, fillOpacity: 0.25, weight: 2.5, opacity: 0.9 }, onEachFeature: function(_f, layer) { layer.on('click', () => onParcelClick(record, layer)); layer.on('mouseover', function() { this.setStyle({ fillOpacity: 0.45, weight: 3.5 }); }); layer.on('mouseout', function() { this.setStyle({ fillOpacity: 0.25, weight: 2.5 }); }); const attrs = record.attributes || {}; const owner = record.owner || {}; const loc = record.location || {}; layer.bindTooltip(`
${record.khasra_no || 'N/A'}
ULPIN:${record.ulpin || 'N/A'}
Land Use:${landUse}
Area:${attrs.area_ha || '?'} Ha
Owner:${owner.name || 'N/A'}
Location:${loc.village || '?'}, ${loc.district || '?'}
Click to view details →
`, { sticky: true, className: 'parcel-tooltip', direction: 'top', offset: [0, -10] }); } }); geoJsonLayer.addTo(map); geoJsonLayer._recordId = record._id; parcelLayers.push(geoJsonLayer); }); if (isAdmin) renderRecordsList(records); } function getLandUseColor(landUse) { const colors = { Agricultural: '#22c55e', Residential: '#3b82f6', Commercial: '#f59e0b', Industrial: '#8b5cf6', Government: '#ef4444', Forest: '#065f46', Wasteland: '#9ca3af' }; return colors[landUse] || '#6b7280'; } function onParcelClick(record) { if (isAdmin) { showAdminDetails(record); flyToRecord(record); } else if (typeof showViewerInfo === 'function') { showViewerInfo(record); } } function flyToRecord(record) { if (!record.geometry) return; const bounds = L.geoJSON(record.geometry).getBounds(); map.fitBounds(bounds, { padding: [60, 60], maxZoom: 16 }); } function initViewRecordMap(record) { const target = document.getElementById('view-record-map'); if (!target) return; if (viewRecordMap) { viewRecordMap.remove(); viewRecordMap = null; } const indiaBounds = L.latLngBounds(L.latLng(4.0, 60.0), L.latLng(40.0, 105.0)); viewRecordMap = L.map('view-record-map', { center: [23.5, 77.5], zoom: 7, minZoom: 4, maxZoom: 18, maxBounds: indiaBounds, maxBoundsViscosity: 1.0, zoomControl: true }); const layers = { osm: L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }), google: L.tileLayer('https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', { maxZoom: 20 }), esri: L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { maxZoom: 18 }) }; layers.osm.addTo(viewRecordMap); let current = 'osm'; document.querySelectorAll('input[name="view-basemap"]').forEach(radio => { radio.addEventListener('change', function() { if (layers[this.value] && this.value !== current) { viewRecordMap.removeLayer(layers[current]); layers[this.value].addTo(viewRecordMap); current = this.value; } }); }); viewRecordMap.on('mousemove', e => { const el = document.getElementById('view-cursor-coords'); if (el) el.textContent = `Lat: ${e.latlng.lat.toFixed(5)}, Lng: ${e.latlng.lng.toFixed(5)}`; }); if (record.geometry) { const lu = (record.attributes && record.attributes.land_use) || ''; const color = getLandUseColor(lu); const layer = L.geoJSON(record.geometry, { style: { color: color || '#ea580c', fillColor: color || '#ea580c', fillOpacity: 0.35, weight: 3 } }); layer.addTo(viewRecordMap); viewRecordMap.fitBounds(layer.getBounds(), { padding: [40, 40] }); } setTimeout(() => { viewRecordMap.invalidateSize(); addIndiaMask(viewRecordMap); }, 200); } function initAddRecordMap() { const target = document.getElementById('add-record-map'); if (!target) return; const indiaBounds = L.latLngBounds(L.latLng(4.0, 60.0), L.latLng(40.0, 105.0)); addRecordMap = L.map('add-record-map', { center: [23.5, 77.5], zoom: 7, minZoom: 4, maxZoom: 18, maxBounds: indiaBounds, maxBoundsViscosity: 1.0, zoomControl: true }); const layers = { osm: L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }), google: L.tileLayer('https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', { maxZoom: 20 }), esri: L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { maxZoom: 18 }) }; layers.osm.addTo(addRecordMap); document.querySelectorAll('input[name="add-basemap"]').forEach(r => { r.addEventListener('change', function() { Object.values(layers).forEach(l => addRecordMap.removeLayer(l)); layers[this.value].addTo(addRecordMap); }); }); addRecordDrawnItems = new L.FeatureGroup(); addRecordMap.addLayer(addRecordDrawnItems); // map.pm.addControls is removed to use custom draw tools box addRecordMap.pm.setPathOptions({ color: '#ea580c', fillColor: '#fed7aa', fillOpacity: 0.4, weight: 3 }); addRecordMap.on('pm:create', async e => { addRecordDrawnItems.clearLayers(); const layer = e.layer; addRecordDrawnItems.addLayer(layer); currentSketchLayer = layer; if (layer.pm) { layer.pm.enable(); } const geometry = layer.toGeoJSON().geometry; await updateGeometryMetricsForAddRecord(geometry); if (typeof scheduleMapLocationLookup === 'function') { const centroid = layer.getBounds().getCenter(); scheduleMapLocationLookup(centroid.lat, centroid.lng, true); } }); addRecordMap.on('pm:edit', async e => { currentSketchLayer = e.layer; await updateGeometryMetricsForAddRecord(e.layer.toGeoJSON().geometry); }); addRecordMap.on('moveend', function() { if (typeof scheduleMapLocationLookup === 'function') { scheduleMapLocationLookup(addRecordMap.getCenter().lat, addRecordMap.getCenter().lng, false); } }); addRecordMap.on('click', function(e) { if (addRecordMap.pm && typeof addRecordMap.pm.globalDrawModeEnabled === 'function' && addRecordMap.pm.globalDrawModeEnabled()) return; if (typeof scheduleMapLocationLookup === 'function') { scheduleMapLocationLookup(e.latlng.lat, e.latlng.lng, true); } }); const startDrawBtn = document.getElementById('btn-start-draw-new'); if (startDrawBtn) { startDrawBtn.addEventListener('click', function() { addRecordDrawnItems.clearLayers(); const formGeom = document.getElementById('form-geometry'); if (formGeom) formGeom.value = ''; if (typeof clearMetricsUI === 'function') clearMetricsUI(); addRecordMap.pm.enableDraw('Polygon', { snappable: true, snapDistance: 20, continueDrawing: false, allowSelfIntersection: false, finishOn: 'dblclick' }); const drawStatus = document.getElementById('draw-status'); if (drawStatus) { drawStatus.innerHTML = 'Drawing mode active. Click to add points. Double-click to finish.'; drawStatus.className = 'bg-yellow-50 border-l-4 border-yellow-500 rounded-r-lg p-3 text-sm text-yellow-800'; } }); } const finishDrawBtn = document.getElementById('btn-finish-draw-new'); if (finishDrawBtn) { finishDrawBtn.addEventListener('click', function() { addRecordMap.pm.disableDraw(); }); } const cancelDrawBtn = document.getElementById('btn-cancel-draw-new'); if (cancelDrawBtn) { cancelDrawBtn.addEventListener('click', function() { addRecordMap.pm.disableDraw(); addRecordDrawnItems.clearLayers(); currentSketchLayer = null; const formGeom = document.getElementById('form-geometry'); if (formGeom) formGeom.value = ''; if (typeof clearMetricsUI === 'function') clearMetricsUI(); const drawStatus = document.getElementById('draw-status'); if (drawStatus) { drawStatus.innerHTML = 'Waiting for map drawing... Draw a parcel on the map to continue.'; drawStatus.className = 'bg-blue-50 border-l-4 border-blue-500 rounded-r-lg p-3 text-sm text-blue-800'; } }); } const clearDrawBtn = document.getElementById('btn-clear-draw-new'); if (clearDrawBtn) { clearDrawBtn.addEventListener('click', function() { addRecordDrawnItems.clearLayers(); currentSketchLayer = null; const formGeom = document.getElementById('form-geometry'); if (formGeom) formGeom.value = ''; if (typeof clearMetricsUI === 'function') clearMetricsUI(); const drawStatus = document.getElementById('draw-status'); if (drawStatus) { drawStatus.innerHTML = 'Waiting for map drawing... Draw a parcel on the map to continue.'; drawStatus.className = 'bg-blue-50 border-l-4 border-blue-500 rounded-r-lg p-3 text-sm text-blue-800'; } }); } setTimeout(() => { addRecordMap.invalidateSize(); addIndiaMask(addRecordMap); }, 200); } ================================================ FILE: static/js/modules/records.js ================================================ /** * records.js - Records list and filtering logic */ function ensureLocationInCatalog(state, district, village) { if (!state || !district || !village) return; if (!locationCatalog[state]) { locationCatalog[state] = {}; } if (!locationCatalog[state][district]) { locationCatalog[state][district] = []; } if (!locationCatalog[state][district].includes(village)) { locationCatalog[state][district].push(village); locationCatalog[state][district] = sortedValues(locationCatalog[state][district]); } } function syncLocationCatalogWithRecords(records) { records.forEach(rec => { const loc = rec.location || {}; ensureLocationInCatalog(loc.state, loc.district, loc.village); }); } function populateStateFilter(records) { const stateFilterEl = document.getElementById('state-filter'); if (!stateFilterEl) return; const selected = stateFilterEl.value || ''; const states = sortedValues(records .map(rec => (rec.location && rec.location.state) || '') .filter(Boolean)); stateFilterEl.innerHTML = ''; states.forEach(state => { const option = document.createElement('option'); option.value = state; option.textContent = state; stateFilterEl.appendChild(option); }); if (selected && states.includes(selected)) { stateFilterEl.value = selected; } } function populateDistrictFilter(records) { const districtFilterEl = document.getElementById('district-filter'); if (!districtFilterEl) return; const selected = districtFilterEl.value || ''; const districts = sortedValues(records .map(rec => (rec.location && rec.location.district) || '') .filter(Boolean)); districtFilterEl.innerHTML = ''; districts.forEach(district => { const option = document.createElement('option'); option.value = district; option.textContent = district; districtFilterEl.appendChild(option); }); if (selected && districts.includes(selected)) { districtFilterEl.value = selected; } } function populateVillageFilter(records) { const villageFilterEl = document.getElementById('village-filter'); if (!villageFilterEl) return; const selected = villageFilterEl.value || ''; const villages = sortedValues(records .map(rec => (rec.location && rec.location.village) || '') .filter(Boolean)); villageFilterEl.innerHTML = ''; villages.forEach(v => { const option = document.createElement('option'); option.value = v; option.textContent = v; villageFilterEl.appendChild(option); }); if (selected && villages.includes(selected)) { villageFilterEl.value = selected; } } function getAdminFilterState() { const query = (document.getElementById('record-search') || {}).value || ''; const landUse = (document.getElementById('land-use-filter') || {}).value || ''; const state = (document.getElementById('state-filter') || {}).value || ''; const district = (document.getElementById('district-filter') || {}).value || ''; const village = (document.getElementById('village-filter') || {}).value || ''; return { query: query.trim().toLowerCase(), landUse, state, district, village }; } function initializeRecordFilters() { const filters = ['state-filter', 'district-filter', 'village-filter', 'land-use-filter']; filters.forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener('change', () => applyAdminFilters(true)); }); const searchInput = document.getElementById('record-search'); if (searchInput) { searchInput.addEventListener('input', debounce(() => applyAdminFilters(true), 400)); } const clearBtn = document.getElementById('btn-clear-filters'); if (clearBtn) { clearBtn.addEventListener('click', () => { filters.forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; }); if (searchInput) searchInput.value = ''; applyAdminFilters(true); }); } } function filterRecordsByState(records, filterState) { return records.filter(rec => { const attrs = rec.attributes || {}; const loc = rec.location || {}; const owner = rec.owner || {}; const searchText = [ rec.khasra_no, rec.ulpin, rec.khata_no, loc.village, loc.district, loc.state, owner.name, attrs.land_use ].join(' ').toLowerCase(); const queryMatch = !filterState.query || searchText.includes(filterState.query); const landUseMatch = !filterState.landUse || attrs.land_use === filterState.landUse; const stateMatch = !filterState.state || loc.state === filterState.state; const districtMatch = !filterState.district || loc.district === filterState.district; return queryMatch && landUseMatch && stateMatch && districtMatch; }); } function switchRecordsView(mode) { const cardsContainer = document.getElementById('records-list-cards'); const tableContainer = document.getElementById('records-list-table'); if (!cardsContainer || !tableContainer) return; recordsViewMode = mode === 'table' ? 'table' : 'cards'; cardsContainer.classList.toggle('hidden', recordsViewMode !== 'cards'); tableContainer.classList.toggle('hidden', recordsViewMode !== 'table'); document.querySelectorAll('.records-view-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.view === recordsViewMode); }); } function renderRecordsList(records) { const cardsEl = document.getElementById('records-list-cards'); const tableEl = document.getElementById('records-list-table'); const noRecordsEl = document.getElementById('no-records'); const countLabel = document.getElementById('records-count-label'); if (!cardsEl || !tableEl) return; if (countLabel) { countLabel.textContent = String(records.length); } if (records.length === 0) { cardsEl.innerHTML = ''; tableEl.innerHTML = ''; if (noRecordsEl) noRecordsEl.classList.remove('hidden'); return; } if (noRecordsEl) noRecordsEl.classList.add('hidden'); cardsEl.innerHTML = records.map(rec => { const attrs = rec.attributes || {}; const owner = rec.owner || {}; const loc = rec.location || {}; const landUse = attrs.land_use || 'Unknown'; const badgeClass = 'badge-' + landUse.toLowerCase(); return `
${rec.khasra_no || 'N/A'} ${rec.deleted ? `Deleted` : ''}
${landUse}
ULPIN: ${rec.ulpin || 'N/A'}
${owner.name || 'No Owner'} | ${attrs.area_ha || '?'} Ha
${loc.village || ''}, ${loc.district || ''}
${loc.state || ''} • ${loc.district || ''}
`; }).join(''); // Attach click handlers for view buttons cardsEl.querySelectorAll('.view-record-btn').forEach((btn, index) => { btn.addEventListener('click', function(e) { e.stopPropagation(); const card = this.closest('.record-card'); if (card) { viewRecordDetails(records[index]._id); } }); }); tableEl.innerHTML = records.map((rec, index) => { const attrs = rec.attributes || {}; const loc = rec.location || {}; const landUse = attrs.land_use || 'Unknown'; const recordValue = calculateValuation(attrs.area_ha, attrs.circle_rate_inr, landUse); return `

${escapeHtml(rec.khasra_no || 'N/A')} ${rec.deleted ? 'Deleted' : ''}

${escapeHtml(rec.ulpin || 'N/A')} | ${escapeHtml(loc.district || 'N/A')}

${escapeHtml(landUse)} ${asNumber(attrs.area_ha).toFixed(2)} Ha Rs. ${formatInr(recordValue)}
`; }).join(''); // Attach click handlers for table view buttons tableEl.querySelectorAll('.view-record-btn-table').forEach((btn, index) => { btn.addEventListener('click', function(e) { e.stopPropagation(); const row = this.closest('.record-table-row'); if (row) { viewRecordDetails(records[index]._id); } }); }); switchRecordsView(recordsViewMode); } function applyAdminFilters(notify) { if (!isAdmin) return; const filterState = getAdminFilterState(); // Use server-side filtering fetchFilteredRecords({ state: filterState.query ? '' : filterState.state, district: filterState.query ? '' : filterState.district, village: filterState.query ? '' : filterState.village, land_use: filterState.landUse, search: filterState.query }).then(filtered => { filteredRecordsCache = filtered; // Update map clearMapLayers(); addRecordsToMap(filtered); // Update records list renderRecordsList(filtered); // Update dashboard from server fetchDashboardAnalytics({ state: filterState.state, land_use: filterState.landUse, district: filterState.district, search: filterState.query }).then(analytics => { renderKpiCardsFromServer(analytics.kpis); renderLandUseDistributionFromServer(analytics.land_use_distribution); renderDistrictOverviewFromServer(analytics.district_overview); renderTopValueParcelFromServer(analytics.top_parcel); renderRecentMutationsFromServer(analytics.recent_mutations); }).catch(err => { console.error('Dashboard analytics failed:', err); }); if (notify) { showToast(`Showing ${filtered.length} record(s).`, 'info'); } }).catch(err => { console.error('Filter failed:', err); showToast('Failed to filter records.', 'error'); }); } ================================================ FILE: static/js/modules/state.js ================================================ /** * state.js - Global state for India LIMS */ let map = null; let viewRecordMap = null; let addRecordMap = null; let addRecordDrawnItems = null; let baseLayers = {}; let currentBaseLayer = null; let parcelLayers = []; let drawnItems = null; let currentSketchLayer = null; let isAdmin = false; let selectedRecordId = null; let selectedRecord = null; let reverseGeocodeTimer = null; let lastGeocodeKey = ''; let lastAutoLocation = { state: '', district: '', village: '' }; let lastGpsDetectedLocation = { state: '', district: '', village: '' }; let allRecordsCache = []; let filteredRecordsCache = []; let recordsViewMode = 'cards'; let locationCatalog = {}; let currentProfile = null; let allUsers = []; let sessionUsername = ''; const DEFAULT_SNAP_DISTANCE = 20; ================================================ FILE: static/js/modules/utils.js ================================================ /** * utils.js - General utility functions for India LIMS */ function showConfirmModal(message, onConfirm) { const overlay = document.createElement('div'); overlay.className = 'fixed inset-0 bg-black bg-opacity-50 z-[9999] flex items-center justify-center p-4'; overlay.style.backdropFilter = 'blur(2px)'; const modal = document.createElement('div'); modal.className = 'bg-white rounded-lg shadow-xl max-w-sm w-full overflow-hidden fade-in'; const content = document.createElement('div'); content.className = 'p-6'; const iconContainer = document.createElement('div'); iconContainer.className = 'mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 mb-4'; iconContainer.innerHTML = ''; const textContainer = document.createElement('div'); textContainer.className = 'text-center'; const title = document.createElement('h3'); title.className = 'text-lg leading-6 font-medium text-gray-900 mb-2'; title.textContent = 'Confirm Action'; const messageEl = document.createElement('p'); messageEl.className = 'text-sm text-gray-500 whitespace-pre-line'; messageEl.textContent = message; textContainer.appendChild(title); textContainer.appendChild(messageEl); content.appendChild(iconContainer); content.appendChild(textContainer); const buttonsContainer = document.createElement('div'); buttonsContainer.className = 'bg-gray-50 px-4 py-3 sm:px-6 flex flex-row-reverse gap-2'; const confirmBtn = document.createElement('button'); confirmBtn.className = 'w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none sm:w-auto sm:text-sm transition-colors cursor-pointer'; confirmBtn.textContent = 'Confirm'; const cancelBtn = document.createElement('button'); cancelBtn.className = 'w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:w-auto sm:text-sm transition-colors cursor-pointer'; cancelBtn.textContent = 'Cancel'; buttonsContainer.appendChild(confirmBtn); buttonsContainer.appendChild(cancelBtn); modal.appendChild(content); modal.appendChild(buttonsContainer); overlay.appendChild(modal); document.body.appendChild(overlay); const close = () => document.body.removeChild(overlay); confirmBtn.addEventListener('click', () => { close(); if (onConfirm) onConfirm(); }); cancelBtn.addEventListener('click', close); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); } function sortedValues(values) { return [...new Set(values)].sort((a, b) => a.localeCompare(b)); } function asNumber(value) { const parsed = parseFloat(value); return Number.isFinite(parsed) ? parsed : 0; } function formatInr(value) { return Math.round(value).toLocaleString('en-IN'); } function escapeHtml(value) { return String(value || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function debounce(fn, wait = 200) { let t = null; return function(...args) { const ctx = this; clearTimeout(t); t = setTimeout(() => fn.apply(ctx, args), wait); }; } function updateAutofillStatus(message, isError) { const statusEl = document.getElementById('map-autofill-status'); if (!statusEl) return; statusEl.textContent = message || ''; statusEl.classList.toggle('text-red-600', !!isError); statusEl.classList.toggle('text-gray-500', !isError); } function fileToBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => resolve(reader.result); reader.onerror = error => reject(error); }); } function calculateValuation(areaHa, circleRateInr, landUse) { const multipliers = { 'Commercial': 2.5, 'Industrial': 1.8, 'Residential': 1.5, 'Agricultural': 1.0, 'Government': 1.2, 'Forest': 0.8, 'Wasteland': 0.5 }; const multiplier = multipliers[landUse] || 1.0; return asNumber(areaHa) * asNumber(circleRateInr) * multiplier; } ================================================ FILE: templates/admin_dashboard.html ================================================ LIMS - Admin Dashboard

Welcome, {{ username }}

Monitor parcels and process mutations instantly.

Total Parcels

0

Total Area (Ha)

0.00

Estimated Value (INR)

0

Mutations

0

Land Type Distribution

District Overview

Top Value Parcel

No parcel data yet.

Recent Mutations

================================================ FILE: templates/login.html ================================================ LIMS - Login

LIMS

Land Information Management System

PUBLIC ACCESS

View Land Records

Search and view property details on the map. No account needed.

{{ captcha_question }}

Viewers can search parcels and view basic details. Owner information is partially masked.

LIMS - Academic Prototype

Student Project - Demonstration Only

================================================ FILE: templates/public_viewer_v2.html ================================================ LIMS - Public Viewer

Filter Parcels

Showing

0

parcels

LIMS v1.0

Land Use Legend

Agricultural
Residential
Commercial
Industrial
Government
Forest
Wasteland

Property Details

================================================ FILE: tests/Testing_Report_20260425_211328.md ================================================ # INDIA LIMS - AUTOMATED TESTING REPORT **Date/Time:** 2026-04-25 21:13:58 ## 1. System Test Cases Summary ### Test Target: `test_logins` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Valid Login (Premithiews) | Submit correct credentials | 200 | 401 | ❌ FAIL | | Valid Login (admin) | Submit correct credentials | 200 | 200 | ✅ PASS | | Valid Login (recovery_sa_20260425_050757.698283Z) | Submit correct credentials | 200 | 401 | ❌ FAIL | | Invalid Username | Submit non-existent username | 401 | 401 | ✅ PASS | | Invalid Password | Submit wrong password for Premithiews | 401 | 401 | ✅ PASS | ### Test Target: `run_role_tests` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | SuperAdmin Setup | Login as SuperAdmin | 200 | 200 | ✅ PASS | | SuperAdmin Action | Create Officer account | 201 | 201 | ✅ PASS | | SuperAdmin Action | Create Admin account | 201 | 201 | ✅ PASS | | SuperAdmin Security | Attempt create another SuperAdmin | 201 | 409 | ❌ FAIL | | SuperAdmin Security | Attempt delete SuperAdmin account | 404 | 404 | ✅ PASS | | Admin Hierarchy | Update Officer details | 200 | 200 | ✅ PASS | | Admin Hierarchy | Attempt create another Admin | 403 | 403 | ✅ PASS | | Admin Hierarchy | Attempt delete SuperAdmin | 404 | 404 | ✅ PASS | | Officer Access | Attempt fetch user list | 403 | 403 | ✅ PASS | ### Test Target: `test_recovery_flow` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Recovery Login | Login with recovery creds | 200 | 401 | ❌ FAIL | ### Test Target: `test_record_soft_delete` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Admin Setup | Login as Admin | 200 | 200 | ✅ PASS | | Record Setup | Admin POST /api/records | 201 | 201 | ✅ PASS | | Soft Delete | Officer DELETE record | 200 | 200 | ✅ PASS | | Data Privacy | Officer GET records | Hidden | Hidden | ✅ PASS | | Hard Delete | Admin hard delete | 200 | 200 | ✅ PASS | ### Test Target: `test_restore_flow` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Restore Record | Admin POST restore | 200 | 200 | ✅ PASS | | Verify Restore | Admin GET record | deleted=False | deleted=False | ✅ PASS | ### Test Target: `check_users_roles` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Data Integrity | Check user admin | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user temp_off_del | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user new_sa | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user off_519a5f | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user off_3a9b28 | Has Role | Has Role | ✅ PASS | --- ## 2. Detailed Execution Logs ### tests/test_logins.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Testing valid login for: Premithiews Status: 401 {'error': 'Invalid username or password.'} Testing valid login for: admin Status: 200 {'redirect': '/admin', 'success': True} Testing valid login for: recovery_sa_20260425_050757.698283Z Status: 401 {'error': 'Invalid username or password.'} Testing Invalid Username Status: 401 {'error': 'Invalid username or password.'} Testing Invalid Password for user: Premithiews Status: 401 {'error': 'Invalid username or password.'} ``` --- ### tests/run_role_tests.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. --- PHASE 1: SuperAdmin Setup --- --- PHASE 2: SuperAdmin Security Checks --- --- PHASE 3: Standard Admin Hierarchy Check --- --- PHASE 4: Officer Access Check --- --- PHASE 5: Cleanup --- Cleanup completed. ``` --- ### tests/test_recovery_flow.py **Script Exit Status:** ❌ FAIL (Exit Code 2) **Standard Output:** ```text Successfully connected to MongoDB Cluster. ``` --- ### tests/test_record_soft_delete.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Admin login... 200 Create officer... 201 Create record... 201 Officer login... Officer delete attempt... 200 Verify record is hidden... Record hidden: True Admin hard delete... 200 ``` --- ### tests/test_restore_flow.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Admin login... Officer soft delete... Admin restore... 200 ``` --- ### tests/check_users_roles.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Found User: admin (superadmin) Found User: temp_off_del (officer) Found User: new_sa (superadmin) Found User: off_519a5f (officer) Found User: off_3a9b28 (officer) ``` --- ================================================ FILE: tests/Testing_Report_20260426_013232.md ================================================ # INDIA LIMS - AUTOMATED TESTING REPORT **Date/Time:** 2026-04-26 01:32:54 ## 1. System Test Cases Summary ### Test Target: `test_logins` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Valid Login (admin) | Submit correct credentials | 200 | 401 | ❌ FAIL | | Valid Login (recovery_sa_20260425_050757.698283Z) | Submit correct credentials | 200 | 401 | ❌ FAIL | | Invalid Username | Submit non-existent username | 401 | 401 | ✅ PASS | | Invalid Password | Submit wrong password for admin | 401 | 401 | ✅ PASS | ### Test Target: `run_role_tests` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | SuperAdmin Setup | Login as SuperAdmin | 200 | 401 | ❌ FAIL | | SuperAdmin Action | Create Officer account | 201 | 403 | ❌ FAIL | | SuperAdmin Action | Create Admin account | 201 | 403 | ❌ FAIL | | SuperAdmin Security | Attempt create another SuperAdmin | 201 | 403 | ❌ FAIL | | SuperAdmin Security | Attempt delete SuperAdmin account | 404 | 403 | ❌ FAIL | | Admin Hierarchy | Update Officer details | 200 | 403 | ❌ FAIL | | Admin Hierarchy | Attempt create another Admin | 403 | 403 | ✅ PASS | | Admin Hierarchy | Attempt delete SuperAdmin | 404 | 403 | ❌ FAIL | | Officer Access | Attempt fetch user list | 403 | 403 | ✅ PASS | ### Test Target: `test_recovery_flow` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Recovery Login | Login with recovery creds | 200 | 401 | ❌ FAIL | ### Test Target: `test_record_soft_delete` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Admin Setup | Login as Admin | 200 | 401 | ❌ FAIL | | Record Setup | Admin POST /api/records | 201 | 403 | ❌ FAIL | ### Test Target: `check_users_roles` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Data Integrity | Check user recovery_sa_20260425_154817.302101Z | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user admin | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user Premithiews | Has Role | Has Role | ✅ PASS | --- ## 2. Detailed Execution Logs ### tests/test_logins.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Testing valid login for: admin Status: 401 {'error': 'Invalid username or password.'} Testing valid login for: recovery_sa_20260425_050757.698283Z Status: 401 {'error': 'Invalid username or password.'} Testing Invalid Username Status: 401 {'error': 'Invalid username or password.'} Testing Invalid Password for user: admin Status: 401 {'error': 'Invalid username or password.'} ``` --- ### tests/run_role_tests.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. --- PHASE 1: SuperAdmin Setup --- --- PHASE 2: SuperAdmin Security Checks --- --- PHASE 3: Standard Admin Hierarchy Check --- --- PHASE 4: Officer Access Check --- --- PHASE 5: Cleanup --- Cleanup completed. ``` --- ### tests/test_recovery_flow.py **Script Exit Status:** ❌ FAIL (Exit Code 2) **Standard Output:** ```text Successfully connected to MongoDB Cluster. ``` --- ### tests/test_record_soft_delete.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Admin login... 401 Create officer... 403 Create record... 403 ``` --- ### tests/test_restore_flow.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Admin login... ``` --- ### tests/check_users_roles.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Found User: recovery_sa_20260425_154817.302101Z (superadmin) Found User: admin (superadmin) Found User: Premithiews (admin) ``` --- ================================================ FILE: tests/Testing_Report_20260426_095300.md ================================================ # INDIA LIMS - AUTOMATED TESTING REPORT **Date/Time:** 2026-04-26 09:53:35 ## 1. System Test Cases Summary ### Test Target: `test_logins` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Valid Login (Premithiews) | Submit correct credentials | 200 | 200 | ✅ PASS | | Valid Login (admin) | Submit correct credentials | 200 | 200 | ✅ PASS | | Valid Login (recovery_sa_20260425_154817.302101Z) | Submit correct credentials | 200 | 200 | ✅ PASS | | Invalid Username | Submit non-existent username | 401 | 401 | ✅ PASS | | Invalid Password | Submit wrong password for Premithiews | 401 | 401 | ✅ PASS | ### Test Target: `robust_role_test` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Discovery | Create temp Admin | 201 | 201 | ✅ PASS | | Provisioning | Create temp Officer | 201 | 201 | ✅ PASS | | Hierarchy | Admin edits Officer | 200 | 200 | ✅ PASS | | Security | Admin attempts delete SuperAdmin | 403/404 | 403 | ✅ PASS | | Hierarchy | Admin creates peer Admin | 201 | 201 | ✅ PASS | ### Test Target: `test_recovery_flow` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Recovery Login | Login with recovery creds | 200 | 200 | ✅ PASS | | Create SuperAdmin | POST /api/users as recovery | 201 | 201 | ✅ PASS | | Delete SuperAdmin | DELETE /api/users/ as recovery | 200 | 200 | ✅ PASS | ### Test Target: `test_record_soft_delete` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Admin Setup | Login as Admin | 200 | 200 | ✅ PASS | | Record Setup | Admin POST /api/records | 201 | 201 | ✅ PASS | | Soft Delete | Officer DELETE record | 200 | 200 | ✅ PASS | | Data Privacy | Officer GET records | Hidden | Hidden | ✅ PASS | | Hard Delete | Admin hard delete | 200 | 200 | ✅ PASS | ### Test Target: `test_restore_flow` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Restore Record | Admin POST restore | 200 | 200 | ✅ PASS | | Verify Restore | Admin GET record | deleted=False | deleted=False | ✅ PASS | ### Test Target: `check_users_roles` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Data Integrity | Check user recovery_sa_20260425_154817.302101Z | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user Premithiews | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user admin | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user peer_db4f | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user peer_d789 | Has Role | Has Role | ✅ PASS | --- ## 2. Detailed Execution Logs ### tests/test_logins.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Testing valid login for: Premithiews Status: 200 {'redirect': '/admin', 'success': True} Testing valid login for: admin Status: 200 {'redirect': '/admin', 'success': True} Testing valid login for: recovery_sa_20260425_154817.302101Z Status: 200 {'redirect': '/admin', 'success': True} Testing Invalid Username Status: 401 {'error': 'Invalid username or password.'} Testing Invalid Password for user: Premithiews Status: 401 {'error': 'Invalid username or password.'} ``` --- ### tests/robust_role_test.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. --- PHASE 1: Account Discovery --- Attempting login for: Premithiews Success! Discovered: Premithiews is superadmin Attempting login for: admin Success! Discovered: admin is superadmin Attempting login for: recovery_sa_20260425_154817.302101Z Success! Discovered: recovery_sa_20260425_154817.302101Z is superadmin --- PHASE 2: Dynamic Provisioning --- No Admin found. Creating temporary Admin: tmp_admin_a7d8 --- PHASE 3: Hierarchy Testing --- Test 1: Admin Managing Officer... Test 2: Admin cannot delete SuperAdmin... --- PHASE 4: Cleanup --- Cleanup completed. ``` --- ### tests/test_recovery_flow.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. ``` **Errors/Warnings:** ```text C:\Users\user\Desktop\zz\tests\test_recovery_flow.py:43: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). ts = datetime.utcnow().strftime('%Y%m%d%H%M%S') ``` --- ### tests/test_record_soft_delete.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Admin login... 200 Create officer... 201 Create record... 201 Officer login... Officer delete attempt... 200 Verify record is hidden... Record hidden: True Admin hard delete... 200 ``` --- ### tests/test_restore_flow.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Admin login... Officer soft delete... Admin restore... 200 ``` --- ### tests/check_users_roles.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Found User: recovery_sa_20260425_154817.302101Z (superadmin) Found User: Premithiews (superadmin) Found User: admin (superadmin) Found User: peer_db4f (admin) Found User: peer_d789 (admin) ``` --- ================================================ FILE: tests/Testing_Report_20260426_125456.md ================================================ # LIMS - AUTOMATED TESTING REPORT **Date/Time:** 2026-04-26 12:55:31 ## 1. System Test Cases Summary ### Test Target: `test_logins` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Valid Login (Premithiews) | Submit correct credentials | 200 | 200 | ✅ PASS | | Valid Login (admin) | Submit correct credentials | 200 | 200 | ✅ PASS | | Valid Login (recovery_sa_20260425_154817.302101Z) | Submit correct credentials | 200 | 200 | ✅ PASS | | Invalid Username | Submit non-existent username | 401 | 401 | ✅ PASS | | Invalid Password | Submit wrong password for Premithiews | 401 | 401 | ✅ PASS | ### Test Target: `robust_role_test` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Discovery | Create temp Admin | 201 | 201 | ✅ PASS | | Provisioning | Create temp Officer | 201 | 201 | ✅ PASS | | Hierarchy | Admin edits Officer | 200 | 200 | ✅ PASS | | Security | Admin attempts delete SuperAdmin | 403/404 | 403 | ✅ PASS | | Hierarchy | Admin creates peer Admin | 201 | 201 | ✅ PASS | ### Test Target: `test_recovery_flow` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Recovery Login | Login with recovery creds | 200 | 200 | ✅ PASS | | Create SuperAdmin | POST /api/users as recovery | 201 | 201 | ✅ PASS | | Delete SuperAdmin | DELETE /api/users/ as recovery | 200 | 200 | ✅ PASS | ### Test Target: `test_record_soft_delete` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Admin Setup | Login as Admin | 200 | 200 | ✅ PASS | | Record Setup | Admin POST /api/records | 201 | 201 | ✅ PASS | | Soft Delete | Officer DELETE record | 200 | 200 | ✅ PASS | | Data Privacy | Officer GET records | Hidden | Hidden | ✅ PASS | | Hard Delete | Admin hard delete | 200 | 200 | ✅ PASS | ### Test Target: `test_restore_flow` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Restore Record | Admin POST restore | 200 | 200 | ✅ PASS | | Verify Restore | Admin GET record | deleted=False | deleted=False | ✅ PASS | ### Test Target: `check_users_roles` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Data Integrity | Check user recovery_sa_20260425_154817.302101Z | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user Premithiews | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user admin | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user peer_db4f | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user peer_d789 | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user peer_05c0 | Has Role | Has Role | ✅ PASS | --- ## 2. Detailed Execution Logs ### tests/test_logins.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Testing valid login for: Premithiews Status: 200 {'redirect': '/admin', 'success': True} Testing valid login for: admin Status: 200 {'redirect': '/admin', 'success': True} Testing valid login for: recovery_sa_20260425_154817.302101Z Status: 200 {'redirect': '/admin', 'success': True} Testing Invalid Username Status: 401 {'error': 'Invalid username or password.'} Testing Invalid Password for user: Premithiews Status: 401 {'error': 'Invalid username or password.'} ``` --- ### tests/robust_role_test.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. --- PHASE 1: Account Discovery --- Attempting login for: Premithiews Success! Discovered: Premithiews is superadmin Attempting login for: admin Success! Discovered: admin is superadmin Attempting login for: recovery_sa_20260425_154817.302101Z Success! Discovered: recovery_sa_20260425_154817.302101Z is superadmin --- PHASE 2: Dynamic Provisioning --- No Admin found. Creating temporary Admin: tmp_admin_0a0b --- PHASE 3: Hierarchy Testing --- Test 1: Admin Managing Officer... Test 2: Admin cannot delete SuperAdmin... --- PHASE 4: Cleanup --- Cleanup completed. ``` --- ### tests/test_recovery_flow.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. ``` **Errors/Warnings:** ```text C:\Users\user\Desktop\zz\tests\test_recovery_flow.py:43: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). ts = datetime.utcnow().strftime('%Y%m%d%H%M%S') ``` --- ### tests/test_record_soft_delete.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Admin login... 200 Create officer... 201 Create record... 201 Officer login... Officer delete attempt... 200 Verify record is hidden... Record hidden: True Admin hard delete... 200 ``` --- ### tests/test_restore_flow.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Admin login... Officer soft delete... Admin restore... 200 ``` --- ### tests/check_users_roles.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Found User: recovery_sa_20260425_154817.302101Z (superadmin) Found User: Premithiews (superadmin) Found User: admin (superadmin) Found User: peer_db4f (admin) Found User: peer_d789 (admin) Found User: peer_05c0 (admin) ``` --- ================================================ FILE: tests/Testing_Report_20260426_132557.md ================================================ # LIMS - AUTOMATED TESTING REPORT **Date/Time:** 2026-04-26 13:25:59 ## 1. System Test Cases Summary *No test cases recorded.* --- ## 2. Detailed Execution Logs ### tests/test_logins.py **Script Exit Status:** ❌ FAIL (Exit Code 1) **Standard Output:** ```text Failed to import app: No module named 'dotenv' ``` --- ### tests/robust_role_test.py **Script Exit Status:** ❌ FAIL (Exit Code 1) **Errors/Warnings:** ```text Traceback (most recent call last): File "C:\Users\user\Desktop\zz\tests\robust_role_test.py", line 18, in from app import app File "C:\Users\user\Desktop\zz\app.py", line 12, in from config import SECRET_KEY, DEBUG, HOST, PORT, MONGO_URI File "C:\Users\user\Desktop\zz\config.py", line 7, in from dotenv import load_dotenv ModuleNotFoundError: No module named 'dotenv' ``` --- ### tests/test_recovery_flow.py **Script Exit Status:** ❌ FAIL (Exit Code 1) **Errors/Warnings:** ```text Traceback (most recent call last): File "C:\Users\user\Desktop\zz\tests\test_recovery_flow.py", line 10, in from app import app File "C:\Users\user\Desktop\zz\app.py", line 12, in from config import SECRET_KEY, DEBUG, HOST, PORT, MONGO_URI File "C:\Users\user\Desktop\zz\config.py", line 7, in from dotenv import load_dotenv ModuleNotFoundError: No module named 'dotenv' ``` --- ### tests/test_record_soft_delete.py **Script Exit Status:** ❌ FAIL (Exit Code 1) **Errors/Warnings:** ```text Traceback (most recent call last): File "C:\Users\user\Desktop\zz\tests\test_record_soft_delete.py", line 15, in from app import app File "C:\Users\user\Desktop\zz\app.py", line 12, in from config import SECRET_KEY, DEBUG, HOST, PORT, MONGO_URI File "C:\Users\user\Desktop\zz\config.py", line 7, in from dotenv import load_dotenv ModuleNotFoundError: No module named 'dotenv' ``` --- ### tests/test_restore_flow.py **Script Exit Status:** ❌ FAIL (Exit Code 1) **Errors/Warnings:** ```text Traceback (most recent call last): File "C:\Users\user\Desktop\zz\tests\test_restore_flow.py", line 11, in from app import app File "C:\Users\user\Desktop\zz\app.py", line 12, in from config import SECRET_KEY, DEBUG, HOST, PORT, MONGO_URI File "C:\Users\user\Desktop\zz\config.py", line 7, in from dotenv import load_dotenv ModuleNotFoundError: No module named 'dotenv' ``` --- ### tests/check_users_roles.py **Script Exit Status:** ❌ FAIL (Exit Code 1) **Errors/Warnings:** ```text Traceback (most recent call last): File "C:\Users\user\Desktop\zz\tests\check_users_roles.py", line 6, in from config import MONGO_URI ModuleNotFoundError: No module named 'config' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "C:\Users\user\Desktop\zz\tests\check_users_roles.py", line 10, in from config import MONGO_URI File "C:\Users\user\Desktop\zz\config.py", line 7, in from dotenv import load_dotenv ModuleNotFoundError: No module named 'dotenv' ``` --- ================================================ FILE: tests/Testing_Report_20260426_132640.md ================================================ # LIMS - AUTOMATED TESTING REPORT **Date/Time:** 2026-04-26 13:27:17 ## 1. System Test Cases Summary ### Test Target: `test_logins` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Valid Login (Premithiews) | Submit correct credentials | 200 | 200 | ✅ PASS | | Valid Login (admin) | Submit correct credentials | 200 | 200 | ✅ PASS | | Valid Login (recovery_sa_20260425_154817.302101Z) | Submit correct credentials | 200 | 200 | ✅ PASS | | Invalid Username | Submit non-existent username | 401 | 401 | ✅ PASS | | Invalid Password | Submit wrong password for Premithiews | 401 | 401 | ✅ PASS | ### Test Target: `robust_role_test` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Discovery | Create temp Admin | 201 | 201 | ✅ PASS | | Provisioning | Create temp Officer | 201 | 201 | ✅ PASS | | Hierarchy | Admin edits Officer | 200 | 200 | ✅ PASS | | Security | Admin attempts delete SuperAdmin | 403/404 | 403 | ✅ PASS | | Hierarchy | Admin creates peer Admin | 201 | 201 | ✅ PASS | ### Test Target: `test_recovery_flow` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Recovery Login | Login with recovery creds | 200 | 200 | ✅ PASS | | Create SuperAdmin | POST /api/users as recovery | 201 | 201 | ✅ PASS | | Delete SuperAdmin | DELETE /api/users/ as recovery | 200 | 200 | ✅ PASS | ### Test Target: `test_record_soft_delete` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Admin Setup | Login as Admin | 200 | 200 | ✅ PASS | | Record Setup | Admin POST /api/records | 201 | 201 | ✅ PASS | | Soft Delete | Officer DELETE record | 200 | 200 | ✅ PASS | | Data Privacy | Officer GET records | Hidden | Hidden | ✅ PASS | | Hard Delete | Admin hard delete | 200 | 200 | ✅ PASS | ### Test Target: `test_restore_flow` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Restore Record | Admin POST restore | 200 | 200 | ✅ PASS | | Verify Restore | Admin GET record | deleted=False | deleted=False | ✅ PASS | ### Test Target: `check_users_roles` | Scenario | Action / Input | Expected | Actual | Result | |---|---|---|---|---| | Data Integrity | Check user recovery_sa_20260425_154817.302101Z | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user Premithiews | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user admin | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user peer_db4f | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user peer_d789 | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user peer_05c0 | Has Role | Has Role | ✅ PASS | | Data Integrity | Check user peer_e39b | Has Role | Has Role | ✅ PASS | --- ## 2. Detailed Execution Logs ### tests/test_logins.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Testing valid login for: Premithiews Status: 200 {'redirect': '/admin', 'success': True} Testing valid login for: admin Status: 200 {'redirect': '/admin', 'success': True} Testing valid login for: recovery_sa_20260425_154817.302101Z Status: 200 {'redirect': '/admin', 'success': True} Testing Invalid Username Status: 401 {'error': 'Invalid username or password.'} Testing Invalid Password for user: Premithiews Status: 401 {'error': 'Invalid username or password.'} ``` --- ### tests/robust_role_test.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. --- PHASE 1: Account Discovery --- Attempting login for: Premithiews Success! Discovered: Premithiews is superadmin Attempting login for: admin Success! Discovered: admin is superadmin Attempting login for: recovery_sa_20260425_154817.302101Z Success! Discovered: recovery_sa_20260425_154817.302101Z is superadmin --- PHASE 2: Dynamic Provisioning --- No Admin found. Creating temporary Admin: tmp_admin_0486 --- PHASE 3: Hierarchy Testing --- Test 1: Admin Managing Officer... Test 2: Admin cannot delete SuperAdmin... --- PHASE 4: Cleanup --- Cleanup completed. ``` --- ### tests/test_recovery_flow.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. ``` **Errors/Warnings:** ```text C:\Users\user\Desktop\zz\tests\test_recovery_flow.py:43: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC). ts = datetime.utcnow().strftime('%Y%m%d%H%M%S') ``` --- ### tests/test_record_soft_delete.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Admin login... 200 Create officer... 201 Create record... 201 Officer login... Officer delete attempt... 200 Verify record is hidden... Record hidden: True Admin hard delete... 200 ``` --- ### tests/test_restore_flow.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Successfully connected to MongoDB Cluster. Admin login... Officer soft delete... Admin restore... 200 ``` --- ### tests/check_users_roles.py **Script Exit Status:** ✅ PASS **Standard Output:** ```text Found User: recovery_sa_20260425_154817.302101Z (superadmin) Found User: Premithiews (superadmin) Found User: admin (superadmin) Found User: peer_db4f (admin) Found User: peer_d789 (admin) Found User: peer_05c0 (admin) Found User: peer_e39b (admin) ``` --- ================================================ FILE: tests/check_users_roles.py ================================================ r""" Simple verifier: list users and their stored roles """ import os, sys, json try: from config import MONGO_URI from utils import resource_path except Exception: sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from config import MONGO_URI from utils import resource_path def report_case(scenario, action, expected, actual, passed): status = "PASS" if passed else "FAIL" print(f"__TEST_RESULT__||{scenario}||{action}||{expected}||{actual}||{status}") def check(): if MONGO_URI: from pymongo import MongoClient import certifi client = MongoClient(MONGO_URI, tlsCAFile=certifi.where(), serverSelectionTimeoutMS=5000) users = list(client.get_database("indialims").users.find({})) client.close() else: with open(os.path.join(resource_path("data"), "users.json"), "r", encoding="utf-8") as f: users = json.load(f) for u in users: uid = u.get('user_id') or u.get('_id') uname = u.get('username') role = u.get('role') print(f"Found User: {uname} ({role})") report_case("Data Integrity", f"Check user {uname}", "Has Role", "Has Role" if role else "No Role", bool(role)) if __name__ == '__main__': check() ================================================ FILE: tests/generate_test_report.py ================================================ import os import sys import subprocess import datetime TEST_SCRIPTS = [ "tests/test_logins.py", "tests/robust_role_test.py", "tests/test_recovery_flow.py", "tests/test_record_soft_delete.py", "tests/test_restore_flow.py", "tests/check_users_roles.py" ] def main(): report_file = os.path.join("tests", f"Testing_Report_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.md") print(f"Generating Test Report: {report_file}") markdown_tables = [] detailed_logs = [] for script in TEST_SCRIPTS: if not os.path.exists(script): continue print(f"Running {script}...") try: result = subprocess.run([sys.executable, script], capture_output=True, text=True, check=False) stdout_lines = result.stdout.split('\n') table_rows = [] clean_stdout = [] for line in stdout_lines: if line.startswith("__TEST_RESULT__||"): parts = line.split("||") if len(parts) == 6: _, scenario, action, expected, actual, status = parts icon = "✅ PASS" if status == "PASS" else "❌ FAIL" table_rows.append(f"| {scenario} | {action} | {expected} | {actual} | {icon} |") else: clean_stdout.append(line) if table_rows: script_name = os.path.basename(script).replace('.py', '') markdown_tables.append(f"### Test Target: `{script_name}`\n") markdown_tables.append("| Scenario | Action / Input | Expected | Actual | Result |") markdown_tables.append("|---|---|---|---|---|") markdown_tables.extend(table_rows) markdown_tables.append("\n") detailed_logs.append({ "script": script, "status": "✅ PASS" if result.returncode == 0 else f"❌ FAIL (Exit Code {result.returncode})", "stdout": "\n".join(clean_stdout), "stderr": result.stderr }) except Exception as e: print(f"Error running {script}: {e}") with open(report_file, "w", encoding="utf-8") as rf: rf.write("# LIMS - AUTOMATED TESTING REPORT\n\n") rf.write(f"**Date/Time:** {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") rf.write("## 1. System Test Cases Summary\n\n") if not markdown_tables: rf.write("*No test cases recorded.*\n\n") for line in markdown_tables: rf.write(line + "\n") rf.write("---\n\n") rf.write("## 2. Detailed Execution Logs\n\n") for log in detailed_logs: rf.write(f"### {log['script']}\n\n") rf.write(f"**Script Exit Status:** {log['status']}\n\n") out_str = log["stdout"].strip() if out_str: rf.write("**Standard Output:**\n```text\n" + out_str + "\n```\n\n") err_str = log["stderr"].strip() if err_str: rf.write("**Errors/Warnings:**\n```text\n" + err_str + "\n```\n\n") rf.write("---\n\n") print(f"\nDone! Test report saved to: {report_file}") if __name__ == "__main__": main() ================================================ FILE: tests/robust_role_test.py ================================================ r""" robust_role_test.py - Improved Role-Based Access Testing Follows the user's logic: 1. Scan user_id_password.json to identify accounts. 2. Verify roles via API. 3. Dynamically create an Admin if none is found. 4. Test hierarchy: SuperAdmin > Admin > Officer. 5. Automatic cleanup. """ import os import sys import json import uuid # Add project root to path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app import app from core import load_users CRED_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "user_id_password.json") def report_case(scenario, action, expected, actual, passed): status = "PASS" if passed else "FAIL" print(f"__TEST_RESULT__||{scenario}||{action}||{expected}||{actual}||{status}") def run_tests(): # 1. Load Credentials if not os.path.exists(CRED_FILE): print(f"Error: {CRED_FILE} not found.") return with open(CRED_FILE, "r", encoding="utf-8") as f: creds = json.load(f) super_admin_cred = None existing_admin_cred = None with app.test_client() as client: print("--- PHASE 1: Account Discovery ---") for entry in creds: uname = entry.get("username") or entry.get("user_id") pwd = entry.get("password") # Try login print(f"Attempting login for: {uname}") resp = client.post("/api/login", json={"username": uname, "password": pwd}) if resp.status_code == 200: # Check role info = client.get("/api/session-info").get_json() role = (info.get("role") or "").lower() print(f" Success! Discovered: {uname} is {role}") if role == "superadmin" and not super_admin_cred: super_admin_cred = {"username": uname, "password": pwd} elif role == "admin" and not existing_admin_cred: existing_admin_cred = {"username": uname, "password": pwd} client.post("/api/logout") else: err_msg = "Unknown error" try: err_msg = resp.get_json().get('error', 'No error msg') except: pass print(f" Failed login for {uname}: {resp.status_code} - {err_msg}") if not super_admin_cred: print("Error: No SuperAdmin account found in user_id_password.json") return # 2. Dynamic Provisioning print("\n--- PHASE 2: Dynamic Provisioning ---") client.post("/api/login", json=super_admin_cred) temp_admin_uname = f"tmp_admin_{uuid.uuid4().hex[:4]}" temp_admin_pwd = "password123" temp_admin_id = None if not existing_admin_cred: print(f"No Admin found. Creating temporary Admin: {temp_admin_uname}") r_create = client.post("/api/users", json={ "username": temp_admin_uname, "password": temp_admin_pwd, "full_name": "Temporary Test Admin", "role": "admin" }) if r_create.status_code in (200, 201): temp_admin_id = r_create.get_json().get("user", {}).get("user_id") existing_admin_cred = {"username": temp_admin_uname, "password": temp_admin_pwd} report_case("Discovery", "Create temp Admin", "201", r_create.status_code, True) else: print(f"Using existing Admin: {existing_admin_cred['username']}") # Create a temp Officer for the Admin to manage temp_off_uname = f"tmp_off_{uuid.uuid4().hex[:4]}" temp_off_id = None r_off = client.post("/api/users", json={ "username": temp_off_uname, "password": "password123", "full_name": "Temporary Test Officer", "role": "officer" }) if r_off.status_code in (200, 201): temp_off_id = r_off.get_json().get("user", {}).get("user_id") report_case("Provisioning", "Create temp Officer", "201", r_off.status_code, True) client.post("/api/logout") # 3. Hierarchy Testing print("\n--- PHASE 3: Hierarchy Testing ---") # Test 1: Admin Power print("Test 1: Admin Managing Officer...") client.post("/api/login", json=existing_admin_cred) r_edit = client.put(f"/api/users/{temp_off_id}", json={"full_name": "Officer Renamed By Admin"}) report_case("Hierarchy", "Admin edits Officer", "200", r_edit.status_code, r_edit.status_code == 200) # Test 2: Admin Restrictions print("Test 2: Admin cannot delete SuperAdmin...") # Get SuperAdmin ID first client.post("/api/logout") client.post("/api/login", json=super_admin_cred) users = client.get("/api/users").get_json() sa_obj = next((u for u in users if u['role'].lower() == 'superadmin'), None) sa_id = sa_obj['user_id'] if sa_obj else "bootstrap-admin-01" client.post("/api/logout") client.post("/api/login", json=existing_admin_cred) r_del_sa = client.delete(f"/api/users/{sa_id}") report_case("Security", "Admin attempts delete SuperAdmin", "403/404", r_del_sa.status_code, r_del_sa.status_code in (403, 404)) # Test 3: Admin creating peer Admin (Should be allowed based on current code) r_peer = client.post("/api/users", json={"username": f"peer_{uuid.uuid4().hex[:4]}", "password": "123", "full_name": "Peer", "role": "admin"}) report_case("Hierarchy", "Admin creates peer Admin", "201", r_peer.status_code, r_peer.status_code == 201) if r_peer.status_code == 201: client.delete(f"/api/users/{r_peer.get_json().get('user', {}).get('user_id')}") client.post("/api/logout") # 4. Cleanup print("\n--- PHASE 4: Cleanup ---") client.post("/api/login", json=super_admin_cred) if temp_off_id: client.delete(f"/api/users/{temp_off_id}") if temp_admin_id: client.delete(f"/api/users/{temp_admin_id}") print("Cleanup completed.") if __name__ == "__main__": run_tests() ================================================ FILE: tests/test_advanced_gis.py ================================================ import os import sys import json import uuid from datetime import datetime # Ensure project root is importable sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app import app def report_case(scenario, action, expected, actual, passed): status = "PASS" if passed else "FAIL" print(f"__TEST_RESULT__||{scenario}||{action}||{expected}||{actual}||{status}") # 1. Setup credentials CRED_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "user_id_password.json") with open(CRED_FILE, "r", encoding="utf-8") as f: creds = json.load(f) # Use Premithiews as the test user admin = next((c for c in creds if c.get('username') == 'Premithiews' or c.get('user_id') == 'Premithiews'), creds[0]) # 2. Test Data # A 1km x 1km square in Delhi area base_poly = { "type": "Polygon", "coordinates": [[[77.100, 28.600], [77.110, 28.600], [77.110, 28.610], [77.100, 28.610], [77.100, 28.600]]] } # An overlapping square overlap_poly = { "type": "Polygon", "coordinates": [[[77.105, 28.605], [77.115, 28.605], [77.115, 28.615], [77.105, 28.615], [77.105, 28.605]]] } # Invalid: Too few points bad_poly = { "type": "Polygon", "coordinates": [[[77.100, 28.600], [77.110, 28.600]]] } base_record_id = None with app.test_client() as client: print("--- STARTING ADVANCED GIS SUITE ---") # STEP 1: LOGIN r_login = client.post('/api/login', json={"username": admin.get('username') or admin.get('user_id'), "password": admin.get('password')}) report_case("Auth", "Admin Login", "200", r_login.status_code, r_login.status_code == 200) # STEP 2: INSERT BASE RECORD print("\nInserting Base Parcel...") base_data = { "khasra_no": "TEST-BASE-001", "khata_no": "KH-99", "location": {"state": "Delhi", "district": "New Delhi", "village": "Chanakyapuri"}, "geometry": base_poly, "land_use": "Institutional", "owner_name": "Initial Owner" } r_base = client.post('/api/records', json=base_data) if r_base.status_code == 201: base_record_id = r_base.get_json().get('record', {}).get('_id') report_case("Insertion", "Insert Base Parcel", "201", r_base.status_code, r_base.status_code == 201) # STEP 3: TEST OVERLAP DETECTION if base_record_id: print("\nTesting Overlap Detection (should be blocked)...") overlap_data = base_data.copy() overlap_data["khasra_no"] = "TEST-OVERLAP-ERR" overlap_data["geometry"] = overlap_poly r_overlap = client.post('/api/records', json=overlap_data) report_case("Security", "Detect Parcel Overlap", "409", r_overlap.status_code, r_overlap.status_code == 409) # STEP 4: TEST GEOMETRY VALIDATION print("\nTesting Bad Geometry (should be blocked)...") bad_data = base_data.copy() bad_data["khasra_no"] = "TEST-BAD-GEOM" bad_data["geometry"] = bad_poly r_bad = client.post('/api/records', json=bad_data) report_case("Security", "Validate Geometry", "400", r_bad.status_code, r_bad.status_code == 400) # STEP 5: TEST OWNERSHIP MUTATION if base_record_id: print("\nTesting Ownership Mutation (Sale Deed)...") mutation_data = { "mutation": True, "new_owner_name": "Purchaser Name", "mutation_type": "Sale Deed", "new_share_pct": 100 } r_mut = client.put(f'/api/records/{base_record_id}', json=mutation_data) report_case("Logic", "Perform Mutation", "200", r_mut.status_code, r_mut.status_code == 200) # STEP 6: CLEANUP if base_record_id: print("\nCleaning up test data...") r_del = client.delete(f'/api/records/{base_record_id}') report_case("Cleanup", "Delete Base Parcel", "200", r_del.status_code, r_del.status_code == 200) print("\n--- ADVANCED GIS SUITE COMPLETE ---") ================================================ FILE: tests/test_logins.py ================================================ r""" Test login flow using Flask test client and credentials in user_id_password.json Usage: .venv\Scripts\python tests\test_logins.py """ import os import sys import json from pprint import pprint # Ensure project root is importable sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) try: from app import app except Exception as e: print(f"Failed to import app: {e}") sys.exit(1) CRED_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "user_id_password.json") if not os.path.exists(CRED_FILE): print(f"Credentials file not found: {CRED_FILE}") sys.exit(1) with open(CRED_FILE, "r", encoding="utf-8") as f: try: creds = json.load(f) except Exception as e: print(f"Failed to parse credentials file: {e}") sys.exit(1) if not isinstance(creds, list): print("Expected JSON array of credentials objects") sys.exit(1) def report_case(scenario, action, expected, actual, passed): status = "PASS" if passed else "FAIL" print(f"__TEST_RESULT__||{scenario}||{action}||{expected}||{actual}||{status}") with app.test_client() as client: # 1. Test Valid Logins for entry in creds: username = entry.get("username") or entry.get("user_id") password = entry.get("password") if not username or not password: continue print(f"\nTesting valid login for: {username}") resp = client.post("/api/login", json={"username": username, "password": password}) print(f"Status: {resp.status_code}") try: pprint(resp.get_json(force=True)) except: print(resp.data.decode()[:200]) report_case(f"Valid Login ({username})", "Submit correct credentials", "200", resp.status_code, resp.status_code == 200) # 2. Test Invalid Username print("\nTesting Invalid Username") resp = client.post("/api/login", json={"username": "doesnotexist_user", "password": "password123"}) print(f"Status: {resp.status_code}") try: pprint(resp.get_json(force=True)) except: print(resp.data.decode()[:200]) report_case("Invalid Username", "Submit non-existent username", "401", resp.status_code, resp.status_code == 401) # 3. Test Invalid Password if creds and len(creds) > 0: valid_user = creds[0].get("username") or creds[0].get("user_id") print(f"\nTesting Invalid Password for user: {valid_user}") resp = client.post("/api/login", json={"username": valid_user, "password": "wrongpassword!"}) print(f"Status: {resp.status_code}") try: pprint(resp.get_json(force=True)) except: print(resp.data.decode()[:200]) report_case("Invalid Password", f"Submit wrong password for {valid_user}", "401", resp.status_code, resp.status_code == 401) ================================================ FILE: tests/test_record_soft_delete.py ================================================ r""" Test soft-delete flow for records: - Login as admin, create a record - Login as officer, soft-delete the record - Verify viewer/officer cannot see soft-deleted record - Login as admin, hard-delete the record """ import os import sys import json import uuid from pprint import pprint sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app import app CRED_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "user_id_password.json") with open(CRED_FILE, "r", encoding="utf-8") as f: creds = json.load(f) admin = None for e in creds: name = e.get('username') or e.get('user_id') if name and name.lower() == 'admin': admin = {'username': name, 'password': e.get('password')} break if not admin: admin = {'username': creds[0].get('username') or creds[0].get('user_id'), 'password': creds[0].get('password')} def report_case(scenario, action, expected, actual, passed): status = "PASS" if passed else "FAIL" print(f"__TEST_RESULT__||{scenario}||{action}||{expected}||{actual}||{status}") record_id = None officer_id = None unique_id = uuid.uuid4().hex[:6] with app.test_client() as client: print("Admin login...") r = client.post('/api/login', json=admin) print(r.status_code) report_case("Admin Setup", "Login as Admin", "200", r.status_code, r.status_code == 200) temp_officer = {'username': f'temp_off_del_{unique_id}', 'password': 'op', 'full_name': 'Temp Officer', 'role': 'officer'} print("Create officer...") resp = client.post('/api/users', json=temp_officer) if resp.is_json: officer_id = resp.get_json().get('user', {}).get('user_id') or resp.get_json().get('user_id') print(resp.status_code) sample = { 'khasra_no': f'K-{unique_id}', 'khata_no': 'KH-1', 'location': {'state': 'TestState', 'district': 'TestDistrict', 'village': 'TestVillage'}, 'geometry': {'type': 'Polygon', 'coordinates': [[[78.0, 20.0],[78.001,20.0],[78.001,20.001],[78.0,20.001],[78.0,20.0]]]}, 'land_use': 'Agricultural', 'owner_name': 'Record Owner' } print("Create record...") r2 = client.post('/api/records', json=sample) print(r2.status_code) if r2.is_json and r2.get_json().get('record'): record_id = r2.get_json()['record'].get('_id') report_case("Record Setup", "Admin POST /api/records", "201", r2.status_code, r2.status_code == 201) if record_id: with app.test_client() as c2: print("Officer login...") c2.post('/api/login', json=temp_officer) print("Officer delete attempt...") del_resp = c2.delete(f'/api/records/{record_id}') print(del_resp.status_code) report_case("Soft Delete", "Officer DELETE record", "200", del_resp.status_code, del_resp.status_code == 200) print("Verify record is hidden...") list_resp = c2.get('/api/records') is_hidden = True if list_resp.is_json: data = list_resp.get_json() if isinstance(data, list): is_hidden = not any(rec.get('_id') == record_id for rec in data) elif isinstance(data, dict) and "records" in data: is_hidden = not any(rec.get('_id') == record_id for rec in data["records"]) print(f"Record hidden: {is_hidden}") report_case("Data Privacy", "Officer GET records", "Hidden", "Hidden" if is_hidden else "Visible", is_hidden) with app.test_client() as adminc: adminc.post('/api/login', json=admin) if record_id: print("Admin hard delete...") del_hard = adminc.delete(f'/api/records/{record_id}') print(del_hard.status_code) report_case("Hard Delete", "Admin hard delete", "200", del_hard.status_code, del_hard.status_code == 200) if officer_id: adminc.delete(f'/api/users/{officer_id}') ================================================ FILE: tests/test_recovery_flow.py ================================================ r""" Recovery account integration smoke test (create + delete superadmin) Run: .venv\Scripts\python tests\test_recovery_flow.py """ import os, sys, json import uuid from datetime import datetime sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app import app CRED_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "user_id_password.json") with open(CRED_FILE, "r", encoding="utf-8") as f: creds = json.load(f) # find recovery creds (explicit or by username prefix) recov = None for c in creds: uname = c.get('username') or c.get('user_id') if uname and uname.startswith('recovery_sa_'): recov = c break if not recov: print("No recovery credentials in user_id_password.json — add them and re-run.") raise SystemExit(1) recov_username = recov.get('username') or recov.get('user_id') recov_password = recov.get('password') def report_case(scenario, action, expected, actual, passed): status = "PASS" if passed else "FAIL" print(f"__TEST_RESULT__||{scenario}||{action}||{expected}||{actual}||{status}") with app.test_client() as client: # Login as recovery r = client.post('/api/login', json={"username": recov_username, "password": recov_password}) report_case("Recovery Login", "Login with recovery creds", "200", r.status_code, r.status_code == 200) if r.status_code != 200: raise SystemExit(2) # Create a new superadmin (unique username) ts = datetime.utcnow().strftime('%Y%m%d%H%M%S') new_username = f"sa_test_create_{ts}_{str(uuid.uuid4())[:6]}" new_sa = {"username": new_username, "password": "TempP@ss123!", "full_name": "SA Test", "role": "superadmin"} resp = client.post('/api/users', json=new_sa) created = resp.status_code in (200, 201) report_case("Create SuperAdmin", "POST /api/users as recovery", "201", resp.status_code, created) created_id = None if resp.is_json: created_id = resp.get_json().get('user', {}).get('user_id') or resp.get_json().get('user_id') # Delete created superadmin (cleanup) if created_id: d = client.delete(f"/api/users/{created_id}") report_case("Delete SuperAdmin", "DELETE /api/users/ as recovery", "200", d.status_code, d.status_code == 200) else: print("No created_id returned; skipping delete step.") ================================================ FILE: tests/test_report_gen.py ================================================ import os import sys import json # Add project root to path sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from report_generator import generate_property_card_pdf, generate_village_excel def test_report_generation(): print("--- Starting Report Generation Test ---") # Ensure test_output directory exists output_dir = os.path.join(os.path.dirname(__file__), 'test_output') os.makedirs(output_dir, exist_ok=True) # 1. Sample Data with different Land Uses to test Multipliers test_records = [ { "_id": "rec-001", "ulpin": "AS1801KAM001", "khasra_no": "101", "khata_no": "202", "location": {"state": "Assam", "district": "Kamrup", "village": "Amingaon"}, "attributes": {"area_ha": 2.5, "land_use": "Commercial", "circle_rate_inr": 500000}, "owner": {"name": "Arjun Sharma", "share_pct": 100, "aadhaar_mask": "XXXX-XXXX-1234"}, "geometry": {"type": "Polygon", "coordinates": [[[91.70, 26.10], [91.71, 26.10], [91.71, 26.11], [91.70, 26.11], [91.70, 26.10]]]}, "mutation_history": [] }, { "_id": "rec-002", "ulpin": "AS1801KAM002", "khasra_no": "102", "khata_no": "203", "location": {"state": "Assam", "district": "Kamrup", "village": "Amingaon"}, "attributes": {"area_ha": 1.2, "land_use": "Agricultural", "circle_rate_inr": 100000}, "owner": {"name": "Laxmi Devi", "share_pct": 100, "aadhaar_mask": "XXXX-XXXX-5678"}, "geometry": {"type": "Polygon", "coordinates": [[[91.72, 26.12], [91.73, 26.12], [91.73, 26.13], [91.72, 26.13], [91.72, 26.12]]]}, "mutation_history": [] } ] # 2. Generate PDF for each record for rec in test_records: print(f"Generating PDF for {rec['ulpin']} ({rec['attributes']['land_use']})...") try: pdf_bytes = generate_property_card_pdf(rec) filename = f"Property_Card_{rec['ulpin']}.pdf" filepath = os.path.join(output_dir, filename) with open(filepath, 'wb') as f: f.write(pdf_bytes) print(f"DONE Saved to {filepath}") except Exception as e: print(f"FAIL Error generating PDF: {e}") # 3. Generate Village Ledger (Excel) print("Generating Village Ledger (Excel) for Amingaon...") try: excel_bytes = generate_village_excel(test_records) filename = "Village_Ledger_Amingaon.xlsx" filepath = os.path.join(output_dir, filename) with open(filepath, 'wb') as f: f.write(excel_bytes) print(f"DONE Saved to {filepath}") except Exception as e: print(f"FAIL Error generating Excel: {e}") print("\n--- Report Generation Test Complete ---") print(f"Results are in: {output_dir}") if __name__ == "__main__": test_report_generation() ================================================ FILE: tests/test_restore_flow.py ================================================ r""" Test restore flow: - Login as admin, create temp officer and a record - Login as officer, soft-delete the record - Login as admin, restore the record - Verify record is present after restore - Cleanup created users and record """ import os, sys, json, uuid sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app import app CRED_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "user_id_password.json") with open(CRED_FILE, "r", encoding="utf-8") as f: creds = json.load(f) admin = None for e in creds: name = e.get('username') or e.get('user_id') if name and name.lower() == 'admin': admin = {'username': name, 'password': e.get('password')} break if not admin: admin = creds[0] def report_case(scenario, action, expected, actual, passed): status = "PASS" if passed else "FAIL" print(f"__TEST_RESULT__||{scenario}||{action}||{expected}||{actual}||{status}") record_id = None officer_id = None unique_id = uuid.uuid4().hex[:6] with app.test_client() as c_admin: print("Admin login...") r = c_admin.post('/api/login', json=admin) temp_officer = {'username': f'temp_off_res_{unique_id}', 'password': 'pw', 'full_name': 'Restore Officer', 'role': 'officer'} resp = c_admin.post('/api/users', json=temp_officer) if resp.is_json: officer_id = resp.get_json().get('user', {}).get('user_id') or resp.get_json().get('user_id') sample = { 'khasra_no': f'REST-{unique_id}', 'khata_no': 'REST-K', 'location': {'state': 'TS', 'district': 'TD', 'village': 'TV'}, 'geometry': {'type': 'Polygon', 'coordinates': [[[78.0,20.0],[78.001,20.0],[78.001,20.001],[78.0,20.001],[78.0,20.0]]]}, 'land_use': 'Agricultural', 'owner_name': 'Restore Owner' } r2 = c_admin.post('/api/records', json=sample) if r2.is_json and r2.get_json().get('record'): record_id = r2.get_json()['record'].get('_id') if record_id: with app.test_client() as c_off: c_off.post('/api/login', json=temp_officer) print("Officer soft delete...") c_off.delete(f'/api/records/{record_id}') with app.test_client() as c_admin2: c_admin2.post('/api/login', json=admin) print("Admin restore...") restore = c_admin2.post(f'/api/records/{record_id}/restore') print(restore.status_code) report_case("Restore Record", "Admin POST restore", "200", restore.status_code, restore.status_code == 200) getr = c_admin2.get(f'/api/records/{record_id}') data = getr.get_json() if getr.is_json else {} is_restored = getr.is_json and data.get('deleted') is False report_case("Verify Restore", "Admin GET record", "deleted=False", f"deleted={not is_restored}", is_restored) if officer_id: c_admin2.delete(f'/api/users/{officer_id}') c_admin2.delete(f'/api/records/{record_id}') ================================================ FILE: utils.py ================================================ """ utils.py - Shared Utilities for India LIMS Common functions used across the application. """ import os import sys def resource_path(relative_path): """Get absolute path to resource, works for dev and for PyInstaller.""" try: # PyInstaller creates a temp folder and stores path in _MEIPASS base_path = sys._MEIPASS except Exception: base_path = os.path.abspath(".") return os.path.join(base_path, relative_path) def safe_divide(numerator, denominator, default=0): """Safely divide two numbers, returning default on division by zero.""" try: return numerator / denominator if denominator else default except (TypeError, ZeroDivisionError): return default