signalk-ais-navionics-converter 1.0.2 → 1.0.4
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/README.md +28 -18
- package/ais-encoder.js +556 -66
- package/img/OpenCpn1.png +0 -0
- package/img/OpenCpn2.png +0 -0
- package/index.js +337 -155
- package/package.json +3 -13
- package/public/src_components_PluginConfigurationPanel_jsx.js +1 -1
- package/src/components/PluginConfigurationPanel.jsx +158 -80
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-ais-navionics-converter",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "SignalK plugin to convert AIS data to NMEA 0183 sentences to TCP clients (e.g. Navionics boating app, OpenCpn) and optional to vesselfinder.com",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"url": "https://github.com/formifan2002/signalk-ais-navionics-converter"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"axios": "^1.13.2"
|
|
20
|
+
"axios": "^1.13.2",
|
|
21
|
+
"ws": "^8.18.0"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@babel/core": "^7.23.0",
|
|
@@ -40,16 +41,5 @@
|
|
|
40
41
|
"engines": {
|
|
41
42
|
"node": ">=14.0.0"
|
|
42
43
|
},
|
|
43
|
-
"files": [
|
|
44
|
-
"index.js",
|
|
45
|
-
"ais-encoder.js",
|
|
46
|
-
"package.json",
|
|
47
|
-
"public/",
|
|
48
|
-
"src/",
|
|
49
|
-
"webpack.config.js",
|
|
50
|
-
".babelrc",
|
|
51
|
-
"README.md",
|
|
52
|
-
"LICENSE"
|
|
53
|
-
],
|
|
54
44
|
"signalk-plugin-enabled-by-default": false
|
|
55
45
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
\*****************************************************/
|
|
16
16
|
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
|
17
17
|
|
|
18
|
-
eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"webpack/sharing/consume/default/react/react\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n\nconst PluginConfigurationPanel = ({\n configuration,\n save\n}) => {\n const [config, setConfig] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(configuration || {});\n const [initialConfig, setInitialConfig] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(configuration);\n const [loading, setLoading] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);\n const [status, setStatus] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');\n const [showDialog, setShowDialog] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);\n const [dialogData, setDialogData] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({\n title: '',\n message: '',\n callback: null\n });\n const [ownMMSI, setOwnMMSI] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);\n const translations = {\n de: {\n general: 'Allgemein',\n tcpServer: 'TCP Server',\n filtering: 'Filterung',\n debugging: 'Debugging',\n vesselFinder: 'VesselFinder',\n cloudVessels: 'Cloud Vessels (AISFleet)',\n tcpPort: 'TCP Port:',\n updateInterval: 'Update-Intervall für geänderte Schiffe (Sekunden):',\n tcpResendInterval: 'Update-Intervall für unveränderte Schiffe (Sekunden):',\n skipWithoutCallsign: 'Schiffe ohne Rufzeichen überspringen',\n skipStaleData: 'Schiffe mit alten Daten überspringen',\n staleDataThreshold: 'Schwellenwert für alte Daten (Minuten):',\n staleDataShipname: 'Zeitstempel zum Schiffsnamen hinzufügen ab (Minuten, 0=deaktiviert):',\n minAlarmSOG: 'Minimale SOG für Alarm (m/s):',\n maxMinutesSOGToZero: 'Maximum Minuten vor SOG auf 0 gesetzt (0=keine Korrektur):',\n logDebugDetails: 'Debug alle Schiff-Details',\n logMMSI: 'Filter Debug-Ausgabe für MMSI:',\n logDebugStale: 'Debug alte Schiffe',\n logDebugJSON: 'Debug JSON-Daten',\n logDebugAIS: 'Debug AIS-Daten',\n logDebugSOG: 'Debug Schiffe mit korrigierter SOG',\n vesselFinderEnabled: 'VesselFinder-Weiterleitung aktivieren',\n vesselFinderHost: 'VesselFinder Host:',\n vesselFinderPort: 'VesselFinder UDP Port:',\n vesselFinderUpdateRate: 'VesselFinder Update Rate (Sekunden):',\n cloudVesselsEnabled: 'Schiffe von AISFleet.com einbeziehen',\n cloudVesselsUpdateInterval: 'Cloud Vessels Update-Intervall (Sekunden):',\n cloudVesselsRadius: 'Radius von eigenem Schiff (Seemeilen):',\n save: 'Speichern',\n cancel: 'Abbruch',\n unsavedWarning: 'Es gibt ungespeicherte Änderungen. Wirklich abbrechen?',\n unsavedTitle: 'Ungespeicherte Änderungen',\n yes: 'Ja',\n no: 'Nein'\n },\n en: {\n general: 'General',\n tcpServer: 'TCP Server',\n filtering: 'Filtering',\n debugging: 'Debugging',\n vesselFinder: 'VesselFinder',\n cloudVessels: 'Cloud Vessels (AISFleet)',\n tcpPort: 'TCP Port:',\n updateInterval: 'Update interval for changed vessels (seconds):',\n tcpResendInterval: 'Update interval for unchanged vessels (seconds):',\n skipWithoutCallsign: 'Skip vessels without callsign',\n skipStaleData: 'Skip vessels with stale data',\n staleDataThreshold: 'Stale data threshold (minutes):',\n staleDataShipname: 'Add timestamp to ship name from (minutes, 0=disabled):',\n minAlarmSOG: 'Minimum SOG for alarm (m/s):',\n maxMinutesSOGToZero: 'Maximum minutes before SOG set to 0 (0=no correction):',\n logDebugDetails: 'Debug all vessel details',\n logMMSI: 'Filter Debug MMSI:',\n logDebugStale: 'Debug stale vessels',\n logDebugJSON: 'Debug JSON data',\n logDebugAIS: 'Debug AIS data',\n logDebugSOG: 'Debug vessels with corrected SOG',\n vesselFinderEnabled: 'Enable VesselFinder forwarding',\n vesselFinderHost: 'VesselFinder Host:',\n vesselFinderPort: 'VesselFinder UDP Port:',\n vesselFinderUpdateRate: 'VesselFinder Update Rate (seconds):',\n cloudVesselsEnabled: 'Include vessels from AISFleet.com',\n cloudVesselsUpdateInterval: 'Cloud Vessels update interval (seconds):',\n cloudVesselsRadius: 'Radius from own vessel (nautical miles):',\n save: 'Save',\n cancel: 'Cancel',\n unsavedWarning: 'There are unsaved changes. Really cancel?',\n unsavedTitle: 'Unsaved changes',\n yes: 'Yes',\n no: 'No'\n }\n };\n const [currentLang, setCurrentLang] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(config.language === 'de' ? 'de' : 'en');\n const t = translations[currentLang];\n\n // Hole eigene MMSI beim Laden\n react__WEBPACK_IMPORTED_MODULE_0___default().useEffect(() => {\n const fetchOwnMMSI = async () => {\n try {\n const protocol = window.location.protocol;\n const hostname = window.location.hostname;\n const port = window.location.port;\n const url = `${protocol}//${hostname}${port ? ':' + port : ''}/signalk/v1/api/self`;\n console.log('Fetching own MMSI from:', url);\n console.log('Protocol:', protocol, 'Hostname:', hostname, 'Port:', port);\n const response = await fetch(url);\n console.log('Response status:', response.status);\n if (response.ok) {\n const data = await response.json();\n console.log('Self data received:', JSON.stringify(data, null, 2));\n\n // Data könnte String oder Objekt sein\n let vesselKey = null;\n if (typeof data === 'string') {\n // String Format: \"vessels.urn:mrn:imo:mmsi:211177520\"\n vesselKey = data.replace('vessels.', '');\n console.log('Parsed vessel key from string:', vesselKey);\n } else if (data.vessels && typeof data.vessels === 'object') {\n // Objekt Format\n const mmsiMatch = Object.keys(data.vessels).find(key => key.includes('mmsi:'));\n vesselKey = mmsiMatch;\n console.log('Found vessel key in object:', vesselKey);\n }\n if (vesselKey) {\n const mmsi = vesselKey.match(/mmsi:(\\d+)/)?.[1];\n console.log('Extracted MMSI:', mmsi);\n if (mmsi) {\n setOwnMMSI(mmsi);\n }\n }\n } else {\n console.error('Failed to fetch self data, status:', response.status);\n }\n } catch (err) {\n console.error('Failed to fetch own MMSI:', err);\n // Fehler ignorieren - Validierung wird einfach nicht aktiviert\n }\n };\n\n // Verzögert starten um sicherzustellen dass DOM ready ist\n setTimeout(fetchOwnMMSI, 500);\n }, []);\n const handleConfigChange = (key, value) => {\n setConfig(prev => ({\n ...prev,\n [key]: value\n }));\n };\n const isMMSIInvalid = () => {\n return ownMMSI && config.logMMSI && config.logMMSI === ownMMSI;\n };\n const checkUnsavedChanges = () => {\n return JSON.stringify(config) !== JSON.stringify(initialConfig);\n };\n const handleSave = () => {\n // Validiere Debug MMSI\n if (isMMSIInvalid()) {\n setStatus('error');\n const errorMsg = currentLang === 'de' ? 'Fehler: Sie können nicht die eigene MMSI zum Filtern verwenden!' : 'Error: You cannot use your own MMSI for filtering!';\n alert(errorMsg);\n return;\n }\n setLoading(true);\n if (save) {\n try {\n const result = save(config);\n if (result && typeof result.then === 'function') {\n // save() gibt ein Promise zurück\n result.then(() => {\n setStatus('success');\n setInitialConfig(config);\n setTimeout(() => setStatus(''), 3000);\n }).catch(err => {\n setStatus('error');\n setTimeout(() => setStatus(''), 3000);\n }).finally(() => {\n setLoading(false);\n });\n } else {\n // save() gibt kein Promise zurück - assume success\n setStatus('success');\n setInitialConfig(config);\n setTimeout(() => setStatus(''), 3000);\n setLoading(false);\n }\n } catch (err) {\n console.error('Error in handleSave:', err);\n setStatus('error');\n setTimeout(() => setStatus(''), 3000);\n setLoading(false);\n }\n }\n };\n const handleLanguageChange = lang => {\n setCurrentLang(lang);\n handleConfigChange('language', lang === 'de');\n };\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.container\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.header\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h2\", {\n style: styles.title\n }, \"AIS to NMEA 0183 Converter\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => window.open('https://github.com/formifan2002/signalk-ais-navionics-converter', '_blank'),\n style: styles.helpButton\n }, \"\\u2139\\uFE0F \", currentLang === 'de' ? 'Hilfe' : 'Help')), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.languageSelector\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => handleLanguageChange('de'),\n style: {\n ...styles.langButton,\n ...(currentLang === 'de' ? styles.langButtonActive : {})\n }\n }, \"Deutsch\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => handleLanguageChange('en'),\n style: {\n ...styles.langButton,\n ...(currentLang === 'en' ? styles.langButtonActive : {})\n }\n }, \"English\")), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.tcpServer), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.tcpPort), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n display: 'block',\n fontSize: '0.85em',\n color: '#666',\n marginTop: '8px',\n marginBottom: '12px',\n fontStyle: 'italic',\n lineHeight: '1.4'\n }\n }, currentLang === 'de' ? 'Dieser Port ist später in der Navionics boating app im Menüpunkt \\'Gekoppelte Geräte\\' als TCP Port anzugeben.' : 'This port must be specified later in the Navionics boating app under the menu item \\'Paired Devices\\' as TCP Port.'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n max: \"65535\",\n value: config.tcpPort || 10113,\n onChange: e => handleConfigChange('tcpPort', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.updateInterval), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.updateInterval || 15,\n onChange: e => handleConfigChange('updateInterval', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.tcpResendInterval), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n value: config.tcpResendInterval || 60,\n onChange: e => handleConfigChange('tcpResendInterval', Number(e.target.value)),\n style: styles.input\n }))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.filtering), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.skipWithoutCallsign || false,\n onChange: e => handleConfigChange('skipWithoutCallsign', e.target.checked)\n }), t.skipWithoutCallsign)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.skipStaleData !== false,\n onChange: e => handleConfigChange('skipStaleData', e.target.checked)\n }), t.skipStaleData)), config.skipStaleData !== false && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.staleDataThreshold), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.staleDataThresholdMinutes || 60,\n onChange: e => handleConfigChange('staleDataThresholdMinutes', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.staleDataShipname), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n value: config.staleDataShipnameAddTime || 5,\n onChange: e => handleConfigChange('staleDataShipnameAddTime', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.minAlarmSOG), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n step: \"0.1\",\n value: config.minAlarmSOG || 0.2,\n onChange: e => handleConfigChange('minAlarmSOG', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.maxMinutesSOGToZero), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n value: config.maxMinutesSOGToZero || 0,\n onChange: e => handleConfigChange('maxMinutesSOGToZero', Number(e.target.value)),\n style: styles.input\n }))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.debugging), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugDetails || false,\n onChange: e => handleConfigChange('logDebugDetails', e.target.checked)\n }), t.logDebugDetails)), config.logDebugDetails && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.logMMSI), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n display: 'block',\n fontSize: '0.85em',\n color: '#666',\n marginTop: '8px',\n marginBottom: '12px',\n fontStyle: 'italic',\n lineHeight: '1.4'\n }\n }, currentLang === 'de' ? 'Debug Ausgaben werden nur für das Schiff mit dieser MMSI erzeugt. Für das eigene Schiff / die eigene MMSI werden keine AIS Daten erzeugt. Wenn das Feld leer bleibt, werden Debug-Ausgaben für alle Schiffe (außer dem eigenen) erzeugt.' : 'Debug output is only generated for the vessel with this MMSI. No AIS data is generated for your own vessel / own MMSI. If the field is left empty, debug output is generated for all vessels (except your own).'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"text\",\n value: config.logMMSI || '',\n onChange: e => handleConfigChange('logMMSI', e.target.value),\n placeholder: \"e.g. 123456789\",\n style: {\n ...styles.input,\n ...(isMMSIInvalid() ? {\n borderColor: '#dc3545',\n backgroundColor: '#fff5f5'\n } : {})\n }\n }), isMMSIInvalid() && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n color: '#dc3545',\n fontSize: '0.85em',\n marginTop: '8px',\n fontWeight: '500'\n }\n }, currentLang === 'de' ? '❌ Sie können nicht die eigene MMSI verwenden!' : '❌ You cannot use your own MMSI!')), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugStale || false,\n onChange: e => handleConfigChange('logDebugStale', e.target.checked)\n }), t.logDebugStale)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugJSON || false,\n onChange: e => handleConfigChange('logDebugJSON', e.target.checked)\n }), t.logDebugJSON)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugAIS || false,\n onChange: e => handleConfigChange('logDebugAIS', e.target.checked)\n }), t.logDebugAIS)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugSOG || false,\n onChange: e => handleConfigChange('logDebugSOG', e.target.checked)\n }), t.logDebugSOG)))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.vesselFinder), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.vesselFinderEnabled || false,\n onChange: e => handleConfigChange('vesselFinderEnabled', e.target.checked)\n }), t.vesselFinderEnabled)), config.vesselFinderEnabled && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.vesselFinderHost), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"text\",\n value: config.vesselFinderHost || 'ais.vesselfinder.com',\n onChange: e => handleConfigChange('vesselFinderHost', e.target.value),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.vesselFinderPort), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n max: \"65535\",\n value: config.vesselFinderPort || 5500,\n onChange: e => handleConfigChange('vesselFinderPort', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.vesselFinderUpdateRate), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.vesselFinderUpdateRate || 60,\n onChange: e => handleConfigChange('vesselFinderUpdateRate', Number(e.target.value)),\n style: styles.input\n })))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.cloudVessels), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.cloudVesselsEnabled !== false,\n onChange: e => handleConfigChange('cloudVesselsEnabled', e.target.checked)\n }), t.cloudVesselsEnabled)), config.cloudVesselsEnabled !== false && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.cloudVesselsUpdateInterval), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.cloudVesselsUpdateInterval || 60,\n onChange: e => handleConfigChange('cloudVesselsUpdateInterval', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.cloudVesselsRadius), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.cloudVesselsRadius || 10,\n onChange: e => handleConfigChange('cloudVesselsRadius', Number(e.target.value)),\n style: styles.input\n })))), status && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n ...styles.statusMessage,\n ...(status === 'error' ? styles.error : styles.success)\n }\n }, status === 'success' ? currentLang === 'de' ? 'Konfiguration gespeichert' : 'Configuration saved' : currentLang === 'de' ? 'Fehler beim Speichern' : 'Error saving'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.buttonGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: handleSave,\n disabled: loading || isMMSIInvalid(),\n style: {\n ...styles.button,\n ...styles.primaryButton,\n ...(isMMSIInvalid() ? {\n opacity: 0.5,\n cursor: 'not-allowed'\n } : {})\n }\n }, t.save), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => {\n if (checkUnsavedChanges()) {\n setDialogData({\n title: t.unsavedTitle,\n message: t.unsavedWarning,\n callback: () => setConfig(initialConfig)\n });\n setShowDialog(true);\n } else {\n setConfig(initialConfig);\n }\n },\n style: {\n ...styles.button,\n ...styles.secondaryButton\n }\n }, t.cancel)), showDialog && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.dialog\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.dialogContent\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h4\", {\n style: styles.dialogTitle\n }, dialogData.title), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"p\", null, dialogData.message), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.dialogButtons\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => setShowDialog(false),\n style: {\n ...styles.button,\n ...styles.secondaryButton\n }\n }, t.no), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => {\n if (dialogData.callback) dialogData.callback();\n setShowDialog(false);\n },\n style: {\n ...styles.button,\n ...styles.primaryButton\n }\n }, t.yes)))));\n};\nconst styles = {\n container: {\n padding: '20px',\n fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto'\n },\n header: {\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n marginBottom: '30px',\n paddingBottom: '20px',\n borderBottom: '2px solid #667eea'\n },\n title: {\n margin: 0,\n fontSize: '1.5em',\n fontWeight: '600',\n color: '#333'\n },\n helpButton: {\n padding: '8px 16px',\n backgroundColor: '#667eea',\n color: 'white',\n border: 'none',\n borderRadius: '6px',\n cursor: 'pointer',\n fontWeight: '500',\n fontSize: '0.95em',\n transition: 'background 0.3s'\n },\n languageSelector: {\n display: 'flex',\n gap: '10px',\n marginBottom: '30px'\n },\n langButton: {\n padding: '8px 16px',\n border: '1px solid #ddd',\n borderRadius: '4px',\n cursor: 'pointer',\n fontWeight: '500',\n backgroundColor: '#f5f5f5'\n },\n langButtonActive: {\n backgroundColor: '#667eea',\n color: 'white',\n borderColor: '#667eea'\n },\n section: {\n marginBottom: '30px'\n },\n sectionTitle: {\n fontSize: '1.2em',\n fontWeight: '600',\n marginBottom: '15px',\n color: '#333',\n borderBottom: '2px solid #667eea',\n paddingBottom: '10px'\n },\n formGroup: {\n marginBottom: '15px'\n },\n label: {\n display: 'block',\n fontWeight: '500',\n marginBottom: '5px',\n color: '#333'\n },\n input: {\n padding: '8px',\n border: '1px solid #ddd',\n borderRadius: '4px',\n fontSize: '1em',\n width: '200px'\n },\n checkbox: {\n display: 'flex',\n alignItems: 'center',\n marginBottom: '8px',\n cursor: 'pointer',\n gap: '8px'\n },\n buttonGroup: {\n display: 'flex',\n gap: '10px',\n marginTop: '30px',\n paddingTop: '20px',\n borderTop: '1px solid #ddd'\n },\n button: {\n padding: '10px 20px',\n border: 'none',\n borderRadius: '4px',\n cursor: 'pointer',\n fontWeight: '500',\n fontSize: '1em'\n },\n primaryButton: {\n backgroundColor: '#667eea',\n color: 'white'\n },\n secondaryButton: {\n backgroundColor: '#6c757d',\n color: 'white'\n },\n statusMessage: {\n padding: '12px',\n borderRadius: '4px',\n marginBottom: '15px',\n fontSize: '0.95em'\n },\n success: {\n backgroundColor: '#d4edda',\n color: '#155724'\n },\n error: {\n backgroundColor: '#f8d7da',\n color: '#721c24'\n },\n dialog: {\n position: 'fixed',\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: 'rgba(0,0,0,0.5)',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n zIndex: 1000\n },\n dialogContent: {\n backgroundColor: 'white',\n padding: '30px',\n borderRadius: '8px',\n maxWidth: '400px',\n boxShadow: '0 4px 6px rgba(0,0,0,0.1)'\n },\n dialogTitle: {\n marginTop: 0,\n marginBottom: '15px',\n fontSize: '1.1em'\n },\n dialogButtons: {\n display: 'flex',\n gap: '10px',\n justifyContent: 'flex-end',\n marginTop: '20px'\n }\n};\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (PluginConfigurationPanel);\n\n//# sourceURL=webpack://signalk-ais-navionics-converter/./src/components/PluginConfigurationPanel.jsx?\n}");
|
|
18
|
+
eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ \"webpack/sharing/consume/default/react/react\");\n/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);\n\nconst PluginConfigurationPanel = ({\n configuration,\n save\n}) => {\n const [config, setConfig] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(configuration || {});\n const [initialConfig, setInitialConfig] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(configuration);\n const [loading, setLoading] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);\n const [status, setStatus] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');\n const [showDialog, setShowDialog] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);\n const [dialogData, setDialogData] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)({\n title: '',\n message: '',\n callback: null\n });\n const [ownMMSI, setOwnMMSI] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);\n const [aisfleetEnabled, setAisfleetEnabled] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);\n const [portError, setPortError] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)('');\n const translations = {\n de: {\n general: 'Allgemein',\n tcpServer: 'TCP Server',\n filtering: 'Filterung',\n debugging: 'Debugging',\n vesselFinder: 'VesselFinder',\n cloudVessels: 'Cloud Vessels (AISFleet)',\n tcpPort: 'TCP Port:',\n wsPort: 'WebSocket Port:',\n updateInterval: 'Update-Intervall für geänderte Schiffe (Sekunden):',\n tcpResendInterval: 'Update-Intervall für unveränderte Schiffe (Sekunden):',\n skipWithoutCallsign: 'Schiffe ohne Rufzeichen überspringen',\n skipStaleData: 'Schiffe mit alten Daten überspringen',\n staleDataThreshold: 'Schwellenwert für alte Daten (Minuten):',\n staleDataShipname: 'Zeitstempel zum Schiffsnamen hinzufügen wenn die letzte Positionsmeldung älter ist als x Minuten (0=deaktiviert):',\n minAlarmSOG: 'SOG (und COG) wird auf 0 gesetzt, wenn die Geschwindigkeit kleiner als x Knoten ist (0=deaktiviert):',\n maxMinutesSOGToZero: 'SOG wird auf 0 gesetzt wenn die letzte Positionsmeldung älter ist als x Minuten (0=keine Korrektur):',\n logDebugDetails: 'Debug Schiff-Details',\n logMMSI: 'Filter Debug-Ausgabe nur für MMSI:',\n logDebugStale: 'Debug alte Schiffe',\n logDebugJSON: 'Debug JSON-Daten',\n logDebugAIS: 'Debug AIS-Daten',\n logDebugSOG: 'Debug Schiffe mit korrigierter SOG',\n vesselFinderEnabled: 'VesselFinder-Weiterleitung aktivieren',\n vesselFinderHost: 'VesselFinder Host:',\n vesselFinderPort: 'VesselFinder UDP Port:',\n vesselFinderUpdateRate: 'VesselFinder Update Rate (Sekunden):',\n cloudVesselsEnabled: 'Schiffe von AISFleet.com einbeziehen',\n cloudVesselsUpdateInterval: 'Cloud Vessels Update-Intervall (Sekunden):',\n cloudVesselsRadius: 'Radius von eigenem Schiff (Seemeilen):',\n portError: 'TCP Port und WebSocket Port müssen unterschiedlich sein',\n save: 'Speichern',\n cancel: 'Abbruch',\n unsavedWarning: 'Es gibt ungespeicherte Änderungen. Wirklich abbrechen?',\n unsavedTitle: 'Ungespeicherte Änderungen',\n yes: 'Ja',\n no: 'Nein'\n },\n en: {\n general: 'General',\n tcpServer: 'TCP Server',\n filtering: 'Filtering',\n debugging: 'Debugging',\n vesselFinder: 'VesselFinder',\n cloudVessels: 'Cloud Vessels (AISFleet)',\n tcpPort: 'TCP Port:',\n wsPort: 'WebSocket Port:',\n updateInterval: 'Update interval for changed vessels (seconds):',\n tcpResendInterval: 'Update interval for unchanged vessels (seconds):',\n skipWithoutCallsign: 'Skip vessels without callsign',\n skipStaleData: 'Skip vessels with stale data',\n staleDataThreshold: 'Stale data threshold (minutes):',\n staleDataShipname: 'Add timestamp to vessel name if the last position report is older than x minutes (0=disabled):',\n minAlarmSOG: 'SOG (and COG) is set to 0 if the speed is less than x knots (0=disabled):',\n maxMinutesSOGToZero: 'SOG is set to 0 if the last position report is older than x minutes (0=no correction):',\n logDebugDetails: 'Debug vessel details',\n logMMSI: 'Filter Debug only for MMSI:',\n logDebugStale: 'Debug stale vessels',\n logDebugJSON: 'Debug JSON data',\n logDebugAIS: 'Debug AIS data',\n logDebugSOG: 'Debug vessels with corrected SOG',\n vesselFinderEnabled: 'Enable VesselFinder forwarding',\n vesselFinderHost: 'VesselFinder Host:',\n vesselFinderPort: 'VesselFinder UDP Port:',\n vesselFinderUpdateRate: 'VesselFinder Update Rate (seconds):',\n cloudVesselsEnabled: 'Include vessels from AISFleet.com',\n cloudVesselsUpdateInterval: 'Cloud Vessels update interval (seconds):',\n cloudVesselsRadius: 'Radius from own vessel (nautical miles):',\n portError: 'TCP Port and WebSocket Port must be different',\n save: 'Save',\n cancel: 'Cancel',\n unsavedWarning: 'There are unsaved changes. Really cancel?',\n unsavedTitle: 'Unsaved changes',\n yes: 'Yes',\n no: 'No'\n }\n };\n const [currentLang, setCurrentLang] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(config.language === 'de' ? 'de' : 'en');\n const t = translations[currentLang];\n\n // Hole eigene MMSI beim Laden\n (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {\n const fetchOwnMMSI = async () => {\n try {\n const protocol = window.location.protocol;\n const hostname = window.location.hostname;\n const port = window.location.port;\n const baseUrl = `${protocol}//${hostname}${port ? ':' + port : ''}`;\n const aisfleetUrl = `${baseUrl}/plugins/aisfleet`;\n const url = `${baseUrl}/signalk/v1/api/self`;\n const response = await fetch(url);\n if (response.ok) {\n const data = await response.json();\n let vesselKey = null;\n if (typeof data === 'string') {\n vesselKey = data.replace('vessels.', '');\n } else if (data.vessels && typeof data.vessels === 'object') {\n const mmsiMatch = Object.keys(data.vessels).find(key => key.includes('mmsi:'));\n vesselKey = mmsiMatch;\n }\n if (vesselKey) {\n const mmsi = vesselKey.match(/mmsi:(\\d+)/)?.[1];\n if (mmsi) setOwnMMSI(mmsi);\n }\n }\n\n // AIS Fleet Plugin prüfen\n try {\n const aisResponse = await fetch(aisfleetUrl);\n if (aisResponse.ok) {\n const aisData = await aisResponse.json();\n setAisfleetEnabled(!!aisData.enabled);\n } else {\n setAisfleetEnabled(false);\n }\n } catch (err) {\n setAisfleetEnabled(false);\n }\n } catch (err) {\n console.error('Failed to fetch own MMSI:', err);\n }\n };\n setTimeout(fetchOwnMMSI, 500);\n }, []);\n const handleConfigChange = (key, value) => {\n setConfig(prev => ({\n ...prev,\n [key]: value\n }));\n\n // Überprüfe Port-Nummern\n if (key === 'tcpPort' || key === 'wsPort') {\n const tcpPort = key === 'tcpPort' ? value : config.tcpPort;\n const wsPort = key === 'wsPort' ? value : config.wsPort;\n if (tcpPort && wsPort && tcpPort !== 0 && wsPort !== 0 && tcpPort === wsPort) {\n setPortError(t.portError);\n } else {\n setPortError('');\n }\n }\n };\n const isMMSIInvalid = () => {\n return ownMMSI && config.logMMSI && config.logMMSI === ownMMSI;\n };\n const isPortInvalid = () => {\n const tcpPort = config.tcpPort || 10113;\n const wsPort = config.wsPort || 10114;\n return tcpPort !== 0 && wsPort !== 0 && tcpPort === wsPort;\n };\n const checkUnsavedChanges = () => {\n return JSON.stringify(config) !== JSON.stringify(initialConfig);\n };\n const handleSave = () => {\n // Validiere Debug MMSI\n if (isMMSIInvalid()) {\n setStatus('error');\n const errorMsg = currentLang === 'de' ? 'Fehler: Sie können nicht die eigene MMSI zum Filtern verwenden!' : 'Error: You cannot use your own MMSI for filtering!';\n alert(errorMsg);\n return;\n }\n\n // Validiere Ports\n if (isPortInvalid()) {\n setStatus('error');\n const errorMsg = currentLang === 'de' ? 'Fehler: TCP Port und WebSocket Port müssen unterschiedlich sein!' : 'Error: TCP Port and WebSocket Port must be different!';\n alert(errorMsg);\n return;\n }\n setLoading(true);\n if (save) {\n try {\n const result = save(config);\n if (result && typeof result.then === 'function') {\n result.then(() => {\n setStatus('success');\n setInitialConfig(config);\n setTimeout(() => setStatus(''), 3000);\n }).catch(err => {\n setStatus('error');\n setTimeout(() => setStatus(''), 3000);\n }).finally(() => {\n setLoading(false);\n });\n } else {\n setStatus('success');\n setInitialConfig(config);\n setTimeout(() => setStatus(''), 3000);\n setLoading(false);\n }\n } catch (err) {\n console.error('Error in handleSave:', err);\n setStatus('error');\n setTimeout(() => setStatus(''), 3000);\n setLoading(false);\n }\n }\n };\n const handleLanguageChange = lang => {\n setCurrentLang(lang);\n handleConfigChange('language', lang === 'de');\n };\n return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.container\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.header\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h2\", {\n style: styles.title\n }, \"AIS to NMEA 0183 Converter\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => window.open('https://github.com/formifan2002/signalk-ais-navionics-converter', '_blank'),\n style: styles.helpButton\n }, \"\\u2139\\uFE0F \", currentLang === 'de' ? 'Hilfe' : 'Help')), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.languageSelector\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => handleLanguageChange('de'),\n style: {\n ...styles.langButton,\n ...(currentLang === 'de' ? styles.langButtonActive : {})\n }\n }, \"Deutsch\"), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => handleLanguageChange('en'),\n style: {\n ...styles.langButton,\n ...(currentLang === 'en' ? styles.langButtonActive : {})\n }\n }, \"English\")), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.tcpServer), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.tcpPort), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n display: 'block',\n fontSize: '0.85em',\n color: '#666',\n marginTop: '8px',\n marginBottom: '12px',\n fontStyle: 'italic',\n lineHeight: '1.4'\n }\n }, currentLang === 'de' ? 'Dieser Port ist später in der Navionics boating app im Menüpunkt \\'Gekoppelte Geräte\\' als TCP Port anzugeben.' : 'This port must be specified later in the Navionics boating app under the menu item \\'Paired Devices\\' as TCP Port.'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n max: \"65535\",\n value: config.tcpPort || 10113,\n onChange: e => handleConfigChange('tcpPort', Number(e.target.value)),\n style: {\n ...styles.input,\n ...(portError ? {\n borderColor: '#dc3545',\n backgroundColor: '#fff5f5'\n } : {})\n }\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.wsPort), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n display: 'block',\n fontSize: '0.85em',\n color: '#666',\n marginTop: '8px',\n marginBottom: '12px',\n fontStyle: 'italic',\n lineHeight: '1.4'\n }\n }, currentLang === 'de' ? 'Über diesen Port werden alle AIS Daten als NMEA0183 und alle Schiffsdaten per JOSN als Websocket gesendet (nicht für Navionics relevant). 0=kein Websocket Server.' : 'This port is used to send all AIS data as NMEA0183 and all vessel data as JSON via Websocket (not relevant for Navionics). 0=no Websocket server.'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n max: \"65535\",\n value: config.wsPort || 10114,\n onChange: e => handleConfigChange('wsPort', Number(e.target.value)),\n style: {\n ...styles.input,\n ...(portError ? {\n borderColor: '#dc3545',\n backgroundColor: '#fff5f5'\n } : {})\n }\n }), portError && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n color: '#dc3545',\n fontSize: '0.85em',\n marginTop: '8px',\n fontWeight: '500'\n }\n }, \"\\u26A0\\uFE0F \", portError)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.updateInterval), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.updateInterval || 15,\n onChange: e => handleConfigChange('updateInterval', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.tcpResendInterval), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n value: config.tcpResendInterval || 60,\n onChange: e => handleConfigChange('tcpResendInterval', Number(e.target.value)),\n style: styles.input\n }))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.filtering), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.skipWithoutCallsign || false,\n onChange: e => handleConfigChange('skipWithoutCallsign', e.target.checked)\n }), t.skipWithoutCallsign)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.skipStaleData !== false,\n onChange: e => handleConfigChange('skipStaleData', e.target.checked)\n }), t.skipStaleData)), config.skipStaleData !== false && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.staleDataThreshold), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.staleDataThresholdMinutes || 60,\n onChange: e => handleConfigChange('staleDataThresholdMinutes', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.staleDataShipname), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n value: config.staleDataShipnameAddTime || 5,\n onChange: e => handleConfigChange('staleDataShipnameAddTime', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.minAlarmSOG), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n step: \"0.1\",\n value: config.minAlarmSOG || 0.2,\n onChange: e => handleConfigChange('minAlarmSOG', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.maxMinutesSOGToZero), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"0\",\n value: config.maxMinutesSOGToZero || 0,\n onChange: e => handleConfigChange('maxMinutesSOGToZero', Number(e.target.value)),\n style: styles.input\n }))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.debugging), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugDetails || false,\n onChange: e => handleConfigChange('logDebugDetails', e.target.checked)\n }), t.logDebugDetails)), config.logDebugDetails && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.logMMSI), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n display: 'block',\n fontSize: '0.85em',\n color: '#666',\n marginTop: '8px',\n marginBottom: '12px',\n fontStyle: 'italic',\n lineHeight: '1.4'\n }\n }, currentLang === 'de' ? 'Debug Ausgaben werden nur für das Schiff mit dieser MMSI erzeugt. Für das eigene Schiff / die eigene MMSI werden keine AIS Daten erzeugt. Wenn das Feld leer bleibt, werden Debug-Ausgaben für alle Schiffe (außer dem eigenen) erzeugt.' : 'Debug output is only generated for the vessel with this MMSI. No AIS data is generated for your own vessel / own MMSI. If the field is left empty, debug output is generated for all vessels (except your own).'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"text\",\n value: config.logMMSI || '',\n onChange: e => handleConfigChange('logMMSI', e.target.value),\n placeholder: \"e.g. 123456789\",\n style: {\n ...styles.input,\n ...(isMMSIInvalid() ? {\n borderColor: '#dc3545',\n backgroundColor: '#fff5f5'\n } : {})\n }\n }), isMMSIInvalid() && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n color: '#dc3545',\n fontSize: '0.85em',\n marginTop: '8px',\n fontWeight: '500'\n }\n }, currentLang === 'de' ? '⚠️ Sie können nicht die eigene MMSI verwenden!' : '⚠️ You cannot use your own MMSI!')), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugStale || false,\n onChange: e => handleConfigChange('logDebugStale', e.target.checked)\n }), t.logDebugStale)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugJSON || false,\n onChange: e => handleConfigChange('logDebugJSON', e.target.checked)\n }), t.logDebugJSON)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugAIS || false,\n onChange: e => handleConfigChange('logDebugAIS', e.target.checked)\n }), t.logDebugAIS)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.logDebugSOG || false,\n onChange: e => handleConfigChange('logDebugSOG', e.target.checked)\n }), t.logDebugSOG)))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.vesselFinder), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.vesselFinderEnabled || false,\n onChange: e => handleConfigChange('vesselFinderEnabled', e.target.checked)\n }), t.vesselFinderEnabled)), config.vesselFinderEnabled && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.vesselFinderHost), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"text\",\n value: config.vesselFinderHost || 'ais.vesselfinder.com',\n onChange: e => handleConfigChange('vesselFinderHost', e.target.value),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.vesselFinderPort), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n max: \"65535\",\n value: config.vesselFinderPort || 5500,\n onChange: e => handleConfigChange('vesselFinderPort', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.vesselFinderUpdateRate), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.vesselFinderUpdateRate || 60,\n onChange: e => handleConfigChange('vesselFinderUpdateRate', Number(e.target.value)),\n style: styles.input\n })))), !aisfleetEnabled && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.section\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h3\", {\n style: styles.sectionTitle\n }, t.cloudVessels), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.checkbox\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"checkbox\",\n checked: config.cloudVesselsEnabled !== false,\n onChange: e => handleConfigChange('cloudVesselsEnabled', e.target.checked)\n }), t.cloudVesselsEnabled)), config.cloudVesselsEnabled !== false && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.cloudVesselsUpdateInterval), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.cloudVesselsUpdateInterval || 60,\n onChange: e => handleConfigChange('cloudVesselsUpdateInterval', Number(e.target.value)),\n style: styles.input\n })), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.formGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"label\", {\n style: styles.label\n }, t.cloudVesselsRadius), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"input\", {\n type: \"number\",\n min: \"1\",\n value: config.cloudVesselsRadius || 10,\n onChange: e => handleConfigChange('cloudVesselsRadius', Number(e.target.value)),\n style: styles.input\n })))), status && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: {\n ...styles.statusMessage,\n ...(status === 'error' ? styles.error : styles.success)\n }\n }, status === 'success' ? currentLang === 'de' ? 'Konfiguration gespeichert' : 'Configuration saved' : currentLang === 'de' ? 'Fehler beim Speichern' : 'Error saving'), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.buttonGroup\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: handleSave,\n disabled: loading || isMMSIInvalid() || isPortInvalid(),\n style: {\n ...styles.button,\n ...styles.primaryButton,\n ...(isMMSIInvalid() || isPortInvalid() ? {\n opacity: 0.5,\n cursor: 'not-allowed'\n } : {})\n }\n }, t.save), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => {\n if (checkUnsavedChanges()) {\n setDialogData({\n title: t.unsavedTitle,\n message: t.unsavedWarning,\n callback: () => setConfig(initialConfig)\n });\n setShowDialog(true);\n } else {\n setConfig(initialConfig);\n }\n },\n style: {\n ...styles.button,\n ...styles.secondaryButton\n }\n }, t.cancel)), showDialog && /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.dialog\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.dialogContent\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"h4\", {\n style: styles.dialogTitle\n }, dialogData.title), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"p\", null, dialogData.message), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"div\", {\n style: styles.dialogButtons\n }, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => setShowDialog(false),\n style: {\n ...styles.button,\n ...styles.secondaryButton\n }\n }, t.no), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(\"button\", {\n onClick: () => {\n if (dialogData.callback) dialogData.callback();\n setShowDialog(false);\n },\n style: {\n ...styles.button,\n ...styles.primaryButton\n }\n }, t.yes)))));\n};\nconst styles = {\n container: {\n padding: '20px',\n fontFamily: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto'\n },\n header: {\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n marginBottom: '30px',\n paddingBottom: '20px',\n borderBottom: '2px solid #667eea'\n },\n title: {\n margin: 0,\n fontSize: '1.5em',\n fontWeight: '600',\n color: '#333'\n },\n helpButton: {\n padding: '8px 16px',\n backgroundColor: '#667eea',\n color: 'white',\n border: 'none',\n borderRadius: '6px',\n cursor: 'pointer',\n fontWeight: '500',\n fontSize: '0.95em',\n transition: 'background 0.3s'\n },\n languageSelector: {\n display: 'flex',\n gap: '10px',\n marginBottom: '30px'\n },\n langButton: {\n padding: '8px 16px',\n border: '1px solid #ddd',\n borderRadius: '4px',\n cursor: 'pointer',\n fontWeight: '500',\n backgroundColor: '#f5f5f5'\n },\n langButtonActive: {\n backgroundColor: '#667eea',\n color: 'white',\n borderColor: '#667eea'\n },\n section: {\n marginBottom: '30px'\n },\n sectionTitle: {\n fontSize: '1.2em',\n fontWeight: '600',\n marginBottom: '15px',\n color: '#333',\n borderBottom: '2px solid #667eea',\n paddingBottom: '10px'\n },\n formGroup: {\n marginBottom: '15px'\n },\n label: {\n display: 'block',\n fontWeight: '500',\n marginBottom: '5px',\n color: '#333'\n },\n input: {\n padding: '8px',\n border: '1px solid #ddd',\n borderRadius: '4px',\n fontSize: '1em',\n width: '200px'\n },\n checkbox: {\n display: 'flex',\n alignItems: 'center',\n marginBottom: '8px',\n cursor: 'pointer',\n gap: '8px'\n },\n buttonGroup: {\n display: 'flex',\n gap: '10px',\n marginTop: '30px',\n paddingTop: '20px',\n borderTop: '1px solid #ddd'\n },\n button: {\n padding: '10px 20px',\n border: 'none',\n borderRadius: '4px',\n cursor: 'pointer',\n fontWeight: '500',\n fontSize: '1em'\n },\n primaryButton: {\n backgroundColor: '#667eea',\n color: 'white'\n },\n secondaryButton: {\n backgroundColor: '#6c757d',\n color: 'white'\n },\n statusMessage: {\n padding: '12px',\n borderRadius: '4px',\n marginBottom: '15px',\n fontSize: '0.95em'\n },\n success: {\n backgroundColor: '#d4edda',\n color: '#155724'\n },\n error: {\n backgroundColor: '#f8d7da',\n color: '#721c24'\n },\n dialog: {\n position: 'fixed',\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: 'rgba(0,0,0,0.5)',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n zIndex: 1000\n },\n dialogContent: {\n backgroundColor: 'white',\n padding: '30px',\n borderRadius: '8px',\n maxWidth: '400px',\n boxShadow: '0 4px 6px rgba(0,0,0,0.1)'\n },\n dialogTitle: {\n marginTop: 0,\n marginBottom: '15px',\n fontSize: '1.1em'\n },\n dialogButtons: {\n display: 'flex',\n gap: '10px',\n justifyContent: 'flex-end',\n marginTop: '20px'\n }\n};\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (PluginConfigurationPanel);\n\n//# sourceURL=webpack://signalk-ais-navionics-converter/./src/components/PluginConfigurationPanel.jsx?\n}");
|
|
19
19
|
|
|
20
20
|
/***/ })
|
|
21
21
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
2
|
|
|
3
3
|
const PluginConfigurationPanel = ({ configuration, save }) => {
|
|
4
4
|
const [config, setConfig] = useState(configuration || {});
|
|
@@ -8,7 +8,9 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
|
|
|
8
8
|
const [showDialog, setShowDialog] = useState(false);
|
|
9
9
|
const [dialogData, setDialogData] = useState({ title: '', message: '', callback: null });
|
|
10
10
|
const [ownMMSI, setOwnMMSI] = useState(null);
|
|
11
|
-
|
|
11
|
+
const [aisfleetEnabled, setAisfleetEnabled] = useState(false);
|
|
12
|
+
const [portError, setPortError] = useState('');
|
|
13
|
+
|
|
12
14
|
const translations = {
|
|
13
15
|
de: {
|
|
14
16
|
general: 'Allgemein',
|
|
@@ -19,19 +21,20 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
|
|
|
19
21
|
cloudVessels: 'Cloud Vessels (AISFleet)',
|
|
20
22
|
|
|
21
23
|
tcpPort: 'TCP Port:',
|
|
24
|
+
wsPort: 'WebSocket Port:',
|
|
22
25
|
updateInterval: 'Update-Intervall für geänderte Schiffe (Sekunden):',
|
|
23
26
|
tcpResendInterval: 'Update-Intervall für unveränderte Schiffe (Sekunden):',
|
|
24
27
|
|
|
25
28
|
skipWithoutCallsign: 'Schiffe ohne Rufzeichen überspringen',
|
|
26
29
|
skipStaleData: 'Schiffe mit alten Daten überspringen',
|
|
27
30
|
staleDataThreshold: 'Schwellenwert für alte Daten (Minuten):',
|
|
28
|
-
staleDataShipname: 'Zeitstempel zum Schiffsnamen hinzufügen
|
|
31
|
+
staleDataShipname: 'Zeitstempel zum Schiffsnamen hinzufügen wenn die letzte Positionsmeldung älter ist als x Minuten (0=deaktiviert):',
|
|
29
32
|
|
|
30
|
-
minAlarmSOG: '
|
|
31
|
-
maxMinutesSOGToZero: '
|
|
33
|
+
minAlarmSOG: 'SOG (und COG) wird auf 0 gesetzt, wenn die Geschwindigkeit kleiner als x Knoten ist (0=deaktiviert):',
|
|
34
|
+
maxMinutesSOGToZero: 'SOG wird auf 0 gesetzt wenn die letzte Positionsmeldung älter ist als x Minuten (0=keine Korrektur):',
|
|
32
35
|
|
|
33
|
-
logDebugDetails: 'Debug
|
|
34
|
-
logMMSI: 'Filter Debug-Ausgabe für MMSI:',
|
|
36
|
+
logDebugDetails: 'Debug Schiff-Details',
|
|
37
|
+
logMMSI: 'Filter Debug-Ausgabe nur für MMSI:',
|
|
35
38
|
logDebugStale: 'Debug alte Schiffe',
|
|
36
39
|
logDebugJSON: 'Debug JSON-Daten',
|
|
37
40
|
logDebugAIS: 'Debug AIS-Daten',
|
|
@@ -46,6 +49,8 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
|
|
|
46
49
|
cloudVesselsUpdateInterval: 'Cloud Vessels Update-Intervall (Sekunden):',
|
|
47
50
|
cloudVesselsRadius: 'Radius von eigenem Schiff (Seemeilen):',
|
|
48
51
|
|
|
52
|
+
portError: 'TCP Port und WebSocket Port müssen unterschiedlich sein',
|
|
53
|
+
|
|
49
54
|
save: 'Speichern',
|
|
50
55
|
cancel: 'Abbruch',
|
|
51
56
|
unsavedWarning: 'Es gibt ungespeicherte Änderungen. Wirklich abbrechen?',
|
|
@@ -62,19 +67,20 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
|
|
|
62
67
|
cloudVessels: 'Cloud Vessels (AISFleet)',
|
|
63
68
|
|
|
64
69
|
tcpPort: 'TCP Port:',
|
|
70
|
+
wsPort: 'WebSocket Port:',
|
|
65
71
|
updateInterval: 'Update interval for changed vessels (seconds):',
|
|
66
72
|
tcpResendInterval: 'Update interval for unchanged vessels (seconds):',
|
|
67
73
|
|
|
68
74
|
skipWithoutCallsign: 'Skip vessels without callsign',
|
|
69
75
|
skipStaleData: 'Skip vessels with stale data',
|
|
70
76
|
staleDataThreshold: 'Stale data threshold (minutes):',
|
|
71
|
-
staleDataShipname: 'Add timestamp to
|
|
77
|
+
staleDataShipname: 'Add timestamp to vessel name if the last position report is older than x minutes (0=disabled):',
|
|
72
78
|
|
|
73
|
-
minAlarmSOG: '
|
|
74
|
-
maxMinutesSOGToZero: '
|
|
79
|
+
minAlarmSOG: 'SOG (and COG) is set to 0 if the speed is less than x knots (0=disabled):',
|
|
80
|
+
maxMinutesSOGToZero: 'SOG is set to 0 if the last position report is older than x minutes (0=no correction):',
|
|
75
81
|
|
|
76
|
-
logDebugDetails: 'Debug
|
|
77
|
-
logMMSI: 'Filter Debug MMSI:',
|
|
82
|
+
logDebugDetails: 'Debug vessel details',
|
|
83
|
+
logMMSI: 'Filter Debug only for MMSI:',
|
|
78
84
|
logDebugStale: 'Debug stale vessels',
|
|
79
85
|
logDebugJSON: 'Debug JSON data',
|
|
80
86
|
logDebugAIS: 'Debug AIS data',
|
|
@@ -89,6 +95,8 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
|
|
|
89
95
|
cloudVesselsUpdateInterval: 'Cloud Vessels update interval (seconds):',
|
|
90
96
|
cloudVesselsRadius: 'Radius from own vessel (nautical miles):',
|
|
91
97
|
|
|
98
|
+
portError: 'TCP Port and WebSocket Port must be different',
|
|
99
|
+
|
|
92
100
|
save: 'Save',
|
|
93
101
|
cancel: 'Cancel',
|
|
94
102
|
unsavedWarning: 'There are unsaved changes. Really cancel?',
|
|
@@ -102,65 +110,80 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
|
|
|
102
110
|
const t = translations[currentLang];
|
|
103
111
|
|
|
104
112
|
// Hole eigene MMSI beim Laden
|
|
105
|
-
|
|
113
|
+
useEffect(() => {
|
|
106
114
|
const fetchOwnMMSI = async () => {
|
|
107
115
|
try {
|
|
108
116
|
const protocol = window.location.protocol;
|
|
109
117
|
const hostname = window.location.hostname;
|
|
110
118
|
const port = window.location.port;
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
const baseUrl = `${protocol}//${hostname}${port ? ':' + port : ''}`;
|
|
120
|
+
const aisfleetUrl = `${baseUrl}/plugins/aisfleet`;
|
|
121
|
+
const url = `${baseUrl}/signalk/v1/api/self`;
|
|
122
|
+
|
|
116
123
|
const response = await fetch(url);
|
|
117
|
-
console.log('Response status:', response.status);
|
|
118
|
-
|
|
119
124
|
if (response.ok) {
|
|
120
125
|
const data = await response.json();
|
|
121
|
-
console.log('Self data received:', JSON.stringify(data, null, 2));
|
|
122
|
-
|
|
123
|
-
// Data könnte String oder Objekt sein
|
|
124
126
|
let vesselKey = null;
|
|
127
|
+
|
|
125
128
|
if (typeof data === 'string') {
|
|
126
|
-
// String Format: "vessels.urn:mrn:imo:mmsi:211177520"
|
|
127
129
|
vesselKey = data.replace('vessels.', '');
|
|
128
|
-
console.log('Parsed vessel key from string:', vesselKey);
|
|
129
130
|
} else if (data.vessels && typeof data.vessels === 'object') {
|
|
130
|
-
// Objekt Format
|
|
131
131
|
const mmsiMatch = Object.keys(data.vessels).find(key => key.includes('mmsi:'));
|
|
132
132
|
vesselKey = mmsiMatch;
|
|
133
|
-
console.log('Found vessel key in object:', vesselKey);
|
|
134
133
|
}
|
|
135
|
-
|
|
134
|
+
|
|
136
135
|
if (vesselKey) {
|
|
137
136
|
const mmsi = vesselKey.match(/mmsi:(\d+)/)?.[1];
|
|
138
|
-
|
|
139
|
-
if (mmsi) {
|
|
140
|
-
setOwnMMSI(mmsi);
|
|
141
|
-
}
|
|
137
|
+
if (mmsi) setOwnMMSI(mmsi);
|
|
142
138
|
}
|
|
143
|
-
}
|
|
144
|
-
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// AIS Fleet Plugin prüfen
|
|
142
|
+
try {
|
|
143
|
+
const aisResponse = await fetch(aisfleetUrl);
|
|
144
|
+
if (aisResponse.ok) {
|
|
145
|
+
const aisData = await aisResponse.json();
|
|
146
|
+
setAisfleetEnabled(!!aisData.enabled);
|
|
147
|
+
} else {
|
|
148
|
+
setAisfleetEnabled(false);
|
|
149
|
+
}
|
|
150
|
+
} catch (err) {
|
|
151
|
+
setAisfleetEnabled(false);
|
|
145
152
|
}
|
|
146
153
|
} catch (err) {
|
|
147
154
|
console.error('Failed to fetch own MMSI:', err);
|
|
148
|
-
// Fehler ignorieren - Validierung wird einfach nicht aktiviert
|
|
149
155
|
}
|
|
150
156
|
};
|
|
151
|
-
|
|
152
|
-
// Verzögert starten um sicherzustellen dass DOM ready ist
|
|
157
|
+
|
|
153
158
|
setTimeout(fetchOwnMMSI, 500);
|
|
154
159
|
}, []);
|
|
155
160
|
|
|
156
161
|
const handleConfigChange = (key, value) => {
|
|
157
162
|
setConfig(prev => ({ ...prev, [key]: value }));
|
|
163
|
+
|
|
164
|
+
// Überprüfe Port-Nummern
|
|
165
|
+
if (key === 'tcpPort' || key === 'wsPort') {
|
|
166
|
+
const tcpPort = key === 'tcpPort' ? value : config.tcpPort;
|
|
167
|
+
const wsPort = key === 'wsPort' ? value : config.wsPort;
|
|
168
|
+
|
|
169
|
+
if (tcpPort && wsPort && tcpPort !== 0 && wsPort !== 0 && tcpPort === wsPort) {
|
|
170
|
+
setPortError(t.portError);
|
|
171
|
+
} else {
|
|
172
|
+
setPortError('');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
158
175
|
};
|
|
159
176
|
|
|
160
177
|
const isMMSIInvalid = () => {
|
|
161
178
|
return ownMMSI && config.logMMSI && config.logMMSI === ownMMSI;
|
|
162
179
|
};
|
|
163
180
|
|
|
181
|
+
const isPortInvalid = () => {
|
|
182
|
+
const tcpPort = config.tcpPort || 10113;
|
|
183
|
+
const wsPort = config.wsPort || 10114;
|
|
184
|
+
return tcpPort !== 0 && wsPort !== 0 && tcpPort === wsPort;
|
|
185
|
+
};
|
|
186
|
+
|
|
164
187
|
const checkUnsavedChanges = () => {
|
|
165
188
|
return JSON.stringify(config) !== JSON.stringify(initialConfig);
|
|
166
189
|
};
|
|
@@ -176,13 +199,22 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
|
|
|
176
199
|
return;
|
|
177
200
|
}
|
|
178
201
|
|
|
202
|
+
// Validiere Ports
|
|
203
|
+
if (isPortInvalid()) {
|
|
204
|
+
setStatus('error');
|
|
205
|
+
const errorMsg = currentLang === 'de'
|
|
206
|
+
? 'Fehler: TCP Port und WebSocket Port müssen unterschiedlich sein!'
|
|
207
|
+
: 'Error: TCP Port and WebSocket Port must be different!';
|
|
208
|
+
alert(errorMsg);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
179
212
|
setLoading(true);
|
|
180
213
|
if (save) {
|
|
181
214
|
try {
|
|
182
215
|
const result = save(config);
|
|
183
216
|
|
|
184
217
|
if (result && typeof result.then === 'function') {
|
|
185
|
-
// save() gibt ein Promise zurück
|
|
186
218
|
result
|
|
187
219
|
.then(() => {
|
|
188
220
|
setStatus('success');
|
|
@@ -197,7 +229,6 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
|
|
|
197
229
|
setLoading(false);
|
|
198
230
|
});
|
|
199
231
|
} else {
|
|
200
|
-
// save() gibt kein Promise zurück - assume success
|
|
201
232
|
setStatus('success');
|
|
202
233
|
setInitialConfig(config);
|
|
203
234
|
setTimeout(() => setStatus(''), 3000);
|
|
@@ -269,8 +300,49 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
|
|
|
269
300
|
max="65535"
|
|
270
301
|
value={config.tcpPort || 10113}
|
|
271
302
|
onChange={(e) => handleConfigChange('tcpPort', Number(e.target.value))}
|
|
272
|
-
style={
|
|
303
|
+
style={{
|
|
304
|
+
...styles.input,
|
|
305
|
+
...(portError ? { borderColor: '#dc3545', backgroundColor: '#fff5f5' } : {})
|
|
306
|
+
}}
|
|
307
|
+
/>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<div style={styles.formGroup}>
|
|
311
|
+
<label style={styles.label}>{t.wsPort}</label>
|
|
312
|
+
<div style={{
|
|
313
|
+
display: 'block',
|
|
314
|
+
fontSize: '0.85em',
|
|
315
|
+
color: '#666',
|
|
316
|
+
marginTop: '8px',
|
|
317
|
+
marginBottom: '12px',
|
|
318
|
+
fontStyle: 'italic',
|
|
319
|
+
lineHeight: '1.4'
|
|
320
|
+
}}>
|
|
321
|
+
{currentLang === 'de'
|
|
322
|
+
? 'Über diesen Port werden alle AIS Daten als NMEA0183 und alle Schiffsdaten per JOSN als Websocket gesendet (nicht für Navionics relevant). 0=kein Websocket Server.'
|
|
323
|
+
: 'This port is used to send all AIS data as NMEA0183 and all vessel data as JSON via Websocket (not relevant for Navionics). 0=no Websocket server.'}
|
|
324
|
+
</div>
|
|
325
|
+
<input
|
|
326
|
+
type="number"
|
|
327
|
+
min="0"
|
|
328
|
+
max="65535"
|
|
329
|
+
value={config.wsPort || 10114}
|
|
330
|
+
onChange={(e) => handleConfigChange('wsPort', Number(e.target.value))}
|
|
331
|
+
style={{
|
|
332
|
+
...styles.input,
|
|
333
|
+
...(portError ? { borderColor: '#dc3545', backgroundColor: '#fff5f5' } : {})
|
|
334
|
+
}}
|
|
273
335
|
/>
|
|
336
|
+
{portError && (
|
|
337
|
+
<div style={{
|
|
338
|
+
color: '#dc3545',
|
|
339
|
+
fontSize: '0.85em',
|
|
340
|
+
marginTop: '8px',
|
|
341
|
+
fontWeight: '500'
|
|
342
|
+
}}>
|
|
343
|
+
⚠️ {portError}
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
274
346
|
</div>
|
|
275
347
|
|
|
276
348
|
<div style={styles.formGroup}>
|
|
@@ -420,8 +492,8 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
|
|
|
420
492
|
fontWeight: '500'
|
|
421
493
|
}}>
|
|
422
494
|
{currentLang === 'de'
|
|
423
|
-
? '
|
|
424
|
-
: '
|
|
495
|
+
? '⚠️ Sie können nicht die eigene MMSI verwenden!'
|
|
496
|
+
: '⚠️ You cannot use your own MMSI!'}
|
|
425
497
|
</div>
|
|
426
498
|
)}
|
|
427
499
|
</div>
|
|
@@ -527,46 +599,48 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
|
|
|
527
599
|
</div>
|
|
528
600
|
|
|
529
601
|
{/* Cloud Vessels */}
|
|
530
|
-
|
|
531
|
-
<
|
|
532
|
-
|
|
533
|
-
<div style={styles.formGroup}>
|
|
534
|
-
<label style={styles.checkbox}>
|
|
535
|
-
<input
|
|
536
|
-
type="checkbox"
|
|
537
|
-
checked={config.cloudVesselsEnabled !== false}
|
|
538
|
-
onChange={(e) => handleConfigChange('cloudVesselsEnabled', e.target.checked)}
|
|
539
|
-
/>
|
|
540
|
-
{t.cloudVesselsEnabled}
|
|
541
|
-
</label>
|
|
542
|
-
</div>
|
|
602
|
+
{!aisfleetEnabled && (
|
|
603
|
+
<div style={styles.section}>
|
|
604
|
+
<h3 style={styles.sectionTitle}>{t.cloudVessels}</h3>
|
|
543
605
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
<div style={styles.formGroup}>
|
|
547
|
-
<label style={styles.label}>{t.cloudVesselsUpdateInterval}</label>
|
|
606
|
+
<div style={styles.formGroup}>
|
|
607
|
+
<label style={styles.checkbox}>
|
|
548
608
|
<input
|
|
549
|
-
type="
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
onChange={(e) => handleConfigChange('cloudVesselsUpdateInterval', Number(e.target.value))}
|
|
553
|
-
style={styles.input}
|
|
609
|
+
type="checkbox"
|
|
610
|
+
checked={config.cloudVesselsEnabled !== false}
|
|
611
|
+
onChange={(e) => handleConfigChange('cloudVesselsEnabled', e.target.checked)}
|
|
554
612
|
/>
|
|
555
|
-
|
|
613
|
+
{t.cloudVesselsEnabled}
|
|
614
|
+
</label>
|
|
615
|
+
</div>
|
|
556
616
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
<
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
617
|
+
{config.cloudVesselsEnabled !== false && (
|
|
618
|
+
<>
|
|
619
|
+
<div style={styles.formGroup}>
|
|
620
|
+
<label style={styles.label}>{t.cloudVesselsUpdateInterval}</label>
|
|
621
|
+
<input
|
|
622
|
+
type="number"
|
|
623
|
+
min="1"
|
|
624
|
+
value={config.cloudVesselsUpdateInterval || 60}
|
|
625
|
+
onChange={(e) => handleConfigChange('cloudVesselsUpdateInterval', Number(e.target.value))}
|
|
626
|
+
style={styles.input}
|
|
627
|
+
/>
|
|
628
|
+
</div>
|
|
629
|
+
|
|
630
|
+
<div style={styles.formGroup}>
|
|
631
|
+
<label style={styles.label}>{t.cloudVesselsRadius}</label>
|
|
632
|
+
<input
|
|
633
|
+
type="number"
|
|
634
|
+
min="1"
|
|
635
|
+
value={config.cloudVesselsRadius || 10}
|
|
636
|
+
onChange={(e) => handleConfigChange('cloudVesselsRadius', Number(e.target.value))}
|
|
637
|
+
style={styles.input}
|
|
638
|
+
/>
|
|
639
|
+
</div>
|
|
640
|
+
</>
|
|
641
|
+
)}
|
|
642
|
+
</div>
|
|
643
|
+
)}
|
|
570
644
|
|
|
571
645
|
{status && (
|
|
572
646
|
<div style={{...styles.statusMessage, ...(status === 'error' ? styles.error : styles.success)}}>
|
|
@@ -579,8 +653,12 @@ const PluginConfigurationPanel = ({ configuration, save }) => {
|
|
|
579
653
|
<div style={styles.buttonGroup}>
|
|
580
654
|
<button
|
|
581
655
|
onClick={handleSave}
|
|
582
|
-
disabled={loading || isMMSIInvalid()}
|
|
583
|
-
style={{
|
|
656
|
+
disabled={loading || isMMSIInvalid() || isPortInvalid()}
|
|
657
|
+
style={{
|
|
658
|
+
...styles.button,
|
|
659
|
+
...styles.primaryButton,
|
|
660
|
+
...((isMMSIInvalid() || isPortInvalid()) ? { opacity: 0.5, cursor: 'not-allowed' } : {})
|
|
661
|
+
}}
|
|
584
662
|
>
|
|
585
663
|
{t.save}
|
|
586
664
|
</button>
|