· 6 years ago · Nov 15, 2019, 06:18 PM
1// ==UserScript==
2// @name Wayfarer-Tools
3// @version 2.0.6-beta
4// @description formerly known as OPR-Tools
5
6// @author 1110101, https://gitlab.com/1110101/opr-tools/graphs/master
7// @match https://wayfarer.nianticlabs.com/review
8// @match https://wayfarer.nianticlabs.com/profile
9// @grant unsafeWindow
10// @grant GM_notification
11// @grant GM_addStyle
12// @downloadURL https://gitlab.com/1110101/opr-tools/raw/feature/wayfarerSupport/opr-tools.user.js
13// @updateURL https://gitlab.com/1110101/opr-tools/raw/feature/wayfarerSupport/opr-tools.user.js
14// @supportURL https://gitlab.com/1110101/opr-tools/issues
15// @require https://cdnjs.cloudflare.com/ajax/libs/alertifyjs-alertify.js/1.0.11/js/alertify.js
16// @require https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.4.4/proj4.js
17
18// ==/UserScript==
19
20// source https://gitlab.com/1110101/opr-tools
21// merge-requests welcome
22
23/*
24MIT License
25
26Permission is hereby granted, free of charge, to any person obtaining a copy
27of this software and associated documentation files (the "Software"), to deal
28in the Software without restriction, including without limitation the rights
29to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
30copies of the Software, and to permit persons to whom the Software is
31furnished to do so, subject to the following conditions:
32
33The above copyright notice and this permission notice shall be included in all
34copies or substantial portions of the Software.
35
36THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
37IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
38FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
39AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
40LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
41OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
42SOFTWARE.
43
44*/
45
46/* globals screen, MutationObserver, addEventListener, localStorage, MutationObserver, GM_addStyle, GM_notification, unsafeWindow, angular, google, alertify, proj4 */
47
48const WFRT = {
49
50 VERSION: 20006,
51
52 PREFERENCES: 'wfrt_prefs',
53
54 OPTIONS: {
55 KEYBOARD_NAV: 'keyboard_nav',
56 NORWAY_MAP_LAYER: 'norway_map_layer',
57 PRESET_FEATURE: 'preset_feature',
58 SCANNER_OFFSET_FEATURE: 'scanner_offset_feature',
59 SCANNER_OFFSET_UI: 'scanner_offset_ui',
60 COMMENT_TEMPLATES: 'comment_templates',
61 MAP_CIRCLE_20: 'map_circle_20',
62 MAP_CIRCLE_40: 'map_circle_40',
63
64 REFRESH: 'refresh',
65 REFRESH_NOTI_DESKTOP: 'refresh_noti_desktop'
66 },
67
68 PREFIX: 'wfrt_',
69 VAR_PREFIX: 'wfrt_var', // used in import/export **only**
70
71 // used for legacy oprt import
72 OPRT: 'oprt_',
73 OPRT_VAR_PREFIX: 'oprt_var',
74 OPRT_PREFERENCES: 'oprt_prefs',
75
76 VAR: { // will be included in import/export
77 SCANNER_OFFSET: 'scanner_offset',
78 MAP_TYPE_1: 'map_type_1',
79 MAP_TYPE_2: 'map_type_2',
80 CUSTOM_PRESETS: 'custom_presets'
81 },
82
83 VERSION_CHECK: 'version_check', // outside var, because it should not get exported
84
85 FROM_REFRESH: 'from_refresh' // sessionStorage
86}
87
88function addGlobalStyle (css) {
89 GM_addStyle(css)
90 // noop after first run
91 addGlobalStyle = () => {} // eslint-disable-line no-func-assign
92}
93
94class Preferences {
95 constructor () {
96 this.options = {}
97 this.defaults = {
98 [WFRT.OPTIONS.KEYBOARD_NAV]: true,
99 [WFRT.OPTIONS.NORWAY_MAP_LAYER]: false,
100 [WFRT.OPTIONS.PRESET_FEATURE]: true,
101 [WFRT.OPTIONS.SCANNER_OFFSET_FEATURE]: false,
102 [WFRT.OPTIONS.SCANNER_OFFSET_UI]: false,
103 [WFRT.OPTIONS.COMMENT_TEMPLATES]: true,
104 [WFRT.OPTIONS.REFRESH]: true,
105 [WFRT.OPTIONS.REFRESH_NOTI_DESKTOP]: true,
106 [WFRT.OPTIONS.MAP_CIRCLE_20]: false,
107 [WFRT.OPTIONS.MAP_CIRCLE_40]: true
108 }
109 this.loadOptions()
110 }
111
112 showPreferencesUI (w) {
113 let inout = new InOut(this)
114 let pageContainer = w.document.querySelector('#content-container')
115 let wfrtPreferences = w.document.querySelector('#wfrt_sidepanel_container')
116
117 if (wfrtPreferences !== null) wfrtPreferences.classList.toggle('hide')
118 else {
119 pageContainer.insertAdjacentHTML('afterbegin', `
120<section id="wfrt_sidepanel_container" style="
121 background: black;
122 border-left: 2px gold inset;
123 border-top: 2px gold inset;
124 border-bottom: 2px gold inset;
125 color: white;
126 position: absolute;
127 right: 0;
128 height: 90%;
129 padding: 0 20px;
130 z-index: 10;
131 width: 400px;
132 ">
133 <div class="row">
134 <div class="col-lg-12">
135 <h4 class="gold">Wayfarer-Tools Preferences</h4>
136 </div>
137 <div class="col-lg-12">
138 <div class="btn-group" role="group">
139 <button id="import_all" class="btn btn-success">Import</button>
140 <button id="export_all" class="btn btn-success">Export</button>
141 </div>
142 </div>
143 </div>
144 <div id="wfrt_options"></div>
145 <a id="wfrt_reload" class="btn btn-warning hide"><span class="glyphicon glyphicon-refresh"></span>
146 Reload to apply changes</a>
147
148 <div style="position: absolute; bottom: 0; left: 0; margin:20px;"><a href="https://t.me/oprtools">${TG_SVG} Wayfarer-Tools Telegram Channel</a></div>
149</section>`)
150
151 let optionsContainer = w.document.getElementById('wfrt_options')
152 let reloadButton = w.document.getElementById('wfrt_reload')
153
154 for (let item in this.options) {
155 // remove unknown or removed options
156 if (strings.options[item] === undefined) {
157 this.remove(item)
158 continue
159 }
160
161 const div = w.document.createElement('div')
162 div.classList.add('checkbox')
163 const label = w.document.createElement('label')
164 const input = w.document.createElement('input')
165 input.type = 'checkbox'
166 input.name = item
167 input.checked = this.options[item]
168 div.appendChild(label)
169 label.appendChild(input)
170 label.appendChild(w.document.createTextNode(strings.options[item]))
171 optionsContainer.insertAdjacentElement('beforeEnd', div)
172 }
173
174 optionsContainer.addEventListener('change', (event) => {
175 this.set(event.target.name, event.target.checked)
176 reloadButton.classList.remove('hide')
177 })
178
179 reloadButton.addEventListener('click', () => {
180 window.location.reload()
181 })
182
183 w.document.getElementById('import_all').addEventListener('click', () => {
184 alertify.okBtn('Import').prompt('Paste here:',
185 (value, event) => {
186 event.preventDefault()
187 if (value === 'undefined' || value === '') {
188 return
189 }
190 inout.importFromString(value)
191 alertify.success(`✔ Imported preferences`)
192 }, event => {
193 event.preventDefault()
194 }
195 )
196 })
197
198 w.document.getElementById('export_all').addEventListener('click', () => {
199 if (navigator.clipboard !== undefined) {
200 navigator.clipboard.writeText(inout.exportAll()).then(() => {
201 alertify.success(`✔ Exported preferences to your clipboard!`)
202 }, () => {
203 // ugly alert as fallback
204 alertify.alert(inout.exportAll())
205 })
206 } else {
207 alertify.alert(inout.exportAll())
208 }
209 }
210 )
211 }
212 }
213
214 loadOptions () {
215 Object.assign(this.options, this.defaults, JSON.parse(localStorage.getItem(WFRT.PREFERENCES)))
216 }
217
218 set (key, value) {
219 this.options[key] = value
220 localStorage.setItem(WFRT.PREFERENCES, JSON.stringify(this.options))
221 }
222
223 get (key) {
224 return this.options[key]
225 }
226
227 remove (key) {
228 delete this.options[key]
229 localStorage.setItem(WFRT.PREFERENCES, JSON.stringify(this.options))
230 }
231
232 exportPrefs () {
233 return JSON.stringify(this.options)
234 }
235
236 importPrefs (string) {
237 try {
238 this.options = JSON.parse(string)
239 localStorage.setItem(WFRT.PREFERENCES, JSON.stringify(this.options))
240 } catch (e) {
241 throw new Error('Could not import preferences!')
242 }
243 }
244}
245
246class InOut {
247 constructor (preferences) {
248 this.preferences = preferences
249 }
250
251 static exportVars () {
252 let exportObject = {}
253 for (const item in WFRT.VAR) {
254 exportObject[WFRT.VAR[item]] = localStorage.getItem(WFRT.PREFIX + WFRT.VAR[item])
255 }
256 return exportObject
257 }
258
259 static importVars (importObject) {
260 for (const item in importObject) {
261 localStorage.setItem(WFRT.PREFIX + item, importObject[item])
262 }
263 }
264
265 importFromString (string) {
266 try {
267 let json = JSON.parse(string)
268
269 if (json.hasOwnProperty(WFRT.PREFERENCES)) { this.preferences.importPrefs(json[WFRT.PREFERENCES]) }
270 if (json.hasOwnProperty(WFRT.VAR_PREFIX)) { InOut.importVars(json[WFRT.VAR_PREFIX]) }
271
272 // legacy import for oprt stuff
273 if (json.hasOwnProperty(WFRT.OPRT_PREFERENCES)) { this.preferences.importPrefs(json[WFRT.OPRT_PREFERENCES]) }
274 if (json.hasOwnProperty(WFRT.OPRT_VAR_PREFIX)) { InOut.importVars(json[WFRT.OPRT_VAR_PREFIX]) }
275 } catch (e) {
276 throw new Error('Import failed')
277 }
278 }
279
280 exportAll () {
281 return JSON.stringify(Object.assign({}, { [WFRT.PREFERENCES]: this.preferences.exportPrefs() }, { [WFRT.VAR_PREFIX]: InOut.exportVars() }))
282 }
283}
284
285function nacistUrl(url_load) {
286
287 url_load = decodeURIComponent(url_load);
288
289 var pano_id;
290
291 if(url_load.indexOf('imagekey') !== -1) {
292 var s1 = url_load.indexOf('!2s') + 3;
293
294 var s2 = url_load.indexOf('&', s1);
295 if(s2 === -1) {
296 pano_id = url_load.substring(s1);
297 } else {
298
299 pano_id = url_load.substring(s1, s2)
300 }
301
302 } else {
303 var s3 = url_load.indexOf('!1s') + 3;
304 pano_id = url_load.substring(s3, url_load.indexOf('!2e', s3));
305 }
306
307 if (!pano_id) pano_id = ' ';
308 //if(pano_id.charAt(0) == '-') pano_id = ''+ pano_id.replace(/%2F/g, '/');
309 if(pano_id.indexOf('/') > -1 && pano_id.substring(0, 2) != 'F:' && pano_id.charAt(0) == '-') {
310 pano_id = 'F:' + pano_id;
311 } else if(pano_id.substring(0, 2) == 'AF' && pano_id.length === 44) {
312 pano_id = 'CAoSLE' + btoa('CAoS' + pano_id).substr(6);
313 }
314 return pano_id;
315
316 }
317
318function getSVLink() {
319for (let e of document.getElementsByTagName("a")) {
320 if (e.target==="_blank" && e.href.includes('apiv3') && e.innerHTML.includes('Zobacz w Mapach Google')) {
321 return e;
322 }
323}
324}
325
326function init () {
327 const w = typeof unsafeWindow === 'undefined' ? window : unsafeWindow
328 let tryNumber = 15
329
330 let wfrtCustomPresets
331
332 let browserLocale = window.navigator.languages[0] || window.navigator.language || 'en'
333
334 let preferences = new Preferences()
335
336 const initWatcher = setInterval(() => {
337 if (tryNumber === 0) {
338 clearInterval(initWatcher)
339 w.document.getElementById('NewSubmissionController')
340 .insertAdjacentHTML('afterBegin', `
341<div id="wfrt_init_failed" class='alert alert-danger'><strong><span class='glyphicon glyphicon-remove'></span> Wayfarer-Tools initialization failed, refresh page</strong></div>
342`)
343 addRefreshContainer()
344 return
345 }
346 if (w.angular) {
347 let err = false
348 try {
349 initAngular()
350 } catch (error) {
351 err = error
352 // console.log(error);
353 }
354 if (!err) {
355 try {
356 initScript()
357 clearInterval(initWatcher)
358 } catch (error) {
359 console.log(error)
360 if (error.message === '41') {
361 addRefreshContainer()
362 }
363 if (error.message !== '42') {
364 clearInterval(initWatcher)
365 }
366 }
367 }
368 }
369 tryNumber--
370 }, 1000)
371
372 function initAngular () {
373 const el = w.document.querySelector('[ng-app="portalApp"]')
374 w.$app = w.angular.element(el)
375 w.$injector = w.$app.injector()
376 w.inject = w.$injector.invoke
377 w.$rootScope = w.$app.scope()
378
379 w.getService = function getService (serviceName) {
380 w.inject([serviceName, function (s) { w[serviceName] = s }])
381 }
382
383 w.$scope = element => w.angular.element(element).scope()
384 }
385
386 function initScript () {
387 // adding CSS
388 addGlobalStyle(GLOBAL_CSS)
389
390 addOptionsButton()
391
392 const subMissionDiv = w.document.getElementById('NewSubmissionController')
393
394 // check if subCtrl exists (should exists if we're on /review)
395 if (subMissionDiv !== null && w.$scope(subMissionDiv).subCtrl !== null) {
396 const subController = w.$scope(subMissionDiv).subCtrl
397 const newPortalData = subController.pageData
398
399 const whatController = w.$scope(w.document.getElementById('WhatIsItController')).whatCtrl
400
401 const answerDiv = w.document.getElementById('AnswersController')
402 const ansController = w.$scope(answerDiv).answerCtrl
403
404 if (subController.errorMessage !== '') {
405 // no portal analysis data available
406 throw new Error(41) // @todo better error code
407 }
408
409 if (typeof newPortalData === 'undefined') {
410 // no submission data present
411 throw new Error(42) // @todo better error code
412 }
413
414 // detect portal edit
415 if (subController.reviewType === 'NEW') {
416 modifyNewPage(ansController, subController, whatController, newPortalData)
417 } else if (subController.reviewType === 'EDIT') {
418 modifyEditPage(ansController, subController, newPortalData)
419 }
420
421 checkIfAutorefresh()
422
423 startExpirationTimer(subController)
424
425 versionCheck()
426 } else if (w.location.pathname.includes('profile')) {
427 modifyProfile()
428 }
429
430 }
431
432 function modifyNewPage (ansController, subController, whatController, newPortalData) {
433 let skipDialog = false
434
435 mapButtons(newPortalData, w.document.querySelector('#map-card .card__footer'), 'afterBegin')
436
437 // mutation observer
438 const bodyObserver = new MutationObserver(mutationList => {
439 for (let mutationRecord of mutationList) {
440 // we just want added nodes with (class:modal). null and undefined check for performance reasons
441 if (mutationRecord.addedNodes.length > 0 && mutationRecord.addedNodes[0].className === 'modal fade ng-isolate-scope') {
442 // adds keyboard-numbers to low quality sub-sub-lists
443 let sublistItems = mutationRecord.addedNodes[0].querySelectorAll('ul.sub-group-list')
444 if (sublistItems !== undefined) {
445 sublistItems.forEach(el => {
446 let i = 1
447 el.querySelectorAll('li > a').forEach(el2 => { el2.insertAdjacentHTML('afterbegin', `<kbd>${i++}</kbd> `) })
448 })
449 let i = 1
450 // adds keyboard numbers to low quality sub-list
451 mutationRecord.addedNodes[0].querySelectorAll('label.sub-group')
452 .forEach(el2 => { el2.insertAdjacentHTML('beforeend', `<kbd class="pull-right ">${i++}</kbd>`) })
453 }
454 // skip "Your analysis has been recorded" dialog
455 if (skipDialog) {
456 if (mutationRecord.addedNodes[0].querySelector('.modal-body button[ng-click="answerCtrl3.reloadPage()"]') !== null) {
457 w.document.location.href = '/review'
458 return
459 }
460 }
461 }
462 }
463 })
464 bodyObserver.observe(w.document.body, { childList: true })
465
466 let newSubmitDiv = w.document.querySelector('.answer-btn-container.bottom-btns')
467 let { submitButton, submitAndNext } = quickSubmitButton(newSubmitDiv, ansController, bodyObserver)
468
469 if (preferences.get(WFRT.OPTIONS.COMMENT_TEMPLATES)) { commentTemplates() }
470
471 /* region presets start */
472 if (preferences.get(WFRT.OPTIONS.PRESET_FEATURE)) {
473 const customPresetUI = `<div class="card" id="wfrt_custom_presets_card"><div class="card__body"><div>Presets <button class="btn btn-default btn-xs" id="addPreset">+</button>
474<button id="getSVCode">Get SV Code</button></div>
475<input type="text" id="getSVCodeText">
476</div>
477 <div class='btn-group' id="wfrt_custom_presets"></div></div></div>
478`
479
480 w.document.querySelector('.card-row-container').insertAdjacentHTML('afterbegin', customPresetUI)
481
482 addCustomPresetButtons()
483
484 // we have to inject the tooltip to angular
485 w.$injector.invoke(['$compile', ($compile) => {
486 let compiledSubmit = $compile(`<span class="glyphicon glyphicon-info-sign darkgray" uib-tooltip-trigger="outsideclick" uib-tooltip-placement="left" tooltip-class="goldBorder" uib-tooltip="(Wayfarer-Tools) Create your own presets for stuff like churches, playgrounds or crosses'.\nHowto: Answer every question you want included and click on the +Button.\n\nTo delete a preset shift-click it."></span> `)(w.$scope(document.getElementById('descriptionDiv')))
487 w.document.getElementById('addPreset').insertAdjacentElement('beforebegin', compiledSubmit[0])
488 }])
489
490 // click listener for +preset button
491 w.document.getElementById('addPreset').addEventListener('click', event => {
492 alertify.okBtn('Save').prompt('New preset name:',
493 (value, event) => {
494 event.preventDefault()
495 if (value === 'undefined' || value === '') {
496 return
497 }
498 saveCustomPreset(value, ansController, whatController)
499 alertify.success(`✔ Created preset <i>${value}</i>`)
500 addCustomPresetButtons()
501 }, event => {
502 event.preventDefault()
503 }
504 )
505 })
506
507 w.document.getElementById('getSVCode').addEventListener('click', event => {
508 var copyText = document.getElementById('getSVCodeText');
509 copyText.value = nacistUrl(getSVLink());
510 copyText.select();
511 copyText.setSelectionRange(0, 99999); /*For mobile devices*/
512
513 /* Copy the text inside the text field */
514 document.execCommand("copy");
515
516 }
517 )
518
519
520 let clickListener = event => {
521 const source = event.target || event.srcElement
522 let value = source.id
523 if (value === '' || event.target.nodeName !== 'BUTTON') {
524 return
525 }
526
527 let preset = wfrtCustomPresets.find(item => item.uid === value)
528
529 if (event.shiftKey) {
530 alertify.log(`Deleted preset <i>${preset.label}</i>`)
531 w.document.getElementById(preset.uid).remove()
532 deleteCustomPreset(preset)
533 return
534 }
535
536 ansController.formData.quality = preset.quality
537 ansController.formData.description = preset.description
538 ansController.formData.cultural = preset.cultural
539 ansController.formData.uniqueness = preset.uniqueness
540 ansController.formData.location = preset.location
541 ansController.formData.safety = preset.safety
542
543 // the controller's set by ID function doesn't work
544 // and autocomplete breaks if there are any spaces
545 // so set the field to the first word from name and match autocomplete by ID
546 // at the very least, I know this will set it and leave the UI looking like it was manually set.
547 whatController.whatInput = preset.nodeName.split(' ')[0]
548 let nodes = whatController.getWhatAutocomplete()
549 for (let i = 0; i < nodes.length; i++) {
550 if (nodes[i].id === preset.nodeId) {
551 whatController.setWhatAutocompleteNode(nodes[i])
552 break
553 }
554 }
555 whatController.whatInput = ''
556
557 // update ui
558 event.target.blur()
559 w.$rootScope.$apply()
560
561 alertify.success(`✔ Applied <i>${preset.label}</i>`)
562 }
563
564 w.document.getElementById('wfrt_custom_presets').addEventListener('click', clickListener, false)
565 }
566 /* endregion presets end */
567
568 // make photo filmstrip scrollable
569 const filmstrip = w.document.getElementById('map-filmstrip')
570 let lastScrollLeft = filmstrip.scrollLeft
571
572 function scrollHorizontally (e) {
573 e = window.event || e
574 if ((('deltaY' in e && e.deltaY !== 0) || ('wheelDeltaY' in e && e.wheelDeltaY !== 0)) && lastScrollLeft === filmstrip.scrollLeft) {
575 e.preventDefault()
576 const delta = (e.wheelDeltaY || -e.deltaY * 25 || -e.detail)
577 filmstrip.scrollLeft -= (delta)
578 lastScrollLeft = filmstrip.scrollLeft
579 }
580 }
581
582 filmstrip.addEventListener('wheel', scrollHorizontally, false)
583 filmstrip.addEventListener('DOMMouseScroll', scrollHorizontally, false)
584
585 let _initMap = subController.initMap
586 subController.initMap = () => {
587 _initMap()
588 mapMarker(subController.markers)
589 }
590
591 mapOriginCircle(subController.map2)
592 mapMarker(subController.markers)
593 mapTypes(subController.map, false)
594 mapTypes(subController.map2, true)
595
596 // hook resetStreetView() and re-apply map types and options to first map. not needed for duplicates because resetMap() just resets the position
597 let _resetStreetView = subController.resetStreetView
598 subController.resetStreetView = () => {
599 _resetStreetView()
600 mapOriginCircle(subController.map2)
601 mapTypes(subController.map2, true)
602 }
603
604 // adding a green 40m circle and a smaller 20m circle around the new location marker that updates on dragEnd
605 let draggableMarkerCircle
606 let draggableMarkerCircleSmall
607 let _showDraggableMarker = subController.showDraggableMarker
608 subController.showDraggableMarker = () => {
609 _showDraggableMarker()
610
611 w.getService('NewSubmissionDataService')
612 let newLocMarker = w.NewSubmissionDataService.getNewLocationMarker()
613 if (preferences.get(WFRT.OPTIONS.MAP_CIRCLE_40)) {
614 google.maps.event.addListener(newLocMarker, 'dragend', function () {
615 if (draggableMarkerCircle == null) {
616 draggableMarkerCircle = new google.maps.Circle({
617 map: subController.map2,
618 center: newLocMarker.position,
619 radius: 40,
620 strokeColor: '#4CAF50', // material green 500
621 strokeOpacity: 1,
622 strokeWeight: 2,
623 fillOpacity: 0
624 })
625 } else draggableMarkerCircle.setCenter(newLocMarker.position)
626 })
627 }
628
629 if (preferences.get(WFRT.OPTIONS.MAP_CIRCLE_20)) {
630 google.maps.event.addListener(newLocMarker, 'dragend', function () {
631 if (draggableMarkerCircleSmall == null) {
632 draggableMarkerCircleSmall = new google.maps.Circle({
633 map: subController.map2,
634 center: newLocMarker.position,
635 radius: 20,
636 strokeColor: '#4CCF50',
637 strokeOpacity: 1,
638 strokeWeight: 2,
639 fillOpacity: 0
640 })
641 } else draggableMarkerCircleSmall.setCenter(newLocMarker.position)
642 })
643 }
644 }
645
646 if (preferences.get(WFRT.OPTIONS.MAP_CIRCLE_40) || preferences.get(WFRT.OPTIONS.MAP_CIRCLE_20)) {
647 document.querySelector('.flex-map-row').insertAdjacentHTML('beforeEnd',
648 `<small id="wfrt_map_legend">
649 ${preferences.get(WFRT.OPTIONS.MAP_CIRCLE_40) ? '<span style="color:#ebbc4a">outer circle:</span> 40m' : ''}
650 ${preferences.get(WFRT.OPTIONS.MAP_CIRCLE_20) ? '<span style="color:#effc4a">inner circle:</span> 20m' : ''}
651 </small>`)
652 }
653
654 // // move portal rating to the right side. don't move on mobile devices / small width
655 // if (screen.availWidth > 768) {
656 // let nodeToMove = w.document.querySelector('div[class="btn-group"]').parentElement
657 // if (subController.hasSupportingImageOrStatement) {
658 // const descDiv = w.document.getElementById('descriptionDiv')
659 // const scorePanel = descDiv.querySelector('div.text-center.hidden-xs')
660 // scorePanel.insertBefore(nodeToMove, scorePanel.firstChild)
661 // } else {
662 // const scorePanel = w.document.querySelector('div[class~="pull-right"]')
663 // scorePanel.insertBefore(nodeToMove, scorePanel.firstChild)
664 // }
665 // }
666
667 // bind click-event to Dup-Images-Filmstrip. result: a click to the detail-image the large version is loaded in another tab
668 const imgDups = w.document.querySelectorAll('#map-filmstrip > ul > li > img')
669 const openFullImage = function () {
670 w.open(`${this.src}=s0`, 'fulldupimage')
671 }
672 for (let imgSep in imgDups) {
673 if (imgDups.hasOwnProperty(imgSep)) {
674 imgDups[imgSep].addEventListener('click', () => {
675 const imgDup = w.document.querySelector('#content > img')
676 if (imgDup !== null) {
677 imgDup.removeEventListener('click', openFullImage)
678 imgDup.addEventListener('click', openFullImage)
679 imgDup.setAttribute('style', 'cursor: pointer;')
680 }
681 })
682 }
683 }
684
685 // add translate buttons to title and description (if existing)
686 let lang = 'en'
687 try { lang = browserLocale.split('-')[0] } catch (e) {}
688 const titleContainer = w.document.querySelector('h1.title-description')
689 const content = titleContainer.innerText.trim()
690 let a = w.document.createElement('a')
691 let span = w.document.createElement('span')
692 span.className = 'glyphicon glyphicon-book'
693 span.innerHTML = ' '
694 a.appendChild(span)
695 a.className = 'translate-title btn btn-default pull-right'
696 a.target = 'translate'
697 a.style.setProperty('padding', '0px 4px')
698 a.href = `https://translate.google.com/#auto/${lang}/${encodeURIComponent(content)}`
699 a.id = 'wfrt_translate_title'
700 titleContainer.insertAdjacentElement('beforeend', a)
701
702 const descContainer = w.document.querySelector('h4.title-description')
703 if (descContainer.innerText !== '<No description>' && descContainer.innerText !== '') {
704 a = w.document.createElement('a')
705 span = w.document.createElement('span')
706 span.className = 'glyphicon glyphicon-book'
707 span.innerHTML = ' '
708 a.appendChild(span)
709 a.className = 'translate-description btn btn-default pull-right'
710 a.target = 'translate'
711 a.style.setProperty('padding', '0px 4px')
712 a.href = `https://translate.google.com/#auto/${lang}/${encodeURIComponent(descContainer.innerText.trim())}`
713 a.id = 'wfrt_translate_desc'
714 descContainer.insertAdjacentElement('beforeend', a)
715 descContainer.insertAdjacentHTML('beforebegin', '<hr>')
716 }
717
718 const supportingStatement = w.document.querySelector('.supporting-statement-central-field p')
719 if (supportingStatement != null && supportingStatement.innerText !== '') {
720 a = w.document.createElement('a')
721 span = w.document.createElement('span')
722 span.className = 'glyphicon glyphicon-book'
723 span.innerHTML = ' '
724 a.appendChild(span)
725 a.className = 'translate-supporting btn btn-default pull-right'
726 a.target = 'translate'
727 a.style.setProperty('padding', '0px 4px')
728 a.href = `https://translate.google.com/#auto/${lang}/${encodeURIComponent(supportingStatement.innerText)}`
729 a.id = 'wfrt_translate_support'
730 supportingStatement.insertAdjacentElement('beforebegin', a)
731 }
732
733 // automatically open the first listed possible duplicate
734 try {
735 const e = w.document.querySelector('#map-filmstrip > ul > li:nth-child(1) > img')
736 if (e !== null) {
737 setTimeout(() => {
738 e.click()
739 }, 500)
740 }
741 } catch (err) {}
742
743 expandWhatIsItBox()
744
745 // Fix rejectComment width
746 let _showLowQualityModal = ansController.showLowQualityModal
747 ansController.showLowQualityModal = () => {
748 _showLowQualityModal()
749 setTimeout(() => {
750 let rejectReasonTA = w.document.querySelector('textarea[ng-model="answerCtrl2.rejectComment"]')
751 rejectReasonTA.style.setProperty('max-width', '100%')
752
753 w.$injector.invoke(['$compile', ($compile) => {
754 let target = w.document.querySelector('.modal-body button:last-child')
755 let compiledSubmit = $compile(`<button id="submitAndSkipLowQuality" class="button-primary" ng-click="answerCtrl2.confirmLowQuality()" ng-disabled="!(answerCtrl2.readyToSubmitSpam())" disabled="disabled">
756 <span class="glyphicon glyphicon-floppy-disk"></span> <span class="glyphicon glyphicon-forward"></span></button>`)(w.$scope(target))
757 target.insertAdjacentElement('beforebegin', compiledSubmit[0])
758 w.document.getElementById('submitAndSkipLowQuality').addEventListener('click', () => {
759 skipDialog = true
760 })
761 }])
762 }, 10)
763 }
764
765 /* global markDuplicatePressed */
766 let _markDuplicatePressed = markDuplicatePressed
767 markDuplicatePressed = (guid) => { // eslint-disable-line no-global-assign
768 _markDuplicatePressed(guid)
769 setTimeout(() => {
770 w.$injector.invoke(['$compile', ($compile) => {
771 let target = w.document.querySelector('.modal-body button:last-child')
772 let compiledSubmit = $compile(`<button id="submitAndSkipDuplicate" class="button-primary" ng-click="answerCtrl2.confirmDuplicate()">
773 <span class="glyphicon glyphicon-floppy-disk"></span> <span class="glyphicon glyphicon-forward"></span></button>`)(w.$scope(target))
774 target.insertAdjacentElement('beforebegin', compiledSubmit[0])
775 w.document.getElementById('submitAndSkipDuplicate').addEventListener('click', () => {
776 skipDialog = true
777 })
778 }])
779 }, 10)
780 }
781
782 /* region keyboard nav */
783 if (preferences.get(WFRT.OPTIONS.KEYBOARD_NAV)) {
784 activateShortcuts()
785 }
786
787 function activateShortcuts () {
788 // keyboard navigation
789 // documentation: https://gitlab.com/1110101/opr-tools#keyboard-navigation
790
791 let currentSelectable = 0
792 let maxItems = 7
793 let selectedReasonGroup = -1
794 let selectedReasonSubGroup = -1
795
796 // Reset when modal is closed
797 let _resetLowQuality = ansController.resetLowQuality
798 ansController.resetLowQuality = () => {
799 _resetLowQuality()
800 selectedReasonGroup = -1
801 selectedReasonSubGroup = -1
802 currentSelectable = 0
803 highlight()
804 }
805
806 // a list of all 6 star button rows, and the two submit buttons
807 let starsAndSubmitButtons = w.document.querySelectorAll('.five-stars, #submitFF')
808
809 function highlight () {
810 starsAndSubmitButtons.forEach((element) => { element.style.setProperty('border', 'none') })
811 if (currentSelectable <= maxItems - 2) {
812 starsAndSubmitButtons[currentSelectable].style.setProperty('border', '2px dashed #E47252')
813 submitAndNext.blur()
814 submitButton.blur()
815 } else if (currentSelectable === 6) {
816 submitAndNext.focus()
817 } else if (currentSelectable === 7) {
818 submitButton.focus()
819 }
820 }
821
822 addEventListener('keydown', (event) => {
823 /*
824 keycodes:
825
826 8: Backspace
827 9: TAB
828 13: Enter
829 16: Shift
830 27: Escape
831 32: Space
832 68: D
833 107: NUMPAD +
834 109: NUMPAD -
835 111: NUMPAD /
836
837 49 - 53: Keys 1-5
838 97 - 101: NUMPAD 1-5
839
840 */
841
842 let numkey = null
843 if (event.keyCode >= 49 && event.keyCode <= 55) {
844 numkey = event.keyCode - 48
845 } else if (event.keyCode >= 97 && event.keyCode <= 103) {
846 numkey = event.keyCode - 96
847 }
848
849 // do not do anything if a text area or a input with type text has focus
850 if (w.document.querySelector('input[type=text]:focus') || w.document.querySelector('textarea:focus')) {
851 return
852 } else if ((event.keyCode === 13 || event.keyCode === 32) && w.document.querySelector('a.button[href="/review"]')) {
853 // "analyze next" button
854 w.document.location.href = '/review'
855 event.preventDefault()
856 } else if ((event.keyCode === 13 || event.keyCode === 32) && w.document.querySelector('[ng-click="answerCtrl2.confirmLowQuality()"]')) {
857 // submit low quality rating
858 w.document.querySelector('[ng-click="answerCtrl2.confirmLowQuality()"]').click()
859 currentSelectable = 0
860 event.preventDefault()
861 } else if ((event.keyCode === 13 || event.keyCode === 32) && w.document.querySelector('[ng-click="answerCtrl2.confirmLowQualityOld()"]')) {
862 // submit low quality rating alternate
863 w.document.querySelector('[ng-click="answerCtrl2.confirmLowQualityOld()"]').click()
864 currentSelectable = 0
865 event.preventDefault()
866 } else if ((event.keyCode === 68) && w.document.querySelector('#content > button')) {
867 // click first/selected duplicate (key D)
868 w.document.querySelector('#content > button').click()
869 currentSelectable = 0
870 event.preventDefault()
871 } else if (event.keyCode === 84) {
872 // click on translate title link (key T)
873 const link = w.document.querySelector('#wfrt_translate_title')
874 if (link) {
875 link.click()
876 event.preventDefault()
877 }
878 } else if (event.keyCode === 89) {
879 // click on translate description link (key Y)
880 const link = w.document.querySelector('#wfrt_translate_desc')
881 if (link) {
882 link.click()
883 event.preventDefault()
884 }
885 } else if (event.keyCode === 85) {
886 // click on translate extra info link (key U)
887 const link = w.document.querySelector('#wfrt_translate_support')
888 if (link) {
889 link.click()
890 event.preventDefault()
891 }
892 }
893 else if ((event.keyCode === 13 || event.keyCode === 32) && w.document.querySelector('[ng-click="answerCtrl2.confirmDuplicate()"]')) {
894 // submit duplicate
895 w.document.querySelector('[ng-click="answerCtrl2.confirmDuplicate()"]').click()
896 currentSelectable = 0
897 event.preventDefault()
898 } else if ((event.keyCode === 13 || event.keyCode === 32) && currentSelectable === maxItems) {
899 // submit normal rating
900 w.document.querySelector('[ng-click="answerCtrl.submitForm()"]').click()
901 event.preventDefault()
902 } else if ((event.keyCode === 27 || event.keyCode === 111) && w.document.querySelector('[ng-click="answerCtrl2.resetDuplicate()"]')) {
903 // close duplicate dialog
904 w.document.querySelector('[ng-click="answerCtrl2.resetDuplicate()"]').click()
905 currentSelectable = 0
906 event.preventDefault()
907 } else if ((event.keyCode === 27 || event.keyCode === 111) && w.document.querySelector('[ng-click="answerCtrl2.resetLowQuality()"]')) {
908 // close low quality ration dialog
909 w.document.querySelector('[ng-click="answerCtrl2.resetLowQuality()"]').click()
910 currentSelectable = 0
911 event.preventDefault()
912 } else if (event.keyCode === 27 || event.keyCode === 111) {
913 // return to first selection (should this be a portal)
914 currentSelectable = 0
915 event.preventDefault()
916 } else if (event.keyCode === 106 || event.keyCode === 220) {
917 // skip portal if possible
918 if (newPortalData.canSkip) {
919 ansController.skipToNext()
920 }
921 } else if (event.keyCode === 72) {
922 showHelp() // @todo
923 } else if (w.document.querySelector('[ng-click="answerCtrl2.confirmLowQuality()"]')) {
924 // Reject reason shortcuts
925 if (numkey != null) {
926 if (selectedReasonGroup === -1) {
927 try {
928 w.document.getElementById('sub-group-' + numkey).click()
929 selectedReasonGroup = numkey - 1
930 w.document.querySelectorAll('label.sub-group kbd').forEach(el => el.classList.add('hide'))
931 } catch (err) {}
932 } else {
933 if (selectedReasonSubGroup === -1) {
934 try {
935 w.document.querySelectorAll('#reject-reason ul ul')[selectedReasonGroup].children[numkey - 1].children[0].click()
936 selectedReasonSubGroup = numkey - 1
937 } catch (err) {}
938 } else {
939 w.document.getElementById('root-label').click()
940 selectedReasonGroup = -1
941 selectedReasonSubGroup = -1
942 w.document.querySelectorAll('label.sub-group kbd').forEach(el => el.classList.remove('hide'))
943 }
944 }
945 event.preventDefault()
946 }
947 } else if ((event.keyCode === 107 || event.keyCode === 9) && currentSelectable < maxItems) {
948 // select next rating
949 currentSelectable++
950 event.preventDefault()
951 } else if ((event.keyCode === 109 || event.keyCode === 16 || event.keyCode === 8) && currentSelectable > 0) {
952 // select previous rating
953 currentSelectable--
954 event.preventDefault()
955 } else if (numkey === null || currentSelectable > maxItems - 2) {
956 return
957 } else if (numkey !== null && event.shiftKey) {
958 try {
959 w.document.getElementsByClassName('customPresetButton')[numkey - 1].click()
960 if (!document.getElementById('submitFF').disabled) {
961 currentSelectable = 6
962 highlight()
963 }
964 } catch (e) {
965 // ignore
966 }
967 } else {
968 // rating 1-5
969 starsAndSubmitButtons[currentSelectable].querySelectorAll('button.button-star')[numkey - 1].click()
970 currentSelectable++
971 }
972 highlight()
973 })
974
975 highlight()
976 }
977
978 /* endregion keyboard nav */
979
980 modifyNewPage = () => {} // eslint-disable-line
981 }
982
983 function modifyEditPage (ansController, subController, newPortalData) {
984 let editDiv = w.document.querySelector('div[ng-show="subCtrl.reviewType===\'EDIT\'"]')
985
986 mapButtons(newPortalData, editDiv, 'afterEnd')
987
988 // mutation observer
989 const bodyObserver = new MutationObserver(mutationList => {
990 for (let mutationRecord of mutationList) {
991 // we just want addednodes with (class:modal). null and undefined check for performance reasons
992 if (mutationRecord.addedNodes.length > 0 &&
993 mutationRecord.addedNodes[0].className === 'modal fade ng-isolate-scope' &&
994 mutationRecord.addedNodes[0].querySelector('.modal-body a[href=\'/review\']') !== null) {
995 w.document.location.href = '/review'
996 }
997 }
998 })
999 bodyObserver.observe(w.document.body, { childList: true })
1000
1001 let newSubmitDiv = w.document.querySelector('.answer-btn-container.bottom-btns')
1002 let { submitButton, submitAndNext } = quickSubmitButton(newSubmitDiv, ansController, bodyObserver)
1003
1004 if (preferences.get(WFRT.OPTIONS.COMMENT_TEMPLATES)) { commentTemplates() }
1005
1006 mapTypes(subController.locationEditsMap, true)
1007
1008 // add translation links to title and description edits
1009 if (newPortalData.titleEdits.length > 1 || newPortalData.descriptionEdits.length > 1) {
1010 for (const titleEditBox of editDiv.querySelectorAll('.titleEditBox.ng-scope')) {
1011 const contentSpan = titleEditBox.querySelector('.poi-edit-text')
1012 let a = w.document.createElement('a')
1013 let span = w.document.createElement('span')
1014 span.className = 'glyphicon glyphicon-book'
1015 span.innerHTML = ' '
1016 a.appendChild(span)
1017 a.className = 'translate-title button btn btn-default pull-right'
1018 a.target = 'translate'
1019 a.style.setProperty('padding', '0px 4px')
1020 a.href = `https://translate.google.com/#auto/${browserLocale.split('-')[0]}/${encodeURIComponent(contentSpan.innerText.trim())}`
1021 contentSpan.style.setProperty('display', 'inline-block')
1022 contentSpan.insertAdjacentElement('beforeEnd', a)
1023 }
1024 }
1025
1026 if (newPortalData.titleEdits.length <= 1) {
1027 let titleDiv = editDiv.querySelector('div[ng-if="!answerCtrl.needsTitleEdit"]')
1028 let a = w.document.createElement('a')
1029 let span = w.document.createElement('span')
1030 span.className = 'glyphicon glyphicon-book'
1031 span.innerHTML = ' '
1032 a.appendChild(span)
1033 a.className = 'translate-title btn btn-default'
1034 a.target = 'translate'
1035 a.style.setProperty('padding', '0px 4px')
1036 a.style.setProperty('margin-left', '14px')
1037 a.href = `https://translate.google.com/#auto/${browserLocale.split('-')[0]}/${encodeURIComponent(titleDiv.innerText.trim())}`
1038 titleDiv.insertAdjacentElement('beforeend', a)
1039 }
1040
1041 if (newPortalData.descriptionEdits.length <= 1) {
1042 let titleDiv = editDiv.querySelector('div[ng-if="!answerCtrl.needsDescriptionEdit"]')
1043 const content = titleDiv.innerText.trim() || ''
1044 if (content !== '<No description>' && content !== '') {
1045 let a = w.document.createElement('a')
1046 let span = w.document.createElement('span')
1047 span.className = 'glyphicon glyphicon-book'
1048 span.innerHTML = ' '
1049 a.appendChild(span)
1050 a.className = 'translate-title btn btn-default'
1051 a.target = 'translate'
1052 a.style.setProperty('padding', '0px 4px')
1053 a.style.setProperty('margin-left', '14px')
1054 a.href = `https://translate.google.com/#auto/${browserLocale.split('-')[0]}/${encodeURIComponent(content)}`
1055 titleDiv.insertAdjacentElement('beforeEnd', a)
1056 }
1057 }
1058
1059 expandWhatIsItBox()
1060
1061 // fix locationEditsMap if only one location edit exists
1062 if (newPortalData.locationEdits.length <= 1 || subController.locationEditsMap.getZoom() > 19) {
1063 subController.locationEditsMap.setZoom(19)
1064 }
1065
1066 /* EDIT PORTAL */
1067 /* region keyboard navigation */
1068
1069 if (preferences.get(WFRT.OPTIONS.KEYBOARD_NAV)) {
1070 activateShortcuts()
1071 }
1072
1073 function activateShortcuts () {
1074 let currentSelectable = 0
1075 let hasLocationEdit = (newPortalData.locationEdits.length > 1)
1076 // counting *true*, please don't shoot me
1077 let maxItems = (newPortalData.descriptionEdits.length > 1) + (newPortalData.titleEdits.length > 1) + (hasLocationEdit) + 2
1078
1079 let mapMarkers
1080 if (hasLocationEdit) mapMarkers = subController.allLocationMarkers
1081 else mapMarkers = []
1082
1083 // a list of all 6 star button rows, and the two submit buttons
1084 let starsAndSubmitButtons = w.document.querySelectorAll(
1085 '.edit-container div[ng-show="subCtrl.pageData.titleEdits.length > 1"]:not(.ng-hide),' +
1086 '.edit-container div[ng-show="subCtrl.pageData.descriptionEdits.length > 1"]:not(.ng-hide),' +
1087 '.edit-container div[ng-show="subCtrl.pageData.locationEdits.length > 1"]:not(.ng-hide),' +
1088 '#submitFF')
1089
1090 /* EDIT PORTAL */
1091 function highlight () {
1092 let el = editDiv.querySelector('.poi-edit-map-unable')
1093 el.style.setProperty('border', 'none')
1094
1095 starsAndSubmitButtons.forEach((element) => { element.style.setProperty('border', 'none') })
1096 if (hasLocationEdit && currentSelectable === maxItems - 3) {
1097 el.style.setProperty('border-left', '4px dashed #ebbc4a')
1098 el.style.setProperty('border-top', '4px dashed #ebbc4a')
1099 el.style.setProperty('border-bottom', '4px dashed #ebbc4a')
1100 el.style.setProperty('border-right', '4px dashed #ebbc4a')
1101 el.style.setProperty('padding', '8px')
1102 // el.style.setProperty('margin-bottom', '0')
1103 submitAndNext.blur()
1104 submitButton.blur()
1105 } else if (currentSelectable < maxItems - 2) {
1106 starsAndSubmitButtons[currentSelectable].style.setProperty('border-left', '4px dashed #ebbc4a')
1107 starsAndSubmitButtons[currentSelectable].style.setProperty('padding-left', '16px')
1108 submitAndNext.blur()
1109 submitButton.blur()
1110 } else if (currentSelectable === maxItems - 2) {
1111 submitAndNext.focus()
1112 } else if (currentSelectable === maxItems) {
1113 submitButton.focus()
1114 }
1115 }
1116
1117 /* EDIT PORTAL */
1118 addEventListener('keydown', (event) => {
1119 /*
1120 Keycodes:
1121
1122 8: Backspace
1123 9: TAB
1124 13: Enter
1125 16: Shift
1126 27: Escape
1127 32: Space
1128 68: D
1129 107: NUMPAD +
1130 109: NUMPAD -
1131 111: NUMPAD /
1132
1133 49 - 53: Keys 1-5
1134 97 - 101: NUMPAD 1-5
1135 */
1136
1137 let numkey = null
1138 if (event.keyCode >= 49 && event.keyCode <= 53) {
1139 numkey = event.keyCode - 48
1140 } else if (event.keyCode >= 97 && event.keyCode <= 101) {
1141 numkey = event.keyCode - 96
1142 }
1143
1144 // do not do anything if a text area or a input with type text has focus
1145 if (w.document.querySelector('input[type=text]:focus') || w.document.querySelector('textarea:focus')) {
1146 return
1147 } else if ((event.keyCode === 13 || event.keyCode === 32) && w.document.querySelector('a.button[href="/review"]')) {
1148 // "analyze next" button
1149 w.document.location.href = '/review'
1150 event.preventDefault()
1151 } else if ((event.keyCode === 13 || event.keyCode === 32) && currentSelectable === maxItems) {
1152 // submit normal rating
1153 w.document.querySelector('[ng-click="answerCtrl.submitForm()"]').click()
1154 event.preventDefault()
1155 } else if (event.keyCode === 27 || event.keyCode === 111) {
1156 // return to first selection (should this be a portal)
1157 currentSelectable = 0
1158 } else if ((event.keyCode === 107 || event.keyCode === 9) && currentSelectable < maxItems) {
1159 // select next rating
1160 currentSelectable++
1161 event.preventDefault()
1162 } else if ((event.keyCode === 109 || event.keyCode === 16 || event.keyCode === 8) && currentSelectable > 0) {
1163 // select previous rating
1164 currentSelectable--
1165 event.preventDefault()
1166 } else if (numkey === null || currentSelectable > maxItems - 2) {
1167 return
1168 } else {
1169 // rating 1-5
1170 if (hasLocationEdit && currentSelectable === maxItems - 3 && numkey <= mapMarkers.length) {
1171 google.maps.event.trigger(angular.element(document.getElementById('NewSubmissionController')).scope().getAllLocationMarkers()[numkey - 1], 'click')
1172 } else {
1173 if (hasLocationEdit) {
1174 numkey = 1
1175 }
1176
1177 // starsAndSubmitButtons[currentSelectable].querySelectorAll('.poi-edit-box')[numkey - 1].click()
1178 currentSelectable++
1179 }
1180 }
1181 highlight()
1182 })
1183
1184 highlight()
1185 }
1186 }
1187
1188 // add map buttons
1189 function mapButtons (newPortalData, targetElement, where) {
1190 // coordinate format conversion
1191 const coordUtm33 = proj4('+proj=longlat', '+proj=utm +zone=33', [newPortalData.lng, newPortalData.lat])
1192 const coordUtm35 = proj4('+proj=longlat', '+proj=utm +zone=35', [newPortalData.lng, newPortalData.lat])
1193 const coordPuwg92 = proj4('+proj=longlat', '+proj=tmerc +lat_0=0 +lon_0=19 +k=0.9993 +x_0=500000 +y_0=-5300000 +ellps=GRS80 +units=m +no_defs', [newPortalData.lng, newPortalData.lat])
1194
1195 const mapButtons = `
1196<a class='btn btn-default' target='intel' href='https://intel.ingress.com/intel?ll=${newPortalData.lat},${newPortalData.lng}&z=17'>Intel</a>
1197<a class='btn btn-default' target='gmaps' href='https://www.google.com/maps/place/${newPortalData.lat},${newPortalData.lng}'>GMaps</a>
1198`
1199 // more map buttons in a dropdown menu
1200 const mapDropdown = `
1201<li><a target='osm' href='https://www.openstreetmap.org/?mlat=${newPortalData.lat}&mlon=${newPortalData.lng}&zoom=16'>OSM</a></li>
1202<li><a target='bing' href='https://bing.com/maps/default.aspx?cp=${newPortalData.lat}~${newPortalData.lng}&lvl=16&style=a'>bing</a></li>
1203<li><a target='heremaps' href='https://wego.here.com/?map=${newPortalData.lat},${newPortalData.lng},17,satellite'>HERE maps</a></li>
1204<li><a targeT='zoomearth' href='https://zoom.earth/#${newPortalData.lat},${newPortalData.lng},18z,sat'>Zoom Earth</a></li>
1205<li role='separator' class='divider'></li>
1206<li><a target='swissgeo' href='http://map.geo.admin.ch/?swisssearch=${newPortalData.lat},${newPortalData.lng}'>CH - Swiss Geo Map</a></li>
1207<li><a target='mapycz' href='https://mapy.cz/zakladni?x=${newPortalData.lng}&y=${newPortalData.lat}&z=17&base=ophoto&source=coor&id=${newPortalData.lng}%2C${newPortalData.lat}&q=${newPortalData.lng}%20${newPortalData.lat}'>CZ-mapy.cz (ortofoto)</a></li>
1208<li><a target='mapycz' href='https://mapy.cz/zakladni?x=${newPortalData.lng}&y=${newPortalData.lat}&z=17&base=ophoto&m3d=1&height=180&yaw=-279.39&pitch=-40.7&source=coor&id=${newPortalData.lng}%2C${newPortalData.lat}&q=${newPortalData.lng}%20${newPortalData.lat}'>CZ-mapy.cz (orto+3D)</a></li>
1209<li><a target='kompass' href='http://maps.kompass.de/#lat=${newPortalData.lat}&lon=${newPortalData.lng}&z=17'>DE - Kompass.maps</a></li>
1210<li><a target='bayernatlas' href='https://geoportal.bayern.de/bayernatlas/index.html?X=${newPortalData.lat}&Y=${newPortalData.lng}&zoom=14&lang=de&bgLayer=luftbild&topic=ba&catalogNodes=122'>DE - BayernAtlas</a></li>
1211<li><a target='pegel' href='http://opr.pegel.dk/?17/${newPortalData.lat}/${newPortalData.lng}'>DK - SDFE Orthophotos</a></li>
1212<li><a target='kortforsyningen' href='https://skraafoto.kortforsyningen.dk/oblivisionjsoff/index.aspx?project=Denmark&lon=${newPortalData.lng}&lat=${newPortalData.lat}'>DK - Kortforsyningen Skråfoto</a></li>
1213<li><a target='maanmittauslaitos' href='https://asiointi.maanmittauslaitos.fi/karttapaikka/?lang=en&share=customMarker&n=${coordUtm35[1].toFixed(3)}&e=${coordUtm35[0].toFixed(3)}&title=${encodeURIComponent(newPortalData.title)}&desc=&zoom=11&layers=%5B%7B%22id%22%3A2%2C%22opacity%22%3A100%7D%5D'>FI - Maanmittauslaitos</a></li>
1214<li><a target='paikkatietoikkuna' href='https://kartta.paikkatietoikkuna.fi/?zoomLevel=11&coord=${coordUtm35[0].toFixed(3)}_${coordUtm35[1].toFixed(3)}&mapLayers=801+100+default&uuid=90246d84-3958-fd8c-cb2c-2510cccca1d3&showMarker=true'>FI - Paikkatietoikkuna</a></li>
1215<li><a target='kakao' href='http://map.kakao.com/?map_type=TYPE_SKYVIEW&map_hybrid=true&q=${newPortalData.lat}%2C${newPortalData.lng}'>KR - Kakao map</a></li>
1216<li><a target='naver' href='http://map.naver.com/?menu=location&lat=${newPortalData.lat}&lng=${newPortalData.lng}&dLevel=14&title=CandidatePortalLocation'>KR - Naver map</a></li>
1217<li><a target='kartverket' href='http://norgeskart.no/#!?project=seeiendom&layers=1002,1014&zoom=17&lat=${coordUtm33[1].toFixed(2)}&lon=${coordUtm33[0].toFixed(2)}&sok=${newPortalData.lat},${newPortalData.lng}'>NO - Kartverket</a></li>
1218<li><a target='kulturminnesok' href='https://www.kulturminnesok.no/search?lat=${newPortalData.lat}&lng=${newPortalData.lng}'>NO - Kulturminnesøk</a></li>
1219<li><a target='norgeibilder' href='https://norgeibilder.no/?x=${Math.round(coordUtm33[0])}&y=${Math.round(coordUtm33[1])}&level=16&utm=33'>NO - Norge i Bilder</a></li>
1220<li><a target='finnno' href='http://kart.finn.no/?lng=${newPortalData.lng}&lat=${newPortalData.lat}&zoom=17&mapType=normap&markers=${newPortalData.lng},${newPortalData.lat},r,'>NO - Finn Kart</a></li>
1221<li><a target='toposvalbard' href='http://toposvalbard.npolar.no/?lat=${newPortalData.lat}&long=${newPortalData.lng}&zoom=17&layer=map'>NO - Polarinstituttet, Svalbard</a></li>
1222<li><a target='geoportal_pl' href='http://mapy.geoportal.gov.pl/imap/?actions=acShowWgButtonPanel_kraj_ORTO&bbox=${coordPuwg92[0] - 127},${coordPuwg92[1] - 63},${coordPuwg92[0] + 127},${coordPuwg92[1] + 63}'>PL - GeoPortal</a></li>
1223<li><a target='yandex' href='https://yandex.ru/maps/?ll=${newPortalData.lng},${newPortalData.lat}&z=18&mode=whatshere&whatshere%5Bpoint%5D=${newPortalData.lng},${newPortalData.lat}&whatshere%5Bzoom%5D=18'>RU - Yandex</a></li>
1224<li><a target='2GIS' href='https://2gis.ru/geo/${newPortalData.lng},${newPortalData.lat}?queryState=center/${newPortalData.lng},${newPortalData.lat}/zoom/13'>RU - 2GIS</a></li>
1225<li><a target='lantmateriet' href='https://kso.etjanster.lantmateriet.se/?e=${Math.round(coordUtm33[0])}&n=${Math.round(coordUtm33[1])}&z=13'>SE - Läntmateriet</a></li>
1226<li><a target='hitta' href='https://www.hitta.se/kartan!~${newPortalData.lat},${newPortalData.lng},18z/tileLayer!l=1'>SE - Hitta.se</a></li>
1227<li><a target='eniro' href='https://kartor.eniro.se/?c=${newPortalData.lat},${newPortalData.lng}&z=17&l=nautical'>SE - Eniro Sjökort</a></li>
1228`
1229 targetElement.insertAdjacentHTML(where, `<div id="wfrt_map_button_group" class='btn-group dropup'>${mapButtons}<div class='btn btn-default dropdown'><span class='caret'></span><ul id="wfrt_map_dropdown" class='dropdown-content dropdown-menu'>${mapDropdown}</div></div>`)
1230 }
1231
1232 // add new button "Submit and reload", skipping "Your analysis has been recorded." dialog
1233 function quickSubmitButton (submitDiv, ansController, bodyObserver) {
1234 let submitButton = submitDiv.querySelector('button.button-primary')
1235 // submitButton.classList.add('btn', 'btn-warning')
1236
1237 let submitAndNext = submitButton.cloneNode(false)
1238 submitButton.addEventListener('click', () => {
1239 bodyObserver.disconnect()
1240 })
1241 submitAndNext.innerHTML = `<span class="glyphicon glyphicon-floppy-disk"></span> <span class="glyphicon glyphicon-forward"></span>`
1242 submitAndNext.title = 'Submit and go to next review'
1243 submitAndNext.id = 'submitFF'
1244 submitAndNext.addEventListener('click', () => {
1245 ansController.openSubmissionCompleteModal = () => {
1246 window.location.assign('/review')
1247 }
1248 })
1249
1250 w.$injector.invoke(['$compile', ($compile) => {
1251 let compiledSubmit = $compile(submitAndNext)(w.$scope(submitDiv))
1252 submitDiv.querySelector('#submitDiv button').insertAdjacentElement('beforeBegin', compiledSubmit[0])
1253 }])
1254 return { submitButton, submitAndNext }
1255 }
1256
1257 function commentTemplates () {
1258 // add text buttons
1259 const textButtons = `
1260<button id='photo' class='btn btn-default textButton' data-tooltip='Indicates a low quality photo'>Photo</button>
1261<button id='private' class='btn btn-default textButton' data-tooltip='Located on private residential property'>Private</button>`
1262 const textDropdown = `
1263<li><a class='textButton' id='school' data-tooltip='Located on school property'>School</a></li>
1264<li><a class='textButton' id='person' data-tooltip='Photo contains 1 or more people'>Person</a></li>
1265<li><a class='textButton' id='perm' data-tooltip='Seasonal or temporary display or item'>Temporary</a></li>
1266<li><a class='textButton' id='location' data-tooltip='Location wrong'>Location</a></li>
1267<li><a class='textButton' id='natural' data-tooltip='Candidate is a natural feature'>Natural</a></li>
1268<li><a class='textButton' id='emergencyway' data-tooltip='Obstructing emergency way'>Emergency Way</a></li>
1269`
1270
1271 const cardAdditionalText = w.document.getElementById('additional-comments-card')
1272 const cardTextBox = cardAdditionalText.querySelector('textarea')
1273
1274 cardAdditionalText.insertAdjacentHTML('beforeend', `<div class="card__footer">
1275<span id="wfrt_comment_button_group" class='btn-group dropup pull-left'>${textButtons}
1276<span class='btn btn-default dropdown'><span class='caret'></span><ul id="wfrt_comment_button_dropdown" class='dropdown-content dropdown-menu'>${textDropdown}</ul>
1277</span></span><span class="hidden-xs pull-right"><button id='clear' class='btn btn-default textButton' data-tooltip='clears the comment box'>Clear</button></span></div>
1278`)
1279
1280 const buttons = w.document.getElementsByClassName('textButton')
1281 for (let b in buttons) {
1282 if (buttons.hasOwnProperty(b)) {
1283 buttons[b].addEventListener('click', event => {
1284 const source = event.target || event.srcElement
1285 let text = cardTextBox.value
1286 if (text.length > 0) {
1287 text += ', '
1288 }
1289 switch (source.id) {
1290 case 'photo':
1291 text += 'Low quality photo'
1292 break
1293 case 'private':
1294 text += 'Private residential property'
1295 break
1296 case 'duplicate':
1297 text += 'Duplicate of previously reviewed portal candidate'
1298 break
1299 case 'school':
1300 text += 'Located on primary or secondary school grounds'
1301 break
1302 case 'person':
1303 text += 'Picture contains one or more people'
1304 break
1305 case 'perm':
1306 text += 'Portal candidate is seasonal or temporary'
1307 break
1308 case 'location':
1309 text += 'Portal candidate\'s location is not on object'
1310 break
1311 case 'emergencyway':
1312 text += 'Portal candidate is obstructing the path of emergency vehicles'
1313 break
1314 case 'natural':
1315 text += 'Portal candidate is a natural feature'
1316 break
1317 case 'clear':
1318 text = ''
1319 break
1320 }
1321
1322 cardTextBox.value = text
1323 cardTextBox.dispatchEvent(new Event('change')) // eslint-disable-line no-undef
1324
1325 event.target.blur()
1326 }, false)
1327 }
1328 }
1329 }
1330
1331 // adding a 40m circle and a smaller 20m circle around the portal (capture range)
1332 function mapOriginCircle (map) {
1333 // noinspection JSUnusedLocalSymbols
1334 if (preferences.get(WFRT.OPTIONS.MAP_CIRCLE_40)) {
1335 const circle40 = new google.maps.Circle({ // eslint-disable-line no-unused-vars
1336 map: map,
1337 center: map.center,
1338 radius: 40,
1339 strokeColor: '#ebbc4a',
1340 strokeOpacity: 0.8,
1341 strokeWeight: 1.5,
1342 fillOpacity: 0
1343 })
1344 }
1345
1346 if (preferences.get(WFRT.OPTIONS.MAP_CIRCLE_20)) {
1347 const circle20 = new google.maps.Circle({ // eslint-disable-line no-unused-vars
1348 map: map,
1349 center: map.center,
1350 radius: 20,
1351 strokeColor: '#eddc4a',
1352 strokeOpacity: 0.8,
1353 strokeWeight: 1.5,
1354 fillOpacity: 0
1355 })
1356 }
1357 }
1358
1359 // replace map markers with a nice circle
1360 function mapMarker (markers) {
1361 for (let i = 0; i < markers.length; ++i) {
1362 const marker = markers[i]
1363 marker.setIcon(POI_MARKER)
1364 }
1365 }
1366
1367 // set available map types
1368 function mapTypes (map, isMainMap) {
1369 const PROVIDERS = {
1370 GOOGLE: 'google',
1371 KARTVERKET: 'kartverket'
1372 }
1373
1374 const types = [
1375 { provider: PROVIDERS.GOOGLE, id: 'roadmap' },
1376 { provider: PROVIDERS.GOOGLE, id: 'terrain' },
1377 { provider: PROVIDERS.GOOGLE, id: 'satellite' },
1378 { provider: PROVIDERS.GOOGLE, id: 'hybrid' }]
1379
1380 if (preferences.get(WFRT.OPTIONS.NORWAY_MAP_LAYER)) {
1381 types.push({ provider: PROVIDERS.KARTVERKET, id: `${PROVIDERS.KARTVERKET}_topo`, code: 'topo4', label: 'NO - Topo' },
1382 { provider: PROVIDERS.KARTVERKET, id: `${PROVIDERS.KARTVERKET}_raster`, code: 'toporaster3', label: 'NO - Raster' },
1383 { provider: PROVIDERS.KARTVERKET, id: `${PROVIDERS.KARTVERKET}_sjo`, code: 'sjokartraster', label: 'NO - Sjøkart' }
1384 )
1385 }
1386
1387 const defaultMapType = 'hybrid'
1388
1389 const mapOptions = {
1390 // re-enabling map scroll zoom and allow zoom with out holding ctrl
1391 scrollwheel: true,
1392 gestureHandling: 'greedy',
1393 // map type selection
1394 mapTypeControl: true,
1395 mapTypeControlOptions: {
1396 mapTypeIds: types.map(t => t.id),
1397 style: google.maps.MapTypeControlStyle.DROPDOWN_MENU
1398 }
1399 }
1400 map.setOptions(mapOptions)
1401
1402 // register custom map types
1403 types.forEach(t => {
1404 switch (t.provider) {
1405 case PROVIDERS.KARTVERKET:
1406 map.mapTypes.set(t.id, new google.maps.ImageMapType({
1407 layer: t.code,
1408 name: t.label,
1409 alt: t.label,
1410 maxZoom: 19,
1411 tileSize: new google.maps.Size(256, 256),
1412 getTileUrl: function (coord, zoom) {
1413 return `//opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=${this.layer}&zoom=${zoom}&x=${coord.x}&y=${coord.y}`
1414 }
1415 }))
1416 break
1417 }
1418 })
1419
1420 // track current selection for position map
1421 let mapType
1422 if (isMainMap) {
1423 mapType = WFRT.PREFIX + WFRT.VAR.MAP_TYPE_1
1424 } else {
1425 mapType = WFRT.PREFIX + WFRT.VAR.MAP_TYPE_2
1426 }
1427
1428 // save selection when changed
1429 map.addListener('maptypeid_changed', function () {
1430 w.localStorage.setItem(mapType, map.getMapTypeId())
1431 })
1432
1433 // get map type saved from last use or fall back to default
1434 map.setMapTypeId(w.localStorage.getItem(mapType) || defaultMapType)
1435 }
1436
1437 // // move submit button to right side of classification-div. don't move on mobile devices / small width
1438 // function moveSubmitButton () {
1439 // const submitDiv = w.document.querySelectorAll('#submitDiv, #submitDiv + .text-center')
1440 //
1441 // if (screen.availWidth > 768) {
1442 // let newSubmitDiv = w.document.createElement('div')
1443 // const classificationRow = w.document.querySelector('.classification-row')
1444 // newSubmitDiv.className = 'col-xs-12 col-sm-6'
1445 // submitDiv[0].style.setProperty('margin-top', '16px')
1446 // newSubmitDiv.appendChild(submitDiv[0])
1447 // newSubmitDiv.appendChild(submitDiv[1])
1448 // classificationRow.insertAdjacentElement('afterend', newSubmitDiv)
1449 //
1450 // // edit-page - remove .col-sm-offset-3 from .classification-row (why did you add this, niantic?
1451 // classificationRow.classList.remove('col-sm-offset-3')
1452 // return newSubmitDiv
1453 // } else {
1454 // return submitDiv[0]
1455 // }
1456 // }
1457
1458 // expand automatically the "What is it?" filter text box
1459 function expandWhatIsItBox () {
1460 try {
1461 const whatController = w.$scope(w.document.getElementById('WhatIsItController')).whatCtrl
1462 setTimeout(() => {
1463 whatController.showWhat = true
1464 w.$rootScope.$apply()
1465 }, 50)
1466 } catch (err) {}
1467 }
1468
1469 function modifyProfile () {
1470 // stats enhancements: add processed by nia, percent processed, progress to next recon badge numbers
1471
1472 let wfrtScannerOffset = 0
1473 if (preferences.get(WFRT.OPTIONS.SCANNER_OFFSET_FEATURE)) {
1474 // get scanner offset from localStorage
1475 wfrtScannerOffset = parseInt(w.localStorage.getItem(WFRT.SCANNER_OFFSET)) || 0
1476 }
1477 const stats = w.document.querySelector('#profile-stats:not(.visible-xs)')
1478
1479 const reviewed = parseInt(stats.children[0].children[0].children[1].innerText)
1480 const accepted = parseInt(stats.children[1].children[1].children[1].innerText)
1481 const rejected = parseInt(stats.children[1].children[2].children[1].innerText)
1482 const duplicated = parseInt(stats.children[1].children[3].children[1].innerText)
1483
1484 const processed = accepted + rejected +duplicated - wfrtScannerOffset
1485 const processedPercent = roundToPrecision(processed / reviewed * 100, 1)
1486
1487 const acceptedPercent = roundToPrecision(accepted / (reviewed) * 100, 1)
1488 const rejectedPercent = roundToPrecision(rejected / (reviewed) * 100, 1)
1489 const duplicatedPercent = roundToPrecision(duplicated / (reviewed) * 100, 1)
1490
1491 const reconBadge = { 100: 'Bronze', 750: 'Silver', 2500: 'Gold', 5000: 'Platin', 10000: 'Black' }
1492 let nextBadgeName, nextBadgeCount
1493
1494 for (const key in reconBadge) {
1495 if (processed <= key) {
1496 nextBadgeCount = key
1497 nextBadgeName = reconBadge[key]
1498 break
1499 }
1500 }
1501 const nextBadgeProcess = processed / nextBadgeCount * 100
1502
1503 const numberSpans = stats.querySelectorAll('span.stats-right')
1504
1505 numberSpans[0].insertAdjacentHTML('beforeend', `, <span class=''>100%</span>`)
1506 numberSpans[1].insertAdjacentHTML('beforeend', `, <span class=''>${acceptedPercent}%</span>`)
1507 numberSpans[2].insertAdjacentHTML('beforeend', `, <span class=''>${rejectedPercent}%</span>`)
1508 numberSpans[3].insertAdjacentHTML('beforeend', `, <span class=''>${duplicatedPercent}%</span>`)
1509
1510 stats.querySelectorAll('h4')[2].insertAdjacentHTML('afterend', `<br>
1511<h4><span class="stats-left">Processed <u>and</u> accepted analyses:</span> <span class="stats-right">${processed}, <span class="ingress-gray">${processedPercent}%</span></span></h4>`)
1512
1513 if (processed < 10000) {
1514 stats.insertAdjacentHTML('beforeEnd', `
1515<br><div>Next Ingress Recon badge tier: <b>${nextBadgeName} (${nextBadgeCount})</b><br>
1516<div class='progress'>
1517<div class='progress-bar progress-bar-warning'
1518role='progressbar'
1519aria-valuenow='${nextBadgeProcess}'
1520aria-valuemin='0'
1521aria-valuemax='100'
1522style='width: ${Math.round(nextBadgeProcess)}%;'
1523title='${nextBadgeCount - processed} to go'>
1524${Math.round(nextBadgeProcess)}%
1525</div></div></div>
1526`)
1527 } else stats.insertAdjacentHTML('beforeEnd', `<hr>`)
1528 stats.insertAdjacentHTML('beforeEnd', `<div><i class="glyphicon glyphicon-share"></i> <input readonly onFocus="this.select();" style="width: 90%;" type="text"
1529value="Reviewed: ${reviewed} / Processed: ${accepted + rejected + duplicated} (Created: ${accepted}/ Rejected: ${rejected}/ Duplicated: ${duplicated}) / ${Math.round(processedPercent)}%"/></div>`)
1530
1531 // ** wayfarer-scanner offset
1532 if (accepted < 10000 && preferences.get(WFRT.OPTIONS.SCANNER_OFFSET_UI)) {
1533 stats.insertAdjacentHTML('beforeEnd', `
1534<div id='scannerOffsetContainer'>
1535<span style="margin-left: 5px" class="ingress-mid-blue pull-left">Scanner offset:</span>
1536<input id="scannerOffset" onFocus="this.select();" type="text" name="scannerOffset" size="8" class="pull-right" value="${wfrtScannerOffset}">
1537</div>`)
1538
1539 // we have to inject the tooltip to angular
1540 w.$injector.invoke(['$compile', ($compile) => {
1541 let compiledSubmit = $compile(`<span class="glyphicon glyphicon-info-sign ingress-gray pull-left" uib-tooltip-trigger="outsideclick" uib-tooltip-placement="left" tooltip-class="goldBorder" uib-tooltip="Use negative values, if scanner is ahead of Wayfarer"></span>`)(w.$scope(stats))
1542 w.document.getElementById('scannerOffsetContainer').insertAdjacentElement('afterbegin', compiledSubmit[0])
1543 }]);
1544
1545 ['change', 'keyup', 'cut', 'paste', 'input'].forEach(e => {
1546 w.document.getElementById('scannerOffset').addEventListener(e, (event) => {
1547 w.localStorage.setItem(WFRT.SCANNER_OFFSET, event.target.value)
1548 })
1549 })
1550 // **
1551 }
1552
1553 modifyProfile = () => {} // eslint-disable-line
1554 }
1555
1556 function addOptionsButton () {
1557 // Add preferences button only once
1558 if (w.document.getElementById('wfrt_preferences_button') !== null) {
1559 return
1560 }
1561
1562 // add wayfarer-tools preferences button
1563 let wfrtPreferencesButton = w.document.createElement('a')
1564 wfrtPreferencesButton.classList.add('brand', 'upgrades-icon', 'pull-right')
1565 wfrtPreferencesButton.addEventListener('click', () => preferences.showPreferencesUI(w))
1566 wfrtPreferencesButton.title = 'Wayfarer-Tools Preferences'
1567 wfrtPreferencesButton.setAttribute('id', 'wfrt_preferences_button')
1568
1569 const prefCog = w.document.createElement('span')
1570 prefCog.classList.add('glyphicon', 'glyphicon-cog')
1571 wfrtPreferencesButton.appendChild(prefCog)
1572
1573 w.document.querySelector('.header .inner-container:last-of-type').insertAdjacentElement('afterbegin', wfrtPreferencesButton)
1574 }
1575
1576 function addRefreshContainer () {
1577 let cbxRefresh = w.document.createElement('input')
1578 let cbxRefreshDesktop = w.document.createElement('input')
1579
1580 cbxRefresh.id = WFRT.OPTIONS.REFRESH
1581 cbxRefresh.type = 'checkbox'
1582 cbxRefresh.checked = preferences.get(WFRT.OPTIONS.REFRESH) === 'true'
1583
1584 cbxRefreshDesktop.id = WFRT.OPTIONS.REFRESH_NOTI_DESKTOP
1585 cbxRefreshDesktop.type = 'checkbox'
1586 cbxRefreshDesktop.checked = preferences.get(WFRT.OPTIONS.REFRESH_NOTI_DESKTOP) === 'true'
1587
1588 let refreshPanel = w.document.createElement('div')
1589 refreshPanel.className = 'panel panel-ingress'
1590
1591 refreshPanel.addEventListener('change', (event) => {
1592 preferences.set(event.target.id, event.target.checked)
1593 if (event.target.checked) {
1594 startRefresh()
1595 } else {
1596 stopRefresh()
1597 }
1598 })
1599
1600 refreshPanel.innerHTML = `
1601<div class='panel-heading'><span class='glyphicon glyphicon-refresh'></span> Refresh <sup>beta</sup> <a href='https://gitlab.com/1110101/opr-tools'><span class='label label-success pull-right'>Wayfarer-Tools</span></a></div>
1602<div id='cbxDiv' class='panel-body bg-primary' style='background:black;'></div>`
1603
1604 refreshPanel.querySelector('#cbxDiv').insertAdjacentElement('afterbegin', appendCheckbox(cbxRefreshDesktop, 'Desktop notification'))
1605 refreshPanel.querySelector('#cbxDiv').insertAdjacentElement('afterbegin', appendCheckbox(cbxRefresh, 'Refresh every 5-10 minutes'))
1606
1607 let colDiv = w.document.createElement('div')
1608 colDiv.className = 'col-md-4 col-md-offset-4'
1609 colDiv.appendChild(refreshPanel)
1610
1611 let rowDiv = w.document.createElement('div')
1612 rowDiv.className = 'row'
1613 rowDiv.appendChild(colDiv)
1614
1615 w.document.getElementById('NewSubmissionController').insertAdjacentElement('beforeend', rowDiv)
1616
1617 cbxRefresh.checked === true ? startRefresh() : stopRefresh()
1618
1619 function appendCheckbox (checkbox, text) {
1620 let label = w.document.createElement('label')
1621 let div = w.document.createElement('div')
1622 div.className = 'checkbox'
1623 label.appendChild(checkbox)
1624 label.appendChild(w.document.createTextNode(text))
1625 div.appendChild(label)
1626 return div
1627 }
1628
1629 addRefreshContainer = () => {} // eslint-disable-line
1630 }
1631
1632 let refreshIntervalID
1633
1634 function startRefresh () {
1635 let time = getRandomIntInclusive(5, 10) * 60000
1636
1637 refreshIntervalID = setInterval(() => {
1638 reloadWayfarer()
1639 }, time)
1640
1641 function reloadWayfarer () {
1642 clearInterval(refreshIntervalID)
1643 w.sessionStorage.setItem(WFRT.FROM_REFRESH, 'true')
1644 w.document.location.reload()
1645 }
1646
1647 // source https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
1648 function getRandomIntInclusive (min, max) {
1649 min = Math.ceil(min)
1650 max = Math.floor(max)
1651 return Math.floor(Math.random() * (max - min + 1)) + min
1652 }
1653 }
1654
1655 function stopRefresh () {
1656 clearInterval(refreshIntervalID)
1657 }
1658
1659 function checkIfAutorefresh () {
1660 if (w.sessionStorage.getItem(WFRT.FROM_REFRESH)) {
1661 // reset flag
1662 w.sessionStorage.removeItem(WFRT.FROM_REFRESH)
1663
1664 if (w.document.hidden) { // if tab in background: flash favicon
1665 let flag = true
1666
1667 if (preferences.get(WFRT.OPTIONS.REFRESH_NOTI_DESKTOP) === 'true') {
1668 GM_notification({
1669 'title': 'Wayfarer - New Wayspot Analysis Available',
1670 'text': 'by Wayfarer-Tools',
1671 'image': 'https://gitlab.com/uploads/-/system/project/avatar/3311015/opr-tools.png'
1672 })
1673 }
1674
1675 let flashId = setInterval(() => {
1676 flag = !flag
1677 changeFavicon(`${flag ? POI_MARKER : '/imgpub/favicon.ico'}`)
1678 }, 1000)
1679
1680 // stop flashing if tab in foreground
1681 addEventListener('visibilitychange', () => {
1682 if (!w.document.hidden) {
1683 changeFavicon('/imgpub/favicon.ico')
1684 clearInterval(flashId)
1685 }
1686 })
1687 }
1688 }
1689 }
1690
1691 function changeFavicon (src) {
1692 let link = w.document.querySelector('link[rel="shortcut icon"]')
1693 link.href = src
1694 }
1695
1696 function startExpirationTimer (subController) {
1697 w.document.querySelector('.header .inner-container:last-of-type').insertAdjacentHTML('afterbegin', '<span id="countdownDisplay"></span>')
1698
1699 let countdownEnd = subController.pageData.expires
1700 let countdownDisplay = document.getElementById('countdownDisplay')
1701 countdownDisplay.style.setProperty('color', 'black')
1702
1703 // Update the count down every 1 second
1704 let counterInterval = setInterval(function () {
1705 // Get todays date and time
1706 let now = new Date().getTime()
1707 // Find the distance between now an the count down date
1708 let distance = countdownEnd - now
1709 // Time calculations for minutes and seconds
1710 let minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60))
1711 let seconds = Math.floor((distance % (1000 * 60)) / 1000)
1712
1713 // Display the result in the element
1714 countdownDisplay.innerText = `${minutes}m ${seconds}s `
1715
1716 if (distance < 0) {
1717 // If the count down is finished, write some text
1718 clearInterval(counterInterval)
1719 countdownDisplay.innerText = 'EXPIRED'
1720 countdownDisplay.style.setProperty('color', 'red')
1721 } else if (distance < 90000) {
1722 countdownDisplay.style.setProperty('color', 'red')
1723 }
1724 }, 1000)
1725 }
1726
1727 function versionCheck () {
1728 if (WFRT.VERSION > (parseInt(w.localStorage.getItem(WFRT.PREFIX + WFRT.VERSION_CHECK)) || WFRT.VERSION - 1)) {
1729 w.localStorage.setItem(WFRT.PREFIX + WFRT.VERSION_CHECK, WFRT.VERSION)
1730
1731 const changelogString = `
1732 <h4><span class="glyphicon glyphicon-asterisk"></span> Wayfarer-Tools was updated:</h4>
1733 <div>${strings.changelog}</div>
1734 `
1735 // show changelog
1736 alertify.closeLogOnClick(false).logPosition('bottom right').delay(0).log(changelogString, (ev) => {
1737 ev.preventDefault()
1738 ev.target.closest('div.default.show').remove()
1739 }).reset()
1740 }
1741 }
1742
1743 function addCustomPresetButtons () {
1744 // add customPreset UI
1745 wfrtCustomPresets = getCustomPresets(w)
1746 let customPresetOptions = ''
1747 for (const customPreset of wfrtCustomPresets) {
1748 customPresetOptions += `<button class='btn btn-default customPresetButton' id='${customPreset.uid}'>${customPreset.label}</button>`
1749 }
1750 w.document.getElementById('wfrt_custom_presets').innerHTML = customPresetOptions
1751 }
1752
1753 function getCustomPresets (w) {
1754 // simply to scope the string we don't need after JSON.parse
1755 let presetsJSON = w.localStorage.getItem(WFRT.PREFIX + WFRT.VAR.CUSTOM_PRESETS)
1756 if (presetsJSON != null && presetsJSON !== '') {
1757 return JSON.parse(presetsJSON)
1758 }
1759 return []
1760 }
1761
1762 function saveCustomPreset (label, ansController, whatController) {
1763 // uid snippet from https://stackoverflow.com/a/47496558/6447397
1764 let preset = {
1765 uid: [...Array(5)].map(() => Math.random().toString(36)[3]).join(''),
1766 label: label,
1767 nodeName: whatController.whatNode.name,
1768 nodeId: whatController.whatNode.id,
1769 quality: ansController.formData.quality,
1770 description: ansController.formData.description,
1771 cultural: ansController.formData.cultural,
1772 uniqueness: ansController.formData.uniqueness,
1773 location: ansController.formData.location,
1774 safety: ansController.formData.safety
1775 }
1776 wfrtCustomPresets.push(preset)
1777 w.localStorage.setItem(WFRT.PREFIX + WFRT.VAR.CUSTOM_PRESETS, JSON.stringify(wfrtCustomPresets))
1778 }
1779
1780 function deleteCustomPreset (preset) {
1781 wfrtCustomPresets = wfrtCustomPresets.filter(item => item.uid !== preset.uid)
1782 w.localStorage.setItem(WFRT.PREFIX + WFRT.VAR.CUSTOM_PRESETS, JSON.stringify(wfrtCustomPresets))
1783 }
1784
1785 function showHelp () {
1786 let helpString = `<a href='https://gitlab.com/1110101/opr-tools'><span class='label label-success'>Wayfarer-Tools</span></a> Key shortcuts<br>
1787 <table class="table table-condensed ">
1788 <thead>
1789 <tr>
1790 <th>Keys</th>
1791 <th>Function</th>
1792 </tr>
1793 </thead>
1794 <tbody>
1795 <tr>
1796 <td><kbd>Keys 1-5</kbd> / <kbd>Numpad 1-5</kbd></td>
1797 <td>Valuate current selected field (the yellow highlighted one)</td>
1798 </tr>
1799 <tr>
1800 <td><kbd>Shift</kbd> + <kbd>Keys 1-5</kbd></td>
1801 <td>Apply custom preset (if exists)</td>
1802 </tr>
1803 <tr>
1804 <td><kbd>Keys 1-7</kbd> / <kbd>Numpad 1-7</kbd></td>
1805 <td>Rejection popup: Select list element</td>
1806 </tr>
1807 <tr>
1808 <td><kbd>D</kbd></td>
1809 <td>Mark current candidate as a duplicate of the opened portal in "duplicates"</td>
1810 </tr>
1811 <tr>
1812 <td><kbd>T</kbd></td>
1813 <td>Open title translation</td>
1814 </tr>
1815 <tr>
1816 <td><kbd>Y</kbd></td>
1817 <td>Open description translation</td>
1818 </tr>
1819 <tr>
1820 <td><kbd>U</kbd></td>
1821 <td>Open supporting statement translation</td>
1822 </tr>
1823 <tr>
1824 <td><kbd>Space</kbd> / <kbd>Enter</kbd> / <kbd>Numpad Enter</kbd></td>
1825 <td>Confirm dialog / Send valuation</td>
1826 </tr>
1827 <tr>
1828 <td><kbd>Tab</kbd> / <kbd>Numpad +</kbd></td>
1829 <td>Next field</td>
1830 </tr>
1831 <tr>
1832 <td><kbd>Shift</kbd> / <kbd>Backspace</kbd> / <kbd>Numpad -</kbd></td>
1833 <td>Previous field</td>
1834 </tr>
1835 <tr>
1836 <td><kbd>Esc</kbd> / <kbd>Numpad /</kbd></td>
1837 <td>First field</td>
1838 </tr>
1839 <tr>
1840 <td><kbd>^</kbd> / <kbd>Numpad *</kbd></td>
1841 <td>Skip Portal (if possible)</td>
1842 </tr>
1843 </tbody>
1844 </table>`
1845
1846 alertify.closeLogOnClick(false).logPosition('bottom right').delay(0).log(helpString, (ev) => {
1847 ev.preventDefault()
1848 ev.target.closest('div.default.show').remove()
1849 }).reset()
1850 }
1851
1852 function roundToPrecision (num, precision) {
1853 let shifter
1854 precision = Number(precision || 0)
1855 if (precision % 1 !== 0) throw new RangeError('precision must be an integer')
1856 shifter = Math.pow(10, precision)
1857 return Math.round(num * shifter) / shifter
1858 }
1859}
1860
1861setTimeout(() => {
1862 init()
1863}, 250)
1864
1865// region const
1866
1867const strings = {
1868 options: {
1869 [WFRT.OPTIONS.COMMENT_TEMPLATES]: 'Comment templates',
1870 [WFRT.OPTIONS.KEYBOARD_NAV]: 'Keyboard navigation',
1871 [WFRT.OPTIONS.NORWAY_MAP_LAYER]: 'Norwegian map layer',
1872 [WFRT.OPTIONS.PRESET_FEATURE]: 'Rating presets',
1873 [WFRT.OPTIONS.REFRESH]: 'Periodically refresh wayfarer if no analysis is available',
1874 [WFRT.OPTIONS.REFRESH_NOTI_DESKTOP]: '↳ With desktop notification',
1875 [WFRT.OPTIONS.SCANNER_OFFSET_FEATURE]: 'Scanner offset',
1876 [WFRT.OPTIONS.SCANNER_OFFSET_UI]: '↳ Display offset input field',
1877 [WFRT.OPTIONS.MAP_CIRCLE_20]: 'Show 20 meter circle around candidate location (minimum portal distance)',
1878 [WFRT.OPTIONS.MAP_CIRCLE_40]: 'Show 40 meter circle around candidate location (capture range)'
1879 },
1880 changelog:
1881 `
1882Version 2.0.6
1883<br>* Added shortcut key U to open supporting statement translation
1884<br>* Fixed countdown timer and percentage breakdowns (thanks to @fotofreund0815)
1885`
1886}
1887
1888const GLOBAL_CSS = `
1889.dropdown {
1890position: relative;
1891display: inline-block;
1892}
1893
1894.dropdown-content {
1895display: none;
1896position: absolute;
1897z-index: 1;
1898margin: 0;
1899}
1900
1901.dropdown:hover .dropdown-content {
1902display: block;
1903}
1904
1905.dropdown-menu > li > a:focus, .dropdown-menu > li > a:hover {
1906background-color: unset;
1907}
1908
1909.dropdown .dropdown-menu {
1910left: 0px;
1911right: unset;
1912width: unset;
1913}
1914
1915.modal-sm {
1916width: 350px !important;
1917}
1918
1919.panel-ingress {
1920background-color: #004746;
1921border: 1px solid #0ff;
1922border-radius: 1px;
1923box-shadow: inset 0 0 6px rgba(255, 255, 255, 1);
1924color: #0ff;
1925}
1926
1927[data-tooltip] {
1928position: relative;
1929cursor: pointer;
1930}
1931
1932[data-tooltip]:before,
1933[data-tooltip]:after {
1934visibility: hidden;
1935-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
1936filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0);
1937opacity: 0;
1938pointer-events: none;
1939}
1940
1941[data-tooltip]:before {
1942position: absolute;
1943top: 150%;
1944left: 50%;
1945margin-bottom: 5px;
1946margin-left: -80px;
1947padding: 7px;
1948width: relative;
1949-webkit-border-radius: 3px;
1950-moz-border-radius: 3px;
1951border-radius: 3px;
1952background-color: #000;
1953background-color: hsla(0, 0%, 20%, 0.9);
1954color: #fff;
1955content: attr(data-tooltip);
1956text-align: center;
1957font-size: 14px;
1958line-height: 1.2;
1959z-index: 100;
1960}
1961
1962[data-tooltip]:after {
1963position: absolute;
1964top: 132%;
1965left: relative;
1966width: 0;
1967border-bottom: 5px solid #000;
1968border-bottom: 5px solid hsla(0, 0%, 20%, 0.9);
1969border-right: 5px solid transparent;
1970border-left: 5px solid transparent;
1971content: " ";
1972font-size: 0;
1973line-height: 0;
1974}
1975
1976[data-tooltip]:hover:before,
1977[data-tooltip]:hover:after {
1978visibility: visible;
1979-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
1980filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
1981opacity: 1;
1982}
1983
1984.titleEditBox:hover {
1985box-shadow: inset 0 0 20px #ebbc4a;
1986}
1987
1988.titleEditBox:active {
1989box-shadow: inset 0 0 15px 2px white;
1990}
1991
1992.group-list li label:hover, ul.sub-group-list a:hover, #root-label:hover {
1993box-shadow: inset 0 0 5px #000000 !important;
1994}
1995
1996.group-list li label:active, ul.sub-group-list a:active, #root-label:active {
1997box-shadow: inset 0 0 10px 2px #000000 !important;
1998}
1999
2000.modal-body .button:focus, .modal-body textarea:focus {
2001outline: 2px dashed #ebbc4a;
2002}
2003
2004.modal-body .button:hover, .gm-style-iw button.button:hover {
2005filter: brightness(150%);
2006}
2007
2008.alertify-logs {
2009z-index: 100;
2010}
2011
2012.alertify .dialog .msg {
2013color: black;
2014}
2015
2016.alertify-logs > .default {
2017background-image: url(/img/ingress-background-dark.png) !important;
2018}
2019
2020.btn-xs {
2021margin-left: 8px;
2022padding: 0px 7px 1px !important;
2023box-shadow: inset 0 0 4px rgba(255, 255, 255, 1);
2024-webkit-box-shadow: inset 0 0 4px rgba(255, 255, 255, 1);
2025-moz-box-shadow: inset 0 0 4px rgba(255, 255, 255, 1);
2026}
2027
2028kbd {
2029display: inline-block;
2030padding: 3px 5px;
2031font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace;
2032line-height: 10px;
2033color: #444d56;
2034vertical-align: middle;
2035background-color: #fafbfc;
2036border: 1px solid #d1d5da;
2037border-bottom-color: #c6cbd1;
2038border-radius: 3px;
2039box-shadow: inset 0 -1px 0 #c6cbd1;
2040}
2041
2042.dropdown-menu {
2043margin: 0 !important;
2044}
2045
2046.opr-yellow {
2047color: #F3EADA;
2048}
2049
2050#submitAndSkipLowQuality, #submitAndSkipDuplicate {
2051margin-left: 32px;
2052margin-right: 32px;
2053}
2054
2055#profile-stats > div {
2056width: 60%;
2057}
2058#scannerOffsetContainer {
2059margin-top: 16px;
2060}
2061
2062#wfrt_preferences_button {
2063 cursor: pointer;
2064 margin-right: 20px;
2065 margin-left: 20px;
2066 color: rgb(157,157,157);
2067}
2068#wfrt_custom_presets_card {
2069 width: 100%;
2070 height: auto;
2071 min-height: unset;
2072 margin-left: 15px;
2073}
2074#submitFF {
2075margin-right: 16px;
2076}
2077
2078@media (min-width: 768px) {
2079div.modal-custom1 {
2080width: 500px;
2081max-width: unset !important;
2082}
2083}
2084`
2085
2086const POI_MARKER = `data:image/png;base64,
2087iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACx
2088jwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAu
2089OWwzfk4AAADlSURBVDhPY/j//z8CTw3U/V8lcvx/MfPX/2Xcd//XyWwDYxAbJAaS63c2Q9aD0Nyg
2090UPS/hPXt/3bD5f93LI7DwFvnJILlSlg//K+XrUc1AKS5jOvx/wU55Vg1I2OQmlKOpzBDIM4G2UyM
2091ZhgGqQW5BOgdBrC/cDkbHwbpAeplAAcONgWEMChMgHoZwCGMTQExGKiXARxN2CSJwUC9VDCAYi9Q
2092HIhVQicpi0ZQ2gYlCrITEigpg5IlqUm5VrILkRdghoBMxeUd5MwE1YxqAAiDvAMKE1DAgmIHFMUg
2093DGKDxDCy838GAPWFoAEBs2EvAAAAAElFTkSuQmCC`
2094
2095// TG SVG Icon from https://commons.wikimedia.org/wiki/File:Telegram_logo.svg
2096const TG_SVG = `
2097<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" style="
2098 width: 16px;
2099 height: 16px;
2100">
2101<defs>
2102 <linearGradient id="b" x1="0.6667" y1="0.1667" x2="0.4167" y2="0.75">
2103 <stop stop-color="#37aee2" offset="0"></stop>
2104 <stop stop-color="#1e96c8" offset="1"></stop>
2105 </linearGradient>
2106 <linearGradient id="w" x1="0.6597" y1="0.4369" x2="0.8512" y2="0.8024">
2107 <stop stop-color="#eff7fc" offset="0"></stop>
2108 <stop stop-color="#fff" offset="1"></stop>
2109 </linearGradient>
2110</defs>
2111<circle cx="120" cy="120" r="120" fill="url(#b)"></circle>
2112<path fill="#c8daea" d="m98 175c-3.8876 0-3.227-1.4679-4.5678-5.1695L82
2113132.2059 170 80"></path>
2114<path fill="#a9c9dd" d="m98 175c3 0 4.3255-1.372
21156-3l16-15.558-19.958-12.035"></path>
2116<path fill="url(#w)" d="m100.04 144.41 48.36 35.729c5.5185 3.0449 9.5014
21171.4684
211810.876-5.1235l19.685-92.763c2.0154-8.0802-3.0801-11.745-8.3594-9.3482l-115.59
2119 44.571c-7.8901 3.1647-7.8441 7.5666-1.4382 9.528l29.663
2120 9.2583 68.673-43.325c3.2419-1.9659 6.2173-0.90899 3.7752 1.2584"></path>
2121</svg>`
2122// endregion