瀏覽代碼

add emitter & receiver

bdestombes 3 年之前
當前提交
48acb2736f

+ 12 - 0
emitter/.editorconfig

@@ -0,0 +1,12 @@
+root = true
+
+[*]
+indent_style = tab
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.yml,*.yaml]
+indent_style = space
+indent_size = 2

+ 3 - 0
emitter/.gitattributes

@@ -0,0 +1,3 @@
+* text=auto
+*.js text eol=lf
+

+ 4 - 0
emitter/.gitignore

@@ -0,0 +1,4 @@
+node_modules
+yarn.lock
+.nyc_output
+coverage

+ 69 - 0
emitter/backend/server.js

@@ -0,0 +1,69 @@
+const express = require('express');
+const { Bundle, Client } = require('node-osc');
+const path = require('path');
+
+const bodyParser = require('body-parser');
+const app = express();
+
+const oscClient = new Client('127.0.0.1', 3333);
+let fseq;
+const alive = [];
+
+const server = require('http').Server(app);
+
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({extended:true}));
+app.use(express.static('./frontend/assets'));
+
+app.get('/', function (req, res) {
+   res.sendFile('./frontend/index.html', { root: path.resolve(__dirname + '/..') })
+});
+
+app.post('/json', function (req, res) {
+	//console.log(req.body);
+	fseq = fseq ? fseq + 1 : 1;
+	let oscBundle;
+	if (req.body.event === 'touchend') {
+		oscBundle = new Bundle(
+			//[ '/tuio/2Dcur', 'source', 'touch@127.0.0.1:5001' ],
+			[ '/tuio/2Dcur', 'alive' ],
+			[ '/tuio/2Dcur', 'fseq', fseq ]
+		);
+	} else {
+		//if (req.body.changedTouches && req.body.changedTouches.length && req.body.changedTouches.length > 0) {
+			const touches = Object.keys(req.body.changedTouches);
+			touches.forEach(touch => {
+				if (!alive.includes(req.body.changedTouches[touch].identifier)) {
+					alive.push(req.body.changedTouches[touch].identifier);
+				}
+			});
+			oscBundle = new Bundle(
+				//[ '/tuio/2Dcur', 'source', 'touch@127.0.0.1:5001' ],
+				[ '/tuio/2Dcur', 'alive', alive.join(' ') ],
+				[ '/tuio/2Dcur', 'fseq', fseq ]
+			);
+			touches.forEach(touch => {
+				console.log(req.body.changedTouches[touch]);
+				oscBundle.append(
+					[
+						'/tuio/2Dcur',
+						'set',
+						req.body.changedTouches[touch].identifier,
+						req.body.changedTouches[touch].clientX / req.body.screenW,
+						req.body.changedTouches[touch].clientY / req.body.screenH,
+						0.0,
+						0.0
+					]
+				);
+			});
+			oscClient.send(oscBundle, () => {
+				res.status(200).json(JSON.stringify(req.body));
+			});
+		//}
+	}
+});
+
+server.listen(5001, function () {
+ console.log('Votre app est disponible sur localhost:5001 !')
+});
+

文件差異過大導致無法顯示
+ 9404 - 0
emitter/frontend/assets/jquery-1.7.2.js


+ 160 - 0
emitter/frontend/index.html

@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<html style="height: 100%;">
+<head>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width">
+	<title>Touch detect and send to Node server</title>
+</head>
+
+<body style="height: 100%;">
+	<div id="box" style="width: 100%; height: 100%; background-color: #EEEEEE;"></div>
+	<script src="/jquery-1.7.2.js"></script>
+	<script>
+		/* simulation de touch events */
+		/*
+		function sendTouchEvent(x, y, element, eventType) {
+			const touchObj = new Touch({
+				identifier: Date.now(),
+				target: element,
+				clientX: x,
+				clientY: y,
+				radiusX: 2.5,
+				radiusY: 2.5,
+				rotationAngle: 10,
+				force: 0.5,
+			});
+
+			const touchEvent = new TouchEvent(eventType, {
+				cancelable: true,
+				bubbles: true,
+				touches: [touchObj],
+				targetTouches: [],
+				changedTouches: [touchObj],
+				shiftKey: true,
+			});
+
+			element.dispatchEvent(touchEvent);
+		}
+		*/
+
+		const box = document.getElementById('box');
+
+		const urlParams = new URLSearchParams(window.location.search);
+		const sectionParam = urlParams.get('section');
+		const section = sectionParam ? parseInt(sectionParam, 10) : 0;
+		console.log(section);
+
+
+		box.addEventListener('touchstart', function(evt) {
+				console.log(evt);
+				box.innerHTML = evt.touches.length;
+				fetch('http://localhost:5001/json', {
+					method: 'POST',
+					mode: 'cors',
+					cache: 'no-cache',
+					credentials: 'same-origin',
+					headers: {
+						'Content-Type': 'application/json'
+					},
+					redirect: 'follow',
+					referrerPolicy: 'no-referrer',
+					body: JSON.stringify({
+						event: 'touchstart',
+						clientX: evt.changedTouches[0].clientX,
+						clientY: evt.changedTouches[0].clientY,
+						force: evt.changedTouches[0].force,
+						identifier: section + evt.changedTouches[0].identifier,
+						pageX: evt.changedTouches[0].pageX,
+						pageY: evt.changedTouches[0].pageY,
+						radiusX: evt.changedTouches[0].radiusX,
+						radiusY: evt.changedTouches[0].radiusY,
+						rotationAngle: evt.changedTouches[0].rotationAngle,
+						screenX: evt.changedTouches[0].screenX,
+						screenY: evt.changedTouches[0].screenY
+					})
+				});
+				evt.preventDefault();
+		});
+		box.addEventListener('touchmove', function(evt) {
+				console.log(evt);
+				const touches = [];
+				for (var i = 0; i < evt.changedTouches.length; i++) {
+					touches[i] = {
+						clientX: evt.changedTouches[i].clientX,
+						clientY: evt.changedTouches[i].clientY,
+						force: evt.changedTouches[i].force,
+						identifier: section + evt.changedTouches[i].identifier,
+						pageX: evt.changedTouches[i].pageX,
+						pageY: evt.changedTouches[i].pageY,
+						radiusX: evt.changedTouches[i].radiusX,
+						radiusY: evt.changedTouches[i].radiusY,
+						rotationAngle: evt.changedTouches[i].rotationAngle,
+						screenX: evt.changedTouches[i].screenX,
+						screenY: evt.changedTouches[i].screenY
+					};
+				};
+				box.innerHTML = evt.touches.length;
+				fetch('http://localhost:5001/json', {
+					method: 'POST',
+					mode: 'cors',
+					cache: 'no-cache',
+					credentials: 'same-origin',
+					headers: {
+						'Content-Type': 'application/json'
+					},
+					redirect: 'follow',
+					referrerPolicy: 'no-referrer',
+					body: JSON.stringify({
+						event: 'touchmove',
+						screenW: $(document).width(),
+						screenH: $(document).height(),
+						changedTouches: touches
+					})
+				});
+				evt.preventDefault();
+		});
+
+		box.addEventListener('touchend', function(evt) {
+				console.log(evt);
+				box.innerHTML = evt.touches.length;
+				fetch('http://localhost:5001/json', {
+					method: 'POST',
+					mode: 'cors',
+					cache: 'no-cache',
+					credentials: 'same-origin',
+					headers: {
+						'Content-Type': 'application/json'
+					},
+					redirect: 'follow',
+					referrerPolicy: 'no-referrer',
+					body: JSON.stringify({
+						event: 'touchend',
+						clientX: evt.changedTouches[0].clientX,
+						clientY: evt.changedTouches[0].clientY,
+						force: evt.changedTouches[0].force,
+						identifier: section + evt.changedTouches[0].identifier,
+						pageX: evt.changedTouches[0].pageX,
+						pageY: evt.changedTouches[0].pageY,
+						radiusX: evt.changedTouches[0].radiusX,
+						radiusY: evt.changedTouches[0].radiusY,
+						rotationAngle: evt.changedTouches[0].rotationAngle,
+						screenX: evt.changedTouches[0].screenX,
+						screenY: evt.changedTouches[0].screenY
+					})
+				});
+				evt.preventDefault();
+		});
+
+		if (!(typeof box.ontouchstart != 'undefined')) {
+				box.style.border = '1px solid red';
+		}
+
+		/*
+		sendTouchEvent(150, 150, box, 'touchstart');
+		sendTouchEvent(220, 200, box, 'touchmove');
+		sendTouchEvent(220, 200, box, 'touchend');
+		*/
+
+		</script>
+</body>
+</html>

+ 10 - 0
emitter/license

@@ -0,0 +1,10 @@
+MIT License
+
+Copyright (c) t1st3 <contact@t1st3.com> (t1st3.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+

+ 926 - 0
emitter/package-lock.json

@@ -0,0 +1,926 @@
+{
+  "name": "http-keep-alive",
+  "version": "0.1.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "http-keep-alive",
+      "version": "0.1.0",
+      "license": "MIT",
+      "dependencies": {
+        "body-parser": "^1.19.0",
+        "express": "^4.17.1",
+        "node-osc": "^6.1.11"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+      "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+      "dependencies": {
+        "mime-types": "~2.1.24",
+        "negotiator": "0.6.2"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+    },
+    "node_modules/binpack": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/binpack/-/binpack-0.1.0.tgz",
+      "integrity": "sha1-vT0JdMPyoERuF99PYLVacqIFqX4="
+    },
+    "node_modules/body-parser": {
+      "version": "1.19.1",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.1.tgz",
+      "integrity": "sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA==",
+      "dependencies": {
+        "bytes": "3.1.1",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "http-errors": "1.8.1",
+        "iconv-lite": "0.4.24",
+        "on-finished": "~2.3.0",
+        "qs": "6.9.6",
+        "raw-body": "2.4.2",
+        "type-is": "~1.6.18"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz",
+      "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+      "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+    },
+    "node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/destroy": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+      "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+    },
+    "node_modules/encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz",
+      "integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==",
+      "dependencies": {
+        "accepts": "~1.3.7",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.19.1",
+        "content-disposition": "0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "0.4.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "~1.1.2",
+        "fresh": "0.5.2",
+        "merge-descriptors": "1.0.1",
+        "methods": "~1.1.2",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "~2.0.7",
+        "qs": "6.9.6",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "0.17.2",
+        "serve-static": "1.14.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "~1.5.0",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+      "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.3",
+        "statuses": "~1.5.0",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+      "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+      "dependencies": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.51.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
+      "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.34",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
+      "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
+      "dependencies": {
+        "mime-db": "1.51.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+      "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/node-osc": {
+      "version": "6.1.11",
+      "resolved": "https://registry.npmjs.org/node-osc/-/node-osc-6.1.11.tgz",
+      "integrity": "sha512-bwFV/UKBBaSNK5pLYeA8Qx0GVc49fKRjmfMwMoypfPwb7RdhyRccW+tTNHcwr/OJ2b01uVKpnTPurSNs4fWnvw==",
+      "dependencies": {
+        "osc-min": "^1.1.1"
+      },
+      "engines": {
+        "node": "^12.22 || ^14.13 || >=16"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/osc-min": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/osc-min/-/osc-min-1.1.2.tgz",
+      "integrity": "sha512-8DbiO8ME85R75stgNVCZtHxB9MNBBNcyy+isNBXrsFeinXGjwNAauvKVmGlfRas5VJWC/mhzIx7spR2gFvWxvg==",
+      "dependencies": {
+        "binpack": "~0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.9.6",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz",
+      "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==",
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz",
+      "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==",
+      "dependencies": {
+        "bytes": "3.1.1",
+        "http-errors": "1.8.1",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "node_modules/send": {
+      "version": "0.17.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
+      "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
+      "dependencies": {
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "destroy": "~1.0.4",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "1.8.1",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "~2.3.0",
+        "range-parser": "~1.2.1",
+        "statuses": "~1.5.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/send/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+    },
+    "node_modules/serve-static": {
+      "version": "1.14.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
+      "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
+      "dependencies": {
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "0.17.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+    },
+    "node_modules/statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    }
+  },
+  "dependencies": {
+    "accepts": {
+      "version": "1.3.7",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+      "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+      "requires": {
+        "mime-types": "~2.1.24",
+        "negotiator": "0.6.2"
+      }
+    },
+    "array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+    },
+    "binpack": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/binpack/-/binpack-0.1.0.tgz",
+      "integrity": "sha1-vT0JdMPyoERuF99PYLVacqIFqX4="
+    },
+    "body-parser": {
+      "version": "1.19.1",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.1.tgz",
+      "integrity": "sha512-8ljfQi5eBk8EJfECMrgqNGWPEY5jWP+1IzkzkGdFFEwFQZZyaZ21UqdaHktgiMlH0xLHqIFtE/u2OYE5dOtViA==",
+      "requires": {
+        "bytes": "3.1.1",
+        "content-type": "~1.0.4",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "http-errors": "1.8.1",
+        "iconv-lite": "0.4.24",
+        "on-finished": "~2.3.0",
+        "qs": "6.9.6",
+        "raw-body": "2.4.2",
+        "type-is": "~1.6.18"
+      }
+    },
+    "bytes": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz",
+      "integrity": "sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg=="
+    },
+    "content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "requires": {
+        "safe-buffer": "5.2.1"
+      }
+    },
+    "content-type": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+      "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+    },
+    "cookie": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+      "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
+    },
+    "cookie-signature": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+      "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+    },
+    "debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "requires": {
+        "ms": "2.0.0"
+      }
+    },
+    "depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+    },
+    "destroy": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+      "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+    },
+    "ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+    },
+    "encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+    },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+    },
+    "etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+    },
+    "express": {
+      "version": "4.17.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz",
+      "integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==",
+      "requires": {
+        "accepts": "~1.3.7",
+        "array-flatten": "1.1.1",
+        "body-parser": "1.19.1",
+        "content-disposition": "0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "0.4.1",
+        "cookie-signature": "1.0.6",
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "~1.1.2",
+        "fresh": "0.5.2",
+        "merge-descriptors": "1.0.1",
+        "methods": "~1.1.2",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "0.1.7",
+        "proxy-addr": "~2.0.7",
+        "qs": "6.9.6",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "0.17.2",
+        "serve-static": "1.14.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "~1.5.0",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      }
+    },
+    "finalhandler": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+      "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+      "requires": {
+        "debug": "2.6.9",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.3.0",
+        "parseurl": "~1.3.3",
+        "statuses": "~1.5.0",
+        "unpipe": "~1.0.0"
+      }
+    },
+    "forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
+    },
+    "fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+    },
+    "http-errors": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+      "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+      "requires": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.1"
+      }
+    },
+    "iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      }
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
+    },
+    "media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+    },
+    "merge-descriptors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+      "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+    },
+    "methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+    },
+    "mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
+    },
+    "mime-db": {
+      "version": "1.51.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz",
+      "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="
+    },
+    "mime-types": {
+      "version": "2.1.34",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz",
+      "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==",
+      "requires": {
+        "mime-db": "1.51.0"
+      }
+    },
+    "ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+    },
+    "negotiator": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+      "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
+    },
+    "node-osc": {
+      "version": "6.1.11",
+      "resolved": "https://registry.npmjs.org/node-osc/-/node-osc-6.1.11.tgz",
+      "integrity": "sha512-bwFV/UKBBaSNK5pLYeA8Qx0GVc49fKRjmfMwMoypfPwb7RdhyRccW+tTNHcwr/OJ2b01uVKpnTPurSNs4fWnvw==",
+      "requires": {
+        "osc-min": "^1.1.1"
+      }
+    },
+    "on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+      "requires": {
+        "ee-first": "1.1.1"
+      }
+    },
+    "osc-min": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/osc-min/-/osc-min-1.1.2.tgz",
+      "integrity": "sha512-8DbiO8ME85R75stgNVCZtHxB9MNBBNcyy+isNBXrsFeinXGjwNAauvKVmGlfRas5VJWC/mhzIx7spR2gFvWxvg==",
+      "requires": {
+        "binpack": "~0"
+      }
+    },
+    "parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+    },
+    "path-to-regexp": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+      "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+    },
+    "proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "requires": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      }
+    },
+    "qs": {
+      "version": "6.9.6",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.6.tgz",
+      "integrity": "sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ=="
+    },
+    "range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
+    },
+    "raw-body": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz",
+      "integrity": "sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ==",
+      "requires": {
+        "bytes": "3.1.1",
+        "http-errors": "1.8.1",
+        "iconv-lite": "0.4.24",
+        "unpipe": "1.0.0"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "send": {
+      "version": "0.17.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
+      "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
+      "requires": {
+        "debug": "2.6.9",
+        "depd": "~1.1.2",
+        "destroy": "~1.0.4",
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "0.5.2",
+        "http-errors": "1.8.1",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "~2.3.0",
+        "range-parser": "~1.2.1",
+        "statuses": "~1.5.0"
+      },
+      "dependencies": {
+        "ms": {
+          "version": "2.1.3",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+          "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+        }
+      }
+    },
+    "serve-static": {
+      "version": "1.14.2",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
+      "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
+      "requires": {
+        "encodeurl": "~1.0.2",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "0.17.2"
+      }
+    },
+    "setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+    },
+    "statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+    },
+    "toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
+    },
+    "type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "requires": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      }
+    },
+    "unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+    },
+    "utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+    },
+    "vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+    }
+  }
+}

+ 16 - 0
emitter/package.json

@@ -0,0 +1,16 @@
+{
+  "name": "http-keep-alive",
+  "version": "0.1.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "t1st3 <contact@t1st3.com> (https://t1st3.com/)",
+  "license": "MIT",
+  "dependencies": {
+    "body-parser": "^1.19.0",
+    "express": "^4.17.1",
+    "node-osc": "^6.1.11"
+  }
+}

+ 45 - 0
emitter/readme.md

@@ -0,0 +1,45 @@
+# objets tangibles - emitter
+
+> Application basée sur Node.js et Express 4 qui envoie des datagrammes TUIO en fonction des touch-events récupérés sur une page web
+
+## Description
+
+Le projet fourni est composé de 2 parties:
+* un backend
+* un frontend
+
+Le backend est une application Node.js basée sur Express; il officie comme émetteur de données TUIO qui sont récupérées à chaque touch-event.
+
+Le frontend est une page web, servie par le backend, qui envoie les touch-events au backend via requête HTTP.
+
+Chaque partie est contenue dans le dossier correspondant.
+
+
+## Pré-requis
+
+* Installer Node.js (v16 ou v17)
+* Installer les dépendances:
+
+```sh
+cd receiver
+npm install
+```
+
+
+## Lancer le serveur
+
+Dans le dossier du projet, lancer la commande suivante:
+
+```sh
+node ./backend/server.js
+```
+
+* La page du frontend sera accessible à l'adresse http://localhost:5001
+* Le serveur envoie les messages TUIO sur le port UDP 3333.
+
+
+## Gestion de la "section" d'identifiants de points
+
+Utiliser le paramètre d'URL `section` pour déterminer la "tranche" dans laquelle les identifiants de points seront compris, par exemple: http://localhost:5001?section=10000
+
+Idéalement, ne pas utiliser des centaines, après quelques minutes de tests, des identifiants supérieurs à 200 pouvaient déjà être notés, d'où l'exemple avec 10000.

+ 12 - 0
receiver/.editorconfig

@@ -0,0 +1,12 @@
+root = true
+
+[*]
+indent_style = tab
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.yml,*.yaml]
+indent_style = space
+indent_size = 2

+ 3 - 0
receiver/.gitattributes

@@ -0,0 +1,3 @@
+* text=auto
+*.js text eol=lf
+

+ 4 - 0
receiver/.gitignore

@@ -0,0 +1,4 @@
+node_modules
+yarn.lock
+.nyc_output
+coverage

+ 110 - 0
receiver/backend/osc-parser.js

@@ -0,0 +1,110 @@
+// OSC parsing based on node-osc
+
+module.exports = (function() {
+    var jspack = require("jspack").jspack,
+
+    decode = function(data) {
+        var message = [],
+        address = decodeString(data);
+        data = address.data;
+
+        if (address.value === "#bundle") {
+            data = decodeBundle(data, message);
+        } else if (data.length > 0) {
+            data = decodeMessage(address, data, message);
+        }
+
+        return message;
+    },
+
+    decodeBundle = function(data, message) {
+        var time = decodeTime(data),
+        bundleSize,
+        content;
+        
+        data = time.data;
+
+        message.push("#bundle");
+        message.push(time.value);
+
+        while (data.length > 0) {
+            bundleSize = decodeInt(data);
+            data = bundleSize.data;
+
+            content = data.slice(0, bundleSize.value);
+            message.push(decode(content));
+
+            data = data.slice(bundleSize.value, data.length);
+        }
+
+        return data;
+    },
+
+    decodeMessage = function(address, data, message) {
+        message.push(address.value);
+
+        var typeTags = decodeString(data);
+        data = typeTags.data;
+        typeTags = typeTags.value;
+
+        if (typeTags[0] === ",") {
+            for (var i = 1; i < typeTags.length; i++) {
+                var arg = decodeByTypeTag(typeTags[i], data);
+                data = arg.data;
+                message.push(arg.value);
+            }
+        }
+
+        return data;
+    },
+
+    decodeByTypeTag = function(typeTag, data) {
+        switch (typeTag) {
+            case "i":
+                return decodeInt(data);
+            case "f":
+                return decodeFloat(data);
+            case "s":
+                return decodeString(data);
+        }
+    },
+
+    decodeInt = function(data) {
+        return {
+            value: jspack.Unpack(">i", data.slice(0, 4))[0],
+            data: data.slice(4)
+        };
+    },
+
+    decodeString = function(data) {
+        var end = 0;
+        while (data[end] && end < data.length) {
+            end++;
+        }
+        return {
+            value: data.toString("ascii", 0, end),
+            data: data.slice(Math.ceil((end + 1) / 4) * 4)
+        };
+    },
+
+    decodeFloat = function(data) {
+        return {
+            value: jspack.Unpack(">f", data.slice(0, 4))[0],
+            data: data.slice(4)
+        };
+    },
+
+    decodeTime = function(data) {
+        var time = jspack.Unpack(">LL", data.slice(0, 8)),
+        seconds = time[0],
+        fraction = time[1];
+        return {
+            value: seconds + fraction / 4294967296,
+            data: data.slice(8)
+        };
+    };
+
+    return {
+        decode: decode
+    };
+}());

+ 64 - 0
receiver/backend/server.js

@@ -0,0 +1,64 @@
+const express = require('express');
+const dgram = require('dgram');
+const oscParser = require('./osc-parser');
+const bodyParser = require('body-parser');
+const path = require('path');
+const app = express();
+
+const server = require('http').Server(app);
+const io = require('socket.io')(server, {
+	transports: ['websocket']
+});
+
+const onSocketListening = function() {
+	const address = udpSocket.address();
+	console.log("Serveur TUIO en écoute sur : " + address.address + ":" + address.port);
+};
+
+const onSocketConnection = function(socket) {
+	udpSocket.on("message", function(msg) {
+		socket.emit("osc", oscParser.decode(msg));
+	});
+};
+
+const udpSocket = dgram.createSocket('udp4');
+udpSocket.on('listening', onSocketListening);
+udpSocket.bind(3333, '127.0.0.1');
+
+//io.listen(server);
+
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({extended:true}));
+app.use(express.static('./frontend/assets'));
+
+app.get('/', function (req, res) {
+   res.sendFile('./frontend/index.html', { root: path.resolve(__dirname + '/..') })
+});
+
+app.get('/json', function (req, res) {
+   res.status(200).json({"message":"ok"})
+});
+
+io.sockets.on('connection', (socket) =>{
+   console.log(`Connecté au client ${socket.id}`);
+	 const dgramCallback = function (buf) {
+		console.log(oscParser.decode(buf));
+    socket.emit("osc", oscParser.decode(buf));
+  };
+
+  // forward UDP packets via socket.io
+  udpSocket.on("message", dgramCallback);
+
+  // prevent memory leak on disconnect
+  socket.on('disconnect', function (socket) {
+    udpSocket.removeListener('message', dgramCallback);
+  });
+});
+
+//io.sockets.on("connection", onSocketConnection);
+
+// on change app par server
+server.listen(5000, function () {
+ console.log('App frontend disponible sur localhost:5000 !')
+});
+

文件差異過大導致無法顯示
+ 9404 - 0
receiver/frontend/assets/jquery-1.7.2.js


文件差異過大導致無法顯示
+ 2976 - 0
receiver/frontend/assets/lodash.js


文件差異過大導致無法顯示
+ 4240 - 0
receiver/frontend/assets/socket.io/socket.io.js


文件差異過大導致無法顯示
+ 1144 - 0
receiver/frontend/assets/tuio.js


+ 118 - 0
receiver/frontend/index.html

@@ -0,0 +1,118 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width">
+	<title>Proof of concept - Objets tangibles</title>
+</head>
+
+<body>
+	<script src="/jquery-1.7.2.js"></script>
+	<script src="/lodash.js"></script>
+	<script src="/socket.io/socket.io.js"></script>
+	<script src="/tuio.js"></script>
+	<style>
+		body {
+			background: #000;
+			margin: 0;
+		}
+		.tuioCursor {
+			background: #fff;
+			height: 8px;
+			left: 0;
+			position: absolute;
+			top: 0;
+			width: 8px;
+		}
+	</style>
+
+	<script>
+				function getHypotenuse(touch1, touch2, screenW, screenH) {
+					var w = Math.abs(touch1.w - touch2.w);
+					var h = Math.abs(touch1.h - touch2.h);
+					return Math.sqrt(Math.pow(w, 2) + Math.pow(h, 2));
+				}
+				$(function() {
+						var client = new Tuio.Client({
+								host: 'http://localhost:5000'
+						}),
+						screenW = $(document).width(),
+						screenH = $(document).height()
+
+						cursors = {},
+
+						onConnect = function() {
+							console.log('connected');
+						},
+
+						onAddTuioCursor = function(addCursor) {
+							var $addCursor = $('<div class="tuioCursor"></div>');
+							$('body').append($addCursor);
+							cursors[addCursor.getCursorId()] = $addCursor;
+							onUpdateTuioCursor(addCursor);
+						},
+
+						onUpdateTuioCursor = function(updateCursor) {
+							var $updateCursor = cursors[updateCursor.getCursorId()];
+							$updateCursor.css({
+								left: updateCursor.getScreenX(screenW),
+								top: updateCursor.getScreenY(screenH)
+							});
+
+							var cursorIds = Object.keys(cursors);
+							var dots = [];
+							cursorIds.forEach(function(cursor) {
+								dots.push({
+									id: cursor,
+									w: cursors[cursor][0].offsetLeft,
+									h: cursors[cursor][0].offsetTop
+								});
+							});
+
+							var groups = {};
+							for (var i = 0; i < dots.length; i++) {
+								for (var j = 0; j < dots.length; j++) {
+									if (j !== i) {
+										var hyp = getHypotenuse(dots[i], dots[j]);
+										console.log(i, j, hyp);
+									}
+								}
+							}
+						},
+
+						onRemoveTuioCursor = function(removeCursor) {
+							//console.log('remove', removeCursor);
+							var $removeCursor = cursors[removeCursor.getCursorId()];
+							$removeCursor.remove();
+							delete[removeCursor.getCursorId()];
+						},
+
+						onAddTuioObject = function(addObject) {
+							console.log(addObject);
+						},
+
+						onUpdateTuioObject = function(updateObject) {
+							console.log(updateObject);
+						},
+
+						onRemoveTuioObject = function(removeObject) {
+							console.log(removeObject);
+						},
+
+						onRefresh = function(time) {
+
+						};
+
+						client.on("connect", onConnect);
+						client.on("addTuioCursor", onAddTuioCursor);
+						client.on("updateTuioCursor", onUpdateTuioCursor);
+						client.on("removeTuioCursor", onRemoveTuioCursor);
+						client.on("addTuioObject", onAddTuioObject);
+						client.on("updateTuioObject", onUpdateTuioObject);
+						client.on("removeTuioObject", onRemoveTuioObject);
+						client.on("refresh", onRefresh);
+						client.connect();
+					});
+		</script>
+</body>
+</html>

+ 10 - 0
receiver/license

@@ -0,0 +1,10 @@
+MIT License
+
+Copyright (c) t1st3 <contact@t1st3.com> (t1st3.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+

文件差異過大導致無法顯示
+ 1262 - 0
receiver/package-lock.json


+ 13 - 0
receiver/package.json

@@ -0,0 +1,13 @@
+{
+  "name": "poc-objets-tangibles",
+  "version": "0.1.0",
+  "description": "",
+  "main": "index.js",
+  "dependencies": {
+    "body-parser": "^1.19.0",
+    "dgram": "^1.0.1",
+    "express": "^4.17.1",
+    "jspack": "0.0.4",
+    "socket.io": "^4.3.2"
+  }
+}

+ 61 - 0
receiver/readme.md

@@ -0,0 +1,61 @@
+# objets tangibles - receiver
+
+> Application basée sur Node.js et Express 4 qui reçoit des datagrammes TUIO et les transmet à une page web via web-sockets
+
+## Description
+
+Le projet fourni est composé de 2 parties:
+* un backend
+* un frontend
+
+Le backend est une application Node.js basée sur Express; il officie comme récepteur de données TUIO et les renvoie vers le frontend au moyen d'un web-socket.
+
+Le frontend est une page web, servie par le backend, qui reçoit les événements émis par le serveur grâce au web-socket.
+
+Chaque partie est contenue dans le dossier correspondant.
+
+
+## Pré-requis
+
+* Installer Node.js (v16 ou v17)
+* Installer les dépendances:
+
+```sh
+cd receiver
+npm install
+```
+
+
+## Lancer le serveur
+
+Dans le dossier du projet, lancer la commande suivante:
+
+```sh
+node ./backend/server.js
+```
+
+* La page du frontend sera accessible à l'adresse http://localhost:5000
+* Le serveur écoute les messages TUIO sur le port UDP 3333.
+
+## Tester la réception de messages TUIO
+
+On utilise le simulateur TUIO pour envoyer des messages. Par défaut, celui-ci émet également sur le port UDP 3333.
+Ce simulateur a été inclus à la racine du projet.
+
+Sur GNU/linux, le simulateur peut être lancé avec la commande suivante:
+
+```sh
+java -Djavax.accessibility.assistive_technologies=" " -jar TuioSimulator.jar
+```
+
+Note: le laguage Java doit être installé sur la machine.
+
+
+
+## État des lieux
+
+Lors de la réception des messages TUIO, la page web calcule la distance entre chaque point enregistré (via théorème de Pythagore), et l'affiche en console (!! la console du navigateur, accessible via F12 sur la page web).
+
+C'est à partir de ces distances entre points qu'on pourra déterminer si ils font partie d'un même groupe. Cette partie là n'est finalement pas fournie ce soir...
+
+