WME MagicWand

The very same thing as same tool in graphic editor: select "similar" colored area and create landmark out of it + Clone, Orthogonalize, Rotate and Resize for landmarks

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name                WME MagicWand
// @namespace           http://en.advisor.travel/wme-magic-wand
// @description         The very same thing as same tool in graphic editor: select "similar" colored area and create landmark out of it + Clone, Orthogonalize, Rotate and Resize for landmarks
// @include             /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor.*$/
// @version             2.2
// @grant               none
// @license             MIT
// @copyright			2018 Vadim Istratov <[email protected]>
// ==/UserScript==

/**
// Special thanks goes to:
// https://github.com/AndriiHeonia/hull
// https://gist.github.com/tixxit/252222
// http://blog.cedric.ws/draw-the-convex-hull-with-canvas-and-javascript
// http://www.iis.sinica.edu.tw/page/jise/2012/201205_10.pdf
// http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment?page=1&tab=active#tab-top
// http://jsfromhell.com/math/is-point-in-poly
// https://gist.github.com/robgaston/8855489
// https://github.com/predein
 */

/**
 * Contributors: justins83
 */

function run_magicwand() {
    var wmelmw_version = "2.2";

    window.wme_magic_wand_debug = false;
    window.wme_magic_wand_profile = false;

    window.wme_magicwand_helpers = {
        isDragging: false,
        draggedNode: null,
        modifiedFeatureControl: null,
        layer: null,
        snap: null
    };

    /* bootstrap, will call initialiseHighlights() */
    function bootstraMagicWand() {
        var bGreasemonkeyServiceDefined = false;

        /* begin running the code! */
        setTimeout(initialiseMagicWand, 500);
    }

    /* helper function */
    function getElClass(classname, node) {
        if (!node) node = document.getElementsByTagName("body")[0];
        var a = [];
        var re = new RegExp('\\b' + classname + '\\b');
        var els = node.getElementsByTagName("*");
        for (var i = 0, j = els.length; i < j; i++)
            if (re.test(els[i].className)) a.push(els[i]);
        return a;
    }

    function getElId(node) {
        return document.getElementById(node);
    }

    /* =========================================================================== */

    function initialiseMagicWand() {
        try {
            if (!((typeof W.map != undefined) && (undefined != typeof W.map.events.register) && (undefined != typeof W.selectionManager.events.register ) && (undefined != typeof W.loginManager.events.register) )) {
                setTimeout(initialiseMagicWand, 1000);
                return;
            }
        } catch (err) {
            setTimeout(initialiseMagicWand, 1000);
            return;
        }

        var userInfo = getElId('user-info');
        var userTabs = getElId('user-tabs');

        if(!getElClass('nav-tabs', userTabs)[0]) {
            setTimeout(initialiseMagicWand, 1000);
            return;
        }

        var navTabs = getElClass('nav-tabs', userTabs)[0];
        var tabContent = getElClass('tab-content', userInfo)[0];

        console.log('WME MagicWand init');

        window.wme_magic_wand = false;
        window.wme_magic_wand_process = false;

        // add new box to left of the map
        var addon = document.createElement('section');
        addon.innerHTML = '<b>WME Magic Wand</b> v' + wmelmw_version;

        section = document.createElement('p');
        section.style.paddingTop = "8px";
        section.style.textIndent = "16px";
        section.id = "magicwand_advanced";
        section.innerHTML = '<b>Advanced Editor Options</b><br/>'
            + '<label>Angle threshold<br/><input type="text" id="_cMagicWandAngleThreshold" name="_cMagicWandAngleThreshold" value="12" size="3" maxlength="2" /></label><br/>'
            + '<label><input type="checkbox" id="_cMagicWandEdit_Rotate" name="_cMagicWandEdit_Rotate" value="1" /> Enable Rotate landmarks</label><br/>'
            + '<label><input type="checkbox" id="_cMagicWandEdit_Resize" name="_cMagicWandEdit_Resize" value="1" /> Enable Resize (no reshape)</label><br/>'
            + '<label><input type="checkbox" id="_cMagicWandHighlight" name="_cMagicWandHighlight" value="1" /> Enable Highlight</label><br/>'
            + '<label><input type="checkbox" id="_cMagicWandStraightHelper" name="_cMagicWandStraightHelper" value="1" /> Enable straight angle helper (hold SHIFT)</label><br/><br/><br/>';
        addon.appendChild(section);

        var section = document.createElement('p');
        section.style.paddingTop = "8px";
        section.style.textIndent = "16px";
        section.id = "magicwand_common";
        section.innerHTML = '<b>Magic wand tool</b><br/>'
            + '<input type="button" id="_bMagicWandProcessClick" name="_bMagicWandProcessClick" value="CLICK TO START MAGIC WAND" style="background-color: green" /><br/><br/>'
            + '<b>Status:</b> <span id="_sMagicWandStatus">Disabled</span><br/>'
            + '<b>Layer:</b> <span id="_sMagicWandUsedLayer"></span><br/>'
            + '<b>Clicked pixel color to match:</b>'
            + '<div id="_dMagicWandColorpicker" style="width: 20px; height: 20px; border: 1px solid black; display: inline-block; margin-left: 10px;">&nbsp;</div><br/>';

        addon.appendChild(section);

        section = document.createElement('p');
        section.style.paddingTop = "8px";
        section.style.textIndent = "16px";
        section.id = "magicwand_advanced";
        section.innerHTML = '<b>Options</b><br/>'
            + 'Landmark type:<br/>'
            + '<select id="_sMagicWandLandmark" name="_sMagicWandLandmark" style="width: 95%"></select><br/><br/>'
            + 'Color match algorithm:<br/>'
            + '<label><input type="radio" id="_rMagicWandColorAlgorithm_color" name="_rMagicWandColorAlgorithm" value="1" checked="checked" /> Color Distance</label><br/>'
            + '<label><input type="radio" id="_rMagicWandColorAlgorithm_lab" name="_rMagicWandColorAlgorithm" value="2" /> Human-eye Similarity</label><br/><br/>'
            + '<label for="_cMagicWandSimilarity">Tolerance</label><br/>Around 4-10, >20 very slow<br/>'
            + '<input type="text" id="_cMagicWandSimilarity" name="_cMagicWandSimilarity" value="8" size="4" maxlength="3" /><br/><br/>'
            + '<label for="_cMagicWandConcavHull">Detailing</label><br/>Around 30-40, the bigger the less detailed<br/>'
            + '<input type="text" id="_cMagicWandConcavHull" name="_cMagicWandConcavHull" value="8" size="4" maxlength="3" /><br/><br/>'
            + '<label for="_cMagicWandSimplification">Landmark simplification</label><br/>Usually 0-5, lesser gives more points in polygon<br/>'
            + '<input type="text" id="_cMagicWandSimplification" name="_cMagicWandSimplification" value="3" size="5" maxlength="4" /><br/><br/>'
            + '<label for="_cMagicWandSampling">Sampling mask size</label><br/>Usually 1-3, larger - smoother and more greedy<br/>'
            + '<input type="text" id="_cMagicWandSampling" name="_cMagicWandSampling" value="3" size="3" maxlength="1" /><br/>';
        addon.appendChild(section);

        var newtab = document.createElement('li');
        newtab.innerHTML = '<a href="#sidepanel-magicwand" data-toggle="tab">MagicWand</a>';
        navTabs.appendChild(newtab);

        addon.id = "sidepanel-magicwand";
        addon.className = "tab-pane";
        tabContent.appendChild(addon);

        populateLandmarks();
        loadWMEMagicWandSettings();

        // UI listeners
        $('#_bMagicWandProcessClick').click(switchMagicWandStatus);
        $('#_cMagicWandEdit_Rotate').change(updateAdvancedEditing);
        $('#_cMagicWandEdit_Resize').change(updateAdvancedEditing);
        $('#_cMagicWandHighlight').change(updateAdvancedEditing);
        $('#_cMagicWandConcavHull').change(updateAdvancedEditing);
        $('#_cMagicWandStraightHelper').change(updateAdvancedEditing);

        // Event listeners
        W.selectionManager.events.register("selectionchanged", null, onLandmarkSelect);
        window.addEventListener("beforeunload", saveWMEMagicWandOptions, false);
        window.addEventListener("keydown", onKeyDown, false);
        window.addEventListener("keyup", onKeyUp, false);

        let extprovobserver = new MutationObserver(function(mutations) {
           mutations.forEach(function(mutation) {
               for (var i = 0; i < mutation.addedNodes.length; i++) {
                   var addedNode = mutation.addedNodes[i];
                   if (addedNode.nodeType === Node.ELEMENT_NODE && $(addedNode).hasClass('address-edit-view')) {
                       if (W.selectionManager.hasSelectedItems() && W.selectionManager.selectedItems[0].model.type === 'venue') {
                           onLandmarkSelect();
                       }
                   }
               }
            });
        });

        extprovobserver.observe(document.getElementById('edit-panel'), { childList: true, subtree: true });

        // Hotkeys
        registerKeyShortcut("WMEMagicWand_CloneLandmark", "Clone Landmark", cloneLandmark, {"C+c": "WMEMagicWand_CloneLandmark"});
        registerKeyShortcut("WMEMagicWand_OrthogonalizeLandmark", "Orthogonalize Landmark", Orthogonalize, {"C+x": "WMEMagicWand_OrthogonalizeLandmark"});
        registerKeyShortcut("WMEMagicWand_SimplifyLandmark", "Simplify Landmark", simplifySelectedLandmark, {"C+j": "WMEMagicWand_SimplifyLandmark"});
        registerKeyShortcut("WMEMagicWand_HighlightLandmark", "Highlight Landmarks", highlightLandmarks, {"C+k": "WMEMagicWand_HighlightLandmark"});

        // Start extension
        WMELandmarkMagicWand();
    }

    function loadWMEMagicWandSettings () {
        if (localStorage.WMEMagicWandScript) {
            console.log("WME MagicWand: loading options");
            var options = JSON.parse(localStorage.WMEMagicWandScript);

            getElId('_cMagicWandEdit_Rotate').checked = typeof options[0] !== 'undefined' ? options[0] : true;
            getElId('_cMagicWandEdit_Resize').checked = typeof options[1] !== 'undefined' && options[1];

            for(var i = 0; i < getElId('_sMagicWandLandmark').options.length; i++) {
                if (getElId('_sMagicWandLandmark').options[i].value === options[2]) {
                    getElId('_sMagicWandLandmark').options[i].selected = true;
                    landmarkTypeSelected = true;
                    break;
                }
            }

            getElId('_cMagicWandSimilarity').value = typeof options[3] !== 'undefined' ? options[3] : 9;
            getElId('_cMagicWandSimplification').value = typeof options[4] !== 'undefined' ? options[4] : 4;
            getElId('_cMagicWandSampling').value = typeof options[5] !== 'undefined' ? options[5] : 3;
            getElId('_cMagicWandAngleThreshold').value = typeof options[6] !== 'undefined' ? options[6] : 12;
            getElId('_cMagicWandHighlight').checked = typeof options[7] !== 'undefined' && options[7];
            getElId('_cMagicWandConcavHull').value = typeof options[8] !== 'undefined' ? options[8] : 40;
            getElId('_cMagicWandStraightHelper').checked = typeof options[9] !== 'undefined' ? options[9] : true;
        }

        updateAdvancedEditing();
    }

    function registerKeyShortcut(action_name, annotation, callback, key_map) {
        W.accelerators.addAction(action_name, {group: 'default'});
        W.accelerators.events.register(action_name, null, callback);
        W.accelerators._registerShortcuts(key_map);
    }

    function saveWMEMagicWandOptions() {
        if (localStorage) {
            console.log("WME MagicWand: saving options");
            var options = [];

            // preserve previous options which may get lost after logout
            if (localStorage.WMEMagicWandScript)
                options = JSON.parse(localStorage.WMEMagicWandScript);

            options[0] = getElId('_cMagicWandEdit_Rotate').checked;
            options[1] = getElId('_cMagicWandEdit_Resize').checked;
            options[2] = getElId('_sMagicWandLandmark').value;
            options[3] = getElId('_cMagicWandSimilarity').value;
            options[4] = getElId('_cMagicWandSimplification').value;
            options[5] = getElId('_cMagicWandSampling').value;
            options[6] = getElId('_cMagicWandAngleThreshold').value;
            options[7] = getElId('_cMagicWandHighlight').checked;
            options[8] = getElId('_cMagicWandConcavHull').value;
            options[8] = getElId('_cMagicWandStraightHelper').checked;

            localStorage.WMEMagicWandScript = JSON.stringify(options);
        }
    }

    var onLandmarkSelect = function (e) {
        var mf = W.map.getControlsByClass('OpenLayers.Control.ModifyFeature')[0];
        if (typeof mf === 'undefined') {
            setTimeout(onLandmarkSelect, 500);
            return;
        }

        insertLandmarkSelectedButtons(e);

        (function () {
            var mf = W.map.getControlsByClass('OpenLayers.Control.ModifyFeature')[0];
            if (typeof mf.wme_magicwand_helper !== 'undefined') {
                return;
            }

            mf.wme_magicwand_helper = true;

            var defaultOnStart = mf.dragControl.onStart;
            var defaultOnComplete = mf.dragControl.onComplete;

            // Reset helpers
            window.wme_magicwand_helpers = {
                isDragging: false,
                draggedNode: null,
                modifiedFeatureVertices: null,
                modifiedFeatureVirtualVertices: null,
                layer: null,
                snap: null
            };

            mf.dragControl.onStart = function (node, t) {
                window.wme_magicwand_helpers.modifiedFeatureVertices = mf.vertices.clone();
                window.wme_magicwand_helpers.modifiedFeatureVirtualVertices = mf.virtualVertices.clone();
                defaultOnStart(node, t);
                onVertexDrag(node);
            };
            mf.dragControl.onComplete = function (node) {
                defaultOnComplete(node);
                onVertexDragComplete();
            };
        })();
    };

    var insertLandmarkSelectedButtons = function(e)
    {
        if(W.selectionManager.selectedItems.length === 0 || W.selectionManager.selectedItems[0].model.type !== 'venue') return;
        if(getElId('_bMagicWandEdit_CloneLandmark') != null) return;

        $('#landmark-edit-general').prepend(
            '<div class="form-group"> \
              <label class="control-label">Advanced options</label> \
              <div class="controls"> \
                <input type="button" id="_bMagicWandEdit_CloneLandmark" name="_bMagicWandEdit_CloneLandmark" class="btn btn-default" value="Clone landmark" title="Ctrl+C (default)" /> \
                <input type="button" id="_bMagicWandEdit_Corners" name="_bMagicWandEdit_Corners" class="btn btn-default" value="Orthogonalize" title="Ctrl+X (default)"/><br/> \
                <input type="button" id="_bMagicWandEdit_Simplify" name="_bMagicWandEdit_Simplify" class="btn btn-default" value="Simplify" title="Ctrl+J (default)"/><br/> \
                <div class="controls-container"> \
                    <input type="checkbox" id="_cLandmarkMagicWandEdit_Rotate" name="_cLandmarkMagicWandEdit_Rotate" value="1" /><label for="_cLandmarkMagicWandEdit_Rotate">Enable Rotate</label>\
                </div>\
                <div class="controls-container"> \
                    <input type="checkbox" id="_cLandmarkWandEdit_Resize" name="_cLandmarkWandEdit_Resize" value="1" /><label for="_cLandmarkWandEdit_Resize">Enable Resize (no reshape)</label>\
                </div>\
              </div> \
            </div>'
        );

        getElId('_cLandmarkMagicWandEdit_Rotate').checked = getElId('_cMagicWandEdit_Rotate').checked;
        getElId('_cLandmarkWandEdit_Resize').checked = getElId('_cMagicWandEdit_Resize').checked;

        $('#_bMagicWandEdit_CloneLandmark').click(cloneLandmark);
        $('#_bMagicWandEdit_Corners').click(Orthogonalize);
        $('#_bMagicWandEdit_Simplify').click(simplifySelectedLandmark);
        $('#_cLandmarkWandEdit_Resize').change(function () {
            getElId('_cMagicWandEdit_Resize').checked = getElId('_cLandmarkWandEdit_Resize').checked;
            updateAdvancedEditing();
        });
        $('#_cLandmarkMagicWandEdit_Rotate').change(function () {
            getElId('_cMagicWandEdit_Rotate').checked = getElId('_cLandmarkMagicWandEdit_Rotate').checked;
            updateAdvancedEditing();
        });


        updateLandmarkControls();
    };

    var awaiting_controls = 0;
    var updateLandmarkControls = function () {
        var ModifyFeatureControl = W.geometryEditing.activeEditor;
        if (ModifyFeatureControl === null) {
            awaiting_controls++;

            // Waiting too long
            if (awaiting_controls > 10) {
                console.log('Something is broken, cannot locale active editor for far too long');
                return;
            }

            setTimeout(updateLandmarkControls, 500);
            return;
        }

        awaiting_controls = 0;

        // Reset modification mode
        ModifyFeatureControl.mode = OL.Control.ModifyFeature.RESHAPE | OL.Control.ModifyFeature.DRAG;

        if ($('#_cMagicWandEdit_Rotate').prop('checked')) {
            ModifyFeatureControl.mode |= OL.Control.ModifyFeature.ROTATE;
        }

        if ($('#_cMagicWandEdit_Resize').prop('checked')) {
            ModifyFeatureControl.mode |= OL.Control.ModifyFeature.RESIZE;
            ModifyFeatureControl.mode &= ~OL.Control.ModifyFeature.RESHAPE; // Do not allow changing the form, keep aspect ratio
        }

        ModifyFeatureControl.resetVertices();
    };

    var simplifySelectedLandmark = function () {
        var selectorManager = W.selectionManager;
        if (!selectorManager.hasSelectedItems() || selectorManager.selectedItems[0].model.type !== "venue" || !selectorManager.selectedItems[0].model.isGeometryEditable()) {
            return;
        }
        var simplifyFactor = $('#_cMagicWandSimplification').val();
        var SelectedLandmark = selectorManager.selectedItems[0];
        var oldGeometry = SelectedLandmark.geometry.clone();

        var LineString = new OL.Geometry.LineString(oldGeometry.components[0].components);
        LineString = LineString.simplify(simplifyFactor);
        var newGeometry = new OL.Geometry.Polygon(new OL.Geometry.LinearRing(LineString.components));

        if (newGeometry.components[0].components.length < oldGeometry.components[0].components.length) {
            var UpdateFeatureGeometry = require("Waze/Action/UpdateFeatureGeometry");
            W.model.actionManager.add(new UpdateFeatureGeometry(SelectedLandmark.model, W.model.venues, oldGeometry, newGeometry));
        }
    };

    var cloneLandmark = function () {
        var selectorManager = W.selectionManager;
        if (!selectorManager.hasSelectedItems() || selectorManager.selectedItems[0].model.type !== 'venue') {
            return;
        }

        var SelectedLandmark = selectorManager.selectedItems[0];
        var ClonedLandmark = SelectedLandmark.clone();
        ClonedLandmark.geometry.move(50, 50); // move to some offset
        ClonedLandmark.geometry.clearBounds();

        var wazefeatureVectorLandmark = require("Waze/Feature/Vector/Landmark");
        var wazeActionAddLandmark = require("Waze/Action/AddLandmark");

        var NewLandmark = new wazefeatureVectorLandmark();
        NewLandmark.geometry = ClonedLandmark.geometry;
        NewLandmark.attributes.categories = SelectedLandmark.model.attributes.categories;

        W.model.actionManager.add(new wazeActionAddLandmark(NewLandmark));
        selectorManager.select([NewLandmark]);
    };

    var Orthogonalize = function() {
        if (W.selectionManager.selectedItems.length <= 0 || W.selectionManager.selectedItems[0].model.type !== 'venue') {
            return;
        }

        var SelectedLandmark = W.selectionManager.selectedItems[0];

        var geom = SelectedLandmark.geometry.clone();
        var components = geom.components[0].components;
        var functor = new OrthogonalizeId(components);

        //if (!functor.isDisabled(components)) {
        //    window.alert('Unable to orthogonalize this polygon');
        //    return;
        //}

        var newWay = functor.action();
        var wazeActionUpdateFeatureGeometry = require("Waze/Action/UpdateFeatureGeometry");

        var removeVertices = [];
        var undoGeometry = SelectedLandmark.geometry.clone();
        for (var i = 0; i < newWay.length; i++) {
            if (newWay[i] === false) {
                removeVertices.push(SelectedLandmark.geometry.components[0].components[i]);
            } else {
                SelectedLandmark.geometry.components[0].components[i].x = newWay[i].x;
                SelectedLandmark.geometry.components[0].components[i].y = newWay[i].y;
            }
        }

        if (removeVertices) {
            SelectedLandmark.geometry.components[0].removeComponents(removeVertices);
        }

        SelectedLandmark.geometry.components[0].clearBounds();

        var action = new wazeActionUpdateFeatureGeometry(SelectedLandmark.model, W.model.venues, undoGeometry, SelectedLandmark.geometry);
        W.model.actionManager.add(action);

        delete undoGeometry;
    };

    var highlightLandmarks = function () {
        if (!$('#_cMagicWandHighlight').prop('checked')) {
            return;
        }

        var geom, components, functor, newWay;

        for (var mark in W.model.venues.objects) {
            var SelectedLandmark = W.model.venues.get(mark);
            if (SelectedLandmark.isPoint()) {
                continue;
            }

            var poly = document.getElementById(SelectedLandmark.geometry.id);
            // check that WME hasn't highlighted this object already
            if (poly == null || mark.state == "Update" || SelectedLandmark.selected) {
              continue;
            }

            // if already highlighted by us or by WME Color Hightlight, avoid conflict and skip
            if (poly.getAttribute("stroke-opacity") == 0.987) {
              continue;
            }

            // if highlighted by mouse over, skip this one
            if (poly.getAttribute("fill") == poly.getAttribute("stroke")) {
              continue;
            }

            // flag this venue as highlighted so we don't update it next time
            poly.setAttribute("stroke-opacity", 0.987);

            geom = SelectedLandmark.geometry.clone();
            components = geom.components[0].components;
            functor = new OrthogonalizeId(components);

            newWay = functor.action();
            for (var i = 0; i < newWay.length; i++) {
                if (newWay[i] === false
                    || Math.abs(SelectedLandmark.geometry.components[0].components[i].x - newWay[i].x) > 2
                    || Math.abs(SelectedLandmark.geometry.components[0].components[i].y - newWay[i].y) > 2
                ) {
                    highlightAPlace(SelectedLandmark, '#FFC138', '#FFD38D');
                    break;
                }
            }
        }
    };

    // WME Color Highlights by Timbones
    function highlightAPlace(venue, fg, bg) {
        var poly = document.getElementById(venue.geometry.id);
        if (venue.isPoint()) {
            poly.setAttribute("fill", fg);
        }

        else { // area
            poly.setAttribute("stroke", fg);
            poly.setAttribute("fill", bg);
        }
    }

    var OrthogonalizeId = function (way) {
        var threshold = getElId('_cMagicWandAngleThreshold').value, // degrees within right or straight to alter
            lowerThreshold = Math.cos((90 - threshold) * Math.PI / 180),
            upperThreshold = Math.cos(threshold * Math.PI / 180);

        this.way = way;

        this.action = function () {
            var nodes = this.way,
                points = nodes.slice(0, nodes.length - 1).map(function (n) {
                    var t = n.clone();
                    var p = t.transform(new OL.Projection("EPSG:900913"), new OL.Projection("EPSG:4326"));
                    p.y = lat2latp(p.y);
                    return p;
                }),
                corner = {i: 0, dotp: 1},
                epsilon = 1e-4,
                i, j, score, motions;

            // Triangle
            if (nodes.length === 4) {
                for (i = 0; i < 1000; i++) {
                    motions = points.map(calcMotion);

                    var tmp = addPoints(points[corner.i], motions[corner.i]);
                    points[corner.i].x = tmp.x;
                    points[corner.i].y = tmp.y;

                    score = corner.dotp;
                    if (score < epsilon) {
                        break;
                    }
                }

                var n = points[corner.i];
                n.y = latp2lat(n.y);
                var pp = n.transform(new OL.Projection("EPSG:4326"), new OL.Projection("EPSG:900913"));

                var id = nodes[corner.i].id;
                for (i = 0; i < nodes.length; i++) {
                    if (nodes[i].id != id) {
                        continue;
                    }

                    nodes[i].x = pp.x;
                    nodes[i].y = pp.y;
                }

                return nodes;
            } else {
                var best,
                    originalPoints = nodes.slice(0, nodes.length - 1).map(function (n) {
                        var t = n.clone();
                        var p = t.transform(new OL.Projection("EPSG:900913"), new OL.Projection("EPSG:4326"));
                        p.y = lat2latp(p.y);
                        return p;
                    });
                    score = Infinity;

                for (i = 0; i < 1000; i++) {
                    motions = points.map(calcMotion);
                    for (j = 0; j < motions.length; j++) {
                        var tmp = addPoints(points[j], motions[j]);
                        points[j].x = tmp.x;
                        points[j].y = tmp.y;
                    }
                    var newScore = squareness(points);
                    if (newScore < score) {
                        best = points.clone();
                        score = newScore;
                    }
                    if (score < epsilon) {
                        break;
                    }
                }

                points = best;

                for (i = 0; i < points.length; i++) {
                    // only move the points that actually moved
                    if (originalPoints[i].x !== points[i].x || originalPoints[i].y !== points[i].y) {
                        var n = points[i];
                        n.y = latp2lat(n.y);
                        var pp = n.transform(new OL.Projection("EPSG:4326"), new OL.Projection("EPSG:900913"));

                        var id = nodes[i].id;
                        for (j = 0; j < nodes.length; j++) {
                            if (nodes[j].id != id) {
                                continue;
                            }

                            nodes[j].x = pp.x;
                            nodes[j].y = pp.y;
                        }
                    }
                }

                // remove empty nodes on straight sections
                for (i = 0; i < points.length; i++) {
                    var dotp = normalizedDotProduct(i, points);
                    if (dotp < -1 + epsilon) {
                        id = nodes[i].id;
                        for (j = 0; j < nodes.length; j++) {
                            if (nodes[j].id != id) {
                                continue;
                            }

                            nodes[j] = false;
                        }
                    }
                }

                return nodes;
            }

            function calcMotion(b, i, array) {
                var a = array[(i - 1 + array.length) % array.length],
                    c = array[(i + 1) % array.length],
                    p = subtractPoints(a, b),
                    q = subtractPoints(c, b),
                    scale, dotp;

                scale = 2 * Math.min(euclideanDistance(p, {x: 0, y: 0}), euclideanDistance(q, {x: 0, y: 0}));
                p = normalizePoint(p, 1.0);
                q = normalizePoint(q, 1.0);

                dotp = filterDotProduct(p.x * q.x + p.y * q.y);

                // nasty hack to deal with almost-straight segments (angle is closer to 180 than to 90/270).
                if (array.length > 3) {
                    if (dotp < -0.707106781186547) {
                        dotp += 1.0;
                    }
                } else if (dotp && Math.abs(dotp) < corner.dotp) {
                    corner.i = i;
                    corner.dotp = Math.abs(dotp);
                }

                return normalizePoint(addPoints(p, q), 0.1 * dotp * scale);
            }
        };

        function squareness(points) {
            return points.reduce(function (sum, val, i, array) {
                var dotp = normalizedDotProduct(i, array);

                dotp = filterDotProduct(dotp);
                return sum + 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1)));
            }, 0);
        }

        function normalizedDotProduct(i, points) {
            var a = points[(i - 1 + points.length) % points.length],
                b = points[i],
                c = points[(i + 1) % points.length],
                p = subtractPoints(a, b),
                q = subtractPoints(c, b);

            p = normalizePoint(p, 1.0);
            q = normalizePoint(q, 1.0);

            return p.x * q.x + p.y * q.y;
        }

        function subtractPoints(a, b) {
            return {x: a.x - b.x, y: a.y - b.y};
        }

        function addPoints(a, b) {
            return {x: a.x + b.x, y: a.y + b.y};
        }

        function euclideanDistance(a, b) {
            var x = a.x - b.x, y = a.y - b.y;
            return Math.sqrt((x * x) + (y * y));
        }

        function normalizePoint(point, scale) {
            var vector = {x: 0, y: 0};
            var length = Math.sqrt(point.x * point.x + point.y * point.y);
            if (length !== 0) {
                vector.x = point.x / length;
                vector.y = point.y / length;
            }

            vector.x *= scale;
            vector.y *= scale;

            return vector;
        }

        function filterDotProduct(dotp) {
            if (lowerThreshold > Math.abs(dotp) || Math.abs(dotp) > upperThreshold) {
                return dotp;
            }

            return 0;
        }

        this.isDisabled = function (nodes) {
            var points = nodes.slice(0, nodes.length - 1).map(function (n) {
                var p = n.toLonLat().transform(new OL.Projection("EPSG:900913"), new OL.Projection("EPSG:4326"));
                return {x: p.lat, y: p.lon};
            });

            return squareness(points);
        };
    };

    var updateAdvancedEditing = function ()
    {
        if ($('#_cMagicWandHighlight').prop('checked')) {
            window.setInterval(highlightLandmarks, 250);
        }

        updateLandmarkControls();

        // var selectorManager = W.selectionManager;
        // if (selectorManager.selectedItems.length > 0 && selectorManager.selectedItems[0].model.type === 'venue') {
        //     selectorManager.selectControl.select(selectorManager.selectedItems[0]);
        // }
    };


    var switchMagicWandStatus = function () {
        window.wme_magic_wand = !window.wme_magic_wand;
        var bgColor, status, btnText;
        if (window.wme_magic_wand) {
            bgColor = 'red';
            btnText = 'CLICK TO STOP MAGIC WAND';
            status = 'Waiting for click'
        } else {
            bgColor = 'green';
            btnText = 'CLICK TO START MAGIC WAND';
            status = 'Disabled'
        }

        $(this).css('background-color', bgColor);
        $(this).val(btnText);
        updateStatus(status);
    };

    function updateStatus(status) {
        $('#_sMagicWandStatus').html(status);
        $('#magicwand_common').hide().show();
    }

    function populateLandmarks() {
        var landmarkTypes = getElId('_sMagicWandLandmark');
        var translations = window.I18n.translations[window.I18n.currentLocale()].venues.categories;

        var filtered_translations = [];
        for (var id in translations) {
            if (!translations.hasOwnProperty(id)) {
                continue;
            }

            filtered_translations.push({
                type_id: id,
                type_name: translations[id]
            });
        }

        // Sorting by name
        filtered_translations = filtered_translations.sort(function (a, b) {
            return a.type_name.localeCompare(b.type_name);
        });

        for (var i = 0; i < filtered_translations.length; i++) {
            id = filtered_translations[i].type_id;
            var type = filtered_translations[i].type_name;

            var usrOption = document.createElement('option');
            var usrText = document.createTextNode(type);
            usrOption.setAttribute('value', id);
            usrOption.appendChild(usrText);
            landmarkTypes.appendChild(usrOption);
        }
    }

    function lat2latp(lat) {
        return 180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * (Math.PI / 180) / 2));
    }

    function latp2lat(a) {
        return 180 / Math.PI * (2 * Math.atan(Math.exp(a * Math.PI / 180)) - Math.PI / 2);
    }

    function WMELandmarkMagicWand() {
        var W = window.W;

        var layer;

        var LatLon;
        var pixel;

        var canvas, draw_canvas, total_tiles, clickCanvasX, clickCanvasY, viewOffsetX, viewOffsetY;
        var context;

        var simplify_param;
        var color_sensitivity;
        var color_distance;
        var color_algorithm;
        var landmark_type;
        var concave_threshold;
        var sampling = 3;
        var detailing = 40;
        var waited_for = 0;
        var is_reload_tiles = true;

        W.map.events.register('moveend', map, function (e) {
            is_reload_tiles = true;
        });

        W.map.events.register('changebaselayer', map, function (e) {
            is_reload_tiles = true;
        });

        W.map.events.register('click', map, function (e) {
            if (!window.wme_magic_wand || window.wme_magic_wand_process) {
                return;
            }

            window.wme_magic_wand_process = true;
            $('#_bMagicWandProcessClick').attr("disabled", "disabled");

            // Get current active layer to process
            layer = null;
            var visible_layers = W.map.getLayersBy("visibility", true);
            for (var l = 0; l < visible_layers.length; l++) {
                if (true === visible_layers[l].isBaseLayer) {
                    layer = visible_layers[l];

                    $('#_sMagicWandUsedLayer').html(layer.name)
                    break;
                }
            }

            if (typeof layer == 'undefined') {
                resetProcessState();
                alert('Please make of the base layers active (default to Google)');
                return;
            }

            if (wme_magic_wand_debug) {
                console.log('WME MagicWand: layer selected', layer.name, layer);
            }

            simplify_param = parseInt(getElId('_cMagicWandSimplification').value);
            color_sensitivity = parseInt(getElId('_cMagicWandSimilarity').value);
            color_distance = parseInt(getElId('_cMagicWandSimilarity').value);
            color_algorithm = getElId("_rMagicWandColorAlgorithm_lab").checked ? "LAB" : "sensitivity";
            landmark_type = getElId("_sMagicWandLandmark").options[getElId("_sMagicWandLandmark").selectedIndex].value;
            concave_threshold = parseFloat(getElId('_cMagicWandSimplification').value);
            sampling = parseInt(getElId('_cMagicWandSampling').value);
            detailing = parseInt(getElId('_cMagicWandConcavHull').value);

            if (wme_magic_wand_debug) {
                console.log('WME MagicWand algorithm:', color_algorithm);
                console.log('WME MagicWand sensitivity:', color_sensitivity);
                console.log('WME MagicWand simplification:', simplify_param);
                console.log('WME MagicWand landmark type:', landmark_type);
                console.log('WME MagicWand sampling mask size:', sampling);
                console.log('WME MagicWand concave hull detailing:', detailing);
            }

            pixel = e.xy;
            LatLon = W.map.getLonLatFromPixel(pixel);

            if (wme_magic_wand_debug) {
                console.log('WME MagicWand: click event', e);
                console.log('WME MagicWand: click event XY', e.xy, ', in map coords', LatLon);
            }

            var tile_size = layer.grid[0][0].size;

            if (wme_magic_wand_debug) {
                console.log('WME MagicWand: grid size in pixels', tile_size);
            }

            updateStatus('Creating canvas');

            if (typeof canvas != 'undefined' && typeof context != 'undefined') {
                if (is_reload_tiles) {
                    canvas.width = tile_size.h * layer.grid[0].length;
                    canvas.height = tile_size.w * layer.grid.length;
                    context.clearRect(0, 0, canvas.width, canvas.height);
                }
            } else {
                canvas = $('<canvas/>')[0];
                canvas.width = tile_size.h * layer.grid[0].length;
                canvas.height = tile_size.w * layer.grid.length;
                context = canvas.getContext('2d');
            }

            if (typeof draw_canvas == 'undefined') {
                draw_canvas = $('<canvas/>')[0];
            }

            draw_canvas.width = canvas.width;
            draw_canvas.height = canvas.height;

            if (wme_magic_wand_debug) {
                $('body').append(draw_canvas);
            }

            total_tiles = layer.grid.length * layer.grid[0].length;
            waited_for = 0;

            if (wme_magic_wand_debug) {
                console.log('WME MagicWand: total tiles in grid', total_tiles);
                console.log('WME MagicWand: canvas', canvas);
                console.log('WME MagicWand: context', context);
            }


            var clientX, clientY;
            var offsetX, offsetY;
            var imageX, imageY;
            var tile, img, location;

            updateStatus('Pre-processing tiles');
            if (wme_magic_wand_debug) {
                console.log('WME MagicWand: trying to load tiles');
            }

            for (var tilerow = 0; tilerow < layer.grid.length; tilerow++) {
                for (var tilei = 0; tilei < layer.grid[tilerow].length; tilei++) {
                    tile = layer.grid[tilerow][tilei];

                    if (tile.bounds.containsLonLat(LatLon, false)) {
                        // Click position on div image
                        clientX = e.pageX;
                        clientY = e.pageY;

                        offsetX = $(tile.imgDiv).offset().left;
                        offsetY = $(tile.imgDiv).offset().top;

                        imageX = clientX - offsetX;
                        imageY = clientY - offsetY;

                        clickCanvasX = tile_size.w * tilei + imageX;
                        clickCanvasY = tile_size.h * tilerow + imageY;

                        viewOffsetX = pixel.x - clickCanvasX;
                        viewOffsetY = pixel.y - clickCanvasY;
                    }

                    // No need to reload tiles
                    if (!is_reload_tiles && !($('img[data-default_url]').length > 0 && $('img[data-coords]').length > 0)) {
                        continue;
                    }

                    updateStatus('Loading tiles');

                    // Have to recreate image - image should have crossOrigin attribute set to "anonymous"
                    img = $('<img/>')[0];
                    $(img).data('tilei', tilei)
                        .data('tilerow', tilerow)
                        .attr('crossOrigin', 'anonymous');

                    img.onload = function () {
                        var img = this;
                        var tilei = $(img).data('tilei');
                        var tilerow = $(img).data('tilerow');

                        // Add tile to canvas
                        context.drawImage(img, tile_size.w * tilei, tile_size.h * tilerow, img.width, img.height);

                        total_tiles--;
                    };

                    img.onerror = function (e) {
                        console.log('WME MagicWand: Cannot load tile: ', e);
                    };

                    var img_url = tile.url;
                    // Experimental support for Map Overlays extension
                    // DO NOT USE FOR EDITS
                    var alt_img = $('img[data-default_url="' + img_url +'"]');
                    if (alt_img.length > 0) {
                        img_url = alt_img[0].src;
                    }

                    location = getLocation(img_url);
                    img.src = img_url + (typeof location.search == 'undefined' || location.search == '' ? '?' : '&') + 'dummy=wmemagicwand';
                }
            }

            if (is_reload_tiles) {
                waitForLoad();
            } else {
                process();
            }

        });

        function waitForLoad() {
            waited_for++;
            if (total_tiles > 0) {
                if (waited_for > 25) {
                    alert('Waiting too long for tiles to be reloaded, tiles left to load: ' + total_tiles);
                    resetProcessState();
                    return;
                }

                window.setTimeout(waitForLoad, 200);
            } else {
                is_reload_tiles = false;
                process();
            }
        }

        function getPixelInfo(canvas_data, x, y) {
            var offset = (y * canvas.width + x) * 4;
            return [canvas_data[offset], canvas_data[offset + 1 ], canvas_data[offset + 2], canvas_data[offset + 3]];
        }

        function getPixelAverageSample(canvas_data, x, y) {
            var sample_info;
            var average = [0, 0, 0, 0];
            var total_samples = 0;
            for (var xi = x - sampling; xi < x + sampling; xi++) {
                for (var yi = y - sampling; yi < y + sampling; yi++) {
                    if (xi < 0 || yi < 0 || xi >= canvas.width || yi >= canvas.height) {
                        continue;
                    }

                    total_samples++;
                    sample_info = getPixelInfo(canvas_data, xi, yi);

                    average[0] += sample_info[0];
                    average[1] += sample_info[1];
                    average[2] += sample_info[2];
                    average[3] += sample_info[3];
                }
            }

            return [average[0] / total_samples, average[1] / total_samples, average[2] / total_samples, average[3] / total_samples];
        }

        function process() {
            var canvas_data = context.getImageData(0, 0, canvas.width, canvas.height).data;
            var ref_pixel = getPixelInfo(canvas_data, clickCanvasX, clickCanvasY);

            if (wme_magic_wand_debug) {
                console.log('WME MagicWand: clicked pixel data', ref_pixel);
            }

            var draw_canvas_context = draw_canvas.getContext('2d');
            draw_canvas_context.drawImage(canvas, 0, 0);

            $('#_dMagicWandColorpicker').css('background-color', 'rgb(' + ref_pixel[0] + ',' + ref_pixel[1] + ',' + ref_pixel[2] + ')');
            $('#magicwand_common').hide().show();

            var current_pixel;
            var processed_pixels = [];
            var polyPixels = [];
            var g = 0;
            var minX = Number.MAX_VALUE;
            var first_pixel = null;

            var stack = [
                [clickCanvasX, clickCanvasY]
            ];

            var x, y, c_pixel, r;
            var viewX, viewY;

            updateStatus('Processing tiles image');

            var id = draw_canvas_context.createImageData(1, 1);
            var d = id.data;
            d[0] = 255;
            d[1] = 0;
            d[2] = 0;
            d[3] = 255; // red

            while (stack.length > 0 && g < 1500000) {
                g++;
                current_pixel = stack.pop();

                // Already processed before
                if (typeof processed_pixels[current_pixel[0] + ',' + current_pixel[1]] != 'undefined') {
                    continue;
                } else {
                    processed_pixels[current_pixel[0] + ',' + current_pixel[1]] = true;
                }

                if (current_pixel[0] < 0 || current_pixel[0] >= canvas.width)
                    continue;
                if (current_pixel[1] < 0 || current_pixel[1] >= canvas.height)
                    continue;

                x = current_pixel[0];
                y = current_pixel[1];
                c_pixel = getPixelAverageSample(canvas_data, x, y);

                if ((color_algorithm == 'sensitivity' && !colorDistance(c_pixel, ref_pixel)) ||
                    (color_algorithm == 'LAB' && calcColorDistance(c_pixel, ref_pixel) > color_distance)) {

                    viewX = x + viewOffsetX;
                    viewY = y + viewOffsetY;

                    if (viewX < minX) {
                        minX = viewX;
                        first_pixel = [viewX, viewY];
                    } else if (viewX == minX && viewY < first_pixel[1]) {
                        first_pixel = [viewX, viewY];
                    }

                    // Outer pixel found
                    polyPixels.push([viewX, viewY]);

                    if (wme_magic_wand_debug) {
                        // Drawing outer border
                        draw_canvas_context.putImageData(id, x, y);
                    }
                } else {
                    // Inner point, add neighboring points to the stack
                    if (wme_magic_wand_debug) {
                        draw_canvas_context.putImageData(id, x, y);
                    }

                    if (typeof processed_pixels[(current_pixel[0] - 1) + ',' + current_pixel[1]] == 'undefined') {
                        stack.push([
                            current_pixel[0] - 1,
                            current_pixel[1]
                        ]);
                    }

                    if (typeof processed_pixels[(current_pixel[0] + 1) + ',' + current_pixel[1]] == 'undefined') {
                        stack.push([
                            current_pixel[0] + 1,
                            current_pixel[1]
                        ]);
                    }

                    if (typeof processed_pixels[(current_pixel[0]) + ',' + current_pixel[1] - 1] == 'undefined') {
                        stack.push([
                            current_pixel[0],
                            current_pixel[1] - 1
                        ]);
                    }

                    if (typeof processed_pixels[(current_pixel[0]) + ',' + current_pixel[1] + 1] == 'undefined') {
                        stack.push([
                            current_pixel[0],
                            current_pixel[1] + 1
                        ]);
                    }

                    // Experimental: with diagonal pixels
                    if (typeof processed_pixels[(current_pixel[0] + 1) + ',' + current_pixel[1] + 1] == 'undefined') {
                        stack.push([
                            current_pixel[0],
                            current_pixel[1] + 1
                        ]);
                    }
                    if (typeof processed_pixels[(current_pixel[0] + 1) + ',' + current_pixel[1] - 1] == 'undefined') {
                        stack.push([
                            current_pixel[0],
                            current_pixel[1] + 1
                        ]);
                    }
                    if (typeof processed_pixels[(current_pixel[0] - 1) + ',' + current_pixel[1] + 1] == 'undefined') {
                        stack.push([
                            current_pixel[0],
                            current_pixel[1] + 1
                        ]);
                    }
                    if (typeof processed_pixels[(current_pixel[0] - 1) + ',' + current_pixel[1] - 1] == 'undefined') {
                        stack.push([
                            current_pixel[0],
                            current_pixel[1] + 1
                        ]);
                    }
                }
            }

            if (wme_magic_wand_debug) {
                console.log('WME MagicWand: iterations done (should be way less than 1,000,000)', g);
                console.log('WME MagicWand: non-processed pixels left (should be 0)', stack.length);
                console.log('WME MagicWand: pixels processed', Object.keys(processed_pixels).length);
                console.log('WME MagicWand: Found pixels (should be way more than 3)', polyPixels.length);
            }

            // Clear unnecessary data
            processed_pixels = [];
            current_pixel = [];
            canvas_data = [];

            if (polyPixels.length > 2) {
                updateStatus('Computing convex hull');

                var points = [];
                for (var j = 0; j < polyPixels.length; j++) {
                    points.push(new Point(polyPixels[j][0], polyPixels[j][1]));
                }

                var convolutionHull = hull(points, 40, ['.x', '.y']);
                createLandmark(convolutionHull, simplify_param);
            } else {
                points = [];
                resetProcessState('Please, try again, no useful points found');
                return;
            }

            points = [];
            resetProcessState();
        }

        function resetProcessState(status_msg) {
            status_msg = typeof status_msg == 'string' ? status_msg : 'Waiting for click';

            window.wme_magic_wand_process = false;
            $('#_bMagicWandProcessClick').removeAttr("disabled");
            updateStatus(status_msg);
        }

        function colorDistance(c_pixel, ref_pixel) {
            return (Math.abs(c_pixel[0] - ref_pixel[0]) <= color_sensitivity &&
                Math.abs(c_pixel[1] - ref_pixel[1]) <= color_sensitivity &&
                Math.abs(c_pixel[2] - ref_pixel[2]) <= color_sensitivity &&
                Math.abs(c_pixel[3] - ref_pixel[3]) <= color_sensitivity);
        }

        function createLandmark(points, simplify) {
            var polyPoints = [];
            var o, point_lonlat;

            for (var k = 0; k < points.length; k++) {
                o = points[k];
                point_lonlat = W.map.getLonLatFromPixel(new OL.Pixel(o.x, o.y));
                polyPoints.push(new OL.Geometry.Point(point_lonlat.lon, point_lonlat.lat));
            }

            var LineString = new OL.Geometry.LineString(polyPoints);
            if (simplify > 0) {
                LineString = LineString.simplify(simplify);
            }

            var wazefeatureVectorLandmark = require("Waze/Feature/Vector/Landmark");
            var wazeActionAddLandmark = require("Waze/Action/AddLandmark");

            var polygon = new OL.Geometry.Polygon(new OL.Geometry.LinearRing(LineString.components));
            var landmark = new wazefeatureVectorLandmark();
            landmark.geometry = polygon;
            landmark.attributes.categories = [landmark_type];

            W.model.actionManager.add(new wazeActionAddLandmark(landmark));
        }

        //
        // Human-eye Similarity algorithm below
        //

        function calcColorDistance(c_pixel, r_pixel) {
            var xyz = rgbToXyz(c_pixel[0], c_pixel[1], c_pixel[2]);
            var lab = xyzToLab(xyz[0], xyz[1], xyz[2]);

            xyz = rgbToXyz(r_pixel[0], r_pixel[1], r_pixel[2]);
            var target_lab = xyzToLab(xyz[0], xyz[1], xyz[2]);

            return cie1994(lab, target_lab, false);

//    return Math.sqrt(Math.pow(c_pixel[0] - r_pixel[0], 2) + Math.pow(c_pixel[1] - r_pixel[1], 2) + Math.pow(c_pixel[2] - r_pixel[2], 2));
        }

// Convert RGB to XYZ
        function rgbToXyz(r, g, b) {
            var _r = (r / 255);
            var _g = (g / 255);
            var _b = (b / 255);

            if (_r > 0.04045) {
                _r = Math.pow(((_r + 0.055) / 1.055), 2.4);
            }
            else {
                _r = _r / 12.92;
            }

            if (_g > 0.04045) {
                _g = Math.pow(((_g + 0.055) / 1.055), 2.4);
            }
            else {
                _g = _g / 12.92;
            }

            if (_b > 0.04045) {
                _b = Math.pow(((_b + 0.055) / 1.055), 2.4);
            }
            else {
                _b = _b / 12.92;
            }

            _r = _r * 100;
            _g = _g * 100;
            _b = _b * 100;

            X = _r * 0.4124 + _g * 0.3576 + _b * 0.1805;
            Y = _r * 0.2126 + _g * 0.7152 + _b * 0.0722;
            Z = _r * 0.0193 + _g * 0.1192 + _b * 0.9505;

            return [X, Y, Z];
        }

// Convert XYZ to LAB
        function xyzToLab(x, y, z) {
            var ref_X = 95.047;
            var ref_Y = 100.000;
            var ref_Z = 108.883;

            var _X = x / ref_X;
            var _Y = y / ref_Y;
            var _Z = z / ref_Z;

            if (_X > 0.008856) {
                _X = Math.pow(_X, (1 / 3));
            }
            else {
                _X = (7.787 * _X) + (16 / 116);
            }

            if (_Y > 0.008856) {
                _Y = Math.pow(_Y, (1 / 3));
            }
            else {
                _Y = (7.787 * _Y) + (16 / 116);
            }

            if (_Z > 0.008856) {
                _Z = Math.pow(_Z, (1 / 3));
            }
            else {
                _Z = (7.787 * _Z) + (16 / 116);
            }

            var CIE_L = (116 * _Y) - 16;
            var CIE_a = 500 * (_X - _Y);
            var CIE_b = 200 * (_Y - _Z);

            return [CIE_L, CIE_a, CIE_b];
        }

        function getLocation(href) {
            var l = document.createElement("a");
            l.href = href;
            return l;
        }

// Finally, use cie1994 to get delta-e using LAB
        function cie1994(x, y, isTextiles) {
            var x = {l: x[0], a: x[1], b: x[2]};
            var y = {l: y[0], a: y[1], b: y[2]};
            labx = x;
            laby = y;
            var k2;
            var k1;
            var kl;
            var kh = 1;
            var kc = 1;
            if (isTextiles) {
                k2 = 0.014;
                k1 = 0.048;
                kl = 2;
            } else {
                k2 = 0.015;
                k1 = 0.045;
                kl = 1;
            }

            var c1 = Math.sqrt(x.a * x.a + x.b * x.b);
            var c2 = Math.sqrt(y.a * y.a + y.b * y.b);

            var sh = 1 + k2 * c1;
            var sc = 1 + k1 * c1;
            var sl = 1;

            var da = x.a - y.a;
            var db = x.b - y.b;
            var dc = c1 - c2;

            var dl = x.l - y.l;
            var dh = Math.sqrt(da * da + db * db - dc * dc);

            return Math.sqrt(Math.pow((dl / (kl * sl)), 2) + Math.pow((dc / (kc * sc)), 2) + Math.pow((dh / (kh * sh)), 2));
        }

        // intersect.js
        function ccw(x1, y1, x2, y2, x3, y3) {
            var cw = ((y3 - y1) * (x2 - x1)) - ((y2 - y1) * (x3 - x1));
            return cw > 0 ? true : cw < 0 ? false : true; // colinear
        }

        function intersect(seg1, seg2) {
          var x1 = seg1[0][0], y1 = seg1[0][1],
              x2 = seg1[1][0], y2 = seg1[1][1],
              x3 = seg2[0][0], y3 = seg2[0][1],
              x4 = seg2[1][0], y4 = seg2[1][1];

            return ccw(x1, y1, x3, y3, x4, y4) !== ccw(x2, y2, x3, y3, x4, y4) && ccw(x1, y1, x2, y2, x3, y3) !== ccw(x1, y1, x2, y2, x4, y4);
        }

        // grid.js
        function Grid(points, cellSize) {
            this._cells = [];
            this._cellSize = cellSize;

            points.forEach(function(point) {
                var cellXY = this.point2CellXY(point),
                    x = cellXY[0],
                    y = cellXY[1];
                if (this._cells[x] === undefined) {
                    this._cells[x] = [];
                }
                if (this._cells[x][y] === undefined) {
                    this._cells[x][y] = [];
                }
                this._cells[x][y].push(point);
            }, this);
        }

        Grid.prototype = {
            cellPoints: function(x, y) { // (Number, Number) -> Array
                return (this._cells[x] !== undefined && this._cells[x][y] !== undefined) ? this._cells[x][y] : [];
            },

            rangePoints: function(bbox) { // (Array) -> Array
                var tlCellXY = this.point2CellXY([bbox[0], bbox[1]]),
                    brCellXY = this.point2CellXY([bbox[2], bbox[3]]),
                    points = [];

                for (var x = tlCellXY[0]; x <= brCellXY[0]; x++) {
                    for (var y = tlCellXY[1]; y <= brCellXY[1]; y++) {
                        points = points.concat(this.cellPoints(x, y));
                    }
                }

                return points;
            },

            removePoint: function(point) { // (Array) -> Array
                var cellXY = this.point2CellXY(point),
                    cell = this._cells[cellXY[0]][cellXY[1]],
                    pointIdxInCell;

                for (var i = 0; i < cell.length; i++) {
                    if (cell[i][0] === point[0] && cell[i][1] === point[1]) {
                        pointIdxInCell = i;
                        break;
                    }
                }

                cell.splice(pointIdxInCell, 1);

                return cell;
            },

            point2CellXY: function(point) { // (Array) -> Array
                var x = parseInt(point[0] / this._cellSize),
                    y = parseInt(point[1] / this._cellSize);
                return [x, y];
            },

            extendBbox: function(bbox, scaleFactor) { // (Array, Number) -> Array
                return [
                    bbox[0] - (scaleFactor * this._cellSize),
                    bbox[1] - (scaleFactor * this._cellSize),
                    bbox[2] + (scaleFactor * this._cellSize),
                    bbox[3] + (scaleFactor * this._cellSize)
                ];
            }
        };

        function grid(points, cellSize) {
            return new Grid(points, cellSize);
        }

        // format.js
        formatUtil = {

            toXy: function(pointset, format) {
                if (format === undefined) {
                    return pointset.slice();
                }
                return pointset.map(function(pt) {
                    /*jslint evil: true */
                    var _getXY = new Function('pt', 'return [pt' + format[0] + ',' + 'pt' + format[1] + '];');
                    return _getXY(pt);
                });
            },

            fromXy: function(pointset, format) {
                if (format === undefined) {
                    return pointset.slice();
                }
                return pointset.map(function(pt) {
                    /*jslint evil: true */
                    var _getObj = new Function('pt', 'var o = {}; o' + format[0] + '= pt[0]; o' + format[1] + '= pt[1]; return o;');
                    return _getObj(pt);
                });
            }

        };

        // convex.js
        function _cross(o, a, b) {
            return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
        }

        function _upperTangent(pointset) {
            var lower = [];
            for (var l = 0; l < pointset.length; l++) {
                while (lower.length >= 2 && (_cross(lower[lower.length - 2], lower[lower.length - 1], pointset[l]) <= 0)) {
                    lower.pop();
                }
                lower.push(pointset[l]);
            }
            lower.pop();
            return lower;
        }

        function _lowerTangent(pointset) {
            var reversed = pointset.reverse(),
                upper = [];
            for (var u = 0; u < reversed.length; u++) {
                while (upper.length >= 2 && (_cross(upper[upper.length - 2], upper[upper.length - 1], reversed[u]) <= 0)) {
                    upper.pop();
                }
                upper.push(reversed[u]);
            }
            upper.pop();
            return upper;
        }

        // pointset has to be sorted by X
        function convex(pointset) {
            var convex,
                upper = _upperTangent(pointset),
                lower = _lowerTangent(pointset);
            convex = lower.concat(upper);
            convex.push(pointset[0]);
            return convex;
        }

        // hull.js

        function _filterDuplicates(pointset) {
            return pointset.filter(function(el, idx, arr) {
                var prevEl = arr[idx - 1];
                return idx === 0 || !(prevEl[0] === el[0] && prevEl[1] === el[1]);
            });
        }

        function _sortByX(pointset) {
            return pointset.sort(function(a, b) {
                if (a[0] == b[0]) {
                    return a[1] - b[1];
                } else {
                    return a[0] - b[0];
                }
            });
        }

        function _sqLength(a, b) {
            return Math.pow(b[0] - a[0], 2) + Math.pow(b[1] - a[1], 2);
        }

        function _cos(o, a, b) {
            var aShifted = [a[0] - o[0], a[1] - o[1]],
                bShifted = [b[0] - o[0], b[1] - o[1]],
                sqALen = _sqLength(o, a),
                sqBLen = _sqLength(o, b),
                dot = aShifted[0] * bShifted[0] + aShifted[1] * bShifted[1];

            return dot / Math.sqrt(sqALen * sqBLen);
        }

        function _intersect(segment, pointset) {
            for (var i = 0; i < pointset.length - 1; i++) {
                var seg = [pointset[i], pointset[i + 1]];
                if (segment[0][0] === seg[0][0] && segment[0][1] === seg[0][1] ||
                    segment[0][0] === seg[1][0] && segment[0][1] === seg[1][1]) {
                    continue;
                }
                if (intersect(segment, seg)) {
                    return true;
                }
            }
            return false;
        }

        function _occupiedArea(pointset) {
            var minX = Infinity,
                minY = Infinity,
                maxX = -Infinity,
                maxY = -Infinity;

            for (var i = pointset.length - 1; i >= 0; i--) {
                if (pointset[i][0] < minX) {
                    minX = pointset[i][0];
                }
                if (pointset[i][1] < minY) {
                    minY = pointset[i][1];
                }
                if (pointset[i][0] > maxX) {
                    maxX = pointset[i][0];
                }
                if (pointset[i][1] > maxY) {
                    maxY = pointset[i][1];
                }
            }

            return [
                maxX - minX, // width
                maxY - minY  // height
            ];
        }

        function _bBoxAround(edge) {
            return [
                Math.min(edge[0][0], edge[1][0]), // left
                Math.min(edge[0][1], edge[1][1]), // top
                Math.max(edge[0][0], edge[1][0]), // right
                Math.max(edge[0][1], edge[1][1])  // bottom
            ];
        }

        function _midPoint(edge, innerPoints, convex) {
            var point = null,
                angle1Cos = MAX_CONCAVE_ANGLE_COS,
                angle2Cos = MAX_CONCAVE_ANGLE_COS,
                a1Cos, a2Cos;

            for (var i = 0; i < innerPoints.length; i++) {
                a1Cos = _cos(edge[0], edge[1], innerPoints[i]);
                a2Cos = _cos(edge[1], edge[0], innerPoints[i]);

                if (a1Cos > angle1Cos && a2Cos > angle2Cos &&
                    !_intersect([edge[0], innerPoints[i]], convex) &&
                    !_intersect([edge[1], innerPoints[i]], convex)) {

                    angle1Cos = a1Cos;
                    angle2Cos = a2Cos;
                    point = innerPoints[i];
                }
            }

            return point;
        }

        function _concave(convex, maxSqEdgeLen, maxSearchArea, grid, edgeSkipList) {
            var edge,
                keyInSkipList,
                scaleFactor,
                midPoint,
                bBoxAround,
                bBoxWidth,
                bBoxHeight,
                midPointInserted = false;

            for (var i = 0; i < convex.length - 1; i++) {
                edge = [convex[i], convex[i + 1]];
                keyInSkipList = edge[0].join() + ',' + edge[1].join();

                if (_sqLength(edge[0], edge[1]) < maxSqEdgeLen ||
                    edgeSkipList[keyInSkipList] === true) { continue; }

                scaleFactor = 0;
                bBoxAround = _bBoxAround(edge);
                do {
                    bBoxAround = grid.extendBbox(bBoxAround, scaleFactor);
                    bBoxWidth = bBoxAround[2] - bBoxAround[0];
                    bBoxHeight = bBoxAround[3] - bBoxAround[1];

                    midPoint = _midPoint(edge, grid.rangePoints(bBoxAround), convex);
                    scaleFactor++;
                }  while (midPoint === null && (maxSearchArea[0] > bBoxWidth || maxSearchArea[1] > bBoxHeight));

                if (bBoxWidth >= maxSearchArea[0] && bBoxHeight >= maxSearchArea[1]) {
                    edgeSkipList[keyInSkipList] = true;
                }

                if (midPoint !== null) {
                    convex.splice(i + 1, 0, midPoint);
                    grid.removePoint(midPoint);
                    midPointInserted = true;
                }
            }

            if (midPointInserted) {
                return _concave(convex, maxSqEdgeLen, maxSearchArea, grid, edgeSkipList);
            }

            return convex;
        }

        function hull(pointset, concavity, format) {
            var convex1,
                concave,
                innerPoints,
                occupiedArea,
                maxSearchArea,
                cellSize,
                points,
                maxEdgeLen = concavity || 20;

            if (pointset.length < 4) {
                return pointset.slice();
            }

            points = _filterDuplicates(_sortByX(formatUtil.toXy(pointset, format)));

            occupiedArea = _occupiedArea(points);
            maxSearchArea = [
                occupiedArea[0] * MAX_SEARCH_BBOX_SIZE_PERCENT,
                occupiedArea[1] * MAX_SEARCH_BBOX_SIZE_PERCENT
            ];

            convex1 = convex(points);
            innerPoints = points.filter(function(pt) {
                return convex1.indexOf(pt) < 0;
            });

            cellSize = Math.ceil(1 / (points.length / (occupiedArea[0] * occupiedArea[1])));

            concave = _concave(
                convex1, Math.pow(maxEdgeLen, 2),
                maxSearchArea, grid(innerPoints, cellSize), {});

            return formatUtil.fromXy(concave, format);
        }

        var MAX_CONCAVE_ANGLE_COS = Math.cos(90 / (180 / Math.PI)); // angle = 90 deg
        var MAX_SEARCH_BBOX_SIZE_PERCENT = 0.6;
    }

    // Point class
    function Point(x, y) {
        this.x = x;
        this.y = y;

        this.toString = function () {
            return "x: " + x + ", y: " + y;
        };
        this.rotateRight = function (p1, p2) {
            // cross product, + is counterclockwise, - is clockwise
            return ((p2.x * y - p2.y * x) - (p1.x * y - p1.y * x) + (p1.x * p2.y - p1.y * p2.x)) < 0;
        };
    }

    Point.prototype.add = function(v){
        return new Point(this.x + v.x, this.y + v.y);
    };
    Point.prototype.clone = function(){
        return new Point(this.x, this.y);
    };
    Point.prototype.degreesTo = function(v){
        var dx = this.x - v.x;
        var dy = this.y - v.y;
        var angle = Math.atan2(dy, dx); // radians
        return angle * (180 / Math.PI); // degrees
    };
    Point.prototype.distance = function(v){
        var x = this.x - v.x;
        var y = this.y - v.y;
        return Math.sqrt(x * x + y * y);
    };
    Point.prototype.equals = function(toCompare){
        return this.x == toCompare.x && this.y == toCompare.y;
    };
    Point.prototype.interpolate = function(v, f){
        return new Point((this.x + v.x) * f, (this.y + v.y) * f);
    };
    Point.prototype.length = function(){
        return Math.sqrt(this.x * this.x + this.y * this.y);
    };
    Point.prototype.normalize = function(thickness){
        var l = this.length();
        this.x = this.x / l * thickness;
        this.y = this.y / l * thickness;
    };
    Point.prototype.orbit = function(origin, arcWidth, arcHeight, degrees){
        var radians = degrees * (Math.PI / 180);
        this.x = origin.x + arcWidth * Math.cos(radians);
        this.y = origin.y + arcHeight * Math.sin(radians);
    };
    Point.prototype.offset = function(dx, dy){
        this.x += dx;
        this.y += dy;
    };
    Point.prototype.subtract = function(v){
        return new Point(this.x - v.x, this.y - v.y);
    };
    Point.prototype.toString = function(){
        return "(x=" + this.x + ", y=" + this.y + ")";
    };

    Point.interpolate = function(pt1, pt2, f){
        return new Point((pt1.x + pt2.x) * f, (pt1.y + pt2.y) * f);
    };
    Point.polar = function(len, angle){
        return new Point(len * Math.cos(angle), len * Math.sin(angle));
    };
    Point.distance = function(pt1, pt2){
        var x = pt1.x - pt2.x;
        var y = pt1.y - pt2.y;
        return Math.sqrt(x * x + y * y);
    };

    var onVertexDrag = function (dragged_node) {
        window.wme_magicwand_helpers.isDragging = true;
        window.wme_magicwand_helpers.draggedNode = dragged_node;

        if (window.event.shiftKey && window.wme_magicwand_helpers.isDragging) {
            startOrthogonalHelper(dragged_node);
        }
    };

    var onVertexDragComplete = function () {
        window.wme_magicwand_helpers.isDragging = false;
        window.wme_magicwand_helpers.draggedNode = null;
        window.wme_magicwand_helpers.modifiedFeatureVertices = null;
        window.wme_magicwand_helpers.modifiedFeatureVirtualVertices = null;
        stopOrthogonalHelper();
    };

    var onKeyDown = function () {
        if (getElId('_cMagicWandStraightHelper').checked && window.event.keyCode === 16 && window.wme_magicwand_helpers.isDragging) {
            startOrthogonalHelper();
        }
    };

    var onKeyUp = function () {
        // Shift key
        if (getElId('_cMagicWandStraightHelper').checked && window.event.keyCode === 16) {
            stopOrthogonalHelper();
        }
    };

    var startOrthogonalHelper = function () {
        var dragged_node = window.wme_magicwand_helpers.draggedNode;

        var components = window.wme_magicwand_helpers.modifiedFeatureVertices;
        var indexOf = null;

        // If dragged node is a real node
        for (var i = 0; i < components.length; i++) {
            if (components[i] === dragged_node) {
                indexOf = i;
                break;
            }
        }

        var prevPointIndex, nextPointIndex;

        // debugger;

        // Maybe we're dragging a new node?
        if (indexOf === null) {
            for (i = 0; i < window.wme_magicwand_helpers.modifiedFeatureVirtualVertices.length; i++) {
                if (window.wme_magicwand_helpers.modifiedFeatureVirtualVertices[i] === dragged_node) {
                    indexOf = i;
                    break;
                }
            }

            if (indexOf !== null) {
                prevPointIndex = indexOf;
                nextPointIndex = indexOf < components.length - 1 ? indexOf + 1 : 0;
            }
        } else {
            prevPointIndex = indexOf > 0 ? indexOf - 1 : components.length - 1;
            nextPointIndex = indexOf < components.length - 1 ? indexOf + 1 : 0;
        }

        if (indexOf === null) {
            console.log('Now that is strange, dragged node not found in vertices');
            return;
        }

        var centerPoint = new OL.Geometry.Point((components[nextPointIndex].geometry.x + components[prevPointIndex].geometry.x) / 2, (components[nextPointIndex].geometry.y + components[prevPointIndex].geometry.y) / 2);
        var radius = Math.sqrt(Math.pow(components[nextPointIndex].geometry.x - components[prevPointIndex].geometry.x, 2) + Math.pow(components[nextPointIndex].geometry.y - components[prevPointIndex].geometry.y, 2)) / 2;

        // Create helper layer and snapping control
        var helperLayer = new OL.Layer.Vector('WMEMagicwand_Helper');
        W.map.addLayer(helperLayer);

        var snap = new OL.Control.Snapping({
            layer: W.map.landmarkLayer,
            targets: [{
                layer: helperLayer,
                tolerance: 25
            }]
        });
        snap.activate();

        helperLayer.addFeatures(new OL.Feature.Vector(OpenLayers.Geometry.Polygon.createRegularPolygon(centerPoint, radius, 500, 0)));

        window.wme_magicwand_helpers.snap = snap;
        window.wme_magicwand_helpers.layer = helperLayer;
    };

    var stopOrthogonalHelper = function () {
        var helpers = window.wme_magicwand_helpers;
        if (!helpers.layer || !helpers.snap) {
            return;
        }

        var layers = W.map.getLayersByName('WMEMagicwand_Helper');
        for (var i = 0; i < layers.length; i++) {
            var l = layers[i];

            l.removeAllFeatures();
            W.map.removeLayer(l);
            l.destroy();
        }

        helpers.snap.deactivate();
        helpers.snap.destroy();

        helpers.snap = null;
        helpers.layer = null;
    };

    /* engage! =================================================================== */
    bootstraMagicWand();
}

/* end ======================================================================= */

// ############################################################################################################################################################
//
// dummyd2's require() patch, modified to perform native require() detection for beta compatibility...
//
//{
if(typeof require === "undefined")
{
   var WMEAPI = {};
   WMEAPI.scripts = document.getElementsByTagName('script');
   WMEAPI.url=null;
   for (var i=0;i<WMEAPI.scripts.length;i++)
   {
      if (WMEAPI.scripts[i].src.indexOf('/assets-editor/js/app')!=-1)
      {
         WMEAPI.url=WMEAPI.scripts[i].src;
         break;
      }
   }
   if (WMEAPI.url==null)
   {
      throw new Error("WME Hack: can't detect WME main JS");
   }
   WMEAPI.require=function (e)
   {
      if (WMEAPI.require.define.modules.hasOwnProperty(e))
      {
         return WMEAPI.require.define.modules[e];
      }
      else
      {
         console.error('Require failed on ' + e, WMEAPI.require.define.modules);
      }
      return null;
   };
   WMEAPI.require.define=function (m)
   {
      if (WMEAPI.require.define.hasOwnProperty('modules') === false)
      {
         WMEAPI.require.define.modules={};
      }
      for (var p in m)
      {
         WMEAPI.require.define.modules[p]=m[p];
      }
   };
   WMEAPI.tmp = window.webpackJsonp;
   WMEAPI.t = function (n)
   {
      if (WMEAPI.s[n])
      {
         return WMEAPI.s[n].exports;
      }
      var r = WMEAPI.s[n] =
      {
         exports: {},
         id: n,
         loaded: !1
      };
      return WMEAPI.e[n].call(r.exports, r, r.exports, WMEAPI.t), r.loaded = !0, r.exports;
   };
   WMEAPI.e=[];
   window.webpackJsonp = function(a, i)
   {
      var api={};
      for (var o, d, u = 0, l = []; u < a.length; u++)
      {
         d = a[u], WMEAPI.r[d] && l.push.apply(l, WMEAPI.r[d]), WMEAPI.r[d] = 0;
      }
      var unknownCount=0;
      var classname, funcStr;
      for (o in i)
      {
         WMEAPI.e[o] = i[o];
         funcStr = i[o].toString();
         classname = funcStr.match(/CLASS_NAME:\"([^\"]*)\"/);
         if (classname)
         {
            api[classname[1].replace(/\./g,'/').replace(/^W\//, 'Waze/')]={index: o, func: WMEAPI.e[o]};
         }
         else
         {
            api['Waze/Unknown/' + unknownCount]={index: o, func: WMEAPI.e[o]};
            unknownCount++;
         }
      }
      for (; l.length;)
      {
         l.shift().call(null, WMEAPI.t);
      }
      WMEAPI.s[0] = 0;
      var module={};
      var apiFuncName;
      unknownCount=0;
      for (o in i)
      {
         funcStr = i[o].toString();
         classname = funcStr.match(/CLASS_NAME:\"([^\"]*)\"/);
         if (classname)
         {
            module={};
            apiFuncName = classname[1].replace(/\./g,'/').replace(/^W\//, 'Waze/');
            module[apiFuncName]=WMEAPI.t(api[apiFuncName].index);
            WMEAPI.require.define(module);
         }
         else
         {
            var matches = funcStr.match(/SEGMENT:"segment",/);
            if (matches)
            {
               module={};
               apiFuncName='Waze/Model/ObjectType';
               module[apiFuncName]=WMEAPI.t(api['Waze/Unknown/' + unknownCount].index);
               WMEAPI.require.define(module);
            }
            unknownCount++;
         }
      }
      window.webpackJsonp=WMEAPI.tmp;
      window.require=WMEAPI.require;
      setTimeout(initWmeMagicWand(), 500);
   };
   WMEAPI.s = {};
   WMEAPI.r = {0: 0};
   WMEAPI.WMEHACK_Injected_script = document.createElement("script");
   WMEAPI.WMEHACK_Injected_script.setAttribute("type", "application/javascript");
   WMEAPI.WMEHACK_Injected_script.src = WMEAPI.url;
   document.body.appendChild(WMEAPI.WMEHACK_Injected_script);
}
else
{
    initWmeMagicWand();
}
//}
//
// end of dummyd2's require() patch
//

function initWmeMagicWand() {
    var DLscript = document.createElement("script");
    DLscript.textContent = run_magicwand.toString() + ' \n' + 'run_magicwand();';
    DLscript.setAttribute("type", "application/javascript");
    document.body.appendChild(DLscript);
}