PageSourceSearch

https://www.auparkpiestany.sk/mapplic/mapplic.js

js auparkpiestany.sk collected 2026-10-06 17:53:59 UTC 96,966 bytes, 3,073 lines download raw bytes

1/*
2 * Mapplic - Custom Interactive Map Plugin by @sekler
3 * Version 7.0
4 * Custom Version 0.1.0
5 * https://www.mapplic.com/
6 */
7
8(function ($) {
9  "use strict";
10  var Mapplic = function (element) {
11    var self = this;
12
13    self.o = {
14      source: "locations.json",
15      selector: "[id^=MLOC] > *, [id^=landmark] > *, svg > #items > *",
16      external: ".mapplic-external",
17      scope: null,
18      customcss: null,
19      iconfile: null,
20      height: "auto",
21      minheight: 400,
22      maxheight: 1100,
23      csv: false,
24      landmark: false,
25      portrait: 860,
26      minimap: false,
27      deeplinking: true,
28      lightbox: true,
29      fullscreen: false,
30      hovertip: true,
31      defaultstyle: null,
32      moretext: null,
33      action: "default",
34      marker: "",
35      developer: false,
36      smartip: false,
37      animations: false,
38      hovertipdesc: false,
39      wayfinder: null,
40
41      // sidebar
42      sidebar: true,
43      sidebartoggle: false,
44      search: true,
45      searchlength: 1,
46      searchfields: ["title", "about", "category"],
47      searchdescription: false,
48      highlight: true,
49      thumbholder: false,
50      autopopulate: false,
51      sortby: "title",
52
53      // zoom
54      zoom: true,
55      clearbutton: true,
56      zoombuttons: true,
57      zoomoutclose: false,
58      closezoomout: true,
59      linknewtab: false,
60      mousewheel: true,
61      mapfill: false,
62      zoommargin: 200,
63      maxscale: 3,
64
65      // UI Colors
66      basecolor: null,
67      bgcolor: null,
68      bgcolor2: null,
69      headingcolor: null,
70      textcolor: null,
71      accentcolor: null,
72    };
73
74    self.loc = {
75      more: "More",
76      search: self.o.searchPlaceHolder,
77      zoomin: "Zoom in",
78      zoomout: "Zoom out",
79      resetzoom: "Reset zoom",
80      levelup: "Level up",
81      leveldown: "Level down",
82      clearsearch: "Clear search",
83      closepopup: "Close popup",
84      clearfilter: "Clear filter",
85      iconfile: "/mapplic/images/icons.svg",
86    };
87
88    self.el = element;
89
90    self.init = function (options) {
91      // merging options with defaults
92      self.o = $.extend(self.o, options);
93      if (typeof mapplic_localization !== "undefined")
94        self.loc = $.extend(self.loc, mapplic_localization);
95      if (self.o.iconfile) self.loc.iconfile = self.o.iconfile;
96
97      self.el.addClass("mapplic-element mapplic-loading");
98
99      // scope
100      if (self.o.scope) self.scope = $(self.o.scope);
101      else self.scope = self.el;
102
103      // process map data
104      var data = self.el.data("mapdata");
105      if (self.el.data("mapdata")) {
106        var mapdata = self.el.data("mapdata");
107        self.el.removeAttr("data-mapdata").removeData("mapdata");
108        processData(mapdata);
109        self.el.removeClass("mapplic-loading");
110      } else if (typeof self.o.source === "string") {
111        // loading .json file with AJAX
112        $.getJSON(self.o.source, function (data) {
113          processData(data);
114          self.el.removeClass("mapplic-loading");
115        }).fail(function () {
116          // Failure: couldn't load JSON file or it is invalid.
117          console.error(
118            "Couldn't load map data. (Make sure to run the script through web server)"
119          );
120          self.el.removeClass("mapplic-loading").addClass("mapplic-error");
121          // alert(
122          //   "Data file missing or invalid!\nMake sure to run the script through web server."
123          // );
124        });
125      } else {
126        // inline json object
127        processData(self.o.source);
128        self.el.removeClass("mapplic-loading");
129      }
130
131      return self;
132    };
133
134    // wayfinder
135    function Wayfinder() {
136      this.param = "pin";
137      this.pin = null;
138
139      this.init = function () {
140        if (typeof window.URL !== "undefined") {
141          const url = new URL(window.location.href);
142          this.pin = url.searchParams.get(this.param);
143        }
144
145        // add dynamic screen size UI scalling
146        const scale = window.innerWidth / 1920;
147
148        self.sidebar.el.css("transform", `scale(${scale})`);
149        self.sidebar.el.css("height", `calc(100% * (1 / ${scale}))`);
150
151        self.container.el.css("width", `calc(100% - (380px * ${scale}))`);
152        self.container.el
153          .find(".mapplic-level-switcher")
154          .css("transform", `scale(calc(1.5 * ${scale}))`);
155
156        return this;
157      };
158
159      this.update = function () {
160        $('[id="wayfinders"]').children().hide();
161        if (this.pin) $(`#${this.pin}`).show();
162      };
163    }
164
165    // tooltip
166    function Tooltip() {
167      this.el = null;
168      this.pin = null;
169      this.shift = 6;
170      this.drop = 0;
171      this.location = null;
172
173      this.init = function (location, check) {
174        var s = this;
175
176        // markup
177        this.el = $("<div></div>").addClass("mapplic-tooltip");
178        this.wrap = $("<div></div>").addClass("mapplic-tooltip-wrap").appendTo(this.el);
179        this.close = $("<button></button>")
180          .append(getIcon("icon-cross"))
181          .addClass("mapplic-tooltip-close")
182          .attr("aria-label", self.loc.closepopup)
183          .on("click touchend", function (e) {
184            e.preventDefault();
185            self.hideLocation();
186            if (!self.o.zoom || self.o.zoomoutclose)
187              self.moveTo(0.5, 0.5, self.fitscale, 400);
188          })
189          .appendTo(this.wrap);
190        if (location?.thumbnail)
191          this.image = $("<img>")
192            .addClass("mapplic-image")
193            .attr("alt", "Location image")
194            .hide()
195            .appendTo(this.wrap);
196        this.body = $("<div></div>")
197          .addClass("mapplic-tooltip-body")
198          .attr("aria-modal", "true")
199          .appendTo(this.wrap);
200        if (!location?.thumbnail) {
201          this.title = $("<h4></h4>")
202            .addClass("mapplic-tooltip-title")
203            .appendTo(this.body);
204        }
205        this.content = $("<div></div>")
206          .addClass("mapplic-tooltip-content")
207          .appendTo(this.body);
208        this.desc = $("<div></div>")
209          .addClass("mapplic-tooltip-description")
210          .appendTo(this.content);
211        this.link = $("<a>" + self.loc.more + "</a>")
212          .addClass("mapplic-popup-link")
213          .attr("href", "#")
214          .hide()
215          .appendTo(this.body);
216        if (self.o.linknewtab) this.link.attr("target", "_blank");
217        this.triangle = $("<div></div>")
218          .addClass("mapplic-tooltip-triangle")
219          .prependTo(this.wrap);
220
221        // no focus on deeplink
222        if (!check) this.body.attr("tabindex", "-1");
223
224        $(".mapplic-layer.mapplic-visible", self.map).append(this.el);
225
226        if (self.o.smartip) self.el.on("positionchanged", { s: s }, this.repos);
227
228        this.el.css({ transform: "scale(" + 1 / self.scale + ")" });
229
230        if (location) this.show(location);
231
232        // close tooltip
233        $(document).on("keyup.mapplic", function (e) {
234          e.stopImmediatePropagation();
235          if (e.keyCode === 27) {
236            self.hideLocation();
237            if (!self.o.zoom || self.o.zoomoutclose)
238              self.moveTo(0.5, 0.5, self.fitscale, 400);
239          }
240        });
241
242        return this;
243      };
244
245      this.repos = function (e) {
246        var s = e.data.s,
247          tx = "-50%",
248          ty = "-100%";
249
250        // vertical
251        var rtop = s.el.offset().top - self.container.el.offset().top;
252        if (rtop < s.wrap.outerHeight() + 36) {
253          s.el.addClass("mapplic-tooltip-bottom");
254          ty = "0%";
255        } else {
256          s.el.removeClass("mapplic-tooltip-bottom");
257          ty = "-100%";
258        }
259
260        // horizontal
261        var rleft = s.el.offset().left - self.container.el.offset().left;
262        if (rleft < s.wrap.outerWidth() / 2) {
263          if (rleft > 12) tx = -(100 + (rleft / s.wrap.outerWidth()) * 100) + 100 + "%";
264          else tx = "-10%";
265        } else if (rleft > self.container.el.outerWidth() - s.wrap.outerWidth() / 2) {
266          if (rleft < self.container.el.outerWidth() - 12)
267            tx =
268              ((self.container.el.outerWidth() - rleft) / s.wrap.outerWidth()) * 100 -
269              100 +
270              "%";
271          else tx = "-90%";
272        } else tx = "-50%";
273
274        s.wrap.css({ transform: "translate(" + tx + ", " + ty + ")" });
275      };
276
277      this.show = function (location) {
278        if (location) {
279          var s = this;
280
281          this.el.attr("data-location", location.id);
282
283          this.location = location;
284          if (self.hovertip) self.hovertip.hide();
285
286          if (location.image) {
287            this.image.attr("src", "");
288            this.image.attr("src", location.image).show();
289          } else this.image?.hide();
290
291          if (location.link) {
292            this.link.attr("href", location.link).css("background-color", "").show();
293            if (location.color) this.link.css("background-color", location.color);
294          } else this.link.hide();
295          if (!location?.thumbnail) this.title.text(location.title);
296          if (location.description) this.desc.html(location.description);
297          else this.desc.empty();
298          this.content[0].scrollTop = 0;
299
300          // shift
301          this.pin = $('.mapplic-pin[data-location="' + location.id + '"]');
302          if (this.pin.length == 0) {
303            this.shift = 20;
304          } else this.shift = Math.abs(parseFloat(this.pin.css("margin-top"))) + 20;
305
306          // making it visible
307          this.el.stop().css({ opacity: 1 }).show();
308          setTimeout(function () {
309            s.body.focus();
310          }, 600);
311          this.position();
312          if (self.o.zoom) this.zoom(location);
313
314          // loading & positioning
315          $("img", this.el)
316            .off("load")
317            .on("load", function () {
318              s.position();
319              if (self.o.zoom) s.zoom(location);
320            });
321        }
322      };
323
324      this.position = function () {
325        if (this.location) {
326          this.el.css({
327            left: this.location.x * 100 + "%",
328            top: this.location.y * 100 + "%",
329          });
330          this.drop = this.el.outerHeight() + this.shift;
331          if (self.o.smartip) this.repos({ data: { s: this } });
332        }
333      };
334
335      this.zoom = function (location) {
336        var ry = 0.5,
337          zoom = location.zoom ? parseFloat(location.zoom) / self.o.maxscale : 1;
338
339        ry =
340          (self.container.el.height() / 2 + this.drop / 2) / self.container.el.height();
341        self.moveTo(location.x, location.y, zoom, 600, ry);
342      };
343
344      this.hide = function () {
345        var s = this;
346
347        this.location = null;
348        this.el.stop().fadeOut(200, function () {
349          $(this).remove();
350        });
351        if (self.o.smartip) self.el.off("positionchanged", this.repos);
352      };
353    }
354
355    // hover tooltip
356    function HoverTooltip() {
357      this.el = null;
358      this.pin = null;
359      this.shift = 6;
360
361      this.init = function () {
362        var s = this;
363
364        // construct
365        this.el = $("<div></div>").addClass("mapplic-tooltip mapplic-hovertip");
366        this.wrap = $("<div></div>").addClass("mapplic-tooltip-wrap").appendTo(this.el);
367        this.title = $("<h4></h4>").addClass("mapplic-tooltip-title").appendTo(this.wrap);
368        if (self.o.hovertipdesc)
369          this.desc = $("<div></div>")
370            .addClass("mapplic-tooltip-description")
371            .appendTo(this.wrap);
372        this.triangle = $("<div></div>")
373          .addClass("mapplic-tooltip-triangle")
374          .appendTo(this.wrap);
375
376        // events
377        // markers + old svg
378        $(self.map)
379          .on("mouseover", ".mapplic-pin", function () {
380            var id = $(this).data("location");
381
382            s.pin = $('.mapplic-pin[data-location="' + id + '"]');
383            s.shift = Math.abs(parseFloat(s.pin.css("margin-top"))) + 20;
384
385            var location = self.l[id];
386            if (location && location.title) s.show(location);
387          })
388          .on("mouseout", function () {
389            s.hide();
390          });
391
392        // new svg
393        if (self.o.selector) {
394          $(self.map)
395            .on("mouseover touchstart", self.o.selector, function () {
396              var location = self.l[$(this).attr("id")];
397              s.shift = 20;
398              if (location && location.title) s.show(location);
399            })
400            .on("mouseout touchend", function () {
401              s.hide();
402            });
403        }
404
405        self.el.on("positionchanged", { s: s }, this.repos);
406
407        self.map.append(this.el);
408
409        return this;
410      };
411
412      this.repos = function (e) {
413        var s = e.data.s,
414          tx = "-50%",
415          ty = "-100%";
416
417        // vertical
418        var rtop = s.el.offset().top - self.container.el.offset().top;
419        if (rtop < s.wrap.outerHeight() + 36) {
420          s.el.addClass("mapplic-tooltip-bottom");
421          ty = "0%";
422        } else {
423          s.el.removeClass("mapplic-tooltip-bottom");
424          ty = "-100%";
425        }
426
427        // horizontal
428        var rleft = s.el.offset().left - self.container.el.offset().left;
429        if (rleft < s.wrap.outerWidth() / 2) {
430          if (rleft > 12) tx = -(100 + (rleft / s.wrap.outerWidth()) * 100) + 100 + "%";
431          else tx = "-10%";
432        } else if (rleft > self.container.el.outerWidth() - s.wrap.outerWidth() / 2) {
433          if (rleft < self.container.el.outerWidth() - 12)
434            tx =
435              ((self.container.el.outerWidth() - rleft) / s.wrap.outerWidth()) * 100 -
436              100 +
437              "%";
438          else tx = "-90%";
439        } else tx = "-50%";
440
441        s.wrap.css({ transform: "translate(" + tx + ", " + ty + ")" });
442      };
443
444      this.show = function (location) {
445        if (self.location != location) {
446          this.title.html(
447            `${
448              location.thumbnail
449                ? `<img style="width:80px; height:80px" src="${location.thumbnail}"/>`
450                : `<h4>${location.title}`
451            }`
452          );
453          if (self.o.hovertipdesc) this.desc.html(location.description);
454          this.position(location);
455
456          this.el.stop().fadeIn(100);
457        }
458        this.repos({ data: { s: this } });
459      };
460
461      this.position = function (location) {
462        if (location) {
463          this.el.css({
464            left: location.x * 100 + "%",
465            top: location.y * 100 + "%",
466          });
467
468          this.drop = this.el.outerHeight() + this.shift;
469        }
470      };
471
472      this.hide = function () {
473        this.el.stop().fadeOut(200);
474      };
475    }
476
477    // lightbox
478    function Lightbox() {
479      this.el = null;
480
481      this.init = function () {
482        // construct
483        this.el = $("<div></div>").addClass("mapplic-lightbox mfp-hide");
484        this.title = $("<h4></h4>").addClass("mapplic-lightbox-title").appendTo(this.el);
485        this.desc = $("<div></div>")
486          .addClass("mapplic-lightbox-description")
487          .appendTo(this.el);
488        this.link = $("<a>" + self.loc.more + "</a>")
489          .addClass("mapplic-popup-link")
490          .attr("href", "#")
491          .hide()
492          .appendTo(this.el);
493        if (self.o.linknewtab) this.link.attr("target", "_blank");
494
495        // append
496        self.el.append(this.el);
497
498        return this;
499      };
500
501      this.show = function (location) {
502        this.location = location;
503
504        this.title.text(location.title);
505        this.desc.html(location.description);
506        showImage();
507        if (location.link) {
508          this.link.attr("href", location.link).css("background-color", "").show();
509          if (location.color) this.link.css("background-color", location.color);
510        } else this.link.hide();
511
512        var s = this;
513
514        $.magnificPopup.open({
515          items: { src: this.el },
516          type: "inline",
517          removalDelay: 300,
518          mainClass: "mfp-fade",
519          callbacks: {
520            beforeClose: function () {
521              s.hide();
522            },
523          },
524        });
525
526        // zoom
527        var zoom = location.zoom ? parseFloat(location.zoom) : 1;
528        if (self.o.zoom) self.moveTo(location.x, location.y, zoom, 600);
529
530        return this.el[0];
531      };
532
533      this.showImage = function (location) {
534        this.location = location;
535
536        var s = this;
537
538        $.magnificPopup.open({
539          items: { src: location.image },
540          ty
540pe: "image",
541          removalDelay: 300,
542          mainClass: "mfp-fade",
543          callbacks: {
544            beforeClose: function () {
545              s.hide();
546            },
547          },
548        });
549
550        // zoom
551        var zoom = location.zoom ? parseFloat(location.zoom) : 1;
552        if (self.o.zoom) self.moveTo(location.x, location.y, zoom, 600);
553      };
554
555      this.hide = function () {
556        this.location = null;
557        self.hideLocation();
558        if (!self.o.zoom || self.o.zoomoutclose)
559          self.moveTo(0.5, 0.5, self.fitscale, 400);
560      };
561    }
562
563    // external
564    function External() {
565      this.el = null;
566
567      this.init = function () {
568        this.el = $("<div></div>").addClass("mapplic-external-content").hide();
569        this.title = $("<h4></h4>").addClass("mapplic-external-title").appendTo(this.el);
570        this.desc = $("<div></div>")
571          .addClass("mapplic-external-description")
572          .appendTo(this.el);
573
574        this.initial = $(self.o.external + " > *:not(.mapplic-external-content)");
575
576        $(self.o.external).append(this.el);
577
578        return this;
579      };
580
581      this.show = function (location) {
582        this.title.text(location.title);
583        this.desc.html(location.description);
584
585        this.initial.hide();
586        this.el.show();
587      };
588
589      this.hide = function () {
590        this.initial.show();
591        this.el.hide();
592      };
593    }
594
595    // deeplinking
596    function Deeplinking() {
597      this.param = "location";
598      this.resolved = false;
599
600      this.init = function () {
601        var s = this;
602
603        window.onpopstate = function (e) {
604          if (e.state) s.check(600);
605          return false;
606        };
607      };
608
609      this.check = function (duration) {
610        var id = this.getUrlParam(this.param);
611        if (id) {
612          self.showLocation(id, duration, true);
613          this.resolved = true;
614        }
615      };
616
617      this.getUrlParam = function (name) {
618        name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
619        var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
620          results = regex.exec(location.search);
621        return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
622      };
623
624      this.update = function (id) {
625        // var url;
626        // if (typeof window.URL !== "undefined") {
627        //   url = new URL(window.location.href);
628        //   url.searchParams.set(this.param, id);
629        //   url = url.href;
630        // } else {
631        //   url =
632        //     window.location.protocol +
633        //     "//" +
634        //     window.location.host +
635        //     window.location.pathname +
636        //     "?" +
637        //     this.param +
638        //     "=" +
639        //     id;
640        // }
641        // window.history.pushState({ path: url }, "", url);
642      };
643
644      this.clear = function () {
645        // var url;
646        // if (typeof window.URL !== "undefined") {
647        //   url = new URL(window.location.href);
648        //   url.searchParams.delete(this.param);
649        //   url = url.href;
650        // } else {
651        //   url = window.location.pathname;
652        // }
653        // history.pushState("", document.title, url);
654      };
655    }
656
657    // old hash deeplinking method for old browsers (starting IE9)
658    function DeeplinkingHash() {
659      this.param = "location";
660      this.resolved = false;
661
662      this.init = function () {
663        var s = this;
664        this.check(0);
665
666        $(window).on("hashchange", function () {
667          //s.check(600);
668        });
669      };
670
671      this.check = function (duration) {
672        var id = location.hash.slice(this.param.length + 2);
673        if (id) {
674          self.showLocation(id, duration, true);
675          this.resolved = true;
676        }
677      };
678
679      this.update = function (id) {
680        window.location.hash = this.param + "-" + id;
681      };
682
683      this.clear = function () {
684        window.location.hash = this.param;
685      };
686    }
687
688    // minimap
689    function Minimap() {
690      this.el = null;
691      this.opacity = null;
692
693      this.init = function () {
694        this.el = $("<div></div>")
695          .addClass("mapplic-minimap")
696          .appendTo(self.container.el);
697        this.el.click(function (e) {
698          e.preventDefault();
699
700          var x = (e.pageX - $(this).offset().left) / $(this).width(),
701            y = (e.pageY - $(this).offset().top) / $(this).height();
702
703          self.moveTo(x, y, self.scale / self.fitscale, 100);
704        });
705
706        return this;
707      };
708
709      this.addLayer = function (data) {
710        var layer = $("<div></div>")
711            .addClass("mapplic-minimap-layer")
712            .attr("data-floor", data.id)
713            .appendTo(this.el),
714          s = this;
715
716        $("<img>")
717          .attr("src", data.minimap)
718          .addClass("mapplic-minimap-background")
719          .attr("aria-hidden", "true")
720          .appendTo(layer);
721        $("<div></div>").addClass("mapplic-minimap-overlay").appendTo(layer);
722        $("<img>")
723          .attr("src", data.minimap)
724          .addClass("mapplic-minimap-active")
725          .attr("aria-hidden", "true")
726          .on("load", function () {
727            s.update();
728          })
729          .appendTo(layer);
730      };
731
732      this.show = function (target) {
733        $(".mapplic-minimap-layer", this.el).hide();
734        $('.mapplic-minimap-layer[data-floor="' + target + '"]', this.el).show();
735      };
736
737      this.update = function (x, y) {
738        var active = $(".mapplic-minimap-active", this.el);
739
740        if (x === undefined) x = self.x;
741        if (y === undefined) y = self.y;
742
743        var width =
744            (self.container.el.width() / self.contentWidth / self.scale) *
745            this.el.width(),
746          height =
747            (self.container.el.height() / self.contentHeight / self.scale) *
748            this.el.height(),
749          top = (-y / self.contentHeight / self.scale) * this.el.height(),
750          left = (-x / self.contentWidth / self.scale) * this.el.width(),
751          right = left + width,
752          bottom = top + height;
753
754        active.each(function () {
755          $(this)[0].style.clip =
756            "rect(" + top + "px, " + right + "px, " + bottom + "px, " + left + "px)";
757        });
758
759        // fade out effect
760        var s = this;
761        this.el.show();
762        this.el.css("opacity", 1.0);
763        clearTimeout(this.opacity);
764        this.opacity = setTimeout(function () {
765          s.el.css("opacity", 0);
766          setTimeout(function () {
767            s.el.hide();
768          }, 600);
769        }, 2000);
770      };
771    }
772
773    // legend
774    function Legend() {
775      this.el = null;
776      this.nr = 0;
777
778      this.init = function () {
779        this.el = $("<div></div>").addClass("mapplic-legend");
780        return this;
781      };
782
783      this.build = function (categories) {
784        var s = this;
785        $.each(categories, function (index, category) {
786          if (category.legend == "true") s.add(category);
787        });
788        if (this.nr > 0) this.el.appendTo(self.el);
789      };
790
791      this.add = function (group) {
792        var toggle = this.newToggle(group, true);
793        if (toggle) toggle.appendTo(this.el);
794        else {
795          var key = $("<span></span>").addClass("mapplic-legend-key");
796          if (group.color) key.css("background-color", group.color);
797          $("<span></span>")
798            .addClass("mapplic-legend-label")
799            .text(group.title)
800            .prepend(key)
801            .appendTo(this.el);
802        }
803        this.nr++;
804      };
805
806      this.toggle = function (group, state) {
807        $('*[id="' + group + '"]', self.map).toggle(state);
808        $('*[data-category*="' + group + '"]', self.map).each(function () {
809          var attr = $(this).attr("data-category");
810          if (attr == group) $(this).toggle(state);
811          else {
812            // multi-group support
813            var groups = attr.split(","),
814              show = false;
815            groups.forEach(function (g) {
816              if ($('.mapplic-toggle > input[data-group="' + g + '"]')[0].checked)
817                show = true;
818            });
819            $(this).toggle(show);
820          }
821        });
822      };
823
824      this.newToggle = function (group, title) {
825        var s = this,
826          toggle = null;
827
828        if (group.toggle == "true") {
829          toggle = $('<label class="mapplic-toggle"></label>');
830          var input = $('<input type="checkbox" checked>')
831            .attr("data-group", group.id)
832            .appendTo(toggle);
833          var circle = $("<span></span>")
834            .addClass("mapplic-toggle-circle")
835            .appendTo(toggle);
836          if (title)
837            $("<span></span>")
838              .addClass("mapplic-legend-label")
839              .text(group.title)
840              .appendTo(toggle);
841          if (group.switchoff == "true") input.prop("checked", false);
842
843          input.change(function () {
844            $('.mapplic-toggle > input[data-group="' + group.id + '"]', self.el).prop(
845              "checked",
846              this.checked
847            );
848            s.toggle(group.id, this.checked);
849          });
850        }
851        return toggle;
852      };
853
854      this.applyToggles = function () {
855        var s = this;
856        $(".mapplic-toggle > input", self.el).each(function () {
857          s.toggle($(this).attr("data-group"), this.checked);
858        });
859      };
860    }
861
862    // groups
863    function Groups() {
864      this.init = function () {};
865
866      this.addGroups = function (groups) {
867        if (groups) {
868          $.each(groups, function (index, group) {
869            self.g[group.id] = group;
870            if (group.style && self.s[group.style] && self.s[group.style].base)
871              group.color = self.s[group.style].base.fill; // overwrite color with style
872          });
873        }
874
875        if (self.o.sidebar) self.sidebar.addCategories(groups);
876      };
877    }
878
879    // directory
880    function Directory() {
881      this.dirs = [];
882      this.filters = {};
883
884      this.init = function () {
885        var s = this,
886          event = "mapready";
887
888        if (self.o.csv) event = "csvready";
889
890        self.el.on(event, function (e, self) {
891          // dir
892          $(".mapplic-dir:not(.mapplic-dir-results)").each(function () {
893            var attribute = $(this).data("attribute"),
894              pattern = $(this).data("pattern"),
895              title = $(this).text();
896
897            if (title) $("<h3><span>" + title + "</span></h3>").appendTo($(this).empty());
898            s.getDirectory(attribute, pattern, attribute).appendTo($(this));
899          });
900
901          // filter
902          $(".mapplic-dir-filter").each(function () {
903            var el = $(this),
904              attribute = el.data("attribute"),
905              options = [];
906
907            // generate options
908            if ($(this).hasClass("mapplic-dir-filter-generate")) {
909              $.each(self.l, function (id, location) {
910                if (location[attribute]) {
911                  var atts = location[attribute].toString().split(",");
912                  atts.forEach(function (att) {
913                    if (options.indexOf(att) === -1) options.push(att);
914                  });
915                }
916              });
917
918              options.forEach(function (option) {
919                var text = option;
920
921                if (attribute == "category" && self.g[option])
922                  text = self.g[option].title;
923                $("<option></option>").attr("value", option).text(text).appendTo(el);
924              });
925            }
926
927            // changed
928            $(this).on("change", function () {
929              if (this.value) s.filters[attribute] = this.value;
930              else delete s.filters[attribute];
931
932              s.search();
933            });
934          });
935        });
936
937        // search
938        $(".mapplic-dir-search").keyup(function (e) {
939          var val = $(this).val();
940
941          if (val) s.filters["keyword"] = val;
942          else delete s.filters["keyword"];
943
944          s.search();
945
946          if (e.keyCode == 13) $(".mapplic-dir-results li > a").filter(":first").click();
947          else if (e.keyCode == 27) s.clearFilters();
948        });
949
950        // results
951        var restitle = $("<h3><span>0</span> found</h3>").appendTo(
952          $(".mapplic-dir-results")
953        );
954        $("<ul></ul>").appendTo($(".mapplic-dir-results"));
955        $("<button></button>")
956          .text("Clear")
957          .append(getIcon("icon-cross"))
958          .addClass("mapplic-dir-results-clear")
959          .appendTo(restitle)
960          .on("click", function () {
961            s.clearFilters();
962          });
963
964        // list/grid
965        var directories = $(".mapplic-dir-view").parent().find(".mapplic-dir");
966        $(".mapplic-dir-view-list").click(function () {
967          $(this).hide();
968          $(".mapplic-dir-view-grid").show();
969          directories.removeClass("mapplic-dir-grid");
970        });
971        $(".mapplic-dir-view-grid").click(function () {
972          $(this).hide();
973          $(".mapplic-dir-view-list").show();
974          directories.addClass("mapplic-dir-grid");
975        });
976
977        return this;
978      };
979
980      this.getDirectory = function (attribute, pattern, sortby) {
981        var s = this,
982          dir = $("<ul></ul>"),
983          regex = new RegExp(pattern, "i");
984        $.each(self.l, function (id, location) {
985          if (attribute && pattern && !regex.test(location[attribute])) return true; // skip if no match
986          if (String(location.hide) != "true") dir.append(s.getItem(location));
987        });
988
989        if (sortby) this.sortby(dir, sortby);
990
991        this.dirs.push(dir);
992
993        return dir;
994      };
995
996      this.getItem = function (location) {
997        if (!location.id) location = self.l[location];
998
999        var item = $("<li></li>")
1000          .addClass("mapplic-dir-item")
1001          .attr("data-location", location.id);
1002        var link = $("<a></a>")
1003          .attr("href", "#")
1004          .click(function (e) {
1005            e.preventDefault();
1006            self.showLocation(location.id, 600);
1007
1008            // scroll back to map on mobile
1009            if (
1010              $(window).width() < 668 &&
1011              (location.action || self.o.action) != "lightbox"
1012            ) {
1013              $("html, body").animate(
1014                {
1015                  scrollTop: self.container.el.offset().top,
1016                },
1017                400
1018              );
1019            }
1020          })
1021          .appendTo(item);
1022
1023        if (self.o.sortby) item.data("sort", location[self.o.sortby]);
1024
1025        if (self.o.thumbholder) {
1026          var thumbnail = this.thumbnail(location.title, location.thumbnail);
1027          if (thumbnail) thumbnail.appendTo(link);
1028        }
1029
1030        $("<h4></h4>").text(location.title).appendTo(link);
1031        $("<span></span>").html(location.about).addClass("mapplic-about").appendTo(link);
1032        if (location.color) item.css("border-color", location.color);
1033
1034        location.list = item;
1035
1036        return item;
1037      };
1038
1039      this.thumbnail = function (name, field) {
1040        var elem = null;
1041
1042        if (field) {
1043          if (
1044            field.match(
1045              /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g
1046            )
1047          )
1048            elem = $("<img>")
1049              .attr("src", field)
1050              .attr("alt", name)
1051              .addClass("mapplic-thumbnail");
1052          else
1053            elem = $("<div></div>")
1054              .addClass("mapplic-thumbnail mapplic-thumbnail-placeholder")
1055              .attr("aria-hidden", "true")
1056              .html(field);
1057        } else if (name) {
1058          var words = name.split(" "),
1059            text = "";
1060
1061          if (words[0]) text += words[0][0];
1062          if (words[1]) text += words[1][0];
1063          elem = $("<div></div>")
1064            .addClass("mapplic-thumbnail mapplic-thumbnail-placeholder")
1065            .attr("aria-hidden", "true")
1066            .text(text.toUpperCase());
1067        }
1068
1069        return elem;
1070      };
1071
1072      this.search = function () {
1073        var s = this;
1074
1075        $(".mapplic-dir-results ul").empty();
1076
1077        if (self.o.highlight) {
1078          self.map.removeClass("mapplic-filtered");
1079          $(".mapplic-highlight", self.map).removeClass("mapplic-highlight");
1080        }
1081
1082        $.each(self.l, function (i, location) {
1083          var matched = true; // matched false, current ||
1084
1085          $.each(s.filters, function (attribute, filter) {
1086            var current = false;
1087
1088            if (attribute == "keyword")
1089              $.each(self.o.searchfields, function (i, field) {
1090                if (location[field] && !current)
1091                  current = !(
1092                    s
1093                      .normalizeString(location[field])
1094                      .indexOf(s.normalizeString(filter)) == -1
1095                  );
1096              });
1097            else if (location[attribute] && location[attribute].indexOf(filter) != -1)
1098              current = true;
1099
1100            matched = matched && current;
1101          });
1102
1103          if (matched) {
1104            $('.mapplic-dir-item[data-location="' + location.id + '"]')
1105              .clone(true)
1106              .appendTo($(".mapplic-dir-results ul")); // results
1107            if (self.o.highlight && !$.isEmptyObject(s.filters) && location.el)
1108              location.el.addClass("mapplic-highlight"); // highlight
1109          }
1110        });
1111
1112        s.sortby($(".mapplic-dir-results ul"), "title"); // alphabetic sort by title
1113
1114        if ($.isEmptyObject(s.filters)) {
1115          $(".mapplic-dir").show();
1116          $(".mapplic-dir-results").hide();
1117        } else {
1118          $(".mapplic-dir").hide();
1119          $(".mapplic-dir-results").show();
1120          if (self.o.highlight) self.map.addClass("mapplic-filtered");
1121        }
1122
1123        $(".mapplic-dir-results h3 span").text(
1124          $(".mapplic-dir-results .mapplic-dir-item").length
1125        ); // count
1126      };
1127
1128      this.clearFilters = function () {
1129        this.filters = {};
1130        $(".mapplic-dir-search").val("");
1131        $(".mapplic-dir-filter").val("");
1132
1133        this.search();
1134      };
1135
1136      this.normalizeString = function (s) {
1137        if (s)
1138          return s
1139            .toLowerCase()
1140            .normalize("NFD")
1141            .replace(/[\u0300-\u036f]/g, "");
1142        else return "";
1143      };
1144
1145      this.sort = function (dir, order) {
1146        if (order === undefined) order = 1;
1147        var items = dir.children(".mapplic-dir-item");
1148
1149        items
1150          .sort(function (a, b) {
1151            var a = $(a).data("sort"),
1152              b = $(b).data("sort");
1153            if (isNaN(a) || isNaN(b)) {
1154              return a < b ? -1 * order : 1 * order;
1155            } else return (a - b) * order;
1156          })
1157          .appendTo(dir);
1158      };
1159
1160      this.sortby = function (dir, attribute, order) {
1161        var s = this;
1162
1163        if (order === undefined) order = 1;
1164        var items = dir.children(".mapplic-dir-item");
1165        items.each(function () {
1166          $(this).data(
1167            "sort",
1168            s.normalizeString(self.l[$(this).data("location")][attribute])
1169          );
1170        });
1171        this.sort(dir, order);
1172      };
1173    }
1174
1175    // sidebar
1176    function Sidebar() {
1177      this.el = null;
1178      this.header = null;
1179      this.input = null;
1180      this.list = null;
1181      this.tags = null;
1182      this.toggle = null;
1183
1184      this.init = function () {
1185        var s = this;
1186
1187        this.el = $("<div></div>").addClass("mapplic-sidebar").appendTo(self.el);
1188
1189        if (self.o.sidebartoggle) {
1190          self.el.addClass("mapplic-dynamic-sidebar");
1191
1192          this.toggle = $("<button></button>")
1193            .append(getIcon("icon-sidebar"))
1194            .addClass("mapplic-button mapplic-sidebar-toggle")
1195            .appendTo(self.container.el);
1196          this.toggle.on("click touchstart", function (e) {
1197            e.preventDefault();
1198            self.el.toggleClass("mapplic-hidden-sidebar");
1199            setTimeout(function () {
1200              var wr = self.container.el.width() / self.contentWidth,
1201                hr = self.container.el.height() / self.contentHeight;
1202
1203              if (self.o.mapfill) {
1204                if (wr > hr) self.fitscale = wr;
1205                else self.fitscale = hr;
1206              } else {
1207                if (wr < hr) self.fitscale = wr;
1208                else self.fitscale = hr;
1209              }
1210
1211              if (
1212                self.container.oldW != self.container.el.width() ||
1213                self.container.oldH != self.container.el.height()
1214              ) {
1215                self.container.oldW = self.container.el.width();
1216                self.container.oldH = self.container.el.height();
1217
1218                if (self.scale * self.contentWidth < self.container.el.width())
1219                  self.moveTo(self.x, self.y, self.scale, 600);
1220              }
1221            }, 400);
1222          });
1223        }
1224
1225        if (self.o.search) {
1226          this.header = $("<div></div>")
1227            .addClass("mapplic-sidebar-header")
1228            .append(getIcon("icon-magnifier"))
1229            .appendTo(this.el);
1230          this.headerwrap = $("<div></div>").appendTo(this.header);
1231
1232          this.input = $("<input>")
1233            .attr({
1234              type: "text",
1235              spellcheck: "false",
1236              placeholder: self.o.searchPlaceHolder,
1237            })
1238            .addClass("mapplic-search-input")
1239            .keyup(function (e) {
1240              var val = $(this).val();
1241
1242              if (val) self.directory.filters["keyword"] = val;
1243              else delete self.directory.filters["keyword"];
1244
1245              s.el.toggleClass("mapplic-sidebar-header-opened", val.length < 1);
1246
1247              s.search();
1248
1249              if (e.keyCode == 13)
1250                $(".mapplic-list-container li > a").filter(":visible:first").click();
1251              else if (e.keyCode == 27) $(this).blur();
1252            });
1253          this.input.focus(function () {
1254            if ($(this).val().length < 1) s.el.addClass("mapplic-sidebar-header-opened");
1255          });
1256
1257          $("body").click(function (e) {
1258            if (!$.contains(s.header[0], e.target) && s.header[0] != e.target)
1259              s.el.removeClass("mapplic-sidebar-header-opened");
1260          });
1261
1262          this.input.appendTo(this.headerwrap);
1263
1264          /*this.clear = $('<button></button>').append(getIcon('icon-cross')).addClass('mapplic-search-clear').attr('aria-label', self.loc.clearsearch).click(function() {
1265						s.el.toggleClass('mapplic-sidebar-header-opened');
1266						//s.input.val('');
1267						//s.search();
1268					}).appendTo(this.headerwrap)*/ this.toggle = $("<button></button>")
1269            .append(getIcon("icon-filter"))
1270            .addClass("mapplic-search-toggle")
1271            .click(function (e) {
1272              e.preventDefault();
1273              s.el.toggleClass("mapplic-sidebar-header-opened");
1274            })
1275            .appendTo(this.headerwrap);
1276
1277          // tags
1278          this.tags = $("<div></div>")
1279            .addClass("mapplic-filter-tags")
1280            .appendTo(this.headerwrap);
1281
1282          // filters
1283          this.filter = $("<div></div>").addClass("mapplic-filter").appendTo(this.header);
1284
1285          // dim
1286          this.dim = $("<div></div>")
1287            .addClass("mapplic-sidebar-dim")
1288            .click(function () {
1289              s.el.removeClass("mapplic-sidebar-header-opened");
1290            })
1291            .appendTo(this.el);
1292        } else this.el.addClass("mapplic-sidebar-nosearch");
1293
1294        // items
1295        var event = "mapready";
1296        if (self.o.csv) event = "csvready";
1297
1298        self.el.on(event, function (e, self) {
1299          s.dir = self.directory
1300            .getDirectory()
1301            .addClass("mapplic-list-container")
1302            .appendTo(s.el);
1303
1304          // alphabetic sort by title
1305          if (self.o.alphabetic) self.directory.sortby(s.dir, "title");
1306        });
1307
1308        if (self.o.searchdescription && self.o.searchfields.indexOf("description") === -1)
1309          self.o.searchfields.push("description");
1310      };
1311
1312      this.addTag = function (item) {
1313        var s = this,
1314          categories = [];
1315
1316        if (self.directory.filters["category"])
1317          categories = self.directory.filters["category"].split(",");
1318
1319        if (!categories.includes(item.id)) {
1320          categories.push(item.id);
1321          self.directory.filters["category"] = categories.toString();
1322
1323          this.el.addClass("mapplic-sidebar-tagsrow");
1324
1325          var tag = $("<button></button>")
1326            .addClass("mapplic-tag")
1327            .text(item.title)
1328            .attr("aria-label", self.loc.clearfilter + ": " + item.title)
1329            .prependTo(this.tags);
1330          $("<span style='color:black;'>X</span>").appendTo(tag);
1331          if (item.color) tag.css("background-color", item.color);
1332          if (item.id) tag.attr("data-id", item.id);
1333
1334          tag
1335            .click(function () {
1336              tag.remove();
1337              if (s.tags.children().length < 1)
1338                s.el.removeClass("mapplic-sidebar-tagsrow");
1339              if (self.directory.filters["category"] == item.id)
1340                delete self.directory.filters["category"];
1341              else if (self.directory.filters["category"]) {
1342                categories = self.directory.filters["category"].split(",");
1343                var i = categories.indexOf(item.id);
1344                if (i > -1) {
1345                  categories.splice(i, 1);
1346                  self.directory.filters["category"] = categories.toString();
1347                }
1348              }
1349              s.search();
1350            })
1351            .appendTo(tag);
1352
1353          s.search();
1354          this.el.removeClass("mapplic-sidebar-header-opened");
1355        }
1356      };
1357
1358      this.addCategories = function (categories) {
1359        var s = this,
1360          list = $("<ul></ul>"),
1361          add = false;
1362
1363        if (categories) {
1364          $.each(categories, function (index, category) {
1365            category.nr = 0;
1366
1367            if (!(category.hide == "true")) {
1368              var item = $("<li></li>")
1369                .addClass("mapplic-list-category")
1370                .attr("data-category", category.id);
1371              var link = $("<a></a>").attr("href", "#").prependTo(item);
1372
1373              var thumbnail = self.directory
1374                .thumbnail(category.title, category.icon)
1375                .appendTo(link);
1376              if (category.color && thumbnail)
1377                thumbnail.css({
1378                  "background-color": category.color,
1379                  "border-color": category.color,
1380                });
1381
1382              var title = $("<h4></h4").text(category.title).appendTo(link);
1383              if (!category.about) title.addClass("mapplic-margin");
1384              else
1385                $("<span></span>")
1386                  .addClass("mapplic-about")
1387                  .html(category.about)
1388                  .appendTo(link);
1389              category.count = $("<span></span>")
1390                .text("(0)")
1391                .addClass("mapplic-list-count")
1392                .appendTo(title);
1393
1394              var toggle = self.legend.newToggle(category);
1395              if (toggle) toggle.appendTo(item);
1396
1397              link.on("click", function (e) {
1398                e.preventDefault();
1399                if (category.nr < 1 && toggle) $("> input", toggle).trigger("click");
1400                else {
1401                  //s.input.val('');
1402                  s.addTag(category);
1403                }
1404              });
1405
1406              category.list = item;
1407              item.appendTo(list);
1408              add = true;
1409            }
1410          });
1411
1412          if (add) {
1413            s.el.addClass("mapplic-sidebar-filterable");
1414
1415            //$('<h5></h5>').text('CATEGORIES').appendTo(s.filter);
1416
1417            list.appendTo(s.filter);
1418          }
1419        }
1420      };
1421
1422      this.countCategory = function () {
1423        $.each(self.g, function (i, group) {
1424          if (group.count) {
1425            group.count.text("(" + group.nr + ")");
1426            if (group.count[0]) {
1427              if (group.nr < 1) group.count[0].style.display = "none";
1428              else group.count[0].style.display = "";
1429            }
1430          }
1431        });
1432      };
1433
1434      // search
1435      this.search = function () {
1436        var s = this;
1437
1438        if (self.o.highlight) {
1439          self.map.removeClass("mapplic-filtered");
1440          $(".mapplic-highlight", self.map).removeClass("mapplic-highlight");
1441        }
1442
1443        $.each(self.l, function (i, location) {
1444          var matched = true; // matched false, current ||
1445
1446          $.each(self.directory.filters, function (attribute, filter) {
1447            var current = false;
1448
1449            if (attribute == "keyword")
1450              $.each(self.o.searchfields, function (i, field) {
1451                if (location[field] && !current)
1452                  current = !(
1453                    self.directory
1454                      .normalizeString(location[field])
1455                      .indexOf(self.directory.normalizeString(filter)) == -1
1456                  );
1457              });
1458            else if (location.category && attribute == "category") {
1459              var categories = location.category;
1460              if (typeof categories == "string") categories = categories.split(",");
1461
1462              categories.forEach(function (category) {
1463                if (self.directory.filters["category"].split(",").includes(category))
1464                  current = true;
1465              });
1466            } else if (location[attribute] && location[attribute].indexOf(filter) != -1)
1467              current = true;
1468
1469            matched = matched && current;
1470          });
1471
1472          var item = $('.mapplic-dir-item[data-location="' + location.id + '"]', self.el);
1473          if (matched) {
1474            item.show();
1475            if (
1476              self.o.highlight &&
1477              !$.isEmptyObject(self.directory.filters) &&
1478              location.el
1479            )
1480              location.el.addClass("mapplic-highlight"); // highlight
1481          } else item.hide();
1482        });
1483
1484        if (!$.isEmptyObject(self.directory.filters)) {
1485          if (self.o.highlight) self.map.addClass("mapplic-filtered");
1486        }
1487      };
1488    }
1489
1490    // developer tools
1491    function DevTools() {
1492      this.el = null;
1493
1494      this.init = function () {
1495        this.el = $("<div></div>")
1496          .addClass("mapplic-coordinates")
1497          .appendTo(self.container.el);
1498        this.el.append("x: ");
1499        $("<code></code>").addClass("mapplic-coordinates-x").appendTo(this.el);
1500        this.el.append(" y: ");
1501        $("<code></code>").addClass("mapplic-coordinates-y").appendTo(this.el);
1502
1503        $(".mapplic-layer", self.map).on("mousemove", function (e) {
1504          var x = (e.pageX - self.map.offset().left) / self.map.width() / self.scale,
1505            y = (e.pageY - self.map.offset().top) / self.map.height() / self.scale;
1506          $(".mapplic-coordinates-x").text(parseFloat(x).toFixed(4));
1507          $(".mapplic-coordinates-y").text(parseFloat(y).toFixed(4));
1508        });
1509
1510        return this;
1511      };
1512    }
1513
1514    // clear button
1515    function ClearButton() {
1516      this.el = null;
1517
1518      this.init = function () {
1519        this.el = $("<button></button>")
1520          .text(self.loc.resetzoom)
1521          .append(getIcon("icon-reset"))
1522          .addClass("mapplic-button mapplic-clear-button")
1523          .appendTo(self.container.el);
1524
1525        if (!self.o.zoombuttons) this.el.css("bottom", "0");
1526
1527        this.el.on("click touchstart", function (e) {
1528          e.preventDefault();
1529          self.hideLocation();
1530          self.moveTo(0.5, 0.5, self.fitscale, 400);
1531        });
1532
1533        return this;
1534      };
1535
1536      this.update = function (scale) {
1537        if (scale == self.fitscale) this.el.stop().fadeOut(200);
1538        else this.el.stop().fadeIn(200);
1539      };
1540    }
1541
1542    // zoom buttons
1543    function ZoomButtons() {
1544      this.el = null;
1545
1546      this.init = function () {
1547        this.el = $("<div></div>")
1548          .addClass("mapplic-zoom-buttons")
1549          .appendTo(self.container.el);
1550
1551        // zoom-in button
1552        this.zoomin = $("<button></button>")
1553          .attr("aria-label", self.loc.zoomin)
1554          .append(getIcon("icon-plus"))
1555          .addClass("mapplic-button mapplic-zoomin-button")
1556          .appendTo(this.el);
1557        this.zoomin.on("click touchstart", function (e) {
1558          e.preventDefault();
1559          self.container.stopMomentum();
1560
1561          var scale = self.scale;
1562          self.scale = normalizeScale(scale + scale * 0.8);
1563
1564          self.x = normalizeX(
1565            self.x - (self.container.el.width() / 2 - self.x) * (self.scale / scale - 1)
1566          );
1567          self.y = normalizeY(
1568            self.y - (self.container.el.height() / 2 - self.y) * (self.scale / scale - 1)
1569          );
1570
1571          zoomTo(self.x, self.y, self.scale, 400, "ease");
1572        });
1573
1574        // zoom-out button
1575        this.zoomout = $("<button></button>")
1576          .attr("aria-label", self.loc.zoomout)
1577          .append(getIcon("icon-minus"))
1578          .addClass("mapplic-button mapplic-zoomout-button")
1579          .appendTo(this.el);
1580        this.zoomout.on("click touchstart", function (e) {
1581          e.preventDefault();
1582          self.container.stopMomentum();
1583
1584          var scale = self.scale;
1585          self.scale = normalizeScale(scale - scale * 0.4);
1586
1587          self.x = normalizeX(
1588            self.x - (self.container.el.width() / 2 - self.x) * (self.scale / scale - 1)
1589          );
1590          self.y = normalizeY(
1591            self.y - (self.container.el.height() / 2 - self.y) * (self.scale / scale - 1)
1592          );
1593
1594          zoomTo(self.x, self.y, self.scale, 400, "ease");
1595        });
1596
1597        return this;
1598      };
1599
1600      this.update = function (scale) {
1601        this.zoomin.removeAttr("disabled");
1602        this.zoomout.removeAttr("disabled");
1603        if (scale == self.fitscale) this.zoomout.attr("disabled", "disabled");
1604        else if (scale == 1) this.zoomin.attr("disabled", "disabled");
1605      };
1606    }
1607
1608    // fullscreen
1609    function Fullscreen() {
1610      this.el = null;
1611      this.fsh = null; // fullscreen placeholder
1612      this.esh = null; // element placeholder
1613
1614      this.init = function () {
1615        var s = this;
1616
1617        this.fph = $("<div></div>").addClass("mapplic-fsh").prependTo("body");
1618        this.eph = $("<div></div>").addClass("mapplic-esh").insertBefore(self.el);
1619
1620        // fullscreen button
1621        this.el = $("<button></button>")
1622          .append(getIcon("icon-fullscreen"))
1623          .append(getIcon("icon-fullscreen-exit"))
1624          .addClass("mapplic-button mapplic-fullscreen-button")
1625          .click(function (e) {
1626            self.el.toggleClass("mapplic-fullscreen");
1627
1628            if (self.el.hasClass("mapplic-fullscreen")) self.el.insertBefore(s.fph);
1629            else self.el.insertBefore(s.eph);
1630
1631            $(document).resize();
1632          })
1633          .appendTo(self.container.el);
1634
1635        if (self.o.wayfinder) {
1636          // wayfinder always in fullscreen mode
1637          this.el.click();
1638        } else {
1639          // esc key
1640          $(document).on("keyup.mapplic", function (e) {
1641            if (e.keyCode === 27 && $(".mapplic-fullscreen")[0]) {
1642              $(".mapplic-element.mapplic-fullscreen").removeClass("mapplic-fullscreen");
1643              self.el.insertBefore(s.eph);
1644              $(document).resize();
1645            }
1646          });
1647        }
1648      };
1649    }
1650
1651    // styles
1652    function Styles() {
1653      this.data = null;
1654
1655      this.init = function (data) {
1656        this.data = data;
1657        this.process(data);
1658        return this;
1659      };
1660
1661      this.process = function (styles) {
1662        var css = "";
1663
1664        // basecolor
1665        if (self.o.basecolor)
1666          css += this.rule(
1667            ".mapplic-fullscreen, .mapplic-legend",
1668            "background-color",
1669            self.o.basecolor
1670          );
1671
1672        // bgcolor
1673        if (self.o.bgcolor) {
1674          css += this.rule(
1675            ".mapplic-button, .mapplic-tooltip-close .mapplic-icon, .mapplic-levels-select, .mapplic-levels button, .mapplic-level-switcher button.mapplic-selected, .mapplic-element *::-webkit-scrollbar-track, .mapplic-list-container, .mapplic-filter, .mapplic-sidebar-header, .mapplic-tooltip-wrap, .mapplic-lightbox, .mapplic-toggle:before",
1676            "background-color",
1677            self.o.bgcolor
1678          );
1679          css += this.rule(
1680            ".mapplic-legend-key, .mapplic-element *::-webkit-scrollbar-thumb",
1681            "border-color",
1682            self.o.bgcolor
1683          );
1684          css += this.rule(
1685            ".mapplic-tooltip:after",
1686            "border-color",
1687            self.o.bgcolor + " transparent transparent transparent !important"
1688          );
1689          css += this.rule(
1690            ".mapplic-tooltip-bottom.mapplic-tooltip:after",
1691            "border-color",
1692            "transparent transparent " + self.o.bgcolor + " transparent !important"
1693          );
1694        }
1695
1696        // bgcolor2
1697        if (self.o.bgcolor2) {
1698          css += this.rule(
1699            ".mapplic-thumbnail-placeholder, .mapplic-level-switcher button, .mapplic-sidebar .mapplic-dir-item > a:hover, .mapplic-sidebar-header-opened.mapplic-sidebar-filterable .mapplic-search-toggle, .mapplic-sidebar .mapplic-dir-item > a:focus, .mapplic-sidebar .mapplic-dir-item.mapplic-active > a, .mapplic-list-category > a:hover, .mapplic-zoom-buttons button:disabled, .mapplic-levels button:disabled",
1700            "background-color",
1701            self.o.bgcolor2
1702          );
1703          css += this.rule("a.mapplic-zoomin-button", "border-color", self.o.bgcolor2);
1704        }
1705
1706        // headingcolor
1707        if (self.o.headingcolor) {
1708          css += this.rule(
1709            ".mapplic-search-input, .mapplic-level-switcher button.mapplic-selected, .mapplic-list-category > a, .mapplic-tooltip-title, .mapplic-lightbox-title, .mapplic-sidebar .mapplic-dir-item h4, .mapplic-element strong, .mapplic-levels-select, .mapplic-list-category h4",
1710            "color",
1711            self.o.headingcolor
1712          );
1713          css += this.rule(".mapplic-icon", "fill", self.o.headingcolor);
1714        }
1715
1716        // textcolor
1717        if (self.o.textcolor)
1718          css += this.rule(
1719            ".mapplic-element, .mapplic-element a, .mapplic-level-switcher button, .mapplic-about, .mapplic-list-category > a .mapplic-list-count, .mapplic-search-input::placeholder, .mapplic-lightbox-description",
1720            "color",
1721            self.o.textcolor
1722          );
1723
1724        // accentcolor
1725        if (self.o.accentcolor)
1726          css += this.rule(
1727            ".mapplic-popup-link, .mapplic-accentcolor",
1728            "background-color",
1729            self.o.accentcolor
1730          );
1731
1732        if (styles) {
1733          self.s = [];
1734
1735          styles.forEach(function (s) {
1736            // styles object
1737            self.s[s.class] = s;
1738
1739            if (s.base) {
1740              css +=
1741                "." +
1742                s.class +
1743                ".mapplic-clickable:not(g), g." +
1744                s.class +
1745                ".mapplic-clickable > * { ";
1746              $.each(s.base, function (prop, val) {
1747                css += prop + ": " + val + "; ";
1748              });
1749              css += "}\n";
1750
1751              css += "." + s.class + ".mapplic-pin {\n";
1752              $.each(s.base, function (prop, val) {
1753                css += "	background-color: " + val + ";\n";
1754                css += "	border-color: " + val + ";\n";
1755              });
1756              css += "}\n\n";
1757            }
1758
1759            if (s.hover) {
1760              css +=
1761                "." +
1762                s.class +
1763                ".mapplic-highlight:not(g), g." +
1764                s.class +
1765                ".mapplic-highlight > *, ." +
1766                s.class +
1767                ".mapplic-clickable:not(g):hover, g." +
1768                s.class +
1769                ".mapplic-clickable:hover > * { ";
1770              $.each(s.hover, function (prop, val) {
1771                css += prop + ": " + val + "; ";
1772              });
1773              css += "}\n";
1774
1775              css +=
1776                "." +
1777                s.class +
1778                ".mapplic-pin.mapplic-highlight, ." +
1779                s.class +
1780                ".mapplic-pin:hover {\n";
1781              $.each(s.hover, function (prop, val) {
1782                css += "	background-color: " + val + ";\n";
1783                css += "	border-color: " + val + ";\n";
1784              });
1785              css += "}\n\n";
1786            }
1787
1788            if (s.active) {
1789              css +=
1790                "." +
1791                s.class +
1792                ".mapplic-active:not(g), g." +
1793                s.class +
1794                ".mapplic-active > * { ";
1795              $.each(s.active, function (prop, val) {
1796                css += prop + ": " + val + " !important; ";
1797              });
1798              css += "}\n";
1799
1800              css += "." + s.class + ".mapplic-pin.mapplic-active {\n";
1801              $.each(s.active, function (prop, val) {
1802                css += "	background-color: " + val + ";\n";
1803                css += "	border-color: " + val + ";\n";
1804              });
1805              css += "}\n\n";
1806            }
1807          });
1808        }
1809
1810        if (self.o.customcss) css += self.o.customcss;
1811
1812        var style = $("<style></style>").html(css).appendTo("body");
1813      };
1814
1815      this.rule = function (selector, attribute, value) {
1816        var css = selector + " {\n";
1817        css += "	" + attribute + ": " + value + ";\n";
1818        css += "}\n\n";
1819
1820        return css;
1821      };
1822    }
1823
1824    function Container() {
1825      this.el = null;
1826      this.oldW = 0;
1827      this.oldH = 0;
1828      (this.position = { x: 0, y: 0 }), (this.momentum = null);
1829      this.levels = {};
1830
1831      this.init = function () {
1832        this.el = $("<div></div>").addClass("mapplic-container").appendTo(self.el);
1833        self.map = $("<div></div>").addClass("mapplic-map").appendTo(this.el);
1834
1835        self.map.css({
1836          width: self.contentWidth,
1837          height: self.contentHeight,
1838        });
1839
1840        return this;
1841      };
1842
1843      // returns container height (px)
1844      this.calcHeight = function (v) {
1845        var val = Math.min(
1846          Math.max(this.getHeight(v), this.getHeight(self.o.minheight)),
1847          this.getHeight(self.o.maxheight)
1848        );
1849
1850        if ($.isNumeric(val)) return val;
1851        else return false;
1852      };
1853
1854      this.getHeight = function (v) {
1855        var val = v.toString().replace("px", "");
1856
1857        if (val == "auto" && self.container.el) {
1858          val = (self.container.el.width() * self.contentHeight) / self.contentWidth;
1859        } else if (val.slice(-1) == "%") {
1860          val = ($(window).height() * val.replace("%", "")) / 100;
1861        }
1862
1863        return val;
1864      };
1865
1866      this.resetZoom = function () {
1867        var init = self.l["init"];
1868        if (init) {
1869          self.switchLevel(init.level);
1870          if (!self.bboxZoom(init.el)) self.zoomLocation(init);
1871        } else self.moveTo(0.5, 0.5, self.fitscale, 0);
1872      };
1873
1874      this.revealChild = function (parent) {
1875        $(".mapplic-pin[data-location^=" + parent.id + "-]", self.map).addClass(
1876          "mapplic-revealed"
1877        );
1878        $(".mapplic-map-image [id^=" + parent.id + "-]", self.map).addClass(
1879          "mapplic-revealed"
1880        );
1881      };
1882
1883      this.revealZoom = function (zoom) {
1884        $(".mapplic-pin[data-reveal]", self.map).each(function () {
1885          var reveal = $(this).data("reveal");
1886          if (zoom * self.o.maxscale >= reveal) $(this).addClass("mapplic-revealed");
1887          else $(this).removeClass("mapplic-revealed");
1888        });
1889      };
1890
1891      this.coverAll = function () {
1892        $(".mapplic-revealed", self.map).removeClass("mapplic-revealed");
1893      };
1894
1895      this.stopMomentum = function () {
1896        cancelAnimationFrame(this.momentum);
1897        if (this.momentum != null) {
1898          self.x = this.position.x;
1899          self.y = this.position.y;
1900        }
1901        this.momentum = null;
1902      };
1903
1904      this.addLevelSwitcher = function () {
1905        if (self.data.levels.length > 1) {
1906          var control = $("<div></div>").addClass("mapplic-level-switcher");
1907          self.data.levels.forEach(function (level, i) {
1908            var button = $("<button></button>")
1909              .attr("data-level", level.id)
1910              .text(level.title)
1911              .prependTo(control)
1912              .click(function (e) {
1913                e.preventDefault();
1914                self.switchLevel(level.id);
1915              });
1916            if (level.show) button.addClass("mapplic-selected");
1917          });
1918
1919          this.el.append(control);
1920
1921          self.el.on("levelswitched", function (e, target) {
1922            $("button", control).removeClass("mapplic-selected");
1923            $('button[data-level="' + target + '"]', control).addClass(
1924              "mapplic-selected"
1925            );
1926          });
1927        }
1928      };
1929
1930      this.addControls = function () {
1931        self.map.addClass("mapplic-zoomable");
1932
1933        document.ondragstart = function () {
1934          return false;
1935        }; // IE drag fix
1936
1937        // momentum
1938        var friction = 0.85,
1939          mouse = { x: 0, y: 0 },
1940          pre = { x: 0, y: 0 },
1941          previous = { x: this.position.x, y: this.position.y },
1942          velocity = { x: 0, y: 0 };
1943
1944        var s = this;
1945        var momentumStep = function () {
1946          s.momentum = requestAnimationFrame(momentumStep);
1947
1948          if (self.map.hasClass("mapplic-dragging")) {
1949            pre.x = previous.x;
1950            pre.y = previous.y;
1951
1952            previous.x = s.position.x;
1953            previous.y = s.position.y;
1954
1955            s.position.x = mouse.x;
1956            s.position.y = mouse.y;
1957
1958            velocity.x = previous.x - pre.x;
1959            velocity.y = previous.y - pre.y;
1960          } else {
1961            s.position.x += velocity.x;
1962            s.position.y += velocity.y;
1963
1964            velocity.x *= friction;
1965            velocity.y *= friction;
1966
1967            if (Math.abs(velocity.x) + Math.abs(velocity.y) < 0.1) {
1968              s.stopMomentum();
1969              self.x = s.position.x;
1970              self.y = s.position.y;
1971            }
1972          }
1973          s.position.x = normalizeX(s.position.x);
1974          s.position.y = normalizeY(s.position.y);
1975
1976          zoomTo(s.position.x, s.position.y);
1977        };
1978
1979        // drag & drop
1980        $(".mapplic-map-image", self.map).on("mousedown", function (e) {
1981          self.dragging = false;
1982          self.map.addClass("mapplic-dragging");
1983
1984          s.stopMomentum();
1985          var initial = {
1986            x: e.pageX - self.x,
1987            y: e.pageY - self.y,
1988          };
1989
1990          mouse.x = normalizeX(e.pageX - initial.x);
1991          mouse.y = normalizeY(e.pageY - initial.y);
1992          momentumStep();
1993
1994          self.map.on("mousemove", function (e) {
1995            self.dragging = true;
1996
1997            mouse.x = normalizeX(e.pageX - initial.x);
1998            mouse.y = normalizeY(e.pageY - initial.y);
1999          });
2000
2001          $(document).on("mouseup.mapplic", function () {
2002            $(document).off("mouseup.mapplic");
2003            self.map.off("mousemove");
2004            self.map.removeClass("mapplic-dragging");
2005          });
2006        });
2007
2008        // mousewheel
2009        if (self.o.mousewheel)
2010          $(".mapplic-map-image", self.el).on(
2011            "mousewheel DOMMouseScroll",
2012            self.mouseWheel
2013          );
2014
2015        // touch
2016        var init1 = null,
2017          init2 = null,
2018          initD = 0,
2019          initScale = null;
2020
2021        $(".mapplic-map-image", self.map).on("touchstart", function (e) {
2022          e.preventDefault();
2023          var eo = e.originalEvent;
2024
2025          if (eo.touches.length == 1) {
2026            self.map.addClass("mapplic-dragging");
2027            self.dragging = false;
2028
2029            s.stopMomentum();
2030
2031            init1 = {
2032              x: eo.touches[0].pageX - self.x,
2033              y: eo.touches[0].pageY - self.y,
2034            };
2035
2036            self.firstcoord = {
2037              x: eo.touches[0].pageX,
2038              y: eo.touches[0].pageY,
2039            };
2040
2041            mouse = {
2042              x: normalizeX(eo.touches[0].pageX - init1.x),
2043              y: normalizeY(eo.touches[0].pageY - init1.y),
2044            };
2045            momentumStep();
2046
2047            self.map.on("touchmove", function (e) {
2048              e.preventDefault();
2049              self.dragging = true;
2050              var eo = e.originalEvent;
2051
2052              if (eo.touches.length == 1) {
2053                mouse.x = normalizeX(eo.touches[0].pageX - init1.x);
2054                mouse.y = normalizeY(eo.touches[0].pageY - init1.y);
2055
2056                self.lastcoord = {
2057                  x: eo.touches[0].pageX,
2058                  y: eo.touches[0].pageY,
2059                };
2060              } else if (eo.touches.length > 1) {
2061                var pos = {
2062                  x: (eo.touches[0].pageX + eo.touches[1].pageX) / 2,
2063                  y: (eo.touches[0].pageY + eo.touches[1].pageY) / 2,
2064                };
2065
2066                var dist =
2067                  Math.sqrt(
2068                    Math.pow(eo.touches[0].pageX - eo.touches[1].pageX, 2) +
2069                      Math.pow(eo.touches[0].pageY - eo.touches[1].pageY, 2)
2070                  ) / initD;
2071
2072                var scale = self.scale;
2073                self.scale = normalizeScale(initScale * dist);
2074
2075                self.x = normalizeX(
2076                  self.x -
2077                    (pos.x - self.container.el.offset().left - self.x) *
2078                      (self.scale / scale - 1)
2079                );
2080                self.y = normalizeY(
2081                  self.y -
2082                    (pos.y - self.container.el.offset().top - self.y) *
2083                      (self.scale / scale - 1)
2084                );
2085
2086                zoomTo(self.x, self.y, self.scale, 100, "ease");
2087              }
2088            });
2089
2090            $(document).on("touchend.mapplic", function (e) {
2091              e.preventDefault();
2092              var dragback = null,
2093                eo = e.originalEvent;
2094
2095              if (eo.touches.length == 0) {
2096                clearTimeout(dragback);
2097                $(document).off("touchend.mapplic");
2098                self.map.off("touchmove");
2099                self.map.removeClass("mapplic-dragging");
2100                self.dragging = false;
2101              } else if (eo.touches.length == 1) {
2102                dragback = setTimeout(function () {
2103                  self.map.addClass("mapplic-dragging");
2104                  self.dragging = true;
2105
2106                  s.stopMomentum();
2107                  init1 = {
2108                    x: eo.touches[0].pageX - self.x,
2109                    y: eo.touches[0].pageY - self.y,
2110                  };
2111
2112                  mouse = {
2113                    x: normalizeX(eo.touches[0].pageX - init1.x),
2114                    y: normalizeY(eo.touches[0].pageY - init1.y),
2115                  };
2116
2117                  momentumStep();
2118                }, 60);
2119              }
2120            });
2121          }
2122
2123          // pinch
2124          else if (eo.touches.length == 2) {
2125            self.dragging = true;
2126            self.map.addClass("mapplic-dragging");
2127
2128            s.stopMomentum();
2129
2130            init2 = {
2131              x: eo.touches[1].pageX - self.x,
2132              y: eo.touches[1].pageY - self.y,
2133            };
2134            initD = Math.sqrt(
2135              Math.pow(init1.x - init2.x, 2) + Math.pow(init1.y - init2.y, 2)
2136            );
2137            initScale = self.scale;
2138          }
2139        });
2140      };
2141    }
2142
2143    // functions
2144    var processData = function (data) {
2145      self.data = data;
2146      self.g = {};
2147      self.l = {};
2148      var shownLevel = null;
2149
2150      // extend options
2151      self.o = $.extend(self.o, data);
2152      $.each(self.el.data(), function (i, v) {
2153        self.o[i] = v;
2154      });
2155      self.o.zoommargin = parseFloat(self.o.zoommargin);
2156      self.o.maxscale = parseFloat(self.o.maxscale);
2157
2158      // more text
2159      if (self.o.moretext) self.loc.more = self.o.moretext;
2160
2161      // height of container
2162      if (self.el.data("height")) self.o.height = self.el.data("height");
2163      self.contentWidth = parseFloat(data.mapwidth);
2164      self.contentHeight = parseFloat(data.mapheight);
2165
2166      // limiting to scale 1
2167      self.contentWidth = self.contentWidth * self.o.maxscale;
2168      self.contentHeight = self.contentHeight * self.o.maxscale;
2169
2170      // create container
2171      self.container = new Container().init();
2172
2173      // styles
2174      self.styles = new Styles().init(self.o.styles);
2175
2176      // create minimap
2177      if (self.o.minimap) self.minimap = new Minimap().init();
2178
2179      // create legend
2180      self.legend = new Legend().init();
2181      self.legend.build(data.groups || data.categories);
2182
2183      // directory
2184      self.directory = new Directory().init();
2185
2186      // create sidebar
2187      if (self.o.sidebar) {
2188        self.sidebar = new Sidebar();
2189        self.sidebar.init();
2190      } else self.container.el.css("width", "100%");
2191
2192      // groups
2193      self.groups = new Groups();
2194      self.groups.addGroups(data.groups || data.categories);
2195
2196      // trigger event
2197      self.el.trigger("mapstart", self);
2198
2199      // iterate through levels
2200      var levelnr = 0,
2201        toload = 0;
2202
2203      if (data.levels) {
2204        $.each(data.levels, function (index, level) {
2205          var source = level.map,
2206            extension = source.substr(source.lastIndexOf(".") + 1).toLowerCase();
2207
2208          // new map layer
2209          var layer = $("<div></div>")
2210            .addClass("mapplic-layer")
2211            .attr("data-floor", level.id)
2212            .appendTo(self.map);
2213          switch (extension) {
2214            // image formats
2215            case "jpg":
2216            case "jpeg":
2217            case "png":
2218            case "gif":
2219              $("<img>")
2220                .attr("src", source)
2221                .attr("aria-hidden", "true")
2222                .addClass("mapplic-map-image")
2223                .appendTo(layer);
2224              if (level.locations) self.addLocations(level.locations, level.id);
2225              break;
2226
2227            // vector format
2228            case "svg":
2229              toload++;
2230              var mapimage = $("<div></div>")
2231                .addClass("mapplic-map-image")
2232                .appendTo(layer);
2233
2234              $("<div></div>").load(source, function () {
2235                // sanitize svg - XSS protection
2236                $("script", this).remove();
2237                mapimage.html($(this).html());
2238
2239                // illustrator duplicate id fix
2240                $(self.o.selector, mapimage).each(function () {
2241                  var id = $(this).attr("id");
2242                  if (id) $(this).attr("id", id.replace(/_[1-9]+_$/g, ""));
2243                });
2244
2245                // add locations
2246                if (level.locations) self.addLocations(level.locations, level.id);
2247
2248                // click event
2249                $(self.o.selector, mapimage).on("click touchend", function (e) {
2250                  // display polygon id as hash
2251                  window.location.hash = "roomId=" + ($(this).attr("id") || "");
2252
2253                  // display coords info inside console
2254                  const pos = adjustedPosition([e.currentTarget]);
2255                  console.info(`x: ${pos?.x?.toFixed(4)}, y: ${pos?.y?.toFixed(4)}`);
2256
2257                  var shift =
2258                    Math.abs(self.firstcoord.x - self.lastcoord.x) +
2259                    Math.abs(self.firstcoord.y - self.lastcoord.y);
2260                  if (!self.dragging || shift < 4)
2261                    self.showLocation($(this).attr("id"), 600);
2262                });
2263
2264                // autopopulate
2265                if (self.o.autopopulate) {
2266                  var ap = [];
2267                  $(self.o.selector, mapimage).each(function () {
2268                    var id = $(this).attr("id"),
2269                      location = self.l[id];
2270
2271                    if (!location) {
2272                      location = {
2273                        id: id,
2274                        title: id.charAt(0).toUpperCase() + id.slice(1),
2275                        pin: "hidden",
2276                      };
2277                      ap.push(location);
2278                    }
2279                  });
2280                  self.addLocations(ap, level.id);
2281                }
2282
2283                // trigger event(s)
2284                self.el.trigger("svgloaded", [mapimage, level.id]);
2285                toload--;
2286                if (toload == 0) mapReady();
2287              });
2288              break;
2289
2290            // others
2291            default:
2292            // alert("File type " + extension + " is not supported!");
2293          }
2294
2295          // create new minimap layer
2296          if (self.minimap) self.minimap.addLayer(level);
2297
2298          // shown level
2299          if (!shownLevel || level.show) shownLevel = level.id;
2300
2301          levelnr++;
2302        });
2303      }
2304
2305      // COMPONENTS
2306      self.tooltips = [];
2307      if (self.o.lightbox && $.magnificPopup) self.lightbox = new Lightbox().init();
2308      if (self.o.hovertip) self.hovertip = new HoverTooltip().init();
2309      if (self.o.external) self.external = new External().init();
2310      if (self.o.clearbutton) self.clearbutton = new ClearButton().init();
2311      if (self.o.zoombuttons) self.zoombuttons = new ZoomButtons().init();
2312      if (self.o.fullscreen) self.fullscreen = new Fullscreen().init();
2313      if (self.o.developer) self.devtools = new DevTools().init();
2314
2315      self.container.addLevelSwitcher();
2316      self.switchLevel(shownLevel);
2317
2318      // enable wayfinder
2319      if (self.o.wayfinder) self.wayfinder = new Wayfinder().init();
2320
2321      if (self.o.portrait === "true") self.o.portrait = true;
2322
2323      // resize
2324      $(window)
2325        .resize(function () {
2326          if (
2327            self.o.portrait == true ||
2328            ($.isNumeric(self.o.portrait) &&
2329              self.el.width() < parseFloat(self.o.portrait))
2330          ) {
2331            self.el.addClass("mapplic-portrait");
2332            if (self.el.hasClass("mapplic-fullscreen"))
2333              self.container.el.height($(window).height());
2334            else self.container.el.height(self.container.calcHeight(self.o.height));
2335          } else {
2336            self.el.removeClass("mapplic-portrait");
2337            self.container.el.height("100%");
2338            self.el.height(self.container.calcHeight(self.o.height));
2339          }
2340
2341          var wr = self.container.el.width() / self.contentWidth,
2342            hr = self.container.el.height() / self.contentHeight;
2343
2344          if (self.o.mapfill) {
2345            if (wr > hr) self.fitscale = wr;
2346            else self.fitscale = hr;
2347          } else {
2348            if (wr < hr) self.fitscale = wr;
2349            else self.fitscale = hr;
2350          }
2351
2352          if (
2353            self.container.oldW != self.container.el.width() ||
2354            self.container.oldH != self.container.el.height()
2355          ) {
2356            self.container.oldW = self.container.el.width();
2357            self.container.oldH = self.container.el.height();
2358
2359            self.container.resetZoom();
2360          }
2361
2362          // adjust map legend position
2363          if (self.el.hasClass("mapplic-portrait")) {
2364            const legendOverflow = 160;
2365            const legendTop = self.container.el.height() - legendOverflow;
2366            self.legend.el.css("top", `${legendTop}px`);
2367
2368            self.legend.el.css("bottom", "");
2369            self.el.css("margin-bottom", "0");
2370
2371            const adjustedY = self.legend.el.height() - legendOverflow;
2372            if (adjustedY > 0) {
2373              self.container.el.css("margin-bottom", `${adjustedY}px`);
2374            }
2375          } else {
2376            self.legend.el.css("top", "");
2377            self.container.el.css("margin-bottom", "0");
2378
2379            const adjustedY = self.legend.el.height() + 20;
2380            self.legend.el.css("bottom", `-${adjustedY}px`);
2381            self.el.css("margin-bottom", `${adjustedY}px`);
2382          }
2383        })
2384        .resize();
2385
2386      // deeplinking
2387      if (self.o.deeplinking) {
2388        if (history.pushState && typeof URL == "function")
2389          self.deeplinking = new Deeplinking();
2390        else self.deeplinking = new DeeplinkingHash();
2391
2392        self.deeplinking.init();
2393      }
2394
2395      // trigger event
2396      if (toload == 0) mapReady();
2397
2398      // controls
2399      if (self.o.zoom) self.container.addControls();
2400      self.firstcoord = self.lastcoord = {};
2401
2402      // link to locations
2403      $(document).on("click", ".mapplic-location", function (e) {
2404        e.preventDefault();
2405        self.showLocation($(this).attr("href").substr(1), 400);
2406        $("html, body").animate({ scrollTop: self.container.el.offset().top }, 400);
2407      });
2408    };
2409
2410    var mapReady = function () {
2411      // separate location array
2412      self.addLocations(self.data.locations);
2413
2414      // trigger event
2415      self.el.trigger("mapready", self);
2416
2417      // apply toggle
2418      self.legend.applyToggles();
2419
2420      // CSV support
2421      if (self.o.csv) {
2422        Papa.parse(self.o.csv, {
2423          header: true,
2424          download: true,
2425          encoding: "UTF-8",
2426          skipEmptyLines: true,
2427          complete: function (results, file) {
2428            self.addLocations(results.data);
2429            $(".mapplic-pin", self.map).css({
2430              transform: "scale(" + 1 / self.scale + ")",
2431            });
2432            if (self.deeplinking) self.deeplinking.check(0);
2433
2434            self.el.trigger("csvready", self);
2435          },
2436        });
2437      }
2438
2439      self.container.resetZoom();
2440      if (self.deeplinking) self.deeplinking.check(0);
2441
2442      // landmark mode
2443      if (self.el.data("landmark")) self.o.landmark = self.el.data("landmark");
2444      if (self.o.landmark) {
2445        // Custom settings
2446        self.o.sidebar = false;
2447        self.o.zoombuttons = false;
2448        self.o.deeplinking = false;
2449        self.showLocation(self.o.landmark, 0, true);
2450      }
2451
2452      // wayfinder pins
2453      if (self.wayfinder) self.wayfinder.update();
2454    };
2455
2456    /* PRIVATE METHODS */
2457
2458    // Web Mercator (EPSG:3857) lat/lng projection
2459    var latlngToPos = function (lat, lng) {
2460      var deltaLng = self.data.rightLng - self.data.leftLng,
2461        bottomLatDegree = (self.data.bottomLat * Math.PI) / 180,
2462        mapWidth = ((self.data.mapwidth / deltaLng) * 360) / (2 * Math.PI),
2463        mapOffsetY =
2464          (mapWidth / 2) *
2465          Math.log((1 + Math.sin(bottomLatDegree)) / (1 - Math.sin(bottomLatDegree)));
2466
2467      lat = (lat * Math.PI) / 180;
2468
2469      return {
2470        x:
2471          ((lng - self.data.leftLng) * (self.data.mapwidth / deltaLng)) /
2472          self.data.mapwidth,
2473        y:
2474          (self.data.mapheight -
2475            ((mapWidth / 2) * Math.log((1 + Math.sin(lat)) / (1 - Math.sin(lat))) -
2476              mapOffsetY)) /
2477          self.data.mapheight,
2478      };
2479    };
2480
2481    var adjustedPosition = function (element) {
2482      if (!element || !(element[0] instanceof SVGElement)) return false;
2483
2484      var viewBox = element[0].closest("svg")?.viewBox?.baseVal;
2485      var bbox = element[0]?.getBBox();
2486      if (!viewBox || !bbox) return false;
2487
2488      return {
2489        x: (bbox.x + bbox.width / 2) / viewBox.width,
2490        y: (bbox.y + bbox.height / 2) / viewBox.height,
2491      };
2492    };
2493
2494    var estimatedPosition = function (element) {
2495      if (!element || !(element[0] instanceof SVGElement)) return false;
2496
2497      var bbox = element[0].getBBox();
2498
2499      var padding = 40,
2500        wr = self.container.el.width() / (bbox.width + padding),
2501        hr = self.container.el.height() / (bbox.height + padding);
2502
2503      return {
2504        x: ((bbox.x + bbox.width / 2) / self.contentWidth) * self.o.maxscale,
2505        y: ((bbox.y + bbox.height / 2) / self.contentHeight) * self.o.maxscale,
2506        scale: Math.min(wr, hr) / self.o.maxscale,
2507      };
2508    };
2509
2510    var getIcon = function (name) {
2511      return (
2512        '<svg class="mapplic-icon mapplic-' +
2513        name +
2514        '" aria-hidden="true"><use xlink:href="' +
2515        self.loc.iconfile +
2516        "#" +
2517        name +
2518        '"></use></svg>'
2519      );
2520    };
2521
2522    // normalizing x, y and scale
2523    var normalizeX = function (x) {
2524      var minX = (self.container.el.width() - self.contentWidth * self.scale).toFixed(4);
2525      if (minX < 0) {
2526        if (x > self.o.zoommargin) x = self.o.zoommargin;
2527        else if (x < minX - self.o.zoommargin) x = minX - self.o.zoommargin;
2528      } else x = minX / 2;
2529
2530      return x;
2531    };
2532
2533    var normalizeY = function (y) {
2534      var minY = (self.container.el.height() - self.contentHeight * self.scale).toFixed(
2535        4
2536      );
2537      if (minY < 0) {
2538        if (y > self.o.zoommargin) y = self.o.zoommargin;
2539        else if (y < minY - self.o.zoommargin) y = minY - self.o.zoommargin;
2540      } else y = minY / 2;
2541
2542      return y;
2543    };
2544
2545    var normalizeScale = function (scale) {
2546      if (self.fitscale > 1) return self.fitscale; // no zoom
2547
2548      if (scale <= self.fitscale) scale = self.fitscale;
2549      else if (scale > 1) scale = 1;
2550
2551      // zoom timeout
2552      clearTimeout(self.zoomTimeout);
2553      self.zoomTimeout = setTimeout(function () {
2554        if (self.zoombuttons) self.zoombuttons.update(scale);
2555        if (self.clearbutton) self.clearbutton.update(scale);
2556        if (scale == self.fitscale) {
2557          self.container.coverAll();
2558          if (self.o.closezoomout) self.hideLocation();
2559        }
2560        self.container.revealZoom(scale);
2561      }, 200);
2562
2563      return scale;
2564    };
2565
2566    var moveTimeout = null;
2567
2568    var zoomTo = function (x, y, scale, d) {
2569      d = typeof d !== "undefined" ? d / 1000 : 0;
2570
2571      // move class
2572      self.el.addClass("mapplic-move");
2573      clearTimeout(moveTimeout);
2574      moveTimeout = setTimeout(function () {
2575        self.el.removeClass("mapplic-move");
2576        self.el.trigger("positionchanged", location);
2577      }, 400);
2578
2579      // transforms
2580      self.map.css({
2581        transition: "transform " + d + "s",
2582        transform:
2583          "translate(" +
2584          x.toFixed(3) +
2585          "px ," +
2586          y.toFixed(3) +
2587          "px) scale(" +
2588          self.scale.toFixed(3) +
2589          ")",
2590      });
2591
2592      if (scale) {
2593        $(".mapplic-pin, .mapplic-tooltip", self.map).css({
2594          transition: "transform " + d + "s",
2595          transform: "scale(" + 1 / scale + ")",
2596        });
2597      }
2598
2599      if (self.minimap) self.minimap.update(x, y);
2600
2601      // trigger event
2602      self.el.trigger("positionchanged", location);
2603    };
2604
2605    var replaceVars = function (template, location) {
2606      template = template.replace(/\{\{([^}]+)\}\}/g, function (match) {
2607        match = match.slice(2, -2);
2608        var sub = match.split(".");
2609        if (sub.length > 1) {
2610          var temp = location;
2611          sub.forEach(function (item) {
2612            if (!temp[item]) {
2613              temp = "{{" + match + "}}";
2614              return;
2615            }
2616            temp = temp[item];
2617          });
2618          return temp;
2619        } else {
2620          if (!location[match]) return "{{" + match + "}}";
2621          return location[match];
2622        }
2623      });
2624      return template;
2625    };
2626
2627    /* PUBLIC METHODS */
2628    var levelTimeout = null;
2629    self.switchLevel = function (target) {
2630      // no such layer
2631      if (!target) return;
2632
2633      var layer = $('.mapplic-layer[data-floor="' + target + '"]', self.map);
2634
2635      // target layer is already active
2636      if (layer.hasClass("mapplic-visible")) return;
2637
2638      // show target layer
2639      var old = $(".mapplic-layer.mapplic-visible", self.map).removeClass(
2640        "mapplic-visible"
2641      );
2642
2643      clearTimeout(levelTimeout);
2644      levelTimeout = setTimeout(function () {
2645        $('.mapplic-layer:not([data-floor="' + target + '"])', self.map).addClass(
2646          "mapplic-hidden"
2647        );
2648      }, 300);
2649      layer.removeClass("mapplic-hidden").addClass("mapplic-visible");
2650
2651      // slide animation
2652      if (self.o.animations) {
2653        var found = false;
2654        $(".mapplic-layer", self.map).each(function () {
2655          if ($(this).data("floor") == target) {
2656            $(this).removeClass("mapplic-layer-up").removeClass("mapplic-layer-down");
2657            found = true;
2658          } else if (found) $(this).addClass("mapplic-layer-up");
2659          else $(this).addClass("mapplic-layer-down");
2660        });
2661      }
2662
2663      // show target minimap layer
2664      if (self.minimap) self.minimap.show(target);
2665
2666      self.level = target;
2667
2668      // trigger event
2669      self.el.trigger("levelswitched", target);
2670    };
2671
2672    self.mouseWheel = function (e, delta) {
2673      e.preventDefault();
2674      self.container.stopMomentum();
2675
2676      var scale = self.scale;
2677
2678      self.scale = normalizeScale(scale + (scale * delta) / 5);
2679
2680      self.x = normalizeX(
2681        self.x -
2682          (e.pageX - self.container.el.offset().left - self.x) * (self.scale / scale - 1)
2683      );
2684      self.y = normalizeY(
2685        self.y -
2686          (e.pageY - self.container.el.offset().top - self.y) * (self.scale / scale - 1)
2687      );
2688
2689      zoomTo(self.x, self.y, self.scale, 200, "ease");
2690    };
2691
2692    self.addTooltip = function (location, check) {
2693      var tooltip = new Tooltip().init(location, check);
2694      self.tooltips.push(tooltip);
2695
2696      return tooltip.wrap[0];
2697    };
2698
2699    self.closeTooltips = function () {
2700      self.tooltips.forEach(function (t, i) {
2701        t.hide();
2702        self.tooltips.splice(i, 1);
2703      });
2704    };
2705
2706    self.moveTo = function (x, y, s, duration, ry) {
2707      duration = typeof duration !== "undefined" ? duration : 400;
2708      ry = typeof ry !== "undefined" ? ry : 0.5;
2709      s = typeof s !== "undefined" ? s : self.scale / self.fitscale;
2710
2711      self.container.stopMomentum();
2712
2713      self.scale = normalizeScale(s);
2714      self.x = normalizeX(
2715        self.container.el.width() * 0.5 - self.scale * self.contentWidth * x
2716      );
2717      self.y = normalizeY(
2718        self.container.el.height() * ry - self.scale * self.contentHeight * y
2719      );
2720
2721      zoomTo(self.x, self.y, self.scale, duration);
2722    };
2723
2724    self.bboxZoom = function (element) {
2725      var pos = estimatedPosition(element);
2726      if (!pos) return false;
2727
2728      self.moveTo(pos.x, pos.y, pos.scale, 600);
2729      return true;
2730    };
2731
2732    // adding locations
2733    self.addLocations = function (locations, levelid) {
2734      $.each(locations, function (index, location) {
2735        if (location.id) self.addLocation(location, levelid);
2736      });
2737    };
2738
2739    self.addLocation = function (location, levelid) {
2740      // jump if location ID exists
2741      if (self.l[location.id]) return true;
2742
2743      // building the location object
2744      self.l[location.id] = location;
2745
2746      // groups
2747      var groups = location.category;
2748      if (groups) {
2749        if (typeof groups == "string") groups = groups.toString().split(",");
2750        groups.forEach(function (group) {
2751          if (self.g[group]) self.g[group].nr++;
2752        });
2753      }
2754
2755      // cascaded fill
2756      var fill =
2757        (location && location.fill) ||
2758        (location.category &&
2759          self.g[location.category] &&
2760          self.g[location.category].color) ||
2761        (location.category &&
2762          location.category[0] &&
2763          self.g[location.category[0]] &&
2764          self.g[location.category[0]].color) ||
2765        self.o.fillcolor ||
2766        false;
2767
2768      // cascade style to attribute styleD!
2769      location.styled =
2770        (location && location.style) ||
2771        (location.category &&
2772          self.g[location.category] &&
2773          self.g[location.category].style) ||
2774        (location.category &&
2775          location.category[0] &&
2776          self.g[location.category[0]] &&
2777          self.g[location.category[0]].style) ||
2778        self.o.defaultstyle ||
2779        false;
2780
2781      if (fill) location.color = fill;
2782      else if (location.styled) location.color = location.styled.base;
2783
2784      // interactive element
2785      var elem = $(
2786        '[id^=MLOC] > *[id="' +
2787          location.id +
2788          '"], [id^=landmark] > *[id="' +
2789          location.id +
2790          '"]',
2791        self.map
2792      );
2793      if (elem.length > 0) {
2794        location.el = elem;
2795        location.el.addClass("mapplic-clickable");
2796
2797        if (location.styled) {
2798          location.el.addClass(location.styled);
2799        } else if (fill) {
2800          location.el.css("fill", fill);
2801          $("> *", location.el).css("fill", fill);
2802        }
2803      }
2804
2805      // first level if not set
2806      if (!location.level) {
2807        if (levelid) location.level = levelid;
2808        else if (location.el && location.el.closest(".mapplic-layer").data("floor"))
2809          location.level = location.el.closest(".mapplic-layer").data("floor");
2810        else location.level = self.data.levels[0].id;
2811      }
2812
2813      // description vars
2814      if (location.description)
2815        location.description = replaceVars(location.description, location);
2816
2817      // geolocation
2818      if (location.lat && location.lng) {
2819        var pos = latlngToPos(location.lat, location.lng);
2820        location.x = pos.x;
2821        location.y = pos.y;
2822      }
2823
2824      // estimated position
2825      if ((!location.x || !location.y) && elem) {
2826        var pos = estimatedPosition(location.el);
2827        location.x = pos.x;
2828        location.y = pos.y;
2829      }
2830
2831      // marker
2832      if (!location.pin) location.pin = self.o.marker;
2833      location.marker = self.addMarker(location);
2834
2835      // reveal mode
2836      if (location.action == "reveal")
2837        $(".mapplic-pin[data-location^=" + location.id + "]", self.map).css(
2838          "visibility",
2839          "hidden"
2840        );
2841
2842      if (self.sidebar) self.sidebar.countCategory();
2843    };
2844
2845    self.addMarker = function (location) {
2846      // hidden marker
2847      if (location.pin.indexOf("hidden") != -1) return false;
2848
2849      var parent = $(
2850        ".mapplic-layer[data-floor=" + location.level + "] .mapplic-map-image",
2851        self.el
2852      );
2853      var marker = $("<a></a>")
2854        .addClass("mapplic-pin")
2855        .addClass(location.pin.replace("hidden", ""))
2856        .attr("aria-label", location.title + " marker")
2857        .css({ top: location.y * 100 + "%", left: location.x * 100 + "%" })
2858        .appendTo(parent);
2859      marker.on("click touchend", function (e) {
2860        if (e.cancelable) e.preventDefault();
2861        if (!self.dragging) self.showLocation(location.id, 600);
2862      });
2863
2864      if (location.label) {
2865        if (
2866          location.label.match(
2867            /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g
2868          )
2869        )
2870          marker.css("background-image", "url(" + location.label + ")");
2871        else $("<span><span>" + location.label + "</span></span>").appendTo(marker);
2872      }
2873      if (location.reveal)
2874        marker.attr("data-reveal", location.reveal).css("visibility", "hidden");
2875      if (location.category) {
2876        location.category = location.category.toString();
2877        marker.attr("data-category", location.category);
2878      }
2879      marker.attr("data-location", location.id);
2880
2881      if (self.o.zoom && self.o.mousewheel)
2882        marker.on("mousewheel DOMMouseScroll", self.mouseWheel);
2883      if (location.styled) marker.addClass(location.styled);
2884      if (location.color && location.pin.indexOf("pin-text") > -1)
2885        marker.css("color", location.color);
2886      else if (location.color)
2887        marker.css({
2888          "background-color": location.color,
2889          "border-color": location.color,
2890        });
2891
2892      location.el = marker;
2893      return marker;
2894    };
2895
2896    // removing locations
2897    self.removeLocations = function () {
2898      for (var key of Object.keys(self.l)) {
2899        self.removeLocation(key);
2900      }
2901    };
2902
2903    self.removeLocation = function (location) {
2904      if (location.id) location = location.id;
2905      delete self.l[location];
2906      $('.mapplic-pin[data-location="' + location + '"]', self.map).remove();
2907      $('.mapplic-tooltip[data-location="' + location + '"]', self.el).remove();
2908      $('.mapplic-list-location[data-location="' + location + '"]', self.el).remove();
2909      $("svg #" + location, self.map).removeClass();
2910    };
2911
2912    self.getLocationData = function (id) {
2913      return self.l[id];
2914    };
2915
2916    self.showLocation = function (id, duration, check) {
2917      var location = (self.location = self.l[id]);
2918      if (!location) return false;
2919
2920      var action =
2921        location.action && location.action != "default" ? location.action : self.o.action;
2922      if (action == "disabled") return false;
2923
2924      // trigger event
2925      self.el.trigger("locationopen", location);
2926
2927      var content = null;
2928      self.closeTooltips();
2929
2930      switch (action) {
2931        case "open-link":
2932          window.location.href = location.link;
2933          return false;
2934        case "open-link-new-tab":
2935          window.open(location.link);
2936          self.location = null;
2937          return false;
2938        case "select":
2939          if (location.el) {
2940            if (location.el.hasClass("mapplic-active")) {
2941              location.el.removeClass("mapplic-active");
2942              if (location.list) location.list.removeClass("mapplic-active");
2943            } else {
2944              location.el.addClass("mapplic-active");
2945              if (location.list) location.list.addClass("mapplic-active");
2946            }
2947          }
2948          return false;
2949        case "none":
2950          self.hideLocation();
2951          self.switchLevel(location.level);
2952          if (!self.bboxZoom(location.el)) self.zoomLocation(location);
2953          break;
2954        case "reveal":
2955          self.hideLocation();
2956          self.switchLevel(location.level);
2957          self.container.revealChild(location);
2958          if (self.o.zoom) self.bboxZoom(location.el);
2959          break;
2960        case "external":
2961          self.hideLocation();
2962          self.switchLevel(location.level);
2963          if (!self.bboxZoom(location.el)) self.zoomLocation(location);
2964          if (self.external) self.external.show(location);
2965          break;
2966        case "lightbox":
2967          self.hideLocation();
2968          self.switchLevel(location.level);
2969          content = self.lightbox.show(location);
2970          break;
2971        case "image":
2972          self.hideLocation();
2973          self.switchLevel(location.level);
2974          self.lightbox.showImage(location);
2975          break;
2976        case "route":
2977          check = true;
2978          break;
2979        default:
2980          self.hideLocation();
2981          self.switchLevel(location.level);
2982          content = self.addTooltip(location, check);
2983      }
2984
2985      self.location = self.l[id];
2986
2987      // active state
2988      $(".mapplic-active", self.scope).removeClass("mapplic-active");
2989      if (location.el) location.el.addClass("mapplic-active");
2990      if (location.list) location.list.addClass("mapplic-active");
2991
2992      // deeplinking
2993      if (self.deeplinking && !check) self.deeplinking.update(id);
2994
2995      // trigger event
2996      self.el.trigger("locationopened", [location, content]);
2997    };
2998
2999    self.zoomLocation = function (loc) {
3000      var zoom = loc.zoom ? parseFloat(loc.zoom) / self.o.maxscale : 1;
3001      if (self.o.zoom) self.moveTo(loc.x, loc.y, zoom, 600);
3002    };
3003
3004    self.hideLocation = function () {
3005      $(".mapplic-active", self.scope).removeClass("mapplic-active");
3006      if (self.deeplinking && self.deeplinking.resolved) self.deeplinking.clear();
3007      if (self.external) self.external.hide();
3008      self.closeTooltips();
3009      self.location = null;
3010
3011      // trigger event
3012      self.el.trigger("locationclosed");
3013    };
3014
3015    self.updateLocation = function (id) {
3016      // remove + add
3017      var location = self.l[id];
3018
3019      if (location.id == id && location.el.is("a")) {
3020        // Geolocation
3021        if (location.lat && location.lng) {
3022          var pos = latlngToPos(location.lat, location.lng);
3023          location.x = pos.x;
3024          location.y = pos.y;
3025        }
3026
3027        var top = location.y * 100,
3028          left = location.x * 100;
3029        location.el.css({ top: top + "%", left: left + "%" });
3030      }
3031    };
3032  };
3033
3034  // jQuery plugin
3035  $.fn.mapplic = function (options) {
3036    return this.each(function () {
3037      var element = $(this);
3038
3039      // plugin already initiated on element
3040      if (element.data("mapplic")) return;
3041
3042      var instance = new Mapplic(element).init(options);
3043
3044      // store plugin object in element's data
3045      element.data("mapplic", instance);
3046    });
3047  };
3048})(jQuery);
3049
3050// call plugin on map instances
3051jQuery(document).ready(function ($) {
3052  $("[id^=mapplic-id]").mapplic();
3053
3054  // dynamic element
3055  var observer = new MutationObserver(function (mutations) {
3056    mutations.forEach(function (mutation) {
3057      var newNodes = mutation.addedNodes;
3058      if (newNodes !== null) {
3059        $(newNodes).each(function () {
3060          var node = $(this);
3061          if (node.is("[id^=mapplic-id]")) node.mapplic();
3062        });
3063      }
3064    });
3065  });
3066
3067  observer.observe(document, {
3068    attributes: false,
3069    childList: true,
3070    characterData: false,
3071    subtree: true,
3072  });
3073});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.