signalk-polar-performance-plugin 0.0.56 → 0.0.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-polar-performance-plugin",
3
- "version": "0.0.56",
3
+ "version": "0.0.58",
4
4
  "description": "A plugin that calculates performance information based on a (CSV) polar diagram.",
5
5
  "main": "plugin/index.js",
6
6
  "scripts": {
package/plugin/index.js CHANGED
@@ -243,89 +243,132 @@ module.exports = function (app) {
243
243
  }
244
244
 
245
245
  if (BSP) {
246
- addValue('performance.boatSpeedDamped', BSP, { units: 'm/s' })
246
+ addValue('performance.boatSpeedDamped', BSP, {
247
+ units: 'm/s',
248
+ description: 'Boat speed after applying damping factor (see Polar Performance Plugin settings).'
249
+ })
247
250
  }
248
251
 
249
252
  if (TWA) {
250
253
  addValue('environment.wind.angleTrueWaterDamped', TWA * port, {
251
- units: 'rad'
254
+ units: 'rad',
255
+ description: 'True Wind Angle after applying damping factor, negative to port (see Polar Performance Plugin settings).'
252
256
  })
253
257
  }
254
258
 
255
259
  if (typeof perfObj.beatAngle !== 'undefined') {
256
260
  if (options.beatAngle === true) {
257
- addValue('performance.beatAngle', perfObj.beatAngle * port)
261
+ addValue('performance.beatAngle', perfObj.beatAngle * port, {
262
+ units: 'rad',
263
+ description: 'The optimal beat/upwind angle for current TWS, negative to port.'
264
+ })
258
265
  }
259
266
  if (options.targetTWA === true) {
260
- addValue('performance.targetAngle', perfObj.beatAngle * port)
267
+ addValue('performance.targetAngle', perfObj.beatAngle * port, {
268
+ units: 'rad',
269
+ description: 'The combined and automatic switching optimal beat or run angle for current TWS, negative to port.'
270
+ })
261
271
  }
262
272
  }
263
273
 
264
- if (typeof perfObj.beatVMG !== 'undefined') {
265
- addValue('performance.beatAngleVelocityMadeGood', perfObj.beatVMG)
274
+ if (typeof perfObj.runAngle !== 'undefined') {
275
+ if (options.beatAngle === true) {
276
+ addValue('performance.gybeAngle', perfObj.runAngle * port, {
277
+ units: 'rad',
278
+ description: 'The optimal run/downwind angle for current TWS, negative to port.'
279
+ })
280
+ }
266
281
  if (options.targetTWA === true) {
267
- addValue('performance.targetVelocityMadeGood', perfObj.beatVMG, {
268
- units: 'm/s'
282
+ addValue('performance.targetAngle', perfObj.runAngle * port, {
283
+ units: 'rad',
284
+ description: 'The combined and automatic switching optimal beat or run angle for current TWS, negative to port.'
269
285
  })
270
286
  }
271
287
  }
272
288
 
273
- if (typeof perfObj.runAngle !== 'undefined') {
274
- if (options.beatAngle === true) {
275
- addValue('performance.gybeAngle', perfObj.runAngle * port)
276
- }
289
+ if (typeof perfObj.beatVMG !== 'undefined') {
290
+ addValue('performance.beatAngleVelocityMadeGood', perfObj.beatVMG, {
291
+ units: 'm/s',
292
+ description: 'The beat/upwind Velocity Made Good for current boat speed and heading.'
293
+ })
277
294
  if (options.targetTWA === true) {
278
- addValue('performance.targetAngle', perfObj.runAngle * port)
295
+ addValue('performance.targetVelocityMadeGood', perfObj.beatVMG, {
296
+ units: 'm/s',
297
+ description: 'The combined and automatic switching beat or run Velocity Made Good for current boat speed and heading.'
298
+ })
279
299
  }
280
300
  }
281
301
 
282
302
  if (typeof perfObj.runVMG !== 'undefined') {
283
- addValue('performance.gybeAngleVelocityMadeGood', perfObj.runVMG)
303
+ addValue('performance.gybeAngleVelocityMadeGood', perfObj.runVMG, {
304
+ units: 'm/s',
305
+ description: 'The run/downwind Velocity Made Good for current boat speed and heading.'
306
+ })
284
307
  if (options.targetTWA === true) {
285
308
  addValue('performance.targetVelocityMadeGood', perfObj.runVMG, {
286
- units: 'rad'
309
+ units: 'm/s',
310
+ description: 'The combined and automatic switching beat or run Velocity Made Good for current TWS and TWA.'
287
311
  })
288
312
  }
289
313
  }
290
314
 
291
315
  if (typeof perfObj.optimumWindAngle !== 'undefined') {
292
316
  addValue('performance.optimumWindAngle', perfObj.optimumWindAngle, {
293
- units: 'rad'
317
+ units: 'rad',
318
+ description: 'The optimum wind angle, negative to port (diff between TWA and environment.wind.directionTrue).'
294
319
  })
295
320
  }
296
321
 
297
322
  if (typeof perfObj.targetSpeed !== 'undefined') {
298
- addValue('performance.targetSpeed', perfObj.targetSpeed)
323
+ addValue('performance.targetSpeed', perfObj.targetSpeed, {
324
+ units: 'm/s',
325
+ description: 'Target boat speed based on current TWA.'
326
+ }
327
+ )
299
328
  }
300
329
 
301
330
  if (typeof perfObj.polarSpeed !== 'undefined') {
302
- addValue('performance.polarSpeed', perfObj.polarSpeed)
303
- addValue('performance.polarSpeedRatio', perfObj.polarSpeedRatio)
331
+ addValue('performance.polarSpeed', perfObj.polarSpeed, {
332
+ units: 'm/s',
333
+ description: 'The polar chart boat speed as per the polar chart for current TWS and TWA.'
334
+ })
335
+ addValue('performance.polarSpeedRatio', perfObj.polarSpeedRatio, {
336
+ units: 'ratio',
337
+ description: 'The ratio between actual boat speed and polar chart boat speed for current TWS and TWA.'
338
+ })
304
339
  if (typeof perfObj.velocityMadeGood !== 'undefined') {
305
- addValue('performance.velocityMadeGood', perfObj.velocityMadeGood)
306
- addValue(
307
- 'performance.polarVelocityMadeGood',
308
- perfObj.polarVelocityMadeGood,
309
- { units: 'm/s' }
310
- )
311
- addValue(
312
- 'performance.polarVelocityMadeGoodRatio',
313
- perfObj.polarVelocityMadeGoodRatio,
314
- { units: 'ratio' }
315
- )
340
+ addValue('performance.velocityMadeGood', perfObj.velocityMadeGood, {
341
+ units: 'm/s',
342
+ description: 'The actual Velocity Made Good based on current heading and boat speed.'
343
+ })
344
+ addValue('performance.polarVelocityMadeGood', perfObj.polarVelocityMadeGood, {
345
+ units: 'm/s',
346
+ description: 'The polar chart Velocity Made Good indicated in the polar for current TWS and TWA.'
347
+ })
348
+ addValue('performance.polarVelocityMadeGoodRatio', perfObj.polarVelocityMadeGoodRatio, {
349
+ units: 'ratio',
350
+ description: 'The ratio between actual Velocity Made Good and polar chart indicated Velocity Made Good for current TWS and TWA.'
351
+ })
316
352
  }
317
353
  }
318
354
 
319
355
  if (typeof perfObj.maxSpeed !== 'undefined') {
320
- addValue('performance.maxSpeed', perfObj.maxSpeed, { units: 'm/s' })
356
+ addValue('performance.maxSpeed', perfObj.maxSpeed, {
357
+ units: 'm/s',
358
+ description: 'Maximum boat speed as per polar chart.'
359
+ })
321
360
  addValue('performance.maxSpeedAngle', perfObj.maxSpeedAngle, {
322
- units: 'rad'
361
+ units: 'rad',
362
+ description: 'The angle to achieve maximum boat speed (not the VMG), based on current TWS, negative to port.'
323
363
  })
324
364
  }
325
365
 
326
366
  if (options.tackTrue === true) {
327
367
  if (typeof perfObj.tackTrue !== 'undefined') {
328
- addValue('performance.tackTrue', perfObj.tackTrue)
368
+ addValue('performance.tackTrue', perfObj.tackTrue, {
369
+ units: 'rad',
370
+ description: 'The Opposite Tack\'s heading relative to True North, based on current TWS.'
371
+ })
329
372
  }
330
373
  }
331
374
 
package/public/index.html CHANGED
@@ -1,51 +1,179 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
- <script type='text/javascript' src="./Chart.min.js"></script>
4
- <script type='text/javascript' src='/jquery/dist/jquery.min.js'></script>
5
- <title>Polar performance</title>
6
- <body style="background-color:#020202;">
3
+ <head>
4
+ <script type='text/javascript' src="./Chart.min.js"></script>
5
+ <script type='text/javascript' src="./jquery-3.7.1-min.js"></script>
6
+ <title>Polar performance</title>
7
+ <style>
8
+ body { background-color: #020202; margin: 0; padding: 0; color: white; font-family: Arial, sans-serif; }
9
+
10
+ .chart-container {
11
+ width: 100%;
12
+ max-width: 1200px;
13
+ margin: 0 auto; /* Center the whole thing */
14
+ }
15
+
16
+ /* New Toolbar Styling - sits below the chart */
17
+ .config-toolbar {
18
+ display: flex;
19
+ justify-content: flex-end; /* Aligns content to the right */
20
+ align-items: center;
21
+ padding: 10px 20px;
22
+ background-color: #0a0a0a; /* Slightly lighter than background */
23
+ border-top: 1px solid #222;
24
+ }
25
+
26
+ .config-toolbar label { font-size: 14px; color: #ccc; margin-right: 8px; }
27
+
28
+ .config-toolbar input {
29
+ width: 50px;
30
+ background: rgba(0, 0, 0, 0.5);
31
+ border: 1px solid #555;
32
+ color: white;
33
+ text-align: center;
34
+ border-radius: 2px;
35
+ padding: 4px;
36
+ font-size: 14px;
37
+ }
38
+ </style>
39
+ </head>
40
+ <body>
7
41
 
8
- <canvas id="myChart" style="width:100%;max-width:1200px"></canvas>
42
+ <div class="chart-container">
43
+ <canvas id="myChart"></canvas>
44
+
45
+ <!-- Configuration Toolbar (Now below the chart) -->
46
+ <div class="config-toolbar">
47
+ <label for="labelPos">Place polar wind speed label at TWA:</label>
48
+ <input type="number" id="labelPos" value="135" min="0" max="180" onchange="updateLabelPos(this.value)">
49
+ </div>
50
+ </div>
9
51
 
10
52
  <script>
11
53
 
12
- var polarSpeed, boatSpeed, TWA
54
+ var polarSpeed, boatSpeed, TWA, TWS
13
55
  var myChart, chartData
14
56
 
57
+ // GLOBAL CONFIG: Load from storage or default to 135
58
+ window.labelTWA = localStorage.getItem('polarLabelTWA') || 135;
59
+ document.getElementById('labelPos').value = window.labelTWA;
60
+
61
+ function updateLabelPos(val) {
62
+ window.labelTWA = val;
63
+ localStorage.setItem('polarLabelTWA', val);
64
+ if (myChart) myChart.update();
65
+ }
66
+
67
+ // --- CUSTOM PLUGIN: DRAW LABELS ---
68
+ Chart.plugins.register({
69
+ afterDraw: function(chartInstance) {
70
+ try {
71
+ var ctx = chartInstance.chart.ctx;
72
+ var targetAngle = parseFloat(window.labelTWA) || 135;
73
+
74
+ // 1. DRAW LABELS
75
+ chartInstance.data.datasets.forEach(function(dataset, i) {
76
+ if (i < 2) return;
77
+
78
+ var meta = chartInstance.getDatasetMeta(i);
79
+ if (meta.hidden) return;
80
+
81
+ var p1 = null, p2 = null;
82
+ var p1Meta = null, p2Meta = null;
83
+
84
+ // Find interpolation bracket
85
+ for (var j = 0; j < dataset.data.length - 1; j++) {
86
+ if (dataset.data[j].x <= targetAngle && dataset.data[j+1].x >= targetAngle) {
87
+ p1 = dataset.data[j];
88
+ p2 = dataset.data[j+1];
89
+ p1Meta = meta.data[j]._model;
90
+ p2Meta = meta.data[j+1]._model;
91
+ break;
92
+ }
93
+ }
94
+
95
+ var x, y;
96
+ if (p1 && p2) {
97
+ var ratio = (p2.x - p1.x === 0) ? 0 : (targetAngle - p1.x) / (p2.x - p1.x);
98
+ x = p1Meta.x + (p2Meta.x - p1Meta.x) * ratio;
99
+ y = p1Meta.y + (p2Meta.y - p1Meta.y) * ratio;
100
+ } else {
101
+ // Fallback
102
+ var closest = null, minDiff = 100;
103
+ dataset.data.forEach(function(d, idx) {
104
+ var diff = Math.abs(d.x - targetAngle);
105
+ if (diff < minDiff) { minDiff = diff; closest = meta.data[idx]._model; }
106
+ });
107
+ if (closest) { x = closest.x; y = closest.y; }
108
+ }
109
+
110
+ // Draw only if on screen and reasonably close to request
111
+ if (x > 0 && y > 0) {
112
+ ctx.save();
113
+ ctx.font = "bold 14px Arial";
114
+ ctx.textAlign = "center";
115
+ ctx.textBaseline = "middle";
116
+ var labelText = String(dataset.label);
117
+
118
+ // Box
119
+ var textWidth = ctx.measureText(labelText).width;
120
+ ctx.fillStyle = "rgba(2,2,2,0.9)";
121
+ ctx.fillRect(x - (textWidth/2) - 3, y - 9, textWidth + 6, 18);
122
+
123
+ // Text
124
+ ctx.fillStyle = dataset.borderColor || '#ffffff';
125
+ ctx.fillText(labelText, x, y);
126
+ ctx.restore();
127
+ }
128
+ });
129
+
130
+ // 2. REDRAW DOTS (Datasets 0 and 1)
131
+ [0, 1].forEach(function(i) {
132
+ var meta = chartInstance.getDatasetMeta(i);
133
+ if (!meta.hidden) {
134
+ meta.data.forEach(function(point) {
135
+ point.draw();
136
+ });
137
+ }
138
+ });
139
+
140
+ } catch (err) {
141
+ console.log("Label draw error: " + err);
142
+ }
143
+ }
144
+ });
145
+
15
146
  $.getJSON("/plugins/signalk-polar-performance-plugin/chartData", function(json) {
16
147
 
17
- // Add actual values dots
148
+ // 1. Add actual values dots to the BEGINNING (Indices 0 and 1)
18
149
  json.datasets.unshift({
19
150
  label: 'Polar Speed',
20
151
  backgroundColor: 'rgba(0,255,0,0.7)',
21
152
  borderColor: 'rgba(0,255,0,0.2)',
22
153
  borderWidth: 1,
23
- radius: 30,
24
- type: 'bubble',
25
- data: [
26
- {
27
- y: 0,
28
- x: 0,
29
- r: 10
30
- }
31
- ]
154
+ radius: 10,
155
+ pointStyle: 'circle',
156
+ data: [{ y: 0, x: 0, r: 10 }]
32
157
  },
33
158
  {
34
159
  label: 'Boat Speed',
35
160
  backgroundColor: 'rgba(0,150,255,0.7)',
36
161
  borderColor: 'rgba(0,150,255,0.2)',
37
162
  borderWidth: 1,
38
- radius: 30,
39
- type: 'bubble',
40
- data: [
41
- {
42
- y: 0,
43
- x: 0,
44
- r: 10
45
- }
46
- ]
163
+ radius: 10,
164
+ pointStyle: 'circle',
165
+ data: [{ y: 0, x: 0, r: 10 }]
47
166
  })
48
167
 
168
+ // 2. BACKUP ORIGINAL COLORS
169
+ for (var i = 2; i < json.datasets.length; i++) {
170
+ if (!json.datasets[i].borderColor) {
171
+ json.datasets[i].borderColor = 'rgba(100, 100, 100, 0.5)';
172
+ }
173
+ json.datasets[i]._originalColor = json.datasets[i].borderColor;
174
+ json.datasets[i].borderWidth = 1;
175
+ }
176
+
49
177
  chartData = json
50
178
  showChart(chartData)
51
179
  })
@@ -65,25 +193,8 @@ function showChart (data) {
65
193
  },
66
194
  spanGaps: true,
67
195
  scales: {
68
- x: {
69
- type: 'linear',
70
- ticks: {
71
- color: 'white',
72
- font: {
73
- size: 14
74
- }
75
- }
76
- },
77
- y: {
78
- type: 'linear',
79
- ticks: {
80
- color: 'white',
81
- font: {
82
- size: 14
83
- }
84
- }
85
- },
86
196
  xAxes: [{
197
+ type: 'linear',
87
198
  scaleLabel: {
88
199
  display: true,
89
200
  labelString: 'True wind angle (TWA)',
@@ -101,6 +212,7 @@ function showChart (data) {
101
212
  }
102
213
  }],
103
214
  yAxes: [{
215
+ type: 'linear',
104
216
  scaleLabel: {
105
217
  display: true,
106
218
  labelString: 'Target boat speed (POL SPD)',
@@ -122,8 +234,6 @@ function showChart (data) {
122
234
  enabled: true,
123
235
  callbacks: {
124
236
  label: function (tooltipItems, data) {
125
- console.log("tooltipItems: " + JSON.stringify(tooltipItems))
126
- console.log("data: " + data)
127
237
  let label = tooltipItems.xLabel + "° " + tooltipItems.yLabel + " kts"
128
238
  return label
129
239
  }
@@ -148,74 +258,61 @@ function connect () {
148
258
  console.log("connect()")
149
259
  ws = new WebSocket((window.location.protocol === 'https:' ? 'wss' : 'ws') + "://" + window.location.host + "/signalk/v1/stream?subscribe=none");
150
260
  ws.onopen = function() {
151
-
152
- // Start listening for value updates
153
261
  startListeners();
154
-
155
262
  ws.onmessage = function(event) {
156
263
  if (event.data.includes('signalk-server')) {
157
- welcomeMessage = event.data;
158
- console.log("Skipping welcome message: " + welcomeMessage)
264
+ console.log("Skipping welcome message")
159
265
  } else {
160
266
  handleData(JSON.parse(event.data));
161
267
  }
162
268
  }
163
-
164
- ws.onclose = function() {
165
- console.log("WebSocket closed")
166
- setTimeout(connect, 500)
167
- }
168
-
169
- ws.onerror = function(err) {
170
- console.log("WebSocket connection error: " + err.message + " - closing connection");
171
- setTimeout(connect, 500)
172
- }
173
-
269
+ ws.onclose = function() { setTimeout(connect, 500) }
270
+ ws.onerror = function(err) { setTimeout(connect, 500) }
174
271
  }
175
272
  }
176
273
 
177
274
  window.addEventListener('focus', function () {
178
275
  if (ws.readyState == 0) {
179
- console.log("Restarting websocket")
180
276
  connect()
181
277
  }
182
278
  })
183
279
 
184
280
  function startListeners () {
185
- var paths = [{'path': 'environment.wind.angleTrueWaterDamped'}, {'path': 'performance.polarSpeed'}, {'path': 'performance.boatSpeedDamped'}]
281
+ var paths = [
282
+ {'path': 'environment.wind.angleTrueWaterDamped'},
283
+ {'path': 'performance.polarSpeed'},
284
+ {'path': 'performance.boatSpeedDamped'},
285
+ {'path': 'environment.wind.speedTrue'}
286
+ ]
186
287
 
187
288
  var subscriptionObject = {
188
289
  "context": "vessels.self",
189
290
  "policy" : "ideal",
190
- "minPeriod": 2000,
291
+ "minPeriod": 1000,
191
292
  "subscribe": paths
192
293
  }
193
294
 
194
- var subscriptionMessage = JSON.stringify(subscriptionObject);
195
- console.log("subscriptionMessage: " + subscriptionMessage);
196
- ws.send(subscriptionMessage);
295
+ ws.send(JSON.stringify(subscriptionObject));
197
296
  }
198
297
 
199
298
  function handleData (data) {
200
- if (typeof data.updates[0].meta != 'undefined') {
201
- return
202
- }
299
+ if (typeof data.updates[0].meta != 'undefined') return
300
+
203
301
  var path = data.updates[0].values[0].path
204
302
  var value = data.updates[0].values[0].value
205
303
 
206
304
  if (path == 'performance.polarSpeed') {
207
305
  polarSpeed = roundDec(msToKts(value), 1)
208
- // console.log('polarSpeed set to %s', polarSpeed)
209
306
  chartData.datasets[0].data[0].y = polarSpeed
210
307
  } else if (path == 'environment.wind.angleTrueWaterDamped') {
211
308
  TWA = roundDec(Math.abs(radToDeg(value)),1)
212
- // console.log('TWA set to %s', TWA)
213
309
  chartData.datasets[0].data[0].x = TWA
214
310
  chartData.datasets[1].data[0].x = TWA
215
311
  } else if (path == 'performance.boatSpeedDamped') {
216
312
  boatSpeed = roundDec(msToKts(value),1)
217
- // console.log('boatSpeed set to %s', boatSpeed)
218
313
  chartData.datasets[1].data[0].y = boatSpeed
314
+ } else if (path == 'environment.wind.speedTrue') {
315
+ TWS = msToKts(value);
219
316
  }
220
317
  }
221
318
 
@@ -225,7 +322,72 @@ connect()
225
322
  setInterval(updateChart, 300)
226
323
 
227
324
  function updateChart () {
228
- myChart.update()
325
+ if (myChart) {
326
+ interpolateColors(TWS);
327
+ myChart.update();
328
+ }
329
+ }
330
+
331
+ // --- Interpolate Colors ---
332
+ function interpolateColors(tws) {
333
+ if (!tws || !chartData) return;
334
+
335
+ for (var i = 2; i < chartData.datasets.length; i++) {
336
+ var dataset = chartData.datasets[i];
337
+ var lineSpeed = parseFloat(dataset.label);
338
+
339
+ if (isNaN(lineSpeed)) continue;
340
+
341
+ var originalColor = dataset._originalColor;
342
+
343
+ if (!originalColor) {
344
+ originalColor = "rgba(100,100,100,0.5)";
345
+ dataset._originalColor = originalColor;
346
+ }
347
+
348
+ var diff = Math.abs(tws - lineSpeed);
349
+ var influenceRange = 2.5;
350
+ var weight = Math.max(0, 1 - (diff / influenceRange));
351
+
352
+ if (weight < 0.05) {
353
+ dataset.borderColor = originalColor;
354
+ dataset.borderWidth = 1;
355
+ dataset.pointBorderColor = originalColor;
356
+ dataset.pointBackgroundColor = originalColor;
357
+ } else {
358
+ var blended = blendColors(originalColor, "#FFFFFF", weight);
359
+ dataset.borderColor = blended;
360
+ dataset.borderWidth = 1 + (3 * weight);
361
+ dataset.pointBorderColor = blended;
362
+ dataset.pointBackgroundColor = blended;
363
+ }
364
+ }
365
+ }
366
+
367
+ function blendColors(color1, color2, weight) {
368
+ var c1 = parseColor(color1);
369
+ var c2 = parseColor(color2);
370
+ var r = Math.round(c1[0] + (c2[0] - c1[0]) * weight);
371
+ var g = Math.round(c1[1] + (c2[1] - c1[1]) * weight);
372
+ var b = Math.round(c1[2] + (c2[2] - c1[2]) * weight);
373
+ return "rgb(" + r + "," + g + "," + b + ")";
374
+ }
375
+
376
+ function parseColor(input) {
377
+ if (!input) return [100,100,100];
378
+ if (input.substr(0,1) == "#") {
379
+ var col = input.slice(1);
380
+ if (col.length == 3) col = col[0]+col[0]+col[1]+col[1]+col[2]+col[2];
381
+ var int = parseInt(col, 16);
382
+ return [(int >> 16) & 255, (int >> 8) & 255, int & 255];
383
+ }
384
+ else if (input.substr(0,3) == "rgb") {
385
+ var parts = input.match(/(\d+)/g);
386
+ if (parts && parts.length >= 3) {
387
+ return [parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2])];
388
+ }
389
+ }
390
+ return [100,100,100];
229
391
  }
230
392
 
231
393
  function radToDeg(radians) {
@@ -246,4 +408,5 @@ function roundDec (value, decimals) {
246
408
  }
247
409
 
248
410
  </script>
249
-
411
+ </body>
412
+ </html>