diff --git a/flows.json b/flows.json index 4c696ba..503dfaf 100644 --- a/flows.json +++ b/flows.json @@ -2152,7 +2152,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n\n", + "format": "\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -2406,7 +2406,7 @@ "width": "12", "height": "6", "head": "", - "format": "\n\n", + "format": "\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -2648,7 +2648,7 @@ "width": "0", "height": "0", "head": "", - "format": "\n\n", + "format": "\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -2857,7 +2857,7 @@ "width": "0", "height": "0", "head": "", - "format": "\n\n\n\n", + "format": "\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -2916,7 +2916,7 @@ "type": "function", "z": "6d6a011bf1913637", "name": "set light settings", - "func": "// On vérifie si le message contient les propriétés attendues\n// avant de mettre à jour les variables globales.\n\nif (msg.payload) {\n \n // Si led_status est présent dans le payload, on stocke\n if (msg.payload.led_status !== undefined) {\n global.set(\"led_status\", msg.payload.led_status);\n }\n\n // Si calibration_led_intensity est présent dans le payload, on stocke\n if (msg.payload.calibration_led_intensity !== undefined) {\n global.set(\"calibration_led_intensity\", msg.payload.calibration_led_intensity);\n }\n}\n\nreturn msg;", + "func": "// On v\u00e9rifie si le message contient les propri\u00e9t\u00e9s attendues\n// avant de mettre \u00e0 jour les variables globales.\n\nif (msg.payload) {\n \n // Si led_status est pr\u00e9sent dans le payload, on stocke\n if (msg.payload.led_status !== undefined) {\n global.set(\"led_status\", msg.payload.led_status);\n }\n\n // Si calibration_led_intensity est pr\u00e9sent dans le payload, on stocke\n if (msg.payload.calibration_led_intensity !== undefined) {\n global.set(\"calibration_led_intensity\", msg.payload.calibration_led_intensity);\n }\n}\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, @@ -3019,7 +3019,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n", + "format": "\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -3150,7 +3150,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n", + "format": "\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -3174,7 +3174,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n\n\n", + "format": "\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -3201,7 +3201,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n\n", + "format": "\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -3227,7 +3227,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n", + "format": "\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -3253,7 +3253,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n\n\n", + "format": "\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -3279,7 +3279,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n\n\n", + "format": "\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -3394,7 +3394,7 @@ "type": "function", "z": "f7ff9c0e54da4b8e", "name": "set object_latlon", - "func": "if (msg.topic) {\n global.set(\"object_lat\", msg.payload.object_lat);\n global.set(\"object_lon\", msg.payload.object_lon);\n}\nreturn msg;\n", + "func": "if (msg.topic) {\n global.set(\"object_lat\", msg.payload.object_lat);\n global.set(\"object_lon\", msg.payload.object_lon);\n if (msg.payload.object_date !== undefined) {\n global.set(\"object_date\", msg.payload.object_date);\n }\n if (msg.payload.object_time !== undefined) {\n global.set(\"object_time\", msg.payload.object_time);\n }\n}\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, @@ -3412,7 +3412,7 @@ "type": "function", "z": "f7ff9c0e54da4b8e", "name": "set object_datetime", - "func": "if (msg.topic) {\n global.set(\"object_date\", msg.payload.object_date);\n global.set(\"object_time\", msg.payload.object_time);\n}\nreturn msg;\n", + "func": "if (msg.topic) {\n global.set(\"object_date\", msg.payload.object_date);\n global.set(\"object_time\", msg.payload.object_time);\n if (msg.payload.object_lat !== undefined) {\n global.set(\"object_lat\", msg.payload.object_lat);\n }\n if (msg.payload.object_lon !== undefined) {\n global.set(\"object_lon\", msg.payload.object_lon);\n }\n}\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, @@ -3662,7 +3662,7 @@ "width": "0", "height": "0", "head": "", - "format": "\n\n\n\n", + "format": "\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -3686,7 +3686,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n\n\n", + "format": "\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -3712,7 +3712,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n", + "format": "\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -3729,7 +3729,7 @@ "type": "function", "z": "71ede8b7dd88d90e", "name": "set acq_params", - "func": "// On récupère le payload\nconst p = msg.payload;\n\n// On ne procède que si le payload est un objet\nif (p && typeof p === 'object') {\n\n // Liste des paramètres à surveiller et à stocker\n // On boucle sur les clés pour éviter de répéter 20 fois \"if(p.x !== undefined)\"\n const keys = [\n \"acq_id\", \"acq_nb_frame\", \"acq_interframe_volume\", \n \"acq_imaged_volume\", \"acq_pumped_volume\", \"acq_comment\",\n \"acq_status\", \"acq_magnification\", \"acq_tube_lens_reference\",\n \"acq_objective_lens_reference\", \"process_pixel_size\",\n \"calibration_pixel_size\", \"calibration_led_intensity\",\n \"sensor_width_um\", \"sensor_height_um\", \"acq_flowcell_thickness\",\n \"acq_interframe_flowrate\", \"acq_stabilization_delay\",\n \"acq_start_timestamp\"\n ];\n\n keys.forEach(key => {\n if (p[key] !== undefined) {\n global.set(key, p[key]);\n }\n });\n\n // Cas particuliers (noms de propriétés différents entre msg et global)\n if (p.progression !== undefined) global.set(\"acq_progression\", p.progression);\n if (p.duration_left !== undefined) global.set(\"acq_duration_left\", p.duration_left);\n}\n\nreturn msg;", + "func": "// On r\u00e9cup\u00e8re le payload\nconst p = msg.payload;\n\n// On ne proc\u00e8de que si le payload est un objet\nif (p && typeof p === 'object') {\n\n // Liste des param\u00e8tres \u00e0 surveiller et \u00e0 stocker\n // On boucle sur les cl\u00e9s pour \u00e9viter de r\u00e9p\u00e9ter 20 fois \"if(p.x !== undefined)\"\n const keys = [\n \"acq_id\", \"acq_nb_frame\", \"acq_interframe_volume\", \n \"acq_imaged_volume\", \"acq_pumped_volume\", \"acq_comment\",\n \"acq_status\", \"acq_magnification\", \"acq_tube_lens_reference\",\n \"acq_objective_lens_reference\", \"process_pixel_size\",\n \"calibration_pixel_size\", \"calibration_led_intensity\",\n \"sensor_width_um\", \"sensor_height_um\", \"acq_flowcell_thickness\",\n \"acq_interframe_flowrate\", \"acq_stabilization_delay\",\n \"acq_start_timestamp\"\n ];\n\n keys.forEach(key => {\n if (p[key] !== undefined) {\n global.set(key, p[key]);\n }\n });\n\n // Cas particuliers (noms de propri\u00e9t\u00e9s diff\u00e9rents entre msg et global)\n if (p.progression !== undefined) global.set(\"acq_progression\", p.progression);\n if (p.duration_left !== undefined) global.set(\"acq_duration_left\", p.duration_left);\n}\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, @@ -3844,7 +3844,7 @@ "type": "function", "z": "71ede8b7dd88d90e", "name": "update_config", - "func": "\n\nconst keys = global.keys(); \nlet config = {}; \n\nkeys.forEach(key => {\n if (!key.startsWith('$')) {\n if (\n key.startsWith('sample_') ||\n key.startsWith('acq_') ||\n key.startsWith('object_') ||\n key.startsWith('process_') ||\n key.startsWith('img_')\n ) {\n config[key] = global.get(key);\n }\n }\n});\n\nmsg.topic = \"imager/image\";\n\nmsg.payload = {\n action: \"update_config\",\n config: config\n};\n\nreturn msg;", + "func": "const keys = global.keys();\nlet config = {};\n\nkeys.forEach(key => {\n if (!key.startsWith('$')) {\n if (\n key.startsWith('sample_') ||\n key.startsWith('acq_') ||\n key.startsWith('object_') ||\n key.startsWith('process_') ||\n key.startsWith('img_')\n ) {\n config[key] = global.get(key);\n }\n }\n});\n\n// Compose fully-qualified IDs for unique EcoTaxa naming\nconst project = config.sample_project || \"\";\nconst sampleRaw = String(config.sample_id || \"\");\nconst acqRaw = String(config.acq_id || \"\");\n\nif (project && !sampleRaw.startsWith(project + \"_\")) {\n config.sample_id = project + \"_\" + sampleRaw;\n}\nconst finalSample = String(config.sample_id || \"\");\nif (finalSample && !acqRaw.startsWith(finalSample + \"_\")) {\n config.acq_id = finalSample + \"_\" + acqRaw;\n}\n\n// Sanitize spaces in IDs to match filesystem paths\n// (the Python imager replaces spaces with underscores in directory names)\nconfig.sample_id = String(config.sample_id || \"\").replace(/ /g, \"_\");\nconfig.acq_id = String(config.acq_id || \"\").replace(/ /g, \"_\");\n\nmsg.topic = \"imager/image\";\n\nmsg.payload = {\n action: \"update_config\",\n config: config\n};\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -3864,7 +3864,7 @@ "type": "function", "z": "71ede8b7dd88d90e", "name": "start acquisition", - "func": "\n\nconst acq_interframe_volume = global.get(\"acq_interframe_volume\") || 0;\nconst acq_nb_frame = global.get(\"acq_nb_frame\") || 0;\nconst acq_stabilization_delay = global.get(\"acq_stabilization_delay\") || 0;\nconst acq_interframe_flowrate = global.get(\"acq_interframe_flowrate\") || 0;\n\nmsg.payload = {\n action: \"image\",\n pump_direction: \"FORWARD\",\n volume: acq_interframe_volume,\n nb_frame: acq_nb_frame,\n sleep: acq_stabilization_delay,\n flowrate: acq_interframe_flowrate,\n};\n\nmsg.topic = \"imager/image\";\n\nreturn msg;", + "func": "const acq_interframe_volume = msg.payload.acq_interframe_volume\n || global.get(\"acq_interframe_volume\") || 0;\nconst acq_nb_frame = msg.payload.acq_nb_frame\n || global.get(\"acq_nb_frame\") || 0;\nconst acq_stabilization_delay = msg.payload.acq_stabilization_delay\n || global.get(\"acq_stabilization_delay\") || 0;\nconst acq_interframe_flowrate = msg.payload.acq_interframe_flowrate\n || global.get(\"acq_interframe_flowrate\") || 0;\n\nmsg.payload = {\n action: \"image\",\n pump_direction: \"FORWARD\",\n volume: acq_interframe_volume,\n nb_frame: acq_nb_frame,\n sleep: acq_stabilization_delay,\n flowrate: acq_interframe_flowrate,\n};\n\nmsg.topic = \"imager/image\";\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -4110,7 +4110,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n", + "format": "\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -4198,7 +4198,7 @@ "width": "0", "height": "0", "head": "", - "format": "\n\n\n\n\n", + "format": "\n\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -4307,7 +4307,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n", + "format": "\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -4333,7 +4333,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n\n", + "format": "\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -4452,7 +4452,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n", + "format": "\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -4482,7 +4482,7 @@ "type": "function", "z": "4fdbd7bacb797c5a", "name": "Insert export column", - "func": "msg.payload = msg.payload.map(item => {\n // extraction de l'acquisition_id\n const acq = item.acquisition_id; // ex: \"A_2\"\n\n // création du chemin export\n item.export = `/ps/data/browse/api/raw/export/ecotaxa/ecotaxa_${acq}.zip`;\n\n return item;\n});\nreturn msg;\n", + "func": "msg.payload = msg.payload.map(item => {\n // extraction de l'acquisition_id\n const acq = item.acquisition_id; // ex: \"A_2\"\n\n // cr\u00e9ation du chemin export\n item.export = `/ps/data/browse/api/raw/export/ecotaxa/ecotaxa_${acq}.zip`;\n\n return item;\n});\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -4567,7 +4567,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n\n\n\n\n", + "format": "\n\n\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -4667,7 +4667,7 @@ "type": "function", "z": "4fdbd7bacb797c5a", "name": "HEATMAP (object_x, object_y)", - "func": "// Heatmap – Flowcell distribution\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\n// Meta from first valid row\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n if (!lines[i].trim().startsWith(\"[\")) {\n firstRow = lines[i].split(\"\\t\");\n break;\n }\n}\nif (!firstRow) return null;\n\nconst meta = {\n sample_id: val(firstRow, \"sample_id\") || \"\",\n project: val(firstRow, \"sample_project\") || \"\",\n acq_id: val(firstRow, \"acq_id\") || \"\"\n};\n\n// Data: x/y for heatmap\nconst data = lines.slice(1)\n .filter(l => !l.trim().startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n x: parseFloat(val(r, \"object_x\")),\n y: parseFloat(val(r, \"object_y\"))\n };\n });\n\nmsg.payload = { meta, data };\nreturn msg;\n", + "func": "// Heatmap \u2013 Flowcell distribution\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\n// Meta from first valid row\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n if (!lines[i].trim().startsWith(\"[\")) {\n firstRow = lines[i].split(\"\\t\");\n break;\n }\n}\nif (!firstRow) return null;\n\nconst meta = {\n sample_id: val(firstRow, \"sample_id\") || \"\",\n project: val(firstRow, \"sample_project\") || \"\",\n acq_id: val(firstRow, \"acq_id\") || \"\"\n};\n\n// Data: x/y for heatmap\nconst data = lines.slice(1)\n .filter(l => !l.trim().startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n x: parseFloat(val(r, \"object_x\")),\n y: parseFloat(val(r, \"object_y\"))\n };\n });\n\nmsg.payload = { meta, data };\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -4687,7 +4687,7 @@ "type": "function", "z": "4fdbd7bacb797c5a", "name": "ESD Histogram (object_equivalent_diameter)", - "func": "// ESD Size Spectrum – TSV → {meta, data[{d}]}\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n if (!lines[i].startsWith(\"[\")) {\n firstRow = lines[i].split(\"\\t\");\n break;\n }\n}\nconst meta = {\n sample_id: firstRow ? (val(firstRow, \"sample_id\") || \"\") : \"\"\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n d: parseFloat(val(r, \"object_equivalent_diameter\"))\n };\n });\n\nmsg.payload = { meta, data };\nreturn msg;\n", + "func": "// ESD Size Spectrum \u2013 TSV \u2192 {meta, data[{d}]}\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n if (!lines[i].startsWith(\"[\")) {\n firstRow = lines[i].split(\"\\t\");\n break;\n }\n}\nconst meta = {\n sample_id: firstRow ? (val(firstRow, \"sample_id\") || \"\") : \"\"\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n d: parseFloat(val(r, \"object_equivalent_diameter\"))\n };\n });\n\nmsg.payload = { meta, data };\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -4707,7 +4707,7 @@ "type": "function", "z": "4fdbd7bacb797c5a", "name": "TIMELINE (sequence index + area)", - "func": "// Timeline – seq index vs object_area\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nlet seq = 0;\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n seq: seq++,\n area: parseFloat(val(r, \"object_area\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", + "func": "// Timeline \u2013 seq index vs object_area\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nlet seq = 0;\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n seq: seq++,\n area: parseFloat(val(r, \"object_area\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -4727,7 +4727,7 @@ "type": "function", "z": "4fdbd7bacb797c5a", "name": "COLORSPACE (Saturation vs Value)", - "func": "// Colorspace – MeanSaturation vs MeanValue\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n s: parseFloat(val(r, \"object_MeanSaturation\")),\n v: parseFloat(val(r, \"object_MeanValue\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", + "func": "// Colorspace \u2013 MeanSaturation vs MeanValue\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n s: parseFloat(val(r, \"object_MeanSaturation\")),\n v: parseFloat(val(r, \"object_MeanValue\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -4747,7 +4747,7 @@ "type": "function", "z": "4fdbd7bacb797c5a", "name": "ASPECT (Width vs Height)", - "func": "// Aspect – width vs height\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n w: parseFloat(val(r, \"object_width\")),\n h: parseFloat(val(r, \"object_height\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", + "func": "// Aspect \u2013 width vs height\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n w: parseFloat(val(r, \"object_width\")),\n h: parseFloat(val(r, \"object_height\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -4767,7 +4767,7 @@ "type": "function", "z": "4fdbd7bacb797c5a", "name": "GREENNESS (custom index + circularity)", - "func": "// Greenness vs Circularity – custom_greenness + object_circ.\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n const hue = parseFloat(val(r, \"object_MeanHue\")) || 0;\n const circ = parseFloat(val(r, \"object_circ.\")) || 0;\n const greenDist = Math.abs(hue - 80);\n const g = Math.max(0, 100 - greenDist);\n return { g, circ };\n });\n\nmsg.payload = { data };\nreturn msg;\n", + "func": "// Greenness vs Circularity \u2013 custom_greenness + object_circ.\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n const hue = parseFloat(val(r, \"object_MeanHue\")) || 0;\n const circ = parseFloat(val(r, \"object_circ.\")) || 0;\n const greenDist = Math.abs(hue - 80);\n const g = Math.max(0, 100 - greenDist);\n return { g, circ };\n });\n\nmsg.payload = { data };\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -4787,7 +4787,7 @@ "type": "function", "z": "4fdbd7bacb797c5a", "name": "COMPLEXITY (Area vs Perimeter)", - "func": "// Complexity – Area vs Perimeter\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n area: parseFloat(val(r, \"object_area\")),\n per: parseFloat(val(r, \"object_perim.\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", + "func": "// Complexity \u2013 Area vs Perimeter\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n area: parseFloat(val(r, \"object_area\")),\n per: parseFloat(val(r, \"object_perim.\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -4807,7 +4807,7 @@ "type": "function", "z": "4fdbd7bacb797c5a", "name": "TEXTURE (Area vs StdValue)", - "func": "// Texture – Area vs StdValue\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n area: parseFloat(val(r, \"object_area\")),\n std: parseFloat(val(r, \"object_StdValue\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", + "func": "// Texture \u2013 Area vs StdValue\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n area: parseFloat(val(r, \"object_area\")),\n std: parseFloat(val(r, \"object_StdValue\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -4827,7 +4827,7 @@ "type": "function", "z": "4fdbd7bacb797c5a", "name": "SOLIDITY (Histogram)", - "func": "// Solidity – histogram of object_solidity\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n sol: parseFloat(val(r, \"object_solidity\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", + "func": "// Solidity \u2013 histogram of object_solidity\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n sol: parseFloat(val(r, \"object_solidity\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -4871,7 +4871,7 @@ "type": "function", "z": "4fdbd7bacb797c5a", "name": "Sample Identity Metadata Only", - "func": "// FUNCTION: Extract minimal metadata for the “Sample Identity” panel\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\n// --- 1. Find first data row (skip [f] / [t] metadata-like lines) ---\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n if (!lines[i].trim().startsWith(\"[\")) {\n firstRow = lines[i].split(\"\\t\");\n break;\n }\n}\nif (!firstRow) return null;\n\n// --- 2. Extract minimal metadata ---\nconst meta = {\n sample_id: val(firstRow, \"sample_id\") || \"\",\n project: val(firstRow, \"sample_project\") || \"\",\n acq_id: val(firstRow, \"acq_id\") || \"\"\n};\n\n// --- 3. Output ONLY the metadata ---\nmsg.payload = { meta };\nreturn msg;\n", + "func": "// FUNCTION: Extract minimal metadata for the \u201cSample Identity\u201d panel\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\n// --- 1. Find first data row (skip [f] / [t] metadata-like lines) ---\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n if (!lines[i].trim().startsWith(\"[\")) {\n firstRow = lines[i].split(\"\\t\");\n break;\n }\n}\nif (!firstRow) return null;\n\n// --- 2. Extract minimal metadata ---\nconst meta = {\n sample_id: val(firstRow, \"sample_id\") || \"\",\n project: val(firstRow, \"sample_project\") || \"\",\n acq_id: val(firstRow, \"acq_id\") || \"\"\n};\n\n// --- 3. Output ONLY the metadata ---\nmsg.payload = { meta };\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5083,7 +5083,7 @@ "type": "function", "z": "4fdbd7bacb797c5a", "name": "EXPLORER", - "func": "// Explorer Function Node\n// TSV → {meta, keys, data: [{id, url, ...keys}]}\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\n// 1. Parse Headers\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const i = headers.indexOf(col);\n return i >= 0 ? row[i] : null;\n};\n\n// 2. Extract Meta from first valid data row\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n // Skip rows starting with '[' (metadata comments)\n if (!lines[i].trim().startsWith(\"[\")) {\n firstRow = lines[i].split(\"\\t\");\n break;\n }\n}\n\n// Default meta\nconst meta = {\n sample_id: \"\",\n project: \"\",\n acq_id: \"\",\n resolution: 1.0 // Default fallback\n};\n\nif (firstRow) {\n meta.sample_id = val(firstRow, \"sample_id\") || \"\";\n meta.project = val(firstRow, \"sample_project\") || \"\";\n meta.acq_id = val(firstRow, \"acq_id\") || \"\";\n \n // CRITICAL: Extract pixel size (microns per pixel)\n // If process_pixel is missing or 0, default to 1 to prevent division by zero\n const px = parseFloat(val(firstRow, \"process_pixel\"));\n if (!isNaN(px) && px > 0) {\n meta.resolution = px;\n }\n}\n\n// 3. Explorer Keys to extract\nconst explorerKeys = [\n \"object_area\", \"object_equivalent_diameter\", \"object_perim.\",\n \"object_major\", \"object_minor\", \"object_width\", \"object_height\",\n \"object_circ.\", \"object_elongation\", \"object_solidity\",\n \"object_eccentricity\", \"object_MeanHue\", \"object_MeanSaturation\",\n \"object_MeanValue\", \"object_StdValue\", \"object_blur_laplacian\"\n];\n\nlet seq = 0;\nconst data = [];\n\n// 4. Process Data Lines\nfor (let i = 1; i < lines.length; i++) {\n const line = lines[i];\n if (line.trim().startsWith(\"[\")) continue;\n\n const row = line.split(\"\\t\");\n\n const item = {\n id: val(row, \"object_id\"),\n // Construct URL\n url: `/ps/node-red-v2/my-images/${val(row,\"object_date\")}/${val(row,\"sample_id\")}/${val(row,\"acq_id\")}/${val(row,\"img_file_name\")}`,\n sequence_index: seq++\n };\n\n // Parse numeric keys\n explorerKeys.forEach(k => {\n let v = parseFloat(val(row, k));\n item[k] = isNaN(v) ? 0 : v;\n });\n\n // Custom calc for greenness (example)\n const hue = item.object_MeanHue || 0;\n const dist = Math.abs(hue - 80);\n item.custom_greenness = Math.max(0, 100 - dist);\n\n data.push(item);\n}\n\nmsg.payload = {\n meta,\n keys: explorerKeys,\n data\n};\n\nreturn msg;", + "func": "// Explorer Function Node\n// TSV \u2192 {meta, keys, data: [{id, url, ...keys}]}\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\n// 1. Parse Headers\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const i = headers.indexOf(col);\n return i >= 0 ? row[i] : null;\n};\n\n// 2. Extract Meta from first valid data row\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n // Skip rows starting with '[' (metadata comments)\n if (!lines[i].trim().startsWith(\"[\")) {\n firstRow = lines[i].split(\"\\t\");\n break;\n }\n}\n\n// Default meta\nconst meta = {\n sample_id: \"\",\n project: \"\",\n acq_id: \"\",\n resolution: 1.0 // Default fallback\n};\n\nif (firstRow) {\n meta.sample_id = val(firstRow, \"sample_id\") || \"\";\n meta.project = val(firstRow, \"sample_project\") || \"\";\n meta.acq_id = val(firstRow, \"acq_id\") || \"\";\n \n // CRITICAL: Extract pixel size (microns per pixel)\n // If process_pixel is missing or 0, default to 1 to prevent division by zero\n const px = parseFloat(val(firstRow, \"process_pixel\"));\n if (!isNaN(px) && px > 0) {\n meta.resolution = px;\n }\n}\n\n// 3. Explorer Keys to extract\nconst explorerKeys = [\n \"object_area\", \"object_equivalent_diameter\", \"object_perim.\",\n \"object_major\", \"object_minor\", \"object_width\", \"object_height\",\n \"object_circ.\", \"object_elongation\", \"object_solidity\",\n \"object_eccentricity\", \"object_MeanHue\", \"object_MeanSaturation\",\n \"object_MeanValue\", \"object_StdValue\", \"object_blur_laplacian\"\n];\n\nlet seq = 0;\nconst data = [];\n\n// 4. Process Data Lines\nfor (let i = 1; i < lines.length; i++) {\n const line = lines[i];\n if (line.trim().startsWith(\"[\")) continue;\n\n const row = line.split(\"\\t\");\n\n const item = {\n id: val(row, \"object_id\"),\n // Construct URL\n url: `/ps/node-red-v2/my-images/${(val(row,\"object_date\")||\"\").replace(/ /g,\"_\")}/${(val(row,\"sample_id\")||\"\").replace(/ /g,\"_\")}/${(val(row,\"acq_id\")||\"\").replace(/ /g,\"_\")}/${val(row,\"img_file_name\")}`,\n sequence_index: seq++\n };\n\n // Parse numeric keys\n explorerKeys.forEach(k => {\n let v = parseFloat(val(row, k));\n item[k] = isNaN(v) ? 0 : v;\n });\n\n // Custom calc for greenness (example)\n const hue = item.object_MeanHue || 0;\n const dist = Math.abs(hue - 80);\n item.custom_greenness = Math.max(0, 100 - dist);\n\n data.push(item);\n}\n\nmsg.payload = {\n meta,\n keys: explorerKeys,\n data\n};\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5110,7 +5110,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n\n\n", + "format": "\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -5158,7 +5158,7 @@ "type": "function", "z": "4fdbd7bacb797c5a", "name": "GALLERY", - "func": "// GALLERY Function Node\n// TSV → { data: [], meta: { resolution: ... }, keys: ... }\n\n// 1. Handle Clear Signals\nif (msg.clear === true || (msg.payload && msg.payload.clear === true)) {\n msg.payload = { data: [], meta: { resolution: 1 }, keys: [] };\n return msg;\n}\n\n// 2. Validate Payload\nif (!msg.payload) return null;\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) {\n msg.payload = { data: [], meta: { resolution: 1 }, keys: [] };\n return msg;\n}\n\n// 3. Parse Headers\nconst headers = lines[0].split('\\t');\nconst get = (row, key) => {\n const idx = headers.indexOf(key);\n return idx >= 0 ? row[idx] : null;\n};\n\n// 4. Extract Meta (Resolution is key here)\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n const L = lines[i].trim();\n if (!L.startsWith('[') && L !== '') {\n firstRow = lines[i].split('\\t');\n break;\n }\n}\n\nconst meta = {\n sample_id: 'N/A',\n project: 'N/A',\n acq_id: 'N/A',\n resolution: 1.0 // Default fallback\n};\n\nif (firstRow) {\n meta.sample_id = get(firstRow, 'sample_id') || \"\";\n meta.project = get(firstRow, 'sample_project') || \"\";\n meta.acq_id = get(firstRow, 'acq_id') || \"\";\n\n // CRITICAL: Extract microns per pixel\n const px = parseFloat(get(firstRow, 'process_pixel'));\n if (!isNaN(px) && px > 0) {\n meta.resolution = px;\n }\n}\n\n// 5. Select Keys to Display\nconst usefulKeys = [\n \"object_area\", \"object_width\", \"object_height\",\n \"object_equivalent_diameter\", \"object_major\", \"object_minor\",\n \"object_MeanHue\", \"object_elongation\", \"object_blur_laplacian\"\n];\n\nconst data = [];\n\n// 6. Process Rows\nfor (let i = 1; i < lines.length; i++) {\n const rowLine = lines[i].trim();\n if (rowLine.startsWith('[') || rowLine === '') continue;\n\n const row = rowLine.split('\\t');\n if (row.length !== headers.length) continue;\n\n const item = {\n id: get(row, 'object_id'),\n // Construct Image URL\n url: `/ps/data/browse/api/preview/big/objects/${get(row, 'object_date')}/${get(row, 'sample_id')}/${get(row, 'acq_id')}/${get(row, 'img_file_name')}`\n };\n\n // Parse numeric values\n usefulKeys.forEach(k => {\n let v = parseFloat(get(row, k));\n item[k] = isNaN(v) ? 0 : v;\n });\n\n data.push(item);\n}\n\nmsg.payload = {\n data,\n keys: usefulKeys,\n meta\n};\n\nreturn msg;", + "func": "// GALLERY Function Node\n// TSV \u2192 { data: [], meta: { resolution: ... }, keys: ... }\n\n// 1. Handle Clear Signals\nif (msg.clear === true || (msg.payload && msg.payload.clear === true)) {\n msg.payload = { data: [], meta: { resolution: 1 }, keys: [] };\n return msg;\n}\n\n// 2. Validate Payload\nif (!msg.payload) return null;\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) {\n msg.payload = { data: [], meta: { resolution: 1 }, keys: [] };\n return msg;\n}\n\n// 3. Parse Headers\nconst headers = lines[0].split('\\t');\nconst get = (row, key) => {\n const idx = headers.indexOf(key);\n return idx >= 0 ? row[idx] : null;\n};\n\n// 4. Extract Meta (Resolution is key here)\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n const L = lines[i].trim();\n if (!L.startsWith('[') && L !== '') {\n firstRow = lines[i].split('\\t');\n break;\n }\n}\n\nconst meta = {\n sample_id: 'N/A',\n project: 'N/A',\n acq_id: 'N/A',\n resolution: 1.0 // Default fallback\n};\n\nif (firstRow) {\n meta.sample_id = get(firstRow, 'sample_id') || \"\";\n meta.project = get(firstRow, 'sample_project') || \"\";\n meta.acq_id = get(firstRow, 'acq_id') || \"\";\n\n // CRITICAL: Extract microns per pixel\n const px = parseFloat(get(firstRow, 'process_pixel'));\n if (!isNaN(px) && px > 0) {\n meta.resolution = px;\n }\n}\n\n// 5. Select Keys to Display\nconst usefulKeys = [\n \"object_area\", \"object_width\", \"object_height\",\n \"object_equivalent_diameter\", \"object_major\", \"object_minor\",\n \"object_MeanHue\", \"object_elongation\", \"object_blur_laplacian\"\n];\n\nconst data = [];\n\n// 6. Process Rows\nfor (let i = 1; i < lines.length; i++) {\n const rowLine = lines[i].trim();\n if (rowLine.startsWith('[') || rowLine === '') continue;\n\n const row = rowLine.split('\\t');\n if (row.length !== headers.length) continue;\n\n const item = {\n id: get(row, 'object_id'),\n // Construct Image URL\n url: `/ps/data/browse/api/preview/big/objects/${(get(row, 'object_date')||'').replace(/ /g,'_')}/${(get(row, 'sample_id')||'').replace(/ /g,'_')}/${(get(row, 'acq_id')||'').replace(/ /g,'_')}/${get(row, 'img_file_name')}`\n };\n\n // Parse numeric values\n usefulKeys.forEach(k => {\n let v = parseFloat(get(row, k));\n item[k] = isNaN(v) ? 0 : v;\n });\n\n data.push(item);\n}\n\nmsg.payload = {\n data,\n keys: usefulKeys,\n meta\n};\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5339,7 +5339,7 @@ "type": "function", "z": "4fdbd7bacb797c5a", "name": "Sample Identity Metadata Only", - "func": "// FUNCTION: Extract all data and metadata\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\n\n// Fonction utilitaire pour mapper les colonnes\nconst rowToObject = (rowCells) => {\n const obj = {};\n headers.forEach((header, index) => {\n obj[header] = rowCells[index] || \"\";\n });\n return obj;\n};\n\nlet allRows = [];\nlet firstDataRow = null;\n\n// --- 1. Parser toutes les lignes ---\nfor (let i = 1; i < lines.length; i++) {\n const cells = lines[i].split(\"\\t\");\n const rowObj = rowToObject(cells);\n\n allRows.push(rowObj);\n\n // Identifier la première vraie ligne de données pour les métadonnées globales\n if (!firstDataRow && !lines[i].trim().startsWith(\"[\")) {\n firstDataRow = rowObj;\n }\n}\n\n// --- 2. Extraire les métadonnées (basées sur la première ligne valide) ---\nconst meta = {\n sample_id: firstDataRow ? firstDataRow[\"sample_id\"] : \"\",\n project: firstDataRow ? firstDataRow[\"sample_project\"] : \"\",\n acq_id: firstDataRow ? firstDataRow[\"acq_id\"] : \"\"\n};\n\n// --- 3. Output : Métadonnées + l'intégralité des données ---\nmsg.payload = {\n meta: meta,\n data: allRows // Contient maintenant tous les enregistrements\n};\n\nreturn msg;", + "func": "// FUNCTION: Extract all data and metadata\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\n\n// Fonction utilitaire pour mapper les colonnes\nconst rowToObject = (rowCells) => {\n const obj = {};\n headers.forEach((header, index) => {\n obj[header] = rowCells[index] || \"\";\n });\n return obj;\n};\n\nlet allRows = [];\nlet firstDataRow = null;\n\n// --- 1. Parser toutes les lignes ---\nfor (let i = 1; i < lines.length; i++) {\n const cells = lines[i].split(\"\\t\");\n const rowObj = rowToObject(cells);\n\n allRows.push(rowObj);\n\n // Identifier la premi\u00e8re vraie ligne de donn\u00e9es pour les m\u00e9tadonn\u00e9es globales\n if (!firstDataRow && !lines[i].trim().startsWith(\"[\")) {\n firstDataRow = rowObj;\n }\n}\n\n// --- 2. Extraire les m\u00e9tadonn\u00e9es (bas\u00e9es sur la premi\u00e8re ligne valide) ---\nconst meta = {\n sample_id: firstDataRow ? firstDataRow[\"sample_id\"] : \"\",\n project: firstDataRow ? firstDataRow[\"sample_project\"] : \"\",\n acq_id: firstDataRow ? firstDataRow[\"acq_id\"] : \"\"\n};\n\n// --- 3. Output : M\u00e9tadonn\u00e9es + l'int\u00e9gralit\u00e9 des donn\u00e9es ---\nmsg.payload = {\n meta: meta,\n data: allRows // Contient maintenant tous les enregistrements\n};\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5423,7 +5423,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n", + "format": "\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -5454,7 +5454,7 @@ "type": "function", "z": "a7825c4c81ad20a0", "name": "Insert export column", - "func": "msg.payload = msg.payload.map(item => {\n // extraction de l'acquisition_id\n const acq = item.acquisition_id; // ex: \"A_2\"\n\n // création du chemin export\n item.export = `/ps/data/browse/api/raw/export/ecotaxa/ecotaxa_${acq}.zip`;\n\n return item;\n});\nreturn msg;\n", + "func": "msg.payload = msg.payload.map(item => {\n // extraction de l'acquisition_id\n const acq = item.acquisition_id; // ex: \"A_2\"\n\n // cr\u00e9ation du chemin export\n item.export = `/ps/data/browse/api/raw/export/ecotaxa/ecotaxa_${acq}.zip`;\n\n return item;\n});\nreturn msg;\n", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5473,10 +5473,10 @@ "id": "1802e96dd833363b", "type": "function", "z": "a7825c4c81ad20a0", - "name": "Size-safe file read", - "func": "const MAX_LINES = 5001;\nconst MAX_SIZE = 10 * 1024 * 1024;\n\nconst filePath = msg.payload.path || msg.filename;\nif (!filePath) { return null; }\n\ntry {\n const stats = fs.statSync(filePath);\n if (stats.size > MAX_SIZE) {\n const fd = fs.openSync(filePath, \"r\");\n const buf = Buffer.alloc(Math.min(stats.size, 2 * 1024 * 1024));\n const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);\n fs.closeSync(fd);\n const partial = buf.toString(\"utf8\", 0, bytesRead);\n const lines = partial.split(\"\\n\");\n msg.payload = lines.slice(0, MAX_LINES).join(\"\\n\");\n node.warn(\"Large TSV (\" + Math.round(stats.size/1024/1024) + \"MB) - limited to \" + MAX_LINES + \" lines\");\n } else {\n msg.payload = fs.readFileSync(filePath, \"utf8\");\n }\n return msg;\n} catch(e) {\n node.error(\"File read error: \" + e.message, msg);\n return null;\n}", + "name": "Streaming TSV reader", + "func": "// Streaming TSV reader \u2014 extracts only needed columns, never loads full file\nconst filePath = msg.payload.path || msg.filename;\nif (!filePath) return null;\n\nconst NEEDED = [\n 'object_x','object_y','object_equivalent_diameter','object_area',\n 'object_MeanSaturation','object_MeanValue','object_width','object_height',\n 'object_MeanHue','object_circ.','object_perim.','object_StdValue',\n 'object_solidity','object_elongation','object_major','object_minor',\n 'object_eccentricity','object_blur_laplacian','object_id','object_date','sample_id',\n 'sample_project','acq_id','img_file_name','process_pixel'\n];\n\nreturn new Promise((resolve) => {\n const columns = {};\n NEEDED.forEach(k => columns[k] = []);\n const colIdx = {};\n const meta = {};\n let totalRows = 0;\n let lineNum = 0;\n let remainder = '';\n\n const stream = fs.createReadStream(filePath, { encoding: 'utf8' });\n\n stream.on('data', chunk => {\n remainder += chunk;\n const lines = remainder.split('\\n');\n remainder = lines.pop();\n\n for (const line of lines) {\n if (!line.trim()) continue;\n lineNum++;\n\n if (lineNum === 1) {\n const headers = line.split('\\t');\n headers.forEach((h, i) => { if (NEEDED.includes(h)) colIdx[h] = i; });\n continue;\n }\n if (line.trim().startsWith('[')) continue;\n\n const fields = line.split('\\t');\n totalRows++;\n\n if (totalRows === 1) {\n meta.sample_id = fields[colIdx['sample_id']] || '';\n meta.sample_project = fields[colIdx['sample_project']] || '';\n meta.acq_id = fields[colIdx['acq_id']] || '';\n meta.process_pixel = fields[colIdx['process_pixel']] || '';\n }\n\n for (const key of NEEDED) {\n if (colIdx[key] !== undefined) {\n columns[key].push(fields[colIdx[key]]);\n }\n }\n }\n });\n\n stream.on('end', () => {\n if (remainder.trim() && !remainder.trim().startsWith('[') && lineNum > 0) {\n const fields = remainder.split('\\t');\n totalRows++;\n for (const key of NEEDED) {\n if (colIdx[key] !== undefined) {\n columns[key].push(fields[colIdx[key]]);\n }\n }\n }\n\n const sid = meta.sample_id || '';\n const proj = meta.sample_project || '';\n meta.display_sample_id = sid.split(proj + '_')[1] || sid;\n meta.display_acq_id = (meta.acq_id || '').split(meta.display_sample_id + '_')[1] || meta.acq_id;\n\n msg.payload = { columns, meta, totalRows };\n resolve(msg);\n });\n\n stream.on('error', err => {\n node.error('TSV stream error: ' + err.message, msg);\n resolve(null);\n });\n});", "outputs": 1, - "timeout": 0, + "timeout": 30, "noerr": 0, "initialize": "", "finalize": "", @@ -5543,7 +5543,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n\n\n\n\n", + "format": "\n\n\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -5611,7 +5611,7 @@ "type": "function", "z": "a7825c4c81ad20a0", "name": "HEATMAP (object_x, object_y)", - "func": "// Heatmap – Flowcell distribution\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\n// Meta from first valid row\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n if (!lines[i].trim().startsWith(\"[\")) {\n firstRow = lines[i].split(\"\\t\");\n break;\n }\n}\nif (!firstRow) return null;\n\nconst meta = {\n sample_id: val(firstRow, \"sample_id\") || \"\",\n project: val(firstRow, \"sample_project\") || \"\",\n acq_id: val(firstRow, \"acq_id\") || \"\"\n};\n\n// Data: x/y for heatmap\nconst data = lines.slice(1)\n .filter(l => !l.trim().startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n x: parseFloat(val(r, \"object_x\")),\n y: parseFloat(val(r, \"object_y\"))\n };\n });\n\nmsg.payload = { meta, data };\nreturn msg;\n", + "func": "// Heatmap \u2014 columnar input with reservoir sampling\nconst { columns, meta, totalRows } = msg.payload;\nconst MAX_POINTS = 10000;\nconst xCol = columns['object_x'];\nconst yCol = columns['object_y'];\nif (!xCol || !yCol) return null;\nconst n = xCol.length;\n\nlet data;\nif (n <= MAX_POINTS) {\n data = new Array(n);\n for (let i = 0; i < n; i++) {\n data[i] = { x: parseFloat(xCol[i]), y: parseFloat(yCol[i]) };\n }\n} else {\n data = new Array(MAX_POINTS);\n for (let i = 0; i < MAX_POINTS; i++) {\n data[i] = { x: parseFloat(xCol[i]), y: parseFloat(yCol[i]) };\n }\n for (let i = MAX_POINTS; i < n; i++) {\n const j = Math.floor(Math.random() * (i + 1));\n if (j < MAX_POINTS) {\n data[j] = { x: parseFloat(xCol[i]), y: parseFloat(yCol[i]) };\n }\n }\n}\n\nmsg.payload = { meta, data, totalRows };\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5631,7 +5631,7 @@ "type": "function", "z": "a7825c4c81ad20a0", "name": "ESD Histogram (object_equivalent_diameter)", - "func": "// ESD Size Spectrum – TSV → {meta, data[{d}]}\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n if (!lines[i].startsWith(\"[\")) {\n firstRow = lines[i].split(\"\\t\");\n break;\n }\n}\nconst meta = {\n sample_id: firstRow ? (val(firstRow, \"sample_id\") || \"\") : \"\"\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n d: parseFloat(val(r, \"object_equivalent_diameter\"))\n };\n });\n\nmsg.payload = { meta, data };\nreturn msg;\n", + "func": "// ESD Histogram \u2014 columnar input, all values for accurate histogram\nconst { columns, meta, totalRows } = msg.payload;\nconst raw = columns['object_equivalent_diameter'];\nif (!raw) return null;\n\nconst data = new Array(raw.length);\nfor (let i = 0; i < raw.length; i++) {\n data[i] = { d: parseFloat(raw[i]) };\n}\n\nmsg.payload = { meta, data, totalRows };\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5651,7 +5651,7 @@ "type": "function", "z": "a7825c4c81ad20a0", "name": "TIMELINE (sequence index + area)", - "func": "// Timeline – seq index vs object_area\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nlet seq = 0;\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n seq: seq++,\n area: parseFloat(val(r, \"object_area\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", + "func": "// Timeline \u2014 columnar input, all values\nconst { columns, meta, totalRows } = msg.payload;\nconst raw = columns['object_area'];\nif (!raw) return null;\n\nconst data = new Array(raw.length);\nfor (let i = 0; i < raw.length; i++) {\n data[i] = { seq: i, area: parseFloat(raw[i]) };\n}\n\nmsg.payload = { data, totalRows };\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5671,7 +5671,7 @@ "type": "function", "z": "a7825c4c81ad20a0", "name": "COLORSPACE (Saturation vs Value)", - "func": "// Colorspace – MeanSaturation vs MeanValue\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n s: parseFloat(val(r, \"object_MeanSaturation\")),\n v: parseFloat(val(r, \"object_MeanValue\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", + "func": "// Colorspace \u2014 columnar input with reservoir sampling\nconst { columns, meta, totalRows } = msg.payload;\nconst MAX_POINTS = 10000;\nconst xCol = columns['object_MeanSaturation'];\nconst yCol = columns['object_MeanValue'];\nif (!xCol || !yCol) return null;\nconst n = xCol.length;\n\nlet data;\nif (n <= MAX_POINTS) {\n data = new Array(n);\n for (let i = 0; i < n; i++) {\n data[i] = { s: parseFloat(xCol[i]), v: parseFloat(yCol[i]) };\n }\n} else {\n data = new Array(MAX_POINTS);\n for (let i = 0; i < MAX_POINTS; i++) {\n data[i] = { s: parseFloat(xCol[i]), v: parseFloat(yCol[i]) };\n }\n for (let i = MAX_POINTS; i < n; i++) {\n const j = Math.floor(Math.random() * (i + 1));\n if (j < MAX_POINTS) {\n data[j] = { s: parseFloat(xCol[i]), v: parseFloat(yCol[i]) };\n }\n }\n}\n\nmsg.payload = { data, totalRows };\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5691,7 +5691,7 @@ "type": "function", "z": "a7825c4c81ad20a0", "name": "ASPECT (Width vs Height)", - "func": "// Aspect – width vs height\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n w: parseFloat(val(r, \"object_width\")),\n h: parseFloat(val(r, \"object_height\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", + "func": "// Aspect \u2014 columnar input with reservoir sampling\nconst { columns, meta, totalRows } = msg.payload;\nconst MAX_POINTS = 10000;\nconst xCol = columns['object_width'];\nconst yCol = columns['object_height'];\nif (!xCol || !yCol) return null;\nconst n = xCol.length;\n\nlet data;\nif (n <= MAX_POINTS) {\n data = new Array(n);\n for (let i = 0; i < n; i++) {\n data[i] = { w: parseFloat(xCol[i]), h: parseFloat(yCol[i]) };\n }\n} else {\n data = new Array(MAX_POINTS);\n for (let i = 0; i < MAX_POINTS; i++) {\n data[i] = { w: parseFloat(xCol[i]), h: parseFloat(yCol[i]) };\n }\n for (let i = MAX_POINTS; i < n; i++) {\n const j = Math.floor(Math.random() * (i + 1));\n if (j < MAX_POINTS) {\n data[j] = { w: parseFloat(xCol[i]), h: parseFloat(yCol[i]) };\n }\n }\n}\n\nmsg.payload = { data, totalRows };\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5711,7 +5711,7 @@ "type": "function", "z": "a7825c4c81ad20a0", "name": "GREENNESS (custom index + circularity)", - "func": "// Greenness vs Circularity – custom_greenness + object_circ.\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n const hue = parseFloat(val(r, \"object_MeanHue\")) || 0;\n const circ = parseFloat(val(r, \"object_circ.\")) || 0;\n const greenDist = Math.abs(hue - 80);\n const g = Math.max(0, 100 - greenDist);\n return { g, circ };\n });\n\nmsg.payload = { data };\nreturn msg;\n", + "func": "// Greenness \u2014 columnar input with reservoir sampling + derived greenness\nconst { columns, meta, totalRows } = msg.payload;\nconst MAX_POINTS = 10000;\nconst hueCol = columns['object_MeanHue'];\nconst circCol = columns['object_circ.'];\nif (!hueCol || !circCol) return null;\nconst n = hueCol.length;\n\nfunction makePoint(i) {\n const hue = parseFloat(hueCol[i]) || 0;\n const circ = parseFloat(circCol[i]) || 0;\n const g = Math.max(0, 100 - Math.abs(hue - 80));\n return { g, circ };\n}\n\nlet data;\nif (n <= MAX_POINTS) {\n data = new Array(n);\n for (let i = 0; i < n; i++) data[i] = makePoint(i);\n} else {\n data = new Array(MAX_POINTS);\n for (let i = 0; i < MAX_POINTS; i++) data[i] = makePoint(i);\n for (let i = MAX_POINTS; i < n; i++) {\n const j = Math.floor(Math.random() * (i + 1));\n if (j < MAX_POINTS) data[j] = makePoint(i);\n }\n}\n\nmsg.payload = { data, totalRows };\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5731,7 +5731,7 @@ "type": "function", "z": "a7825c4c81ad20a0", "name": "COMPLEXITY (Area vs Perimeter)", - "func": "// Complexity – Area vs Perimeter\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n area: parseFloat(val(r, \"object_area\")),\n per: parseFloat(val(r, \"object_perim.\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", + "func": "// Complexity \u2014 columnar input with reservoir sampling\nconst { columns, meta, totalRows } = msg.payload;\nconst MAX_POINTS = 10000;\nconst xCol = columns['object_area'];\nconst yCol = columns['object_perim.'];\nif (!xCol || !yCol) return null;\nconst n = xCol.length;\n\nlet data;\nif (n <= MAX_POINTS) {\n data = new Array(n);\n for (let i = 0; i < n; i++) {\n data[i] = { area: parseFloat(xCol[i]), per: parseFloat(yCol[i]) };\n }\n} else {\n data = new Array(MAX_POINTS);\n for (let i = 0; i < MAX_POINTS; i++) {\n data[i] = { area: parseFloat(xCol[i]), per: parseFloat(yCol[i]) };\n }\n for (let i = MAX_POINTS; i < n; i++) {\n const j = Math.floor(Math.random() * (i + 1));\n if (j < MAX_POINTS) {\n data[j] = { area: parseFloat(xCol[i]), per: parseFloat(yCol[i]) };\n }\n }\n}\n\nmsg.payload = { data, totalRows };\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5751,7 +5751,7 @@ "type": "function", "z": "a7825c4c81ad20a0", "name": "TEXTURE (Area vs StdValue)", - "func": "// Texture – Area vs StdValue\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n area: parseFloat(val(r, \"object_area\")),\n std: parseFloat(val(r, \"object_StdValue\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", + "func": "// Texture \u2014 columnar input with reservoir sampling\nconst { columns, meta, totalRows } = msg.payload;\nconst MAX_POINTS = 10000;\nconst xCol = columns['object_area'];\nconst yCol = columns['object_StdValue'];\nif (!xCol || !yCol) return null;\nconst n = xCol.length;\n\nlet data;\nif (n <= MAX_POINTS) {\n data = new Array(n);\n for (let i = 0; i < n; i++) {\n data[i] = { area: parseFloat(xCol[i]), std: parseFloat(yCol[i]) };\n }\n} else {\n data = new Array(MAX_POINTS);\n for (let i = 0; i < MAX_POINTS; i++) {\n data[i] = { area: parseFloat(xCol[i]), std: parseFloat(yCol[i]) };\n }\n for (let i = MAX_POINTS; i < n; i++) {\n const j = Math.floor(Math.random() * (i + 1));\n if (j < MAX_POINTS) {\n data[j] = { area: parseFloat(xCol[i]), std: parseFloat(yCol[i]) };\n }\n }\n}\n\nmsg.payload = { data, totalRows };\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5771,7 +5771,7 @@ "type": "function", "z": "a7825c4c81ad20a0", "name": "SOLIDITY (Histogram)", - "func": "// Solidity – histogram of object_solidity\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\nconst data = lines.slice(1)\n .filter(l => !l.startsWith(\"[\"))\n .map(l => {\n const r = l.split(\"\\t\");\n return {\n sol: parseFloat(val(r, \"object_solidity\"))\n };\n });\n\nmsg.payload = { data };\nreturn msg;\n", + "func": "// Solidity Histogram \u2014 columnar input, all values\nconst { columns, meta, totalRows } = msg.payload;\nconst raw = columns['object_solidity'];\nif (!raw) return null;\n\nconst data = new Array(raw.length);\nfor (let i = 0; i < raw.length; i++) {\n data[i] = { sol: parseFloat(raw[i]) };\n}\n\nmsg.payload = { data, totalRows };\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -5815,7 +5815,7 @@ "type": "function", "z": "a7825c4c81ad20a0", "name": "Sample Identity Metadata Only", - "func": "// FUNCTION: Extract minimal metadata for the “Sample Identity” panel\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const idx = headers.indexOf(col);\n return idx >= 0 ? row[idx] : null;\n};\n\n// --- 1. Find first data row (skip [f] / [t] metadata-like lines) ---\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n if (!lines[i].trim().startsWith(\"[\")) {\n firstRow = lines[i].split(\"\\t\");\n break;\n }\n}\nif (!firstRow) return null;\n\n// --- 2. Extract minimal metadata ---\nconst meta = {\n sample_id: val(firstRow, \"sample_id\") || \"\",\n project: val(firstRow, \"sample_project\") || \"\",\n acq_id: val(firstRow, \"acq_id\") || \"\"\n};\n\n// --- 3. Output ONLY the metadata ---\nmsg.payload = { meta };\nreturn msg;\n", + "func": "// Sample Identity \u2014 extract metadata from columnar input\nconst { meta } = msg.payload;\nmsg.payload = { meta: {\n sample_id: meta.sample_id || '',\n project: meta.sample_project || '',\n acq_id: meta.acq_id || ''\n}};\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -6027,7 +6027,7 @@ "type": "function", "z": "a7825c4c81ad20a0", "name": "EXPLORER", - "func": "// Explorer Function Node\n// TSV → {meta, keys, data: [{id, url, ...keys}]}\n\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\n// 1. Parse Headers\nconst headers = lines[0].split(\"\\t\");\nconst val = (row, col) => {\n const i = headers.indexOf(col);\n return i >= 0 ? row[i] : null;\n};\n\n// 2. Extract Meta from first valid data row\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n // Skip rows starting with '[' (metadata comments)\n if (!lines[i].trim().startsWith(\"[\")) {\n firstRow = lines[i].split(\"\\t\");\n break;\n }\n}\n\n// Default meta\nconst meta = {\n sample_id: \"\",\n project: \"\",\n acq_id: \"\",\n resolution: 1.0 // Default fallback\n};\n\nif (firstRow) {\n meta.sample_id = val(firstRow, \"sample_id\") || \"\";\n meta.project = val(firstRow, \"sample_project\") || \"\";\n meta.acq_id = val(firstRow, \"acq_id\") || \"\";\n \n // CRITICAL: Extract pixel size (microns per pixel)\n // If process_pixel is missing or 0, default to 1 to prevent division by zero\n const px = parseFloat(val(firstRow, \"process_pixel\"));\n if (!isNaN(px) && px > 0) {\n meta.resolution = px;\n }\n}\n\n// 3. Explorer Keys to extract\nconst explorerKeys = [\n \"object_area\", \"object_equivalent_diameter\", \"object_perim.\",\n \"object_major\", \"object_minor\", \"object_width\", \"object_height\",\n \"object_circ.\", \"object_elongation\", \"object_solidity\",\n \"object_eccentricity\", \"object_MeanHue\", \"object_MeanSaturation\",\n \"object_MeanValue\", \"object_StdValue\", \"object_blur_laplacian\"\n];\n\nlet seq = 0;\nconst data = [];\n\n// 4. Process Data Lines\nfor (let i = 1; i < lines.length; i++) {\n const line = lines[i];\n if (line.trim().startsWith(\"[\")) continue;\n\n const row = line.split(\"\\t\");\n\n const item = {\n id: val(row, \"object_id\"),\n // Construct URL\n url: msg.tsvDir ? `/ps/node-red-v2/my-images/${msg.tsvDir}/${val(row,\"img_file_name\")}` : `/ps/node-red-v2/my-images/${val(row,\"object_date\")}/${val(row,\"sample_id\")}/${val(row,\"acq_id\")}/${val(row,\"img_file_name\")}`,\n sequence_index: seq++\n };\n\n // Parse numeric keys\n explorerKeys.forEach(k => {\n let v = parseFloat(val(row, k));\n item[k] = isNaN(v) ? 0 : v;\n });\n\n // Custom calc for greenness (example)\n const hue = item.object_MeanHue || 0;\n const dist = Math.abs(hue - 80);\n item.custom_greenness = Math.max(0, 100 - dist);\n\n data.push(item);\n}\n\n// Cap rows to prevent browser crash on huge datasets\nconst MAX_ROWS = 5000;\nif (data.length > MAX_ROWS) data.length = MAX_ROWS;\n\nmsg.payload = {\n meta,\n keys: explorerKeys,\n data\n};\n\nreturn msg;", + "func": "// Explorer \u2014 columnar input, pre-sorted by area (largest first), capped at 5000\nconst { columns, meta, totalRows } = msg.payload;\nconst MAX_ROWS = 5000;\nconst idCol = columns['object_id'];\nif (!idCol) return null;\n\nconst explorerKeys = [\n \"object_area\",\"object_equivalent_diameter\",\"object_perim.\",\n \"object_major\",\"object_minor\",\"object_width\",\"object_height\",\n \"object_circ.\",\"object_elongation\",\"object_solidity\",\n \"object_eccentricity\",\"object_MeanHue\",\"object_MeanSaturation\",\n \"object_MeanValue\",\"object_StdValue\",\"object_blur_laplacian\"\n];\n\nconst px = parseFloat(meta.process_pixel);\nconst resolution = (!isNaN(px) && px > 0) ? px : 1.0;\n\n// Build all rows first (lightweight: ~200 bytes each)\nconst allRows = [];\nfor (let i = 0; i < idCol.length; i++) {\n const item = {\n id: columns['object_id'][i],\n url: `/ps/node-red-v2/my-images/${(columns['object_date'][i]||'').replace(/ /g,'_')}/${(columns['sample_id'][i]||'').replace(/ /g,'_')}/${(columns['acq_id'][i]||'').replace(/ /g,'_')}/${columns['img_file_name'][i]}`,\n sequence_index: i\n };\n explorerKeys.forEach(k => {\n const v = parseFloat((columns[k] || [])[i]);\n item[k] = isNaN(v) ? 0 : v;\n });\n const hue = item.object_MeanHue || 0;\n item.custom_greenness = Math.max(0, 100 - Math.abs(hue - 80));\n allRows.push(item);\n}\n\n// Pre-sort by area descending (the default sort in the UI)\nallRows.sort((a, b) => b.object_area - a.object_area);\n\n// Cap to MAX_ROWS\nconst data = allRows.length > MAX_ROWS ? allRows.slice(0, MAX_ROWS) : allRows;\n\nmsg.payload = {\n meta: { sample_id: meta.sample_id, project: meta.sample_project, acq_id: meta.acq_id, resolution },\n keys: explorerKeys,\n data,\n totalRows,\n showing: data.length\n};\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -6054,7 +6054,7 @@ "width": 0, "height": 0, "head": "", - "format": "\n\n\n\n", + "format": "\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -6102,7 +6102,7 @@ "type": "function", "z": "a7825c4c81ad20a0", "name": "GALLERY", - "func": "// GALLERY Function Node\n// TSV → { data: [], meta: { resolution: ... }, keys: ... }\n\n// 1. Handle Clear Signals\nif (msg.clear === true || (msg.payload && msg.payload.clear === true)) {\n msg.payload = { data: [], meta: { resolution: 1 }, keys: [] };\n return msg;\n}\n\n// 2. Validate Payload\nif (!msg.payload) return null;\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) {\n msg.payload = { data: [], meta: { resolution: 1 }, keys: [] };\n return msg;\n}\n\n// 3. Parse Headers\nconst headers = lines[0].split('\\t');\nconst get = (row, key) => {\n const idx = headers.indexOf(key);\n return idx >= 0 ? row[idx] : null;\n};\n\n// 4. Extract Meta (Resolution is key here)\nlet firstRow = null;\nfor (let i = 1; i < lines.length; i++) {\n const L = lines[i].trim();\n if (!L.startsWith('[') && L !== '') {\n firstRow = lines[i].split('\\t');\n break;\n }\n}\n\nconst meta = {\n sample_id: 'N/A',\n project: 'N/A',\n acq_id: 'N/A',\n resolution: 1.0 // Default fallback\n};\n\nif (firstRow) {\n meta.sample_id = get(firstRow, 'sample_id') || \"\";\n meta.project = get(firstRow, 'sample_project') || \"\";\n meta.acq_id = get(firstRow, 'acq_id') || \"\";\n\n // CRITICAL: Extract microns per pixel\n const px = parseFloat(get(firstRow, 'process_pixel'));\n if (!isNaN(px) && px > 0) {\n meta.resolution = px;\n }\n}\n\n// 5. Select Keys to Display\nconst usefulKeys = [\n \"object_area\", \"object_width\", \"object_height\",\n \"object_equivalent_diameter\", \"object_major\", \"object_minor\",\n \"object_MeanHue\", \"object_elongation\", \"object_blur_laplacian\"\n];\n\nconst data = [];\n\n// 6. Process Rows\nfor (let i = 1; i < lines.length; i++) {\n const rowLine = lines[i].trim();\n if (rowLine.startsWith('[') || rowLine === '') continue;\n\n const row = rowLine.split('\\t');\n if (row.length !== headers.length) continue;\n\n const item = {\n id: get(row, 'object_id'),\n // Construct Image URL\n url: msg.tsvDir ? `/ps/node-red-v2/my-images/${msg.tsvDir}/${get(row, 'img_file_name')}` : `/ps/node-red-v2/my-images/${get(row, 'object_date')}/${get(row, 'sample_id')}/${get(row, 'acq_id')}/${get(row, 'img_file_name')}`\n };\n\n // Parse numeric values\n usefulKeys.forEach(k => {\n let v = parseFloat(get(row, k));\n item[k] = isNaN(v) ? 0 : v;\n });\n\n data.push(item);\n}\n\n// Cap rows to prevent browser crash on huge datasets\nconst MAX_ROWS = 5000;\nif (data.length > MAX_ROWS) data.length = MAX_ROWS;\n\nmsg.payload = {\n data,\n keys: usefulKeys,\n meta\n};\n\nreturn msg;", + "func": "// Gallery \u2014 columnar input, pre-sorted by area (largest first), capped at 5000\nif (msg.clear === true || (msg.payload && msg.payload.clear === true)) {\n msg.payload = { data: [], meta: { resolution: 1 }, keys: [] };\n return msg;\n}\n\nconst { columns, meta, totalRows } = msg.payload;\nif (!columns) {\n msg.payload = { data: [], meta: { resolution: 1 }, keys: [] };\n return msg;\n}\n\nconst MAX_ROWS = 5000;\nconst idCol = columns['object_id'];\nif (!idCol) {\n msg.payload = { data: [], meta: { resolution: 1 }, keys: [] };\n return msg;\n}\n\nconst usefulKeys = [\n \"object_area\",\"object_width\",\"object_height\",\n \"object_equivalent_diameter\",\"object_major\",\"object_minor\",\n \"object_MeanHue\",\"object_elongation\",\"object_blur_laplacian\"\n];\n\nconst px = parseFloat(meta.process_pixel);\nconst resolution = (!isNaN(px) && px > 0) ? px : 1.0;\n\n// Build all rows\nconst allRows = [];\nfor (let i = 0; i < idCol.length; i++) {\n const item = {\n id: columns['object_id'][i],\n url: `/ps/node-red-v2/my-images/${(columns['object_date'][i]||'').replace(/ /g,'_')}/${(columns['sample_id'][i]||'').replace(/ /g,'_')}/${(columns['acq_id'][i]||'').replace(/ /g,'_')}/${columns['img_file_name'][i]}`\n };\n usefulKeys.forEach(k => {\n const v = parseFloat((columns[k] || [])[i]);\n item[k] = isNaN(v) ? 0 : v;\n });\n allRows.push(item);\n}\n\n// Pre-sort by area descending (default sort in UI)\nallRows.sort((a, b) => b.object_area - a.object_area);\n\n// Cap to MAX_ROWS\nconst data = allRows.length > MAX_ROWS ? allRows.slice(0, MAX_ROWS) : allRows;\n\nmsg.payload = {\n data,\n keys: usefulKeys,\n meta: { sample_id: meta.sample_id, project: meta.sample_project, acq_id: meta.acq_id, resolution },\n totalRows,\n showing: data.length\n};\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -6267,7 +6267,7 @@ "type": "function", "z": "a7825c4c81ad20a0", "name": "Sample Identity Metadata Only", - "func": "// FUNCTION: Extract all data and metadata\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\n\n// Fonction utilitaire pour mapper les colonnes\nconst rowToObject = (rowCells) => {\n const obj = {};\n headers.forEach((header, index) => {\n obj[header] = rowCells[index] || \"\";\n });\n return obj;\n};\n\nlet allRows = [];\nlet firstDataRow = null;\n\n// --- 1. Parser toutes les lignes ---\nfor (let i = 1; i < lines.length; i++) {\n const cells = lines[i].split(\"\\t\");\n const rowObj = rowToObject(cells);\n\n allRows.push(rowObj);\n\n // Identifier la première vraie ligne de données pour les métadonnées globales\n if (!firstDataRow && !lines[i].trim().startsWith(\"[\")) {\n firstDataRow = rowObj;\n }\n}\n\n// --- 2. Extraire les métadonnées (basées sur la première ligne valide) ---\nconst meta = {\n sample_id: firstDataRow ? firstDataRow[\"sample_id\"] : \"\",\n project: firstDataRow ? firstDataRow[\"sample_project\"] : \"\",\n acq_id: firstDataRow ? firstDataRow[\"acq_id\"] : \"\"\n};\n\n// --- 3. Output : Métadonnées + l'intégralité des données ---\nmsg.payload = {\n meta: meta,\n data: allRows // Contient maintenant tous les enregistrements\n};\n\nreturn msg;", + "func": "// FUNCTION: Extract all data and metadata\nconst lines = msg.payload.toString().trim().split(/\\r?\\n/);\nif (lines.length < 2) return null;\n\nconst headers = lines[0].split(\"\\t\");\n\n// Fonction utilitaire pour mapper les colonnes\nconst rowToObject = (rowCells) => {\n const obj = {};\n headers.forEach((header, index) => {\n obj[header] = rowCells[index] || \"\";\n });\n return obj;\n};\n\nlet allRows = [];\nlet firstDataRow = null;\n\n// --- 1. Parser toutes les lignes ---\nfor (let i = 1; i < lines.length; i++) {\n const cells = lines[i].split(\"\\t\");\n const rowObj = rowToObject(cells);\n\n allRows.push(rowObj);\n\n // Identifier la premi\u00e8re vraie ligne de donn\u00e9es pour les m\u00e9tadonn\u00e9es globales\n if (!firstDataRow && !lines[i].trim().startsWith(\"[\")) {\n firstDataRow = rowObj;\n }\n}\n\n// --- 2. Extraire les m\u00e9tadonn\u00e9es (bas\u00e9es sur la premi\u00e8re ligne valide) ---\nconst meta = {\n sample_id: firstDataRow ? firstDataRow[\"sample_id\"] : \"\",\n project: firstDataRow ? firstDataRow[\"sample_project\"] : \"\",\n acq_id: firstDataRow ? firstDataRow[\"acq_id\"] : \"\"\n};\n\n// --- 3. Output : M\u00e9tadonn\u00e9es + l'int\u00e9gralit\u00e9 des donn\u00e9es ---\nmsg.payload = {\n meta: meta,\n data: allRows // Contient maintenant tous les enregistrements\n};\n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, @@ -6450,7 +6450,7 @@ "type": "function", "z": "6fac9fa6a894b293", "name": "set light settings", - "func": "// On vérifie si le message contient les propriétés attendues\n// avant de mettre à jour les variables globales.\n\nif (msg.payload) {\n\n // Si led_status est présent dans le payload, on stocke\n if (msg.payload.led_status !== undefined) {\n global.set(\"led_status\", msg.payload.led_status);\n }\n\n // Si calibration_led_intensity est présent dans le payload, on stocke\n if (msg.payload.calibration_led_intensity !== undefined) {\n global.set(\"calibration_led_intensity\", msg.payload.calibration_led_intensity);\n }\n}\n\nreturn msg;", + "func": "// On v\u00e9rifie si le message contient les propri\u00e9t\u00e9s attendues\n// avant de mettre \u00e0 jour les variables globales.\n\nif (msg.payload) {\n\n // Si led_status est pr\u00e9sent dans le payload, on stocke\n if (msg.payload.led_status !== undefined) {\n global.set(\"led_status\", msg.payload.led_status);\n }\n\n // Si calibration_led_intensity est pr\u00e9sent dans le payload, on stocke\n if (msg.payload.calibration_led_intensity !== undefined) {\n global.set(\"calibration_led_intensity\", msg.payload.calibration_led_intensity);\n }\n}\n\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, @@ -6758,7 +6758,7 @@ "width": "0", "height": "0", "head": "", - "format": "\n\n\n\n\n", + "format": "\n\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -6863,7 +6863,7 @@ "width": "0", "height": "0", "head": "", - "format": "\n\n\n\n\n\n", + "format": "\n\n\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -7092,7 +7092,7 @@ "width": "0", "height": "0", "head": "", - "format": "\n\n\n\n", + "format": "\n\n\n\n", "storeOutMessages": true, "passthru": true, "resendOnRefresh": true, @@ -7706,7 +7706,7 @@ "type": "comment", "z": "e6d820ba0f4f184e", "g": "039d9046b10d6840", - "name": "system ⚠️", + "name": "system \u26a0\ufe0f", "info": "", "x": 1200, "y": 80,