Compare commits
No commits in common. "main" and "428697cd24dc810ecd858c63f124c2adbedcfd15" have entirely different histories.
main
...
428697cd24
51
.drone.yml
51
.drone.yml
@ -1,51 +0,0 @@
|
|||||||
kind: pipeline
|
|
||||||
type: docker
|
|
||||||
name: deploy-polisplexity
|
|
||||||
|
|
||||||
clone:
|
|
||||||
depth: 1
|
|
||||||
submodules: false
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: install dependencies and run Django checks
|
|
||||||
image: python:3.10
|
|
||||||
environment:
|
|
||||||
DATABASE_URL:
|
|
||||||
from_secret: DATABASE_URL
|
|
||||||
SECRET_KEY:
|
|
||||||
from_secret: SECRET_KEY
|
|
||||||
DEBUG: "False"
|
|
||||||
NEO4J_URI:
|
|
||||||
from_secret: NEO4J_URI
|
|
||||||
NEO4J_USERNAME:
|
|
||||||
from_secret: NEO4J_USERNAME
|
|
||||||
NEO4J_PASSWORD:
|
|
||||||
from_secret: NEO4J_PASSWORD
|
|
||||||
OPENAI_API_KEY:
|
|
||||||
from_secret: OPENAI_API_KEY
|
|
||||||
PAGE_ACCESS_TOKEN:
|
|
||||||
from_secret: PAGE_ACCESS_TOKEN
|
|
||||||
VERIFY_TOKEN:
|
|
||||||
from_secret: VERIFY_TOKEN
|
|
||||||
commands:
|
|
||||||
- python -m pip install --upgrade pip
|
|
||||||
- pip install -r requirements.txt
|
|
||||||
- python manage.py check --deploy --fail-level ERROR
|
|
||||||
- echo "✅ Django deploy checks passed"
|
|
||||||
|
|
||||||
- name: deploy to production server
|
|
||||||
image: appleboy/drone-ssh
|
|
||||||
settings:
|
|
||||||
host: 191.101.233.39
|
|
||||||
username: drone
|
|
||||||
port: 22
|
|
||||||
key:
|
|
||||||
from_secret: PROD_SSH_KEY_B
|
|
||||||
script:
|
|
||||||
- cd /home/polisplexity/polisplexity
|
|
||||||
- git pull origin main
|
|
||||||
- docker compose down
|
|
||||||
- docker compose up -d --build
|
|
||||||
- docker compose exec web python manage.py migrate --noinput
|
|
||||||
- docker compose exec web python manage.py collectstatic --noinput
|
|
||||||
- echo "🚀 Production deployment complete"
|
|
30
.env
Normal file
30
.env
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Database Configuration
|
||||||
|
POSTGRES_DB=polisplexity
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=mysecretpassword
|
||||||
|
|
||||||
|
# Django Environment Variables
|
||||||
|
DEBUG=True
|
||||||
|
SECRET_KEY=django-insecure-%*=%u3gv38cv*2iwy)m^)flo3p4w7ol*n5*-7lr*i4^u+(v=#q
|
||||||
|
ALLOWED_HOSTS=127.0.0.1,localhost,app.polisplexity.tech,191.101.233.39,srv566867.hstgr.cloud
|
||||||
|
|
||||||
|
# Database URL
|
||||||
|
DATABASE_URL=postgres://postgres:mysecretpassword@db:5432/polisplexity
|
||||||
|
|
||||||
|
# Static and Media Files
|
||||||
|
STATIC_URL=/static/
|
||||||
|
STATIC_ROOT=/app/static
|
||||||
|
MEDIA_URL=/media/
|
||||||
|
MEDIA_ROOT=/app/media
|
||||||
|
|
||||||
|
# Neo4j Database Configuration
|
||||||
|
NEO4J_URI=neo4j+s://74d433fb.databases.neo4j.io
|
||||||
|
NEO4J_USERNAME=neo4j
|
||||||
|
NEO4J_PASSWORD=4Y5-ppefHkgEiLr-l0qzbf8wNJw0zkOmRmk7cSkSrTg
|
||||||
|
|
||||||
|
# OpenAI API Key
|
||||||
|
OPENAI_API_KEY=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
|
||||||
|
|
||||||
|
# Facebook API Tokens
|
||||||
|
PAGE_ACCESS_TOKEN=EAAIq9z4rVPIBOxJxRnmbjIUsqJ9ZB5hZC9MF4qN64VNpxUCYguMCqUNKSsAjQZAcD9hlhZCv2RcV4GOIFC3Ni6VGoMp3rTFlLwtXxFIklj0FqZAVqSh7i0QT3Kwt9SCx9V9iioSsyFhUQrnpTXZCoDPJy0i2kMkzkY5ZA58hieSeQZBZARz3ZC7XeZCi5uSZBXYCeatGuAZDZD
|
||||||
|
VERIFY_TOKEN=YzQ2VWcODWO922j30HZ9AV113kAisTjcacc3wzURPvFjHCOWjcYP39ThgCWlPQ1w
|
32
.gitignore
vendored
32
.gitignore
vendored
@ -1,32 +0,0 @@
|
|||||||
# Python
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*.sqlite3
|
|
||||||
|
|
||||||
# Environment
|
|
||||||
.env
|
|
||||||
*.env
|
|
||||||
*.env.*
|
|
||||||
|
|
||||||
# Django
|
|
||||||
db.sqlite3
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# VSCode
|
|
||||||
.vscode/
|
|
||||||
*.code-workspace
|
|
||||||
|
|
||||||
# Media/static/runtime
|
|
||||||
/media
|
|
||||||
/static
|
|
||||||
/cache
|
|
||||||
*.dump.sql
|
|
||||||
*.load
|
|
||||||
*.sql
|
|
||||||
|
|
||||||
# System
|
|
||||||
.DS_Store
|
|
||||||
pxy_city_digital_twins/__backup__/
|
|
||||||
Dockerfile.dev
|
|
||||||
docker-compose.override.yml
|
|
||||||
docker-compose.override.yml
|
|
0
.gitmodules
vendored
0
.gitmodules
vendored
43
Dockerfile
43
Dockerfile
@ -1,40 +1,17 @@
|
|||||||
# Etapa base: Python oficial
|
# Use official Python image
|
||||||
FROM python:3.10-slim as base
|
FROM python:3.10
|
||||||
|
|
||||||
# Variables de entorno para producción
|
# Set working directory
|
||||||
ENV PYTHONDONTWRITEBYTECODE 1
|
|
||||||
ENV PYTHONUNBUFFERED 1
|
|
||||||
|
|
||||||
# Instala dependencias del sistema (incluye lo necesario para mysqlclient)
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
build-essential \
|
|
||||||
libgeos-dev \
|
|
||||||
libspatialindex-dev \
|
|
||||||
libproj-dev \
|
|
||||||
proj-data \
|
|
||||||
proj-bin \
|
|
||||||
gdal-bin \
|
|
||||||
libgdal-dev \
|
|
||||||
python3-dev \
|
|
||||||
pkg-config \
|
|
||||||
default-libmysqlclient-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Crea directorio de trabajo
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copia requirements primero (mejor cacheo)
|
# Copy application code
|
||||||
COPY requirements.txt .
|
|
||||||
|
|
||||||
# Instala dependencias Python
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# Copia el resto del proyecto
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Expone el puerto del contenedor
|
# Install dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Expose the application port
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Comando por defecto para producción con Gunicorn
|
# Run Gunicorn server
|
||||||
CMD ["gunicorn", "polisplexity.wsgi:application", "--bind", "0.0.0.0:8000", "--workers=3", "--timeout=120"]
|
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "polisplexity.wsgi:application"]
|
||||||
|
|
||||||
|
1
cache/6c4edc84e061c770f138cce1d051028ec44885c3.json
vendored
Normal file
1
cache/6c4edc84e061c770f138cce1d051028ec44885c3.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
cache/771b2511416e7ff4c83fc38f292274f35e855074.json
vendored
Normal file
1
cache/771b2511416e7ff4c83fc38f292274f35e855074.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
cache/9e0a6000da8bf7662e1c0bca98b9f70539ecf034.json
vendored
Normal file
1
cache/9e0a6000da8bf7662e1c0bca98b9f70539ecf034.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version": 0.6, "generator": "Overpass API 0.7.62.5 1bd436f1", "osm3s": {"timestamp_osm_base": "2025-05-12T23:01:58Z", "copyright": "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL."}, "elements": []}
|
1
cache/b3344dd7b6b8381e28a1ecadb3139da9d2b34eeb.json
vendored
Normal file
1
cache/b3344dd7b6b8381e28a1ecadb3139da9d2b34eeb.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
cache/ce8e8fa8d1ee4e1b554c330158ac018978fdd4af.json
vendored
Normal file
1
cache/ce8e8fa8d1ee4e1b554c330158ac018978fdd4af.json
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
core/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
core/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/admin.cpython-310.pyc
Normal file
BIN
core/__pycache__/admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/apps.cpython-310.pyc
Normal file
BIN
core/__pycache__/apps.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/models.cpython-310.pyc
Normal file
BIN
core/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/urls.cpython-310.pyc
Normal file
BIN
core/__pycache__/urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/views.cpython-310.pyc
Normal file
BIN
core/__pycache__/views.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/migrations/__pycache__/0001_initial.cpython-310.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/migrations/__pycache__/0002_usermenu.cpython-310.pyc
Normal file
BIN
core/migrations/__pycache__/0002_usermenu.cpython-310.pyc
Normal file
Binary file not shown.
BIN
core/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
core/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
@ -1,53 +1,66 @@
|
|||||||
{% extends 'pxy_dashboard/partials/base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}Polisplexity Portal{% endblock title %}
|
{% block title %}Polisplexity Portal{% endblock title %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.card-img-top {
|
||||||
|
height: 200px; /* Fixed height for consistency */
|
||||||
|
object-fit: cover; /* Ensures image covers the area nicely */
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
transform: scale(1.05); /* Slight zoom effect on hover for interactivity */
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock extra_css %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="container mt-4">
|
||||||
<div class="row justify-content-center">
|
<h2 class="text-center mb-4">Welcome to Polisplexity</h2>
|
||||||
<div class="col-12 text-center mb-4">
|
<p class="text-center mb-4">Select one of the options Bellow</p>
|
||||||
<h2 class="mt-3">Welcome to Polisplexity</h2>
|
|
||||||
<p class="text-muted">Select one of the available options</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% for category, items in grouped_menu_items.items %}
|
{% for category, items in grouped_menu_items.items %}
|
||||||
{% if not forloop.first %}
|
{% if not forloop.first %}
|
||||||
<div class="row mt-4">
|
<hr> <!-- Horizontal separator for every new category except the first one -->
|
||||||
<div class="col-12">
|
|
||||||
<hr class="border-secondary">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h4 class="text-primary">{{ category.name }}</h4>
|
|
||||||
<p class="text-muted">{{ category.description }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-3 mb-4">
|
||||||
|
<h3> {{ category.name }}</h3>
|
||||||
|
<small> {{ category.description }}</small>
|
||||||
|
</div>
|
||||||
{% for menu_item in items %}
|
{% for menu_item in items %}
|
||||||
<div class="col-sm-6 col-lg-3">
|
<div class="col-12 col-md-3 mb-4">
|
||||||
<div class="card d-block">
|
<div class="card h-100">
|
||||||
{% if menu_item.image %}
|
{% if menu_item.image %}
|
||||||
<img class="card-img-top" src="{{ menu_item.image.url }}" alt="{{ menu_item.title }}">
|
<!-- Display Image if available -->
|
||||||
{% elif menu_item.icon %}
|
<img src="{{ menu_item.image.url }}" class="card-img-top" alt="{{ menu_item.title }}">
|
||||||
<div class="card-header text-center bg-light-subtle">
|
{% elif menu_item.icon %}
|
||||||
<span class="material-symbols-rounded display-4 text-primary">{{ menu_item.icon }}</span>
|
<!-- Display Icon if image is not available but icon is -->
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<span class="material-symbols-rounded md-48">{{ menu_item.icon }}</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{ menu_item.title }}</h5>
|
||||||
|
<p class="card-text">{{ menu_item.description }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-white">
|
||||||
|
<a href="{{ menu_item.url }}" class="btn btn-primary">{{ menu_item.url_text }}</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">{{ menu_item.title }}</h5>
|
|
||||||
<p class="card-text">{{ menu_item.description }}</p>
|
|
||||||
<a href="{{ menu_item.url }}" class="btn btn-sm btn-primary stretched-link">{{ menu_item.url_text }}</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
$('[data-bs-toggle="tooltip"]').tooltip() // Initialize Bootstrap tooltips
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock extra_js %}
|
||||||
|
BIN
db.sqlite3
Normal file
BIN
db.sqlite3
Normal file
Binary file not shown.
@ -5,12 +5,12 @@ services:
|
|||||||
image: postgres:15
|
image: postgres:15
|
||||||
container_name: polisplexity_postgres
|
container_name: polisplexity_postgres
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
|
||||||
- "5434:5432"
|
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
ports:
|
||||||
|
- "5434:5432"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@ -24,21 +24,16 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- .:/app # Ensure correct project structure
|
||||||
ports:
|
ports:
|
||||||
- "8010:8002"
|
- "8010:8001"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
command: >
|
command: >
|
||||||
sh -c "python manage.py migrate &&
|
sh -c "python manage.py migrate &&
|
||||||
python manage.py collectstatic --noinput &&
|
python manage.py collectstatic --noinput &&
|
||||||
gunicorn polisplexity.wsgi:application --bind 0.0.0.0:8002 --workers=2 --timeout=180"
|
exec gunicorn --bind 0.0.0.0:8001 polisplexity.wsgi:application"
|
||||||
volumes:
|
|
||||||
- static_data:/app/static
|
|
||||||
- media_data:/app/media
|
|
||||||
- ./staticfiles:/app/staticfiles
|
|
||||||
# - .:/app # ←❌ No lo uses en producción: desactiva para evitar sobrescribir
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
static_data:
|
|
||||||
media_data:
|
|
||||||
|
BIN
media/images/logos/logo_U4MX.png
Normal file
BIN
media/images/logos/logo_U4MX.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
media/media/images/menu/01_Forms_image.webp
Normal file
BIN
media/media/images/menu/01_Forms_image.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 708 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
BIN
polisplexity/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
polisplexity/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
polisplexity/__pycache__/settings.cpython-310.pyc
Normal file
BIN
polisplexity/__pycache__/settings.cpython-310.pyc
Normal file
Binary file not shown.
BIN
polisplexity/__pycache__/urls.cpython-310.pyc
Normal file
BIN
polisplexity/__pycache__/urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
polisplexity/__pycache__/wsgi.cpython-310.pyc
Normal file
BIN
polisplexity/__pycache__/wsgi.cpython-310.pyc
Normal file
Binary file not shown.
@ -1,40 +1,43 @@
|
|||||||
|
"""
|
||||||
|
Django settings for polisplexity project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 5.0.3.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
import dj_database_url
|
import dj_database_url
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env file
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
BASE_URL = "https://app.polisplexity.tech"
|
BASE_URL = "https://app.polisplexity.tech"
|
||||||
|
|
||||||
import sys
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
sys.path.append(str(BASE_DIR))
|
|
||||||
|
|
||||||
|
|
||||||
# Core security settings
|
|
||||||
SECRET_KEY = os.getenv("SECRET_KEY")
|
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||||
DEBUG = os.getenv("DEBUG", "False") == "True"
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = os.getenv("DEBUG") == "True"
|
||||||
|
|
||||||
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "").split(",")
|
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "").split(",")
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
# Django built-in apps
|
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
"django.contrib.contenttypes",
|
"django.contrib.contenttypes",
|
||||||
"django.contrib.sessions",
|
"django.contrib.sessions",
|
||||||
"django.contrib.messages",
|
"django.contrib.messages",
|
||||||
"django.contrib.staticfiles",
|
"django.contrib.staticfiles",
|
||||||
"django.contrib.sites", # Required for allauth
|
|
||||||
|
|
||||||
# Allauth
|
|
||||||
"allauth",
|
|
||||||
"allauth.account",
|
|
||||||
"allauth.socialaccount",
|
|
||||||
"allauth.socialaccount.providers.github", # GitHub login only
|
|
||||||
|
|
||||||
# Your custom apps
|
|
||||||
"core",
|
"core",
|
||||||
"pxy_de",
|
"pxy_de",
|
||||||
"pxy_cr",
|
"pxy_cr",
|
||||||
@ -45,40 +48,14 @@ INSTALLED_APPS = [
|
|||||||
"pxy_meta_pages",
|
"pxy_meta_pages",
|
||||||
"pxy_langchain",
|
"pxy_langchain",
|
||||||
"pxy_neo4j",
|
"pxy_neo4j",
|
||||||
"pxy_dashboard",
|
|
||||||
"pxy_dashboard.custom",
|
|
||||||
"pxy_dashboard.apps",
|
|
||||||
"pxy_dashboard.components",
|
|
||||||
"pxy_dashboard.layouts",
|
|
||||||
|
|
||||||
# Third-party apps
|
|
||||||
"crispy_forms",
|
|
||||||
"crispy_bootstrap5",
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
|
||||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
|
||||||
|
|
||||||
SITE_ID = int(os.getenv("SITE_ID", 1))
|
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
|
||||||
"django.contrib.auth.backends.ModelBackend", # default
|
|
||||||
"allauth.account.auth_backends.AuthenticationBackend", # allauth support
|
|
||||||
]
|
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = "/"
|
|
||||||
ACCOUNT_LOGOUT_REDIRECT_URL = "/accounts/login/"
|
|
||||||
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
"allauth.account.middleware.AccountMiddleware",
|
|
||||||
"pxy_dashboard.middleware.LoginRequiredMiddleware",
|
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
]
|
]
|
||||||
@ -88,10 +65,7 @@ ROOT_URLCONF = "polisplexity.urls"
|
|||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
"DIRS": [
|
"DIRS": [os.path.join(BASE_DIR, "templates")],
|
||||||
os.path.join(BASE_DIR, "templates"),
|
|
||||||
os.path.join(BASE_DIR, "pxy_dashboard", "templates"),
|
|
||||||
],
|
|
||||||
"APP_DIRS": True,
|
"APP_DIRS": True,
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"context_processors": [
|
"context_processors": [
|
||||||
@ -99,17 +73,14 @@ TEMPLATES = [
|
|||||||
"django.template.context_processors.request",
|
"django.template.context_processors.request",
|
||||||
"django.contrib.auth.context_processors.auth",
|
"django.contrib.auth.context_processors.auth",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
"pxy_dashboard.context_processors.sidebar_context",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
WSGI_APPLICATION = "polisplexity.wsgi.application"
|
WSGI_APPLICATION = "polisplexity.wsgi.application"
|
||||||
|
|
||||||
# Database
|
# Database Configuration
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": dj_database_url.config(default=os.getenv("DATABASE_URL"))
|
"default": dj_database_url.config(default=os.getenv("DATABASE_URL"))
|
||||||
}
|
}
|
||||||
@ -128,50 +99,29 @@ TIME_ZONE = "UTC"
|
|||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
# Static & Media Files
|
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = "/static/"
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
|
STATIC_ROOT = os.path.join(BASE_DIR, "static") # Ensure this line is correct
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
# Add this if missing
|
||||||
os.path.join(BASE_DIR, "polisplexity/pxy_dashboard/static"), # Jidox assets
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # Fixes mixed content issues
|
||||||
]
|
CSRF_TRUSTED_ORIGINS = ['https://app.polisplexity.tech'] # Allow CSRF over HTTPS
|
||||||
|
|
||||||
MEDIA_URL = "/media/"
|
MEDIA_URL = "/media/"
|
||||||
MEDIA_ROOT = os.path.join(BASE_DIR, "mediafiles")
|
MEDIA_ROOT = os.getenv("MEDIA_ROOT", os.path.join(BASE_DIR, "media"))
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
# External services
|
# Facebook API Tokens
|
||||||
PAGE_ACCESS_TOKEN = os.getenv("PAGE_ACCESS_TOKEN")
|
PAGE_ACCESS_TOKEN = os.getenv("PAGE_ACCESS_TOKEN")
|
||||||
VERIFY_TOKEN = os.getenv("VERIFY_TOKEN")
|
VERIFY_TOKEN = os.getenv("VERIFY_TOKEN")
|
||||||
|
|
||||||
# Async-safe for Neo4j or Celery
|
|
||||||
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
|
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
|
||||||
# ...pero silenciamos la comprobación que falla en producción:
|
|
||||||
SILENCED_SYSTEM_CHECKS = ["async.E001"]
|
|
||||||
|
|
||||||
# Neo4j
|
# Neo4j Database Configuration
|
||||||
NEO4J_URI = os.getenv("NEO4J_URI")
|
NEO4J_URI = os.getenv("NEO4J_URI")
|
||||||
NEO4J_USERNAME = os.getenv("NEO4J_USERNAME")
|
NEO4J_USERNAME = os.getenv("NEO4J_USERNAME")
|
||||||
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD")
|
NEO4J_PASSWORD = os.getenv("NEO4J_PASSWORD")
|
||||||
|
|
||||||
# OpenAI
|
# OpenAI API Key
|
||||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
||||||
|
|
||||||
# CSRF protection for production
|
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
|
||||||
"https://app.polisplexity.tech",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Support for secure reverse proxy (e.g., Nginx or Hostinger HTTPS proxy)
|
|
||||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
|
||||||
|
|
||||||
|
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
|
||||||
EMAIL_HOST = "smtp.hostinger.com"
|
|
||||||
EMAIL_PORT = 465
|
|
||||||
EMAIL_USE_SSL = True
|
|
||||||
EMAIL_HOST_USER = "noreply@polisplexity.tech" # Cambia esto por tu correo real
|
|
||||||
EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") # Mejor usar .env
|
|
||||||
DEFAULT_FROM_EMAIL = "Polisplexity <noreply@polisplexity.tech>"
|
|
||||||
|
@ -25,15 +25,12 @@ admin.site.index_title = "Welcome to Polisplexity City Technologies Portal"
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("accounts/", include("allauth.urls")), # ← Add this line
|
path('', include('core.urls')),
|
||||||
path('', include('pxy_dashboard.urls')),
|
|
||||||
path('core', include('core.urls')),
|
|
||||||
path('', include('pxy_city_digital_twins.urls')),
|
path('', include('pxy_city_digital_twins.urls')),
|
||||||
path('pxy_whatsapp/', include('pxy_whatsapp.urls')),
|
path('pxy_whatsapp/', include('pxy_whatsapp.urls')),
|
||||||
path('bots/', include('pxy_bots.urls')),
|
path('bots/', include('pxy_bots.urls')), # Webhook URL: /bots/webhook/<bot_name>/
|
||||||
path('pxy_meta_pages/', include('pxy_meta_pages.urls', namespace='pxy_meta_pages')),
|
path('pxy_meta_pages/', include('pxy_meta_pages.urls', namespace='pxy_meta_pages')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
BIN
pxy_bots/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_bots/__pycache__/admin.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_bots/__pycache__/apps.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/apps.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_bots/__pycache__/handlers.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/handlers.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_bots/__pycache__/models.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_bots/__pycache__/urls.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_bots/__pycache__/utils.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/utils.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_bots/__pycache__/views.cpython-310.pyc
Normal file
BIN
pxy_bots/__pycache__/views.cpython-310.pyc
Normal file
Binary file not shown.
@ -1,26 +1,21 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import TelegramBot
|
from .models import TelegramBot
|
||||||
|
from django.utils.html import format_html
|
||||||
|
|
||||||
@admin.register(TelegramBot)
|
@admin.register(TelegramBot)
|
||||||
class TelegramBotAdmin(admin.ModelAdmin):
|
class TelegramBotAdmin(admin.ModelAdmin):
|
||||||
list_display = ("name", "username", "is_active", "get_assistant_name")
|
list_display = ("name", "username", "is_active", "get_assistant_name", "set_webhook_action")
|
||||||
search_fields = ("name", "username")
|
search_fields = ("name", "username")
|
||||||
list_filter = ("is_active",)
|
list_filter = ("is_active",)
|
||||||
actions = ["set_webhooks"]
|
actions = ["set_webhooks"]
|
||||||
|
|
||||||
@admin.action(description="Set webhooks for selected bots")
|
@admin.action(description="Set webhooks for selected bots")
|
||||||
def set_webhooks(self, request, queryset):
|
def set_webhooks(self, request, queryset):
|
||||||
base_url = f"{request.scheme}://{request.get_host()}"
|
base_url = request.build_absolute_uri("/")[:-1] # Get base server URL
|
||||||
|
results = []
|
||||||
for bot in queryset:
|
for bot in queryset:
|
||||||
if bot.is_active:
|
if bot.is_active:
|
||||||
try:
|
try:
|
||||||
if not bot.assistant:
|
|
||||||
self.message_user(
|
|
||||||
request,
|
|
||||||
f"Bot {bot.name} has no assistant configured.",
|
|
||||||
level="warning",
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
result = bot.set_webhook(base_url)
|
result = bot.set_webhook(base_url)
|
||||||
self.message_user(
|
self.message_user(
|
||||||
request,
|
request,
|
||||||
@ -33,6 +28,7 @@ class TelegramBotAdmin(admin.ModelAdmin):
|
|||||||
f"Failed to set webhook for {bot.name}: {str(e)}",
|
f"Failed to set webhook for {bot.name}: {str(e)}",
|
||||||
level="error",
|
level="error",
|
||||||
)
|
)
|
||||||
|
results.append(result)
|
||||||
else:
|
else:
|
||||||
self.message_user(
|
self.message_user(
|
||||||
request,
|
request,
|
||||||
@ -43,4 +39,14 @@ class TelegramBotAdmin(admin.ModelAdmin):
|
|||||||
def get_assistant_name(self, obj):
|
def get_assistant_name(self, obj):
|
||||||
"""Show the name of the assistant linked to the bot."""
|
"""Show the name of the assistant linked to the bot."""
|
||||||
return obj.assistant.name if obj.assistant else "None"
|
return obj.assistant.name if obj.assistant else "None"
|
||||||
|
|
||||||
get_assistant_name.short_description = "Assistant Name"
|
get_assistant_name.short_description = "Assistant Name"
|
||||||
|
|
||||||
|
def set_webhook_action(self, obj):
|
||||||
|
"""Button in the Django admin to manually trigger webhook setup."""
|
||||||
|
return format_html(
|
||||||
|
'<a class="button" href="{}">Set Webhook</a>',
|
||||||
|
f"/admin/pxy_bots/set_webhook/{obj.id}/"
|
||||||
|
)
|
||||||
|
|
||||||
|
set_webhook_action.short_description = "Webhook"
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from telegram import Update, ForceReply
|
from telegram import Update, ForceReply
|
||||||
import logging
|
import logging
|
||||||
from pxy_openai.assistants import OpenAIAssistant
|
from pxy_openai.assistants import OpenAIAssistant
|
||||||
from pxy_bots.models import TelegramBot
|
from .models import TelegramBot
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
from pxy_langchain.models import AIAssistant
|
from pxy_langchain.models import AIAssistant
|
||||||
from pxy_langchain.services import LangchainAIService
|
from pxy_langchain.services import LangchainAIService
|
||||||
@ -33,26 +33,18 @@ async def help_command(update: Update):
|
|||||||
user = update.effective_user
|
user = update.effective_user
|
||||||
await update.message.reply_text(f"Help! How can I assist you, {user.first_name}?")
|
await update.message.reply_text(f"Help! How can I assist you, {user.first_name}?")
|
||||||
|
|
||||||
|
|
||||||
async def handle_location(update: Update):
|
async def handle_location(update: Update):
|
||||||
"""Respond to a location message."""
|
"""Respond to a location message."""
|
||||||
location = update.message.location
|
location = update.message.location
|
||||||
if location:
|
if location:
|
||||||
lat = location.latitude
|
await update.message.reply_text(
|
||||||
lon = location.longitude
|
f"Thanks for sharing your location! Latitude: {location.latitude}, Longitude: {location.longitude}"
|
||||||
text = (
|
)
|
||||||
"🚀 Your Digital Twin is ready!\n\n"
|
|
||||||
f"🌍 Latitude: {lat}\n"
|
|
||||||
f"🌍 Longitude: {lon}\n\n"
|
|
||||||
"🔗 Access it here:\n"
|
|
||||||
f"https://app.polisplexity.tech/city/digital/twin/osm_city/?lat={lat}&long={lon}&scale=0.1\n\n"
|
|
||||||
"Thanks for sharing your location!")
|
|
||||||
await update.message.reply_text(text)
|
|
||||||
else:
|
else:
|
||||||
await update.message.reply_text("Please share your location.")
|
await update.message.reply_text("Please share your location.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def respond(update, bot_name):
|
async def respond(update, bot_name):
|
||||||
"""Respond to user messages using the LangChain AI service."""
|
"""Respond to user messages using the LangChain AI service."""
|
||||||
try:
|
try:
|
@ -1,11 +0,0 @@
|
|||||||
from .common import start, help_command, handle_location, respond
|
|
||||||
from .handlers_citizen import next_truck, report_trash, private_pickup, green_balance
|
|
||||||
from .handlers_city import next_route, complete_stop, missed_stop, my_eco_score as city_eco_score
|
|
||||||
from .handlers_private import available_jobs, accept_job, next_pickup, complete_pickup, my_eco_score as private_eco_score
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"start", "help_command", "handle_location", "respond",
|
|
||||||
"next_truck", "report_trash", "private_pickup", "green_balance",
|
|
||||||
"next_route", "complete_stop", "missed_stop", "city_eco_score",
|
|
||||||
"available_jobs", "accept_job", "next_pickup", "complete_pickup", "private_eco_score"
|
|
||||||
]
|
|
@ -1,30 +0,0 @@
|
|||||||
from telegram import Update, KeyboardButton, ReplyKeyboardMarkup
|
|
||||||
|
|
||||||
async def next_truck(update: Update):
|
|
||||||
if update.message.location:
|
|
||||||
lat, lon = update.message.location.latitude, update.message.location.longitude
|
|
||||||
await update.message.reply_text(
|
|
||||||
f"🚛 El camión pasa por tu zona ({lat}, {lon}) mañana a las 8:00 AM.")
|
|
||||||
else:
|
|
||||||
keyboard = [[KeyboardButton("📍 Enviar ubicación", request_location=True)]]
|
|
||||||
markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=True)
|
|
||||||
await update.message.reply_text(
|
|
||||||
"Mándame tu ubicación para decirte cuándo pasa el camión.",
|
|
||||||
reply_markup=markup)
|
|
||||||
|
|
||||||
async def report_trash(update: Update):
|
|
||||||
user_text = update.message.text
|
|
||||||
await update.message.reply_text(f"🌱 Calculé tu CO₂ por '{user_text}' y te di 5 Monedas Verdes. ¡Chido!")
|
|
||||||
|
|
||||||
async def private_pickup(update: Update):
|
|
||||||
if update.message.location:
|
|
||||||
await update.message.reply_text("🛵 ¡Pepe de la motito va en camino! Llega en 10 min.")
|
|
||||||
else:
|
|
||||||
keyboard = [[KeyboardButton("📍 Enviar ubicación", request_location=True)]]
|
|
||||||
markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=True)
|
|
||||||
await update.message.reply_text(
|
|
||||||
"Mándame tu ubicación para buscar un reco privado.",
|
|
||||||
reply_markup=markup)
|
|
||||||
|
|
||||||
async def green_balance(update: Update):
|
|
||||||
await update.message.reply_text("💰 Llevas 23 Monedas Verdes acumuladas y evitaste 15 kg CO₂.")
|
|
@ -1,13 +0,0 @@
|
|||||||
from telegram import Update
|
|
||||||
|
|
||||||
async def next_route(update: Update):
|
|
||||||
await update.message.reply_text("🚛 Hoy te toca: Calle Reforma, Av. Juárez y Callejón Verde.")
|
|
||||||
|
|
||||||
async def complete_stop(update: Update):
|
|
||||||
await update.message.reply_text("✅ Parada marcada como recolectada. ¡Buen trabajo!")
|
|
||||||
|
|
||||||
async def missed_stop(update: Update):
|
|
||||||
await update.message.reply_text("🚧 Parada marcada como NO recolectada.")
|
|
||||||
|
|
||||||
async def my_eco_score(update: Update):
|
|
||||||
await update.message.reply_text("🌿 Llevas 120 Monedas Verdes este mes por tu eficiencia.")
|
|
@ -1,16 +0,0 @@
|
|||||||
from telegram import Update
|
|
||||||
|
|
||||||
async def available_jobs(update: Update):
|
|
||||||
await update.message.reply_text("📝 Hay 2 chambas cerca: Calle Pino y Calle Limón.")
|
|
||||||
|
|
||||||
async def accept_job(update: Update):
|
|
||||||
await update.message.reply_text("👌 Chamba aceptada. Ve a Calle Pino 123.")
|
|
||||||
|
|
||||||
async def next_pickup(update: Update):
|
|
||||||
await update.message.reply_text("➡️ Tu siguiente recolección es en Calle Limón 45.")
|
|
||||||
|
|
||||||
async def complete_pickup(update: Update):
|
|
||||||
await update.message.reply_text("✅ ¡Recolección completada! Te ganaste 3 Monedas Verdes.")
|
|
||||||
|
|
||||||
async def my_eco_score(update: Update):
|
|
||||||
await update.message.reply_text("🏆 Tienes 45 Monedas Verdes acumuladas este mes.")
|
|
@ -1,34 +0,0 @@
|
|||||||
# Generated by Django 5.0.3 on 2025-05-20 08:04
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('pxy_bots', '0005_remove_telegrambot_assistant_id_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='TelegramConversation',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('user_id', models.CharField(max_length=64)),
|
|
||||||
('started_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('bot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='conversations', to='pxy_bots.telegrambot')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='TelegramMessage',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('direction', models.CharField(choices=[('in', 'In'), ('out', 'Out')], max_length=4)),
|
|
||||||
('content', models.TextField()),
|
|
||||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('response_time_ms', models.IntegerField(blank=True, null=True)),
|
|
||||||
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='pxy_bots.telegramconversation')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
BIN
pxy_bots/migrations/__pycache__/0001_initial.cpython-310.pyc
Normal file
BIN
pxy_bots/migrations/__pycache__/0001_initial.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
pxy_bots/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pxy_bots/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
@ -1,4 +1,3 @@
|
|||||||
import requests
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from pxy_langchain.models import AIAssistant # Now referencing LangChain AI assistants
|
from pxy_langchain.models import AIAssistant # Now referencing LangChain AI assistants
|
||||||
|
|
||||||
@ -46,40 +45,3 @@ class TelegramBot(models.Model):
|
|||||||
return response.json()
|
return response.json()
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Failed to set webhook for {self.name}: {response.json()}")
|
raise ValueError(f"Failed to set webhook for {self.name}: {response.json()}")
|
||||||
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
class TelegramConversation(models.Model):
|
|
||||||
bot = models.ForeignKey(
|
|
||||||
'TelegramBot',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='conversations'
|
|
||||||
)
|
|
||||||
user_id = models.CharField(max_length=64)
|
|
||||||
started_at = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.user_id} @ {self.started_at:%Y-%m-%d %H:%M}"
|
|
||||||
|
|
||||||
class TelegramMessage(models.Model):
|
|
||||||
IN = 'in'
|
|
||||||
OUT = 'out'
|
|
||||||
DIRECTION_CHOICES = [
|
|
||||||
(IN, 'In'),
|
|
||||||
(OUT, 'Out'),
|
|
||||||
]
|
|
||||||
|
|
||||||
conversation = models.ForeignKey(
|
|
||||||
TelegramConversation,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='messages'
|
|
||||||
)
|
|
||||||
direction = models.CharField(max_length=4, choices=DIRECTION_CHOICES)
|
|
||||||
content = models.TextField()
|
|
||||||
timestamp = models.DateTimeField(auto_now_add=True)
|
|
||||||
response_time_ms = models.IntegerField(null=True, blank=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"[{self.direction}] {self.content[:30]}…"
|
|
||||||
|
|
||||||
|
16
pxy_bots/set_webhook.py
Normal file
16
pxy_bots/set_webhook.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import requests
|
||||||
|
from pxy_bots.models import TelegramBot
|
||||||
|
|
||||||
|
BASE_URL = "https://your-domain.com/bots/webhook/"
|
||||||
|
|
||||||
|
def set_telegram_webhooks():
|
||||||
|
"""Sets webhooks for all active bots."""
|
||||||
|
bots = TelegramBot.objects.filter(is_active=True)
|
||||||
|
for bot in bots:
|
||||||
|
webhook_url = f"{BASE_URL}{bot.name}/"
|
||||||
|
response = requests.post(
|
||||||
|
f"https://api.telegram.org/bot{bot.token}/setWebhook",
|
||||||
|
data={"url": webhook_url}
|
||||||
|
)
|
||||||
|
print(f"Webhook for {bot.name} ({bot.username}) set to: {webhook_url}")
|
||||||
|
print("Response:", response.json())
|
@ -1,164 +1,68 @@
|
|||||||
import os
|
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
|
|
||||||
import openai
|
|
||||||
from telegram import Update, Bot
|
from telegram import Update, Bot
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
|
|
||||||
from .models import TelegramBot
|
from .models import TelegramBot
|
||||||
from pxy_langchain.services import LangchainAIService
|
from pxy_langchain.services import LangchainAIService
|
||||||
from .handlers import (
|
from .handlers import dream_city_command, start, help_command, handle_location
|
||||||
start, help_command, handle_location,
|
import logging
|
||||||
next_truck, report_trash, private_pickup, green_balance,
|
|
||||||
next_route, complete_stop, missed_stop, city_eco_score,
|
|
||||||
available_jobs, accept_job, next_pickup, complete_pickup, private_eco_score
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
openai.api_key = os.getenv("OPENAI_API_KEY")
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_location_message(update):
|
|
||||||
if update.message.location:
|
|
||||||
await handle_location(update)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_citizen_commands(update, text):
|
|
||||||
if text == "/start":
|
|
||||||
await start(update)
|
|
||||||
elif text == "/help":
|
|
||||||
await help_command(update)
|
|
||||||
elif text == "/next_truck":
|
|
||||||
await next_truck(update)
|
|
||||||
elif text == "/report_trash":
|
|
||||||
await report_trash(update)
|
|
||||||
elif text == "/private_pickup":
|
|
||||||
await private_pickup(update)
|
|
||||||
elif text == "/green_balance":
|
|
||||||
await green_balance(update)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_city_commands(update, text):
|
|
||||||
if text == "/start":
|
|
||||||
await start(update)
|
|
||||||
elif text == "/help":
|
|
||||||
await help_command(update)
|
|
||||||
elif text == "/next_route":
|
|
||||||
await next_route(update)
|
|
||||||
elif text == "/complete_stop":
|
|
||||||
await complete_stop(update)
|
|
||||||
elif text == "/missed_stop":
|
|
||||||
await missed_stop(update)
|
|
||||||
elif text == "/my_eco_score":
|
|
||||||
await city_eco_score(update)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_private_commands(update, text):
|
|
||||||
if text == "/start":
|
|
||||||
await start(update)
|
|
||||||
elif text == "/help":
|
|
||||||
await help_command(update)
|
|
||||||
elif text == "/available_jobs":
|
|
||||||
await available_jobs(update)
|
|
||||||
elif text.startswith("/accept_job"):
|
|
||||||
await accept_job(update)
|
|
||||||
elif text == "/next_pickup":
|
|
||||||
await next_pickup(update)
|
|
||||||
elif text == "/complete_pickup":
|
|
||||||
await complete_pickup(update)
|
|
||||||
elif text == "/my_eco_score":
|
|
||||||
await private_eco_score(update)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def transcribe_with_whisper(update, bot):
|
|
||||||
# 1) Descarga el audio
|
|
||||||
tg_file = await bot.get_file(update.message.voice.file_id)
|
|
||||||
download_path = f"/tmp/{update.message.voice.file_id}.ogg"
|
|
||||||
await tg_file.download_to_drive(download_path)
|
|
||||||
|
|
||||||
# 2) Llama al endpoint de transcripción
|
|
||||||
with open(download_path, "rb") as audio:
|
|
||||||
# Como response_format="text", esto retorna un str
|
|
||||||
transcript_str = openai.audio.transcriptions.create(
|
|
||||||
model="gpt-4o-transcribe", # o "whisper-1"
|
|
||||||
file=audio,
|
|
||||||
response_format="text",
|
|
||||||
language="es"
|
|
||||||
)
|
|
||||||
return transcript_str.strip()
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
async def telegram_webhook(request, bot_name):
|
async def telegram_webhook(request, bot_name):
|
||||||
|
"""
|
||||||
|
Webhook view that handles Telegram updates asynchronously and only uses LangChain.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info(f"Webhook called for bot: {bot_name}")
|
logger.info(f"Webhook called for bot: {bot_name}")
|
||||||
|
|
||||||
# Carga bot (solo ORM en sync_to_async)
|
# Step 1: Fetch the bot instance asynchronously
|
||||||
try:
|
try:
|
||||||
bot_instance = await sync_to_async(TelegramBot.objects.get)(
|
bot_instance = await sync_to_async(TelegramBot.objects.get)(name=bot_name, is_active=True)
|
||||||
name=bot_name, is_active=True
|
logger.info(f"Loaded bot configuration: {bot_instance}")
|
||||||
)
|
|
||||||
except TelegramBot.DoesNotExist:
|
except TelegramBot.DoesNotExist:
|
||||||
|
logger.error(f"Bot '{bot_name}' not found or inactive.")
|
||||||
return JsonResponse({"error": f"Bot '{bot_name}' not found."}, status=400)
|
return JsonResponse({"error": f"Bot '{bot_name}' not found."}, status=400)
|
||||||
|
|
||||||
|
# Step 2: Ensure the bot has a LangChain assistant
|
||||||
if not bot_instance.assistant:
|
if not bot_instance.assistant:
|
||||||
|
logger.error(f"No assistant configured for bot '{bot_name}'.")
|
||||||
return JsonResponse({"error": "Assistant not configured."}, status=400)
|
return JsonResponse({"error": "Assistant not configured."}, status=400)
|
||||||
if request.method != "POST":
|
|
||||||
return JsonResponse({"error": "Invalid request method"}, status=400)
|
|
||||||
|
|
||||||
payload = json.loads(request.body.decode("utf-8"))
|
# Step 3: Process POST request from Telegram
|
||||||
update = Update.de_json(payload, Bot(token=bot_instance.token))
|
if request.method == "POST":
|
||||||
|
try:
|
||||||
|
request_body = json.loads(request.body.decode("utf-8"))
|
||||||
|
update = Update.de_json(request_body, Bot(token=bot_instance.token))
|
||||||
|
logger.info(f"Update received: {update}")
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"Failed to decode JSON: {e}")
|
||||||
|
return JsonResponse({"error": "Invalid JSON payload"}, status=400)
|
||||||
|
|
||||||
if not update.message:
|
# Step 4: Route commands to the appropriate handlers
|
||||||
return JsonResponse({"status": "no message"})
|
if update.message:
|
||||||
|
if update.message.text == "/start":
|
||||||
|
await start(update)
|
||||||
|
elif update.message.text == "/help":
|
||||||
|
await help_command(update)
|
||||||
|
elif update.message.text == "/dream_city":
|
||||||
|
await dream_city_command(update)
|
||||||
|
elif update.message.location:
|
||||||
|
await handle_location(update)
|
||||||
|
else:
|
||||||
|
# Step 5: Process AI-generated response using LangChain
|
||||||
|
assistant_instance = await sync_to_async(LangchainAIService)(bot_instance.assistant)
|
||||||
|
bot_response = await sync_to_async(assistant_instance.generate_response)(update.message.text)
|
||||||
|
|
||||||
|
# Step 6: Send the response back to Telegram
|
||||||
|
await update.message.reply_text(bot_response)
|
||||||
|
|
||||||
# 1) Geolocalización
|
|
||||||
if await handle_location_message(update):
|
|
||||||
return JsonResponse({"status": "ok"})
|
return JsonResponse({"status": "ok"})
|
||||||
|
|
||||||
# 2) Voz: transcribe y report_trash
|
logger.warning("Received non-POST request")
|
||||||
if update.message.voice:
|
return JsonResponse({"error": "Invalid request method"}, status=400)
|
||||||
bot = Bot(token=bot_instance.token)
|
|
||||||
transcript = await transcribe_with_whisper(update, bot)
|
|
||||||
if not transcript:
|
|
||||||
await update.message.reply_text(
|
|
||||||
"No pude entender tu mensaje de voz. Intenta de nuevo."
|
|
||||||
)
|
|
||||||
return JsonResponse({"status": "ok"})
|
|
||||||
assistant_instance = await sync_to_async(LangchainAIService)(bot_instance.assistant)
|
|
||||||
bot_response = await sync_to_async(assistant_instance.generate_response)(transcript)
|
|
||||||
await update.message.reply_text(bot_response)
|
|
||||||
return JsonResponse({"status": "ok"})
|
|
||||||
|
|
||||||
# 3) Comandos de texto
|
|
||||||
text = update.message.text or ""
|
|
||||||
if bot_name == "PepeBasuritaCoinsBot" and await dispatch_citizen_commands(update, text):
|
|
||||||
return JsonResponse({"status": "ok"})
|
|
||||||
if bot_name == "PepeCamioncitoBot" and await dispatch_city_commands(update, text):
|
|
||||||
return JsonResponse({"status": "ok"})
|
|
||||||
if bot_name == "PepeMotitoBot" and await dispatch_private_commands(update, text):
|
|
||||||
return JsonResponse({"status": "ok"})
|
|
||||||
|
|
||||||
# 4) Fallback LLM
|
|
||||||
assistant_instance = await sync_to_async(LangchainAIService)(bot_instance.assistant)
|
|
||||||
bot_response = await sync_to_async(assistant_instance.generate_response)(text)
|
|
||||||
await update.message.reply_text(bot_response)
|
|
||||||
|
|
||||||
return JsonResponse({"status": "ok"})
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in webhook: {e}")
|
logger.error(f"Error in webhook: {e}")
|
||||||
|
1
pxy_city_digital_twins
Submodule
1
pxy_city_digital_twins
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit bf4767d280cb31a73b62ebb6ff912c604423de52
|
@ -1,75 +0,0 @@
|
|||||||
# Polisplexity Digital Twin Viewer
|
|
||||||
|
|
||||||
This application is a Django-based 3D digital twin city renderer using A-Frame and real-world OpenStreetMap (OSM) data. It allows visualization of buildings, fiber paths, cell towers, and other urban infrastructure in a simulated, interactive WebVR environment.
|
|
||||||
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
- 🔲 **Building extrusion from OSM**: Downloads building footprints with geometry and height/levels metadata and extrudes them into 3D blocks.
|
|
||||||
- 🛰️ **Street network rendering**: Downloads local driving network and represents it visually as 3D fiber links.
|
|
||||||
- 🏙️ **Recentered city layout**: All elements are normalized to a `(0,0)` coordinate center and scaled down to allow a bird’s-eye view or giant-perspective simulation.
|
|
||||||
- 📡 **A-Frame-based environment**: Uses `aframe-environment-component` for sky, lighting, ground, and interactions.
|
|
||||||
- 🎯 **Status gauges**: Each building displays a status gauge with a rotating ring and transparent glass core, labeled with mock status data.
|
|
||||||
- 🧠 **Per-entity click interaction**: Clicking on a gauge changes its color and toggles the status (mocked).
|
|
||||||
- 🌐 **Dynamic generation by coordinates**: Any city view can be created dynamically via URL parameters like `lat`, `long`, and `scale`.
|
|
||||||
|
|
||||||
## 🏗️ Stack
|
|
||||||
|
|
||||||
| Component | Technology |
|
|
||||||
|--------------------|-----------------------------|
|
|
||||||
| Backend | Django 5.x |
|
|
||||||
| Mapping API | `osmnx`, `shapely`, `geopandas` |
|
|
||||||
| Frontend (3D) | A-Frame 1.7.0 |
|
|
||||||
| Visualization Libs | `aframe-environment-component` |
|
|
||||||
| Deployment Ready? | Yes, via Docker + Gunicorn |
|
|
||||||
|
|
||||||
## 🔌 Example Usage
|
|
||||||
|
|
||||||
To load a city block from Centro Histórico, Mexico City:
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
[http://localhost:8001/city/digital/twin/osm\_city/?lat=19.391097\&long=-99.157815\&scale=0.1](http://localhost:8001/city/digital/twin/osm_city/?lat=19.391097&long=-99.157815&scale=0.1)
|
|
||||||
|
|
||||||
````
|
|
||||||
|
|
||||||
## 🧪 Directory Highlights
|
|
||||||
|
|
||||||
- `pxy_city_digital_twins/views.py`: Request handler that decides which generator to use (`osm_city`, `random_city`, etc.)
|
|
||||||
- `services/osm_city.py`: Main generator for real-world urban geometry based on lat/lon.
|
|
||||||
- `templates/pxy_city_digital_twins/city_digital_twin.html`: A-Frame scene renderer.
|
|
||||||
- `templates/pxy_city_digital_twins/_status_gauge.html`: UI fragment for interactive gauges on city elements.
|
|
||||||
|
|
||||||
## 📦 Dependencies
|
|
||||||
|
|
||||||
Add these to `requirements.txt`:
|
|
||||||
|
|
||||||
```txt
|
|
||||||
osmnx>=1.9.3
|
|
||||||
shapely
|
|
||||||
geopandas
|
|
||||||
````
|
|
||||||
|
|
||||||
Optional (for better performance in prod):
|
|
||||||
|
|
||||||
```txt
|
|
||||||
gunicorn
|
|
||||||
dj-database-url
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚧 To-Do
|
|
||||||
|
|
||||||
* [ ] Load `status` from a real database or agent simulation
|
|
||||||
* [ ] Add 3D models (e.g., trees, street furniture)
|
|
||||||
* [ ] Support texture-mapped facades
|
|
||||||
* [ ] Add time-based simulation / animation
|
|
||||||
* [ ] Integrate sensor/IoT mock data stream
|
|
||||||
|
|
||||||
## 👀 Screenshot
|
|
||||||
|
|
||||||
> *Coming soon* — consider generating A-Frame scene screenshots automatically using headless browser tools.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Maintained by [Hadox Research Labs](https://hadox.org)**
|
|
||||||
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class PxyCityDigitalTwinsConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "pxy_city_digital_twins"
|
|
@ -1,25 +0,0 @@
|
|||||||
# Generated by Django 5.0.3 on 2025-05-21 21:44
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='WastePickup',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('subdivision', models.CharField(max_length=100)),
|
|
||||||
('vehicle', models.CharField(max_length=50)),
|
|
||||||
('step_id', models.CharField(max_length=50)),
|
|
||||||
('quarter', models.PositiveSmallIntegerField()),
|
|
||||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,11 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
class WastePickup(models.Model):
|
|
||||||
subdivision = models.CharField(max_length=100)
|
|
||||||
vehicle = models.CharField(max_length=50)
|
|
||||||
step_id = models.CharField(max_length=50)
|
|
||||||
quarter = models.PositiveSmallIntegerField() # 1 through 4
|
|
||||||
timestamp = models.DateTimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.subdivision}/{self.vehicle}/{self.step_id} → Q{self.quarter} @ {self.timestamp}"
|
|
@ -1,155 +0,0 @@
|
|||||||
import random
|
|
||||||
from .network import compute_mst_fiber_paths, compute_network_summary
|
|
||||||
|
|
||||||
GRID_SIZE = 5
|
|
||||||
SPACING = 15 # Distance between objects
|
|
||||||
|
|
||||||
|
|
||||||
def generate_com_con_city_data(lat, long):
|
|
||||||
"""
|
|
||||||
Generate a digital twin for a real-world city (e.g., Concepción).
|
|
||||||
Returns towers, fiber paths, wifi hotspots, and a summary.
|
|
||||||
"""
|
|
||||||
random.seed(f"{lat},{long}")
|
|
||||||
|
|
||||||
center_x = lat
|
|
||||||
center_z = long
|
|
||||||
|
|
||||||
towers = generate_towers(center_x, center_z)
|
|
||||||
fiber_paths = compute_mst_fiber_paths(towers)
|
|
||||||
wifi_hotspots = generate_wifi_hotspots(center_x, center_z)
|
|
||||||
summary = compute_network_summary(towers, fiber_paths, wifi_hotspots)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'towers': towers,
|
|
||||||
'fiber_paths': fiber_paths,
|
|
||||||
'wifi_hotspots': wifi_hotspots,
|
|
||||||
'network_summary': summary,
|
|
||||||
}
|
|
||||||
|
|
||||||
def generate_towers(center_x, center_z, mode="streets"):
|
|
||||||
"""
|
|
||||||
Generate towers either in a 'grid' or at realistic 'streets' (mocked).
|
|
||||||
mode: "grid" | "streets"
|
|
||||||
"""
|
|
||||||
if mode == "streets":
|
|
||||||
return generate_street_corner_towers(center_x, center_z)
|
|
||||||
else:
|
|
||||||
return generate_grid_towers(center_x, center_z)
|
|
||||||
|
|
||||||
|
|
||||||
import osmnx as ox
|
|
||||||
|
|
||||||
def generate_street_corner_towers(center_x, center_z, min_towers=10):
|
|
||||||
"""
|
|
||||||
Get real intersections from OSM and convert them to local x/z positions
|
|
||||||
relative to center_x / center_z (in meters). Fallbacks to mocked layout if needed.
|
|
||||||
"""
|
|
||||||
print("📍 Starting generate_street_corner_towers()")
|
|
||||||
print(f"→ center_x: {center_x}, center_z: {center_z}")
|
|
||||||
|
|
||||||
point = (center_x, center_z)
|
|
||||||
print(f"→ Using real lat/lon: {point}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
for dist in [100, 200, 500, 1000]:
|
|
||||||
print(f"🛰️ Trying OSM download at radius: {dist} meters...")
|
|
||||||
G = ox.graph_from_point(point, dist=dist, network_type='all')
|
|
||||||
G_undirected = G.to_undirected()
|
|
||||||
degrees = dict(G_undirected.degree())
|
|
||||||
intersections = [n for n, d in degrees.items() if d >= 3]
|
|
||||||
print(f" ✅ Found {len(intersections)} valid intersections.")
|
|
||||||
if len(intersections) >= min_towers:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError("No sufficient intersections found.")
|
|
||||||
|
|
||||||
nodes, _ = ox.graph_to_gdfs(G)
|
|
||||||
origin_lon = nodes.loc[intersections]['x'].mean()
|
|
||||||
origin_lat = nodes.loc[intersections]['y'].mean()
|
|
||||||
print(f"📌 Using origin_lon: {origin_lon:.6f}, origin_lat: {origin_lat:.6f} for local projection")
|
|
||||||
|
|
||||||
def latlon_to_sim(lon, lat):
|
|
||||||
dx = (lon - origin_lon) * 111320
|
|
||||||
dz = (lat - origin_lat) * 110540
|
|
||||||
return center_x + dx, center_z + dz
|
|
||||||
|
|
||||||
towers = []
|
|
||||||
for i, node_id in enumerate(intersections):
|
|
||||||
row = nodes.loc[node_id]
|
|
||||||
x_sim, z_sim = latlon_to_sim(row['x'], row['y'])
|
|
||||||
print(f" 🗼 Tower #{i+1} at sim position: x={x_sim:.2f}, z={z_sim:.2f}")
|
|
||||||
towers.append(make_tower(x_sim, z_sim, i + 1))
|
|
||||||
|
|
||||||
print(f"✅ Done. Total towers returned: {len(towers)}\n")
|
|
||||||
return towers
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ OSM tower generation failed: {e}")
|
|
||||||
print("⚠️ Falling back to mocked tower layout.")
|
|
||||||
|
|
||||||
# Return 3x3 fixed grid as fallback
|
|
||||||
offsets = [(-30, -30), (-30, 0), (-30, 30),
|
|
||||||
(0, -30), (0, 0), (0, 30),
|
|
||||||
(30, -30), (30, 0), (30, 30)]
|
|
||||||
|
|
||||||
towers = []
|
|
||||||
for i, (dx, dz) in enumerate(offsets):
|
|
||||||
x = center_x + dx
|
|
||||||
z = center_z + dz
|
|
||||||
towers.append(make_tower(x, z, i + 1))
|
|
||||||
|
|
||||||
print(f"✅ Fallback returned {len(towers)} towers.\n")
|
|
||||||
return towers
|
|
||||||
|
|
||||||
|
|
||||||
def generate_grid_towers(center_x, center_z):
|
|
||||||
"""Generates a 5×5 grid of towers around the city center."""
|
|
||||||
towers = []
|
|
||||||
for i in range(GRID_SIZE):
|
|
||||||
for j in range(GRID_SIZE):
|
|
||||||
x = center_x + (i - GRID_SIZE // 2) * SPACING
|
|
||||||
z = center_z + (j - GRID_SIZE // 2) * SPACING
|
|
||||||
towers.append({
|
|
||||||
'id': len(towers) + 1,
|
|
||||||
'status': 'Active' if random.random() > 0.2 else 'Inactive',
|
|
||||||
'position_x': x,
|
|
||||||
'position_y': 0,
|
|
||||||
'position_z': z,
|
|
||||||
'height': random.randint(40, 60),
|
|
||||||
'range': random.randint(500, 1000),
|
|
||||||
'color': '#ff4500'
|
|
||||||
})
|
|
||||||
return towers
|
|
||||||
|
|
||||||
|
|
||||||
def generate_wifi_hotspots(center_x, center_z):
|
|
||||||
"""Places 10 Wi-Fi hotspots randomly around the city center."""
|
|
||||||
hotspots = []
|
|
||||||
bound = SPACING * GRID_SIZE / 2
|
|
||||||
for i in range(10):
|
|
||||||
x = center_x + random.uniform(-bound, bound)
|
|
||||||
z = center_z + random.uniform(-bound, bound)
|
|
||||||
hotspots.append({
|
|
||||||
'id': i + 1,
|
|
||||||
'position_x': x,
|
|
||||||
'position_y': 1.5,
|
|
||||||
'position_z': z,
|
|
||||||
'status': 'Online' if random.random() > 0.2 else 'Offline',
|
|
||||||
'radius': random.randint(1, 3),
|
|
||||||
'color': '#32cd32'
|
|
||||||
})
|
|
||||||
return hotspots
|
|
||||||
|
|
||||||
def make_tower(x, z, id):
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'status': 'Active',
|
|
||||||
'position_x': x,
|
|
||||||
'position_y': 0,
|
|
||||||
'position_z': z,
|
|
||||||
'height': 50,
|
|
||||||
'range': 1000,
|
|
||||||
'color': '#ff4500'
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
|||||||
import math
|
|
||||||
|
|
||||||
def rectangular_layout(num_elements, max_dimension):
|
|
||||||
grid_size = int(math.sqrt(num_elements))
|
|
||||||
spacing = max_dimension // grid_size
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'position_x': (i % grid_size) * spacing,
|
|
||||||
'position_z': (i // grid_size) * spacing
|
|
||||||
}
|
|
||||||
for i in range(num_elements)
|
|
||||||
]
|
|
||||||
|
|
||||||
def circular_layout(num_elements, radius):
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'position_x': radius * math.cos(2 * math.pi * i / num_elements),
|
|
||||||
'position_z': radius * math.sin(2 * math.pi * i / num_elements)
|
|
||||||
}
|
|
||||||
for i in range(num_elements)
|
|
||||||
]
|
|
||||||
|
|
||||||
def diagonal_layout(num_elements, max_position):
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'position_x': i * max_position // num_elements,
|
|
||||||
'position_z': i * max_position // num_elements
|
|
||||||
}
|
|
||||||
for i in range(num_elements)
|
|
||||||
]
|
|
||||||
|
|
||||||
def triangular_layout(num_elements):
|
|
||||||
positions = []
|
|
||||||
row_length = 1
|
|
||||||
while num_elements > 0:
|
|
||||||
for i in range(row_length):
|
|
||||||
if num_elements <= 0:
|
|
||||||
break
|
|
||||||
positions.append({
|
|
||||||
'position_x': i * 10 - (row_length - 1) * 5, # Spread out each row symmetrically
|
|
||||||
'position_z': row_length * 10
|
|
||||||
})
|
|
||||||
num_elements -= 1
|
|
||||||
row_length += 1
|
|
||||||
return positions
|
|
@ -1,63 +0,0 @@
|
|||||||
import networkx as nx
|
|
||||||
import math
|
|
||||||
|
|
||||||
def compute_distance(t1, t2):
|
|
||||||
"""
|
|
||||||
Compute Euclidean distance between two towers in the horizontal plane.
|
|
||||||
"""
|
|
||||||
dx = t1['position_x'] - t2['position_x']
|
|
||||||
dz = t1['position_z'] - t2['position_z']
|
|
||||||
return math.sqrt(dx**2 + dz**2)
|
|
||||||
|
|
||||||
def compute_mst_fiber_paths(towers):
|
|
||||||
"""
|
|
||||||
Given a list of tower dictionaries, compute a Minimum Spanning Tree (MST)
|
|
||||||
and return a list of fiber paths connecting the towers.
|
|
||||||
"""
|
|
||||||
G = nx.Graph()
|
|
||||||
# Add towers as nodes
|
|
||||||
for tower in towers:
|
|
||||||
G.add_node(tower['id'], **tower)
|
|
||||||
|
|
||||||
# Add edges: compute pairwise distances
|
|
||||||
n = len(towers)
|
|
||||||
for i in range(n):
|
|
||||||
for j in range(i+1, n):
|
|
||||||
d = compute_distance(towers[i], towers[j])
|
|
||||||
G.add_edge(towers[i]['id'], towers[j]['id'], weight=d)
|
|
||||||
|
|
||||||
# Compute MST
|
|
||||||
mst = nx.minimum_spanning_tree(G)
|
|
||||||
|
|
||||||
fiber_paths = []
|
|
||||||
for edge in mst.edges(data=True):
|
|
||||||
id1, id2, data = edge
|
|
||||||
# Find towers corresponding to these IDs
|
|
||||||
tower1 = next(t for t in towers if t['id'] == id1)
|
|
||||||
tower2 = next(t for t in towers if t['id'] == id2)
|
|
||||||
|
|
||||||
fiber_paths.append({
|
|
||||||
'id': len(fiber_paths) + 1,
|
|
||||||
'start_x': tower1['position_x'],
|
|
||||||
'start_z': tower1['position_z'],
|
|
||||||
'end_x': tower2['position_x'],
|
|
||||||
'end_z': tower2['position_z'],
|
|
||||||
'mid_x': (tower1['position_x'] + tower2['position_x']) / 2,
|
|
||||||
'mid_y': 0.1, # Slightly above the ground
|
|
||||||
'mid_z': (tower1['position_z'] + tower2['position_z']) / 2,
|
|
||||||
'length': data['weight'],
|
|
||||||
# Optionally, compute the angle in degrees if needed:
|
|
||||||
'angle': math.degrees(math.atan2(tower2['position_x'] - tower1['position_x'],
|
|
||||||
tower2['position_z'] - tower1['position_z'])),
|
|
||||||
'status': 'Connected',
|
|
||||||
'color': '#4682b4'
|
|
||||||
})
|
|
||||||
return fiber_paths
|
|
||||||
|
|
||||||
def compute_network_summary(towers, fiber_paths, wifi_hotspots):
|
|
||||||
total_fiber = sum(fiber['length'] for fiber in fiber_paths)
|
|
||||||
return {
|
|
||||||
'num_towers': len(towers),
|
|
||||||
'total_fiber_length': total_fiber,
|
|
||||||
'num_wifi': len(wifi_hotspots),
|
|
||||||
}
|
|
@ -1,120 +0,0 @@
|
|||||||
import osmnx as ox
|
|
||||||
import shapely
|
|
||||||
import random
|
|
||||||
import uuid
|
|
||||||
import networkx as nx
|
|
||||||
from matplotlib import cm
|
|
||||||
|
|
||||||
def generate_osm_city_data(lat, lon, dist=400, scale=1.0):
|
|
||||||
print(f"🏙️ Fetching OSM buildings and network at ({lat}, {lon})")
|
|
||||||
|
|
||||||
scale_factor = scale
|
|
||||||
status_options = ["OK", "Warning", "Critical", "Offline"]
|
|
||||||
|
|
||||||
# ————— STREET NETWORK —————
|
|
||||||
G = ox.graph_from_point((lat, lon), dist=dist, network_type='drive').to_undirected()
|
|
||||||
degree = dict(G.degree())
|
|
||||||
max_degree = max(degree.values()) if degree else 1
|
|
||||||
color_map = cm.get_cmap("plasma")
|
|
||||||
|
|
||||||
# ————— BUILDINGS —————
|
|
||||||
tags = {"building": True}
|
|
||||||
gdf = ox.features_from_point((lat, lon), tags=tags, dist=dist)
|
|
||||||
gdf = gdf[gdf.geometry.type.isin(["Polygon", "MultiPolygon"])].to_crs(epsg=3857)
|
|
||||||
gdf["centroid"] = gdf.geometry.centroid
|
|
||||||
|
|
||||||
raw_buildings = []
|
|
||||||
for i, row in gdf.iterrows():
|
|
||||||
centroid = row["centroid"]
|
|
||||||
polygon = row["geometry"]
|
|
||||||
building_id = f"BLD-{uuid.uuid4().hex[:6].upper()}"
|
|
||||||
status = random.choice(status_options)
|
|
||||||
|
|
||||||
try:
|
|
||||||
height = float(row.get("height", None))
|
|
||||||
except:
|
|
||||||
height = float(row.get("building:levels", 3)) * 3.2 if row.get("building:levels") else 10.0
|
|
||||||
|
|
||||||
try:
|
|
||||||
node = ox.distance.nearest_nodes(G, X=centroid.x, Y=centroid.y)
|
|
||||||
node_degree = degree.get(node, 0)
|
|
||||||
except:
|
|
||||||
node_degree = 0
|
|
||||||
|
|
||||||
norm_value = node_degree / max_degree
|
|
||||||
rgba = color_map(norm_value)
|
|
||||||
hex_color = '#%02x%02x%02x' % tuple(int(c * 255) for c in rgba[:3])
|
|
||||||
|
|
||||||
raw_buildings.append({
|
|
||||||
"id": building_id,
|
|
||||||
"raw_x": centroid.x,
|
|
||||||
"raw_z": centroid.y,
|
|
||||||
"width": polygon.bounds[2] - polygon.bounds[0],
|
|
||||||
"depth": polygon.bounds[3] - polygon.bounds[1],
|
|
||||||
"height": height,
|
|
||||||
"color": hex_color,
|
|
||||||
"status": status,
|
|
||||||
})
|
|
||||||
|
|
||||||
# ————— CENTER AND SCALE —————
|
|
||||||
if raw_buildings:
|
|
||||||
avg_x = sum(b['raw_x'] for b in raw_buildings) / len(raw_buildings)
|
|
||||||
avg_z = sum(b['raw_z'] for b in raw_buildings) / len(raw_buildings)
|
|
||||||
|
|
||||||
buildings = [{
|
|
||||||
"id": b['id'],
|
|
||||||
"position_x": (b['raw_x'] - avg_x) * scale_factor,
|
|
||||||
"position_z": (b['raw_z'] - avg_z) * scale_factor,
|
|
||||||
"width": b['width'] * scale_factor,
|
|
||||||
"depth": b['depth'] * scale_factor,
|
|
||||||
"height": b['height'] * scale_factor,
|
|
||||||
"color": b['color'],
|
|
||||||
"status": b['status'],
|
|
||||||
} for b in raw_buildings]
|
|
||||||
else:
|
|
||||||
buildings = []
|
|
||||||
|
|
||||||
return {"buildings": buildings}
|
|
||||||
|
|
||||||
|
|
||||||
def generate_osm_road_midpoints_only(lat, lon, dist=400, scale=1.0):
|
|
||||||
import osmnx as ox
|
|
||||||
import networkx as nx
|
|
||||||
from shapely.geometry import LineString
|
|
||||||
import geopandas as gpd
|
|
||||||
import random
|
|
||||||
|
|
||||||
print(f"🛣️ Fetching road midpoints only at ({lat}, {lon})")
|
|
||||||
|
|
||||||
# Obtener grafo y convertir a GeoDataFrame con geometría
|
|
||||||
G = ox.graph_from_point((lat, lon), dist=dist, network_type='drive').to_undirected()
|
|
||||||
edge_gdf = ox.graph_to_gdfs(G, nodes=False, edges=True)
|
|
||||||
|
|
||||||
# Proyectar a metros (3857)
|
|
||||||
edge_gdf = edge_gdf.to_crs(epsg=3857)
|
|
||||||
|
|
||||||
midpoints_raw = []
|
|
||||||
for _, row in edge_gdf.iterrows():
|
|
||||||
geom = row.get('geometry', None)
|
|
||||||
if isinstance(geom, LineString) and geom.length > 0:
|
|
||||||
midpoint = geom.interpolate(0.5, normalized=True)
|
|
||||||
midpoints_raw.append((midpoint.x, midpoint.y))
|
|
||||||
|
|
||||||
if not midpoints_raw:
|
|
||||||
return {"roads": []}
|
|
||||||
|
|
||||||
avg_x = sum(x for x, _ in midpoints_raw) / len(midpoints_raw)
|
|
||||||
avg_z = sum(z for _, z in midpoints_raw) / len(midpoints_raw)
|
|
||||||
|
|
||||||
midpoints = [{
|
|
||||||
"id": f"RD-{i}",
|
|
||||||
"position_x": (x - avg_x) * scale,
|
|
||||||
"position_z": (z - avg_z) * scale,
|
|
||||||
"status": random.choice(["OK", "Warning", "Critical", "Offline"])
|
|
||||||
} for i, (x, z) in enumerate(midpoints_raw)]
|
|
||||||
|
|
||||||
# DEBUG
|
|
||||||
for i in range(min(5, len(midpoints))):
|
|
||||||
print(f"🧭 Road {i}: x={midpoints[i]['position_x']:.2f}, z={midpoints[i]['position_z']:.2f}")
|
|
||||||
|
|
||||||
return {"roads": midpoints}
|
|
@ -1,25 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
def get_environment_preset(lat, long):
|
|
||||||
"""
|
|
||||||
Determines the A-Frame environment preset based on latitude and longitude.
|
|
||||||
You can adjust the logic to suit your needs.
|
|
||||||
"""
|
|
||||||
# Example logic: adjust these thresholds as needed
|
|
||||||
if lat >= 60 or lat <= -60:
|
|
||||||
return 'snow' # Polar regions: snow environment
|
|
||||||
elif lat >= 30 or lat <= -30:
|
|
||||||
return 'forest' # Mid-latitudes: forest environment
|
|
||||||
elif long >= 100:
|
|
||||||
return 'goldmine' # Arbitrary example: for far east longitudes, a 'goldmine' preset
|
|
||||||
else:
|
|
||||||
return 'desert' # Default to desert for lower latitudes and moderate longitudes
|
|
||||||
|
|
||||||
|
|
||||||
def get_environment_by_lat(lat):
|
|
||||||
if lat > 60 or lat < -60:
|
|
||||||
return 'yeti'
|
|
||||||
elif 30 < lat < 60 or -30 > lat > -60:
|
|
||||||
return 'forest'
|
|
||||||
else:
|
|
||||||
return 'desert'
|
|
@ -1,81 +0,0 @@
|
|||||||
import random
|
|
||||||
from .layouts import rectangular_layout, circular_layout, diagonal_layout, triangular_layout
|
|
||||||
|
|
||||||
|
|
||||||
def generate_random_city_data(innovation_pct=100, technology_pct=100, science_pct=100, max_position=100, radius=50):
|
|
||||||
num_buildings = random.randint(5, 35)
|
|
||||||
num_lamps = random.randint(5, 100)
|
|
||||||
num_trees = random.randint(5, 55)
|
|
||||||
|
|
||||||
# Buildings layout distribution
|
|
||||||
num_rectangular_buildings = int(num_buildings * innovation_pct / 100)
|
|
||||||
num_circular_buildings = (num_buildings - num_rectangular_buildings) // 2
|
|
||||||
num_triangular_buildings = num_buildings - num_rectangular_buildings - num_circular_buildings
|
|
||||||
|
|
||||||
building_positions = rectangular_layout(num_rectangular_buildings, max_position) + \
|
|
||||||
circular_layout(num_circular_buildings, radius) + \
|
|
||||||
triangular_layout(num_triangular_buildings)
|
|
||||||
|
|
||||||
# Lamps layout distribution
|
|
||||||
num_triangular_lamps = int(num_lamps * technology_pct / 100)
|
|
||||||
num_circular_lamps = (num_lamps - num_triangular_lamps) // 2
|
|
||||||
num_diagonal_lamps = num_lamps - num_triangular_lamps - num_circular_lamps
|
|
||||||
|
|
||||||
lamp_positions = triangular_layout(num_triangular_lamps) + \
|
|
||||||
circular_layout(num_circular_lamps, radius) + \
|
|
||||||
diagonal_layout(num_diagonal_lamps, max_position)
|
|
||||||
|
|
||||||
# Trees layout distribution
|
|
||||||
num_circular_trees = int(num_trees * science_pct / 100)
|
|
||||||
num_triangular_trees = (num_trees - num_circular_trees) // 2
|
|
||||||
num_diagonal_trees = num_trees - num_circular_trees - num_triangular_trees
|
|
||||||
|
|
||||||
tree_positions = circular_layout(num_circular_trees, radius) + \
|
|
||||||
triangular_layout(num_triangular_trees) + \
|
|
||||||
diagonal_layout(num_diagonal_trees, max_position)
|
|
||||||
|
|
||||||
buildings = [
|
|
||||||
{
|
|
||||||
'id': i + 1,
|
|
||||||
'status': random.choice(['Occupied', 'Vacant', 'Under Construction']),
|
|
||||||
'position_x': pos['position_x'],
|
|
||||||
'position_z': pos['position_z'],
|
|
||||||
'height': random.randint(10, 50),
|
|
||||||
'width': random.randint(5, 20),
|
|
||||||
'depth': random.randint(5, 20),
|
|
||||||
'color': random.choice(['#8a2be2', '#5f9ea0', '#ff6347', '#4682b4']),
|
|
||||||
'file': ''
|
|
||||||
} for i, pos in enumerate(building_positions)
|
|
||||||
]
|
|
||||||
|
|
||||||
lamps = [
|
|
||||||
{
|
|
||||||
'id': i + 1,
|
|
||||||
'status': random.choice(['Functional', 'Non-functional']),
|
|
||||||
'position_x': pos['position_x'],
|
|
||||||
'position_z': pos['position_z'],
|
|
||||||
'height': random.randint(3, 10),
|
|
||||||
'color': random.choice(['#ffff00', '#ff0000', '#00ff00']),
|
|
||||||
} for i, pos in enumerate(lamp_positions)
|
|
||||||
]
|
|
||||||
|
|
||||||
trees = [
|
|
||||||
{
|
|
||||||
'id': i + 1,
|
|
||||||
'status': random.choice(['Healthy', 'Diseased', 'Wilting']),
|
|
||||||
'position_x': pos['position_x'],
|
|
||||||
'position_z': pos['position_z'],
|
|
||||||
'height': random.randint(5, 30),
|
|
||||||
'radius_bottom': random.uniform(0.1, 0.5),
|
|
||||||
'radius_top': random.uniform(0.5, 2.0),
|
|
||||||
'color_trunk': '#8b4513',
|
|
||||||
'color_leaves': random.choice(['#228b22', '#90ee90', '#8b4513']),
|
|
||||||
} for i, pos in enumerate(tree_positions)
|
|
||||||
]
|
|
||||||
|
|
||||||
return {
|
|
||||||
'buildings': buildings,
|
|
||||||
'lamps': lamps,
|
|
||||||
'trees': trees,
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
|||||||
import json
|
|
||||||
import polyline
|
|
||||||
from django.conf import settings
|
|
||||||
from pxy_dashboard.apps.models import OptScenario
|
|
||||||
|
|
||||||
def get_dispatch_data_for(subdivision):
|
|
||||||
"""
|
|
||||||
Load the last OptScenario, decode its VROOM JSON for this subdivision,
|
|
||||||
and return a list of routes, each with 'route_id', 'coords', and 'steps'.
|
|
||||||
"""
|
|
||||||
scenario = OptScenario.objects.last()
|
|
||||||
if not scenario or not scenario.dispatch_json:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# load & slice
|
|
||||||
with open(scenario.dispatch_json.path, encoding='utf-8') as f:
|
|
||||||
raw = json.load(f)
|
|
||||||
raw_subdiv = raw.get(subdivision)
|
|
||||||
if not raw_subdiv:
|
|
||||||
return None
|
|
||||||
|
|
||||||
dispatch_data = []
|
|
||||||
for route in raw_subdiv.get('routes', []):
|
|
||||||
vehicle = route.get('vehicle')
|
|
||||||
# decode polyline geometry → [ [lon, lat], … ]
|
|
||||||
coords = []
|
|
||||||
if route.get('geometry'):
|
|
||||||
pts = polyline.decode(route['geometry'])
|
|
||||||
coords = [[lng, lat] for lat, lng in pts]
|
|
||||||
|
|
||||||
# build steps
|
|
||||||
steps = []
|
|
||||||
for step in route.get('steps', []):
|
|
||||||
lon, lat = step['location']
|
|
||||||
popup = (
|
|
||||||
f"{step.get('type','job').title()}<br>"
|
|
||||||
f"ID: {step.get('id','–')}<br>"
|
|
||||||
f"Load: {step.get('load',[0])[0]} kg"
|
|
||||||
)
|
|
||||||
steps.append({
|
|
||||||
'position': [lon, lat],
|
|
||||||
'popup': popup,
|
|
||||||
'step_type': step.get('type','job'),
|
|
||||||
})
|
|
||||||
|
|
||||||
dispatch_data.append({
|
|
||||||
'route_id': str(vehicle),
|
|
||||||
'coords': coords,
|
|
||||||
'steps': steps,
|
|
||||||
})
|
|
||||||
|
|
||||||
return dispatch_data
|
|
@ -1,38 +0,0 @@
|
|||||||
<a-entity
|
|
||||||
class="status-gauge"
|
|
||||||
gauge-click-toggle
|
|
||||||
position="0 {{ offset_y|default:'3' }} 0"
|
|
||||||
scale="0.3 0.3 0.3">
|
|
||||||
|
|
||||||
<!-- Glass core -->
|
|
||||||
<a-circle
|
|
||||||
radius="0.6"
|
|
||||||
class="glass-core"
|
|
||||||
color="{{ ring_color|default:'#00FFFF' }}"
|
|
||||||
material="shader: standard; transparent: true; opacity: 0.3; metalness: 0.8; roughness: 0.1; side: double"
|
|
||||||
rotation="0 90 0">
|
|
||||||
</a-circle>
|
|
||||||
|
|
||||||
<!-- Animated outer ring -->
|
|
||||||
<a-ring
|
|
||||||
class="gauge-ring"
|
|
||||||
radius-inner="0.7"
|
|
||||||
radius-outer="0.9"
|
|
||||||
color="{{ ring_color|default:'#00FFFF' }}"
|
|
||||||
material="shader: flat; opacity: 0.8; side: double"
|
|
||||||
rotation="0 90 0"
|
|
||||||
animation="property: rotation; to: 0 90 0; loop: true; dur: 2000; easing: linear">
|
|
||||||
</a-ring>
|
|
||||||
|
|
||||||
<!-- Dynamic Text -->
|
|
||||||
<a-text
|
|
||||||
class="gauge-label"
|
|
||||||
value="ID: {{ id }}\n{{ status }}"
|
|
||||||
align="center"
|
|
||||||
color="#FFFFFF"
|
|
||||||
width="2"
|
|
||||||
side="double"
|
|
||||||
position="0 0 0.02"
|
|
||||||
rotation="0 90 0">
|
|
||||||
</a-text>
|
|
||||||
</a-entity>
|
|
@ -1,38 +0,0 @@
|
|||||||
<a-entity
|
|
||||||
class="status-gauge-augmented"
|
|
||||||
gauge-click-toggle
|
|
||||||
position="{{ pos_x|default:'0' }} {{ pos_y|default:'1.5' }} {{ pos_z|default:'0' }}"
|
|
||||||
scale="{{ scale|default:'0.3 0.3 0.3' }}">
|
|
||||||
|
|
||||||
<!-- Glass core -->
|
|
||||||
<a-circle
|
|
||||||
radius="0.6"
|
|
||||||
class="glass-core"
|
|
||||||
color="{{ ring_color|default:'#00FFFF' }}"
|
|
||||||
material="shader: standard; transparent: true; opacity: 0.3; metalness: 0.8; roughness: 0.1; side: double"
|
|
||||||
rotation="0 0 0">
|
|
||||||
</a-circle>
|
|
||||||
|
|
||||||
<!-- Animated outer ring -->
|
|
||||||
<a-ring
|
|
||||||
class="gauge-ring"
|
|
||||||
radius-inner="0.7"
|
|
||||||
radius-outer="0.9"
|
|
||||||
color="{{ ring_color|default:'#00FFFF' }}"
|
|
||||||
material="shader: flat; opacity: 0.8; side: double"
|
|
||||||
rotation="0 0 0"
|
|
||||||
animation="property: rotation; to: 0 90 0; loop: true; dur: 2000; easing: linear">
|
|
||||||
</a-ring>
|
|
||||||
|
|
||||||
<!-- Text label -->
|
|
||||||
<a-text
|
|
||||||
class="gauge-label"
|
|
||||||
value="{{ label|default:'Status\nOK' }}"
|
|
||||||
align="center"
|
|
||||||
color="#FFFFFF"
|
|
||||||
width="2"
|
|
||||||
side="double"
|
|
||||||
position="0 0 0.02"
|
|
||||||
rotation="0 0 0">
|
|
||||||
</a-text>
|
|
||||||
</a-entity>
|
|
@ -1,31 +0,0 @@
|
|||||||
<!-- Glass core -->
|
|
||||||
<a-circle
|
|
||||||
radius="1"
|
|
||||||
class="glass-core"
|
|
||||||
color="{{ ring_color|default:'#00FFFF' }}"
|
|
||||||
material="shader: standard; transparent: true; opacity: 0.3; metalness: 0.8; roughness: 0.1; side: double"
|
|
||||||
rotation="0 90 0">
|
|
||||||
</a-circle>
|
|
||||||
|
|
||||||
<!-- Animated outer ring -->
|
|
||||||
<a-ring
|
|
||||||
class="gauge-ring"
|
|
||||||
radius-inner="1.2"
|
|
||||||
radius-outer="1.4"
|
|
||||||
color="{{ ring_color|default:'#00FFFF' }}"
|
|
||||||
material="shader: flat; opacity: 0.8; side: double"
|
|
||||||
rotation="0 90 0"
|
|
||||||
animation="property: rotation; to: 0 90 0; loop: true; dur: 2000; easing: linear">
|
|
||||||
</a-ring>
|
|
||||||
|
|
||||||
<!-- Dynamic Text -->
|
|
||||||
<a-text
|
|
||||||
class="gauge-label"
|
|
||||||
value="ID: {{ id }}\n{{ status }}"
|
|
||||||
align="center"
|
|
||||||
color="#FFFFFF"
|
|
||||||
width="2"
|
|
||||||
side="double"
|
|
||||||
position="0 0 0.02"
|
|
||||||
rotation="0 90 0">
|
|
||||||
</a-text>
|
|
@ -1,131 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>Waste Route Visualization</title>
|
|
||||||
<!-- A-Frame core -->
|
|
||||||
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
|
|
||||||
<script src="https://raw.githack.com/AR-js-org/AR.js/3.4.5/aframe/build/aframe-ar.js"></script>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<a-scene id="scene">
|
|
||||||
<!-- Camera & cursor -->
|
|
||||||
<a-entity
|
|
||||||
id="mainCamera"
|
|
||||||
camera look-controls wasd-controls
|
|
||||||
position="0 5 10">
|
|
||||||
<a-cursor color="#ffffff"></a-cursor>
|
|
||||||
</a-entity>
|
|
||||||
|
|
||||||
<!-- Invisible ground-plane fallback -->
|
|
||||||
<a-plane id="basePlane"
|
|
||||||
rotation="-90 0 0"
|
|
||||||
color="#444" opacity="0.0">
|
|
||||||
</a-plane>
|
|
||||||
|
|
||||||
<!-- 1) Draw city buildings from your OSM data -->
|
|
||||||
{% for building in city_data.buildings %}
|
|
||||||
<a-entity id="{{ building.id }}" status="{{ building.status }}">
|
|
||||||
<a-box position="{{ building.position_x }} {{ building.height }} {{ building.position_z }}"
|
|
||||||
width="{{ building.width }}"
|
|
||||||
height="{{ building.height }}"
|
|
||||||
depth="{{ building.depth }}"
|
|
||||||
color="{{ building.color }}"
|
|
||||||
opacity="0.8">
|
|
||||||
</a-box>
|
|
||||||
<a-entity position="{{ building.position_x }} {{ building.height }} {{ building.position_z }}">
|
|
||||||
|
|
||||||
</a-entity>
|
|
||||||
</a-entity>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- 2) Draw each job-sphere & gauge from step_positions -->
|
|
||||||
{% for pos in step_positions %}
|
|
||||||
<a-entity position="{{ pos.x }} 1 {{ pos.z }}">
|
|
||||||
<a-sphere radius="10"
|
|
||||||
color="#FF4136"
|
|
||||||
emissive="#FF4136"
|
|
||||||
opacity="0.7">
|
|
||||||
</a-sphere>
|
|
||||||
<!-- gauge above the sphere -->
|
|
||||||
<a-entity class="status-gauge" gauge-click-toggle position="11 3 0">
|
|
||||||
{% include "pxy_city_digital_twins/_status_gauge_waste.html" with ring_color="#FF4136" status=pos.step.step_type id=pos.step.id %}
|
|
||||||
</a-entity>
|
|
||||||
</a-entity>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</a-scene>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.querySelector('#scene').addEventListener('loaded', () => {
|
|
||||||
const sceneEl = document.getElementById('scene');
|
|
||||||
// 3) rel_coords was passed in context
|
|
||||||
const rel = {{ rel_coords|safe }};
|
|
||||||
|
|
||||||
// Compute bounding box & stageSize
|
|
||||||
const xs = rel.map(r => r[0]), zs = rel.map(r => r[1]);
|
|
||||||
const minX = Math.min(...xs), maxX = Math.max(...xs);
|
|
||||||
const minZ = Math.min(...zs), maxZ = Math.max(...zs);
|
|
||||||
const width = maxX - minX, height = maxZ - minZ;
|
|
||||||
const stageSize = Math.ceil(Math.max(width, height) * 1.2);
|
|
||||||
|
|
||||||
// Update invisible base-plane
|
|
||||||
const base = document.getElementById('basePlane');
|
|
||||||
base.setAttribute('width', width * 1.5);
|
|
||||||
base.setAttribute('height', height * 1.5);
|
|
||||||
base.setAttribute('position',
|
|
||||||
`${(minX+maxX)/2} 0 ${(minZ+maxZ)/2}`);
|
|
||||||
|
|
||||||
// Add environment
|
|
||||||
const envEl = document.createElement('a-entity');
|
|
||||||
envEl.setAttribute('environment', `
|
|
||||||
preset: forest;
|
|
||||||
stageSize: ${stageSize};
|
|
||||||
ground: flat;
|
|
||||||
grid: true;
|
|
||||||
gridColor: #00ffff;
|
|
||||||
skyColor: #000000;
|
|
||||||
fog: 0.3;
|
|
||||||
lighting: subtle;
|
|
||||||
`);
|
|
||||||
sceneEl.appendChild(envEl);
|
|
||||||
|
|
||||||
// Draw & animate each segment
|
|
||||||
const pulseDuration = 600;
|
|
||||||
rel.forEach(([x1,z1], i) => {
|
|
||||||
if (i + 1 >= rel.length) return;
|
|
||||||
const [x2,z2] = rel[i+1];
|
|
||||||
const seg = document.createElement('a-entity');
|
|
||||||
seg.setAttribute('line', `
|
|
||||||
start: ${x1.toFixed(2)} 1 ${z1.toFixed(2)};
|
|
||||||
end: ${x2.toFixed(2)} 1 ${z2.toFixed(2)};
|
|
||||||
color: #0077FF;
|
|
||||||
opacity: 0.6
|
|
||||||
`);
|
|
||||||
seg.setAttribute('animation__color', `
|
|
||||||
property: line.color;
|
|
||||||
from: #0077FF;
|
|
||||||
to: #FF4136;
|
|
||||||
dur: ${pulseDuration};
|
|
||||||
delay: ${i * pulseDuration};
|
|
||||||
dir: alternate;
|
|
||||||
loop: true
|
|
||||||
`);
|
|
||||||
sceneEl.appendChild(seg);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Position camera at first step
|
|
||||||
if (rel.length) {
|
|
||||||
const [fx,fz] = rel[0];
|
|
||||||
const cam = document.getElementById('mainCamera');
|
|
||||||
cam.setAttribute('position', `${fx.toFixed(2)} 6 ${fz.toFixed(2)+6}`);
|
|
||||||
cam.setAttribute('look-at', `${fx.toFixed(2)} 1 ${fz.toFixed(2)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
<!--
|
|
||||||
-->
|
|
@ -1,65 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Digital Twin City - AR</title>
|
|
||||||
<!-- A-Frame & AR.js -->
|
|
||||||
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
|
|
||||||
<script src="https://raw.githack.com/AR-js-org/AR.js/3.4.5/aframe/build/aframe-ar.js"></script>
|
|
||||||
|
|
||||||
<!-- Billboard: make objects face the camera -->
|
|
||||||
<script>
|
|
||||||
AFRAME.registerComponent('billboard', {
|
|
||||||
schema: {type: 'selector'},
|
|
||||||
tick: function () {
|
|
||||||
if (!this.data) return;
|
|
||||||
this.el.object3D.lookAt(this.data.object3D.position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Toggle gauge color/status -->
|
|
||||||
<script>
|
|
||||||
AFRAME.registerComponent('gauge-click-toggle', {
|
|
||||||
init: function () {
|
|
||||||
const colors = ["#00FF00", "#FFFF00", "#FF0000", "#00FFFF"];
|
|
||||||
const statuses = ["OK", "Warning", "Critical", "Offline"];
|
|
||||||
let i = 0;
|
|
||||||
const ring = this.el.querySelector('.gauge-ring');
|
|
||||||
const label = this.el.querySelector('.gauge-label');
|
|
||||||
|
|
||||||
this.el.addEventListener('click', () => {
|
|
||||||
i = (i + 1) % colors.length;
|
|
||||||
if (ring) ring.setAttribute('color', colors[i]);
|
|
||||||
if (label) {
|
|
||||||
const current = label.getAttribute('value');
|
|
||||||
const idLine = current.split("\n")[0];
|
|
||||||
label.setAttribute('value', `${idLine}\n${statuses[i]}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<a-scene embedded arjs="sourceType: webcam; debugUIEnabled: false" vr-mode-ui="enabled: false">
|
|
||||||
|
|
||||||
<!-- Cámara y controles -->
|
|
||||||
<a-entity id="mainCamera" camera look-controls wasd-controls position="0 2 5">
|
|
||||||
<a-cursor color="#FF0000"></a-cursor>
|
|
||||||
</a-entity>
|
|
||||||
|
|
||||||
<!-- Solo los gauges -->
|
|
||||||
{% for road in city_data.roads %}
|
|
||||||
<a-entity id="{{ road.id }}" status="{{ road.status }}">
|
|
||||||
<a-entity
|
|
||||||
position="{{ road.position_x }} 1.5 {{ road.position_z }}">
|
|
||||||
{% include "pxy_city_digital_twins/_status_gauge.html" with ring_color="#FF6600" offset_y="0" status=road.status id=road.id %}
|
|
||||||
</a-entity>
|
|
||||||
</a-entity>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</a-scene>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,218 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Digital Twin City</title>
|
|
||||||
<!-- A-Frame 1.7.0 & environment component -->
|
|
||||||
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
|
|
||||||
<script src="https://unpkg.com/aframe-environment-component/dist/aframe-environment-component.min.js"></script>
|
|
||||||
|
|
||||||
<!-- 1) Simple “look-at” component to face the camera -->
|
|
||||||
<script>
|
|
||||||
AFRAME.registerComponent('billboard', {
|
|
||||||
schema: {type: 'selector'},
|
|
||||||
tick: function () {
|
|
||||||
if (!this.data) return;
|
|
||||||
// Make this entity face the camera each frame
|
|
||||||
this.el.object3D.lookAt(this.data.object3D.position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
AFRAME.registerComponent('gauge-click-toggle', {
|
|
||||||
init: function () {
|
|
||||||
const colors = ["#00FF00", "#FFFF00", "#FF0000", "#00FFFF"];
|
|
||||||
const statuses = ["OK", "Warning", "Critical", "Offline"];
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
const ring = this.el.querySelector('.gauge-ring');
|
|
||||||
const label = this.el.querySelector('.gauge-label');
|
|
||||||
|
|
||||||
this.el.addEventListener('click', () => {
|
|
||||||
i = (i + 1) % colors.length;
|
|
||||||
if (ring) ring.setAttribute('color', colors[i]);
|
|
||||||
if (label) {
|
|
||||||
const current = label.getAttribute('value');
|
|
||||||
const idLine = current.split("\n")[0]; // Preserve ID line
|
|
||||||
label.setAttribute('value', `${idLine}\n${statuses[i]}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<a-scene environment="preset: tron; groundTexture: walk; dressing: trees; fog: 0.7">
|
|
||||||
|
|
||||||
<!-- Camera & Controls (give it an id for look-at) -->
|
|
||||||
<a-entity id="mainCamera" camera look-controls wasd-controls position="0 2 5">
|
|
||||||
<a-cursor color="#FF0000"></a-cursor>
|
|
||||||
</a-entity>
|
|
||||||
|
|
||||||
<!-- Optional: Transparent ground plane (comment out if you want only environment ground) -->
|
|
||||||
<a-plane position="0 -0.1 0" rotation="-90 0 0"
|
|
||||||
width="200" height="200"
|
|
||||||
color="#444" opacity="0.3">
|
|
||||||
</a-plane>
|
|
||||||
|
|
||||||
<!-- Buildings -->
|
|
||||||
{% for building in city_data.buildings %}
|
|
||||||
<a-entity id="{{ building.id }}" status="{{ building.status }}">
|
|
||||||
<!-- Building geometry -->
|
|
||||||
<a-box position="{{ building.position_x }} 1 {{ building.position_z }}"
|
|
||||||
width="{{ building.width }}" height="{{ building.height }}" depth="{{ building.depth }}"
|
|
||||||
color="{{ building.color }}">
|
|
||||||
</a-box>
|
|
||||||
|
|
||||||
<a-entity
|
|
||||||
position="{{ building.position_x }} {{ building.height }} {{ building.position_z }}">
|
|
||||||
{% include "pxy_city_digital_twins/_status_gauge.html" with ring_color="#00FFFF" offset_y="1.50" status=building.status id=building.id %}
|
|
||||||
</a-entity>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</a-entity>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- Lamps -->
|
|
||||||
{% for lamp in city_data.lamps %}
|
|
||||||
<a-entity id="{{ lamp.id }}" status="{{ lamp.status }}" position="{{ lamp.position_x }} 1 {{ lamp.position_z }}">
|
|
||||||
<!-- Lamp geometry -->
|
|
||||||
<a-cone radius-bottom="0.1" radius-top="0.5"
|
|
||||||
height="{{ lamp.height }}" color="{{ lamp.color }}">
|
|
||||||
</a-cone>
|
|
||||||
<a-sphere radius="0.2" color="#FFFFFF"
|
|
||||||
position="0 {{ lamp.height }} 0">
|
|
||||||
</a-sphere>
|
|
||||||
|
|
||||||
<!-- Label: billboard to camera -->
|
|
||||||
<a-entity position="0 3 0" billboard="#mainCamera">
|
|
||||||
<a-plane width="4" height="1.2" color="#000" opacity="0.5"></a-plane>
|
|
||||||
<a-text value="Status: {{ lamp.status }}"
|
|
||||||
width="4"
|
|
||||||
align="center"
|
|
||||||
color="#FFF"
|
|
||||||
position="0 0 0.01">
|
|
||||||
</a-text>
|
|
||||||
</a-entity>
|
|
||||||
</a-entity>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- Trees -->
|
|
||||||
{% for tree in city_data.trees %}
|
|
||||||
<a-entity id="{{ tree.id }}" status="{{ tree.status }}" position="{{ tree.position_x }} 1 {{ tree.position_z }}">
|
|
||||||
<!-- Tree trunk & leaves -->
|
|
||||||
<a-cone radius-bottom="{{ tree.radius_bottom }}"
|
|
||||||
radius-top="{{ tree.radius_top }}"
|
|
||||||
height="{{ tree.height }}"
|
|
||||||
color="{{ tree.color_trunk }}">
|
|
||||||
</a-cone>
|
|
||||||
<a-sphere radius="{{ tree.radius_top }}"
|
|
||||||
color="{{ tree.color_leaves }}"
|
|
||||||
position="0 {{ tree.height }} 0">
|
|
||||||
</a-sphere>
|
|
||||||
|
|
||||||
<!-- Label: billboard to camera -->
|
|
||||||
<a-entity position="0 3 0" billboard="#mainCamera">
|
|
||||||
<a-plane width="4" height="1.2" color="#000" opacity="0.5"></a-plane>
|
|
||||||
<a-text value="Status: {{ tree.status }}"
|
|
||||||
width="4"
|
|
||||||
align="center"
|
|
||||||
color="#FFF"
|
|
||||||
position="0 0 0.01">
|
|
||||||
</a-text>
|
|
||||||
</a-entity>
|
|
||||||
</a-entity>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- Cell Towers -->
|
|
||||||
{% for tower in city_data.towers %}
|
|
||||||
<a-entity id="tower{{ tower.id }}"
|
|
||||||
position="{{ tower.position_x }} {{ tower.position_y }} {{ tower.position_z }}">
|
|
||||||
<!-- Base tower cylinder -->
|
|
||||||
<a-cylinder height="{{ tower.height }}" radius="1" color="{{ tower.color }}"></a-cylinder>
|
|
||||||
|
|
||||||
<!-- Animated signal ring near top -->
|
|
||||||
<a-ring color="#FF0000"
|
|
||||||
radius-inner="2"
|
|
||||||
radius-outer="2.5"
|
|
||||||
position="0 {{ tower.height|add:'1' }} 0"
|
|
||||||
rotation="-90 0 0"
|
|
||||||
animation="property: scale; to: 1.5 1.5 1.5; dir: alternate; dur: 1000; loop: true">
|
|
||||||
</a-ring>
|
|
||||||
|
|
||||||
<!-- Tower label: billboard to camera -->
|
|
||||||
<a-entity position="0 -5 0" billboard="#mainCamera">
|
|
||||||
<a-plane width="4" height="1.2" color="#000" opacity="0.5"></a-plane>
|
|
||||||
<a-text value="📡 Tower {{ tower.id }} - {{ tower.status }}"
|
|
||||||
width="4"
|
|
||||||
align="center"
|
|
||||||
color="#FFF"
|
|
||||||
position="0 0 0.01">
|
|
||||||
</a-text>
|
|
||||||
</a-entity>
|
|
||||||
</a-entity>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- Fiber Paths (Cylinders) -->
|
|
||||||
{% for fiber in city_data.fiber_paths %}
|
|
||||||
<a-entity>
|
|
||||||
<a-cylinder position="{{ fiber.mid_x }} {{ fiber.mid_y }} {{ fiber.mid_z }}"
|
|
||||||
height="{{ fiber.length }}"
|
|
||||||
radius="0.1"
|
|
||||||
rotation="90 {{ fiber.angle }} 0"
|
|
||||||
color="{{ fiber.color }}">
|
|
||||||
</a-cylinder>
|
|
||||||
|
|
||||||
<!-- Fiber label: billboard to camera -->
|
|
||||||
<a-entity position="{{ fiber.start_x }} 3 {{ fiber.start_z }}" billboard="#mainCamera">
|
|
||||||
<a-plane width="4" height="1.2" color="#000" opacity="0.5"></a-plane>
|
|
||||||
<a-text value="🔗 Fiber Path {{ fiber.id }} - {{ fiber.status }}"
|
|
||||||
width="4"
|
|
||||||
align="center"
|
|
||||||
color="{{ fiber.color }}"
|
|
||||||
position="0 0 0.01">
|
|
||||||
</a-text>
|
|
||||||
</a-entity>
|
|
||||||
</a-entity>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- Wi-Fi Hotspots -->
|
|
||||||
{% for wifi in city_data.wifi_hotspots %}
|
|
||||||
<a-entity id="wifi{{ wifi.id }}"
|
|
||||||
position="{{ wifi.position_x }} {{ wifi.position_y }} {{ wifi.position_z }}">
|
|
||||||
<!-- Hotspot sphere (animated) -->
|
|
||||||
<a-sphere radius="{{ wifi.radius }}" color="{{ wifi.color }}"
|
|
||||||
animation="property: scale; to: 1.5 1.5 1.5; dir: alternate; dur: 1500; loop: true">
|
|
||||||
</a-sphere>
|
|
||||||
|
|
||||||
<!-- Coverage area (fixed or dynamic) -->
|
|
||||||
<a-sphere radius="5"
|
|
||||||
color="#00FFFF"
|
|
||||||
opacity="0.2"
|
|
||||||
position="0 {{ wifi.radius }} 0">
|
|
||||||
</a-sphere>
|
|
||||||
|
|
||||||
<!-- Wi-Fi label: billboard to camera -->
|
|
||||||
<a-entity position="0 3 0" billboard="#mainCamera">
|
|
||||||
<a-plane width="4" height="1.2" color="#000" opacity="0.5"></a-plane>
|
|
||||||
<a-text value="📶 WiFi {{ wifi.id }} - {{ wifi.status }}"
|
|
||||||
width="4"
|
|
||||||
align="center"
|
|
||||||
color="#FFF"
|
|
||||||
position="0 0 0.01">
|
|
||||||
</a-text>
|
|
||||||
</a-entity>
|
|
||||||
</a-entity>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
|
|
||||||
</a-scene>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,59 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Gauge Ahead</title>
|
|
||||||
<!-- A-Frame & AR.js -->
|
|
||||||
<script src="https://aframe.io/releases/1.3.0/aframe.min.js"></script>
|
|
||||||
<script src="https://raw.githack.com/AR-js-org/AR.js/3.4.5/aframe/build/aframe-ar.js"></script>
|
|
||||||
|
|
||||||
<!-- Billboard & click toggle (ya los usas en tu sistema) -->
|
|
||||||
<script>
|
|
||||||
AFRAME.registerComponent('billboard', {
|
|
||||||
schema: {type: 'selector'},
|
|
||||||
tick: function () {
|
|
||||||
if (!this.data) return;
|
|
||||||
this.el.object3D.lookAt(this.data.object3D.position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
AFRAME.registerComponent('gauge-click-toggle', {
|
|
||||||
init: function () {
|
|
||||||
const colors = ["#00FF00", "#FFFF00", "#FF0000", "#00FFFF"];
|
|
||||||
const statuses = ["OK", "Warning", "Critical", "Offline"];
|
|
||||||
let i = 0;
|
|
||||||
const ring = this.el.querySelector('.gauge-ring');
|
|
||||||
const label = this.el.querySelector('.gauge-label');
|
|
||||||
|
|
||||||
this.el.addEventListener('click', () => {
|
|
||||||
i = (i + 1) % colors.length;
|
|
||||||
if (ring) ring.setAttribute('color', colors[i]);
|
|
||||||
if (label) {
|
|
||||||
const current = label.getAttribute('value');
|
|
||||||
const idLine = current.split("\n")[0];
|
|
||||||
label.setAttribute('value', `${idLine}\n${statuses[i]}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<a-scene embedded arjs="sourceType: webcam; debugUIEnabled: false" vr-mode-ui="enabled: false">
|
|
||||||
|
|
||||||
<!-- Cámara -->
|
|
||||||
<a-entity id="mainCamera" camera look-controls wasd-controls position="0 2 5">
|
|
||||||
<a-cursor color="#FF0000"></a-cursor>
|
|
||||||
</a-entity>
|
|
||||||
|
|
||||||
<!-- Gauge fijo visible a 3 unidades frente a cámara -->
|
|
||||||
<a-entity position="0 1.5 -3">
|
|
||||||
{% include "pxy_city_digital_twins/_status_gauge_augmented.html" with pos_x="0" pos_y="1.6" pos_z="-3" scale="2.7 2.7 2.7" ring_color="#00FF00" label="WELCOME\nUSER" %}
|
|
||||||
|
|
||||||
|
|
||||||
</a-entity>
|
|
||||||
|
|
||||||
</a-scene>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,198 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>Waste Route Visualization</title>
|
|
||||||
<!-- A-Frame core -->
|
|
||||||
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
|
|
||||||
<!-- A-Frame environment effects -->
|
|
||||||
<script src="https://unpkg.com/aframe-environment-component/dist/aframe-environment-component.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<a-scene id="scene">
|
|
||||||
<!-- Camera & cursor -->
|
|
||||||
<a-entity
|
|
||||||
id="mainCamera"
|
|
||||||
camera look-controls wasd-controls
|
|
||||||
position="0 5 10">
|
|
||||||
<a-cursor color="#ffffff"></a-cursor>
|
|
||||||
</a-entity>
|
|
||||||
|
|
||||||
<!-- Invisible ground-plane fallback -->
|
|
||||||
<a-plane id="basePlane"
|
|
||||||
rotation="-90 0 0"
|
|
||||||
color="#444" opacity="0.0">
|
|
||||||
</a-plane>
|
|
||||||
|
|
||||||
<!-- 1) Draw city buildings from your OSM data -->
|
|
||||||
{% for building in city_data.buildings %}
|
|
||||||
<a-entity id="{{ building.id }}" status="{{ building.status }}">
|
|
||||||
<a-box position="{{ building.position_x }} {{ building.height }} {{ building.position_z }}"
|
|
||||||
width="{{ building.width }}"
|
|
||||||
height="{{ building.height }}"
|
|
||||||
depth="{{ building.depth }}"
|
|
||||||
color="{{ building.color }}"
|
|
||||||
opacity="0.8">
|
|
||||||
</a-box>
|
|
||||||
</a-entity>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- 2) Draw each job-sphere & gauge from step_positions -->
|
|
||||||
{% for pos in step_positions %}
|
|
||||||
{% with sid=pos.step.id stype=pos.step.step_type %}
|
|
||||||
<a-entity id="step-{{ sid }}" position="{{ pos.x }} 1 {{ pos.z }}">
|
|
||||||
<!-- sphere now semi-transparent by default -->
|
|
||||||
<a-sphere id="sphere-{{ sid }}"
|
|
||||||
radius="10"
|
|
||||||
color="#FF4136"
|
|
||||||
emissive="#FF4136"
|
|
||||||
transparent="true"
|
|
||||||
opacity="0.4">
|
|
||||||
</a-sphere>
|
|
||||||
|
|
||||||
<!-- gauge above the sphere -->
|
|
||||||
<a-entity
|
|
||||||
class="status-gauge"
|
|
||||||
gauge-click-toggle
|
|
||||||
position="11 3 0"
|
|
||||||
data-step-id="{{ sid }}"
|
|
||||||
data-step-type="{{ stype }}">
|
|
||||||
{% include "pxy_city_digital_twins/_status_gauge_waste.html" with ring_color="#FF4136" status=stype id=sid %}
|
|
||||||
</a-entity>
|
|
||||||
</a-entity>
|
|
||||||
{% endwith %}
|
|
||||||
{% endfor %}
|
|
||||||
</a-scene>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Expose Django vars into JS
|
|
||||||
const subdivision = "{{ subdivision }}";
|
|
||||||
const vehicle = "{{ vehicle }}";
|
|
||||||
const recordUrl = "{% url 'waste-record-pickup' subdivision=subdivision vehicle=vehicle %}";
|
|
||||||
|
|
||||||
document.querySelector('#scene').addEventListener('loaded', () => {
|
|
||||||
const sceneEl = document.getElementById('scene');
|
|
||||||
const rel = {{ rel_coords|safe }};
|
|
||||||
|
|
||||||
// 3) Compute bounding box & stageSize
|
|
||||||
const xs = rel.map(r => r[0]), zs = rel.map(r => r[1]);
|
|
||||||
const minX = Math.min(...xs), maxX = Math.max(...xs);
|
|
||||||
const minZ = Math.min(...zs), maxZ = Math.max(...zs);
|
|
||||||
const width = maxX - minX, height = maxZ - minZ;
|
|
||||||
const stageSize = Math.ceil(Math.max(width, height) * 1.2);
|
|
||||||
|
|
||||||
// 4) Update invisible base-plane
|
|
||||||
const base = document.getElementById('basePlane');
|
|
||||||
base.setAttribute('width', width * 1.5);
|
|
||||||
base.setAttribute('height', height * 1.5);
|
|
||||||
base.setAttribute('position',
|
|
||||||
`${(minX+maxX)/2} 0 ${(minZ+maxZ)/2}`);
|
|
||||||
|
|
||||||
// 5) Add environment
|
|
||||||
const envEl = document.createElement('a-entity');
|
|
||||||
envEl.setAttribute('environment', `
|
|
||||||
preset: forest;
|
|
||||||
stageSize: ${stageSize};
|
|
||||||
ground: flat;
|
|
||||||
grid: true;
|
|
||||||
gridColor: #00ffff;
|
|
||||||
skyColor: #000000;
|
|
||||||
fog: 0.3;
|
|
||||||
lighting: subtle;
|
|
||||||
`);
|
|
||||||
sceneEl.appendChild(envEl);
|
|
||||||
|
|
||||||
// 6) Draw & animate each segment
|
|
||||||
const pulseDuration = 600;
|
|
||||||
rel.forEach(([x1,z1], i) => {
|
|
||||||
if (i + 1 >= rel.length) return;
|
|
||||||
const [x2,z2] = rel[i+1];
|
|
||||||
const seg = document.createElement('a-entity');
|
|
||||||
seg.setAttribute('line', `
|
|
||||||
start: ${x1.toFixed(2)} 1 ${z1.toFixed(2)};
|
|
||||||
end: ${x2.toFixed(2)} 1 ${z2.toFixed(2)};
|
|
||||||
color: #0077FF;
|
|
||||||
opacity: 0.6
|
|
||||||
`);
|
|
||||||
seg.setAttribute('animation__color', `
|
|
||||||
property: line.color;
|
|
||||||
from: #0077FF;
|
|
||||||
to: #FF4136;
|
|
||||||
dur: ${pulseDuration};
|
|
||||||
delay: ${i * pulseDuration};
|
|
||||||
dir: alternate;
|
|
||||||
loop: true
|
|
||||||
`);
|
|
||||||
sceneEl.appendChild(seg);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 7) Position camera at first step
|
|
||||||
if (rel.length) {
|
|
||||||
const [fx,fz] = rel[0];
|
|
||||||
const cam = document.getElementById('mainCamera');
|
|
||||||
cam.setAttribute('position', `${fx.toFixed(2)} 6 ${fz.toFixed(2)+6}`);
|
|
||||||
cam.setAttribute('look-at', `${fx.toFixed(2)} 1 ${fz.toFixed(2)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
// 8) Record pickups by clicking each gauge
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// CSRF helper
|
|
||||||
function getCookie(name) {
|
|
||||||
const v = document.cookie.match('(^|;)\\s*'+name+'\\s*=\\s*([^;]+)');
|
|
||||||
return v ? v.pop() : '';
|
|
||||||
}
|
|
||||||
const csrftoken = getCookie('csrftoken');
|
|
||||||
|
|
||||||
// Track click counts & define colors
|
|
||||||
const clickCounts = {};
|
|
||||||
const colors = ['#2ECC40','#FFDC00','#FF851B','#FF4136'];
|
|
||||||
|
|
||||||
document.querySelectorAll('.status-gauge').forEach(el => {
|
|
||||||
const stepId = el.dataset.stepId;
|
|
||||||
clickCounts[stepId] = 0;
|
|
||||||
|
|
||||||
el.addEventListener('click', () => {
|
|
||||||
// cycle 1→4
|
|
||||||
clickCounts[stepId] = (clickCounts[stepId] % 4) + 1;
|
|
||||||
const quarter = clickCounts[stepId];
|
|
||||||
|
|
||||||
// recolor ring
|
|
||||||
const ring = el.querySelector('.gauge-ring');
|
|
||||||
if (ring) ring.setAttribute('color', colors[quarter-1]);
|
|
||||||
|
|
||||||
// recolor sphere
|
|
||||||
const sph = document.getElementById(`sphere-${stepId}`);
|
|
||||||
if (sph) {
|
|
||||||
sph.setAttribute(
|
|
||||||
'material',
|
|
||||||
`color: ${colors[quarter-1]};
|
|
||||||
emissive: ${colors[quarter-1]};
|
|
||||||
transparent: true; opacity: 0.4`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// log & POST
|
|
||||||
console.log('Recording pickup', { step_id: stepId, quarter }, '→', recordUrl);
|
|
||||||
fetch(recordUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrftoken
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ step_id: stepId, quarter })
|
|
||||||
}).then(res => {
|
|
||||||
if (!res.ok) {
|
|
||||||
console.error('Record pickup failed', res.status);
|
|
||||||
} else {
|
|
||||||
console.log('Pickup recorded');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,190 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Waste Route Debug</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: sans-serif; padding: 2rem; }
|
|
||||||
h1, h2 { margin-bottom: 0.5rem; }
|
|
||||||
pre { background: #f6f6f6; padding: 1rem; overflow-x: auto; }
|
|
||||||
ol { margin-left: 1.5rem; }
|
|
||||||
li { margin-bottom: 0.5rem; }
|
|
||||||
|
|
||||||
/* map container */
|
|
||||||
#route-map {
|
|
||||||
width: 600px; height: 400px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
#route-map svg {
|
|
||||||
width: 100%; height: 100%; display: block;
|
|
||||||
background: #f0f8ff;
|
|
||||||
}
|
|
||||||
.route-line {
|
|
||||||
fill: none;
|
|
||||||
stroke: #0074D9;
|
|
||||||
stroke-width: 2;
|
|
||||||
}
|
|
||||||
.coord-point {
|
|
||||||
fill: #0074D9;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
.step-point {
|
|
||||||
fill: #FF4136;
|
|
||||||
stroke: #fff;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
.building-point {
|
|
||||||
fill: #2ECC40;
|
|
||||||
stroke: #fff;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin-top: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
th, td {
|
|
||||||
border: 1px solid #aaa;
|
|
||||||
padding: 0.5rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
th { background: #ddd; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>Waste Route Debug</h1>
|
|
||||||
<p><strong>Subdivision:</strong> {{ subdivision }}</p>
|
|
||||||
<p><strong>Vehicle:</strong> {{ vehicle }}</p>
|
|
||||||
|
|
||||||
<div id="route-map">
|
|
||||||
<svg></svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="dump"></div>
|
|
||||||
|
|
||||||
<h2>Buildings</h2>
|
|
||||||
{% if city_data.buildings %}
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Position X</th>
|
|
||||||
<th>Position Z</th>
|
|
||||||
<th>Width</th>
|
|
||||||
<th>Depth</th>
|
|
||||||
<th>Height</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for b in city_data.buildings %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ b.id }}</td>
|
|
||||||
<td>{{ b.status }}</td>
|
|
||||||
<td>{{ b.position_x|floatformat:2 }}</td>
|
|
||||||
<td>{{ b.position_z|floatformat:2 }}</td>
|
|
||||||
<td>{{ b.width|floatformat:2 }}</td>
|
|
||||||
<td>{{ b.depth|floatformat:2 }}</td>
|
|
||||||
<td>{{ b.height|floatformat:2 }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% else %}
|
|
||||||
<p><em>No buildings generated.</em></p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const route = JSON.parse('{{ selected_route_json|escapejs }}');
|
|
||||||
const coords = route.coords || [];
|
|
||||||
|
|
||||||
// 1) Draw SVG route + steps
|
|
||||||
if (coords.length > 1) {
|
|
||||||
const svg = document.querySelector('#route-map svg');
|
|
||||||
const W = svg.clientWidth, H = svg.clientHeight, M = 20;
|
|
||||||
const lons = coords.map(c=>c[0]), lats = coords.map(c=>c[1]);
|
|
||||||
const minX = Math.min(...lons), maxX = Math.max(...lons);
|
|
||||||
const minY = Math.min(...lats), maxY = Math.max(...lats);
|
|
||||||
const scaleX = (W - 2*M)/(maxX - minX || 1);
|
|
||||||
const scaleY = (H - 2*M)/(maxY - minY || 1);
|
|
||||||
|
|
||||||
function project([lon, lat]) {
|
|
||||||
const x = M + (lon - minX)*scaleX;
|
|
||||||
const y = H - (M + (lat - minY)*scaleY);
|
|
||||||
return [x,y];
|
|
||||||
}
|
|
||||||
|
|
||||||
// route line
|
|
||||||
const pts = coords.map(project).map(p=>p.join(',')).join(' ');
|
|
||||||
const line = document.createElementNS('http://www.w3.org/2000/svg','polyline');
|
|
||||||
line.setAttribute('points', pts);
|
|
||||||
line.setAttribute('class', 'route-line');
|
|
||||||
svg.appendChild(line);
|
|
||||||
|
|
||||||
// decoded-coord dots
|
|
||||||
coords.forEach(c => {
|
|
||||||
const [x,y] = project(c);
|
|
||||||
const dot = document.createElementNS('http://www.w3.org/2000/svg','circle');
|
|
||||||
dot.setAttribute('cx', x);
|
|
||||||
dot.setAttribute('cy', y);
|
|
||||||
dot.setAttribute('r', 2);
|
|
||||||
dot.setAttribute('class', 'coord-point');
|
|
||||||
svg.appendChild(dot);
|
|
||||||
});
|
|
||||||
|
|
||||||
// step markers
|
|
||||||
(route.steps||[]).forEach(s => {
|
|
||||||
const [x,y] = project(s.position);
|
|
||||||
const mark = document.createElementNS('http://www.w3.org/2000/svg','circle');
|
|
||||||
mark.setAttribute('cx', x);
|
|
||||||
mark.setAttribute('cy', y);
|
|
||||||
mark.setAttribute('r', 5);
|
|
||||||
mark.setAttribute('class', 'step-point');
|
|
||||||
svg.appendChild(mark);
|
|
||||||
});
|
|
||||||
|
|
||||||
// OPTIONAL: if you ever pass raw building lon/lat into city_data,
|
|
||||||
// you could plot them here as green circles:
|
|
||||||
/*
|
|
||||||
({{ city_data.buildings|length }} && city_data.buildings.forEach(b => {
|
|
||||||
const geo = [b.lon, b.lat];
|
|
||||||
const [x,y] = project(geo);
|
|
||||||
const bp = document.createElementNS('http://www.w3.org/2000/svg','circle');
|
|
||||||
bp.setAttribute('cx', x);
|
|
||||||
bp.setAttribute('cy', y);
|
|
||||||
bp.setAttribute('r', 4);
|
|
||||||
bp.setAttribute('class', 'building-point');
|
|
||||||
svg.appendChild(bp);
|
|
||||||
}))
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Dump coords & steps below
|
|
||||||
let out = '';
|
|
||||||
out += '<h2>Coordinates</h2>';
|
|
||||||
out += '<pre>' + JSON.stringify(coords, null, 2) + '</pre>';
|
|
||||||
out += '<h2>Steps</h2>';
|
|
||||||
if (route.steps && route.steps.length) {
|
|
||||||
out += '<ol>';
|
|
||||||
route.steps.forEach((s,i) => {
|
|
||||||
out += '<li>';
|
|
||||||
out += `<strong>Step #${i+1}</strong><br>`;
|
|
||||||
out += `<strong>Type:</strong> ${s.step_type}<br>`;
|
|
||||||
out += `<strong>Position:</strong> ${JSON.stringify(s.position)}<br>`;
|
|
||||||
out += `<strong>Popup HTML:</strong> <code>${s.popup
|
|
||||||
.replace(/</g,'<')
|
|
||||||
.replace(/>/g,'>')}</code>`;
|
|
||||||
out += '</li>';
|
|
||||||
});
|
|
||||||
out += '</ol>';
|
|
||||||
} else {
|
|
||||||
out += '<p><em>(no steps)</em></p>';
|
|
||||||
}
|
|
||||||
document.getElementById('dump').innerHTML = out;
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,57 +0,0 @@
|
|||||||
{% load static %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Ruta No Encontrada</title>
|
|
||||||
<!-- A-Frame & environment component -->
|
|
||||||
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
|
|
||||||
<script src="https://unpkg.com/aframe-environment-component/dist/aframe-environment-component.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<a-scene environment="preset: yavapai; skyType: atmosphere; skyColor: #001840; fog: 0.6; groundColor: #444; groundTexture: walk; dressing: none;">
|
|
||||||
|
|
||||||
<!-- Camera & Controls -->
|
|
||||||
<a-entity id="mainCamera" camera look-controls wasd-controls position="0 2 4">
|
|
||||||
<a-cursor color="#FF0000"></a-cursor>
|
|
||||||
</a-entity>
|
|
||||||
|
|
||||||
<!-- Ground plane -->
|
|
||||||
<a-plane position="0 -0.1 0" rotation="-90 0 0" width="200" height="200" color="#444" opacity="0.3"></a-plane>
|
|
||||||
|
|
||||||
<!-- Animated box as placeholder for a happy truck -->
|
|
||||||
<a-box color="#FF4136" depth="1" height="0.5" width="2" position="0 0.25 -3"
|
|
||||||
animation="property: rotation; to: 0 360 0; loop: true; dur: 3000">
|
|
||||||
</a-box>
|
|
||||||
|
|
||||||
<!-- Message -->
|
|
||||||
<a-text value="Ruta no encontrada"
|
|
||||||
align="center"
|
|
||||||
position="0 1 -3"
|
|
||||||
color="#FFF"
|
|
||||||
width="4">
|
|
||||||
</a-text>
|
|
||||||
|
|
||||||
</a-scene>
|
|
||||||
|
|
||||||
<!-- Sugerencia de URL si aplica -->
|
|
||||||
{% if first_subdivision %}
|
|
||||||
<div style="
|
|
||||||
position: absolute;
|
|
||||||
bottom: 20px;
|
|
||||||
left: 20px;
|
|
||||||
background: rgba(0,0,0,0.6);
|
|
||||||
color: #fff;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: sans-serif;
|
|
||||||
">
|
|
||||||
Quizá quieras ver la ruta por defecto en
|
|
||||||
<a href="{% url 'waste_route' first_subdivision %}{% if first_route %}?route={{ first_route }}{% endif %}"
|
|
||||||
style="color: #ffd700; text-decoration: underline;">
|
|
||||||
{{ first_subdivision }}{% if first_route %} (ruta {{ first_route }}){% endif %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -1,21 +0,0 @@
|
|||||||
from django.urls import path
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
# Digital Twin (normal)
|
|
||||||
path('city/digital/twin/<uuid:city_id>/', views.city_digital_twin, name='city_digital_twin_uuid'),
|
|
||||||
path('city/digital/twin/<str:city_id>/', views.city_digital_twin, name='city_digital_twin_str'),
|
|
||||||
|
|
||||||
# Augmented Digital Twin
|
|
||||||
path('city/augmented/digital/twin/<uuid:city_id>/', views.city_augmented_digital_twin, name='city_augmented_digital_twin_uuid'),
|
|
||||||
path('city/augmented/digital/twin/<str:city_id>/', views.city_augmented_digital_twin, name='city_augmented_digital_twin_str'),
|
|
||||||
|
|
||||||
path('city/virtual/reality/digital/twin/waste/<str:subdivision>/<str:vehicle>/', views.waste_route, name='waste_route'),
|
|
||||||
path('city/digital/twin/waste/debug/<str:subdivision>/<str:vehicle>/', views.waste_route_debug, name='waste_route_debug'),
|
|
||||||
|
|
||||||
path('city/augmented/reality/digital/twin/waste/<str:subdivision>/<str:vehicle>/', views.augmented_waste_route, name='waste_route'),
|
|
||||||
|
|
||||||
path('city/augmented/reality/digital/twin/waste/<str:subdivision>/<str:vehicle>/record/', views.record_pickup, name="waste-record-pickup"),
|
|
||||||
|
|
||||||
|
|
||||||
]
|
|
@ -1,403 +0,0 @@
|
|||||||
from django.shortcuts import render, get_object_or_404
|
|
||||||
from django.http import Http404
|
|
||||||
import random
|
|
||||||
import math
|
|
||||||
from .services.presets import get_environment_preset
|
|
||||||
import networkx as nx
|
|
||||||
from .services.layouts import (
|
|
||||||
rectangular_layout,
|
|
||||||
circular_layout,
|
|
||||||
diagonal_layout,
|
|
||||||
triangular_layout,
|
|
||||||
)
|
|
||||||
from .services.random_city import generate_random_city_data
|
|
||||||
from .services.com_con_city import generate_com_con_city_data
|
|
||||||
from .services.osm_city import generate_osm_city_data, generate_osm_road_midpoints_only
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def city_digital_twin(request, city_id, innovation_pct=None, technology_pct=None, science_pct=None):
|
|
||||||
try:
|
|
||||||
lat = float(request.GET.get('lat', 0))
|
|
||||||
long = float(request.GET.get('long', 0))
|
|
||||||
scale = float(request.GET.get('scale', 1.0)) # default to 1.0 (normal scale)
|
|
||||||
|
|
||||||
if city_id == "osm_city":
|
|
||||||
city_data = generate_osm_city_data(lat, long,scale=scale)
|
|
||||||
elif city_id == "com_con":
|
|
||||||
city_data = generate_com_con_city_data(lat, long)
|
|
||||||
elif city_id == "random_city":
|
|
||||||
city_data = generate_random_city_data()
|
|
||||||
elif city_id == "dream":
|
|
||||||
innovation_pct = innovation_pct or request.GET.get('innovation', 0)
|
|
||||||
technology_pct = technology_pct or request.GET.get('technology', 0)
|
|
||||||
science_pct = science_pct or request.GET.get('science', 0)
|
|
||||||
|
|
||||||
innovation_pct = int(innovation_pct)
|
|
||||||
technology_pct = int(technology_pct)
|
|
||||||
science_pct = int(science_pct)
|
|
||||||
|
|
||||||
city_data = generate_random_city_data(innovation_pct, technology_pct, science_pct)
|
|
||||||
else:
|
|
||||||
city_data = get_city_data(city_id)
|
|
||||||
|
|
||||||
if not city_data:
|
|
||||||
city_data = get_example_data()
|
|
||||||
|
|
||||||
preset = get_environment_preset(lat, long)
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'city_data': city_data,
|
|
||||||
'environment_preset': preset,
|
|
||||||
'lat': lat,
|
|
||||||
'long': long,
|
|
||||||
}
|
|
||||||
return render(request, 'pxy_city_digital_twins/city_digital_twin.html', context)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
raise Http404("Invalid data provided.")
|
|
||||||
|
|
||||||
|
|
||||||
def get_city_data(city_id):
|
|
||||||
# Implement fetching logic here
|
|
||||||
# This is a mock function to demonstrate fetching logic
|
|
||||||
if str(city_id) == "1" or str(city_id) == "123e4567-e89b-12d3-a456-426614174000":
|
|
||||||
return {
|
|
||||||
# Real data retrieval logic goes here
|
|
||||||
}
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_example_data():
|
|
||||||
return {
|
|
||||||
'buildings': [
|
|
||||||
{
|
|
||||||
'id': 1,
|
|
||||||
'status': 'Occupied',
|
|
||||||
'position_x': 0,
|
|
||||||
'height': 10,
|
|
||||||
'position_z': 0,
|
|
||||||
'width': 5,
|
|
||||||
'depth': 5,
|
|
||||||
'color': '#8a2be2',
|
|
||||||
'file': '', # No file for a simple box representation
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 2,
|
|
||||||
'status': 'Vacant',
|
|
||||||
'position_x': 10,
|
|
||||||
'height': 15,
|
|
||||||
'position_z': 10,
|
|
||||||
'width': 7,
|
|
||||||
'depth': 7,
|
|
||||||
'color': '#5f9ea0',
|
|
||||||
'file': '', # No file for a simple box representation
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'lamps': [
|
|
||||||
{
|
|
||||||
'id': 1,
|
|
||||||
'status': 'Functional',
|
|
||||||
'position_x': 3,
|
|
||||||
'position_z': 3,
|
|
||||||
'height': 4,
|
|
||||||
'color': '#ffff00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 2,
|
|
||||||
'status': 'Broken',
|
|
||||||
'position_x': 8,
|
|
||||||
'position_z': 8,
|
|
||||||
'height': 4,
|
|
||||||
'color': '#ff0000',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'trees': [
|
|
||||||
{
|
|
||||||
'id': 1,
|
|
||||||
'status': 'Healthy',
|
|
||||||
'position_x': 5,
|
|
||||||
'position_z': 5,
|
|
||||||
'height': 6,
|
|
||||||
'radius_bottom': 0.2,
|
|
||||||
'radius_top': 1,
|
|
||||||
'color_trunk': '#8b4513',
|
|
||||||
'color_leaves': '#228b22',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 2,
|
|
||||||
'status': 'Diseased',
|
|
||||||
'position_x': 15,
|
|
||||||
'position_z': 15,
|
|
||||||
'height': 6,
|
|
||||||
'radius_bottom': 0.2,
|
|
||||||
'radius_top': 1,
|
|
||||||
'color_trunk': '#a0522d',
|
|
||||||
'color_leaves': '#6b8e23',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
def city_augmented_digital_twin(request, city_id):
|
|
||||||
try:
|
|
||||||
lat = float(request.GET.get('lat', 0))
|
|
||||||
long = float(request.GET.get('long', 0))
|
|
||||||
scale = float(request.GET.get('scale', 1.0)) # default to 1.0
|
|
||||||
|
|
||||||
if city_id == "osm_city":
|
|
||||||
city_data = generate_osm_road_midpoints_only(lat, long, scale=scale)
|
|
||||||
elif city_id == "random_city":
|
|
||||||
city_data = generate_random_city_data()
|
|
||||||
elif city_id == "gauge_ahead":
|
|
||||||
# No city data needed, just render the special AR scene
|
|
||||||
return render(request, 'pxy_city_digital_twins/spawn_gauge_ahead.html')
|
|
||||||
else:
|
|
||||||
raise Http404("Unsupported city_id for AR view")
|
|
||||||
|
|
||||||
preset = get_environment_preset(lat, long)
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'city_data': city_data,
|
|
||||||
'environment_preset': preset,
|
|
||||||
'lat': lat,
|
|
||||||
'long': long,
|
|
||||||
}
|
|
||||||
return render(request, 'pxy_city_digital_twins/city_augmented_digital_twin.html', context)
|
|
||||||
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
raise Http404("Invalid parameters provided.")
|
|
||||||
|
|
||||||
|
|
||||||
from django.shortcuts import render
|
|
||||||
from django.http import Http404
|
|
||||||
from .services.waste_routes import get_dispatch_data_for
|
|
||||||
import json
|
|
||||||
|
|
||||||
def waste_route_debug(request, subdivision, vehicle):
|
|
||||||
# 1) load all routes for this subdivision
|
|
||||||
dispatch_data = get_dispatch_data_for(subdivision)
|
|
||||||
if not dispatch_data:
|
|
||||||
return render(request, 'pxy_city_digital_twins/waste_route_default.html', {
|
|
||||||
'subdivision': subdivision
|
|
||||||
})
|
|
||||||
|
|
||||||
# 2) pick your route by the required `vehicle` path-param
|
|
||||||
try:
|
|
||||||
selected = next(r for r in dispatch_data if r['route_id'] == vehicle)
|
|
||||||
except StopIteration:
|
|
||||||
return render(request, 'pxy_city_digital_twins/waste_route_not_found.html', {
|
|
||||||
'subdivision': subdivision
|
|
||||||
})
|
|
||||||
|
|
||||||
# 3) derive a “center” latitude/longitude from that route:
|
|
||||||
coords = selected.get('coords', [])
|
|
||||||
if coords:
|
|
||||||
avg_lon = sum(pt[0] for pt in coords) / len(coords)
|
|
||||||
avg_lat = sum(pt[1] for pt in coords) / len(coords)
|
|
||||||
else:
|
|
||||||
steps = selected.get('steps', [])
|
|
||||||
avg_lon = sum(s['position'][0] for s in steps) / len(steps)
|
|
||||||
avg_lat = sum(s['position'][1] for s in steps) / len(steps)
|
|
||||||
|
|
||||||
# 4) generate your OSM‐based city around that center
|
|
||||||
city_data = generate_osm_city_data(avg_lat, avg_lon)
|
|
||||||
|
|
||||||
# 5) sanitize building heights (replace NaN or missing with default 10.0)
|
|
||||||
default_height = 10.0
|
|
||||||
for b in city_data.get('buildings', []):
|
|
||||||
h = b.get('height')
|
|
||||||
if not isinstance(h, (int, float)) or (isinstance(h, float) and math.isnan(h)):
|
|
||||||
b['height'] = default_height
|
|
||||||
|
|
||||||
# 6) render the VR template
|
|
||||||
return render(request, 'pxy_city_digital_twins/waste_route_debug.html', {
|
|
||||||
'subdivision': subdivision,
|
|
||||||
'vehicle': vehicle,
|
|
||||||
'selected_route_json': json.dumps(selected),
|
|
||||||
'city_data': city_data,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import math
|
|
||||||
import json
|
|
||||||
from django.shortcuts import render
|
|
||||||
from pyproj import Transformer
|
|
||||||
|
|
||||||
from .services.osm_city import generate_osm_city_data
|
|
||||||
from .services.waste_routes import get_dispatch_data_for
|
|
||||||
|
|
||||||
def waste_route(request, subdivision, vehicle):
|
|
||||||
"""
|
|
||||||
URL: /waste/<subdivision>/<vehicle>/
|
|
||||||
Renders a single vehicle's waste‐collection route in VR,
|
|
||||||
overlaid on an OSM‐generated city around the route center.
|
|
||||||
"""
|
|
||||||
# 1) load all routes for this subdivision
|
|
||||||
dispatch_data = get_dispatch_data_for(subdivision)
|
|
||||||
if not dispatch_data:
|
|
||||||
return render(request, 'pxy_city_digital_twins/waste_route_default.html', {
|
|
||||||
'subdivision': subdivision
|
|
||||||
})
|
|
||||||
|
|
||||||
# 2) pick your route by the required `vehicle` path-param
|
|
||||||
try:
|
|
||||||
selected = next(r for r in dispatch_data if r['route_id'] == vehicle)
|
|
||||||
except StopIteration:
|
|
||||||
return render(request, 'pxy_city_digital_twins/waste_route_not_found.html', {
|
|
||||||
'subdivision': subdivision
|
|
||||||
})
|
|
||||||
|
|
||||||
# 3) derive center lon/lat from coords (or fallback to steps)
|
|
||||||
coords = selected.get('coords', [])
|
|
||||||
if coords:
|
|
||||||
avg_lon = sum(pt[0] for pt in coords) / len(coords)
|
|
||||||
avg_lat = sum(pt[1] for pt in coords) / len(coords)
|
|
||||||
else:
|
|
||||||
steps = selected.get('steps', [])
|
|
||||||
avg_lon = sum(s['position'][0] for s in steps) / len(steps)
|
|
||||||
avg_lat = sum(s['position'][1] for s in steps) / len(steps)
|
|
||||||
|
|
||||||
# 4) generate your OSM‐based city around that center
|
|
||||||
city_data = generate_osm_city_data(avg_lat, avg_lon)
|
|
||||||
|
|
||||||
# 5) sanitize building heights (replace NaN or missing with default 10.0)
|
|
||||||
default_height = 10.0
|
|
||||||
for b in city_data.get('buildings', []):
|
|
||||||
h = b.get('height')
|
|
||||||
if not isinstance(h, (int, float)) or (isinstance(h, float) and math.isnan(h)):
|
|
||||||
b['height'] = default_height
|
|
||||||
|
|
||||||
# 6) project all coords (and steps) to Web Mercator, recenter them
|
|
||||||
transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
|
|
||||||
# route
|
|
||||||
merc = [transformer.transform(lon, lat) for lon, lat in coords]
|
|
||||||
if not merc:
|
|
||||||
merc = [transformer.transform(*s['position']) for s in selected.get('steps', [])]
|
|
||||||
avg_x = sum(x for x, _ in merc) / len(merc)
|
|
||||||
avg_z = sum(z for _, z in merc) / len(merc)
|
|
||||||
rel_coords = [[x - avg_x, z - avg_z] for x, z in merc]
|
|
||||||
|
|
||||||
# steps
|
|
||||||
step_positions = []
|
|
||||||
for step in selected.get('steps', []):
|
|
||||||
lon, lat = step['position']
|
|
||||||
x, z = transformer.transform(lon, lat)
|
|
||||||
step_positions.append({
|
|
||||||
'x': x - avg_x,
|
|
||||||
'z': z - avg_z,
|
|
||||||
'step_id': step.get('id'),
|
|
||||||
'step_type': step.get('step_type'),
|
|
||||||
'popup': step.get('popup'),
|
|
||||||
})
|
|
||||||
|
|
||||||
# 7) render
|
|
||||||
return render(request, 'pxy_city_digital_twins/virtual_waste_route.html', {
|
|
||||||
'subdivision': subdivision,
|
|
||||||
'vehicle': vehicle,
|
|
||||||
'selected_route_json': json.dumps(selected),
|
|
||||||
'city_data': city_data,
|
|
||||||
'rel_coords': rel_coords,
|
|
||||||
'step_positions': step_positions,
|
|
||||||
})
|
|
||||||
|
|
||||||
def augmented_waste_route(request, subdivision, vehicle):
|
|
||||||
"""
|
|
||||||
URL: /waste/<subdivision>/<vehicle>/
|
|
||||||
Renders a single vehicle's waste‐collection route in VR,
|
|
||||||
overlaid on an OSM‐generated city around the route center.
|
|
||||||
"""
|
|
||||||
# 1) load all routes for this subdivision
|
|
||||||
dispatch_data = get_dispatch_data_for(subdivision)
|
|
||||||
if not dispatch_data:
|
|
||||||
return render(request, 'pxy_city_digital_twins/waste_route_default.html', {
|
|
||||||
'subdivision': subdivision
|
|
||||||
})
|
|
||||||
|
|
||||||
# 2) pick your route by the required `vehicle` path-param
|
|
||||||
try:
|
|
||||||
selected = next(r for r in dispatch_data if r['route_id'] == vehicle)
|
|
||||||
except StopIteration:
|
|
||||||
return render(request, 'pxy_city_digital_twins/waste_route_not_found.html', {
|
|
||||||
'subdivision': subdivision
|
|
||||||
})
|
|
||||||
|
|
||||||
# 3) derive center lon/lat from coords (or fallback to steps)
|
|
||||||
coords = selected.get('coords', [])
|
|
||||||
if coords:
|
|
||||||
avg_lon = sum(pt[0] for pt in coords) / len(coords)
|
|
||||||
avg_lat = sum(pt[1] for pt in coords) / len(coords)
|
|
||||||
else:
|
|
||||||
steps = selected.get('steps', [])
|
|
||||||
avg_lon = sum(s['position'][0] for s in steps) / len(steps)
|
|
||||||
avg_lat = sum(s['position'][1] for s in steps) / len(steps)
|
|
||||||
|
|
||||||
# 4) generate your OSM‐based city around that center
|
|
||||||
city_data = generate_osm_city_data(avg_lat, avg_lon)
|
|
||||||
|
|
||||||
# 5) sanitize building heights (replace NaN or missing with default 10.0)
|
|
||||||
default_height = 10.0
|
|
||||||
for b in city_data.get('buildings', []):
|
|
||||||
h = b.get('height')
|
|
||||||
if not isinstance(h, (int, float)) or (isinstance(h, float) and math.isnan(h)):
|
|
||||||
b['height'] = default_height
|
|
||||||
|
|
||||||
# 6) project all coords (and steps) to Web Mercator, recenter them
|
|
||||||
transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
|
|
||||||
# route
|
|
||||||
merc = [transformer.transform(lon, lat) for lon, lat in coords]
|
|
||||||
if not merc:
|
|
||||||
merc = [transformer.transform(*s['position']) for s in selected.get('steps', [])]
|
|
||||||
avg_x = sum(x for x, _ in merc) / len(merc)
|
|
||||||
avg_z = sum(z for _, z in merc) / len(merc)
|
|
||||||
rel_coords = [[x - avg_x, z - avg_z] for x, z in merc]
|
|
||||||
|
|
||||||
# steps
|
|
||||||
step_positions = []
|
|
||||||
for step in selected.get('steps', []):
|
|
||||||
lon, lat = step['position']
|
|
||||||
x, z = transformer.transform(lon, lat)
|
|
||||||
step_positions.append({
|
|
||||||
'x': x - avg_x,
|
|
||||||
'z': z - avg_z,
|
|
||||||
'step': step
|
|
||||||
})
|
|
||||||
|
|
||||||
# 7) render
|
|
||||||
return render(request, 'pxy_city_digital_twins/augmented_waste_route.html', {
|
|
||||||
'subdivision': subdivision,
|
|
||||||
'vehicle': vehicle,
|
|
||||||
'selected_route_json': json.dumps(selected),
|
|
||||||
'city_data': city_data,
|
|
||||||
'rel_coords': rel_coords,
|
|
||||||
'step_positions': step_positions,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
import json
|
|
||||||
from django.http import JsonResponse, HttpResponseBadRequest
|
|
||||||
from django.views.decorators.http import require_POST
|
|
||||||
from django.views.decorators.csrf import csrf_exempt # or use the csrf token client-side
|
|
||||||
from .models import WastePickup
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
@require_POST
|
|
||||||
def record_pickup(request, subdivision, vehicle):
|
|
||||||
"""
|
|
||||||
Expect JSON body:
|
|
||||||
{ "step_id": "<id>", "quarter": <1-4> }
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
payload = json.loads(request.body)
|
|
||||||
step_id = payload['step_id']
|
|
||||||
quarter = int(payload['quarter'])
|
|
||||||
if quarter not in (1,2,3,4):
|
|
||||||
raise ValueError
|
|
||||||
except (KeyError, ValueError, json.JSONDecodeError):
|
|
||||||
return HttpResponseBadRequest("Invalid payload")
|
|
||||||
|
|
||||||
WastePickup.objects.create(
|
|
||||||
subdivision=subdivision,
|
|
||||||
vehicle=vehicle,
|
|
||||||
step_id=step_id,
|
|
||||||
quarter=quarter
|
|
||||||
)
|
|
||||||
return JsonResponse({"status":"ok"})
|
|
BIN
pxy_cr/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pxy_cr/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_cr/__pycache__/admin.cpython-310.pyc
Normal file
BIN
pxy_cr/__pycache__/admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_cr/__pycache__/apps.cpython-310.pyc
Normal file
BIN
pxy_cr/__pycache__/apps.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_cr/__pycache__/models.cpython-310.pyc
Normal file
BIN
pxy_cr/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
BIN
pxy_cr/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
pxy_cr/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
@ -1,10 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
from .models import SidebarMenuItem
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(SidebarMenuItem)
|
|
||||||
class SidebarMenuAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ("label", "type", "url", "order", "parent")
|
|
||||||
list_filter = ("type", "parent")
|
|
||||||
search_fields = ("label", "url")
|
|
||||||
ordering = ("order",)
|
|
@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class PxyDashboardConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'pxy_dashboard'
|
|
@ -1,47 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
from .models import Country, GeoScenario, OptScenario
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Country)
|
|
||||||
class CountryAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('code', 'name')
|
|
||||||
search_fields = ('code', 'name')
|
|
||||||
ordering = ('name',)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(GeoScenario)
|
|
||||||
class GeoScenarioAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name', 'country', 'upload_date', 'geographic_field_name')
|
|
||||||
list_filter = ('country', 'upload_date')
|
|
||||||
search_fields = ('name', 'country__name')
|
|
||||||
readonly_fields = ('upload_date',)
|
|
||||||
fieldsets = (
|
|
||||||
(None, {
|
|
||||||
'fields': (
|
|
||||||
'name',
|
|
||||||
'country',
|
|
||||||
'geographic_field_name',
|
|
||||||
'csv_file',
|
|
||||||
'upload_date',
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(OptScenario)
|
|
||||||
class OptScenarioAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ("name", "type_of_waste", "strategy", "geo_scenario", "upload_date")
|
|
||||||
list_filter = ("type_of_waste", "strategy", "upload_date")
|
|
||||||
search_fields = ("name",)
|
|
||||||
readonly_fields = ("upload_date",)
|
|
||||||
fieldsets = (
|
|
||||||
(None, {
|
|
||||||
"fields": ("name", "type_of_waste", "strategy", "geo_scenario")
|
|
||||||
}),
|
|
||||||
("Archivos del escenario", {
|
|
||||||
"fields": ("optimized_csv", "dispatch_json")
|
|
||||||
}),
|
|
||||||
("Metadata", {
|
|
||||||
"fields": ("upload_date",)
|
|
||||||
}),
|
|
||||||
)
|
|
@ -1,5 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
class AppsConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'pxy_dashboard.apps'
|
|
@ -1,58 +0,0 @@
|
|||||||
# Generated by Django 5.0.3 on 2025-05-19 00:37
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Country',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('code', models.CharField(max_length=3, unique=True)),
|
|
||||||
('name', models.CharField(max_length=100)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'País',
|
|
||||||
'verbose_name_plural': 'Países',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='GeoScenario',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(help_text='Nombre del escenario geográfico base', max_length=255)),
|
|
||||||
('upload_date', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('geographic_field_name', models.CharField(help_text='Columna con el identificador geográfico (ej. N_URBANO, PUEBLO)', max_length=100)),
|
|
||||||
('csv_file', models.FileField(upload_to='geo_scenarios/')),
|
|
||||||
('country', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='apps.country')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Escenario Geográfico',
|
|
||||||
'verbose_name_plural': 'Escenarios Geográficos',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='OptScenario',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(help_text='Nombre del escenario de optimización', max_length=255)),
|
|
||||||
('type_of_waste', models.CharField(help_text='Tipo de residuo (ej. orgánico, reciclable)', max_length=100)),
|
|
||||||
('strategy', models.CharField(help_text='Método de optimización utilizado', max_length=100)),
|
|
||||||
('upload_date', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('optimized_csv', models.FileField(upload_to='opt_scenarios/')),
|
|
||||||
('geo_scenario', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opt_scenarios', to='apps.geoscenario')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Escenario de Optimización',
|
|
||||||
'verbose_name_plural': 'Escenarios de Optimización',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.0.3 on 2025-05-19 21:03
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('apps', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='optscenario',
|
|
||||||
name='dispatch_json',
|
|
||||||
field=models.FileField(blank=True, help_text='Archivo JSON con el resultado de despacho por ruta', null=True, upload_to='opt_scenarios/json/'),
|
|
||||||
),
|
|
||||||
]
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user