abdullah-abbas Roundabout Editor

أداة تعديل الدوارات (تحفظ الإعدادات والمكان)

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         abdullah-abbas Roundabout Editor
// @namespace    https://greasyfork.org/users/30701-justins83-waze
// @version      2025.12.28.16.34
// @description  أداة تعديل الدوارات (تحفظ الإعدادات والمكان)
// @include      https://www.waze.com/editor*
// @include      https://www.waze.com/*/editor*
// @include      https://beta.waze.com/*
// @exclude      https://www.waze.com/user/editor*
// @require      https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @connect      greasyfork.org
// @author       JustinS83 (Modified by Abdullah Abbas)
// @grant        none
// ==/UserScript==

/* global W, WazeWrap, OpenLayers, require, $, _, I18n */

(function() {

    var RAUtilWindow = null;
    var UpdateSegmentGeometry, MoveNode, MultiAction;
    var drc_layer;
	let wEvents;

    // إعدادات افتراضية
    var settings = {
        top: "15%",
        left: "25%",
        expanded: true,
        angles: false
    };

    function bootstrap(tries = 1) {
        if (typeof W !== 'undefined' && W.map && W.model && typeof WazeWrap !== 'undefined' && WazeWrap.Ready) {
            init();
        }
        else if (tries < 1000) {
            setTimeout(function () {bootstrap(++tries);}, 200);
        }
    }

    bootstrap();

    function init(){
        injectCss();
        loadSettings(); // استرجاع الإعدادات المحفوظة

        try {
            if(window.require) {
                UpdateSegmentGeometry = require('Waze/Action/UpdateSegmentGeometry');
                MoveNode = require("Waze/Action/MoveNode");
                MultiAction = require("Waze/Action/MultiAction");
            }
        } catch(e) { console.error("AA RA: Modules Error", e); }

        if(W.map.events) wEvents = W.map.events;
	    else wEvents = W.map.getMapEventsListener();

        RAUtilWindow = document.createElement('div');
        RAUtilWindow.id = "RAUtilWindow";
        RAUtilWindow.className = "aa-panel";

        // تطبيق الموقع المحفوظ
        RAUtilWindow.style.top = settings.top;
        RAUtilWindow.style.left = settings.left;

        // --- HTML ---
        var alertsHTML = `
            <div id="header" class="aa-header">
                <span><i class="fa fa-refresh"></i> تعديل الدوار</span>
                <span id="collapser" class="fa fa-minus-square aa-collapse-btn"></span>
            </div>

            <div id="divWrappers" class="aa-content">
                <div class="aa-section">
                    <label class="aa-label">
                        <input type="checkbox" id="chkRARoundaboutAngles"> تفعيل عرض الزوايا
                    </label>
                </div>

                <div class="aa-section aa-border">
                    <div class="aa-sec-title" style="color:#2980b9">الإزاحة (تحريك)</div>
                    <div class="aa-input-wrap">
                        <input type="text" id="shiftAmount" value="1" class="aa-input"> <span>متر</span>
                    </div>

                    <div class="aa-grid-arrows">
                        <div></div>
                        <div id="RAShiftUpBtn" class="aa-btn aa-blue" title="أعلى"><i class="fa fa-arrow-up"></i></div>
                        <div></div>

                        <div id="RAShiftLeftBtn" class="aa-btn aa-blue" title="يسار"><i class="fa fa-arrow-left"></i></div>
                        <div id="RAShiftDownBtn" class="aa-btn aa-blue" title="أسفل"><i class="fa fa-arrow-down"></i></div>
                        <div id="RAShiftRightBtn" class="aa-btn aa-blue" title="يمين"><i class="fa fa-arrow-right"></i></div>
                    </div>
                </div>

                <div class="aa-flex-row">
                    <div class="aa-section aa-border aa-flex-1">
                        <div class="aa-sec-title" style="color:#8e44ad">تدوير</div>
                        <div class="aa-input-wrap">
                            <input type="text" id="rotationAmount" value="5" class="aa-input"> <span>°</span>
                        </div>
                        <div class="aa-flex-center">
                            <div id="RARotateLeftBtn" class="aa-btn aa-purple" title="عكس عقارب الساعة"><i class="fa fa-undo"></i></div>
                            <div id="RARotateRightBtn" class="aa-btn aa-purple" title="مع عقارب الساعة"><i class="fa fa-repeat"></i></div>
                        </div>
                    </div>

                    <div class="aa-section aa-border aa-flex-1">
                        <div class="aa-sec-title" style="color:#27ae60">القطر</div>
                        <div class="aa-flex-center" style="margin-top: 35px;">
                            <div id="diameterChangeDecreaseBtn" class="aa-btn aa-red" title="تصغير"><i class="fa fa-compress"></i></div>
                            <div id="diameterChangeIncreaseBtn" class="aa-btn aa-green" title="تكبير"><i class="fa fa-expand"></i></div>
                        </div>
                    </div>
                </div>

                <div class="aa-section aa-border">
                    <div class="aa-sec-title" style="color:#e67e22">ضبط العقد</div>
                    <div class="aa-nodes-container">
                        <div class="aa-node-box">
                            <div class="aa-node-name">عقدة A</div>
                            <div class="aa-node-btns">
                                <span id="btnMoveANodeIn" class="aa-text-btn aa-orange">إدخال</span>
                                <span id="btnMoveANodeOut" class="aa-text-btn aa-orange">إخراج</span>
                            </div>
                        </div>
                        <div class="aa-sep"></div>
                        <div class="aa-node-box">
                            <div class="aa-node-name">عقدة B</div>
                            <div class="aa-node-btns">
                                <span id="btnMoveBNodeIn" class="aa-text-btn aa-orange">إدخال</span>
                                <span id="btnMoveBNodeOut" class="aa-text-btn aa-orange">إخراج</span>
                            </div>
                        </div>
                    </div>
                </div>

            </div>
        `;

        RAUtilWindow.innerHTML = alertsHTML;
        document.body.appendChild(RAUtilWindow);

        // تطبيق حالة الطي/التوسيع المحفوظة
        if (!settings.expanded) {
            $("#divWrappers").hide();
            $("#collapser").removeClass("fa-minus-square").addClass("fa-plus-square");
        }

        // تطبيق خيار الزوايا المحفوظ
        if (settings.angles) {
            $("#chkRARoundaboutAngles").prop('checked', true);
        }

        bindEvents();
        makeDraggable(RAUtilWindow, document.getElementById('header'));

        W.selectionManager.events.register("selectionchanged", null, checkDisplayTool);

        // تفعيل رسم الزوايا إذا كان محفوظاً
        if(settings.angles){
             wEvents.register("zoomend", null, DrawRoundaboutAngles);
             wEvents.register("moveend", null, DrawRoundaboutAngles);
        }
    }

    // --- إدارة الإعدادات ---
    function loadSettings() {
        var loaded = localStorage.getItem("WME_RAUtil_AA_Settings");
        if (loaded) {
            try {
                settings = $.extend(settings, JSON.parse(loaded));
            } catch(e) { console.log("Error loading settings"); }
        }
    }

    function saveSettings() {
        settings.top = RAUtilWindow.style.top;
        settings.left = RAUtilWindow.style.left;
        settings.expanded = $("#divWrappers").is(":visible");
        settings.angles = $("#chkRARoundaboutAngles").is(":checked");

        localStorage.setItem("WME_RAUtil_AA_Settings", JSON.stringify(settings));
    }

    // --- ربط الأزرار ---
    function bindEvents() {
        $('#RAShiftLeftBtn').click(function(e){ e.stopPropagation(); runLogic('ShiftLong', -$('#shiftAmount').val()); });
        $('#RAShiftRightBtn').click(function(e){ e.stopPropagation(); runLogic('ShiftLong', $('#shiftAmount').val()); });
        $('#RAShiftUpBtn').click(function(e){ e.stopPropagation(); runLogic('ShiftLat', $('#shiftAmount').val()); });
        $('#RAShiftDownBtn').click(function(e){ e.stopPropagation(); runLogic('ShiftLat', -$('#shiftAmount').val()); });

        $('#RARotateLeftBtn').click(function(e){ e.stopPropagation(); runLogic('Rotate', $('#rotationAmount').val()); });
        $('#RARotateRightBtn').click(function(e){ e.stopPropagation(); runLogic('Rotate', -$('#rotationAmount').val()); });

        $('#diameterChangeDecreaseBtn').click(function(e){ e.stopPropagation(); runLogic('Diameter', -1); });
        $('#diameterChangeIncreaseBtn').click(function(e){ e.stopPropagation(); runLogic('Diameter', 1); });

        $('#btnMoveANodeIn').click(function(){moveNodeIn(WazeWrap.getSelectedFeatures()[0].WW.getObjectModel().attributes.id, WazeWrap.getSelectedFeatures()[0].WW.getObjectModel().attributes.fromNodeID);});
        $('#btnMoveANodeOut').click(function(){moveNodeOut(WazeWrap.getSelectedFeatures()[0].WW.getObjectModel().attributes.id, WazeWrap.getSelectedFeatures()[0].WW.getObjectModel().attributes.fromNodeID);});
        $('#btnMoveBNodeIn').click(function(){moveNodeIn(WazeWrap.getSelectedFeatures()[0].WW.getObjectModel().attributes.id, WazeWrap.getSelectedFeatures()[0].WW.getObjectModel().attributes.toNodeID);});
        $('#btnMoveBNodeOut').click(function(){moveNodeOut(WazeWrap.getSelectedFeatures()[0].WW.getObjectModel().attributes.id, WazeWrap.getSelectedFeatures()[0].WW.getObjectModel().attributes.toNodeID);});

        $('#shiftAmount, #rotationAmount').keypress(function(event) {
            if ((event.which != 46 || $(this).val().indexOf('.') != -1) && (event.which < 48 || event.which > 57))
                event.preventDefault();
        });

        // زر التصغير مع الحفظ
        $('#collapser').click(function(){
            $("#divWrappers").slideToggle("fast", function() {
                saveSettings(); // حفظ الحالة بعد الانتهاء من الحركة
            });
            $(this).toggleClass("fa-minus-square fa-plus-square");
        });

        // خيار الزوايا مع الحفظ
        $("#chkRARoundaboutAngles").click(function(){
            saveSettings();
            if($(this).is(":checked")){
                wEvents.register("zoomend", null, DrawRoundaboutAngles);
                wEvents.register("moveend", null, DrawRoundaboutAngles);
                DrawRoundaboutAngles();
                if(drc_layer) drc_layer.setVisibility(true);
            } else {
                wEvents.unregister("zoomend", null, DrawRoundaboutAngles);
                wEvents.unregister("moveend", null, DrawRoundaboutAngles);
                if(drc_layer) drc_layer.setVisibility(false);
            }
        });
    }

    function runLogic(action, value) {
        var segObj = WazeWrap.getSelectedFeatures()[0];
        if(!segObj) return;

        if(action === 'ShiftLong') {
            var convertedCoords = WazeWrap.Geometry.ConvertTo4326(segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][0], segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][1]);
            var gpsOffset = WazeWrap.Geometry.CalculateLongOffsetGPS(value, convertedCoords.lon, convertedCoords.lat);
            ShiftSegmentsNodesLong(segObj, gpsOffset);
        }
        else if(action === 'ShiftLat') {
            var convertedCoords = WazeWrap.Geometry.ConvertTo4326(segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][0], segObj.WW.getAttributes().geoJSONGeometry.coordinates[0][1]);
            var gpsOffset = WazeWrap.Geometry.CalculateLatOffsetGPS(value, convertedCoords.lon, convertedCoords.lat);
            ShiftSegmentNodesLat(segObj, gpsOffset);
        }
        else if(action === 'Rotate') RotateRA(segObj, value);
        else if(action === 'Diameter') ChangeDiameter(segObj, value);
    }

    function checkDisplayTool(){
        if(WazeWrap.hasSelectedFeatures() && WazeWrap.getSelectedFeatures()[0].WW.getType() === 'segment'){
            var allRA = true;
            for (let i = 0; i < WazeWrap.getSelectedFeatures().length; i++){
                if(!WazeWrap.Model.isRoundaboutSegmentID(WazeWrap.getSelectedFeatures()[i].WW.getObjectModel().attributes.id))
                    allRA = false;
            }
            if(!allRA || WazeWrap.getSelectedFeatures().length === 0)
                $('#RAUtilWindow').css({'visibility': 'hidden'});
            else{
                $('#RAUtilWindow').css({'visibility': 'visible'});
            }
        } else {
            $('#RAUtilWindow').css({'visibility': 'hidden'});
        }
    }

    function ShiftSegmentNodesLat(segObj, latOffset){
        var RASegs = WazeWrap.Model.getAllRoundaboutSegmentsFromObj(segObj);
        var multiaction = new MultiAction();
        for(let i=0; i<RASegs.length; i++){
            segObj = W.model.segments.getObjectById(RASegs[i]);
            var newGeometry = structuredClone(segObj.attributes.geoJSONGeometry);
            for(let j=1; j < newGeometry.coordinates.length-1; j++) newGeometry.coordinates[j][1] += latOffset;
            multiaction.doSubAction(W.model, new UpdateSegmentGeometry(segObj, segObj.attributes.geoJSONGeometry, newGeometry));
            var node = W.model.nodes.objects[segObj.attributes.toNodeID];
            if(segObj.attributes.revDirection) node = W.model.nodes.objects[segObj.attributes.fromNodeID];
            var newNodeGeometry = structuredClone(node.attributes.geoJSONGeometry);
            newNodeGeometry.coordinates[1] += latOffset;
            var connectedSegObjs = {};
            for(var k=0;k<node.attributes.segIDs.length;k++) connectedSegObjs[node.attributes.segIDs[k]] = structuredClone(W.model.segments.getObjectById(node.attributes.segIDs[k]).attributes.geoJSONGeometry);
            multiaction.doSubAction(W.model, new MoveNode(node, node.attributes.geoJSONGeometry, newNodeGeometry, connectedSegObjs, {}));
        }
        W.model.actionManager.add(multiaction);
    }

    function ShiftSegmentsNodesLong(segObj, longOffset){
        var RASegs = WazeWrap.Model.getAllRoundaboutSegmentsFromObj(segObj);
        var multiaction = new MultiAction();
        for(let i=0; i<RASegs.length; i++){
            segObj = W.model.segments.getObjectById(RASegs[i]);
            var newGeometry = structuredClone(segObj.attributes.geoJSONGeometry);
            for(let j=1; j < newGeometry.coordinates.length-1; j++) newGeometry.coordinates[j][0] += longOffset;
            multiaction.doSubAction(W.model, new UpdateSegmentGeometry(segObj, segObj.attributes.geoJSONGeometry, newGeometry));
            var node = W.model.nodes.objects[segObj.attributes.toNodeID];
            if(segObj.attributes.revDirection) node = W.model.nodes.objects[segObj.attributes.fromNodeID];
            var newNodeGeometry = structuredClone(node.attributes.geoJSONGeometry);
            newNodeGeometry.coordinates[0] += longOffset;
            var connectedSegObjs = {};
            for(let k=0;k<node.attributes.segIDs.length;k++) connectedSegObjs[node.attributes.segIDs[k]] = structuredClone(W.model.segments.getObjectById(node.attributes.segIDs[k]).attributes.geoJSONGeometry);
            multiaction.doSubAction(W.model, new MoveNode(node, node.attributes.geoJSONGeometry, newNodeGeometry, connectedSegObjs, {}));
        }
        W.model.actionManager.add(multiaction);
    }

    function RotateRA(segObj, angle){
        var RASegs = WazeWrap.Model.getAllRoundaboutSegmentsFromObj(segObj);
        var raCenter = W.model.junctions.objects[segObj.WW.getAttributes().junctionID].attributes.geoJSONGeometry.coordinates;
        var multiaction = new MultiAction();
        for(let i=0; i<RASegs.length; i++){
            segObj = W.model.segments.getObjectById(RASegs[i]);
            var newGeometry = structuredClone(segObj.attributes.geoJSONGeometry);
            var originalLength = segObj.attributes.geoJSONGeometry.coordinates.length;
            var center = raCenter;
            var segPoints = [];
            for(let j=0; j<originalLength;j++) segPoints.push(new OpenLayers.Geometry.Point(segObj.attributes.geoJSONGeometry.coordinates[j][0], segObj.attributes.geoJSONGeometry.coordinates[j][1]));
            var newPoints = rotatePoints(center, segPoints, angle);
            for(let j=1; j<originalLength-1;j++) newGeometry.coordinates[j] = [newPoints[j].x, newPoints[j].y];
            multiaction.doSubAction(W.model, new UpdateSegmentGeometry(segObj, segObj.attributes.geoJSONGeometry, newGeometry));
            var node = W.model.nodes.objects[segObj.attributes.toNodeID];
            if(segObj.attributes.revDirection) node = W.model.nodes.objects[segObj.attributes.fromNodeID];
            var nodePoints = [];
            var newNodeGeometry = structuredClone(node.attributes.geoJSONGeometry);
            nodePoints.push(new OpenLayers.Geometry.Point(node.attributes.geoJSONGeometry.coordinates[0], node.attributes.geoJSONGeometry.coordinates[1]));
            nodePoints.push(new OpenLayers.Geometry.Point(node.attributes.geoJSONGeometry.coordinates[0], node.attributes.geoJSONGeometry.coordinates[1]));
            var gps = rotatePoints(center, nodePoints, angle);
            newNodeGeometry.coordinates = [gps[0].x, gps[0].y];
            var connectedSegObjs = {};
            for(let k=0;k<node.attributes.segIDs.length;k++) connectedSegObjs[node.attributes.segIDs[k]] = structuredClone(W.model.segments.getObjectById(node.attributes.segIDs[k]).attributes.geoJSONGeometry);
            multiaction.doSubAction(W.model, new MoveNode(node, node.attributes.geoJSONGeometry, newNodeGeometry, connectedSegObjs, {}));
        }
        W.model.actionManager.add(multiaction);
    }

    function ChangeDiameter(segObj, amount){
        var RASegs = WazeWrap.Model.getAllRoundaboutSegmentsFromObj(segObj);
        var raCenter = W.model.junctions.objects[segObj.WW.getAttributes().junctionID].attributes.geoJSONGeometry.coordinates;
        let { lon: centerX, lat: centerY } = WazeWrap.Geometry.ConvertTo900913(raCenter);
        for(let i=0; i<RASegs.length; i++){
            segObj = W.model.segments.getObjectById(RASegs[i]);
            var newGeometry = structuredClone(segObj.attributes.geoJSONGeometry);
            for(let j=1; j < newGeometry.coordinates.length-1; j++){
                let pt = segObj.attributes.geoJSONGeometry.coordinates[j];
                let { lon: pointX, lat: pointY } = WazeWrap.Geometry.ConvertTo900913(pt);
                let h = Math.sqrt(Math.abs(Math.pow(pointX - centerX, 2) + Math.pow(pointY - centerY, 2)));
                let ratio = (h + amount)/h;
                let x = centerX + (pointX - centerX) * ratio;
                let y = centerY + (pointY - centerY) * ratio;
                let { lon: newX, lat: newY } = WazeWrap.Geometry.ConvertTo4326([x, y]);
                newGeometry.coordinates[j] = [newX, newY];
            }
            W.model.actionManager.add(new UpdateSegmentGeometry(segObj, segObj.attributes.geoJSONGeometry, newGeometry));
            var node = W.model.nodes.objects[segObj.attributes.toNodeID];
            if(segObj.attributes.revDirection) node = W.model.nodes.objects[segObj.attributes.fromNodeID];
            var newNodeGeometry = structuredClone(node.attributes.geoJSONGeometry);
            let { lon: pointX, lat: pointY } = WazeWrap.Geometry.ConvertTo900913(newNodeGeometry.coordinates);
            let h = Math.sqrt(Math.abs(Math.pow(pointX - centerX, 2) + Math.pow(pointY - centerY, 2)));
            let ratio = (h + amount)/h;
            let x = centerX + (pointX - centerX) * ratio;
            let y = centerY + (pointY - centerY) * ratio;
            let { lon: newX, lat: newY } = WazeWrap.Geometry.ConvertTo4326([x, y]);
            newNodeGeometry.coordinates = [newX, newY];
            var connectedSegObjs = {};
            for(let j=0;j<node.attributes.segIDs.length;j++) connectedSegObjs[node.attributes.segIDs[j]] = structuredClone(W.model.segments.getObjectById(node.attributes.segIDs[j]).attributes.geoJSONGeometry);
            W.model.actionManager.add(new MoveNode(node, node.attributes.geoJSONGeometry, newNodeGeometry, connectedSegObjs, {}));
        }
        if($("#chkRARoundaboutAngles").is(":checked")) DrawRoundaboutAngles();
    }

    function moveNodeIn(sourceSegID, nodeID){
        let isANode = true;
        let curSeg = W.model.segments.getObjectById(sourceSegID);
        if (curSeg.attributes.geoJSONGeometry.coordinates.length > 2) {
            if(nodeID === curSeg.attributes.toNodeID) isANode = false;
            let node = W.model.nodes.getObjectById(nodeID);
            let currNodePOS = structuredClone(node.attributes.geoJSONGeometry.coordinates);
            let otherSeg;
            let nodeSegs = [...node.attributes.segIDs];
            nodeSegs = _.without(nodeSegs, sourceSegID);
            for(let i=0; i<nodeSegs.length; i++){
                let s = W.model.segments.getObjectById(nodeSegs[i]);
                if(s.attributes.junctionID){ otherSeg = s; break; }
            }
            var multiaction = new MultiAction();
            var newNodeGeometry = { type: 'Point', coordinates: structuredClone(curSeg.attributes.geoJSONGeometry.coordinates[isANode ? 1 : curSeg.attributes.geoJSONGeometry.coordinates.length - 2]) };
            let newSegGeo = structuredClone(curSeg.attributes.geoJSONGeometry);
            newSegGeo.coordinates.splice(isANode ? 1 : newSegGeo.coordinates.length - 2, 1);
            multiaction.doSubAction(W.model, new UpdateSegmentGeometry(curSeg, curSeg.attributes.geoJSONGeometry, newSegGeo));
            var connectedSegObjs = {};
            for(var j=0;j<node.attributes.segIDs.length;j++) connectedSegObjs[node.attributes.segIDs[j]] = structuredClone(W.model.segments.getObjectById(node.attributes.segIDs[j]).attributes.geoJSONGeometry);
            multiaction.doSubAction(W.model, new MoveNode(node, node.attributes.geoJSONGeometry, newNodeGeometry, connectedSegObjs, {}));
            if((otherSeg.attributes.revDirection && !curSeg.attributes.revDirection) || (!otherSeg.attributes.revDirection && curSeg.attributes.revDirection)) isANode = !isANode;
            let newGeo = structuredClone(otherSeg.attributes.geoJSONGeometry);
            newGeo.coordinates.splice(isANode ? -1 : 1, 0, [currNodePOS[0], currNodePOS[1]]);
            multiaction.doSubAction(W.model, new UpdateSegmentGeometry(otherSeg, otherSeg.attributes.geoJSONGeometry, newGeo));
            W.model.actionManager.add(multiaction);
            if($("#chkRARoundaboutAngles").is(":checked")) DrawRoundaboutAngles();
        }
    }

    function moveNodeOut(sourceSegID, nodeID){
        let isANode = true;
        let curSeg = W.model.segments.getObjectById(sourceSegID);
        if(nodeID === curSeg.attributes.toNodeID) isANode = false;
        let node = W.model.nodes.getObjectById(nodeID);
        let currNodePOS = structuredClone(node.attributes.geoJSONGeometry.coordinates);
        let otherSeg;
        let nodeSegs = [...node.attributes.segIDs];
        nodeSegs = _.without(nodeSegs, sourceSegID);
        for(let i=0; i<nodeSegs.length; i++){
            let s = W.model.segments.getObjectById(nodeSegs[i]);
            if(s.attributes.junctionID){ otherSeg = s; break; }
        }
        if(otherSeg.attributes.geoJSONGeometry.coordinates.length > 2){
            let newSegGeo = structuredClone(curSeg.attributes.geoJSONGeometry);
            newSegGeo.coordinates.splice(isANode ? 1 : newSegGeo.coordinates.length - 1, 0, [currNodePOS[0], currNodePOS[1]]);
            var multiaction = new MultiAction();
            multiaction.doSubAction(W.model, new UpdateSegmentGeometry(curSeg, curSeg.attributes.geoJSONGeometry, newSegGeo));
            if((otherSeg.attributes.revDirection && !curSeg.attributes.revDirection) || (!otherSeg.attributes.revDirection && curSeg.attributes.revDirection)) isANode = !isANode;
            var newNodeGeometry = { type: 'Point', coordinates: structuredClone(otherSeg.attributes.geoJSONGeometry.coordinates[isANode ? otherSeg.attributes.geoJSONGeometry.coordinates.length - 2 : 1]) };
            let newGeo = structuredClone(otherSeg.attributes.geoJSONGeometry);
            newGeo.coordinates.splice(isANode ? -2 : 1, 1);
            multiaction.doSubAction(W.model, new UpdateSegmentGeometry(otherSeg, otherSeg.attributes.geoJSONGeometry, newGeo));
            var connectedSegObjs = {};
            for(var j=0; j < node.attributes.segIDs.length;j++) connectedSegObjs[node.attributes.segIDs[j]] = structuredClone(W.model.segments.getObjectById(node.attributes.segIDs[j]).attributes.geoJSONGeometry);
            multiaction.doSubAction(W.model, new MoveNode(node, node.attributes.geoJSONGeometry, newNodeGeometry, connectedSegObjs, {}));
            W.model.actionManager.add(multiaction);
            if($("#chkRARoundaboutAngles").is(":checked")) DrawRoundaboutAngles();
        }
    }

    function rotatePoints(origin, points, angle){
        var lineFeature = new OpenLayers.Feature.Vector(new OpenLayers.Geometry.LineString(points),null,null);
        lineFeature.geometry.rotate(angle, new OpenLayers.Geometry.Point(origin[0], origin[1]));
        return [].concat(lineFeature.geometry.components);
    }

    function DrawRoundaboutAngles(){
        var layers = W.map.getLayersBy("uniqueName","__DrawRoundaboutAngles");
        if(layers.length > 0) drc_layer = layers[0];
        else {
            let drc_style = new OpenLayers.Style({
                fillOpacity: 0.0, strokeOpacity: 1.0, fillColor: "#FF40C0", strokeColor: "${strokeColor}", strokeWidth: 10,
                fontWeight: "bold", pointRadius: 0, label : "${labelText}", fontFamily: "Tahoma, Courier New",
                labelOutlineColor: "#FFFFFF", labelOutlineWidth: 3, fontColor: "${labelColor}", fontSize: "10px"
            });
            drc_layer = new OpenLayers.Layer.Vector("Roundabout Angles", { displayInLayerSwitcher: true, uniqueName: "__DrawRoundaboutAngles", styleMap: new OpenLayers.StyleMap(drc_style) });
            W.map.addLayer(drc_layer);
            drc_layer.setVisibility(true);
        }
        if (drc_layer.visibility == false || W.map.getZoom() < 1) { drc_layer.removeAllFeatures(); return; }

        var rsegments = {};
        for (let iseg in W.model.segments.objects) {
            let isegment = W.model.segments.getObjectById(iseg);
            if (isegment.getOLGeometry() !== null && isegment.attributes.junctionID != undefined) {
                let rsegs = rsegments[isegment.attributes.junctionID];
                if (rsegs == undefined) rsegments[isegment.attributes.junctionID] = rsegs = new Array();
                rsegs.push(isegment);
            }
        }
        var drc_features = [];

        for (let irid in rsegments) {
            let rsegs = rsegments[irid];
            let isegment = rsegs[0];
            let nodes = [];
            let nodes_x = [];
            let nodes_y = [];
            nodes = rsegs.map(seg => seg.attributes.fromNodeID);
            nodes = [...nodes, ...rsegs.map(seg => seg.attributes.toNodeID)];
            nodes = _.uniq(nodes);
            let node_objects = W.model.nodes.getByIds(nodes);
            nodes_x = node_objects.map(n => n.getOLGeometry().x);
            nodes_y = node_objects.map(n => n.getOLGeometry().y);
            let sr_x = 0; let sr_y = 0; let radius = 0; let numNodes = nodes_x.length;
            if (numNodes >= 1) {
                let junction = W.model.junctions.getObjectById(irid);
                sr_x = junction.getOLGeometry().x;
                sr_y = junction.getOLGeometry().y;
                let angles = []; let rr = -1; let r_ix;
                for(let i=0; i<nodes_x.length; i++) {
                    let dx = nodes_x[i] - sr_x; let dy = nodes_y[i] - sr_y;
                    let rr2 = dx*dx + dy*dy;
                    if (rr < rr2) { rr = rr2; r_ix = i; }
                    let angle = Math.atan2(dy, dx);
                    angle = (360.0 + (angle * 180.0 / Math.PI));
                    if (angle < 0.0) angle += 360.0; if (angle > 360.0) angle -= 360.0;
                    angles.push(angle);
                }
                radius = Math.sqrt(rr);
                angles = angles.sort(function(a,b) { return a - b; });
                angles.push( angles[0] + 360.0);
                angles = angles.sort(function(a,b) { return a - b; });
                let drc_color = (numNodes <= 4) ? "#0040FF" : "#002080";
                let drc_point = new OpenLayers.Geometry.Point(sr_x, sr_y );
                let drc_circle = new OpenLayers.Geometry.Polygon.createRegularPolygon( drc_point, radius, 10 * W.map.getZoom() );
                let drc_feature = new OpenLayers.Feature.Vector(drc_circle, {labelText: "", labelColor: "#000000", strokeColor: drc_color, });
                drc_features.push(drc_feature);

                if (numNodes >= 2 && numNodes <= 4 && W.map.getZoom() >= 5) {
                    for(let i=0; i<nodes_x.length; i++) {
                        let ix = nodes_x[i]; let iy = nodes_y[i];
                        let startPt = new OpenLayers.Geometry.Point( sr_x, sr_y );
                        let endPt = new OpenLayers.Geometry.Point( ix, iy );
                        let line = new OpenLayers.Geometry.LineString([startPt, endPt]);
                        let style = {strokeColor:drc_color, strokeWidth:2};
                        drc_features.push(new OpenLayers.Feature.Vector(line, {}, style));
                    }
                    let angles_float = [];
                    for(let i=0; i<angles.length - 1; i++) {
                        let ang = angles[i+1] - angles[i+0];
                        if (ang < 0) ang += 360.0; if (ang < 0) ang += 360.0;
                        if (ang < 135.0) ang = ang - 90.0; else ang = ang - 180.0;
                        angles_float.push( ang );
                    }
                    for(let i=0; i<angles.length - 1; i++) {
                        let arad = (angles[i+0] + angles[i+1]) * 0.5 * Math.PI / 180.0;
                        let ex = sr_x + Math.cos (arad) * radius * 0.5;
                        let ey = sr_y + Math.sin (arad) * radius * 0.5;
                        let angint = Math.round(angles_float[i] * 100)/100;
                        let kolor = "#004000";
                        if (angint <= -15 || angint >= 15) kolor = "#FF0000";
                        else if (angint <= -13 || angint >= 13) kolor = "#FFC000";
                        let pt = new OpenLayers.Geometry.Point(ex, ey);
                        drc_features.push(new OpenLayers.Feature.Vector( pt, {labelText: (angint + "°"), labelColor: kolor } ));
                    }
                } else {
                    for(let i=0; i < nodes_x.length; i++) {
                        let ix = nodes_x[i]; let iy = nodes_y[i];
                        let startPt = new OpenLayers.Geometry.Point( sr_x, sr_y );
                        let endPt = new OpenLayers.Geometry.Point( ix, iy );
                        let line = new OpenLayers.Geometry.LineString([startPt, endPt]);
                        let style = {strokeColor:drc_color, strokeWidth:2};
                        drc_features.push(new OpenLayers.Feature.Vector(line, {}, style));
                    }
                }
                let p1 = new OpenLayers.Geometry.Point( nodes_x[r_ix], nodes_y[r_ix] );
                let p2 = new OpenLayers.Geometry.Point( sr_x, sr_y );
                let line = new OpenLayers.Geometry.LineString([p1, p2]);
                let geo_radius = line.getGeodesicLength(W.map.getProjectionObject());
                let diam = geo_radius * 2.0;
                let center_pt = new OpenLayers.Geometry.Point(sr_x, sr_y);
                drc_features.push(new OpenLayers.Feature.Vector( center_pt, {labelText: (diam.toFixed(0) + "m"), labelColor: "#000000" } ));
            }
        }
        drc_layer.removeAllFeatures();
        drc_layer.addFeatures(drc_features);
    }

    function injectCss() {
        var css = `
            @import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;700&display=swap');

            .aa-panel {
                position: fixed; top: 15%; left: 25%; width: 300px;
                background-color: #fdfdfd;
                border: 1px solid #ccc;
                border-radius: 8px;
                box-shadow: 0 10px 30px rgba(0,0,0,0.3);
                font-family: 'Cairo', sans-serif;
                direction: rtl;
                z-index: 9999;
                overflow: hidden;
            }
            .aa-header {
                background: linear-gradient(135deg, #2980b9, #2c3e50);
                color: white; padding: 10px 15px;
                font-weight: bold; border-radius: 8px 8px 0 0;
                cursor: move; display: flex; justify-content: space-between; align-items: center;
            }
            .aa-collapse-btn { cursor: pointer; }
            .aa-content { padding: 10px; }
            .aa-section { margin-bottom: 8px; }
            .aa-border { background: #f9f9f9; border: 1px solid #eee; border-radius: 6px; padding: 8px; text-align: center; }
            .aa-sec-title { font-size: 13px; font-weight: bold; margin-bottom: 5px; border-bottom: 1px solid #e0e0e0; padding-bottom: 3px; }
            .aa-input-wrap { display: flex; justify-content: center; align-items: center; margin-bottom: 5px; }
            .aa-input { width: 40px; text-align: center; border: 1px solid #ccc; border-radius: 4px; padding: 2px; margin-left: 5px; }
            .aa-grid-arrows { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 3px; width: 100px; margin: 0 auto; }
            .aa-flex-row { display: flex; gap: 8px; margin-bottom: 8px; }
            .aa-flex-1 { flex: 1; }
            .aa-flex-center { display: flex; justify-content: space-around; }

            /* الأزرار المحسنة */
            .aa-btn {
                width: 35px; height: 35px;
                border-radius: 50%;
                display: flex; align-items: center; justify-content: center;
                cursor: pointer; color: white;
                box-shadow: 0 2px 4px rgba(0,0,0,0.2);
                transition: transform 0.1s;
                font-size: 16px;
                margin: 0 auto;
            }
            .aa-btn:active { transform: scale(0.95); }
            .aa-blue { background: linear-gradient(#3498db, #2980b9); }
            .aa-purple { background: linear-gradient(#9b59b6, #8e44ad); }
            .aa-green { background: linear-gradient(#2ecc71, #27ae60); }
            .aa-red { background: linear-gradient(#e74c3c, #c0392b); }

            /* أزرار العقد */
            .aa-nodes-container { display: flex; justify-content: space-between; font-size: 12px; }
            .aa-node-box { flex: 1; }
            .aa-node-name { margin-bottom: 3px; font-weight: bold; color: #555; }
            .aa-node-btns { display: flex; justify-content: center; gap: 2px; }
            .aa-text-btn {
                display: inline-block; padding: 2px 8px; border-radius: 12px;
                color: white; font-size: 10px; cursor: pointer;
            }
            .aa-orange { background: #e67e22; }
            .aa-sep { width: 1px; background: #ddd; margin: 0 5px; }
        `;
        $('<style type="text/css">' + css + '</style>').appendTo('head');
    }

    function makeDraggable(elmnt, handle) {
        var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        handle.onmousedown = dragMouseDown;
        function dragMouseDown(e) { e = e || window.event; if(e.target.id === 'collapser' || e.target.id === 'collapserLink') return; e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; }
        function elementDrag(e) { e = e || window.event; e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; elmnt.style.top = (elmnt.offsetTop - pos2) + "px"; elmnt.style.left = (elmnt.offsetLeft - pos1) + "px"; saveSettings(); }
        function closeDragElement() { document.onmouseup = null; document.onmousemove = null; }
    }

})();