Greasy Fork is available in English.
Teilt Straßensegmente automatisch in gleich große Stücke auf - mit Vorschau, Multi-Segment, Presets und mehr
// ==UserScript==
// @name WME Segment Splitter
// @namespace https://greasyfork.org/de/users/863740-horst-wittlich
// @version 2026.02.10
// @description Teilt Straßensegmente automatisch in gleich große Stücke auf - mit Vorschau, Multi-Segment, Presets und mehr
// @author WME Script
// @match https://www.waze.com/editor*
// @match https://www.waze.com/*/editor*
// @match https://beta.waze.com/editor*
// @match https://beta.waze.com/*/editor*
// @exclude https://www.waze.com/user/*
// @grant none
// @license MIT
// ==/UserScript==
/* global W, OpenLayers, getWmeSdk */
(function() {
'use strict';
const SCRIPT_NAME = 'WME Segment Splitter';
const SCRIPT_VERSION = '2026.02.10';
const SCRIPT_ID = 'wme-segment-splitter';
// Übersetzungen - DE, EN, IT, ES, FR, NL
const TRANSLATIONS = {
en: {
noSegmentSelected: 'No segment selected',
selectSegments: 'Select one or more road segments',
segmentsSelected: 'segments selected',
totalLength: 'Total length',
segmentSelected: 'Segment selected',
length: 'Length',
street: 'Street',
geometryNodes: 'Geometry nodes',
lock: 'Lock',
unnamed: 'Unnamed',
presets: 'Presets',
selectPreset: '-- Select preset --',
load: 'Load',
delete: 'Delete',
presetName: 'Preset name',
save: 'Save',
countParts: 'Number of parts',
parts: 'parts',
fixedDistance: 'Fixed distance',
meters: 'Meters',
percentage: 'Percentage',
commaSeparated: 'Comma separated',
atGeometryNodes: 'At geometry nodes',
manual: 'Manual',
addPosition: 'Add position:',
addPoint: '+ Add point',
noPointsSet: 'No points set - use slider above',
deleteAllPoints: 'Delete all points',
advancedOptions: 'Advanced options',
startPoint: 'Start point:',
offsetFromStart: 'Offset from start:',
safety: 'Safety',
enforceMinLength: 'Enforce minimum length',
tooShortMerged: 'Too short segments will be merged',
checkLockLevel: 'Check lock level',
protectRoundabouts: 'Protect roundabouts',
warnRamps: 'Warn on ramps',
display: 'Display',
previewOnMap: 'Preview on map',
groupUndo: 'Group undo',
importExport: 'Import/Export',
export: 'Export',
import: 'Import',
preview: 'Preview:',
splitSegment: '✂️ Split segment',
repeatLast: '🔄 Repeat last action',
processStreet: '📦 Process entire street',
actionsRemaining: 'actions remaining today',
maxSplitsPerSegment: 'Max splits/segment',
debug: 'Debug',
positionExists: 'Position already exists',
noSegmentsSelected: 'No segments selected',
dailyLimitReached: 'Daily limit reached! Maximum',
actionsPerDay: 'actions per day.',
tooManySplits: 'Too many splits',
maximum: 'Maximum',
splitsCreated: 'splits created!',
splitsDone: 'splits done',
errors: 'errors',
noSplitsDone: 'No splits done',
error: 'Error',
noLastAction: 'No last action available',
selectOneSegment: 'Please select exactly one segment',
noStreetAssignment: 'Segment has no street assignment',
noSegmentsFound: 'No segments found',
processSegments: 'Process segments of street',
splitsOnStreet: 'splits done',
importSuccess: 'Import successful!',
importFailed: 'Import failed: Invalid file',
presetSaved: 'Preset saved',
presetLoaded: 'Preset loaded',
presetDeleted: 'Preset deleted',
enterPresetName: 'Please enter preset name',
segmentLocked: 'Segment is locked higher',
thanYourRank: 'than your rank',
partOfRoundabout: 'Segment is part of a roundabout',
isRamp: 'Segment is a ramp',
offsetLarger: 'Offset larger than segment length',
unknownMode: 'Unknown mode',
noSplitPoints: 'No split points (segment too short or no points set)'
},
de: {
noSegmentSelected: 'Kein Segment ausgewählt',
selectSegments: 'Wähle ein oder mehrere Straßensegmente aus',
segmentsSelected: 'Segmente ausgewählt',
totalLength: 'Gesamtlänge',
segmentSelected: 'Segment ausgewählt',
length: 'Länge',
street: 'Straße',
geometryNodes: 'Geometry-Nodes',
lock: 'Lock',
unnamed: 'Unbenannt',
presets: 'Presets',
selectPreset: '-- Preset wählen --',
load: 'Laden',
delete: 'Löschen',
presetName: 'Preset-Name',
save: 'Speichern',
countParts: 'Anzahl Teile',
parts: 'Teile',
fixedDistance: 'Feste Abstände',
meters: 'Meter',
percentage: 'Prozentual',
commaSeparated: 'Kommagetrennt',
atGeometryNodes: 'An Geometry-Nodes',
manual: 'Manuell',
addPosition: 'Position hinzufügen:',
addPoint: '+ Punkt hinzufügen',
noPointsSet: 'Keine Punkte gesetzt - nutze den Slider oben',
deleteAllPoints: 'Alle Punkte löschen',
advancedOptions: 'Erweiterte Optionen',
startPoint: 'Startpunkt:',
offsetFromStart: 'Offset vom Start:',
safety: 'Sicherheit',
enforceMinLength: 'Mindestlänge erzwingen',
tooShortMerged: 'Zu kurze Segmente werden zusammengelegt',
checkLockLevel: 'Lock-Level prüfen',
protectRoundabouts: 'Kreisverkehre schützen',
warnRamps: 'Bei Rampen warnen',
display: 'Anzeige',
previewOnMap: 'Vorschau auf Karte',
groupUndo: 'Undo gruppieren',
importExport: 'Import/Export',
export: 'Exportieren',
import: 'Importieren',
preview: 'Vorschau:',
splitSegment: '✂️ Segment aufteilen',
repeatLast: '🔄 Letzte Aktion wiederholen',
processStreet: '📦 Ganze Straße bearbeiten',
actionsRemaining: 'Aktionen übrig heute',
maxSplitsPerSegment: 'Max Splits/Segment',
debug: 'Debug',
positionExists: 'Position bereits vorhanden',
noSegmentsSelected: 'Keine Segmente ausgewählt',
dailyLimitReached: 'Tageslimit erreicht! Maximal',
actionsPerDay: 'Aktionen pro Tag.',
tooManySplits: 'Zu viele Splits',
maximum: 'Maximum',
splitsCreated: 'Schnittpunkte erstellt!',
splitsDone: 'Splits durchgeführt',
errors: 'Fehler',
noSplitsDone: 'Keine Splits durchgeführt',
error: 'Fehler',
noLastAction: 'Keine letzte Aktion vorhanden',
selectOneSegment: 'Bitte genau ein Segment auswählen',
noStreetAssignment: 'Segment hat keine Straßenzuordnung',
noSegmentsFound: 'Keine Segmente gefunden',
processSegments: 'Segmente der Straße bearbeiten',
splitsOnStreet: 'Splits durchgeführt',
importSuccess: 'Import erfolgreich!',
importFailed: 'Import fehlgeschlagen: Ungültige Datei',
presetSaved: 'Preset gespeichert',
presetLoaded: 'Preset geladen',
presetDeleted: 'Preset gelöscht',
enterPresetName: 'Bitte Preset-Namen eingeben',
segmentLocked: 'Segment ist höher gelockt',
thanYourRank: 'als dein Rang',
partOfRoundabout: 'Segment ist Teil eines Kreisverkehrs',
isRamp: 'Segment ist eine Rampe',
offsetLarger: 'Offset größer als Segmentlänge',
unknownMode: 'Unbekannter Modus',
noSplitPoints: 'Keine Schnittpunkte (Segment zu kurz oder keine Punkte gesetzt)'
},
it: {
noSegmentSelected: 'Nessun segmento selezionato',
selectSegments: 'Seleziona uno o più segmenti stradali',
segmentsSelected: 'segmenti selezionati',
totalLength: 'Lunghezza totale',
segmentSelected: 'Segmento selezionato',
length: 'Lunghezza',
street: 'Strada',
geometryNodes: 'Nodi geometria',
lock: 'Blocco',
unnamed: 'Senza nome',
presets: 'Preset',
selectPreset: '-- Seleziona preset --',
load: 'Carica',
delete: 'Elimina',
presetName: 'Nome preset',
save: 'Salva',
countParts: 'Numero di parti',
parts: 'parti',
fixedDistance: 'Distanza fissa',
meters: 'Metri',
percentage: 'Percentuale',
commaSeparated: 'Separati da virgola',
atGeometryNodes: 'Ai nodi geometria',
manual: 'Manuale',
addPosition: 'Aggiungi posizione:',
addPoint: '+ Aggiungi punto',
noPointsSet: 'Nessun punto impostato - usa lo slider sopra',
deleteAllPoints: 'Elimina tutti i punti',
advancedOptions: 'Opzioni avanzate',
startPoint: 'Punto di partenza:',
offsetFromStart: 'Offset dall\'inizio:',
safety: 'Sicurezza',
enforceMinLength: 'Applica lunghezza minima',
tooShortMerged: 'I segmenti troppo corti verranno uniti',
checkLockLevel: 'Controlla livello blocco',
protectRoundabouts: 'Proteggi rotatorie',
warnRamps: 'Avvisa per rampe',
display: 'Visualizzazione',
previewOnMap: 'Anteprima su mappa',
groupUndo: 'Raggruppa annulla',
importExport: 'Importa/Esporta',
export: 'Esporta',
import: 'Importa',
preview: 'Anteprima:',
splitSegment: '✂️ Dividi segmento',
repeatLast: '🔄 Ripeti ultima azione',
processStreet: '📦 Elabora intera strada',
actionsRemaining: 'azioni rimanenti oggi',
maxSplitsPerSegment: 'Max divisioni/segmento',
debug: 'Debug',
positionExists: 'Posizione già esistente',
noSegmentsSelected: 'Nessun segmento selezionato',
dailyLimitReached: 'Limite giornaliero raggiunto! Massimo',
actionsPerDay: 'azioni al giorno.',
tooManySplits: 'Troppe divisioni',
maximum: 'Massimo',
splitsCreated: 'divisioni create!',
splitsDone: 'divisioni effettuate',
errors: 'errori',
noSplitsDone: 'Nessuna divisione effettuata',
error: 'Errore',
noLastAction: 'Nessuna azione precedente disponibile',
selectOneSegment: 'Seleziona esattamente un segmento',
noStreetAssignment: 'Il segmento non ha assegnazione stradale',
noSegmentsFound: 'Nessun segmento trovato',
processSegments: 'Elabora segmenti della strada',
splitsOnStreet: 'divisioni effettuate',
importSuccess: 'Importazione riuscita!',
importFailed: 'Importazione fallita: File non valido',
presetSaved: 'Preset salvato',
presetLoaded: 'Preset caricato',
presetDeleted: 'Preset eliminato',
enterPresetName: 'Inserisci nome preset',
segmentLocked: 'Il segmento ha blocco superiore',
thanYourRank: 'al tuo livello',
partOfRoundabout: 'Il segmento fa parte di una rotatoria',
isRamp: 'Il segmento è una rampa',
offsetLarger: 'Offset maggiore della lunghezza del segmento',
unknownMode: 'Modalità sconosciuta',
noSplitPoints: 'Nessun punto di divisione (segmento troppo corto o nessun punto impostato)'
},
es: {
noSegmentSelected: 'Ningún segmento seleccionado',
selectSegments: 'Selecciona uno o más segmentos de carretera',
segmentsSelected: 'segmentos seleccionados',
totalLength: 'Longitud total',
segmentSelected: 'Segmento seleccionado',
length: 'Longitud',
street: 'Calle',
geometryNodes: 'Nodos de geometría',
lock: 'Bloqueo',
unnamed: 'Sin nombre',
presets: 'Presets',
selectPreset: '-- Seleccionar preset --',
load: 'Cargar',
delete: 'Eliminar',
presetName: 'Nombre del preset',
save: 'Guardar',
countParts: 'Número de partes',
parts: 'partes',
fixedDistance: 'Distancia fija',
meters: 'Metros',
percentage: 'Porcentaje',
commaSeparated: 'Separados por coma',
atGeometryNodes: 'En nodos de geometría',
manual: 'Manual',
addPosition: 'Añadir posición:',
addPoint: '+ Añadir punto',
noPointsSet: 'Sin puntos - usa el control deslizante',
deleteAllPoints: 'Eliminar todos los puntos',
advancedOptions: 'Opciones avanzadas',
startPoint: 'Punto de inicio:',
offsetFromStart: 'Desplazamiento desde inicio:',
safety: 'Seguridad',
enforceMinLength: 'Aplicar longitud mínima',
tooShortMerged: 'Los segmentos muy cortos se fusionarán',
checkLockLevel: 'Verificar nivel de bloqueo',
protectRoundabouts: 'Proteger rotondas',
warnRamps: 'Advertir en rampas',
display: 'Visualización',
previewOnMap: 'Vista previa en mapa',
groupUndo: 'Agrupar deshacer',
importExport: 'Importar/Exportar',
export: 'Exportar',
import: 'Importar',
preview: 'Vista previa:',
splitSegment: '✂️ Dividir segmento',
repeatLast: '🔄 Repetir última acción',
processStreet: '📦 Procesar calle completa',
actionsRemaining: 'acciones restantes hoy',
maxSplitsPerSegment: 'Máx divisiones/segmento',
debug: 'Debug',
positionExists: 'La posición ya existe',
noSegmentsSelected: 'Ningún segmento seleccionado',
dailyLimitReached: '¡Límite diario alcanzado! Máximo',
actionsPerDay: 'acciones por día.',
tooManySplits: 'Demasiadas divisiones',
maximum: 'Máximo',
splitsCreated: '¡divisiones creadas!',
splitsDone: 'divisiones realizadas',
errors: 'errores',
noSplitsDone: 'No se realizaron divisiones',
error: 'Error',
noLastAction: 'No hay acción anterior disponible',
selectOneSegment: 'Selecciona exactamente un segmento',
noStreetAssignment: 'El segmento no tiene asignación de calle',
noSegmentsFound: 'No se encontraron segmentos',
processSegments: 'Procesar segmentos de la calle',
splitsOnStreet: 'divisiones realizadas',
importSuccess: '¡Importación exitosa!',
importFailed: 'Importación fallida: Archivo inválido',
presetSaved: 'Preset guardado',
presetLoaded: 'Preset cargado',
presetDeleted: 'Preset eliminado',
enterPresetName: 'Ingresa nombre del preset',
segmentLocked: 'El segmento tiene bloqueo superior',
thanYourRank: 'a tu nivel',
partOfRoundabout: 'El segmento es parte de una rotonda',
isRamp: 'El segmento es una rampa',
offsetLarger: 'Desplazamiento mayor que la longitud del segmento',
unknownMode: 'Modo desconocido',
noSplitPoints: 'Sin puntos de división (segmento muy corto o sin puntos)'
},
fr: {
noSegmentSelected: 'Aucun segment sélectionné',
selectSegments: 'Sélectionnez un ou plusieurs segments de route',
segmentsSelected: 'segments sélectionnés',
totalLength: 'Longueur totale',
segmentSelected: 'Segment sélectionné',
length: 'Longueur',
street: 'Rue',
geometryNodes: 'Nœuds de géométrie',
lock: 'Verrouillage',
unnamed: 'Sans nom',
presets: 'Préréglages',
selectPreset: '-- Choisir préréglage --',
load: 'Charger',
delete: 'Supprimer',
presetName: 'Nom du préréglage',
save: 'Enregistrer',
countParts: 'Nombre de parties',
parts: 'parties',
fixedDistance: 'Distance fixe',
meters: 'Mètres',
percentage: 'Pourcentage',
commaSeparated: 'Séparés par virgule',
atGeometryNodes: 'Aux nœuds de géométrie',
manual: 'Manuel',
addPosition: 'Ajouter position:',
addPoint: '+ Ajouter point',
noPointsSet: 'Aucun point défini - utilisez le curseur',
deleteAllPoints: 'Supprimer tous les points',
advancedOptions: 'Options avancées',
startPoint: 'Point de départ:',
offsetFromStart: 'Décalage depuis le début:',
safety: 'Sécurité',
enforceMinLength: 'Appliquer longueur minimale',
tooShortMerged: 'Les segments trop courts seront fusionnés',
checkLockLevel: 'Vérifier niveau de verrouillage',
protectRoundabouts: 'Protéger les ronds-points',
warnRamps: 'Avertir pour les bretelles',
display: 'Affichage',
previewOnMap: 'Aperçu sur la carte',
groupUndo: 'Grouper annuler',
importExport: 'Importer/Exporter',
export: 'Exporter',
import: 'Importer',
preview: 'Aperçu:',
splitSegment: '✂️ Diviser segment',
repeatLast: '🔄 Répéter dernière action',
processStreet: '📦 Traiter rue entière',
actionsRemaining: 'actions restantes aujourd\'hui',
maxSplitsPerSegment: 'Max divisions/segment',
debug: 'Debug',
positionExists: 'Position déjà existante',
noSegmentsSelected: 'Aucun segment sélectionné',
dailyLimitReached: 'Limite quotidienne atteinte! Maximum',
actionsPerDay: 'actions par jour.',
tooManySplits: 'Trop de divisions',
maximum: 'Maximum',
splitsCreated: 'divisions créées!',
splitsDone: 'divisions effectuées',
errors: 'erreurs',
noSplitsDone: 'Aucune division effectuée',
error: 'Erreur',
noLastAction: 'Aucune action précédente disponible',
selectOneSegment: 'Sélectionnez exactement un segment',
noStreetAssignment: 'Le segment n\'a pas d\'attribution de rue',
noSegmentsFound: 'Aucun segment trouvé',
processSegments: 'Traiter les segments de la rue',
splitsOnStreet: 'divisions effectuées',
importSuccess: 'Importation réussie!',
importFailed: 'Importation échouée: Fichier invalide',
presetSaved: 'Préréglage enregistré',
presetLoaded: 'Préréglage chargé',
presetDeleted: 'Préréglage supprimé',
enterPresetName: 'Entrez le nom du préréglage',
segmentLocked: 'Le segment a un verrouillage supérieur',
thanYourRank: 'à votre niveau',
partOfRoundabout: 'Le segment fait partie d\'un rond-point',
isRamp: 'Le segment est une bretelle',
offsetLarger: 'Décalage supérieur à la longueur du segment',
unknownMode: 'Mode inconnu',
noSplitPoints: 'Aucun point de division (segment trop court ou aucun point défini)'
},
nl: {
noSegmentSelected: 'Geen segment geselecteerd',
selectSegments: 'Selecteer een of meer wegsegmenten',
segmentsSelected: 'segmenten geselecteerd',
totalLength: 'Totale lengte',
segmentSelected: 'Segment geselecteerd',
length: 'Lengte',
street: 'Straat',
geometryNodes: 'Geometrie-knooppunten',
lock: 'Vergrendeling',
unnamed: 'Naamloos',
presets: 'Voorinstellingen',
selectPreset: '-- Kies voorinstelling --',
load: 'Laden',
delete: 'Verwijderen',
presetName: 'Naam voorinstelling',
save: 'Opslaan',
countParts: 'Aantal delen',
parts: 'delen',
fixedDistance: 'Vaste afstand',
meters: 'Meter',
percentage: 'Percentage',
commaSeparated: 'Kommagescheiden',
atGeometryNodes: 'Bij geometrie-knooppunten',
manual: 'Handmatig',
addPosition: 'Positie toevoegen:',
addPoint: '+ Punt toevoegen',
noPointsSet: 'Geen punten ingesteld - gebruik de schuifregelaar',
deleteAllPoints: 'Alle punten verwijderen',
advancedOptions: 'Geavanceerde opties',
startPoint: 'Startpunt:',
offsetFromStart: 'Offset vanaf start:',
safety: 'Veiligheid',
enforceMinLength: 'Minimale lengte afdwingen',
tooShortMerged: 'Te korte segmenten worden samengevoegd',
checkLockLevel: 'Vergrendelingsniveau controleren',
protectRoundabouts: 'Rotondes beschermen',
warnRamps: 'Waarschuwen bij opritten',
display: 'Weergave',
previewOnMap: 'Voorbeeld op kaart',
groupUndo: 'Ongedaan maken groeperen',
importExport: 'Importeren/Exporteren',
export: 'Exporteren',
import: 'Importeren',
preview: 'Voorbeeld:',
splitSegment: '✂️ Segment splitsen',
repeatLast: '🔄 Laatste actie herhalen',
processStreet: '📦 Hele straat verwerken',
actionsRemaining: 'acties over vandaag',
maxSplitsPerSegment: 'Max splits/segment',
debug: 'Debug',
positionExists: 'Positie bestaat al',
noSegmentsSelected: 'Geen segmenten geselecteerd',
dailyLimitReached: 'Dagelijkse limiet bereikt! Maximaal',
actionsPerDay: 'acties per dag.',
tooManySplits: 'Te veel splits',
maximum: 'Maximum',
splitsCreated: 'splits gemaakt!',
splitsDone: 'splits uitgevoerd',
errors: 'fouten',
noSplitsDone: 'Geen splits uitgevoerd',
error: 'Fout',
noLastAction: 'Geen vorige actie beschikbaar',
selectOneSegment: 'Selecteer precies één segment',
noStreetAssignment: 'Segment heeft geen straattoewijzing',
noSegmentsFound: 'Geen segmenten gevonden',
processSegments: 'Segmenten van straat verwerken',
splitsOnStreet: 'splits uitgevoerd',
importSuccess: 'Import succesvol!',
importFailed: 'Import mislukt: Ongeldig bestand',
presetSaved: 'Voorinstelling opgeslagen',
presetLoaded: 'Voorinstelling geladen',
presetDeleted: 'Voorinstelling verwijderd',
enterPresetName: 'Voer naam voorinstelling in',
segmentLocked: 'Segment heeft hogere vergrendeling',
thanYourRank: 'dan jouw niveau',
partOfRoundabout: 'Segment maakt deel uit van een rotonde',
isRamp: 'Segment is een oprit',
offsetLarger: 'Offset groter dan segmentlengte',
unknownMode: 'Onbekende modus',
noSplitPoints: 'Geen splitpunten (segment te kort of geen punten ingesteld)'
}
};
let currentLang = 'en'; // Default
// Spracherkennung: 1. WME, 2. Browser, 3. Default (EN)
function detectLanguage() {
// 1. WME Spracheinstellung
try {
const wmeLang = I18n?.currentLocale?.() || I18n?.locale || W?.Config?.language;
if (wmeLang) {
const lang = wmeLang.substring(0, 2).toLowerCase();
if (TRANSLATIONS[lang]) {
log(`Sprache erkannt (WME): ${lang}`);
return lang;
}
}
} catch (e) {}
// 2. Browser Sprache
try {
const browserLang = (navigator.language || navigator.userLanguage || 'en').substring(0, 2).toLowerCase();
if (TRANSLATIONS[browserLang]) {
log(`Sprache erkannt (Browser): ${browserLang}`);
return browserLang;
}
} catch (e) {}
// 3. Default
log('Sprache: Default (en)');
return 'en';
}
function t(key) {
return TRANSLATIONS[currentLang]?.[key] || TRANSLATIONS['en'][key] || key;
}
let wmeSDK = null; // WME SDK instance
let settings = {
mode: 'count',
splitCount: 2,
splitDistance: 10,
minSegmentLength: 6,
enforceMinLength: true,
percentages: [50],
startFrom: 'a',
offset: 0,
showPreview: true,
groupUndo: true,
protectRoundabouts: true,
warnRamps: true,
checkLockLevel: true
};
let presets = [];
let previewLayer = null;
let manualPoints = [];
let isManualMode = false;
let tabPane = null;
let isInitialized = false;
// Tägliches Limit tracking
function getDailyUsage() {
const stored = localStorage.getItem(`${SCRIPT_ID}-daily-usage`);
if (stored) {
try {
const data = JSON.parse(stored);
const today = new Date().toDateString();
if (data.date === today) {
return data.count;
}
} catch (e) {}
}
return 0;
}
function incrementDailyUsage() {
const today = new Date().toDateString();
const currentCount = getDailyUsage();
localStorage.setItem(`${SCRIPT_ID}-daily-usage`, JSON.stringify({
date: today,
count: currentCount + 1
}));
}
function getRemainingDailyUsage() {
return MAX_SPLITS_PER_DAY - getDailyUsage();
}
function log(msg, data) {
if (data !== undefined) {
console.log(`[${SCRIPT_NAME}] ${msg}`, data);
} else {
console.log(`[${SCRIPT_NAME}] ${msg}`);
}
}
function getElement(id) {
return tabPane?.querySelector(`#${id}`) || document.getElementById(id);
}
function init() {
if (isInitialized) {
log('Bereits initialisiert, überspringe');
return;
}
log('Starte Initialisierung...');
log('window.SDK_INITIALIZED:', !!window.SDK_INITIALIZED);
log('W vorhanden:', !!window.W);
log('W.userscripts:', !!window.W?.userscripts);
// Warte auf SDK-Initialisierung (bevorzugt)
if (window.SDK_INITIALIZED && typeof window.SDK_INITIALIZED.then === 'function') {
log('Warte auf SDK_INITIALIZED Promise...');
window.SDK_INITIALIZED.then(onSDKReady).catch(err => {
log('SDK_INITIALIZED Fehler:', err);
fallbackInit();
});
} else {
log('SDK_INITIALIZED nicht verfügbar, nutze Fallback');
fallbackInit();
}
}
function fallbackInit() {
if (isInitialized) return;
if (W?.userscripts?.state?.isReady) {
log('WME bereits ready, initialisiere direkt');
onWmeReady();
} else if (W?.userscripts?.state?.isInitialized) {
log('WME initialisiert, warte auf wme-ready');
document.addEventListener('wme-ready', onWmeReady, { once: true });
} else {
log('Warte auf wme-initialized');
document.addEventListener('wme-initialized', () => {
document.addEventListener('wme-ready', onWmeReady, { once: true });
}, { once: true });
}
}
function onSDKReady() {
if (isInitialized) return;
try {
// Initialisiere das SDK
wmeSDK = getWmeSdk({
scriptId: SCRIPT_ID,
scriptName: SCRIPT_NAME
});
log('WME SDK erfolgreich initialisiert', wmeSDK);
} catch (e) {
log('SDK-Initialisierung fehlgeschlagen:', e.message);
wmeSDK = null;
}
onWmeReady();
}
function onWmeReady() {
if (isInitialized) return;
isInitialized = true;
// Sprache erkennen
currentLang = detectLanguage();
log(`Sprache: ${currentLang}`);
log(`${SCRIPT_NAME} v${SCRIPT_VERSION} wird initialisiert...`);
// Versuche SDK zu initialisieren falls noch nicht geschehen
if (!wmeSDK && typeof getWmeSdk === 'function') {
try {
wmeSDK = getWmeSdk({
scriptId: SCRIPT_ID,
scriptName: SCRIPT_NAME
});
log('WME SDK nachträglich initialisiert');
} catch (e) {
log('SDK nicht verfügbar, nutze Fallback:', e.message);
}
}
// Debug: Zeige verfügbare Split-Methoden
log('=== Verfügbare Split-Methoden ===');
// SplitSegments (mit s am Ende!) ist die korrekte Action
try {
const SplitSegmentsAction = require('Waze/Action/SplitSegments');
log('require Waze/Action/SplitSegments:', typeof SplitSegmentsAction);
} catch (e) {
log('require Waze/Action/SplitSegments: nicht verfügbar');
}
try {
const AddNodeAction = require('Waze/Action/AddNode');
log('require Waze/Action/AddNode:', typeof AddNodeAction);
} catch (e) {
log('require Waze/Action/AddNode: nicht verfügbar');
}
if (wmeSDK?.DataModel?.Segments) {
log('SDK Segments Methoden:', Object.keys(wmeSDK.DataModel.Segments));
}
log('================================');
loadSettings();
loadPresets();
createPreviewLayer();
createUI();
log('Initialisierung abgeschlossen');
log('SDK verfügbar:', !!wmeSDK);
}
function loadSettings() {
const saved = localStorage.getItem(`${SCRIPT_ID}-settings`);
if (saved) {
try {
settings = { ...settings, ...JSON.parse(saved) };
} catch (e) {
log('Fehler beim Laden der Einstellungen');
}
}
}
function saveSettings() {
localStorage.setItem(`${SCRIPT_ID}-settings`, JSON.stringify(settings));
}
function loadPresets() {
const saved = localStorage.getItem(`${SCRIPT_ID}-presets`);
if (saved) {
try {
presets = JSON.parse(saved);
} catch (e) {
presets = [];
}
}
}
function savePresets() {
localStorage.setItem(`${SCRIPT_ID}-presets`, JSON.stringify(presets));
}
function exportSettings() {
const data = { settings, presets };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${SCRIPT_ID}-export.json`;
a.click();
URL.revokeObjectURL(url);
}
function importSettings(file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (data.settings) settings = { ...settings, ...data.settings };
if (data.presets) presets = data.presets;
saveSettings();
savePresets();
updateUI();
showStatus(t('importSuccess'), 'success');
} catch (err) {
showStatus(t('importFailed'), 'error');
}
};
reader.readAsText(file);
}
function createPreviewLayer() {
const layerName = `${SCRIPT_ID}-preview`;
// Prüfe ob Layer bereits existiert
try {
const layers = W.map.getLayersByName('SegmentSplitterPreview');
if (layers && layers.length > 0) {
previewLayer = layers[0];
log('Existierenden Preview-Layer wiederverwendet');
return;
}
} catch (e) {}
for (let i = 0; i < W.map.layers.length; i++) {
const layer = W.map.layers[i];
if (layer.uniqueName === layerName || layer.name === 'SegmentSplitterPreview') {
previewLayer = layer;
log('Existierenden Preview-Layer wiederverwendet');
return;
}
}
try {
const existing = W.map.getLayerByUniqueName?.(layerName);
if (existing) {
previewLayer = existing;
log('Existierenden Preview-Layer wiederverwendet');
return;
}
} catch (e) {}
try {
previewLayer = new OpenLayers.Layer.Vector('SegmentSplitterPreview', {
displayInLayerSwitcher: false,
uniqueName: layerName
});
W.map.addLayer(previewLayer);
log('Neuen Preview-Layer erstellt');
} catch (e) {
log('Konnte Preview-Layer nicht erstellen: ' + e.message);
previewLayer = null;
}
}
function clearPreview() {
if (previewLayer) {
previewLayer.destroyFeatures();
}
}
// Haversine-Formel für Distanzberechnung in Metern (WGS84)
function haversineDistance(lon1, lat1, lon2, lat2) {
const R = 6371000;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
const _0x = [0x1e, 0x0a];
const MAX_SPLITS_PER_SEGMENT = _0x[0];
const MAX_SPLITS_PER_DAY = _0x[1];
// Extrahiere Koordinaten aus verschiedenen Geometrie-Formaten
function getGeometryCoordinates(segment) {
let geometry = null;
// Neue API: getOLGeometry() oder getGeometry()
if (typeof segment.getOLGeometry === 'function') {
geometry = segment.getOLGeometry();
} else if (typeof segment.getGeometry === 'function') {
geometry = segment.getGeometry();
} else {
// Fallback auf alte API
geometry = segment.geometry || segment.attributes?.geometry;
}
if (!geometry) {
log('Keine Geometrie für Segment', segment.attributes?.id);
return [];
}
// GeoJSON Format (coordinates Array) - WGS84
if (geometry.coordinates && Array.isArray(geometry.coordinates)) {
return geometry.coordinates.map(coord => ({
lon: coord[0],
lat: coord[1]
}));
}
// OpenLayers Format (components Array) - Web Mercator
if (geometry.components && Array.isArray(geometry.components)) {
return geometry.components.map(p => {
if (p.x !== undefined && p.y !== undefined) {
const lon = (p.x / 20037508.34) * 180;
let lat = (p.y / 20037508.34) * 180;
lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180)) - Math.PI / 2);
return { lon, lat };
}
return { lon: p.lon || p.x, lat: p.lat || p.y };
});
}
// getVertices Methode
if (typeof geometry.getVertices === 'function') {
const vertices = geometry.getVertices();
return vertices.map(p => {
if (p.x !== undefined && p.y !== undefined) {
const lon = (p.x / 20037508.34) * 180;
let lat = (p.y / 20037508.34) * 180;
lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180)) - Math.PI / 2);
return { lon, lat };
}
return { lon: p.lon || p.x, lat: p.lat || p.y };
});
}
log('Unbekanntes Geometrie-Format', geometry);
return [];
}
function calculateSegmentLength(segment) {
const coords = getGeometryCoordinates(segment);
if (coords.length < 2) {
log('Zu wenige Koordinaten:', coords.length);
return 0;
}
let totalLength = 0;
for (let i = 0; i < coords.length - 1; i++) {
totalLength += haversineDistance(
coords[i].lon, coords[i].lat,
coords[i+1].lon, coords[i+1].lat
);
}
log(`Segment ${segment.attributes?.id}: ${totalLength.toFixed(2)}m (${coords.length} Punkte)`);
return totalLength;
}
// Berechne Punkt auf Segment bei normalisierter Position (0-1)
// Gibt GeoJSON Point zurück für SDK, oder OpenLayers Point für Fallback
function getPointAtPosition(segment, normalizedPosition, returnGeoJSON = false) {
const coords = getGeometryCoordinates(segment);
if (coords.length < 2) {
log('Zu wenige Koordinaten für getPointAtPosition');
return null;
}
// Berechne Segmentlängen
const lengths = [];
let totalLength = 0;
for (let i = 0; i < coords.length - 1; i++) {
const len = haversineDistance(
coords[i].lon, coords[i].lat,
coords[i+1].lon, coords[i+1].lat
);
lengths.push(len);
totalLength += len;
}
const targetLength = totalLength * normalizedPosition;
let accumulatedLength = 0;
for (let i = 0; i < lengths.length; i++) {
if (accumulatedLength + lengths[i] >= targetLength) {
const segmentRatio = (targetLength - accumulatedLength) / lengths[i];
const lon = coords[i].lon + segmentRatio * (coords[i+1].lon - coords[i].lon);
const lat = coords[i].lat + segmentRatio * (coords[i+1].lat - coords[i].lat);
log(`Punkt bei ${(normalizedPosition*100).toFixed(1)}%: lon=${lon.toFixed(6)}, lat=${lat.toFixed(6)}`);
// Für SDK: GeoJSON Point zurückgeben
if (returnGeoJSON) {
return {
type: 'Point',
coordinates: [lon, lat]
};
}
// Für Preview/Fallback: OpenLayers Point
try {
const olPoint = W.userscripts.toOLGeometry({
type: 'Point',
coordinates: [lon, lat]
});
return olPoint;
} catch (e) {
// Manuell konvertieren
const x = lon * 20037508.34 / 180;
const y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180);
const yMerc = y * 20037508.34 / 180;
return new OpenLayers.Geometry.Point(x, yMerc);
}
}
accumulatedLength += lengths[i];
}
return null;
}
function drawPreview(segment, splitPositions) {
clearPreview();
if (!settings.showPreview || !segment || !previewLayer || splitPositions.length === 0) return;
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F'];
const coords = getGeometryCoordinates(segment);
if (coords.length < 2) return;
// Zeichne Schnittpunkte
splitPositions.forEach((pos, idx) => {
const point = getPointAtPosition(segment, pos, false);
if (point) {
const marker = new OpenLayers.Feature.Vector(
point,
{},
{
pointRadius: 8,
fillColor: '#FF0000',
fillOpacity: 0.8,
strokeColor: '#FFFFFF',
strokeWidth: 2,
label: `${idx + 1}`,
fontColor: '#FFFFFF',
fontSize: '10px',
fontWeight: 'bold',
labelYOffset: 0
}
);
previewLayer.addFeatures([marker]);
}
});
// Zeichne farbige Segmentabschnitte
const allPositions = [0, ...splitPositions, 1].sort((a, b) => a - b);
const totalLength = calculateSegmentLength(segment);
for (let i = 0; i < allPositions.length - 1; i++) {
const startPos = allPositions[i];
const endPos = allPositions[i + 1];
const sectionPoints = [];
sectionPoints.push(getPointAtPosition(segment, startPos, false));
let accLen = 0;
const lengths = [];
for (let j = 0; j < coords.length - 1; j++) {
lengths.push(haversineDistance(coords[j].lon, coords[j].lat, coords[j+1].lon, coords[j+1].lat));
}
const totalLen = lengths.reduce((a,b) => a+b, 0);
accLen = 0;
for (let j = 0; j < coords.length - 1; j++) {
accLen += lengths[j];
const posAtPoint = accLen / totalLen;
if (posAtPoint > startPos && posAtPoint < endPos) {
try {
const pt = W.userscripts.toOLGeometry({
type: 'Point',
coordinates: [coords[j+1].lon, coords[j+1].lat]
});
sectionPoints.push(pt);
} catch (e) {}
}
}
sectionPoints.push(getPointAtPosition(segment, endPos, false));
const validPoints = sectionPoints.filter(p => p !== null);
if (validPoints.length >= 2) {
const line = new OpenLayers.Geometry.LineString(validPoints);
const feature = new OpenLayers.Feature.Vector(line, {}, {
strokeColor: colors[i % colors.length],
strokeWidth: 6,
strokeOpacity: 0.7
});
previewLayer.addFeatures([feature]);
const midPoint = validPoints[Math.floor(validPoints.length / 2)];
const sectionLength = totalLength * (endPos - startPos);
const label = new OpenLayers.Feature.Vector(midPoint, {}, {
label: `${sectionLength.toFixed(1)}m`,
fontColor: '#000000',
fontSize: '11px',
fontWeight: 'bold',
labelOutlineColor: '#FFFFFF',
labelOutlineWidth: 3
});
previewLayer.addFeatures([label]);
}
}
log(`Vorschau gezeichnet: ${splitPositions.length} Schnittpunkte`);
}
function setupMapClickHandler() {
// Map-Click-Handler deaktiviert - WME fängt Klicks ab
// Stattdessen nutzen wir jetzt Slider + Button im UI
}
function onMapClick(e) {
// Nicht mehr verwendet - WME fängt Klicks ab
// Manuelle Punkte werden jetzt über Slider + Button gesetzt
}
function findNearestPositionOnSegment(segment, clickPoint) {
const coords = getGeometryCoordinates(segment);
if (coords.length < 2) return null;
const clickLon = (clickPoint.x / 20037508.34) * 180;
let clickLat = (clickPoint.y / 20037508.34) * 180;
clickLat = 180 / Math.PI * (2 * Math.atan(Math.exp(clickLat * Math.PI / 180)) - Math.PI / 2);
let minDist = Infinity;
let bestPos = null;
const lengths = [];
let totalLength = 0;
for (let i = 0; i < coords.length - 1; i++) {
const len = haversineDistance(coords[i].lon, coords[i].lat, coords[i+1].lon, coords[i+1].lat);
lengths.push(len);
totalLength += len;
}
let accLength = 0;
for (let i = 0; i < coords.length - 1; i++) {
const p1 = coords[i];
const p2 = coords[i + 1];
const dx = p2.lon - p1.lon;
const dy = p2.lat - p1.lat;
const t = Math.max(0, Math.min(1,
((clickLon - p1.lon) * dx + (clickLat - p1.lat) * dy) / (dx * dx + dy * dy)
));
const projLon = p1.lon + t * dx;
const projLat = p1.lat + t * dy;
const dist = haversineDistance(clickLon, clickLat, projLon, projLat);
if (dist < minDist) {
minDist = dist;
bestPos = (accLength + t * lengths[i]) / totalLength;
}
accLength += lengths[i];
}
return minDist < 50 ? bestPos : null;
}
function updateManualPointsList() {
const list = getElement(`${SCRIPT_ID}-manual-points`);
if (!list) return;
const segment = getSelectedSegments()[0];
const totalLength = segment ? calculateSegmentLength(segment) : 0;
list.innerHTML = manualPoints.length > 0
? manualPoints.map((pos, idx) => `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 3px 4px; background: ${idx % 2 === 0 ? '#f5f5f5' : '#fff'}; border-radius: 2px; margin-bottom: 2px;">
<span><strong>${idx + 1}.</strong> ${(pos * totalLength).toFixed(1)}m (${(pos * 100).toFixed(1)}%)</span>
<button class="manual-point-remove" data-index="${idx}" style="padding: 2px 8px; cursor: pointer; background: #ff6b6b; color: white; border: none; border-radius: 2px;">×</button>
</div>
`).join('')
: `<em style="color: #888;">${t('noPointsSet')}</em>`;
list.querySelectorAll('.manual-point-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
const idx = parseInt(e.target.dataset.index);
manualPoints.splice(idx, 1);
updateManualPointsList();
updatePreview();
});
});
}
// Zeigt einen temporären Marker für die aktuelle Slider-Position
function updateManualPreviewMarker() {
if (!isManualMode || !settings.showPreview) return;
const segments = getSelectedSegments();
if (segments.length !== 1) return;
const segment = segments[0];
const slider = getElement(`${SCRIPT_ID}-manual-slider`);
if (!slider) return;
const pos = parseInt(slider.value) / 100;
// Zeichne die bestehenden Punkte plus den temporären
const allPositions = [...manualPoints];
if (!allPositions.some(p => Math.abs(p - pos) < 0.01)) {
// Füge temporären Punkt hinzu (wird anders gezeichnet)
drawPreviewWithTemp(segment, manualPoints, pos);
} else {
drawPreview(segment, manualPoints);
}
}
// Zeichnet Vorschau mit temporärem Marker
function drawPreviewWithTemp(segment, positions, tempPos) {
clearPreview();
if (!previewLayer || !segment) return;
const coords = getGeometryCoordinates(segment);
if (coords.length < 2) return;
// Zeichne bestehende Schnittpunkte
positions.forEach((pos, idx) => {
const point = getPointAtPosition(segment, pos, false);
if (point) {
const marker = new OpenLayers.Feature.Vector(point, {}, {
pointRadius: 8,
fillColor: '#FF0000',
fillOpacity: 0.8,
strokeColor: '#FFFFFF',
strokeWidth: 2,
label: `${idx + 1}`,
fontColor: '#FFFFFF',
fontSize: '10px',
fontWeight: 'bold'
});
previewLayer.addFeatures([marker]);
}
});
// Zeichne temporären Marker (gelb, gestrichelt)
const tempPoint = getPointAtPosition(segment, tempPos, false);
if (tempPoint) {
const tempMarker = new OpenLayers.Feature.Vector(tempPoint, {}, {
pointRadius: 10,
fillColor: '#FFD700',
fillOpacity: 0.6,
strokeColor: '#FF8C00',
strokeWidth: 3,
strokeDashstyle: 'dash',
label: `${(tempPos * 100).toFixed(0)}%`,
fontColor: '#000',
fontSize: '11px',
fontWeight: 'bold',
labelYOffset: -18
});
previewLayer.addFeatures([tempMarker]);
}
}
async function createUI() {
const result = W.userscripts.registerSidebarTab(SCRIPT_ID);
result.tabLabel.innerText = 'Split';
result.tabLabel.title = SCRIPT_NAME;
tabPane = result.tabPane;
await W.userscripts.waitForElementConnected(tabPane);
log('TabPane im DOM verfügbar');
tabPane.innerHTML = buildUIHTML();
await new Promise(resolve => setTimeout(resolve, 50));
log('UI HTML eingefügt');
setupEventListeners();
updatePresetSelect();
setupMapClickHandler();
W.selectionManager.events.register('selectionchanged', null, onSelectionChanged);
log('Event-Listener registriert');
onSelectionChanged();
}
function buildUIHTML() {
return `
<div style="padding: 10px; font-size: 12px;">
<div style="margin-bottom: 10px; border-bottom: 1px solid #ccc; padding-bottom: 5px; font-size: 11px; font-weight: bold;">
✂️ Segment Splitter v${SCRIPT_VERSION}
</div>
<div id="${SCRIPT_ID}-segment-info" style="margin-bottom: 10px; padding: 8px; background: #f5f5f5; border-radius: 4px;">
<strong>${t('noSegmentSelected')}</strong>
</div>
<div id="${SCRIPT_ID}-warnings" style="margin-bottom: 10px; display: none;"></div>
<details style="margin-bottom: 10px;">
<summary style="cursor: pointer; font-weight: bold; padding: 5px; background: #e8e8e8; border-radius: 4px;">
📁 ${t('presets')}
</summary>
<div style="padding: 8px; background: #f9f9f9; border-radius: 0 0 4px 4px;">
<select id="${SCRIPT_ID}-preset-select" style="width: 100%; margin-bottom: 5px;">
<option value="">${t('selectPreset')}</option>
</select>
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
<button id="${SCRIPT_ID}-preset-load" style="flex: 1; padding: 4px;">${t('load')}</button>
<button id="${SCRIPT_ID}-preset-delete" style="flex: 1; padding: 4px;">${t('delete')}</button>
</div>
<div style="display: flex; gap: 5px;">
<input type="text" id="${SCRIPT_ID}-preset-name" placeholder="${t('presetName')}" style="flex: 2; padding: 4px;">
<button id="${SCRIPT_ID}-preset-save" style="flex: 1; padding: 4px;">${t('save')}</button>
</div>
</div>
</details>
<div style="margin-bottom: 10px; padding: 8px; background: #f0f0f0; border-radius: 4px;">
<div style="margin-bottom: 8px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="${SCRIPT_ID}-mode" value="count" ${settings.mode === 'count' ? 'checked' : ''}>
<span style="margin-left: 5px;"><strong>${t('countParts')}</strong></span>
</label>
<div style="margin-left: 20px; margin-top: 4px;">
<input type="number" id="${SCRIPT_ID}-count" value="${settings.splitCount}" min="2" max="50" style="width: 50px;"> ${t('parts')}
</div>
</div>
<div style="margin-bottom: 8px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="${SCRIPT_ID}-mode" value="distance" ${settings.mode === 'distance' ? 'checked' : ''}>
<span style="margin-left: 5px;"><strong>${t('fixedDistance')}</strong></span>
</label>
<div style="margin-left: 20px; margin-top: 4px;">
<input type="number" id="${SCRIPT_ID}-distance" value="${settings.splitDistance}" min="1" max="1000" style="width: 50px;"> ${t('meters')}
</div>
</div>
<div style="margin-bottom: 8px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="${SCRIPT_ID}-mode" value="percent" ${settings.mode === 'percent' ? 'checked' : ''}>
<span style="margin-left: 5px;"><strong>${t('percentage')}</strong></span>
</label>
<div style="margin-left: 20px; margin-top: 4px;">
<input type="text" id="${SCRIPT_ID}-percentages" value="${settings.percentages.join(', ')}" style="width: 100px;" placeholder="25, 50, 75">
<small style="display: block; color: #666;">${t('commaSeparated')}</small>
</div>
</div>
<div style="margin-bottom: 8px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="${SCRIPT_ID}-mode" value="geometry" ${settings.mode === 'geometry' ? 'checked' : ''}>
<span style="margin-left: 5px;"><strong>${t('atGeometryNodes')}</strong></span>
</label>
</div>
<div>
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="radio" name="${SCRIPT_ID}-mode" value="manual" ${settings.mode === 'manual' ? 'checked' : ''}>
<span style="margin-left: 5px;"><strong>${t('manual')}</strong></span>
</label>
<div id="${SCRIPT_ID}-manual-container" style="margin-left: 20px; margin-top: 4px; display: ${settings.mode === 'manual' ? 'block' : 'none'};">
<div style="margin-bottom: 8px;">
<label style="font-size: 11px;"><strong>${t('addPosition')}</strong></label>
<div style="display: flex; gap: 5px; align-items: center; margin-top: 4px;">
<input type="range" id="${SCRIPT_ID}-manual-slider" min="1" max="99" value="50" style="flex: 1;">
<input type="number" id="${SCRIPT_ID}-manual-percent" min="1" max="99" value="50" style="width: 45px;" title="%">
<span style="font-size: 11px;">%</span>
</div>
<div style="display: flex; gap: 5px; margin-top: 4px;">
<button id="${SCRIPT_ID}-manual-add" style="flex: 1; padding: 4px; background: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer;">${t('addPoint')}</button>
</div>
</div>
<div id="${SCRIPT_ID}-manual-points" style="max-height: 100px; overflow-y: auto; font-size: 11px; border: 1px solid #ddd; padding: 4px; border-radius: 3px; background: #fff;">
<em>${t('noPointsSet')}</em>
</div>
<button id="${SCRIPT_ID}-manual-clear" style="margin-top: 4px; padding: 4px 8px; width: 100%;">${t('deleteAllPoints')}</button>
</div>
</div>
</div>
<details style="margin-bottom: 10px;">
<summary style="cursor: pointer; font-weight: bold; padding: 5px; background: #e8e8e8; border-radius: 4px;">
⚙️ ${t('advancedOptions')}
</summary>
<div style="padding: 8px; background: #f9f9f9; border-radius: 0 0 4px 4px;">
<div style="margin-bottom: 8px;">
<label><strong>${t('startPoint')}</strong></label><br>
<label style="margin-right: 10px;">
<input type="radio" name="${SCRIPT_ID}-start" value="a" ${settings.startFrom === 'a' ? 'checked' : ''}> A-Node
</label>
<label>
<input type="radio" name="${SCRIPT_ID}-start" value="b" ${settings.startFrom === 'b' ? 'checked' : ''}> B-Node
</label>
</div>
<div>
<label><strong>${t('offsetFromStart')}</strong></label><br>
<input type="number" id="${SCRIPT_ID}-offset" value="${settings.offset}" min="0" max="1000" style="width: 50px;"> ${t('meters')}
</div>
</div>
</details>
<details style="margin-bottom: 10px;">
<summary style="cursor: pointer; font-weight: bold; padding: 5px; background: #e8e8e8; border-radius: 4px;">
🛡️ ${t('safety')}
</summary>
<div style="padding: 8px; background: #f9f9f9; border-radius: 0 0 4px 4px;">
<label style="display: block; margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid #ddd;">
<input type="checkbox" id="${SCRIPT_ID}-enforce-minlength" ${settings.enforceMinLength ? 'checked' : ''}>
<strong>${t('enforceMinLength')}</strong>
<div style="margin-left: 20px; margin-top: 4px;">
<input type="number" id="${SCRIPT_ID}-minlength" value="${settings.minSegmentLength}" min="1" max="100" style="width: 50px;"> ${t('meters')}
<small style="display: block; color: #666;">${t('tooShortMerged')}</small>
</div>
</label>
<label style="display: block; margin-bottom: 5px;">
<input type="checkbox" id="${SCRIPT_ID}-check-lock" ${settings.checkLockLevel ? 'checked' : ''}>
${t('checkLockLevel')}
</label>
<label style="display: block; margin-bottom: 5px;">
<input type="checkbox" id="${SCRIPT_ID}-protect-roundabouts" ${settings.protectRoundabouts ? 'checked' : ''}>
${t('protectRoundabouts')}
</label>
<label style="display: block;">
<input type="checkbox" id="${SCRIPT_ID}-warn-ramps" ${settings.warnRamps ? 'checked' : ''}>
${t('warnRamps')}
</label>
</div>
</details>
<details style="margin-bottom: 10px;">
<summary style="cursor: pointer; font-weight: bold; padding: 5px; background: #e8e8e8; border-radius: 4px;">
👁️ ${t('display')}
</summary>
<div style="padding: 8px; background: #f9f9f9; border-radius: 0 0 4px 4px;">
<label style="display: block; margin-bottom: 5px;">
<input type="checkbox" id="${SCRIPT_ID}-show-preview" ${settings.showPreview ? 'checked' : ''}>
${t('previewOnMap')}
</label>
<label style="display: block;">
<input type="checkbox" id="${SCRIPT_ID}-group-undo" ${settings.groupUndo ? 'checked' : ''}>
${t('groupUndo')}
</label>
</div>
</details>
<details style="margin-bottom: 10px;">
<summary style="cursor: pointer; font-weight: bold; padding: 5px; background: #e8e8e8; border-radius: 4px;">
📤 ${t('importExport')}
</summary>
<div style="padding: 8px; background: #f9f9f9; border-radius: 0 0 4px 4px;">
<button id="${SCRIPT_ID}-export" style="width: 100%; padding: 5px; margin-bottom: 5px;">${t('export')}</button>
<input type="file" id="${SCRIPT_ID}-import-file" accept=".json" style="display: none;">
<button id="${SCRIPT_ID}-import" style="width: 100%; padding: 5px;">${t('import')}</button>
</div>
</details>
<div id="${SCRIPT_ID}-preview" style="margin-bottom: 10px; padding: 8px; background: #e8f4e8; border-radius: 4px; display: none;">
<strong>${t('preview')}</strong>
<div id="${SCRIPT_ID}-preview-text"></div>
</div>
<button id="${SCRIPT_ID}-split-btn" style="width: 100%; padding: 10px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; margin-bottom: 5px;" disabled>
${t('splitSegment')}
</button>
<button id="${SCRIPT_ID}-repeat-btn" style="width: 100%; padding: 8px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; margin-bottom: 5px;" disabled>
${t('repeatLast')}
</button>
<button id="${SCRIPT_ID}-batch-btn" style="width: 100%; padding: 8px; background: #FF9800; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;" disabled>
${t('processStreet')}
</button>
<div id="${SCRIPT_ID}-status" style="margin-top: 10px; padding: 8px; border-radius: 4px; display: none;"></div>
<div id="${SCRIPT_ID}-daily-limit" style="margin-top: 10px; padding: 6px; background: #fff3cd; border-radius: 4px; font-size: 11px; text-align: center;">
📊 <strong>${getRemainingDailyUsage()}</strong>/${MAX_SPLITS_PER_DAY} ${t('actionsRemaining')}
</div>
<div style="margin-top: 8px; font-size: 10px; color: #888; text-align: center;">
${t('maxSplitsPerSegment')}: ${MAX_SPLITS_PER_SEGMENT}
</div>
</div>
`;
}
function setupEventListeners() {
const getEl = (id) => tabPane.querySelector(`#${id}`);
const addListener = (id, event, handler) => {
const el = getEl(id);
if (el) {
el.addEventListener(event, handler);
} else {
log(`Element nicht gefunden: ${id}`);
}
};
// Mode selection
const modeRadios = tabPane.querySelectorAll(`input[name="${SCRIPT_ID}-mode"]`);
log(`Gefundene Mode-Radios: ${modeRadios.length}`);
modeRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
log(`Mode geändert zu: ${e.target.value}`);
settings.mode = e.target.value;
isManualMode = e.target.value === 'manual';
const manualContainer = getEl(`${SCRIPT_ID}-manual-container`);
if (manualContainer) manualContainer.style.display = isManualMode ? 'block' : 'none';
if (isManualMode) manualPoints = [];
saveSettings();
updatePreview();
});
});
addListener(`${SCRIPT_ID}-count`, 'input', (e) => {
settings.splitCount = parseInt(e.target.value) || 2;
saveSettings();
updatePreview();
});
addListener(`${SCRIPT_ID}-distance`, 'input', (e) => {
settings.splitDistance = parseFloat(e.target.value) || 10;
saveSettings();
updatePreview();
});
addListener(`${SCRIPT_ID}-percentages`, 'input', (e) => {
settings.percentages = e.target.value.split(',')
.map(s => parseFloat(s.trim()))
.filter(n => !isNaN(n) && n > 0 && n < 100);
saveSettings();
updatePreview();
});
tabPane.querySelectorAll(`input[name="${SCRIPT_ID}-start"]`).forEach(radio => {
radio.addEventListener('change', (e) => {
settings.startFrom = e.target.value;
saveSettings();
updatePreview();
});
});
addListener(`${SCRIPT_ID}-offset`, 'input', (e) => {
settings.offset = parseFloat(e.target.value) || 0;
saveSettings();
updatePreview();
});
addListener(`${SCRIPT_ID}-enforce-minlength`, 'change', (e) => {
settings.enforceMinLength = e.target.checked;
saveSettings();
updatePreview();
});
addListener(`${SCRIPT_ID}-minlength`, 'input', (e) => {
settings.minSegmentLength = parseFloat(e.target.value) || 6;
saveSettings();
updatePreview();
});
addListener(`${SCRIPT_ID}-check-lock`, 'change', (e) => {
settings.checkLockLevel = e.target.checked;
saveSettings();
onSelectionChanged();
});
addListener(`${SCRIPT_ID}-protect-roundabouts`, 'change', (e) => {
settings.protectRoundabouts = e.target.checked;
saveSettings();
onSelectionChanged();
});
addListener(`${SCRIPT_ID}-warn-ramps`, 'change', (e) => {
settings.warnRamps = e.target.checked;
saveSettings();
onSelectionChanged();
});
addListener(`${SCRIPT_ID}-show-preview`, 'change', (e) => {
settings.showPreview = e.target.checked;
saveSettings();
updatePreview();
});
addListener(`${SCRIPT_ID}-group-undo`, 'change', (e) => {
settings.groupUndo = e.target.checked;
saveSettings();
});
addListener(`${SCRIPT_ID}-manual-clear`, 'click', () => {
manualPoints = [];
updateManualPointsList();
updatePreview();
});
// Manueller Modus: Slider und Eingabefeld synchronisieren
const manualSlider = getEl(`${SCRIPT_ID}-manual-slider`);
const manualPercent = getEl(`${SCRIPT_ID}-manual-percent`);
if (manualSlider && manualPercent) {
manualSlider.addEventListener('input', (e) => {
manualPercent.value = e.target.value;
updateManualPreviewMarker();
});
manualPercent.addEventListener('input', (e) => {
let val = parseInt(e.target.value) || 50;
val = Math.max(1, Math.min(99, val));
manualSlider.value = val;
updateManualPreviewMarker();
});
}
addListener(`${SCRIPT_ID}-manual-add`, 'click', () => {
const percentInput = getEl(`${SCRIPT_ID}-manual-percent`);
if (!percentInput) return;
const percent = parseInt(percentInput.value) || 50;
const pos = percent / 100;
// Prüfe ob Position bereits existiert (mit Toleranz)
const exists = manualPoints.some(p => Math.abs(p - pos) < 0.01);
if (exists) {
showStatus(t('positionExists'), 'error');
return;
}
manualPoints.push(pos);
manualPoints.sort((a, b) => a - b);
updateManualPointsList();
updatePreview();
log(`Manueller Punkt hinzugefügt bei ${percent}%`);
});
addListener(`${SCRIPT_ID}-split-btn`, 'click', () => {
log('Split-Button geklickt');
splitSegments();
});
addListener(`${SCRIPT_ID}-repeat-btn`, 'click', repeatLastAction);
addListener(`${SCRIPT_ID}-batch-btn`, 'click', batchProcessStreet);
addListener(`${SCRIPT_ID}-preset-save`, 'click', saveCurrentPreset);
addListener(`${SCRIPT_ID}-preset-load`, 'click', loadSelectedPreset);
addListener(`${SCRIPT_ID}-preset-delete`, 'click', deleteSelectedPreset);
addListener(`${SCRIPT_ID}-export`, 'click', exportSettings);
addListener(`${SCRIPT_ID}-import`, 'click', () => {
const fileInput = getEl(`${SCRIPT_ID}-import-file`);
if (fileInput) fileInput.click();
});
addListener(`${SCRIPT_ID}-import-file`, 'change', (e) => {
if (e.target.files[0]) importSettings(e.target.files[0]);
});
log('Alle Event-Listener registriert');
}
function saveCurrentPreset() {
const nameInput = getElement(`${SCRIPT_ID}-preset-name`);
const name = nameInput?.value?.trim();
if (!name) {
showStatus(t('enterPresetName'), 'error');
return;
}
const preset = {
name,
mode: settings.mode,
splitCount: settings.splitCount,
splitDistance: settings.splitDistance,
minSegmentLength: settings.minSegmentLength,
enforceMinLength: settings.enforceMinLength,
percentages: [...settings.percentages],
startFrom: settings.startFrom,
offset: settings.offset
};
const existingIdx = presets.findIndex(p => p.name === name);
if (existingIdx >= 0) {
presets[existingIdx] = preset;
} else {
presets.push(preset);
}
savePresets();
updatePresetSelect();
if (nameInput) nameInput.value = '';
showStatus(`${t('presetSaved')}: "${name}"`, 'success');
}
function loadSelectedPreset() {
const select = getElement(`${SCRIPT_ID}-preset-select`);
const preset = presets.find(p => p.name === select?.value);
if (!preset) return;
settings.mode = preset.mode;
settings.splitCount = preset.splitCount;
settings.splitDistance = preset.splitDistance;
settings.minSegmentLength = preset.minSegmentLength;
settings.enforceMinLength = preset.enforceMinLength ?? true;
settings.percentages = [...preset.percentages];
settings.startFrom = preset.startFrom;
settings.offset = preset.offset;
saveSettings();
updateUI();
showStatus(`${t('presetLoaded')}: "${preset.name}"`, 'success');
}
function deleteSelectedPreset() {
const select = getElement(`${SCRIPT_ID}-preset-select`);
const idx = presets.findIndex(p => p.name === select?.value);
if (idx >= 0) {
const name = presets[idx].name;
presets.splice(idx, 1);
savePresets();
updatePresetSelect();
showStatus(`${t('presetDeleted')}: "${name}"`, 'success');
}
}
function updatePresetSelect() {
const select = tabPane?.querySelector(`#${SCRIPT_ID}-preset-select`);
if (!select) return;
select.innerHTML = `<option value="">${t('selectPreset')}</option>` +
presets.map(p => `<option value="${p.name}">${p.name}</option>`).join('');
}
function updateUI() {
if (!tabPane) return;
const setChecked = (selector, value) => {
const el = tabPane.querySelector(selector);
if (el) el.checked = value;
};
const setValue = (id, value) => {
const el = tabPane.querySelector(`#${id}`);
if (el) el.value = value;
};
setChecked(`input[name="${SCRIPT_ID}-mode"][value="${settings.mode}"]`, true);
setValue(`${SCRIPT_ID}-count`, settings.splitCount);
setValue(`${SCRIPT_ID}-distance`, settings.splitDistance);
setChecked(`#${SCRIPT_ID}-enforce-minlength`, settings.enforceMinLength);
setValue(`${SCRIPT_ID}-minlength`, settings.minSegmentLength);
setValue(`${SCRIPT_ID}-percentages`, settings.percentages.join(', '));
setChecked(`input[name="${SCRIPT_ID}-start"][value="${settings.startFrom}"]`, true);
setValue(`${SCRIPT_ID}-offset`, settings.offset);
setChecked(`#${SCRIPT_ID}-check-lock`, settings.checkLockLevel);
setChecked(`#${SCRIPT_ID}-protect-roundabouts`, settings.protectRoundabouts);
setChecked(`#${SCRIPT_ID}-warn-ramps`, settings.warnRamps);
setChecked(`#${SCRIPT_ID}-show-preview`, settings.showPreview);
setChecked(`#${SCRIPT_ID}-group-undo`, settings.groupUndo);
isManualMode = settings.mode === 'manual';
const manualContainer = tabPane.querySelector(`#${SCRIPT_ID}-manual-container`);
if (manualContainer) manualContainer.style.display = isManualMode ? 'block' : 'none';
updatePreview();
}
function getSelectedSegments() {
try {
// Methode 1: SDK (bevorzugt)
if (wmeSDK?.Editing?.getSelection) {
const selection = wmeSDK.Editing.getSelection();
log('SDK Selection:', selection);
if (selection?.segments?.length > 0) {
// SDK gibt IDs zurück, wir brauchen die Objekte
const segments = selection.segments
.map(id => W.model.segments.getObjectById(id))
.filter(s => s != null);
log(`SDK: ${segments.length} Segmente ausgewählt`);
return segments;
}
}
// Methode 2: WME Features (neu)
const wmeFeatures = W.selectionManager.getSelectedWMEFeatures?.() || [];
if (wmeFeatures.length > 0) {
const segments = wmeFeatures
.filter(f => f.model?.type === 'segment' || f._wmeObject?.type === 'segment')
.map(f => f.model || f._wmeObject);
if (segments.length > 0) {
log(`WME Features: ${segments.length} Segmente ausgewählt`);
return segments;
}
}
// Methode 3: Alte getSelectedFeatures
const features = W.selectionManager.getSelectedFeatures?.() || [];
if (features.length > 0) {
const segments = features
.filter(f => {
const model = f.model || f._wmeObject || f;
return model?.type === 'segment' || model?.attributes?.type === 'segment';
})
.map(f => f.model || f._wmeObject || f);
if (segments.length > 0) {
log(`getSelectedFeatures: ${segments.length} Segmente ausgewählt`);
return segments;
}
}
// Methode 4: Direkt aus selectionManager.selectedItems
if (W.selectionManager.selectedItems) {
const items = Array.isArray(W.selectionManager.selectedItems)
? W.selectionManager.selectedItems
: Object.values(W.selectionManager.selectedItems);
const segments = items.filter(item => {
const type = item?.type || item?.model?.type || item?.attributes?.type;
return type === 'segment';
});
if (segments.length > 0) {
log(`selectedItems: ${segments.length} Segmente ausgewählt`);
return segments;
}
}
// Methode 5: getSelectedDataModelObjects (neueste API)
if (W.selectionManager.getSelectedDataModelObjects) {
const objects = W.selectionManager.getSelectedDataModelObjects();
const segments = objects.filter(obj => obj?.type === 'segment');
if (segments.length > 0) {
log(`getSelectedDataModelObjects: ${segments.length} Segmente ausgewählt`);
return segments;
}
}
log('Keine Segmente gefunden mit allen Methoden');
return [];
} catch (e) {
log('Fehler bei getSelectedSegments:', e.message);
return [];
}
}
function getStreetName(segment) {
const streetID = segment.attributes.primaryStreetID;
if (!streetID) return t('unnamed');
const street = W.model.streets.getObjectById(streetID);
return street?.attributes?.name || t('unnamed');
}
function getUserRank() {
return W.loginManager.user?.attributes?.rank || 0;
}
function isRoundabout(segment) {
return segment.attributes.junctionID != null;
}
function isRamp(segment) {
return segment.attributes.roadType === 4;
}
function onSelectionChanged() {
clearPreview();
manualPoints = [];
const infoDiv = getElement(`${SCRIPT_ID}-segment-info`);
const warningsDiv = getElement(`${SCRIPT_ID}-warnings`);
const splitBtn = getElement(`${SCRIPT_ID}-split-btn`);
const repeatBtn = getElement(`${SCRIPT_ID}-repeat-btn`);
const batchBtn = getElement(`${SCRIPT_ID}-batch-btn`);
const previewDiv = getElement(`${SCRIPT_ID}-preview`);
if (!infoDiv) {
log('UI-Elemente nicht gefunden');
return;
}
// Debug: Zeige was im selectionManager ist
log('=== Selection Debug ===');
log('selectionManager:', W.selectionManager);
if (W.selectionManager.getSelectedWMEFeatures) {
log('getSelectedWMEFeatures:', W.selectionManager.getSelectedWMEFeatures());
}
if (W.selectionManager.getSelectedFeatures) {
log('getSelectedFeatures:', W.selectionManager.getSelectedFeatures());
}
if (W.selectionManager.getSelectedDataModelObjects) {
log('getSelectedDataModelObjects:', W.selectionManager.getSelectedDataModelObjects());
}
if (wmeSDK?.Editing?.getSelection) {
log('SDK getSelection:', wmeSDK.Editing.getSelection());
}
log('=======================');
const segments = getSelectedSegments();
const warnings = [];
if (segments.length === 0) {
infoDiv.innerHTML = `<strong>${t('noSegmentSelected')}</strong><br><small>${t('selectSegments')}</small>`;
if (splitBtn) splitBtn.disabled = true;
if (repeatBtn) repeatBtn.disabled = true;
if (batchBtn) batchBtn.disabled = true;
if (previewDiv) previewDiv.style.display = 'none';
if (warningsDiv) warningsDiv.style.display = 'none';
return;
}
if (segments.length > 1) {
const totalLength = segments.reduce((sum, s) => sum + calculateSegmentLength(s), 0);
infoDiv.innerHTML = `
<strong>${segments.length} ${t('segmentsSelected')}</strong><br>
<small>${t('totalLength')}: ${totalLength.toFixed(2)} ${t('meters')}</small>
`;
} else {
const segment = segments[0];
const length = calculateSegmentLength(segment);
const coords = getGeometryCoordinates(segment);
infoDiv.innerHTML = `
<strong>${t('segmentSelected')}</strong><br>
<small>ID: ${segment.attributes.id}</small><br>
<small>${t('length')}: ${length.toFixed(2)} ${t('meters')}</small><br>
<small>${t('street')}: ${getStreetName(segment)}</small><br>
<small>${t('geometryNodes')}: ${coords.length}</small><br>
<small>${t('lock')}: ${(segment.attributes.lockRank || 0) + 1}</small>
`;
}
segments.forEach(segment => {
if (settings.checkLockLevel) {
const lockRank = segment.attributes.lockRank || 0;
const userRank = getUserRank();
if (lockRank > userRank) {
warnings.push(`⚠️ ${t('segmentLocked')} (${lockRank + 1}) ${t('thanYourRank')} (${userRank + 1})`);
}
}
if (settings.protectRoundabouts && isRoundabout(segment)) {
warnings.push(`🚫 Segment ${segment.attributes.id} ${t('partOfRoundabout')}`);
}
if (settings.warnRamps && isRamp(segment)) {
warnings.push(`⚠️ Segment ${segment.attributes.id} ${t('isRamp')}`);
}
});
if (warningsDiv) {
if (warnings.length > 0) {
warningsDiv.style.display = 'block';
warningsDiv.innerHTML = warnings.map(w =>
`<div style="padding: 4px 8px; margin-bottom: 4px; background: #fff3cd; border-radius: 4px; font-size: 11px;">${w}</div>`
).join('');
} else {
warningsDiv.style.display = 'none';
}
}
const hasBlockingWarnings = warnings.some(w => w.startsWith('🚫'));
if (splitBtn) splitBtn.disabled = hasBlockingWarnings;
if (repeatBtn) repeatBtn.disabled = !localStorage.getItem(`${SCRIPT_ID}-lastAction`);
if (batchBtn) batchBtn.disabled = segments.length !== 1;
updatePreview();
updateManualPointsList();
}
function updatePreview() {
const previewDiv = getElement(`${SCRIPT_ID}-preview`);
const previewText = getElement(`${SCRIPT_ID}-preview-text`);
const splitBtn = getElement(`${SCRIPT_ID}-split-btn`);
const segments = getSelectedSegments();
if (segments.length === 0) {
if (previewDiv) previewDiv.style.display = 'none';
clearPreview();
return;
}
if (segments.length > 1) {
const results = segments.map(s => {
const length = calculateSegmentLength(s);
return calculateSplitPoints(length);
});
const totalCuts = results.reduce((sum, r) => sum + (r.cuts || 0), 0);
const totalParts = results.reduce((sum, r) => sum + (r.count || 0), 0);
if (previewDiv) {
previewDiv.style.display = 'block';
previewDiv.style.background = '#e8f4e8';
}
if (previewText) {
previewText.innerHTML = `
<span style="color: green;">
${segments.length} Segmente → ${totalParts} Teile<br>
${totalCuts} Schnittpunkte gesamt
</span>
`;
}
clearPreview();
return;
}
const segment = segments[0];
const length = calculateSegmentLength(segment);
const splitInfo = calculateSplitPoints(length);
log('Split-Info berechnet:', splitInfo);
if (splitInfo.error) {
if (previewDiv) {
previewDiv.style.display = 'block';
previewDiv.style.background = '#ffe8e8';
}
if (previewText) previewText.innerHTML = `<span style="color: red;">${splitInfo.error}</span>`;
if (splitBtn) splitBtn.disabled = true;
clearPreview();
return;
}
if (previewDiv) {
previewDiv.style.display = 'block';
previewDiv.style.background = '#e8f4e8';
}
if (previewText) {
previewText.innerHTML = `
<span style="color: green;">
${splitInfo.count} Teilstücke<br>
Je ca. ${splitInfo.segmentLength.toFixed(2)} Meter<br>
${splitInfo.cuts} Schnittpunkte
</span>
`;
}
if (splitBtn) splitBtn.disabled = false;
if (settings.showPreview && splitInfo.positions) {
drawPreview(segment, splitInfo.positions);
}
}
function calculateSplitPoints(totalLength) {
let count, segmentLength, positions = [];
const effectiveLength = totalLength - settings.offset;
if (effectiveLength <= 0) {
return { error: t('offsetLarger') };
}
const offsetRatio = settings.offset / totalLength;
switch (settings.mode) {
case 'count':
count = settings.splitCount;
segmentLength = effectiveLength / count;
for (let i = 1; i < count; i++) {
let pos = offsetRatio + (i / count) * (1 - offsetRatio);
if (settings.startFrom === 'b') pos = 1 - pos;
positions.push(pos);
}
break;
case 'distance':
const fullSegments = Math.floor(effectiveLength / settings.splitDistance);
const remainder = effectiveLength - (fullSegments * settings.splitDistance);
if (settings.enforceMinLength && remainder > 0 && remainder < settings.minSegmentLength && fullSegments > 0) {
count = fullSegments;
for (let i = 1; i < count; i++) {
let pos = offsetRatio + (i * settings.splitDistance / totalLength);
if (settings.startFrom === 'b') pos = 1 - pos;
positions.push(pos);
}
segmentLength = settings.splitDistance;
} else {
count = fullSegments + (remainder > 0 ? 1 : 0);
for (let i = 1; i < count; i++) {
let pos = offsetRatio + (i * settings.splitDistance / totalLength);
if (pos >= 1) break;
if (settings.startFrom === 'b') pos = 1 - pos;
positions.push(pos);
}
segmentLength = settings.splitDistance;
}
count = positions.length + 1;
break;
case 'percent':
positions = settings.percentages.map(p => {
let pos = p / 100;
if (settings.startFrom === 'b') pos = 1 - pos;
return pos;
});
count = positions.length + 1;
segmentLength = totalLength / count;
break;
case 'geometry':
const segment = getSelectedSegments()[0];
if (segment) {
const coords = getGeometryCoordinates(segment);
if (coords.length > 2) {
let accLength = 0;
const lengths = [];
for (let i = 0; i < coords.length - 1; i++) {
lengths.push(haversineDistance(
coords[i].lon, coords[i].lat,
coords[i+1].lon, coords[i+1].lat
));
}
const geoTotalLength = lengths.reduce((a, b) => a + b, 0);
for (let i = 1; i < coords.length - 1; i++) {
accLength += lengths[i - 1];
positions.push(accLength / geoTotalLength);
}
}
}
count = positions.length + 1;
segmentLength = totalLength / count;
break;
case 'manual':
positions = [...manualPoints];
count = positions.length + 1;
segmentLength = count > 1 ? totalLength / count : totalLength;
break;
default:
return { error: t('unknownMode') };
}
if (positions.length === 0) {
return { error: t('noSplitPoints') };
}
positions.sort((a, b) => a - b);
if (settings.enforceMinLength && settings.mode !== 'distance') {
positions = enforceMinimumLength(positions, totalLength);
}
return {
count: positions.length + 1,
segmentLength: segmentLength,
cuts: positions.length,
positions: positions
};
}
function enforceMinimumLength(positions, totalLength) {
if (positions.length === 0) return positions;
const minRatio = settings.minSegmentLength / totalLength;
let result = [];
let allPositions = [0, ...positions, 1];
for (let i = 1; i < allPositions.length - 1; i++) {
const segmentBefore = allPositions[i] - (result.length > 0 ? result[result.length - 1] : 0);
const segmentAfter = allPositions[i + 1] - allPositions[i];
if (segmentAfter < minRatio && i < allPositions.length - 2) {
continue;
}
if (segmentBefore < minRatio) {
continue;
}
result.push(allPositions[i]);
}
if (result.length > 0) {
const lastSegment = 1 - result[result.length - 1];
if (lastSegment < minRatio) {
result.pop();
}
}
return result;
}
function splitSegments() {
const segments = getSelectedSegments();
if (segments.length === 0) {
showStatus(t('noSegmentsSelected'), 'error');
return;
}
// Prüfe tägliches Limit
const remainingToday = getRemainingDailyUsage();
if (remainingToday <= 0) {
showStatus(`${t('dailyLimitReached')} ${MAX_SPLITS_PER_DAY} ${t('actionsPerDay')}`, 'error');
log('Tageslimit erreicht');
return;
}
log(`Starte Split für ${segments.length} Segment(e)`);
log(`SDK verfügbar: ${!!wmeSDK}`);
log(`Verbleibende Aktionen heute: ${remainingToday}`);
let totalSplits = 0;
let errors = [];
// Speichere letzte Aktion
localStorage.setItem(`${SCRIPT_ID}-lastAction`, JSON.stringify({
mode: settings.mode,
splitCount: settings.splitCount,
splitDistance: settings.splitDistance,
minSegmentLength: settings.minSegmentLength,
enforceMinLength: settings.enforceMinLength,
percentages: settings.percentages,
startFrom: settings.startFrom,
offset: settings.offset
}));
try {
segments.forEach(segment => {
try {
const result = splitSingleSegment(segment);
totalSplits += result;
} catch (e) {
errors.push(`Segment ${segment.attributes.id}: ${e.message}`);
log(`Fehler bei Segment ${segment.attributes.id}:`, e);
}
});
if (errors.length > 0) {
showStatus(`${totalSplits} ${t('splitsDone')}, ${errors.length} ${t('errors')}`, 'error');
log('Fehler:', errors);
} else if (totalSplits > 0) {
// Zähle als eine Aktion (nicht pro Split)
incrementDailyUsage();
const remaining = getRemainingDailyUsage();
showStatus(`${totalSplits} ${t('splitsCreated')} (${remaining}/${MAX_SPLITS_PER_DAY} ${t('actionsRemaining')})`, 'success');
} else {
showStatus(t('noSplitsDone'), 'error');
}
const repeatBtn = getElement(`${SCRIPT_ID}-repeat-btn`);
if (repeatBtn) repeatBtn.disabled = false;
clearPreview();
} catch (e) {
showStatus(`${t('error')}: ${e.message}`, 'error');
log(`Allgemeiner Fehler:`, e);
}
}
function splitSingleSegment(segment) {
const length = calculateSegmentLength(segment);
log(`Segment ${segment.attributes.id}: Länge = ${length.toFixed(2)}m`);
const splitInfo = calculateSplitPoints(length);
log(`Split-Info:`, splitInfo);
if (splitInfo.error) {
throw new Error(splitInfo.error);
}
const positions = splitInfo.positions;
if (!positions || positions.length === 0) {
throw new Error(t('noSplitPoints'));
}
// Prüfe Maximum Splits pro Segment
if (positions.length > MAX_SPLITS_PER_SEGMENT) {
throw new Error(`${t('tooManySplits')} (${positions.length})! ${t('maximum')}: ${MAX_SPLITS_PER_SEGMENT}`);
}
// Lade SplitSegments Action (mit s am Ende!)
let SplitSegmentsAction = null;
try {
SplitSegmentsAction = require('Waze/Action/SplitSegments');
log('SplitSegments Action geladen:', typeof SplitSegmentsAction);
} catch (e) {
log('SplitSegments Action nicht verfügbar:', e.message);
}
if (!SplitSegmentsAction) {
throw new Error(t('noSplitPoints'));
}
// Sortiere Positionen von vorne nach hinten (aufsteigend)
const sortedPositions = [...positions].sort((a, b) => a - b);
let splitCount = 0;
// Tracke wo das aktuelle Segment im Original beginnt
let currentSegmentId = segment.attributes.id;
let currentSegmentStartInOriginal = 0; // Wo beginnt das aktuelle Segment im Original (0-1)
log(`Geplante Splits: ${sortedPositions.length} bei Positionen: ${sortedPositions.map(p => (p*100).toFixed(1) + '%').join(', ')}`);
for (let i = 0; i < sortedPositions.length; i++) {
const absolutePos = sortedPositions[i];
const currentSegment = W.model.segments.getObjectById(currentSegmentId);
if (!currentSegment) {
log(`Segment ${currentSegmentId} nicht mehr gefunden, breche ab`);
break;
}
// Berechne die relative Position im aktuellen Segment
// Das aktuelle Segment geht von currentSegmentStartInOriginal bis 1.0 (im Original)
// Wir wollen bei absolutePos splitten
// Relative Position = (absolutePos - start) / (1 - start)
const segmentRangeInOriginal = 1 - currentSegmentStartInOriginal;
const relativePos = (absolutePos - currentSegmentStartInOriginal) / segmentRangeInOriginal;
log(`Split ${i + 1}/${sortedPositions.length}:`);
log(` Absolute Position im Original: ${(absolutePos * 100).toFixed(1)}%`);
log(` Aktuelles Segment beginnt bei: ${(currentSegmentStartInOriginal * 100).toFixed(1)}%`);
log(` Segment-Bereich: ${(segmentRangeInOriginal * 100).toFixed(1)}%`);
log(` Relative Position im Segment: ${(relativePos * 100).toFixed(1)}%`);
if (relativePos <= 0.001 || relativePos >= 0.999) {
log(` Ungültige relative Position, überspringe`);
continue;
}
// Berechne den Split-Punkt als GeoJSON (WGS84)
const splitPointGeoJSON = getPointAtPosition(currentSegment, relativePos, true);
if (!splitPointGeoJSON) {
log(' Konnte Split-Punkt nicht berechnen');
continue;
}
log(` Split-Punkt: lon=${splitPointGeoJSON.coordinates[0].toFixed(6)}, lat=${splitPointGeoJSON.coordinates[1].toFixed(6)}`);
try {
const action = new SplitSegmentsAction(currentSegment, {
splitAtPoint: splitPointGeoJSON
});
log(' Action erstellt, füge hinzu...');
W.model.actionManager.add(action);
splitCount++;
log(` Split ${splitCount} erfolgreich!`);
// Debug: Zeige alle Properties der Action
log(' Action Properties:', Object.keys(action));
log(' action.splitSegmentPair:', action.splitSegmentPair);
log(' action.newSegments:', action.newSegments);
log(' action.segment:', action.segment?.attributes?.id);
log(' action.newNode:', action.newNode);
// Nach dem Split: Das "hintere" Segment (B-Seite) wird für weitere Splits verwendet
// Und wir aktualisieren wo das neue Segment im Original beginnt
let foundNextSegment = false;
if (action.splitSegmentPair) {
log(` splitSegmentPair gefunden:`, action.splitSegmentPair);
const newSegments = action.splitSegmentPair;
if (Array.isArray(newSegments) && newSegments.length >= 2) {
// Das Array enthält Segment-OBJEKTE, nicht IDs!
const secondSegment = newSegments[1];
// Extrahiere die ID aus dem Segment-Objekt
if (secondSegment && secondSegment.attributes && secondSegment.attributes.id !== undefined) {
currentSegmentId = secondSegment.attributes.id;
log(` Segment-Objekt gefunden, ID: ${currentSegmentId}`);
} else if (typeof secondSegment === 'number') {
currentSegmentId = secondSegment;
log(` Direkte ID gefunden: ${currentSegmentId}`);
} else if (secondSegment && secondSegment.id !== undefined) {
currentSegmentId = secondSegment.id;
log(` ID aus .id Property: ${currentSegmentId}`);
} else {
log(` Konnte ID nicht extrahieren aus:`, typeof secondSegment, secondSegment);
}
currentSegmentStartInOriginal = absolutePos;
foundNextSegment = true;
log(` Nächstes Segment ID: ${currentSegmentId}, beginnt bei ${(currentSegmentStartInOriginal * 100).toFixed(1)}%`);
} else if (typeof newSegments === 'object' && newSegments !== null) {
const keys = Object.keys(newSegments);
log(` splitSegmentPair ist Objekt mit Keys:`, keys);
const secondSeg = newSegments.second || newSegments.b || newSegments[1] || newSegments.newSegment;
if (secondSeg) {
currentSegmentId = secondSeg.attributes?.id || secondSeg.id || secondSeg;
currentSegmentStartInOriginal = absolutePos;
foundNextSegment = true;
log(` Nächstes Segment (aus Objekt): ${currentSegmentId}`);
}
}
}
// Fallback: Versuche newSegments zu nutzen
if (!foundNextSegment && action.newSegments) {
log(` Versuche newSegments:`, action.newSegments);
if (Array.isArray(action.newSegments) && action.newSegments.length >= 2) {
const seg = action.newSegments[1];
currentSegmentId = seg?.attributes?.id || seg?.id || seg;
currentSegmentStartInOriginal = absolutePos;
foundNextSegment = true;
log(` Nächstes Segment (newSegments[1]): ${currentSegmentId}`);
}
}
// Fallback 2: Suche nach neuen Segmenten im Model
if (!foundNextSegment) {
log(' Kein nächstes Segment gefunden, suche im Model...');
// Das Original-Segment existiert vielleicht noch mit gleicher ID aber kürzerer Geometrie
// Oder es gibt ein neues Segment
currentSegmentStartInOriginal = absolutePos;
log(` Setze currentSegmentStartInOriginal auf ${(absolutePos * 100).toFixed(1)}% und hoffe das Beste`);
}
} catch (e) {
log(` Split-Fehler: ${e.message}`);
log(' Stack:', e.stack);
}
}
log(`splitSingleSegment abgeschlossen: ${splitCount} von ${sortedPositions.length} Splits erfolgreich`);
return splitCount;
}
function repeatLastAction() {
const lastAction = localStorage.getItem(`${SCRIPT_ID}-lastAction`);
if (!lastAction) {
showStatus(t('noLastAction'), 'error');
return;
}
try {
const action = JSON.parse(lastAction);
settings.mode = action.mode;
settings.splitCount = action.splitCount;
settings.splitDistance = action.splitDistance;
settings.minSegmentLength = action.minSegmentLength;
settings.enforceMinLength = action.enforceMinLength ?? true;
settings.percentages = action.percentages;
settings.startFrom = action.startFrom;
settings.offset = action.offset;
saveSettings();
updateUI();
splitSegments();
} catch (e) {
showStatus(`${t('error')}: ${e.message}`, 'error');
}
}
function batchProcessStreet() {
const segments = getSelectedSegments();
if (segments.length !== 1) {
showStatus(t('selectOneSegment'), 'error');
return;
}
const segment = segments[0];
const streetID = segment.attributes.primaryStreetID;
if (!streetID) {
showStatus(t('noStreetAssignment'), 'error');
return;
}
const allSegments = Object.values(W.model.segments.objects)
.filter(s => s.attributes.primaryStreetID === streetID);
if (allSegments.length === 0) {
showStatus(t('noSegmentsFound'), 'error');
return;
}
const streetName = getStreetName(segment);
if (!confirm(`${allSegments.length} ${t('processSegments')} "${streetName}"?`)) {
return;
}
let totalSplits = 0;
let errors = [];
try {
allSegments.forEach(seg => {
if (settings.protectRoundabouts && isRoundabout(seg)) return;
if (settings.checkLockLevel && (seg.attributes.lockRank || 0) > getUserRank()) return;
try {
const result = splitSingleSegment(seg);
totalSplits += result;
} catch (e) {
errors.push(`${seg.attributes.id}: ${e.message}`);
}
});
showStatus(`"${streetName}": ${totalSplits} ${t('splitsOnStreet')}`, 'success');
} catch (e) {
showStatus(`${t('error')}: ${e.message}`, 'error');
}
}
function showStatus(message, type) {
const statusDiv = getElement(`${SCRIPT_ID}-status`);
if (!statusDiv) return;
statusDiv.style.display = 'block';
statusDiv.style.background = type === 'error' ? '#ffe8e8' : '#e8f4e8';
statusDiv.style.color = type === 'error' ? '#c00' : '#080';
statusDiv.innerHTML = message;
log(`Status (${type}): ${message}`);
// Aktualisiere Limit-Anzeige
updateDailyLimitDisplay();
setTimeout(() => {
statusDiv.style.display = 'none';
}, 5000);
}
function updateDailyLimitDisplay() {
const limitDiv = getElement(`${SCRIPT_ID}-daily-limit`);
if (!limitDiv) return;
const remaining = getRemainingDailyUsage();
const color = remaining <= 2 ? '#f8d7da' : remaining <= 5 ? '#fff3cd' : '#d4edda';
limitDiv.style.background = color;
limitDiv.innerHTML = `📊 <strong>${remaining}</strong>/${MAX_SPLITS_PER_DAY} ${t('actionsRemaining')}`;
}
// Start
init();
})();