first commit
This commit is contained in:
commit
d1c1e7fca4
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
cobertura.mbtiles
|
||||
hex_mapa2025-10-01_2025-12-14.gpkg
|
||||
13
INFO_DEPURAR.txt
Normal file
13
INFO_DEPURAR.txt
Normal file
@ -0,0 +1,13 @@
|
||||
- descargar imagen del mapa - sección activa
|
||||
|
||||
DEPURAR ALERTS
|
||||
[ ] carga unitaria
|
||||
la validación de la etiqueta no es correcta
|
||||
|
||||
[ ] carga masiva
|
||||
[ ] descarga plantilla
|
||||
[ya] permitir descargar geopackage
|
||||
|
||||
crt: 19.381885015253673, -99.17663944307311
|
||||
|
||||
Por favor ingrese una etiqueta para el marcador
|
||||
35
buscar_data_layers.py
Normal file
35
buscar_data_layers.py
Normal file
@ -0,0 +1,35 @@
|
||||
from pymbtiles import MBtiles
|
||||
import mapbox_vector_tile
|
||||
import gzip
|
||||
|
||||
mb = MBtiles("./static/salida.mbtiles")
|
||||
|
||||
found = False
|
||||
|
||||
for z in range(
|
||||
int(mb.meta.get("minzoom", 0)),
|
||||
int(mb.meta.get("maxzoom", 14)) + 1
|
||||
):
|
||||
for x in range(0, 2**z):
|
||||
for y in range(0, 2**z):
|
||||
tile = mb.read_tile(z, x, y)
|
||||
if tile:
|
||||
try:
|
||||
# 🔑 DESCOMPRESIÓN
|
||||
tile = gzip.decompress(tile)
|
||||
except OSError:
|
||||
# no estaba comprimido
|
||||
pass
|
||||
|
||||
decoded = mapbox_vector_tile.decode(tile)
|
||||
|
||||
print("✅ Tile encontrado")
|
||||
print("z:", z, "x:", x, "y:", y)
|
||||
print("Layers:", decoded.keys())
|
||||
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
break
|
||||
if found:
|
||||
break
|
||||
33
help.txt
Normal file
33
help.txt
Normal file
@ -0,0 +1,33 @@
|
||||
1.- Convertir gpkg a geojson (lo hice con geopandas)
|
||||
2.- Convertir geojson a mbtiles (linux tippercanoe):
|
||||
|
||||
tippecanoe \
|
||||
-o cobertura.mbtiles \
|
||||
-l municipios \
|
||||
-Z5 \
|
||||
-z18 \
|
||||
--extend-zooms-if-still-dropping \
|
||||
--drop-densest-as-needed \
|
||||
--simplification=2 \
|
||||
--simplification-at-maximum-zoom=1 \
|
||||
--no-tiny-polygon-reduction \
|
||||
geo_pandas_limpio.geojson
|
||||
|
||||
3.- Renderizar datos con maplibre
|
||||
|
||||
|
||||
|
||||
|
||||
tippecanoe \
|
||||
-o cobertura.mbtiles \
|
||||
-l municipios \
|
||||
-Z5 \
|
||||
-z18 \
|
||||
--base-zoom=8 \
|
||||
--simplification=0.5 \
|
||||
--simplification-at-maximum-zoom=1 \
|
||||
--no-tiny-polygon-reduction \
|
||||
--no-feature-limit \
|
||||
--no-tile-size-limit \
|
||||
--extend-zooms-if-still-dropping \
|
||||
geo_pandas_limpio.geojson
|
||||
0
log/access.log
Normal file
0
log/access.log
Normal file
0
log/error.log
Normal file
0
log/error.log
Normal file
97
main.py
Normal file
97
main.py
Normal file
@ -0,0 +1,97 @@
|
||||
from flask import Flask, Response, abort, render_template, send_file
|
||||
from flask_cors import CORS
|
||||
from pymbtiles import MBtiles
|
||||
import os
|
||||
|
||||
from cachetools import LRUCache
|
||||
|
||||
tile_cache = LRUCache(maxsize=5000)
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
MBTILES_PATH = os.path.join(app.root_path, "static", "data-files/cobertura.mbtiles")
|
||||
|
||||
def flip_y(z, y):
|
||||
return (1 << z) - 1 - y
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
@app.route("/tiles/<int:z>/<int:x>/<int:y>.pbf")
|
||||
def tiles(z, x, y):
|
||||
y = flip_y(z, y)
|
||||
key = (z, x, y)
|
||||
|
||||
if key in tile_cache:
|
||||
tile = tile_cache[key]
|
||||
else:
|
||||
with MBtiles(MBTILES_PATH) as mbtiles:
|
||||
tile = mbtiles.read_tile(z, x, y)
|
||||
if tile is None:
|
||||
abort(404)
|
||||
if isinstance(tile, tuple):
|
||||
tile = tile[0]
|
||||
tile_cache[key] = tile
|
||||
|
||||
return Response(
|
||||
tile,
|
||||
mimetype="application/x-protobuf",
|
||||
headers={
|
||||
"Content-Encoding": "gzip",
|
||||
"Cache-Control": "public, max-age=86400"
|
||||
}
|
||||
)
|
||||
|
||||
# return Response(
|
||||
# tile,
|
||||
# mimetype="application/x-protobuf",
|
||||
# headers={
|
||||
# "Content-Encoding": "gzip",
|
||||
# "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"
|
||||
# }
|
||||
# )
|
||||
|
||||
|
||||
@app.route("/api/poligonos-edos")
|
||||
def poligonos_edos():
|
||||
response = send_file(
|
||||
os.path.join(app.root_path, "static/data-files", "poligonos_edos_mx.json"),
|
||||
mimetype="application/geo+json",
|
||||
max_age=86400
|
||||
)
|
||||
response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
return response
|
||||
|
||||
# @app.route("/api/poligonos-edos")
|
||||
# def poligonos_edos():
|
||||
# response = send_file(
|
||||
# os.path.join(app.root_path, "static/data-files", "poligonos_edos_mx.json"),
|
||||
# mimetype="application/geo+json",
|
||||
# max_age=0
|
||||
# )
|
||||
# response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
|
||||
# response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
# return response
|
||||
|
||||
|
||||
@app.route("/file/download/<path:filename>")
|
||||
def download_file(filename):
|
||||
file_path = os.path.join(app.root_path, "static/data-files", filename)
|
||||
if not os.path.isfile(file_path):
|
||||
abort(404)
|
||||
return send_file(
|
||||
file_path,
|
||||
as_attachment=True
|
||||
)
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(error):
|
||||
return render_template("404.html"), 404
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, threaded=True)
|
||||
48
mapa-cobertura.conf
Normal file
48
mapa-cobertura.conf
Normal file
@ -0,0 +1,48 @@
|
||||
# sudo apachectl configtest
|
||||
Listen 8086
|
||||
<VirtualHost *:8086>
|
||||
ServerAdmin davidix1991@gmail.com
|
||||
ServerName mapa-cobertura.temporal.work
|
||||
ServerAlias mapa-cobertura.temporal.work
|
||||
DocumentRoot /var/www/mapa-cobertura
|
||||
|
||||
WSGIDaemonProcess app_map_cobertura user=www-data group=www-data threads=6 python-home=/var/www/mapa-cobertura/.venv
|
||||
WSGIScriptAlias / /var/www/mapa-cobertura/mapa-cobertura.wsgi
|
||||
|
||||
ErrorLog /var/www/mapa-cobertura/log/error.log
|
||||
CustomLog /var/www/mapa-cobertura/log/access.log combined
|
||||
|
||||
<Directory /var/www/mapa-cobertura>
|
||||
WSGIProcessGroup app_map_cobertura
|
||||
WSGIApplicationGroup %{GLOBAL}
|
||||
Order deny,allow
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
# Habilitar caché para todas las solicitudes
|
||||
CacheEnable disk /
|
||||
|
||||
# Configuración de caché
|
||||
<IfModule mod_cache_disk.c>
|
||||
CacheRoot /var/cache/apache2/mod_cache_disk
|
||||
CacheDirLevels 2
|
||||
CacheDirLength 1
|
||||
# [bytes] Tamaño máximo de archivo a almacenar en caché
|
||||
CacheMaxFileSize 1000000
|
||||
# CacheMinFileSize bytes
|
||||
CacheMinFileSize 1
|
||||
CacheIgnoreHeaders Set-Cookie
|
||||
CacheIgnoreNoLastMod On
|
||||
|
||||
<FilesMatch "\.(jpg|jpeg|png|gif|css|js)$">
|
||||
# Indica si el caché está funcionando
|
||||
Header set X-Cache "HIT from Apache"
|
||||
# Expiración por defecto (1 hora)
|
||||
CacheDefaultExpire 3600
|
||||
# Expiración máxima (1 día)
|
||||
CacheMaxExpire 86400
|
||||
CacheLastModifiedFactor 0.5
|
||||
</FilesMatch>
|
||||
</IfModule>
|
||||
|
||||
</VirtualHost>
|
||||
14
mapa-cobertura.wsgi
Normal file
14
mapa-cobertura.wsgi
Normal file
@ -0,0 +1,14 @@
|
||||
import sys
|
||||
import logging
|
||||
|
||||
# ruta de linux al proyecto de flask
|
||||
sys.path.insert(0, '/var/www/mapa-cobertura.temporal.work')
|
||||
|
||||
# ruta de linux al ambiente virtual de flask
|
||||
sys.path.insert(0, '/var/www/mapa-cobertura.temporal.work/.venv/lib/python3.13/site-packages')
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
|
||||
|
||||
# Import and run the Flask app
|
||||
from main import app as application
|
||||
11
requirements.txt
Normal file
11
requirements.txt
Normal file
@ -0,0 +1,11 @@
|
||||
blinker==1.9.0
|
||||
cachetools==6.2.4
|
||||
click==8.3.1
|
||||
colorama==0.4.6
|
||||
Flask==3.1.2
|
||||
flask-cors==6.0.2
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.6
|
||||
MarkupSafe==3.0.3
|
||||
pymbtiles==0.5.0
|
||||
Werkzeug==3.1.4
|
||||
273
static/css/index/index.css
Normal file
273
static/css/index/index.css
Normal file
@ -0,0 +1,273 @@
|
||||
/* ===============================
|
||||
CONTAINER Y MAPA
|
||||
=============================== */
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
color: #333;
|
||||
background: #f5f7fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
CONTENEDOR GENERAL DEL COLLAPSE
|
||||
=============================== */
|
||||
#pin-form {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
width: 300px;
|
||||
max-width: 90vw;
|
||||
border: 1px solid #e1e5e9;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
CABECERA DEL FORMULARIO (No colapsable)
|
||||
=============================== */
|
||||
.form-header {
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
CONTENIDO PRINCIPAL COLAPSABLE
|
||||
=============================== */
|
||||
.collapsible-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
SECCIONES COLAPSABLES INTERNAS
|
||||
=============================== */
|
||||
.collapse-section {
|
||||
border-bottom: 1px solid #e1e5e9;
|
||||
}
|
||||
|
||||
.collapse-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.collapse-header {
|
||||
padding: 14px 16px;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e1e5e9;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.collapse-header:hover {
|
||||
background: #edf2f7;
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
transition: transform 0.3s ease;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.collapse-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
CHECKBOX PARA CONTROLAR COLLAPSE (Truco CSS)
|
||||
=============================== */
|
||||
#collapse-toggle,
|
||||
#collapse-toggle-1,
|
||||
#collapse-toggle-2,
|
||||
#collapse-toggle-3 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Cuando el checkbox está checked, mostramos el contenido */
|
||||
#collapse-toggle:checked ~ .collapsible-content,
|
||||
#collapse-toggle-1:checked ~ .collapse-content,
|
||||
#collapse-toggle-2:checked ~ .collapse-content,
|
||||
#collapse-toggle-3:checked ~ .collapse-content {
|
||||
max-height: 1000px;
|
||||
opacity: 1;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Rotamos el icono cuando está expandido */
|
||||
#collapse-toggle:checked ~ .form-header .collapse-icon,
|
||||
#collapse-toggle-1:checked ~ .collapse-header .collapse-icon,
|
||||
#collapse-toggle-2:checked ~ .collapse-header .collapse-icon,
|
||||
#collapse-toggle-3:checked ~ .collapse-header .collapse-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
FORM ELEMENTS
|
||||
=============================== */
|
||||
.form-group {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.9rem;
|
||||
color: #4a5568;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #cbd5e0;
|
||||
border-radius: 6px;
|
||||
font-size: 0.95rem;
|
||||
transition: border 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #1976d2;
|
||||
box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.1);
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #1976d2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 16px;
|
||||
width: calc(100% - 32px);
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.button-primary:hover {
|
||||
background: #1565c0;
|
||||
}
|
||||
|
||||
.csv-section {
|
||||
padding: 16px;
|
||||
border-top: 1px solid #e1e5e9;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
color: #2d3748;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 95%;
|
||||
padding: 5px;
|
||||
background: #edf2f7;
|
||||
color: #2d3748;
|
||||
border: 1px solid #cbd5e0;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
margin-bottom: 10px;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.button-secondary:hover {
|
||||
background: #e2e8f0;
|
||||
border-color: #a0aec0;
|
||||
}
|
||||
|
||||
.csv-file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-upload-label {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 95%;
|
||||
padding: 5px;
|
||||
background: #f0f9ff;
|
||||
color: #0369a1;
|
||||
border: 1px dashed #7dd3fc;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.file-upload-label:hover {
|
||||
background: #e0f2fe;
|
||||
border-color: #0ea5e9;
|
||||
}
|
||||
|
||||
/* .tooltip {
|
||||
font-size: 0.85rem;
|
||||
color: #718096;
|
||||
line-height: 1.4;
|
||||
} */
|
||||
|
||||
|
||||
|
||||
/* ===============================
|
||||
ESTADO ACTIVO (SIEMPRE VISIBLE)
|
||||
=============================== */
|
||||
.active-state-section {
|
||||
padding: 16px;
|
||||
background: #fff7ed;
|
||||
border-top: 1px solid #fed7aa;
|
||||
display: block !important;
|
||||
max-height: none !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
53
static/css/index/loading.css
Normal file
53
static/css/index/loading.css
Normal file
@ -0,0 +1,53 @@
|
||||
/* ===============================
|
||||
INDICADOR DE CARGA
|
||||
=============================== */
|
||||
.loading-indicator {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 2000;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 20px 30px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
text-align: center;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #1976d2;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 15px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
48
static/css/index/notifications.css
Normal file
48
static/css/index/notifications.css
Normal file
@ -0,0 +1,48 @@
|
||||
/* ===============================
|
||||
NOTIFICACIONES
|
||||
=============================== */
|
||||
.notification {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 350px;
|
||||
animation: slideInRight 0.3s ease;
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
background: linear-gradient(135deg, #4caf50, #66bb6a);
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
background: linear-gradient(135deg, #f44336, #e53935);
|
||||
}
|
||||
|
||||
.notification.info {
|
||||
background: linear-gradient(135deg, #2196f3, #1976d2);
|
||||
}
|
||||
|
||||
.notification i {
|
||||
margin-right: 10px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
53
static/css/index/tooltip.css
Normal file
53
static/css/index/tooltip.css
Normal file
@ -0,0 +1,53 @@
|
||||
/* ===============================
|
||||
TOOLTIPS
|
||||
=============================== */
|
||||
|
||||
|
||||
|
||||
/* Contenedor */
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Texto del tooltip */
|
||||
.tooltip .tooltip-text {
|
||||
visibility: hidden;
|
||||
width: 220px;
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
text-align: left; /* Alineado a la izquierda para mejor lectura */
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 125%;
|
||||
|
||||
/* CAMBIO CLAVE: Alineación a la derecha */
|
||||
left: 0; /* Empieza desde el icono */
|
||||
margin-left: 0; /* Eliminamos el margen negativo que lo sacaba de pantalla */
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
box-shadow: 0px 4px 10px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* Ajuste de la flecha para que coincida con el icono a la izquierda */
|
||||
.tooltip .tooltip-text::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 10px; /* La flecha ahora se queda cerca del inicio del cuadro */
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: #333 transparent transparent transparent;
|
||||
}
|
||||
|
||||
/* Mostrar al pasar el mouse */
|
||||
.tooltip:hover .tooltip-text {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
34
static/data-files/poligonos_edos_mx.json
Normal file
34
static/data-files/poligonos_edos_mx.json
Normal file
File diff suppressed because one or more lines are too long
1
static/data-files/template_coordenadas_pines.csv
Normal file
1
static/data-files/template_coordenadas_pines.csv
Normal file
@ -0,0 +1 @@
|
||||
Etiqueta_pin,Estado,Municipio,Localidad,Latitud,Longitud,Comentario
|
||||
|
137
static/js/index/01_cobertura.js
Normal file
137
static/js/index/01_cobertura.js
Normal file
@ -0,0 +1,137 @@
|
||||
// ===============================
|
||||
// CONFIGURACIÓN GENERAL
|
||||
// ===============================
|
||||
|
||||
|
||||
const TILE_URL = location.href + "/tiles/{z}/{x}/{y}.pbf";
|
||||
const SOURCE_LAYER = "municipios";
|
||||
|
||||
// ===============================
|
||||
// MAPA
|
||||
// ===============================
|
||||
const map = new maplibregl.Map({
|
||||
container: "map",
|
||||
|
||||
center: [-102.20, 23.3646],
|
||||
zoom: 5,
|
||||
minZoom: 5,
|
||||
maxZoom: 17,
|
||||
style: {
|
||||
version: 8,
|
||||
sources: {
|
||||
// Fondo raster OSM
|
||||
osm: {
|
||||
type: "raster",
|
||||
tiles: [
|
||||
"https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
"https://b.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
"https://c.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
],
|
||||
tileSize: 256,
|
||||
attribution: "© OpenStreetMap contributors"
|
||||
},
|
||||
|
||||
// Vector tiles (MBTiles)
|
||||
municipios: {
|
||||
type: "vector",
|
||||
tiles: [TILE_URL],
|
||||
minzoom: 5,
|
||||
maxzoom: 18
|
||||
}
|
||||
},
|
||||
|
||||
layers: [
|
||||
// ===============================
|
||||
// BASE RASTER
|
||||
// ===============================
|
||||
{
|
||||
id: "osm-base",
|
||||
type: "raster",
|
||||
source: "osm",
|
||||
maxzoom: 18
|
||||
},
|
||||
|
||||
// ===============================
|
||||
// POLÍGONOS (RELLENO)
|
||||
// ===============================
|
||||
{
|
||||
id: "municipios-fill",
|
||||
type: "fill",
|
||||
source: "municipios",
|
||||
"source-layer": SOURCE_LAYER,
|
||||
minzoom: 5,
|
||||
maxzoom: 18,
|
||||
paint: {
|
||||
"fill-color": "#1976d2",
|
||||
"fill-opacity": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
5, 0.8,
|
||||
8, 0.18,
|
||||
12, 0.45,
|
||||
18, 0.15
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// ===============================
|
||||
// BORDES DE POLÍGONOS
|
||||
// ===============================
|
||||
{
|
||||
id: "municipios-outline",
|
||||
type: "line",
|
||||
source: "municipios",
|
||||
"source-layer": SOURCE_LAYER,
|
||||
minzoom: 6,
|
||||
maxzoom: 18,
|
||||
paint: {
|
||||
"line-color": "#0d47a1",
|
||||
"line-width": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
6, 0.2,
|
||||
12, 1.2,
|
||||
18, 3
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// ===============================
|
||||
// PUNTOS (DETALLE FINO)
|
||||
// ===============================
|
||||
{
|
||||
id: "municipios-points",
|
||||
type: "circle",
|
||||
source: "municipios",
|
||||
"source-layer": SOURCE_LAYER,
|
||||
minzoom: 14,
|
||||
maxzoom: 18,
|
||||
paint: {
|
||||
"circle-radius": [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["zoom"],
|
||||
14, 3,
|
||||
18, 6
|
||||
],
|
||||
"circle-color": "#0d47a1",
|
||||
"circle-opacity": 0.7
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// ===============================
|
||||
// CONTROLES
|
||||
// ===============================
|
||||
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||
|
||||
// ===============================
|
||||
// DEBUG (opcional)
|
||||
// ===============================
|
||||
// map.on("load", () => {
|
||||
// console.log("✅ Mapa cargado correctamente");
|
||||
// });
|
||||
84
static/js/index/02_edos-poligonos.js
Normal file
84
static/js/index/02_edos-poligonos.js
Normal file
@ -0,0 +1,84 @@
|
||||
import {dic_edos} from './99_functions.js';
|
||||
|
||||
map.on("load", () => {
|
||||
|
||||
if (map.getSource("edos")) return; // 🔐 protección
|
||||
|
||||
map.addSource("edos", {
|
||||
type: "geojson",
|
||||
data: "/api/poligonos-edos",
|
||||
generateId: true
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: "edos-fill",
|
||||
type: "fill",
|
||||
source: "edos",
|
||||
paint: { "fill-opacity": 0 }
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: "edos-outline",
|
||||
type: "line",
|
||||
source: "edos",
|
||||
paint: {
|
||||
"line-color": [
|
||||
"case",
|
||||
["boolean", ["feature-state", "hover"], false],
|
||||
"#9b2226",
|
||||
"#ffb703"
|
||||
],
|
||||
"line-width": [
|
||||
"case",
|
||||
["boolean", ["feature-state", "hover"], false],
|
||||
4,
|
||||
1.5
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
let hoveredId = null;
|
||||
|
||||
|
||||
map.on("mousemove", "edos-fill", e => {
|
||||
|
||||
let id_entidad = e.features[0].properties.cve_ent;
|
||||
let nombreEstado = dic_edos[id_entidad] || "No identificado";
|
||||
|
||||
|
||||
// ACTUALIZACIÓN: Mostrar en el formulario
|
||||
const labelEstado = document.getElementById("nombre-estado-activo");
|
||||
if (labelEstado) {
|
||||
labelEstado.innerText = nombreEstado;
|
||||
}
|
||||
|
||||
if (e.features.length) {
|
||||
if (hoveredId !== null) {
|
||||
map.setFeatureState({ source: "edos", id: hoveredId }, { hover: false });
|
||||
}
|
||||
hoveredId = e.features[0].id;
|
||||
map.setFeatureState({ source: "edos", id: hoveredId }, { hover: true });
|
||||
map.getCanvas().style.cursor = "pointer";
|
||||
}
|
||||
});
|
||||
|
||||
// También es buena idea limpiar el texto cuando el mouse sale del mapa
|
||||
map.on("mouseleave", "edos-fill", () => {
|
||||
if (hoveredId !== null) {
|
||||
map.setFeatureState({ source: "edos", id: hoveredId }, { hover: false });
|
||||
}
|
||||
|
||||
// ACTUALIZACIÓN: Limpiar el nombre al salir
|
||||
const labelEstado = document.getElementById("nombre-estado-activo");
|
||||
if (labelEstado) {
|
||||
labelEstado.innerText = "Ninguno";
|
||||
}
|
||||
|
||||
hoveredId = null;
|
||||
map.getCanvas().style.cursor = "";
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
67
static/js/index/03_carga_pin_unitarios.js
Normal file
67
static/js/index/03_carga_pin_unitarios.js
Normal file
@ -0,0 +1,67 @@
|
||||
import { showNotification } from './99_functions.js';
|
||||
|
||||
// ===============================
|
||||
// 1. LÓGICA DE PINES UNITARIOS (FORMULARIO)
|
||||
// ===============================
|
||||
const allMarkers = [];
|
||||
|
||||
// 💡 DESCOMENTADA: Necesaria para que el validador funcione
|
||||
function isValidCoord(value, min, max) {
|
||||
return !isNaN(value) && value >= min && value <= max;
|
||||
}
|
||||
|
||||
function hasFourDecimals(value) {
|
||||
const s = value.toString();
|
||||
if (!s.includes('.')) return false;
|
||||
return s.split(".")[1].length >= 4;
|
||||
}
|
||||
|
||||
document.getElementById("add-pin").addEventListener("click", () => {
|
||||
const labelInput = document.getElementById("pin-label");
|
||||
const latInput = document.getElementById("pin-lat");
|
||||
const lngInput = document.getElementById("pin-lng");
|
||||
|
||||
const label = labelInput.value.trim();
|
||||
const lat = parseFloat(latInput.value);
|
||||
const lng = parseFloat(lngInput.value);
|
||||
|
||||
// Validación unificada mejorada
|
||||
const isInvalid =
|
||||
!label ||
|
||||
!isValidCoord(lat, -90, 90) ||
|
||||
!isValidCoord(lng, -180, 180) ||
|
||||
!hasFourDecimals(latInput.value) ||
|
||||
!hasFourDecimals(lngInput.value);
|
||||
|
||||
if (isInvalid) {
|
||||
showNotification("Todos los campos son obligatorios: Etiqueta, Lat/Lng válidas y mínimo 4 decimales.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Si pasa la validación, procedemos:
|
||||
const newMarker = new maplibregl.Marker({ color: "#d32f2f" })
|
||||
.setLngLat([lng, lat])
|
||||
.setPopup(
|
||||
new maplibregl.Popup({ offset: 25 })
|
||||
.setHTML(`<strong>${label}</strong><br/>Lat: ${lat}<br/>Lng: ${lng}`)
|
||||
)
|
||||
.addTo(map);
|
||||
|
||||
allMarkers.push(newMarker);
|
||||
|
||||
map.flyTo({
|
||||
center: [lng, lat],
|
||||
zoom: 12,
|
||||
essential: true
|
||||
});
|
||||
|
||||
// Limpiar campos
|
||||
labelInput.value = "";
|
||||
latInput.value = "";
|
||||
lngInput.value = "";
|
||||
|
||||
showNotification("Marcador agregado con éxito", "success");
|
||||
|
||||
|
||||
});
|
||||
|
||||
95
static/js/index/04_carga_pines_masivo.js
Normal file
95
static/js/index/04_carga_pines_masivo.js
Normal file
@ -0,0 +1,95 @@
|
||||
import { showNotification } from './99_functions.js';
|
||||
|
||||
const csvFileInputEl = document.getElementById('csv-file');
|
||||
const loadingIndicator = document.getElementById('loading-indicator');
|
||||
|
||||
document.getElementById('csv-file').addEventListener('change', function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// emular loading de carga de archivo csv
|
||||
if (this.files.length > 0) {
|
||||
loadingIndicator.style.display = 'block';
|
||||
// Simular carga
|
||||
setTimeout(() => {
|
||||
loadingIndicator.style.display = 'none';
|
||||
// showNotification('Archivo CSV cargado exitosamente', 'success');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// PapaParse convierte el texto del CSV a objetos JSON automáticamente
|
||||
Papa.parse(file, {
|
||||
header: true, // Importante: Usa tus encabezados (Etiqueta_pin, Latitud, etc.)
|
||||
skipEmptyLines: true,
|
||||
dynamicTyping: true, // Convierte lat/lng a números automáticamente
|
||||
complete: function (results) {
|
||||
const data = results.data;
|
||||
if (data.length === 0) {
|
||||
showNotification("El archivo CSV está vacío.");
|
||||
return;
|
||||
}
|
||||
procesarYPintarPines(data);
|
||||
},
|
||||
error: function (err) {
|
||||
console.error("Error al procesar CSV:", err);
|
||||
showNotification("Hubo un error al leer el archivo.", "error");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function procesarYPintarPines(puntos) {
|
||||
// Usaremos esto para centrar el mapa en todos los puntos cargados
|
||||
const bounds = new maplibregl.LngLatBounds();
|
||||
let puntosValidos = 0;
|
||||
|
||||
puntos.forEach(punto => {
|
||||
// Extraer datos usando tus encabezados exactos
|
||||
const lat = parseFloat(punto.Latitud);
|
||||
const lng = parseFloat(punto.Longitud);
|
||||
const nombre = punto.Etiqueta_pin || "Sin nombre";
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
// Crear el HTML del Popup con toda tu información
|
||||
const contenido = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 150px;">
|
||||
<h4 style="margin: 0 0 5px 0; color: #d32f2f;">${nombre}</h4>
|
||||
<p style="margin: 0; font-size: 11px;">
|
||||
<b>Estado:</b> ${punto.Estado || 'N/A'}<br>
|
||||
<b>Municipio:</b> ${punto.Municipio || 'N/A'}<br>
|
||||
<b>Localidad:</b> ${punto.Localidad || 'N/A'}<br>
|
||||
<b>Nota:</b> ${punto.Comentario || '...'}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
// Crear marcador y añadirlo al mapa
|
||||
const marker = new maplibregl.Marker({ color: "#d32f2f" })
|
||||
.setLngLat([lng, lat])
|
||||
.setPopup(new maplibregl.Popup({ offset: 25 }).setHTML(contenido))
|
||||
.addTo(map);
|
||||
|
||||
// Guardar en tu lista global (la que definimos en pasos anteriores)
|
||||
if (typeof allMarkers !== 'undefined') {
|
||||
allMarkers.push(marker);
|
||||
}
|
||||
|
||||
// Extender los límites para que el mapa sepa qué área cubrir
|
||||
bounds.extend([lng, lat]);
|
||||
puntosValidos++;
|
||||
}
|
||||
});
|
||||
|
||||
if (puntosValidos > 0) {
|
||||
// Mover la cámara para encuadrar todos los pines nuevos
|
||||
map.fitBounds(bounds, { padding: 50, maxZoom: 15 });
|
||||
showNotification(`Éxito: Se cargaron ${puntosValidos} pines.`, "success");
|
||||
} else {
|
||||
showNotification("Error: No se cargaron pines. Verifica el archivo CSV en los valores de 'latitud' y 'longitud' .", "error");
|
||||
}
|
||||
}
|
||||
|
||||
97
static/js/index/05_collapse.js
Normal file
97
static/js/index/05_collapse.js
Normal file
@ -0,0 +1,97 @@
|
||||
import { showNotification } from './99_functions.js';
|
||||
|
||||
|
||||
// Script para el funcionamiento del collapse - VERSIÓN MEJORADA
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const collapseHeader = document.getElementById('form-collapse-header');
|
||||
const collapseContent = document.getElementById('form-collapse-content');
|
||||
const collapseIcon = document.getElementById('collapse-icon');
|
||||
|
||||
// Función mejorada para detectar dispositivo
|
||||
function isMobileOrTablet() {
|
||||
return window.innerWidth <= 1024; // Tablets y móviles
|
||||
}
|
||||
|
||||
// Función para detectar tablet grande
|
||||
function isLargeTablet() {
|
||||
return window.innerWidth >= 769 && window.innerWidth <= 1024;
|
||||
}
|
||||
|
||||
// Función para detectar móvil o tablet pequeña
|
||||
function isSmallDevice() {
|
||||
return window.innerWidth <= 768;
|
||||
}
|
||||
|
||||
// Función para aplicar estado inicial
|
||||
function applyInitialState() {
|
||||
if (isMobileOrTablet()) {
|
||||
collapseContent.classList.add('collapsed');
|
||||
collapseIcon.classList.remove('rotated');
|
||||
|
||||
// Si es tablet grande, mostrar el header azul
|
||||
if (isLargeTablet()) {
|
||||
collapseHeader.style.background = 'linear-gradient(135deg, #1976d2, #2196f3)';
|
||||
collapseHeader.style.color = 'white';
|
||||
}
|
||||
} else {
|
||||
collapseContent.classList.remove('collapsed');
|
||||
collapseIcon.classList.add('rotated');
|
||||
}
|
||||
}
|
||||
|
||||
// Aplicar estado inicial
|
||||
applyInitialState();
|
||||
|
||||
// Alternar collapse al hacer clic en el header
|
||||
collapseHeader.addEventListener('click', function () {
|
||||
// Solo funciona en dispositivos móviles/tablets
|
||||
if (!isMobileOrTablet()) return;
|
||||
|
||||
collapseContent.classList.toggle('collapsed');
|
||||
collapseIcon.classList.toggle('rotated');
|
||||
|
||||
// Animar el icono
|
||||
collapseIcon.style.transition = 'transform 0.3s ease';
|
||||
|
||||
// Mostrar/ocultar notificación
|
||||
if (collapseContent.classList.contains('collapsed')) {
|
||||
showNotification('Formulario colapsado. Toque para expandir.', 'info', 2000);
|
||||
} else {
|
||||
showNotification('Formulario expandido', 'info', 2000);
|
||||
}
|
||||
});
|
||||
|
||||
// Ajustar al cambiar el tamaño de la ventana
|
||||
window.addEventListener('resize', function () {
|
||||
applyInitialState();
|
||||
});
|
||||
|
||||
// Colapsar automáticamente al agregar un marcador (solo en móviles/tablets)
|
||||
const addPinButton = document.getElementById('add-pin');
|
||||
if (addPinButton) {
|
||||
addPinButton.addEventListener('click', function () {
|
||||
if (isMobileOrTablet() && !collapseContent.classList.contains('collapsed')) {
|
||||
setTimeout(() => {
|
||||
collapseContent.classList.add('collapsed');
|
||||
collapseIcon.classList.remove('rotated');
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Colapsar al subir archivo CSV (solo en móviles/tablets)
|
||||
const csvFileInput = document.getElementById('csv-file');
|
||||
if (csvFileInput) {
|
||||
csvFileInput.addEventListener('change', function () {
|
||||
if (isMobileOrTablet() && !collapseContent.classList.contains('collapsed') && this.files.length > 0) {
|
||||
setTimeout(() => {
|
||||
collapseContent.classList.add('collapsed');
|
||||
collapseIcon.classList.remove('rotated');
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
115
static/js/index/99_functions.js
Normal file
115
static/js/index/99_functions.js
Normal file
@ -0,0 +1,115 @@
|
||||
let dic_edos = {
|
||||
'01': 'Aguascalientes',
|
||||
'02': 'Baja California',
|
||||
'03': 'Baja California Sur',
|
||||
'04': 'Campeche',
|
||||
'05': 'Coahuila',
|
||||
'06': 'Colima',
|
||||
'07': 'Chiapas',
|
||||
'08': 'Chihuahua',
|
||||
'09': 'Ciudad de México',
|
||||
'10': 'Durango',
|
||||
'11': 'Guanajuato',
|
||||
'12': 'Guerrero',
|
||||
'13': 'Hidalgo',
|
||||
'14': 'Jalisco',
|
||||
'15': 'Estado de México',
|
||||
'16': 'Michoacán',
|
||||
'17': 'Morelos',
|
||||
'18': 'Nayarit',
|
||||
'19': 'Nuevo León',
|
||||
'20': 'Oaxaca',
|
||||
'21': 'Puebla',
|
||||
'22': 'Querétaro',
|
||||
'23': 'Quintana Roo',
|
||||
'24': 'San Luis Potosí',
|
||||
'25': 'Sinaloa',
|
||||
'26': 'Sonora',
|
||||
'27': 'Tabasco',
|
||||
'28': 'Tamaulipas',
|
||||
'29': 'Tlaxcala',
|
||||
'30': 'Veracruz',
|
||||
'31': 'Yucatán',
|
||||
'32': 'Zacatecas',
|
||||
}
|
||||
|
||||
|
||||
function showNotification(message, type = 'info', duration = 5000) {
|
||||
const notificationArea = document.getElementById('notification-area');
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${type}`;
|
||||
|
||||
let icon = 'info-circle';
|
||||
// por default el tipo es info
|
||||
if (type === 'success') icon = 'check-circle';
|
||||
if (type === 'error') icon = 'exclamation-circle';
|
||||
|
||||
notification.innerHTML = `
|
||||
<i class="fas fa-${icon}"></i>
|
||||
<span>${message}</span>
|
||||
`;
|
||||
|
||||
notificationArea.appendChild(notification);
|
||||
|
||||
// Eliminar notificación después de la duración
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideInRight 0.3s ease reverse';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
|
||||
// function crearPin({ lat, lng, label = "Sin etiqueta", popupHTML = null }) {
|
||||
// const map = getMap();
|
||||
// if (!map) return null;
|
||||
|
||||
// const popup = new maplibregl.Popup({ offset: 25 }).setHTML(
|
||||
// popupHTML ||
|
||||
// `<strong>${label}</strong><br>Lat: ${lat}<br>Lng: ${lng}`
|
||||
// );
|
||||
|
||||
// const marker = new maplibregl.Marker({ color: "#d32f2f" })
|
||||
// .setLngLat([lng, lat])
|
||||
// .setPopup(popup)
|
||||
// .addTo(map);
|
||||
|
||||
// allMarkers.push(marker);
|
||||
// return marker;
|
||||
// }
|
||||
|
||||
// function crearPin({ lat, lng, label = "Sin etiqueta", popupHTML = null }) {
|
||||
// const map = getMap(); // Asegúrate de que esta función exista y devuelva el objeto map
|
||||
// if (!map) return null;
|
||||
|
||||
// const popup = new maplibregl.Popup({ offset: 25 }).setHTML(
|
||||
// popupHTML || `<strong>${label}</strong><br>Lat: ${lat}<br>Lng: ${lng}`
|
||||
// );
|
||||
|
||||
// const marker = new maplibregl.Marker({ color: "#d32f2f" })
|
||||
// .setLngLat([lng, lat])
|
||||
// .setPopup(popup)
|
||||
// .addTo(map);
|
||||
|
||||
// return marker; // Importante devolverlo para poder guardarlo en el array del otro archivo
|
||||
// }
|
||||
|
||||
// En 99_functions.js (Sugerencia de limpieza)
|
||||
function crearPin({ lat, lng, label = "Sin etiqueta", popupHTML = null }) {
|
||||
const map = getMap();
|
||||
if (!map) return null;
|
||||
|
||||
const popup = new maplibregl.Popup({ offset: 25 }).setHTML(
|
||||
popupHTML || `<strong>${label}</strong><br>Lat: ${lat}<br>Lng: ${lng}`
|
||||
);
|
||||
|
||||
const marker = new maplibregl.Marker({ color: "#d32f2f" })
|
||||
.setLngLat([lng, lat])
|
||||
.setPopup(popup)
|
||||
.addTo(map);
|
||||
|
||||
return marker; // <-- Esto es lo que permite que tu allMarkers.push() funcione en el otro archivo
|
||||
}
|
||||
|
||||
|
||||
|
||||
export { dic_edos, showNotification, crearPin };
|
||||
45
templates/404.html
Normal file
45
templates/404.html
Normal file
@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>404 - Página no encontrada</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background: #0f172a;
|
||||
color: #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
font-size: 5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
p {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
a {
|
||||
color: #38bdf8;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>404</h1>
|
||||
<p>La página que buscas no existe.</p>
|
||||
<a href="/">Volver al mapa</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
183
templates/index.html
Normal file
183
templates/index.html
Normal file
@ -0,0 +1,183 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mapa Cobertura Red Móvil 4G/5G</title>
|
||||
|
||||
<link href="https://unpkg.com/maplibre-gl@3.6.0/dist/maplibre-gl.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- {# cdn manejo csv #} -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
|
||||
|
||||
<!-- {# estilos css #} -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/index/tooltip.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/index/notifications.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/index/loading.css') }}">
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='css/index/index.css')}}">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
|
||||
<!-- FORMULARIO MEJORADO CON COLLAPSE -->
|
||||
<div id="pin-form">
|
||||
<!-- COLLAPSE PRINCIPAL DEL FORMULARIO -->
|
||||
<input type="checkbox" id="collapse-toggle" />
|
||||
|
||||
<!-- CABECERA COLAPSABLE -->
|
||||
<label for="collapse-toggle" class="form-header collapsible-header">
|
||||
<div class="header-left">
|
||||
<i class="fas fa-map-pin"></i>
|
||||
<strong>Marcadores</strong>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<i class="fas fa-chevron-down collapse-icon" id="collapse-icon" style="color: white !important;"></i>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<!-- CONTENIDO COLAPSABLE -->
|
||||
<div class="collapsible-content">
|
||||
|
||||
<!-- COLLAPSE 1: Formulario de carga unitaria -->
|
||||
<div class="collapse-section">
|
||||
<input type="checkbox" id="collapse-toggle-1" />
|
||||
<label for="collapse-toggle-1" class="collapse-header">
|
||||
<span><i class="fa-solid fa-location-crosshairs"></i> Carga Individual</span>
|
||||
<i class="fas fa-chevron-down collapse-icon"></i>
|
||||
</label>
|
||||
<div class="collapse-content">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pin-label">Etiqueta</label>
|
||||
<input type="text" id="pin-label" class="form-input" placeholder="Ej. Sitio A, Torre B, etc." />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pin-lat">Latitud</label>
|
||||
<input type="number" id="pin-lat" class="form-input" step="0.0001" placeholder="19.3818" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pin-lng">Longitud</label>
|
||||
<input type="number" id="pin-lng" class="form-input" step="0.0001" placeholder="-99.1766" />
|
||||
</div>
|
||||
|
||||
<button id="add-pin" type="button" class="button-primary">
|
||||
<i class="fas fa-plus-circle"></i> Colocar Marcador
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- COLLAPSE 2: Carga masiva -->
|
||||
<div class="collapse-section">
|
||||
<input type="checkbox" id="collapse-toggle-2" />
|
||||
<label for="collapse-toggle-2" class="collapse-header">
|
||||
<span><i class="fa-solid fa-file-csv"></i> Carga Masiva Marcadores</span>
|
||||
<i class="fas fa-chevron-down collapse-icon"></i>
|
||||
</label>
|
||||
<div class="collapse-content">
|
||||
<div class="csv-section">
|
||||
|
||||
<div class="tooltip">
|
||||
<i class="fas fa-info-circle" style="color: #1976d2; font-size: 0.9rem;"></i>
|
||||
<span class="tooltip-text">El archivo CSV debe contener columnas: etiqueta, latitud (eje. 19.3818), longitud (eje. -99.1766 ). Use la
|
||||
plantilla como referencia.</span>
|
||||
</div>
|
||||
<a href="{{url_for('download_file', filename='template_coordenadas_pines.csv')}}" class="button-secondary">
|
||||
<i class="fas fa-download"></i> Descargar Plantilla
|
||||
</a>
|
||||
|
||||
<input type="file" id="csv-file" class="csv-file-input" accept=".csv" />
|
||||
<label for="csv-file" class="file-upload-label">
|
||||
<i class="fas fa-upload"></i> Subir Plantilla
|
||||
</label>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- COLLAPSE 3: Descarga de capas -->
|
||||
<div class="collapse-section">
|
||||
<input type="checkbox" id="collapse-toggle-3" />
|
||||
<label for="collapse-toggle-3" class="collapse-header">
|
||||
<span><i class="fa-regular fa-map"></i> Descarga de Capas</span>
|
||||
<i class="fas fa-chevron-down collapse-icon"></i>
|
||||
</label>
|
||||
<div class="collapse-content">
|
||||
<div style="padding: 16px;">
|
||||
<a href="{{url_for('download_file', filename='hex_mapa2025-10-01_2025-12-14.gpkg')}}" class="button-secondary">
|
||||
Descargar Capa de Cobertura (gpkg)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<!-- SECCIÓN NO COLAPSABLE: Estado seleccionado -->
|
||||
<div class="active-state-section">
|
||||
<div class="form-group" style="padding: 0; margin: 0;">
|
||||
<label style="color: #9b2226; font-weight: 600;">Estado seleccionado:</label>
|
||||
<span id="nombre-estado-activo" style="font-weight: bold; color: #9b2226;">
|
||||
Ninguno
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- INDICADOR DE CARGA -->
|
||||
<div id="loading-indicator" class="loading-indicator">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">Cargando datos...</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- NOTIFICACIÓN (se añadirá dinámicamente) -->
|
||||
<div id="notification-area"></div>
|
||||
|
||||
<!-- {# CDN map libre #} -->
|
||||
<script src="https://unpkg.com/maplibre-gl@3.6.0/dist/maplibre-gl.js"></script>
|
||||
|
||||
<!-- {# 1.- database hexagonos cobertura #} -->
|
||||
<script src="{{url_for('static', filename='js/index/01_cobertura.js')}}"></script>
|
||||
|
||||
<!-- {# 2.- poligonos estados #} -->
|
||||
<script src="{{url_for('static', filename='js/index/02_edos-poligonos.js')}}" type="module"></script>
|
||||
|
||||
<!-- carga de pines unitarios -->
|
||||
<script src="{{url_for('static', filename='js/index/03_carga_pin_unitarios.js')}}" type="module"></script>
|
||||
|
||||
<!-- carga de pines masivos -->
|
||||
<script src="{{url_for('static', filename='js/index/04_carga_pines_masivo.js')}}" type="module"></script>
|
||||
|
||||
<!-- {# Inicialización del mapa #} -->
|
||||
<!-- <script src="{{ url_for('static', filename='js/index/map-init.js') }}"></script> -->
|
||||
|
||||
<!-- {# collapse #} -->
|
||||
<!-- <script src="{{ url_for('static', filename='js/index/05_collapse.js') }}" type="module"></script> -->
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user