[ { "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": false, "options": [ { "label": "Alto", "value": 3, "type": "num" }, { "label": "Normal", "value": 2, "type": "num" }, { "label": "Bajo", "value": 1, "type": "num" }, { "label": "Sin estimación", "value": "0", "type": "str" } ], "payload": "", "topic": "topic", "topicType": "msg", "className": "", "typeIsComboBox": true, "msgTrigger": "onChange", "x": 310, "y": 280, "wires": [ [ "c2fd67bee1b6e31f" ] ] }, { "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": 700, "y": 300, "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" ] ] }, { "id": "c2fd67bee1b6e31f", "type": "switch", "z": "56fb69eb622c3f3b", "name": "Limpiamos tráfico", "property": "payload", "propertyType": "msg", "rules": [ { "t": "eq", "v": "0", "vt": "num" }, { "t": "else" } ], "checkall": "true", "repair": false, "outputs": 2, "x": 470, "y": 280, "wires": [ [ "ea73de854d597030" ], [ "5eedba05f8e677de" ] ] }, { "id": "ea73de854d597030", "type": "change", "z": "56fb69eb622c3f3b", "name": "", "rules": [ { "t": "delete", "p": "trafico", "pt": "flow" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 690, "y": 260, "wires": [ [] ] } ]