· 6 years ago · Jan 29, 2020, 08:26 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 107: +
837
838 49 - 53: Keys 1-5
839 97 - 101: NUMPAD 1-5
840
841 */
842
843 let numkey = null
844 if (event.keyCode >= 49 && event.keyCode <= 55) {
845 numkey = event.keyCode - 48
846 } else if (event.keyCode >= 97 && event.keyCode <= 103) {
847 numkey = event.keyCode - 96
848 }
849
850 if (event.keyCode === 107) {
851
852 let submitButton = document.getElementById('submitFF');
853
854 submitButton.click();
855
856 }
857
858 // do not do anything if a text area or a input with type text has focus
859 if (w.document.querySelector('input[type=text]:focus') || w.document.querySelector('textarea:focus')) {
860 return
861 } else if ((event.keyCode === 13 || event.keyCode === 32) && w.document.querySelector('a.button[href="/review"]')) {
862 // "analyze next" button
863 w.document.location.href = '/review'
864 event.preventDefault()
865 } else if ((event.keyCode === 13 || event.keyCode === 32) && w.document.querySelector('[ng-click="answerCtrl2.confirmLowQuality()"]')) {
866 // submit low quality rating
867 w.document.querySelector('[ng-click="answerCtrl2.confirmLowQuality()"]').click()
868 currentSelectable = 0
869 event.preventDefault()
870 } else if ((event.keyCode === 13 || event.keyCode === 32) && w.document.querySelector('[ng-click="answerCtrl2.confirmLowQualityOld()"]')) {
871 // submit low quality rating alternate
872 w.document.querySelector('[ng-click="answerCtrl2.confirmLowQualityOld()"]').click()
873 currentSelectable = 0
874 event.preventDefault()
875 } else if ((event.keyCode === 68) && w.document.querySelector('#content > button')) {
876 // click first/selected duplicate (key D)
877 w.document.querySelector('#content > button').click()
878 currentSelectable = 0
879 event.preventDefault()
880 } else if (event.keyCode === 84) {
881 // click on translate title link (key T)
882 const link = w.document.querySelector('#wfrt_translate_title')
883 if (link) {
884 link.click()
885 event.preventDefault()
886 }
887 } else if (event.keyCode === 89) {
888 // click on translate description link (key Y)
889 const link = w.document.querySelector('#wfrt_translate_desc')
890 if (link) {
891 link.click()
892 event.preventDefault()
893 }
894 } else if (event.keyCode === 85) {
895 // click on translate extra info link (key U)
896 const link = w.document.querySelector('#wfrt_translate_support')
897 if (link) {
898 link.click()
899 event.preventDefault()
900 }
901 }
902 else if ((event.keyCode === 13 || event.keyCode === 32) && w.document.querySelector('[ng-click="answerCtrl2.confirmDuplicate()"]')) {
903 // submit duplicate
904 w.document.querySelector('[ng-click="answerCtrl2.confirmDuplicate()"]').click()
905 currentSelectable = 0
906 event.preventDefault()
907 } else if ((event.keyCode === 13 || event.keyCode === 32) && currentSelectable === maxItems) {
908 // submit normal rating
909 w.document.querySelector('[ng-click="answerCtrl.submitForm()"]').click()
910 event.preventDefault()
911 } else if ((event.keyCode === 27 || event.keyCode === 111) && w.document.querySelector('[ng-click="answerCtrl2.resetDuplicate()"]')) {
912 // close duplicate dialog
913 w.document.querySelector('[ng-click="answerCtrl2.resetDuplicate()"]').click()
914 currentSelectable = 0
915 event.preventDefault()
916 } else if ((event.keyCode === 27 || event.keyCode === 111) && w.document.querySelector('[ng-click="answerCtrl2.resetLowQuality()"]')) {
917 // close low quality ration dialog
918 w.document.querySelector('[ng-click="answerCtrl2.resetLowQuality()"]').click()
919 currentSelectable = 0
920 event.preventDefault()
921 } else if (event.keyCode === 27 || event.keyCode === 111) {
922 // return to first selection (should this be a portal)
923 currentSelectable = 0
924 event.preventDefault()
925 } else if (event.keyCode === 106 || event.keyCode === 220) {
926 // skip portal if possible
927 if (newPortalData.canSkip) {
928 ansController.skipToNext()
929 }
930 } else if (event.keyCode === 72) {
931 showHelp() // @todo
932 } else if (w.document.querySelector('[ng-click="answerCtrl2.confirmLowQuality()"]')) {
933 // Reject reason shortcuts
934 if (numkey != null) {
935 if (selectedReasonGroup === -1) {
936 try {
937 w.document.getElementById('sub-group-' + numkey).click()
938 selectedReasonGroup = numkey - 1
939 w.document.querySelectorAll('label.sub-group kbd').forEach(el => el.classList.add('hide'))
940 } catch (err) {}
941 } else {
942 if (selectedReasonSubGroup === -1) {
943 try {
944 w.document.querySelectorAll('#reject-reason ul ul')[selectedReasonGroup].children[numkey - 1].children[0].click()
945 selectedReasonSubGroup = numkey - 1
946 } catch (err) {}
947 } else {
948 w.document.getElementById('root-label').click()
949 selectedReasonGroup = -1
950 selectedReasonSubGroup = -1
951 w.document.querySelectorAll('label.sub-group kbd').forEach(el => el.classList.remove('hide'))
952 }
953 }
954 event.preventDefault()
955 }
956 } else if ((event.keyCode === 107 || event.keyCode === 9) && currentSelectable < maxItems) {
957 // select next rating
958 currentSelectable++
959 event.preventDefault()
960 } else if ((event.keyCode === 109 || event.keyCode === 16 || event.keyCode === 8) && currentSelectable > 0) {
961 // select previous rating
962 currentSelectable--
963 event.preventDefault()
964 } else if (numkey === null || currentSelectable > maxItems - 2) {
965 return
966 } else if (numkey !== null && event.shiftKey) {
967 try {
968 w.document.getElementsByClassName('customPresetButton')[numkey - 1].click()
969 if (!document.getElementById('submitFF').disabled) {
970 currentSelectable = 6
971 highlight()
972 }
973 } catch (e) {
974 // ignore
975 }
976 } else {
977 // rating 1-5
978 starsAndSubmitButtons[currentSelectable].querySelectorAll('button.button-star')[numkey - 1].click()
979 currentSelectable++
980 }
981 highlight()
982 })
983
984 highlight()
985 }
986
987 /* endregion keyboard nav */
988
989 modifyNewPage = () => {} // eslint-disable-line
990 }
991
992 function modifyEditPage (ansController, subController, newPortalData) {
993 let editDiv = w.document.querySelector('div[ng-show="subCtrl.reviewType===\'EDIT\'"]')
994
995 mapButtons(newPortalData, editDiv, 'afterEnd')
996
997 // mutation observer
998 const bodyObserver = new MutationObserver(mutationList => {
999 for (let mutationRecord of mutationList) {
1000 // we just want addednodes with (class:modal). null and undefined check for performance reasons
1001 if (mutationRecord.addedNodes.length > 0 &&
1002 mutationRecord.addedNodes[0].className === 'modal fade ng-isolate-scope' &&
1003 mutationRecord.addedNodes[0].querySelector('.modal-body a[href=\'/review\']') !== null) {
1004 w.document.location.href = '/review'
1005 }
1006 }
1007 })
1008 bodyObserver.observe(w.document.body, { childList: true })
1009
1010 let newSubmitDiv = w.document.querySelector('.answer-btn-container.bottom-btns')
1011 let { submitButton, submitAndNext } = quickSubmitButton(newSubmitDiv, ansController, bodyObserver)
1012
1013 if (preferences.get(WFRT.OPTIONS.COMMENT_TEMPLATES)) { commentTemplates() }
1014
1015 mapTypes(subController.locationEditsMap, true)
1016
1017 // add translation links to title and description edits
1018 if (newPortalData.titleEdits.length > 1 || newPortalData.descriptionEdits.length > 1) {
1019 for (const titleEditBox of editDiv.querySelectorAll('.titleEditBox.ng-scope')) {
1020 const contentSpan = titleEditBox.querySelector('.poi-edit-text')
1021 let a = w.document.createElement('a')
1022 let span = w.document.createElement('span')
1023 span.className = 'glyphicon glyphicon-book'
1024 span.innerHTML = ' '
1025 a.appendChild(span)
1026 a.className = 'translate-title button btn btn-default pull-right'
1027 a.target = 'translate'
1028 a.style.setProperty('padding', '0px 4px')
1029 a.href = `https://translate.google.com/#auto/${browserLocale.split('-')[0]}/${encodeURIComponent(contentSpan.innerText.trim())}`
1030 contentSpan.style.setProperty('display', 'inline-block')
1031 contentSpan.insertAdjacentElement('beforeEnd', a)
1032 }
1033 }
1034
1035 if (newPortalData.titleEdits.length <= 1) {
1036 let titleDiv = editDiv.querySelector('div[ng-if="!answerCtrl.needsTitleEdit"]')
1037 let a = w.document.createElement('a')
1038 let span = w.document.createElement('span')
1039 span.className = 'glyphicon glyphicon-book'
1040 span.innerHTML = ' '
1041 a.appendChild(span)
1042 a.className = 'translate-title btn btn-default'
1043 a.target = 'translate'
1044 a.style.setProperty('padding', '0px 4px')
1045 a.style.setProperty('margin-left', '14px')
1046 a.href = `https://translate.google.com/#auto/${browserLocale.split('-')[0]}/${encodeURIComponent(titleDiv.innerText.trim())}`
1047 titleDiv.insertAdjacentElement('beforeend', a)
1048 }
1049
1050 if (newPortalData.descriptionEdits.length <= 1) {
1051 let titleDiv = editDiv.querySelector('div[ng-if="!answerCtrl.needsDescriptionEdit"]')
1052 const content = titleDiv.innerText.trim() || ''
1053 if (content !== '<No description>' && content !== '') {
1054 let a = w.document.createElement('a')
1055 let span = w.document.createElement('span')
1056 span.className = 'glyphicon glyphicon-book'
1057 span.innerHTML = ' '
1058 a.appendChild(span)
1059 a.className = 'translate-title btn btn-default'
1060 a.target = 'translate'
1061 a.style.setProperty('padding', '0px 4px')
1062 a.style.setProperty('margin-left', '14px')
1063 a.href = `https://translate.google.com/#auto/${browserLocale.split('-')[0]}/${encodeURIComponent(content)}`
1064 titleDiv.insertAdjacentElement('beforeEnd', a)
1065 }
1066 }
1067
1068 expandWhatIsItBox()
1069
1070 // fix locationEditsMap if only one location edit exists
1071 if (newPortalData.locationEdits.length <= 1 || subController.locationEditsMap.getZoom() > 19) {
1072 subController.locationEditsMap.setZoom(19)
1073 }
1074
1075 /* EDIT PORTAL */
1076 /* region keyboard navigation */
1077
1078 if (preferences.get(WFRT.OPTIONS.KEYBOARD_NAV)) {
1079 activateShortcuts()
1080 }
1081
1082 function activateShortcuts () {
1083 let currentSelectable = 0
1084 let hasLocationEdit = (newPortalData.locationEdits.length > 1)
1085 // counting *true*, please don't shoot me
1086 let maxItems = (newPortalData.descriptionEdits.length > 1) + (newPortalData.titleEdits.length > 1) + (hasLocationEdit) + 2
1087
1088 let mapMarkers
1089 if (hasLocationEdit) mapMarkers = subController.allLocationMarkers
1090 else mapMarkers = []
1091
1092 // a list of all 6 star button rows, and the two submit buttons
1093 let starsAndSubmitButtons = w.document.querySelectorAll(
1094 '.edit-container div[ng-show="subCtrl.pageData.titleEdits.length > 1"]:not(.ng-hide),' +
1095 '.edit-container div[ng-show="subCtrl.pageData.descriptionEdits.length > 1"]:not(.ng-hide),' +
1096 '.edit-container div[ng-show="subCtrl.pageData.locationEdits.length > 1"]:not(.ng-hide),' +
1097 '#submitFF')
1098
1099 /* EDIT PORTAL */
1100 function highlight () {
1101 let el = editDiv.querySelector('.poi-edit-map-unable')
1102 el.style.setProperty('border', 'none')
1103
1104 starsAndSubmitButtons.forEach((element) => { element.style.setProperty('border', 'none') })
1105 if (hasLocationEdit && currentSelectable === maxItems - 3) {
1106 el.style.setProperty('border-left', '4px dashed #ebbc4a')
1107 el.style.setProperty('border-top', '4px dashed #ebbc4a')
1108 el.style.setProperty('border-bottom', '4px dashed #ebbc4a')
1109 el.style.setProperty('border-right', '4px dashed #ebbc4a')
1110 el.style.setProperty('padding', '8px')
1111 // el.style.setProperty('margin-bottom', '0')
1112 submitAndNext.blur()
1113 submitButton.blur()
1114 } else if (currentSelectable < maxItems - 2) {
1115 starsAndSubmitButtons[currentSelectable].style.setProperty('border-left', '4px dashed #ebbc4a')
1116 starsAndSubmitButtons[currentSelectable].style.setProperty('padding-left', '16px')
1117 submitAndNext.blur()
1118 submitButton.blur()
1119 } else if (currentSelectable === maxItems - 2) {
1120 submitAndNext.focus()
1121 } else if (currentSelectable === maxItems) {
1122 submitButton.focus()
1123 }
1124 }
1125
1126 /* EDIT PORTAL */
1127 addEventListener('keydown', (event) => {
1128 /*
1129 Keycodes:
1130
1131 8: Backspace
1132 9: TAB
1133 13: Enter
1134 16: Shift
1135 27: Escape
1136 32: Space
1137 68: D
1138 107: NUMPAD +
1139 109: NUMPAD -
1140 111: NUMPAD /
1141
1142 49 - 53: Keys 1-5
1143 97 - 101: NUMPAD 1-5
1144 */
1145
1146 let numkey = null
1147 if (event.keyCode >= 49 && event.keyCode <= 53) {
1148 numkey = event.keyCode - 48
1149 } else if (event.keyCode >= 97 && event.keyCode <= 101) {
1150 numkey = event.keyCode - 96
1151 }
1152 if (event.keyCode === 107) {
1153
1154 let submitButton = document.getElementById('submitFF');
1155
1156 submitButton.click();
1157
1158 }
1159 // do not do anything if a text area or a input with type text has focus
1160 if (w.document.querySelector('input[type=text]:focus') || w.document.querySelector('textarea:focus')) {
1161 return
1162 } else if ((event.keyCode === 13 || event.keyCode === 32) && w.document.querySelector('a.button[href="/review"]')) {
1163 // "analyze next" button
1164 w.document.location.href = '/review'
1165 event.preventDefault()
1166 } else if ((event.keyCode === 13 || event.keyCode === 32) && currentSelectable === maxItems) {
1167 // submit normal rating
1168 w.document.querySelector('[ng-click="answerCtrl.submitForm()"]').click()
1169 event.preventDefault()
1170 } else if (event.keyCode === 27 || event.keyCode === 111) {
1171 // return to first selection (should this be a portal)
1172 currentSelectable = 0
1173 } else if ((event.keyCode === 107 || event.keyCode === 9) && currentSelectable < maxItems) {
1174 // select next rating
1175 currentSelectable++
1176 event.preventDefault()
1177 } else if ((event.keyCode === 109 || event.keyCode === 16 || event.keyCode === 8) && currentSelectable > 0) {
1178 // select previous rating
1179 currentSelectable--
1180 event.preventDefault()
1181 } else if (numkey === null || currentSelectable > maxItems - 2) {
1182 return
1183 } else {
1184 // rating 1-5
1185 if (hasLocationEdit && currentSelectable === maxItems - 3 && numkey <= mapMarkers.length) {
1186 google.maps.event.trigger(angular.element(document.getElementById('NewSubmissionController')).scope().getAllLocationMarkers()[numkey - 1], 'click')
1187 } else {
1188 if (hasLocationEdit) {
1189 numkey = 1
1190 }
1191
1192 // starsAndSubmitButtons[currentSelectable].querySelectorAll('.poi-edit-box')[numkey - 1].click()
1193 currentSelectable++
1194 }
1195 }
1196 highlight()
1197 })
1198
1199 highlight()
1200 }
1201 }
1202
1203 // add map buttons
1204 function mapButtons (newPortalData, targetElement, where) {
1205 // coordinate format conversion
1206 const coordUtm33 = proj4('+proj=longlat', '+proj=utm +zone=33', [newPortalData.lng, newPortalData.lat])
1207 const coordUtm35 = proj4('+proj=longlat', '+proj=utm +zone=35', [newPortalData.lng, newPortalData.lat])
1208 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])
1209
1210 const mapButtons = `
1211<a class='btn btn-default' target='intel' href='https://intel.ingress.com/intel?ll=${newPortalData.lat},${newPortalData.lng}&z=17'>Intel</a>
1212<a class='btn btn-default' target='gmaps' href='https://www.google.com/maps/place/${newPortalData.lat},${newPortalData.lng}'>GMaps</a>
1213`
1214 // more map buttons in a dropdown menu
1215 const mapDropdown = `
1216<li><a target='osm' href='https://www.openstreetmap.org/?mlat=${newPortalData.lat}&mlon=${newPortalData.lng}&zoom=16'>OSM</a></li>
1217<li><a target='bing' href='https://bing.com/maps/default.aspx?cp=${newPortalData.lat}~${newPortalData.lng}&lvl=16&style=a'>bing</a></li>
1218<li><a target='heremaps' href='https://wego.here.com/?map=${newPortalData.lat},${newPortalData.lng},17,satellite'>HERE maps</a></li>
1219<li><a targeT='zoomearth' href='https://zoom.earth/#${newPortalData.lat},${newPortalData.lng},18z,sat'>Zoom Earth</a></li>
1220<li role='separator' class='divider'></li>
1221<li><a target='swissgeo' href='http://map.geo.admin.ch/?swisssearch=${newPortalData.lat},${newPortalData.lng}'>CH - Swiss Geo Map</a></li>
1222<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>
1223<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>
1224<li><a target='kompass' href='http://maps.kompass.de/#lat=${newPortalData.lat}&lon=${newPortalData.lng}&z=17'>DE - Kompass.maps</a></li>
1225<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>
1226<li><a target='pegel' href='http://opr.pegel.dk/?17/${newPortalData.lat}/${newPortalData.lng}'>DK - SDFE Orthophotos</a></li>
1227<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>
1228<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>
1229<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>
1230<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>
1231<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>
1232<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>
1233<li><a target='kulturminnesok' href='https://www.kulturminnesok.no/search?lat=${newPortalData.lat}&lng=${newPortalData.lng}'>NO - Kulturminnesøk</a></li>
1234<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>
1235<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>
1236<li><a target='toposvalbard' href='http://toposvalbard.npolar.no/?lat=${newPortalData.lat}&long=${newPortalData.lng}&zoom=17&layer=map'>NO - Polarinstituttet, Svalbard</a></li>
1237<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>
1238<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>
1239<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>
1240<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>
1241<li><a target='hitta' href='https://www.hitta.se/kartan!~${newPortalData.lat},${newPortalData.lng},18z/tileLayer!l=1'>SE - Hitta.se</a></li>
1242<li><a target='eniro' href='https://kartor.eniro.se/?c=${newPortalData.lat},${newPortalData.lng}&z=17&l=nautical'>SE - Eniro Sjökort</a></li>
1243`
1244 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>`)
1245 }
1246
1247 // add new button "Submit and reload", skipping "Your analysis has been recorded." dialog
1248 function quickSubmitButton (submitDiv, ansController, bodyObserver) {
1249 let submitButton = submitDiv.querySelector('button.button-primary')
1250 // submitButton.classList.add('btn', 'btn-warning')
1251
1252 let submitAndNext = submitButton.cloneNode(false)
1253 submitButton.addEventListener('click', () => {
1254 bodyObserver.disconnect()
1255 })
1256 submitAndNext.innerHTML = `<span class="glyphicon glyphicon-floppy-disk"></span> <span class="glyphicon glyphicon-forward"></span>`
1257 submitAndNext.title = 'Submit and go to next review'
1258 submitAndNext.id = 'submitFF'
1259 submitAndNext.addEventListener('click', () => {
1260 ansController.openSubmissionCompleteModal = () => {
1261 window.location.assign('/review')
1262 }
1263 })
1264
1265 w.$injector.invoke(['$compile', ($compile) => {
1266 let compiledSubmit = $compile(submitAndNext)(w.$scope(submitDiv))
1267 submitDiv.querySelector('#submitDiv button').insertAdjacentElement('beforeBegin', compiledSubmit[0])
1268 }])
1269 return { submitButton, submitAndNext }
1270 }
1271
1272 function commentTemplates () {
1273 // add text buttons
1274 const textButtons = `
1275<button id='photo' class='btn btn-default textButton' data-tooltip='Indicates a low quality photo'>Photo</button>
1276<button id='private' class='btn btn-default textButton' data-tooltip='Located on private residential property'>Private</button>`
1277 const textDropdown = `
1278<li><a class='textButton' id='school' data-tooltip='Located on school property'>School</a></li>
1279<li><a class='textButton' id='person' data-tooltip='Photo contains 1 or more people'>Person</a></li>
1280<li><a class='textButton' id='perm' data-tooltip='Seasonal or temporary display or item'>Temporary</a></li>
1281<li><a class='textButton' id='location' data-tooltip='Location wrong'>Location</a></li>
1282<li><a class='textButton' id='natural' data-tooltip='Candidate is a natural feature'>Natural</a></li>
1283<li><a class='textButton' id='emergencyway' data-tooltip='Obstructing emergency way'>Emergency Way</a></li>
1284`
1285
1286 const cardAdditionalText = w.document.getElementById('additional-comments-card')
1287 const cardTextBox = cardAdditionalText.querySelector('textarea')
1288
1289 cardAdditionalText.insertAdjacentHTML('beforeend', `<div class="card__footer">
1290<span id="wfrt_comment_button_group" class='btn-group dropup pull-left'>${textButtons}
1291<span class='btn btn-default dropdown'><span class='caret'></span><ul id="wfrt_comment_button_dropdown" class='dropdown-content dropdown-menu'>${textDropdown}</ul>
1292</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>
1293`)
1294
1295 const buttons = w.document.getElementsByClassName('textButton')
1296 for (let b in buttons) {
1297 if (buttons.hasOwnProperty(b)) {
1298 buttons[b].addEventListener('click', event => {
1299 const source = event.target || event.srcElement
1300 let text = cardTextBox.value
1301 if (text.length > 0) {
1302 text += ', '
1303 }
1304 switch (source.id) {
1305 case 'photo':
1306 text += 'Low quality photo'
1307 break
1308 case 'private':
1309 text += 'Private residential property'
1310 break
1311 case 'duplicate':
1312 text += 'Duplicate of previously reviewed portal candidate'
1313 break
1314 case 'school':
1315 text += 'Located on primary or secondary school grounds'
1316 break
1317 case 'person':
1318 text += 'Picture contains one or more people'
1319 break
1320 case 'perm':
1321 text += 'Portal candidate is seasonal or temporary'
1322 break
1323 case 'location':
1324 text += 'Portal candidate\'s location is not on object'
1325 break
1326 case 'emergencyway':
1327 text += 'Portal candidate is obstructing the path of emergency vehicles'
1328 break
1329 case 'natural':
1330 text += 'Portal candidate is a natural feature'
1331 break
1332 case 'clear':
1333 text = ''
1334 break
1335 }
1336
1337 cardTextBox.value = text
1338 cardTextBox.dispatchEvent(new Event('change')) // eslint-disable-line no-undef
1339
1340 event.target.blur()
1341 }, false)
1342 }
1343 }
1344 }
1345
1346 // adding a 40m circle and a smaller 20m circle around the portal (capture range)
1347 function mapOriginCircle (map) {
1348 // noinspection JSUnusedLocalSymbols
1349 if (preferences.get(WFRT.OPTIONS.MAP_CIRCLE_40)) {
1350 const circle40 = new google.maps.Circle({ // eslint-disable-line no-unused-vars
1351 map: map,
1352 center: map.center,
1353 radius: 40,
1354 strokeColor: '#ebbc4a',
1355 strokeOpacity: 0.8,
1356 strokeWeight: 1.5,
1357 fillOpacity: 0
1358 })
1359 }
1360
1361 if (preferences.get(WFRT.OPTIONS.MAP_CIRCLE_20)) {
1362 const circle20 = new google.maps.Circle({ // eslint-disable-line no-unused-vars
1363 map: map,
1364 center: map.center,
1365 radius: 20,
1366 strokeColor: '#eddc4a',
1367 strokeOpacity: 0.8,
1368 strokeWeight: 1.5,
1369 fillOpacity: 0
1370 })
1371 }
1372 }
1373
1374 // replace map markers with a nice circle
1375 function mapMarker (markers) {
1376 for (let i = 0; i < markers.length; ++i) {
1377 const marker = markers[i]
1378 marker.setIcon(POI_MARKER)
1379 }
1380 }
1381
1382 // set available map types
1383 function mapTypes (map, isMainMap) {
1384 const PROVIDERS = {
1385 GOOGLE: 'google',
1386 KARTVERKET: 'kartverket'
1387 }
1388
1389 const types = [
1390 { provider: PROVIDERS.GOOGLE, id: 'roadmap' },
1391 { provider: PROVIDERS.GOOGLE, id: 'terrain' },
1392 { provider: PROVIDERS.GOOGLE, id: 'satellite' },
1393 { provider: PROVIDERS.GOOGLE, id: 'hybrid' }]
1394
1395 if (preferences.get(WFRT.OPTIONS.NORWAY_MAP_LAYER)) {
1396 types.push({ provider: PROVIDERS.KARTVERKET, id: `${PROVIDERS.KARTVERKET}_topo`, code: 'topo4', label: 'NO - Topo' },
1397 { provider: PROVIDERS.KARTVERKET, id: `${PROVIDERS.KARTVERKET}_raster`, code: 'toporaster3', label: 'NO - Raster' },
1398 { provider: PROVIDERS.KARTVERKET, id: `${PROVIDERS.KARTVERKET}_sjo`, code: 'sjokartraster', label: 'NO - Sjøkart' }
1399 )
1400 }
1401
1402 const defaultMapType = 'hybrid'
1403
1404 const mapOptions = {
1405 // re-enabling map scroll zoom and allow zoom with out holding ctrl
1406 scrollwheel: true,
1407 gestureHandling: 'greedy',
1408 // map type selection
1409 mapTypeControl: true,
1410 mapTypeControlOptions: {
1411 mapTypeIds: types.map(t => t.id),
1412 style: google.maps.MapTypeControlStyle.DROPDOWN_MENU
1413 }
1414 }
1415 map.setOptions(mapOptions)
1416
1417 // register custom map types
1418 types.forEach(t => {
1419 switch (t.provider) {
1420 case PROVIDERS.KARTVERKET:
1421 map.mapTypes.set(t.id, new google.maps.ImageMapType({
1422 layer: t.code,
1423 name: t.label,
1424 alt: t.label,
1425 maxZoom: 19,
1426 tileSize: new google.maps.Size(256, 256),
1427 getTileUrl: function (coord, zoom) {
1428 return `//opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=${this.layer}&zoom=${zoom}&x=${coord.x}&y=${coord.y}`
1429 }
1430 }))
1431 break
1432 }
1433 })
1434
1435 // track current selection for position map
1436 let mapType
1437 if (isMainMap) {
1438 mapType = WFRT.PREFIX + WFRT.VAR.MAP_TYPE_1
1439 } else {
1440 mapType = WFRT.PREFIX + WFRT.VAR.MAP_TYPE_2
1441 }
1442
1443 // save selection when changed
1444 map.addListener('maptypeid_changed', function () {
1445 w.localStorage.setItem(mapType, map.getMapTypeId())
1446 })
1447
1448 // get map type saved from last use or fall back to default
1449 map.setMapTypeId(w.localStorage.getItem(mapType) || defaultMapType)
1450 }
1451
1452 // // move submit button to right side of classification-div. don't move on mobile devices / small width
1453 // function moveSubmitButton () {
1454 // const submitDiv = w.document.querySelectorAll('#submitDiv, #submitDiv + .text-center')
1455 //
1456 // if (screen.availWidth > 768) {
1457 // let newSubmitDiv = w.document.createElement('div')
1458 // const classificationRow = w.document.querySelector('.classification-row')
1459 // newSubmitDiv.className = 'col-xs-12 col-sm-6'
1460 // submitDiv[0].style.setProperty('margin-top', '16px')
1461 // newSubmitDiv.appendChild(submitDiv[0])
1462 // newSubmitDiv.appendChild(submitDiv[1])
1463 // classificationRow.insertAdjacentElement('afterend', newSubmitDiv)
1464 //
1465 // // edit-page - remove .col-sm-offset-3 from .classification-row (why did you add this, niantic?
1466 // classificationRow.classList.remove('col-sm-offset-3')
1467 // return newSubmitDiv
1468 // } else {
1469 // return submitDiv[0]
1470 // }
1471 // }
1472
1473 // expand automatically the "What is it?" filter text box
1474 function expandWhatIsItBox () {
1475 try {
1476 const whatController = w.$scope(w.document.getElementById('WhatIsItController')).whatCtrl
1477 setTimeout(() => {
1478 whatController.showWhat = true
1479 w.$rootScope.$apply()
1480 }, 50)
1481 } catch (err) {}
1482 }
1483
1484 function modifyProfile () {
1485 // stats enhancements: add processed by nia, percent processed, progress to next recon badge numbers
1486
1487 let wfrtScannerOffset = 0
1488 if (preferences.get(WFRT.OPTIONS.SCANNER_OFFSET_FEATURE)) {
1489 // get scanner offset from localStorage
1490 wfrtScannerOffset = parseInt(w.localStorage.getItem(WFRT.SCANNER_OFFSET)) || 0
1491 }
1492 const stats = w.document.querySelector('#profile-stats:not(.visible-xs)')
1493
1494 const reviewed = parseInt(stats.children[0].children[0].children[1].innerText)
1495 const accepted = parseInt(stats.children[1].children[1].children[1].innerText)
1496 const rejected = parseInt(stats.children[1].children[2].children[1].innerText)
1497 const duplicated = parseInt(stats.children[1].children[3].children[1].innerText)
1498
1499 const processed = accepted + rejected +duplicated - wfrtScannerOffset
1500 const processedPercent = roundToPrecision(processed / reviewed * 100, 1)
1501
1502 const acceptedPercent = roundToPrecision(accepted / (reviewed) * 100, 1)
1503 const rejectedPercent = roundToPrecision(rejected / (reviewed) * 100, 1)
1504 const duplicatedPercent = roundToPrecision(duplicated / (reviewed) * 100, 1)
1505
1506 const reconBadge = { 100: 'Bronze', 750: 'Silver', 2500: 'Gold', 5000: 'Platin', 10000: 'Black' }
1507 let nextBadgeName, nextBadgeCount
1508
1509 for (const key in reconBadge) {
1510 if (processed <= key) {
1511 nextBadgeCount = key
1512 nextBadgeName = reconBadge[key]
1513 break
1514 }
1515 }
1516 const nextBadgeProcess = processed / nextBadgeCount * 100
1517
1518 const numberSpans = stats.querySelectorAll('span.stats-right')
1519
1520 numberSpans[0].insertAdjacentHTML('beforeend', `, <span class=''>100%</span>`)
1521 numberSpans[1].insertAdjacentHTML('beforeend', `, <span class=''>${acceptedPercent}%</span>`)
1522 numberSpans[2].insertAdjacentHTML('beforeend', `, <span class=''>${rejectedPercent}%</span>`)
1523 numberSpans[3].insertAdjacentHTML('beforeend', `, <span class=''>${duplicatedPercent}%</span>`)
1524
1525 stats.querySelectorAll('h4')[2].insertAdjacentHTML('afterend', `<br>
1526<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>`)
1527
1528 if (processed < 10000) {
1529 stats.insertAdjacentHTML('beforeEnd', `
1530<br><div>Next Ingress Recon badge tier: <b>${nextBadgeName} (${nextBadgeCount})</b><br>
1531<div class='progress'>
1532<div class='progress-bar progress-bar-warning'
1533role='progressbar'
1534aria-valuenow='${nextBadgeProcess}'
1535aria-valuemin='0'
1536aria-valuemax='100'
1537style='width: ${Math.round(nextBadgeProcess)}%;'
1538title='${nextBadgeCount - processed} to go'>
1539${Math.round(nextBadgeProcess)}%
1540</div></div></div>
1541`)
1542 } else stats.insertAdjacentHTML('beforeEnd', `<hr>`)
1543 stats.insertAdjacentHTML('beforeEnd', `<div><i class="glyphicon glyphicon-share"></i> <input readonly onFocus="this.select();" style="width: 90%;" type="text"
1544value="Reviewed: ${reviewed} / Processed: ${accepted + rejected + duplicated} (Created: ${accepted}/ Rejected: ${rejected}/ Duplicated: ${duplicated}) / ${Math.round(processedPercent)}%"/></div>`)
1545
1546 // ** wayfarer-scanner offset
1547 if (accepted < 10000 && preferences.get(WFRT.OPTIONS.SCANNER_OFFSET_UI)) {
1548 stats.insertAdjacentHTML('beforeEnd', `
1549<div id='scannerOffsetContainer'>
1550<span style="margin-left: 5px" class="ingress-mid-blue pull-left">Scanner offset:</span>
1551<input id="scannerOffset" onFocus="this.select();" type="text" name="scannerOffset" size="8" class="pull-right" value="${wfrtScannerOffset}">
1552</div>`)
1553
1554 // we have to inject the tooltip to angular
1555 w.$injector.invoke(['$compile', ($compile) => {
1556 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))
1557 w.document.getElementById('scannerOffsetContainer').insertAdjacentElement('afterbegin', compiledSubmit[0])
1558 }]);
1559
1560 ['change', 'keyup', 'cut', 'paste', 'input'].forEach(e => {
1561 w.document.getElementById('scannerOffset').addEventListener(e, (event) => {
1562 w.localStorage.setItem(WFRT.SCANNER_OFFSET, event.target.value)
1563 })
1564 })
1565 // **
1566 }
1567
1568 modifyProfile = () => {} // eslint-disable-line
1569 }
1570
1571 function addOptionsButton () {
1572 // Add preferences button only once
1573 if (w.document.getElementById('wfrt_preferences_button') !== null) {
1574 return
1575 }
1576
1577 // add wayfarer-tools preferences button
1578 let wfrtPreferencesButton = w.document.createElement('a')
1579 wfrtPreferencesButton.classList.add('brand', 'upgrades-icon', 'pull-right')
1580 wfrtPreferencesButton.addEventListener('click', () => preferences.showPreferencesUI(w))
1581 wfrtPreferencesButton.title = 'Wayfarer-Tools Preferences'
1582 wfrtPreferencesButton.setAttribute('id', 'wfrt_preferences_button')
1583
1584 const prefCog = w.document.createElement('span')
1585 prefCog.classList.add('glyphicon', 'glyphicon-cog')
1586 wfrtPreferencesButton.appendChild(prefCog)
1587
1588 w.document.querySelector('.header .inner-container:last-of-type').insertAdjacentElement('afterbegin', wfrtPreferencesButton)
1589 }
1590
1591 function addRefreshContainer () {
1592 let cbxRefresh = w.document.createElement('input')
1593 let cbxRefreshDesktop = w.document.createElement('input')
1594
1595 cbxRefresh.id = WFRT.OPTIONS.REFRESH
1596 cbxRefresh.type = 'checkbox'
1597 cbxRefresh.checked = preferences.get(WFRT.OPTIONS.REFRESH) === 'true'
1598
1599 cbxRefreshDesktop.id = WFRT.OPTIONS.REFRESH_NOTI_DESKTOP
1600 cbxRefreshDesktop.type = 'checkbox'
1601 cbxRefreshDesktop.checked = preferences.get(WFRT.OPTIONS.REFRESH_NOTI_DESKTOP) === 'true'
1602
1603 let refreshPanel = w.document.createElement('div')
1604 refreshPanel.className = 'panel panel-ingress'
1605
1606 refreshPanel.addEventListener('change', (event) => {
1607 preferences.set(event.target.id, event.target.checked)
1608 if (event.target.checked) {
1609 startRefresh()
1610 } else {
1611 stopRefresh()
1612 }
1613 })
1614
1615 refreshPanel.innerHTML = `
1616<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>
1617<div id='cbxDiv' class='panel-body bg-primary' style='background:black;'></div>`
1618
1619 refreshPanel.querySelector('#cbxDiv').insertAdjacentElement('afterbegin', appendCheckbox(cbxRefreshDesktop, 'Desktop notification'))
1620 refreshPanel.querySelector('#cbxDiv').insertAdjacentElement('afterbegin', appendCheckbox(cbxRefresh, 'Refresh every 5-10 minutes'))
1621
1622 let colDiv = w.document.createElement('div')
1623 colDiv.className = 'col-md-4 col-md-offset-4'
1624 colDiv.appendChild(refreshPanel)
1625
1626 let rowDiv = w.document.createElement('div')
1627 rowDiv.className = 'row'
1628 rowDiv.appendChild(colDiv)
1629
1630 w.document.getElementById('NewSubmissionController').insertAdjacentElement('beforeend', rowDiv)
1631
1632 cbxRefresh.checked === true ? startRefresh() : stopRefresh()
1633
1634 function appendCheckbox (checkbox, text) {
1635 let label = w.document.createElement('label')
1636 let div = w.document.createElement('div')
1637 div.className = 'checkbox'
1638 label.appendChild(checkbox)
1639 label.appendChild(w.document.createTextNode(text))
1640 div.appendChild(label)
1641 return div
1642 }
1643
1644 addRefreshContainer = () => {} // eslint-disable-line
1645 }
1646
1647 let refreshIntervalID
1648
1649 function startRefresh () {
1650 let time = getRandomIntInclusive(5, 10) * 60000
1651
1652 refreshIntervalID = setInterval(() => {
1653 reloadWayfarer()
1654 }, time)
1655
1656 function reloadWayfarer () {
1657 clearInterval(refreshIntervalID)
1658 w.sessionStorage.setItem(WFRT.FROM_REFRESH, 'true')
1659 w.document.location.reload()
1660 }
1661
1662 // source https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
1663 function getRandomIntInclusive (min, max) {
1664 min = Math.ceil(min)
1665 max = Math.floor(max)
1666 return Math.floor(Math.random() * (max - min + 1)) + min
1667 }
1668 }
1669
1670 function stopRefresh () {
1671 clearInterval(refreshIntervalID)
1672 }
1673
1674 function checkIfAutorefresh () {
1675 if (w.sessionStorage.getItem(WFRT.FROM_REFRESH)) {
1676 // reset flag
1677 w.sessionStorage.removeItem(WFRT.FROM_REFRESH)
1678
1679 if (w.document.hidden) { // if tab in background: flash favicon
1680 let flag = true
1681
1682 if (preferences.get(WFRT.OPTIONS.REFRESH_NOTI_DESKTOP) === 'true') {
1683 GM_notification({
1684 'title': 'Wayfarer - New Wayspot Analysis Available',
1685 'text': 'by Wayfarer-Tools',
1686 'image': 'https://gitlab.com/uploads/-/system/project/avatar/3311015/opr-tools.png'
1687 })
1688 }
1689
1690 let flashId = setInterval(() => {
1691 flag = !flag
1692 changeFavicon(`${flag ? POI_MARKER : '/imgpub/favicon.ico'}`)
1693 }, 1000)
1694
1695 // stop flashing if tab in foreground
1696 addEventListener('visibilitychange', () => {
1697 if (!w.document.hidden) {
1698 changeFavicon('/imgpub/favicon.ico')
1699 clearInterval(flashId)
1700 }
1701 })
1702 }
1703 }
1704 }
1705
1706 function changeFavicon (src) {
1707 let link = w.document.querySelector('link[rel="shortcut icon"]')
1708 link.href = src
1709 }
1710
1711 function startExpirationTimer (subController) {
1712 w.document.querySelector('.header .inner-container:last-of-type').insertAdjacentHTML('afterbegin', '<span id="countdownDisplay"></span>')
1713
1714 let countdownEnd = subController.pageData.expires
1715 let countdownDisplay = document.getElementById('countdownDisplay')
1716 countdownDisplay.style.setProperty('color', 'black')
1717
1718 // Update the count down every 1 second
1719 let counterInterval = setInterval(function () {
1720 // Get todays date and time
1721 let now = new Date().getTime()
1722 // Find the distance between now an the count down date
1723 let distance = countdownEnd - now
1724 // Time calculations for minutes and seconds
1725 let minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60))
1726 let seconds = Math.floor((distance % (1000 * 60)) / 1000)
1727
1728 // Display the result in the element
1729 countdownDisplay.innerText = `${minutes}m ${seconds}s `
1730
1731 if (distance < 0) {
1732 // If the count down is finished, write some text
1733 clearInterval(counterInterval)
1734 countdownDisplay.innerText = 'EXPIRED'
1735 countdownDisplay.style.setProperty('color', 'red')
1736 } else if (distance < 90000) {
1737 countdownDisplay.style.setProperty('color', 'red')
1738 }
1739 }, 1000)
1740 }
1741
1742 function versionCheck () {
1743 if (WFRT.VERSION > (parseInt(w.localStorage.getItem(WFRT.PREFIX + WFRT.VERSION_CHECK)) || WFRT.VERSION - 1)) {
1744 w.localStorage.setItem(WFRT.PREFIX + WFRT.VERSION_CHECK, WFRT.VERSION)
1745
1746 const changelogString = `
1747 <h4><span class="glyphicon glyphicon-asterisk"></span> Wayfarer-Tools was updated:</h4>
1748 <div>${strings.changelog}</div>
1749 `
1750 // show changelog
1751 alertify.closeLogOnClick(false).logPosition('bottom right').delay(0).log(changelogString, (ev) => {
1752 ev.preventDefault()
1753 ev.target.closest('div.default.show').remove()
1754 }).reset()
1755 }
1756 }
1757
1758 function addCustomPresetButtons () {
1759 // add customPreset UI
1760 wfrtCustomPresets = getCustomPresets(w)
1761 let customPresetOptions = ''
1762 for (const customPreset of wfrtCustomPresets) {
1763 customPresetOptions += `<button class='btn btn-default customPresetButton' id='${customPreset.uid}'>${customPreset.label}</button>`
1764 }
1765 w.document.getElementById('wfrt_custom_presets').innerHTML = customPresetOptions
1766 }
1767
1768 function getCustomPresets (w) {
1769 // simply to scope the string we don't need after JSON.parse
1770 let presetsJSON = w.localStorage.getItem(WFRT.PREFIX + WFRT.VAR.CUSTOM_PRESETS)
1771 if (presetsJSON != null && presetsJSON !== '') {
1772 return JSON.parse(presetsJSON)
1773 }
1774 return []
1775 }
1776
1777 function saveCustomPreset (label, ansController, whatController) {
1778 // uid snippet from https://stackoverflow.com/a/47496558/6447397
1779 let preset = {
1780 uid: [...Array(5)].map(() => Math.random().toString(36)[3]).join(''),
1781 label: label,
1782 nodeName: whatController.whatNode.name,
1783 nodeId: whatController.whatNode.id,
1784 quality: ansController.formData.quality,
1785 description: ansController.formData.description,
1786 cultural: ansController.formData.cultural,
1787 uniqueness: ansController.formData.uniqueness,
1788 location: ansController.formData.location,
1789 safety: ansController.formData.safety
1790 }
1791 wfrtCustomPresets.push(preset)
1792 w.localStorage.setItem(WFRT.PREFIX + WFRT.VAR.CUSTOM_PRESETS, JSON.stringify(wfrtCustomPresets))
1793 }
1794
1795 function deleteCustomPreset (preset) {
1796 wfrtCustomPresets = wfrtCustomPresets.filter(item => item.uid !== preset.uid)
1797 w.localStorage.setItem(WFRT.PREFIX + WFRT.VAR.CUSTOM_PRESETS, JSON.stringify(wfrtCustomPresets))
1798 }
1799
1800 function showHelp () {
1801 let helpString = `<a href='https://gitlab.com/1110101/opr-tools'><span class='label label-success'>Wayfarer-Tools</span></a> Key shortcuts<br>
1802 <table class="table table-condensed ">
1803 <thead>
1804 <tr>
1805 <th>Keys</th>
1806 <th>Function</th>
1807 </tr>
1808 </thead>
1809 <tbody>
1810 <tr>
1811 <td><kbd>Keys 1-5</kbd> / <kbd>Numpad 1-5</kbd></td>
1812 <td>Valuate current selected field (the yellow highlighted one)</td>
1813 </tr>
1814 <tr>
1815 <td><kbd>Shift</kbd> + <kbd>Keys 1-5</kbd></td>
1816 <td>Apply custom preset (if exists)</td>
1817 </tr>
1818 <tr>
1819 <td><kbd>Keys 1-7</kbd> / <kbd>Numpad 1-7</kbd></td>
1820 <td>Rejection popup: Select list element</td>
1821 </tr>
1822 <tr>
1823 <td><kbd>D</kbd></td>
1824 <td>Mark current candidate as a duplicate of the opened portal in "duplicates"</td>
1825 </tr>
1826 <tr>
1827 <td><kbd>T</kbd></td>
1828 <td>Open title translation</td>
1829 </tr>
1830 <tr>
1831 <td><kbd>Y</kbd></td>
1832 <td>Open description translation</td>
1833 </tr>
1834 <tr>
1835 <td><kbd>U</kbd></td>
1836 <td>Open supporting statement translation</td>
1837 </tr>
1838 <tr>
1839 <td><kbd>Space</kbd> / <kbd>Enter</kbd> / <kbd>Numpad Enter</kbd></td>
1840 <td>Confirm dialog / Send valuation</td>
1841 </tr>
1842 <tr>
1843 <td><kbd>Tab</kbd> / <kbd>Numpad +</kbd></td>
1844 <td>Next field</td>
1845 </tr>
1846 <tr>
1847 <td><kbd>Shift</kbd> / <kbd>Backspace</kbd> / <kbd>Numpad -</kbd></td>
1848 <td>Previous field</td>
1849 </tr>
1850 <tr>
1851 <td><kbd>Esc</kbd> / <kbd>Numpad /</kbd></td>
1852 <td>First field</td>
1853 </tr>
1854 <tr>
1855 <td><kbd>^</kbd> / <kbd>Numpad *</kbd></td>
1856 <td>Skip Portal (if possible)</td>
1857 </tr>
1858 </tbody>
1859 </table>`
1860
1861 alertify.closeLogOnClick(false).logPosition('bottom right').delay(0).log(helpString, (ev) => {
1862 ev.preventDefault()
1863 ev.target.closest('div.default.show').remove()
1864 }).reset()
1865 }
1866
1867 function roundToPrecision (num, precision) {
1868 let shifter
1869 precision = Number(precision || 0)
1870 if (precision % 1 !== 0) throw new RangeError('precision must be an integer')
1871 shifter = Math.pow(10, precision)
1872 return Math.round(num * shifter) / shifter
1873 }
1874}
1875
1876setTimeout(() => {
1877 init()
1878}, 250)
1879
1880// region const
1881
1882const strings = {
1883 options: {
1884 [WFRT.OPTIONS.COMMENT_TEMPLATES]: 'Comment templates',
1885 [WFRT.OPTIONS.KEYBOARD_NAV]: 'Keyboard navigation',
1886 [WFRT.OPTIONS.NORWAY_MAP_LAYER]: 'Norwegian map layer',
1887 [WFRT.OPTIONS.PRESET_FEATURE]: 'Rating presets',
1888 [WFRT.OPTIONS.REFRESH]: 'Periodically refresh wayfarer if no analysis is available',
1889 [WFRT.OPTIONS.REFRESH_NOTI_DESKTOP]: '↳ With desktop notification',
1890 [WFRT.OPTIONS.SCANNER_OFFSET_FEATURE]: 'Scanner offset',
1891 [WFRT.OPTIONS.SCANNER_OFFSET_UI]: '↳ Display offset input field',
1892 [WFRT.OPTIONS.MAP_CIRCLE_20]: 'Show 20 meter circle around candidate location (minimum portal distance)',
1893 [WFRT.OPTIONS.MAP_CIRCLE_40]: 'Show 40 meter circle around candidate location (capture range)'
1894 },
1895 changelog:
1896 `
1897Version 2.0.6
1898<br>* Added shortcut key U to open supporting statement translation
1899<br>* Fixed countdown timer and percentage breakdowns (thanks to @fotofreund0815)
1900`
1901}
1902
1903const GLOBAL_CSS = `
1904.dropdown {
1905position: relative;
1906display: inline-block;
1907}
1908
1909.dropdown-content {
1910display: none;
1911position: absolute;
1912z-index: 1;
1913margin: 0;
1914}
1915
1916.dropdown:hover .dropdown-content {
1917display: block;
1918}
1919
1920.dropdown-menu > li > a:focus, .dropdown-menu > li > a:hover {
1921background-color: unset;
1922}
1923
1924.dropdown .dropdown-menu {
1925left: 0px;
1926right: unset;
1927width: unset;
1928}
1929
1930.modal-sm {
1931width: 350px !important;
1932}
1933
1934.panel-ingress {
1935background-color: #004746;
1936border: 1px solid #0ff;
1937border-radius: 1px;
1938box-shadow: inset 0 0 6px rgba(255, 255, 255, 1);
1939color: #0ff;
1940}
1941
1942[data-tooltip] {
1943position: relative;
1944cursor: pointer;
1945}
1946
1947[data-tooltip]:before,
1948[data-tooltip]:after {
1949visibility: hidden;
1950-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
1951filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0);
1952opacity: 0;
1953pointer-events: none;
1954}
1955
1956[data-tooltip]:before {
1957position: absolute;
1958top: 150%;
1959left: 50%;
1960margin-bottom: 5px;
1961margin-left: -80px;
1962padding: 7px;
1963width: relative;
1964-webkit-border-radius: 3px;
1965-moz-border-radius: 3px;
1966border-radius: 3px;
1967background-color: #000;
1968background-color: hsla(0, 0%, 20%, 0.9);
1969color: #fff;
1970content: attr(data-tooltip);
1971text-align: center;
1972font-size: 14px;
1973line-height: 1.2;
1974z-index: 100;
1975}
1976
1977[data-tooltip]:after {
1978position: absolute;
1979top: 132%;
1980left: relative;
1981width: 0;
1982border-bottom: 5px solid #000;
1983border-bottom: 5px solid hsla(0, 0%, 20%, 0.9);
1984border-right: 5px solid transparent;
1985border-left: 5px solid transparent;
1986content: " ";
1987font-size: 0;
1988line-height: 0;
1989}
1990
1991[data-tooltip]:hover:before,
1992[data-tooltip]:hover:after {
1993visibility: visible;
1994-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
1995filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
1996opacity: 1;
1997}
1998
1999.titleEditBox:hover {
2000box-shadow: inset 0 0 20px #ebbc4a;
2001}
2002
2003.titleEditBox:active {
2004box-shadow: inset 0 0 15px 2px white;
2005}
2006
2007.group-list li label:hover, ul.sub-group-list a:hover, #root-label:hover {
2008box-shadow: inset 0 0 5px #000000 !important;
2009}
2010
2011.group-list li label:active, ul.sub-group-list a:active, #root-label:active {
2012box-shadow: inset 0 0 10px 2px #000000 !important;
2013}
2014
2015.modal-body .button:focus, .modal-body textarea:focus {
2016outline: 2px dashed #ebbc4a;
2017}
2018
2019.modal-body .button:hover, .gm-style-iw button.button:hover {
2020filter: brightness(150%);
2021}
2022
2023.alertify-logs {
2024z-index: 100;
2025}
2026
2027.alertify .dialog .msg {
2028color: black;
2029}
2030
2031.alertify-logs > .default {
2032background-image: url(/img/ingress-background-dark.png) !important;
2033}
2034
2035.btn-xs {
2036margin-left: 8px;
2037padding: 0px 7px 1px !important;
2038box-shadow: inset 0 0 4px rgba(255, 255, 255, 1);
2039-webkit-box-shadow: inset 0 0 4px rgba(255, 255, 255, 1);
2040-moz-box-shadow: inset 0 0 4px rgba(255, 255, 255, 1);
2041}
2042
2043kbd {
2044display: inline-block;
2045padding: 3px 5px;
2046font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, monospace;
2047line-height: 10px;
2048color: #444d56;
2049vertical-align: middle;
2050background-color: #fafbfc;
2051border: 1px solid #d1d5da;
2052border-bottom-color: #c6cbd1;
2053border-radius: 3px;
2054box-shadow: inset 0 -1px 0 #c6cbd1;
2055}
2056
2057.dropdown-menu {
2058margin: 0 !important;
2059}
2060
2061.opr-yellow {
2062color: #F3EADA;
2063}
2064
2065#submitAndSkipLowQuality, #submitAndSkipDuplicate {
2066margin-left: 32px;
2067margin-right: 32px;
2068}
2069
2070#profile-stats > div {
2071width: 60%;
2072}
2073#scannerOffsetContainer {
2074margin-top: 16px;
2075}
2076
2077#wfrt_preferences_button {
2078 cursor: pointer;
2079 margin-right: 20px;
2080 margin-left: 20px;
2081 color: rgb(157,157,157);
2082}
2083#wfrt_custom_presets_card {
2084 width: 100%;
2085 height: auto;
2086 min-height: unset;
2087 margin-left: 15px;
2088}
2089#submitFF {
2090margin-right: 16px;
2091}
2092
2093@media (min-width: 768px) {
2094div.modal-custom1 {
2095width: 500px;
2096max-width: unset !important;
2097}
2098}
2099`
2100
2101const POI_MARKER = `data:image/png;base64,
2102iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACx
2103jwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAu
2104OWwzfk4AAADlSURBVDhPY/j//z8CTw3U/V8lcvx/MfPX/2Xcd//XyWwDYxAbJAaS63c2Q9aD0Nyg
2105UPS/hPXt/3bD5f93LI7DwFvnJILlSlg//K+XrUc1AKS5jOvx/wU55Vg1I2OQmlKOpzBDIM4G2UyM
2106ZhgGqQW5BOgdBrC/cDkbHwbpAeplAAcONgWEMChMgHoZwCGMTQExGKiXARxN2CSJwUC9VDCAYi9Q
2107HIhVQicpi0ZQ2gYlCrITEigpg5IlqUm5VrILkRdghoBMxeUd5MwE1YxqAAiDvAMKE1DAgmIHFMUg
2108DGKDxDCy838GAPWFoAEBs2EvAAAAAElFTkSuQmCC`
2109
2110// TG SVG Icon from https://commons.wikimedia.org/wiki/File:Telegram_logo.svg
2111const TG_SVG = `
2112<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" style="
2113 width: 16px;
2114 height: 16px;
2115">
2116<defs>
2117 <linearGradient id="b" x1="0.6667" y1="0.1667" x2="0.4167" y2="0.75">
2118 <stop stop-color="#37aee2" offset="0"></stop>
2119 <stop stop-color="#1e96c8" offset="1"></stop>
2120 </linearGradient>
2121 <linearGradient id="w" x1="0.6597" y1="0.4369" x2="0.8512" y2="0.8024">
2122 <stop stop-color="#eff7fc" offset="0"></stop>
2123 <stop stop-color="#fff" offset="1"></stop>
2124 </linearGradient>
2125</defs>
2126<circle cx="120" cy="120" r="120" fill="url(#b)"></circle>
2127<path fill="#c8daea" d="m98 175c-3.8876 0-3.227-1.4679-4.5678-5.1695L82
2128132.2059 170 80"></path>
2129<path fill="#a9c9dd" d="m98 175c3 0 4.3255-1.372
21306-3l16-15.558-19.958-12.035"></path>
2131<path fill="url(#w)" d="m100.04 144.41 48.36 35.729c5.5185 3.0449 9.5014
21321.4684
213310.876-5.1235l19.685-92.763c2.0154-8.0802-3.0801-11.745-8.3594-9.3482l-115.59
2134 44.571c-7.8901 3.1647-7.8441 7.5666-1.4382 9.528l29.663
2135 9.2583 68.673-43.325c3.2419-1.9659 6.2173-0.90899 3.7752 1.2584"></path>
2136</svg>`
2137// endregion