PorFin™ | Herramienta de Supervivencia Urbana
PorFin™
🛡️ Herramienta de Supervivencia Urbana
Cuando nadie ve nada,
PorFin™ interpreta la normativa,
protege tus derechos
y te acompaña hasta el destino.
/*======================================================*/
/* CABECERA */
/*======================================================*/
#topBar{
position:absolute;
top:12px;
left:12px;
right:12px;
z-index:9000;
display:flex;
justify-content:space-between;
align-items:center;
padding:12px 16px;
background:rgba(8,18,30,.85);
backdrop-filter:blur(14px);
border-radius:18px;
color:white;
}
.logoPorfin{
font-size:20px;
font-weight:700;
}
.estadoEscudo{
padding:8px 14px;
border-radius:30px;
background:#1d4ed8;
font-size:14px;
font-weight:bold;
}
#buscadorDestino{
position:absolute;
top:80px;
left:12px;
right:12px;
display:flex;
gap:10px;
z-index:9000;
}
#buscadorDestino input{
flex:1;
padding:14px;
border:none;
border-radius:16px;
font-size:16px;
outline:none;
}
#buscadorDestino button{
width:56px;
border:none;
border-radius:16px;
font-size:22px;
background:#2563eb;
color:white;
cursor:pointer;
}
function actualizarHalo(color,texto){
const estado=document.getElementById(“estadoEscudo”);
estado.innerHTML=texto;
estado.style.background=color;
}
actualizarHalo(“#16a34a”,”🟢 Halo Verde”);
/*======================================================*/
/* YO + HALO PORFIN™ */
/*======================================================*/
.halo-yo{
position:relative;
width:74px;
height:74px;
display:flex;
align-items:center;
justify-content:center;
}
.halo-luz{
position:absolute;
width:72px;
height:72px;
border-radius:50%;
animation:haloPulse 2s infinite;
opacity:.85;
}
.halo-verde{
background:rgba(34,197,94,.45);
box-shadow:
0 0 15px #22c55e,
0 0 30px #22c55e,
0 0 45px #22c55e;
}
.halo-ambar{
background:rgba(251,191,36,.45);
box-shadow:
0 0 15px #fbbf24,
0 0 30px #fbbf24,
0 0 45px #fbbf24;
}
.avatar{
position:absolute;
width:54px;
height:54px;
border-radius:50%;
border:3px solid white;
overflow:hidden;
background:white;
z-index:5;
}
.avatar img{
width:100%;
height:100%;
object-fit:cover;
}
@keyframes haloPulse{
0%{
transform:scale(.9);
opacity:.45;
}
50%{
transform:scale(1.18);
opacity:.95;
}
100%{
transform:scale(.9);
opacity:.45;
}
}
/*======================================================*/
/* MARCADOR YO */
/*======================================================*/
let yoMarker;
function crearMarcadorYO(lat,lng,color=”verde”){
const claseHalo=color===”verde”
?”halo-verde”
:”halo-ambar”;
const icon=L.divIcon({
className:””,
iconSize:[74,74],
iconAnchor:[37,37],
html:`
`
});
if(yoMarker){
yoMarker.setLatLng([lat,lng]);
yoMarker.setIcon(icon);
}else{
yoMarker=L.marker([lat,lng],{
icon:icon
}).addTo(map);
}
}
/*======================================================*/
/* MARCADOR YO */
/*======================================================*/
let yoMarker;
function crearMarcadorYO(lat,lng,color=”verde”){
const claseHalo=color===”verde”
?”halo-verde”
:”halo-ambar”;
const icon=L.divIcon({
className:””,
iconSize:[74,74],
iconAnchor:[37,37],
html:`
`
});
if(yoMarker){
yoMarker.setLatLng([lat,lng]);
yoMarker.setIcon(icon);
}else{
yoMarker=L.marker([lat,lng],{
icon:icon
}).addTo(map);
}
}
crearMarcadorYO(lat,lng,”verde”);
/*======================================================*/
/* BLOQUE 5 – GPS + SEGUIMIENTO PORFIN™ */
/*======================================================*/
let watchID = null;
function iniciarGPS() {
if (!navigator.geolocation) {
alert(“Este dispositivo no dispone de GPS.”);
return;
}
watchID = navigator.geolocation.watchPosition(
actualizarPosicion,
errorGPS,
{
enableHighAccuracy: true,
maximumAge: 1000,
timeout: 10000
}
);
}
function actualizarPosicion(pos) {
const lat = pos.coords.latitude;
const lng = pos.coords.longitude;
const velocidad = pos.coords.speed
? Math.round(pos.coords.speed * 3.6)
: 0;
const rumbo = pos.coords.heading ?? 0;
crearMarcadorYO(lat, lng, estadoHalo);
const punto = proyectarYO(lat, lng, rumbo);
map.setView(punto, map.getZoom(), {
animate: true
});
if (rumbo !== null && velocidad > 5) {
document.getElementById(“map”).style.transform =
`rotate(${-rumbo}deg)`;
}
document.getElementById(“velocidad”).innerHTML =
velocidad + ” km/h”;
}
function proyectarYO(lat, lng, rumbo) {
const punto = map.project([lat, lng], map.getZoom());
const alto = map.getSize().y;
const offset = alto * 0.28;
const rad = rumbo * Math.PI / 180;
const nuevo = punto.add([
Math.sin(rad) * offset * 0.25,
-Math.cos(rad) * offset
]);
return map.unproject(nuevo, map.getZoom());
}
function errorGPS(error) {
console.log(error);
}
function detenerGPS() {
if (watchID !== null) {
navigator.geolocation.clearWatch(watchID);
watchID = null;
}
}
iniciarGPS();
/*======================================================*/
/* BLOQUE 6 · NAVEGACIÓN INTELIGENTE PORFIN™ */
/*======================================================*/
let rutaControl = null;
let destinoActual = null;
async function buscarDestino() {
const texto = document.getElementById(“destinoInput”).value.trim();
if (texto === “”) return;
try {
const respuesta = await fetch(
“https://nominatim.openstreetmap.org/search?format=json&q=” +
encodeURIComponent(texto)
);
const datos = await respuesta.json();
if (!datos.length) {
alert(“Destino no encontrado.”);
return;
}
destinoActual = L.latLng(
parseFloat(datos[0].lat),
parseFloat(datos[0].lon)
);
calcularRuta(destinoActual);
} catch (e) {
console.error(e);
alert(“Error buscando el destino.”);
}
}
function calcularRuta(destino) {
if (!yoMarker) return;
if (rutaControl) {
map.removeControl(rutaControl);
}
rutaControl = L.Routing.control({
waypoints: [
yoMarker.getLatLng(),
destino
],
routeWhileDragging: false,
addWaypoints: false,
draggableWaypoints: false,
fitSelectedRoutes: true,
show: false,
createMarker: function () {
return null;
},
lineOptions: {
styles: [
{
color: “#00b7ff”,
opacity: 0.9,
weight: 7
}
]
}
})
.on(“routesfound”, function(e){
const ruta = e.routes[0];
const km = (ruta.summary.totalDistance/1000).toFixed(1);
const min = Math.round(ruta.summary.totalTime/60);
console.log(“Ruta:”,km+” km”,min+” min”);
hablar(
“Ruta calculada. ” +
km +
” kilómetros. Tiempo estimado ” +
min +
” minutos.”
);
})
.addTo(map);
}
function cancelarRuta(){
if(rutaControl){
map.removeControl(rutaControl);
rutaControl=null;
}
destinoActual=null;
hablar(“Ruta cancelada.”);
}
/*======================================================*/
/* BLOQUE 7 · ATLAS NORMATIVO PORFIN™ */
/*======================================================*/
const atlasNormativo = {
“madrid”:{
halo:”verde”,
mensaje:”Puedes estacionar gratuitamente en SER.”
},
“guadalajara”:{
halo:”ambar”,
mensaje:”Zona Azul regulada. Consulta condiciones.”
},
“barcelona”:{
halo:”ambar”,
mensaje:”Zona regulada con limitaciones.”
},
“valencia”:{
halo:”verde”,
mensaje:”Estacionamiento permitido con Tarjeta.”
},
“zaragoza”:{
halo:”verde”,
mensaje:”Zona ORA gratuita.”
}
};
let municipioActual=””;
async function actualizarNormativa(lat,lng){
try{
const respuesta=await fetch(
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=12&accept-language=es`
);
const datos=await respuesta.json();
const municipio=
(datos.address.city||
datos.address.town||
datos.address.village||
datos.address.municipality||
“”).toLowerCase();
if(municipio!==municipioActual){
municipioActual=municipio;
cambiarHaloMunicipio(municipio);
}
}catch(e){
console.log(e);
}
}
function cambiarHaloMunicipio(nombre){
const norma=atlasNormativo[nombre];
if(!norma){
estadoHalo=”ambar”;
crearMarcadorYO(
yoMarker.getLatLng().lat,
yoMarker.getLatLng().lng,
“ambar”
);
return;
}
estadoHalo=norma.halo;
crearMarcadorYO(
yoMarker.getLatLng().lat,
yoMarker.getLatLng().lng,
estadoHalo
);
hablar(norma.mensaje);
console.log(“Municipio:”,nombre);
console.log(“Normativa:”,norma.mensaje);
}
actualizarNormativa(lat,lng);
/*======================================================*/
/* BLOQUE 8 · MIRADA ADELANTADA™ */
/*======================================================*/
let aviso150=false;
let aviso100=false;
let aviso50=false;
let aviso25=false;
let aviso10=false;
function reiniciarMiradaAdelantada(){
aviso150=false;
aviso100=false;
aviso50=false;
aviso25=false;
aviso10=false;
}
function comprobarMiradaAdelantada(){
if(!destinoActual) return;
if(!yoMarker) return;
const yo=yoMarker.getLatLng();
const distancia=calcularDistancia(
yo.lat,
yo.lng,
destinoActual.lat,
destinoActual.lng
);
if(distancia<=150 && !aviso150){
aviso150=true;
hablar("Nos acercamos.");
mostrarMensajeHalo(
"🟡 Nos acercamos al destino."
);
}
if(distancia<=100 && !aviso100){
aviso100=true;
hablar("Puedes aparcar con tu Tarjeta de Estacionamiento según la normativa.");
mostrarMensajeHalo(
"🟢 Consulta el Halo PorFin."
);
}
if(distancia<=50 && !aviso50){
aviso50=true;
hablar("Entrando en la calle del destino.");
mostrarMensajeHalo(
"📍 Calle actual."
);
}
if(distancia<=25 && !aviso25){
aviso25=true;
hablar("Atención a las zonas de carga y descarga.");
mostrarMensajeHalo(
"⚠ Revisa Carga y Descarga."
);
}
if(distancia<=10 && !aviso10){
aviso10=true;
hablar("Destino alcanzado.");
mostrarMensajeHalo(
"🛡 Halo confirmado."
);
}
}
function mostrarMensajeHalo(texto){
let caja=document.getElementById("haloInfo");
if(!caja){
caja=document.createElement("div");
caja.id="haloInfo";
caja.style.position="absolute";
caja.style.top="20px";
caja.style.left="50%";
caja.style.transform="translateX(-50%)";
caja.style.zIndex="9999";
caja.style.background="rgba(6,18,6,.90)";
caja.style.color="white";
caja.style.padding="12px 18px";
caja.style.borderRadius="18px";
caja.style.fontWeight="600";
caja.style.backdropFilter="blur(12px)";
caja.style.border="1px solid rgba(255,255,255,.15)";
document.getElementById("map-wrapper").appendChild(caja);
}
caja.innerHTML=texto;
clearTimeout(caja.timer);
caja.timer=setTimeout(()=>{
caja.remove();
},5000);
}
/*======================================================*/
/* BLOQUE 9 · MOTOR DE NAVEGACIÓN PORFIN™ */
/*======================================================*/
let ultimoRumbo = 0;
function actualizarNavegacion(lat,lng,heading,speed){
if(!map) return;
if(speed < 3){
map.panTo([lat,lng],{
animate:true,
duration:0.5
});
return;
}
if(heading !== null && !isNaN(heading)){
ultimoRumbo = heading;
}
const punto = proyectarYO(lat,lng,ultimoRumbo);
map.panTo(punto,{
animate:true,
duration:0.8
});
girarMapa(ultimoRumbo);
}
function girarMapa(rumbo){
const pane = map.getPane("mapPane");
if(!pane) return;
pane.style.transformOrigin="50% 50%";
pane.style.transition="transform .35s linear";
pane.style.transform=`rotate(${-rumbo}deg)`;
}
function detenerRotacionMapa(){
const pane = map.getPane("mapPane");
if(!pane) return;
pane.style.transform="rotate(0deg)";
}
actualizarNavegacion(
lat,
lng,
pos.coords.heading,
velocidad
);
actualizarNavegacion(
lat,
lng,
pos.coords.heading,
velocidad
);
detenerRotacionMapa();
/*======================================================*/
/* BLOQUE 10 · ATLAS NORMATIVO INTELIGENTE */
/*======================================================*/
let Atlas = {};
/*--------------------------------------*/
/* CARGAR BASE NORMATIVA */
/*--------------------------------------*/
async function cargarAtlas(){
try{
const respuesta = await fetch("atlas_normativo.json");
Atlas = await respuesta.json();
console.log(
"✅ Atlas cargado:",
Object.keys(Atlas).length,
"municipios"
);
}catch(error){
console.error("Error cargando Atlas",error);
}
}
/*--------------------------------------*/
/* BUSCAR MUNICIPIO */
/*--------------------------------------*/
function obtenerNormativa(nombreCiudad){
if(!nombreCiudad) return null;
const ciudad = nombreCiudad
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g,"")
.trim();
return Atlas[ciudad] || null;
}
/*--------------------------------------*/
/* ACTUALIZAR HALO */
/*--------------------------------------*/
function actualizarHaloDesdeAtlas(ciudad){
const norma = obtenerNormativa(ciudad);
if(!norma){
estadoHalo="ambar";
return;
}
estadoHalo=norma.halo;
if(userMarker){
userMarker.setIcon(
createYoIcon(estadoHalo)
);
}
}
/*--------------------------------------*/
/* MENSAJE NORMATIVO */
/*--------------------------------------*/
function mensajeNormativa(ciudad){
const norma=obtenerNormativa(ciudad);
if(!norma){
return "Normativa pendiente de incorporar.";
}
return norma.descripcion;
}
{
"guadalajara": {
"halo": "ambar",
"descripcion": "Zona Azul regulada. Consulte condiciones municipales."
},
"madrid": {
"halo": "verde",
"descripcion": "SER gratuito para Personas con Movilidad Reducida."
},
"barcelona": {
"halo": "ambar",
"descripcion": "Zona Verde gratuita. Zona Azul con limitaciones."
}
}