// ==UserScript==
// @author NvlblNm
// @id wayfarer-planner@NvlblNm
// @name Wayfarer Planner
// @category Layer
// @version 1.181
// @namespace https://gitlab.com/NvlblNm/wayfarer/
// @downloadURL https://raw.githubusercontent.com/IITC-CE/Community-plugins/master/dist/NvlblNm/wayfarer-planner.user.js
// @updateURL https://raw.githubusercontent.com/IITC-CE/Community-plugins/master/dist/NvlblNm/wayfarer-planner.meta.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 = `
' } 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`
)}