// ==UserScript== // @id wayfarer-planner@NvlblNm // @name IITC plugin: Wayfarer Planner // @category Layer // @version 1.181 // @namespace https://gitlab.com/NvlblNm/wayfarer/ // @downloadURL https://gitlab.com/NvlblNm/wayfarer/raw/master/wayfarer-planner.user.js // @updateURL https://gitlab.com/NvlblNm/wayfarer/raw/master/wayfarer-planner.user.js // @homepageURL https://gitlab.com/NvlblNm/wayfarer/ // @description Place markers on the map for your candidates in Wayfarer. // @match https://intel.ingress.com/* // @grant none // ==/UserScript== /* forked from https://github.com/Wintervorst/iitc/raw/master/plugins/totalrecon/ */ /* eslint-env es6 */ /* eslint no-var: "error" */ /* globals L, map */ /* globals GM_info, $, dialog */ function wrapper(pluginInfo) { // eslint-disable-line no-extra-semi 'use strict' // PLUGIN START /////////////////////////////////////////////////////// let editmarker = null let isPlacingMarkers = false let markercollection = [] let plottedmarkers = {} let plottedtitles = {} let plottedsubmitrange = {} let plottedinteractrange = {} let plottedcells = {} // Define the layers created by the plugin, one for each marker status const mapLayers = { potential: { color: 'grey', title: 'Potential' }, held: { color: 'yellow', title: 'On hold' }, submitted: { color: 'orange', title: 'Submitted' }, voting: { color: 'brown', title: 'Voting' }, NIANTIC_REVIEW: { color: 'pink', title: 'Niantic Review' }, live: { color: 'green', title: 'Accepted' }, rejected: { color: 'red', title: 'Rejected' }, appealed: { color: 'black', title: 'Appealed' }, potentialedit: { color: 'cornflowerblue', title: 'Potential location edit' }, sentedit: { color: 'purple', title: 'Sent location edit' } } const defaultSettings = { showTitles: true, showRadius: false, showInteractionRadius: false, showVotingProximity: false, scriptURL: '', disableDraggingMarkers: false, enableCoordinatesEdit: true, enableImagePreview: true, // Default settings for map displays. // Colors are in hexadecimal for compatibiltiy with color picker submitRadiusColor: '#000000', submitRadiusOpacity: 1.0, submitRadiusFillColor: '#808080', submitRadiusFillOpacity: 0.4, interactRadiusColor: '#808080', interactRadiusOpacity: 1.0, interactRadiusFillColor: '#000000', interactRadiusFillOpacity: 0.0, votingProximityColor: '#000000', votingProximityOpacity: 0.5, votingProximityFillColor: '#FFA500', votingProximityFillOpacity: 0.3, // Creates arrays containing the marker types and radius settings. // This prevents needing code for checking whether marker arrays exist throughout. // Color is not included in settings by default unless the user changes color. markers: { potential: { submitRadius: true, interactRadius: true }, held: { submitRadius: true, interactRadius: true }, submitted: { submitRadius: true, interactRadius: true }, voting: { submitRadius: true, interactRadius: true }, NIANTIC_REVIEW: { submitRadius: true, interactRadius: true }, live: { submitRadius: true, interactRadius: true }, rejected: { submitRadius: true, interactRadius: true }, appealed: { submitRadius: true, interactRadius: true }, potentialedit: { submitRadius: true, interactRadius: true }, sentedit: { submitRadius: true, interactRadius: true } } } let settings = defaultSettings function saveSettings() { localStorage.wayfarer_planner_settings = JSON.stringify(settings) } function loadSettings() { const tmp = localStorage.wayfarer_planner_settings if (!tmp) { upgradeSettings() return } try { settings = Object.assign({}, settings, JSON.parse(tmp)) } catch (e) { // eslint-disable-line no-empty } } // importing from totalrecon_settings will be removed after a little while function upgradeSettings() { const tmp = localStorage.totalrecon_settings if (!tmp) { return } try { settings = JSON.parse(tmp) } catch (e) { // eslint-disable-line no-empty } saveSettings() localStorage.removeItem('totalrecon_settings') } function getStoredData() { const url = settings.scriptURL if (!url) { markercollection = [] drawMarkers() return } $.ajax({ url, type: 'GET', dataType: 'text', success: function (data, status, header) { try { markercollection = JSON.parse(data) } catch (e) { console.log( 'Wayfarer Planner. Exception parsing response: ', e ) // eslint-disable-line no-console alert('Wayfarer Planner. Exception parsing response.') return } processCustomMarkers() initializeLayers() drawMarkers() }, error: function (x, y, z) { console.log('Wayfarer Planner. Error message: ', x, y, z) // eslint-disable-line no-console alert( "Wayfarer Planner. Failed to retrieve data from the scriptURL.\r\nVerify that you're using the right URL and that you don't use any extension that blocks access to google." ) } }) } function processCustomMarkers() { // Add any unexpected marker types to mapLayers const mapLayersSet = new Set(Object.keys(mapLayers)) const newMarkers = markercollection.filter( (marker) => !mapLayersSet.has(marker.status) ) for (const marker of newMarkers) { const markerStatus = marker.status mapLayers[markerStatus] = { title: markerStatus.charAt(0).toUpperCase() + markerStatus.slice(1) } } // Define a list of default colors for new markers. const colorList = [ '#27AE60', '#73C6B6', '#AEB6BF', '#EDBB99', '#AF601A', '#CB4335', '#F1948A', '#2874A6', '#D6EAF8', '#239B56', '#909497', '#FAE5D3', '#85C1E9', '#9B59B6', '#E67E22', '#2980B9', '#F2F3F4', '#F1C40F', '#BB8FCE', '#FAD7A0', '#C0392B', '#F6DDCC', '#1ABC9C', '#117A65', '#283747', '#B7950B', '#6C3483', '#D0ECE7', '#82E0AA' ] let colorListIndex = 0 for (const markerId in mapLayers) { // Add custom markers to settings if they weren't already in there. if (!settings.markers[markerId]) { settings.markers[markerId] = { submitRadius: true, interactRadius: true } } // Assign a default color to each custom marker. if (!mapLayers[markerId].color) { if (colorListIndex === colorList.length) { colorListIndex = 0 } mapLayers[markerId].color = colorList[colorListIndex] colorListIndex++ } // Overwrite default colors with saved color settings if they exist. mapLayers[markerId].color = settings.markers[markerId].color || mapLayers[markerId].color } } function initializeLayers() { Object.values(mapLayers).forEach((data) => { if (!data.initialized) { const layer = new L.featureGroup() data.layer = layer window.addLayerGroup('Wayfarer - ' + data.title, layer, true) layer.on('click', (e) => { markerClicked(e) }) data.initialized = true } }) } function drawMarker(candidate) { if ( candidate !== undefined && candidate.lat !== '' && candidate.lng !== '' ) { addMarkerToLayer(candidate) addTitleToLayer(candidate) addCircleToLayer(candidate) addVotingProximity(candidate) } } function addCircleToLayer(candidate) { if ( settings.showInteractionRadius && settings.markers[candidate.status].interactRadius ) { const latlng = L.latLng(candidate.lat, candidate.lng) const circleOptions = { color: settings.interactRadiusColor, opacity: settings.interactRadiusOpacity, fillColor: settings.interactRadiusFillColor, fillOpacity: settings.interactRadiusFillOpacity, weight: 1, clickable: false, interactive: false } const range = 80 const circle = new L.Circle(latlng, range, circleOptions) const existingMarker = plottedmarkers[candidate.id] existingMarker.layer.addLayer(circle) plottedinteractrange[candidate.id] = circle } // Draw the 20 metre submit radius if ( settings.showRadius && settings.markers[candidate.status].submitRadius ) { const latlng = L.latLng(candidate.lat, candidate.lng) const circleOptions = { color: settings.submitRadiusColor, opacity: settings.submitRadiusOpacity, fillColor: settings.submitRadiusFillColor, fillOpacity: settings.submitRadiusFillOpacity, weight: 1, clickable: false, interactive: false } const range = 20 const circle = new L.Circle(latlng, range, circleOptions) const existingMarker = plottedmarkers[candidate.id] existingMarker.layer.addLayer(circle) plottedsubmitrange[candidate.id] = circle } } function removeExistingCircle(guid) { const existingCircle = plottedsubmitrange[guid] if (existingCircle !== undefined) { const existingMarker = plottedmarkers[guid] existingMarker.layer.removeLayer(existingCircle) delete plottedsubmitrange[guid] } const existingInteractCircle = plottedinteractrange[guid] if (existingInteractCircle !== undefined) { const existingMarker = plottedmarkers[guid] existingMarker.layer.removeLayer(existingInteractCircle) delete plottedinteractrange[guid] } } function addTitleToLayer(candidate) { if (settings.showTitles) { const title = candidate.title if (title !== '') { const portalLatLng = L.latLng(candidate.lat, candidate.lng) const titleMarker = L.marker(portalLatLng, { icon: L.divIcon({ className: 'wayfarer-planner-name', iconAnchor: [100, 5], iconSize: [200, 10], html: title }), data: candidate }) const existingMarker = plottedmarkers[candidate.id] existingMarker.layer.addLayer(titleMarker) plottedtitles[candidate.id] = titleMarker } } } function removeExistingTitle(guid) { const existingTitle = plottedtitles[guid] if (existingTitle !== undefined) { const existingMarker = plottedmarkers[guid] existingMarker.layer.removeLayer(existingTitle) delete plottedtitles[guid] } } function addVotingProximity(candidate) { if (settings.showVotingProximity && candidate.status === 'voting') { const cell = S2.S2Cell.FromLatLng( { lat: candidate.lat, lng: candidate.lng }, 17 ) const surrounding = cell.getSurrounding() surrounding.push(cell) for (let i = 0; i < surrounding.length; i++) { const cellId = surrounding[i].toString() if (!plottedcells[cellId]) { plottedcells[cellId] = { candidateIds: [], polygon: null } const vertexes = surrounding[i].getCornerLatLngs() const polygon = L.polygon(vertexes, { color: settings.votingProximityColor, opacity: settings.votingProximityOpacity, fillColor: settings.votingProximityFillColor, fillOpacity: settings.votingProximityFillOpacity, weight: 1 }) plottedcells[cellId].polygon = polygon polygon.addTo(map) } if ( plottedcells[cellId].candidateIds.indexOf(candidate.id) === -1 ) { plottedcells[cellId].candidateIds.push(candidate.id) } } } } function removeExistingVotingProximity(guid) { Object.entries(plottedcells).forEach( ([cellId, { candidateIds, polygon }]) => { plottedcells[cellId].candidateIds = candidateIds.filter( (id) => id !== guid ) if (plottedcells[cellId].candidateIds.length === 0) { map.removeLayer(polygon) delete plottedcells[cellId] } } ) } function removeExistingMarker(guid) { const existingMarker = plottedmarkers[guid] if (existingMarker !== undefined) { existingMarker.layer.removeLayer(existingMarker.marker) removeExistingTitle(guid) removeExistingCircle(guid) removeExistingVotingProximity(guid) } } function addMarkerToLayer(candidate) { removeExistingMarker(candidate.id) const portalLatLng = L.latLng(candidate.lat, candidate.lng) const layerData = mapLayers[candidate.status] const markerColor = layerData.color const markerLayer = layerData.layer let draggable = true if (settings.disableDraggingMarkers) { draggable = false } const marker = createGenericMarker(portalLatLng, markerColor, { title: candidate.title, id: candidate.id, data: candidate, draggable }) marker.on('dragend', function (e) { const data = e.target.options.data const latlng = marker.getLatLng() data.lat = latlng.lat data.lng = latlng.lng drawInputPopop(latlng, data) }) marker.on('dragstart', function (e) { const guid = e.target.options.data.id removeExistingTitle(guid) removeExistingCircle(guid) }) markerLayer.addLayer(marker) plottedmarkers[candidate.id] = { marker, layer: markerLayer } } function clearAllLayers() { Object.values(mapLayers).forEach((data) => data.layer.clearLayers()) Object.values(plottedcells).forEach((data) => map.removeLayer(data.polygon) ) /* clear marker storage */ plottedmarkers = {} plottedtitles = {} plottedsubmitrange = {} plottedinteractrange = {} plottedcells = {} } function drawMarkers() { clearAllLayers() markercollection.forEach(drawMarker) } function onMapClick(e) { if (isPlacingMarkers) { if (editmarker != null) { map.removeLayer(editmarker) } const marker = createGenericMarker(e.latlng, 'pink', { title: 'Place your mark!' }) editmarker = marker marker.addTo(map) drawInputPopop(e.latlng) } } function drawInputPopop(latlng, markerData) { const formpopup = L.popup() let title = '' let description = '' let id = '' let submitteddate = '' let nickname = '' let lat = '' let lng = '' let status = 'potential' let imageUrl = '' if (markerData !== undefined) { id = markerData.id title = markerData.title description = markerData.description submitteddate = markerData.submitteddate nickname = markerData.nickname status = markerData.status imageUrl = markerData.candidateimageurl lat = parseFloat(markerData.lat).toFixed(6) lng = parseFloat(markerData.lng).toFixed(6) } else { lat = latlng.lat.toFixed(6) lng = latlng.lng.toFixed(6) } formpopup.setLatLng(latlng) const options = Object.keys(mapLayers) .map( (id) => '' ) .join('') let coordinates = ` ` if (settings.enableCoordinatesEdit) { coordinates = ` ` } let image = '' let largeImageUrl = imageUrl if (imageUrl.includes('googleusercontent')) { largeImageUrl = largeImageUrl.replace(/(=.*)?$/, '=s0') imageUrl = imageUrl.replace(/(=.*)?$/, '=s200') } if ( imageUrl !== '' && imageUrl !== undefined && settings.enableImagePreview ) { image = `` } let formContent = `
${image}
»
${coordinates}
` if (id !== '') { formContent += 'Delete 🗑️' } if ( imageUrl !== '' && imageUrl !== undefined && !settings.enableImagePreview ) { formContent += ' Image' } const align = id !== '' ? 'float: right' : 'box-sizing: border-box; text-align: right; display: inline-block; width: 100%' formContent += ` Street View` formpopup.setContent(formContent + '
') formpopup.openOn(map) const deleteLink = formpopup._contentNode.querySelector( '#deletePortalCandidate' ) if (deleteLink != null) { deleteLink.addEventListener('click', (e) => confirmDeleteCandidate(e, id) ) } const expander = formpopup._contentNode.querySelector('.wayfarer-expander') expander.addEventListener('click', function () { expander.parentNode.classList.toggle('wayfarer__expanded') }) const previewImageElement = document.querySelector('.loading') previewImageElement.onload = function () { previewImageElement.classList.remove('loading') } } function confirmDeleteCandidate(e, id) { e.preventDefault() if (!confirm('Do you want to remove this candidate?')) { return } const formData = new FormData() formData.append('status', 'delete') formData.append('id', id) $.ajax({ url: settings.scriptURL, type: 'POST', data: formData, processData: false, contentType: false, success: function (data, status, header) { removeExistingMarker(id) for (let i = 0; i < markercollection.length; i++) { if (markercollection[i].id === id) { markercollection.splice(i, 1) break } } map.closePopup() }, error: function (x, y, z) { console.log('Wayfarer Planner. Error message: ', x, y, z) // eslint-disable-line no-console alert('Wayfarer Planner. Failed to send data to the scriptURL') } }) } function markerClicked(event) { // bind data to edit form if (editmarker != null) { map.removeLayer(editmarker) editmarker = null } drawInputPopop(event.layer.getLatLng(), event.layer.options.data) } function getGenericMarkerSvg(color, markerClassName) { const markerTemplate = ` ` return markerTemplate.replace(/%COLOR%/g, color) } function getGenericMarkerIcon(color, className) { return L.divIcon({ iconSize: new L.Point(25, 41), iconAnchor: new L.Point(12, 41), html: getGenericMarkerSvg(color, ''), className: className || 'leaflet-iitc-divicon-generic-marker' }) } function createGenericMarker(ll, color, options) { options = options || {} const markerOpt = $.extend( { icon: getGenericMarkerIcon(color || '#a24ac3') }, options ) return L.marker(ll, markerOpt) } function editMarkerColors() { // Create the HTML code for the marker options. let html = '' const hideCheckboxes = !( settings.showRadius || settings.showInteractionRadius ) const firstColumnWidth = hideCheckboxes ? '100px' : '70px' const boxWidth = hideCheckboxes ? '185px' : '300px' // Through all markers that have been loaded. Generate one row per marker. const keys = Object.keys(mapLayers) for (let i = 0; i < keys.length; i++) { const markerId = keys[i] const markerDetails = mapLayers[markerId] let colorValue = markerDetails.color const submitRadius = settings.markers[markerId].submitRadius const interactRadius = settings.markers[markerId].interactRadius // Make sure that the color is in #rrggbb format for color picker. const ctx = document.createElement('canvas').getContext('2d') ctx.fillStyle = colorValue colorValue = ctx.fillStyle html += `

${markerDetails.title}:
${getGenericMarkerSvg( colorValue, `class = "marker-icon" id = "marker.${markerId}.svg"` )}
` if (!hideCheckboxes) { html += '
' if (settings.showRadius) { html += `
Submit Radius
` } if (settings.showInteractionRadius) { html += `
Interact Radius
` } html += '
' } html += '

' } const container = dialog({ id: 'markerColors', width: boxWidth, html: html, title: 'Planner Marker Customisation' }) const div = container[0] div.addEventListener('change', (event) => { const id = event.target.id const splitId = id.split('.') if (event.target.type === 'checkbox') { // Update the marker radius data and add to the settings const value = event.target.checked settings.markers[splitId[0]][splitId[1]] = value } else { const value = event.target.value const markerId = [splitId[1]] // Update the marker color data on the form document.getElementById( `marker.${markerId}.colorPicker` ).value = value const svg = document.getElementById(`marker.${markerId}.svg`) svg.querySelectorAll('path, circle').forEach( (path) => (path.style.fill = value) ) // Update the marker color data on the map and in the settings mapLayers[markerId].color = value settings.markers[markerId].color = value } saveSettings() drawMarkers() }) } function editMapFeatures() { // Create the HTML for the general map display options. let html = '' const optionSetting = [ 'submitRadius', 'submitRadiusFill', 'interactRadius', 'interactRadiusFill', 'votingProximity', 'votingProximityFill' ] const optionTitle = [ 'Submit Radius Border', 'Submit Radius Fill', 'Interact Radius Border', 'Interact Radius Fill', 'Voting Proximity Border', 'Voting Proximity Fill' ] // HTML template which is used for each row of the display. const optionHTML = `Color: Opacity: ` // Loop through all of the option settings and insert their values into above template. for (let i = 0; i < optionSetting.length; i++) { const colorValue = settings[`${optionSetting[i]}Color`] const opacityValue = settings[`${optionSetting[i]}Opacity`] html += `

${optionTitle[i]}
${optionHTML .replace('idColor', `${optionSetting[i]}Color`) .replace('colorValue', colorValue) .replace('idOpacity', `${optionSetting[i]}Opacity`) .replace( `value='${opacityValue}'`, `value='${opacityValue}' selected` )}

` } const container = dialog({ id: 'plannermMapFeatures', width: '220px', html: html, title: 'Planner Map Customisation' }) const div = container[0] // If changes are made to settings, save the changes and update the map. div.addEventListener('change', (event) => { const id = event.target.id const value = event.target.value settings[id] = value saveSettings() drawMarkers() }) } function showDialog() { if (window.isSmartphone()) { window.show('map') } const html = `


Update candidate data

Customise Map Visuals

Customise Marker Appearance

` const container = dialog({ width: 'auto', html, title: 'Wayfarer Planner', buttons: { OK: function () { const newUrl = txtInput.value if (!txtInput.reportValidity()) { return } if (newUrl !== '') { if ( !newUrl.startsWith( 'https://script.google.com/macros/' ) ) { alert( 'The URL of the script seems to be wrong, please paste the URL provided after "creating the webapp".' ) return } if ( newUrl.includes('echo') || !newUrl.endsWith('exec') ) { alert( 'You must use the short URL provided by "creating the webapp", not the long one after executing the script.' ) return } if (newUrl.includes(' ')) { alert( "Warning, the URL contains at least one space. Check that you've copied it properly." ) return } } if (newUrl !== settings.scriptURL) { settings.scriptURL = newUrl saveSettings() getStoredData() } container.dialog('close') } } }) const div = container[0] const txtInput = div.querySelector('#txtScriptUrl') txtInput.value = settings.scriptURL const linkRefresh = div.querySelector('.wayfarer-refresh') linkRefresh.addEventListener('click', () => { settings.scriptURL = txtInput.value saveSettings() getStoredData() }) const chkShowTitles = div.querySelector('#chkShowTitles') chkShowTitles.checked = settings.showTitles chkShowTitles.addEventListener('change', (e) => { settings.showTitles = chkShowTitles.checked saveSettings() drawMarkers() }) const chkShowRadius = div.querySelector('#chkShowRadius') chkShowRadius.checked = settings.showRadius chkShowRadius.addEventListener('change', (e) => { settings.showRadius = chkShowRadius.checked saveSettings() drawMarkers() }) const chkShowInteractRadius = div.querySelector( '#chkShowInteractRadius' ) chkShowInteractRadius.checked = settings.showInteractionRadius chkShowInteractRadius.addEventListener('change', (e) => { settings.showInteractionRadius = chkShowInteractRadius.checked saveSettings() drawMarkers() }) const chkShowVotingProximity = div.querySelector( '#chkShowVotingProximity' ) chkShowVotingProximity.checked = settings.showVotingProximity chkShowVotingProximity.addEventListener('change', (e) => { settings.showVotingProximity = chkShowVotingProximity.checked saveSettings() drawMarkers() }) const chkEnableDraggingMarkers = div.querySelector( '#chkEnableDraggingMarkers' ) chkEnableDraggingMarkers.checked = !settings.disableDraggingMarkers chkEnableDraggingMarkers.addEventListener('change', (e) => { settings.disableDraggingMarkers = !chkEnableDraggingMarkers.checked saveSettings() drawMarkers() }) const chkEnableCoordinatesEdit = div.querySelector( '#chkEnableCoordinatesEdit' ) chkEnableCoordinatesEdit.checked = settings.enableCoordinatesEdit chkEnableCoordinatesEdit.addEventListener('change', (e) => { settings.enableCoordinatesEdit = chkEnableCoordinatesEdit.checked saveSettings() }) const chkEnableImagePreview = div.querySelector( '#chkEnableImagePreview' ) chkEnableImagePreview.checked = settings.enableImagePreview chkEnableImagePreview.addEventListener('change', (e) => { settings.enableImagePreview = chkEnableImagePreview.checked saveSettings() }) txtInput.addEventListener('input', (e) => { if (txtInput.value) { try { new URL(txtInput.value) // eslint-disable-line no-new if ( txtInput.value.startsWith( 'https://script.google.com/macros/' ) ) { $('.toggle-create-waypoints').show() return } } catch (error) {} } $('.toggle-create-waypoints').hide() }) const plannerEditMapFeatures = div.querySelector( '#plannerEditMapFeatures' ) plannerEditMapFeatures.addEventListener('click', function (e) { editMapFeatures() e.preventDefault() return false }) const plannerEditMarkerColors = div.querySelector( '#plannerEditMarkerColors' ) plannerEditMarkerColors.addEventListener('click', function (e) { editMarkerColors() e.preventDefault() return false }) } // Initialize the plugin const setup = function () { loadSettings() $('