wayfarer/wayfarer-planner.user.js
2024-06-15 14:54:56 +00:00

1676 lines
56 KiB
JavaScript

// ==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) =>
'<option value="' +
id +
'"' +
(id === status ? ' selected="selected"' : '') +
'>' +
mapLayers[id].title +
'</option>'
)
.join('')
let coordinates = `<input name="lat" type="hidden" value="${lat}">
<input name="lng" type="hidden" value="${lng}">`
if (settings.enableCoordinatesEdit) {
coordinates = `<label>Latitude
<input name="lat" type="text" autocomplete="off" value="${lat}">
</label>
<label>Longitude
<input name="lng" type="text" autocomplete="off" value="${lng}">
</label>`
}
let image = ''
let largeImageUrl = imageUrl
if (imageUrl.includes('googleusercontent')) {
largeImageUrl = largeImageUrl.replace(/(=.*)?$/, '=s0')
imageUrl = imageUrl.replace(/(=.*)?$/, '=s200')
}
if (
imageUrl !== '' &&
imageUrl !== undefined &&
settings.enableImagePreview
) {
image = `<a href="${largeImageUrl}" target="_blank" class="imagePreviewContainer"><img class="imagePreview loading" src="${imageUrl}"></a>`
}
let formContent = `<div class="wayfarer-planner-popup"><form id="submit-to-wayfarer">
<label>Status
<select name="status">${options}</select>
</label>
<label>Title
<input name="title" type="text" autocomplete="off" placeholder="Title (required)" required value="${title}">
</label>
<label>Description
<input name="description" type="text" autocomplete="off" placeholder="Description" value="${description}">
</label>
${image}
<div class='wayfarer-expander' title='Click to expand additional fields'>»</div>
<div class='wayfarer-extraData'>
${coordinates}
<label>Submitter
<input name="submitter" type="text" value="${nickname}" disabled>
</label>
<label>Submitted date
<input name="submitteddate" type="text" autocomplete="off" placeholder="dd-mm-jjjj" value="${submitteddate}">
</label>
<label>Image URL
<input name="candidateimageurl" type="text" autocomplete="off" placeholder="http://?.googleusercontent.com/***" value="${imageUrl}">
</label>
</div>
<input name="id" type="hidden" value="${id}">
<input name="nickname" type="hidden" value="${window.PLAYER.nickname}">
<button type="submit" id='wayfarer-submit'>Send</button>
</form>`
if (id !== '') {
formContent +=
'<a style="padding:4px; display: inline-block;" id="deletePortalCandidate">Delete 🗑️</a>'
}
if (
imageUrl !== '' &&
imageUrl !== undefined &&
!settings.enableImagePreview
) {
formContent +=
' <a href="' +
largeImageUrl +
'" style="padding:4px; float:right;" target="_blank">Image</a>'
}
const align =
id !== ''
? 'float: right'
: 'box-sizing: border-box; text-align: right; display: inline-block; width: 100%'
formContent += ` <a href="https://www.google.com/maps?layer=c&cbll=${lat},${lng}" style="padding:4px; ${align};" target="_blank">Street View</a>`
formpopup.setContent(formContent + '</div>')
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 = `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" baseProfile="full" viewBox="0 0 25 41" width="25" height="41" ${markerClassName}>
<path d="M19.4,3.1c-3.3-3.3-6.1-3.3-6.9-3.1c-0.6,0-3.7,0-6.9,3.1c-4,4-1.3,9.4-1.3,9.4s5.6,14.6,6.3,16.3c0.6,1.2,1.3,1.5,1.7,1.5c0,0,0,0,0.2,0h0.2c0.4,0,1.2-0.4,1.7-1.5c0.8-1.7,6.3-16.3,6.3-16.3S23.5,7.2,19.4,3.1z M13.1,12.4c-2.3,0.4-4.4-1.5-4-4c0.2-1.3,1.3-2.5,2.9-2.9c2.3-0.4,4.4,1.5,4,4C15.6,11,14.4,12.2,13.1,12.4z" fill="%COLOR%" stroke="#fff"/>
<path d="M12.5,34.1c1.9,0,3.5,1.5,3.5,3.5c0,1.9-1.5,3.5-3.5,3.5S9,39.5,9,37.5c0-1.2,0.6-2.2,1.5-2.9 C11.1,34.3,11.8,34.1,12.5,34.1z" fill="%COLOR%" stroke="#fff"/>
</svg>`
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 += `<p class="marker-colors">
<div class="options-row">
<div class="options-label-col" style="width: ${firstColumnWidth};">
<div class="label-title">${markerDetails.title}:
</div>
</div>
<div class="options-marker-col">
<div id='marker.${markerId}.color' class="marker-icon-container">
${getGenericMarkerSvg(
colorValue,
`class = "marker-icon" id = "marker.${markerId}.svg"`
)}
<input type="color" class="marker-color-input" id="marker.${markerId}" value="${colorValue}">
</div>
</div>
<div>
<input type="color" class="options-color-input-box" id="marker.${markerId}.colorPicker" value="${colorValue}">
</div>`
if (!hideCheckboxes) {
html += '<div class="options-checkbox-col">'
if (settings.showRadius) {
html += `<div class="options-radius-row">
<input type='checkbox' id='${markerId}.submitRadius' ${
submitRadius ? 'checked' : ''
} value=true>
<span class='radius-label'> Submit Radius</span>
</div>`
}
if (settings.showInteractionRadius) {
html += `<div class="options-radius-row">
<input type='checkbox' id='${markerId}.interactRadius' ${
interactRadius ? 'checked' : ''
} value=true>
<span class='radius-label'> Interact Radius</span>
</div>`
}
html += '</div>'
}
html += '</div></p>'
}
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: <input type='color' id='idColor' value='colorValue'>
Opacity: <select id='idOpacity'>
<option value='0'>0.0</option>
<option value='0.1'>0.1</option>
<option value='0.2'>0.2</option>
<option value='0.3'>0.3</option>
<option value='0.4'>0.4</option>
<option value='0.5'>0.5</option>
<option value='0.6'>0.6</option>
<option value='0.7'>0.7</option>
<option value='0.8'>0.8</option>
<option value='0.9'>0.9</option>
<option value='1'>1.0</option>
</select>`
// 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 += `<p class='planner-colors'>${optionTitle[i]}<br>
${optionHTML
.replace('idColor', `${optionSetting[i]}Color`)
.replace('colorValue', colorValue)
.replace('idOpacity', `${optionSetting[i]}Opacity`)
.replace(
`value='${opacityValue}'`,
`value='${opacityValue}' selected`
)}
</p>`
}
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 = `<p><label for="txtScriptUrl">Url for the script</label><br><input type="url" id="txtScriptUrl" spellcheck="false" placeholder="https://script.google.com/macros/***/exec"></p>
<p><a class='wayfarer-refresh'>Update candidate data</a></p>
<p><input type="checkbox" id="chkShowTitles"><label for="chkShowTitles">Show titles</label></p>
<p><input type="checkbox" id="chkShowRadius"><label for="chkShowRadius">Show submit radius</label></p>
<p><input type="checkbox" id="chkShowInteractRadius"><label for="chkShowInteractRadius">Show interaction radius</label></p>
<p><input type="checkbox" id="chkShowVotingProximity"><label for="chkShowVotingProximity">Show voting proximity</label></p>
<p><input type="checkbox" id="chkEnableDraggingMarkers"><label for="chkEnableDraggingMarkers">Enable Dragging Markers</label></p>
<p><input type="checkbox" id="chkEnableCoordinatesEdit"><label for="chkEnableCoordinatesEdit">Enable Coordinates Edit</label></p>
<p><input type="checkbox" id="chkEnableImagePreview"><label for="chkEnableImagePreview">Enable Image Preview</label></p>
<p><a id='plannerEditMapFeatures'>Customise Map Visuals</a></p>
<p><a id='plannerEditMarkerColors'>Customise Marker Appearance</a></p>
`
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()
$('<style>')
.prop('type', 'text/css')
.html(
`
.wayfarer-planner-popup {
width:200px;
}
.wayfarer-planner-popup a {
color: #ffce00;
}
.wayfarer-planner-name {
font-size: 12px;
font-weight: bold;
color: gold;
opacity: 0.7;
text-align: center;
text-shadow: -1px -1px #000, 1px -1px #000, -1px 1px #000, 1px 1px #000, 0 0 2px #000;
pointer-events: none;
}
#txtScriptUrl {
width: 100%;
}
.wayfarer-planner__disabled {
opacity: 0.8;
pointer-events: none;
}
#submit-to-wayfarer {
position: relative;
}
#submit-to-wayfarer input,
#submit-to-wayfarer select {
width: 100%;
}
#submit-to-wayfarer input {
color: #CCC;
}
#submit-to-wayfarer label {
margin-top: 5px;
display: block;
color: #fff;
}
#wayfarer-submit {
height: 30px;
margin-top: 10px;
width: 100%;
}
.wayfarer-expander {
cursor: pointer;
transform: rotate(90deg) translate(-1px, 1px);
transition: transform .2s ease-out 0s;
position: absolute;
right: 0;
}
.wayfarer-extraData {
max-height: 0;
overflow: hidden;
margin-top: 1em;
}
.wayfarer__expanded .wayfarer-expander {
transform: rotate(270deg) translate(1px, -3px);
}
.wayfarer__expanded .wayfarer-extraData {
max-height: none;
margin-top: 0em;
}
.toggle-create-waypoints{
box-shadow: 0 0 5px;
cursor:pointer;
font-weight: bold;
color: #000!important;
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
border-radius: 4px;
border-bottom: none;
}
.toggle-create-waypoints:hover{
text-decoration:none;
}
.toggle-create-waypoints.active{
background-color:#ffce00;
}
#submit-to-wayfarer .imagePreviewContainer{
display:block;
margin-top:5px;
text-align:center;
}
#submit-to-wayfarer .imagePreview{
max-width:100%;
max-height:150px;
}
.options-row {
display: flex;
align-items: center;
}
.options-label-col {
width: 70px;
text-align: right;
margin-right: 4px;
}
.options-marker-col {
width: 30px;
text-align: center;
position: relative;
}
.marker-color-input {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1; /* Higher than the z-index of the marker SVG */
opacity: 0;
}
.options-marker-col .marker-icon {
z-index: 0; /* Lower than the z-index of the color input */
}
.options-checkbox-col {
display: flex;
flex-direction: column;
}
.options-radius-row {
align-items: center;
display: flex;
}
.radius-label {
flex: 1;
text-align: left;
}
.options-color-input-box {
width: 30px;
height: 30px;
margin-left: 12px;
margin-right: 12px;
}
.loading {
visibility: hidden;
height:150px;
}
`
)
.appendTo('head')
$('body').on('submit', '#submit-to-wayfarer', function (e) {
e.preventDefault()
map.closePopup()
$.ajax({
url: settings.scriptURL,
type: 'POST',
data: new FormData(e.currentTarget),
processData: false,
contentType: false,
success: function (data, status, header) {
drawMarker(data)
let markerAlreadyExists = false
for (let i = 0; i < markercollection.length; i++) {
if (markercollection[i].id === data.id) {
Object.assign(markercollection[i], data)
markerAlreadyExists = true
break
}
}
if (!markerAlreadyExists) {
markercollection.push(data)
}
if (editmarker != null) {
map.removeLayer(editmarker)
editmarker = null
}
},
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.\r\nVerify that you're using the right URL and that you don't use any extension that blocks access to google."
)
}
})
})
map.on('click', onMapClick)
const toolbox = document.getElementById('toolbox')
const toolboxLink = document.createElement('a')
toolboxLink.textContent = 'Wayfarer'
toolboxLink.title = 'Settings for Wayfarer Planner'
toolboxLink.addEventListener('click', showDialog)
toolbox.appendChild(toolboxLink)
if (settings.scriptURL) {
getStoredData()
} else {
showDialog()
}
L.Control.CreatePoints = L.Control.extend({
onAdd: function (map) {
const button = L.DomUtil.create('a')
button.classList.add('toggle-create-waypoints')
if (!settings.scriptURL) {
button.style.display = 'none'
}
button.href = '#'
button.innerHTML = 'P+'
return button
},
onRemove: function (map) {
// Nothing to do here
}
})
L.control.createpoints = function (opts) {
return new L.Control.CreatePoints(opts)
}
L.control.createpoints({ position: 'topleft' }).addTo(map)
$('.toggle-create-waypoints').on('click', function (e) {
e.preventDefault()
e.stopPropagation()
$(this).toggleClass('active')
isPlacingMarkers = !isPlacingMarkers
if (!isPlacingMarkers && editmarker != null) {
map.closePopup()
map.removeLayer(editmarker)
editmarker = null
}
})
}
/** S2 Geometry functions
S2 extracted from Regions Plugin
https:static.iitc.me/build/release/plugins/regions.user.js
*/
const d2r = Math.PI / 180.0
const r2d = 180.0 / Math.PI
const S2 = {}
function LatLngToXYZ(latLng) {
const phi = latLng.lat * d2r
const theta = latLng.lng * d2r
const cosphi = Math.cos(phi)
return [
Math.cos(theta) * cosphi,
Math.sin(theta) * cosphi,
Math.sin(phi)
]
}
function XYZToLatLng(xyz) {
const lat = Math.atan2(
xyz[2],
Math.sqrt(xyz[0] * xyz[0] + xyz[1] * xyz[1])
)
const lng = Math.atan2(xyz[1], xyz[0])
return { lat: lat * r2d, lng: lng * r2d }
}
function largestAbsComponent(xyz) {
const temp = [Math.abs(xyz[0]), Math.abs(xyz[1]), Math.abs(xyz[2])]
if (temp[0] > temp[1]) {
if (temp[0] > temp[2]) {
return 0
}
return 2
}
if (temp[1] > temp[2]) {
return 1
}
return 2
}
function faceXYZToUV(face, xyz) {
let u, v
switch (face) {
case 0:
u = xyz[1] / xyz[0]
v = xyz[2] / xyz[0]
break
case 1:
u = -xyz[0] / xyz[1]
v = xyz[2] / xyz[1]
break
case 2:
u = -xyz[0] / xyz[2]
v = -xyz[1] / xyz[2]
break
case 3:
u = xyz[2] / xyz[0]
v = xyz[1] / xyz[0]
break
case 4:
u = xyz[2] / xyz[1]
v = -xyz[0] / xyz[1]
break
case 5:
u = -xyz[1] / xyz[2]
v = -xyz[0] / xyz[2]
break
default:
throw { error: 'Invalid face' }
}
return [u, v]
}
function XYZToFaceUV(xyz) {
let face = largestAbsComponent(xyz)
if (xyz[face] < 0) {
face += 3
}
const uv = faceXYZToUV(face, xyz)
return [face, uv]
}
function FaceUVToXYZ(face, uv) {
const u = uv[0]
const v = uv[1]
switch (face) {
case 0:
return [1, u, v]
case 1:
return [-u, 1, v]
case 2:
return [-u, -v, 1]
case 3:
return [-1, -v, -u]
case 4:
return [v, -1, -u]
case 5:
return [v, u, -1]
default:
throw { error: 'Invalid face' }
}
}
function STToUV(st) {
const singleSTtoUV = function (st) {
if (st >= 0.5) {
return (1 / 3.0) * (4 * st * st - 1)
}
return (1 / 3.0) * (1 - 4 * (1 - st) * (1 - st))
}
return [singleSTtoUV(st[0]), singleSTtoUV(st[1])]
}
function UVToST(uv) {
const singleUVtoST = function (uv) {
if (uv >= 0) {
return 0.5 * Math.sqrt(1 + 3 * uv)
}
return 1 - 0.5 * Math.sqrt(1 - 3 * uv)
}
return [singleUVtoST(uv[0]), singleUVtoST(uv[1])]
}
function STToIJ(st, order) {
const maxSize = 1 << order
const singleSTtoIJ = function (st) {
const ij = Math.floor(st * maxSize)
return Math.max(0, Math.min(maxSize - 1, ij))
}
return [singleSTtoIJ(st[0]), singleSTtoIJ(st[1])]
}
function IJToST(ij, order, offsets) {
const maxSize = 1 << order
return [(ij[0] + offsets[0]) / maxSize, (ij[1] + offsets[1]) / maxSize]
}
// S2Cell class
S2.S2Cell = function () {}
// static method to construct
S2.S2Cell.FromLatLng = function (latLng, level) {
const xyz = LatLngToXYZ(latLng)
const faceuv = XYZToFaceUV(xyz)
const st = UVToST(faceuv[1])
const ij = STToIJ(st, level)
return S2.S2Cell.FromFaceIJ(faceuv[0], ij, level)
}
S2.S2Cell.FromFaceIJ = function (face, ij, level) {
const cell = new S2.S2Cell()
cell.face = face
cell.ij = ij
cell.level = level
return cell
}
S2.S2Cell.prototype.toString = function () {
return (
'F' +
this.face +
'ij[' +
this.ij[0] +
',' +
this.ij[1] +
']@' +
this.level
)
}
S2.S2Cell.prototype.getLatLng = function () {
const st = IJToST(this.ij, this.level, [0.5, 0.5])
const uv = STToUV(st)
const xyz = FaceUVToXYZ(this.face, uv)
return XYZToLatLng(xyz)
}
S2.S2Cell.prototype.getCornerLatLngs = function () {
const offsets = [
[0.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
[1.0, 0.0]
]
return offsets.map((offset) => {
const st = IJToST(this.ij, this.level, offset)
const uv = STToUV(st)
const xyz = FaceUVToXYZ(this.face, uv)
return XYZToLatLng(xyz)
})
}
S2.S2Cell.prototype.getSurrounding = function (deltas) {
const fromFaceIJWrap = function (face, ij, level) {
const maxSize = 1 << level
if (
ij[0] >= 0 &&
ij[1] >= 0 &&
ij[0] < maxSize &&
ij[1] < maxSize
) {
// no wrapping out of bounds
return S2.S2Cell.FromFaceIJ(face, ij, level)
}
// the new i,j are out of range.
// with the assumption that they're only a little past the borders we can just take the points as
// just beyond the cube face, project to XYZ, then re-create FaceUV from the XYZ vector
let st = IJToST(ij, level, [0.5, 0.5])
let uv = STToUV(st)
const xyz = FaceUVToXYZ(face, uv)
const faceuv = XYZToFaceUV(xyz)
face = faceuv[0]
uv = faceuv[1]
st = UVToST(uv)
ij = STToIJ(st, level)
return S2.S2Cell.FromFaceIJ(face, ij, level)
}
const face = this.face
const i = this.ij[0]
const j = this.ij[1]
const level = this.level
if (!deltas) {
deltas = [
{ a: -1, b: 0 },
{ a: 0, b: -1 },
{ a: 1, b: 0 },
{ a: 0, b: 1 },
{ a: -1, b: -1 },
{ a: 1, b: 1 },
{ a: -1, b: 1 },
{ a: 1, b: -1 }
]
}
return deltas.map(function (values) {
return fromFaceIJWrap(face, [i + values.a, j + values.b], level)
})
}
// PLUGIN END //////////////////////////////////////////////////////////
setup.info = pluginInfo // add the script info data to the function as a property
// if IITC has already booted, immediately run the 'setup' function
if (window.iitcLoaded) {
setup()
} else {
if (!window.bootPlugins) {
window.bootPlugins = []
}
window.bootPlugins.push(setup)
}
}
// wrapper end
;(function () {
const pluginInfo = {}
if (typeof GM_info !== 'undefined' && GM_info && GM_info.script) {
pluginInfo.script = {
version: GM_info.script.version,
name: GM_info.script.name,
description: GM_info.script.description
}
}
// Greasemonkey. It will be quite hard to debug
if (
typeof unsafeWindow !== 'undefined' ||
typeof GM_info === 'undefined' ||
GM_info.scriptHandler !== 'Tampermonkey'
) {
// inject code into site context
const script = document.createElement('script')
script.appendChild(
document.createTextNode(
'(' + wrapper + ')(' + JSON.stringify(pluginInfo) + ');'
)
)
;(
document.body ||
document.head ||
document.documentElement
).appendChild(script)
} else {
// Tampermonkey, run code directly
wrapper(pluginInfo)
}
})()