您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Opens a PL in an existing WME window/tab.
// ==UserScript== // @name WME PL Jump // @description Opens a PL in an existing WME window/tab. // @version 0.0.11 // @author SAR85 // @copyright SAR85 // @license CC BY-NC-ND // @grant none // @include https://www.waze.com/editor/* // @include https://www.waze.com/*/editor/* // @include https://editor-beta.waze.com/* // @namespace https://greasyfork.org/users/9321 // @require https://greasyfork.org/scripts/9794-wlib/code/wLib.js?version=106098 // ==/UserScript== /* global OL */ /* global W */ /* global wLib */ (function () { 'use strict'; var app, AppView, jumpInput, JumpView, LinkView, Link, links, LinkCollection, tab; /** * Checks for necessary page elements or objects before initializing * script. * @param tries {Number} The number of tries bootstrapping has been * attempted. */ function bootstrap(tries) { tries = tries || 0; if ('undefined' !== typeof wLib && window.$ && window.Backbone && $('#edit-buttons').size()) { init(); } else if (tries < 20) { window.setTimeout(function () { bootstrap(tries + 1); }, 1000); } } /** * Initializes global variables and sets up HTML and event listeners. */ function init() { var tabContent = '<div id="pljumptabcontent"></div>'; if (!$('#pljumpinput').size()) { tab = new wLib.Interface.Tab('PLJump', tabContent); initializeLink(); initializeCollection(); links = new LinkCollection; initializeViews(); jumpInput = new JumpView({ collection: links }); app = new AppView({ collection: links }); updateAlert(); } } function updateAlert() { var version = '0.0.11'; var versionChanges = ''; var previousVersion; versionChanges += 'WME PL Jump v' + version + ' changes:\n'; versionChanges += '-Fixed disabled buttons issue.\n'; if (localStorage === void 0) { return; } previousVersion = localStorage.pljumpVersion; if (version !== previousVersion) { alert(versionChanges); localStorage.pljumpVersion = version; } }; function initializeLink() { /** * Class representing PLs. */ Link = Backbone.Model.extend({ defaults: { link: '', lonLat: null, segments: null, mapProblem: null, nodes: null, venues: null, selectionInBounds: false, selectionOnScreen: false, updateRequest: null, zoom: null }, itemsToSelect: [], modelObject: null, /** * Checks whether the PLs items to select are in the data bounds * and in view. */ isSelectionOnScreen: function () { return this.attributes.selectionInBounds && this.attributes.selectionOnScreen; }, /** * Checks whether the PL has items to select or not. */ hasItems: function () { return this.attributes.segments || this.attributes.nodes || this.attributes.venues || this.attributes.updateRequest || this.attributes.mapProblem; }, /** * Parses PL to extract the location and selected item info. */ initialize: function () { var extractedData = {}, link = this.get('link'), lat, lon, lonLat, mapProblem, nodes, segments, updateRequest, venues, zoom; link.replace(/%3D/g, '='); lat = link.match(/lat=(-?\d+\.\d+)/); lon = link.match(/lon=(-?\d+\.\d+)/); nodes = link.match(/nodes=((\d+,?)+)/); segments = link.match(/segments=((\d+,?)+)/); updateRequest = link.match(/mapUpdateRequest=(\d+)/); mapProblem = link.match(/mapProblem=(\d%2F\d+)/); venues = link.match(/venues=((\d+\.?)+)/); zoom = link.match(/zoom=(\d+)/); extractedData.lat = lat && lat.length === 2 ? parseFloat(lat[1]) : null; extractedData.lon = lon && lon.length === 2 ? parseFloat(lon[1]) : null; extractedData.segments = segments && segments.length > 1 ? segments[1].split(',') : null; extractedData.venues = venues ? venues[1].split(',') : null; extractedData.nodes = nodes && nodes.length > 1 ? nodes[1].split(',') : null; extractedData.updateRequest = updateRequest && updateRequest.length === 2 ? updateRequest[1] : null; extractedData.mapProblem = mapProblem && mapProblem.length === 2 ? mapProblem[1].replace('%2F', '/') : null; extractedData.zoom = zoom && zoom.length === 2 ? parseInt(zoom[1]) : null; this.set({ 'segments': extractedData.segments, 'mapProblem': extractedData.mapProblem, 'nodes': extractedData.nodes, 'venues': extractedData.venues, 'updateRequest': extractedData.updateRequest, 'zoom': extractedData.zoom }); if (extractedData.lon && extractedData.lat) { lonLat = new OL.LonLat(extractedData.lon, extractedData.lat); lonLat.transform(W.map.displayProjection, W.map.getProjectionObject()); if (W.map.isValidLonLat(lonLat)) { this.set('lonLat', lonLat); } } else { this.set('lonLat', { lon: 'None', lat: 'None' }); } this.on('change:link', this.onLinkChanged); }, /** * Private method to compile WME objects for selection. * @private */ createSelection: function () { var i, itemsToSelect = [], itemType, itemTypes = ['segments', 'nodes', 'venues'], mapBounds = W.map.getExtent(), modelObject, n, selectionInBounds = true, selectionOnScreen = true; var getObject = function (element) { var object = W.model[itemType].get(element); if (object) { itemsToSelect.push(object); if (!mapBounds.intersectsBounds( object.geometry.getBounds())) { selectionOnScreen = false; } } else { selectionInBounds = false; } }; for (i = 0, n = itemTypes.length; i < n; i++) { itemType = itemTypes[i]; _.each(this.attributes[itemType], getObject, this); } if (this.attributes.updateRequest || this.attributes.mapProblem) { modelObject = W.model.mapUpdateRequests.get( this.attributes.updateRequest) || W.model.problems.get( this.attributes.mapProblem) || null; if (!modelObject) { selectionInBounds = false; } else if (!mapBounds.intersectsBounds( modelObject.attributes.geometry.getBounds())) { selectionOnScreen = false; } } this.set({ 'selectionInBounds': selectionInBounds, 'selectionOnScreen': selectionOnScreen }); this.modelObject = modelObject; this.itemsToSelect = itemsToSelect; return this; }, /** * Pans the map to the lat & lon location specified in the PL. * @private */ moveTo: function () { var zoom = this.get('zoom') || W.map.getZoom(); if (this.attributes.lonLat) { W.map.moveTo(this.attributes.lonLat, zoom); } return this; }, /** * Public method to go to a Link and select its objects. */ open: function (forceMove) { this.createSelection(); if (this.hasItems()) { this.select(); if (forceMove || !this.isSelectionOnScreen()) { this.moveTo(); } } else { this.moveTo(); } return this; }, /** * Re-parse the link if it changes. */ onLinkChanged: function () { this.initialize(); }, /** * Selects objects extracted from the PL. * @private */ select: function () { var selectItems = function () { this.createSelection(); if (this.modelObject) { W.commands.execute('problems:show', this.modelObject); } if (this.itemsToSelect.length > 0) { W.selectionManager.select(this.itemsToSelect); } }; wLib.Model.onModelReady(selectItems, this.isSelectionOnScreen(), this); return this; } }); } function initializeCollection() { /** * Class representing collection of PLs. */ LinkCollection = Backbone.Collection.extend({ model: Link, getLinkID: function () { var id = 0; return function () { return id++; }; } () }); } function initializeViews() { /** * Class containing the main app interface and logic. */ AppView = Backbone.View.extend({ collection: null, /** * Gets the stored options from localStorage and returns * them as an object. */ options: function () { var defaultOptions = { 'trackHistory': true, 'trackSelections': false }; var options = localStorage && localStorage.pljOptions; options = options && JSON.parse(options) || defaultOptions; return options; } (), clearButtonCss: { 'margin-bottom': '10px' }, divCss: { 'overflow-y': 'scroll', 'max-height': '400px' }, tableDivCss: { 'overflow-y': 'scroll', 'max-height': '400px', 'width': '292px' }, el: $('#pljumptabcontent'), template: function () { var $div = $('<div/>'), $table = $('<table/>').attr('id', 'plj-history-table'), $clearButton = $('<button/>').attr('id', 'plj-clear-table'). css(this.clearButtonCss).text('Clear all entries'), $trackHistory = $('<div/>').append($('<input type="checkbox" id="plj-track-history"><label>Track map move history</label>')), $trackSelections = $('<div/>').append($('<input type="checkbox" id="plj-track-selections"><label>Track selections</label>')), $infoText = $('<p>Click a table entry below to select it. To force the map to move to the PL location, Ctrl+Click.</p>'); $div.append($trackHistory); $div.append($trackSelections); $div.append($clearButton.wrap('<div>').parent()); $div.append($infoText.wrap('<div>').parent()); $div.append($table.wrap('<div>').parent(). css(this.tableDivCss)); return $div; }, events: { 'click #plj-clear-table': 'onClearTableClicked', 'change #plj-track-history': 'onTrackHistoryChange', 'change #plj-track-selections': 'onTrackSelectionsChange' }, initialize: function () { this.listenTo(this.collection, 'add', this.addLink); this.render(); this.onTrackHistoryChange(); this.onTrackSelectionsChange(); }, render: function () { this.$el.append(this.template()); this.$trackHistory = this.$el.find('#plj-track-history'); this.$trackHistory.prop('checked', this.options['trackHistory']); this.$trackSelections = this.$el.find('#plj-track-selections'); this.$trackSelections.prop('checked', this.options['trackSelections']); this.$table = this.$el.find('#plj-history-table'); this.$clearButton = this.$el.find('#plj-clear-table'); return this; }, /** * Adds a new link to the table. */ addLink: function (link) { var view = new LinkView({ model: link }); this.$table.prepend(view.render().$el); }, /** * Tracks map moves and determines if the location changed * after a move. This filters out zoom-only "moves". */ mapLocationChanged: function () { var lastLocation, locationChanged, newLocation; var getMapLocation = function () { var lonLat = W.map.getCenter(), zoom = W.map.getZoom(); return { lon: lonLat.lon, lat: lonLat.lat, zoom: zoom }; }; var compareLocations = function () { newLocation = getMapLocation(); if (newLocation.lon !== lastLocation.lon || newLocation.lat !== lastLocation.lat) { lastLocation = newLocation; locationChanged = true; } else { locationChanged = false; } }; lastLocation = getMapLocation(); W.map.events.register('moveend', this, compareLocations); return function () { return locationChanged; }; } (), /** * Callback for clearing the link history. */ onClearTableClicked: function (e) { this.$table.find('tr').remove(); this.collection.reset(); }, /** * Callback for map move. */ onMapMove: function () { if (this.mapLocationChanged()) { this.collection.add({ link: $('.WazeControlPermalink a').prop('href') }); } }, /** * Callback for selection change. */ onSelectionChanged: function () { if (this.selectionChanged()) { this.collection.add({ link: $('.WazeControlPermalink a').prop('href') }); } }, /** * Callback for track history checkbox change. */ onTrackHistoryChange: function (e) { var track = this.$trackHistory.prop('checked'); W.map.events.unregister('moveend', this, this.onMapMove); if (track) { W.map.events.register('moveend', this, this.onMapMove); } this.saveOption('trackHistory', track); }, /** * Callback for track selections checkbox change. */ onTrackSelectionsChange: function (e) { var track = this.$trackSelections.prop('checked'); W.selectionManager.events.unregister('selectionchanged', this, this.onSelectionChanged); if (track) { W.selectionManager.events.register('selectionchanged', this, this.onSelectionChanged); } this.saveOption('trackSelections', track); }, /** * Saves app options to localStorage. */ saveOption: function (key, value) { if (localStorage === void 0) { return; } this.options[key] = value; localStorage.pljOptions = JSON.stringify(this.options); }, /** * Tracks selection changes to determine if the same object is * selected consecutively. */ selectionChanged: function () { var lastSelection, newSelection, selectionChanged; var getSelectedItem = function () { return W.selectionManager.hasSelectedItems() && W.selectionManager.selectedItems[0]; }; var compareSelection = function () { newSelection = getSelectedItem(); if (W.selectionManager.hasSelectedItems() && lastSelection !== newSelection) { lastSelection = newSelection; selectionChanged = true; } else { selectionChanged = false; } }; lastSelection = getSelectedItem(); W.selectionManager.events.register('selectionchanged', this, compareSelection); return function () { return selectionChanged; }; } () }); /** * Class for displaying link data in a table. */ LinkView = Backbone.View.extend({ tagName: 'tr', tdCss: { 'padding': '5px', 'border': '1px solid gray', 'border-right': 'none', 'cursor': 'pointer' }, linkInfoCss: { 'margin': 0 }, linkRemoveCss: { 'padding': '5px 5px 5px 10px', 'border': '1px solid gray', 'border-left': 'none', 'text-align': 'center', 'font-weight': 'bold' }, template: function () { var $nameCell = $('<td/>'). css(this.tdCss).addClass('plj-link'), $deleteCell = $('<td/>'). css(this.linkRemoveCss). addClass('plj-remove-link'). append($('<a/>').text('X')), attributes = this.model.attributes, lonLat, objectsText = []; if (attributes.lonLat) { lonLat = attributes.lonLat.clone(); lonLat.transform(W.map.getProjectionObject(), W.model.segments.projection); } objectsText.push('<b>Lon:</b> ' + (lonLat ? lonLat.lon.toFixed(3) + '\xB0' : 'None') + ' <b>Lat:</b> ' + (lonLat ? lonLat.lat.toFixed(3) + '\xB0' : 'None') + ' <b>Zoom:</b> ' + (attributes.zoom ? attributes.zoom : 'None')); if (this.model.hasItems()) { if (attributes.updateRequest) { objectsText.push('<b>Update Request:</b> ' + attributes.updateRequest); } if (attributes.mapProblem) { objectsText.push('<b>Map Problem:</b> ' + attributes.mapProblem); } if (attributes.segments) { objectsText.push('<b>Segments:</b> ' + attributes.segments.join(', ')); } if (attributes.nodes) { objectsText.push('<b>Nodes:</b> ' + attributes.nodes.join(', ')); } if (attributes.venues) { objectsText.push('<b>Places:</b> ' + attributes.venues.join(', ')); } } _.each(objectsText, function (text) { $nameCell.append( $('<p/>').css(this.linkInfoCss).html(text)); }, this); return [$nameCell, $deleteCell]; }, events: { 'click .plj-link': 'onLinkClicked', 'click .plj-remove-link': 'onRemoveClicked' }, initialize: function () { this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'destroy', this.remove); }, render: function () { var cells = this.template(); this.$nameCell = cells[0]; this.$deleteCell = cells[1]; this.$el.empty(); this.$el.append(this.$nameCell).append(this.$deleteCell); return this; }, /** * Callback for clicking a link. */ onLinkClicked: function (e) { var forceMove = e && e.ctrlKey; W.map.events.unregister('moveend', app, app.onMapMove); this.model.open(forceMove); app.onTrackHistoryChange(); }, /** * Callback for removing a link. */ onRemoveClicked: function (e) { this.model.destroy(); } }); /** * View for text input box and buttons for manual PL input. */ JumpView = Backbone.View.extend({ collection: null, tagName: 'div', template: function () { return '<input type="text" id="pljumpinput" placeholder="WME PL Jump"></input><button id="pljumpbutton" style="margin-left: 3px">Select</button><button id="pljumpbutton-move" style="margin-left: 3px">Select & Jump</button>'; }, events: { 'click #pljumpbutton': 'onJumpClick', 'click #pljumpbutton-move': 'onJumpMoveClick', 'keyup #pljumpinput': 'onInputChanged' }, initialize: function () { this.render(); this.onInputChanged(); }, render: function () { this.$el.html(this.template()); this.$el.css({ 'float': 'right', 'margin': function () { return wLib.isBetaEditor ? '4px 0 0 0' : '15px'; }() }); this.input = this.$el.find('#pljumpinput'); this.jumpButton = this.$el.find('#pljumpbutton'); this.jumpMoveButton = this.$el.find('#pljumpbutton-move'); this.$el.insertAfter($('#edit-buttons')); return this; }, /** * Callback for clicking the jump button. */ onJumpClick: function (e, forceMove) { var linkText = this.input.val(), newLink; if (linkText) { newLink = this.collection.add({ link: linkText }); newLink.open(forceMove); } this.input.val(''); this.onInputChanged(); }, /** * Callback for clicking the jump and move button. */ onJumpMoveClick: function (e) { this.onJumpClick(e, true); }, /** * Callback for keyup in the input box. * Disables the buttons if the textbox is empty. * If the key is 'enter', triggers the jump button click. */ onInputChanged: function (e) { /* this.jumpButton.prop('disabled', this.input.val() === '' ? true : false); this.jumpMoveButton.prop('disabled', this.input.val() === '' ? true : false); */ if (e && e.keyCode === 13) { this.onJumpClick(); } } }); } bootstrap(); } ());