WME Util Singleton

Utility singleton for Waze map editor scripts

Skrip ini tidak untuk dipasang secara langsung. Ini adalah pustaka skrip lain untuk disertakan dengan direktif meta // @require https://update.greasyfork.org/scripts/15731/98588/WME%20Util%20Singleton.js

// ==UserScript==
// @name         WME Util Singleton
// @namespace    http://tampermonkey.net/
// @version      0.203
// @description  Utility singleton for Waze map editor scripts
// @author       slemmon
// @match        https://www.waze.com/editor/*
// @match        https://editor-beta.waze.com/editor/*
// @grant        none
// ==/UserScript==
/* jshint -W097 */
'use strict';

/**
 *  =============================================
 *  USER INFORMATION
 *  =============================================
 *
 * This is a util class to be @required by other scripts.  Consider the API a bit fluid at
 * this stage as this class is new and not very fleshed out.  Documentation for each class
 * method / property will appear just before the method / property.
 * 
 * The Ext JS framework is loaded and once loaded the W.ux.Util singleton is
 * created.  Next we fire a jQuery event of "extready" into the document body
 * so that any scripts waiting on Ext JS and the W.ux.Util singleton know they 
 * may proceed.
 * 
 * Scripts using W.ux.Util should add the following to listen for the 'extready'
 * event:
 *     $(document.body).on('extready', function () {
 *         // script logic - may still need to check for dom ready and / or
 *         // the OpenLayers map's existence here before proceeding
 *     });
 */
$.getScript('https://cdnjs.cloudflare.com/ajax/libs/extjs/6.0.0/ext-all.js', function () {
    
    Ext.define('W.ux.Util', {
        /**
         * Adds CSS style rule(s) to the page
         * @param {String/String[]/String...} rule An array of CSS style rule strings or
         * any number of CSS style rule params
         */
        injectStyleRules(rule) {
            var styleTag = this.styleTag,
                args = Array.prototype.slice.call(arguments),
                div;
            
            // allows you to pass in an array, string, or n number of string params
            rule = args.length === 1 ? args.shift() : args;
            
            // if the style tag is found cached then create at the end of the <body>
            // and cache a reference to it
            if (!styleTag) {
                div = $("<div />", {
                    html: '&shy;<style></style>'
                }).appendTo("body");
                
                // cache a ref to the style tag
                styleTag = this.styleTag = div.find('style');
            }
            
            // append to the style tag the style rule / rules passed
            styleTag.append(Ext.Array.from(rule).join(''));
        },
        
        /**
         * Returns the Waze editor map or false if the map is not found
         * @return {Object/Boolean} The map instance used by Waze or false if not found
         */
        getMap: function () {
            return (W && W.map) ? W.map : false;
        },
        
        /**
         * Simple util to return an array of [a, b] type endpoint sub-arrays given a complete
         * array of points (vertices) from a ring polygon geometry.  For example, if you passed:
         * [x, y, z] in what would be returned is [[x, y], [y, z], [z, x]].
         * @param {Array} points Array of all points in the ring polygon geometry
         * @return {Array} An array of endpoint arrays from the passed points
         */
        getEndpoints: function (points) {
            var endpoints = [],
                len = points.length,
                pointB;
            
            points.forEach(function (point, i) {
                var pointB = (points[i + 1]) ? points[i + 1] : points[0];
                endpoints.push([points[i], pointB]);
            });
            
            return endpoints;
        },
        
        // http://jsfiddle.net/justin_c_rounds/Gd2S2/
        /**
         * Finds the points where two lines intersect.  That could be intersections where
         * the lines would literally overlap or could be where the extension of a line would
         * logically overlap.
         * @param {Number} line1StartX The starting x coordinate of line 1
         * @param {Number} line1StartY The starting y coordinate of line 1
         * @param {Number} line1EndX The ending x coordinate of line 1
         * @param {Number} line1EndY The ending y coordinate of line 1
         * @param {Number} line2StartX The starting x coordinate of line 2
         * @param {Number} line2StartY The starting y coordinate of line 2
         * @param {Number} line2EndX The ending x coordinate of line 2
         * @param {Number} line2EndY The ending y coordinate of line 2
         * @return {Object} Returns an object with the following key / value info
         * 
         *     - x {Number} the x coordinate of the intersection or null if there is no intersection
         *     - y {Number} the y coordinate of the intersection or null if there is no intersection
         *     - onLine1 {Boolean} true if the intersection falls on line 1 otherwise false
         *     - onLine2 {Boolean} true if the intersection falls on line 2 otherwise false
         */
        findIntersectionPoint: function (line1StartX, line1StartY, line1EndX, line1EndY, line2StartX, line2StartY, line2EndX, line2EndY) {
            // if the lines intersect, the result contains the x and y of the intersection
            // (treating the lines as infinite) and booleans for whether line segment 1 
            // or line segment 2 contain the point
            var denominator, a, b, numerator1, numerator2, result = {
                x: null,
                y: null,
                onLine1: false,
                onLine2: false
            };
            
            // we'll assume two arrays of two points were passed so we'll split them out
            if (Ext.isArray(line1StartX)) {
                line2EndY = line1StartY[1].y;
                line2EndX = line1StartY[1].x;
                line2StartY = line1StartY[0].y;
                line2StartX = line1StartY[0].x;
                
                line1EndY = line1StartX[1].y;
                line1EndX = line1StartX[1].x;
                line1StartY = line1StartX[0].y;
                line1StartX = line1StartX[0].x;
            }

            denominator = ((line2EndY - line2StartY) * (line1EndX - line1StartX)) - ((line2EndX - line2StartX) * (line1EndY - line1StartY));
            if (denominator == 0) {
                return result;
            }
            a = line1StartY - line2StartY;
            b = line1StartX - line2StartX;
            numerator1 = ((line2EndX - line2StartX) * a) - ((line2EndY - line2StartY) * b);
            numerator2 = ((line1EndX - line1StartX) * a) - ((line1EndY - line1StartY) * b);
            a = numerator1 / denominator;
            b = numerator2 / denominator;

            // if we cast these lines infinitely in both directions, they intersect here:
            result.x = line1StartX + (a * (line1EndX - line1StartX));
            result.y = line1StartY + (a * (line1EndY - line1StartY));
            /*
            // it is worth noting that this should be the same as:
            x = line2StartX + (b * (line2EndX - line2StartX));
            y = line2StartX + (b * (line2EndY - line2StartY));
            */
            // if line1 is a segment and line2 is infinite, they intersect if:
            if (a > 0 && a < 1) {
                result.onLine1 = true;
            }
            // if line2 is a segment and line1 is infinite, they intersect if:
            if (b > 0 && b < 1) {
                result.onLine2 = true;
            }
            // if line1 and line2 are segments, they intersect if both of the above are true
            return result;
        },
        
        /**
         * Returns the first two intersecting features from a layer
         * @param {Object} layer The layer from which to find intersecting features
         * @return {Array} An array containing the first two intersecting features
         * found or an empty array if no intersection features were found
         */
        getIntersection: function (layer) {
            if (!layer) {
                return false;
            }
            
            var features = layer.features,
                len = features.length,
                intersected = [],
                i = 0,
                j, feature, featureGeo, candidate, candidateGeo, intersects;
            
            for (; i < len; i++) {
                feature = features[i];
                featureGeo = feature.geometry;
                j = 0;
                for (; j < len; j++) {
                    candidate = features[j];
                    candidateGeo = candidate.geometry;
                    intersects = featureGeo.intersects(candidateGeo);
                    
                    if (intersects && (featureGeo !== candidateGeo)) {
                        intersected = [feature, candidate];
                        break;
                    }
                }
                if (intersected.length) {
                    break;
                }
            }
            
            return intersected;
        },
        
        /**
         * Get a new polygon feature from an array of points
         * @param {Array} points The array of OpenLayers.Geometry.Points used to make the polygon feature
         * @param {Object} [attribs] Optional object to be mapped to the attributes property of the feature
         * @param {Object} [style] Optional style object
         * @return {Object} The polygon feature generaeted from the provided points
         */
        getPolyFromPoints: function (points, attribs, style) {
            var ring = new OpenLayers.Geometry.LinearRing(points),
                polygon = new OpenLayers.Geometry.Polygon([ring]), 
                feature = new OpenLayers.Feature.Vector(polygon, attribs, style);

            return feature;
        },
        
        /**
         * @private
         * Internal method used by the getMergedPolygon method
         */
        applyIntersectionPoints: function (pointsArrA, pointsArrB, altPoly, segmentsArrA, segmentsArrB, copyPoints) {
            var me = this,
                intersect, junction;
            
            pointsArrA.forEach(function (first, i) {
                first.isInternal = altPoly.containsPoint(first);
                delete first.isJunction;
                var junctions = [];
                pointsArrB.forEach(function (second, j) {
                    intersect = me.findIntersectionPoint(segmentsArrA[i], segmentsArrB[j]);
                    if (intersect.onLine1 && intersect.onLine2) {
                        junction = new OpenLayers.Geometry.Point(intersect.x, intersect.y);
                        junction.isJunction = true;
                        junctions.unshift(junction);
                    }
                });
                junctions.sort(function (a, b) {
                    var aDist = first.distanceTo(a),
                        bDist = first.distanceTo(b);

                    return aDist > bDist ? -1 : ((bDist > aDist) ? 1 : 0);
                });
                junctions.forEach(function (item) {
                    copyPoints.splice(copyPoints.indexOf(pointsArrA[i]) + 1, 0, item);
                });
            });
        },
        
        /**
         * Return a single polygon feature from the outline of two intersecting
         * polygons
         * @param {Object/Object[]} polyA The first polygon or feataure owning the polygon
         * to be merged.  May also be an array of both constituent polygons in which case
         * the second param if passed will be ignored
         * @param {Object} polyB The second polygon or feature owning the polygon to
         * be merged
         * return {Object} The merged polygon vector feature
         */
        getMergedPolygon: function (polyA, polyB) {
            if (Ext.isArray(polyA)) {
                polyB = polyA[1];
                polyA = polyA[0];
            }
            
            // set polyA and B to be the polygon geometries if the parent vector feature was passed in
            polyA = polyA.CLASS_NAME === "OpenLayers.Feature.Vector" ? polyA.geometry : polyA;
            polyB = polyB.CLASS_NAME === "OpenLayers.Feature.Vector" ? polyB.geometry : polyB;
            
            var me = this,
                pointsA = polyA.getVertices(),
                pointsB = polyB.getVertices(),
                copyPointsA = pointsA.slice(),
                copyPointsB = pointsB.slice(),
                segmentsA = me.getEndpoints(pointsA),
                segmentsB = me.getEndpoints(pointsB),
                initialX = Infinity,
                initial, activePoints, hostPoly, activePoint, union, altPoints,
                activeIndex, next, nextAltIndex, i, altNext, altA, altB,
                centroidA, candidateAIntersects, centroidB, candidateBIntersects,
                mids, mid;
            
            // Set OpenLayers.geometry.Points in the points / vertices array where the two
            // polygons intersect
            me.applyIntersectionPoints(pointsA, pointsB, polyB, segmentsA, segmentsB, copyPointsA);
            me.applyIntersectionPoints(pointsB, pointsA, polyA, segmentsB, segmentsA, copyPointsB);

            // function to find the starting point for when we walk through the polygon to get
            // its outline.  The initial point will be the one that's furthest left
            (function () {
                var evalPoints = function (points) {
                    points.forEach(function (point) {
                        if (point.x < initialX) {
                            initialX = point.x;
                            initial = point;
                            activePoints = points;
                            hostPoly = (activePoints === copyPointsA) ? polyA : polyB;
                        }
                    });
                };

                evalPoints(copyPointsA);
                evalPoints(copyPointsB);
            })();

            // the union array will hold the points of the new polygon perimeter
            union = [initial];
            altPoints = (activePoints === copyPointsA) ? copyPointsB : copyPointsA;
            while (activePoint !== initial) {
                activePoint = activePoint || initial;

                // find the current pointer and the next point from it
                activeIndex = activePoints.indexOf(activePoint);
                next = activePoints[activeIndex + 1] || activePoints[0];
                
                // if the next point is not a junction add it to the union array
                if (!next.isJunction) {
                    if (next !== initial) {
                        union.push(next);
                    }
                    activePoint = next;
                } else { // the point is a junction / intersect point
                    union.push(next); // add it to the union array
                    nextAltIndex;
                    // find the next perimeter junction
                    for (i = 0; i < altPoints.length; i++) {
                        if (altPoints[i].x === next.x && altPoints[i].y === next.y) {
                            nextAltIndex = i;
                            break;
                        }
                    }

                    // using the next junction find the point ahead and behind it
                    altNext = altPoints[nextAltIndex];
                    altA = altPoints[nextAltIndex - 1] || altPoints[altPoints.length - 1];
                    altB = altPoints[nextAltIndex + 1] || altPoints[0];

                    // see if the point behind it crosses the initial polygon
                    centroidA = new OpenLayers.Geometry.Point((altNext.x + altA.x)/2, (altNext.y + altA.y)/2);
                    candidateAIntersects = hostPoly.containsPoint(centroidA);
                    centroidA.destroy();

                    // see if the point ahead of it crosses the initial polygon
                    centroidB = new OpenLayers.Geometry.Point((altNext.x + altB.x)/2, (altNext.y + altB.y)/2);
                    candidateBIntersects = hostPoly.containsPoint(centroidB);
                    centroidB.destroy();

                    // the mids array will be the points from the companion polygon
                    // between the two intersections currently being inspected
                    mids = [];
                    mid = {};
                    //if one of the lines does not intersect (its posible for there
                    // to be no points between the junctions)
                    if (!candidateAIntersects || !candidateBIntersects) {
                        i = nextAltIndex;
                        // find all the points between the junctions and add them to the
                        // mids array to be then added to the union array
                        while (!mid.isJunction) {
                            if (!candidateAIntersects) {
                                i = (i - 1 > -1) ? i - 1 : altPoints.length - 1;
                            } else {
                                i = (i + 1 < altPoints.length) ? i + 1 : 0;
                            }
                            mid = altPoints[i];
                            if (mid && !mid.isJunction) {
                                mids.push(mid);
                            }
                        }
                    }

                    // add the points between the junctions from the companion polygon
                    union = union.concat(mids);

                    // if we're at the junction add the junction point corresponding within
                    // the initial polygon
                    if (mid.isJunction) {
                        for (i = 0; i < activePoints.length; i++) {
                            if (activePoints[i].x === mid.x && activePoints[i].y === mid.y) {
                                activePoint = activePoints[i];
                                union.push(activePoint);
                                break;
                            }
                        }
                    }
                }
                // continue the loop until all points from the initial and companion polygon
                // have been included to create a merged perimeter
            }

            return this.getPolyFromPoints(union);
        }
    });
    
    // this announces to any listening scripts that Ext and the W.ux.Util
    // singleton are now ready for use
    $(document.body).trigger('extready');
});