[
{
"id": "ae9e82a0a0e1eb89",
"type": "tab",
"label": "Cálculos de H3",
"disabled": false,
"info": "",
"env": []
},
{
"id": "f511dd2230a153e7",
"type": "tab",
"label": "Grafíco",
"disabled": false,
"info": "",
"env": []
},
{
"id": "230815bb0628a63e",
"type": "tab",
"label": "Modelo",
"disabled": false,
"info": "",
"env": []
},
{
"id": "56fb69eb622c3f3b",
"type": "tab",
"label": "Mapa",
"disabled": false,
"info": "",
"env": []
},
{
"id": "748b5921246ec468",
"type": "postgreSQLConfig",
"name": "PostGIS",
"host": "10.10.5.32",
"hostFieldType": "str",
"port": 5432,
"portFieldType": "num",
"database": "postgres",
"databaseFieldType": "str",
"ssl": "false",
"sslFieldType": "bool",
"applicationName": "",
"applicationNameType": "str",
"max": "500",
"maxFieldType": "num",
"idle": "100000",
"idleFieldType": "num",
"connectionTimeout": "300000",
"connectionTimeoutFieldType": "num",
"user": "postgres",
"userFieldType": "str",
"password": "tfmuocdfcarvajal",
"passwordFieldType": "str"
},
{
"id": "3afa9de4406d30d8",
"type": "ui-base",
"name": "Mapa de demanda",
"path": "/dashboard",
"appIcon": "",
"includeClientData": true,
"acceptsClientConfig": [
"ui-notification",
"ui-control"
],
"showPathInSidebar": false,
"headerContent": "dashboard",
"navigationStyle": "temporary",
"titleBarStyle": "fixed",
"showReconnectNotification": true,
"notificationDisplayTime": 1,
"showDisconnectNotification": true,
"allowInstall": true
},
{
"id": "c81bf3ad6297e603",
"type": "ui-theme",
"name": "Default Theme",
"colors": {
"surface": "#ffffff",
"primary": "#0094CE",
"bgPage": "#eeeeee",
"groupBg": "#ffffff",
"groupOutline": "#cccccc"
},
"sizes": {
"density": "default",
"pagePadding": "12px",
"groupGap": "12px",
"groupBorderRadius": "4px",
"widgetGap": "12px"
}
},
{
"id": "b332967d63ddbdfe",
"type": "ui-page",
"name": "Estimaciones",
"ui": "3afa9de4406d30d8",
"path": "/estimaciones",
"icon": "home",
"layout": "flex",
"theme": "c81bf3ad6297e603",
"breakpoints": [
{
"name": "Default",
"px": "0",
"cols": "3"
},
{
"name": "Tablet",
"px": "576",
"cols": "6"
},
{
"name": "Small Desktop",
"px": "768",
"cols": "9"
},
{
"name": "Desktop",
"px": "1024",
"cols": "12"
}
],
"order": 1,
"className": "",
"visible": true,
"disabled": false
},
{
"id": "f8afeee042444067",
"type": "ui-group",
"name": "Mapa",
"page": "b332967d63ddbdfe",
"width": 12,
"height": "9",
"order": 2,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "bb66042c3e7c9cb3",
"type": "ui-group",
"name": "Configuración",
"page": "b332967d63ddbdfe",
"width": 3,
"height": "7",
"order": 1,
"showTitle": true,
"className": "",
"visible": "true",
"disabled": "false",
"groupType": "default"
},
{
"id": "3005f842c2ae482e",
"type": "inject",
"z": "ae9e82a0a0e1eb89",
"name": "Definimos el nivel 8",
"props": [
{
"p": "payload"
},
{
"p": "nivel_hexagono",
"v": "8",
"vt": "num"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 130,
"y": 260,
"wires": [
[
"ee9152d1f76ccaac"
]
]
},
{
"id": "41f3663b6be9dc20",
"type": "debug",
"z": "ae9e82a0a0e1eb89",
"name": "Resultado",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 580,
"y": 260,
"wires": []
},
{
"id": "ee9152d1f76ccaac",
"type": "function",
"z": "ae9e82a0a0e1eb89",
"name": "Realizamos los calculos",
"func": "// Node-RED Function Node Script\n// Genera tabla de hexágonos recorribles según velocidad y tiempo\n\n// Obtener parámetros de entrada\nconst nivel_hexagono = msg.nivel_hexagono || 8; // nivel H3 por defecto\nconst velocidad_min = msg.velocidad_min || 10; // km/h mínima\nconst velocidad_max = msg.velocidad_max || 30; // km/h máxima \nconst tiempo_minutos = msg.tiempo_minutos || 20; // minutos disponibles\n\nlet h3 = h3Js\n\n// Validar que tenemos las librerías necesarias\nif (typeof h3 === 'undefined') {\n node.error(\"H3 library not available\");\n return null;\n}\n\ntry {\n // Calcular longitud del lado del hexágono en metros\n const longitud_lado = h3.getHexagonEdgeLengthAvg(nivel_hexagono, 'm');\n\n // Convertir velocidades de km/h a m/s\n const velocidad_min_ms = (velocidad_min * 1000) / 3600;\n const velocidad_max_ms = (velocidad_max * 1000) / 3600;\n\n // Convertir tiempo de minutos a segundos\n const tiempo_segundos = tiempo_minutos * 60;\n\n // Calcular distancias máximas recorribles\n const distancia_min = velocidad_min_ms * tiempo_segundos;\n const distancia_max = velocidad_max_ms * tiempo_segundos;\n\n // Calcular número de hexágonos en cada dirección\n const hexagonos_min = Math.floor(distancia_min / longitud_lado);\n const hexagonos_max = Math.floor(distancia_max / longitud_lado);\n\n // Generar tabla de resultados\n const tabla_resultados = [];\n\n for (let num_hexagonos = hexagonos_min; num_hexagonos <= hexagonos_max; num_hexagonos++) {\n const distancia_recorrida = num_hexagonos * longitud_lado;\n const velocidad_requerida = (distancia_recorrida / tiempo_segundos) * 3.6; // Convertir a km/h\n\n tabla_resultados.push({\n num_hexagonos: num_hexagonos,\n distancia_metros: Math.round(distancia_recorrida),\n distancia_km: (distancia_recorrida / 1000).toFixed(2),\n velocidad_requerida_kmh: velocidad_requerida.toFixed(2),\n nivel_h3: nivel_hexagono,\n lado_hexagono_m: Math.round(longitud_lado)\n });\n }\n\n // Preparar mensaje de salida\n msg.payload = {\n parametros: {\n nivel_hexagono: nivel_hexagono,\n velocidad_min_kmh: velocidad_min,\n velocidad_max_kmh: velocidad_max,\n tiempo_minutos: tiempo_minutos,\n lado_hexagono_metros: Math.round(longitud_lado)\n },\n tabla: tabla_resultados,\n resumen: {\n hexagonos_minimos: hexagonos_min,\n hexagonos_maximos: hexagonos_max,\n rango_hexagonos: `${hexagonos_min} - ${hexagonos_max}`,\n distancia_minima_km: (hexagonos_min * longitud_lado / 1000).toFixed(2),\n distancia_maxima_km: (hexagonos_max * longitud_lado / 1000).toFixed(2)\n }\n };\n\n // Agregar información adicional para debugging\n msg.hexagonInfo = {\n nivel: nivel_hexagono,\n longitud_lado: longitud_lado\n };\n\n return msg;\n\n} catch (error) {\n node.error(\"Error processing hexagon data: \" + error.message);\n msg.error = error.message;\n return msg;\n}",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
{
"var": "h3Js",
"module": "h3-js"
}
],
"x": 370,
"y": 260,
"wires": [
[
"41f3663b6be9dc20"
]
]
},
{
"id": "0934bc707c9b611d",
"type": "comment",
"z": "ae9e82a0a0e1eb89",
"name": "Cálculo de la tabla de recorrido para la grua",
"info": "",
"x": 210,
"y": 180,
"wires": []
},
{
"id": "d801ff754db4d5e8",
"type": "inject",
"z": "ae9e82a0a0e1eb89",
"name": "Definimos el nivel 7",
"props": [
{
"p": "payload"
},
{
"p": "nivel_hexagono",
"v": "7",
"vt": "num"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 130,
"y": 220,
"wires": [
[
"ee9152d1f76ccaac"
]
]
},
{
"id": "9ca4fbaf930ad8e1",
"type": "inject",
"z": "ae9e82a0a0e1eb89",
"name": "Definimos el nivel 9",
"props": [
{
"p": "payload"
},
{
"p": "nivel_hexagono",
"v": "9",
"vt": "num"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 130,
"y": 300,
"wires": [
[
"ee9152d1f76ccaac"
]
]
},
{
"id": "ab02b07ce8843526",
"type": "worldmap",
"z": "f511dd2230a153e7",
"name": "",
"lat": "",
"lon": "",
"zoom": "",
"layer": "",
"cluster": "",
"maxage": "",
"usermenu": "show",
"layers": "show",
"panit": "false",
"panlock": "false",
"zoomlock": "false",
"hiderightclick": "false",
"coords": "false",
"showgrid": "false",
"showruler": "false",
"allowFileDrop": "false",
"path": "/worldmap",
"overlist": "DR,CO,RA,DN",
"maplist": "OSMG,OSMC,EsriC,EsriS,UKOS",
"mapname": "",
"mapurl": "",
"mapopt": "",
"mapwms": false,
"x": 440,
"y": 280,
"wires": []
},
{
"id": "10f4dc8ae329f33e",
"type": "inject",
"z": "f511dd2230a153e7",
"name": "Mover mapa",
"props": [
{
"p": "payload.command",
"v": "{\"lat\":\"40.40684\",\"lon\":\"-3.5711476\"}",
"vt": "json"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 290,
"y": 240,
"wires": [
[
"ab02b07ce8843526"
]
]
},
{
"id": "5a0676c5ba4e943b",
"type": "inject",
"z": "f511dd2230a153e7",
"name": "Zoom",
"props": [
{
"p": "payload.command",
"v": "{\"zoom\":15}",
"vt": "json"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 270,
"y": 280,
"wires": [
[
"ab02b07ce8843526"
]
]
},
{
"id": "a94de8711099eff7",
"type": "inject",
"z": "f511dd2230a153e7",
"name": "House",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "{\"name\":\"Casa\",\"icon\":\"home\",\"lat\":\"40.40412499978462\",\"lon\":\"-3.5643343002563848\"}",
"payloadType": "json",
"x": 270,
"y": 320,
"wires": [
[
"ab02b07ce8843526"
]
]
},
{
"id": "33cf314d5cca7423",
"type": "worldmap in",
"z": "f511dd2230a153e7",
"name": "",
"path": "/worldmap",
"events": "connect,disconnect,point,layer,bounds,files,draw,other",
"x": 460,
"y": 760,
"wires": [
[
"bae0846569936702"
]
]
},
{
"id": "bae0846569936702",
"type": "debug",
"z": "f511dd2230a153e7",
"name": "debug 1",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 610,
"y": 760,
"wires": []
},
{
"id": "acae736839e9942c",
"type": "postgresql",
"z": "f511dd2230a153e7",
"name": "Creación de la tabla de servicios_geo",
"query": "SELECT id_servicio,\n ST_AsGeoJSON(geom)::json AS geom\nFROM servicios_geo;",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 390,
"y": 60,
"wires": [
[
"e0f0d5d6ac05abf7"
]
]
},
{
"id": "d9c3487d5a4cd9f2",
"type": "inject",
"z": "f511dd2230a153e7",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 100,
"wires": [
[
"8115125206120309"
]
]
},
{
"id": "e0f0d5d6ac05abf7",
"type": "debug",
"z": "f511dd2230a153e7",
"name": "debug 2",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 660,
"y": 120,
"wires": []
},
{
"id": "b34c1a218cd45651",
"type": "postgresql",
"z": "f511dd2230a153e7",
"name": "Consulta h3",
"query": "SELECT h3_lat_lng_to_cell(POINT('37.7749, -122.4194'), 9) AS h3_index;",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 290,
"y": 180,
"wires": [
[
"e0f0d5d6ac05abf7"
]
]
},
{
"id": "b6be2c6139d939f0",
"type": "postgresql",
"z": "f511dd2230a153e7",
"name": "Consultar solo 500",
"query": "SELECT id_servicio,\n ST_AsGeoJSON(geom)::json AS geom\nFROM servicios_geo\nORDER BY id_servicio\nLIMIT 500",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 410,
"y": 120,
"wires": [
[
"e0f0d5d6ac05abf7"
]
]
},
{
"id": "9fbe677b4f48e523",
"type": "postgresql",
"z": "f511dd2230a153e7",
"name": "Hablitar extension h3",
"query": "CREATE EXTENSION h3;",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 540,
"y": 200,
"wires": [
[
"e0f0d5d6ac05abf7"
]
]
},
{
"id": "2af33006f75c5667",
"type": "postgresql",
"z": "f511dd2230a153e7",
"name": "Consulta h3",
"query": "SELECT h3_lat_lng_to_cell(\n point(ST_X(geom), ST_Y(geom)), -- lon, lat\n 7\n ) AS h3,\n COUNT(*) AS total\nFROM servicios_geo\nGROUP BY h3\nORDER BY total DESC;\n",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 310,
"y": 500,
"wires": [
[
"e0f0d5d6ac05abf7"
]
]
},
{
"id": "8115125206120309",
"type": "postgresql",
"z": "f511dd2230a153e7",
"name": "Consulta h3",
"query": "SELECT h3_lat_lng_to_cell(\n point(ST_Y(geom), ST_X(geom)), -- lat, lon\n 7\n ) AS h3,\n COUNT(*) AS total\nFROM servicios_geo\nGROUP BY h3\nORDER BY total DESC;",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 310,
"y": 560,
"wires": [
[
"e0f0d5d6ac05abf7"
]
]
},
{
"id": "1b3c9cf08f33d013",
"type": "postgresql",
"z": "f511dd2230a153e7",
"name": "Consulta h3 con geojson",
"query": "SELECT \n h3_lat_lng_to_cell(point(ST_Y(geom), ST_X(geom)), 8) AS h3,\n COUNT(*) AS total\nFROM servicios_geo\nGROUP BY h3;",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 350,
"y": 620,
"wires": [
[
"307dd85fbc2e3dc9"
]
]
},
{
"id": "77fb49d72430c2b0",
"type": "inject",
"z": "f511dd2230a153e7",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 130,
"y": 620,
"wires": [
[
"1b3c9cf08f33d013"
]
]
},
{
"id": "b660069d0a42ba72",
"type": "debug",
"z": "f511dd2230a153e7",
"name": "GEOJSON",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 710,
"y": 580,
"wires": []
},
{
"id": "fa52e6e1059e9acc",
"type": "worldmap",
"z": "f511dd2230a153e7",
"name": "",
"lat": "40.41341559118809",
"lon": "-3.695097056880839",
"zoom": "9",
"layer": "OSMC",
"cluster": "",
"maxage": "",
"usermenu": "show",
"layers": "show",
"panit": "false",
"panlock": "false",
"zoomlock": "false",
"hiderightclick": "false",
"coords": "none",
"showgrid": "false",
"showruler": "false",
"allowFileDrop": "false",
"path": "/worldmap",
"overlist": "",
"maplist": "OSMG,OSMC,EsriT",
"mapname": "",
"mapurl": "",
"mapopt": "",
"mapwms": false,
"x": 700,
"y": 620,
"wires": []
},
{
"id": "307dd85fbc2e3dc9",
"type": "function",
"z": "f511dd2230a153e7",
"name": "Pruebas",
"func": "let h3 = h3Js;\n\n// Validar que tenemos las librerías necesarias\nif (typeof h3 === 'undefined') {\n node.error(\"H3 library not available\");\n return null;\n}\n\nconst payload = msg.payload;\n\n// Obtener el total máximo para calcular el porcentaje\nconst maxTotal = Math.max(...payload.map(i => parseFloat(i.total)));\n\nconst features = [];\n\n// --- COLORES SEGÚN PORCENTAJE ---\nfunction getFillColor(pct) {\n if (pct <= 25) return \"#00ff00\"; // verde\n if (pct <= 50) return \"#ffff00\"; // amarillo\n if (pct <= 75) return \"#ffa500\"; // naranja\n return \"#ff0000\"; // rojo\n}\n\npayload.forEach(item => {\n const h3Index = item.h3;\n const total = parseFloat(item.total);\n\n try {\n const hexBoundary = h3.cellToBoundary(h3Index, true);\n const center = h3.cellToLatLng(h3Index);\n\n // Calcular porcentaje respecto al máximo\n const pct = maxTotal > 0 ? (total / maxTotal) * 100 : 0;\n\n const fillColor = getFillColor(pct);\n\n const feature = {\n \"type\": \"Feature\",\n \"properties\": {\n \"h3\": h3Index,\n \"total\": total,\n \"pct\": pct.toFixed(2),\n \"name\": `H3: ${h3Index} - ${pct.toFixed(1)}%`,\n \"resolution\": h3.getResolution(h3Index),\n \"lat\": center[0],\n \"lon\": center[1],\n\n // ESTILO\n \"fill\": fillColor,\n \"fill-opacity\": 0.5,\n \"stroke\": \"#000000\",\n \"stroke-width\": 1,\n \"stroke-opacity\": 0.9\n },\n \"geometry\": {\n \"type\": \"Polygon\",\n \"coordinates\": [hexBoundary]\n }\n };\n\n features.push(feature);\n\n } catch (error) {\n node.warn(`Error procesando H3 index ${h3Index}: ${error}`);\n }\n});\n\n// FeatureCollection final\nmsg.payload = {\n \"type\": \"FeatureCollection\",\n \"features\": features,\n \"Layer\": \"H3 Layer\"\n};\n\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
{
"var": "h3Js",
"module": "h3-js"
}
],
"x": 540,
"y": 620,
"wires": [
[
"fa52e6e1059e9acc",
"b660069d0a42ba72"
]
]
},
{
"id": "27eb0e7ea2d102b6",
"type": "function",
"z": "f511dd2230a153e7",
"name": "Funciona en modo básico con colores",
"func": "let h3 = h3Js;\n\n// Validar que tenemos las librerías necesarias\nif (typeof h3 === 'undefined') {\n node.error(\"H3 library not available\");\n return null;\n}\n\nconst payload = msg.payload;\nconst features = [];\n\n// --- FUNCIONES PARA ASIGNAR COLORES SEGÚN EL TOTAL ---\nfunction getFillColor(total) {\n if (total <= 10) return \"#00ff00\"; // verde\n if (total <= 50) return \"#ffff00\"; // amarillo\n if (total <= 100) return \"#ffa500\"; // naranja\n return \"#ff0000\"; // rojo\n}\n\nfunction getStrokeColor(total) {\n return \"#000000\"; // negro (puedes cambiarlo o hacerlo dinámico también)\n}\n\n// Recorrer cada elemento del array\npayload.forEach(item => {\n const h3Index = item.h3;\n const total = parseInt(item.total);\n\n try {\n // Obtener los vértices del hexágono H3\n const hexBoundary = h3.cellToBoundary(h3Index, true);\n\n // Obtener el centro del hexágono\n const center = h3.cellToLatLng(h3Index);\n\n // Colores dinámicos\n const fillColor = getFillColor(total);\n const strokeColor = getStrokeColor(total);\n\n // Crear feature GeoJSON con estilo\n const feature = {\n \"type\": \"Feature\",\n \"properties\": {\n \"h3\": h3Index,\n \"total\": total,\n \"value\": total,\n \"name\": `H3: ${h3Index} - Total: ${total}`,\n \"resolution\": h3.getResolution(h3Index),\n \"lat\": center[0],\n \"lon\": center[1],\n\n // ---- ESTILO AÑADIDO ----\n \"fill\": fillColor,\n \"fill-opacity\": 0.5, // ajustable\n \"stroke\": strokeColor,\n \"stroke-width\": 1, // ajustable\n \"stroke-opacity\": 0.9 // ajustable\n },\n \"geometry\": {\n \"type\": \"Polygon\",\n \"coordinates\": [hexBoundary]\n }\n };\n\n features.push(feature);\n\n } catch (error) {\n node.warn(`Error procesando H3 index ${h3Index}: ${error}`);\n }\n});\n\n// Crear el GeoJSON completo\nconst geojson = {\n \"type\": \"FeatureCollection\",\n \"features\": features,\n \"Layer\": \"H3 Layer\"\n};\n\nmsg.payload = geojson;\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
{
"var": "h3Js",
"module": "h3-js"
}
],
"x": 210,
"y": 760,
"wires": [
[]
]
},
{
"id": "e66c124d5be0c92e",
"type": "postgresql",
"z": "f511dd2230a153e7",
"name": "Consultar solo 500",
"query": "SELECT *\nFROM servicios_geo\nWHERE tipo_servicio != 'Base'\nLIMIT 500;",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 790,
"y": 340,
"wires": [
[
"95e107f031aca0fa"
]
]
},
{
"id": "b76d4632ea730780",
"type": "inject",
"z": "f511dd2230a153e7",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 610,
"y": 340,
"wires": [
[
"e66c124d5be0c92e"
]
]
},
{
"id": "95e107f031aca0fa",
"type": "debug",
"z": "f511dd2230a153e7",
"name": "debug 3",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 960,
"y": 340,
"wires": []
},
{
"id": "b2c5c01b68565205",
"type": "function",
"z": "f511dd2230a153e7",
"name": "Con colores por riesgo y demanda",
"func": "// NODE-RED FUNCTION: Visualizar H3 con predicciones y grúas\nconst h3 = h3Js; // Asegúrate de que h3Js está cargado globalmente\n\n// Validar librerías\nif (typeof h3 === 'undefined') {\n node.error(\"H3 library not available\");\n return null;\n}\n\nconst payload = msg.payload;\n\n// Extraer datos del payload de tu API\nconst week = payload.week || 0;\nconst dow = payload.dow || 0;\nconst hour = payload.hour || 0;\nconst recommendedH3 = payload.recommended_h3 || [];\nconst statistics = payload.statistics || [];\nconst coverage = payload.coverage || {};\nconst predictionData = payload.prediction_data || []; // Si tienes datos de predicción por celda\n\n// Si no hay datos de predicción, podemos generarlos a partir de statistics\nlet allH3Data = [];\nif (predictionData.length > 0) {\n // Si la API ya devuelve datos por celda\n allH3Data = predictionData;\n} else if (statistics.length > 0) {\n // Crear datos a partir de statistics (solo celdas con grúas)\n allH3Data = statistics.map(stat => ({\n h3: stat.h3,\n expected_demand: stat.expected_demand,\n nulo_probability: stat.nulo_probability,\n risk: stat.risk,\n coverage_radius: stat.coverage_radius,\n is_grua: true\n }));\n}\n\nconst features = [];\n\n// --- COLORES SEGÚN RIESGO ---\nfunction getRiskColor(risk, maxRisk) {\n if (maxRisk <= 0) return \"#00ff00\";\n\n const pct = (risk / maxRisk) * 100;\n\n if (pct <= 25) return \"#00ff00\"; // verde (bajo riesgo)\n if (pct <= 50) return \"#ffff00\"; // amarillo\n if (pct <= 75) return \"#ffa500\"; // naranja\n return \"#ff0000\"; // rojo (alto riesgo)\n}\n\n// --- COLORES SEGÚN DEMANDA ---\nfunction getDemandColor(demand) {\n if (demand < 2) return \"#00ff00\"; // verde (baja demanda)\n if (demand < 5) return \"#ffff00\"; // amarillo (media demanda)\n return \"#ff0000\"; // rojo (alta demanda)\n}\n\n// --- COLORES PARA GRÚAS ---\nfunction getGruaColor(index) {\n const colors = [\"#0000ff\", \"#ff00ff\", \"#00ffff\", \"#800080\"];\n return colors[index % colors.length];\n}\n\n// Calcular máximos para escalado de colores\nconst maxRisk = allH3Data.length > 0 ? Math.max(...allH3Data.map(i => parseFloat(i.risk || 0))) : 1;\nconst maxDemand = allH3Data.length > 0 ? Math.max(...allH3Data.map(i => parseFloat(i.expected_demand || 0))) : 1;\n\n// 1. Primero, pintar todas las celdas H3 con predicciones\nallH3Data.forEach((item, idx) => {\n const h3Index = item.h3;\n const risk = parseFloat(item.risk || 0);\n const demand = parseFloat(item.expected_demand || 0);\n const nuloProb = parseFloat(item.nulo_probability || 0);\n const isGrua = item.is_grua || false;\n const coverageRadius = item.coverage_radius || 0;\n\n try {\n const hexBoundary = h3.cellToBoundary(h3Index, true);\n const center = h3.cellToLatLng(h3Index);\n\n // Determinar color basado en riesgo\n const fillColor = getRiskColor(risk, maxRisk);\n\n // Opacidad diferente para grúas\n const fillOpacity = isGrua ? 0.8 : 0.4;\n\n // Ancho de borde diferente para grúas\n const strokeWidth = isGrua ? 3 : 1;\n const strokeColor = isGrua ? \"#000000\" : \"#333333\";\n\n // Texto para tooltip\n let tooltip = `H3: ${h3Index}`;\n tooltip += `\\\\nDemanda: ${demand.toFixed(2)}`;\n tooltip += `\\\\nProb. Nulo: ${(nuloProb * 100).toFixed(1)}%`;\n tooltip += `\\\\nRiesgo: ${risk.toFixed(3)}`;\n if (isGrua) {\n tooltip += `\\\\n🚨 GRÚA (Radio: ${coverageRadius})`;\n }\n\n const feature = {\n \"type\": \"Feature\",\n \"properties\": {\n \"h3\": h3Index,\n \"risk\": risk,\n \"demand\": demand,\n \"nulo_probability\": nuloProb,\n \"coverage_radius\": coverageRadius,\n \"is_grua\": isGrua,\n \"name\": tooltip,\n \"lat\": center[0],\n \"lon\": center[1],\n\n // ESTILO\n \"fill\": fillColor,\n \"fill-opacity\": fillOpacity,\n \"stroke\": strokeColor,\n \"stroke-width\": strokeWidth,\n \"stroke-opacity\": 0.9,\n\n // Metadata adicional\n \"marker-type\": isGrua ? \"grua\" : \"prediction\"\n },\n \"geometry\": {\n \"type\": \"Polygon\",\n \"coordinates\": [hexBoundary]\n }\n };\n\n features.push(feature);\n\n } catch (error) {\n node.warn(`Error procesando H3 index ${h3Index}: ${error}`);\n }\n});\n\n// 2. Añadir áreas de cobertura de las grúas (círculos)\nstatistics.forEach((grua, gruaIndex) => {\n try {\n const h3Index = grua.h3;\n const radius = grua.coverage_radius || 12;\n const cellsCovered = grua.cells_covered || 0;\n\n // Obtener celdas en el radio de cobertura\n const coveredCells = h3.gridDisk(h3Index, radius);\n\n // Crear polígono para el área de cobertura (opcional, puede ser pesado)\n // En su lugar, podemos crear un círculo aproximado\n\n const center = h3.cellToLatLng(h3Index);\n\n // Crear feature para el área de cobertura\n const coverageFeature = {\n \"type\": \"Feature\",\n \"properties\": {\n \"name\": `Área cobertura Grúa ${gruaIndex + 1}`,\n \"grua_h3\": h3Index,\n \"radius\": radius,\n \"cells_covered\": cellsCovered,\n \"risk_covered\": grua.risk_covered || 0,\n \"lat\": center[0],\n \"lon\": center[1],\n\n // ESTILO (semi-transparente)\n \"fill\": getGruaColor(gruaIndex),\n \"fill-opacity\": 0.2,\n \"stroke\": getGruaColor(gruaIndex),\n \"stroke-width\": 2,\n \"stroke-opacity\": 0.5,\n \"marker-type\": \"coverage_area\"\n },\n \"geometry\": {\n \"type\": \"Point\",\n \"coordinates\": [center[1], center[0]]\n }\n };\n\n features.push(coverageFeature);\n\n // Añadir marcador para la grúa\n const gruaMarker = {\n \"type\": \"Feature\",\n \"properties\": {\n \"name\": `🚨 Grúa ${gruaIndex + 1}`,\n \"description\": `Demanda: ${grua.expected_demand?.toFixed(2) || 0}\\\\nRadio: ${radius}\\\\nCeldas cubiertas: ${cellsCovered}`,\n \"grua_index\": gruaIndex + 1,\n \"lat\": center[0],\n \"lon\": center[1],\n\n // ESTILO para marcador\n \"marker-color\": getGruaColor(gruaIndex),\n \"marker-size\": \"large\",\n \"marker-symbol\": \"warehouse\",\n \"marker-type\": \"grua_marker\"\n },\n \"geometry\": {\n \"type\": \"Point\",\n \"coordinates\": [center[1], center[0]]\n }\n };\n\n features.push(gruaMarker);\n\n } catch (error) {\n node.warn(`Error procesando área de cobertura para grúa ${grua.h3}: ${error}`);\n }\n});\n\n// 3. Añadir información general como feature\nconst infoFeature = {\n \"type\": \"Feature\",\n \"properties\": {\n \"name\": \"Información del Análisis\",\n \"description\": `Semana ${week}, Día ${dow}, Hora ${hour}\\\\n` +\n `Cobertura: ${coverage.coverage_percentage?.toFixed(2) || 0}%\\\\n` +\n `Riesgo cubierto: ${coverage.risk_coverage_percentage?.toFixed(2) || 0}%\\\\n` +\n `Grúas recomendadas: ${recommendedH3.length}`,\n \"marker-type\": \"info\",\n \"marker-color\": \"#333333\",\n \"marker-symbol\": \"info\"\n },\n \"geometry\": {\n \"type\": \"Point\",\n \"coordinates\": [-3.7038, 40.4168] // Madrid como referencia\n }\n};\n\nfeatures.push(infoFeature);\n\n// FeatureCollection final\nmsg.payload = {\n \"type\": \"FeatureCollection\",\n \"features\": features,\n \"metadata\": {\n \"week\": week,\n \"dow\": dow,\n \"hour\": hour,\n \"total_gruas\": recommendedH3.length,\n \"coverage_percentage\": coverage.coverage_percentage || 0,\n \"risk_coverage_percentage\": coverage.risk_coverage_percentage || 0,\n \"total_cells\": coverage.total_cells || 0,\n \"cells_covered\": coverage.cells_covered || 0\n },\n \"Layer\": \"H3 Predictions & Gruas\"\n};\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
{
"var": "h3Js",
"module": "h3-js"
}
],
"x": 340,
"y": 960,
"wires": [
[
"ac4ac0ee965cef89",
"572948d3a25d485f"
]
]
},
{
"id": "ac4ac0ee965cef89",
"type": "worldmap",
"z": "f511dd2230a153e7",
"name": "",
"lat": "",
"lon": "",
"zoom": "",
"layer": "",
"cluster": "",
"maxage": "",
"usermenu": "show",
"layers": "show",
"panit": "false",
"panlock": "false",
"zoomlock": "false",
"hiderightclick": "false",
"coords": "false",
"showgrid": "false",
"showruler": "false",
"allowFileDrop": "false",
"path": "/worldmap",
"overlist": "DR,CO,RA,DN",
"maplist": "OSMG,OSMC,EsriC,EsriS,UKOS",
"mapname": "",
"mapurl": "",
"mapopt": "",
"mapwms": false,
"x": 640,
"y": 960,
"wires": []
},
{
"id": "32e2494f462cc6f4",
"type": "link in",
"z": "f511dd2230a153e7",
"name": "link in 1",
"links": [
"b63c955f07e230f1",
"c1552a91a92da199"
],
"x": 125,
"y": 1080,
"wires": [
[
"572948d3a25d485f",
"4bbf565e7701cb10"
]
]
},
{
"id": "2fc1745ca3803fcb",
"type": "function",
"z": "f511dd2230a153e7",
"name": "Solo ver las gruas",
"func": "// VERSIÓN MÍNIMA - Solo muestra las grúas\nconst h3 = h3Js;\nconst payload = msg.payload;\nconst recommendedH3 = payload.recommended_h3 || [];\n\nconst features = recommendedH3.map((h3Index, index) => {\n try {\n const center = h3.cellToLatLng(h3Index);\n return {\n \"type\": \"Feature\",\n \"properties\": {\n \"name\": `Grúa ${index + 1}: ${h3Index}`,\n \"h3\": h3Index,\n },\n \"geometry\": {\n \"type\": \"Point\",\n \"coordinates\": [center[1], center[0]]\n }\n };\n } catch (e) {\n return null;\n }\n}).filter(f => f !== null);\n\nmsg.payload = {\n \"type\": \"FeatureCollection\",\n \"features\": features,\n \"icon\": \"truck\",\n \"layer\": \"H3 Gruas\"\n};\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
{
"var": "h3Js",
"module": "h3-js"
}
],
"x": 290,
"y": 920,
"wires": [
[
"ac4ac0ee965cef89",
"572948d3a25d485f"
]
]
},
{
"id": "572948d3a25d485f",
"type": "debug",
"z": "f511dd2230a153e7",
"name": "debug 17",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 640,
"y": 1080,
"wires": []
},
{
"id": "19a26466059a3db1",
"type": "function",
"z": "f511dd2230a153e7",
"name": "Con colores con probabilidad",
"func": "// NODE-RED FUNCTION: Visualizar H3 con predicciones y grúas\nconst h3 = h3Js; // Asegúrate de que h3Js está cargado globalmente\n\n// Validar librerías\nif (typeof h3 === 'undefined') {\n node.error(\"H3 library not available\");\n return null;\n}\n\nconst payload = msg.payload;\n\n// Extraer datos del payload de tu API\nconst week = payload.week || 0;\nconst dow = payload.dow || 0;\nconst hour = payload.hour || 0;\nconst recommendedH3 = payload.recommended_h3 || [];\nconst statistics = payload.statistics || [];\nconst coverage = payload.coverage || {};\nconst predictionData = payload.prediction_data || []; // Si tienes datos de predicción por celda\n\n// Si no hay datos de predicción, podemos generarlos a partir de statistics\nlet allH3Data = [];\nif (predictionData.length > 0) {\n // Si la API ya devuelve datos por celda\n allH3Data = predictionData;\n} else if (statistics.length > 0) {\n // Crear datos a partir de statistics (solo celdas con grúas)\n allH3Data = statistics.map(stat => ({\n h3: stat.h3,\n expected_demand: stat.expected_demand,\n nulo_probability: stat.nulo_probability,\n risk: stat.risk,\n coverage_radius: stat.coverage_radius,\n is_grua: true\n }));\n}\n\nconst features = [];\n\n// --- COLORES SEGÚN PROBABILIDAD DE NULO ---\nfunction getNuloColor(nuloProbability) {\n // Convertir a porcentaje (si viene como decimal)\n const pct = nuloProbability <= 1 ? nuloProbability * 100 : nuloProbability;\n\n if (pct < 25) return \"#00ff00\"; // verde (baja probabilidad de nulo)\n if (pct < 50) return \"#ffff00\"; // amarillo\n if (pct < 75) return \"#ffa500\"; // naranja\n return \"#ff0000\"; // rojo (alta probabilidad de nulo)\n}\n\n// --- COLORES PARA GRÚAS ---\nfunction getGruaColor(index) {\n const colors = [\"#0000ff\", \"#ff00ff\", \"#00ffff\", \"#800080\"];\n return colors[index % colors.length];\n}\n\n// 1. Primero, pintar todas las celdas H3 con predicciones\nallH3Data.forEach((item, idx) => {\n const h3Index = item.h3;\n const risk = parseFloat(item.risk || 0);\n const demand = parseFloat(item.expected_demand || 0);\n const nuloProb = parseFloat(item.nulo_probability || 0);\n const isGrua = item.is_grua || false;\n const coverageRadius = item.coverage_radius || 0;\n\n try {\n const hexBoundary = h3.cellToBoundary(h3Index, true);\n const center = h3.cellToLatLng(h3Index);\n\n // Determinar color basado en probabilidad de nulo\n const fillColor = getNuloColor(nuloProb);\n\n // Opacidad diferente para grúas\n const fillOpacity = isGrua ? 0.8 : 0.4;\n\n // Ancho de borde diferente para grúas\n const strokeWidth = isGrua ? 3 : 1;\n const strokeColor = isGrua ? \"#000000\" : \"#333333\";\n\n // Texto para tooltip\n let tooltip = `H3: ${h3Index}`;\n tooltip += `\\\\nDemanda: ${demand.toFixed(2)}`;\n tooltip += `\\\\nProb. Nulo: ${(nuloProb * 100).toFixed(1)}%`;\n tooltip += `\\\\nRiesgo: ${risk.toFixed(3)}`;\n if (isGrua) {\n tooltip += `\\\\n🚨 GRÚA (Radio: ${coverageRadius})`;\n }\n\n const feature = {\n \"type\": \"Feature\",\n \"properties\": {\n \"h3\": h3Index,\n \"risk\": risk,\n \"demand\": demand,\n \"nulo_probability\": nuloProb,\n \"coverage_radius\": coverageRadius,\n \"is_grua\": isGrua,\n \"name\": tooltip,\n \"lat\": center[0],\n \"lon\": center[1],\n\n // ESTILO\n \"fill\": fillColor,\n \"fill-opacity\": fillOpacity,\n \"stroke\": strokeColor,\n \"stroke-width\": strokeWidth,\n \"stroke-opacity\": 0.9,\n\n // Metadata adicional\n \"marker-type\": isGrua ? \"grua\" : \"prediction\",\n // Añadir nivel de color para referencia\n \"nulo_level\": nuloProb <= 0.25 ? \"bajo\" :\n nuloProb <= 0.5 ? \"medio\" :\n nuloProb <= 0.75 ? \"alto\" : \"muy_alto\"\n },\n \"geometry\": {\n \"type\": \"Polygon\",\n \"coordinates\": [hexBoundary]\n }\n };\n\n features.push(feature);\n\n } catch (error) {\n node.warn(`Error procesando H3 index ${h3Index}: ${error}`);\n }\n});\n\n// 2. Añadir áreas de cobertura de las grúas (círculos)\nstatistics.forEach((grua, gruaIndex) => {\n try {\n const h3Index = grua.h3;\n const radius = grua.coverage_radius || 12;\n const cellsCovered = grua.cells_covered || 0;\n\n // Obtener celdas en el radio de cobertura\n const coveredCells = h3.gridDisk(h3Index, radius);\n\n // Crear polígono para el área de cobertura (opcional, puede ser pesado)\n // En su lugar, podemos crear un círculo aproximado\n\n const center = h3.cellToLatLng(h3Index);\n\n // Crear feature para el área de cobertura\n const coverageFeature = {\n \"type\": \"Feature\",\n \"properties\": {\n \"name\": `Área cobertura Grúa ${gruaIndex + 1}`,\n \"grua_h3\": h3Index,\n \"radius\": radius,\n \"cells_covered\": cellsCovered,\n \"risk_covered\": grua.risk_covered || 0,\n \"lat\": center[0],\n \"lon\": center[1],\n\n // ESTILO (semi-transparente)\n \"fill\": getGruaColor(gruaIndex),\n \"fill-opacity\": 0.2,\n \"stroke\": getGruaColor(gruaIndex),\n \"stroke-width\": 2,\n \"stroke-opacity\": 0.5,\n \"marker-type\": \"coverage_area\"\n },\n \"geometry\": {\n \"type\": \"Point\",\n \"coordinates\": [center[1], center[0]]\n }\n };\n\n features.push(coverageFeature);\n\n // Añadir marcador para la grúa\n const gruaMarker = {\n \"type\": \"Feature\",\n \"properties\": {\n \"name\": `🚨 Grúa ${gruaIndex + 1}`,\n \"description\": `Demanda: ${grua.expected_demand?.toFixed(2) || 0}\\\\nProb. Nulo: ${(grua.nulo_probability * 100).toFixed(1)}%\\\\nRadio: ${radius}\\\\nCeldas cubiertas: ${cellsCovered}`,\n \"grua_index\": gruaIndex + 1,\n \"lat\": center[0],\n \"lon\": center[1],\n\n // ESTILO para marcador\n \"marker-color\": getGruaColor(gruaIndex),\n \"marker-size\": \"large\",\n \"marker-symbol\": \"warehouse\",\n \"marker-type\": \"grua_marker\"\n },\n \"geometry\": {\n \"type\": \"Point\",\n \"coordinates\": [center[1], center[0]]\n }\n };\n\n features.push(gruaMarker);\n\n } catch (error) {\n node.warn(`Error procesando área de cobertura para grúa ${grua.h3}: ${error}`);\n }\n});\n\n// 3. Añadir información general como feature\nconst infoFeature = {\n \"type\": \"Feature\",\n \"properties\": {\n \"name\": \"Información del Análisis\",\n \"description\": `Semana ${week}, Día ${dow}, Hora ${hour}\\\\n` +\n `Cobertura: ${coverage.coverage_percentage?.toFixed(2) || 0}%\\\\n` +\n `Riesgo cubierto: ${coverage.risk_coverage_percentage?.toFixed(2) || 0}%\\\\n` +\n `Grúas recomendadas: ${recommendedH3.length}`,\n \"marker-type\": \"info\",\n \"marker-color\": \"#333333\",\n \"marker-symbol\": \"info\"\n },\n \"geometry\": {\n \"type\": \"Point\",\n \"coordinates\": [-3.7038, 40.4168] // Madrid como referencia\n }\n};\n\nfeatures.push(infoFeature);\n\n// FeatureCollection final\nmsg.payload = {\n \"type\": \"FeatureCollection\",\n \"features\": features,\n \"metadata\": {\n \"week\": week,\n \"dow\": dow,\n \"hour\": hour,\n \"total_gruas\": recommendedH3.length,\n \"coverage_percentage\": coverage.coverage_percentage || 0,\n \"risk_coverage_percentage\": coverage.risk_coverage_percentage || 0,\n \"total_cells\": coverage.total_cells || 0,\n \"cells_covered\": coverage.cells_covered || 0,\n \"color_schema\": \"nulo_probability\" // Indicar qué esquema de color se usa\n },\n \"Layer\": \"H3 Predictions & Gruas\"\n};\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
{
"var": "h3Js",
"module": "h3-js"
}
],
"x": 320,
"y": 1000,
"wires": [
[
"ac4ac0ee965cef89",
"572948d3a25d485f"
]
]
},
{
"id": "4bbf565e7701cb10",
"type": "function",
"z": "f511dd2230a153e7",
"name": "Con colores con probabilidad",
"func": "// NODE-RED FUNCTION: Visualizar H3 con predicciones y grúas\nconst h3 = h3Js; // Asegúrate de que h3Js está cargado globalmente\n\n// Validar librerías\nif (typeof h3 === 'undefined') {\n node.error(\"H3 library not available\");\n return null;\n}\n\nconst payload = msg.payload;\n\n// Extraer datos del payload de tu API\nconst week = payload.semana || 0;\nconst dow = payload.dia_semana || 0;\nconst hour = payload.hora || 0;\nconst recommendedH3 = payload.h3_recomendados || [];\nconst statistics = payload.estadisticas || [];\nconst coverage = payload.cobertura || {};\nconst predictionData = payload.datos_prediccion || []; // Si tienes datos de predicción por celda\n\n// Si no hay datos de predicción, podemos generarlos a partir de statistics\nlet allH3Data = [];\nif (predictionData.length > 0) {\n // Si la API ya devuelve datos por celda\n allH3Data = predictionData;\n} else if (statistics.length > 0) {\n // Crear datos a partir de statistics (solo celdas con grúas)\n allH3Data = statistics.map(stat => ({\n h3: stat.h3,\n expected_demand: stat.expected_demand,\n nulo_probability: stat.nulo_probability,\n risk: stat.risk,\n coverage_radius: stat.coverage_radius,\n is_grua: true\n }));\n}\n\nconst features = [];\n\n// --- FUNCIÓN PARA GRADIENTE DE COLOR SEGÚN PROBABILIDAD DE NULO ---\nfunction getNuloGradientColor(nuloProbability) {\n // Asegurar que el valor esté entre 0 y 1\n let pct = Math.max(0, Math.min(1, nuloProbability));\n\n // Gradiente de verde (0) a rojo (1)\n const red = Math.floor(255 * pct); // Aumenta con la probabilidad\n const green = Math.floor(255 * (1 - pct)); // Disminuye con la probabilidad\n const blue = 0; // Sin componente azul\n\n return `rgb(${red}, ${green}, ${blue})`;\n}\n\n// --- FUNCIÓN PARA CREAR CÍRCULO GEOJSON ---\nfunction createCircle(center, radiusInKm, points = 32) {\n const coords = [];\n const [lat, lng] = center;\n\n // Radio de la Tierra en kilómetros\n const earthRadius = 6371;\n\n // Convertir radio de kilómetros a radianes\n const radiusRad = radiusInKm / earthRadius;\n\n for (let i = 0; i < points; i++) {\n const angle = (i * 2 * Math.PI) / points;\n\n // Coordenadas del punto en el círculo\n const circleLat = Math.asin(\n Math.sin(lat * Math.PI / 180) * Math.cos(radiusRad) +\n Math.cos(lat * Math.PI / 180) * Math.sin(radiusRad) * Math.cos(angle)\n ) * 180 / Math.PI;\n\n const circleLng = lng + Math.atan2(\n Math.sin(angle) * Math.sin(radiusRad) * Math.cos(lat * Math.PI / 180),\n Math.cos(radiusRad) - Math.sin(lat * Math.PI / 180) * Math.sin(circleLat * Math.PI / 180)\n ) * 180 / Math.PI;\n\n coords.push([circleLng, circleLat]);\n }\n\n // Cerrar el polígono\n coords.push(coords[0]);\n\n return coords;\n}\n\n// --- COLORES PARA GRÚAS ---\nfunction getGruaColor(index) {\n const colors = [\"#0000ff\", \"#ff00ff\", \"#00ffff\", \"#800080\", \"#008000\", \"#800000\"];\n return colors[index % colors.length];\n}\n\n// 1. Primero, pintar todas las celdas H3 con predicciones\nallH3Data.forEach((item, idx) => {\n const h3Index = item.h3;\n const risk = parseFloat(item.risk || 0);\n const demand = parseFloat(item.expected_demand || 0);\n const nuloProb = parseFloat(item.nulo_probability || 0);\n const isGrua = item.is_grua || false;\n const coverageRadius = item.coverage_radius || 0;\n\n try {\n const hexBoundary = h3.cellToBoundary(h3Index, true);\n const center = h3.cellToLatLng(h3Index);\n\n // Determinar color con gradiente basado en probabilidad de nulo\n const fillColor = getNuloGradientColor(nuloProb);\n\n // Opacidad variable según probabilidad de nulo (más opaco para alta probabilidad)\n const fillOpacity = isGrua ? 0.8 : (0.3 + (nuloProb * 0.5));\n\n // Ancho de borde diferente para grúas\n const strokeWidth = isGrua ? 3 : 1;\n const strokeColor = isGrua ? \"#000000\" :\n nuloProb > 0.5 ? \"#222222\" : \"#555555\";\n\n // Texto para tooltip\n let tooltip = `H3: ${h3Index}`;\n tooltip += ` Demanda: ${demand.toFixed(2)}`;\n tooltip += ` Prob. Nulo: ${(nuloProb * 100).toFixed(1)}%`;\n tooltip += ` Riesgo: ${risk.toFixed(3)}`;\n if (isGrua) {\n tooltip += `\\\\n🚨 GRÚA (Radio: ${coverageRadius} celdas)`;\n }\n\n const feature = {\n \"type\": \"Feature\",\n \"properties\": {\n \"h3\": h3Index,\n \"risk\": risk,\n \"demand\": demand,\n \"nulo_probability\": nuloProb,\n \"nulo_percentage\": (nuloProb * 100).toFixed(1),\n \"coverage_radius\": coverageRadius,\n \"is_grua\": isGrua,\n \"name\": tooltip,\n \"lat\": center[0],\n \"lon\": center[1],\n\n // ESTILO\n \"fill\": fillColor,\n \"fill-opacity\": fillOpacity,\n \"stroke\": strokeColor,\n \"stroke-width\": strokeWidth,\n \"stroke-opacity\": 0.9,\n\n // Metadata adicional para leyenda\n \"marker-type\": isGrua ? \"grua\" : \"prediction\",\n \"color_intensity\": nuloProb,\n \"color_category\": nuloProb < 0.25 ? \"bajo\" :\n nuloProb < 0.5 ? \"medio-bajo\" :\n nuloProb < 0.75 ? \"medio-alto\" : \"alto\"\n },\n \"geometry\": {\n \"type\": \"Polygon\",\n \"coordinates\": [hexBoundary]\n }\n };\n\n features.push(feature);\n\n } catch (error) {\n node.warn(`Error procesando H3 index ${h3Index}: ${error}`);\n }\n});\n\n// 2. Añadir áreas de cobertura de las grúas como círculos\nstatistics.forEach((grua, gruaIndex) => {\n try {\n const h3Index = grua.h3;\n const radius = grua.coverage_radius || 12;\n const cellsCovered = grua.cells_covered || 0;\n const nuloProb = parseFloat(grua.nulo_probability || 0);\n\n const center = h3.cellToLatLng(h3Index);\n\n // Calcular radio aproximado en kilómetros\n // Nota: El radio en H3 es en número de celdas, no en km\n // Para convertir a km, necesitamos la resolución de las celdas\n // Asumimos resolución 9 (aprox 0.1 km² por celda)\n const cellAreaKm2 = 0.2; // Área aproximada de celda H3 res 9 en km²\n const cellDiameterKm = Math.sqrt(cellAreaKm2) * 2; // Diámetro aproximado en km\n const radiusKm = radius * cellDiameterKm;\n\n // Crear círculo para el área de cobertura\n const circleCoords = createCircle(center, radiusKm, 64);\n\n // Color para esta grúa\n const gruaColor = getGruaColor(gruaIndex);\n\n // Crear feature para el área de cobertura (círculo)\n const coverageFeature = {\n \"type\": \"Feature\",\n \"properties\": {\n \"name\": `Área cobertura Grúa ${gruaIndex + 1}`,\n \"description\": `Radio: ${radius}
celdas (≈${radiusKm.toFixed(1)} km)
` +\n `Celdas cubiertas: ${cellsCovered}
` +\n `Prob. Nulo: ${(nuloProb * 100).toFixed(1)}%`,\n \"grua_h3\": h3Index,\n \"grua_index\": gruaIndex + 1,\n \"radius_cells\": radius,\n \"radius_km\": radiusKm.toFixed(1),\n \"cells_covered\": cellsCovered,\n \"risk_covered\": grua.risk_covered || 0,\n \"lat\": center[0],\n \"lon\": center[1],\n \"grua_color\": gruaColor,\n\n // ESTILO (semi-transparente)\n \"fill\": gruaColor,\n \"fill-opacity\": 0.15,\n \"stroke\": gruaColor,\n \"stroke-width\": 3,\n \"stroke-opacity\": 0.6,\n \"stroke-dasharray\": \"5,5\", // Línea punteada para mejor visibilidad\n \"marker-type\": \"coverage_area\"\n },\n \"geometry\": {\n \"type\": \"Polygon\",\n \"coordinates\": [circleCoords]\n }\n };\n\n features.push(coverageFeature);\n\n // Añadir marcador para la grúa en el centro\n const gruaMarker = {\n \"type\": \"Feature\",\n \"properties\": {\n \"name\": `🚨 Grúa ${gruaIndex + 1}`,\n \"description\": `
Prob. Nulo: ${(nuloProb * 100).toFixed(1)}%
` +\n `Demanda: ${grua.expected_demand?.toFixed(2) || 0}
` +\n `Radio: ${radius} celdas (≈${radiusKm.toFixed(1)} km)
` +\n `Celdas cubiertas: ${cellsCovered}`,\n \"grua_index\": gruaIndex + 1,\n \"grua_color\": gruaColor,\n \"lat\": center[0],\n \"lon\": center[1],\n\n // ESTILO para marcador\n \"marker-color\": gruaColor,\n \"marker-size\": \"large\",\n \"marker-symbol\": \"truck\",\n \"marker-type\": \"grua_center\"\n },\n \"geometry\": {\n \"type\": \"Point\",\n \"coordinates\": [center[1], center[0]]\n }\n };\n\n features.push(gruaMarker);\n\n } catch (error) {\n node.warn(`Error procesando área de cobertura para grúa ${grua.h3}: ${error}`);\n }\n});\n\n// 3. Añadir información general como feature\nconst infoFeature = {\n \"type\": \"Feature\",\n \"properties\": {\n \"name\": \"Información del Análisis\",\n \"description\": `Semana: ${week} Día: ${dow} Hora: ${hour}
` +\n `Cobertura: ${coverage.coverage_percentage?.toFixed(2) || 0}%
` +\n `Riesgo cubierto: ${coverage.risk_coverage_percentage?.toFixed(2) || 0}%
` +\n `Grúas recomendadas: ${recommendedH3.length}
` +\n `🎨 Colores: Gradiente por probabilidad de nulo
` +\n `⭕ Círculos: Áreas de cobertura de grúas`,\n \"marker-type\": \"info\",\n \"marker-color\": \"#333333\",\n \"marker-symbol\": \"info\"\n },\n \"geometry\": {\n \"type\": \"Point\",\n \"coordinates\": [-3.7038, 40.4168] // Madrid como referencia\n }\n};\n\nfeatures.push(infoFeature);\n\n// FeatureCollection final\nmsg.payload = {\n \"type\": \"FeatureCollection\",\n \"features\": features,\n \"metadata\": {\n \"week\": week,\n \"dow\": dow,\n \"hour\": hour,\n \"total_gruas\": recommendedH3.length,\n \"coverage_percentage\": coverage.coverage_percentage || 0,\n \"risk_coverage_percentage\": coverage.risk_coverage_percentage || 0,\n \"total_cells\": coverage.total_cells || 0,\n \"cells_covered\": coverage.cells_covered || 0,\n \"color_schema\": \"nulo_probability_gradient\",\n \"gradient_min\": \"#00ff00\", // Verde\n \"gradient_max\": \"#ff0000\", // Rojo\n \"color_explanation\": \"Verde (0% nulo) → Rojo (100% nulo)\"\n },\n \"layer\": \"H3\"\n};\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [
{
"var": "h3Js",
"module": "h3-js"
}
],
"x": 320,
"y": 1040,
"wires": [
[
"572948d3a25d485f",
"ac4ac0ee965cef89"
]
]
},
{
"id": "c7c7d9aedc600352",
"type": "postgresql",
"z": "230815bb0628a63e",
"name": "Generamos la vista básica de H3 con 8",
"query": "-- 1.1 Vista básica con h3 (nivel 8)\nCREATE OR REPLACE VIEW vw_servicios_h3 AS\nSELECT\n id,\n id_servicio,\n fecha_entrada,\n fecha_servicio,\n codigo_grua,\n tipo_servicio,\n geom,\n h3_lat_lng_to_cell(point(ST_Y(geom), ST_X(geom)), 8) AS h3,\n EXTRACT(HOUR FROM fecha_servicio AT TIME ZONE 'UTC')::int AS hour,\n EXTRACT(DOW FROM fecha_servicio AT TIME ZONE 'UTC')::int AS dow, -- 0 = Domingo\n EXTRACT(WEEK FROM fecha_servicio AT TIME ZONE 'UTC')::int AS week,\n CASE WHEN tipo_servicio = 'Requerimiento nulo' THEN 1 ELSE 0 END AS is_nulo\nFROM servicios_geo;",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 360,
"y": 120,
"wires": [
[
"eab6c7a9df1e6c8c"
]
]
},
{
"id": "e65ea3eb95cf6c9f",
"type": "inject",
"z": "230815bb0628a63e",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 120,
"wires": [
[
"c7c7d9aedc600352"
]
]
},
{
"id": "eab6c7a9df1e6c8c",
"type": "debug",
"z": "230815bb0628a63e",
"name": "debug 4",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 940,
"y": 120,
"wires": []
},
{
"id": "a1c78ddc8ef1f1fd",
"type": "postgresql",
"z": "230815bb0628a63e",
"name": "Generamos el nivel de demanda por hora y día de la semana modelo SQL",
"query": "-- Conteo por (dia de la semana y hora)\nCREATE OR REPLACE VIEW vw_total_by_hour_dow AS\nSELECT\n dow,\n hour,\n COUNT(*) AS total_hour_dow\nFROM vw_servicios_h3\nGROUP BY dow, hour\nORDER BY dow, hour;\n\n--- Calcular valores para día de la semana y hora, asignamos un demand_level\nCREATE OR REPLACE VIEW vw_hour_levels_dow AS\nWITH counts AS (\n SELECT dow, hour, total_hour_dow\n FROM vw_total_by_hour_dow\n),\npercentiles_per_dow AS (\n SELECT\n dow,\n percentile_cont(0.3333) WITHIN GROUP (ORDER BY total_hour_dow) AS q1,\n percentile_cont(0.6666) WITHIN GROUP (ORDER BY total_hour_dow) AS q2\n FROM counts\n GROUP BY dow\n)\nSELECT\n c.dow,\n c.hour,\n c.total_hour_dow,\n CASE\n WHEN c.total_hour_dow <= p.q1 THEN 'baja'\n WHEN c.total_hour_dow <= p.q2 THEN 'media'\n ELSE 'alta'\n END AS demand_level\nFROM counts c\nJOIN percentiles_per_dow p ON p.dow = c.dow\nORDER BY c.dow, c.hour;",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 460,
"y": 180,
"wires": [
[
"e9272483eeab8c19"
]
]
},
{
"id": "e9272483eeab8c19",
"type": "debug",
"z": "230815bb0628a63e",
"name": "debug 5",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 940,
"y": 180,
"wires": []
},
{
"id": "0bec605824bbb7ea",
"type": "inject",
"z": "230815bb0628a63e",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 180,
"wires": [
[
"a1c78ddc8ef1f1fd"
]
]
},
{
"id": "6b5cc73d3ff597e3",
"type": "postgresql",
"z": "230815bb0628a63e",
"name": "Generamos la vista de servicios etiquetados por demanda según el horario",
"query": "CREATE OR REPLACE VIEW vw_servicios_labeled AS\nSELECT s.*,\n hl.demand_level\nFROM vw_servicios_h3 s\nLEFT JOIN vw_hour_levels_dow hl\n ON s.dow = hl.dow AND s.hour = hl.hour;",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 470,
"y": 240,
"wires": [
[
"ae7888f46928631e"
]
]
},
{
"id": "b2f0b35e19e565c8",
"type": "postgresql",
"z": "230815bb0628a63e",
"name": "Generamos la vista de demanda_h3_hour",
"query": "--- Generamos la vista de la demanda h3 por hora modelo sql\nCREATE OR REPLACE VIEW demanda_h3_hour AS\nSELECT\n s.h3,\n s.hour,\n s.dow,\n hl.demand_level,\n COUNT(*)::int AS total_events,\n SUM(s.is_nulo)::int AS total_nulos,\n CASE WHEN COUNT(*)=0 THEN 0.0 ELSE SUM(s.is_nulo)::double precision / COUNT(*) END AS nulo_rate\nFROM vw_servicios_labeled s\nLEFT JOIN vw_hour_levels_dow hl\n ON hl.dow = s.dow\n AND hl.hour = s.hour\nGROUP BY s.h3, s.hour, s.dow, hl.demand_level;",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 360,
"y": 300,
"wires": [
[
"b7999a625ea2a4f7"
]
]
},
{
"id": "94425a5d1997a6de",
"type": "postgresql",
"z": "230815bb0628a63e",
"name": "Verificaciones",
"query": "--- revision de valores\nSELECT * FROM vw_hour_levels_dow ORDER BY dow, hour;\n--- por servicio tiene que tener un nivel de demanda\nSELECT * FROM vw_servicios_labeled LIMIT 20;\n--- la agregación de los valores es correcta\nSELECT * FROM demanda_h3_hour ORDER BY total_events DESC LIMIT 20;",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 280,
"y": 360,
"wires": [
[
"f11aaa2122049f7e"
]
]
},
{
"id": "6181e6dc62c93438",
"type": "inject",
"z": "230815bb0628a63e",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 240,
"wires": [
[
"6b5cc73d3ff597e3"
]
]
},
{
"id": "01d5f95df74d1a4b",
"type": "inject",
"z": "230815bb0628a63e",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 300,
"wires": [
[
"b2f0b35e19e565c8"
]
]
},
{
"id": "1c5af8b39a382a49",
"type": "inject",
"z": "230815bb0628a63e",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 360,
"wires": [
[
"94425a5d1997a6de"
]
]
},
{
"id": "ae7888f46928631e",
"type": "debug",
"z": "230815bb0628a63e",
"name": "debug 6",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 940,
"y": 240,
"wires": []
},
{
"id": "b7999a625ea2a4f7",
"type": "debug",
"z": "230815bb0628a63e",
"name": "debug 7",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 940,
"y": 300,
"wires": []
},
{
"id": "f11aaa2122049f7e",
"type": "debug",
"z": "230815bb0628a63e",
"name": "debug 8",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 940,
"y": 360,
"wires": []
},
{
"id": "1a960e62640178c4",
"type": "inject",
"z": "230815bb0628a63e",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 480,
"wires": [
[
"239bb9ceb6c8f1a2"
]
]
},
{
"id": "239bb9ceb6c8f1a2",
"type": "http request",
"z": "230815bb0628a63e",
"name": "",
"method": "GET",
"ret": "txt",
"paytoqs": "ignore",
"url": "http://10.10.14.70:5000/predict?hour=11&dow=1&total_events=3",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 270,
"y": 480,
"wires": [
[
"3bbe9a72ae47d0d3"
]
]
},
{
"id": "3bbe9a72ae47d0d3",
"type": "debug",
"z": "230815bb0628a63e",
"name": "debug 9",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 440,
"y": 480,
"wires": []
},
{
"id": "df2b3c5fccde910c",
"type": "postgresql",
"z": "230815bb0628a63e",
"name": "Generamos la vista de demanda_h3_hour_ml",
"query": "--- Generamos la vista de la demanda h3 por hora modelo ml\nCREATE OR REPLACE VIEW demanda_h3_hour_ml AS\nSELECT\n h3,\n week,\n dow,\n hour,\n COUNT(*)::int AS total_events,\n SUM(is_nulo)::int AS total_nulos,\n CASE\n WHEN COUNT(*) = 0 THEN 0\n ELSE SUM(is_nulo)::float / COUNT(*)\n END AS nulo_rate\nFROM vw_servicios_h3\nGROUP BY h3, week, dow, hour;",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 380,
"y": 420,
"wires": [
[
"4b916996a32da5f8"
]
]
},
{
"id": "010bdecaa9c98232",
"type": "inject",
"z": "230815bb0628a63e",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 420,
"wires": [
[
"df2b3c5fccde910c"
]
]
},
{
"id": "4b916996a32da5f8",
"type": "debug",
"z": "230815bb0628a63e",
"name": "debug 10",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 940,
"y": 420,
"wires": []
},
{
"id": "29d73c2064e1eba6",
"type": "inject",
"z": "230815bb0628a63e",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 60,
"wires": [
[
"f56d3d761eefad37"
]
]
},
{
"id": "f56d3d761eefad37",
"type": "postgresql",
"z": "230815bb0628a63e",
"name": "Limpieza de la vistas",
"query": "DROP VIEW IF EXISTS vw_servicios_h3 CASCADE;",
"postgreSQLConfig": "748b5921246ec468",
"split": false,
"rowsPerMsg": 1,
"outputs": 1,
"x": 300,
"y": 60,
"wires": [
[
"716a760504fb3ce1"
]
]
},
{
"id": "716a760504fb3ce1",
"type": "debug",
"z": "230815bb0628a63e",
"name": "debug 11",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 940,
"y": 60,
"wires": []
},
{
"id": "3b3a9ea3a19ff7e5",
"type": "http request",
"z": "230815bb0628a63e",
"name": "TRAIN",
"method": "POST",
"ret": "txt",
"paytoqs": "ignore",
"url": "http://python:5000/train",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 250,
"y": 540,
"wires": [
[
"5290cdca42f99d2c"
]
]
},
{
"id": "4faac7a3217d6419",
"type": "inject",
"z": "230815bb0628a63e",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 540,
"wires": [
[
"3b3a9ea3a19ff7e5"
]
]
},
{
"id": "5290cdca42f99d2c",
"type": "debug",
"z": "230815bb0628a63e",
"name": "debug 12",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 440,
"y": 540,
"wires": []
},
{
"id": "ce865e88acfa3de3",
"type": "http request",
"z": "230815bb0628a63e",
"name": "MODELS",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "http://python:5000/models",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 260,
"y": 600,
"wires": [
[
"43f0ea10327964bc"
]
]
},
{
"id": "41583ff4f043ae6d",
"type": "inject",
"z": "230815bb0628a63e",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 600,
"wires": [
[
"ce865e88acfa3de3"
]
]
},
{
"id": "43f0ea10327964bc",
"type": "debug",
"z": "230815bb0628a63e",
"name": "debug 13",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 440,
"y": 600,
"wires": []
},
{
"id": "740a140db7cd93f9",
"type": "inject",
"z": "230815bb0628a63e",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 660,
"wires": [
[
"7d0ae91a06114c05"
]
]
},
{
"id": "7d0ae91a06114c05",
"type": "http request",
"z": "230815bb0628a63e",
"name": "RECOMMEND",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "http://10.10.11.211:5000/recommend?week=42&dow=3&hour=14&k=2",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 280,
"y": 660,
"wires": [
[
"f27c12eab34c0d48"
]
]
},
{
"id": "f27c12eab34c0d48",
"type": "debug",
"z": "230815bb0628a63e",
"name": "debug 14",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 620,
"y": 660,
"wires": []
},
{
"id": "c630ce9f80c77046",
"type": "http request",
"z": "230815bb0628a63e",
"name": "RECOMMEND - OPTIMIZED GRUAS",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "http://python:5000/recommend?week=42&dow=3&hour=14&optimize=true&min_gruas=1&max_gruas=5&include_cell_data=true",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 350,
"y": 700,
"wires": [
[
"1d6ef76f3145f9d3",
"b63c955f07e230f1"
]
]
},
{
"id": "1d6ef76f3145f9d3",
"type": "debug",
"z": "230815bb0628a63e",
"name": "debug 15",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 620,
"y": 700,
"wires": []
},
{
"id": "e50b886e6d4a366e",
"type": "inject",
"z": "230815bb0628a63e",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 700,
"wires": [
[
"c630ce9f80c77046"
]
]
},
{
"id": "b96227cc7c8c3bcd",
"type": "inject",
"z": "230815bb0628a63e",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 740,
"wires": [
[
"dea2827d47fce819"
]
]
},
{
"id": "dea2827d47fce819",
"type": "http request",
"z": "230815bb0628a63e",
"name": "RECOMMEND - OPTIMIZED COVERAGED",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "http://python:5000/recommend?week=42&dow=3&hour=14&optimize=true&target_coverage=95&include_cell_data=true",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 370,
"y": 740,
"wires": [
[
"289ab66bd5b46e06",
"b63c955f07e230f1"
]
]
},
{
"id": "289ab66bd5b46e06",
"type": "debug",
"z": "230815bb0628a63e",
"name": "debug 16",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 620,
"y": 740,
"wires": []
},
{
"id": "b63c955f07e230f1",
"type": "link out",
"z": "230815bb0628a63e",
"name": "link out 1",
"mode": "link",
"links": [
"32e2494f462cc6f4"
],
"x": 265,
"y": 900,
"wires": []
},
{
"id": "36934d545031c7fd",
"type": "inject",
"z": "230815bb0628a63e",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 110,
"y": 960,
"wires": [
[
"148399ac5cb60fa4"
]
]
},
{
"id": "148399ac5cb60fa4",
"type": "http request",
"z": "230815bb0628a63e",
"name": "PREDICT",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "http://10.10.11.211:5000/predict?model_type=demand",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 260,
"y": 960,
"wires": [
[
"7b7e2d456a6ff158"
]
]
},
{
"id": "7b7e2d456a6ff158",
"type": "debug",
"z": "230815bb0628a63e",
"name": "debug 18",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 400,
"y": 960,
"wires": []
},
{
"id": "cee897cd4088d738",
"type": "ui-template",
"z": "56fb69eb622c3f3b",
"group": "f8afeee042444067",
"page": "",
"ui": "",
"name": "Mapa Embebido",
"order": 1,
"width": 12,
"height": "9",
"head": "",
"format": "",
"storeOutMessages": true,
"passthru": true,
"resendOnRefresh": true,
"templateScope": "local",
"className": "",
"x": 120,
"y": 80,
"wires": [
[]
]
},
{
"id": "c2bb3385f15f27e1",
"type": "ui-text-input",
"z": "56fb69eb622c3f3b",
"group": "bb66042c3e7c9cb3",
"name": "Calendario",
"label": "Fecha Estimación:",
"order": 1,
"width": 0,
"height": 0,
"topic": "topic",
"topicType": "msg",
"mode": "datetime-local",
"tooltip": "Añada la fecha en la que quiera la estimación ",
"delay": 300,
"passthru": false,
"sendOnDelay": false,
"sendOnBlur": true,
"sendOnEnter": true,
"className": "",
"clearable": true,
"sendOnClear": false,
"icon": "",
"iconPosition": "left",
"iconInnerPosition": "inside",
"x": 330,
"y": 40,
"wires": [
[
"a37879075935bb78"
]
]
},
{
"id": "8236702597bcb3d2",
"type": "ui-slider",
"z": "56fb69eb622c3f3b",
"group": "bb66042c3e7c9cb3",
"name": "Cobertura",
"label": "Cobertura",
"tooltip": "",
"order": 3,
"width": 0,
"height": 0,
"passthru": false,
"outs": "end",
"topic": "topic",
"topicType": "msg",
"thumbLabel": "true",
"showTicks": "false",
"min": "50",
"max": "95",
"step": 1,
"className": "",
"iconPrepend": "",
"iconAppend": "",
"color": "green",
"colorTrack": "red",
"colorThumb": "",
"showTextField": true,
"x": 320,
"y": 140,
"wires": [
[
"387fe5c1fac19afe"
]
]
},
{
"id": "a67e9a88c70fb435",
"type": "ui-number-input",
"z": "56fb69eb622c3f3b",
"group": "bb66042c3e7c9cb3",
"name": "Gruas",
"label": "Número de Grúas disponibles",
"order": 4,
"width": 0,
"height": 0,
"topic": "topic",
"topicType": "msg",
"min": "1",
"max": 10,
"step": 1,
"tooltip": "Número de gruas disponibles para las estimaciones",
"passthru": false,
"sendOnBlur": true,
"sendOnEnter": true,
"className": "",
"clearable": true,
"icon": "",
"iconPosition": "left",
"iconInnerPosition": "inside",
"spinner": "default",
"x": 310,
"y": 200,
"wires": [
[
"d62279b67e0184f1"
]
]
},
{
"id": "e2fe0aeabbab5ac6",
"type": "ui-button-group",
"z": "56fb69eb622c3f3b",
"name": "Opciones",
"group": "bb66042c3e7c9cb3",
"order": 2,
"width": "",
"height": "",
"label": "Parámetros para las estimaciones:",
"className": "",
"rounded": false,
"useThemeColors": true,
"passthru": false,
"options": [
{
"label": "Optimo",
"icon": "map-search",
"value": "optimo",
"valueType": "str",
"color": "#009933"
},
{
"label": "Grúas ",
"icon": "truck-flatbed",
"value": "gruas",
"valueType": "str",
"color": "#999999"
}
],
"topic": "topic",
"topicType": "msg",
"x": 320,
"y": 80,
"wires": [
[
"9e77a7b3c8e11460"
]
]
},
{
"id": "a8f18f06a75851a1",
"type": "ui-button",
"z": "56fb69eb622c3f3b",
"group": "bb66042c3e7c9cb3",
"name": "Limpieza",
"label": "Borrar los datos del mapa",
"order": 7,
"width": 0,
"height": 0,
"emulateClick": true,
"tooltip": "",
"color": "",
"bgcolor": "",
"className": "",
"icon": "",
"iconPosition": "left",
"payload": "{\"command\":{\"clearlayer\":[\"H3\",\"layer2\"]}}",
"payloadType": "json",
"topic": "topic",
"topicType": "msg",
"buttonColor": "",
"textColor": "",
"iconColor": "",
"enableClick": true,
"enablePointerdown": false,
"pointerdownPayload": "",
"pointerdownPayloadType": "str",
"enablePointerup": false,
"pointerupPayload": "",
"pointerupPayloadType": "str",
"x": 320,
"y": 360,
"wires": [
[
"26adce90744a441d"
]
]
},
{
"id": "f78b5cc7dc506422",
"type": "ui-button",
"z": "56fb69eb622c3f3b",
"group": "bb66042c3e7c9cb3",
"name": "Mostrar",
"label": "Calcular los datos",
"order": 6,
"width": 0,
"height": 0,
"emulateClick": false,
"tooltip": "",
"color": "",
"bgcolor": "",
"className": "",
"icon": "",
"iconPosition": "left",
"payload": "",
"payloadType": "str",
"topic": "topic",
"topicType": "msg",
"buttonColor": "",
"textColor": "",
"iconColor": "",
"enableClick": true,
"enablePointerdown": false,
"pointerdownPayload": "",
"pointerdownPayloadType": "str",
"enablePointerup": false,
"pointerupPayload": "",
"pointerupPayloadType": "str",
"x": 120,
"y": 520,
"wires": [
[
"fa837f38ed1dbc1e"
]
]
},
{
"id": "fe084cbbae83b4fa",
"type": "worldmap",
"z": "56fb69eb622c3f3b",
"name": "",
"lat": "",
"lon": "",
"zoom": "",
"layer": "",
"cluster": "",
"maxage": "",
"usermenu": "show",
"layers": "show",
"panit": "false",
"panlock": "false",
"zoomlock": "false",
"hiderightclick": "false",
"coords": "false",
"showgrid": "false",
"showruler": "false",
"allowFileDrop": "false",
"path": "/worldmap",
"overlist": "DR,CO,RA,DN",
"maplist": "OSMG,OSMC,EsriC,EsriS,UKOS",
"mapname": "",
"mapurl": "",
"mapopt": "",
"mapwms": false,
"x": 140,
"y": 120,
"wires": []
},
{
"id": "f42fd41cad44b2d1",
"type": "link in",
"z": "56fb69eb622c3f3b",
"name": "IN - WorldMAP",
"links": [
"26adce90744a441d"
],
"x": 45,
"y": 120,
"wires": [
[
"fe084cbbae83b4fa"
]
]
},
{
"id": "26adce90744a441d",
"type": "link out",
"z": "56fb69eb622c3f3b",
"name": "OUT - Limpiar mapa",
"mode": "link",
"links": [
"f42fd41cad44b2d1"
],
"x": 695,
"y": 360,
"wires": []
},
{
"id": "a796d79ddb167eb1",
"type": "ui-dropdown",
"z": "56fb69eb622c3f3b",
"group": "bb66042c3e7c9cb3",
"name": "Tráfico",
"label": "Densidad del tráfico (opcional)",
"tooltip": "",
"order": 5,
"width": 0,
"height": 0,
"passthru": false,
"multiple": false,
"chips": false,
"clearable": true,
"options": [
{
"label": "Alto",
"value": 3,
"type": "num"
},
{
"label": "Normal",
"value": 2,
"type": "num"
},
{
"label": "Bajo",
"value": 1,
"type": "num"
}
],
"payload": "",
"topic": "topic",
"topicType": "msg",
"className": "",
"typeIsComboBox": true,
"msgTrigger": "onChange",
"x": 310,
"y": 280,
"wires": [
[
"5eedba05f8e677de"
]
]
},
{
"id": "ebf80c2abc415017",
"type": "ui-text",
"z": "56fb69eb622c3f3b",
"group": "bb66042c3e7c9cb3",
"order": 8,
"width": 0,
"height": null,
"name": "Resultados",
"label": "",
"format": "{{msg.payload}}",
"layout": "row-left",
"style": true,
"font": "Helvetica, sans-serif",
"fontSize": 16,
"color": "#717171",
"wrapText": false,
"className": "",
"value": "payload",
"valueType": "msg",
"x": 590,
"y": 420,
"wires": []
},
{
"id": "a37879075935bb78",
"type": "change",
"z": "56fb69eb622c3f3b",
"name": "",
"rules": [
{
"t": "set",
"p": "calendario",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 530,
"y": 40,
"wires": [
[]
]
},
{
"id": "9e77a7b3c8e11460",
"type": "change",
"z": "56fb69eb622c3f3b",
"name": "",
"rules": [
{
"t": "set",
"p": "opcion",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 540,
"y": 80,
"wires": [
[]
]
},
{
"id": "5387e2228122ee16",
"type": "inject",
"z": "56fb69eb622c3f3b",
"name": "Incializar Valores",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "1",
"topic": "",
"payload": "iso",
"payloadType": "date",
"x": 130,
"y": 40,
"wires": [
[
"c2bb3385f15f27e1",
"e2fe0aeabbab5ac6",
"8236702597bcb3d2",
"a67e9a88c70fb435",
"a796d79ddb167eb1"
]
]
},
{
"id": "387fe5c1fac19afe",
"type": "change",
"z": "56fb69eb622c3f3b",
"name": "",
"rules": [
{
"t": "set",
"p": "cobertura",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 530,
"y": 140,
"wires": [
[]
]
},
{
"id": "d62279b67e0184f1",
"type": "change",
"z": "56fb69eb622c3f3b",
"name": "",
"rules": [
{
"t": "set",
"p": "gruas",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 540,
"y": 200,
"wires": [
[]
]
},
{
"id": "5eedba05f8e677de",
"type": "change",
"z": "56fb69eb622c3f3b",
"name": "",
"rules": [
{
"t": "set",
"p": "trafico",
"pt": "flow",
"to": "payload",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 740,
"y": 280,
"wires": [
[]
]
},
{
"id": "8afe796111cb8c8d",
"type": "switch",
"z": "56fb69eb622c3f3b",
"name": "opcion",
"property": "opcion",
"propertyType": "flow",
"rules": [
{
"t": "eq",
"v": "gruas",
"vt": "str"
},
{
"t": "eq",
"v": "optimo",
"vt": "str"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 3,
"x": 90,
"y": 780,
"wires": [
[
"16926b851536025d"
],
[
"9c6f7af87438a4bd"
],
[
"ec90eeee993b765a"
]
]
},
{
"id": "447113b576ecfcba",
"type": "ui-notification",
"z": "56fb69eb622c3f3b",
"ui": "3afa9de4406d30d8",
"position": "top right",
"colorDefault": true,
"color": "#000000",
"displayTime": "10",
"showCountdown": true,
"outputs": 1,
"allowDismiss": true,
"dismissText": "Cerrar",
"allowConfirm": true,
"confirmText": "Aceptar",
"raw": false,
"className": "",
"name": "",
"x": 630,
"y": 880,
"wires": [
[]
]
},
{
"id": "fa837f38ed1dbc1e",
"type": "function",
"z": "56fb69eb622c3f3b",
"name": "Calculamos la fecha",
"func": "let fecha = new Date(flow.get(\"calendario\"));\n\nif (isNaN(fecha.getTime())) {\n node.error(\"Fecha inválida\", msg);\n return null;\n}\n\nconst firstDayOfYear = new Date(fecha.getFullYear(), 0, 1);\n// @ts-ignore\nconst pastDaysOfYear = (fecha - firstDayOfYear) / 86400000;\nconst week = Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);\n\nconst dow = fecha.getDay();\n\nconst hour = fecha.getHours();\n\nmsg.payload = { week, dow, hour };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 300,
"y": 520,
"wires": [
[
"8afe796111cb8c8d"
]
]
},
{
"id": "ec90eeee993b765a",
"type": "change",
"z": "56fb69eb622c3f3b",
"name": "",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "Es necesario indicar que calculo se necesita si por cobertura o por número de gruas.",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 340,
"y": 880,
"wires": [
[
"447113b576ecfcba"
]
]
},
{
"id": "9c6f7af87438a4bd",
"type": "switch",
"z": "56fb69eb622c3f3b",
"name": "cobertura",
"property": "cobertura",
"propertyType": "flow",
"rules": [
{
"t": "istype",
"v": "number",
"vt": "number"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 260,
"y": 780,
"wires": [
[
"91d413e80d8139af"
],
[
"9af49346793df573"
]
]
},
{
"id": "9af49346793df573",
"type": "change",
"z": "56fb69eb622c3f3b",
"name": "",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "Es necesario indicar la cobertura requerida",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 460,
"y": 820,
"wires": [
[
"447113b576ecfcba"
]
]
},
{
"id": "11b08389769a3a9f",
"type": "http request",
"z": "56fb69eb622c3f3b",
"name": "",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 870,
"y": 760,
"wires": [
[
"c1552a91a92da199"
]
]
},
{
"id": "91d413e80d8139af",
"type": "change",
"z": "56fb69eb622c3f3b",
"name": "",
"rules": [
{
"t": "set",
"p": "url",
"pt": "msg",
"to": "\"http://python:5000/recommend?week=\" & payload.week &\t\"&dow=\" & payload.dow &\t\"&hour=\" & payload.hour &\t\"&traffic=\" & $flowContext(\"trafico\") &\t\"&optimize=true&include_cell_data=true&min_gruas=1&max_gruas=\" & $flowContext(\"gruas\") &\t\"&target_coverage=\" & $flowContext(\"cobertura\")",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 690,
"y": 760,
"wires": [
[
"11b08389769a3a9f"
]
]
},
{
"id": "c1552a91a92da199",
"type": "link out",
"z": "56fb69eb622c3f3b",
"name": "link out 2",
"mode": "link",
"links": [
"32e2494f462cc6f4",
"68707cddaa9512b9"
],
"x": 1055,
"y": 620,
"wires": []
},
{
"id": "16926b851536025d",
"type": "switch",
"z": "56fb69eb622c3f3b",
"name": "gruas",
"property": "gruas",
"propertyType": "flow",
"rules": [
{
"t": "istype",
"v": "number",
"vt": "number"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 250,
"y": 680,
"wires": [
[
"dcd489d1ae63d998"
],
[
"1cd6a1eb10de5864"
]
]
},
{
"id": "1cd6a1eb10de5864",
"type": "change",
"z": "56fb69eb622c3f3b",
"name": "",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "Es necesario indicar las gruas disponibles",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 460,
"y": 720,
"wires": [
[
"447113b576ecfcba"
]
]
},
{
"id": "dcd489d1ae63d998",
"type": "change",
"z": "56fb69eb622c3f3b",
"name": "",
"rules": [
{
"t": "set",
"p": "url",
"pt": "msg",
"to": "\"http://python:5000/recommend?week=\" & payload.week &\t\"&dow=\" & payload.dow &\t\"&hour=\" & payload.hour &\t\"&traffic=\" & $flowContext(\"trafico\") &\t\"&optimize=false&include_cell_data=true&k=\" & $flowContext(\"gruas\")",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 690,
"y": 660,
"wires": [
[
"2e7fc0d864b209e7"
]
]
},
{
"id": "2e7fc0d864b209e7",
"type": "http request",
"z": "56fb69eb622c3f3b",
"name": "",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 870,
"y": 660,
"wires": [
[
"c1552a91a92da199"
]
]
},
{
"id": "4de317cc487a47c6",
"type": "change",
"z": "56fb69eb622c3f3b",
"name": "",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "\"Resultados:
\" & \"Riesgo cubierto: \" & payload.cobertura.risk_coverage_percentage &\t \" %
\" & \"Cobertura total: \" & payload.cobertura.coverage_percentage &\t \" %\"",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 360,
"y": 420,
"wires": [
[
"ebf80c2abc415017"
]
]
},
{
"id": "68707cddaa9512b9",
"type": "link in",
"z": "56fb69eb622c3f3b",
"name": "link in 2",
"links": [
"c1552a91a92da199"
],
"x": 215,
"y": 420,
"wires": [
[
"4de317cc487a47c6"
]
]
}
]