Generamos el gráfico de posicionamiento de H3

This commit is contained in:
2025-12-11 19:16:25 +01:00
parent 28f9362268
commit 2da1c9b591

View File

@@ -1,4 +1,12 @@
[ [
{
"id": "ae9e82a0a0e1eb89",
"type": "tab",
"label": "Cálculos de H3",
"disabled": false,
"info": "",
"env": []
},
{ {
"id": "f511dd2230a153e7", "id": "f511dd2230a153e7",
"type": "tab", "type": "tab",
@@ -7,6 +15,173 @@
"info": "", "info": "",
"env": [] "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": "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", "id": "ab02b07ce8843526",
"type": "worldmap", "type": "worldmap",
@@ -35,8 +210,478 @@
"mapurl": "", "mapurl": "",
"mapopt": "", "mapopt": "",
"mapwms": false, "mapwms": false,
"x": 360, "x": 440,
"y": 140, "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)), 9) 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\nORDER BY id_servicio\nLIMIT 5",
"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": [] "wires": []
} }
] ]