initial commit

Hunter
Tuesday, June 30th, 2026 at 2:57 PM

.gitignorenew
LICENSEnew
README.mdnew
https_serve.pynew
index.htmlnew
manifest.jsonnew
readme_images/screenshot.jpgnew
resources/app.jsnew
resources/demo.jsonnew
resources/flyer.jsnew
resources/format.jsnew
resources/icon.pngnew
resources/leaflet/LICENSE-leafletnew
resources/leaflet/LICENSE-nogapnew
resources/leaflet/leaflet-nogap.jsnew
resources/leaflet/leaflet.cssnew
resources/leaflet/leaflet.jsnew
resources/map.jsnew
resources/nav.jsnew
resources/panel.jsnew
resources/pdf.jsnew
resources/qr.jsnew
resources/styles.cssnew
serve.pynew
sw.jsnew

.gitignore
 1 +# Local HTTPS dev certs
 2 +/.https_certs/
 3 +
 4 +# Actual event data; demo.json is the committed fallback
 5 +/resources/events.json
 6 +
 7 +# Python
 8 +__pycache__/
 9 +*.pyc
10 +venv/
LICENSE
  1 +                    GNU GENERAL PUBLIC LICENSE
  2 +                       Version 3, 29 June 2007
  3 +
  4 + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
  5 + Everyone is permitted to copy and distribute verbatim copies
  6 + of this license document, but changing it is not allowed.
  7 +
  8 +                            Preamble
  9 +
 10 +  The GNU General Public License is a free, copyleft license for
 11 +software and other kinds of works.
 12 +
 13 +  The licenses for most software and other practical works are designed
 14 +to take away your freedom to share and change the works.  By contrast,
 15 +the GNU General Public License is intended to guarantee your freedom to
 16 +share and change all versions of a program--to make sure it remains free
 17 +software for all its users.  We, the Free Software Foundation, use the
 18 +GNU General Public License for most of our software; it applies also to
 19 +any other work released this way by its authors.  You can apply it to
 20 +your programs, too.
 21 +
 22 +  When we speak of free software, we are referring to freedom, not
 23 +price.  Our General Public Licenses are designed to make sure that you
 24 +have the freedom to distribute copies of free software (and charge for
 25 +them if you wish), that you receive source code or can get it if you
 26 +want it, that you can change the software or use pieces of it in new
 27 +free programs, and that you know you can do these things.
 28 +
 29 +  To protect your rights, we need to prevent others from denying you
 30 +these rights or asking you to surrender the rights.  Therefore, you have
 31 +certain responsibilities if you distribute copies of the software, or if
 32 +you modify it: responsibilities to respect the freedom of others.
 33 +
 34 +  For example, if you distribute copies of such a program, whether
 35 +gratis or for a fee, you must pass on to the recipients the same
 36 +freedoms that you received.  You must make sure that they, too, receive
 37 +or can get the source code.  And you must show them these terms so they
 38 +know their rights.
 39 +
 40 +  Developers that use the GNU GPL protect your rights with two steps:
 41 +(1) assert copyright on the software, and (2) offer you this License
 42 +giving you legal permission to copy, distribute and/or modify it.
 43 +
 44 +  For the developers' and authors' protection, the GPL clearly explains
 45 +that there is no warranty for this free software.  For both users' and
 46 +authors' sake, the GPL requires that modified versions be marked as
 47 +changed, so that their problems will not be attributed erroneously to
 48 +authors of previous versions.
 49 +
 50 +  Some devices are designed to deny users access to install or run
 51 +modified versions of the software inside them, although the manufacturer
 52 +can do so.  This is fundamentally incompatible with the aim of
 53 +protecting users' freedom to change the software.  The systematic
 54 +pattern of such abuse occurs in the area of products for individuals to
 55 +use, which is precisely where it is most unacceptable.  Therefore, we
 56 +have designed this version of the GPL to prohibit the practice for those
 57 +products.  If such problems arise substantially in other domains, we
 58 +stand ready to extend this provision to those domains in future versions
 59 +of the GPL, as needed to protect the freedom of users.
 60 +
 61 +  Finally, every program is threatened constantly by software patents.
 62 +States should not allow patents to restrict development and use of
 63 +software on general-purpose computers, but in those that do, we wish to
 64 +avoid the special danger that patents applied to a free program could
 65 +make it effectively proprietary.  To prevent this, the GPL assures that
 66 +patents cannot be used to render the program non-free.
 67 +
 68 +  The precise terms and conditions for copying, distribution and
 69 +modification follow.
 70 +
 71 +                       TERMS AND CONDITIONS
 72 +
 73 +  0. Definitions.
 74 +
 75 +  "This License" refers to version 3 of the GNU General Public License.
 76 +
 77 +  "Copyright" also means copyright-like laws that apply to other kinds of
 78 +works, such as semiconductor masks.
 79 +
 80 +  "The Program" refers to any copyrightable work licensed under this
 81 +License.  Each licensee is addressed as "you".  "Licensees" and
 82 +"recipients" may be individuals or organizations.
 83 +
 84 +  To "modify" a work means to copy from or adapt all or part of the work
 85 +in a fashion requiring copyright permission, other than the making of an
 86 +exact copy.  The resulting work is called a "modified version" of the
 87 +earlier work or a work "based on" the earlier work.
 88 +
 89 +  A "covered work" means either the unmodified Program or a work based
 90 +on the Program.
 91 +
 92 +  To "propagate" a work means to do anything with it that, without
 93 +permission, would make you directly or secondarily liable for
 94 +infringement under applicable copyright law, except executing it on a
 95 +computer or modifying a private copy.  Propagation includes copying,
 96 +distribution (with or without modification), making available to the
 97 +public, and in some countries other activities as well.
 98 +
 99 +  To "convey" a work means any kind of propagation that enables other
100 +parties to make or receive copies.  Mere interaction with a user through
101 +a computer network, with no transfer of a copy, is not conveying.
102 +
103 +  An interactive user interface displays "Appropriate Legal Notices"
104 +to the extent that it includes a convenient and prominently visible
105 +feature that (1) displays an appropriate copyright notice, and (2)
106 +tells the user that there is no warranty for the work (except to the
107 +extent that warranties are provided), that licensees may convey the
108 +work under this License, and how to view a copy of this License.  If
109 +the interface presents a list of user commands or options, such as a
110 +menu, a prominent item in the list meets this criterion.
111 +
112 +  1. Source Code.
113 +
114 +  The "source code" for a work means the preferred form of the work
115 +for making modifications to it.  "Object code" means any non-source
116 +form of a work.
117 +
118 +  A "Standard Interface" means an interface that either is an official
119 +standard defined by a recognized standards body, or, in the case of
120 +interfaces specified for a particular programming language, one that
121 +is widely used among developers working in that language.
122 +
123 +  The "System Libraries" of an executable work include anything, other
124 +than the work as a whole, that (a) is included in the normal form of
125 +packaging a Major Component, but which is not part of that Major
126 +Component, and (b) serves only to enable use of the work with that
127 +Major Component, or to implement a Standard Interface for which an
128 +implementation is available to the public in source code form.  A
129 +"Major Component", in this context, means a major essential component
130 +(kernel, window system, and so on) of the specific operating system
131 +(if any) on which the executable work runs, or a compiler used to
132 +produce the work, or an object code interpreter used to run it.
133 +
134 +  The "Corresponding Source" for a work in object code form means all
135 +the source code needed to generate, install, and (for an executable
136 +work) run the object code and to modify the work, including scripts to
137 +control those activities.  However, it does not include the work's
138 +System Libraries, or general-purpose tools or generally available free
139 +programs which are used unmodified in performing those activities but
140 +which are not part of the work.  For example, Corresponding Source
141 +includes interface definition files associated with source files for
142 +the work, and the source code for shared libraries and dynamically
143 +linked subprograms that the work is specifically designed to require,
144 +such as by intimate data communication or control flow between those
145 +subprograms and other parts of the work.
146 +
147 +  The Corresponding Source need not include anything that users
148 +can regenerate automatically from other parts of the Corresponding
149 +Source.
150 +
151 +  The Corresponding Source for a work in source code form is that
152 +same work.
153 +
154 +  2. Basic Permissions.
155 +
156 +  All rights granted under this License are granted for the term of
157 +copyright on the Program, and are irrevocable provided the stated
158 +conditions are met.  This License explicitly affirms your unlimited
159 +permission to run the unmodified Program.  The output from running a
160 +covered work is covered by this License only if the output, given its
161 +content, constitutes a covered work.  This License acknowledges your
162 +rights of fair use or other equivalent, as provided by copyright law.
163 +
164 +  You may make, run and propagate covered works that you do not
165 +convey, without conditions so long as your license otherwise remains
166 +in force.  You may convey covered works to others for the sole purpose
167 +of having them make modifications exclusively for you, or provide you
168 +with facilities for running those works, provided that you comply with
169 +the terms of this License in conveying all material for which you do
170 +not control copyright.  Those thus making or running the covered works
171 +for you must do so exclusively on your behalf, under your direction
172 +and control, on terms that prohibit them from making any copies of
173 +your copyrighted material outside their relationship with you.
174 +
175 +  Conveying under any other circumstances is permitted solely under
176 +the conditions stated below.  Sublicensing is not allowed; section 10
177 +makes it unnecessary.
178 +
179 +  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 +
181 +  No covered work shall be deemed part of an effective technological
182 +measure under any applicable law fulfilling obligations under article
183 +11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 +similar laws prohibiting or restricting circumvention of such
185 +measures.
186 +
187 +  When you convey a covered work, you waive any legal power to forbid
188 +circumvention of technological measures to the extent such circumvention
189 +is effected by exercising rights under this License with respect to
190 +the covered work, and you disclaim any intention to limit operation or
191 +modification of the work as a means of enforcing, against the work's
192 +users, your or third parties' legal rights to forbid circumvention of
193 +technological measures.
194 +
195 +  4. Conveying Verbatim Copies.
196 +
197 +  You may convey verbatim copies of the Program's source code as you
198 +receive it, in any medium, provided that you conspicuously and
199 +appropriately publish on each copy an appropriate copyright notice;
200 +keep intact all notices stating that this License and any
201 +non-permissive terms added in accord with section 7 apply to the code;
202 +keep intact all notices of the absence of any warranty; and give all
203 +recipients a copy of this License along with the Program.
204 +
205 +  You may charge any price or no price for each copy that you convey,
206 +and you may offer support or warranty protection for a fee.
207 +
208 +  5. Conveying Modified Source Versions.
209 +
210 +  You may convey a work based on the Program, or the modifications to
211 +produce it from the Program, in the form of source code under the
212 +terms of section 4, provided that you also meet all of these conditions:
213 +
214 +    a) The work must carry prominent notices stating that you modified
215 +    it, and giving a relevant date.
216 +
217 +    b) The work must carry prominent notices stating that it is
218 +    released under this License and any conditions added under section
219 +    7.  This requirement modifies the requirement in section 4 to
220 +    "keep intact all notices".
221 +
222 +    c) You must license the entire work, as a whole, under this
223 +    License to anyone who comes into possession of a copy.  This
224 +    License will therefore apply, along with any applicable section 7
225 +    additional terms, to the whole of the work, and all its parts,
226 +    regardless of how they are packaged.  This License gives no
227 +    permission to license the work in any other way, but it does not
228 +    invalidate such permission if you have separately received it.
229 +
230 +    d) If the work has interactive user interfaces, each must display
231 +    Appropriate Legal Notices; however, if the Program has interactive
232 +    interfaces that do not display Appropriate Legal Notices, your
233 +    work need not make them do so.
234 +
235 +  A compilation of a covered work with other separate and independent
236 +works, which are not by their nature extensions of the covered work,
237 +and which are not combined with it such as to form a larger program,
238 +in or on a volume of a storage or distribution medium, is called an
239 +"aggregate" if the compilation and its resulting copyright are not
240 +used to limit the access or legal rights of the compilation's users
241 +beyond what the individual works permit.  Inclusion of a covered work
242 +in an aggregate does not cause this License to apply to the other
243 +parts of the aggregate.
244 +
245 +  6. Conveying Non-Source Forms.
246 +
247 +  You may convey a covered work in object code form under the terms
248 +of sections 4 and 5, provided that you also convey the
249 +machine-readable Corresponding Source under the terms of this License,
250 +in one of these ways:
251 +
252 +    a) Convey the object code in, or embodied in, a physical product
253 +    (including a physical distribution medium), accompanied by the
254 +    Corresponding Source fixed on a durable physical medium
255 +    customarily used for software interchange.
256 +
257 +    b) Convey the object code in, or embodied in, a physical product
258 +    (including a physical distribution medium), accompanied by a
259 +    written offer, valid for at least three years and valid for as
260 +    long as you offer spare parts or customer support for that product
261 +    model, to give anyone who possesses the object code either (1) a
262 +    copy of the Corresponding Source for all the software in the
263 +    product that is covered by this License, on a durable physical
264 +    medium customarily used for software interchange, for a price no
265 +    more than your reasonable cost of physically performing this
266 +    conveying of source, or (2) access to copy the
267 +    Corresponding Source from a network server at no charge.
268 +
269 +    c) Convey individual copies of the object code with a copy of the
270 +    written offer to provide the Corresponding Source.  This
271 +    alternative is allowed only occasionally and noncommercially, and
272 +    only if you received the object code with such an offer, in accord
273 +    with subsection 6b.
274 +
275 +    d) Convey the object code by offering access from a designated
276 +    place (gratis or for a charge), and offer equivalent access to the
277 +    Corresponding Source in the same way through the same place at no
278 +    further charge.  You need not require recipients to copy the
279 +    Corresponding Source along with the object code.  If the place to
280 +    copy the object code is a network server, the Corresponding Source
281 +    may be on a different server (operated by you or a third party)
282 +    that supports equivalent copying facilities, provided you maintain
283 +    clear directions next to the object code saying where to find the
284 +    Corresponding Source.  Regardless of what server hosts the
285 +    Corresponding Source, you remain obligated to ensure that it is
286 +    available for as long as needed to satisfy these requirements.
287 +
288 +    e) Convey the object code using peer-to-peer transmission, provided
289 +    you inform other peers where the object code and Corresponding
290 +    Source of the work are being offered to the general public at no
291 +    charge under subsection 6d.
292 +
293 +  A separable portion of the object code, whose source code is excluded
294 +from the Corresponding Source as a System Library, need not be
295 +included in conveying the object code work.
296 +
297 +  A "User Product" is either (1) a "consumer product", which means any
298 +tangible personal property which is normally used for personal, family,
299 +or household purposes, or (2) anything designed or sold for incorporation
300 +into a dwelling.  In determining whether a product is a consumer product,
301 +doubtful cases shall be resolved in favor of coverage.  For a particular
302 +product received by a particular user, "normally used" refers to a
303 +typical or common use of that class of product, regardless of the status
304 +of the particular user or of the way in which the particular user
305 +actually uses, or expects or is expected to use, the product.  A product
306 +is a consumer product regardless of whether the product has substantial
307 +commercial, industrial or non-consumer uses, unless such uses represent
308 +the only significant mode of use of the product.
309 +
310 +  "Installation Information" for a User Product means any methods,
311 +procedures, authorization keys, or other information required to install
312 +and execute modified versions of a covered work in that User Product from
313 +a modified version of its Corresponding Source.  The information must
314 +suffice to ensure that the continued functioning of the modified object
315 +code is in no case prevented or interfered with solely because
316 +modification has been made.
317 +
318 +  If you convey an object code work under this section in, or with, or
319 +specifically for use in, a User Product, and the conveying occurs as
320 +part of a transaction in which the right of possession and use of the
321 +User Product is transferred to the recipient in perpetuity or for a
322 +fixed term (regardless of how the transaction is characterized), the
323 +Corresponding Source conveyed under this section must be accompanied
324 +by the Installation Information.  But this requirement does not apply
325 +if neither you nor any third party retains the ability to install
326 +modified object code on the User Product (for example, the work has
327 +been installed in ROM).
328 +
329 +  The requirement to provide Installation Information does not include a
330 +requirement to continue to provide support service, warranty, or updates
331 +for a work that has been modified or installed by the recipient, or for
332 +the User Product in which it has been modified or installed.  Access to a
333 +network may be denied when the modification itself materially and
334 +adversely affects the operation of the network or violates the rules and
335 +protocols for communication across the network.
336 +
337 +  Corresponding Source conveyed, and Installation Information provided,
338 +in accord with this section must be in a format that is publicly
339 +documented (and with an implementation available to the public in
340 +source code form), and must require no special password or key for
341 +unpacking, reading or copying.
342 +
343 +  7. Additional Terms.
344 +
345 +  "Additional permissions" are terms that supplement the terms of this
346 +License by making exceptions from one or more of its conditions.
347 +Additional permissions that are applicable to the entire Program shall
348 +be treated as though they were included in this License, to the extent
349 +that they are valid under applicable law.  If additional permissions
350 +apply only to part of the Program, that part may be used separately
351 +under those permissions, but the entire Program remains governed by
352 +this License without regard to the additional permissions.
353 +
354 +  When you convey a copy of a covered work, you may at your option
355 +remove any additional permissions from that copy, or from any part of
356 +it.  (Additional permissions may be written to require their own
357 +removal in certain cases when you modify the work.)  You may place
358 +additional permissions on material, added by you to a covered work,
359 +for which you have or can give appropriate copyright permission.
360 +
361 +  Notwithstanding any other provision of this License, for material you
362 +add to a covered work, you may (if authorized by the copyright holders of
363 +that material) supplement the terms of this License with terms:
364 +
365 +    a) Disclaiming warranty or limiting liability differently from the
366 +    terms of sections 15 and 16 of this License; or
367 +
368 +    b) Requiring preservation of specified reasonable legal notices or
369 +    author attributions in that material or in the Appropriate Legal
370 +    Notices displayed by works containing it; or
371 +
372 +    c) Prohibiting misrepresentation of the origin of that material, or
373 +    requiring that modified versions of such material be marked in
374 +    reasonable ways as different from the original version; or
375 +
376 +    d) Limiting the use for publicity purposes of names of licensors or
377 +    authors of the material; or
378 +
379 +    e) Declining to grant rights under trademark law for use of some
380 +    trade names, trademarks, or service marks; or
381 +
382 +    f) Requiring indemnification of licensors and authors of that
383 +    material by anyone who conveys the material (or modified versions of
384 +    it) with contractual assumptions of liability to the recipient, for
385 +    any liability that these contractual assumptions directly impose on
386 +    those licensors and authors.
387 +
388 +  All other non-permissive additional terms are considered "further
389 +restrictions" within the meaning of section 10.  If the Program as you
390 +received it, or any part of it, contains a notice stating that it is
391 +governed by this License along with a term that is a further
392 +restriction, you may remove that term.  If a license document contains
393 +a further restriction but permits relicensing or conveying under this
394 +License, you may add to a covered work material governed by the terms
395 +of that license document, provided that the further restriction does
396 +not survive such relicensing or conveying.
397 +
398 +  If you add terms to a covered work in accord with this section, you
399 +must place, in the relevant source files, a statement of the
400 +additional terms that apply to those files, or a notice indicating
401 +where to find the applicable terms.
402 +
403 +  Additional terms, permissive or non-permissive, may be stated in the
404 +form of a separately written license, or stated as exceptions;
405 +the above requirements apply either way.
406 +
407 +  8. Termination.
408 +
409 +  You may not propagate or modify a covered work except as expressly
410 +provided under this License.  Any attempt otherwise to propagate or
411 +modify it is void, and will automatically terminate your rights under
412 +this License (including any patent licenses granted under the third
413 +paragraph of section 11).
414 +
415 +  However, if you cease all violation of this License, then your
416 +license from a particular copyright holder is reinstated (a)
417 +provisionally, unless and until the copyright holder explicitly and
418 +finally terminates your license, and (b) permanently, if the copyright
419 +holder fails to notify you of the violation by some reasonable means
420 +prior to 60 days after the cessation.
421 +
422 +  Moreover, your license from a particular copyright holder is
423 +reinstated permanently if the copyright holder notifies you of the
424 +violation by some reasonable means, this is the first time you have
425 +received notice of violation of this License (for any work) from that
426 +copyright holder, and you cure the violation prior to 30 days after
427 +your receipt of the notice.
428 +
429 +  Termination of your rights under this section does not terminate the
430 +licenses of parties who have received copies or rights from you under
431 +this License.  If your rights have been terminated and not permanently
432 +reinstated, you do not qualify to receive new licenses for the same
433 +material under section 10.
434 +
435 +  9. Acceptance Not Required for Having Copies.
436 +
437 +  You are not required to accept this License in order to receive or
438 +run a copy of the Program.  Ancillary propagation of a covered work
439 +occurring solely as a consequence of using peer-to-peer transmission
440 +to receive a copy likewise does not require acceptance.  However,
441 +nothing other than this License grants you permission to propagate or
442 +modify any covered work.  These actions infringe copyright if you do
443 +not accept this License.  Therefore, by modifying or propagating a
444 +covered work, you indicate your acceptance of this License to do so.
445 +
446 +  10. Automatic Licensing of Downstream Recipients.
447 +
448 +  Each time you convey a covered work, the recipient automatically
449 +receives a license from the original licensors, to run, modify and
450 +propagate that work, subject to this License.  You are not responsible
451 +for enforcing compliance by third parties with this License.
452 +
453 +  An "entity transaction" is a transaction transferring control of an
454 +organization, or substantially all assets of one, or subdividing an
455 +organization, or merging organizations.  If propagation of a covered
456 +work results from an entity transaction, each party to that
457 +transaction who receives a copy of the work also receives whatever
458 +licenses to the work the party's predecessor in interest had or could
459 +give under the previous paragraph, plus a right to possession of the
460 +Corresponding Source of the work from the predecessor in interest, if
461 +the predecessor has it or can get it with reasonable efforts.
462 +
463 +  You may not impose any further restrictions on the exercise of the
464 +rights granted or affirmed under this License.  For example, you may
465 +not impose a license fee, royalty, or other charge for exercise of
466 +rights granted under this License, and you may not initiate litigation
467 +(including a cross-claim or counterclaim in a lawsuit) alleging that
468 +any patent claim is infringed by making, using, selling, offering for
469 +sale, or importing the Program or any portion of it.
470 +
471 +  11. Patents.
472 +
473 +  A "contributor" is a copyright holder who authorizes use under this
474 +License of the Program or a work on which the Program is based.  The
475 +work thus licensed is called the contributor's "contributor version".
476 +
477 +  A contributor's "essential patent claims" are all patent claims
478 +owned or controlled by the contributor, whether already acquired or
479 +hereafter acquired, that would be infringed by some manner, permitted
480 +by this License, of making, using, or selling its contributor version,
481 +but do not include claims that would be infringed only as a
482 +consequence of further modification of the contributor version.  For
483 +purposes of this definition, "control" includes the right to grant
484 +patent sublicenses in a manner consistent with the requirements of
485 +this License.
486 +
487 +  Each contributor grants you a non-exclusive, worldwide, royalty-free
488 +patent license under the contributor's essential patent claims, to
489 +make, use, sell, offer for sale, import and otherwise run, modify and
490 +propagate the contents of its contributor version.
491 +
492 +  In the following three paragraphs, a "patent license" is any express
493 +agreement or commitment, however denominated, not to enforce a patent
494 +(such as an express permission to practice a patent or covenant not to
495 +sue for patent infringement).  To "grant" such a patent license to a
496 +party means to make such an agreement or commitment not to enforce a
497 +patent against the party.
498 +
499 +  If you convey a covered work, knowingly relying on a patent license,
500 +and the Corresponding Source of the work is not available for anyone
501 +to copy, free of charge and under the terms of this License, through a
502 +publicly available network server or other readily accessible means,
503 +then you must either (1) cause the Corresponding Source to be so
504 +available, or (2) arrange to deprive yourself of the benefit of the
505 +patent license for this particular work, or (3) arrange, in a manner
506 +consistent with the requirements of this License, to extend the patent
507 +license to downstream recipients.  "Knowingly relying" means you have
508 +actual knowledge that, but for the patent license, your conveying the
509 +covered work in a country, or your recipient's use of the covered work
510 +in a country, would infringe one or more identifiable patents in that
511 +country that you have reason to believe are valid.
512 +
513 +  If, pursuant to or in connection with a single transaction or
514 +arrangement, you convey, or propagate by procuring conveyance of, a
515 +covered work, and grant a patent license to some of the parties
516 +receiving the covered work authorizing them to use, propagate, modify
517 +or convey a specific copy of the covered work, then the patent license
518 +you grant is automatically extended to all recipients of the covered
519 +work and works based on it.
520 +
521 +  A patent license is "discriminatory" if it does not include within
522 +the scope of its coverage, prohibits the exercise of, or is
523 +conditioned on the non-exercise of one or more of the rights that are
524 +specifically granted under this License.  You may not convey a covered
525 +work if you are a party to an arrangement with a third party that is
526 +in the business of distributing software, under which you make payment
527 +to the third party based on the extent of your activity of conveying
528 +the work, and under which the third party grants, to any of the
529 +parties who would receive the covered work from you, a discriminatory
530 +patent license (a) in connection with copies of the covered work
531 +conveyed by you (or copies made from those copies), or (b) primarily
532 +for and in connection with specific products or compilations that
533 +contain the covered work, unless you entered into that arrangement,
534 +or that patent license was granted, prior to 28 March 2007.
535 +
536 +  Nothing in this License shall be construed as excluding or limiting
537 +any implied license or other defenses to infringement that may
538 +otherwise be available to you under applicable patent law.
539 +
540 +  12. No Surrender of Others' Freedom.
541 +
542 +  If conditions are imposed on you (whether by court order, agreement or
543 +otherwise) that contradict the conditions of this License, they do not
544 +excuse you from the conditions of this License.  If you cannot convey a
545 +covered work so as to satisfy simultaneously your obligations under this
546 +License and any other pertinent obligations, then as a consequence you may
547 +not convey it at all.  For example, if you agree to terms that obligate you
548 +to collect a royalty for further conveying from those to whom you convey
549 +the Program, the only way you could satisfy both those terms and this
550 +License would be to refrain entirely from conveying the Program.
551 +
552 +  13. Use with the GNU Affero General Public License.
553 +
554 +  Notwithstanding any other provision of this License, you have
555 +permission to link or combine any covered work with a work licensed
556 +under version 3 of the GNU Affero General Public License into a single
557 +combined work, and to convey the resulting work.  The terms of this
558 +License will continue to apply to the part which is the covered work,
559 +but the special requirements of the GNU Affero General Public License,
560 +section 13, concerning interaction through a network will apply to the
561 +combination as such.
562 +
563 +  14. Revised Versions of this License.
564 +
565 +  The Free Software Foundation may publish revised and/or new versions of
566 +the GNU General Public License from time to time.  Such new versions will
567 +be similar in spirit to the present version, but may differ in detail to
568 +address new problems or concerns.
569 +
570 +  Each version is given a distinguishing version number.  If the
571 +Program specifies that a certain numbered version of the GNU General
572 +Public License "or any later version" applies to it, you have the
573 +option of following the terms and conditions either of that numbered
574 +version or of any later version published by the Free Software
575 +Foundation.  If the Program does not specify a version number of the
576 +GNU General Public License, you may choose any version ever published
577 +by the Free Software Foundation.
578 +
579 +  If the Program specifies that a proxy can decide which future
580 +versions of the GNU General Public License can be used, that proxy's
581 +public statement of acceptance of a version permanently authorizes you
582 +to choose that version for the Program.
583 +
584 +  Later license versions may give you additional or different
585 +permissions.  However, no additional obligations are imposed on any
586 +author or copyright holder as a result of your choosing to follow a
587 +later version.
588 +
589 +  15. Disclaimer of Warranty.
590 +
591 +  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 +APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 +PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 +IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 +ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 +
600 +  16. Limitation of Liability.
601 +
602 +  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 +SUCH DAMAGES.
611 +
612 +  17. Interpretation of Sections 15 and 16.
613 +
614 +  If the disclaimer of warranty and limitation of liability provided
615 +above cannot be given local legal effect according to their terms,
616 +reviewing courts shall apply local law that most closely approximates
617 +an absolute waiver of all civil liability in connection with the
618 +Program, unless a warranty or assumption of liability accompanies a
619 +copy of the Program in return for a fee.
620 +
621 +                     END OF TERMS AND CONDITIONS
622 +
623 +            How to Apply These Terms to Your New Programs
624 +
625 +  If you develop a new program, and you want it to be of the greatest
626 +possible use to the public, the best way to achieve this is to make it
627 +free software which everyone can redistribute and change under these terms.
628 +
629 +  To do so, attach the following notices to the program.  It is safest
630 +to attach them to the start of each source file to most effectively
631 +state the exclusion of warranty; and each file should have at least
632 +the "copyright" line and a pointer to where the full notice is found.
633 +
634 +    <one line to give the program's name and a brief idea of what it does.>
635 +    Copyright (C) <year>  <name of author>
636 +
637 +    This program is free software: you can redistribute it and/or modify
638 +    it under the terms of the GNU General Public License as published by
639 +    the Free Software Foundation, either version 3 of the License, or
640 +    (at your option) any later version.
641 +
642 +    This program is distributed in the hope that it will be useful,
643 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
644 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
645 +    GNU General Public License for more details.
646 +
647 +    You should have received a copy of the GNU General Public License
648 +    along with this program.  If not, see <https://www.gnu.org/licenses/>.
649 +
650 +Also add information on how to contact you by electronic and paper mail.
651 +
652 +  If the program does terminal interaction, make it output a short
653 +notice like this when it starts in an interactive mode:
654 +
655 +    <program>  Copyright (C) <year>  <name of author>
656 +    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 +    This is free software, and you are welcome to redistribute it
658 +    under certain conditions; type `show c' for details.
659 +
660 +The hypothetical commands `show w' and `show c' should show the appropriate
661 +parts of the General Public License.  Of course, your program's commands
662 +might be different; for a GUI interface, you would use an "about box".
663 +
664 +  You should also get your employer (if you work as a programmer) or school,
665 +if any, to sign a "copyright disclaimer" for the program, if necessary.
666 +For more information on this, and how to apply and follow the GNU GPL, see
667 +<https://www.gnu.org/licenses/>.
668 +
669 +  The GNU General Public License does not permit incorporating your program
670 +into proprietary programs.  If your program is a subroutine library, you
671 +may consider it more useful to permit linking proprietary applications with
672 +the library.  If this is what you want to do, use the GNU Lesser General
673 +Public License instead of this License.  But first, please read
674 +<https://www.gnu.org/licenses/why-not-lgpl.html>.
README.md
 1 +# /events 🐞
 2 +
 3 +Community-scale infrastructure for hosting local meetups and workshops.
 4 +
 5 +<p align="center">
 6 +	<a href="https://hunterirving.github.io/events"><img src="readme_images/screenshot.jpg" width=170>
 7 +	</a>
 8 +</p>
 9 +
10 +## key features
11 +- Auto-generated printable flyers
12 +- RSVP by E-mail
13 +- Installable as a [Progressive Web App](https://hunterirving.github.io/web_workshop/pages/pwa) for offline use
14 +
15 +## usage
16 +Add an `events.json` file to `/resources` to override the events in `demo.json`. Upcoming events will be displayed on the map. If no upcoming events are present, past events will be shown instead.
17 +
18 +## licenses
19 +
20 +This project is licensed under the <a href="LICENSE">GNU General Public License v3.0</a>.
21 +
22 +### third-party resources
23 +
24 +- **[Leaflet](https://leafletjs.com)** 1.9.4 under the <a href="resources/leaflet/LICENSE-leaflet">BSD 2-Clause License</a>.
25 +- **[Leaflet.TileLayer.NoGap](https://github.com/Leaflet/Leaflet.TileLayer.NoGap)** (modified), under the [Beerware License](resources/leaflet/LICENSE-nogap).
https_serve.py
  1 +#!/usr/bin/env python3
  2 +"""
  3 +Serve this project over HTTPS on your local network so the browser will hand it
  4 +geolocation + device-orientation (both require a secure context off localhost).
  5 +
  6 +Uses mkcert to mint a locally-trusted certificate for this machine's LAN IP. A
  7 +device won't trust that cert until it trusts mkcert's root CA, so we print two
  8 +QR codes:
  9 +
 10 +	1. an HTTP link that hands the device the mkcert root CA (the public root
 11 +	   cert, no secret; served to any device until you stop the script)
 12 +	2. an HTTPS link to the app itself
 13 +
 14 +Both servers shut down together on Ctrl+C.
 15 +
 16 +After scanning the first QR code, install + trust the CA on the device:
 17 +	iOS:     Settings → General → VPN & Device Management → install the profile,
 18 +	         then Settings → General → About → Certificate Trust Settings →
 19 +	         enable full trust
 20 +	Android: open the downloaded file and follow the prompt to install it as a
 21 +	         CA certificate
 22 +
 23 +Run with --reset to revoke the CA and delete the local certificates.
 24 +"""
 25 +
 26 +import http.server
 27 +import socketserver
 28 +import ssl
 29 +import sys
 30 +import os
 31 +import subprocess
 32 +import threading
 33 +from pathlib import Path
 34 +
 35 +import serve  # shared helpers: venv setup, QR printing, IP/port, QuietHandler
 36 +
 37 +CERT_HTTP_PORT = 8001  # preferred; falls back to the next free port
 38 +HTTPS_PORT = 8443      # preferred; falls back to the next free port
 39 +SCRIPT_DIR = Path(__file__).parent.absolute()
 40 +CERT_DIR = SCRIPT_DIR / ".https_certs"
 41 +CERT_FILE = CERT_DIR / "cert.pem"
 42 +KEY_FILE = CERT_DIR / "key.pem"
 43 +
 44 +# Path the phone fetches the root CA from. Anything else over HTTP 404s.
 45 +CA_DOWNLOAD_PATH = "/rootCA.pem"
 46 +
 47 +
 48 +def run_in_venv():
 49 +	"""Re-run this script in serve.py's shared venv (qrcode), forwarding any
 50 +	user flags (eg. --reset) to the re-exec."""
 51 +	python_path = serve.setup_venv()
 52 +	try:
 53 +		subprocess.check_call([str(python_path), __file__, "--in-venv", *sys.argv[1:]])
 54 +	except (KeyboardInterrupt, subprocess.CalledProcessError):
 55 +		pass
 56 +	sys.exit(0)
 57 +
 58 +
 59 +# get_local_ip, find_available_port, print_qr_code, and QuietHandler are shared
 60 +# with serve.py — reached via the `serve.` prefix below.
 61 +
 62 +
 63 +def mkcert_install_command():
 64 +	"""Best-guess (command, package-manager-label) to install mkcert on this
 65 +	platform, or (None, None) if no known manager is on PATH."""
 66 +	from shutil import which
 67 +	if sys.platform == "darwin":
 68 +		if which("brew"):
 69 +			return (["brew", "install", "mkcert"], "Homebrew")
 70 +	elif sys.platform == "win32":
 71 +		if which("choco"):
 72 +			return (["choco", "install", "mkcert", "-y"], "Chocolatey")
 73 +		if which("scoop"):
 74 +			return (["scoop", "install", "mkcert"], "Scoop")
 75 +	else:  # linux / other unix
 76 +		# Most distros package mkcert; pick whichever manager is present.
 77 +		if which("apt"):
 78 +			return (["sudo", "apt", "install", "-y", "mkcert"], "apt")
 79 +		if which("dnf"):
 80 +			return (["sudo", "dnf", "install", "-y", "mkcert"], "dnf")
 81 +		if which("pacman"):
 82 +			return (["sudo", "pacman", "-S", "--noconfirm", "mkcert"], "pacman")
 83 +		if which("brew"):
 84 +			return (["brew", "install", "mkcert"], "Homebrew")
 85 +	return (None, None)
 86 +
 87 +
 88 +def manual_install_hint():
 89 +	"""Platform-appropriate manual install instructions for mkcert."""
 90 +	if sys.platform == "darwin":
 91 +		return "brew install mkcert  (https://github.com/FiloSottile/mkcert)"
 92 +	if sys.platform == "win32":
 93 +		return "choco install mkcert  OR  scoop install mkcert  (https://github.com/FiloSottile/mkcert)"
 94 +	return "install 'mkcert' via your package manager (https://github.com/FiloSottile/mkcert)"
 95 +
 96 +
 97 +def require_mkcert():
 98 +	"""Ensure mkcert is on PATH, offering to install it if it isn't."""
 99 +	from shutil import which
100 +	if which("mkcert") is not None:
101 +		return
102 +
103 +	print("mkcert is required but was not found on PATH.")
104 +	cmd, label = mkcert_install_command()
105 +	if cmd is None:
106 +		print(f"  Install it manually:  {manual_install_hint()}")
107 +		sys.exit(1)
108 +
109 +	answer = input(f"Install it now with {label} ({' '.join(cmd)})? [y/N] ").strip().lower()
110 +	if answer not in ("y", "yes"):
111 +		print(f"  Install it manually:  {manual_install_hint()}")
112 +		sys.exit(1)
113 +
114 +	try:
115 +		subprocess.check_call(cmd)
116 +	except (subprocess.CalledProcessError, KeyboardInterrupt):
117 +		print(f"\nInstall failed. Install it manually:  {manual_install_hint()}")
118 +		sys.exit(1)
119 +
120 +	if which("mkcert") is None:
121 +		print("mkcert still not on PATH after install. Open a new terminal and retry.")
122 +		sys.exit(1)
123 +
124 +
125 +def ca_root_file():
126 +	"""Path to mkcert's root CA cert (rootCA.pem)."""
127 +	caroot = subprocess.run(
128 +		["mkcert", "-CAROOT"], capture_output=True, text=True
129 +	).stdout.strip()
130 +	return Path(caroot) / "rootCA.pem"
131 +
132 +
133 +def ensure_ca_installed():
134 +	"""Ensure mkcert's local CA exists and is trusted by this machine.
135 +	May prompt for your password to write to the system trust store."""
136 +	if not ca_root_file().exists():
137 +		print("Setting up mkcert local CA (you may be prompted for your password)...")
138 +		subprocess.check_call(["mkcert", "-install"])
139 +
140 +
141 +def ensure_leaf_cert(local_ip):
142 +	"""Mint a cert/key for this machine's LAN IP (and localhost) if missing."""
143 +	CERT_DIR.mkdir(parents=True, exist_ok=True)
144 +	if CERT_FILE.exists() and KEY_FILE.exists():
145 +		return
146 +	print(f"Minting certificate for {local_ip}...")
147 +	subprocess.check_call([
148 +		"mkcert",
149 +		"-cert-file", str(CERT_FILE),
150 +		"-key-file", str(KEY_FILE),
151 +		local_ip, "localhost", "127.0.0.1",
152 +	])
153 +
154 +
155 +def start_ca_server(local_ip):
156 +	"""Start a plain-HTTP server that hands out the mkcert root CA, and return
157 +	it running. The CA is the *public* root cert (no secret), so there's no
158 +	reason to limit how many times or to how many devices it's served. Leave
159 +	it up alongside the HTTPS server so you can set up multiple devices, and
160 +	tear both down together on Ctrl+C. Caller owns shutdown."""
161 +	ca_file = ca_root_file()
162 +	if not ca_file.exists():
163 +		print(f"Error: root CA not found at {ca_file}")
164 +		sys.exit(1)
165 +
166 +	ca_bytes = ca_file.read_bytes()
167 +
168 +	class CAHandler(http.server.BaseHTTPRequestHandler):
169 +		def log_message(self, *args):
170 +			pass
171 +
172 +		def do_GET(self):
173 +			# Redirect bare visits to the download path so a typo'd / still works.
174 +			if self.path in ("/", "/index.html"):
175 +				self.send_response(302)
176 +				self.send_header("Location", CA_DOWNLOAD_PATH)
177 +				self.end_headers()
178 +				return
179 +			if self.path != CA_DOWNLOAD_PATH:
180 +				self.send_error(404)
181 +				return
182 +			# x-x509-ca-cert makes iOS offer to install it as a CA profile.
183 +			self.send_response(200)
184 +			self.send_header("Content-Type", "application/x-x509-ca-cert")
185 +			self.send_header("Content-Length", str(len(ca_bytes)))
186 +			self.send_header(
187 +				"Content-Disposition", 'attachment; filename="rootCA.pem"'
188 +			)
189 +			self.end_headers()
190 +			try:
191 +				self.wfile.write(ca_bytes)
192 +			except (BrokenPipeError, ConnectionResetError):
193 +				return
194 +
195 +	port = serve.find_available_port(CERT_HTTP_PORT)
196 +	if port is None:
197 +		print(f"Error: no free port near {CERT_HTTP_PORT} for the CA server.")
198 +		sys.exit(1)
199 +	http.server.HTTPServer.allow_reuse_address = True
200 +	httpd = http.server.HTTPServer(("", port), CAHandler)
201 +	threading.Thread(target=httpd.serve_forever, daemon=True).start()
202 +
203 +	cert_url = f"http://{local_ip}:{port}{CA_DOWNLOAD_PATH}"
204 +	print("=" * 69)
205 +	print("STEP 1: install + trust the local CA on each device (first time only)")
206 +	print("=" * 69)
207 +	serve.print_qr_code(cert_url, "Scan to download the root certificate:")
208 +	print(f"  {cert_url}")
209 +	print("\nOn iOS, after downloading:")
210 +	print("  • Settings → General → VPN & Device Management → install the profile")
211 +	print("  • Settings → General → About → Certificate Trust Settings →")
212 +	print("    toggle ON full trust for the mkcert CA")
213 +	print("\nOn Android, after downloading:")
214 +	print("  • open the downloaded file, or Settings → Security → Encryption &")
215 +	print("    credentials → Install a certificate → CA certificate, then confirm")
216 +	print("  • installing the CA is what makes it trusted; there's no separate")
217 +	print("    trust toggle (exact menu names vary by version/manufacturer)")
218 +	print("\n(skip this step on a device that already trusts the CA from a previous run)")
219 +	return httpd
220 +
221 +
222 +def serve_https(local_ip, ca_httpd=None):
223 +	"""Serve SCRIPT_DIR over HTTPS. Runs until Ctrl+C, then also shuts down the
224 +	CA HTTP server (if given)."""
225 +	os.chdir(SCRIPT_DIR)
226 +
227 +	# serve.QuietHandler's keep-alive applies just as well to the TLS connection:
228 +	# the app's files reuse one connection instead of a fresh handshake per file.
229 +	ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
230 +	ctx.load_cert_chain(certfile=str(CERT_FILE), keyfile=str(KEY_FILE))
231 +
232 +	port = serve.find_available_port(HTTPS_PORT)
233 +	if port is None:
234 +		print(f"Error: no free port near {HTTPS_PORT} for the HTTPS server.")
235 +		if ca_httpd is not None:
236 +			ca_httpd.shutdown()
237 +			ca_httpd.server_close()
238 +		sys.exit(1)
239 +
240 +	with socketserver.ThreadingTCPServer(("", port), serve.QuietHandler) as httpd:
241 +		httpd.daemon_threads = True
242 +		httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
243 +		https_url = f"https://{local_ip}:{port}/"
244 +
245 +		print("\n" + "=" * 59)
246 +		print("STEP 2: open the app over HTTPS (once the device trusts the CA)")
247 +		print("=" * 59)
248 +		serve.print_qr_code(https_url, "Scan to open the app over HTTPS:")
249 +		print(f"  {https_url}")
250 +		print("\n(HTTPS off localhost is what lets the browser grant location +")
251 +		print(" device orientation, which the compass / nav mode need)")
252 +		print("\nPress Ctrl+C to stop the server(s)")
253 +
254 +		try:
255 +			httpd.serve_forever()
256 +		except KeyboardInterrupt:
257 +			print("\n\nShutting down...")
258 +		finally:
259 +			if ca_httpd is not None:
260 +				ca_httpd.shutdown()
261 +				ca_httpd.server_close()
262 +
263 +
264 +def caroot_dir():
265 +	"""Path to mkcert's CAROOT folder (holds rootCA.pem and rootCA-key.pem)."""
266 +	caroot = subprocess.run(
267 +		["mkcert", "-CAROOT"], capture_output=True, text=True
268 +	).stdout.strip()
269 +	return Path(caroot) if caroot else None
270 +
271 +
272 +def reset_certs():
273 +	"""Opt-in teardown, in two stages so it's safe alongside other mkcert use.
274 +
275 +	mkcert keeps ONE shared CA per machine, so anything else you run with
276 +	mkcert trusts the same CA. Stage 1 removes only THIS tool's own leaf certs
277 +	(always safe). Stage 2 removes the shared mkcert CA itself (untrust + optional
278 +	key deletion). Guarded, because it affects every project that uses
279 +	mkcert, not just this one."""
280 +	from shutil import rmtree, which
281 +
282 +	# Stage 1: our own leaf certs.
283 +	if CERT_DIR.exists():
284 +		rmtree(CERT_DIR)
285 +		print(f"Removed this tool's local certificates ({CERT_DIR}).")
286 +	else:
287 +		print("No local certificates to remove.")
288 +
289 +	# Stage 2: the shared mkcert CA. Skip entirely unless asked, since other
290 +	# projects on this machine may rely on it and we can't detect them.
291 +	print(
292 +		"\nThe mkcert CA itself is SHARED across everything you use mkcert for"
293 +		" on this computer, not just this tool. Leaving it installed is normal."
294 +	)
295 +	answer = input(
296 +		"Also remove the shared mkcert CA (untrust it system-wide)? [y/N] "
297 +	).strip().lower()
298 +	if answer not in ("y", "yes"):
299 +		print("Left the mkcert CA in place.")
300 +		return
301 +
302 +	if which("mkcert"):
303 +		try:
304 +			subprocess.check_call(["mkcert", "-uninstall"])
305 +			print("Stopped this computer from trusting the mkcert CA.")
306 +		except subprocess.CalledProcessError as e:
307 +			print(f"mkcert -uninstall failed: {e}")
308 +
309 +	# mkcert -uninstall removes trust but does NOT delete the CA. The private key
310 +	# (rootCA-key.pem) is the sensitive material, so offer to delete it too, with
311 +	# a separate confirmation since it's irreversible and shared.
312 +	caroot = caroot_dir()
313 +	if caroot and (caroot / "rootCA-key.pem").exists():
314 +		print(
315 +			"\nThe CA's PRIVATE KEY is still on disk at:\n"
316 +			f"  {caroot}\n"
317 +			"Per mkcert: \"the rootCA-key.pem file ... gives complete power to\n"
318 +			"intercept secure requests from your machine. Do not share it.\"\n"
319 +			"Deleting it removes the shared CA entirely; future mkcert use\n"
320 +			"(here or in any project) will generate a brand-new one."
321 +		)
322 +		ans2 = input("Delete the CA key + folder now? [y/N] ").strip().lower()
323 +		if ans2 in ("y", "yes"):
324 +			rmtree(caroot)
325 +			print(f"Removed {caroot}")
326 +		else:
327 +			print("Left the CA key in place.")
328 +
329 +	print("\nNote: this does NOT touch any device; remove or untrust the CA\n"
330 +		"there separately (see the instructions printed during install).")
331 +
332 +
333 +def start():
334 +	require_mkcert()
335 +
336 +	local_ip = serve.get_local_ip()
337 +	if not local_ip:
338 +		print("Error: could not determine this machine's LAN IP.")
339 +		print("Make sure you're connected to a network and try again.")
340 +		sys.exit(1)
341 +
342 +	ensure_ca_installed()
343 +	ensure_leaf_cert(local_ip)
344 +	ca_httpd = start_ca_server(local_ip)
345 +	serve_https(local_ip, ca_httpd=ca_httpd)
346 +
347 +
348 +def main():
349 +	if "--in-venv" not in sys.argv:
350 +		run_in_venv()
351 +	else:
352 +		try:
353 +			if "--reset" in sys.argv:
354 +				reset_certs()
355 +			else:
356 +				start()
357 +		except KeyboardInterrupt:
358 +			print("\nCancelled.")
359 +			sys.exit(130)
360 +
361 +
362 +if __name__ == "__main__":
363 +	main()
index.html
 1 +<!DOCTYPE html>
 2 +<html>
 3 +	<head>
 4 +		<title>events</title>
 5 +		<meta charset="utf-8">
 6 +		<meta name="viewport" content="width=device-width, initial-scale=1">
 7 +		<meta name="theme-color" content="antiquewhite">
 8 +		<meta name="apple-mobile-web-app-capable" content="yes">
 9 +		<meta name="apple-mobile-web-app-status-bar-style" content="default">
10 +		<meta name="apple-mobile-web-app-title" content="events">
11 +		<link rel="manifest" href="manifest.json">
12 +		<link rel="apple-touch-icon" href="resources/icon.png">
13 +		<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🐞</text></svg>">
14 +		<script>
15 +			// events.json is the real (gitignored) data; demo.json is the committed
16 +			// fallback used when no events.json is present.
17 +			function fetchJson(url) {
18 +				return fetch(url).then(function (res) {
19 +					if (!res.ok) { throw new Error("HTTP " + res.status); }
20 +					return res.json();
21 +				});
22 +			}
23 +			window.eventsData = fetchJson("resources/events.json")
24 +				.catch(function () { return fetchJson("resources/demo.json"); });
25 +		</script>
26 +		<link rel="stylesheet" href="resources/leaflet/leaflet.css">
27 +		<link rel="stylesheet" href="resources/styles.css">
28 +	</head>
29 +	<body>
30 +		<div id="boxes">
31 +			<div id="left-box">
32 +				<div id="map-stage">
33 +					<div id="map-container"></div>
34 +				</div>
35 +				<div id="compass" aria-hidden="true">
36 +					<svg viewBox="0 0 40 40" width="40" height="40">
37 +						<polygon class="needle-n" points="20,5 13,20 27,20"></polygon>
38 +						<polygon class="needle-s" points="20,35 13,20 27,20"></polygon>
39 +						<circle class="needle-hub" cx="20" cy="20" r="2.5"></circle>
40 +					</svg>
41 +					<svg class="compass-ring" viewBox="0 0 48 48" width="48" height="48">
42 +						<circle class="ring-arc" cx="24" cy="24" r="22.75" pathLength="100"></circle>
43 +					</svg>
44 +				</div>
45 +			</div>
46 +			<div id="right-box">
47 +				<h1 id="text-title"></h1>
48 +				<div id="text-content"></div>
49 +				<div id="action-wrap"></div>
50 +			</div>
51 +		</div>
52 +
53 +		<!-- print-only flyer; populated from the selected event on print, hidden on screen -->
54 +		<div id="flyer" aria-hidden="true"></div>
55 +
56 +		<script>
57 +			// cache the app shell + map tiles for fast revisits and offline use
58 +			if ("serviceWorker" in navigator) {
59 +				navigator.serviceWorker.register("sw.js").catch(function () {}); // ignore on file://
60 +			}
61 +		</script>
62 +		<script src="resources/nav.js"></script>
63 +		<script src="resources/format.js"></script>
64 +		<script src="resources/qr.js"></script>
65 +		<script src="resources/pdf.js"></script>
66 +		<script src="resources/flyer.js"></script>
67 +		<script src="resources/panel.js"></script>
68 +		<script src="resources/map.js"></script>
69 +		<script src="resources/app.js"></script>
70 +	</body>
71 +</html>
manifest.json
 1 +{
 2 +	"id": "./",
 3 +	"name": "events",
 4 +	"short_name": "events",
 5 +	"description": "events",
 6 +	"start_url": "./",
 7 +	"scope": "./",
 8 +	"display": "standalone",
 9 +	"background_color": "antiquewhite",
10 +	"theme_color": "antiquewhite",
11 +	"icons": [
12 +		{
13 +			"src": "resources/icon.png",
14 +			"sizes": "1024x1024",
15 +			"type": "image/png",
16 +			"purpose": "any maskable"
17 +		}
18 +	]
19 +}
readme_images/screenshot.jpg
Binary files differ.
resources/app.js
 1 +(function () {
 2 +	if (!window.location.search) { window.panel.paintIntro(); }
 3 +	window.eventsData
 4 +		.then(function (data) {
 5 +			window.eventsMap.setEvents(data);
 6 +			// preset the panel color for a deep-linked event that will actually be shown,
 7 +			// so it never flashes antiquewhite first. A passed event is only shown (and so
 8 +			// only worth presetting) in past mode, i.e. when nothing is upcoming.
 9 +			var deepLinked = window.fmt.eventFromUrl(data);
10 +			var shown = deepLinked && (!window.fmt.hasPassed(deepLinked) ||
11 +				!window.fmt.upcomingEvents(data).length);
12 +			if (shown) { window.panel.presetEventColor(deepLinked); }
13 +			window.eventsMap.load();
14 +		})
15 +		.catch(function () {
16 +			document.getElementById("text-title").textContent = "Couldn't load events";
17 +			document.getElementById("text-content").textContent = "If you opened this file directly, serve it over http (./serve.py) so the browser can fetch events.json.";
18 +		});
19 +})();
resources/demo.json
  1 +[
  2 +	{
  3 +		"title": "Pin, Patch, & Trinket Swap",
  4 +		"venue": "Seattle Public Library, Beacon Hill Branch",
  5 +		"address": "2821 Beacon Ave S, Seattle, WA 98144",
  6 +		"description": "Trade enamel pins, patches, and trinkets. Bring your doubles. There's usually a 'free finds' box for newcomers to start a collection. Bex is bringing her full national-park pin collection just to show off, and Hannah's brining a hand-crank button maker so you can make a custom pin to take home or swap.",
  7 +		"color": "darkseagreen",
  8 +		"start": "2026-03-23T16:00",
  9 +		"end": "2026-03-23T18:00",
 10 +		"price": "Free",
 11 +		"ageRange": "All ages",
 12 +		"rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
 13 +		"latitude": 47.5780,
 14 +		"longitude": -122.3114
 15 +	},
 16 +	{
 17 +		"title": "HTML Day",
 18 +		"venue": "Volunteer Park",
 19 +		"address": "1247 15th Ave E, Seattle, WA 98112",
 20 +		"description": "A laid-back afternoon of writing HTML outdoors. Bring a laptop and build whatever you like: a blog, a shrine to your favorite obscure band, or a delightfully weird personal site that's uniquely you. It's all about expressing yourself and carving out your own space on the World Wide Web. We'll bring a few printed cheat sheets and a power strip or two. Beginners genuinely welcome; someone will help you get your first page online. Iris is bringing a portable hotspot and a tiny printer that spits out everyone's finished URLs on receipt paper as keepsakes, and Cosmo is starting a webring and wants to link everyone's pages together before the afternoon's out.",
 21 +		"color": "mediumturquoise",
 22 +		"start": "2026-03-28T11:00",
 23 +		"end": "2026-03-28T14:00",
 24 +		"price": "Free",
 25 +		"ageRange": "All ages",
 26 +		"rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
 27 +		"latitude": 47.6303,
 28 +		"longitude": -122.3157
 29 +	},
 30 +	{
 31 +		"title": "Sunday Stitch & Chat",
 32 +		"venue": "Top Pot Doughnuts",
 33 +		"address": "609 Summit Ave E, Seattle, WA 98102",
 34 +		"description": "A relaxed knitting circle for all skill levels. Bring a project you've been working on or start a new one. Jen is bringing a basket of hand-dyed sock yarn she over-ordered and wants to give away, and Olive promises to finally unveil the giant intarsia blanket she's been working on since last winter.",
 35 +		"color": "#d1c6f7",
 36 +		"start": "2026-04-05T14:00",
 37 +		"end": "2026-04-05T16:00",
 38 +		"price": "Free",
 39 +		"ageRange": "",
 40 +		"rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
 41 +		"latitude": 47.6253,
 42 +		"longitude": -122.3274
 43 +	},
 44 +	{
 45 +		"title": "Seed Swap",
 46 +		"venue": "P-Patch Community Garden",
 47 +		"address": "2451 15th Ave W, Seattle, WA 98119",
 48 +		"description": "Bring seeds to trade, or just come and take some - no one leaves empty-handed. The table this week is unusually good: Nick is bringing Virginia-native pawpaw seeds, Fern is sharing rare hot pepper seeds, and Josh has a tin of heirloom tomato seeds in varieties you won't find in any store. Irving is bringing a stack of their handmade, reusable seed packets so you'll have something nice to carry your finds home in.",
 49 +		"color": "yellowgreen",
 50 +		"start": "2026-05-02T10:00",
 51 +		"end": "2026-05-02T13:00",
 52 +		"price": "Free",
 53 +		"ageRange": "All ages",
 54 +		"rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
 55 +		"latitude": 47.6432,
 56 +		"longitude": -122.376
 57 +	},
 58 +	{
 59 +		"title": "Saturday Flea Market",
 60 +		"venue": "Cal Anderson Park",
 61 +		"address": "1635 11th Ave, Seattle, WA 98122",
 62 +		"description": "A sprawling, open-air flea market with a little bit of everything. This week's tables include a refurbished retrocomputer vendor, a stand selling homemade pies by the slice or whole, and a small coop of baby chickens for anyone starting a backyard flock. Bring cash and a tote bag.",
 63 +		"color": "gold",
 64 +		"start": "2026-05-09T08:00",
 65 +		"end": "2026-05-09T12:00",
 66 +		"price": "Free",
 67 +		"ageRange": "All ages",
 68 +		"rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
 69 +		"latitude": 47.617,
 70 +		"longitude": -122.3191
 71 +	},
 72 +	{
 73 +		"title": "Chess in the Park",
 74 +		"venue": "Occidental Square",
 75 +		"address": "117 S Washington St, Seattle, WA 98104",
 76 +		"description": "Friendly games at every level. Boards provided, but feel free to bring your own. Casual ladder for anyone who wants to track results. Greg is bringing a hand-carved travel set and a thermos of coffee to share, and Yuki has been studying one opening trap all week and is determined to spring it on someone. Looking to mix things up? Irving is bringing a handmade minishogi set. Hit them up if you want to learn how to play.",
 77 +		"color": "#b2cbe6",
 78 +		"start": "2026-05-13T17:30",
 79 +		"end": "2026-05-13T19:30",
 80 +		"price": "Free",
 81 +		"ageRange": "All ages",
 82 +		"rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
 83 +		"latitude": 47.6003,
 84 +		"longitude": -122.3331
 85 +	},
 86 +	{
 87 +		"title": "Beginner Skate Meetup",
 88 +		"venue": "Seattle Center Skatepark",
 89 +		"address": "220 Thomas St, Seattle, WA 98109",
 90 +		"description": "Low-pressure morning session for new and returning skaters. A couple of experienced folks happy to show the basics. Helmets strongly encouraged. Cira is bringing a box of loaner pads and a couple of spare boards in different sizes, and Devon is bringing a boombox + a stack of mix CDs.",
 91 +		"color": "#fc981e",
 92 +		"start": "2026-05-23T10:00",
 93 +		"end": "2026-05-23T12:00",
 94 +		"price": "Free",
 95 +		"ageRange": "All ages",
 96 +		"rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
 97 +		"latitude": 47.6247,
 98 +		"longitude": -122.3504
 99 +	},
100 +	{
101 +		"title": "Typewriter Poetry in the Park",
102 +		"venue": "Discovery Park",
103 +		"address": "3801 Discovery Park Blvd, Seattle, WA 98199",
104 +		"description": "An afternoon of writing poems the analog way, on real typewriters. We'll bring two mid-century machines to share, but you're welcome to show up with any kind of writing device (including pen and paper). One of our regulars is bringing an e-ink writerdeck to demo, so you can see what it's like to type on the typewriter's modern, distraction-free descendants. No experience necessary. Prompts provided for anyone staring at a blank page.",
105 +		"color": "#c7bbab",
106 +		"start": "2026-06-03T16:00",
107 +		"end": "2026-06-03T18:00",
108 +		"price": "Free",
109 +		"ageRange": "All ages",
110 +		"rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
111 +		"latitude": 47.658,
112 +		"longitude": -122.406
113 +	},
114 +	{
115 +		"title": "Zine-Making Workshop",
116 +		"venue": "Hing Hay Park",
117 +		"address": "423 Maynard Ave S, Seattle, WA 98104",
118 +		"description": "Draw, fold, and photocopy your way to your very own zine. We'll supply scissors, glue sticks, old magazines to cut up, and plenty of paper. You supply the ideas (or borrow ours). We'll have a copy machine running off a solar-powered battery, so you can print and assemble finished copies right there in the sun. As a bonus, we'll have a little free pile of zines about how to make zines (covering everything from the classic one-page eight-fold to stapled mini-comics) so first-timers have something to flip through and steal ideas from.",
119 +		"color": "#f29bc5",
120 +		"start": "2026-06-06T13:00",
121 +		"end": "2026-06-06T16:00",
122 +		"price": "Free",
123 +		"ageRange": "All ages",
124 +		"rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
125 +		"latitude": 47.5985,
126 +		"longitude": -122.3238
127 +	},
128 +	{
129 +		"title": "Mario Kart 64 Tournament",
130 +		"venue": "Gas Works Park",
131 +		"address": "2101 N Northlake Way, Seattle, WA 98103",
132 +		"description": "Three... Two... One... GO! Drop the pedal to the metal on the big screen, as you go for gold in this 4-player, round robin tournament. We'll have a smattering of other games to sample while you wait, including Tetris Attack, ChuChu Rocket, and Super Smash Bros Melee (BYOC). We'll have a couch, rug, and CRT set up on the Great Mound.",
133 +		"color": "#12a33e",
134 +		"start": "2026-06-12T19:30",
135 +		"end": "2026-06-13T01:00",
136 +		"price": "Free",
137 +		"ageRange": "18+",
138 +		"rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
139 +		"latitude": 47.6456,
140 +		"longitude": -122.3344
141 +	}
142 +]
resources/flyer.js
  1 +// Print-only flyer for the selected event. Most browsers get a print-CSS DOM flyer +
  2 +// window.print; Safari stamps a non-removable header/footer on printed web content, so
  3 +// there the flyer is drawn into a vector PDF (pdf.js) and opened in a new tab.
  4 +
  5 +(function () {
  6 +	var SVG_NS = "http://www.w3.org/2000/svg";
  7 +	var TAB_COUNT = 10;
  8 +	var QUIET = 0; // no baked-in quiet zone
  9 +
 10 +	// Safari (desktop/iOS) but not the other browsers whose UA also carries "Safari".
 11 +	// Safari takes the PDF path; the print CSS swap stays gated off (.flyer-print).
 12 +	var isSafari = /Safari/.test(navigator.userAgent) &&
 13 +		!/Chrome|Chromium|CriOS|Edg|Android/.test(navigator.userAgent);
 14 +	if (!isSafari) { document.documentElement.classList.add("flyer-print"); }
 15 +
 16 +	var flyerEl = null;
 17 +	var building = false; // guards against the link + beforeprint paths double-building
 18 +
 19 +	function el(tag, cls, text) {
 20 +		var n = document.createElement(tag);
 21 +		if (cls) { n.className = cls; }
 22 +		if (text != null) { n.textContent = text; }
 23 +		return n;
 24 +	}
 25 +
 26 +	// QR for `text` as an <svg> `mm` square. Dark modules merge into per-row run rects.
 27 +	function qrSvg(text, mm) {
 28 +		var matrix = window.qr.createMatrix(text);
 29 +		var n = matrix.length;
 30 +		var total = n + QUIET * 2;
 31 +
 32 +		var svg = document.createElementNS(SVG_NS, "svg");
 33 +		svg.setAttribute("viewBox", "0 0 " + total + " " + total);
 34 +		svg.setAttribute("width", mm + "mm");
 35 +		svg.setAttribute("height", mm + "mm");
 36 +		svg.setAttribute("shape-rendering", "crispEdges");
 37 +		svg.classList.add("flyer-qr");
 38 +
 39 +		var bg = document.createElementNS(SVG_NS, "rect");
 40 +		bg.setAttribute("width", total);
 41 +		bg.setAttribute("height", total);
 42 +		bg.setAttribute("fill", "#fff");
 43 +		svg.appendChild(bg);
 44 +
 45 +		for (var r = 0; r < n; r++) {
 46 +			var c = 0;
 47 +			while (c < n) {
 48 +				if (!matrix[r][c]) { c++; continue; }
 49 +				var start = c;
 50 +				while (c < n && matrix[r][c]) { c++; }
 51 +				var rect = document.createElementNS(SVG_NS, "rect");
 52 +				rect.setAttribute("x", start + QUIET);
 53 +				rect.setAttribute("y", r + QUIET);
 54 +				rect.setAttribute("width", c - start);
 55 +				rect.setAttribute("height", 1);
 56 +				rect.setAttribute("fill", "#000");
 57 +				svg.appendChild(rect);
 58 +			}
 59 +		}
 60 +		return svg;
 61 +	}
 62 +
 63 +	// .flyer-flow (the fitFlow-sized element) holds a floated QR the description wraps around.
 64 +	function buildBody(m) {
 65 +		var body = el("div", "flyer-body");
 66 +		body.appendChild(el("h1", "flyer-title", m.title));
 67 +
 68 +		var flow = el("div", "flyer-flow");
 69 +		flow.appendChild(qrSvg(window.fmt.eventUrl(m), QR_BODY)); // max-size; fitQr shrinks to the line grid
 70 +		flow.appendChild(el("p", "flyer-desc", m.description));
 71 +		var ul = el("ul", "flyer-details");
 72 +		window.fmt.detailItems(m).forEach(function (line) {
 73 +			ul.appendChild(el("li", null, line));
 74 +		});
 75 +		flow.appendChild(ul);
 76 +		body.appendChild(flow);
 77 +
 78 +		return body;
 79 +	}
 80 +
 81 +	function buildTab(m) {
 82 +		var inner = el("div", "flyer-tab-inner");
 83 +		var text = el("div", "flyer-tab-text");
 84 +		text.appendChild(el("div", "flyer-tab-title", m.title));
 85 +		text.appendChild(el("div", "flyer-tab-when", window.fmt.startLine(m)));
 86 +		text.appendChild(el("div", "flyer-tab-where", m.venue));
 87 +		inner.appendChild(text);
 88 +		inner.appendChild(qrSvg(window.fmt.eventUrl(m), 16));
 89 +		return inner;
 90 +	}
 91 +
 92 +	function buildTabs(m) {
 93 +		var tabs = el("div", "flyer-tabs");
 94 +		var template = buildTab(m);
 95 +		for (var i = 0; i < TAB_COUNT; i++) {
 96 +			var tab = el("div", "flyer-tab");
 97 +			tab.appendChild(template.cloneNode(true));
 98 +			tabs.appendChild(tab);
 99 +		}
100 +		return tabs;
101 +	}
102 +
103 +	// Tabs are identical: fit the first's text and copy the font-size to the rest.
104 +	function fitTabs(tabsEl) {
105 +		var inners = tabsEl.querySelectorAll(".flyer-tab-inner");
106 +		if (!inners.length) { return; }
107 +		var inner = inners[0];
108 +		var text = inner.querySelector(".flyer-tab-text");
109 +		// transforms don't affect layout: the rotated tab lays out in its unrotated frame
110 +		var maxH = inner.clientHeight;
111 +		var lo = 6, hi = 16;
112 +		while (lo < hi) {
113 +			var mid = Math.ceil((lo + hi) / 2);
114 +			inner.style.fontSize = mid + "pt";
115 +			if (text.offsetHeight <= maxH) { lo = mid; } else { hi = mid - 1; }
116 +		}
117 +		var size = lo + "pt";
118 +		for (var i = 0; i < inners.length; i++) { inners[i].style.fontSize = size; }
119 +	}
120 +
121 +	// White leading inside a line box, in em: Times metrics (ascent .683, descent .217,
122 +	// caps .662) with CSS half-leading. Page gaps are equalized as ink, so each margin
123 +	// is widened by the leading of the line boxes facing it.
124 +	function leadBelow(lineHeight) { return (lineHeight - 0.9) / 2 + 0.217; }
125 +	function leadAbove(lineHeight) { return (lineHeight - 0.9) / 2 + 0.683 - 0.662; }
126 +
127 +	// Largest QR (<= 42mm) whose float band ends on the flow's line grid (0.2mm short so
128 +	// rounding can't re-narrow the boundary line), so the first full-width line clears
129 +	// the float at the same 7mm gutter the text gets. Text size beats QR size.
130 +	function alignedQrMm(lineHmm) {
131 +		var n = Math.floor((2 + QR_BODY + 7) / lineHmm);
132 +		return n * lineHmm - (2 + 7) - 0.2;
133 +	}
134 +
135 +	// Run after fitFlow: shrinking the float only frees space, so the fitted size still fits.
136 +	function fitQr(flow) {
137 +		var qr = flow.querySelector(".flyer-qr");
138 +		var size = alignedQrMm(parseFloat(flow.style.fontSize) * 1.3 * 25.4 / 96); // CSS px -> mm
139 +		qr.setAttribute("width", size + "mm");
140 +		qr.setAttribute("height", size + "mm");
141 +	}
142 +
143 +	// Largest quarter-px font-size at which the flow fits maxH. The flow is a stretched
144 +	// flex child, so collapse the stretch (flex:none, height:auto) to measure content.
145 +	function fitFlow(flow, maxH) {
146 +		flow.style.flex = "none";
147 +		flow.style.height = "auto";
148 +		flow.style.lineHeight = "1.3";
149 +		var lo = 32, hi = 256; // quarter-px units
150 +		while (lo < hi) {
151 +			var mid = Math.ceil((lo + hi) / 2);
152 +			flow.style.fontSize = mid / 4 + "px";
153 +			if (flow.getBoundingClientRect().height <= maxH) { lo = mid; } else { hi = mid - 1; }
154 +		}
155 +		flow.style.fontSize = lo / 4 + "px";
156 +		flow.style.flex = "";
157 +		flow.style.height = "";
158 +	}
159 +
160 +	// Build the flyer for `m`, size its body text to fit, open the print dialog. Lays out
161 +	// offscreen at the print width first so measurements reflect true print layout.
162 +	function build(m) {
163 +		flyerEl = document.getElementById("flyer");
164 +		if (!flyerEl) { return; }
165 +		flyerEl.textContent = "";
166 +
167 +		var body = buildBody(m);
168 +		var tabs = buildTabs(m);
169 +		flyerEl.appendChild(body);
170 +		flyerEl.appendChild(tabs);
171 +
172 +		flyerEl.classList.add("flyer-measuring");
173 +		var flow = body.querySelector(".flyer-flow");
174 +		var title = body.querySelector(".flyer-title");
175 +		var titleStyle = getComputedStyle(title);
176 +		// avail height from the rendered title block, not the flex-stretched flow box
177 +		var titleBlock = title.getBoundingClientRect().height +
178 +			parseFloat(titleStyle.marginTop) + parseFloat(titleStyle.marginBottom);
179 +		var flyerH = flyerEl.getBoundingClientRect().height;
180 +		var tabsH = tabs.getBoundingClientRect().height;
181 +		var maxH = flyerH - tabsH - titleBlock;
182 +		fitFlow(flow, Math.max(maxH, 0));
183 +
184 +		// Equalize the three page gaps as ink (see leadBelow/leadAbove): comp is the net
185 +		// height the widened margins cost beyond 7mm; if positive, refit with it reserved.
186 +		var titlePx = parseFloat(titleStyle.fontSize);
187 +		function gapComp(f) { return (leadBelow(1.3) + leadAbove(1.3)) * f - leadBelow(1.05) * titlePx; }
188 +		var fs = parseFloat(flow.style.fontSize);
189 +		var comp = gapComp(fs);
190 +		if (comp > 0) {
191 +			fitFlow(flow, Math.max(maxH - comp, 0));
192 +			fs = parseFloat(flow.style.fontSize);
193 +			comp = gapComp(fs);
194 +		}
195 +		fitQr(flow);
196 +
197 +		// leftover slack split a third per gap; measured after fitQr frees wrapped lines
198 +		flow.style.flex = "none";
199 +		flow.style.height = "auto";
200 +		var s3 = Math.max(maxH - comp - flow.getBoundingClientRect().height, 0) / 3;
201 +		flow.style.flex = "";
202 +		flow.style.height = "";
203 +		var b7 = 7 * 96 / 25.4; // 7mm base in px
204 +		title.style.marginBottom = (b7 + s3 + leadBelow(1.3) * fs - leadBelow(1.05) * titlePx) + "px";
205 +		var ul = flow.querySelector(".flyer-details");
206 +		ul.style.marginTop = (b7 + s3) + "px";
207 +		ul.style.marginBottom = (b7 + s3 + leadAbove(1.3) * fs) + "px";
208 +
209 +		fitTabs(tabs);
210 +
211 +		flyerEl.classList.remove("flyer-measuring");
212 +	}
213 +
214 +	// ---- PDF path (Safari) ----
215 +	// Mirrors the DOM flyer's CSS in flyer-local mm with a top-left origin; px()/py()
216 +	// are the only crossing into PDF points (origin bottom-left, y-up).
217 +
218 +	var MM = 72 / 25.4; // mm -> pt
219 +	var PAGE_W = 215.9, PAGE_H = 279.4; // US Letter
220 +	var BOX_W = 197.3, BOX_H = 266.7; // flyer box (shared Letter/A4 safe area)
221 +	var BOX_X = (PAGE_W - BOX_W) / 2, BOX_Y = 6.35; // centered, 6.35mm top margin
222 +	var TABS_H = 78, TAB_W = BOX_W / TAB_COUNT;
223 +	var DASH = { width: 0.75, dash: [2.25, 2.25] }; // 1px dashed at print scale
224 +	var TITLE_PT = 48, TITLE_LINE_H = TITLE_PT * 1.05 / MM;
225 +	var QR_BODY = 42; // mm, body QR max; alignedQrMm shrinks it to the line grid
226 +	var QR_TAB = 16; // mm, tab QR
227 +	var TAB_PAD_V = 2.5, TAB_PAD_TEXT = 1.5, TAB_PAD_QR = 2.5, TAB_GAP = 2.5;
228 +	var TAB_TEXT_W = TABS_H - TAB_PAD_TEXT - TAB_PAD_QR - TAB_GAP - QR_TAB;
229 +	var TAB_CONTENT_H = TAB_W - 2 * TAB_PAD_V; // cross-axis space (clientHeight in fitTabs)
230 +
231 +	function px(x) { return (BOX_X + x) * MM; }
232 +	function py(y) { return (PAGE_H - (BOX_Y + y)) * MM; }
233 +
234 +	// mm width of str at sizePt, via the vendored AFM tables
235 +	function tw(str, font, sizePt) { return window.pdf.widthOf(str, font, sizePt) / MM; }
236 +
237 +	// baseline's mm offset from the top of a line box (CSS half-leading, Times metrics)
238 +	function baselineOff(sizePt, lineHmm) {
239 +		return ((lineHmm * MM - sizePt * 0.9) / 2 + sizePt * 0.683) / MM;
240 +	}
241 +
242 +	// Word-wrap str into [{ text, yTop }], yTop in mm from y0. availFn(yTop) gives the
243 +	// usable width at a line's top (how the floated QR narrows lines beside it). Words
244 +	// wider than a line break mid-word (overflow-wrap: break-word).
245 +	function wrap(str, font, sizePt, availFn, y0, lineHmm) {
246 +		var lines = [];
247 +		var cur = "";
248 +		var y = y0;
249 +		function push(s) { lines.push({ text: s, yTop: y }); y += lineHmm; }
250 +		String(str).split(/\s+/).filter(Boolean).forEach(function (word) {
251 +			var cand = cur ? cur + " " + word : word;
252 +			if (tw(cand, font, sizePt) <= availFn(y)) { cur = cand; return; }
253 +			if (cur) { push(cur); cur = ""; }
254 +			while (tw(word, font, sizePt) > availFn(y)) {
255 +				var k = 1; // longest fitting prefix; min 1 char so we always advance
256 +				while (k < word.length && tw(word.slice(0, k + 1), font, sizePt) <= availFn(y)) { k++; }
257 +				push(word.slice(0, k));
258 +				word = word.slice(k);
259 +			}
260 +			cur = word;
261 +		});
262 +		if (cur) { push(cur); }
263 +		return lines;
264 +	}
265 +
266 +	// title block: lines at 48pt/1.05 plus the h1's 1.5mm top / 7mm bottom margins
267 +	function layoutTitle(m) {
268 +		var lines = wrap(m.title, "times", TITLE_PT, function () { return BOX_W; }, 1.5, TITLE_LINE_H);
269 +		return { lines: lines, block: 1.5 + lines.length * TITLE_LINE_H + 7 };
270 +	}
271 +
272 +	// Mirrors buildBody/fitFlow in flow-local mm: lines beside the floated QR wrap at the
273 +	// narrowed width; the flow is at least the float band tall.
274 +	function layoutFlow(m, pxSize, qrMm, gapTopMm, gapBotMm) {
275 +		var size = pxSize * 0.75; // CSS px -> pt
276 +		var lineH = size * 1.3 / MM;
277 +		var em = size / MM;
278 +		var band = 2 + qrMm + 7; // float margin box: 2mm top + QR + 7mm bottom
279 +		var narrow = BOX_W - (qrMm + 7); // line width beside the float (7mm margin-left)
280 +		var runs = [];
281 +		var y = 0;
282 +		function availAt(yy) { return yy < band ? narrow : BOX_W; }
283 +		var descLines = wrap(m.description, "times", size, availAt, 0, lineH);
284 +		descLines.forEach(function (line, i) {
285 +			// justify: pad word gaps to fill the line; last line stays ragged
286 +			var gaps = line.text.split(" ").length - 1;
287 +			var ws = (i < descLines.length - 1 && gaps > 0)
288 +				? (availAt(line.yTop) - tw(line.text, "times", size)) * MM / gaps : 0;
289 +			runs.push({ text: line.text, x: 0, yTop: line.yTop, ws: ws });
290 +			y = line.yTop + lineH;
291 +		});
292 +		y += gapTopMm; // ul margin-top
293 +		var indent = 1.2 * em; // ul padding-left
294 +		var bulletW = tw("•", "times", size);
295 +		window.fmt.detailItems(m).forEach(function (item) {
296 +			y += 0.15 * em; // li margin-top
297 +			var lines = wrap(item, "times", size, function (yy) { return availAt(yy) - indent; }, y, lineH);
298 +			runs.push({ text: "•", x: indent - 0.45 * em - bulletW, yTop: lines[0].yTop });
299 +			lines.forEach(function (line) {
300 +				runs.push({ text: line.text, x: indent, yTop: line.yTop });
301 +				y = line.yTop + lineH;
302 +			});
303 +		});
304 +		y += gapBotMm; // ul margin-bottom (counts toward the flow's BFC height)
305 +		return { height: Math.max(y, band), size: size, lineH: lineH, qr: qrMm, runs: runs };
306 +	}
307 +
308 +	// fitFlow + fitQr's mirror: fit at the max QR, then final layout at the grid-aligned QR
309 +	function fitPdfFlow(m, maxH) {
310 +		var lo = 32, hi = 256; // quarter-px units
311 +		while (lo < hi) {
312 +			var mid = Math.ceil((lo + hi) / 2);
313 +			if (layoutFlow(m, mid / 4, QR_BODY, 7, 7).height <= maxH) { lo = mid; } else { hi = mid - 1; }
314 +		}
315 +		return layoutFlow(m, lo / 4, alignedQrMm(lo / 4 * 0.75 * 1.3 / MM), 7, 7);
316 +	}
317 +
318 +	// QR as rects (white backing + black per-row runs), top-left at (x, y). emit places one rect.
319 +	function qrPaint(emit, text, x, y, sizeMm) {
320 +		var matrix = window.qr.createMatrix(text);
321 +		var n = matrix.length;
322 +		var mod = sizeMm / n;
323 +		emit(x, y, sizeMm, sizeMm, 1);
324 +		for (var r = 0; r < n; r++) {
325 +			var c = 0;
326 +			while (c < n) {
327 +				if (!matrix[r][c]) { c++; continue; }
328 +				var start = c;
329 +				while (c < n && matrix[r][c]) { c++; }
330 +				emit(x + start * mod, y + r * mod, (c - start) * mod, mod, 0);
331 +			}
332 +		}
333 +	}
334 +
335 +	// record a QR's rects once so the 10 tabs replay them
336 +	function qrRuns(text, sizeMm) {
337 +		var runs = [];
338 +		qrPaint(function (x, y, w, h, gray) { runs.push([x, y, w, h, gray]); }, text, 0, 0, sizeMm);
339 +		return runs;
340 +	}
341 +
342 +	// Tab text column (bold title, when, where) at base size s pt, in inner-local coords:
343 +	// u along the 78mm axis, v across the 19.73mm one.
344 +	function layoutTabText(m, s) {
345 +		var runs = [];
346 +		var v = 0;
347 +		function add(str, font, sizePt, lineH, marginTop) {
348 +			v += marginTop;
349 +			wrap(str, font, sizePt, function () { return TAB_TEXT_W; }, v, lineH).forEach(function (line) {
350 +				runs.push({ text: line.text, font: font, size: sizePt, vTop: line.yTop, lineH: lineH });
351 +				v = line.yTop + lineH;
352 +			});
353 +		}
354 +		add(m.title, "timesBold", 1.3 * s, 1.3 * s * 1.1 / MM, 0);
355 +		add(window.fmt.startLine(m), "times", s, s * 1.15 / MM, 0.1 * s / MM);
356 +		add(m.venue, "times", s, s * 1.15 / MM, 0.1 * s / MM);
357 +		return { runs: runs, height: v };
358 +	}
359 +
360 +	// fitTabs's mirror: largest base pt size whose text stack fits the strip
361 +	function fitPdfTab(m) {
362 +		var lo = 6, hi = 16;
363 +		while (lo < hi) {
364 +			var mid = Math.ceil((lo + hi) / 2);
365 +			if (layoutTabText(m, mid).height <= TAB_CONTENT_H) { lo = mid; } else { hi = mid - 1; }
366 +		}
367 +		return layoutTabText(m, lo);
368 +	}
369 +
370 +	// One tab's rotated content. The inner box is centered on its cell and rotated 90deg CW,
371 +	// so local (u, v) maps to flyer-local (cx + TAB_W/2 - v, cy - TABS_H/2 + u): u runs down
372 +	// the page, QR at the foot. Rects swap w/h; text uses the writer's rotate90.
373 +	function paintTab(doc, i, tabText, runs) {
374 +		var cx = i * TAB_W + TAB_W / 2;
375 +		var cy = BOX_H - TABS_H / 2;
376 +		function mapX(v) { return cx + TAB_W / 2 - v; }
377 +		function mapY(u) { return cy - TABS_H / 2 + u; }
378 +		var vText = TAB_PAD_V + (TAB_CONTENT_H - tabText.height) / 2; // align-items: center
379 +		tabText.runs.forEach(function (run) {
380 +			var v = vText + run.vTop + baselineOff(run.size, run.lineH);
381 +			doc.text(run.text, px(mapX(v)), py(mapY(TAB_PAD_TEXT)),
382 +				{ font: run.font, size: run.size, rotate90: true });
383 +		});
384 +		var u0 = TAB_PAD_TEXT + TAB_TEXT_W + TAB_GAP;
385 +		var v0 = TAB_PAD_V + (TAB_CONTENT_H - QR_TAB) / 2;
386 +		runs.forEach(function (r) {
387 +			var u = u0 + r[0], v = v0 + r[1], w = r[2], h = r[3];
388 +			doc.rect(px(mapX(v + h)), py(mapY(u) + w), h * MM, w * MM, r[4]);
389 +		});
390 +	}
391 +
392 +	// the tear-from-body line plus the 11 cut lines bounding the 10 tabs
393 +	function paintCutLines(doc) {
394 +		var top = BOX_H - TABS_H;
395 +		doc.dashedLine(px(0), py(top), px(BOX_W), py(top), DASH);
396 +		for (var i = 0; i <= TAB_COUNT; i++) {
397 +			doc.dashedLine(px(i * TAB_W), py(top), px(i * TAB_W), py(BOX_H), DASH);
398 +		}
399 +	}
400 +
401 +	// Draw the whole flyer for `m`; returns the PDF file bytes.
402 +	function buildPdf(m) {
403 +		var doc = window.pdf.create(PAGE_W * MM, PAGE_H * MM);
404 +		var title = layoutTitle(m);
405 +		title.lines.forEach(function (line) {
406 +			doc.text(line.text, px(0), py(line.yTop + baselineOff(TITLE_PT, TITLE_LINE_H)),
407 +				{ font: "times", size: TITLE_PT });
408 +		});
409 +		var flowTop = title.block;
410 +		var avail = BOX_H - TABS_H - flowTop;
411 +		// build()'s mirror: ink-equalized gaps with the fit slack split a third per gap
412 +		var titleEm = TITLE_PT / MM; // title font size in mm
413 +		function gapComp(e) { return (leadBelow(1.3) + leadAbove(1.3)) * e - leadBelow(1.05) * titleEm; }
414 +		var flow = fitPdfFlow(m, avail);
415 +		var comp = gapComp(flow.size / MM);
416 +		if (comp > 0) {
417 +			flow = fitPdfFlow(m, avail - comp);
418 +			comp = gapComp(flow.size / MM);
419 +		}
420 +		var s3 = Math.max(avail - comp - flow.height, 0) / 3;
421 +		flow = layoutFlow(m, flow.size / 0.75, flow.qr,
422 +			7 + s3, 7 + s3 + leadAbove(1.3) * flow.size / MM);
423 +		flowTop += s3 + leadBelow(1.3) * flow.size / MM - leadBelow(1.05) * titleEm;
424 +		flow.runs.forEach(function (run) {
425 +			doc.text(run.text, px(run.x), py(flowTop + run.yTop + baselineOff(flow.size, flow.lineH)),
426 +				{ font: "times", size: flow.size, wordSpacing: run.ws });
427 +		});
428 +		var url = window.fmt.eventUrl(m);
429 +		qrPaint(function (x, y, w, h, gray) {
430 +			doc.rect(px(x), py(y + h), w * MM, h * MM, gray);
431 +		}, url, BOX_W - flow.qr, flowTop + 2, flow.qr);
432 +		paintCutLines(doc);
433 +		var tabText = fitPdfTab(m);
434 +		var tabQr = qrRuns(url, QR_TAB);
435 +		for (var i = 0; i < TAB_COUNT; i++) { paintTab(doc, i, tabText, tabQr); }
436 +		return doc.end();
437 +	}
438 +
439 +	var pdfUrl = null;
440 +
441 +	// Safari can't script-print a blob PDF (WebKit), so open it in a new tab and let the
442 +	// user print from Safari's viewer. Synchronous so window.open keeps the gesture
443 +	// allowance. Old blob URL revoked on next build, not after open (tab loads it async).
444 +	function openPdf(m) {
445 +		var bytes = buildPdf(m);
446 +		if (pdfUrl) { URL.revokeObjectURL(pdfUrl); }
447 +		pdfUrl = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
448 +		window.open(pdfUrl, "_blank");
449 +	}
450 +
451 +	window.addEventListener("pagehide", function () {
452 +		if (pdfUrl) { URL.revokeObjectURL(pdfUrl); pdfUrl = null; }
453 +	});
454 +
455 +	// afterprint, not synchronously after print(): some engines render async
456 +	window.addEventListener("afterprint", function () {
457 +		if (flyerEl) { flyerEl.textContent = ""; }
458 +		building = false;
459 +	});
460 +
461 +	function printFlyer(m) {
462 +		if (!m) { return; }
463 +		if (isSafari) { openPdf(m); return; }
464 +		if (building) { return; }
465 +		building = true;
466 +		build(m);
467 +		window.print();
468 +	}
469 +
470 +	// Cmd/Ctrl+P with an event selected: beforeprint can't cancel Safari's dialog, but
471 +	// keydown can preempt it and route to the PDF.
472 +	if (isSafari) {
473 +		window.addEventListener("keydown", function (e) {
474 +			if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey &&
475 +				(e.key === "p" || e.key === "P")) {
476 +				var m = window.panel && window.panel.getSelectedEvent && window.panel.getSelectedEvent();
477 +				if (!m) { return; } // intro showing -> let Safari print the page
478 +				e.preventDefault();
479 +				openPdf(m);
480 +			}
481 +		});
482 +	}
483 +
484 +	// beforeprint fires before the dialog, so build here so any browser-initiated print
485 +	// gets the flyer instead of the live app. (Link path sets `building` and we skip.)
486 +	window.addEventListener("beforeprint", function () {
487 +		if (isSafari || building) { return; }
488 +		var m = window.panel && window.panel.getSelectedEvent && window.panel.getSelectedEvent();
489 +		if (!m) { return; } // intro showing -> let the browser proceed
490 +		building = true;
491 +		build(m);
492 +	});
493 +
494 +	window.flyer = { print: printFlyer, buildPdf: buildPdf, enabled: true };
495 +})();
resources/format.js
  1 +// Pure date/text formatting helpers + email-obfuscation.
  2 +
  3 +(function () {
  4 +	var WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
  5 +	var MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
  6 +	var MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  7 +
  8 +	function ordinal(n) {
  9 +		var s = ["th", "st", "nd", "rd"], v = n % 100;
 10 +		return n + (s[(v - 20) % 10] || s[v] || s[0]);
 11 +	}
 12 +
 13 +	// Parse "2026-06-07T14:00" as local time (no timezone applied).
 14 +	function parseLocal(iso) {
 15 +		var p = iso.split(/[-T:]/);
 16 +		return new Date(p[0], p[1] - 1, p[2], p[3], p[4]);
 17 +	}
 18 +
 19 +	// "2:00" / "9:30pm" - am/pm only on the end so a range reads "1:00 - 4:00pm".
 20 +	function clockTime(d, withMeridian) {
 21 +		var h = d.getHours(), min = d.getMinutes();
 22 +		var mer = h >= 12 ? "pm" : "am";
 23 +		h = h % 12 || 12;
 24 +		return h + ":" + (min < 10 ? "0" + min : min) + (withMeridian ? mer : "");
 25 +	}
 26 +
 27 +	// "1:00 - 4:00pm" when start/end share a meridian; "11:00am - 1:00pm" when they
 28 +	// differ, so a cross-noon/midnight range isn't ambiguous. forceStart keeps the start
 29 +	// meridian even when shared, so a multiday range doesn't read as a same-day span.
 30 +	function timeRange(start, end, forceStart) {
 31 +		var sameMer = (start.getHours() >= 12) === (end.getHours() >= 12);
 32 +		return clockTime(start, forceStart || !sameMer) + " - " + clockTime(end, true);
 33 +	}
 34 +
 35 +	// "Sunday, June 7th"
 36 +	function dateLabel(d) {
 37 +		return WEEKDAYS[d.getDay()] + ", " + MONTH_NAMES[d.getMonth()] + " " + ordinal(d.getDate());
 38 +	}
 39 +
 40 +	function sameDay(a, b) {
 41 +		return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
 42 +	}
 43 +
 44 +	// " (Today!)" / " (Tomorrow)" / "" against `now` at call time (no live refresh).
 45 +	// A passed event takes ", <year>" instead, so the year is clear to viewers in a
 46 +	// later year landing on the same day-of-month; the "(Ended)" marker is appended
 47 +	// separately by the caller so it can sit at the end of a multiday range.
 48 +	function relativeDayTag(m, d, now) {
 49 +		now = now || new Date();
 50 +		if (hasPassed(m, now)) { return ", " + d.getFullYear(); }
 51 +		if (sameDay(d, now)) { return " (Today!)"; }
 52 +		var tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
 53 +		if (sameDay(d, tomorrow)) { return " (Tomorrow)"; }
 54 +		return "";
 55 +	}
 56 +
 57 +	// An event has "passed" once its end time is in the past.
 58 +	function hasPassed(m, now) {
 59 +		return parseLocal(m.end) < (now || new Date());
 60 +	}
 61 +
 62 +	// Upcoming events (end not yet past), soonest start first. Caller snapshots `now`.
 63 +	function upcomingEvents(events, now) {
 64 +		now = now || new Date();
 65 +		return events.filter(function (m) { return !hasPassed(m, now); })
 66 +			.sort(function (a, b) { return parseLocal(a.start) - parseLocal(b.start); });
 67 +	}
 68 +
 69 +	// Up to `limit` passed events, most-recently-ended first. Caller snapshots `now`.
 70 +	function pastEvents(events, now, limit) {
 71 +		now = now || new Date();
 72 +		return events.filter(function (m) { return hasPassed(m, now); })
 73 +			.sort(function (a, b) { return parseLocal(b.end) - parseLocal(a.end); })
 74 +			.slice(0, limit == null ? 10 : limit);
 75 +	}
 76 +
 77 +	// "June 7th" - short date for the intro table's date column. withYear appends ", 2025"
 78 +	// so past events read unambiguously in a later year.
 79 +	function shortDate(m, withYear) {
 80 +		var d = parseLocal(m.start);
 81 +		return MONTH_NAMES[d.getMonth()] + " " + ordinal(d.getDate()) + (withYear ? ", " + d.getFullYear() : "");
 82 +	}
 83 +
 84 +	// "Sunday, June 7th from 1:00 - 4:00pm"
 85 +	function dateLine(m) {
 86 +		var start = parseLocal(m.start), end = parseLocal(m.end);
 87 +		return dateLabel(start) + " from " + timeRange(start, end);
 88 +	}
 89 +
 90 +	// "Sunday, June 7th at 1:00pm" - start only (flyer tear-tabs).
 91 +	function startLine(m) {
 92 +		var start = parseLocal(m.start);
 93 +		return dateLabel(start) + " at " + clockTime(start, true);
 94 +	}
 95 +
 96 +	// Detail bullets, one per <li>. Same-day events get separate date + time bullets.
 97 +	// Upcoming multiday events collapse into one "date time - date time" bullet so each
 98 +	// endpoint carries its day; once past, they split into a date-range line + time-range
 99 +	// line ("(Ended)" trailing), letting each time pair with its date. The year shows on
100 +	// the end date only, or on both dates when the range spans two years.
101 +	// Blank fields are dropped. address is intentionally not shown (kept in data for
102 +	// later). withRelativeDay appends "(Today!)" / "(Ended)" - panel only.
103 +	function detailItems(m, withRelativeDay) {
104 +		var start = parseLocal(m.start), end = parseLocal(m.end);
105 +		var single = sameDay(start, end);
106 +		var ended = withRelativeDay && hasPassed(m) ? " (Ended)" : "";
107 +		var startLabel = dateLabel(start) + (withRelativeDay ? relativeDayTag(m, start) : "");
108 +		var when;
109 +		if (single) {
110 +			when = [startLabel + ended, timeRange(start, end)];
111 +		} else if (ended) {
112 +			var sameYear = start.getFullYear() === end.getFullYear();
113 +			var startYear = sameYear ? "" : ", " + start.getFullYear();
114 +			when = [dateLabel(start) + startYear + " - " + dateLabel(end) + ", " + end.getFullYear() + ended, timeRange(start, end, true)];
115 +		} else {
116 +			when = [startLabel + " " + clockTime(start, true) + " - " + dateLabel(end) + " " + clockTime(end, true)];
117 +		}
118 +		return when.concat([m.venue, m.price, m.ageRange]).filter(function (s) {
119 +			return s && s.trim();
120 +		});
121 +	}
122 +
123 +	function easeInOut(t) {
124 +		return t < 0.5 ? 0.5 * Math.sqrt(2 * t) : 1 - 0.5 * Math.sqrt(2 * (1 - t));
125 +	}
126 +
127 +	// ROT13 to keep RSVP addresses out of page source; each event's `rsvp` is stored rotated.
128 +	function rot(str) {
129 +		var input = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
130 +		var output = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm";
131 +		return str.split("").map(function (x) {
132 +			var i = input.indexOf(x);
133 +			return i > -1 ? output[i] : x;
134 +		}).join("");
135 +	}
136 +
137 +	// RSVP mailto: decode the rotated address, prepopulate subject + body. The event URL
138 +	// goes on its own trailing line so mail clients auto-link it.
139 +	function mailtoHref(m) {
140 +		var url = eventUrl(m);
141 +		var subject = "RSVP for " + m.title + " 🐞";
142 +		var body = "I'm confirming my RSVP for the following event:\n\n" +
143 +			m.title + "\n" + startLine(m) + "\n\n" +
144 +			url + "\n\n· · · · ·\n\nAny notes or comments? Add them here:\n\n\n";
145 +		return "mailto:" + rot(m.rsvp) +
146 +			"?subject=" + encodeURIComponent(subject) +
147 +			"&body=" + encodeURIComponent(body);
148 +	}
149 +
150 +	function pad2(n) { return n < 10 ? "0" + n : "" + n; }
151 +
152 +	// "20260610T183000" - floating local time (no zone). JSON times are venue wall-clock,
153 +	// so calendars show them in the viewer's local time.
154 +	function icsLocal(d) {
155 +		return d.getFullYear() + pad2(d.getMonth() + 1) + pad2(d.getDate()) +
156 +			"T" + pad2(d.getHours()) + pad2(d.getMinutes()) + "00";
157 +	}
158 +
159 +	// "20260610T183000Z" - UTC stamp for DTSTAMP/UID.
160 +	function icsStamp(d) {
161 +		return d.getUTCFullYear() + pad2(d.getUTCMonth() + 1) + pad2(d.getUTCDate()) +
162 +			"T" + pad2(d.getUTCHours()) + pad2(d.getUTCMinutes()) + pad2(d.getUTCSeconds()) + "Z";
163 +	}
164 +
165 +	// Escape a TEXT value per RFC 5545.
166 +	function icsEscape(str) {
167 +		return String(str)
168 +			.replace(/\\/g, "\\\\")
169 +			.replace(/;/g, "\\;")
170 +			.replace(/,/g, "\\,")
171 +			.replace(/\r?\n/g, "\\n");
172 +	}
173 +
174 +	// Fold a content line to <=75 octets per RFC 5545. Counts UTF-8 bytes (not chars)
175 +	// and never splits a multibyte sequence across the fold.
176 +	function icsFold(line) {
177 +		var out = "", run = 0, limit = 75;
178 +		for (var i = 0; i < line.length; i++) {
179 +			var ch = line[i];
180 +			var bytes = encodeURIComponent(ch).replace(/%[0-9A-F]{2}/gi, "x").length;
181 +			if (run + bytes > limit) { out += "\r\n "; run = 1; } // leading space counts as 1
182 +			out += ch;
183 +			run += bytes;
184 +		}
185 +		return out;
186 +	}
187 +
188 +	// Single-event iCalendar (.ics). Floating local time; venue+address as LOCATION.
189 +	function icsContent(m) {
190 +		var start = parseLocal(m.start), end = parseLocal(m.end);
191 +		var location = m.address ? m.venue + ", " + m.address : m.venue;
192 +		var uid = icsLocal(start) + "-" + Math.abs(hashStr(m.title)) + "@events";
193 +		var lines = [
194 +			"BEGIN:VCALENDAR",
195 +			"VERSION:2.0",
196 +			"PRODID:-//events//EN",
197 +			"CALSCALE:GREGORIAN",
198 +			"BEGIN:VEVENT",
199 +			"UID:" + uid,
200 +			"DTSTAMP:" + icsStamp(new Date()),
201 +			"DTSTART:" + icsLocal(start),
202 +			"DTEND:" + icsLocal(end),
203 +			"SUMMARY:" + icsEscape(m.title),
204 +			"LOCATION:" + icsEscape(location),
205 +			"URL:" + icsEscape(eventUrl(m)),
206 +			"DESCRIPTION:" + icsEscape(m.description),
207 +			"END:VEVENT",
208 +			"END:VCALENDAR"
209 +		];
210 +		return lines.map(icsFold).join("\r\n") + "\r\n";
211 +	}
212 +
213 +	// Stable hash so the same event yields the same UID across downloads (calendars
214 +	// dedupe/update on UID).
215 +	function hashStr(str) {
216 +		var h = 0;
217 +		for (var i = 0; i < str.length; i++) { h = (h * 31 + str.charCodeAt(i)) | 0; }
218 +		return h;
219 +	}
220 +
221 +	// Deep-link id: title slug + date, so two same-named events on different days don't
222 +	// collide, e.g. "front-porch-jam-session-2026-06-12".
223 +	function eventSlug(m) {
224 +		var title = m.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "event";
225 +		return title + "-" + m.start.slice(0, 10);
226 +	}
227 +
228 +	// Deep link to an event: current page sans query/hash + ?<slug>.
229 +	function eventUrl(m) {
230 +		var base = window.location.href.split(/[?#]/)[0];
231 +		return base + "?" + encodeURIComponent(eventSlug(m));
232 +	}
233 +
234 +	function isApplePlatform() {
235 +		return /Macintosh|iPhone|iPad|iPod/.test(navigator.userAgent);
236 +	}
237 +
238 +	// Driving-directions deep link (destination only, so the app fills in the origin).
239 +	// Prefer the street address; fall back to lat,long when blank.
240 +	// maps:// opens Apple Maps directly; Google Maps everywhere else.
241 +	function directionsHref(m) {
242 +		var dest = encodeURIComponent(m.address || (m.latitude + "," + m.longitude));
243 +		return isApplePlatform()
244 +			? "maps://?daddr=" + dest + "&dirflg=d"
245 +			: "https://www.google.com/maps/dir/?api=1&destination=" + dest + "&travelmode=driving";
246 +	}
247 +
248 +	// Resolve the current URL's ?<slug> back to its event, or null. Inverse of eventUrl.
249 +	function eventFromUrl(events) {
250 +		var slug = decodeURIComponent(window.location.search.slice(1));
251 +		if (!slug) { return null; }
252 +		for (var i = 0; i < events.length; i++) {
253 +			if (eventSlug(events[i]) === slug) { return events[i]; }
254 +		}
255 +		return null;
256 +	}
257 +
258 +	function icsFilename(m) {
259 +		return eventSlug(m) + ".ics";
260 +	}
261 +
262 +	window.fmt = {
263 +		MONTH_ABBR: MONTH_ABBR,
264 +		parseLocal: parseLocal,
265 +		hasPassed: hasPassed,
266 +		upcomingEvents: upcomingEvents,
267 +		pastEvents: pastEvents,
268 +		shortDate: shortDate,
269 +		dateLine: dateLine,
270 +		startLine: startLine,
271 +		detailItems: detailItems,
272 +		easeInOut: easeInOut,
273 +		mailtoHref: mailtoHref,
274 +		icsContent: icsContent,
275 +		icsFilename: icsFilename,
276 +		eventSlug: eventSlug,
277 +		eventUrl: eventUrl,
278 +		directionsHref: directionsHref,
279 +		eventFromUrl: eventFromUrl
280 +	};
281 +})();
resources/icon.png
Binary files differ.
resources/leaflet/LICENSE-leaflet
 1 +BSD 2-Clause License
 2 +
 3 +Copyright (c) 2010-2023, Volodymyr Agafonkin
 4 +Copyright (c) 2010-2011, CloudMade
 5 +All rights reserved.
 6 +
 7 +Redistribution and use in source and binary forms, with or without
 8 +modification, are permitted provided that the following conditions are met:
 9 +
10 +1. Redistributions of source code must retain the above copyright notice, this
11 +   list of conditions and the following disclaimer.
12 +
13 +2. Redistributions in binary form must reproduce the above copyright notice,
14 +   this list of conditions and the following disclaimer in the documentation
15 +   and/or other materials provided with the distribution.
16 +
17 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21 +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23 +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24 +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25 +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
resources/leaflet/LICENSE-nogap
1 +"THE BEER-WARE LICENSE":
2 +<ivan@sanchezortega.es> wrote this file. As long as you retain this notice you
3 +can do whatever you want with this stuff. If we meet some day, and you think
4 +this stuff is worth it, you can buy me a beer in return.
resources/leaflet/leaflet-nogap.js
  1 +// @class TileLayer
  2 +
  3 +L.TileLayer.mergeOptions({
  4 +	// @option keepBuffer
  5 +	// The amount of tiles outside the visible map area to be kept in the stitched
  6 +	// `TileLayer`.
  7 +
  8 +	// @option dumpToCanvas: Boolean = true
  9 +	// Whether to dump loaded tiles to a `<canvas>` to prevent some rendering
 10 +	// artifacts. (Disabled by default in IE)
 11 +	dumpToCanvas: L.Browser.canvas && !L.Browser.ie,
 12 +});
 13 +
 14 +L.TileLayer.include({
 15 +	// Backing-store density. The canvas is sized in CSS px (so Leaflet's positioning is
 16 +	// unchanged) but its backing store is ratio× larger and the context is pre-scaled by
 17 +	// ratio, so high-res source tiles keep their detail instead of being flattened to 1x.
 18 +	// Full DPR for sharpness; the crash from large canvases is bounded by a small keepBuffer
 19 +	// (see the tileLayer options in script.js) rather than by capping ratio.
 20 +	_canvasRatio: function() {
 21 +		return Math.max(1, Math.round(window.devicePixelRatio || 1));
 22 +	},
 23 +
 24 +	_onUpdateLevel: function(z) {
 25 +		if (this.options.dumpToCanvas) {
 26 +			// base Leaflet calls this with only z; mirror its own zIndex math off _tileZoom
 27 +			// (the original plugin read an undefined `zoom` arg here, yielding NaN). Correct
 28 +			// z-index stacking lets a new level's canvas sit over the old until it's pruned,
 29 +			// so no opacity tricks are needed to avoid a pop on zoom.
 30 +			var curZoom = this._tileZoom;
 31 +			this._levels[z].canvas.style.zIndex =
 32 +				this.options.maxZoom - Math.abs(curZoom - z);
 33 +		}
 34 +	},
 35 +
 36 +	_onRemoveLevel: function(z) {
 37 +		if (this.options.dumpToCanvas) {
 38 +			L.DomUtil.remove(this._levels[z].canvas);
 39 +		}
 40 +	},
 41 +
 42 +	_onCreateLevel: function(level) {
 43 +		if (this.options.dumpToCanvas) {
 44 +			level.canvas = L.DomUtil.create(
 45 +				"canvas",
 46 +				"leaflet-tile-container leaflet-zoom-animated",
 47 +				this._container
 48 +			);
 49 +			level.canvas.style.pointerEvents = "none";
 50 +			level.ctx = level.canvas.getContext("2d");
 51 +			this._resetCanvasSize(level);
 52 +		}
 53 +	},
 54 +
 55 +	_removeTile: function(key) {
 56 +		if (this.options.dumpToCanvas) {
 57 +			var tile = this._tiles[key];
 58 +			var level = this._levels[tile.coords.z];
 59 +			var tileSize = this.getTileSize();
 60 +
 61 +			if (level) {
 62 +				// Where in the canvas should this tile go?
 63 +				var offset = L.point(tile.coords.x, tile.coords.y)
 64 +					.subtract(level.canvasRange.min)
 65 +					.scaleBy(this.getTileSize());
 66 +
 67 +				level.ctx.clearRect(offset.x, offset.y, tileSize.x, tileSize.y);
 68 +			}
 69 +		}
 70 +
 71 +		L.GridLayer.prototype._removeTile.call(this, key);
 72 +	},
 73 +
 74 +	_resetCanvasSize: function(level) {
 75 +		var buff = this.options.keepBuffer,
 76 +			pixelBounds = this._getTiledPixelBounds(this._map.getCenter()),
 77 +			tileRange = this._pxBoundsToTileRange(pixelBounds),
 78 +			tileSize = this.getTileSize();
 79 +
 80 +		tileRange.min = tileRange.min.subtract([buff, buff]); // This adds the no-prune buffer
 81 +		tileRange.max = tileRange.max.add([buff + 1, buff + 1]);
 82 +
 83 +		var pixelRange = L.bounds(
 84 +				tileRange.min.scaleBy(tileSize),
 85 +				tileRange.max.add([1, 1]).scaleBy(tileSize) // This prevents an off-by-one when checking if tiles are inside
 86 +			),
 87 +			mustRepositionCanvas = false,
 88 +			neededSize = pixelRange.max.subtract(pixelRange.min);
 89 +
 90 +		// Resize the canvas, if needed, and only to make it bigger. CSS size stays in CSS px;
 91 +		// backing store is ratio× larger so high-DPR tiles keep their detail (see _canvasRatio).
 92 +		var ratio = this._canvasRatio();
 93 +		if (
 94 +			neededSize.x > level.canvas.width / ratio ||
 95 +			neededSize.y > level.canvas.height / ratio
 96 +		) {
 97 +			// Resizing canvases erases the currently drawn content, I'm afraid.
 98 +			// To keep it, dump the pixels to another canvas, then display it on
 99 +			// top. This could be done with getImageData/putImageData, but that
100 +			// would break for tainted canvases (in non-CORS tilesets). Copy at
101 +			// backing-store resolution (1:1, no ctx scale) to avoid resampling.
102 +			var oldSize = { x: level.canvas.width, y: level.canvas.height };
103 +			// console.info('Resizing canvas from ', oldSize, 'to ', neededSize);
104 +
105 +			var tmpCanvas = L.DomUtil.create("canvas");
106 +			tmpCanvas.width = oldSize.x;
107 +			tmpCanvas.height = oldSize.y;
108 +			tmpCanvas.getContext("2d").drawImage(level.canvas, 0, 0);
109 +			// var data = level.ctx.getImageData(0, 0, oldSize.x, oldSize.y);
110 +
111 +			level.canvas.style.width = neededSize.x + "px";
112 +			level.canvas.style.height = neededSize.y + "px";
113 +			level.canvas.width = neededSize.x * ratio;
114 +			level.canvas.height = neededSize.y * ratio;
115 +			// drawImage at native backing-store coords, so reset any prior ctx scale first
116 +			level.ctx.setTransform(1, 0, 0, 1, 0, 0);
117 +			level.ctx.drawImage(tmpCanvas, 0, 0);
118 +			// every other draw in this layer works in CSS px; pre-scale so it lands on the
119 +			// ratio× backing store
120 +			level.ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
121 +			// level.ctx.putImageData(data, 0, 0, 0, 0, oldSize.x, oldSize.y);
122 +		}
123 +
124 +		// Translate the canvas contents if it's moved around. This is a whole-canvas
125 +		// backing-store copy, so run it at native scale (offset in backing-store px) and
126 +		// restore the CSS-px ctx scale afterward.
127 +		if (level.canvasRange) {
128 +			var offset = level.canvasRange.min
129 +				.subtract(tileRange.min)
130 +				.scaleBy(this.getTileSize())
131 +				.multiplyBy(ratio);
132 +
133 +			// 			console.info('Offsetting by ', offset);
134 +
135 +			level.ctx.setTransform(1, 0, 0, 1, 0, 0);
136 +
137 +			if (!L.Browser.safari) {
138 +				// By default, canvases copy things "on top of" existing pixels, but we want
139 +				// this to *replace* the existing pixels when doing a drawImage() call.
140 +				// This will also clear the sides, so no clearRect() calls are needed to make room
141 +				// for the new tiles.
142 +				level.ctx.globalCompositeOperation = "copy";
143 +				level.ctx.drawImage(level.canvas, offset.x, offset.y);
144 +				level.ctx.globalCompositeOperation = "source-over";
145 +			} else {
146 +				// Safari clears the canvas when copying from itself :-(
147 +				if (!this._tmpCanvas) {
148 +					var t = (this._tmpCanvas = L.DomUtil.create("canvas"));
149 +					t.width = level.canvas.width;
150 +					t.height = level.canvas.height;
151 +					this._tmpContext = t.getContext("2d");
152 +				}
153 +				this._tmpContext.clearRect(
154 +					0,
155 +					0,
156 +					level.canvas.width,
157 +					level.canvas.height
158 +				);
159 +				this._tmpContext.drawImage(level.canvas, 0, 0);
160 +				level.ctx.clearRect(0, 0, level.canvas.width, level.canvas.height);
161 +				level.ctx.drawImage(this._tmpCanvas, offset.x, offset.y);
162 +			}
163 +
164 +			level.ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
165 +			mustRepositionCanvas = true; // Wait until new props are set
166 +		}
167 +
168 +		level.canvasRange = tileRange;
169 +		level.canvasPxRange = pixelRange;
170 +		level.canvasOrigin = pixelRange.min;
171 +
172 +		// console.log('Canvas tile range: ', level, tileRange.min, tileRange.max );
173 +		// console.log('Canvas pixel range: ', pixelRange.min, pixelRange.max );
174 +		// console.log('Level origin: ', level.origin );
175 +
176 +		if (mustRepositionCanvas) {
177 +			this._setCanvasZoomTransform(
178 +				level,
179 +				this._map.getCenter(),
180 +				this._map.getZoom()
181 +			);
182 +		}
183 +	},
184 +
185 +	/// set transform/position of canvas, in addition to the transform/position of the individual tile container
186 +	_setZoomTransform: function(level, center, zoom) {
187 +		L.GridLayer.prototype._setZoomTransform.call(this, level, center, zoom);
188 +		if (this.options.dumpToCanvas) {
189 +			this._setCanvasZoomTransform(level, center, zoom);
190 +		}
191 +	},
192 +
193 +	// This will get called twice:
194 +	// * From _setZoomTransform
195 +	// * When the canvas has shifted due to a new tile being loaded
196 +	_setCanvasZoomTransform: function(level, center, zoom) {
197 +		// console.log('_setCanvasZoomTransform', level, center, zoom);
198 +		if (!level.canvasOrigin) {
199 +			return;
200 +		}
201 +		var scale = this._map.getZoomScale(zoom, level.zoom),
202 +			translate = level.canvasOrigin
203 +				.multiplyBy(scale)
204 +				.subtract(this._map._getNewPixelOrigin(center, zoom))
205 +				.round();
206 +
207 +		if (L.Browser.any3d) {
208 +			L.DomUtil.setTransform(level.canvas, translate, scale);
209 +		} else {
210 +			L.DomUtil.setPosition(level.canvas, translate);
211 +		}
212 +	},
213 +
214 +	_onOpaqueTile: function(tile) {
215 +		if (!this.options.dumpToCanvas) {
216 +			return;
217 +		}
218 +
219 +		// Guard against an NS_ERROR_NOT_AVAILABLE (or similar) exception
220 +		// when a non-image-tile has been loaded (e.g. a WMS error).
221 +		// Checking for tile.el.complete is not enough, as it has been
222 +		// already marked as loaded and ready somehow.
223 +		try {
224 +			this.dumpPixels(tile.coords, tile.el);
225 +		} catch (ex) {
226 +			return this.fire("tileerror", {
227 +				error: "Could not copy tile pixels: " + ex,
228 +				tile: tile,
229 +				coods: tile.coords,
230 +			});
231 +		}
232 +
233 +		// If dumping the pixels was successful, then hide the tile.
234 +		// Do not remove the tile itself, as it is needed to check if the whole
235 +		// level (and its canvas) should be removed (via level.el.children.length)
236 +		tile.el.style.display = "none";
237 +	},
238 +
239 +	// @section Extension methods
240 +	// @uninheritable
241 +
242 +	// @method dumpPixels(coords: Object, imageSource: CanvasImageSource): this
243 +	// Dumps pixels from the given `CanvasImageSource` into the layer, into
244 +	// the space for the tile represented by the `coords` tile coordinates (an object
245 +	// like `{x: Number, y: Number, z: Number}`; the image source must have the
246 +	// same size as the `tileSize` option for the layer. Has no effect if `dumpToCanvas`
247 +	// is `false`.
248 +	dumpPixels: function(coords, imageSource) {
249 +		var level = this._levels[coords.z],
250 +			tileSize = this.getTileSize();
251 +
252 +		if (!level.canvasRange || !this.options.dumpToCanvas) {
253 +			return;
254 +		}
255 +
256 +		// Check if the tile is inside the currently visible map bounds
257 +		// There is a possible race condition when tiles are loaded after they
258 +		// have been panned outside of the map.
259 +		if (!level.canvasRange.contains(coords)) {
260 +			this._resetCanvasSize(level);
261 +		}
262 +
263 +		// Where in the canvas should this tile go?
264 +		var offset = L.point(coords.x, coords.y)
265 +			.subtract(level.canvasRange.min)
266 +			.scaleBy(this.getTileSize());
267 +
268 +		level.ctx.drawImage(imageSource, offset.x, offset.y, tileSize.x, tileSize.y);
269 +
270 +		// TODO: Clear the pixels of other levels' canvases where they overlap
271 +		// this newly dumped tile.
272 +		return this;
273 +	},
274 +});
resources/leaflet/leaflet.css
  1 +/* required styles */
  2 +
  3 +.leaflet-pane,
  4 +.leaflet-tile,
  5 +.leaflet-marker-icon,
  6 +.leaflet-marker-shadow,
  7 +.leaflet-tile-container,
  8 +.leaflet-pane > svg,
  9 +.leaflet-pane > canvas,
 10 +.leaflet-zoom-box,
 11 +.leaflet-image-layer,
 12 +.leaflet-layer {
 13 +	position: absolute;
 14 +	left: 0;
 15 +	top: 0;
 16 +	}
 17 +.leaflet-container {
 18 +	overflow: hidden;
 19 +	}
 20 +.leaflet-tile,
 21 +.leaflet-marker-icon,
 22 +.leaflet-marker-shadow {
 23 +	-webkit-user-select: none;
 24 +	   -moz-user-select: none;
 25 +	        user-select: none;
 26 +	  -webkit-user-drag: none;
 27 +	}
 28 +/* Prevents IE11 from highlighting tiles in blue */
 29 +.leaflet-tile::selection {
 30 +	background: transparent;
 31 +}
 32 +/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
 33 +.leaflet-safari .leaflet-tile {
 34 +	image-rendering: -webkit-optimize-contrast;
 35 +	}
 36 +/* hack that prevents hw layers "stretching" when loading new tiles */
 37 +.leaflet-safari .leaflet-tile-container {
 38 +	width: 1600px;
 39 +	height: 1600px;
 40 +	-webkit-transform-origin: 0 0;
 41 +	}
 42 +.leaflet-marker-icon,
 43 +.leaflet-marker-shadow {
 44 +	display: block;
 45 +	}
 46 +/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
 47 +/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
 48 +.leaflet-container .leaflet-overlay-pane svg {
 49 +	max-width: none !important;
 50 +	max-height: none !important;
 51 +	}
 52 +.leaflet-container .leaflet-marker-pane img,
 53 +.leaflet-container .leaflet-shadow-pane img,
 54 +.leaflet-container .leaflet-tile-pane img,
 55 +.leaflet-container img.leaflet-image-layer,
 56 +.leaflet-container .leaflet-tile {
 57 +	max-width: none !important;
 58 +	max-height: none !important;
 59 +	width: auto;
 60 +	padding: 0;
 61 +	}
 62 +
 63 +.leaflet-container img.leaflet-tile {
 64 +	/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
 65 +	mix-blend-mode: plus-lighter;
 66 +}
 67 +
 68 +.leaflet-container.leaflet-touch-zoom {
 69 +	-ms-touch-action: pan-x pan-y;
 70 +	touch-action: pan-x pan-y;
 71 +	}
 72 +.leaflet-container.leaflet-touch-drag {
 73 +	-ms-touch-action: pinch-zoom;
 74 +	/* Fallback for FF which doesn't support pinch-zoom */
 75 +	touch-action: none;
 76 +	touch-action: pinch-zoom;
 77 +}
 78 +.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
 79 +	-ms-touch-action: none;
 80 +	touch-action: none;
 81 +}
 82 +.leaflet-container {
 83 +	-webkit-tap-highlight-color: transparent;
 84 +}
 85 +.leaflet-container a {
 86 +	-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
 87 +}
 88 +.leaflet-tile {
 89 +	filter: inherit;
 90 +	visibility: hidden;
 91 +	}
 92 +.leaflet-tile-loaded {
 93 +	visibility: inherit;
 94 +	}
 95 +.leaflet-zoom-box {
 96 +	width: 0;
 97 +	height: 0;
 98 +	-moz-box-sizing: border-box;
 99 +	     box-sizing: border-box;
100 +	z-index: 800;
101 +	}
102 +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
103 +.leaflet-overlay-pane svg {
104 +	-moz-user-select: none;
105 +	}
106 +
107 +.leaflet-pane         { z-index: 400; }
108 +
109 +.leaflet-tile-pane    { z-index: 200; }
110 +.leaflet-overlay-pane { z-index: 400; }
111 +.leaflet-shadow-pane  { z-index: 500; }
112 +.leaflet-marker-pane  { z-index: 600; }
113 +.leaflet-tooltip-pane   { z-index: 650; }
114 +.leaflet-popup-pane   { z-index: 700; }
115 +
116 +.leaflet-map-pane canvas { z-index: 100; }
117 +.leaflet-map-pane svg    { z-index: 200; }
118 +
119 +.leaflet-vml-shape {
120 +	width: 1px;
121 +	height: 1px;
122 +	}
123 +.lvml {
124 +	behavior: url(#default#VML);
125 +	display: inline-block;
126 +	position: absolute;
127 +	}
128 +
129 +
130 +/* control positioning */
131 +
132 +.leaflet-control {
133 +	position: relative;
134 +	z-index: 800;
135 +	pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
136 +	pointer-events: auto;
137 +	}
138 +.leaflet-top,
139 +.leaflet-bottom {
140 +	position: absolute;
141 +	z-index: 1000;
142 +	pointer-events: none;
143 +	}
144 +.leaflet-top {
145 +	top: 0;
146 +	}
147 +.leaflet-right {
148 +	right: 0;
149 +	}
150 +.leaflet-bottom {
151 +	bottom: 0;
152 +	}
153 +.leaflet-left {
154 +	left: 0;
155 +	}
156 +.leaflet-control {
157 +	float: left;
158 +	clear: both;
159 +	}
160 +.leaflet-right .leaflet-control {
161 +	float: right;
162 +	}
163 +.leaflet-top .leaflet-control {
164 +	margin-top: 10px;
165 +	}
166 +.leaflet-bottom .leaflet-control {
167 +	margin-bottom: 10px;
168 +	}
169 +.leaflet-left .leaflet-control {
170 +	margin-left: 10px;
171 +	}
172 +.leaflet-right .leaflet-control {
173 +	margin-right: 10px;
174 +	}
175 +
176 +
177 +/* zoom and fade animations */
178 +
179 +.leaflet-fade-anim .leaflet-popup {
180 +	opacity: 0;
181 +	-webkit-transition: opacity 0.2s linear;
182 +	   -moz-transition: opacity 0.2s linear;
183 +	        transition: opacity 0.2s linear;
184 +	}
185 +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
186 +	opacity: 1;
187 +	}
188 +.leaflet-zoom-animated {
189 +	-webkit-transform-origin: 0 0;
190 +	    -ms-transform-origin: 0 0;
191 +	        transform-origin: 0 0;
192 +	}
193 +svg.leaflet-zoom-animated {
194 +	will-change: transform;
195 +}
196 +
197 +.leaflet-zoom-anim .leaflet-zoom-animated {
198 +	-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
199 +	   -moz-transition:    -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
200 +	        transition:         transform 0.25s cubic-bezier(0,0,0.25,1);
201 +	}
202 +.leaflet-zoom-anim .leaflet-tile,
203 +.leaflet-pan-anim .leaflet-tile {
204 +	-webkit-transition: none;
205 +	   -moz-transition: none;
206 +	        transition: none;
207 +	}
208 +
209 +.leaflet-zoom-anim .leaflet-zoom-hide {
210 +	visibility: hidden;
211 +	}
212 +
213 +
214 +/* cursors */
215 +
216 +.leaflet-interactive {
217 +	cursor: pointer;
218 +	}
219 +.leaflet-grab {
220 +	cursor: -webkit-grab;
221 +	cursor:    -moz-grab;
222 +	cursor:         grab;
223 +	}
224 +.leaflet-crosshair,
225 +.leaflet-crosshair .leaflet-interactive {
226 +	cursor: crosshair;
227 +	}
228 +.leaflet-popup-pane,
229 +.leaflet-control {
230 +	cursor: auto;
231 +	}
232 +.leaflet-dragging .leaflet-grab,
233 +.leaflet-dragging .leaflet-grab .leaflet-interactive,
234 +.leaflet-dragging .leaflet-marker-draggable {
235 +	cursor: move;
236 +	cursor: -webkit-grabbing;
237 +	cursor:    -moz-grabbing;
238 +	cursor:         grabbing;
239 +	}
240 +
241 +/* marker & overlays interactivity */
242 +.leaflet-marker-icon,
243 +.leaflet-marker-shadow,
244 +.leaflet-image-layer,
245 +.leaflet-pane > svg path,
246 +.leaflet-tile-container {
247 +	pointer-events: none;
248 +	}
249 +
250 +.leaflet-marker-icon.leaflet-interactive,
251 +.leaflet-image-layer.leaflet-interactive,
252 +.leaflet-pane > svg path.leaflet-interactive,
253 +svg.leaflet-image-layer.leaflet-interactive path {
254 +	pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
255 +	pointer-events: auto;
256 +	}
257 +
258 +/* visual tweaks */
259 +
260 +.leaflet-container {
261 +	background: #ddd;
262 +	outline-offset: 1px;
263 +	}
264 +.leaflet-container a {
265 +	color: #0078A8;
266 +	}
267 +.leaflet-zoom-box {
268 +	border: 2px dotted #38f;
269 +	background: rgba(255,255,255,0.5);
270 +	}
271 +
272 +
273 +/* general typography */
274 +.leaflet-container {
275 +	font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
276 +	font-size: 12px;
277 +	font-size: 0.75rem;
278 +	line-height: 1.5;
279 +	}
280 +
281 +
282 +/* general toolbar styles */
283 +
284 +.leaflet-bar {
285 +	box-shadow: 0 1px 5px rgba(0,0,0,0.65);
286 +	border-radius: 4px;
287 +	}
288 +.leaflet-bar a {
289 +	background-color: #fff;
290 +	border-bottom: 1px solid #ccc;
291 +	width: 26px;
292 +	height: 26px;
293 +	line-height: 26px;
294 +	display: block;
295 +	text-align: center;
296 +	text-decoration: none;
297 +	color: black;
298 +	}
299 +.leaflet-bar a,
300 +.leaflet-control-layers-toggle {
301 +	background-position: 50% 50%;
302 +	background-repeat: no-repeat;
303 +	display: block;
304 +	}
305 +.leaflet-bar a:hover,
306 +.leaflet-bar a:focus {
307 +	background-color: #f4f4f4;
308 +	}
309 +.leaflet-bar a:first-child {
310 +	border-top-left-radius: 4px;
311 +	border-top-right-radius: 4px;
312 +	}
313 +.leaflet-bar a:last-child {
314 +	border-bottom-left-radius: 4px;
315 +	border-bottom-right-radius: 4px;
316 +	border-bottom: none;
317 +	}
318 +.leaflet-bar a.leaflet-disabled {
319 +	cursor: default;
320 +	background-color: #f4f4f4;
321 +	color: #bbb;
322 +	}
323 +
324 +.leaflet-touch .leaflet-bar a {
325 +	width: 30px;
326 +	height: 30px;
327 +	line-height: 30px;
328 +	}
329 +.leaflet-touch .leaflet-bar a:first-child {
330 +	border-top-left-radius: 2px;
331 +	border-top-right-radius: 2px;
332 +	}
333 +.leaflet-touch .leaflet-bar a:last-child {
334 +	border-bottom-left-radius: 2px;
335 +	border-bottom-right-radius: 2px;
336 +	}
337 +
338 +/* zoom control */
339 +
340 +.leaflet-control-zoom-in,
341 +.leaflet-control-zoom-out {
342 +	font: bold 18px 'Lucida Console', Monaco, monospace;
343 +	text-indent: 1px;
344 +	}
345 +
346 +.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out  {
347 +	font-size: 22px;
348 +	}
349 +
350 +
351 +/* layers control */
352 +
353 +.leaflet-control-layers {
354 +	box-shadow: 0 1px 5px rgba(0,0,0,0.4);
355 +	background: #fff;
356 +	border-radius: 5px;
357 +	}
358 +.leaflet-control-layers-toggle {
359 +	/* background-image (images/layers.png) dropped: no layers control, images vendored out */
360 +	width: 36px;
361 +	height: 36px;
362 +	}
363 +.leaflet-retina .leaflet-control-layers-toggle {
364 +	/* background-image (images/layers-2x.png) dropped: see above */
365 +	background-size: 26px 26px;
366 +	}
367 +.leaflet-touch .leaflet-control-layers-toggle {
368 +	width: 44px;
369 +	height: 44px;
370 +	}
371 +.leaflet-control-layers .leaflet-control-layers-list,
372 +.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
373 +	display: none;
374 +	}
375 +.leaflet-control-layers-expanded .leaflet-control-layers-list {
376 +	display: block;
377 +	position: relative;
378 +	}
379 +.leaflet-control-layers-expanded {
380 +	padding: 6px 10px 6px 6px;
381 +	color: #333;
382 +	background: #fff;
383 +	}
384 +.leaflet-control-layers-scrollbar {
385 +	overflow-y: scroll;
386 +	overflow-x: hidden;
387 +	padding-right: 5px;
388 +	}
389 +.leaflet-control-layers-selector {
390 +	margin-top: 2px;
391 +	position: relative;
392 +	top: 1px;
393 +	}
394 +.leaflet-control-layers label {
395 +	display: block;
396 +	font-size: 13px;
397 +	font-size: 1.08333em;
398 +	}
399 +.leaflet-control-layers-separator {
400 +	height: 0;
401 +	border-top: 1px solid #ddd;
402 +	margin: 5px -10px 5px -6px;
403 +	}
404 +
405 +/* Default icon URLs */
406 +.leaflet-default-icon-path {
407 +	/* background-image (images/marker-icon.png) dropped: app uses L.divIcon only, images vendored out */
408 +	}
409 +
410 +
411 +/* attribution and scale controls */
412 +
413 +.leaflet-container .leaflet-control-attribution {
414 +	background: #fff;
415 +	background: rgba(255, 255, 255, 0.8);
416 +	margin: 0;
417 +	}
418 +.leaflet-control-attribution,
419 +.leaflet-control-scale-line {
420 +	padding: 0 5px;
421 +	color: #333;
422 +	line-height: 1.4;
423 +	}
424 +.leaflet-control-attribution a {
425 +	text-decoration: none;
426 +	}
427 +.leaflet-control-attribution a:hover,
428 +.leaflet-control-attribution a:focus {
429 +	text-decoration: underline;
430 +	}
431 +.leaflet-attribution-flag {
432 +	display: inline !important;
433 +	vertical-align: baseline !important;
434 +	width: 1em;
435 +	height: 0.6669em;
436 +	}
437 +.leaflet-left .leaflet-control-scale {
438 +	margin-left: 5px;
439 +	}
440 +.leaflet-bottom .leaflet-control-scale {
441 +	margin-bottom: 5px;
442 +	}
443 +.leaflet-control-scale-line {
444 +	border: 2px solid #777;
445 +	border-top: none;
446 +	line-height: 1.1;
447 +	padding: 2px 5px 1px;
448 +	white-space: nowrap;
449 +	-moz-box-sizing: border-box;
450 +	     box-sizing: border-box;
451 +	background: rgba(255, 255, 255, 0.8);
452 +	text-shadow: 1px 1px #fff;
453 +	}
454 +.leaflet-control-scale-line:not(:first-child) {
455 +	border-top: 2px solid #777;
456 +	border-bottom: none;
457 +	margin-top: -2px;
458 +	}
459 +.leaflet-control-scale-line:not(:first-child):not(:last-child) {
460 +	border-bottom: 2px solid #777;
461 +	}
462 +
463 +.leaflet-touch .leaflet-control-attribution,
464 +.leaflet-touch .leaflet-control-layers,
465 +.leaflet-touch .leaflet-bar {
466 +	box-shadow: none;
467 +	}
468 +.leaflet-touch .leaflet-control-layers,
469 +.leaflet-touch .leaflet-bar {
470 +	border: 2px solid rgba(0,0,0,0.2);
471 +	background-clip: padding-box;
472 +	}
473 +
474 +
475 +/* popup */
476 +
477 +.leaflet-popup {
478 +	position: absolute;
479 +	text-align: center;
480 +	margin-bottom: 20px;
481 +	}
482 +.leaflet-popup-content-wrapper {
483 +	padding: 1px;
484 +	text-align: left;
485 +	border-radius: 12px;
486 +	}
487 +.leaflet-popup-content {
488 +	margin: 13px 24px 13px 20px;
489 +	line-height: 1.3;
490 +	font-size: 13px;
491 +	font-size: 1.08333em;
492 +	min-height: 1px;
493 +	}
494 +.leaflet-popup-content p {
495 +	margin: 17px 0;
496 +	margin: 1.3em 0;
497 +	}
498 +.leaflet-popup-tip-container {
499 +	width: 40px;
500 +	height: 20px;
501 +	position: absolute;
502 +	left: 50%;
503 +	margin-top: -1px;
504 +	margin-left: -20px;
505 +	overflow: hidden;
506 +	pointer-events: none;
507 +	}
508 +.leaflet-popup-tip {
509 +	width: 17px;
510 +	height: 17px;
511 +	padding: 1px;
512 +
513 +	margin: -10px auto 0;
514 +	pointer-events: auto;
515 +
516 +	-webkit-transform: rotate(45deg);
517 +	   -moz-transform: rotate(45deg);
518 +	    -ms-transform: rotate(45deg);
519 +	        transform: rotate(45deg);
520 +	}
521 +.leaflet-popup-content-wrapper,
522 +.leaflet-popup-tip {
523 +	background: white;
524 +	color: #333;
525 +	box-shadow: 0 3px 14px rgba(0,0,0,0.4);
526 +	}
527 +.leaflet-container a.leaflet-popup-close-button {
528 +	position: absolute;
529 +	top: 0;
530 +	right: 0;
531 +	border: none;
532 +	text-align: center;
533 +	width: 24px;
534 +	height: 24px;
535 +	font: 16px/24px Tahoma, Verdana, sans-serif;
536 +	color: #757575;
537 +	text-decoration: none;
538 +	background: transparent;
539 +	}
540 +.leaflet-container a.leaflet-popup-close-button:hover,
541 +.leaflet-container a.leaflet-popup-close-button:focus {
542 +	color: #585858;
543 +	}
544 +.leaflet-popup-scrolled {
545 +	overflow: auto;
546 +	}
547 +
548 +.leaflet-oldie .leaflet-popup-content-wrapper {
549 +	-ms-zoom: 1;
550 +	}
551 +.leaflet-oldie .leaflet-popup-tip {
552 +	width: 24px;
553 +	margin: 0 auto;
554 +
555 +	-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
556 +	filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
557 +	}
558 +
559 +.leaflet-oldie .leaflet-control-zoom,
560 +.leaflet-oldie .leaflet-control-layers,
561 +.leaflet-oldie .leaflet-popup-content-wrapper,
562 +.leaflet-oldie .leaflet-popup-tip {
563 +	border: 1px solid #999;
564 +	}
565 +
566 +
567 +/* div icon */
568 +
569 +.leaflet-div-icon {
570 +	background: #fff;
571 +	border: 1px solid #666;
572 +	}
573 +
574 +
575 +/* Tooltip */
576 +/* Base styles for the element that has a tooltip */
577 +.leaflet-tooltip {
578 +	position: absolute;
579 +	padding: 6px;
580 +	background-color: #fff;
581 +	border: 1px solid #fff;
582 +	border-radius: 3px;
583 +	color: #222;
584 +	white-space: nowrap;
585 +	-webkit-user-select: none;
586 +	-moz-user-select: none;
587 +	-ms-user-select: none;
588 +	user-select: none;
589 +	pointer-events: none;
590 +	box-shadow: 0 1px 3px rgba(0,0,0,0.4);
591 +	}
592 +.leaflet-tooltip.leaflet-interactive {
593 +	cursor: pointer;
594 +	pointer-events: auto;
595 +	}
596 +.leaflet-tooltip-top:before,
597 +.leaflet-tooltip-bottom:before,
598 +.leaflet-tooltip-left:before,
599 +.leaflet-tooltip-right:before {
600 +	position: absolute;
601 +	pointer-events: none;
602 +	border: 6px solid transparent;
603 +	background: transparent;
604 +	content: "";
605 +	}
606 +
607 +/* Directions */
608 +
609 +.leaflet-tooltip-bottom {
610 +	margin-top: 6px;
611 +}
612 +.leaflet-tooltip-top {
613 +	margin-top: -6px;
614 +}
615 +.leaflet-tooltip-bottom:before,
616 +.leaflet-tooltip-top:before {
617 +	left: 50%;
618 +	margin-left: -6px;
619 +	}
620 +.leaflet-tooltip-top:before {
621 +	bottom: 0;
622 +	margin-bottom: -12px;
623 +	border-top-color: #fff;
624 +	}
625 +.leaflet-tooltip-bottom:before {
626 +	top: 0;
627 +	margin-top: -12px;
628 +	margin-left: -6px;
629 +	border-bottom-color: #fff;
630 +	}
631 +.leaflet-tooltip-left {
632 +	margin-left: -6px;
633 +}
634 +.leaflet-tooltip-right {
635 +	margin-left: 6px;
636 +}
637 +.leaflet-tooltip-left:before,
638 +.leaflet-tooltip-right:before {
639 +	top: 50%;
640 +	margin-top: -6px;
641 +	}
642 +.leaflet-tooltip-left:before {
643 +	right: 0;
644 +	margin-right: -12px;
645 +	border-left-color: #fff;
646 +	}
647 +.leaflet-tooltip-right:before {
648 +	left: 0;
649 +	margin-left: -12px;
650 +	border-right-color: #fff;
651 +	}
652 +
653 +/* Printing */
654 +
655 +@media print {
656 +	/* Prevent printers from removing background-images of controls. */
657 +	.leaflet-control {
658 +		-webkit-print-color-adjust: exact;
659 +		print-color-adjust: exact;
660 +		}
661 +	}
resources/leaflet/leaflet.js
1 +/* @preserve
2 + * Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com
3 + * (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade
4 + */
5 +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n<o;n++)for(e in i=arguments[n])t[e]=i[e];return t}var R=Object.create||function(t){return N.prototype=t,new N};function N(){}function a(t,e){var i,n=Array.prototype.slice;return t.bind?t.bind.apply(t,n.call(arguments,1)):(i=n.call(arguments,2),function(){return t.apply(e,i.length?i.concat(n.call(arguments)):arguments)})}var D=0;function h(t){return"_leaflet_id"in t||(t._leaflet_id=++D),t._leaflet_id}function j(t,e,i){var n,o,s=function(){n=!1,o&&(r.apply(i,o),o=!1)},r=function(){n?o=arguments:(t.apply(i,arguments),setTimeout(s,e),n=!0)};return r}function H(t,e,i){var n=e[1],e=e[0],o=n-e;return t===n&&i?t:((t-e)%o+o)%o+e}function u(){return!1}function i(t,e){return!1===e?t:(e=Math.pow(10,void 0===e?6:e),Math.round(t*e)/e)}function W(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}function F(t){return W(t).split(/\s+/)}function c(t,e){for(var i in Object.prototype.hasOwnProperty.call(t,"options")||(t.options=t.options?R(t.options):{}),e)t.options[i]=e[i];return t.options}function U(t,e,i){var n,o=[];for(n in t)o.push(encodeURIComponent(i?n.toUpperCase():n)+"="+encodeURIComponent(t[n]));return(e&&-1!==e.indexOf("?")?"&":"?")+o.join("&")}var V=/\{ *([\w_ -]+) *\}/g;function q(t,i){return t.replace(V,function(t,e){e=i[e];if(void 0===e)throw new Error("No value provided for variable "+t);return e="function"==typeof e?e(i):e})}var d=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)};function G(t,e){for(var i=0;i<t.length;i++)if(t[i]===e)return i;return-1}var K="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";function Y(t){return window["webkit"+t]||window["moz"+t]||window["ms"+t]}var X=0;function J(t){var e=+new Date,i=Math.max(0,16-(e-X));return X=e+i,window.setTimeout(t,i)}var $=window.requestAnimationFrame||Y("RequestAnimationFrame")||J,Q=window.cancelAnimationFrame||Y("CancelAnimationFrame")||Y("CancelRequestAnimationFrame")||function(t){window.clearTimeout(t)};function x(t,e,i){if(!i||$!==J)return $.call(window,a(t,e));t.call(e)}function r(t){t&&Q.call(window,t)}var tt={__proto__:null,extend:l,create:R,bind:a,get lastId(){return D},stamp:h,throttle:j,wrapNum:H,falseFn:u,formatNum:i,trim:W,splitWords:F,setOptions:c,getParamString:U,template:q,isArray:d,indexOf:G,emptyImageUrl:K,requestFn:$,cancelFn:Q,requestAnimFrame:x,cancelAnimFrame:r};function et(){}et.extend=function(t){function e(){c(this),this.initialize&&this.initialize.apply(this,arguments),this.callInitHooks()}var i,n=e.__super__=this.prototype,o=R(n);for(i in(o.constructor=e).prototype=o,this)Object.prototype.hasOwnProperty.call(this,i)&&"prototype"!==i&&"__super__"!==i&&(e[i]=this[i]);if(t.statics&&l(e,t.statics),t.includes){var s=t.includes;if("undefined"!=typeof L&&L&&L.Mixin){s=d(s)?s:[s];for(var r=0;r<s.length;r++)s[r]===L.Mixin.Events&&console.warn("Deprecated include of L.Mixin.Events: this property will be removed in future releases, please inherit from L.Evented instead.",(new Error).stack)}l.apply(null,[o].concat(t.includes))}return l(o,t),delete o.statics,delete o.includes,o.options&&(o.options=n.options?R(n.options):{},l(o.options,t.options)),o._initHooks=[],o.callInitHooks=function(){if(!this._initHooksCalled){n.callInitHooks&&n.callInitHooks.call(this),this._initHooksCalled=!0;for(var t=0,e=o._initHooks.length;t<e;t++)o._initHooks[t].call(this)}},e},et.include=function(t){var e=this.prototype.options;return l(this.prototype,t),t.options&&(this.prototype.options=e,this.mergeOptions(t.options)),this},et.mergeOptions=function(t){return l(this.prototype.options,t),this},et.addInitHook=function(t){var e=Array.prototype.slice.call(arguments,1),i="function"==typeof t?t:function(){this[t].apply(this,e)};return this.prototype._initHooks=this.prototype._initHooks||[],this.prototype._initHooks.push(i),this};var e={on:function(t,e,i){if("object"==typeof t)for(var n in t)this._on(n,t[n],e);else for(var o=0,s=(t=F(t)).length;o<s;o++)this._on(t[o],e,i);return this},off:function(t,e,i){if(arguments.length)if("object"==typeof t)for(var n in t)this._off(n,t[n],e);else{t=F(t);for(var o=1===arguments.length,s=0,r=t.length;s<r;s++)o?this._off(t[s]):this._off(t[s],e,i)}else delete this._events;return this},_on:function(t,e,i,n){"function"!=typeof e?console.warn("wrong listener type: "+typeof e):!1===this._listens(t,e,i)&&(e={fn:e,ctx:i=i===this?void 0:i},n&&(e.once=!0),this._events=this._events||{},this._events[t]=this._events[t]||[],this._events[t].push(e))},_off:function(t,e,i){var n,o,s;if(this._events&&(n=this._events[t]))if(1===arguments.length){if(this._firingCount)for(o=0,s=n.length;o<s;o++)n[o].fn=u;delete this._events[t]}else"function"!=typeof e?console.warn("wrong listener type: "+typeof e):!1!==(e=this._listens(t,e,i))&&(i=n[e],this._firingCount&&(i.fn=u,this._events[t]=n=n.slice()),n.splice(e,1))},fire:function(t,e,i){if(this.listens(t,i)){var n=l({},e,{type:t,target:this,sourceTarget:e&&e.sourceTarget||this});if(this._events){var o=this._events[t];if(o){this._firingCount=this._firingCount+1||1;for(var s=0,r=o.length;s<r;s++){var a=o[s],h=a.fn;a.once&&this.off(t,h,a.ctx),h.call(a.ctx||this,n)}this._firingCount--}}i&&this._propagateEvent(n)}return this},listens:function(t,e,i,n){"string"!=typeof t&&console.warn('"string" type argument expected');var o=e,s=("function"!=typeof e&&(n=!!e,i=o=void 0),this._events&&this._events[t]);if(s&&s.length&&!1!==this._listens(t,o,i))return!0;if(n)for(var r in this._eventParents)if(this._eventParents[r].listens(t,e,i,n))return!0;return!1},_listens:function(t,e,i){if(this._events){var n=this._events[t]||[];if(!e)return!!n.length;i===this&&(i=void 0);for(var o=0,s=n.length;o<s;o++)if(n[o].fn===e&&n[o].ctx===i)return o}return!1},once:function(t,e,i){if("object"==typeof t)for(var n in t)this._on(n,t[n],e,!0);else for(var o=0,s=(t=F(t)).length;o<s;o++)this._on(t[o],e,i,!0);return this},addEventParent:function(t){return this._eventParents=this._eventParents||{},this._eventParents[h(t)]=t,this},removeEventParent:function(t){return this._eventParents&&delete this._eventParents[h(t)],this},_propagateEvent:function(t){for(var e in this._eventParents)this._eventParents[e].fire(t.type,l({layer:t.target,propagatedFrom:t.target},t),!0)}},it=(e.addEventListener=e.on,e.removeEventListener=e.clearAllEventListeners=e.off,e.addOneTimeEventListener=e.once,e.fireEvent=e.fire,e.hasEventListeners=e.listens,et.extend(e));function p(t,e,i){this.x=i?Math.round(t):t,this.y=i?Math.round(e):e}var nt=Math.trunc||function(t){return 0<t?Math.floor(t):Math.ceil(t)};function m(t,e,i){return t instanceof p?t:d(t)?new p(t[0],t[1]):null==t?t:"object"==typeof t&&"x"in t&&"y"in t?new p(t.x,t.y):new p(t,e,i)}function f(t,e){if(t)for(var i=e?[t,e]:t,n=0,o=i.length;n<o;n++)this.extend(i[n])}function _(t,e){return!t||t instanceof f?t:new f(t,e)}function s(t,e){if(t)for(var i=e?[t,e]:t,n=0,o=i.length;n<o;n++)this.extend(i[n])}function g(t,e){return t instanceof s?t:new s(t,e)}function v(t,e,i){if(isNaN(t)||isNaN(e))throw new Error("Invalid LatLng object: ("+t+", "+e+")");this.lat=+t,this.lng=+e,void 0!==i&&(this.alt=+i)}function w(t,e,i){return t instanceof v?t:d(t)&&"object"!=typeof t[0]?3===t.length?new v(t[0],t[1],t[2]):2===t.length?new v(t[0],t[1]):null:null==t?t:"object"==typeof t&&"lat"in t?new v(t.lat,"lng"in t?t.lng:t.lon,t.alt):void 0===e?null:new v(t,e,i)}p.prototype={clone:function(){return new p(this.x,this.y)},add:function(t){return this.clone()._add(m(t))},_add:function(t){return this.x+=t.x,this.y+=t.y,this},subtract:function(t){return this.clone()._subtract(m(t))},_subtract:function(t){return this.x-=t.x,this.y-=t.y,this},divideBy:function(t){return this.clone()._divideBy(t)},_divideBy:function(t){return this.x/=t,this.y/=t,this},multiplyBy:function(t){return this.clone()._multiplyBy(t)},_multiplyBy:function(t){return this.x*=t,this.y*=t,this},scaleBy:function(t){return new p(this.x*t.x,this.y*t.y)},unscaleBy:function(t){return new p(this.x/t.x,this.y/t.y)},round:function(){return this.clone()._round()},_round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this},floor:function(){return this.clone()._floor()},_floor:function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this},ceil:function(){return this.clone()._ceil()},_ceil:function(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this},trunc:function(){return this.clone()._trunc()},_trunc:function(){return this.x=nt(this.x),this.y=nt(this.y),this},distanceTo:function(t){var e=(t=m(t)).x-this.x,t=t.y-this.y;return Math.sqrt(e*e+t*t)},equals:function(t){return(t=m(t)).x===this.x&&t.y===this.y},contains:function(t){return t=m(t),Math.abs(t.x)<=Math.abs(this.x)&&Math.abs(t.y)<=Math.abs(this.y)},toString:function(){return"Point("+i(this.x)+", "+i(this.y)+")"}},f.prototype={extend:function(t){var e,i;if(t){if(t instanceof p||"number"==typeof t[0]||"x"in t)e=i=m(t);else if(e=(t=_(t)).min,i=t.max,!e||!i)return this;this.min||this.max?(this.min.x=Math.min(e.x,this.min.x),this.max.x=Math.max(i.x,this.max.x),this.min.y=Math.min(e.y,this.min.y),this.max.y=Math.max(i.y,this.max.y)):(this.min=e.clone(),this.max=i.clone())}return this},getCenter:function(t){return m((this.min.x+this.max.x)/2,(this.min.y+this.max.y)/2,t)},getBottomLeft:function(){return m(this.min.x,this.max.y)},getTopRight:function(){return m(this.max.x,this.min.y)},getTopLeft:function(){return this.min},getBottomRight:function(){return this.max},getSize:function(){return this.max.subtract(this.min)},contains:function(t){var e,i;return(t=("number"==typeof t[0]||t instanceof p?m:_)(t))instanceof f?(e=t.min,i=t.max):e=i=t,e.x>=this.min.x&&i.x<=this.max.x&&e.y>=this.min.y&&i.y<=this.max.y},intersects:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>=e.x&&n.x<=i.x,t=t.y>=e.y&&n.y<=i.y;return o&&t},overlaps:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>e.x&&n.x<i.x,t=t.y>e.y&&n.y<i.y;return o&&t},isValid:function(){return!(!this.min||!this.max)},pad:function(t){var e=this.min,i=this.max,n=Math.abs(e.x-i.x)*t,t=Math.abs(e.y-i.y)*t;return _(m(e.x-n,e.y-t),m(i.x+n,i.y+t))},equals:function(t){return!!t&&(t=_(t),this.min.equals(t.getTopLeft())&&this.max.equals(t.getBottomRight()))}},s.prototype={extend:function(t){var e,i,n=this._southWest,o=this._northEast;if(t instanceof v)i=e=t;else{if(!(t instanceof s))return t?this.extend(w(t)||g(t)):this;if(e=t._southWest,i=t._northEast,!e||!i)return this}return n||o?(n.lat=Math.min(e.lat,n.lat),n.lng=Math.min(e.lng,n.lng),o.lat=Math.max(i.lat,o.lat),o.lng=Math.max(i.lng,o.lng)):(this._southWest=new v(e.lat,e.lng),this._northEast=new v(i.lat,i.lng)),this},pad:function(t){var e=this._southWest,i=this._northEast,n=Math.abs(e.lat-i.lat)*t,t=Math.abs(e.lng-i.lng)*t;return new s(new v(e.lat-n,e.lng-t),new v(i.lat+n,i.lng+t))},getCenter:function(){return new v((this._southWest.lat+this._northEast.lat)/2,(this._southWest.lng+this._northEast.lng)/2)},getSouthWest:function(){return this._southWest},getNorthEast:function(){return this._northEast},getNorthWest:function(){return new v(this.getNorth(),this.getWest())},getSouthEast:function(){return new v(this.getSouth(),this.getEast())},getWest:function(){return this._southWest.lng},getSouth:function(){return this._southWest.lat},getEast:function(){return this._northEast.lng},getNorth:function(){return this._northEast.lat},contains:function(t){t=("number"==typeof t[0]||t instanceof v||"lat"in t?w:g)(t);var e,i,n=this._southWest,o=this._northEast;return t instanceof s?(e=t.getSouthWest(),i=t.getNorthEast()):e=i=t,e.lat>=n.lat&&i.lat<=o.lat&&e.lng>=n.lng&&i.lng<=o.lng},intersects:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>=e.lat&&n.lat<=i.lat,t=t.lng>=e.lng&&n.lng<=i.lng;return o&&t},overlaps:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>e.lat&&n.lat<i.lat,t=t.lng>e.lng&&n.lng<i.lng;return o&&t},toBBoxString:function(){return[this.getWest(),this.getSouth(),this.getEast(),this.getNorth()].join(",")},equals:function(t,e){return!!t&&(t=g(t),this._southWest.equals(t.getSouthWest(),e)&&this._northEast.equals(t.getNorthEast(),e))},isValid:function(){return!(!this._southWest||!this._northEast)}};var ot={latLngToPoint:function(t,e){t=this.projection.project(t),e=this.scale(e);return this.transformation._transform(t,e)},pointToLatLng:function(t,e){e=this.scale(e),t=this.transformation.untransform(t,e);return this.projection.unproject(t)},project:function(t){return this.projection.project(t)},unproject:function(t){return this.projection.unproject(t)},scale:function(t){return 256*Math.pow(2,t)},zoom:function(t){return Math.log(t/256)/Math.LN2},getProjectedBounds:function(t){var e;return this.infinite?null:(e=this.projection.bounds,t=this.scale(t),new f(this.transformation.transform(e.min,t),this.transformation.transform(e.max,t)))},infinite:!(v.prototype={equals:function(t,e){return!!t&&(t=w(t),Math.max(Math.abs(this.lat-t.lat),Math.abs(this.lng-t.lng))<=(void 0===e?1e-9:e))},toString:function(t){return"LatLng("+i(this.lat,t)+", "+i(this.lng,t)+")"},distanceTo:function(t){return st.distance(this,w(t))},wrap:function(){return st.wrapLatLng(this)},toBounds:function(t){var t=180*t/40075017,e=t/Math.cos(Math.PI/180*this.lat);return g([this.lat-t,this.lng-e],[this.lat+t,this.lng+e])},clone:function(){return new v(this.lat,this.lng,this.alt)}}),wrapLatLng:function(t){var e=this.wrapLng?H(t.lng,this.wrapLng,!0):t.lng;return new v(this.wrapLat?H(t.lat,this.wrapLat,!0):t.lat,e,t.alt)},wrapLatLngBounds:function(t){var e=t.getCenter(),i=this.wrapLatLng(e),n=e.lat-i.lat,e=e.lng-i.lng;return 0==n&&0==e?t:(i=t.getSouthWest(),t=t.getNorthEast(),new s(new v(i.lat-n,i.lng-e),new v(t.lat-n,t.lng-e)))}},st=l({},ot,{wrapLng:[-180,180],R:6371e3,distance:function(t,e){var i=Math.PI/180,n=t.lat*i,o=e.lat*i,s=Math.sin((e.lat-t.lat)*i/2),e=Math.sin((e.lng-t.lng)*i/2),t=s*s+Math.cos(n)*Math.cos(o)*e*e,i=2*Math.atan2(Math.sqrt(t),Math.sqrt(1-t));return this.R*i}}),rt=6378137,rt={R:rt,MAX_LATITUDE:85.0511287798,project:function(t){var e=Math.PI/180,i=this.MAX_LATITUDE,i=Math.max(Math.min(i,t.lat),-i),i=Math.sin(i*e);return new p(this.R*t.lng*e,this.R*Math.log((1+i)/(1-i))/2)},unproject:function(t){var e=180/Math.PI;return new v((2*Math.atan(Math.exp(t.y/this.R))-Math.PI/2)*e,t.x*e/this.R)},bounds:new f([-(rt=rt*Math.PI),-rt],[rt,rt])};function at(t,e,i,n){d(t)?(this._a=t[0],this._b=t[1],this._c=t[2],this._d=t[3]):(this._a=t,this._b=e,this._c=i,this._d=n)}function ht(t,e,i,n){return new at(t,e,i,n)}at.prototype={transform:function(t,e){return this._transform(t.clone(),e)},_transform:function(t,e){return t.x=(e=e||1)*(this._a*t.x+this._b),t.y=e*(this._c*t.y+this._d),t},untransform:function(t,e){return new p((t.x/(e=e||1)-this._b)/this._a,(t.y/e-this._d)/this._c)}};var lt=l({},st,{code:"EPSG:3857",projection:rt,transformation:ht(lt=.5/(Math.PI*rt.R),.5,-lt,.5)}),ut=l({},lt,{code:"EPSG:900913"});function ct(t){return document.createElementNS("http://www.w3.org/2000/svg",t)}function dt(t,e){for(var i,n,o,s,r="",a=0,h=t.length;a<h;a++){for(i=0,n=(o=t[a]).length;i<n;i++)r+=(i?"L":"M")+(s=o[i]).x+" "+s.y;r+=e?b.svg?"z":"x":""}return r||"M0 0"}var _t=document.documentElement.style,pt="ActiveXObject"in window,mt=pt&&!document.addEventListener,n="msLaunchUri"in navigator&&!("documentMode"in document),ft=y("webkit"),gt=y("android"),vt=y("android 2")||y("android 3"),yt=parseInt(/WebKit\/([0-9]+)|$/.exec(navigator.userAgent)[1],10),yt=gt&&y("Google")&&yt<537&&!("AudioNode"in window),xt=!!window.opera,wt=!n&&y("chrome"),bt=y("gecko")&&!ft&&!xt&&!pt,Pt=!wt&&y("safari"),Lt=y("phantom"),o="OTransition"in _t,Tt=0===navigator.platform.indexOf("Win"),Mt=pt&&"transition"in _t,zt="WebKitCSSMatrix"in window&&"m11"in new window.WebKitCSSMatrix&&!vt,_t="MozPerspective"in _t,Ct=!window.L_DISABLE_3D&&(Mt||zt||_t)&&!o&&!Lt,Zt="undefined"!=typeof orientation||y("mobile"),St=Zt&&ft,Et=Zt&&zt,kt=!window.PointerEvent&&window.MSPointerEvent,Ot=!(!window.PointerEvent&&!kt),At="ontouchstart"in window||!!window.TouchEvent,Bt=!window.L_NO_TOUCH&&(At||Ot),It=Zt&&xt,Rt=Zt&&bt,Nt=1<(window.devicePixelRatio||window.screen.deviceXDPI/window.screen.logicalXDPI),Dt=function(){var t=!1;try{var e=Object.defineProperty({},"passive",{get:function(){t=!0}});window.addEventListener("testPassiveEventSupport",u,e),window.removeEventListener("testPassiveEventSupport",u,e)}catch(t){}return t}(),jt=!!document.createElement("canvas").getContext,Ht=!(!document.createElementNS||!ct("svg").createSVGRect),Wt=!!Ht&&((Wt=document.createElement("div")).innerHTML="<svg/>","http://www.w3.org/2000/svg"===(Wt.firstChild&&Wt.firstChild.namespaceURI));function y(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var b={ie:pt,ielt9:mt,edge:n,webkit:ft,android:gt,android23:vt,androidStock:yt,opera:xt,chrome:wt,gecko:bt,safari:Pt,phantom:Lt,opera12:o,win:Tt,ie3d:Mt,webkit3d:zt,gecko3d:_t,any3d:Ct,mobile:Zt,mobileWebkit:St,mobileWebkit3d:Et,msPointer:kt,pointer:Ot,touch:Bt,touchNative:At,mobileOpera:It,mobileGecko:Rt,retina:Nt,passiveEvents:Dt,canvas:jt,svg:Ht,vml:!Ht&&function(){try{var t=document.createElement("div"),e=(t.innerHTML='<v:shape adj="1"/>',t.firstChild);return e.style.behavior="url(#default#VML)",e&&"object"==typeof e.adj}catch(t){return!1}}(),inlineSvg:Wt,mac:0===navigator.platform.indexOf("Mac"),linux:0===navigator.platform.indexOf("Linux")},Ft=b.msPointer?"MSPointerDown":"pointerdown",Ut=b.msPointer?"MSPointerMove":"pointermove",Vt=b.msPointer?"MSPointerUp":"pointerup",qt=b.msPointer?"MSPointerCancel":"pointercancel",Gt={touchstart:Ft,touchmove:Ut,touchend:Vt,touchcancel:qt},Kt={touchstart:function(t,e){e.MSPOINTER_TYPE_TOUCH&&e.pointerType===e.MSPOINTER_TYPE_TOUCH&&O(e);ee(t,e)},touchmove:ee,touchend:ee,touchcancel:ee},Yt={},Xt=!1;function Jt(t,e,i){return"touchstart"!==e||Xt||(document.addEventListener(Ft,$t,!0),document.addEventListener(Ut,Qt,!0),document.addEventListener(Vt,te,!0),document.addEventListener(qt,te,!0),Xt=!0),Kt[e]?(i=Kt[e].bind(this,i),t.addEventListener(Gt[e],i,!1),i):(console.warn("wrong event specified:",e),u)}function $t(t){Yt[t.pointerId]=t}function Qt(t){Yt[t.pointerId]&&(Yt[t.pointerId]=t)}function te(t){delete Yt[t.pointerId]}function ee(t,e){if(e.pointerType!==(e.MSPOINTER_TYPE_MOUSE||"mouse")){for(var i in e.touches=[],Yt)e.touches.push(Yt[i]);e.changedTouches=[e],t(e)}}var ie=200;function ne(t,i){t.addEventListener("dblclick",i);var n,o=0;function e(t){var e;1!==t.detail?n=t.detail:"mouse"===t.pointerType||t.sourceCapabilities&&!t.sourceCapabilities.firesTouchEvents||((e=Ne(t)).some(function(t){return t instanceof HTMLLabelElement&&t.attributes.for})&&!e.some(function(t){return t instanceof HTMLInputElement||t instanceof HTMLSelectElement})||((e=Date.now())-o<=ie?2===++n&&i(function(t){var e,i,n={};for(i in t)e=t[i],n[i]=e&&e.bind?e.bind(t):e;return(t=n).type="dblclick",n.detail=2,n.isTrusted=!1,n._simulated=!0,n}(t)):n=1,o=e))}return t.addEventListener("click",e),{dblclick:i,simDblclick:e}}var oe,se,re,ae,he,le,ue=we(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),ce=we(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===ce||"OTransition"===ce?ce+"End":"transitionend";function _e(t){return"string"==typeof t?document.getElementById(t):t}function pe(t,e){var i=t.style[e]||t.currentStyle&&t.currentStyle[e];return"auto"===(i=i&&"auto"!==i||!document.defaultView?i:(t=document.defaultView.getComputedStyle(t,null))?t[e]:null)?null:i}function P(t,e,i){t=document.createElement(t);return t.className=e||"",i&&i.appendChild(t),t}function T(t){var e=t.parentNode;e&&e.removeChild(t)}function me(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function fe(t){var e=t.parentNode;e&&e.lastChild!==t&&e.appendChild(t)}function ge(t){var e=t.parentNode;e&&e.firstChild!==t&&e.insertBefore(t,e.firstChild)}function ve(t,e){return void 0!==t.classList?t.classList.contains(e):0<(t=xe(t)).length&&new RegExp("(^|\\s)"+e+"(\\s|$)").test(t)}function M(t,e){var i;if(void 0!==t.classList)for(var n=F(e),o=0,s=n.length;o<s;o++)t.classList.add(n[o]);else ve(t,e)||ye(t,((i=xe(t))?i+" ":"")+e)}function z(t,e){void 0!==t.classList?t.classList.remove(e):ye(t,W((" "+xe(t)+" ").replace(" "+e+" "," ")))}function ye(t,e){void 0===t.className.baseVal?t.className=e:t.className.baseVal=e}function xe(t){return void 0===(t=t.correspondingElement?t.correspondingElement:t).className.baseVal?t.className:t.className.baseVal}function C(t,e){if("opacity"in t.style)t.style.opacity=e;else if("filter"in t.style){var i=!1,n="DXImageTransform.Microsoft.Alpha";try{i=t.filters.item(n)}catch(t){if(1===e)return}e=Math.round(100*e),i?(i.Enabled=100!==e,i.Opacity=e):t.style.filter+=" progid:"+n+"(opacity="+e+")"}}function we(t){for(var e=document.documentElement.style,i=0;i<t.length;i++)if(t[i]in e)return t[i];return!1}function be(t,e,i){e=e||new p(0,0);t.style[ue]=(b.ie3d?"translate("+e.x+"px,"+e.y+"px)":"translate3d("+e.x+"px,"+e.y+"px,0)")+(i?" scale("+i+")":"")}function Z(t,e){t._leaflet_pos=e,b.any3d?be(t,e):(t.style.left=e.x+"px",t.style.top=e.y+"px")}function Pe(t){return t._leaflet_pos||new p(0,0)}function Le(){S(window,"dragstart",O)}function Te(){k(window,"dragstart",O)}function Me(t){for(;-1===t.tabIndex;)t=t.parentNode;t.style&&(ze(),le=(he=t).style.outlineStyle,t.style.outlineStyle="none",S(window,"keydown",ze))}function ze(){he&&(he.style.outlineStyle=le,le=he=void 0,k(window,"keydown",ze))}function Ce(t){for(;!((t=t.parentNode).offsetWidth&&t.offsetHeight||t===document.body););return t}function Ze(t){var e=t.getBoundingClientRect();return{x:e.width/t.offsetWidth||1,y:e.height/t.offsetHeight||1,boundingClientRect:e}}ae="onselectstart"in document?(re=function(){S(window,"selectstart",O)},function(){k(window,"selectstart",O)}):(se=we(["userSelect","WebkitUserSelect","OUserSelect","MozUserSelect","msUserSelect"]),re=function(){var t;se&&(t=document.documentElement.style,oe=t[se],t[se]="none")},function(){se&&(document.documentElement.style[se]=oe,oe=void 0)});pt={__proto__:null,TRANSFORM:ue,TRANSITION:ce,TRANSITION_END:de,get:_e,getStyle:pe,create:P,remove:T,empty:me,toFront:fe,toBack:ge,hasClass:ve,addClass:M,removeClass:z,setClass:ye,getClass:xe,setOpacity:C,testProp:we,setTransform:be,setPosition:Z,getPosition:Pe,get disableTextSelection(){return re},get enableTextSelection(){return ae},disableImageDrag:Le,enableImageDrag:Te,preventOutline:Me,restoreOutline:ze,getSizedParentNode:Ce,getScale:Ze};function S(t,e,i,n){if(e&&"object"==typeof e)for(var o in e)ke(t,o,e[o],i);else for(var s=0,r=(e=F(e)).length;s<r;s++)ke(t,e[s],i,n);return this}var E="_leaflet_events";function k(t,e,i,n){if(1===arguments.length)Se(t),delete t[E];else if(e&&"object"==typeof e)for(var o in e)Oe(t,o,e[o],i);else if(e=F(e),2===arguments.length)Se(t,function(t){return-1!==G(e,t)});else for(var s=0,r=e.length;s<r;s++)Oe(t,e[s],i,n);return this}function Se(t,e){for(var i in t[E]){var n=i.split(/\d/)[0];e&&!e(n)||Oe(t,n,null,null,i)}}var Ee={mouseenter:"mouseover",mouseleave:"mouseout",wheel:!("onwheel"in window)&&"mousewheel"};function ke(e,t,i,n){var o,s,r=t+h(i)+(n?"_"+h(n):"");e[E]&&e[E][r]||(s=o=function(t){return i.call(n||e,t||window.event)},!b.touchNative&&b.pointer&&0===t.indexOf("touch")?o=Jt(e,t,o):b.touch&&"dblclick"===t?o=ne(e,o):"addEventListener"in e?"touchstart"===t||"touchmove"===t||"wheel"===t||"mousewheel"===t?e.addEventListener(Ee[t]||t,o,!!b.passiveEvents&&{passive:!1}):"mouseenter"===t||"mouseleave"===t?e.addEventListener(Ee[t],o=function(t){t=t||window.event,We(e,t)&&s(t)},!1):e.addEventListener(t,s,!1):e.attachEvent("on"+t,o),e[E]=e[E]||{},e[E][r]=o)}function Oe(t,e,i,n,o){o=o||e+h(i)+(n?"_"+h(n):"");var s,r,i=t[E]&&t[E][o];i&&(!b.touchNative&&b.pointer&&0===e.indexOf("touch")?(n=t,r=i,Gt[s=e]?n.removeEventListener(Gt[s],r,!1):console.warn("wrong event specified:",s)):b.touch&&"dblclick"===e?(n=i,(r=t).removeEventListener("dblclick",n.dblclick),r.removeEventListener("click",n.simDblclick)):"removeEventListener"in t?t.removeEventListener(Ee[e]||e,i,!1):t.detachEvent("on"+e,i),t[E][o]=null)}function Ae(t){return t.stopPropagation?t.stopPropagation():t.originalEvent?t.originalEvent._stopped=!0:t.cancelBubble=!0,this}function Be(t){return ke(t,"wheel",Ae),this}function Ie(t){return S(t,"mousedown touchstart dblclick contextmenu",Ae),t._leaflet_disable_click=!0,this}function O(t){return t.preventDefault?t.preventDefault():t.returnValue=!1,this}function Re(t){return O(t),Ae(t),this}function Ne(t){if(t.composedPath)return t.composedPath();for(var e=[],i=t.target;i;)e.push(i),i=i.parentNode;return e}function De(t,e){var i,n;return e?(n=(i=Ze(e)).boundingClientRect,new p((t.clientX-n.left)/i.x-e.clientLeft,(t.clientY-n.top)/i.y-e.clientTop)):new p(t.clientX,t.clientY)}var je=b.linux&&b.chrome?window.devicePixelRatio:b.mac?3*window.devicePixelRatio:0<window.devicePixelRatio?2*window.devicePixelRatio:1;function He(t){return b.edge?t.wheelDeltaY/2:t.deltaY&&0===t.deltaMode?-t.deltaY/je:t.deltaY&&1===t.deltaMode?20*-t.deltaY:t.deltaY&&2===t.deltaMode?60*-t.deltaY:t.deltaX||t.deltaZ?0:t.wheelDelta?(t.wheelDeltaY||t.wheelDelta)/2:t.detail&&Math.abs(t.detail)<32765?20*-t.detail:t.detail?t.detail/-32765*60:0}function We(t,e){var i=e.relatedTarget;if(!i)return!0;try{for(;i&&i!==t;)i=i.parentNode}catch(t){return!1}return i!==t}var mt={__proto__:null,on:S,off:k,stopPropagation:Ae,disableScrollPropagation:Be,disableClickPropagation:Ie,preventDefault:O,stop:Re,getPropagationPath:Ne,getMousePosition:De,getWheelDelta:He,isExternalTarget:We,addListener:S,removeListener:k},Fe=it.extend({run:function(t,e,i,n){this.stop(),this._el=t,this._inProgress=!0,this._duration=i||.25,this._easeOutPower=1/Math.max(n||.5,.2),this._startPos=Pe(t),this._offset=e.subtract(this._startPos),this._startTime=+new Date,this.fire("start"),this._animate()},stop:function(){this._inProgress&&(this._step(!0),this._complete())},_animate:function(){this._animId=x(this._animate,this),this._step()},_step:function(t){var e=+new Date-this._startTime,i=1e3*this._duration;e<i?this._runFrame(this._easeOut(e/i),t):(this._runFrame(1),this._complete())},_runFrame:function(t,e){t=this._startPos.add(this._offset.multiplyBy(t));e&&t._round(),Z(this._el,t),this.fire("step")},_complete:function(){r(this._animId),this._inProgress=!1,this.fire("end")},_easeOut:function(t){return 1-Math.pow(1-t,this._easeOutPower)}}),A=it.extend({options:{crs:lt,center:void 0,zoom:void 0,minZoom:void 0,maxZoom:void 0,layers:[],maxBounds:void 0,renderer:void 0,zoomAnimation:!0,zoomAnimationThreshold:4,fadeAnimation:!0,markerZoomAnimation:!0,transform3DLimit:8388608,zoomSnap:1,zoomDelta:1,trackResize:!0},initialize:function(t,e){e=c(this,e),this._handlers=[],this._layers={},this._zoomBoundLayers={},this._sizeChanged=!0,this._initContainer(t),this._initLayout(),this._onResize=a(this._onResize,this),this._initEvents(),e.maxBounds&&this.setMaxBounds(e.maxBounds),void 0!==e.zoom&&(this._zoom=this._limitZoom(e.zoom)),e.center&&void 0!==e.zoom&&this.setView(w(e.center),e.zoom,{reset:!0}),this.callInitHooks(),this._zoomAnimated=ce&&b.any3d&&!b.mobileOpera&&this.options.zoomAnimation,this._zoomAnimated&&(this._createAnimProxy(),S(this._proxy,de,this._catchTransitionEnd,this)),this._addLayers(this.options.layers)},setView:function(t,e,i){if((e=void 0===e?this._zoom:this._limitZoom(e),t=this._limitCenter(w(t),e,this.options.maxBounds),i=i||{},this._stop(),this._loaded&&!i.reset&&!0!==i)&&(void 0!==i.animate&&(i.zoom=l({animate:i.animate},i.zoom),i.pan=l({animate:i.animate,duration:i.duration},i.pan)),this._zoom!==e?this._tryAnimatedZoom&&this._tryAnimatedZoom(t,e,i.zoom):this._tryAnimatedPan(t,i.pan)))return clearTimeout(this._sizeTimer),this;return this._resetView(t,e,i.pan&&i.pan.noMoveStart),this},setZoom:function(t,e){return this._loaded?this.setView(this.getCenter(),t,{zoom:e}):(this._zoom=t,this)},zoomIn:function(t,e){return t=t||(b.any3d?this.options.zoomDelta:1),this.setZoom(this._zoom+t,e)},zoomOut:function(t,e){return t=t||(b.any3d?this.options.zoomDelta:1),this.setZoom(this._zoom-t,e)},setZoomAround:function(t,e,i){var n=this.getZoomScale(e),o=this.getSize().divideBy(2),t=(t instanceof p?t:this.latLngToContainerPoint(t)).subtract(o).multiplyBy(1-1/n),n=this.containerPointToLatLng(o.add(t));return this.setView(n,e,{zoom:i})},_getBoundsCenterZoom:function(t,e){e=e||{},t=t.getBounds?t.getBounds():g(t);var i=m(e.paddingTopLeft||e.padding||[0,0]),n=m(e.paddingBottomRight||e.padding||[0,0]),o=this.getBoundsZoom(t,!1,i.add(n));return(o="number"==typeof e.maxZoom?Math.min(e.maxZoom,o):o)===1/0?{center:t.getCenter(),zoom:o}:(e=n.subtract(i).divideBy(2),n=this.project(t.getSouthWest(),o),i=this.project(t.getNorthEast(),o),{center:this.unproject(n.add(i).divideBy(2).add(e),o),zoom:o})},fitBounds:function(t,e){if((t=g(t)).isValid())return t=this._getBoundsCenterZoom(t,e),this.setView(t.center,t.zoom,e);throw new Error("Bounds are not valid.")},fitWorld:function(t){return this.fitBounds([[-90,-180],[90,180]],t)},panTo:function(t,e){return this.setView(t,this._zoom,{pan:e})},panBy:function(t,e){var i;return e=e||{},(t=m(t).round()).x||t.y?(!0===e.animate||this.getSize().contains(t)?(this._panAnim||(this._panAnim=new Fe,this._panAnim.on({step:this._onPanTransitionStep,end:this._onPanTransitionEnd},this)),e.noMoveStart||this.fire("movestart"),!1!==e.animate?(M(this._mapPane,"leaflet-pan-anim"),i=this._getMapPanePos().subtract(t).round(),this._panAnim.run(this._mapPane,i,e.duration||.25,e.easeLinearity)):(this._rawPanBy(t),this.fire("move").fire("moveend"))):this._resetView(this.unproject(this.project(this.getCenter()).add(t)),this.getZoom()),this):this.fire("moveend")},flyTo:function(n,o,t){if(!1===(t=t||{}).animate||!b.any3d)return this.setView(n,o,t);this._stop();var s=this.project(this.getCenter()),r=this.project(n),e=this.getSize(),a=this._zoom,h=(n=w(n),o=void 0===o?a:o,Math.max(e.x,e.y)),i=h*this.getZoomScale(a,o),l=r.distanceTo(s)||1,u=1.42,c=u*u;function d(t){t=(i*i-h*h+(t?-1:1)*c*c*l*l)/(2*(t?i:h)*c*l),t=Math.sqrt(t*t+1)-t;return t<1e-9?-18:Math.log(t)}function _(t){return(Math.exp(t)-Math.exp(-t))/2}function p(t){return(Math.exp(t)+Math.exp(-t))/2}var m=d(0);function f(t){return h*(p(m)*(_(t=m+u*t)/p(t))-_(m))/c}var g=Date.now(),v=(d(1)-m)/u,y=t.duration?1e3*t.duration:1e3*v*.8;return this._moveStart(!0,t.noMoveStart),function t(){var e=(Date.now()-g)/y,i=(1-Math.pow(1-e,1.5))*v;e<=1?(this._flyToFrame=x(t,this),this._move(this.unproject(s.add(r.subtract(s).multiplyBy(f(i)/l)),a),this.getScaleZoom(h/(e=i,h*(p(m)/p(m+u*e))),a),{flyTo:!0})):this._move(n,o)._moveEnd(!0)}.call(this),this},flyToBounds:function(t,e){t=this._getBoundsCenterZoom(t,e);return this.flyTo(t.center,t.zoom,e)},setMaxBounds:function(t){return t=g(t),this.listens("moveend",this._panInsideMaxBounds)&&this.off("moveend",this._panInsideMaxBounds),t.isValid()?(this.options.maxBounds=t,this._loaded&&this._panInsideMaxBounds(),this.on("moveend",this._panInsideMaxBounds)):(this.options.maxBounds=null,this)},setMinZoom:function(t){var e=this.options.minZoom;return this.options.minZoom=t,this._loaded&&e!==t&&(this.fire("zoomlevelschange"),this.getZoom()<this.options.minZoom)?this.setZoom(t):this},setMaxZoom:function(t){var e=this.options.maxZoom;return this.options.maxZoom=t,this._loaded&&e!==t&&(this.fire("zoomlevelschange"),this.getZoom()>this.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,e){this._enforcingBounds=!0;var i=this.getCenter(),t=this._limitCenter(i,this._zoom,g(t));return i.equals(t)||this.panTo(t,e),this._enforcingBounds=!1,this},panInside:function(t,e){var i=m((e=e||{}).paddingTopLeft||e.padding||[0,0]),n=m(e.paddingBottomRight||e.padding||[0,0]),o=this.project(this.getCenter()),t=this.project(t),s=this.getPixelBounds(),i=_([s.min.add(i),s.max.subtract(n)]),s=i.getSize();return i.contains(t)||(this._enforcingBounds=!0,n=t.subtract(i.getCenter()),i=i.extend(t).getSize().subtract(s),o.x+=n.x<0?-i.x:i.x,o.y+=n.y<0?-i.y:i.y,this.panTo(this.unproject(o),e),this._enforcingBounds=!1),this},invalidateSize:function(t){if(!this._loaded)return this;t=l({animate:!1,pan:!0},!0===t?{animate:!0}:t);var e=this.getSize(),i=(this._sizeChanged=!0,this._lastCenter=null,this.getSize()),n=e.divideBy(2).round(),o=i.divideBy(2).round(),n=n.subtract(o);return n.x||n.y?(t.animate&&t.pan?this.panBy(n):(t.pan&&this._rawPanBy(n),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(a(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:e,newSize:i})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){var e,i;return t=this._locateOptions=l({timeout:1e4,watch:!1},t),"geolocation"in navigator?(e=a(this._handleGeolocationResponse,this),i=a(this._handleGeolocationError,this),t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t)):this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var e;this._container._leaflet_id&&(e=t.code,t=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout"),this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+t+"."}))},_handleGeolocationResponse:function(t){if(this._container._leaflet_id){var e,i,n=new v(t.coords.latitude,t.coords.longitude),o=n.toBounds(2*t.coords.accuracy),s=this._locateOptions,r=(s.setView&&(e=this.getBoundsZoom(o),this.setView(n,s.maxZoom?Math.min(e,s.maxZoom):e)),{latlng:n,bounds:o,timestamp:t.timestamp});for(i in t.coords)"number"==typeof t.coords[i]&&(r[i]=t.coords[i]);this.fire("locationfound",r)}},addHandler:function(t,e){return e&&(e=this[t]=new e(this),this._handlers.push(e),this.options[t]&&e.enable()),this},remove:function(){if(this._initEvents(!0),this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}for(var t in void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),T(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(r(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload"),this._layers)this._layers[t].remove();for(t in this._panes)T(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,e){e=P("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),e||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter.clone():this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new s(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,e,i){t=g(t),i=m(i||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),t=t.getSouthEast(),i=this.getSize().subtract(i),t=_(this.project(t,n),this.project(r,n)).getSize(),r=b.any3d?this.options.zoomSnap:1,a=i.x/t.x,i=i.y/t.y,t=e?Math.max(a,i):Math.min(a,i),n=this.getScaleZoom(t,n);return r&&(n=Math.round(n/(r/100))*(r/100),n=e?Math.ceil(n/r)*r:Math.floor(n/r)*r),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new p(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,e){t=this._getTopLeftPoint(t,e);return new f(t,t.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,e){var i=this.options.crs;return e=void 0===e?this._zoom:e,i.scale(t)/i.scale(e)},getScaleZoom:function(t,e){var i=this.options.crs,t=(e=void 0===e?this._zoom:e,i.zoom(t*i.scale(e)));return isNaN(t)?1/0:t},project:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.latLngToPoint(w(t),e)},unproject:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.pointToLatLng(m(t),e)},layerPointToLatLng:function(t){t=m(t).add(this.getPixelOrigin());return this.unproject(t)},latLngToLayerPoint:function(t){return this.project(w(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(w(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(g(t))},distance:function(t,e){return this.options.crs.distance(w(t),w(e))},containerPointToLayerPoint:function(t){return m(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return m(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){t=this.containerPointToLayerPoint(m(t));return this.layerPointToLatLng(t)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(w(t)))},mouseEventToContainerPoint:function(t){return De(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){t=this._container=_e(t);if(!t)throw new Error("Map container not found.");if(t._leaflet_id)throw new Error("Map container is already initialized.");S(t,"scroll",this._onScroll,this),this._containerId=h(t)},_initLayout:function(){var t=this._container,e=(this._fadeAnimated=this.options.fadeAnimation&&b.any3d,M(t,"leaflet-container"+(b.touch?" leaflet-touch":"")+(b.retina?" leaflet-retina":"")+(b.ielt9?" leaflet-oldie":"")+(b.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":"")),pe(t,"position"));"absolute"!==e&&"relative"!==e&&"fixed"!==e&&"sticky"!==e&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),Z(this._mapPane,new p(0,0)),this.createPane("tilePane"),this.createPane("overlayPane"),this.createPane("shadowPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(M(t.markerPane,"leaflet-zoom-hide"),M(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,e,i){Z(this._mapPane,new p(0,0));var n=!this._loaded,o=(this._loaded=!0,e=this._limitZoom(e),this.fire("viewprereset"),this._zoom!==e);this._moveStart(o,i)._move(t,e)._moveEnd(o),this.fire("viewreset"),n&&this.fire("load")},_moveStart:function(t,e){return t&&this.fire("zoomstart"),e||this.fire("movestart"),this},_move:function(t,e,i,n){void 0===e&&(e=this._zoom);var o=this._zoom!==e;return this._zoom=e,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),n?i&&i.pinch&&this.fire("zoom",i):((o||i&&i.pinch)&&this.fire("zoom",i),this.fire("move",i)),this},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return r(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){Z(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={};var e=t?k:S;e((this._targets[h(this._container)]=this)._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup",this._handleDOMEvent,this),this.options.trackResize&&e(window,"resize",this._onResize,this),b.any3d&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){r(this._resizeRequest),this._resizeRequest=x(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,e){for(var i,n=[],o="mouseout"===e||"mouseover"===e,s=t.target||t.srcElement,r=!1;s;){if((i=this._targets[h(s)])&&("click"===e||"preclick"===e)&&this._draggableMoved(i)){r=!0;break}if(i&&i.listens(e,!0)){if(o&&!We(s,t))break;if(n.push(i),o)break}if(s===this._container)break;s=s.parentNode}return n=n.length||r||o||!this.listens(e,!0)?n:[this]},_isClickDisabled:function(t){for(;t&&t!==this._container;){if(t._leaflet_disable_click)return!0;t=t.parentNode}},_handleDOMEvent:function(t){var e,i=t.target||t.srcElement;!this._loaded||i._leaflet_disable_events||"click"===t.type&&this._isClickDisabled(i)||("mousedown"===(e=t.type)&&Me(i),this._fireDOMEvent(t,e))},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,i){"click"===t.type&&((a=l({},t)).type="preclick",this._fireDOMEvent(a,a.type,i));var n=this._findEventTargets(t,e);if(i){for(var o=[],s=0;s<i.length;s++)i[s].listens(e,!0)&&o.push(i[s]);n=o.concat(n)}if(n.length){"contextmenu"===e&&O(t);var r,a=n[0],h={originalEvent:t};for("keypress"!==t.type&&"keydown"!==t.type&&"keyup"!==t.type&&(r=a.getLatLng&&(!a._radius||a._radius<=10),h.containerPoint=r?this.latLngToContainerPoint(a.getLatLng()):this.mouseEventToContainerPoint(t),h.layerPoint=this.containerPointToLayerPoint(h.containerPoint),h.latlng=r?a.getLatLng():this.layerPointToLatLng(h.layerPoint)),s=0;s<n.length;s++)if(n[s].fire(e,h,!0),h.originalEvent._stopped||!1===n[s].options.bubblingMouseEvents&&-1!==G(this._mouseEvents,e))return}},_draggableMoved:function(t){return(t=t.dragging&&t.dragging.enabled()?t:this).dragging&&t.dragging.moved()||this.boxZoom&&this.boxZoom.moved()},_clearHandlers:function(){for(var t=0,e=this._handlers.length;t<e;t++)this._handlers[t].disable()},whenReady:function(t,e){return this._loaded?t.call(e||this,{target:this}):this.on("load",t,e),this},_getMapPanePos:function(){return Pe(this._mapPane)||new p(0,0)},_moved:function(){var t=this._getMapPanePos();return t&&!t.equals([0,0])},_getTopLeftPoint:function(t,e){return(t&&void 0!==e?this._getNewPixelOrigin(t,e):this.getPixelOrigin()).subtract(this._getMapPanePos())},_getNewPixelOrigin:function(t,e){var i=this.getSize()._divideBy(2);return this.project(t,e)._subtract(i)._add(this._getMapPanePos())._round()},_latLngToNewLayerPoint:function(t,e,i){i=this._getNewPixelOrigin(i,e);return this.project(t,e)._subtract(i)},_latLngBoundsToNewLayerBounds:function(t,e,i){i=this._getNewPixelOrigin(i,e);return _([this.project(t.getSouthWest(),e)._subtract(i),this.project(t.getNorthWest(),e)._subtract(i),this.project(t.getSouthEast(),e)._subtract(i),this.project(t.getNorthEast(),e)._subtract(i)])},_getCenterLayerPoint:function(){return this.containerPointToLayerPoint(this.getSize()._divideBy(2))},_getCenterOffset:function(t){return this.latLngToLayerPoint(t).subtract(this._getCenterLayerPoint())},_limitCenter:function(t,e,i){var n,o;return!i||(n=this.project(t,e),o=this.getSize().divideBy(2),o=new f(n.subtract(o),n.add(o)),o=this._getBoundsOffset(o,i,e),Math.abs(o.x)<=1&&Math.abs(o.y)<=1)?t:this.unproject(n.add(o),e)},_limitOffset:function(t,e){var i;return e?(i=new f((i=this.getPixelBounds()).min.add(t),i.max.add(t)),t.add(this._getBoundsOffset(i,e))):t},_getBoundsOffset:function(t,e,i){e=_(this.project(e.getNorthEast(),i),this.project(e.getSouthWest(),i)),i=e.min.subtract(t.min),e=e.max.subtract(t.max);return new p(this._rebound(i.x,-e.x),this._rebound(i.y,-e.y))},_rebound:function(t,e){return 0<t+e?Math.round(t-e)/2:Math.max(0,Math.ceil(t))-Math.max(0,Math.floor(e))},_limitZoom:function(t){var e=this.getMinZoom(),i=this.getMaxZoom(),n=b.any3d?this.options.zoomSnap:1;return n&&(t=Math.round(t/n)*n),Math.max(e,Math.min(i,t))},_onPanTransitionStep:function(){this.fire("move")},_onPanTransitionEnd:function(){z(this._mapPane,"leaflet-pan-anim"),this.fire("moveend")},_tryAnimatedPan:function(t,e){t=this._getCenterOffset(t)._trunc();return!(!0!==(e&&e.animate)&&!this.getSize().contains(t))&&(this.panBy(t,e),!0)},_createAnimProxy:function(){var t=this._proxy=P("div","leaflet-proxy leaflet-zoom-animated");this._panes.mapPane.appendChild(t),this.on("zoomanim",function(t){var e=ue,i=this._proxy.style[e];be(this._proxy,this.project(t.center,t.zoom),this.getZoomScale(t.zoom,1)),i===this._proxy.style[e]&&this._animatingZoom&&this._onZoomTransitionEnd()},this),this.on("load moveend",this._animMoveEnd,this),this._on("unload",this._destroyAnimProxy,this)},_destroyAnimProxy:function(){T(this._proxy),this.off("load moveend",this._animMoveEnd,this),delete this._proxy},_animMoveEnd:function(){var t=this.getCenter(),e=this.getZoom();be(this._proxy,this.project(t,e),this.getZoomScale(e,1))},_catchTransitionEnd:function(t){this._animatingZoom&&0<=t.propertyName.indexOf("transform")&&this._onZoomTransitionEnd()},_nothingToAnimate:function(){return!this._container.getElementsByClassName("leaflet-zoom-animated").length},_tryAnimatedZoom:function(t,e,i){if(!this._animatingZoom){if(i=i||{},!this._zoomAnimated||!1===i.animate||this._nothingToAnimate()||Math.abs(e-this._zoom)>this.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),n=this._getCenterOffset(t)._divideBy(1-1/n);if(!0!==i.animate&&!this.getSize().contains(n))return!1;x(function(){this._moveStart(!0,i.noMoveStart||!1)._animateZoom(t,e,!0)},this)}return!0},_animateZoom:function(t,e,i,n){this._mapPane&&(i&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=e,M(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:e,noUpdate:n}),this._tempFireZoomEvent||(this._tempFireZoomEvent=this._zoom!==this._animateToZoom),this._move(this._animateToCenter,this._animateToZoom,void 0,!0),setTimeout(a(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&z(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom,void 0,!0),this._tempFireZoomEvent&&this.fire("zoom"),delete this._tempFireZoomEvent,this.fire("move"),this._moveEnd(!0))}});function Ue(t){return new B(t)}var B=et.extend({options:{position:"topright"},initialize:function(t){c(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var e=this._map;return e&&e.removeControl(this),this.options.position=t,e&&e.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var e=this._container=this.onAdd(t),i=this.getPosition(),t=t._controlCorners[i];return M(e,"leaflet-control"),-1!==i.indexOf("bottom")?t.insertBefore(e,t.firstChild):t.appendChild(e),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(T(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0<t.screenX&&0<t.screenY&&this._map.getContainer().focus()}}),Ve=(A.include({addControl:function(t){return t.addTo(this),this},removeControl:function(t){return t.remove(),this},_initControlPos:function(){var i=this._controlCorners={},n="leaflet-",o=this._controlContainer=P("div",n+"control-container",this._container);function t(t,e){i[t+e]=P("div",n+t+" "+n+e,o)}t("top","left"),t("top","right"),t("bottom","left"),t("bottom","right")},_clearControlPos:function(){for(var t in this._controlCorners)T(this._controlCorners[t]);T(this._controlContainer),delete this._controlCorners,delete this._controlContainer}}),B.extend({options:{collapsed:!0,position:"topright",autoZIndex:!0,hideSingleBase:!1,sortLayers:!1,sortFunction:function(t,e,i,n){return i<n?-1:n<i?1:0}},initialize:function(t,e,i){for(var n in c(this,i),this._layerControlInputs=[],this._layers=[],this._lastZIndex=0,this._handlingClick=!1,this._preventClick=!1,t)this._addLayer(t[n],n);for(n in e)this._addLayer(e[n],n,!0)},onAdd:function(t){this._initLayout(),this._update(),(this._map=t).on("zoomend",this._checkDisabledLayers,this);for(var e=0;e<this._layers.length;e++)this._layers[e].layer.on("add remove",this._onLayerChange,this);return this._container},addTo:function(t){return B.prototype.addTo.call(this,t),this._expandIfNotCollapsed()},onRemove:function(){this._map.off("zoomend",this._checkDisabledLayers,this);for(var t=0;t<this._layers.length;t++)this._layers[t].layer.off("add remove",this._onLayerChange,this)},addBaseLayer:function(t,e){return this._addLayer(t,e),this._map?this._update():this},addOverlay:function(t,e){return this._addLayer(t,e,!0),this._map?this._update():this},removeLayer:function(t){t.off("add remove",this._onLayerChange,this);t=this._getLayer(h(t));return t&&this._layers.splice(this._layers.indexOf(t),1),this._map?this._update():this},expand:function(){M(this._container,"leaflet-control-layers-expanded"),this._section.style.height=null;var t=this._map.getSize().y-(this._container.offsetTop+50);return t<this._section.clientHeight?(M(this._section,"leaflet-control-layers-scrollbar"),this._section.style.height=t+"px"):z(this._section,"leaflet-control-layers-scrollbar"),this._checkDisabledLayers(),this},collapse:function(){return z(this._container,"leaflet-control-layers-expanded"),this},_initLayout:function(){var t="leaflet-control-layers",e=this._container=P("div",t),i=this.options.collapsed,n=(e.setAttribute("aria-haspopup",!0),Ie(e),Be(e),this._section=P("section",t+"-list")),o=(i&&(this._map.on("click",this.collapse,this),S(e,{mouseenter:this._expandSafely,mouseleave:this.collapse},this)),this._layersLink=P("a",t+"-toggle",e));o.href="#",o.title="Layers",o.setAttribute("role","button"),S(o,{keydown:function(t){13===t.keyCode&&this._expandSafely()},click:function(t){O(t),this._expandSafely()}},this),i||this.expand(),this._baseLayersList=P("div",t+"-base",n),this._separator=P("div",t+"-separator",n),this._overlaysList=P("div",t+"-overlays",n),e.appendChild(n)},_getLayer:function(t){for(var e=0;e<this._layers.length;e++)if(this._layers[e]&&h(this._layers[e].layer)===t)return this._layers[e]},_addLayer:function(t,e,i){this._map&&t.on("add remove",this._onLayerChange,this),this._layers.push({layer:t,name:e,overlay:i}),this.options.sortLayers&&this._layers.sort(a(function(t,e){return this.options.sortFunction(t.layer,e.layer,t.name,e.name)},this)),this.options.autoZIndex&&t.setZIndex&&(this._lastZIndex++,t.setZIndex(this._lastZIndex)),this._expandIfNotCollapsed()},_update:function(){if(this._container){me(this._baseLayersList),me(this._overlaysList),this._layerControlInputs=[];for(var t,e,i,n=0,o=0;o<this._layers.length;o++)i=this._layers[o],this._addItem(i),e=e||i.overlay,t=t||!i.overlay,n+=i.overlay?0:1;this.options.hideSingleBase&&(this._baseLayersList.style.display=(t=t&&1<n)?"":"none"),this._separator.style.display=e&&t?"":"none"}return this},_onLayerChange:function(t){this._handlingClick||this._update();var e=this._getLayer(h(t.target)),t=e.overlay?"add"===t.type?"overlayadd":"overlayremove":"add"===t.type?"baselayerchange":null;t&&this._map.fire(t,e)},_createRadioElement:function(t,e){t='<input type="radio" class="leaflet-control-layers-selector" name="'+t+'"'+(e?' checked="checked"':"")+"/>",e=document.createElement("div");return e.innerHTML=t,e.firstChild},_addItem:function(t){var e,i=document.createElement("label"),n=this._map.hasLayer(t.layer),n=(t.overlay?((e=document.createElement("input")).type="checkbox",e.className="leaflet-control-layers-selector",e.defaultChecked=n):e=this._createRadioElement("leaflet-base-layers_"+h(this),n),this._layerControlInputs.push(e),e.layerId=h(t.layer),S(e,"click",this._onInputClick,this),document.createElement("span")),o=(n.innerHTML=" "+t.name,document.createElement("span"));return i.appendChild(o),o.appendChild(e),o.appendChild(n),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(i),this._checkDisabledLayers(),i},_onInputClick:function(){if(!this._preventClick){var t,e,i=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=i.length-1;0<=s;s--)t=i[s],e=this._getLayer(t.layerId).layer,t.checked?n.push(e):t.checked||o.push(e);for(s=0;s<o.length;s++)this._map.hasLayer(o[s])&&this._map.removeLayer(o[s]);for(s=0;s<n.length;s++)this._map.hasLayer(n[s])||this._map.addLayer(n[s]);this._handlingClick=!1,this._refocusOnMap()}},_checkDisabledLayers:function(){for(var t,e,i=this._layerControlInputs,n=this._map.getZoom(),o=i.length-1;0<=o;o--)t=i[o],e=this._getLayer(t.layerId).layer,t.disabled=void 0!==e.options.minZoom&&n<e.options.minZoom||void 0!==e.options.maxZoom&&n>e.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expandSafely:function(){var t=this._section,e=(this._preventClick=!0,S(t,"click",O),this.expand(),this);setTimeout(function(){k(t,"click",O),e._preventClick=!1})}})),qe=B.extend({options:{position:"topleft",zoomInText:'<span aria-hidden="true">+</span>',zoomInTitle:"Zoom in",zoomOutText:'<span aria-hidden="true">&#x2212;</span>',zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=P("div",e+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,e+"-in",i,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,e+"-out",i,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoom<this._map.getMaxZoom()&&this._map.zoomIn(this._map.options.zoomDelta*(t.shiftKey?3:1))},_zoomOut:function(t){!this._disabled&&this._map._zoom>this._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,e,i,n,o){i=P("a",i,n);return i.innerHTML=t,i.href="#",i.title=e,i.setAttribute("role","button"),i.setAttribute("aria-label",e),Ie(i),S(i,"click",Re),S(i,"click",o,this),S(i,"click",this._refocusOnMap,this),i},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";z(this._zoomInButton,e),z(this._zoomOutButton,e),this._zoomInButton.setAttribute("aria-disabled","false"),this._zoomOutButton.setAttribute("aria-disabled","false"),!this._disabled&&t._zoom!==t.getMinZoom()||(M(this._zoomOutButton,e),this._zoomOutButton.setAttribute("aria-disabled","true")),!this._disabled&&t._zoom!==t.getMaxZoom()||(M(this._zoomInButton,e),this._zoomInButton.setAttribute("aria-disabled","true"))}}),Ge=(A.mergeOptions({zoomControl:!0}),A.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new qe,this.addControl(this.zoomControl))}),B.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var e="leaflet-control-scale",i=P("div",e),n=this.options;return this._addScales(n,e+"-line",i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=P("div",e,i)),t.imperial&&(this._iScale=P("div",e,i))},_update:function(){var t=this._map,e=t.getSize().y/2,t=t.distance(t.containerPointToLatLng([0,e]),t.containerPointToLatLng([this.options.maxWidth,e]));this._updateScales(t)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var e=this._getRoundNum(t);this._updateScale(this._mScale,e<1e3?e+" m":e/1e3+" km",e/t)},_updateImperial:function(t){var e,i,t=3.2808399*t;5280<t?(i=this._getRoundNum(e=t/5280),this._updateScale(this._iScale,i+" mi",i/e)):(i=this._getRoundNum(t),this._updateScale(this._iScale,i+" ft",i/t))},_updateScale:function(t,e,i){t.style.width=Math.round(this.options.maxWidth*i)+"px",t.innerHTML=e},_getRoundNum:function(t){var e=Math.pow(10,(Math.floor(t)+"").length-1),t=t/e;return e*(t=10<=t?10:5<=t?5:3<=t?3:2<=t?2:1)}})),Ke=B.extend({options:{position:"bottomright",prefix:'<a href="https://leafletjs.com" title="A JavaScript library for interactive maps">'+(b.inlineSvg?'<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="12" height="8" viewBox="0 0 12 8" class="leaflet-attribution-flag"><path fill="#4C7BE1" d="M0 0h12v4H0z"/><path fill="#FFD500" d="M0 4h12v3H0z"/><path fill="#E0BC00" d="M0 7h12v1H0z"/></svg> ':"")+"Leaflet</a>"},initialize:function(t){c(this,t),this._attributions={}},onAdd:function(t){for(var e in(t.attributionControl=this)._container=P("div","leaflet-control-attribution"),Ie(this._container),t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return this._update(),t.on("layeradd",this._addAttribution,this),this._container},onRemove:function(t){t.off("layeradd",this._addAttribution,this)},_addAttribution:function(t){t.layer.getAttribution&&(this.addAttribution(t.layer.getAttribution()),t.layer.once("remove",function(){this.removeAttribution(t.layer.getAttribution())},this))},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t,e=[];for(t in this._attributions)this._attributions[t]&&e.push(t);var i=[];this.options.prefix&&i.push(this.options.prefix),e.length&&i.push(e.join(", ")),this._container.innerHTML=i.join(' <span aria-hidden="true">|</span> ')}}}),n=(A.mergeOptions({attributionControl:!0}),A.addInitHook(function(){this.options.attributionControl&&(new Ke).addTo(this)}),B.Layers=Ve,B.Zoom=qe,B.Scale=Ge,B.Attribution=Ke,Ue.layers=function(t,e,i){return new Ve(t,e,i)},Ue.zoom=function(t){return new qe(t)},Ue.scale=function(t){return new Ge(t)},Ue.attribution=function(t){return new Ke(t)},et.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}})),ft=(n.addTo=function(t,e){return t.addHandler(e,this),this},{Events:e}),Ye=b.touch?"touchstart mousedown":"mousedown",Xe=it.extend({options:{clickTolerance:3},initialize:function(t,e,i,n){c(this,n),this._element=t,this._dragStartTarget=e||t,this._preventOutline=i},enable:function(){this._enabled||(S(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Xe._dragging===this&&this.finishDrag(!0),k(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){var e,i;this._enabled&&(this._moved=!1,ve(this._element,"leaflet-zoom-anim")||(t.touches&&1!==t.touches.length?Xe._dragging===this&&this.finishDrag():Xe._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((Xe._dragging=this)._preventOutline&&Me(this._element),Le(),re(),this._moving||(this.fire("down"),i=t.touches?t.touches[0]:t,e=Ce(this._element),this._startPoint=new p(i.clientX,i.clientY),this._startPos=Pe(this._element),this._parentScale=Ze(e),i="mousedown"===t.type,S(document,i?"mousemove":"touchmove",this._onMove,this),S(document,i?"mouseup":"touchend touchcancel",this._onUp,this)))))},_onMove:function(t){var e;this._enabled&&(t.touches&&1<t.touches.length?this._moved=!0:!(e=new p((e=t.touches&&1===t.touches.length?t.touches[0]:t).clientX,e.clientY)._subtract(this._startPoint)).x&&!e.y||Math.abs(e.x)+Math.abs(e.y)<this.options.clickTolerance||(e.x/=this._parentScale.x,e.y/=this._parentScale.y,O(t),this._moved||(this.fire("dragstart"),this._moved=!0,M(document.body,"leaflet-dragging"),this._lastTarget=t.target||t.srcElement,window.SVGElementInstance&&this._lastTarget instanceof window.SVGElementInstance&&(this._lastTarget=this._lastTarget.correspondingUseElement),M(this._lastTarget,"leaflet-drag-target")),this._newPos=this._startPos.add(e),this._moving=!0,this._lastEvent=t,this._updatePosition()))},_updatePosition:function(){var t={originalEvent:this._lastEvent};this.fire("predrag",t),Z(this._element,this._newPos),this.fire("drag",t)},_onUp:function(){this._enabled&&this.finishDrag()},finishDrag:function(t){z(document.body,"leaflet-dragging"),this._lastTarget&&(z(this._lastTarget,"leaflet-drag-target"),this._lastTarget=null),k(document,"mousemove touchmove",this._onMove,this),k(document,"mouseup touchend touchcancel",this._onUp,this),Te(),ae();var e=this._moved&&this._moving;this._moving=!1,Xe._dragging=!1,e&&this.fire("dragend",{noInertia:t,distance:this._newPos.distanceTo(this._startPos)})}});function Je(t,e,i){for(var n,o,s,r,a,h,l,u=[1,4,2,8],c=0,d=t.length;c<d;c++)t[c]._code=si(t[c],e);for(s=0;s<4;s++){for(h=u[s],n=[],c=0,o=(d=t.length)-1;c<d;o=c++)r=t[c],a=t[o],r._code&h?a._code&h||((l=oi(a,r,h,e,i))._code=si(l,e),n.push(l)):(a._code&h&&((l=oi(a,r,h,e,i))._code=si(l,e),n.push(l)),n.push(r));t=n}return t}function $e(t,e){var i,n,o,s,r,a,h;if(!t||0===t.length)throw new Error("latlngs not passed");I(t)||(console.warn("latlngs are not flat! Only the first ring will be used"),t=t[0]);for(var l=w([0,0]),u=g(t),c=(u.getNorthWest().distanceTo(u.getSouthWest())*u.getNorthEast().distanceTo(u.getNorthWest())<1700&&(l=Qe(t)),t.length),d=[],_=0;_<c;_++){var p=w(t[_]);d.push(e.project(w([p.lat-l.lat,p.lng-l.lng])))}for(_=r=a=h=0,i=c-1;_<c;i=_++)n=d[_],o=d[i],s=n.y*o.x-o.y*n.x,a+=(n.x+o.x)*s,h+=(n.y+o.y)*s,r+=3*s;u=0===r?d[0]:[a/r,h/r],u=e.unproject(m(u));return w([u.lat+l.lat,u.lng+l.lng])}function Qe(t){for(var e=0,i=0,n=0,o=0;o<t.length;o++){var s=w(t[o]);e+=s.lat,i+=s.lng,n++}return w([e/n,i/n])}var ti,gt={__proto__:null,clipPolygon:Je,polygonCenter:$e,centroid:Qe};function ei(t,e){if(e&&t.length){var i=t=function(t,e){for(var i=[t[0]],n=1,o=0,s=t.length;n<s;n++)(function(t,e){var i=e.x-t.x,e=e.y-t.y;return i*i+e*e})(t[n],t[o])>e&&(i.push(t[n]),o=n);o<s-1&&i.push(t[s-1]);return i}(t,e=e*e),n=i.length,o=new(typeof Uint8Array!=void 0+""?Uint8Array:Array)(n);o[0]=o[n-1]=1,function t(e,i,n,o,s){var r,a,h,l=0;for(a=o+1;a<=s-1;a++)h=ri(e[a],e[o],e[s],!0),l<h&&(r=a,l=h);n<l&&(i[r]=1,t(e,i,n,o,r),t(e,i,n,r,s))}(i,o,e,0,n-1);var s,r=[];for(s=0;s<n;s++)o[s]&&r.push(i[s]);return r}return t.slice()}function ii(t,e,i){return Math.sqrt(ri(t,e,i,!0))}function ni(t,e,i,n,o){var s,r,a,h=n?ti:si(t,i),l=si(e,i);for(ti=l;;){if(!(h|l))return[t,e];if(h&l)return!1;a=si(r=oi(t,e,s=h||l,i,o),i),s===h?(t=r,h=a):(e=r,l=a)}}function oi(t,e,i,n,o){var s,r,a=e.x-t.x,e=e.y-t.y,h=n.min,n=n.max;return 8&i?(s=t.x+a*(n.y-t.y)/e,r=n.y):4&i?(s=t.x+a*(h.y-t.y)/e,r=h.y):2&i?(s=n.x,r=t.y+e*(n.x-t.x)/a):1&i&&(s=h.x,r=t.y+e*(h.x-t.x)/a),new p(s,r,o)}function si(t,e){var i=0;return t.x<e.min.x?i|=1:t.x>e.max.x&&(i|=2),t.y<e.min.y?i|=4:t.y>e.max.y&&(i|=8),i}function ri(t,e,i,n){var o=e.x,e=e.y,s=i.x-o,r=i.y-e,a=s*s+r*r;return 0<a&&(1<(a=((t.x-o)*s+(t.y-e)*r)/a)?(o=i.x,e=i.y):0<a&&(o+=s*a,e+=r*a)),s=t.x-o,r=t.y-e,n?s*s+r*r:new p(o,e)}function I(t){return!d(t[0])||"object"!=typeof t[0][0]&&void 0!==t[0][0]}function ai(t){return console.warn("Deprecated use of _flat, please use L.LineUtil.isFlat instead."),I(t)}function hi(t,e){var i,n,o,s,r,a;if(!t||0===t.length)throw new Error("latlngs not passed");I(t)||(console.warn("latlngs are not flat! Only the first ring will be used"),t=t[0]);for(var h=w([0,0]),l=g(t),u=(l.getNorthWest().distanceTo(l.getSouthWest())*l.getNorthEast().distanceTo(l.getNorthWest())<1700&&(h=Qe(t)),t.length),c=[],d=0;d<u;d++){var _=w(t[d]);c.push(e.project(w([_.lat-h.lat,_.lng-h.lng])))}for(i=d=0;d<u-1;d++)i+=c[d].distanceTo(c[d+1])/2;if(0===i)a=c[0];else for(n=d=0;d<u-1;d++)if(o=c[d],s=c[d+1],i<(n+=r=o.distanceTo(s))){a=[s.x-(r=(n-i)/r)*(s.x-o.x),s.y-r*(s.y-o.y)];break}l=e.unproject(m(a));return w([l.lat+h.lat,l.lng+h.lng])}var vt={__proto__:null,simplify:ei,pointToSegmentDistance:ii,closestPointOnSegment:function(t,e,i){return ri(t,e,i)},clipSegment:ni,_getEdgeIntersection:oi,_getBitCode:si,_sqClosestPointOnSegment:ri,isFlat:I,_flat:ai,polylineCenter:hi},yt={project:function(t){return new p(t.lng,t.lat)},unproject:function(t){return new v(t.y,t.x)},bounds:new f([-180,-90],[180,90])},xt={R:6378137,R_MINOR:6356752.314245179,bounds:new f([-20037508.34279,-15496570.73972],[20037508.34279,18764656.23138]),project:function(t){var e=Math.PI/180,i=this.R,n=t.lat*e,o=this.R_MINOR/i,o=Math.sqrt(1-o*o),s=o*Math.sin(n),s=Math.tan(Math.PI/4-n/2)/Math.pow((1-s)/(1+s),o/2),n=-i*Math.log(Math.max(s,1e-10));return new p(t.lng*e*i,n)},unproject:function(t){for(var e,i=180/Math.PI,n=this.R,o=this.R_MINOR/n,s=Math.sqrt(1-o*o),r=Math.exp(-t.y/n),a=Math.PI/2-2*Math.atan(r),h=0,l=.1;h<15&&1e-7<Math.abs(l);h++)e=s*Math.sin(a),e=Math.pow((1-e)/(1+e),s/2),a+=l=Math.PI/2-2*Math.atan(r*e)-a;return new v(a*i,t.x*i/n)}},wt={__proto__:null,LonLat:yt,Mercator:xt,SphericalMercator:rt},Pt=l({},st,{code:"EPSG:3395",projection:xt,transformation:ht(bt=.5/(Math.PI*xt.R),.5,-bt,.5)}),li=l({},st,{code:"EPSG:4326",projection:yt,transformation:ht(1/180,1,-1/180,.5)}),Lt=l({},ot,{projection:yt,transformation:ht(1,0,-1,0),scale:function(t){return Math.pow(2,t)},zoom:function(t){return Math.log(t)/Math.LN2},distance:function(t,e){var i=e.lng-t.lng,e=e.lat-t.lat;return Math.sqrt(i*i+e*e)},infinite:!0}),o=(ot.Earth=st,ot.EPSG3395=Pt,ot.EPSG3857=lt,ot.EPSG900913=ut,ot.EPSG4326=li,ot.Simple=Lt,it.extend({options:{pane:"overlayPane",attribution:null,bubblingMouseEvents:!0},addTo:function(t){return t.addLayer(this),this},remove:function(){return this.removeFrom(this._map||this._mapToAdd)},removeFrom:function(t){return t&&t.removeLayer(this),this},getPane:function(t){return this._map.getPane(t?this.options[t]||t:this.options.pane)},addInteractiveTarget:function(t){return this._map._targets[h(t)]=this},removeInteractiveTarget:function(t){return delete this._map._targets[h(t)],this},getAttribution:function(){return this.options.attribution},_layerAdd:function(t){var e,i=t.target;i.hasLayer(this)&&(this._map=i,this._zoomAnimated=i._zoomAnimated,this.getEvents&&(e=this.getEvents(),i.on(e,this),this.once("remove",function(){i.off(e,this)},this)),this.onAdd(i),this.fire("add"),i.fire("layeradd",{layer:this}))}})),ui=(A.include({addLayer:function(t){var e;if(t._layerAdd)return e=h(t),this._layers[e]||((this._layers[e]=t)._mapToAdd=this,t.beforeAdd&&t.beforeAdd(this),this.whenReady(t._layerAdd,t)),this;throw new Error("The provided object is not a Layer.")},removeLayer:function(t){var e=h(t);return this._layers[e]&&(this._loaded&&t.onRemove(this),delete this._layers[e],this._loaded&&(this.fire("layerremove",{layer:t}),t.fire("remove")),t._map=t._mapToAdd=null),this},hasLayer:function(t){return h(t)in this._layers},eachLayer:function(t,e){for(var i in this._layers)t.call(e,this._layers[i]);return this},_addLayers:function(t){for(var e=0,i=(t=t?d(t)?t:[t]:[]).length;e<i;e++)this.addLayer(t[e])},_addZoomLimit:function(t){isNaN(t.options.maxZoom)&&isNaN(t.options.minZoom)||(this._zoomBoundLayers[h(t)]=t,this._updateZoomLevels())},_removeZoomLimit:function(t){t=h(t);this._zoomBoundLayers[t]&&(delete this._zoomBoundLayers[t],this._updateZoomLevels())},_updateZoomLevels:function(){var t,e=1/0,i=-1/0,n=this._getZoomSpan();for(t in this._zoomBoundLayers)var o=this._zoomBoundLayers[t].options,e=void 0===o.minZoom?e:Math.min(e,o.minZoom),i=void 0===o.maxZoom?i:Math.max(i,o.maxZoom);this._layersMaxZoom=i===-1/0?void 0:i,this._layersMinZoom=e===1/0?void 0:e,n!==this._getZoomSpan()&&this.fire("zoomlevelschange"),void 0===this.options.maxZoom&&this._layersMaxZoom&&this.getZoom()>this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()<this._layersMinZoom&&this.setZoom(this._layersMinZoom)}}),o.extend({initialize:function(t,e){var i,n;if(c(this,e),this._layers={},t)for(i=0,n=t.length;i<n;i++)this.addLayer(t[i])},addLayer:function(t){var e=this.getLayerId(t);return this._layers[e]=t,this._map&&this._map.addLayer(t),this},removeLayer:function(t){t=t in this._layers?t:this.getLayerId(t);return this._map&&this._layers[t]&&this._map.removeLayer(this._layers[t]),delete this._layers[t],this},hasLayer:function(t){return("number"==typeof t?t:this.getLayerId(t))in this._layers},clearLayers:function(){return this.eachLayer(this.removeLayer,this)},invoke:function(t){var e,i,n=Array.prototype.slice.call(arguments,1);for(e in this._layers)(i=this._layers[e])[t]&&i[t].apply(i,n);return this},onAdd:function(t){this.eachLayer(t.addLayer,t)},onRemove:function(t){this.eachLayer(t.removeLayer,t)},eachLayer:function(t,e){for(var i in this._layers)t.call(e,this._layers[i]);return this},getLayer:function(t){return this._layers[t]},getLayers:function(){var t=[];return this.eachLayer(t.push,t),t},setZIndex:function(t){return this.invoke("setZIndex",t)},getLayerId:h})),ci=ui.extend({addLayer:function(t){return this.hasLayer(t)?this:(t.addEventParent(this),ui.prototype.addLayer.call(this,t),this.fire("layeradd",{layer:t}))},removeLayer:function(t){return this.hasLayer(t)?((t=t in this._layers?this._layers[t]:t).removeEventParent(this),ui.prototype.removeLayer.call(this,t),this.fire("layerremove",{layer:t})):this},setStyle:function(t){return this.invoke("setStyle",t)},bringToFront:function(){return this.invoke("bringToFront")},bringToBack:function(){return this.invoke("bringToBack")},getBounds:function(){var t,e=new s;for(t in this._layers){var i=this._layers[t];e.extend(i.getBounds?i.getBounds():i.getLatLng())}return e}}),di=et.extend({options:{popupAnchor:[0,0],tooltipAnchor:[0,0],crossOrigin:!1},initialize:function(t){c(this,t)},createIcon:function(t){return this._createIcon("icon",t)},createShadow:function(t){return this._createIcon("shadow",t)},_createIcon:function(t,e){var i=this._getIconUrl(t);if(i)return i=this._createImg(i,e&&"IMG"===e.tagName?e:null),this._setIconStyles(i,t),!this.options.crossOrigin&&""!==this.options.crossOrigin||(i.crossOrigin=!0===this.options.crossOrigin?"":this.options.crossOrigin),i;if("icon"===t)throw new Error("iconUrl not set in Icon options (see the docs).");return null},_setIconStyles:function(t,e){var i=this.options,n=i[e+"Size"],n=m(n="number"==typeof n?[n,n]:n),o=m("shadow"===e&&i.shadowAnchor||i.iconAnchor||n&&n.divideBy(2,!0));t.className="leaflet-marker-"+e+" "+(i.className||""),o&&(t.style.marginLeft=-o.x+"px",t.style.marginTop=-o.y+"px"),n&&(t.style.width=n.x+"px",t.style.height=n.y+"px")},_createImg:function(t,e){return(e=e||document.createElement("img")).src=t,e},_getIconUrl:function(t){return b.retina&&this.options[t+"RetinaUrl"]||this.options[t+"Url"]}});var _i=di.extend({options:{iconUrl:"marker-icon.png",iconRetinaUrl:"marker-icon-2x.png",shadowUrl:"marker-shadow.png",iconSize:[25,41],iconAnchor:[12,41],popupAnchor:[1,-34],tooltipAnchor:[16,-28],shadowSize:[41,41]},_getIconUrl:function(t){return"string"!=typeof _i.imagePath&&(_i.imagePath=this._detectIconPath()),(this.options.imagePath||_i.imagePath)+di.prototype._getIconUrl.call(this,t)},_stripUrl:function(t){function e(t,e,i){return(e=e.exec(t))&&e[i]}return(t=e(t,/^url\((['"])?(.+)\1\)$/,2))&&e(t,/^(.*)marker-icon\.png$/,1)},_detectIconPath:function(){var t=P("div","leaflet-default-icon-path",document.body),e=pe(t,"background-image")||pe(t,"backgroundImage");return document.body.removeChild(t),(e=this._stripUrl(e))?e:(t=document.querySelector('link[href$="leaflet.css"]'))?t.href.substring(0,t.href.length-"leaflet.css".length-1):""}}),pi=n.extend({initialize:function(t){this._marker=t},addHooks:function(){var t=this._marker._icon;this._draggable||(this._draggable=new Xe(t,t,!0)),this._draggable.on({dragstart:this._onDragStart,predrag:this._onPreDrag,drag:this._onDrag,dragend:this._onDragEnd},this).enable(),M(t,"leaflet-marker-draggable")},removeHooks:function(){this._draggable.off({dragstart:this._onDragStart,predrag:this._onPreDrag,drag:this._onDrag,dragend:this._onDragEnd},this).disable(),this._marker._icon&&z(this._marker._icon,"leaflet-marker-draggable")},moved:function(){return this._draggable&&this._draggable._moved},_adjustPan:function(t){var e=this._marker,i=e._map,n=this._marker.options.autoPanSpeed,o=this._marker.options.autoPanPadding,s=Pe(e._icon),r=i.getPixelBounds(),a=i.getPixelOrigin(),a=_(r.min._subtract(a).add(o),r.max._subtract(a).subtract(o));a.contains(s)||(o=m((Math.max(a.max.x,s.x)-a.max.x)/(r.max.x-a.max.x)-(Math.min(a.min.x,s.x)-a.min.x)/(r.min.x-a.min.x),(Math.max(a.max.y,s.y)-a.max.y)/(r.max.y-a.max.y)-(Math.min(a.min.y,s.y)-a.min.y)/(r.min.y-a.min.y)).multiplyBy(n),i.panBy(o,{animate:!1}),this._draggable._newPos._add(o),this._draggable._startPos._add(o),Z(e._icon,this._draggable._newPos),this._onDrag(t),this._panRequest=x(this._adjustPan.bind(this,t)))},_onDragStart:function(){this._oldLatLng=this._marker.getLatLng(),this._marker.closePopup&&this._marker.closePopup(),this._marker.fire("movestart").fire("dragstart")},_onPreDrag:function(t){this._marker.options.autoPan&&(r(this._panRequest),this._panRequest=x(this._adjustPan.bind(this,t)))},_onDrag:function(t){var e=this._marker,i=e._shadow,n=Pe(e._icon),o=e._map.layerPointToLatLng(n);i&&Z(i,n),e._latlng=o,t.latlng=o,t.oldLatLng=this._oldLatLng,e.fire("move",t).fire("drag",t)},_onDragEnd:function(t){r(this._panRequest),delete this._oldLatLng,this._marker.fire("moveend").fire("dragend",t)}}),mi=o.extend({options:{icon:new _i,interactive:!0,keyboard:!0,title:"",alt:"Marker",zIndexOffset:0,opacity:1,riseOnHover:!1,riseOffset:250,pane:"markerPane",shadowPane:"shadowPane",bubblingMouseEvents:!1,autoPanOnFocus:!0,draggable:!1,autoPan:!1,autoPanPadding:[50,50],autoPanSpeed:10},initialize:function(t,e){c(this,e),this._latlng=w(t)},onAdd:function(t){this._zoomAnimated=this._zoomAnimated&&t.options.markerZoomAnimation,this._zoomAnimated&&t.on("zoomanim",this._animateZoom,this),this._initIcon(),this.update()},onRemove:function(t){this.dragging&&this.dragging.enabled()&&(this.options.draggable=!0,this.dragging.removeHooks()),delete this.dragging,this._zoomAnimated&&t.off("zoomanim",this._animateZoom,this),this._removeIcon(),this._removeShadow()},getEvents:function(){return{zoom:this.update,viewreset:this.update}},getLatLng:function(){return this._latlng},setLatLng:function(t){var e=this._latlng;return this._latlng=w(t),this.update(),this.fire("move",{oldLatLng:e,latlng:this._latlng})},setZIndexOffset:function(t){return this.options.zIndexOffset=t,this.update()},getIcon:function(){return this.options.icon},setIcon:function(t){return this.options.icon=t,this._map&&(this._initIcon(),this.update()),this._popup&&this.bindPopup(this._popup,this._popup.options),this},getElement:function(){return this._icon},update:function(){var t;return this._icon&&this._map&&(t=this._map.latLngToLayerPoint(this._latlng).round(),this._setPos(t)),this},_initIcon:function(){var t=this.options,e="leaflet-zoom-"+(this._zoomAnimated?"animated":"hide"),i=t.icon.createIcon(this._icon),n=!1,i=(i!==this._icon&&(this._icon&&this._removeIcon(),n=!0,t.title&&(i.title=t.title),"IMG"===i.tagName&&(i.alt=t.alt||"")),M(i,e),t.keyboard&&(i.tabIndex="0",i.setAttribute("role","button")),this._icon=i,t.riseOnHover&&this.on({mouseover:this._bringToFront,mouseout:this._resetZIndex}),this.options.autoPanOnFocus&&S(i,"focus",this._panOnFocus,this),t.icon.createShadow(this._shadow)),o=!1;i!==this._shadow&&(this._removeShadow(),o=!0),i&&(M(i,e),i.alt=""),this._shadow=i,t.opacity<1&&this._updateOpacity(),n&&this.getPane().appendChild(this._icon),this._initInteraction(),i&&o&&this.getPane(t.shadowPane).appendChild(this._shadow)},_removeIcon:function(){this.options.riseOnHover&&this.off({mouseover:this._bringToFront,mouseout:this._resetZIndex}),this.options.autoPanOnFocus&&k(this._icon,"focus",this._panOnFocus,this),T(this._icon),this.removeInteractiveTarget(this._icon),this._icon=null},_removeShadow:function(){this._shadow&&T(this._shadow),this._shadow=null},_setPos:function(t){this._icon&&Z(this._icon,t),this._shadow&&Z(this._shadow,t),this._zIndex=t.y+this.options.zIndexOffset,this._resetZIndex()},_updateZIndex:function(t){this._icon&&(this._icon.style.zIndex=this._zIndex+t)},_animateZoom:function(t){t=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center).round();this._setPos(t)},_initInteraction:function(){var t;this.options.interactive&&(M(this._icon,"leaflet-interactive"),this.addInteractiveTarget(this._icon),pi&&(t=this.options.draggable,this.dragging&&(t=this.dragging.enabled(),this.dragging.disable()),this.dragging=new pi(this),t&&this.dragging.enable()))},setOpacity:function(t){return this.options.opacity=t,this._map&&this._updateOpacity(),this},_updateOpacity:function(){var t=this.options.opacity;this._icon&&C(this._icon,t),this._shadow&&C(this._shadow,t)},_bringToFront:function(){this._updateZIndex(this.options.riseOffset)},_resetZIndex:function(){this._updateZIndex(0)},_panOnFocus:function(){var t,e,i=this._map;i&&(t=(e=this.options.icon.options).iconSize?m(e.iconSize):m(0,0),e=e.iconAnchor?m(e.iconAnchor):m(0,0),i.panInside(this._latlng,{paddingTopLeft:e,paddingBottomRight:t.subtract(e)}))},_getPopupAnchor:function(){return this.options.icon.options.popupAnchor},_getTooltipAnchor:function(){return this.options.icon.options.tooltipAnchor}});var fi=o.extend({options:{stroke:!0,color:"#3388ff",weight:3,opacity:1,lineCap:"round",lineJoin:"round",dashArray:null,dashOffset:null,fill:!1,fillColor:null,fillOpacity:.2,fillRule:"evenodd",interactive:!0,bubblingMouseEvents:!0},beforeAdd:function(t){this._renderer=t.getRenderer(this)},onAdd:function(){this._renderer._initPath(this),this._reset(),this._renderer._addPath(this)},onRemove:function(){this._renderer._removePath(this)},redraw:function(){return this._map&&this._renderer._updatePath(this),this},setStyle:function(t){return c(this,t),this._renderer&&(this._renderer._updateStyle(this),this.options.stroke&&t&&Object.prototype.hasOwnProperty.call(t,"weight")&&this._updateBounds()),this},bringToFront:function(){return this._renderer&&this._renderer._bringToFront(this),this},bringToBack:function(){return this._renderer&&this._renderer._bringToBack(this),this},getElement:function(){return this._path},_reset:function(){this._project(),this._update()},_clickTolerance:function(){return(this.options.stroke?this.options.weight/2:0)+(this._renderer.options.tolerance||0)}}),gi=fi.extend({options:{fill:!0,radius:10},initialize:function(t,e){c(this,e),this._latlng=w(t),this._radius=this.options.radius},setLatLng:function(t){var e=this._latlng;return this._latlng=w(t),this.redraw(),this.fire("move",{oldLatLng:e,latlng:this._latlng})},getLatLng:function(){return this._latlng},setRadius:function(t){return this.options.radius=this._radius=t,this.redraw()},getRadius:function(){return this._radius},setStyle:function(t){var e=t&&t.radius||this._radius;return fi.prototype.setStyle.call(this,t),this.setRadius(e),this},_project:function(){this._point=this._map.latLngToLayerPoint(this._latlng),this._updateBounds()},_updateBounds:function(){var t=this._radius,e=this._radiusY||t,i=this._clickTolerance(),t=[t+i,e+i];this._pxBounds=new f(this._point.subtract(t),this._point.add(t))},_update:function(){this._map&&this._updatePath()},_updatePath:function(){this._renderer._updateCircle(this)},_empty:function(){return this._radius&&!this._renderer._bounds.intersects(this._pxBounds)},_containsPoint:function(t){return t.distanceTo(this._point)<=this._radius+this._clickTolerance()}});var vi=gi.extend({initialize:function(t,e,i){if(c(this,e="number"==typeof e?l({},i,{radius:e}):e),this._latlng=w(t),isNaN(this.options.radius))throw new Error("Circle radius cannot be NaN");this._mRadius=this.options.radius},setRadius:function(t){return this._mRadius=t,this.redraw()},getRadius:function(){return this._mRadius},getBounds:function(){var t=[this._radius,this._radiusY||this._radius];return new s(this._map.layerPointToLatLng(this._point.subtract(t)),this._map.layerPointToLatLng(this._point.add(t)))},setStyle:fi.prototype.setStyle,_project:function(){var t,e,i,n,o,s=this._latlng.lng,r=this._latlng.lat,a=this._map,h=a.options.crs;h.distance===st.distance?(n=Math.PI/180,o=this._mRadius/st.R/n,t=a.project([r+o,s]),e=a.project([r-o,s]),e=t.add(e).divideBy(2),i=a.unproject(e).lat,n=Math.acos((Math.cos(o*n)-Math.sin(r*n)*Math.sin(i*n))/(Math.cos(r*n)*Math.cos(i*n)))/n,!isNaN(n)&&0!==n||(n=o/Math.cos(Math.PI/180*r)),this._point=e.subtract(a.getPixelOrigin()),this._radius=isNaN(n)?0:e.x-a.project([i,s-n]).x,this._radiusY=e.y-t.y):(o=h.unproject(h.project(this._latlng).subtract([this._mRadius,0])),this._point=a.latLngToLayerPoint(this._latlng),this._radius=this._point.x-a.latLngToLayerPoint(o).x),this._updateBounds()}});var yi=fi.extend({options:{smoothFactor:1,noClip:!1},initialize:function(t,e){c(this,e),this._setLatLngs(t)},getLatLngs:function(){return this._latlngs},setLatLngs:function(t){return this._setLatLngs(t),this.redraw()},isEmpty:function(){return!this._latlngs.length},closestLayerPoint:function(t){for(var e=1/0,i=null,n=ri,o=0,s=this._parts.length;o<s;o++)for(var r=this._parts[o],a=1,h=r.length;a<h;a++){var l,u,c=n(t,l=r[a-1],u=r[a],!0);c<e&&(e=c,i=n(t,l,u))}return i&&(i.distance=Math.sqrt(e)),i},getCenter:function(){if(this._map)return hi(this._defaultShape(),this._map.options.crs);throw new Error("Must add layer to map before using getCenter()")},getBounds:function(){return this._bounds},addLatLng:function(t,e){return e=e||this._defaultShape(),t=w(t),e.push(t),this._bounds.extend(t),this.redraw()},_setLatLngs:function(t){this._bounds=new s,this._latlngs=this._convertLatLngs(t)},_defaultShape:function(){return I(this._latlngs)?this._latlngs:this._latlngs[0]},_convertLatLngs:function(t){for(var e=[],i=I(t),n=0,o=t.length;n<o;n++)i?(e[n]=w(t[n]),this._bounds.extend(e[n])):e[n]=this._convertLatLngs(t[n]);return e},_project:function(){var t=new f;this._rings=[],this._projectLatlngs(this._latlngs,this._rings,t),this._bounds.isValid()&&t.isValid()&&(this._rawPxBounds=t,this._updateBounds())},_updateBounds:function(){var t=this._clickTolerance(),t=new p(t,t);this._rawPxBounds&&(this._pxBounds=new f([this._rawPxBounds.min.subtract(t),this._rawPxBounds.max.add(t)]))},_projectLatlngs:function(t,e,i){var n,o,s=t[0]instanceof v,r=t.length;if(s){for(o=[],n=0;n<r;n++)o[n]=this._map.latLngToLayerPoint(t[n]),i.extend(o[n]);e.push(o)}else for(n=0;n<r;n++)this._projectLatlngs(t[n],e,i)},_clipPoints:function(){var t=this._renderer._bounds;if(this._parts=[],this._pxBounds&&this._pxBounds.intersects(t))if(this.options.noClip)this._parts=this._rings;else for(var e,i,n,o,s=this._parts,r=0,a=0,h=this._rings.length;r<h;r++)for(e=0,i=(o=this._rings[r]).length;e<i-1;e++)(n=ni(o[e],o[e+1],t,e,!0))&&(s[a]=s[a]||[],s[a].push(n[0]),n[1]===o[e+1]&&e!==i-2||(s[a].push(n[1]),a++))},_simplifyPoints:function(){for(var t=this._parts,e=this.options.smoothFactor,i=0,n=t.length;i<n;i++)t[i]=ei(t[i],e)},_update:function(){this._map&&(this._clipPoints(),this._simplifyPoints(),this._updatePath())},_updatePath:function(){this._renderer._updatePoly(this)},_containsPoint:function(t,e){var i,n,o,s,r,a,h=this._clickTolerance();if(this._pxBounds&&this._pxBounds.contains(t))for(i=0,s=this._parts.length;i<s;i++)for(n=0,o=(r=(a=this._parts[i]).length)-1;n<r;o=n++)if((e||0!==n)&&ii(t,a[o],a[n])<=h)return!0;return!1}});yi._flat=ai;var xi=yi.extend({options:{fill:!0},isEmpty:function(){return!this._latlngs.length||!this._latlngs[0].length},getCenter:function(){if(this._map)return $e(this._defaultShape(),this._map.options.crs);throw new Error("Must add layer to map before using getCenter()")},_convertLatLngs:function(t){var t=yi.prototype._convertLatLngs.call(this,t),e=t.length;return 2<=e&&t[0]instanceof v&&t[0].equals(t[e-1])&&t.pop(),t},_setLatLngs:function(t){yi.prototype._setLatLngs.call(this,t),I(this._latlngs)&&(this._latlngs=[this._latlngs])},_defaultShape:function(){return(I(this._latlngs[0])?this._latlngs:this._latlngs[0])[0]},_clipPoints:function(){var t=this._renderer._bounds,e=this.options.weight,e=new p(e,e),t=new f(t.min.subtract(e),t.max.add(e));if(this._parts=[],this._pxBounds&&this._pxBounds.intersects(t))if(this.options.noClip)this._parts=this._rings;else for(var i,n=0,o=this._rings.length;n<o;n++)(i=Je(this._rings[n],t,!0)).length&&this._parts.push(i)},_updatePath:function(){this._renderer._updatePoly(this,!0)},_containsPoint:function(t){var e,i,n,o,s,r,a,h,l=!1;if(!this._pxBounds||!this._pxBounds.contains(t))return!1;for(o=0,a=this._parts.length;o<a;o++)for(s=0,r=(h=(e=this._parts[o]).length)-1;s<h;r=s++)i=e[s],n=e[r],i.y>t.y!=n.y>t.y&&t.x<(n.x-i.x)*(t.y-i.y)/(n.y-i.y)+i.x&&(l=!l);return l||yi.prototype._containsPoint.call(this,t,!0)}});var wi=ci.extend({initialize:function(t,e){c(this,e),this._layers={},t&&this.addData(t)},addData:function(t){var e,i,n,o=d(t)?t:t.features;if(o){for(e=0,i=o.length;e<i;e++)((n=o[e]).geometries||n.geometry||n.features||n.coordinates)&&this.addData(n);return this}var s,r=this.options;return(!r.filter||r.filter(t))&&(s=bi(t,r))?(s.feature=Zi(t),s.defaultOptions=s.options,this.resetStyle(s),r.onEachFeature&&r.onEachFeature(t,s),this.addLayer(s)):this},resetStyle:function(t){return void 0===t?this.eachLayer(this.resetStyle,this):(t.options=l({},t.defaultOptions),this._setLayerStyle(t,this.options.style),this)},setStyle:function(e){return this.eachLayer(function(t){this._setLayerStyle(t,e)},this)},_setLayerStyle:function(t,e){t.setStyle&&("function"==typeof e&&(e=e(t.feature)),t.setStyle(e))}});function bi(t,e){var i,n,o,s,r="Feature"===t.type?t.geometry:t,a=r?r.coordinates:null,h=[],l=e&&e.pointToLayer,u=e&&e.coordsToLatLng||Li;if(!a&&!r)return null;switch(r.type){case"Point":return Pi(l,t,i=u(a),e);case"MultiPoint":for(o=0,s=a.length;o<s;o++)i=u(a[o]),h.push(Pi(l,t,i,e));return new ci(h);case"LineString":case"MultiLineString":return n=Ti(a,"LineString"===r.type?0:1,u),new yi(n,e);case"Polygon":case"MultiPolygon":return n=Ti(a,"Polygon"===r.type?1:2,u),new xi(n,e);case"GeometryCollection":for(o=0,s=r.geometries.length;o<s;o++){var c=bi({geometry:r.geometries[o],type:"Feature",properties:t.properties},e);c&&h.push(c)}return new ci(h);case"FeatureCollection":for(o=0,s=r.features.length;o<s;o++){var d=bi(r.features[o],e);d&&h.push(d)}return new ci(h);default:throw new Error("Invalid GeoJSON object.")}}function Pi(t,e,i,n){return t?t(e,i):new mi(i,n&&n.markersInheritOptions&&n)}function Li(t){return new v(t[1],t[0],t[2])}function Ti(t,e,i){for(var n,o=[],s=0,r=t.length;s<r;s++)n=e?Ti(t[s],e-1,i):(i||Li)(t[s]),o.push(n);return o}function Mi(t,e){return void 0!==(t=w(t)).alt?[i(t.lng,e),i(t.lat,e),i(t.alt,e)]:[i(t.lng,e),i(t.lat,e)]}function zi(t,e,i,n){for(var o=[],s=0,r=t.length;s<r;s++)o.push(e?zi(t[s],I(t[s])?0:e-1,i,n):Mi(t[s],n));return!e&&i&&0<o.length&&o.push(o[0].slice()),o}function Ci(t,e){return t.feature?l({},t.feature,{geometry:e}):Zi(e)}function Zi(t){return"Feature"===t.type||"FeatureCollection"===t.type?t:{type:"Feature",properties:{},geometry:t}}Tt={toGeoJSON:function(t){return Ci(this,{type:"Point",coordinates:Mi(this.getLatLng(),t)})}};function Si(t,e){return new wi(t,e)}mi.include(Tt),vi.include(Tt),gi.include(Tt),yi.include({toGeoJSON:function(t){var e=!I(this._latlngs);return Ci(this,{type:(e?"Multi":"")+"LineString",coordinates:zi(this._latlngs,e?1:0,!1,t)})}}),xi.include({toGeoJSON:function(t){var e=!I(this._latlngs),i=e&&!I(this._latlngs[0]),t=zi(this._latlngs,i?2:e?1:0,!0,t);return Ci(this,{type:(i?"Multi":"")+"Polygon",coordinates:t=e?t:[t]})}}),ui.include({toMultiPoint:function(e){var i=[];return this.eachLayer(function(t){i.push(t.toGeoJSON(e).geometry.coordinates)}),Ci(this,{type:"MultiPoint",coordinates:i})},toGeoJSON:function(e){var i,n,t=this.feature&&this.feature.geometry&&this.feature.geometry.type;return"MultiPoint"===t?this.toMultiPoint(e):(i="GeometryCollection"===t,n=[],this.eachLayer(function(t){t.toGeoJSON&&(t=t.toGeoJSON(e),i?n.push(t.geometry):"FeatureCollection"===(t=Zi(t)).type?n.push.apply(n,t.features):n.push(t))}),i?Ci(this,{geometries:n,type:"GeometryCollection"}):{type:"FeatureCollection",features:n})}});var Mt=Si,Ei=o.extend({options:{opacity:1,alt:"",interactive:!1,crossOrigin:!1,errorOverlayUrl:"",zIndex:1,className:""},initialize:function(t,e,i){this._url=t,this._bounds=g(e),c(this,i)},onAdd:function(){this._image||(this._initImage(),this.options.opacity<1&&this._updateOpacity()),this.options.interactive&&(M(this._image,"leaflet-interactive"),this.addInteractiveTarget(this._image)),this.getPane().appendChild(this._image),this._reset()},onRemove:function(){T(this._image),this.options.interactive&&this.removeInteractiveTarget(this._image)},setOpacity:function(t){return this.options.opacity=t,this._image&&this._updateOpacity(),this},setStyle:function(t){return t.opacity&&this.setOpacity(t.opacity),this},bringToFront:function(){return this._map&&fe(this._image),this},bringToBack:function(){return this._map&&ge(this._image),this},setUrl:function(t){return this._url=t,this._image&&(this._image.src=t),this},setBounds:function(t){return this._bounds=g(t),this._map&&this._reset(),this},getEvents:function(){var t={zoom:this._reset,viewreset:this._reset};return this._zoomAnimated&&(t.zoomanim=this._animateZoom),t},setZIndex:function(t){return this.options.zIndex=t,this._updateZIndex(),this},getBounds:function(){return this._bounds},getElement:function(){return this._image},_initImage:function(){var t="IMG"===this._url.tagName,e=this._image=t?this._url:P("img");M(e,"leaflet-image-layer"),this._zoomAnimated&&M(e,"leaflet-zoom-animated"),this.options.className&&M(e,this.options.className),e.onselectstart=u,e.onmousemove=u,e.onload=a(this.fire,this,"load"),e.onerror=a(this._overlayOnError,this,"error"),!this.options.crossOrigin&&""!==this.options.crossOrigin||(e.crossOrigin=!0===this.options.crossOrigin?"":this.options.crossOrigin),this.options.zIndex&&this._updateZIndex(),t?this._url=e.src:(e.src=this._url,e.alt=this.options.alt)},_animateZoom:function(t){var e=this._map.getZoomScale(t.zoom),t=this._map._latLngBoundsToNewLayerBounds(this._bounds,t.zoom,t.center).min;be(this._image,t,e)},_reset:function(){var t=this._image,e=new f(this._map.latLngToLayerPoint(this._bounds.getNorthWest()),this._map.latLngToLayerPoint(this._bounds.getSouthEast())),i=e.getSize();Z(t,e.min),t.style.width=i.x+"px",t.style.height=i.y+"px"},_updateOpacity:function(){C(this._image,this.options.opacity)},_updateZIndex:function(){this._image&&void 0!==this.options.zIndex&&null!==this.options.zIndex&&(this._image.style.zIndex=this.options.zIndex)},_overlayOnError:function(){this.fire("error");var t=this.options.errorOverlayUrl;t&&this._url!==t&&(this._url=t,this._image.src=t)},getCenter:function(){return this._bounds.getCenter()}}),ki=Ei.extend({options:{autoplay:!0,loop:!0,keepAspectRatio:!0,muted:!1,playsInline:!0},_initImage:function(){var t="VIDEO"===this._url.tagName,e=this._image=t?this._url:P("video");if(M(e,"leaflet-image-layer"),this._zoomAnimated&&M(e,"leaflet-zoom-animated"),this.options.className&&M(e,this.options.className),e.onselectstart=u,e.onmousemove=u,e.onloadeddata=a(this.fire,this,"load"),t){for(var i=e.getElementsByTagName("source"),n=[],o=0;o<i.length;o++)n.push(i[o].src);this._url=0<i.length?n:[e.src]}else{d(this._url)||(this._url=[this._url]),!this.options.keepAspectRatio&&Object.prototype.hasOwnProperty.call(e.style,"objectFit")&&(e.style.objectFit="fill"),e.autoplay=!!this.options.autoplay,e.loop=!!this.options.loop,e.muted=!!this.options.muted,e.playsInline=!!this.options.playsInline;for(var s=0;s<this._url.length;s++){var r=P("source");r.src=this._url[s],e.appendChild(r)}}}});var Oi=Ei.extend({_initImage:function(){var t=this._image=this._url;M(t,"leaflet-image-layer"),this._zoomAnimated&&M(t,"leaflet-zoom-animated"),this.options.className&&M(t,this.options.className),t.onselectstart=u,t.onmousemove=u}});var Ai=o.extend({options:{interactive:!1,offset:[0,0],className:"",pane:void 0,content:""},initialize:function(t,e){t&&(t instanceof v||d(t))?(this._latlng=w(t),c(this,e)):(c(this,t),this._source=e),this.options.content&&(this._content=this.options.content)},openOn:function(t){return(t=arguments.length?t:this._source._map).hasLayer(this)||t.addLayer(this),this},close:function(){return this._map&&this._map.removeLayer(this),this},toggle:function(t){return this._map?this.close():(arguments.length?this._source=t:t=this._source,this._prepareOpen(),this.openOn(t._map)),this},onAdd:function(t){this._zoomAnimated=t._zoomAnimated,this._container||this._initLayout(),t._fadeAnimated&&C(this._container,0),clearTimeout(this._removeTimeout),this.getPane().appendChild(this._container),this.update(),t._fadeAnimated&&C(this._container,1),this.bringToFront(),this.options.interactive&&(M(this._container,"leaflet-interactive"),this.addInteractiveTarget(this._container))},onRemove:function(t){t._fadeAnimated?(C(this._container,0),this._removeTimeout=setTimeout(a(T,void 0,this._container),200)):T(this._container),this.options.interactive&&(z(this._container,"leaflet-interactive"),this.removeInteractiveTarget(this._container))},getLatLng:function(){return this._latlng},setLatLng:function(t){return this._latlng=w(t),this._map&&(this._updatePosition(),this._adjustPan()),this},getContent:function(){return this._content},setContent:function(t){return this._content=t,this.update(),this},getElement:function(){return this._container},update:function(){this._map&&(this._container.style.visibility="hidden",this._updateContent(),this._updateLayout(),this._updatePosition(),this._container.style.visibility="",this._adjustPan())},getEvents:function(){var t={zoom:this._updatePosition,viewreset:this._updatePosition};return this._zoomAnimated&&(t.zoomanim=this._animateZoom),t},isOpen:function(){return!!this._map&&this._map.hasLayer(this)},bringToFront:function(){return this._map&&fe(this._container),this},bringToBack:function(){return this._map&&ge(this._container),this},_prepareOpen:function(t){if(!(i=this._source)._map)return!1;if(i instanceof ci){var e,i=null,n=this._source._layers;for(e in n)if(n[e]._map){i=n[e];break}if(!i)return!1;this._source=i}if(!t)if(i.getCenter)t=i.getCenter();else if(i.getLatLng)t=i.getLatLng();else{if(!i.getBounds)throw new Error("Unable to get source layer LatLng.");t=i.getBounds().getCenter()}return this.setLatLng(t),this._map&&this.update(),!0},_updateContent:function(){if(this._content){var t=this._contentNode,e="function"==typeof this._content?this._content(this._source||this):this._content;if("string"==typeof e)t.innerHTML=e;else{for(;t.hasChildNodes();)t.removeChild(t.firstChild);t.appendChild(e)}this.fire("contentupdate")}},_updatePosition:function(){var t,e,i;this._map&&(e=this._map.latLngToLayerPoint(this._latlng),t=m(this.options.offset),i=this._getAnchor(),this._zoomAnimated?Z(this._container,e.add(i)):t=t.add(e).add(i),e=this._containerBottom=-t.y,i=this._containerLeft=-Math.round(this._containerWidth/2)+t.x,this._container.style.bottom=e+"px",this._container.style.left=i+"px")},_getAnchor:function(){return[0,0]}}),Bi=(A.include({_initOverlay:function(t,e,i,n){var o=e;return o instanceof t||(o=new t(n).setContent(e)),i&&o.setLatLng(i),o}}),o.include({_initOverlay:function(t,e,i,n){var o=i;return o instanceof t?(c(o,n),o._source=this):(o=e&&!n?e:new t(n,this)).setContent(i),o}}),Ai.extend({options:{pane:"popupPane",offset:[0,7],maxWidth:300,minWidth:50,maxHeight:null,autoPan:!0,autoPanPaddingTopLeft:null,autoPanPaddingBottomRight:null,autoPanPadding:[5,5],keepInView:!1,closeButton:!0,autoClose:!0,closeOnEscapeKey:!0,className:""},openOn:function(t){return!(t=arguments.length?t:this._source._map).hasLayer(this)&&t._popup&&t._popup.options.autoClose&&t.removeLayer(t._popup),t._popup=this,Ai.prototype.openOn.call(this,t)},onAdd:function(t){Ai.prototype.onAdd.call(this,t),t.fire("popupopen",{popup:this}),this._source&&(this._source.fire("popupopen",{popup:this},!0),this._source instanceof fi||this._source.on("preclick",Ae))},onRemove:function(t){Ai.prototype.onRemove.call(this,t),t.fire("popupclose",{popup:this}),this._source&&(this._source.fire("popupclose",{popup:this},!0),this._source instanceof fi||this._source.off("preclick",Ae))},getEvents:function(){var t=Ai.prototype.getEvents.call(this);return(void 0!==this.options.closeOnClick?this.options.closeOnClick:this._map.options.closePopupOnClick)&&(t.preclick=this.close),this.options.keepInView&&(t.moveend=this._adjustPan),t},_initLayout:function(){var t="leaflet-popup",e=this._container=P("div",t+" "+(this.options.className||"")+" leaflet-zoom-animated"),i=this._wrapper=P("div",t+"-content-wrapper",e);this._contentNode=P("div",t+"-content",i),Ie(e),Be(this._contentNode),S(e,"contextmenu",Ae),this._tipContainer=P("div",t+"-tip-container",e),this._tip=P("div",t+"-tip",this._tipContainer),this.options.closeButton&&((i=this._closeButton=P("a",t+"-close-button",e)).setAttribute("role","button"),i.setAttribute("aria-label","Close popup"),i.href="#close",i.innerHTML='<span aria-hidden="true">&#215;</span>',S(i,"click",function(t){O(t),this.close()},this))},_updateLayout:function(){var t=this._contentNode,e=t.style,i=(e.width="",e.whiteSpace="nowrap",t.offsetWidth),i=Math.min(i,this.options.maxWidth),i=(i=Math.max(i,this.options.minWidth),e.width=i+1+"px",e.whiteSpace="",e.height="",t.offsetHeight),n=this.options.maxHeight,o="leaflet-popup-scrolled";(n&&n<i?(e.height=n+"px",M):z)(t,o),this._containerWidth=this._container.offsetWidth},_animateZoom:function(t){var t=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center),e=this._getAnchor();Z(this._container,t.add(e))},_adjustPan:function(){var t,e,i,n,o,s,r,a;this.options.autoPan&&(this._map._panAnim&&this._map._panAnim.stop(),this._autopanning?this._autopanning=!1:(t=this._map,e=parseInt(pe(this._container,"marginBottom"),10)||0,e=this._container.offsetHeight+e,a=this._containerWidth,(i=new p(this._containerLeft,-e-this._containerBottom))._add(Pe(this._container)),i=t.layerPointToContainerPoint(i),o=m(this.options.autoPanPadding),n=m(this.options.autoPanPaddingTopLeft||o),o=m(this.options.autoPanPaddingBottomRight||o),s=t.getSize(),r=0,i.x+a+o.x>s.x&&(r=i.x+a-s.x+o.x),i.x-r-n.x<(a=0)&&(r=i.x-n.x),i.y+e+o.y>s.y&&(a=i.y+e-s.y+o.y),i.y-a-n.y<0&&(a=i.y-n.y),(r||a)&&(this.options.keepInView&&(this._autopanning=!0),t.fire("autopanstart").panBy([r,a]))))},_getAnchor:function(){return m(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}})),Ii=(A.mergeOptions({closePopupOnClick:!0}),A.include({openPopup:function(t,e,i){return this._initOverlay(Bi,t,e,i).openOn(this),this},closePopup:function(t){return(t=arguments.length?t:this._popup)&&t.close(),this}}),o.include({bindPopup:function(t,e){return this._popup=this._initOverlay(Bi,this._popup,t,e),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t){return this._popup&&(this instanceof ci||(this._popup._source=this),this._popup._prepareOpen(t||this._latlng)&&this._popup.openOn(this._map)),this},closePopup:function(){return this._popup&&this._popup.close(),this},togglePopup:function(){return this._popup&&this._popup.toggle(this),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var e;this._popup&&this._map&&(Re(t),e=t.layer||t.target,this._popup._source!==e||e instanceof fi?(this._popup._source=e,this.openPopup(t.latlng)):this._map.hasLayer(this._popup)?this.closePopup():this.openPopup(t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}}),Ai.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,opacity:.9},onAdd:function(t){Ai.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&(this.addEventParent(this._source),this._source.fire("tooltipopen",{tooltip:this},!0))},onRemove:function(t){Ai.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&(this.removeEventParent(this._source),this._source.fire("tooltipclose",{tooltip:this},!0))},getEvents:function(){var t=Ai.prototype.getEvents.call(this);return this.options.permanent||(t.preclick=this.close),t},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=P("div",t),this._container.setAttribute("role","tooltip"),this._container.setAttribute("id","leaflet-tooltip-"+h(this))},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var e,i=this._map,n=this._container,o=i.latLngToContainerPoint(i.getCenter()),i=i.layerPointToContainerPoint(t),s=this.options.direction,r=n.offsetWidth,a=n.offsetHeight,h=m(this.options.offset),l=this._getAnchor(),i="top"===s?(e=r/2,a):"bottom"===s?(e=r/2,0):(e="center"===s?r/2:"right"===s?0:"left"===s?r:i.x<o.x?(s="right",0):(s="left",r+2*(h.x+l.x)),a/2);t=t.subtract(m(e,i,!0)).add(h).add(l),z(n,"leaflet-tooltip-right"),z(n,"leaflet-tooltip-left"),z(n,"leaflet-tooltip-top"),z(n,"leaflet-tooltip-bottom"),M(n,"leaflet-tooltip-"+s),Z(n,t)},_updatePosition:function(){var t=this._map.latLngToLayerPoint(this._latlng);this._setPosition(t)},setOpacity:function(t){this.options.opacity=t,this._container&&C(this._container,t)},_animateZoom:function(t){t=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center);this._setPosition(t)},_getAnchor:function(){return m(this._source&&this._source._getTooltipAnchor&&!this.options.sticky?this._source._getTooltipAnchor():[0,0])}})),Ri=(A.include({openTooltip:function(t,e,i){return this._initOverlay(Ii,t,e,i).openOn(this),this},closeTooltip:function(t){return t.close(),this}}),o.include({bindTooltip:function(t,e){return this._tooltip&&this.isTooltipOpen()&&this.unbindTooltip(),this._tooltip=this._initOverlay(Ii,this._tooltip,t,e),this._initTooltipInteractions(),this._tooltip.options.permanent&&this._map&&this._map.hasLayer(this)&&this.openTooltip(),this},unbindTooltip:function(){return this._tooltip&&(this._initTooltipInteractions(!0),this.closeTooltip(),this._tooltip=null),this},_initTooltipInteractions:function(t){var e,i;!t&&this._tooltipHandlersAdded||(e=t?"off":"on",i={remove:this.closeTooltip,move:this._moveTooltip},this._tooltip.options.permanent?i.add=this._openTooltip:(i.mouseover=this._openTooltip,i.mouseout=this.closeTooltip,i.click=this._openTooltip,this._map?this._addFocusListeners():i.add=this._addFocusListeners),this._tooltip.options.sticky&&(i.mousemove=this._moveTooltip),this[e](i),this._tooltipHandlersAdded=!t)},openTooltip:function(t){return this._tooltip&&(this instanceof ci||(this._tooltip._source=this),this._tooltip._prepareOpen(t)&&(this._tooltip.openOn(this._map),this.getElement?this._setAriaDescribedByOnLayer(this):this.eachLayer&&this.eachLayer(this._setAriaDescribedByOnLayer,this))),this},closeTooltip:function(){if(this._tooltip)return this._tooltip.close()},toggleTooltip:function(){return this._tooltip&&this._tooltip.toggle(this),this},isTooltipOpen:function(){return this._tooltip.isOpen()},setTooltipContent:function(t){return this._tooltip&&this._tooltip.setContent(t),this},getTooltip:function(){return this._tooltip},_addFocusListeners:function(){this.getElement?this._addFocusListenersOnLayer(this):this.eachLayer&&this.eachLayer(this._addFocusListenersOnLayer,this)},_addFocusListenersOnLayer:function(t){var e="function"==typeof t.getElement&&t.getElement();e&&(S(e,"focus",function(){this._tooltip._source=t,this.openTooltip()},this),S(e,"blur",this.closeTooltip,this))},_setAriaDescribedByOnLayer:function(t){t="function"==typeof t.getElement&&t.getElement();t&&t.setAttribute("aria-describedby",this._tooltip._container.id)},_openTooltip:function(t){var e;this._tooltip&&this._map&&(this._map.dragging&&this._map.dragging.moving()&&!this._openOnceFlag?(this._openOnceFlag=!0,(e=this)._map.once("moveend",function(){e._openOnceFlag=!1,e._openTooltip(t)})):(this._tooltip._source=t.layer||t.target,this.openTooltip(this._tooltip.options.sticky?t.latlng:void 0)))},_moveTooltip:function(t){var e=t.latlng;this._tooltip.options.sticky&&t.originalEvent&&(t=this._map.mouseEventToContainerPoint(t.originalEvent),t=this._map.containerPointToLayerPoint(t),e=this._map.layerPointToLatLng(t)),this._tooltip.setLatLng(e)}}),di.extend({options:{iconSize:[12,12],html:!1,bgPos:null,className:"leaflet-div-icon"},createIcon:function(t){var t=t&&"DIV"===t.tagName?t:document.createElement("div"),e=this.options;return e.html instanceof Element?(me(t),t.appendChild(e.html)):t.innerHTML=!1!==e.html?e.html:"",e.bgPos&&(e=m(e.bgPos),t.style.backgroundPosition=-e.x+"px "+-e.y+"px"),this._setIconStyles(t,"icon"),t},createShadow:function(){return null}}));di.Default=_i;var Ni=o.extend({options:{tileSize:256,opacity:1,updateWhenIdle:b.mobile,updateWhenZooming:!0,updateInterval:200,zIndex:1,bounds:null,minZoom:0,maxZoom:void 0,maxNativeZoom:void 0,minNativeZoom:void 0,noWrap:!1,pane:"tilePane",className:"",keepBuffer:2},initialize:function(t){c(this,t)},onAdd:function(){this._initContainer(),this._levels={},this._tiles={},this._resetView()},beforeAdd:function(t){t._addZoomLimit(this)},onRemove:function(t){this._removeAllTiles(),T(this._container),t._removeZoomLimit(this),this._container=null,this._tileZoom=void 0},bringToFront:function(){return this._map&&(fe(this._container),this._setAutoZIndex(Math.max)),this},bringToBack:function(){return this._map&&(ge(this._container),this._setAutoZIndex(Math.min)),this},getContainer:function(){return this._container},setOpacity:function(t){return this.options.opacity=t,this._updateOpacity(),this},setZIndex:function(t){return this.options.zIndex=t,this._updateZIndex(),this},isLoading:function(){return this._loading},redraw:function(){var t;return this._map&&(this._removeAllTiles(),(t=this._clampZoom(this._map.getZoom()))!==this._tileZoom&&(this._tileZoom=t,this._updateLevels()),this._update()),this},getEvents:function(){var t={viewprereset:this._invalidateAll,viewreset:this._resetView,zoom:this._resetView,moveend:this._onMoveEnd};return this.options.updateWhenIdle||(this._onMove||(this._onMove=j(this._onMoveEnd,this.options.updateInterval,this)),t.move=this._onMove),this._zoomAnimated&&(t.zoomanim=this._animateZoom),t},createTile:function(){return document.createElement("div")},getTileSize:function(){var t=this.options.tileSize;return t instanceof p?t:new p(t,t)},_updateZIndex:function(){this._container&&void 0!==this.options.zIndex&&null!==this.options.zIndex&&(this._container.style.zIndex=this.options.zIndex)},_setAutoZIndex:function(t){for(var e,i=this.getPane().children,n=-t(-1/0,1/0),o=0,s=i.length;o<s;o++)e=i[o].style.zIndex,i[o]!==this._container&&e&&(n=t(n,+e));isFinite(n)&&(this.options.zIndex=n+t(-1,1),this._updateZIndex())},_updateOpacity:function(){if(this._map&&!b.ielt9){C(this._container,this.options.opacity);var t,e=+new Date,i=!1,n=!1;for(t in this._tiles){var o,s=this._tiles[t];s.current&&s.loaded&&(o=Math.min(1,(e-s.loaded)/200),C(s.el,o),o<1?i=!0:(s.active?n=!0:this._onOpaqueTile(s),s.active=!0))}n&&!this._noPrune&&this._pruneTiles(),i&&(r(this._fadeFrame),this._fadeFrame=x(this._updateOpacity,this))}},_onOpaqueTile:u,_initContainer:function(){this._container||(this._container=P("div","leaflet-layer "+(this.options.className||"")),this._updateZIndex(),this.options.opacity<1&&this._updateOpacity(),this.getPane().appendChild(this._container))},_updateLevels:function(){var t=this._tileZoom,e=this.options.maxZoom;if(void 0!==t){for(var i in this._levels)i=Number(i),this._levels[i].el.children.length||i===t?(this._levels[i].el.style.zIndex=e-Math.abs(t-i),this._onUpdateLevel(i)):(T(this._levels[i].el),this._removeTilesAtZoom(i),this._onRemoveLevel(i),delete this._levels[i]);var n=this._levels[t],o=this._map;return n||((n=this._levels[t]={}).el=P("div","leaflet-tile-container leaflet-zoom-animated",this._container),n.el.style.zIndex=e,n.origin=o.project(o.unproject(o.getPixelOrigin()),t).round(),n.zoom=t,this._setZoomTransform(n,o.getCenter(),o.getZoom()),u(n.el.offsetWidth),this._onCreateLevel(n)),this._level=n}},_onUpdateLevel:u,_onRemoveLevel:u,_onCreateLevel:u,_pruneTiles:function(){if(this._map){var t,e,i,n=this._map.getZoom();if(n>this.options.maxZoom||n<this.options.minZoom)this._removeAllTiles();else{for(t in this._tiles)(i=this._tiles[t]).retain=i.current;for(t in this._tiles)(i=this._tiles[t]).current&&!i.active&&(e=i.coords,this._retainParent(e.x,e.y,e.z,e.z-5)||this._retainChildren(e.x,e.y,e.z,e.z+2));for(t in this._tiles)this._tiles[t].retain||this._removeTile(t)}}},_removeTilesAtZoom:function(t){for(var e in this._tiles)this._tiles[e].coords.z===t&&this._removeTile(e)},_removeAllTiles:function(){for(var t in this._tiles)this._removeTile(t)},_invalidateAll:function(){for(var t in this._levels)T(this._levels[t].el),this._onRemoveLevel(Number(t)),delete this._levels[t];this._removeAllTiles(),this._tileZoom=void 0},_retainParent:function(t,e,i,n){var t=Math.floor(t/2),e=Math.floor(e/2),i=i-1,o=new p(+t,+e),o=(o.z=i,this._tileCoordsToKey(o)),o=this._tiles[o];return o&&o.active?o.retain=!0:(o&&o.loaded&&(o.retain=!0),n<i&&this._retainParent(t,e,i,n))},_retainChildren:function(t,e,i,n){for(var o=2*t;o<2*t+2;o++)for(var s=2*e;s<2*e+2;s++){var r=new p(o,s),r=(r.z=i+1,this._tileCoordsToKey(r)),r=this._tiles[r];r&&r.active?r.retain=!0:(r&&r.loaded&&(r.retain=!0),i+1<n&&this._retainChildren(o,s,i+1,n))}},_resetView:function(t){t=t&&(t.pinch||t.flyTo);this._setView(this._map.getCenter(),this._map.getZoom(),t,t)},_animateZoom:function(t){this._setView(t.center,t.zoom,!0,t.noUpdate)},_clampZoom:function(t){var e=this.options;return void 0!==e.minNativeZoom&&t<e.minNativeZoom?e.minNativeZoom:void 0!==e.maxNativeZoom&&e.maxNativeZoom<t?e.maxNativeZoom:t},_setView:function(t,e,i,n){var o=Math.round(e),o=void 0!==this.options.maxZoom&&o>this.options.maxZoom||void 0!==this.options.minZoom&&o<this.options.minZoom?void 0:this._clampZoom(o),s=this.options.updateWhenZooming&&o!==this._tileZoom;n&&!s||(this._tileZoom=o,this._abortLoading&&this._abortLoading(),this._updateLevels(),this._resetGrid(),void 0!==o&&this._update(t),i||this._pruneTiles(),this._noPrune=!!i),this._setZoomTransforms(t,e)},_setZoomTransforms:function(t,e){for(var i in this._levels)this._setZoomTransform(this._levels[i],t,e)},_setZoomTransform:function(t,e,i){var n=this._map.getZoomScale(i,t.zoom),e=t.origin.multiplyBy(n).subtract(this._map._getNewPixelOrigin(e,i)).round();b.any3d?be(t.el,e,n):Z(t.el,e)},_resetGrid:function(){var t=this._map,e=t.options.crs,i=this._tileSize=this.getTileSize(),n=this._tileZoom,o=this._map.getPixelWorldBounds(this._tileZoom);o&&(this._globalTileRange=this._pxBoundsToTileRange(o)),this._wrapX=e.wrapLng&&!this.options.noWrap&&[Math.floor(t.project([0,e.wrapLng[0]],n).x/i.x),Math.ceil(t.project([0,e.wrapLng[1]],n).x/i.y)],this._wrapY=e.wrapLat&&!this.options.noWrap&&[Math.floor(t.project([e.wrapLat[0],0],n).y/i.x),Math.ceil(t.project([e.wrapLat[1],0],n).y/i.y)]},_onMoveEnd:function(){this._map&&!this._map._animatingZoom&&this._update()},_getTiledPixelBounds:function(t){var e=this._map,i=e._animatingZoom?Math.max(e._animateToZoom,e.getZoom()):e.getZoom(),i=e.getZoomScale(i,this._tileZoom),t=e.project(t,this._tileZoom).floor(),e=e.getSize().divideBy(2*i);return new f(t.subtract(e),t.add(e))},_update:function(t){var e=this._map;if(e){var i=this._clampZoom(e.getZoom());if(void 0===t&&(t=e.getCenter()),void 0!==this._tileZoom){var n,e=this._getTiledPixelBounds(t),o=this._pxBoundsToTileRange(e),s=o.getCenter(),r=[],e=this.options.keepBuffer,a=new f(o.getBottomLeft().subtract([e,-e]),o.getTopRight().add([e,-e]));if(!(isFinite(o.min.x)&&isFinite(o.min.y)&&isFinite(o.max.x)&&isFinite(o.max.y)))throw new Error("Attempted to load an infinite number of tiles");for(n in this._tiles){var h=this._tiles[n].coords;h.z===this._tileZoom&&a.contains(new p(h.x,h.y))||(this._tiles[n].current=!1)}if(1<Math.abs(i-this._tileZoom))this._setView(t,i);else{for(var l=o.min.y;l<=o.max.y;l++)for(var u=o.min.x;u<=o.max.x;u++){var c,d=new p(u,l);d.z=this._tileZoom,this._isValidTile(d)&&((c=this._tiles[this._tileCoordsToKey(d)])?c.current=!0:r.push(d))}if(r.sort(function(t,e){return t.distanceTo(s)-e.distanceTo(s)}),0!==r.length){this._loading||(this._loading=!0,this.fire("loading"));for(var _=document.createDocumentFragment(),u=0;u<r.length;u++)this._addTile(r[u],_);this._level.el.appendChild(_)}}}}},_isValidTile:function(t){var e=this._map.options.crs;if(!e.infinite){var i=this._globalTileRange;if(!e.wrapLng&&(t.x<i.min.x||t.x>i.max.x)||!e.wrapLat&&(t.y<i.min.y||t.y>i.max.y))return!1}return!this.options.bounds||(e=this._tileCoordsToBounds(t),g(this.options.bounds).overlaps(e))},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var e=this._map,i=this.getTileSize(),n=t.scaleBy(i),i=n.add(i);return[e.unproject(n,t.z),e.unproject(i,t.z)]},_tileCoordsToBounds:function(t){t=this._tileCoordsToNwSe(t),t=new s(t[0],t[1]);return t=this.options.noWrap?t:this._map.wrapLatLngBounds(t)},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var t=t.split(":"),e=new p(+t[0],+t[1]);return e.z=+t[2],e},_removeTile:function(t){var e=this._tiles[t];e&&(T(e.el),delete this._tiles[t],this.fire("tileunload",{tile:e.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){M(t,"leaflet-tile");var e=this.getTileSize();t.style.width=e.x+"px",t.style.height=e.y+"px",t.onselectstart=u,t.onmousemove=u,b.ielt9&&this.options.opacity<1&&C(t,this.options.opacity)},_addTile:function(t,e){var i=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),a(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&x(a(this._tileReady,this,t,null,o)),Z(o,i),this._tiles[n]={el:o,coords:t,current:!0},e.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,e,i){e&&this.fire("tileerror",{error:e,tile:i,coords:t});var n=this._tileCoordsToKey(t);(i=this._tiles[n])&&(i.loaded=+new Date,this._map._fadeAnimated?(C(i.el,0),r(this._fadeFrame),this._fadeFrame=x(this._updateOpacity,this)):(i.active=!0,this._pruneTiles()),e||(M(i.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:i.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),b.ielt9||!this._map._fadeAnimated?x(this._pruneTiles,this):setTimeout(a(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var e=new p(this._wrapX?H(t.x,this._wrapX):t.x,this._wrapY?H(t.y,this._wrapY):t.y);return e.z=t.z,e},_pxBoundsToTileRange:function(t){var e=this.getTileSize();return new f(t.min.unscaleBy(e).floor(),t.max.unscaleBy(e).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var Di=Ni.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1,referrerPolicy:!1},initialize:function(t,e){this._url=t,(e=c(this,e)).detectRetina&&b.retina&&0<e.maxZoom?(e.tileSize=Math.floor(e.tileSize/2),e.zoomReverse?(e.zoomOffset--,e.minZoom=Math.min(e.maxZoom,e.minZoom+1)):(e.zoomOffset++,e.maxZoom=Math.max(e.minZoom,e.maxZoom-1)),e.minZoom=Math.max(0,e.minZoom)):e.zoomReverse?e.minZoom=Math.min(e.maxZoom,e.minZoom):e.maxZoom=Math.max(e.minZoom,e.maxZoom),"string"==typeof e.subdomains&&(e.subdomains=e.subdomains.split("")),this.on("tileunload",this._onTileRemove)},setUrl:function(t,e){return this._url===t&&void 0===e&&(e=!0),this._url=t,e||this.redraw(),this},createTile:function(t,e){var i=document.createElement("img");return S(i,"load",a(this._tileOnLoad,this,e,i)),S(i,"error",a(this._tileOnError,this,e,i)),!this.options.crossOrigin&&""!==this.options.crossOrigin||(i.crossOrigin=!0===this.options.crossOrigin?"":this.options.crossOrigin),"string"==typeof this.options.referrerPolicy&&(i.referrerPolicy=this.options.referrerPolicy),i.alt="",i.src=this.getTileUrl(t),i},getTileUrl:function(t){var e={r:b.retina?"@2x":"",s:this._getSubdomain(t),x:t.x,y:t.y,z:this._getZoomForUrl()};return this._map&&!this._map.options.crs.infinite&&(t=this._globalTileRange.max.y-t.y,this.options.tms&&(e.y=t),e["-y"]=t),q(this._url,l(e,this.options))},_tileOnLoad:function(t,e){b.ielt9?setTimeout(a(t,this,null,e),0):t(null,e)},_tileOnError:function(t,e,i){var n=this.options.errorTileUrl;n&&e.getAttribute("src")!==n&&(e.src=n),t(i,e)},_onTileRemove:function(t){t.tile.onload=null},_getZoomForUrl:function(){var t=this._tileZoom,e=this.options.maxZoom;return(t=this.options.zoomReverse?e-t:t)+this.options.zoomOffset},_getSubdomain:function(t){t=Math.abs(t.x+t.y)%this.options.subdomains.length;return this.options.subdomains[t]},_abortLoading:function(){var t,e,i;for(t in this._tiles)this._tiles[t].coords.z!==this._tileZoom&&((i=this._tiles[t].el).onload=u,i.onerror=u,i.complete||(i.src=K,e=this._tiles[t].coords,T(i),delete this._tiles[t],this.fire("tileabort",{tile:i,coords:e})))},_removeTile:function(t){var e=this._tiles[t];if(e)return e.el.setAttribute("src",K),Ni.prototype._removeTile.call(this,t)},_tileReady:function(t,e,i){if(this._map&&(!i||i.getAttribute("src")!==K))return Ni.prototype._tileReady.call(this,t,e,i)}});function ji(t,e){return new Di(t,e)}var Hi=Di.extend({defaultWmsParams:{service:"WMS",request:"GetMap",layers:"",styles:"",format:"image/jpeg",transparent:!1,version:"1.1.1"},options:{crs:null,uppercase:!1},initialize:function(t,e){this._url=t;var i,n=l({},this.defaultWmsParams);for(i in e)i in this.options||(n[i]=e[i]);var t=(e=c(this,e)).detectRetina&&b.retina?2:1,o=this.getTileSize();n.width=o.x*t,n.height=o.y*t,this.wmsParams=n},onAdd:function(t){this._crs=this.options.crs||t.options.crs,this._wmsVersion=parseFloat(this.wmsParams.version);var e=1.3<=this._wmsVersion?"crs":"srs";this.wmsParams[e]=this._crs.code,Di.prototype.onAdd.call(this,t)},getTileUrl:function(t){var e=this._tileCoordsToNwSe(t),i=this._crs,i=_(i.project(e[0]),i.project(e[1])),e=i.min,i=i.max,e=(1.3<=this._wmsVersion&&this._crs===li?[e.y,e.x,i.y,i.x]:[e.x,e.y,i.x,i.y]).join(","),i=Di.prototype.getTileUrl.call(this,t);return i+U(this.wmsParams,i,this.options.uppercase)+(this.options.uppercase?"&BBOX=":"&bbox=")+e},setParams:function(t,e){return l(this.wmsParams,t),e||this.redraw(),this}});Di.WMS=Hi,ji.wms=function(t,e){return new Hi(t,e)};var Wi=o.extend({options:{padding:.1},initialize:function(t){c(this,t),h(this),this._layers=this._layers||{}},onAdd:function(){this._container||(this._initContainer(),M(this._container,"leaflet-zoom-animated")),this.getPane().appendChild(this._container),this._update(),this.on("update",this._updatePaths,this)},onRemove:function(){this.off("update",this._updatePaths,this),this._destroyContainer()},getEvents:function(){var t={viewreset:this._reset,zoom:this._onZoom,moveend:this._update,zoomend:this._onZoomEnd};return this._zoomAnimated&&(t.zoomanim=this._onAnimZoom),t},_onAnimZoom:function(t){this._updateTransform(t.center,t.zoom)},_onZoom:function(){this._updateTransform(this._map.getCenter(),this._map.getZoom())},_updateTransform:function(t,e){var i=this._map.getZoomScale(e,this._zoom),n=this._map.getSize().multiplyBy(.5+this.options.padding),o=this._map.project(this._center,e),n=n.multiplyBy(-i).add(o).subtract(this._map._getNewPixelOrigin(t,e));b.any3d?be(this._container,n,i):Z(this._container,n)},_reset:function(){for(var t in this._update(),this._updateTransform(this._center,this._zoom),this._layers)this._layers[t]._reset()},_onZoomEnd:function(){for(var t in this._layers)this._layers[t]._project()},_updatePaths:function(){for(var t in this._layers)this._layers[t]._update()},_update:function(){var t=this.options.padding,e=this._map.getSize(),i=this._map.containerPointToLayerPoint(e.multiplyBy(-t)).round();this._bounds=new f(i,i.add(e.multiplyBy(1+2*t)).round()),this._center=this._map.getCenter(),this._zoom=this._map.getZoom()}}),Fi=Wi.extend({options:{tolerance:0},getEvents:function(){var t=Wi.prototype.getEvents.call(this);return t.viewprereset=this._onViewPreReset,t},_onViewPreReset:function(){this._postponeUpdatePaths=!0},onAdd:function(){Wi.prototype.onAdd.call(this),this._draw()},_initContainer:function(){var t=this._container=document.createElement("canvas");S(t,"mousemove",this._onMouseMove,this),S(t,"click dblclick mousedown mouseup contextmenu",this._onClick,this),S(t,"mouseout",this._handleMouseOut,this),t._leaflet_disable_events=!0,this._ctx=t.getContext("2d")},_destroyContainer:function(){r(this._redrawRequest),delete this._ctx,T(this._container),k(this._container),delete this._container},_updatePaths:function(){if(!this._postponeUpdatePaths){for(var t in this._redrawBounds=null,this._layers)this._layers[t]._update();this._redraw()}},_update:function(){var t,e,i,n;this._map._animatingZoom&&this._bounds||(Wi.prototype._update.call(this),t=this._bounds,e=this._container,i=t.getSize(),n=b.retina?2:1,Z(e,t.min),e.width=n*i.x,e.height=n*i.y,e.style.width=i.x+"px",e.style.height=i.y+"px",b.retina&&this._ctx.scale(2,2),this._ctx.translate(-t.min.x,-t.min.y),this.fire("update"))},_reset:function(){Wi.prototype._reset.call(this),this._postponeUpdatePaths&&(this._postponeUpdatePaths=!1,this._updatePaths())},_initPath:function(t){this._updateDashArray(t);t=(this._layers[h(t)]=t)._order={layer:t,prev:this._drawLast,next:null};this._drawLast&&(this._drawLast.next=t),this._drawLast=t,this._drawFirst=this._drawFirst||this._drawLast},_addPath:function(t){this._requestRedraw(t)},_removePath:function(t){var e=t._order,i=e.next,e=e.prev;i?i.prev=e:this._drawLast=e,e?e.next=i:this._drawFirst=i,delete t._order,delete this._layers[h(t)],this._requestRedraw(t)},_updatePath:function(t){this._extendRedrawBounds(t),t._project(),t._update(),this._requestRedraw(t)},_updateStyle:function(t){this._updateDashArray(t),this._requestRedraw(t)},_updateDashArray:function(t){if("string"==typeof t.options.dashArray){for(var e,i=t.options.dashArray.split(/[, ]+/),n=[],o=0;o<i.length;o++){if(e=Number(i[o]),isNaN(e))return;n.push(e)}t.options._dashArray=n}else t.options._dashArray=t.options.dashArray},_requestRedraw:function(t){this._map&&(this._extendRedrawBounds(t),this._redrawRequest=this._redrawRequest||x(this._redraw,this))},_extendRedrawBounds:function(t){var e;t._pxBounds&&(e=(t.options.weight||0)+1,this._redrawBounds=this._redrawBounds||new f,this._redrawBounds.extend(t._pxBounds.min.subtract([e,e])),this._redrawBounds.extend(t._pxBounds.max.add([e,e])))},_redraw:function(){this._redrawRequest=null,this._redrawBounds&&(this._redrawBounds.min._floor(),this._redrawBounds.max._ceil()),this._clear(),this._draw(),this._redrawBounds=null},_clear:function(){var t,e=this._redrawBounds;e?(t=e.getSize(),this._ctx.clearRect(e.min.x,e.min.y,t.x,t.y)):(this._ctx.save(),this._ctx.setTransform(1,0,0,1,0,0),this._ctx.clearRect(0,0,this._container.width,this._container.height),this._ctx.restore())},_draw:function(){var t,e,i=this._redrawBounds;this._ctx.save(),i&&(e=i.getSize(),this._ctx.beginPath(),this._ctx.rect(i.min.x,i.min.y,e.x,e.y),this._ctx.clip()),this._drawing=!0;for(var n=this._drawFirst;n;n=n.next)t=n.layer,(!i||t._pxBounds&&t._pxBounds.intersects(i))&&t._updatePath();this._drawing=!1,this._ctx.restore()},_updatePoly:function(t,e){if(this._drawing){var i,n,o,s,r=t._parts,a=r.length,h=this._ctx;if(a){for(h.beginPath(),i=0;i<a;i++){for(n=0,o=r[i].length;n<o;n++)s=r[i][n],h[n?"lineTo":"moveTo"](s.x,s.y);e&&h.closePath()}this._fillStroke(h,t)}}},_updateCircle:function(t){var e,i,n,o;this._drawing&&!t._empty()&&(e=t._point,i=this._ctx,n=Math.max(Math.round(t._radius),1),1!=(o=(Math.max(Math.round(t._radiusY),1)||n)/n)&&(i.save(),i.scale(1,o)),i.beginPath(),i.arc(e.x,e.y/o,n,0,2*Math.PI,!1),1!=o&&i.restore(),this._fillStroke(i,t))},_fillStroke:function(t,e){var i=e.options;i.fill&&(t.globalAlpha=i.fillOpacity,t.fillStyle=i.fillColor||i.color,t.fill(i.fillRule||"evenodd")),i.stroke&&0!==i.weight&&(t.setLineDash&&t.setLineDash(e.options&&e.options._dashArray||[]),t.globalAlpha=i.opacity,t.lineWidth=i.weight,t.strokeStyle=i.color,t.lineCap=i.lineCap,t.lineJoin=i.lineJoin,t.stroke())},_onClick:function(t){for(var e,i,n=this._map.mouseEventToLayerPoint(t),o=this._drawFirst;o;o=o.next)(e=o.layer).options.interactive&&e._containsPoint(n)&&(("click"===t.type||"preclick"===t.type)&&this._map._draggableMoved(e)||(i=e));this._fireEvent(!!i&&[i],t)},_onMouseMove:function(t){var e;!this._map||this._map.dragging.moving()||this._map._animatingZoom||(e=this._map.mouseEventToLayerPoint(t),this._handleMouseHover(t,e))},_handleMouseOut:function(t){var e=this._hoveredLayer;e&&(z(this._container,"leaflet-interactive"),this._fireEvent([e],t,"mouseout"),this._hoveredLayer=null,this._mouseHoverThrottled=!1)},_handleMouseHover:function(t,e){if(!this._mouseHoverThrottled){for(var i,n,o=this._drawFirst;o;o=o.next)(i=o.layer).options.interactive&&i._containsPoint(e)&&(n=i);n!==this._hoveredLayer&&(this._handleMouseOut(t),n&&(M(this._container,"leaflet-interactive"),this._fireEvent([n],t,"mouseover"),this._hoveredLayer=n)),this._fireEvent(!!this._hoveredLayer&&[this._hoveredLayer],t),this._mouseHoverThrottled=!0,setTimeout(a(function(){this._mouseHoverThrottled=!1},this),32)}},_fireEvent:function(t,e,i){this._map._fireDOMEvent(e,i||e.type,t)},_bringToFront:function(t){var e,i,n=t._order;n&&(e=n.next,i=n.prev,e&&((e.prev=i)?i.next=e:e&&(this._drawFirst=e),n.prev=this._drawLast,(this._drawLast.next=n).next=null,this._drawLast=n,this._requestRedraw(t)))},_bringToBack:function(t){var e,i,n=t._order;n&&(e=n.next,(i=n.prev)&&((i.next=e)?e.prev=i:i&&(this._drawLast=i),n.prev=null,n.next=this._drawFirst,this._drawFirst.prev=n,this._drawFirst=n,this._requestRedraw(t)))}});function Ui(t){return b.canvas?new Fi(t):null}var Vi=function(){try{return document.namespaces.add("lvml","urn:schemas-microsoft-com:vml"),function(t){return document.createElement("<lvml:"+t+' class="lvml">')}}catch(t){}return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}(),zt={_initContainer:function(){this._container=P("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(Wi.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var e=t._container=Vi("shape");M(e,"leaflet-vml-shape "+(this.options.className||"")),e.coordsize="1 1",t._path=Vi("path"),e.appendChild(t._path),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){var e=t._container;this._container.appendChild(e),t.options.interactive&&t.addInteractiveTarget(e)},_removePath:function(t){var e=t._container;T(e),t.removeInteractiveTarget(e),delete this._layers[h(t)]},_updateStyle:function(t){var e=t._stroke,i=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(e=e||(t._stroke=Vi("stroke")),o.appendChild(e),e.weight=n.weight+"px",e.color=n.color,e.opacity=n.opacity,n.dashArray?e.dashStyle=d(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):e.dashStyle="",e.endcap=n.lineCap.replace("butt","flat"),e.joinstyle=n.lineJoin):e&&(o.removeChild(e),t._stroke=null),n.fill?(i=i||(t._fill=Vi("fill")),o.appendChild(i),i.color=n.fillColor||n.color,i.opacity=n.fillOpacity):i&&(o.removeChild(i),t._fill=null)},_updateCircle:function(t){var e=t._point.round(),i=Math.round(t._radius),n=Math.round(t._radiusY||i);this._setPath(t,t._empty()?"M0 0":"AL "+e.x+","+e.y+" "+i+","+n+" 0,23592600")},_setPath:function(t,e){t._path.v=e},_bringToFront:function(t){fe(t._container)},_bringToBack:function(t){ge(t._container)}},qi=b.vml?Vi:ct,Gi=Wi.extend({_initContainer:function(){this._container=qi("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=qi("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){T(this._container),k(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_update:function(){var t,e,i;this._map._animatingZoom&&this._bounds||(Wi.prototype._update.call(this),e=(t=this._bounds).getSize(),i=this._container,this._svgSize&&this._svgSize.equals(e)||(this._svgSize=e,i.setAttribute("width",e.x),i.setAttribute("height",e.y)),Z(i,t.min),i.setAttribute("viewBox",[t.min.x,t.min.y,e.x,e.y].join(" ")),this.fire("update"))},_initPath:function(t){var e=t._path=qi("path");t.options.className&&M(e,t.options.className),t.options.interactive&&M(e,"leaflet-interactive"),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){T(t._path),t.removeInteractiveTarget(t._path),delete this._layers[h(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var e=t._path,t=t.options;e&&(t.stroke?(e.setAttribute("stroke",t.color),e.setAttribute("stroke-opacity",t.opacity),e.setAttribute("stroke-width",t.weight),e.setAttribute("stroke-linecap",t.lineCap),e.setAttribute("stroke-linejoin",t.lineJoin),t.dashArray?e.setAttribute("stroke-dasharray",t.dashArray):e.removeAttribute("stroke-dasharray"),t.dashOffset?e.setAttribute("stroke-dashoffset",t.dashOffset):e.removeAttribute("stroke-dashoffset")):e.setAttribute("stroke","none"),t.fill?(e.setAttribute("fill",t.fillColor||t.color),e.setAttribute("fill-opacity",t.fillOpacity),e.setAttribute("fill-rule",t.fillRule||"evenodd")):e.setAttribute("fill","none"))},_updatePoly:function(t,e){this._setPath(t,dt(t._parts,e))},_updateCircle:function(t){var e=t._point,i=Math.max(Math.round(t._radius),1),n="a"+i+","+(Math.max(Math.round(t._radiusY),1)||i)+" 0 1,0 ",e=t._empty()?"M0 0":"M"+(e.x-i)+","+e.y+n+2*i+",0 "+n+2*-i+",0 ";this._setPath(t,e)},_setPath:function(t,e){t._path.setAttribute("d",e)},_bringToFront:function(t){fe(t._path)},_bringToBack:function(t){ge(t._path)}});function Ki(t){return b.svg||b.vml?new Gi(t):null}b.vml&&Gi.include(zt),A.include({getRenderer:function(t){t=(t=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer)||(this._renderer=this._createRenderer());return this.hasLayer(t)||this.addLayer(t),t},_getPaneRenderer:function(t){var e;return"overlayPane"!==t&&void 0!==t&&(void 0===(e=this._paneRenderers[t])&&(e=this._createRenderer({pane:t}),this._paneRenderers[t]=e),e)},_createRenderer:function(t){return this.options.preferCanvas&&Ui(t)||Ki(t)}});var Yi=xi.extend({initialize:function(t,e){xi.prototype.initialize.call(this,this._boundsToLatLngs(t),e)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=g(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Gi.create=qi,Gi.pointsToPath=dt,wi.geometryToLayer=bi,wi.coordsToLatLng=Li,wi.coordsToLatLngs=Ti,wi.latLngToCoords=Mi,wi.latLngsToCoords=zi,wi.getFeature=Ci,wi.asFeature=Zi,A.mergeOptions({boxZoom:!0});var _t=n.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){S(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){k(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){T(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),re(),Le(),this._startPoint=this._map.mouseEventToContainerPoint(t),S(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=P("div","leaflet-zoom-box",this._container),M(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var t=new f(this._point,this._startPoint),e=t.getSize();Z(this._box,t.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(T(this._box),z(this._container,"leaflet-crosshair")),ae(),Te(),k(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){1!==t.which&&1!==t.button||(this._finish(),this._moved&&(this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(a(this._resetState,this),0),t=new s(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point)),this._map.fitBounds(t).fire("boxzoomend",{boxZoomBounds:t})))},_onKeyDown:function(t){27===t.keyCode&&(this._finish(),this._clearDeferredResetState(),this._resetState())}}),Ct=(A.addInitHook("addHandler","boxZoom",_t),A.mergeOptions({doubleClickZoom:!0}),n.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var e=this._map,i=e.getZoom(),n=e.options.zoomDelta,i=t.originalEvent.shiftKey?i-n:i+n;"center"===e.options.doubleClickZoom?e.setZoom(i):e.setZoomAround(t.containerPoint,i)}})),Zt=(A.addInitHook("addHandler","doubleClickZoom",Ct),A.mergeOptions({dragging:!0,inertia:!0,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0}),n.extend({addHooks:function(){var t;this._draggable||(t=this._map,this._draggable=new Xe(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))),M(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){z(this._map._container,"leaflet-grab"),z(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t,e=this._map;e._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity?(t=g(this._map.options.maxBounds),this._offsetLimit=_(this._map.latLngToContainerPoint(t.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(t.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))):this._offsetLimit=null,e.fire("movestart").fire("dragstart"),e.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){var e,i;this._map.options.inertia&&(e=this._lastTime=+new Date,i=this._lastPos=this._draggable._absPos||this._draggable._newPos,this._positions.push(i),this._times.push(e),this._prunePositions(e)),this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1<this._positions.length&&50<t-this._times[0];)this._positions.shift(),this._times.shift()},_onZoomEnd:function(){var t=this._map.getSize().divideBy(2),e=this._map.latLngToLayerPoint([0,0]);this._initialWorldOffset=e.subtract(t).x,this._worldWidth=this._map.getPixelWorldBounds().getSize().x},_viscousLimit:function(t,e){return t-(t-e)*this._viscosity},_onPreDragLimit:function(){var t,e;this._viscosity&&this._offsetLimit&&(t=this._draggable._newPos.subtract(this._draggable._startPos),e=this._offsetLimit,t.x<e.min.x&&(t.x=this._viscousLimit(t.x,e.min.x)),t.y<e.min.y&&(t.y=this._viscousLimit(t.y,e.min.y)),t.x>e.max.x&&(t.x=this._viscousLimit(t.x,e.max.x)),t.y>e.max.y&&(t.y=this._viscousLimit(t.y,e.max.y)),this._draggable._newPos=this._draggable._startPos.add(t))},_onPreDragWrap:function(){var t=this._worldWidth,e=Math.round(t/2),i=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-e+i)%t+e-i,n=(n+e+i)%t-e-i,t=Math.abs(o+i)<Math.abs(n+i)?o:n;this._draggable._absPos=this._draggable._newPos.clone(),this._draggable._newPos.x=t},_onDragEnd:function(t){var e,i,n,o,s=this._map,r=s.options,a=!r.inertia||t.noInertia||this._times.length<2;s.fire("dragend",t),!a&&(this._prunePositions(+new Date),t=this._lastPos.subtract(this._positions[0]),a=(this._lastTime-this._times[0])/1e3,e=r.easeLinearity,a=(t=t.multiplyBy(e/a)).distanceTo([0,0]),i=Math.min(r.inertiaMaxSpeed,a),t=t.multiplyBy(i/a),n=i/(r.inertiaDeceleration*e),(o=t.multiplyBy(-n/2).round()).x||o.y)?(o=s._limitOffset(o,s.options.maxBounds),x(function(){s.panBy(o,{duration:n,easeLinearity:e,noMoveStart:!0,animate:!0})})):s.fire("moveend")}})),St=(A.addInitHook("addHandler","dragging",Zt),A.mergeOptions({keyboard:!0,keyboardPanDelta:80}),n.extend({keyCodes:{left:[37],right:[39],down:[40],up:[38],zoomIn:[187,107,61,171],zoomOut:[189,109,54,173]},initialize:function(t){this._map=t,this._setPanDelta(t.options.keyboardPanDelta),this._setZoomDelta(t.options.zoomDelta)},addHooks:function(){var t=this._map._container;t.tabIndex<=0&&(t.tabIndex="0"),S(t,{focus:this._onFocus,blur:this._onBlur,mousedown:this._onMouseDown},this),this._map.on({focus:this._addHooks,blur:this._removeHooks},this)},removeHooks:function(){this._removeHooks(),k(this._map._container,{focus:this._onFocus,blur:this._onBlur,mousedown:this._onMouseDown},this),this._map.off({focus:this._addHooks,blur:this._removeHooks},this)},_onMouseDown:function(){var t,e,i;this._focused||(i=document.body,t=document.documentElement,e=i.scrollTop||t.scrollTop,i=i.scrollLeft||t.scrollLeft,this._map._container.focus(),window.scrollTo(i,e))},_onFocus:function(){this._focused=!0,this._map.fire("focus")},_onBlur:function(){this._focused=!1,this._map.fire("blur")},_setPanDelta:function(t){for(var e=this._panKeys={},i=this.keyCodes,n=0,o=i.left.length;n<o;n++)e[i.left[n]]=[-1*t,0];for(n=0,o=i.right.length;n<o;n++)e[i.right[n]]=[t,0];for(n=0,o=i.down.length;n<o;n++)e[i.down[n]]=[0,t];for(n=0,o=i.up.length;n<o;n++)e[i.up[n]]=[0,-1*t]},_setZoomDelta:function(t){for(var e=this._zoomKeys={},i=this.keyCodes,n=0,o=i.zoomIn.length;n<o;n++)e[i.zoomIn[n]]=t;for(n=0,o=i.zoomOut.length;n<o;n++)e[i.zoomOut[n]]=-t},_addHooks:function(){S(document,"keydown",this._onKeyDown,this)},_removeHooks:function(){k(document,"keydown",this._onKeyDown,this)},_onKeyDown:function(t){if(!(t.altKey||t.ctrlKey||t.metaKey)){var e,i,n=t.keyCode,o=this._map;if(n in this._panKeys)o._panAnim&&o._panAnim._inProgress||(i=this._panKeys[n],t.shiftKey&&(i=m(i).multiplyBy(3)),o.options.maxBounds&&(i=o._limitOffset(m(i),o.options.maxBounds)),o.options.worldCopyJump?(e=o.wrapLatLng(o.unproject(o.project(o.getCenter()).add(i))),o.panTo(e)):o.panBy(i));else if(n in this._zoomKeys)o.setZoom(o.getZoom()+(t.shiftKey?3:1)*this._zoomKeys[n]);else{if(27!==n||!o._popup||!o._popup.options.closeOnEscapeKey)return;o.closePopup()}Re(t)}}})),Et=(A.addInitHook("addHandler","keyboard",St),A.mergeOptions({scrollWheelZoom:!0,wheelDebounceTime:40,wheelPxPerZoomLevel:60}),n.extend({addHooks:function(){S(this._map._container,"wheel",this._onWheelScroll,this),this._delta=0},removeHooks:function(){k(this._map._container,"wheel",this._onWheelScroll,this)},_onWheelScroll:function(t){var e=He(t),i=this._map.options.wheelDebounceTime,e=(this._delta+=e,this._lastMousePos=this._map.mouseEventToContainerPoint(t),this._startTime||(this._startTime=+new Date),Math.max(i-(+new Date-this._startTime),0));clearTimeout(this._timer),this._timer=setTimeout(a(this._performZoom,this),e),Re(t)},_performZoom:function(){var t=this._map,e=t.getZoom(),i=this._map.options.zoomSnap||0,n=(t._stop(),this._delta/(4*this._map.options.wheelPxPerZoomLevel)),n=4*Math.log(2/(1+Math.exp(-Math.abs(n))))/Math.LN2,i=i?Math.ceil(n/i)*i:n,n=t._limitZoom(e+(0<this._delta?i:-i))-e;this._delta=0,this._startTime=null,n&&("center"===t.options.scrollWheelZoom?t.setZoom(e+n):t.setZoomAround(this._lastMousePos,e+n))}})),kt=(A.addInitHook("addHandler","scrollWheelZoom",Et),A.mergeOptions({tapHold:b.touchNative&&b.safari&&b.mobile,tapTolerance:15}),n.extend({addHooks:function(){S(this._map._container,"touchstart",this._onDown,this)},removeHooks:function(){k(this._map._container,"touchstart",this._onDown,this)},_onDown:function(t){var e;clearTimeout(this._holdTimeout),1===t.touches.length&&(e=t.touches[0],this._startPos=this._newPos=new p(e.clientX,e.clientY),this._holdTimeout=setTimeout(a(function(){this._cancel(),this._isTapValid()&&(S(document,"touchend",O),S(document,"touchend touchcancel",this._cancelClickPrevent),this._simulateEvent("contextmenu",e))},this),600),S(document,"touchend touchcancel contextmenu",this._cancel,this),S(document,"touchmove",this._onMove,this))},_cancelClickPrevent:function t(){k(document,"touchend",O),k(document,"touchend touchcancel",t)},_cancel:function(){clearTimeout(this._holdTimeout),k(document,"touchend touchcancel contextmenu",this._cancel,this),k(document,"touchmove",this._onMove,this)},_onMove:function(t){t=t.touches[0];this._newPos=new p(t.clientX,t.clientY)},_isTapValid:function(){return this._newPos.distanceTo(this._startPos)<=this._map.options.tapTolerance},_simulateEvent:function(t,e){t=new MouseEvent(t,{bubbles:!0,cancelable:!0,view:window,screenX:e.screenX,screenY:e.screenY,clientX:e.clientX,clientY:e.clientY});t._simulated=!0,e.target.dispatchEvent(t)}})),Ot=(A.addInitHook("addHandler","tapHold",kt),A.mergeOptions({touchZoom:b.touch,bounceAtZoomLimits:!0}),n.extend({addHooks:function(){M(this._map._container,"leaflet-touch-zoom"),S(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){z(this._map._container,"leaflet-touch-zoom"),k(this._map._container,"touchstart",this._onTouchStart,this)},_onTouchStart:function(t){var e,i,n=this._map;!t.touches||2!==t.touches.length||n._animatingZoom||this._zooming||(e=n.mouseEventToContainerPoint(t.touches[0]),i=n.mouseEventToContainerPoint(t.touches[1]),this._centerPoint=n.getSize()._divideBy(2),this._startLatLng=n.containerPointToLatLng(this._centerPoint),"center"!==n.options.touchZoom&&(this._pinchStartLatLng=n.containerPointToLatLng(e.add(i)._divideBy(2))),this._startDist=e.distanceTo(i),this._startZoom=n.getZoom(),this._moved=!1,this._zooming=!0,n._stop(),S(document,"touchmove",this._onTouchMove,this),S(document,"touchend touchcancel",this._onTouchEnd,this),O(t))},_onTouchMove:function(t){if(t.touches&&2===t.touches.length&&this._zooming){var e=this._map,i=e.mouseEventToContainerPoint(t.touches[0]),n=e.mouseEventToContainerPoint(t.touches[1]),o=i.distanceTo(n)/this._startDist;if(this._zoom=e.getScaleZoom(o,this._startZoom),!e.options.bounceAtZoomLimits&&(this._zoom<e.getMinZoom()&&o<1||this._zoom>e.getMaxZoom()&&1<o)&&(this._zoom=e._limitZoom(this._zoom)),"center"===e.options.touchZoom){if(this._center=this._startLatLng,1==o)return}else{i=i._add(n)._divideBy(2)._subtract(this._centerPoint);if(1==o&&0===i.x&&0===i.y)return;this._center=e.unproject(e.project(this._pinchStartLatLng,this._zoom).subtract(i),this._zoom)}this._moved||(e._moveStart(!0,!1),this._moved=!0),r(this._animRequest);n=a(e._move,e,this._center,this._zoom,{pinch:!0,round:!1},void 0);this._animRequest=x(n,this,!0),O(t)}},_onTouchEnd:function(){this._moved&&this._zooming?(this._zooming=!1,r(this._animRequest),k(document,"touchmove",this._onTouchMove,this),k(document,"touchend touchcancel",this._onTouchEnd,this),this._map.options.zoomAnimation?this._map._animateZoom(this._center,this._map._limitZoom(this._zoom),!0,this._map.options.zoomSnap):this._map._resetView(this._center,this._map._limitZoom(this._zoom))):this._zooming=!1}})),Xi=(A.addInitHook("addHandler","touchZoom",Ot),A.BoxZoom=_t,A.DoubleClickZoom=Ct,A.Drag=Zt,A.Keyboard=St,A.ScrollWheelZoom=Et,A.TapHold=kt,A.TouchZoom=Ot,t.Bounds=f,t.Browser=b,t.CRS=ot,t.Canvas=Fi,t.Circle=vi,t.CircleMarker=gi,t.Class=et,t.Control=B,t.DivIcon=Ri,t.DivOverlay=Ai,t.DomEvent=mt,t.DomUtil=pt,t.Draggable=Xe,t.Evented=it,t.FeatureGroup=ci,t.GeoJSON=wi,t.GridLayer=Ni,t.Handler=n,t.Icon=di,t.ImageOverlay=Ei,t.LatLng=v,t.LatLngBounds=s,t.Layer=o,t.LayerGroup=ui,t.LineUtil=vt,t.Map=A,t.Marker=mi,t.Mixin=ft,t.Path=fi,t.Point=p,t.PolyUtil=gt,t.Polygon=xi,t.Polyline=yi,t.Popup=Bi,t.PosAnimation=Fe,t.Projection=wt,t.Rectangle=Yi,t.Renderer=Wi,t.SVG=Gi,t.SVGOverlay=Oi,t.TileLayer=Di,t.Tooltip=Ii,t.Transformation=at,t.Util=tt,t.VideoOverlay=ki,t.bind=a,t.bounds=_,t.canvas=Ui,t.circle=function(t,e,i){return new vi(t,e,i)},t.circleMarker=function(t,e){return new gi(t,e)},t.control=Ue,t.divIcon=function(t){return new Ri(t)},t.extend=l,t.featureGroup=function(t,e){return new ci(t,e)},t.geoJSON=Si,t.geoJson=Mt,t.gridLayer=function(t){return new Ni(t)},t.icon=function(t){return new di(t)},t.imageOverlay=function(t,e,i){return new Ei(t,e,i)},t.latLng=w,t.latLngBounds=g,t.layerGroup=function(t,e){return new ui(t,e)},t.map=function(t,e){return new A(t,e)},t.marker=function(t,e){return new mi(t,e)},t.point=m,t.polygon=function(t,e){return new xi(t,e)},t.polyline=function(t,e){return new yi(t,e)},t.popup=function(t,e){return new Bi(t,e)},t.rectangle=function(t,e){return new Yi(t,e)},t.setOptions=c,t.stamp=h,t.svg=Ki,t.svgOverlay=function(t,e,i){return new Oi(t,e,i)},t.tileLayer=ji,t.tooltip=function(t,e){return new Ii(t,e)},t.transformation=ht,t.version="1.9.4",t.videoOverlay=function(t,e,i){return new ki(t,e,i)},window.L);t.noConflict=function(){return window.L=Xi,this},window.L=t});
6 +//# sourceMappingURL=leaflet.js.map
resources/map.js
  1 +// The Leaflet map: event markers, the rotate gesture engine (#map-stage spun as a unit,
  2 +// since Leaflet 1.x can't rotate its own panes), and the loader for vendored Leaflet +
  3 +// the NoGap tile layer. Taps forward to window.panel; rotation is handed to window.initNav.
  4 +
  5 +(function () {
  6 +	var fmt = window.fmt;
  7 +
  8 +	var events = [];
  9 +	var map = null;
 10 +	var IS_TOUCH = window.matchMedia("(pointer: coarse)").matches;
 11 +	var stageAngle = 0; // current map rotation in degrees, set by the rotate gesture
 12 +
 13 +	// Average lat/long of all events, so the page opens framing the events (no location
 14 +	// permission until the compass is tapped). Fallback: Seattle.
 15 +	function eventsCenter() {
 16 +		if (!events.length) { return [47.6062, -122.3321]; }
 17 +		var lat = 0, lng = 0;
 18 +		events.forEach(function (m) { lat += m.latitude; lng += m.longitude; });
 19 +		return [lat / events.length, lng / events.length];
 20 +	}
 21 +
 22 +	// If the URL carries ?<slug>, open that event. Returns true if it selected one.
 23 +	function selectFromUrl(select) {
 24 +		var m = fmt.eventFromUrl(events);
 25 +		if (!m) { return false; }
 26 +		select(m, false);
 27 +		return true;
 28 +	}
 29 +
 30 +	// Rewrite the address bar to the event's deep link (no reload / history entry).
 31 +	function syncUrl(m) {
 32 +		if (window.history && history.replaceState) {
 33 +			history.replaceState(null, "", fmt.eventUrl(m));
 34 +		}
 35 +	}
 36 +
 37 +	function clearUrl() {
 38 +		if (window.history && history.replaceState && window.location.search) {
 39 +			history.replaceState(null, "", window.location.pathname);
 40 +		}
 41 +	}
 42 +
 43 +	function initMap() {
 44 +		// snapshot "now" once so marker filtering, z-ordering, and the intro all agree
 45 +		var now = new Date();
 46 +		// soonest first; passed events removed. With nothing upcoming, fall back to the
 47 +		// 10 most-recently-ended events (most recent first) so the map isn't empty.
 48 +		var upcoming = fmt.upcomingEvents(events, now);
 49 +		var pastMode = upcoming.length === 0;
 50 +		events = pastMode ? fmt.pastEvents(events, now, 10) : upcoming;
 51 +
 52 +		map = L.map("map-container", { attributionControl: false, doubleClickZoom: false, zoomControl: false, scrollWheelZoom: false, keyboard: false, maxZoom: 19, bounceAtZoomLimits: false, zoomSnap: 0 }).setView(eventsCenter(), 12.5);
 53 +
 54 +		// CartoDB tile scale matching display density for crisp tiles. Hardcoded (not
 55 +		// detectRetina, which shifts zoom and shrinks labels). carto serves up to @4x.
 56 +		var dpr = window.devicePixelRatio || 1;
 57 +		var scale = dpr >= 4 ? "@4x" : dpr >= 3 ? "@3x" : dpr >= 2 ? "@2x" : "";
 58 +		var carto = L.tileLayer("https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}" + scale + ".png", {
 59 +			attribution: "",
 60 +			subdomains: "abcd",
 61 +			maxZoom: 19,
 62 +			// NoGap canvas spans keepBuffer; at full DPR each canvas is ratio² in memory,
 63 +			// so a small buffer keeps it under Safari's renderer limit (default 2)
 64 +			keepBuffer: 1
 65 +		});
 66 +
 67 +		carto.addTo(map);
 68 +
 69 +		var mapEl = map.getContainer();
 70 +		map.on("mousedown", function () { mapEl.classList.add("map-grabbing"); });
 71 +		document.addEventListener("mouseup", function () { mapEl.classList.remove("map-grabbing"); });
 72 +
 73 +		initSmoothWheel();
 74 +
 75 +		// background tap (not a marker, not a drag/hold) deselects; Leaflet's "click"
 76 +		// fires only for genuine background taps
 77 +		map.on("click", function () { window.panel.deselectEvent(); clearUrl(); });
 78 +
 79 +		// container may have been sized after init; force a recalc
 80 +		setTimeout(function () {
 81 +			if (map) { map.invalidateSize(); }
 82 +		}, 200);
 83 +
 84 +		function makeIcon(m) {
 85 +			var d = fmt.parseLocal(m.start);
 86 +			return L.divIcon({
 87 +				className: "",
 88 +				html: "<div class=\"event-pin\" style=\"background:" + m.color + "\">" +
 89 +					"<span class=\"pin-month\">" + fmt.MONTH_ABBR[d.getMonth()] + "</span>" +
 90 +					"<span class=\"pin-day\">" + d.getDate() + "</span>" +
 91 +					"</div>",
 92 +				iconSize: [48, 48],
 93 +				iconAnchor: [24, 24]
 94 +			});
 95 +		}
 96 +
 97 +		// Leaflet bakes a latitude term into each marker's z-index, and that spread can
 98 +		// exceed a +1 step, so bump by a large amount to guarantee the newest tap wins.
 99 +		var Z_STEP = 10000;
100 +		// seed z by date (events sorted soonest-first) so soonest sit on top at load;
101 +		// topEventZ starts above every seed so the first tap still lifts over all
102 +		var topEventZ = (events.length + 1) * Z_STEP;
103 +
104 +		// Lift the event, type its panel, recenter - shared by marker taps and deep links.
105 +		function selectMarker(m, animate) {
106 +			topEventZ += Z_STEP;
107 +			m._marker.setZIndexOffset(topEventZ);
108 +			window.panel.selectEvent(m);
109 +			syncUrl(m);
110 +			// nav mode: stop following so the pan isn't fought + restart the ease-back
111 +			if (window.navInteract) { window.navInteract(); }
112 +			// low easeLinearity bends the default pan into a clear ease-in-out
113 +			map.panTo([m.latitude, m.longitude], { animate: animate, duration: 0.7, easeLinearity: 0.1 });
114 +		}
115 +
116 +		events.forEach(function (m, i) {
117 +			var marker = L.marker([m.latitude, m.longitude], { icon: makeIcon(m) }).addTo(map);
118 +			marker.setZIndexOffset((events.length - i) * Z_STEP);
119 +			m._marker = marker; // so selectEvent can reach this pin's DOM
120 +			marker.on("click", function () { selectMarker(m, true); });
121 +		});
122 +
123 +		window.panel.setIntroData(events, selectMarker, pastMode);
124 +
125 +		// deep-linked? open it. otherwise show intro.
126 +		if (!selectFromUrl(selectMarker)) { window.panel.paintIntro(); }
127 +
128 +		if (IS_TOUCH) {
129 +			var rotation = initRotate();
130 +			if (window.initNav) { window.initNav(map, rotation); }
131 +		}
132 +	}
133 +
134 +	// Desktop wheel/trackpad. Leaflet's stock scrollWheelZoom debounces and steps, which
135 +	// feels sluggish; instead ease map._move() toward a goal zoom every frame (the touch
136 +	// pinch path) anchored under the cursor, and pan via _rawPanBy so markers ride along
137 +	// (_move only re-renders layers on zoom change). zoomSnap 0 lets fractional zoom flow.
138 +	function initSmoothWheel() {
139 +		var el = map.getContainer();
140 +		var mode = null; // "pinch" | "wheel" | "pan" while a gesture is in flight
141 +		var goalZoom = 0, cursorPoint = null, centerPoint = null, anchorLatLng = null;
142 +		var rafId = null, idleTimer = null, moved = false, panning = false;
143 +		var prevCenter = null, prevZoom = 0;
144 +
145 +		function step() {
146 +			// something else moved the map (marker panTo, drag) - yield to it
147 +			if (moved && (!map.getCenter().equals(prevCenter) || map.getZoom() !== prevZoom)) {
148 +				clearTimeout(idleTimer);
149 +				settle();
150 +				return;
151 +			}
152 +			var zoom = map.getZoom() + (goalZoom - map.getZoom()) * 0.3;
153 +			// keep the latlng under the cursor fixed while zooming
154 +			var center = map.unproject(map.project(anchorLatLng, zoom).subtract(cursorPoint.subtract(centerPoint)), zoom);
155 +			if (!moved) { map._moveStart(true, false); moved = true; }
156 +			map._move(center, zoom);
157 +			prevCenter = map.getCenter();
158 +			prevZoom = map.getZoom();
159 +			rafId = requestAnimationFrame(step);
160 +		}
161 +
162 +		// end whichever gesture is in flight so tiles settle and moveend fires
163 +		function settle() {
164 +			mode = null;
165 +			if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; }
166 +			if (moved) { moved = false; map._moveEnd(true); }
167 +			if (panning) { panning = false; map.fire("moveend"); }
168 +		}
169 +
170 +		function armIdle() {
171 +			clearTimeout(idleTimer);
172 +			idleTimer = setTimeout(settle, 200);
173 +		}
174 +
175 +		function beginZoom(clientX, clientY) {
176 +			if (rafId === null) {
177 +				map._stop();
178 +				goalZoom = map.getZoom();
179 +				centerPoint = map.getSize().divideBy(2);
180 +				cursorPoint = map.mouseEventToContainerPoint({ clientX: clientX, clientY: clientY });
181 +				anchorLatLng = map.containerPointToLatLng(cursorPoint);
182 +				rafId = requestAnimationFrame(step);
183 +			}
184 +			armIdle();
185 +		}
186 +
187 +		function looksLikeMouse(e) {
188 +			if (e.deltaMode !== 0) { return true; }
189 +			return typeof e.wheelDeltaY === "number" && e.wheelDeltaY !== 0 && e.wheelDeltaY % 120 === 0;
190 +		}
191 +
192 +		el.addEventListener("wheel", function (e) {
193 +			e.preventDefault();
194 +			var next;
195 +			if (e.ctrlKey) { next = "pinch"; }
196 +			else if (mode === "pan" || mode === "wheel") { next = mode; } // locked while in flight
197 +			else { next = looksLikeMouse(e) ? "wheel" : "pan"; }
198 +			if (mode !== null && next !== mode) { clearTimeout(idleTimer); settle(); }
199 +			mode = next;
200 +			if (mode === "pan") {
201 +				if (!panning) { panning = true; map.fire("movestart"); }
202 +				map._rawPanBy(L.point(e.deltaX, e.deltaY));
203 +				map.fire("move");
204 +				armIdle();
205 +			} else {
206 +				beginZoom(e.clientX, e.clientY);
207 +				var dy = e.deltaMode === 1 ? e.deltaY * 20 : e.deltaY; // line deltas (firefox mice) -> px
208 +				goalZoom = map._limitZoom(goalZoom - dy * (mode === "pinch" ? 0.01 : 0.003));
209 +				cursorPoint = map.mouseEventToContainerPoint(e);
210 +			}
211 +		}, { passive: false });
212 +
213 +		// Safari desktop fires gesture* events with a cumulative scale instead of
214 +		// ctrl+wheel. Touch devices also have GestureEvent - skip them for native pinch.
215 +		if (!IS_TOUCH && "GestureEvent" in window) {
216 +			var gestureBaseZoom = 0;
217 +			el.addEventListener("gesturestart", function (e) {
218 +				e.preventDefault();
219 +				if (mode !== null && mode !== "pinch") { clearTimeout(idleTimer); settle(); }
220 +				mode = "pinch";
221 +				beginZoom(e.clientX, e.clientY);
222 +				gestureBaseZoom = goalZoom;
223 +			});
224 +			el.addEventListener("gesturechange", function (e) {
225 +				e.preventDefault();
226 +				if (rafId === null) { return; }
227 +				goalZoom = map._limitZoom(gestureBaseZoom + Math.log2(e.scale));
228 +				cursorPoint = map.mouseEventToContainerPoint(e);
229 +				armIdle();
230 +			});
231 +			el.addEventListener("gestureend", function (e) { e.preventDefault(); });
232 +		}
233 +	}
234 +
235 +	// Rotate the whole #map-stage (oversized to the panel diagonal) via a CSS transform
236 +	// and patch Leaflet's coordinate math so its native pinch-zoom/pan still land. Touch only.
237 +	function initRotate() {
238 +		var stage = document.getElementById("map-stage");
239 +		var panel = document.getElementById("left-box");
240 +		var compass = document.getElementById("compass");
241 +
242 +		// Tapping a marker in the rotated stage's clipped corner makes iOS Safari "reveal"
243 +		// it by auto-scrolling the container (despite overflow:hidden), shifting the stage
244 +		// off-center. Snap any such scroll back to origin. The reveal may move #left-box or
245 +		// the document, so neutralize both; the root scroller emits on window.
246 +		function pinScroll(eventTarget, scroller) {
247 +			eventTarget.addEventListener("scroll", function () {
248 +				if (scroller.scrollLeft || scroller.scrollTop) { scroller.scrollTo(0, 0); }
249 +			}, { passive: true });
250 +		}
251 +		pinScroll(panel, panel);
252 +		var root = document.scrollingElement || document.documentElement;
253 +		pinScroll(window, root);
254 +
255 +		// square of the panel's diagonal so a rotated map never exposes the corners
256 +		function sizeStage() {
257 +			var w = panel.clientWidth, h = panel.clientHeight;
258 +			var diag = Math.ceil(Math.hypot(w, h));
259 +			stage.style.width = diag + "px";
260 +			stage.style.height = diag + "px";
261 +			applyStageTransform();
262 +			map.invalidateSize();
263 +		}
264 +
265 +		// stage is anchored at the panel center (top/left 50%); translate by half its size
266 +		// to center, then rotate (translate before rotate so the pivot is the panel center)
267 +		function applyStageTransform() {
268 +			var half = (parseFloat(stage.style.width) || 0) / 2;
269 +			stage.style.transform = "translate(" + (-half) + "px, " + (-half) + "px) rotate(" + stageAngle + "deg)";
270 +			// pins counter-rotate so their date text stays upright
271 +			stage.style.setProperty("--pin-counter", (-stageAngle) + "deg");
272 +			// needle turns with the map so red keeps pointing to true north
273 +			compass.style.setProperty("--compass-angle", stageAngle + "deg");
274 +		}
275 +
276 +		function normalizeDeg(d) {
277 +			return ((d + 180) % 360 + 360) % 360 - 180;
278 +		}
279 +
280 +		// invert the stage rotation about the panel center to map a screen point into
281 +		// stage-local coords (getBoundingClientRect gives only the axis-aligned box)
282 +		function clientToStage(clientX, clientY) {
283 +			var r = panel.getBoundingClientRect();
284 +			var cx = r.left + r.width / 2;
285 +			var cy = r.top + r.height / 2;
286 +			var rad = (-stageAngle * Math.PI) / 180;
287 +			var dx = clientX - cx, dy = clientY - cy;
288 +			var rx = dx * Math.cos(rad) - dy * Math.sin(rad);
289 +			var ry = dx * Math.sin(rad) + dy * Math.cos(rad);
290 +			var half = (parseFloat(stage.style.width) || 0) / 2;
291 +			return { x: rx + half, y: ry + half };
292 +		}
293 +
294 +		// Leaflet derives container coords from the bounding rect, wrong once rotated.
295 +		map.mouseEventToContainerPoint = function (e) {
296 +			var p = clientToStage(e.clientX, e.clientY);
297 +			return L.point(p.x, p.y);
298 +		};
299 +
300 +		// one-finger pan: Leaflet computes the delta in raw screen px, so rotate it by
301 +		// -stageAngle so the map tracks the finger.
302 +		var _dragUpdate = L.Draggable.prototype._updatePosition;
303 +		L.Draggable.prototype._updatePosition = function () {
304 +			if (stageAngle && this._startPos && this._newPos) {
305 +				var rad = (-stageAngle * Math.PI) / 180;
306 +				var cos = Math.cos(rad), sin = Math.sin(rad);
307 +				var dx = this._newPos.x - this._startPos.x;
308 +				var dy = this._newPos.y - this._startPos.y;
309 +				this._newPos = this._startPos.add(L.point(dx * cos - dy * sin, dx * sin + dy * cos));
310 +			}
311 +			_dragUpdate.call(this);
312 +		};
313 +
314 +		// a panel touch sliding onto the map is handed off by iOS as a fresh map touchstart;
315 +		// flag gestures starting outside the stage so the drag below can refuse it
316 +		var panelGesture = false;
317 +		var panelGestureEnded = 0;
318 +		document.addEventListener("touchstart", function (e) {
319 +			if (!stage.contains(e.target)) { panelGesture = true; }
320 +		}, { passive: true, capture: true });
321 +		function endPanelGesture(e) {
322 +			if (e.touches.length === 0 && panelGesture) {
323 +				panelGesture = false;
324 +				panelGestureEnded = Date.now();
325 +			}
326 +		}
327 +		document.addEventListener("touchend", endPanelGesture, { passive: true, capture: true });
328 +		document.addEventListener("touchcancel", endPanelGesture, { passive: true, capture: true });
329 +
330 +		// the rotated ancestor inflates the pane's bounding box, so Leaflet's cached
331 +		// "parent scale" is bogus and distorts the drag. Force it to 1:1.
332 +		var _dragDown = L.Draggable.prototype._onDown;
333 +		L.Draggable.prototype._onDown = function (e) {
334 +			if (panelGesture || Date.now() - panelGestureEnded < 250) { return; }
335 +			_dragDown.call(this, e);
336 +			if (this._parentScale) { this._parentScale = { x: 1, y: 1 }; }
337 +		};
338 +
339 +		// --- two-finger rotate, layered on Leaflet's native pinch ---
340 +		var ROTATE_DEADZONE = 8; // deg of twist before rotation engages
341 +		var rotGesture = null;
342 +
343 +		function fingerAngle(t0, t1) {
344 +			return Math.atan2(t1.clientY - t0.clientY, t1.clientX - t0.clientX) * 180 / Math.PI;
345 +		}
346 +
347 +		// A one-finger touch landing while a zoom animates is dropped (Leaflet won't route
348 +		// to the drag handler until the anim ends), so end the in-flight zoom at the
349 +		// earliest capture point. _stop() doesn't cancel the zoom anim; _onZoomTransitionEnd
350 +		// clears _animatingZoom. Capture phase so it runs first.
351 +		stage.addEventListener("touchstart", function (e) {
352 +			if (e.touches.length === 1 && map._animatingZoom) {
353 +				map._onZoomTransitionEnd();
354 +			}
355 +		}, { passive: true, capture: true });
356 +
357 +		stage.addEventListener("touchstart", function (e) {
358 +			// rotation allowed in nav mode too; nav.js treats a manual twist as free-look
359 +			if (e.touches.length === 2) {
360 +				rotGesture = { startAngle: fingerAngle(e.touches[0], e.touches[1]), engaged: false, baseAngle: stageAngle };
361 +			}
362 +		}, { passive: true });
363 +
364 +		stage.addEventListener("touchmove", function (e) {
365 +			if (!rotGesture || e.touches.length !== 2) { return; }
366 +			var cur = fingerAngle(e.touches[0], e.touches[1]);
367 +			var twist = normalizeDeg(cur - rotGesture.startAngle);
368 +			if (!rotGesture.engaged) {
369 +				if (Math.abs(twist) < ROTATE_DEADZONE) { return; } // dead-zone keeps pure pinch from rotating
370 +				rotGesture.engaged = true;
371 +				rotGesture.startAngle = cur; // take accumulated twist as the new zero so it doesn't jump
372 +				rotGesture.baseAngle = stageAngle;
373 +				twist = 0;
374 +			}
375 +			stageAngle = normalizeDeg(rotGesture.baseAngle + twist);
376 +			applyStageTransform();
377 +		}, { passive: true });
378 +
379 +		function endRotate(e) {
380 +			if (rotGesture && e.touches.length < 2) { rotGesture = null; }
381 +		}
382 +		stage.addEventListener("touchend", endRotate);
383 +		stage.addEventListener("touchcancel", endRotate);
384 +
385 +		window.addEventListener("resize", sizeStage);
386 +		sizeStage();
387 +
388 +		// interface consumed by nav.js
389 +		return {
390 +			stage: stage,
391 +			panel: panel,
392 +			compass: compass,
393 +			getAngle: function () { return stageAngle; },
394 +			setAngle: function (deg) { stageAngle = deg; applyStageTransform(); },
395 +			applyTransform: applyStageTransform,
396 +			clientToStage: clientToStage,
397 +			normalizeDeg: normalizeDeg
398 +		};
399 +	}
400 +
401 +	function mapFailed() {
402 +		var el = document.getElementById("map-container");
403 +		el.className = "map-error";
404 +		el.textContent = "Unable to load map.";
405 +	}
406 +
407 +	// NoGap composites each level's tiles onto one <canvas> so fractional zoom has no
408 +	// per-tile seams. Loads after Leaflet (extends L.TileLayer); init regardless of failure.
409 +	function loadNoGapThenInit() {
410 +		var s = document.createElement("script");
411 +		s.src = "resources/leaflet/leaflet-nogap.js";
412 +		s.onload = initMap;
413 +		s.onerror = initMap;
414 +		document.body.appendChild(s);
415 +	}
416 +
417 +	// Leaflet is vendored under resources/leaflet/ so the map works offline from first load.
418 +	function loadLeaflet() {
419 +		if (window.L) { loadNoGapThenInit(); return; }
420 +		var s = document.createElement("script");
421 +		s.src = "resources/leaflet/leaflet.js";
422 +		s.onload = function () { window.L ? loadNoGapThenInit() : mapFailed(); };
423 +		s.onerror = mapFailed;
424 +		document.body.appendChild(s);
425 +	}
426 +
427 +	window.eventsMap = {
428 +		setEvents: function (data) { events = data; },
429 +		load: loadLeaflet
430 +	};
431 +})();
resources/nav.js
  1 +// Nav mode: tap the compass to request location + orientation, drop a marker at
  2 +// the user's position, and follow them heading-up. Panning or pinch-rotating away
  3 +// suspends following; after 3s of no interaction the ladybug eases back and follow
  4 +// resumes. Touch only - script.js calls this from initMap once rotation is ready.
  5 +
  6 +(function () {
  7 +	var LADYBUG = "🐞";
  8 +	var FOLLOW_IDLE_MS = 3000;
  9 +	var RING_REWIND_MS = 1200; // arc drain on touch; must stay under FOLLOW_IDLE_MS
 10 +	var RING_FADE_MS = 200;    // matches the 0.2s #compass box-shadow transition
 11 +	var RING_LEAD_MS = 100;    // arc completes a hair early so takeover feels instant
 12 +	// ease-in-out with a firm landing: a flat tail reads as lag at takeover
 13 +	var RING_FILL_EASE = "cubic-bezier(0.45, 0, 0.6, 0.9)";
 14 +	var FOLLOW_ANCHOR_Y = 0.80; // fraction down the panel: leaves room "ahead"
 15 +	var NAV_TRANSITION_MS = 900;
 16 +	var EASE_TAU_MS = 600;      // dead-reckon correction smoothing (bigger = smoother/laggier)
 17 +	var VEL_SMOOTH = 0.5;       // per-fix blend of new velocity into the running estimate
 18 +	var MAX_PREDICT_MS = 4000;  // stop extrapolating this long after the last fix
 19 +	var SNAP_DIST_M = 500;      // a fix this far from the rendered bug just teleports
 20 +
 21 +	window.initNav = function (map, rotation) {
 22 +		var compass = rotation.compass;
 23 +		var panel = rotation.panel;
 24 +		var ringArc = compass.querySelector(".ring-arc");
 25 +
 26 +		// require an orientation sensor: keeps the compass off desktop touchscreens that
 27 +		// can't give a heading.
 28 +		if (!("DeviceOrientationEvent" in window)) { return; }
 29 +		compass.classList.add("compass-enabled");
 30 +
 31 +		// dedicated pane keeps the ladybug above every event square regardless of z math
 32 +		map.createPane("ladybugPane").style.zIndex = 700;
 33 +
 34 +		// Pane for the location->event line, between tilePane (200) and markerPane (600).
 35 +		// We position the line ourselves every frame (see renderLine), so it must NOT carry
 36 +		// leaflet-zoom-animated, or Leaflet's zoom transform would fight our writes.
 37 +		var linePane = map.createPane("linePane");
 38 +		linePane.style.zIndex = 250;
 39 +
 40 +		var heading = 0;          // device heading in deg (0 = north)
 41 +		var compassOn = false;    // heading-up follow mode
 42 +		var compassStarted = false;
 43 +		var headingReady = false; // a real reading has arrived
 44 +		var pendingEntry = false; // entered nav but waiting for first heading
 45 +
 46 +		var following = false;
 47 +		var followIdleTimer = null;
 48 +		var navAnim = null;
 49 +		var rotAnimating = false;
 50 +
 51 +		var marker = null;        // Leaflet marker for the ladybug
 52 +		var lastFix = null;       // { lat, lng, t } latest raw reading
 53 +		var vel = { lat: 0, lng: 0 }; // deg/s, smoothed across fixes
 54 +		var predictOn = false;
 55 +		var watching = false;
 56 +
 57 +		// Line to the active event: a standalone SVG overlay. We reproject
 58 +		// the two endpoints every frame via latLngToContainerPoint, so the
 59 +		// line stays glued with constant 4px stroke.
 60 +		var DRAW_MS = 300;
 61 +		var lineSvg = null;
 62 +		var lineEl = null;
 63 +		var activeTarget = null;  // { latlng, color }
 64 +		var drawStart = 0;
 65 +		var lineRAF = null;
 66 +
 67 +
 68 +		// the panel-screen point the ladybug is pinned to while following
 69 +		function followAnchorPoint() {
 70 +			var r = panel.getBoundingClientRect();
 71 +			return { x: r.left + r.width / 2, y: r.top + r.height * FOLLOW_ANCHOR_Y };
 72 +		}
 73 +
 74 +		function makeIcon() {
 75 +			return L.divIcon({
 76 +				className: "",
 77 +				html: "<div class=\"ladybug-pin\">" + LADYBUG + "</div>",
 78 +				iconSize: [40, 40],
 79 +				iconAnchor: [20, 20]
 80 +			});
 81 +		}
 82 +
 83 +		// --- line to active event ---
 84 +
 85 +		function ensureLineSvg() {
 86 +			if (lineSvg) { return; }
 87 +			var NS = "http://www.w3.org/2000/svg";
 88 +			lineSvg = document.createElementNS(NS, "svg");
 89 +			lineSvg.setAttribute("class", "ladybug-line-svg");
 90 +			lineEl = document.createElementNS(NS, "line");
 91 +			lineEl.setAttribute("class", "ladybug-line");
 92 +			lineSvg.appendChild(lineEl);
 93 +			linePane.appendChild(lineSvg);
 94 +		}
 95 +
 96 +		function setLineTarget(latlng, color) {
 97 +			ensureLineSvg();
 98 +			activeTarget = { latlng: latlng, color: color };
 99 +			lineEl.setAttribute("stroke", color);
100 +			drawStart = performance.now();
101 +			lineSvg.classList.add("visible");
102 +			renderLine();
103 +			if (!lineRAF) { lineRAF = requestAnimationFrame(tickLine); }
104 +		}
105 +
106 +		function clearLine() {
107 +			activeTarget = null;
108 +			if (lineRAF) { cancelAnimationFrame(lineRAF); lineRAF = null; }
109 +			if (lineSvg) { lineSvg.classList.remove("visible"); }
110 +		}
111 +
112 +		// linePane's local space is the map pane's space, offset from the container by the
113 +		// pane's live translation; subtract it to place a container point inside the pane.
114 +		// Grow the visible end from the ladybug toward the event over DRAW_MS.
115 +		function renderLine() {
116 +			if (!activeTarget || !marker || !lineEl) { return; }
117 +			var off = L.DomUtil.getPosition(map.getPane("mapPane")) || L.point(0, 0);
118 +			var ac = map.latLngToContainerPoint(marker.getLatLng());
119 +			var bc = map.latLngToContainerPoint(activeTarget.latlng);
120 +			var a = { x: ac.x - off.x, y: ac.y - off.y };
121 +			var b = { x: bc.x - off.x, y: bc.y - off.y };
122 +			var p = Math.min(1, (performance.now() - drawStart) / DRAW_MS);
123 +			var e = p * p * (3 - 2 * p); // smoothstep
124 +			lineEl.setAttribute("x1", a.x);
125 +			lineEl.setAttribute("y1", a.y);
126 +			lineEl.setAttribute("x2", a.x + (b.x - a.x) * e);
127 +			lineEl.setAttribute("y2", a.y + (b.y - a.y) * e);
128 +		}
129 +
130 +		// keeps the line glued during one-finger pan and the ladybug glide (both move the
131 +		// map outside Leaflet's zoom rAF)
132 +		function tickLine() {
133 +			if (!activeTarget || !marker) { clearLine(); return; }
134 +			renderLine();
135 +			lineRAF = requestAnimationFrame(tickLine);
136 +		}
137 +
138 +		// two-finger gestures move the map on Leaflet's own rAF; rendering on its move/zoom
139 +		// events puts the line in the same phase as the markers
140 +		map.on("move zoom", renderLine);
141 +
142 +		window.navLine = { setTarget: setLineTarget, clear: clearLine };
143 +
144 +		// --- following ---
145 +
146 +		function startFollowing() {
147 +			following = true;
148 +			clearTimeout(followIdleTimer);
149 +			followIdleTimer = null;
150 +			pinToAnchor();
151 +			if (compassOn) { animateRotation(); }
152 +		}
153 +
154 +		function stopFollowing() {
155 +			following = false;
156 +			pendingEntry = false;
157 +			if (navAnim) { cancelAnimationFrame(navAnim); navAnim = null; }
158 +			clearTimeout(followIdleTimer);
159 +			followIdleTimer = null;
160 +		}
161 +
162 +		// pan (no animation) so the ladybug lands on the anchor point under the current
163 +		// rotation. Inverting the anchor through the rotation makes the map spin about it.
164 +		function pinToAnchor() {
165 +			if (!following || !marker) { return; }
166 +			var a = followAnchorPoint();
167 +			var target = rotation.clientToStage(a.x, a.y);
168 +			var cur = map.latLngToContainerPoint(marker.getLatLng());
169 +			var dx = cur.x - target.x, dy = cur.y - target.y;
170 +			if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) { return; }
171 +			map.panBy([dx, dy], { animate: false });
172 +		}
173 +
174 +		// Dead-reckon: extrapolate the last fix along its velocity and ease the rendered
175 +		// position toward that moving target, so the ladybug glides between readings. Fresh
176 +		// fixes steer the target; the ease supplies accel/decel. Extrapolation is capped
177 +		// so a stalled GPS doesn't walk the bug off; once converged the loop idles.
178 +		function startPredict() {
179 +			if (predictOn) { return; }
180 +			predictOn = true;
181 +			var prev = performance.now();
182 +			var step = function (now) {
183 +				if (!marker || !lastFix) { predictOn = false; return; }
184 +				var dt = Math.min(now - prev, 100); // clamp tab-switch gaps
185 +				prev = now;
186 +				var ahead = Math.min(now - lastFix.t, MAX_PREDICT_MS) / 1000;
187 +				var tLat = lastFix.lat + vel.lat * ahead;
188 +				var tLng = lastFix.lng + vel.lng * ahead;
189 +				var cur = marker.getLatLng();
190 +				var k = 1 - Math.exp(-dt / EASE_TAU_MS); // frame-rate independent
191 +				marker.setLatLng([cur.lat + (tLat - cur.lat) * k, cur.lng + (tLng - cur.lng) * k]);
192 +				if (following) { pinToAnchor(); }
193 +				var z = map.getZoom();
194 +				var px = map.project(marker.getLatLng(), z).distanceTo(map.project(L.latLng(tLat, tLng), z));
195 +				var still = Math.hypot(vel.lat, vel.lng) < 1e-7;
196 +				if (px <= 0.5 && (still || now - lastFix.t >= MAX_PREDICT_MS)) { predictOn = false; return; }
197 +				requestAnimationFrame(step);
198 +			};
199 +			requestAnimationFrame(step);
200 +		}
201 +
202 +		// --- compass ring (suspend/countdown visuals) ---
203 +
204 +		var ringFadeTimer = null;
205 +		var ringFillTimer = null;   // delays the refill until fade/rewind lands
206 +		var ringWaitUntil = 0;      // when the in-flight fade or rewind ends
207 +
208 +		function ringSet(offset, transition) {
209 +			ringArc.style.transition = transition;
210 +			ringArc.style.strokeDashoffset = offset;
211 +		}
212 +
213 +		// snap the arc to empty, flushed so a transition set right after still runs
214 +		function ringReset() {
215 +			clearTimeout(ringFadeTimer);
216 +			ringFadeTimer = null;
217 +			clearTimeout(ringFillTimer);
218 +			ringFillTimer = null;
219 +			ringArc.style.transition = "none";
220 +			ringArc.style.strokeDashoffset = 100;
221 +			ringArc.style.opacity = "";
222 +			void ringArc.getBoundingClientRect();
223 +		}
224 +
225 +		// touching the map suspends following (free-look). The countdown is NOT started
226 +		// here (only on release) so holding a finger down keeps control indefinitely.
227 +		function suspendFollow() {
228 +			if (!compassOn) { return; }
229 +			following = false;
230 +			if (navAnim) { cancelAnimationFrame(navAnim); navAnim = null; }
231 +			clearTimeout(followIdleTimer); // a new touch cancels a running countdown
232 +			followIdleTimer = null;
233 +			clearTimeout(ringFadeTimer);
234 +			ringFadeTimer = null;
235 +			clearTimeout(ringFillTimer);
236 +			ringFillTimer = null;
237 +			ringArc.style.opacity = "";
238 +			if (!compass.classList.contains("paused")) {
239 +				// engaged follow: blue ring fades to the grey track; arc regrows later
240 +				compass.classList.add("paused");
241 +				ringSet(100, "none");
242 +				ringWaitUntil = performance.now() + RING_FADE_MS;
243 +			} else if (ringArc.style.strokeDashoffset !== "100") {
244 +				// arc has charge: drain it (a drain already in flight keeps its clock)
245 +				ringSet(100, "stroke-dashoffset " + RING_REWIND_MS + "ms ease-out");
246 +				ringWaitUntil = performance.now() + RING_REWIND_MS;
247 +			}
248 +		}
249 +
250 +		// start the countdown once the LAST finger lifts; if fingers remain, keep waiting
251 +		function armEaseBack(e) {
252 +			if (!compassOn) { return; }
253 +			if (e && e.touches && e.touches.length > 0) { return; }
254 +			clearTimeout(followIdleTimer);
255 +			followIdleTimer = setTimeout(easeBack, FOLLOW_IDLE_MS);
256 +			// arc refills in sync with the countdown; let any in-flight fade/rewind land
257 +			// first, landing RING_LEAD_MS early so takeover feels instant
258 +			var wait = Math.max(0, ringWaitUntil - performance.now());
259 +			var dur = FOLLOW_IDLE_MS - wait - RING_LEAD_MS;
260 +			clearTimeout(ringFillTimer);
261 +			if (wait > 0) {
262 +				ringFillTimer = setTimeout(function () {
263 +					ringFillTimer = null;
264 +					ringSet(0, "stroke-dashoffset " + dur + "ms " + RING_FILL_EASE);
265 +				}, wait);
266 +			} else {
267 +				ringReset();
268 +				ringSet(0, "stroke-dashoffset " + dur + "ms " + RING_FILL_EASE);
269 +			}
270 +		}
271 +
272 +		// timer fired: the app takes over. The completed arc fades over the same 0.2s the
273 +		// inset ring fades grey->blue.
274 +		function easeBack() {
275 +			followIdleTimer = null;
276 +			compass.classList.remove("paused");
277 +			ringArc.style.transition = "opacity 0.2s ease";
278 +			ringArc.style.opacity = "0";
279 +			clearTimeout(ringFadeTimer);
280 +			ringFadeTimer = setTimeout(ringReset, 200);
281 +			animateToAnchor();
282 +		}
283 +
284 +		// glide the ladybug to the anchor AND rotate heading-up together off one ease, then
285 +		// engage following. Re-aims at the LIVE heading every frame so the glide lands with
286 +		// no leftover snap (the heading drifts during the 600ms).
287 +		function animateToAnchor() {
288 +			if (!compassOn) { return; }
289 +			if (navAnim) { cancelAnimationFrame(navAnim); navAnim = null; }
290 +			if (!marker) { startFollowing(); return; }
291 +
292 +			var startAngle = rotation.getAngle();
293 +			var dAngle = rotation.normalizeDeg(rotation.normalizeDeg(-heading) - startAngle);
294 +			var t0 = performance.now();
295 +			var ePrev = 0;
296 +
297 +			var step = function (now) {
298 +				if (!compassOn) { navAnim = null; return; }
299 +				if (followIdleTimer) { navAnim = null; return; } // user interrupted
300 +				var p = (now - t0) / NAV_TRANSITION_MS;
301 +				if (p >= 1) {
302 +					rotation.setAngle(rotation.normalizeDeg(-heading));
303 +					navAnim = null;
304 +					startFollowing();
305 +					return;
306 +				}
307 +				// keep turning the same way if the target slips across the ±180 seam
308 +				var d = rotation.normalizeDeg(rotation.normalizeDeg(-heading) - startAngle);
309 +				if (d - dAngle > 180) { d -= 360; } else if (dAngle - d > 180) { d += 360; }
310 +				dAngle = d;
311 +				var e = p * p * p * (p * (p * 6 - 15) + 10); // smootherstep
312 +				// move by the fraction of REMAINING distance this step covers, so cumulative
313 +				// progress is exactly `e` (frame-invariant despite panBy shifting the frame)
314 +				var frac = ePrev < 1 ? (e - ePrev) / (1 - ePrev) : 1;
315 +				ePrev = e;
316 +				rotation.setAngle(rotation.normalizeDeg(startAngle + dAngle * e));
317 +				var a = followAnchorPoint();
318 +				var target = rotation.clientToStage(a.x, a.y);
319 +				var cur = map.latLngToContainerPoint(marker.getLatLng());
320 +				map.panBy([(cur.x - target.x) * frac, (cur.y - target.y) * frac], { animate: false });
321 +				navAnim = requestAnimationFrame(step);
322 +			};
323 +			navAnim = requestAnimationFrame(step);
324 +		}
325 +
326 +		// --- rotation (heading-up) ---
327 +
328 +		// ease stageAngle toward the live heading; runs only while following.
329 +		function animateRotation() {
330 +			if (rotAnimating) { return; }
331 +			rotAnimating = true;
332 +			var step = function () {
333 +				if (!compassOn || !following) { rotAnimating = false; return; }
334 +				var delta = rotation.normalizeDeg(-heading - rotation.getAngle());
335 +				if (Math.abs(delta) < 0.05) {
336 +					rotation.setAngle(rotation.normalizeDeg(-heading));
337 +					pinToAnchor();
338 +					rotAnimating = false;
339 +					return;
340 +				}
341 +				rotation.setAngle(rotation.getAngle() + delta * 0.2); // exp ease
342 +				pinToAnchor(); // re-pin same tick so the ladybug never lags the spin
343 +				requestAnimationFrame(step);
344 +			};
345 +			requestAnimationFrame(step);
346 +		}
347 +
348 +		// --- orientation ---
349 +
350 +		function startCompass() {
351 +			if (compassStarted) { return; }
352 +			compassStarted = true;
353 +			var onReading = function (e) {
354 +				var h;
355 +				if (typeof e.webkitCompassHeading === "number") {
356 +					h = e.webkitCompassHeading; // iOS: deg clockwise from north
357 +				} else if (typeof e.alpha === "number") {
358 +					h = 360 - e.alpha;
359 +				} else {
360 +					return;
361 +				}
362 +				heading = h;
363 +				var first = !headingReady;
364 +				headingReady = true;
365 +				if (first && pendingEntry) { pendingEntry = false; animateToAnchor(); return; }
366 +				if (compassOn) { animateRotation(); }
367 +			};
368 +			window.addEventListener("deviceorientationabsolute", onReading, true);
369 +			window.addEventListener("deviceorientation", onReading, true);
370 +		}
371 +
372 +		function requestOrientationPermission() {
373 +			var DOE = window.DeviceOrientationEvent;
374 +			if (DOE && typeof DOE.requestPermission === "function") {
375 +				return DOE.requestPermission().then(function (s) { return s === "granted"; }).catch(function () { return false; });
376 +			}
377 +			return Promise.resolve(true);
378 +		}
379 +
380 +		// --- location ---
381 +
382 +		map.on("locationfound", function (e) {
383 +			var now = performance.now();
384 +			var firstFix = !marker;
385 +			if (firstFix) {
386 +				marker = L.marker(e.latlng, { icon: makeIcon(), interactive: false, pane: "ladybugPane" }).addTo(map);
387 +				// fade in: add .visible next frame so the opacity transition runs from 0
388 +				var pin = marker._icon && marker._icon.querySelector(".ladybug-pin");
389 +				if (pin) { requestAnimationFrame(function () { pin.classList.add("visible"); }); }
390 +				// event already selected before location came on: draw the line now
391 +				var active = window.getActiveEvent && window.getActiveEvent();
392 +				if (active) { setLineTarget(active.latlng, active.color); }
393 +			} else {
394 +				var dt = (now - lastFix.t) / 1000;
395 +				if (map.distance(e.latlng, marker.getLatLng()) >= SNAP_DIST_M) {
396 +					// huge jump (first good fix after a bad one): no glide across town
397 +					vel.lat = 0;
398 +					vel.lng = 0;
399 +					marker.setLatLng(e.latlng);
400 +					if (following) { pinToAnchor(); }
401 +				} else if (dt >= 10) {
402 +					// stale gap: old velocity means nothing
403 +					vel.lat = 0;
404 +					vel.lng = 0;
405 +				} else if (dt > 0.05) {
406 +					// velocity from the last two fixes drives the dead-reckoner; blending
407 +					// damps jitter (sub-50ms bursts keep the running value)
408 +					vel.lat += ((e.latlng.lat - lastFix.lat) / dt - vel.lat) * VEL_SMOOTH;
409 +					vel.lng += ((e.latlng.lng - lastFix.lng) / dt - vel.lng) * VEL_SMOOTH;
410 +				}
411 +			}
412 +			lastFix = { lat: e.latlng.lat, lng: e.latlng.lng, t: now };
413 +			if (!firstFix) { startPredict(); }
414 +			// first fix: ease from the current view to the anchored ladybug rather than
415 +			// hard-jumping. animateToAnchor pans relative to the marker, so just engaging
416 +			// it (once a heading is ready) gives the fly-in for free.
417 +			if (firstFix && compassOn) {
418 +				if (headingReady) { animateToAnchor(); }
419 +				else { pendingEntry = true; }
420 +			}
421 +		});
422 +
423 +		function startWatching() {
424 +			if (watching) { return; }
425 +			watching = true;
426 +			map.locate({ watch: true, enableHighAccuracy: true, maximumAge: 10000 });
427 +		}
428 +
429 +		// --- compass tap (enter/exit nav mode) ---
430 +		function enterNav() {
431 +			requestOrientationPermission().then(function (ok) {
432 +				if (!ok) { return; } // orientation denied: stay off
433 +				startCompass();
434 +				startWatching();
435 +				compassOn = true;
436 +				compass.classList.remove("paused");
437 +				ringReset();
438 +				compass.classList.add("active");
439 +				// ease to anchor + rotate heading-up together; defer to the first reading
440 +				// if none yet, so it rotates too (not move-then-rotate)
441 +				if (headingReady) { animateToAnchor(); }
442 +				else { pendingEntry = true; }
443 +			});
444 +		}
445 +
446 +		function exitNav() {
447 +			compassOn = false;
448 +			compass.classList.remove("active", "paused");
449 +			ringReset();
450 +			stopFollowing();
451 +			// angle holds where it is - no snap back to north
452 +		}
453 +
454 +		compass.style.pointerEvents = "auto"; // CSS sets none; nav makes it tappable
455 +		compass.style.cursor = "pointer";
456 +		compass.addEventListener("click", function () {
457 +			if (compassOn) { exitNav(); } else { enterNav(); }
458 +		});
459 +
460 +		// a map gesture in nav mode is a free-look: suspend on touchdown, start the
461 +		// ease-back only once the finger(s) release
462 +		var mapEl = map.getContainer();
463 +		mapEl.addEventListener("touchstart", suspendFollow, { passive: true });
464 +		mapEl.addEventListener("touchend", armEaseBack, { passive: true });
465 +		mapEl.addEventListener("touchcancel", armEaseBack, { passive: true });
466 +
467 +		// selecting an event counts as an interaction: suspend follow so the pan plays
468 +		// out, then restart the countdown. No-op outside nav mode (compassOn guards).
469 +		window.navInteract = function () {
470 +			suspendFollow();
471 +			armEaseBack();
472 +		};
473 +	};
474 +})();
resources/panel.js
  1 +// Right panel: title/body/RSVP text, its color-shift + character-by-character typing, and
  2 +// which event is selected. map.js calls selectEvent / deselectEvent on taps; nav.js reads
  3 +// window.getActiveEvent and draws the location->event line via window.navLine.
  4 +
  5 +(function () {
  6 +	var fmt = window.fmt;
  7 +
  8 +	var intro = {
  9 +		color: "antiquewhite",
 10 +		title: "Upcoming Events"
 11 +	};
 12 +	// flipped on by setIntroData when there are no upcoming events: the intro lists the
 13 +	// most-recently-passed events under a "Past Events" heading instead.
 14 +	var pastMode = false;
 15 +
 16 +	var rightBox = document.getElementById("right-box");
 17 +	var themeColorMeta = document.querySelector('meta[name="theme-color"]');
 18 +	var titleEl = document.getElementById("text-title");
 19 +	var contentEl = document.getElementById("text-content");
 20 +	var actionWrap = document.getElementById("action-wrap");
 21 +	var swapTimer = null;
 22 +	var linkTimer = null;
 23 +	var typeRaf = null;
 24 +	var currentEvent = null;
 25 +	var activePin = null; // .event-pin element of the selected marker (square -> circle)
 26 +	var introEvents = []; // upcoming events for the intro table, soonest first
 27 +	var introDataReady = false; // true once map.js hands over the events (post-fetch)
 28 +	var selectFromIntro = null; // map.js's selectMarker, so table rows can activate an event
 29 +	var introListEl = null; // the current intro grid, so fitIntroColumns can re-measure on resize
 30 +
 31 +	function introTitle() { return pastMode ? "Past Events" : intro.title; }
 32 +
 33 +	var CHAR_MS = readMs("--type-char-ms");
 34 +
 35 +	function readMs(name) {
 36 +		var raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
 37 +		var ms = parseFloat(raw);
 38 +		return /s$/.test(raw) && !/ms$/.test(raw) ? ms * 1000 : ms;
 39 +	}
 40 +
 41 +	function clearTyping() {
 42 +		if (typeRaf !== null) { cancelAnimationFrame(typeRaf); typeRaf = null; }
 43 +	}
 44 +
 45 +	// Lay the full text in up front, split into a "shown" span and a trailing "hidden"
 46 +	// (visibility:hidden) span. Layout/wrapping/height are final from frame one, so typing
 47 +	// only moves the boundary
 48 +	function prepTyped(el, text) {
 49 +		el.textContent = "";
 50 +		var shown = document.createElement("span");
 51 +		var hidden = document.createElement("span");
 52 +		hidden.className = "type-hidden";
 53 +		hidden.textContent = text;
 54 +		el.appendChild(shown);
 55 +		el.appendChild(hidden);
 56 +		return { shown: shown, hidden: hidden, text: text };
 57 +	}
 58 +
 59 +	function typeStream(segments) {
 60 +		var prepped = segments.map(function (seg) {
 61 +			// flat segments (RSVP labels) already reserve width via a CSS sizer; flowing
 62 +			// body text gets the split-span treatment to pre-reserve wrapping + height
 63 +			var p = seg.flat
 64 +				? { shown: seg.el, hidden: null, text: seg.text }
 65 +				: prepTyped(seg.el, seg.text);
 66 +			// list marker stays hidden until this segment's first char reveals
 67 +			p.markerEl = seg.markerEl || null;
 68 +			return p;
 69 +		});
 70 +		// total time scales with text length; progress through it is smoothstep-eased.
 71 +		// rAF-driven (one reflow/frame) so CHAR_MS can sit below the ~1ms timer floor.
 72 +		var total = prepped.reduce(function (n, s) { return n + s.text.length; }, 0);
 73 +		var duration = total * CHAR_MS;
 74 +		var start = null;
 75 +		function frame(now) {
 76 +			if (start === null) { start = now; }
 77 +			var t = duration > 0 ? Math.min(1, (now - start) / duration) : 1;
 78 +			var eased = t * t * (3 - 2 * t); // smoothstep: zero velocity at both ends
 79 +			var revealed = Math.min(total, Math.round(eased * total));
 80 +			var n = revealed;
 81 +			prepped.forEach(function (s) {
 82 +				var len = s.text.length;
 83 +				var c = n <= 0 ? 0 : (n >= len ? len : n);
 84 +				s.shown.textContent = s.text.slice(0, c);
 85 +				if (s.hidden) { s.hidden.textContent = s.text.slice(c); }
 86 +				if (s.markerEl && c > 0) { s.markerEl.classList.add("li-typing"); }
 87 +				n -= len;
 88 +			});
 89 +			typeRaf = revealed < total ? requestAnimationFrame(frame) : null;
 90 +		}
 91 +		typeRaf = requestAnimationFrame(frame);
 92 +		return duration;
 93 +	}
 94 +
 95 +	// Build the event's .ics in memory and trigger a download. On iOS, tapping the file
 96 +	// offers "Add to Calendar"; desktop drops it in Downloads.
 97 +	function downloadICS(m) {
 98 +		if (!m) { return; }
 99 +		var blob = new Blob([fmt.icsContent(m)], { type: "text/calendar;charset=utf-8" });
100 +		var url = URL.createObjectURL(blob);
101 +		var a = document.createElement("a");
102 +		a.href = url;
103 +		a.download = fmt.icsFilename(m);
104 +		document.body.appendChild(a);
105 +		a.click();
106 +		document.body.removeChild(a);
107 +		setTimeout(function () { URL.revokeObjectURL(url); }, 0);
108 +	}
109 +
110 +	var canShare = typeof navigator.share === "function";
111 +
112 +	// "Copy Link" -> "Copied Link!" feedback: swap instantly, hold, swap back
113 +	function flashCopied(a) {
114 +		a.textContent = "Copied Link!";
115 +		a._copyTimer = setTimeout(function () {
116 +			a.textContent = "Copy Link";
117 +			a._copyTimer = null;
118 +		}, 3000);
119 +	}
120 +
121 +	var LINKS = [
122 +		{ text: "RSVP by E-mail", onClick: function (_a, m) { window.location.href = fmt.mailtoHref(m); } },
123 +		{ text: "Add to Calendar", onClick: function (a, m) { downloadICS(m); } },
124 +		// maps:// via location.href: the OS intercepts the scheme and the page stays put,
125 +		// so no orphaned about:blank tab. https links still get a real tab.
126 +		{ text: "Get Directions", onClick: function (a, m) {
127 +			var href = fmt.directionsHref(m);
128 +			if (href.indexOf("maps:") === 0) { window.location.href = href; }
129 +			else { window.open(href, "_blank"); }
130 +		} },
131 +		{ text: canShare ? "Share Link" : "Copy Link", onClick: function (a, m) {
132 +			var url = fmt.eventUrl(m);
133 +			if (canShare) {
134 +				navigator.share({ url: url }).catch(function () {});
135 +			} else if (navigator.clipboard && !a._copyTimer) {
136 +				navigator.clipboard.writeText(url);
137 +				flashCopied(a);
138 +			}
139 +		} },
140 +		// on Safari this opens a printable PDF in a new tab instead of the print dialog
141 +		window.flyer.enabled ? { text: "Print Flyer", onClick: function (a, m) { window.flyer.print(m); } } : null,
142 +	].filter(Boolean);
143 +
144 +	function buildLinkRows() {
145 +		actionWrap.innerHTML = "";
146 +		return LINKS.map(function (def) {
147 +			var row = document.createElement("div");
148 +			row.className = "action-row";
149 +
150 +			var box = document.createElement("span");
151 +			box.className = "action-link action-typing";
152 +
153 +			var sizer = document.createElement("span");
154 +			sizer.className = "action-sizer";
155 +			sizer.textContent = def.text;
156 +
157 +			var shown = document.createElement("span");
158 +			shown.className = "action-shown";
159 +
160 +			box.appendChild(sizer);
161 +			box.appendChild(shown);
162 +			row.appendChild(box);
163 +			actionWrap.appendChild(row);
164 +
165 +			return { el: shown, row: row, def: def };
166 +		});
167 +	}
168 +
169 +	// Build the detail <ul> inside #text-content and return its <li>s for typing. Each <li>
170 +	// carries its text on ._text; the marker + hanging indent come from CSS.
171 +	function buildDetailList(items) {
172 +		var ul = document.createElement("ul");
173 +		ul.className = "detail-list";
174 +		contentEl.appendChild(ul);
175 +		return items.map(function (text) {
176 +			var li = document.createElement("li");
177 +			li._text = text;
178 +			ul.appendChild(li);
179 +			return li;
180 +		});
181 +	}
182 +
183 +	// Build the intro's upcoming-events table and return the segments to type (one per
184 +	// cell). Rows are clickable. No events -> a single "No upcoming events." line. A flex
185 +	// list of <div>s (not a <table>) so columns collapse cleanly.
186 +	function buildIntroList() {
187 +		introListEl = null;
188 +		if (!introEvents.length) {
189 +			var p = document.createElement("p");
190 +			contentEl.appendChild(p);
191 +			return [{ el: p, text: pastMode ? "No past events." : "No upcoming events." }];
192 +		}
193 +
194 +		var list = document.createElement("div");
195 +		list.className = "intro-list";
196 +		contentEl.appendChild(list);
197 +		introListEl = list;
198 +
199 +		var segments = [];
200 +		introEvents.forEach(function (m) {
201 +			var row = document.createElement("div");
202 +			row.className = "intro-row";
203 +			row.addEventListener("click", function () {
204 +				if (selectFromIntro) { selectFromIntro(m, true); }
205 +			});
206 +
207 +			// each cell types into a child span so its final width is reserved (prepTyped)
208 +			[
209 +				{ text: m.title, cls: "intro-name" },
210 +				{ text: fmt.shortDate(m, pastMode), cls: "intro-when" },
211 +				{ text: m.venue, cls: "intro-where" }
212 +			].forEach(function (col) {
213 +				var cell = document.createElement("div");
214 +				cell.className = col.cls;
215 +				var span = document.createElement("span");
216 +				cell.appendChild(span);
217 +				row.appendChild(cell);
218 +				segments.push({ el: span, text: col.text });
219 +			});
220 +
221 +			list.appendChild(row);
222 +		});
223 +
224 +		return segments;
225 +	}
226 +
227 +	// True if any title cell is clipped at the current column layout. With date + location
228 +	// held rigid, a long title (or a title squeezed by them) ellipsizes here, which is the
229 +	// signal to shed a column.
230 +	function titleClipped() {
231 +		var cells = introListEl.querySelectorAll(".intro-name");
232 +		for (var i = 0; i < cells.length; i++) {
233 +			if (cells[i].scrollWidth - cells[i].clientWidth > 1) { return true; }
234 +		}
235 +		return false;
236 +	}
237 +
238 +	// Shed columns by priority until the title fits: drop the date (.cols-2), then the
239 +	// location (.cols-1). Re-measured on build and on container resize. Reading scrollWidth
240 +	// forces the reflow between steps.
241 +	function fitIntroColumns() {
242 +		if (!introListEl || !introListEl.isConnected) { return; }
243 +		introListEl.classList.remove("cols-2", "cols-1");
244 +		if (titleClipped()) {
245 +			introListEl.classList.add("cols-2");
246 +			if (titleClipped()) { introListEl.classList.add("cols-1"); }
247 +		}
248 +	}
249 +
250 +	function swapLinks(links) {
251 +		links.forEach(function (link) {
252 +			var def = link.def;
253 +			var a = document.createElement("a");
254 +			a.className = "action-link";
255 +			a.textContent = def.text;
256 +			if (def.href) {
257 +				a.href = def.href(currentEvent);
258 +			} else {
259 +				a.href = "#";
260 +				a.addEventListener("click", function (e) {
261 +					e.preventDefault();
262 +					def.onClick(a, currentEvent);
263 +				});
264 +			}
265 +			link.row.replaceChild(a, link.row.firstChild);
266 +		});
267 +	}
268 +
269 +	// Round the active pin into a circle (and square the previous one back) via a class.
270 +	function setActivePin(m) {
271 +		if (activePin) { activePin.classList.remove("active"); }
272 +		activePin = null;
273 +		var icon = m && m._marker && m._marker._icon;
274 +		var pin = icon && icon.querySelector(".event-pin");
275 +		if (pin) { pin.classList.add("active"); activePin = pin; }
276 +	}
277 +
278 +	function selectEvent(m) {
279 +		if (m === currentEvent) { return; } // already viewing it; don't retype
280 +		currentEvent = m;
281 +		setActivePin(m);
282 +		// draw the location->event line (no-op unless nav mode dropped a ladybug)
283 +		if (window.navLine) { window.navLine.setTarget(L.latLng(m.latitude, m.longitude), m.color); }
284 +		transitionTo(m.color, function () {
285 +			var links = buildLinkRows();
286 +			// description types into its own <p>, then each detail <li> as its own segment
287 +			var para = document.createElement("p");
288 +			contentEl.appendChild(para);
289 +			var details = buildDetailList(fmt.detailItems(m, true));
290 +			var segments = [
291 +				{ el: titleEl, text: m.title },
292 +				{ el: para, text: m.description }
293 +			];
294 +			details.forEach(function (li) {
295 +				segments.push({ el: li, text: li._text, markerEl: li });
296 +			});
297 +			links.forEach(function (link) {
298 +				segments.push({ el: link.el, text: link.def.text, flat: true });
299 +			});
300 +			var duration = typeStream(segments);
301 +			// once the whole stream finishes, make the links clickable
302 +			linkTimer = setTimeout(function () {
303 +				swapLinks(links);
304 +			}, duration);
305 +		});
306 +	}
307 +
308 +	// nav.js asks for this on its first location fix: if an event is already selected, it
309 +	// draws the line to it immediately.
310 +	window.getActiveEvent = function () {
311 +		if (!currentEvent) { return null; }
312 +		return { latlng: L.latLng(currentEvent.latitude, currentEvent.longitude), color: currentEvent.color };
313 +	};
314 +
315 +	// Background tap deselects and types the intro back in.
316 +	function deselectEvent() {
317 +		if (currentEvent === null) { return; } // already showing the intro
318 +		currentEvent = null;
319 +		setActivePin(null);
320 +		if (window.navLine) { window.navLine.clear(); }
321 +		transitionTo(intro.color, function () {
322 +			var segments = [{ el: titleEl, text: introTitle() }];
323 +			typeStream(segments.concat(buildIntroList()));
324 +			fitIntroColumns();
325 +		});
326 +	}
327 +
328 +	// Drive the panel background and the iOS Safari top/bottom bars off one color so the
329 +	// bars track the selected event.
330 +	function setEventColor(color) {
331 +		// :root so the body strips (overscroll/safe-area) and the panel both pick it up
332 +		document.documentElement.style.setProperty("--event-color", color);
333 +		if (themeColorMeta) themeColorMeta.setAttribute("content", color);
334 +	}
335 +
336 +	// Shared transition: shift color, fade the text out, then run `paint` once faded.
337 +	function transitionTo(color, paint) {
338 +		clearTimeout(swapTimer);
339 +		clearTimeout(linkTimer);
340 +		clearTyping();
341 +
342 +		setEventColor(color);
343 +		titleEl.classList.add("fading");
344 +		contentEl.classList.add("fading");
345 +		actionWrap.classList.add("fading");
346 +
347 +		swapTimer = setTimeout(function () {
348 +			contentEl.textContent = "";
349 +			actionWrap.innerHTML = "";
350 +			// reset scroll now (old text faded + cleared) so the prior event never flashes back
351 +			rightBox.scrollTop = 0;
352 +
353 +			titleEl.classList.remove("fading");
354 +			contentEl.classList.remove("fading");
355 +			actionWrap.classList.remove("fading");
356 +
357 +			paint();
358 +		}, 125);
359 +	}
360 +
361 +	// Intro fully shown (no typing). used on root page load, where typing is skipped.
362 +	function paintIntro() {
363 +		setEventColor(intro.color);
364 +		titleEl.textContent = introTitle();
365 +		contentEl.textContent = "";
366 +		actionWrap.innerHTML = "";
367 +		if (!introDataReady) { return; } // pre-fetch: title only, no table yet
368 +		buildIntroList().forEach(function (seg) { seg.el.textContent = seg.text; });
369 +		fitIntroColumns();
370 +	}
371 +
372 +	// map.js hands over the upcoming events (soonest first) + selectMarker, before the
373 +	// first paintIntro / deselect, so the table can build from real data.
374 +	function setIntroData(events, selectFn, past) {
375 +		introEvents = events;
376 +		selectFromIntro = selectFn;
377 +		pastMode = !!past;
378 +		introDataReady = true;
379 +	}
380 +
381 +	// Set the panel color up front (before any text paints) so a deep-linked event shows in
382 +	// its own color instead of fading in from antiquewhite. Zero the color-shift duration
383 +	// for this one set so it snaps, then restore.
384 +	function presetEventColor(m) {
385 +		var root = document.documentElement;
386 +		root.style.setProperty("--color-shift-ms", "0ms");
387 +		setEventColor(m.color);
388 +		void root.offsetWidth; // force reflow so the instant set lands before restore
389 +		root.style.removeProperty("--color-shift-ms");
390 +	}
391 +
392 +	// Re-fit the intro columns when the panel changes width (rotation, window resize).
393 +	if (typeof ResizeObserver === "function") {
394 +		new ResizeObserver(fitIntroColumns).observe(rightBox);
395 +	}
396 +
397 +	window.panel = {
398 +		selectEvent: selectEvent,
399 +		deselectEvent: deselectEvent,
400 +		paintIntro: paintIntro,
401 +		setIntroData: setIntroData,
402 +		presetEventColor: presetEventColor,
403 +		// the event currently shown, or null on the intro (lets flyer.js print on Cmd+P)
404 +		getSelectedEvent: function () { return currentEvent; }
405 +	};
406 +})();
resources/pdf.js
  1 +// Minimal single-page PDF writer for the flyer's Safari path: vector text (built-in
  2 +// Times-Roman/Times-Bold, no embedding), filled rects (QR modules), dashed lines (cut
  3 +// marks). No DOM. Pure data in, bytes out, so it also runs under node. Coordinates are
  4 +// raw PDF points, origin bottom-left (y-up); callers flip. Text is WinAnsi-encoded
  5 +// (ASCII/Latin-1 pass through, common Win-1252 punctuation mapped, else "?"); widthOf uses
  6 +// the same encoding via the vendored AFM widths so wrapping matches what renders.
  7 +
  8 +(function (root) {
  9 +	// unicode -> WinAnsi byte for the 0x80-0x9F slots worth mapping
 10 +	var UNI2WIN = {
 11 +		0x20AC: 0x80, 0x201A: 0x82, 0x0192: 0x83, 0x201E: 0x84, 0x2026: 0x85,
 12 +		0x2020: 0x86, 0x2021: 0x87, 0x02C6: 0x88, 0x2030: 0x89, 0x0160: 0x8A,
 13 +		0x2039: 0x8B, 0x0152: 0x8C, 0x017D: 0x8E, 0x2018: 0x91, 0x2019: 0x92,
 14 +		0x201C: 0x93, 0x201D: 0x94, 0x2022: 0x95, 0x2013: 0x96, 0x2014: 0x97,
 15 +		0x02DC: 0x98, 0x2122: 0x99, 0x0161: 0x9A, 0x203A: 0x9B, 0x0153: 0x9C,
 16 +		0x017E: 0x9E, 0x0178: 0x9F
 17 +	};
 18 +
 19 +	// Adobe core AFM advance widths (1/1000 em), codes 32-255 in WinAnsi order.
 20 +	// Zeros mark codes with no WinAnsi glyph; encode() never emits those.
 21 +	var W_TIMES = parseWidths("250 333 408 500 500 833 778 180 333 333 500 564 250 333 250 278 500 500 500 500 500 500 500 500 500 500 278 278 564 564 564 444 921 722 667 667 722 611 556 722 722 333 389 722 611 889 722 722 556 722 667 556 611 722 722 944 722 722 611 333 278 333 469 500 333 444 500 444 500 444 333 500 500 278 278 500 278 778 500 500 500 500 333 389 278 500 500 722 500 500 444 480 200 480 541 0 500 0 333 500 444 1000 500 500 333 1000 556 333 889 0 611 0 0 333 333 444 444 350 500 1000 333 980 389 333 722 0 444 722 250 333 500 500 500 500 200 500 333 760 276 500 564 333 760 333 400 564 300 300 333 500 453 250 333 300 310 500 750 750 750 444 722 722 722 722 722 722 889 667 611 611 611 611 333 333 333 333 722 722 722 722 722 722 722 564 722 722 722 722 722 722 556 500 444 444 444 444 444 444 667 444 444 444 444 444 278 278 278 278 500 500 500 500 500 500 500 564 500 500 500 500 500 500 500 500");
 22 +	var W_TIMES_BOLD = parseWidths("250 333 555 500 500 1000 833 278 333 333 500 570 250 333 250 278 500 500 500 500 500 500 500 500 500 500 333 333 570 570 570 500 930 722 667 722 722 667 611 778 778 389 500 778 667 944 722 778 611 778 722 556 667 722 722 1000 722 722 667 333 278 333 581 500 333 500 556 444 556 444 333 500 556 278 333 556 278 833 556 500 556 556 444 389 333 556 500 722 500 500 444 394 220 394 520 0 500 0 333 500 500 1000 500 500 333 1000 556 333 1000 0 667 0 0 333 333 500 500 350 500 1000 333 1000 389 333 722 0 444 722 250 333 500 500 500 500 220 500 333 747 300 500 570 333 747 333 400 570 300 300 333 556 540 250 333 300 330 500 750 750 750 500 722 722 722 722 722 722 1000 722 667 667 667 667 389 389 389 389 722 722 778 778 778 778 778 570 778 722 722 722 722 722 611 556 500 500 500 500 500 500 722 444 444 444 444 444 278 278 278 278 500 556 500 500 500 500 500 570 500 556 556 556 556 500 556 500");
 23 +
 24 +	function parseWidths(s) {
 25 +		return s.split(" ").map(Number);
 26 +	}
 27 +
 28 +	// string -> array of WinAnsi byte values (unmappable code points become "?")
 29 +	function encode(str) {
 30 +		var bytes = [];
 31 +		Array.from(String(str)).forEach(function (ch) {
 32 +			var cp = ch.codePointAt(0);
 33 +			if (cp >= 0x20 && cp <= 0x7E) { bytes.push(cp); return; }
 34 +			if (cp >= 0xA0 && cp <= 0xFF) { bytes.push(cp); return; }
 35 +			bytes.push(UNI2WIN[cp] || 0x3F);
 36 +		});
 37 +		return bytes;
 38 +	}
 39 +
 40 +	function widthOf(str, font, size) {
 41 +		var tbl = font === "timesBold" ? W_TIMES_BOLD : W_TIMES;
 42 +		var w = 0;
 43 +		encode(str).forEach(function (b) {
 44 +			w += tbl[b - 32] || 500;
 45 +		});
 46 +		return w * size / 1000;
 47 +	}
 48 +
 49 +	// PDF literal string body: escape delimiters, octal-escape non-ASCII bytes
 50 +	// (always 3 digits so a following literal digit can't extend the escape)
 51 +	function escapeBytes(bytes) {
 52 +		return bytes.map(function (b) {
 53 +			if (b === 0x28 || b === 0x29 || b === 0x5C) { return "\\" + String.fromCharCode(b); }
 54 +			if (b < 32 || b > 126) { return "\\" + ("00" + b.toString(8)).slice(-3); }
 55 +			return String.fromCharCode(b);
 56 +		}).join("");
 57 +	}
 58 +
 59 +	// compact decimal, never exponential
 60 +	function num(n) {
 61 +		var s = n.toFixed(3).replace(/\.?0+$/, "");
 62 +		return s === "-0" ? "0" : s;
 63 +	}
 64 +
 65 +	function create(widthPt, heightPt) {
 66 +		var ops = [];
 67 +		var lastGray = null;
 68 +		var lastWordSp = 0;
 69 +
 70 +		// x,y position the baseline start. rotate90 renders the run reading down the page
 71 +		// (baseline 0,-1; glyph-up 1,0). wordSpacing (PDF Tw) persists across BT/ET, so
 72 +		// emit only on change.
 73 +		function text(str, x, y, opts) {
 74 +			var f = opts.font === "timesBold" ? "/F2" : "/F1";
 75 +			var ws = opts.wordSpacing || 0;
 76 +			var tw = ws !== lastWordSp ? num(ws) + " Tw " : "";
 77 +			lastWordSp = ws;
 78 +			var pos = opts.rotate90
 79 +				? "0 -1 1 0 " + num(x) + " " + num(y) + " Tm"
 80 +				: num(x) + " " + num(y) + " Td";
 81 +			ops.push("BT " + f + " " + num(opts.size) + " Tf " + tw + pos +
 82 +				" (" + escapeBytes(encode(str)) + ") Tj ET");
 83 +			lastGray = null; // BT/ET doesn't touch fill color, but stay conservative
 84 +		}
 85 +
 86 +		// filled rect anchored at its bottom-left; gray 0 = black, 1 = white
 87 +		function rect(x, y, w, h, gray) {
 88 +			var g = gray || 0;
 89 +			if (g !== lastGray) {
 90 +				ops.push(num(g) + " g");
 91 +				lastGray = g;
 92 +			}
 93 +			ops.push(num(x) + " " + num(y) + " " + num(w) + " " + num(h) + " re f");
 94 +		}
 95 +
 96 +		function dashedLine(x1, y1, x2, y2, opts) {
 97 +			ops.push("q [" + num(opts.dash[0]) + " " + num(opts.dash[1]) + "] 0 d " +
 98 +				num(opts.width) + " w 0 G " +
 99 +				num(x1) + " " + num(y1) + " m " + num(x2) + " " + num(y2) + " l S Q");
100 +		}
101 +
102 +		// assemble as a binary string (char codes <= 0xFF) so string offsets are byte
103 +		// offsets, then convert to a Uint8Array
104 +		function end() {
105 +			var stream = ops.join("\n");
106 +			var objects = [
107 +				"<< /Type /Catalog /Pages 2 0 R >>",
108 +				"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
109 +				"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 " + num(widthPt) + " " + num(heightPt) + "]" +
110 +					" /Resources << /Font << /F1 5 0 R /F2 6 0 R >> >> /Contents 4 0 R >>",
111 +				"<< /Length " + stream.length + " >>\nstream\n" + stream + "\nendstream",
112 +				"<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman /Encoding /WinAnsiEncoding >>",
113 +				"<< /Type /Font /Subtype /Type1 /BaseFont /Times-Bold /Encoding /WinAnsiEncoding >>"
114 +			];
115 +			var out = "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n";
116 +			var offsets = [];
117 +			objects.forEach(function (body, i) {
118 +				offsets.push(out.length);
119 +				out += (i + 1) + " 0 obj\n" + body + "\nendobj\n";
120 +			});
121 +			var xref = out.length;
122 +			out += "xref\n0 " + (objects.length + 1) + "\n0000000000 65535 f \n";
123 +			offsets.forEach(function (off) {
124 +				out += ("000000000" + off).slice(-10) + " 00000 n \n";
125 +			});
126 +			out += "trailer\n<< /Size " + (objects.length + 1) + " /Root 1 0 R >>\n" +
127 +				"startxref\n" + xref + "\n%%EOF\n";
128 +			var bytes = new Uint8Array(out.length);
129 +			for (var i = 0; i < out.length; i++) { bytes[i] = out.charCodeAt(i); }
130 +			return bytes;
131 +		}
132 +
133 +		return { text: text, rect: rect, dashedLine: dashedLine, end: end };
134 +	}
135 +
136 +	var api = { create: create, widthOf: widthOf };
137 +	root.pdf = api;
138 +	if (typeof module !== "undefined" && module.exports) { module.exports = api; }
139 +})(typeof window !== "undefined" ? window : globalThis);
resources/qr.js
  1 +// Self-contained QR encoder (byte mode, EC level M, versions 1-40).
  2 +// createMatrix(text) returns a square matrix[row][col] of 0/1 modules
  3 +
  4 +(function () {
  5 +	// Galois Field tables for Reed-Solomon
  6 +	var GF_EXP = new Array(512);
  7 +	var GF_LOG = new Array(256);
  8 +
  9 +	(function initGF() {
 10 +		var x = 1;
 11 +		for (var i = 0; i < 255; i++) {
 12 +			GF_EXP[i] = x;
 13 +			GF_LOG[x] = i;
 14 +			x <<= 1;
 15 +			if (x & 0x100) x ^= 0x11d;
 16 +		}
 17 +		for (var j = 255; j < 512; j++) {
 18 +			GF_EXP[j] = GF_EXP[j - 255];
 19 +		}
 20 +	})();
 21 +
 22 +	function gfMul(a, b) {
 23 +		if (a === 0 || b === 0) return 0;
 24 +		return GF_EXP[GF_LOG[a] + GF_LOG[b]];
 25 +	}
 26 +
 27 +	// Alignment pattern positions lookup table (from QR spec)
 28 +	var ALIGNMENT_POSITIONS = [
 29 +		null,
 30 +		[],
 31 +		[6, 18],
 32 +		[6, 22],
 33 +		[6, 26],
 34 +		[6, 30],
 35 +		[6, 34],
 36 +		[6, 22, 38],
 37 +		[6, 24, 42],
 38 +		[6, 26, 46],
 39 +		[6, 28, 50],
 40 +		[6, 30, 54],
 41 +		[6, 32, 58],
 42 +		[6, 34, 62],
 43 +		[6, 26, 46, 66],
 44 +		[6, 26, 48, 70],
 45 +		[6, 26, 50, 74],
 46 +		[6, 30, 54, 78],
 47 +		[6, 30, 56, 82],
 48 +		[6, 30, 58, 86],
 49 +		[6, 34, 62, 90],
 50 +		[6, 28, 50, 72, 94],
 51 +		[6, 26, 50, 74, 98],
 52 +		[6, 30, 54, 78, 102],
 53 +		[6, 28, 54, 80, 106],
 54 +		[6, 32, 58, 84, 110],
 55 +		[6, 30, 58, 86, 114],
 56 +		[6, 34, 62, 90, 118],
 57 +		[6, 26, 50, 74, 98, 122],
 58 +		[6, 30, 54, 78, 102, 126],
 59 +		[6, 26, 52, 78, 104, 130],
 60 +		[6, 30, 56, 82, 108, 134],
 61 +		[6, 34, 60, 86, 112, 138],
 62 +		[6, 30, 58, 86, 114, 142],
 63 +		[6, 34, 62, 90, 118, 146],
 64 +		[6, 30, 54, 78, 102, 126, 150],
 65 +		[6, 24, 50, 76, 102, 128, 154],
 66 +		[6, 28, 54, 80, 106, 132, 158],
 67 +		[6, 32, 58, 84, 110, 136, 162],
 68 +		[6, 26, 54, 82, 110, 138, 166],
 69 +		[6, 30, 58, 86, 114, 142, 170]
 70 +	];
 71 +
 72 +	// EC_TABLE: [totalDataCodewords, ecPerBlock, group1Blocks, group1Data, group2Blocks, group2Data]
 73 +	var EC_TABLE = [
 74 +		null,
 75 +		[16, 10, 1, 16, 0, 0],     // V1
 76 +		[28, 16, 1, 28, 0, 0],     // V2
 77 +		[44, 26, 1, 44, 0, 0],     // V3
 78 +		[64, 18, 2, 32, 0, 0],     // V4
 79 +		[86, 24, 2, 43, 0, 0],     // V5
 80 +		[108, 16, 4, 27, 0, 0],    // V6
 81 +		[124, 18, 4, 31, 0, 0],    // V7
 82 +		[154, 22, 2, 38, 2, 39],   // V8
 83 +		[182, 22, 3, 36, 2, 37],   // V9
 84 +		[216, 26, 4, 43, 1, 44],   // V10
 85 +		[254, 30, 1, 50, 4, 51],   // V11
 86 +		[290, 22, 6, 36, 2, 37],   // V12
 87 +		[334, 22, 8, 37, 1, 38],   // V13
 88 +		[365, 24, 4, 40, 5, 41],   // V14
 89 +		[415, 24, 5, 41, 5, 42],   // V15
 90 +		[453, 28, 7, 45, 3, 46],   // V16
 91 +		[507, 28, 10, 46, 1, 47],  // V17
 92 +		[563, 26, 9, 43, 4, 44],   // V18
 93 +		[627, 26, 3, 44, 11, 45],  // V19
 94 +		[669, 26, 3, 41, 13, 42],  // V20
 95 +		[714, 26, 17, 42, 0, 0],   // V21
 96 +		[782, 28, 17, 46, 0, 0],   // V22
 97 +		[860, 28, 4, 47, 14, 48],  // V23
 98 +		[914, 28, 6, 45, 14, 46],  // V24
 99 +		[1000, 28, 8, 47, 13, 48], // V25
100 +		[1062, 28, 19, 46, 4, 47], // V26
101 +		[1128, 28, 22, 45, 3, 46], // V27
102 +		[1193, 28, 3, 45, 23, 46], // V28
103 +		[1267, 28, 21, 45, 7, 46], // V29
104 +		[1373, 28, 19, 47, 10, 48],// V30
105 +		[1455, 28, 2, 46, 29, 47], // V31
106 +		[1541, 28, 10, 46, 23, 47],// V32
107 +		[1631, 28, 14, 46, 21, 47],// V33
108 +		[1725, 28, 14, 46, 23, 47],// V34
109 +		[1812, 28, 12, 47, 26, 48],// V35
110 +		[1914, 28, 6, 47, 34, 48], // V36
111 +		[1992, 28, 29, 46, 14, 47],// V37
112 +		[2102, 28, 13, 46, 32, 47],// V38
113 +		[2216, 28, 40, 47, 7, 48], // V39
114 +		[2334, 28, 18, 47, 31, 48] // V40
115 +	];
116 +
117 +	function getVersion(byteLength) {
118 +		for (var v = 1; v <= 40; v++) {
119 +			var charCountBits = v <= 9 ? 8 : 16;
120 +			var dataBits = 4 + charCountBits + byteLength * 8;
121 +			var dataBytes = Math.ceil(dataBits / 8);
122 +			if (dataBytes <= EC_TABLE[v][0]) return v;
123 +		}
124 +		return 40;
125 +	}
126 +
127 +	function createQR(text) {
128 +		var bytes = new TextEncoder().encode(text);
129 +		var version = getVersion(bytes.length);
130 +		var size = version * 4 + 17;
131 +		var matrix = Array.from({ length: size }, function () { return Array(size).fill(null); });
132 +
133 +		addFinderPatterns(matrix, size);
134 +		addSeparators(matrix, size);
135 +		addTimingPatterns(matrix, size);
136 +		addAlignmentPatterns(matrix, version, size);
137 +		addDarkModule(matrix, version);
138 +
139 +		reserveFormatAreas(matrix, size);
140 +		if (version >= 7) reserveVersionAreas(matrix, size);
141 +
142 +		var reserved = createReservedMap(matrix, size);
143 +
144 +		var data = encodeData(bytes, version);
145 +		var ecData = addErrorCorrection(data, version);
146 +		placeData(matrix, ecData, size);
147 +
148 +		var mask = applyBestMask(matrix, size, reserved);
149 +		addFormatInfo(matrix, size, mask);
150 +		if (version >= 7) addVersionInfo(matrix, version);
151 +
152 +		return matrix;
153 +	}
154 +
155 +	// Finder pattern: 7x7 with specific structure
156 +	// Outer black border, inner white border, center 3x3 black
157 +	function addFinderPatterns(matrix, size) {
158 +		var positions = [
159 +			[0, 0],           // top-left
160 +			[0, size - 7],    // top-right
161 +			[size - 7, 0]     // bottom-left
162 +		];
163 +
164 +		for (var p = 0; p < positions.length; p++) {
165 +			var startRow = positions[p][0], startCol = positions[p][1];
166 +			for (var r = 0; r < 7; r++) {
167 +				for (var c = 0; c < 7; c++) {
168 +					var isBlack;
169 +					if (r === 0 || r === 6 || c === 0 || c === 6) {
170 +						isBlack = true; // outer border
171 +					} else if (r === 1 || r === 5 || c === 1 || c === 5) {
172 +						isBlack = false; // inner white border
173 +					} else {
174 +						isBlack = true; // center 3x3
175 +					}
176 +					matrix[startRow + r][startCol + c] = isBlack ? 1 : 0;
177 +				}
178 +			}
179 +		}
180 +	}
181 +
182 +	// Separators: 1-module white border around finder patterns
183 +	function addSeparators(matrix, size) {
184 +		for (var i = 0; i < 8; i++) {
185 +			matrix[7][i] = 0;
186 +			matrix[i][7] = 0;
187 +		}
188 +		for (var j = 0; j < 8; j++) {
189 +			matrix[7][size - 8 + j] = 0;
190 +			matrix[j][size - 8] = 0;
191 +		}
192 +		for (var k = 0; k < 8; k++) {
193 +			matrix[size - 8][k] = 0;
194 +			matrix[size - 8 + k][7] = 0;
195 +		}
196 +	}
197 +
198 +	// Timing patterns: alternating modules on row 6 and column 6
199 +	function addTimingPatterns(matrix, size) {
200 +		for (var i = 0; i < size; i++) {
201 +			var bit = i % 2 === 0 ? 1 : 0;
202 +			if (matrix[6][i] === null) matrix[6][i] = bit;
203 +			if (matrix[i][6] === null) matrix[i][6] = bit;
204 +		}
205 +	}
206 +
207 +	// Alignment patterns: 5x5 with black border, white inner, black center
208 +	function addAlignmentPatterns(matrix, version, size) {
209 +		if (version < 2) return;
210 +
211 +		var positions = ALIGNMENT_POSITIONS[version];
212 +
213 +		for (var ri = 0; ri < positions.length; ri++) {
214 +			for (var ci = 0; ci < positions.length; ci++) {
215 +				var row = positions[ri], col = positions[ci];
216 +				if (isInFinderPattern(row, col, size)) continue; // skip finder overlap
217 +
218 +				for (var r = -2; r <= 2; r++) {
219 +					for (var c = -2; c <= 2; c++) {
220 +						var isBlack;
221 +						if (Math.abs(r) === 2 || Math.abs(c) === 2) {
222 +							isBlack = true;  // outer border
223 +						} else if (r === 0 && c === 0) {
224 +							isBlack = true;  // center
225 +						} else {
226 +							isBlack = false; // inner white ring
227 +						}
228 +						matrix[row + r][col + c] = isBlack ? 1 : 0;
229 +					}
230 +				}
231 +			}
232 +		}
233 +	}
234 +
235 +	function isInFinderPattern(row, col, size) {
236 +		if (row <= 7 && col <= 7) return true;
237 +		if (row <= 7 && col >= size - 8) return true;
238 +		if (row >= size - 8 && col <= 7) return true;
239 +		return false;
240 +	}
241 +
242 +	// Dark module: always at matrix[4*version+9][8] per spec
243 +	function addDarkModule(matrix, version) {
244 +		matrix[4 * version + 9][8] = 1;
245 +	}
246 +
247 +	// Reserve format info areas (filled in later)
248 +	function reserveFormatAreas(matrix, size) {
249 +		for (var i = 0; i < 9; i++) {
250 +			if (matrix[8][i] === null) matrix[8][i] = 0;
251 +			if (matrix[i][8] === null) matrix[i][8] = 0;
252 +		}
253 +		for (var j = 0; j < 8; j++) {
254 +			if (matrix[8][size - 1 - j] === null) matrix[8][size - 1 - j] = 0;
255 +		}
256 +		for (var k = 0; k < 7; k++) {
257 +			if (matrix[size - 1 - k][8] === null) matrix[size - 1 - k][8] = 0;
258 +		}
259 +	}
260 +
261 +	// Reserve version info areas (version 7+)
262 +	function reserveVersionAreas(matrix, size) {
263 +		for (var i = 0; i < 6; i++) {
264 +			for (var j = 0; j < 3; j++) {
265 +				matrix[i][size - 11 + j] = 0;
266 +			}
267 +		}
268 +		for (var k = 0; k < 6; k++) {
269 +			for (var m = 0; m < 3; m++) {
270 +				matrix[size - 11 + m][k] = 0;
271 +			}
272 +		}
273 +	}
274 +
275 +	function createReservedMap(matrix, size) {
276 +		var reserved = Array.from({ length: size }, function () { return Array(size).fill(false); });
277 +
278 +		for (var row = 0; row < size; row++) {
279 +			for (var col = 0; col < size; col++) {
280 +				if (matrix[row][col] !== null) {
281 +					reserved[row][col] = true;
282 +				}
283 +			}
284 +		}
285 +
286 +		return reserved;
287 +	}
288 +
289 +	function encodeData(bytes, version) {
290 +		var bits = [];
291 +		var charCountBits = version <= 9 ? 8 : 16;
292 +
293 +		bits.push(0, 1, 0, 0); // mode: byte
294 +
295 +		for (var i = charCountBits - 1; i >= 0; i--) {
296 +			bits.push((bytes.length >> i) & 1); // character count
297 +		}
298 +
299 +		for (var b = 0; b < bytes.length; b++) {
300 +			for (var k = 7; k >= 0; k--) {
301 +				bits.push((bytes[b] >> k) & 1);
302 +			}
303 +		}
304 +
305 +		// terminator (up to 4 zeros)
306 +		var capacity = EC_TABLE[version][0] * 8;
307 +		for (var t = 0; t < 4 && bits.length < capacity; t++) {
308 +			bits.push(0);
309 +		}
310 +
311 +		while (bits.length % 8 !== 0 && bits.length < capacity) {
312 +			bits.push(0); // pad to byte boundary
313 +		}
314 +
315 +		// pad codewords
316 +		var padBytes = [0xEC, 0x11];
317 +		var padIndex = 0;
318 +		while (bits.length < capacity) {
319 +			var pad = padBytes[padIndex++ % 2];
320 +			for (var pb = 7; pb >= 0; pb--) {
321 +				bits.push((pad >> pb) & 1);
322 +			}
323 +		}
324 +
325 +		return bits;
326 +	}
327 +
328 +	function addErrorCorrection(data, version) {
329 +		var row = EC_TABLE[version];
330 +		var totalCap = row[0], ecPerBlock = row[1];
331 +		var g1Count = row[2], g1Data = row[3], g2Count = row[4], g2Data = row[5];
332 +
333 +		var dataBytes = [];
334 +		for (var i = 0; i < data.length; i += 8) {
335 +			var byte = 0;
336 +			for (var j = 0; j < 8; j++) byte = (byte << 1) | (data[i + j] || 0);
337 +			dataBytes.push(byte);
338 +		}
339 +
340 +		while (dataBytes.length < totalCap) {
341 +			dataBytes.push(dataBytes.length % 2 === 0 ? 0xEC : 0x11); // pad to capacity
342 +		}
343 +
344 +		var blocks = [];
345 +		var ecBlocks = [];
346 +		var offset = 0;
347 +
348 +		for (var a = 0; a < g1Count; a++) {
349 +			var b1 = dataBytes.slice(offset, offset + g1Data);
350 +			blocks.push(b1);
351 +			ecBlocks.push(generateECBytes(b1, ecPerBlock));
352 +			offset += g1Data;
353 +		}
354 +
355 +		for (var c = 0; c < g2Count; c++) {
356 +			var b2 = dataBytes.slice(offset, offset + g2Data);
357 +			blocks.push(b2);
358 +			ecBlocks.push(generateECBytes(b2, ecPerBlock));
359 +			offset += g2Data;
360 +		}
361 +
362 +		var resultBytes = [];
363 +		var maxDataSize = Math.max(g1Data, g2Data);
364 +
365 +		// interleave data byte-by-byte across all blocks
366 +		for (var d = 0; d < maxDataSize; d++) {
367 +			for (var e = 0; e < blocks.length; e++) {
368 +				if (d < blocks[e].length) {
369 +					resultBytes.push(blocks[e][d]);
370 +				}
371 +			}
372 +		}
373 +
374 +		// interleave EC byte-by-byte (EC sizes are always equal)
375 +		for (var f = 0; f < ecPerBlock; f++) {
376 +			for (var g = 0; g < ecBlocks.length; g++) {
377 +				resultBytes.push(ecBlocks[g][f]);
378 +			}
379 +		}
380 +
381 +		var resultBits = [];
382 +		for (var rb = 0; rb < resultBytes.length; rb++) {
383 +			for (var h = 7; h >= 0; h--) resultBits.push((resultBytes[rb] >> h) & 1);
384 +		}
385 +		return resultBits;
386 +	}
387 +
388 +	function generateECBytes(data, ecCount) {
389 +		var gen = [1];
390 +		for (var i = 0; i < ecCount; i++) {
391 +			var next = new Array(gen.length + 1).fill(0);
392 +			for (var j = 0; j < gen.length; j++) {
393 +				next[j] ^= gen[j];
394 +				next[j + 1] ^= gfMul(gen[j], GF_EXP[i]);
395 +			}
396 +			for (var k = 0; k < next.length; k++) gen[k] = next[k];
397 +			gen.length = next.length;
398 +		}
399 +
400 +		var remainder = new Array(ecCount).fill(0);
401 +		for (var d = 0; d < data.length; d++) {
402 +			var factor = data[d] ^ remainder[0];
403 +			remainder.shift();
404 +			remainder.push(0);
405 +			for (var m = 0; m < ecCount; m++) {
406 +				remainder[m] ^= gfMul(gen[m + 1], factor);
407 +			}
408 +		}
409 +
410 +		return remainder;
411 +	}
412 +
413 +	// Place data in zigzag pattern from bottom-right, skipping column 6
414 +	function placeData(matrix, data, size) {
415 +		var bitIndex = 0;
416 +		var up = true;
417 +
418 +		for (var col = size - 1; col >= 1; col -= 2) {
419 +			if (col === 6) col = 5;  // skip timing column
420 +
421 +			for (var i = 0; i < size; i++) {
422 +				var row = up ? size - 1 - i : i;
423 +
424 +				for (var j = 0; j < 2; j++) {
425 +					var c = col - j;
426 +					if (matrix[row][c] === null) {
427 +						matrix[row][c] = bitIndex < data.length ? data[bitIndex++] : 0;
428 +					}
429 +				}
430 +			}
431 +			up = !up;
432 +		}
433 +	}
434 +
435 +	function applyBestMask(matrix, size, reserved) {
436 +		var bestMask = 0;
437 +		var bestPenalty = Infinity;
438 +
439 +		for (var mask = 0; mask < 8; mask++) {
440 +			var copy = matrix.map(function (r) { return r.slice(); });
441 +			applyMask(copy, size, mask, reserved);
442 +			var penalty = calculatePenalty(copy, size);
443 +
444 +			if (penalty < bestPenalty) {
445 +				bestPenalty = penalty;
446 +				bestMask = mask;
447 +			}
448 +		}
449 +
450 +		applyMask(matrix, size, bestMask, reserved);
451 +		return bestMask;
452 +	}
453 +
454 +	function applyMask(matrix, size, mask, reserved) {
455 +		for (var row = 0; row < size; row++) {
456 +			for (var col = 0; col < size; col++) {
457 +				if (reserved[row][col]) continue;
458 +
459 +				var invert = false;
460 +				switch (mask) {
461 +					case 0: invert = (row + col) % 2 === 0; break;
462 +					case 1: invert = row % 2 === 0; break;
463 +					case 2: invert = col % 3 === 0; break;
464 +					case 3: invert = (row + col) % 3 === 0; break;
465 +					case 4: invert = (Math.floor(row / 2) + Math.floor(col / 3)) % 2 === 0; break;
466 +					case 5: invert = (row * col) % 2 + (row * col) % 3 === 0; break;
467 +					case 6: invert = ((row * col) % 2 + (row * col) % 3) % 2 === 0; break;
468 +					case 7: invert = ((row + col) % 2 + (row * col) % 3) % 2 === 0; break;
469 +				}
470 +
471 +				if (invert) matrix[row][col] ^= 1;
472 +			}
473 +		}
474 +	}
475 +
476 +	function calculatePenalty(matrix, size) {
477 +		var penalty = 0;
478 +
479 +		// rule 1: 5+ consecutive same-color modules
480 +		for (var i = 0; i < size; i++) {
481 +			var rowRun = 1, colRun = 1;
482 +			for (var j = 1; j < size; j++) {
483 +				rowRun = matrix[i][j] === matrix[i][j - 1] ? rowRun + 1 : 1;
484 +				if (rowRun === 5) penalty += 3;
485 +				else if (rowRun > 5) penalty += 1;
486 +
487 +				colRun = matrix[j][i] === matrix[j - 1][i] ? colRun + 1 : 1;
488 +				if (colRun === 5) penalty += 3;
489 +				else if (colRun > 5) penalty += 1;
490 +			}
491 +		}
492 +
493 +		// rule 2: 2x2 blocks of same color
494 +		for (var r = 0; r < size - 1; r++) {
495 +			for (var c = 0; c < size - 1; c++) {
496 +				var v = matrix[r][c];
497 +				if (v === matrix[r][c + 1] && v === matrix[r + 1][c] && v === matrix[r + 1][c + 1]) {
498 +					penalty += 3;
499 +				}
500 +			}
501 +		}
502 +
503 +		return penalty;
504 +	}
505 +
506 +	function addFormatInfo(matrix, size, mask) {
507 +		var format = (0 << 3) | mask; // Level M (00) + Mask
508 +		var rem = format;
509 +		for (var i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
510 +		var bits = ((format << 10) | rem) ^ 0x5412;
511 +
512 +		function getBit(i) { return (bits >> i) & 1; }
513 +
514 +		var coords = [
515 +			[8, 0], [8, 1], [8, 2], [8, 3], [8, 4], [8, 5], [8, 7], [8, 8], [7, 8], [5, 8], [4, 8], [3, 8], [2, 8], [1, 8], [0, 8]
516 +		];
517 +
518 +		for (var c = 0; c < 15; c++) {
519 +			var r = coords[c][0], col = coords[c][1];
520 +			var bit = getBit(c);
521 +			matrix[r][col] = bit; // first copy
522 +			// second copy, mirrored around the other finders
523 +			if (c < 8) {
524 +				matrix[8][size - 1 - c] = bit;
525 +			} else {
526 +				matrix[size - 1 - (14 - c)][8] = bit;
527 +			}
528 +		}
529 +	}
530 +
531 +	function addVersionInfo(matrix, version) {
532 +		if (version < 7) return;
533 +
534 +		// 18-bit BCH Error Corrected Version Codes
535 +		var versionBits = {
536 +			7:  0x07C94, 8:  0x085BC, 9:  0x09A99, 10: 0x0A4D3,
537 +			11: 0x0BBF6, 12: 0x0C762, 13: 0x0D847, 14: 0x0E60D,
538 +			15: 0x0F928, 16: 0x10B78, 17: 0x1145D, 18: 0x12A17,
539 +			19: 0x13532, 20: 0x149A6, 21: 0x15683, 22: 0x168C9,
540 +			23: 0x177EC, 24: 0x18EC4, 25: 0x191E1, 26: 0x1AFAB,
541 +			27: 0x1B08E, 28: 0x1CC1A, 29: 0x1D33F, 30: 0x1ED75,
542 +			31: 0x1F250, 32: 0x209D5, 33: 0x216F0, 34: 0x228BA,
543 +			35: 0x2379F, 36: 0x24B0B, 37: 0x2542E, 38: 0x26A64,
544 +			39: 0x27541, 40: 0x28C69
545 +		};
546 +
547 +		var bits = versionBits[version];
548 +		var size = matrix.length;
549 +
550 +		for (var i = 0; i < 18; i++) {
551 +			var bit = (bits >> i) & 1;
552 +			var a = Math.floor(i / 3);
553 +			var b = i % 3;
554 +
555 +			matrix[size - 11 + b][a] = bit; // bottom-left block
556 +			matrix[a][size - 11 + b] = bit; // top-right block
557 +		}
558 +	}
559 +
560 +	window.qr = { createMatrix: createQR };
561 +})();
resources/styles.css
  1 +:root {
  2 +	--color-shift-ms: 350ms;
  3 +	--type-char-ms: 0.5ms;
  4 +	--event-color: antiquewhite;
  5 +	--link-pad: 6px;
  6 +}
  7 +
  8 +html, body {
  9 +	margin: 0;
 10 +	height: 100%;
 11 +	overflow: hidden;
 12 +	overscroll-behavior: none;
 13 +	background-color: var(--event-color);
 14 +	transition: background-color var(--color-shift-ms) ease-in-out;
 15 +	font-family: serif;
 16 +}
 17 +
 18 +::selection {
 19 +	background: antiquewhite;
 20 +}
 21 +
 22 +#boxes {
 23 +	display: flex;
 24 +	flex-direction: row;
 25 +	height: 100vh;
 26 +	height: 100dvh;
 27 +}
 28 +#left-box {
 29 +	width: 50%;
 30 +	height: 100%;
 31 +	box-sizing: border-box;
 32 +	position: relative;
 33 +	overflow: hidden;
 34 +	-webkit-user-select: none;
 35 +	user-select: none;
 36 +}
 37 +#right-box {
 38 +	width: 50%;
 39 +	height: 100%;
 40 +	box-sizing: border-box;
 41 +	container: text-box / inline-size;
 42 +	padding: 22px;
 43 +	padding-top: 14px;
 44 +	margin: 0;
 45 +	color: black;
 46 +	overflow-y: scroll;
 47 +	overscroll-behavior: contain;
 48 +	scrollbar-color: #00000060 transparent;
 49 +	background-color: var(--event-color);
 50 +	transition: background-color var(--color-shift-ms) ease-in-out;
 51 +}
 52 +
 53 +#right-box ::selection {
 54 +	background: black;
 55 +	color: var(--event-color);
 56 +}
 57 +#right-box h1 {
 58 +	margin: 0;
 59 +	font-weight: normal;
 60 +	font-size: 38px;
 61 +	margin-bottom: -3px;
 62 +	min-height: 44px;
 63 +}
 64 +#right-box p, #text-content {
 65 +	font-size: 22px;
 66 +	line-height: 29px;
 67 +	margin-top: 16px;
 68 +}
 69 +#text-content > p {
 70 +	margin: 0;
 71 +}
 72 +.detail-list {
 73 +	margin: 16px 0 0;
 74 +	padding-left: 1.2em;
 75 +	list-style-position: outside;
 76 +}
 77 +.detail-list li {
 78 +	margin-top: 2px;
 79 +	list-style: disc;
 80 +}
 81 +.detail-list li::marker {
 82 +	color: transparent;
 83 +}
 84 +.detail-list li.li-typing::marker {
 85 +	color: currentColor;
 86 +}
 87 +
 88 +.intro-list {
 89 +	display: grid;
 90 +	grid-template-columns: minmax(0, max-content) minmax(0, 1fr) max-content minmax(0, 1fr) max-content;
 91 +	column-gap: 16px;
 92 +	row-gap: 2px;
 93 +	font-size: 22px;
 94 +	line-height: 29px;
 95 +}
 96 +.intro-row {
 97 +	display: grid;
 98 +	grid-column: 1 / -1;
 99 +	grid-template-columns: subgrid;
100 +	align-items: baseline;
101 +	padding: var(--link-pad);
102 +	margin: 0 calc(-1 * var(--link-pad));
103 +	cursor: pointer;
104 +	-webkit-tap-highlight-color: transparent;
105 +}
106 +.intro-name {
107 +	grid-column: 1;
108 +	min-width: 0;
109 +	white-space: nowrap;
110 +	overflow: hidden;
111 +	text-overflow: ellipsis;
112 +	text-decoration: underline;
113 +	text-underline-offset: 3px;
114 +}
115 +.intro-when {
116 +	grid-column: 3;
117 +	white-space: nowrap;
118 +}
119 +.intro-where {
120 +	grid-column: 5;
121 +	min-width: 0;
122 +	white-space: nowrap;
123 +	overflow: hidden;
124 +	text-overflow: ellipsis;
125 +}
126 +@media (hover: hover) {
127 +	.intro-row:hover {
128 +		background: black;
129 +		color: antiquewhite;
130 +	}
131 +	.intro-row:hover .intro-name {
132 +		text-decoration: none;
133 +	}
134 +}
135 +
136 +/* panel.js drops columns by class when the title would otherwise ellipsize: date first
137 +   (.cols-2), then location (.cols-1). Title is always shown. */
138 +.intro-list.cols-2 {
139 +	grid-template-columns: minmax(0, max-content) minmax(0, 1fr) max-content;
140 +}
141 +.intro-list.cols-2 .intro-when {
142 +	display: none;
143 +}
144 +.intro-list.cols-2 .intro-where {
145 +	grid-column: 3;
146 +}
147 +.intro-list.cols-1 {
148 +	grid-template-columns: minmax(0, 1fr);
149 +}
150 +.intro-list.cols-1 .intro-when,
151 +.intro-list.cols-1 .intro-where {
152 +	display: none;
153 +}
154 +
155 +#text-content, #text-title, #action-wrap {
156 +	transition: opacity 0.125s ease-in-out;
157 +}
158 +.type-hidden {
159 +	visibility: hidden;
160 +}
161 +#text-content.fading, #text-title.fading, #action-wrap.fading {
162 +	opacity: 0;
163 +}
164 +
165 +#action-wrap {
166 +	margin-top: 25px;
167 +	margin-bottom: 50px;
168 +	text-align: center;
169 +}
170 +.action-row {
171 +	font-size: 22px;
172 +	line-height: 29px;
173 +}
174 +.action-row + .action-row {
175 +	margin-top: 18px;
176 +}
177 +.action-link {
178 +	display: inline-block;
179 +	color: #000000;
180 +	text-decoration: underline;
181 +	text-underline-offset: 3px;
182 +	padding: var(--link-pad);
183 +	margin: calc(-1 * var(--link-pad));
184 +	cursor: pointer;
185 +	-webkit-tap-highlight-color: transparent;
186 +}
187 +.intro-row, .action-link {
188 +	position: relative;
189 +}
190 +.intro-row::before, .intro-row::after,
191 +.action-link::before, .action-link::after {
192 +	content: "";
193 +	position: absolute;
194 +	left: 0;
195 +	right: 0;
196 +	height: 3px;
197 +}
198 +.intro-row::before, .action-link::before {
199 +	top: -3px;
200 +}
201 +.intro-row::after, .action-link::after {
202 +	bottom: -3px;
203 +}
204 +@media (hover: hover) {
205 +	a.action-link:hover {
206 +		background: black;
207 +		color: var(--event-color);
208 +		text-decoration: none;
209 +	}
210 +}
211 +
212 +.action-typing {
213 +	position: relative;
214 +	display: inline-block;
215 +	text-align: left;
216 +	white-space: pre;
217 +}
218 +.action-sizer {
219 +	visibility: hidden;
220 +}
221 +.action-shown {
222 +	position: absolute;
223 +	left: var(--link-pad);
224 +	top: var(--link-pad);
225 +	text-decoration: underline;
226 +	text-underline-offset: 3px;
227 +}
228 +
229 +#map-stage {
230 +	position: absolute;
231 +	top: 50%;
232 +	left: 50%;
233 +	transform-origin: center center;
234 +	will-change: transform;
235 +}
236 +#map-stage:not([style*="width"]) {
237 +	inset: 0;
238 +	top: 0;
239 +	left: 0;
240 +}
241 +
242 +#map-container {
243 +	height: 100%;
244 +}
245 +
246 +#map-container .leaflet-tile,
247 +#map-container .leaflet-tile-container,
248 +#map-container canvas.leaflet-tile-container {
249 +	pointer-events: none;
250 +}
251 +
252 +#map-container .leaflet-zoom-anim .leaflet-zoom-animated {
253 +	transition: transform 0.12s cubic-bezier(0, 0, 0.25, 1);
254 +}
255 +
256 +#map-container.map-grabbing,
257 +#map-container.map-grabbing .leaflet-grab,
258 +#map-container.map-grabbing .leaflet-interactive {
259 +	cursor: grabbing !important;
260 +}
261 +
262 +#compass {
263 +	display: none;
264 +	position: absolute;
265 +	right: 12px;
266 +	bottom: 12px;
267 +	z-index: 1000;
268 +	width: 48px;
269 +	height: 48px;
270 +	border-radius: 50%;
271 +	background: rgba(255, 255, 255, 0.7);
272 +	-webkit-backdrop-filter: blur(8px);
273 +	backdrop-filter: blur(8px);
274 +	-webkit-tap-highlight-color: transparent;
275 +	box-shadow: 0 1px 3px #00000040;
276 +	transition: box-shadow 0.12s linear;
277 +	align-items: center;
278 +	justify-content: center;
279 +	pointer-events: none;
280 +}
281 +#compass.compass-enabled {
282 +	display: flex;
283 +}
284 +@media (pointer: coarse) {
285 +	#compass::after {
286 +		content: "";
287 +		position: absolute;
288 +		top: 0;
289 +		left: 0;
290 +		right: -12px;
291 +		bottom: -12px;
292 +		border-radius: 24px 0 0 0;
293 +	}
294 +}
295 +#compass svg {
296 +	display: block;
297 +	transform-origin: center center;
298 +	transform: rotate(var(--compass-angle, 0deg));
299 +}
300 +#compass .needle-n { fill: #e8483a; }
301 +#compass .needle-s { fill: #b8bcc2; }
302 +#compass .needle-hub { fill: #fafafa; stroke: #b8bcc2; stroke-width: 1; }
303 +#compass.active {
304 +	box-shadow: 0 1px 3px #00000040, inset 0 0 0 2.5px cornflowerblue;
305 +}
306 +#compass.active.paused {
307 +	box-shadow: 0 1px 3px #00000040, inset 0 0 0 2.5px #b8bcc2;
308 +}
309 +#compass .compass-ring {
310 +	position: absolute;
311 +	inset: 0;
312 +	transform: rotate(-90deg);
313 +	pointer-events: none;
314 +}
315 +#compass .ring-arc {
316 +	fill: none;
317 +	stroke: cornflowerblue;
318 +	stroke-width: 2.5;
319 +	stroke-dasharray: 100;
320 +	stroke-dashoffset: 100;
321 +}
322 +
323 +.ladybug-line-svg {
324 +	position: absolute;
325 +	top: 0;
326 +	left: 0;
327 +	width: 1px;
328 +	height: 1px;
329 +	pointer-events: none;
330 +	overflow: visible;
331 +	display: none;
332 +}
333 +.ladybug-line-svg.visible {
334 +	display: block;
335 +}
336 +.ladybug-line {
337 +	stroke-width: 4;
338 +	stroke-linecap: round;
339 +	fill: none;
340 +}
341 +
342 +.ladybug-pin {
343 +	width: 40px;
344 +	height: 40px;
345 +	display: flex;
346 +	align-items: center;
347 +	justify-content: center;
348 +	font-size: 30px;
349 +	line-height: 1;
350 +	transform: rotate(var(--pin-counter, 0deg));
351 +	transform-origin: center center;
352 +	opacity: 0;
353 +	transition: opacity 0.4s ease;
354 +}
355 +.ladybug-pin.visible {
356 +	opacity: 1;
357 +}
358 +
359 +.map-error {
360 +	display: flex;
361 +	align-items: center;
362 +	justify-content: center;
363 +	box-sizing: border-box;
364 +	padding: 22px;
365 +	background: #f0eee4;
366 +	color: black;
367 +	font-weight: normal;
368 +	font-size: 38px;
369 +	text-align: center;
370 +	-webkit-user-select: none;
371 +	user-select: none;
372 +}
373 +
374 +.event-pin {
375 +	width: 48px;
376 +	height: 48px;
377 +	box-sizing: border-box;
378 +	padding: 4px 6px;
379 +	transform: rotate(var(--pin-counter, 0deg));
380 +	transform-origin: center center;
381 +	display: flex;
382 +	flex-direction: column;
383 +	justify-content: center;
384 +	font-family: -apple-system, Arial, Helvetica, sans-serif;
385 +	line-height: 1.1;
386 +	color: black;
387 +	text-align: center;
388 +	box-shadow: 0 1px 3px #00000040;
389 +	cursor: pointer;
390 +	border-radius: 0;
391 +	transition: border-radius 150ms ease-in-out;
392 +}
393 +.event-pin.active {
394 +	border-radius: 50%;
395 +}
396 +.event-pin .pin-month {
397 +	font-size: 11px;
398 +	text-transform: uppercase;
399 +	letter-spacing: 0.5px;
400 +}
401 +.event-pin .pin-day {
402 +	font-size: 18px;
403 +	font-weight: bold;
404 +}
405 +
406 +@media (orientation: portrait) {
407 +	#boxes {
408 +		flex-direction: column;
409 +	}
410 +	#left-box, #right-box {
411 +		width: 100%;
412 +		height: 50%;
413 +	}
414 +}
415 +
416 +#flyer {
417 +	display: none;
418 +}
419 +
420 +#flyer.flyer-measuring {
421 +	display: flex;
422 +	flex-direction: column;
423 +	position: fixed;
424 +	left: -9999px;
425 +	top: 0;
426 +	width: 197.3mm;
427 +	height: 266.7mm;
428 +	box-sizing: border-box;
429 +	font-family: serif;
430 +	color: #000;
431 +	background: #fff;
432 +}
433 +
434 +.flyer-body {
435 +	flex: 1 1 auto;
436 +	display: flex;
437 +	flex-direction: column;
438 +	min-height: 0;
439 +	overflow: hidden;
440 +}
441 +.flyer-title {
442 +	margin: 1.5mm 0 7mm;
443 +	font-weight: normal;
444 +	font-size: 48pt;
445 +	line-height: 1.05;
446 +	overflow-wrap: break-word;
447 +}
448 +.flyer-flow {
449 +	flex: 1 1 auto;
450 +	min-height: 0;
451 +	display: flow-root;
452 +}
453 +.flyer-desc {
454 +	margin: 0;
455 +	text-align: justify;
456 +}
457 +.flyer-details {
458 +	margin: 7mm 0;
459 +	padding-left: 1.2em;
460 +	list-style: disc;
461 +}
462 +.flyer-details li {
463 +	margin-top: 0.15em;
464 +}
465 +
466 +.flyer-flow > .flyer-qr {
467 +	float: right;
468 +	margin-left: 7mm;
469 +	margin-bottom: 7mm;
470 +	margin-top: 2mm;
471 +}
472 +
473 +.flyer-tabs {
474 +	flex: 0 0 auto;
475 +	display: flex;
476 +	height: 78mm;
477 +	border-top: 1px dashed #000;
478 +}
479 +.flyer-tab {
480 +	flex: 1 1 0;
481 +	position: relative;
482 +	overflow: hidden;
483 +	border-left: 1px dashed #000;
484 +}
485 +.flyer-tab:last-child {
486 +	border-right: 1px dashed #000;
487 +}
488 +.flyer-tab-inner {
489 +	position: absolute;
490 +	top: 50%;
491 +	left: 50%;
492 +	width: 78mm;
493 +	height: 19.73mm;
494 +	transform: translate(-50%, -50%) rotate(90deg);
495 +	transform-origin: center center;
496 +	box-sizing: border-box;
497 +	padding: 2.5mm 2.5mm 2.5mm 1.5mm;
498 +	display: flex;
499 +	flex-direction: row;
500 +	align-items: center;
501 +	justify-content: center;
502 +	gap: 2.5mm;
503 +	overflow: hidden;
504 +	font-size: 7pt;
505 +}
506 +.flyer-tab-text {
507 +	flex: 1 1 auto;
508 +	min-width: 0;
509 +	text-align: left;
510 +}
511 +.flyer-tab-title {
512 +	font-size: 1.3em;
513 +	font-weight: bold;
514 +	line-height: 1.1;
515 +	overflow-wrap: break-word;
516 +}
517 +.flyer-tab-when,
518 +.flyer-tab-where {
519 +	margin-top: 0.1em;
520 +	line-height: 1.15;
521 +}
522 +.flyer-tab .flyer-qr {
523 +	flex: 0 0 auto;
524 +}
525 +
526 +@page {
527 +	size: auto;
528 +	margin: 0;
529 +}
530 +
531 +@media print {
532 +	.flyer-print * {
533 +		-webkit-print-color-adjust: exact !important;
534 +		print-color-adjust: exact !important;
535 +	}
536 +	html.flyer-print, .flyer-print body {
537 +		background: #fff !important;
538 +	}
539 +	.flyer-print #boxes {
540 +		display: none !important;
541 +	}
542 +	.flyer-print #flyer {
543 +		display: flex !important;
544 +		flex-direction: column;
545 +		width: 197.3mm;
546 +		height: 266.7mm;
547 +		margin: 6.35mm auto;
548 +		box-sizing: border-box;
549 +		font-family: serif;
550 +		color: #000;
551 +		background: #fff;
552 +	}
553 +}
serve.py
  1 +#!/usr/bin/env python3
  2 +"""
  3 +Starts HTTP server for local testing
  4 +Automatically manages a virtual environment for dependencies
  5 +"""
  6 +
  7 +import http.server
  8 +import socketserver
  9 +import socket
 10 +import sys
 11 +import os
 12 +import subprocess
 13 +from pathlib import Path
 14 +
 15 +DEFAULT_PORT = 8000
 16 +SCRIPT_DIR = Path(__file__).parent.absolute()
 17 +VENV_DIR = SCRIPT_DIR / "venv"
 18 +
 19 +
 20 +def setup_venv():
 21 +	"""Create and setup virtual environment if it doesn't exist"""
 22 +	# Determine the path to pip and python in the venv
 23 +	if sys.platform == "win32":
 24 +		pip_path = VENV_DIR / "Scripts" / "pip"
 25 +		python_path = VENV_DIR / "Scripts" / "python"
 26 +	else:
 27 +		pip_path = VENV_DIR / "bin" / "pip"
 28 +		python_path = VENV_DIR / "bin" / "python3"
 29 +
 30 +	# Check if venv needs to be created or recreated
 31 +	if not VENV_DIR.exists() or not python_path.exists():
 32 +		if VENV_DIR.exists():
 33 +			print("Virtual environment incomplete, recreating...")
 34 +			import shutil
 35 +			shutil.rmtree(VENV_DIR)
 36 +		else:
 37 +			print("Creating virtual environment...")
 38 +
 39 +		try:
 40 +			subprocess.check_call([sys.executable, "-m", "venv", str(VENV_DIR)])
 41 +			print("Virtual environment created successfully.")
 42 +		except subprocess.CalledProcessError as e:
 43 +			print(f"Error creating virtual environment: {e}")
 44 +			sys.exit(1)
 45 +
 46 +	# Ensure pip is available
 47 +	if not pip_path.exists():
 48 +		print("Installing pip in virtual environment...")
 49 +		try:
 50 +			subprocess.check_call([str(python_path), "-m", "ensurepip", "--upgrade"])
 51 +		except subprocess.CalledProcessError as e:
 52 +			print(f"Error ensuring pip: {e}")
 53 +			sys.exit(1)
 54 +
 55 +	check = subprocess.run(
 56 +		[str(python_path), "-c", "import qrcode"],
 57 +		capture_output=True
 58 +	)
 59 +	if check.returncode != 0:
 60 +		try:
 61 +			subprocess.check_call([str(python_path), "-m", "pip", "install", "-q", "qrcode"])
 62 +		except subprocess.CalledProcessError:
 63 +			print("Note: Could not install qrcode (offline?). QR codes will be unavailable.\n")
 64 +
 65 +	return python_path
 66 +
 67 +
 68 +def run_in_venv():
 69 +	"""Re-run this script in the virtual environment"""
 70 +	python_path = setup_venv()
 71 +
 72 +	# Re-run this script with the venv Python
 73 +	try:
 74 +		subprocess.check_call([str(python_path), __file__, "--in-venv"])
 75 +	except (KeyboardInterrupt, subprocess.CalledProcessError):
 76 +		pass
 77 +	sys.exit(0)
 78 +
 79 +
 80 +def get_local_ip():
 81 +	"""Get the LAN IP other devices on the network can reach this machine at,
 82 +	or None if it can't be determined."""
 83 +	try:
 84 +		# Connect to a public DNS server (doesn't actually send data) to learn
 85 +		# which local interface/IP would be used to reach the network.
 86 +		s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 87 +		s.connect(("8.8.8.8", 80))
 88 +		local_ip = s.getsockname()[0]
 89 +		s.close()
 90 +		return local_ip
 91 +	except Exception:
 92 +		return None
 93 +
 94 +
 95 +def find_available_port(start_port=DEFAULT_PORT, max_attempts=20):
 96 +	"""Return the first bindable port at or after start_port. Uses SO_REUSEADDR
 97 +	so a socket lingering in TIME_WAIT from a prior run doesn't block us."""
 98 +	for port in range(start_port, start_port + max_attempts):
 99 +		s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
100 +		s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
101 +		try:
102 +			s.bind(("", port))
103 +			return port
104 +		except OSError:
105 +			continue
106 +		finally:
107 +			s.close()
108 +	return None
109 +
110 +
111 +def print_qr_code(url, caption="Scan to connect:"):
112 +	"""Generate and print a QR code using block characters."""
113 +	try:
114 +		import qrcode
115 +
116 +		qr = qrcode.QRCode(
117 +			version=1,
118 +			error_correction=qrcode.constants.ERROR_CORRECT_L,
119 +			box_size=1,
120 +			border=1,
121 +		)
122 +		qr.add_data(url)
123 +		qr.make(fit=True)
124 +
125 +		matrix = qr.get_matrix()
126 +
127 +		# Half-block chars pack two matrix rows per terminal line, keeping the
128 +		# code compact and roughly square.
129 +		print(f"\n{caption}\n")
130 +		for y in range(0, len(matrix), 2):
131 +			line = " "
132 +			for x in range(len(matrix[y])):
133 +				top = matrix[y][x]
134 +				bottom = matrix[y + 1][x] if y + 1 < len(matrix) else False
135 +				if top and bottom:
136 +					line += "█"
137 +				elif top:
138 +					line += "▀"
139 +				elif bottom:
140 +					line += "▄"
141 +				else:
142 +					line += " "
143 +			print(line)
144 +	except ImportError:
145 +		print(f"\n{caption}\n  (QR unavailable; open this URL manually) {url}")
146 +	except Exception as e:
147 +		print(f"\nCould not generate QR code: {e}\n  open manually: {url}")
148 +
149 +
150 +class QuietHandler(http.server.SimpleHTTPRequestHandler):
151 +	"""SimpleHTTPRequestHandler that keeps connections alive, disables caching,
152 +	silences logging, and swallows broken-pipe noise. Shared by serve.py and
153 +	https_serve.py."""
154 +	# HTTP/1.1 keeps the connection alive across requests so loading the app's
155 +	# files reuses one connection instead of a fresh one per file. Requires a
156 +	# threaded server, or a held-open connection would block all others.
157 +	protocol_version = "HTTP/1.1"
158 +
159 +	def end_headers(self):
160 +		self.send_header("Cache-Control", "no-cache")
161 +		super().end_headers()
162 +
163 +	def log_message(self, format, *args):
164 +		pass
165 +
166 +	def handle(self):
167 +		try:
168 +			super().handle()
169 +		except (BrokenPipeError, ConnectionResetError):
170 +			# Browser cancelled the request (normal for media streaming/preloading)
171 +			pass
172 +
173 +
174 +def start_server():
175 +	"""Start the HTTP server (runs after venv is set up)"""
176 +	# Change to script directory
177 +	os.chdir(SCRIPT_DIR)
178 +
179 +	# Find an available port
180 +	port = find_available_port(DEFAULT_PORT)
181 +
182 +	if port is None:
183 +		print(f"Error: Could not find an available port (tried {DEFAULT_PORT}-{DEFAULT_PORT + 19})")
184 +		sys.exit(1)
185 +
186 +	# Get local IP for network access
187 +	local_ip = get_local_ip()
188 +
189 +	try:
190 +		with socketserver.ThreadingTCPServer(("", port), QuietHandler) as httpd:
191 +			httpd.daemon_threads = True
192 +			local_url = f"http://localhost:{port}"
193 +
194 +			print(f"\nServer running on port {port}")
195 +			print(f"Local access:   {local_url}")
196 +
197 +			if local_ip:
198 +				network_url = f"http://{local_ip}:{port}"
199 +				print(f"Network access: {network_url}")
200 +				print_qr_code(network_url)
201 +			else:
202 +				print("Network access: unavailable (could not determine LAN IP)")
203 +
204 +			print("\nPress Ctrl+C to stop the server")
205 +
206 +			# Serve forever
207 +			httpd.serve_forever()
208 +
209 +	except KeyboardInterrupt:
210 +		print("\n\nShutting down server...")
211 +		sys.exit(0)
212 +	except Exception as e:
213 +		print(f"\nError starting server: {e}")
214 +		sys.exit(1)
215 +
216 +
217 +def main():
218 +	"""Main entry point"""
219 +	# Check if we're already running in venv
220 +	if "--in-venv" not in sys.argv:
221 +		run_in_venv()
222 +	else:
223 +		start_server()
224 +
225 +
226 +if __name__ == "__main__":
227 +	main()
sw.js
 1 +// Caches the app shell (so it works offline) and map tiles (so panning/zooming
 2 +// reuses previously-viewed tiles instead of refetching). Leaflet is vendored in
 3 +// the shell, so map tiles are the only remaining network dependency.
 4 +
 5 +const SHELL_CACHE = "app-shell-v8";
 6 +const TILE_CACHE = "map-tiles-v1"; // CORS fetches so the cached responses aren't opaque
 7 +const KEEP = new Set([SHELL_CACHE, TILE_CACHE]);
 8 +
 9 +// Every file the app needs to boot with no network.
10 +const SHELL = [
11 +	"./",
12 +	"index.html",
13 +	"manifest.json",
14 +	"resources/icon.png",
15 +	"resources/format.js",
16 +	"resources/qr.js",
17 +	"resources/pdf.js",
18 +	"resources/flyer.js",
19 +	"resources/panel.js",
20 +	"resources/map.js",
21 +	"resources/app.js",
22 +	"resources/nav.js",
23 +	"resources/styles.css",
24 +	"resources/leaflet/leaflet-nogap.js",
25 +	"resources/demo.json",
26 +	"resources/leaflet/leaflet.js",
27 +	"resources/leaflet/leaflet.css",
28 +];
29 +
30 +// Real (gitignored) data; overrides demo.json when present, but often absent. Cached
31 +// best-effort so a 404 can't fail the install — the app boots fine on demo.json alone.
32 +const OPTIONAL = ["resources/events.json"];
33 +
34 +self.addEventListener("install", (e) => {
35 +	self.skipWaiting();
36 +	// Atomic precache of the shell: any failure rejects, so the prior shell cache
37 +	// survives a bad install. waitUntil resolves only once the full shell is cached,
38 +	// so activate won't drop the old version until this one is complete.
39 +	e.waitUntil(caches.open(SHELL_CACHE).then((cache) =>
40 +		cache.addAll(SHELL).then(() =>
41 +			Promise.all(OPTIONAL.map((u) => cache.add(u).catch(() => {})))
42 +		)
43 +	));
44 +});
45 +
46 +self.addEventListener("activate", (e) => e.waitUntil(
47 +	Promise.all([
48 +		self.clients.claim(),
49 +		// drop old cache versions (e.g. superseded shells/tiles)
50 +		caches.keys().then((keys) => Promise.all(
51 +			keys.filter((k) => !KEEP.has(k)).map((k) => caches.delete(k))
52 +		)),
53 +	])
54 +));
55 +
56 +function isCacheableTile(url) {
57 +	return url.hostname.endsWith("basemaps.cartocdn.com"); // map tiles
58 +}
59 +
60 +// network-first for same-origin app files: fresh when online, cached when not,
61 +// so edits show up on reload but the app still boots offline.
62 +async function shellResponse(request) {
63 +	const cache = await caches.open(SHELL_CACHE);
64 +	try {
65 +		const resp = await fetch(request);
66 +		if (resp.ok) cache.put(request, resp.clone());
67 +		return resp;
68 +	} catch {
69 +		const hit = await cache.match(request) || await cache.match("index.html");
70 +		if (hit) return hit;
71 +		throw new Error("offline and not cached");
72 +	}
73 +}
74 +
75 +// cache-first for map tiles: serve a cached copy instantly, else fetch and store.
76 +async function tileResponse(url) {
77 +	const cache = await caches.open(TILE_CACHE);
78 +	const hit = await cache.match(url.href);
79 +	if (hit) return hit;
80 +	const resp = await fetch(url.href, { mode: "cors", credentials: "omit" });
81 +	if (resp.ok) cache.put(url.href, resp.clone());
82 +	return resp;
83 +}
84 +
85 +self.addEventListener("fetch", (event) => {
86 +	if (event.request.method !== "GET") return;
87 +	const url = new URL(event.request.url);
88 +
89 +	if (isCacheableTile(url)) {
90 +		event.respondWith(tileResponse(url));
91 +		return;
92 +	}
93 +	// same-origin navigations + assets -> app shell
94 +	if (url.origin === self.location.origin) {
95 +		event.respondWith(shellResponse(event.request));
96 +	}
97 +});