`;
/** @type{JQuery}*/
const modDialog = window.dialog({
id: 'pNavmodDialog',
title: getString('pNavmodDialogTitle'),
html,
width: 'auto',
height: 'auto',
buttons: {
Skip: {
id: 'btnSkip',
text: getString('btnSkipText'),
click () {
i++;
if (i == changeList.length) {
modDialog.dialog('close');
} else {
poi = changeList[i];
updateUI(modDialog, poi, i);
}
}
}
}
});
let poi = changeList[i];
$('#pNavPoiInfo', modDialog).on('click', function () {
if (window.plugin.pnav.settings.webhookUrl) {
sendMessage(`<@${pNavId}> ${poi.oldType === 'pokestop' ? 'stop' : poi.oldType}-info ${poi.oldName}`);
} else {
const input = $('#copyInput');
input.show();
input.val(`<@${pNavId}> ${poi.oldType === 'pokestop' ? 'stop' : poi.oldType}-info ${poi.oldName}`);
copyfieldvalue('copyInput');
input.hide();
}
});
$('#pNavPoiId', modDialog).on('input', function (e) {
const valid = e.target.validity.valid;
const value = e.target.valueAsNumber;
if (valid && value && value > 0) {
$('#pNavModCommand', modDialog).prop('style', 'margin-top:5px');
$('#pNavModCommand', modDialog).prop('title', getString('pNavModCommandTitleEnabled', {send}));
} else {
$('#pNavModCommand', modDialog).css('color', 'darkgray');
$('#pNavModCommand', modDialog).css('text-decoration', 'none');
$('#pNavModCommand', modDialog).css('border', '1px solid darkgray');
$('#pNavModCommand', modDialog).prop('title', getString('pNavModCommandTitleDisabled', {send}));
}
});
$('#pNavModCommand', modDialog).on('click', function () {
if ($('#pNavPoiId', modDialog).val() && (/^\d*$/).test($('#pNavPoiId', modDialog).val())) {
sendModCommand($('#pNavPoiId', modDialog).val(), poi);
updateDone([poi]);
i++;
if (i == changeList.length) {
modDialog.dialog('close');
} else {
poi = changeList[i];
updateUI(modDialog, poi, i);
}
}
});
$('#address').on('click', () => {
$.ajax(`https://nominatim.openstreetmap.org/reverse?lat=${changeList[i].lat}&lon=${changeList[i].lng}&format=json&addressdetails=0`, {
success: (data) => {
if (data && data.display_name) {
$('#addressdetails').text(data.display_name);
$('#address').prop('hidden', true);
$('#addressdetails').prop('hidden', false);
}
}
});
});
console.log(modDialog);
modDialog.css('width', `${modDialog[0].offsetWidth}px`);
$('#pNavModNrMax', modDialog).text(changeList.length);
updateUI(modDialog, poi, i);
} else {
alert(getString('alertNoModifications'));
}
};
/**
* updates the export state after an edit step.
* @param {editData[]} changeList - list of changes that were made.
*/
function updateDone (changeList) {
const pogoData = localStorage['plugin-pogo'] ? JSON.parse(localStorage['plugin-pogo']) : {};
const pogoGyms = pogoData.gyms ?? {};
const pogoStops = pogoData.pokestops ?? {};
changeList.forEach((change) => {
if (Object.keys(pogoStops).includes(change.guid)) {
pNavData.pokestop[change.guid] = pogoStops[change.guid];
if (Object.keys(pNavData.gym).includes(change.guid)) {
delete pNavData.gym[change.guid];
}
} else if (Object.keys(pogoGyms).includes(change.guid)) {
pNavData.gym[change.guid] = pogoGyms[change.guid];
if (Object.keys(pNavData.pokestop).includes(change.guid)) {
delete pNavData.pokestop[change.guid];
}
} else {
if (Object.keys(pNavData.pokestop).includes(change.guid)) {
delete pNavData.pokestop[change.guid];
} else {
delete pNavData.gym[change.guid];
}
}
});
saveToLocalStorage();
}
function updateUI (dialog, poi, i) {
$('#pNavOldPoiName', dialog).text(poi.oldName);
$('#pNavModNrCur', dialog).text(i + 1);
$('#pNavChangesMade', dialog).empty();
$('#addressdetails').text('')
.prop('hidden', true);
$('#address').prop('hidden', false);
for (const [
key,
value
] of Object.entries(poi.edits)) {
$('#pNavChangesMade', dialog).append(`
${key} => ${value}
`);
}
$('#pNavPoiId', dialog).val('');
$('#pNavModCommand', dialog).css('color', 'darkgray');
$('#pNavModCommand', dialog).css('border', '1px solid darkgray');
$('#pNavModCommand', dialog).css('cursor', 'default');
$('#pNavModCommand', dialog).css('text-decoration', 'none');
$('#pNavModCommand', dialog).prop('title', getString('pNavModCommandTitleDisabled', {send: Boolean(window.plugin.pnav.settings.webhookUrl)}));
}
function sendModCommand (poiId, changes) {
let command = '';
if (changes.edits.type && changes.edits.type === 'none') {
command = `<@${pNavId}> delete poi ${poiId}`;
} else {
command = `<@${pNavId}> update poi ${poiId}`;
for (const [
key,
value
] of Object.entries(changes.edits)) {
command += ` «${key}: ${value}»`;
}
}
if (window.plugin.pnav.settings.webhookUrl) {
sendMessage(command);
} else {
const input = $('#copyInput');
input.show();
input.val(command);
copyfieldvalue('copyInput');
input.hide();
}
}
/**
* Checks for Modifications in PoGoTools Data.
* @return {editData[]} returns a list of edits.
*/
function checkForModifications () {
const pogoData = localStorage['plugin-pogo'] ? JSON.parse(localStorage['plugin-pogo']) : {};
const pogoStops = (pogoData && pogoData.pokestops) ? pogoData.pokestops : {};
const keysStops = Object.keys(pogoStops);
const pogoGyms = pogoData && pogoData.gyms ? pogoData.gyms : {};
const keysGyms = Object.keys(pogoGyms);
let changeList = [];
if (pogoData) {
Object.values(pNavData.pokestop).forEach(function (stop) {
/** @type {editData}*/
let detectedChanges = {edits: {}};
let newData;
if (!keysStops.includes(stop.guid)) {
if (keysGyms.includes(stop.guid)) {
detectedChanges.edits.type = 'gym';
newData = pogoGyms[stop.guid];
if (newData.isEx) {
detectedChanges.edits.ex_eligible = 1;
}
} else {
detectedChanges.edits.type = 'none';
}
} else {
newData = pogoStops[stop.guid];
}
// compare data
if (newData) {
if (newData.name !== stop.name) {
detectedChanges.edits.name = newData.name;
}
// not eqeqeq because sometimes the lat and lng were numbers for me, but most of the time they were strings in Pogo Tools. Maybe there's a bug with that...
if (newData.lat != stop.lat) {
detectedChanges.edits.latitude = newData.lat;
}
// not eqeqeq because sometimes the lat and lng were numbers for me, but most of the time they were strings in Pogo Tools. Maybe there's a bug with that...
if (newData.lng != stop.lng) {
detectedChanges.edits.longitude = newData.lng;
}
}
if (Object.keys(detectedChanges.edits).length > 0) {
detectedChanges.oldName = stop.name;
detectedChanges.oldType = 'pokestop';
detectedChanges.guid = stop.guid;
detectedChanges.lat = stop.lat;
detectedChanges.lng = stop.lng;
changeList.push(detectedChanges);
}
});
Object.values(pNavData.gym).forEach(function (gym) {
/** @type {editData}*/
let detectedChanges = {edits: {}};
let newData;
if (!keysGyms.includes(gym.guid)) {
if (keysStops.includes(gym.guid)) {
detectedChanges.edits.type = 'pokestop';
newData = pogoStops[gym.guid];
} else {
detectedChanges.edits.type = 'none';
}
} else {
newData = pogoGyms[gym.guid];
}
// compare data
if (newData) {
if (newData.name !== gym.name) {
detectedChanges.edits.name = newData.name;
}
// not eqeqeq because sometimes the lat and lng were numbers for me, but most of the time they were strings in Pogo Tools. Maybe there's a bug with that...
if (newData.lat != gym.lat) {
detectedChanges.edits.latitude = newData.lat;
}
// not eqeqeq because sometimes the lat and lng were numbers for me, but most of the time they were strings in Pogo Tools. Maybe there's a bug with that...
if (newData.lng != gym.lng) {
detectedChanges.edits.longitude = newData.lng;
}
if (Boolean(newData.isEx) !== Boolean(gym.isEx)) { // that treats undefined as false, otherwise undefined and false would be unequal, even with != instead of !==.
const newEx = newData.isEx ? newData.isEx : false;
detectedChanges.edits.ex_eligible = newEx ? 1 : 0;
}
}
if (Object.keys(detectedChanges.edits).length > 0) {
detectedChanges.oldName = gym.name;
detectedChanges.oldType = 'gym';
detectedChanges.guid = gym.guid;
detectedChanges.lat = gym.lat;
detectedChanges.lng = gym.lng;
changeList.push(detectedChanges);
}
});
}
return changeList;
}
function saveToLocalStorage () {
localStorage['plugin-pnav-done-pokestop'] = JSON.stringify(pNavData.pokestop);
localStorage['plugin-pnav-done-gym'] = JSON.stringify(pNavData.gym);
}
/**
* Edit Data that lists what edits should be made.
* @typedef {object} editData
* @property {string} oldType - expected pokestop or gym
* @property {string} oldName
* @property {string} guid
* @property {string} lat
* @property {string} lng
* @property {object} edits
* @property {string} [edits.latitude]
* @property {string} [edits.longitude]
* @property {string} [edits.name]
* @property {string} [edits.type] - expected pokestop, gym or none
* @property {number} [edits.ex_eligible]
*/
/**
* data about portals
* @typedef {object} portalData
* @property {string} type
* @property {string} guid
* @property {string} name
* @property {string} lat
* @property {string} lng
* @property {boolean} [isEx]
*/
/**
* data like it is stored in PoGoTools Plugin (mainly portalData without type field).
* @external
* @typedef {object} pogoToolsData
* @property {string} guid
* @property {string} name
* @property {string} lat
* @property {string} lng
* @property {boolean} [isEx]
*/
/**
* Checks if a single Poi has been modified
* @param {portalData} currentData
* @return {editData | null} returns the found changes or null if none were found or a problem occurred.
*/
function checkForSingleModification (currentData) {
/** @type {editData} */
let changes = {edits: {}};
/** @type {portalData} */
let savedData;
if (pNavData.pokestop[currentData.guid]) {
savedData = pNavData.pokestop[currentData.guid];
savedData.type = 'pokestop';
} else if (pNavData.gym[currentData.guid]) {
savedData = pNavData.gym[currentData.guid];
savedData.type = 'gym';
} else {
return null;
}
if (currentData.type !== savedData.type) {
changes.edits.type = currentData.type;
}
if (currentData.lat != savedData.lat) {
changes.edits.latitude = currentData.lat.toString();
}
if (currentData.lng != savedData.lng) {
changes.edits.longitude = currentData.lng.toString();
}
if (currentData.name != savedData.name) {
changes.edits.name = currentData.name;
}
if (Boolean(currentData.isEx) !== Boolean(savedData.isEx)) { // to cope with undefined == false etc.
changes.edits.ex_eligible = currentData.isEx ? 1 : 0;
}
if (Object.keys(changes.edits).length > 0) {
changes.oldName = savedData.name;
changes.oldType = savedData.type;
changes.guid = savedData.guid;
changes.lat = savedData.lat;
changes.lng = savedData.lng;
return changes;
} else {
return null;
}
}
/**
* fetch previous data from local storage and add
* @param {string} type - expected values pokestop or gym
* @return {pogoToolsData[] | null} returns the data to export or null if Pogo Tools Data was not found.
*/
function gatherExportData (type) {
/** @type {pogoToolsData[]}*/
let pogoData = localStorage['plugin-pogo'] ? JSON.parse(localStorage['plugin-pogo']) : {};
if (pogoData[`${type}s`]) {
pogoData = Object.values(pogoData[`${type}s`]);
const doneGuids = (Object.keys(pNavData.pokestop).concat(Object.keys(pNavData.gym)));
const distanceNotCheckable =
window.plugin.pnav.settings.lat === null ||
window.plugin.pnav.settings.lng === null ||
window.plugin.pnav.settings.radius === null;
/** @type {pogoToolsData[]} */
let exportData = pogoData.filter(function (object) {
return (
!doneGuids.includes(object.guid) &&
(distanceNotCheckable ||
checkDistance(object.lat, object.lng, window.plugin.pnav.settings.lat, window.plugin.pnav.settings.lng) <= window.plugin.pnav.settings.radius)
);
});
return exportData;
}
return null;
}
/**
* @param {string} type - the type of locations to export (expected values pokestop or gym)
*/
window.plugin.pnav.bulkExport = function (type) {
if (!window.plugin.pnav.timer) {
let data = gatherExportData(type);
if (!data) {
alert(getString('alertProblemPogoTools'));
return;
}
if (window.plugin.pnav.settings.useBot) {
botExport(data, type); // jump to BotExport immediately before opening the dialog, this is not needed!
return;
}
let i = 0;
window.onbeforeunload = function () {
saveState(data, type, i);
return null;
};
window.plugin.pnav.timer = setInterval(() => {
if (i < data.length && data.length > 0) {
normalExport(data, type, thisDialog, i).then((result) => {
i = result;
});
} else {
saveState(data, type, i);
clearInterval(window.plugin.pnav.timer);
window.plugin.pnav.timer = null;
window.onbeforeunload = null;
}
}, wait);
let dialog = window.dialog({
id: 'bulkExportProgress',
html: `
${getString('exportStateTextExporting')}
`,
width: 'auto',
title: getString('bulkExportProgressTitle'),
buttons: {
OK: {
text: getString('bulkExportProgressButtonText'),
title: getString('bulkExportProgressButtonTitle'),
click () {
saveState(data, type, i);
clearInterval(window.plugin.pnav.timer);
window.plugin.pnav.timer = null;
// eslint-disable-next-line no-underscore-dangle
if (window._current_highlighter === getString('portalHighlighterName')) { // re-validate highlighter if it is enabled.
window.changePortalHighlights(getString('portalHighlighterName'));
}
dialog.dialog('close');
}
}
}
});
let thisDialog = dialog.parent();
$('.ui-button.ui-dialog-titlebar-button-close', thisDialog).on(
'click',
function () {
saveState(data, type, i);
clearInterval(window.plugin.pnav.timer);
window.plugin.pnav.timer = null;
}
);
if (data.length > 0) {
normalExport(data, type, dialog, 0).then((result) => {
i = result;
}); // start immediately instead of waiting 2 seconds.
} else {
updateExportDialog(thisDialog, 0, 0, 0);
}
} else {
console.error('Bulk Export already running!');
}
};
/**
* One Export step when the Companion Discord bot should be used.
* @param {pogoToolsData[]} data all locations that need exporting
* @param {string} type the location type of the given data
*/
function botExport (data, type) {
/** @type {portalData[]} */
let exportdata = [...data];
exportdata.forEach((element) => {
element.type = type; // convert pogoToolsData to portalData
});
let formData = new FormData();
let date = new Date();
formData.append('content', `<@${companionId}> cm`);
formData.append('username', window.plugin.pnav.settings.name);
formData.append('file', new Blob([JSON.stringify(exportdata, null, 2)], {type: 'application/json'}), `creations-${window.plugin.pnav.settings.name}-${date.getUTCFullYear()}-${date.getUTCMonth()}-${date.getUTCDate()}_${date.getUTCHours()}:${date.getUTCMinutes()}.json`);
$.ajax({
method: 'POST',
url: window.plugin.pnav.settings.webhookUrl,
contentType: false,
processData: false,
data: formData,
error (jgXHR, textStatus, errorThrown) {
console.error(`${textStatus} - ${errorThrown}`);
},
success () {
saveState(data, type);
// eslint-disable-next-line no-underscore-dangle
if (window._current_highlighter === getString('portalHighlighterName')) { // re-validate highlighter if it is enabled.
window.changePortalHighlights(getString('portalHighlighterName'));
}
}
});
}
/**
* One Export step when only the WebHook should be used.
* @async
* @param {object} data all locations that need exporting
* @param {string} type the location type of the given data
* @param {HTMLElement} dialog the dialog of the bulk export
* @param {number} i the current index
* @return {number} the new index after the export step (normally old index + 1).
*/
async function normalExport (data, type, dialog, i) {
if (i % 10 == 0) {
saveState(data, type, i); // sometimes save the state in case someone exits IITC Mobile without using the Back Button
}
let entry = data[i];
let lat = entry.lat;
let lng = entry.lng;
// escaping Hyphens in Portal Names
let name = entry.name;
let prefix = `<@${pNavId}> `;
let ex = Boolean(entry.isEx);
let options = ex ? ' "ex_eligible: 1"' : '';
let content = `${prefix}create poi ${type} «${name}» ${lat} ${lng}${options}`;
const params = {
username: window.plugin.pnav.settings.name,
avatar_url: '',
content
};
let success = await fetch(window.plugin.pnav.settings.webhookUrl, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(params)
})
.then((response) => {
if (!response.ok) {
console.error(`HTTP Error: ${response.status} - ${response.statusText}${response.bodyUsed ? `; body: ${response.body}` : ''}`);
}
return response.ok;
})
.catch((error) => {
console.error(error);
return false;
});
if (success) {
updateExportDialog(dialog, i + 1, Object.keys(data).length, (Object.keys(data).length - (i + 1)) * (wait / 1000));
return i + 1; // return the new i (old i + 1)!
} else {
return i;
}
}
/**
* assembles the edit data for the companion bot and sends it.
* @param {editData[]} [changes] - optional list of changes that should be transferred.
*/
function botEdit (changes) {
if (window.plugin.pnav.settings.webhookUrl === null) {
console.error('no Webhook URL present!');
return;
}
if (typeof changes == 'undefined') {
changes = checkForModifications();
}
if (changes.length == 0) {
console.log('nothing to export!');
return;
}
let data = new FormData();
let date = new Date();
data.append('content', `<@${companionId}> e`);
data.append('username', window.plugin.pnav.settings.name);
data.append('file', new Blob([JSON.stringify(changes, null, 2)], {type: 'application/json'}), `edits-${window.plugin.pnav.settings.name}-${date.getUTCFullYear()}-${date.getUTCMonth()}-${date.getUTCDate()}_${date.getUTCHours()}:${date.getUTCMinutes()}.json`);
$.ajax({
method: 'POST',
url: window.plugin.pnav.settings.webhookUrl,
contentType: false,
processData: false,
data,
error (jgXHR, textStatus, errorThrown) {
console.error(`${textStatus} - ${errorThrown}`);
},
success () {
updateDone(changes);
// eslint-disable-next-line no-underscore-dangle
if (window._current_highlighter === getString('portalHighlighterName')) { // re-validate highlighter if it is enabled.
window.changePortalHighlights(getString('portalHighlighterName'));
}
}
});
}
/**
* updates the bulk export dialog (called by the specific export functions).
* @param {HTMLElement} dialog the export dialog
* @param {number} cur current export state
* @param {number} max count of all elements that need exporting
* @param {number} time remaining time
*/
function updateExportDialog (dialog, cur, max, time) {
if ($('#exportProgressBar', dialog)) {
$('#exportProgressBar', dialog).val(cur);
}
if ($('#exportNumber', dialog)) {
$('#exportNumber', dialog).text(cur);
}
if ($('#exportTimeRemaining', dialog)) {
$('#exportTimeRemaining', dialog).text(time);
}
if (cur >= max) {
$('#exportState', dialog).text(getString('exportStateTextReady'));
const okayButton = $('.ui-button', dialog.parentElement).not('.ui-dialog-titlebar-button');
okayButton.text('OK');
okayButton.prop('title', '');
}
}
/*
*the idea of the following function was taken from https://stackoverflow.com/a/14561433
*by User talkol (https://stackoverflow.com/users/1025458/talkol).
*The License is CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/)
*The Code was slightly adapted.
*/
/**
* @param {number} lat1
* @param {number} lon1
* @param {number} lat2
* @param {number} lon2
* @return {number}
*/
function checkDistance (lat1, lon1, lat2, lon2) {
const R = 6371;
let x1 = lat2 - lat1;
let dLat = (x1 * Math.PI) / 180;
let x2 = lon2 - lon1;
let dLon = (x2 * Math.PI) / 180;
let a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
let c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
let d = R * c;
return d;
}
/**
* @param {string} id - the id of the input field to copy from
* @return {bool} - returns if copying was successful
*/
function copyfieldvalue (id) {
let field = document.getElementById(id);
field.focus();
field.setSelectionRange(0, field.value.length);
field.select();
return copySelectionText();
}
function copySelectionText () {
let copysuccess;
try {
copysuccess = document.execCommand('copy');
} catch (e) {
copysuccess = false;
}
return copysuccess;
}
// source: Oscar Zanota on Dev.to (https://dev.to/oskarcodes/send-automated-discord-messages-through-webhooks-using-javascript-1p01)
function sendMessage (msg) {
let params = {
username: window.plugin.pnav.settings.name,
avatar_url: '',
content: msg
};
request.open('POST', window.plugin.pnav.settings.webhookUrl);
request.setRequestHeader('Content-type', 'application/json');
request.send(JSON.stringify(params), false);
}
function modifyPortalDetails (data) {
const detailsObserver = new MutationObserver(waitForPogoButtons);
const statusObserver = new MutationObserver(waitForPogoStatus);
const send = Boolean(window.plugin.pnav.settings.webhookUrl);
console.log(data);
let guid = data.guid;
selectedGuid = guid;
if (!window.plugin.pogo) {
window.removeHook('portalDetailsUpdated', modifyPortalDetails);
setTimeout(function () {
$('#portaldetails').append(`