commit 1f3e319b9ddb3e5704cf8c5cc5a3860b4a469c08
Author: Hunter
Date: Tue, 30 Jun 2026 14:57:01 -0400
initial commit
Diffstat:
25 files changed, 5959 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,10 @@
+# Local HTTPS dev certs
+/.https_certs/
+
+# Actual event data; demo.json is the committed fallback
+/resources/events.json
+
+# Python
+__pycache__/
+*.pyc
+venv/
diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/README.md b/README.md
@@ -0,0 +1,25 @@
+# /events 🐞
+
+Community-scale infrastructure for hosting local meetups and workshops.
+
+<p align="center">
+ <a href="https://hunterirving.github.io/events"><img src="readme_images/screenshot.jpg" width=170>
+ </a>
+</p>
+
+## key features
+- Auto-generated printable flyers
+- RSVP by E-mail
+- Installable as a [Progressive Web App](https://hunterirving.github.io/web_workshop/pages/pwa) for offline use
+
+## usage
+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.
+
+## licenses
+
+This project is licensed under the <a href="LICENSE">GNU General Public License v3.0</a>.
+
+### third-party resources
+
+- **[Leaflet](https://leafletjs.com)** 1.9.4 under the <a href="resources/leaflet/LICENSE-leaflet">BSD 2-Clause License</a>.
+- **[Leaflet.TileLayer.NoGap](https://github.com/Leaflet/Leaflet.TileLayer.NoGap)** (modified), under the [Beerware License](resources/leaflet/LICENSE-nogap).
diff --git a/https_serve.py b/https_serve.py
@@ -0,0 +1,363 @@
+#!/usr/bin/env python3
+"""
+Serve this project over HTTPS on your local network so the browser will hand it
+geolocation + device-orientation (both require a secure context off localhost).
+
+Uses mkcert to mint a locally-trusted certificate for this machine's LAN IP. A
+device won't trust that cert until it trusts mkcert's root CA, so we print two
+QR codes:
+
+ 1. an HTTP link that hands the device the mkcert root CA (the public root
+ cert, no secret; served to any device until you stop the script)
+ 2. an HTTPS link to the app itself
+
+Both servers shut down together on Ctrl+C.
+
+After scanning the first QR code, install + trust the CA on the device:
+ iOS: Settings → General → VPN & Device Management → install the profile,
+ then Settings → General → About → Certificate Trust Settings →
+ enable full trust
+ Android: open the downloaded file and follow the prompt to install it as a
+ CA certificate
+
+Run with --reset to revoke the CA and delete the local certificates.
+"""
+
+import http.server
+import socketserver
+import ssl
+import sys
+import os
+import subprocess
+import threading
+from pathlib import Path
+
+import serve # shared helpers: venv setup, QR printing, IP/port, QuietHandler
+
+CERT_HTTP_PORT = 8001 # preferred; falls back to the next free port
+HTTPS_PORT = 8443 # preferred; falls back to the next free port
+SCRIPT_DIR = Path(__file__).parent.absolute()
+CERT_DIR = SCRIPT_DIR / ".https_certs"
+CERT_FILE = CERT_DIR / "cert.pem"
+KEY_FILE = CERT_DIR / "key.pem"
+
+# Path the phone fetches the root CA from. Anything else over HTTP 404s.
+CA_DOWNLOAD_PATH = "/rootCA.pem"
+
+
+def run_in_venv():
+ """Re-run this script in serve.py's shared venv (qrcode), forwarding any
+ user flags (eg. --reset) to the re-exec."""
+ python_path = serve.setup_venv()
+ try:
+ subprocess.check_call([str(python_path), __file__, "--in-venv", *sys.argv[1:]])
+ except (KeyboardInterrupt, subprocess.CalledProcessError):
+ pass
+ sys.exit(0)
+
+
+# get_local_ip, find_available_port, print_qr_code, and QuietHandler are shared
+# with serve.py — reached via the `serve.` prefix below.
+
+
+def mkcert_install_command():
+ """Best-guess (command, package-manager-label) to install mkcert on this
+ platform, or (None, None) if no known manager is on PATH."""
+ from shutil import which
+ if sys.platform == "darwin":
+ if which("brew"):
+ return (["brew", "install", "mkcert"], "Homebrew")
+ elif sys.platform == "win32":
+ if which("choco"):
+ return (["choco", "install", "mkcert", "-y"], "Chocolatey")
+ if which("scoop"):
+ return (["scoop", "install", "mkcert"], "Scoop")
+ else: # linux / other unix
+ # Most distros package mkcert; pick whichever manager is present.
+ if which("apt"):
+ return (["sudo", "apt", "install", "-y", "mkcert"], "apt")
+ if which("dnf"):
+ return (["sudo", "dnf", "install", "-y", "mkcert"], "dnf")
+ if which("pacman"):
+ return (["sudo", "pacman", "-S", "--noconfirm", "mkcert"], "pacman")
+ if which("brew"):
+ return (["brew", "install", "mkcert"], "Homebrew")
+ return (None, None)
+
+
+def manual_install_hint():
+ """Platform-appropriate manual install instructions for mkcert."""
+ if sys.platform == "darwin":
+ return "brew install mkcert (https://github.com/FiloSottile/mkcert)"
+ if sys.platform == "win32":
+ return "choco install mkcert OR scoop install mkcert (https://github.com/FiloSottile/mkcert)"
+ return "install 'mkcert' via your package manager (https://github.com/FiloSottile/mkcert)"
+
+
+def require_mkcert():
+ """Ensure mkcert is on PATH, offering to install it if it isn't."""
+ from shutil import which
+ if which("mkcert") is not None:
+ return
+
+ print("mkcert is required but was not found on PATH.")
+ cmd, label = mkcert_install_command()
+ if cmd is None:
+ print(f" Install it manually: {manual_install_hint()}")
+ sys.exit(1)
+
+ answer = input(f"Install it now with {label} ({' '.join(cmd)})? [y/N] ").strip().lower()
+ if answer not in ("y", "yes"):
+ print(f" Install it manually: {manual_install_hint()}")
+ sys.exit(1)
+
+ try:
+ subprocess.check_call(cmd)
+ except (subprocess.CalledProcessError, KeyboardInterrupt):
+ print(f"\nInstall failed. Install it manually: {manual_install_hint()}")
+ sys.exit(1)
+
+ if which("mkcert") is None:
+ print("mkcert still not on PATH after install. Open a new terminal and retry.")
+ sys.exit(1)
+
+
+def ca_root_file():
+ """Path to mkcert's root CA cert (rootCA.pem)."""
+ caroot = subprocess.run(
+ ["mkcert", "-CAROOT"], capture_output=True, text=True
+ ).stdout.strip()
+ return Path(caroot) / "rootCA.pem"
+
+
+def ensure_ca_installed():
+ """Ensure mkcert's local CA exists and is trusted by this machine.
+ May prompt for your password to write to the system trust store."""
+ if not ca_root_file().exists():
+ print("Setting up mkcert local CA (you may be prompted for your password)...")
+ subprocess.check_call(["mkcert", "-install"])
+
+
+def ensure_leaf_cert(local_ip):
+ """Mint a cert/key for this machine's LAN IP (and localhost) if missing."""
+ CERT_DIR.mkdir(parents=True, exist_ok=True)
+ if CERT_FILE.exists() and KEY_FILE.exists():
+ return
+ print(f"Minting certificate for {local_ip}...")
+ subprocess.check_call([
+ "mkcert",
+ "-cert-file", str(CERT_FILE),
+ "-key-file", str(KEY_FILE),
+ local_ip, "localhost", "127.0.0.1",
+ ])
+
+
+def start_ca_server(local_ip):
+ """Start a plain-HTTP server that hands out the mkcert root CA, and return
+ it running. The CA is the *public* root cert (no secret), so there's no
+ reason to limit how many times or to how many devices it's served. Leave
+ it up alongside the HTTPS server so you can set up multiple devices, and
+ tear both down together on Ctrl+C. Caller owns shutdown."""
+ ca_file = ca_root_file()
+ if not ca_file.exists():
+ print(f"Error: root CA not found at {ca_file}")
+ sys.exit(1)
+
+ ca_bytes = ca_file.read_bytes()
+
+ class CAHandler(http.server.BaseHTTPRequestHandler):
+ def log_message(self, *args):
+ pass
+
+ def do_GET(self):
+ # Redirect bare visits to the download path so a typo'd / still works.
+ if self.path in ("/", "/index.html"):
+ self.send_response(302)
+ self.send_header("Location", CA_DOWNLOAD_PATH)
+ self.end_headers()
+ return
+ if self.path != CA_DOWNLOAD_PATH:
+ self.send_error(404)
+ return
+ # x-x509-ca-cert makes iOS offer to install it as a CA profile.
+ self.send_response(200)
+ self.send_header("Content-Type", "application/x-x509-ca-cert")
+ self.send_header("Content-Length", str(len(ca_bytes)))
+ self.send_header(
+ "Content-Disposition", 'attachment; filename="rootCA.pem"'
+ )
+ self.end_headers()
+ try:
+ self.wfile.write(ca_bytes)
+ except (BrokenPipeError, ConnectionResetError):
+ return
+
+ port = serve.find_available_port(CERT_HTTP_PORT)
+ if port is None:
+ print(f"Error: no free port near {CERT_HTTP_PORT} for the CA server.")
+ sys.exit(1)
+ http.server.HTTPServer.allow_reuse_address = True
+ httpd = http.server.HTTPServer(("", port), CAHandler)
+ threading.Thread(target=httpd.serve_forever, daemon=True).start()
+
+ cert_url = f"http://{local_ip}:{port}{CA_DOWNLOAD_PATH}"
+ print("=" * 69)
+ print("STEP 1: install + trust the local CA on each device (first time only)")
+ print("=" * 69)
+ serve.print_qr_code(cert_url, "Scan to download the root certificate:")
+ print(f" {cert_url}")
+ print("\nOn iOS, after downloading:")
+ print(" • Settings → General → VPN & Device Management → install the profile")
+ print(" • Settings → General → About → Certificate Trust Settings →")
+ print(" toggle ON full trust for the mkcert CA")
+ print("\nOn Android, after downloading:")
+ print(" • open the downloaded file, or Settings → Security → Encryption &")
+ print(" credentials → Install a certificate → CA certificate, then confirm")
+ print(" • installing the CA is what makes it trusted; there's no separate")
+ print(" trust toggle (exact menu names vary by version/manufacturer)")
+ print("\n(skip this step on a device that already trusts the CA from a previous run)")
+ return httpd
+
+
+def serve_https(local_ip, ca_httpd=None):
+ """Serve SCRIPT_DIR over HTTPS. Runs until Ctrl+C, then also shuts down the
+ CA HTTP server (if given)."""
+ os.chdir(SCRIPT_DIR)
+
+ # serve.QuietHandler's keep-alive applies just as well to the TLS connection:
+ # the app's files reuse one connection instead of a fresh handshake per file.
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+ ctx.load_cert_chain(certfile=str(CERT_FILE), keyfile=str(KEY_FILE))
+
+ port = serve.find_available_port(HTTPS_PORT)
+ if port is None:
+ print(f"Error: no free port near {HTTPS_PORT} for the HTTPS server.")
+ if ca_httpd is not None:
+ ca_httpd.shutdown()
+ ca_httpd.server_close()
+ sys.exit(1)
+
+ with socketserver.ThreadingTCPServer(("", port), serve.QuietHandler) as httpd:
+ httpd.daemon_threads = True
+ httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
+ https_url = f"https://{local_ip}:{port}/"
+
+ print("\n" + "=" * 59)
+ print("STEP 2: open the app over HTTPS (once the device trusts the CA)")
+ print("=" * 59)
+ serve.print_qr_code(https_url, "Scan to open the app over HTTPS:")
+ print(f" {https_url}")
+ print("\n(HTTPS off localhost is what lets the browser grant location +")
+ print(" device orientation, which the compass / nav mode need)")
+ print("\nPress Ctrl+C to stop the server(s)")
+
+ try:
+ httpd.serve_forever()
+ except KeyboardInterrupt:
+ print("\n\nShutting down...")
+ finally:
+ if ca_httpd is not None:
+ ca_httpd.shutdown()
+ ca_httpd.server_close()
+
+
+def caroot_dir():
+ """Path to mkcert's CAROOT folder (holds rootCA.pem and rootCA-key.pem)."""
+ caroot = subprocess.run(
+ ["mkcert", "-CAROOT"], capture_output=True, text=True
+ ).stdout.strip()
+ return Path(caroot) if caroot else None
+
+
+def reset_certs():
+ """Opt-in teardown, in two stages so it's safe alongside other mkcert use.
+
+ mkcert keeps ONE shared CA per machine, so anything else you run with
+ mkcert trusts the same CA. Stage 1 removes only THIS tool's own leaf certs
+ (always safe). Stage 2 removes the shared mkcert CA itself (untrust + optional
+ key deletion). Guarded, because it affects every project that uses
+ mkcert, not just this one."""
+ from shutil import rmtree, which
+
+ # Stage 1: our own leaf certs.
+ if CERT_DIR.exists():
+ rmtree(CERT_DIR)
+ print(f"Removed this tool's local certificates ({CERT_DIR}).")
+ else:
+ print("No local certificates to remove.")
+
+ # Stage 2: the shared mkcert CA. Skip entirely unless asked, since other
+ # projects on this machine may rely on it and we can't detect them.
+ print(
+ "\nThe mkcert CA itself is SHARED across everything you use mkcert for"
+ " on this computer, not just this tool. Leaving it installed is normal."
+ )
+ answer = input(
+ "Also remove the shared mkcert CA (untrust it system-wide)? [y/N] "
+ ).strip().lower()
+ if answer not in ("y", "yes"):
+ print("Left the mkcert CA in place.")
+ return
+
+ if which("mkcert"):
+ try:
+ subprocess.check_call(["mkcert", "-uninstall"])
+ print("Stopped this computer from trusting the mkcert CA.")
+ except subprocess.CalledProcessError as e:
+ print(f"mkcert -uninstall failed: {e}")
+
+ # mkcert -uninstall removes trust but does NOT delete the CA. The private key
+ # (rootCA-key.pem) is the sensitive material, so offer to delete it too, with
+ # a separate confirmation since it's irreversible and shared.
+ caroot = caroot_dir()
+ if caroot and (caroot / "rootCA-key.pem").exists():
+ print(
+ "\nThe CA's PRIVATE KEY is still on disk at:\n"
+ f" {caroot}\n"
+ "Per mkcert: \"the rootCA-key.pem file ... gives complete power to\n"
+ "intercept secure requests from your machine. Do not share it.\"\n"
+ "Deleting it removes the shared CA entirely; future mkcert use\n"
+ "(here or in any project) will generate a brand-new one."
+ )
+ ans2 = input("Delete the CA key + folder now? [y/N] ").strip().lower()
+ if ans2 in ("y", "yes"):
+ rmtree(caroot)
+ print(f"Removed {caroot}")
+ else:
+ print("Left the CA key in place.")
+
+ print("\nNote: this does NOT touch any device; remove or untrust the CA\n"
+ "there separately (see the instructions printed during install).")
+
+
+def start():
+ require_mkcert()
+
+ local_ip = serve.get_local_ip()
+ if not local_ip:
+ print("Error: could not determine this machine's LAN IP.")
+ print("Make sure you're connected to a network and try again.")
+ sys.exit(1)
+
+ ensure_ca_installed()
+ ensure_leaf_cert(local_ip)
+ ca_httpd = start_ca_server(local_ip)
+ serve_https(local_ip, ca_httpd=ca_httpd)
+
+
+def main():
+ if "--in-venv" not in sys.argv:
+ run_in_venv()
+ else:
+ try:
+ if "--reset" in sys.argv:
+ reset_certs()
+ else:
+ start()
+ except KeyboardInterrupt:
+ print("\nCancelled.")
+ sys.exit(130)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/index.html b/index.html
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>events</title>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="theme-color" content="antiquewhite">
+ <meta name="apple-mobile-web-app-capable" content="yes">
+ <meta name="apple-mobile-web-app-status-bar-style" content="default">
+ <meta name="apple-mobile-web-app-title" content="events">
+ <link rel="manifest" href="manifest.json">
+ <link rel="apple-touch-icon" href="resources/icon.png">
+ <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>">
+ <script>
+ // events.json is the real (gitignored) data; demo.json is the committed
+ // fallback used when no events.json is present.
+ function fetchJson(url) {
+ return fetch(url).then(function (res) {
+ if (!res.ok) { throw new Error("HTTP " + res.status); }
+ return res.json();
+ });
+ }
+ window.eventsData = fetchJson("resources/events.json")
+ .catch(function () { return fetchJson("resources/demo.json"); });
+ </script>
+ <link rel="stylesheet" href="resources/leaflet/leaflet.css">
+ <link rel="stylesheet" href="resources/styles.css">
+ </head>
+ <body>
+ <div id="boxes">
+ <div id="left-box">
+ <div id="map-stage">
+ <div id="map-container"></div>
+ </div>
+ <div id="compass" aria-hidden="true">
+ <svg viewBox="0 0 40 40" width="40" height="40">
+ <polygon class="needle-n" points="20,5 13,20 27,20"></polygon>
+ <polygon class="needle-s" points="20,35 13,20 27,20"></polygon>
+ <circle class="needle-hub" cx="20" cy="20" r="2.5"></circle>
+ </svg>
+ <svg class="compass-ring" viewBox="0 0 48 48" width="48" height="48">
+ <circle class="ring-arc" cx="24" cy="24" r="22.75" pathLength="100"></circle>
+ </svg>
+ </div>
+ </div>
+ <div id="right-box">
+ <h1 id="text-title"></h1>
+ <div id="text-content"></div>
+ <div id="action-wrap"></div>
+ </div>
+ </div>
+
+ <!-- print-only flyer; populated from the selected event on print, hidden on screen -->
+ <div id="flyer" aria-hidden="true"></div>
+
+ <script>
+ // cache the app shell + map tiles for fast revisits and offline use
+ if ("serviceWorker" in navigator) {
+ navigator.serviceWorker.register("sw.js").catch(function () {}); // ignore on file://
+ }
+ </script>
+ <script src="resources/nav.js"></script>
+ <script src="resources/format.js"></script>
+ <script src="resources/qr.js"></script>
+ <script src="resources/pdf.js"></script>
+ <script src="resources/flyer.js"></script>
+ <script src="resources/panel.js"></script>
+ <script src="resources/map.js"></script>
+ <script src="resources/app.js"></script>
+ </body>
+</html>
diff --git a/manifest.json b/manifest.json
@@ -0,0 +1,19 @@
+{
+ "id": "./",
+ "name": "events",
+ "short_name": "events",
+ "description": "events",
+ "start_url": "./",
+ "scope": "./",
+ "display": "standalone",
+ "background_color": "antiquewhite",
+ "theme_color": "antiquewhite",
+ "icons": [
+ {
+ "src": "resources/icon.png",
+ "sizes": "1024x1024",
+ "type": "image/png",
+ "purpose": "any maskable"
+ }
+ ]
+}
diff --git a/readme_images/screenshot.jpg b/readme_images/screenshot.jpg
Binary files differ.
diff --git a/resources/app.js b/resources/app.js
@@ -0,0 +1,19 @@
+(function () {
+ if (!window.location.search) { window.panel.paintIntro(); }
+ window.eventsData
+ .then(function (data) {
+ window.eventsMap.setEvents(data);
+ // preset the panel color for a deep-linked event that will actually be shown,
+ // so it never flashes antiquewhite first. A passed event is only shown (and so
+ // only worth presetting) in past mode, i.e. when nothing is upcoming.
+ var deepLinked = window.fmt.eventFromUrl(data);
+ var shown = deepLinked && (!window.fmt.hasPassed(deepLinked) ||
+ !window.fmt.upcomingEvents(data).length);
+ if (shown) { window.panel.presetEventColor(deepLinked); }
+ window.eventsMap.load();
+ })
+ .catch(function () {
+ document.getElementById("text-title").textContent = "Couldn't load events";
+ document.getElementById("text-content").textContent = "If you opened this file directly, serve it over http (./serve.py) so the browser can fetch events.json.";
+ });
+})();
diff --git a/resources/demo.json b/resources/demo.json
@@ -0,0 +1,142 @@
+[
+ {
+ "title": "Pin, Patch, & Trinket Swap",
+ "venue": "Seattle Public Library, Beacon Hill Branch",
+ "address": "2821 Beacon Ave S, Seattle, WA 98144",
+ "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.",
+ "color": "darkseagreen",
+ "start": "2026-03-23T16:00",
+ "end": "2026-03-23T18:00",
+ "price": "Free",
+ "ageRange": "All ages",
+ "rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
+ "latitude": 47.5780,
+ "longitude": -122.3114
+ },
+ {
+ "title": "HTML Day",
+ "venue": "Volunteer Park",
+ "address": "1247 15th Ave E, Seattle, WA 98112",
+ "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.",
+ "color": "mediumturquoise",
+ "start": "2026-03-28T11:00",
+ "end": "2026-03-28T14:00",
+ "price": "Free",
+ "ageRange": "All ages",
+ "rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
+ "latitude": 47.6303,
+ "longitude": -122.3157
+ },
+ {
+ "title": "Sunday Stitch & Chat",
+ "venue": "Top Pot Doughnuts",
+ "address": "609 Summit Ave E, Seattle, WA 98102",
+ "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.",
+ "color": "#d1c6f7",
+ "start": "2026-04-05T14:00",
+ "end": "2026-04-05T16:00",
+ "price": "Free",
+ "ageRange": "",
+ "rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
+ "latitude": 47.6253,
+ "longitude": -122.3274
+ },
+ {
+ "title": "Seed Swap",
+ "venue": "P-Patch Community Garden",
+ "address": "2451 15th Ave W, Seattle, WA 98119",
+ "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.",
+ "color": "yellowgreen",
+ "start": "2026-05-02T10:00",
+ "end": "2026-05-02T13:00",
+ "price": "Free",
+ "ageRange": "All ages",
+ "rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
+ "latitude": 47.6432,
+ "longitude": -122.376
+ },
+ {
+ "title": "Saturday Flea Market",
+ "venue": "Cal Anderson Park",
+ "address": "1635 11th Ave, Seattle, WA 98122",
+ "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.",
+ "color": "gold",
+ "start": "2026-05-09T08:00",
+ "end": "2026-05-09T12:00",
+ "price": "Free",
+ "ageRange": "All ages",
+ "rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
+ "latitude": 47.617,
+ "longitude": -122.3191
+ },
+ {
+ "title": "Chess in the Park",
+ "venue": "Occidental Square",
+ "address": "117 S Washington St, Seattle, WA 98104",
+ "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.",
+ "color": "#b2cbe6",
+ "start": "2026-05-13T17:30",
+ "end": "2026-05-13T19:30",
+ "price": "Free",
+ "ageRange": "All ages",
+ "rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
+ "latitude": 47.6003,
+ "longitude": -122.3331
+ },
+ {
+ "title": "Beginner Skate Meetup",
+ "venue": "Seattle Center Skatepark",
+ "address": "220 Thomas St, Seattle, WA 98109",
+ "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.",
+ "color": "#fc981e",
+ "start": "2026-05-23T10:00",
+ "end": "2026-05-23T12:00",
+ "price": "Free",
+ "ageRange": "All ages",
+ "rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
+ "latitude": 47.6247,
+ "longitude": -122.3504
+ },
+ {
+ "title": "Typewriter Poetry in the Park",
+ "venue": "Discovery Park",
+ "address": "3801 Discovery Park Blvd, Seattle, WA 98199",
+ "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.",
+ "color": "#c7bbab",
+ "start": "2026-06-03T16:00",
+ "end": "2026-06-03T18:00",
+ "price": "Free",
+ "ageRange": "All ages",
+ "rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
+ "latitude": 47.658,
+ "longitude": -122.406
+ },
+ {
+ "title": "Zine-Making Workshop",
+ "venue": "Hing Hay Park",
+ "address": "423 Maynard Ave S, Seattle, WA 98104",
+ "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.",
+ "color": "#f29bc5",
+ "start": "2026-06-06T13:00",
+ "end": "2026-06-06T16:00",
+ "price": "Free",
+ "ageRange": "All ages",
+ "rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
+ "latitude": 47.5985,
+ "longitude": -122.3238
+ },
+ {
+ "title": "Mario Kart 64 Tournament",
+ "venue": "Gas Works Park",
+ "address": "2101 N Northlake Way, Seattle, WA 98103",
+ "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.",
+ "color": "#12a33e",
+ "start": "2026-06-12T19:30",
+ "end": "2026-06-13T01:00",
+ "price": "Free",
+ "ageRange": "18+",
+ "rsvp": "lbhe_ebgngrq_rznvy_nqqerff@urer.pbz",
+ "latitude": 47.6456,
+ "longitude": -122.3344
+ }
+]
diff --git a/resources/flyer.js b/resources/flyer.js
@@ -0,0 +1,495 @@
+// Print-only flyer for the selected event. Most browsers get a print-CSS DOM flyer +
+// window.print; Safari stamps a non-removable header/footer on printed web content, so
+// there the flyer is drawn into a vector PDF (pdf.js) and opened in a new tab.
+
+(function () {
+ var SVG_NS = "http://www.w3.org/2000/svg";
+ var TAB_COUNT = 10;
+ var QUIET = 0; // no baked-in quiet zone
+
+ // Safari (desktop/iOS) but not the other browsers whose UA also carries "Safari".
+ // Safari takes the PDF path; the print CSS swap stays gated off (.flyer-print).
+ var isSafari = /Safari/.test(navigator.userAgent) &&
+ !/Chrome|Chromium|CriOS|Edg|Android/.test(navigator.userAgent);
+ if (!isSafari) { document.documentElement.classList.add("flyer-print"); }
+
+ var flyerEl = null;
+ var building = false; // guards against the link + beforeprint paths double-building
+
+ function el(tag, cls, text) {
+ var n = document.createElement(tag);
+ if (cls) { n.className = cls; }
+ if (text != null) { n.textContent = text; }
+ return n;
+ }
+
+ // QR for `text` as an <svg> `mm` square. Dark modules merge into per-row run rects.
+ function qrSvg(text, mm) {
+ var matrix = window.qr.createMatrix(text);
+ var n = matrix.length;
+ var total = n + QUIET * 2;
+
+ var svg = document.createElementNS(SVG_NS, "svg");
+ svg.setAttribute("viewBox", "0 0 " + total + " " + total);
+ svg.setAttribute("width", mm + "mm");
+ svg.setAttribute("height", mm + "mm");
+ svg.setAttribute("shape-rendering", "crispEdges");
+ svg.classList.add("flyer-qr");
+
+ var bg = document.createElementNS(SVG_NS, "rect");
+ bg.setAttribute("width", total);
+ bg.setAttribute("height", total);
+ bg.setAttribute("fill", "#fff");
+ svg.appendChild(bg);
+
+ for (var r = 0; r < n; r++) {
+ var c = 0;
+ while (c < n) {
+ if (!matrix[r][c]) { c++; continue; }
+ var start = c;
+ while (c < n && matrix[r][c]) { c++; }
+ var rect = document.createElementNS(SVG_NS, "rect");
+ rect.setAttribute("x", start + QUIET);
+ rect.setAttribute("y", r + QUIET);
+ rect.setAttribute("width", c - start);
+ rect.setAttribute("height", 1);
+ rect.setAttribute("fill", "#000");
+ svg.appendChild(rect);
+ }
+ }
+ return svg;
+ }
+
+ // .flyer-flow (the fitFlow-sized element) holds a floated QR the description wraps around.
+ function buildBody(m) {
+ var body = el("div", "flyer-body");
+ body.appendChild(el("h1", "flyer-title", m.title));
+
+ var flow = el("div", "flyer-flow");
+ flow.appendChild(qrSvg(window.fmt.eventUrl(m), QR_BODY)); // max-size; fitQr shrinks to the line grid
+ flow.appendChild(el("p", "flyer-desc", m.description));
+ var ul = el("ul", "flyer-details");
+ window.fmt.detailItems(m).forEach(function (line) {
+ ul.appendChild(el("li", null, line));
+ });
+ flow.appendChild(ul);
+ body.appendChild(flow);
+
+ return body;
+ }
+
+ function buildTab(m) {
+ var inner = el("div", "flyer-tab-inner");
+ var text = el("div", "flyer-tab-text");
+ text.appendChild(el("div", "flyer-tab-title", m.title));
+ text.appendChild(el("div", "flyer-tab-when", window.fmt.startLine(m)));
+ text.appendChild(el("div", "flyer-tab-where", m.venue));
+ inner.appendChild(text);
+ inner.appendChild(qrSvg(window.fmt.eventUrl(m), 16));
+ return inner;
+ }
+
+ function buildTabs(m) {
+ var tabs = el("div", "flyer-tabs");
+ var template = buildTab(m);
+ for (var i = 0; i < TAB_COUNT; i++) {
+ var tab = el("div", "flyer-tab");
+ tab.appendChild(template.cloneNode(true));
+ tabs.appendChild(tab);
+ }
+ return tabs;
+ }
+
+ // Tabs are identical: fit the first's text and copy the font-size to the rest.
+ function fitTabs(tabsEl) {
+ var inners = tabsEl.querySelectorAll(".flyer-tab-inner");
+ if (!inners.length) { return; }
+ var inner = inners[0];
+ var text = inner.querySelector(".flyer-tab-text");
+ // transforms don't affect layout: the rotated tab lays out in its unrotated frame
+ var maxH = inner.clientHeight;
+ var lo = 6, hi = 16;
+ while (lo < hi) {
+ var mid = Math.ceil((lo + hi) / 2);
+ inner.style.fontSize = mid + "pt";
+ if (text.offsetHeight <= maxH) { lo = mid; } else { hi = mid - 1; }
+ }
+ var size = lo + "pt";
+ for (var i = 0; i < inners.length; i++) { inners[i].style.fontSize = size; }
+ }
+
+ // White leading inside a line box, in em: Times metrics (ascent .683, descent .217,
+ // caps .662) with CSS half-leading. Page gaps are equalized as ink, so each margin
+ // is widened by the leading of the line boxes facing it.
+ function leadBelow(lineHeight) { return (lineHeight - 0.9) / 2 + 0.217; }
+ function leadAbove(lineHeight) { return (lineHeight - 0.9) / 2 + 0.683 - 0.662; }
+
+ // Largest QR (<= 42mm) whose float band ends on the flow's line grid (0.2mm short so
+ // rounding can't re-narrow the boundary line), so the first full-width line clears
+ // the float at the same 7mm gutter the text gets. Text size beats QR size.
+ function alignedQrMm(lineHmm) {
+ var n = Math.floor((2 + QR_BODY + 7) / lineHmm);
+ return n * lineHmm - (2 + 7) - 0.2;
+ }
+
+ // Run after fitFlow: shrinking the float only frees space, so the fitted size still fits.
+ function fitQr(flow) {
+ var qr = flow.querySelector(".flyer-qr");
+ var size = alignedQrMm(parseFloat(flow.style.fontSize) * 1.3 * 25.4 / 96); // CSS px -> mm
+ qr.setAttribute("width", size + "mm");
+ qr.setAttribute("height", size + "mm");
+ }
+
+ // Largest quarter-px font-size at which the flow fits maxH. The flow is a stretched
+ // flex child, so collapse the stretch (flex:none, height:auto) to measure content.
+ function fitFlow(flow, maxH) {
+ flow.style.flex = "none";
+ flow.style.height = "auto";
+ flow.style.lineHeight = "1.3";
+ var lo = 32, hi = 256; // quarter-px units
+ while (lo < hi) {
+ var mid = Math.ceil((lo + hi) / 2);
+ flow.style.fontSize = mid / 4 + "px";
+ if (flow.getBoundingClientRect().height <= maxH) { lo = mid; } else { hi = mid - 1; }
+ }
+ flow.style.fontSize = lo / 4 + "px";
+ flow.style.flex = "";
+ flow.style.height = "";
+ }
+
+ // Build the flyer for `m`, size its body text to fit, open the print dialog. Lays out
+ // offscreen at the print width first so measurements reflect true print layout.
+ function build(m) {
+ flyerEl = document.getElementById("flyer");
+ if (!flyerEl) { return; }
+ flyerEl.textContent = "";
+
+ var body = buildBody(m);
+ var tabs = buildTabs(m);
+ flyerEl.appendChild(body);
+ flyerEl.appendChild(tabs);
+
+ flyerEl.classList.add("flyer-measuring");
+ var flow = body.querySelector(".flyer-flow");
+ var title = body.querySelector(".flyer-title");
+ var titleStyle = getComputedStyle(title);
+ // avail height from the rendered title block, not the flex-stretched flow box
+ var titleBlock = title.getBoundingClientRect().height +
+ parseFloat(titleStyle.marginTop) + parseFloat(titleStyle.marginBottom);
+ var flyerH = flyerEl.getBoundingClientRect().height;
+ var tabsH = tabs.getBoundingClientRect().height;
+ var maxH = flyerH - tabsH - titleBlock;
+ fitFlow(flow, Math.max(maxH, 0));
+
+ // Equalize the three page gaps as ink (see leadBelow/leadAbove): comp is the net
+ // height the widened margins cost beyond 7mm; if positive, refit with it reserved.
+ var titlePx = parseFloat(titleStyle.fontSize);
+ function gapComp(f) { return (leadBelow(1.3) + leadAbove(1.3)) * f - leadBelow(1.05) * titlePx; }
+ var fs = parseFloat(flow.style.fontSize);
+ var comp = gapComp(fs);
+ if (comp > 0) {
+ fitFlow(flow, Math.max(maxH - comp, 0));
+ fs = parseFloat(flow.style.fontSize);
+ comp = gapComp(fs);
+ }
+ fitQr(flow);
+
+ // leftover slack split a third per gap; measured after fitQr frees wrapped lines
+ flow.style.flex = "none";
+ flow.style.height = "auto";
+ var s3 = Math.max(maxH - comp - flow.getBoundingClientRect().height, 0) / 3;
+ flow.style.flex = "";
+ flow.style.height = "";
+ var b7 = 7 * 96 / 25.4; // 7mm base in px
+ title.style.marginBottom = (b7 + s3 + leadBelow(1.3) * fs - leadBelow(1.05) * titlePx) + "px";
+ var ul = flow.querySelector(".flyer-details");
+ ul.style.marginTop = (b7 + s3) + "px";
+ ul.style.marginBottom = (b7 + s3 + leadAbove(1.3) * fs) + "px";
+
+ fitTabs(tabs);
+
+ flyerEl.classList.remove("flyer-measuring");
+ }
+
+ // ---- PDF path (Safari) ----
+ // Mirrors the DOM flyer's CSS in flyer-local mm with a top-left origin; px()/py()
+ // are the only crossing into PDF points (origin bottom-left, y-up).
+
+ var MM = 72 / 25.4; // mm -> pt
+ var PAGE_W = 215.9, PAGE_H = 279.4; // US Letter
+ var BOX_W = 197.3, BOX_H = 266.7; // flyer box (shared Letter/A4 safe area)
+ var BOX_X = (PAGE_W - BOX_W) / 2, BOX_Y = 6.35; // centered, 6.35mm top margin
+ var TABS_H = 78, TAB_W = BOX_W / TAB_COUNT;
+ var DASH = { width: 0.75, dash: [2.25, 2.25] }; // 1px dashed at print scale
+ var TITLE_PT = 48, TITLE_LINE_H = TITLE_PT * 1.05 / MM;
+ var QR_BODY = 42; // mm, body QR max; alignedQrMm shrinks it to the line grid
+ var QR_TAB = 16; // mm, tab QR
+ var TAB_PAD_V = 2.5, TAB_PAD_TEXT = 1.5, TAB_PAD_QR = 2.5, TAB_GAP = 2.5;
+ var TAB_TEXT_W = TABS_H - TAB_PAD_TEXT - TAB_PAD_QR - TAB_GAP - QR_TAB;
+ var TAB_CONTENT_H = TAB_W - 2 * TAB_PAD_V; // cross-axis space (clientHeight in fitTabs)
+
+ function px(x) { return (BOX_X + x) * MM; }
+ function py(y) { return (PAGE_H - (BOX_Y + y)) * MM; }
+
+ // mm width of str at sizePt, via the vendored AFM tables
+ function tw(str, font, sizePt) { return window.pdf.widthOf(str, font, sizePt) / MM; }
+
+ // baseline's mm offset from the top of a line box (CSS half-leading, Times metrics)
+ function baselineOff(sizePt, lineHmm) {
+ return ((lineHmm * MM - sizePt * 0.9) / 2 + sizePt * 0.683) / MM;
+ }
+
+ // Word-wrap str into [{ text, yTop }], yTop in mm from y0. availFn(yTop) gives the
+ // usable width at a line's top (how the floated QR narrows lines beside it). Words
+ // wider than a line break mid-word (overflow-wrap: break-word).
+ function wrap(str, font, sizePt, availFn, y0, lineHmm) {
+ var lines = [];
+ var cur = "";
+ var y = y0;
+ function push(s) { lines.push({ text: s, yTop: y }); y += lineHmm; }
+ String(str).split(/\s+/).filter(Boolean).forEach(function (word) {
+ var cand = cur ? cur + " " + word : word;
+ if (tw(cand, font, sizePt) <= availFn(y)) { cur = cand; return; }
+ if (cur) { push(cur); cur = ""; }
+ while (tw(word, font, sizePt) > availFn(y)) {
+ var k = 1; // longest fitting prefix; min 1 char so we always advance
+ while (k < word.length && tw(word.slice(0, k + 1), font, sizePt) <= availFn(y)) { k++; }
+ push(word.slice(0, k));
+ word = word.slice(k);
+ }
+ cur = word;
+ });
+ if (cur) { push(cur); }
+ return lines;
+ }
+
+ // title block: lines at 48pt/1.05 plus the h1's 1.5mm top / 7mm bottom margins
+ function layoutTitle(m) {
+ var lines = wrap(m.title, "times", TITLE_PT, function () { return BOX_W; }, 1.5, TITLE_LINE_H);
+ return { lines: lines, block: 1.5 + lines.length * TITLE_LINE_H + 7 };
+ }
+
+ // Mirrors buildBody/fitFlow in flow-local mm: lines beside the floated QR wrap at the
+ // narrowed width; the flow is at least the float band tall.
+ function layoutFlow(m, pxSize, qrMm, gapTopMm, gapBotMm) {
+ var size = pxSize * 0.75; // CSS px -> pt
+ var lineH = size * 1.3 / MM;
+ var em = size / MM;
+ var band = 2 + qrMm + 7; // float margin box: 2mm top + QR + 7mm bottom
+ var narrow = BOX_W - (qrMm + 7); // line width beside the float (7mm margin-left)
+ var runs = [];
+ var y = 0;
+ function availAt(yy) { return yy < band ? narrow : BOX_W; }
+ var descLines = wrap(m.description, "times", size, availAt, 0, lineH);
+ descLines.forEach(function (line, i) {
+ // justify: pad word gaps to fill the line; last line stays ragged
+ var gaps = line.text.split(" ").length - 1;
+ var ws = (i < descLines.length - 1 && gaps > 0)
+ ? (availAt(line.yTop) - tw(line.text, "times", size)) * MM / gaps : 0;
+ runs.push({ text: line.text, x: 0, yTop: line.yTop, ws: ws });
+ y = line.yTop + lineH;
+ });
+ y += gapTopMm; // ul margin-top
+ var indent = 1.2 * em; // ul padding-left
+ var bulletW = tw("•", "times", size);
+ window.fmt.detailItems(m).forEach(function (item) {
+ y += 0.15 * em; // li margin-top
+ var lines = wrap(item, "times", size, function (yy) { return availAt(yy) - indent; }, y, lineH);
+ runs.push({ text: "•", x: indent - 0.45 * em - bulletW, yTop: lines[0].yTop });
+ lines.forEach(function (line) {
+ runs.push({ text: line.text, x: indent, yTop: line.yTop });
+ y = line.yTop + lineH;
+ });
+ });
+ y += gapBotMm; // ul margin-bottom (counts toward the flow's BFC height)
+ return { height: Math.max(y, band), size: size, lineH: lineH, qr: qrMm, runs: runs };
+ }
+
+ // fitFlow + fitQr's mirror: fit at the max QR, then final layout at the grid-aligned QR
+ function fitPdfFlow(m, maxH) {
+ var lo = 32, hi = 256; // quarter-px units
+ while (lo < hi) {
+ var mid = Math.ceil((lo + hi) / 2);
+ if (layoutFlow(m, mid / 4, QR_BODY, 7, 7).height <= maxH) { lo = mid; } else { hi = mid - 1; }
+ }
+ return layoutFlow(m, lo / 4, alignedQrMm(lo / 4 * 0.75 * 1.3 / MM), 7, 7);
+ }
+
+ // QR as rects (white backing + black per-row runs), top-left at (x, y). emit places one rect.
+ function qrPaint(emit, text, x, y, sizeMm) {
+ var matrix = window.qr.createMatrix(text);
+ var n = matrix.length;
+ var mod = sizeMm / n;
+ emit(x, y, sizeMm, sizeMm, 1);
+ for (var r = 0; r < n; r++) {
+ var c = 0;
+ while (c < n) {
+ if (!matrix[r][c]) { c++; continue; }
+ var start = c;
+ while (c < n && matrix[r][c]) { c++; }
+ emit(x + start * mod, y + r * mod, (c - start) * mod, mod, 0);
+ }
+ }
+ }
+
+ // record a QR's rects once so the 10 tabs replay them
+ function qrRuns(text, sizeMm) {
+ var runs = [];
+ qrPaint(function (x, y, w, h, gray) { runs.push([x, y, w, h, gray]); }, text, 0, 0, sizeMm);
+ return runs;
+ }
+
+ // Tab text column (bold title, when, where) at base size s pt, in inner-local coords:
+ // u along the 78mm axis, v across the 19.73mm one.
+ function layoutTabText(m, s) {
+ var runs = [];
+ var v = 0;
+ function add(str, font, sizePt, lineH, marginTop) {
+ v += marginTop;
+ wrap(str, font, sizePt, function () { return TAB_TEXT_W; }, v, lineH).forEach(function (line) {
+ runs.push({ text: line.text, font: font, size: sizePt, vTop: line.yTop, lineH: lineH });
+ v = line.yTop + lineH;
+ });
+ }
+ add(m.title, "timesBold", 1.3 * s, 1.3 * s * 1.1 / MM, 0);
+ add(window.fmt.startLine(m), "times", s, s * 1.15 / MM, 0.1 * s / MM);
+ add(m.venue, "times", s, s * 1.15 / MM, 0.1 * s / MM);
+ return { runs: runs, height: v };
+ }
+
+ // fitTabs's mirror: largest base pt size whose text stack fits the strip
+ function fitPdfTab(m) {
+ var lo = 6, hi = 16;
+ while (lo < hi) {
+ var mid = Math.ceil((lo + hi) / 2);
+ if (layoutTabText(m, mid).height <= TAB_CONTENT_H) { lo = mid; } else { hi = mid - 1; }
+ }
+ return layoutTabText(m, lo);
+ }
+
+ // One tab's rotated content. The inner box is centered on its cell and rotated 90deg CW,
+ // so local (u, v) maps to flyer-local (cx + TAB_W/2 - v, cy - TABS_H/2 + u): u runs down
+ // the page, QR at the foot. Rects swap w/h; text uses the writer's rotate90.
+ function paintTab(doc, i, tabText, runs) {
+ var cx = i * TAB_W + TAB_W / 2;
+ var cy = BOX_H - TABS_H / 2;
+ function mapX(v) { return cx + TAB_W / 2 - v; }
+ function mapY(u) { return cy - TABS_H / 2 + u; }
+ var vText = TAB_PAD_V + (TAB_CONTENT_H - tabText.height) / 2; // align-items: center
+ tabText.runs.forEach(function (run) {
+ var v = vText + run.vTop + baselineOff(run.size, run.lineH);
+ doc.text(run.text, px(mapX(v)), py(mapY(TAB_PAD_TEXT)),
+ { font: run.font, size: run.size, rotate90: true });
+ });
+ var u0 = TAB_PAD_TEXT + TAB_TEXT_W + TAB_GAP;
+ var v0 = TAB_PAD_V + (TAB_CONTENT_H - QR_TAB) / 2;
+ runs.forEach(function (r) {
+ var u = u0 + r[0], v = v0 + r[1], w = r[2], h = r[3];
+ doc.rect(px(mapX(v + h)), py(mapY(u) + w), h * MM, w * MM, r[4]);
+ });
+ }
+
+ // the tear-from-body line plus the 11 cut lines bounding the 10 tabs
+ function paintCutLines(doc) {
+ var top = BOX_H - TABS_H;
+ doc.dashedLine(px(0), py(top), px(BOX_W), py(top), DASH);
+ for (var i = 0; i <= TAB_COUNT; i++) {
+ doc.dashedLine(px(i * TAB_W), py(top), px(i * TAB_W), py(BOX_H), DASH);
+ }
+ }
+
+ // Draw the whole flyer for `m`; returns the PDF file bytes.
+ function buildPdf(m) {
+ var doc = window.pdf.create(PAGE_W * MM, PAGE_H * MM);
+ var title = layoutTitle(m);
+ title.lines.forEach(function (line) {
+ doc.text(line.text, px(0), py(line.yTop + baselineOff(TITLE_PT, TITLE_LINE_H)),
+ { font: "times", size: TITLE_PT });
+ });
+ var flowTop = title.block;
+ var avail = BOX_H - TABS_H - flowTop;
+ // build()'s mirror: ink-equalized gaps with the fit slack split a third per gap
+ var titleEm = TITLE_PT / MM; // title font size in mm
+ function gapComp(e) { return (leadBelow(1.3) + leadAbove(1.3)) * e - leadBelow(1.05) * titleEm; }
+ var flow = fitPdfFlow(m, avail);
+ var comp = gapComp(flow.size / MM);
+ if (comp > 0) {
+ flow = fitPdfFlow(m, avail - comp);
+ comp = gapComp(flow.size / MM);
+ }
+ var s3 = Math.max(avail - comp - flow.height, 0) / 3;
+ flow = layoutFlow(m, flow.size / 0.75, flow.qr,
+ 7 + s3, 7 + s3 + leadAbove(1.3) * flow.size / MM);
+ flowTop += s3 + leadBelow(1.3) * flow.size / MM - leadBelow(1.05) * titleEm;
+ flow.runs.forEach(function (run) {
+ doc.text(run.text, px(run.x), py(flowTop + run.yTop + baselineOff(flow.size, flow.lineH)),
+ { font: "times", size: flow.size, wordSpacing: run.ws });
+ });
+ var url = window.fmt.eventUrl(m);
+ qrPaint(function (x, y, w, h, gray) {
+ doc.rect(px(x), py(y + h), w * MM, h * MM, gray);
+ }, url, BOX_W - flow.qr, flowTop + 2, flow.qr);
+ paintCutLines(doc);
+ var tabText = fitPdfTab(m);
+ var tabQr = qrRuns(url, QR_TAB);
+ for (var i = 0; i < TAB_COUNT; i++) { paintTab(doc, i, tabText, tabQr); }
+ return doc.end();
+ }
+
+ var pdfUrl = null;
+
+ // Safari can't script-print a blob PDF (WebKit), so open it in a new tab and let the
+ // user print from Safari's viewer. Synchronous so window.open keeps the gesture
+ // allowance. Old blob URL revoked on next build, not after open (tab loads it async).
+ function openPdf(m) {
+ var bytes = buildPdf(m);
+ if (pdfUrl) { URL.revokeObjectURL(pdfUrl); }
+ pdfUrl = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
+ window.open(pdfUrl, "_blank");
+ }
+
+ window.addEventListener("pagehide", function () {
+ if (pdfUrl) { URL.revokeObjectURL(pdfUrl); pdfUrl = null; }
+ });
+
+ // afterprint, not synchronously after print(): some engines render async
+ window.addEventListener("afterprint", function () {
+ if (flyerEl) { flyerEl.textContent = ""; }
+ building = false;
+ });
+
+ function printFlyer(m) {
+ if (!m) { return; }
+ if (isSafari) { openPdf(m); return; }
+ if (building) { return; }
+ building = true;
+ build(m);
+ window.print();
+ }
+
+ // Cmd/Ctrl+P with an event selected: beforeprint can't cancel Safari's dialog, but
+ // keydown can preempt it and route to the PDF.
+ if (isSafari) {
+ window.addEventListener("keydown", function (e) {
+ if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey &&
+ (e.key === "p" || e.key === "P")) {
+ var m = window.panel && window.panel.getSelectedEvent && window.panel.getSelectedEvent();
+ if (!m) { return; } // intro showing -> let Safari print the page
+ e.preventDefault();
+ openPdf(m);
+ }
+ });
+ }
+
+ // beforeprint fires before the dialog, so build here so any browser-initiated print
+ // gets the flyer instead of the live app. (Link path sets `building` and we skip.)
+ window.addEventListener("beforeprint", function () {
+ if (isSafari || building) { return; }
+ var m = window.panel && window.panel.getSelectedEvent && window.panel.getSelectedEvent();
+ if (!m) { return; } // intro showing -> let the browser proceed
+ building = true;
+ build(m);
+ });
+
+ window.flyer = { print: printFlyer, buildPdf: buildPdf, enabled: true };
+})();
diff --git a/resources/format.js b/resources/format.js
@@ -0,0 +1,281 @@
+// Pure date/text formatting helpers + email-obfuscation.
+
+(function () {
+ var WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
+ var MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
+ var MONTH_ABBR = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+
+ function ordinal(n) {
+ var s = ["th", "st", "nd", "rd"], v = n % 100;
+ return n + (s[(v - 20) % 10] || s[v] || s[0]);
+ }
+
+ // Parse "2026-06-07T14:00" as local time (no timezone applied).
+ function parseLocal(iso) {
+ var p = iso.split(/[-T:]/);
+ return new Date(p[0], p[1] - 1, p[2], p[3], p[4]);
+ }
+
+ // "2:00" / "9:30pm" - am/pm only on the end so a range reads "1:00 - 4:00pm".
+ function clockTime(d, withMeridian) {
+ var h = d.getHours(), min = d.getMinutes();
+ var mer = h >= 12 ? "pm" : "am";
+ h = h % 12 || 12;
+ return h + ":" + (min < 10 ? "0" + min : min) + (withMeridian ? mer : "");
+ }
+
+ // "1:00 - 4:00pm" when start/end share a meridian; "11:00am - 1:00pm" when they
+ // differ, so a cross-noon/midnight range isn't ambiguous. forceStart keeps the start
+ // meridian even when shared, so a multiday range doesn't read as a same-day span.
+ function timeRange(start, end, forceStart) {
+ var sameMer = (start.getHours() >= 12) === (end.getHours() >= 12);
+ return clockTime(start, forceStart || !sameMer) + " - " + clockTime(end, true);
+ }
+
+ // "Sunday, June 7th"
+ function dateLabel(d) {
+ return WEEKDAYS[d.getDay()] + ", " + MONTH_NAMES[d.getMonth()] + " " + ordinal(d.getDate());
+ }
+
+ function sameDay(a, b) {
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
+ }
+
+ // " (Today!)" / " (Tomorrow)" / "" against `now` at call time (no live refresh).
+ // A passed event takes ", <year>" instead, so the year is clear to viewers in a
+ // later year landing on the same day-of-month; the "(Ended)" marker is appended
+ // separately by the caller so it can sit at the end of a multiday range.
+ function relativeDayTag(m, d, now) {
+ now = now || new Date();
+ if (hasPassed(m, now)) { return ", " + d.getFullYear(); }
+ if (sameDay(d, now)) { return " (Today!)"; }
+ var tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
+ if (sameDay(d, tomorrow)) { return " (Tomorrow)"; }
+ return "";
+ }
+
+ // An event has "passed" once its end time is in the past.
+ function hasPassed(m, now) {
+ return parseLocal(m.end) < (now || new Date());
+ }
+
+ // Upcoming events (end not yet past), soonest start first. Caller snapshots `now`.
+ function upcomingEvents(events, now) {
+ now = now || new Date();
+ return events.filter(function (m) { return !hasPassed(m, now); })
+ .sort(function (a, b) { return parseLocal(a.start) - parseLocal(b.start); });
+ }
+
+ // Up to `limit` passed events, most-recently-ended first. Caller snapshots `now`.
+ function pastEvents(events, now, limit) {
+ now = now || new Date();
+ return events.filter(function (m) { return hasPassed(m, now); })
+ .sort(function (a, b) { return parseLocal(b.end) - parseLocal(a.end); })
+ .slice(0, limit == null ? 10 : limit);
+ }
+
+ // "June 7th" - short date for the intro table's date column. withYear appends ", 2025"
+ // so past events read unambiguously in a later year.
+ function shortDate(m, withYear) {
+ var d = parseLocal(m.start);
+ return MONTH_NAMES[d.getMonth()] + " " + ordinal(d.getDate()) + (withYear ? ", " + d.getFullYear() : "");
+ }
+
+ // "Sunday, June 7th from 1:00 - 4:00pm"
+ function dateLine(m) {
+ var start = parseLocal(m.start), end = parseLocal(m.end);
+ return dateLabel(start) + " from " + timeRange(start, end);
+ }
+
+ // "Sunday, June 7th at 1:00pm" - start only (flyer tear-tabs).
+ function startLine(m) {
+ var start = parseLocal(m.start);
+ return dateLabel(start) + " at " + clockTime(start, true);
+ }
+
+ // Detail bullets, one per <li>. Same-day events get separate date + time bullets.
+ // Upcoming multiday events collapse into one "date time - date time" bullet so each
+ // endpoint carries its day; once past, they split into a date-range line + time-range
+ // line ("(Ended)" trailing), letting each time pair with its date. The year shows on
+ // the end date only, or on both dates when the range spans two years.
+ // Blank fields are dropped. address is intentionally not shown (kept in data for
+ // later). withRelativeDay appends "(Today!)" / "(Ended)" - panel only.
+ function detailItems(m, withRelativeDay) {
+ var start = parseLocal(m.start), end = parseLocal(m.end);
+ var single = sameDay(start, end);
+ var ended = withRelativeDay && hasPassed(m) ? " (Ended)" : "";
+ var startLabel = dateLabel(start) + (withRelativeDay ? relativeDayTag(m, start) : "");
+ var when;
+ if (single) {
+ when = [startLabel + ended, timeRange(start, end)];
+ } else if (ended) {
+ var sameYear = start.getFullYear() === end.getFullYear();
+ var startYear = sameYear ? "" : ", " + start.getFullYear();
+ when = [dateLabel(start) + startYear + " - " + dateLabel(end) + ", " + end.getFullYear() + ended, timeRange(start, end, true)];
+ } else {
+ when = [startLabel + " " + clockTime(start, true) + " - " + dateLabel(end) + " " + clockTime(end, true)];
+ }
+ return when.concat([m.venue, m.price, m.ageRange]).filter(function (s) {
+ return s && s.trim();
+ });
+ }
+
+ function easeInOut(t) {
+ return t < 0.5 ? 0.5 * Math.sqrt(2 * t) : 1 - 0.5 * Math.sqrt(2 * (1 - t));
+ }
+
+ // ROT13 to keep RSVP addresses out of page source; each event's `rsvp` is stored rotated.
+ function rot(str) {
+ var input = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+ var output = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm";
+ return str.split("").map(function (x) {
+ var i = input.indexOf(x);
+ return i > -1 ? output[i] : x;
+ }).join("");
+ }
+
+ // RSVP mailto: decode the rotated address, prepopulate subject + body. The event URL
+ // goes on its own trailing line so mail clients auto-link it.
+ function mailtoHref(m) {
+ var url = eventUrl(m);
+ var subject = "RSVP for " + m.title + " 🐞";
+ var body = "I'm confirming my RSVP for the following event:\n\n" +
+ m.title + "\n" + startLine(m) + "\n\n" +
+ url + "\n\n· · · · ·\n\nAny notes or comments? Add them here:\n\n\n";
+ return "mailto:" + rot(m.rsvp) +
+ "?subject=" + encodeURIComponent(subject) +
+ "&body=" + encodeURIComponent(body);
+ }
+
+ function pad2(n) { return n < 10 ? "0" + n : "" + n; }
+
+ // "20260610T183000" - floating local time (no zone). JSON times are venue wall-clock,
+ // so calendars show them in the viewer's local time.
+ function icsLocal(d) {
+ return d.getFullYear() + pad2(d.getMonth() + 1) + pad2(d.getDate()) +
+ "T" + pad2(d.getHours()) + pad2(d.getMinutes()) + "00";
+ }
+
+ // "20260610T183000Z" - UTC stamp for DTSTAMP/UID.
+ function icsStamp(d) {
+ return d.getUTCFullYear() + pad2(d.getUTCMonth() + 1) + pad2(d.getUTCDate()) +
+ "T" + pad2(d.getUTCHours()) + pad2(d.getUTCMinutes()) + pad2(d.getUTCSeconds()) + "Z";
+ }
+
+ // Escape a TEXT value per RFC 5545.
+ function icsEscape(str) {
+ return String(str)
+ .replace(/\\/g, "\\\\")
+ .replace(/;/g, "\\;")
+ .replace(/,/g, "\\,")
+ .replace(/\r?\n/g, "\\n");
+ }
+
+ // Fold a content line to <=75 octets per RFC 5545. Counts UTF-8 bytes (not chars)
+ // and never splits a multibyte sequence across the fold.
+ function icsFold(line) {
+ var out = "", run = 0, limit = 75;
+ for (var i = 0; i < line.length; i++) {
+ var ch = line[i];
+ var bytes = encodeURIComponent(ch).replace(/%[0-9A-F]{2}/gi, "x").length;
+ if (run + bytes > limit) { out += "\r\n "; run = 1; } // leading space counts as 1
+ out += ch;
+ run += bytes;
+ }
+ return out;
+ }
+
+ // Single-event iCalendar (.ics). Floating local time; venue+address as LOCATION.
+ function icsContent(m) {
+ var start = parseLocal(m.start), end = parseLocal(m.end);
+ var location = m.address ? m.venue + ", " + m.address : m.venue;
+ var uid = icsLocal(start) + "-" + Math.abs(hashStr(m.title)) + "@events";
+ var lines = [
+ "BEGIN:VCALENDAR",
+ "VERSION:2.0",
+ "PRODID:-//events//EN",
+ "CALSCALE:GREGORIAN",
+ "BEGIN:VEVENT",
+ "UID:" + uid,
+ "DTSTAMP:" + icsStamp(new Date()),
+ "DTSTART:" + icsLocal(start),
+ "DTEND:" + icsLocal(end),
+ "SUMMARY:" + icsEscape(m.title),
+ "LOCATION:" + icsEscape(location),
+ "URL:" + icsEscape(eventUrl(m)),
+ "DESCRIPTION:" + icsEscape(m.description),
+ "END:VEVENT",
+ "END:VCALENDAR"
+ ];
+ return lines.map(icsFold).join("\r\n") + "\r\n";
+ }
+
+ // Stable hash so the same event yields the same UID across downloads (calendars
+ // dedupe/update on UID).
+ function hashStr(str) {
+ var h = 0;
+ for (var i = 0; i < str.length; i++) { h = (h * 31 + str.charCodeAt(i)) | 0; }
+ return h;
+ }
+
+ // Deep-link id: title slug + date, so two same-named events on different days don't
+ // collide, e.g. "front-porch-jam-session-2026-06-12".
+ function eventSlug(m) {
+ var title = m.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "event";
+ return title + "-" + m.start.slice(0, 10);
+ }
+
+ // Deep link to an event: current page sans query/hash + ?<slug>.
+ function eventUrl(m) {
+ var base = window.location.href.split(/[?#]/)[0];
+ return base + "?" + encodeURIComponent(eventSlug(m));
+ }
+
+ function isApplePlatform() {
+ return /Macintosh|iPhone|iPad|iPod/.test(navigator.userAgent);
+ }
+
+ // Driving-directions deep link (destination only, so the app fills in the origin).
+ // Prefer the street address; fall back to lat,long when blank.
+ // maps:// opens Apple Maps directly; Google Maps everywhere else.
+ function directionsHref(m) {
+ var dest = encodeURIComponent(m.address || (m.latitude + "," + m.longitude));
+ return isApplePlatform()
+ ? "maps://?daddr=" + dest + "&dirflg=d"
+ : "https://www.google.com/maps/dir/?api=1&destination=" + dest + "&travelmode=driving";
+ }
+
+ // Resolve the current URL's ?<slug> back to its event, or null. Inverse of eventUrl.
+ function eventFromUrl(events) {
+ var slug = decodeURIComponent(window.location.search.slice(1));
+ if (!slug) { return null; }
+ for (var i = 0; i < events.length; i++) {
+ if (eventSlug(events[i]) === slug) { return events[i]; }
+ }
+ return null;
+ }
+
+ function icsFilename(m) {
+ return eventSlug(m) + ".ics";
+ }
+
+ window.fmt = {
+ MONTH_ABBR: MONTH_ABBR,
+ parseLocal: parseLocal,
+ hasPassed: hasPassed,
+ upcomingEvents: upcomingEvents,
+ pastEvents: pastEvents,
+ shortDate: shortDate,
+ dateLine: dateLine,
+ startLine: startLine,
+ detailItems: detailItems,
+ easeInOut: easeInOut,
+ mailtoHref: mailtoHref,
+ icsContent: icsContent,
+ icsFilename: icsFilename,
+ eventSlug: eventSlug,
+ eventUrl: eventUrl,
+ directionsHref: directionsHref,
+ eventFromUrl: eventFromUrl
+ };
+})();
diff --git a/resources/icon.png b/resources/icon.png
Binary files differ.
diff --git a/resources/leaflet/LICENSE-leaflet b/resources/leaflet/LICENSE-leaflet
@@ -0,0 +1,26 @@
+BSD 2-Clause License
+
+Copyright (c) 2010-2023, Volodymyr Agafonkin
+Copyright (c) 2010-2011, CloudMade
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/resources/leaflet/LICENSE-nogap b/resources/leaflet/LICENSE-nogap
@@ -0,0 +1,4 @@
+"THE BEER-WARE LICENSE":
+<ivan@sanchezortega.es> wrote this file. As long as you retain this notice you
+can do whatever you want with this stuff. If we meet some day, and you think
+this stuff is worth it, you can buy me a beer in return.
diff --git a/resources/leaflet/leaflet-nogap.js b/resources/leaflet/leaflet-nogap.js
@@ -0,0 +1,274 @@
+// @class TileLayer
+
+L.TileLayer.mergeOptions({
+ // @option keepBuffer
+ // The amount of tiles outside the visible map area to be kept in the stitched
+ // `TileLayer`.
+
+ // @option dumpToCanvas: Boolean = true
+ // Whether to dump loaded tiles to a `<canvas>` to prevent some rendering
+ // artifacts. (Disabled by default in IE)
+ dumpToCanvas: L.Browser.canvas && !L.Browser.ie,
+});
+
+L.TileLayer.include({
+ // Backing-store density. The canvas is sized in CSS px (so Leaflet's positioning is
+ // unchanged) but its backing store is ratio× larger and the context is pre-scaled by
+ // ratio, so high-res source tiles keep their detail instead of being flattened to 1x.
+ // Full DPR for sharpness; the crash from large canvases is bounded by a small keepBuffer
+ // (see the tileLayer options in script.js) rather than by capping ratio.
+ _canvasRatio: function() {
+ return Math.max(1, Math.round(window.devicePixelRatio || 1));
+ },
+
+ _onUpdateLevel: function(z) {
+ if (this.options.dumpToCanvas) {
+ // base Leaflet calls this with only z; mirror its own zIndex math off _tileZoom
+ // (the original plugin read an undefined `zoom` arg here, yielding NaN). Correct
+ // z-index stacking lets a new level's canvas sit over the old until it's pruned,
+ // so no opacity tricks are needed to avoid a pop on zoom.
+ var curZoom = this._tileZoom;
+ this._levels[z].canvas.style.zIndex =
+ this.options.maxZoom - Math.abs(curZoom - z);
+ }
+ },
+
+ _onRemoveLevel: function(z) {
+ if (this.options.dumpToCanvas) {
+ L.DomUtil.remove(this._levels[z].canvas);
+ }
+ },
+
+ _onCreateLevel: function(level) {
+ if (this.options.dumpToCanvas) {
+ level.canvas = L.DomUtil.create(
+ "canvas",
+ "leaflet-tile-container leaflet-zoom-animated",
+ this._container
+ );
+ level.canvas.style.pointerEvents = "none";
+ level.ctx = level.canvas.getContext("2d");
+ this._resetCanvasSize(level);
+ }
+ },
+
+ _removeTile: function(key) {
+ if (this.options.dumpToCanvas) {
+ var tile = this._tiles[key];
+ var level = this._levels[tile.coords.z];
+ var tileSize = this.getTileSize();
+
+ if (level) {
+ // Where in the canvas should this tile go?
+ var offset = L.point(tile.coords.x, tile.coords.y)
+ .subtract(level.canvasRange.min)
+ .scaleBy(this.getTileSize());
+
+ level.ctx.clearRect(offset.x, offset.y, tileSize.x, tileSize.y);
+ }
+ }
+
+ L.GridLayer.prototype._removeTile.call(this, key);
+ },
+
+ _resetCanvasSize: function(level) {
+ var buff = this.options.keepBuffer,
+ pixelBounds = this._getTiledPixelBounds(this._map.getCenter()),
+ tileRange = this._pxBoundsToTileRange(pixelBounds),
+ tileSize = this.getTileSize();
+
+ tileRange.min = tileRange.min.subtract([buff, buff]); // This adds the no-prune buffer
+ tileRange.max = tileRange.max.add([buff + 1, buff + 1]);
+
+ var pixelRange = L.bounds(
+ tileRange.min.scaleBy(tileSize),
+ tileRange.max.add([1, 1]).scaleBy(tileSize) // This prevents an off-by-one when checking if tiles are inside
+ ),
+ mustRepositionCanvas = false,
+ neededSize = pixelRange.max.subtract(pixelRange.min);
+
+ // Resize the canvas, if needed, and only to make it bigger. CSS size stays in CSS px;
+ // backing store is ratio× larger so high-DPR tiles keep their detail (see _canvasRatio).
+ var ratio = this._canvasRatio();
+ if (
+ neededSize.x > level.canvas.width / ratio ||
+ neededSize.y > level.canvas.height / ratio
+ ) {
+ // Resizing canvases erases the currently drawn content, I'm afraid.
+ // To keep it, dump the pixels to another canvas, then display it on
+ // top. This could be done with getImageData/putImageData, but that
+ // would break for tainted canvases (in non-CORS tilesets). Copy at
+ // backing-store resolution (1:1, no ctx scale) to avoid resampling.
+ var oldSize = { x: level.canvas.width, y: level.canvas.height };
+ // console.info('Resizing canvas from ', oldSize, 'to ', neededSize);
+
+ var tmpCanvas = L.DomUtil.create("canvas");
+ tmpCanvas.width = oldSize.x;
+ tmpCanvas.height = oldSize.y;
+ tmpCanvas.getContext("2d").drawImage(level.canvas, 0, 0);
+ // var data = level.ctx.getImageData(0, 0, oldSize.x, oldSize.y);
+
+ level.canvas.style.width = neededSize.x + "px";
+ level.canvas.style.height = neededSize.y + "px";
+ level.canvas.width = neededSize.x * ratio;
+ level.canvas.height = neededSize.y * ratio;
+ // drawImage at native backing-store coords, so reset any prior ctx scale first
+ level.ctx.setTransform(1, 0, 0, 1, 0, 0);
+ level.ctx.drawImage(tmpCanvas, 0, 0);
+ // every other draw in this layer works in CSS px; pre-scale so it lands on the
+ // ratio× backing store
+ level.ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
+ // level.ctx.putImageData(data, 0, 0, 0, 0, oldSize.x, oldSize.y);
+ }
+
+ // Translate the canvas contents if it's moved around. This is a whole-canvas
+ // backing-store copy, so run it at native scale (offset in backing-store px) and
+ // restore the CSS-px ctx scale afterward.
+ if (level.canvasRange) {
+ var offset = level.canvasRange.min
+ .subtract(tileRange.min)
+ .scaleBy(this.getTileSize())
+ .multiplyBy(ratio);
+
+ // console.info('Offsetting by ', offset);
+
+ level.ctx.setTransform(1, 0, 0, 1, 0, 0);
+
+ if (!L.Browser.safari) {
+ // By default, canvases copy things "on top of" existing pixels, but we want
+ // this to *replace* the existing pixels when doing a drawImage() call.
+ // This will also clear the sides, so no clearRect() calls are needed to make room
+ // for the new tiles.
+ level.ctx.globalCompositeOperation = "copy";
+ level.ctx.drawImage(level.canvas, offset.x, offset.y);
+ level.ctx.globalCompositeOperation = "source-over";
+ } else {
+ // Safari clears the canvas when copying from itself :-(
+ if (!this._tmpCanvas) {
+ var t = (this._tmpCanvas = L.DomUtil.create("canvas"));
+ t.width = level.canvas.width;
+ t.height = level.canvas.height;
+ this._tmpContext = t.getContext("2d");
+ }
+ this._tmpContext.clearRect(
+ 0,
+ 0,
+ level.canvas.width,
+ level.canvas.height
+ );
+ this._tmpContext.drawImage(level.canvas, 0, 0);
+ level.ctx.clearRect(0, 0, level.canvas.width, level.canvas.height);
+ level.ctx.drawImage(this._tmpCanvas, offset.x, offset.y);
+ }
+
+ level.ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
+ mustRepositionCanvas = true; // Wait until new props are set
+ }
+
+ level.canvasRange = tileRange;
+ level.canvasPxRange = pixelRange;
+ level.canvasOrigin = pixelRange.min;
+
+ // console.log('Canvas tile range: ', level, tileRange.min, tileRange.max );
+ // console.log('Canvas pixel range: ', pixelRange.min, pixelRange.max );
+ // console.log('Level origin: ', level.origin );
+
+ if (mustRepositionCanvas) {
+ this._setCanvasZoomTransform(
+ level,
+ this._map.getCenter(),
+ this._map.getZoom()
+ );
+ }
+ },
+
+ /// set transform/position of canvas, in addition to the transform/position of the individual tile container
+ _setZoomTransform: function(level, center, zoom) {
+ L.GridLayer.prototype._setZoomTransform.call(this, level, center, zoom);
+ if (this.options.dumpToCanvas) {
+ this._setCanvasZoomTransform(level, center, zoom);
+ }
+ },
+
+ // This will get called twice:
+ // * From _setZoomTransform
+ // * When the canvas has shifted due to a new tile being loaded
+ _setCanvasZoomTransform: function(level, center, zoom) {
+ // console.log('_setCanvasZoomTransform', level, center, zoom);
+ if (!level.canvasOrigin) {
+ return;
+ }
+ var scale = this._map.getZoomScale(zoom, level.zoom),
+ translate = level.canvasOrigin
+ .multiplyBy(scale)
+ .subtract(this._map._getNewPixelOrigin(center, zoom))
+ .round();
+
+ if (L.Browser.any3d) {
+ L.DomUtil.setTransform(level.canvas, translate, scale);
+ } else {
+ L.DomUtil.setPosition(level.canvas, translate);
+ }
+ },
+
+ _onOpaqueTile: function(tile) {
+ if (!this.options.dumpToCanvas) {
+ return;
+ }
+
+ // Guard against an NS_ERROR_NOT_AVAILABLE (or similar) exception
+ // when a non-image-tile has been loaded (e.g. a WMS error).
+ // Checking for tile.el.complete is not enough, as it has been
+ // already marked as loaded and ready somehow.
+ try {
+ this.dumpPixels(tile.coords, tile.el);
+ } catch (ex) {
+ return this.fire("tileerror", {
+ error: "Could not copy tile pixels: " + ex,
+ tile: tile,
+ coods: tile.coords,
+ });
+ }
+
+ // If dumping the pixels was successful, then hide the tile.
+ // Do not remove the tile itself, as it is needed to check if the whole
+ // level (and its canvas) should be removed (via level.el.children.length)
+ tile.el.style.display = "none";
+ },
+
+ // @section Extension methods
+ // @uninheritable
+
+ // @method dumpPixels(coords: Object, imageSource: CanvasImageSource): this
+ // Dumps pixels from the given `CanvasImageSource` into the layer, into
+ // the space for the tile represented by the `coords` tile coordinates (an object
+ // like `{x: Number, y: Number, z: Number}`; the image source must have the
+ // same size as the `tileSize` option for the layer. Has no effect if `dumpToCanvas`
+ // is `false`.
+ dumpPixels: function(coords, imageSource) {
+ var level = this._levels[coords.z],
+ tileSize = this.getTileSize();
+
+ if (!level.canvasRange || !this.options.dumpToCanvas) {
+ return;
+ }
+
+ // Check if the tile is inside the currently visible map bounds
+ // There is a possible race condition when tiles are loaded after they
+ // have been panned outside of the map.
+ if (!level.canvasRange.contains(coords)) {
+ this._resetCanvasSize(level);
+ }
+
+ // Where in the canvas should this tile go?
+ var offset = L.point(coords.x, coords.y)
+ .subtract(level.canvasRange.min)
+ .scaleBy(this.getTileSize());
+
+ level.ctx.drawImage(imageSource, offset.x, offset.y, tileSize.x, tileSize.y);
+
+ // TODO: Clear the pixels of other levels' canvases where they overlap
+ // this newly dumped tile.
+ return this;
+ },
+});
diff --git a/resources/leaflet/leaflet.css b/resources/leaflet/leaflet.css
@@ -0,0 +1,661 @@
+/* required styles */
+
+.leaflet-pane,
+.leaflet-tile,
+.leaflet-marker-icon,
+.leaflet-marker-shadow,
+.leaflet-tile-container,
+.leaflet-pane > svg,
+.leaflet-pane > canvas,
+.leaflet-zoom-box,
+.leaflet-image-layer,
+.leaflet-layer {
+ position: absolute;
+ left: 0;
+ top: 0;
+ }
+.leaflet-container {
+ overflow: hidden;
+ }
+.leaflet-tile,
+.leaflet-marker-icon,
+.leaflet-marker-shadow {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ -webkit-user-drag: none;
+ }
+/* Prevents IE11 from highlighting tiles in blue */
+.leaflet-tile::selection {
+ background: transparent;
+}
+/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
+.leaflet-safari .leaflet-tile {
+ image-rendering: -webkit-optimize-contrast;
+ }
+/* hack that prevents hw layers "stretching" when loading new tiles */
+.leaflet-safari .leaflet-tile-container {
+ width: 1600px;
+ height: 1600px;
+ -webkit-transform-origin: 0 0;
+ }
+.leaflet-marker-icon,
+.leaflet-marker-shadow {
+ display: block;
+ }
+/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
+/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
+.leaflet-container .leaflet-overlay-pane svg {
+ max-width: none !important;
+ max-height: none !important;
+ }
+.leaflet-container .leaflet-marker-pane img,
+.leaflet-container .leaflet-shadow-pane img,
+.leaflet-container .leaflet-tile-pane img,
+.leaflet-container img.leaflet-image-layer,
+.leaflet-container .leaflet-tile {
+ max-width: none !important;
+ max-height: none !important;
+ width: auto;
+ padding: 0;
+ }
+
+.leaflet-container img.leaflet-tile {
+ /* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
+ mix-blend-mode: plus-lighter;
+}
+
+.leaflet-container.leaflet-touch-zoom {
+ -ms-touch-action: pan-x pan-y;
+ touch-action: pan-x pan-y;
+ }
+.leaflet-container.leaflet-touch-drag {
+ -ms-touch-action: pinch-zoom;
+ /* Fallback for FF which doesn't support pinch-zoom */
+ touch-action: none;
+ touch-action: pinch-zoom;
+}
+.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
+ -ms-touch-action: none;
+ touch-action: none;
+}
+.leaflet-container {
+ -webkit-tap-highlight-color: transparent;
+}
+.leaflet-container a {
+ -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
+}
+.leaflet-tile {
+ filter: inherit;
+ visibility: hidden;
+ }
+.leaflet-tile-loaded {
+ visibility: inherit;
+ }
+.leaflet-zoom-box {
+ width: 0;
+ height: 0;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ z-index: 800;
+ }
+/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
+.leaflet-overlay-pane svg {
+ -moz-user-select: none;
+ }
+
+.leaflet-pane { z-index: 400; }
+
+.leaflet-tile-pane { z-index: 200; }
+.leaflet-overlay-pane { z-index: 400; }
+.leaflet-shadow-pane { z-index: 500; }
+.leaflet-marker-pane { z-index: 600; }
+.leaflet-tooltip-pane { z-index: 650; }
+.leaflet-popup-pane { z-index: 700; }
+
+.leaflet-map-pane canvas { z-index: 100; }
+.leaflet-map-pane svg { z-index: 200; }
+
+.leaflet-vml-shape {
+ width: 1px;
+ height: 1px;
+ }
+.lvml {
+ behavior: url(#default#VML);
+ display: inline-block;
+ position: absolute;
+ }
+
+
+/* control positioning */
+
+.leaflet-control {
+ position: relative;
+ z-index: 800;
+ pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
+ pointer-events: auto;
+ }
+.leaflet-top,
+.leaflet-bottom {
+ position: absolute;
+ z-index: 1000;
+ pointer-events: none;
+ }
+.leaflet-top {
+ top: 0;
+ }
+.leaflet-right {
+ right: 0;
+ }
+.leaflet-bottom {
+ bottom: 0;
+ }
+.leaflet-left {
+ left: 0;
+ }
+.leaflet-control {
+ float: left;
+ clear: both;
+ }
+.leaflet-right .leaflet-control {
+ float: right;
+ }
+.leaflet-top .leaflet-control {
+ margin-top: 10px;
+ }
+.leaflet-bottom .leaflet-control {
+ margin-bottom: 10px;
+ }
+.leaflet-left .leaflet-control {
+ margin-left: 10px;
+ }
+.leaflet-right .leaflet-control {
+ margin-right: 10px;
+ }
+
+
+/* zoom and fade animations */
+
+.leaflet-fade-anim .leaflet-popup {
+ opacity: 0;
+ -webkit-transition: opacity 0.2s linear;
+ -moz-transition: opacity 0.2s linear;
+ transition: opacity 0.2s linear;
+ }
+.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
+ opacity: 1;
+ }
+.leaflet-zoom-animated {
+ -webkit-transform-origin: 0 0;
+ -ms-transform-origin: 0 0;
+ transform-origin: 0 0;
+ }
+svg.leaflet-zoom-animated {
+ will-change: transform;
+}
+
+.leaflet-zoom-anim .leaflet-zoom-animated {
+ -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
+ -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
+ transition: transform 0.25s cubic-bezier(0,0,0.25,1);
+ }
+.leaflet-zoom-anim .leaflet-tile,
+.leaflet-pan-anim .leaflet-tile {
+ -webkit-transition: none;
+ -moz-transition: none;
+ transition: none;
+ }
+
+.leaflet-zoom-anim .leaflet-zoom-hide {
+ visibility: hidden;
+ }
+
+
+/* cursors */
+
+.leaflet-interactive {
+ cursor: pointer;
+ }
+.leaflet-grab {
+ cursor: -webkit-grab;
+ cursor: -moz-grab;
+ cursor: grab;
+ }
+.leaflet-crosshair,
+.leaflet-crosshair .leaflet-interactive {
+ cursor: crosshair;
+ }
+.leaflet-popup-pane,
+.leaflet-control {
+ cursor: auto;
+ }
+.leaflet-dragging .leaflet-grab,
+.leaflet-dragging .leaflet-grab .leaflet-interactive,
+.leaflet-dragging .leaflet-marker-draggable {
+ cursor: move;
+ cursor: -webkit-grabbing;
+ cursor: -moz-grabbing;
+ cursor: grabbing;
+ }
+
+/* marker & overlays interactivity */
+.leaflet-marker-icon,
+.leaflet-marker-shadow,
+.leaflet-image-layer,
+.leaflet-pane > svg path,
+.leaflet-tile-container {
+ pointer-events: none;
+ }
+
+.leaflet-marker-icon.leaflet-interactive,
+.leaflet-image-layer.leaflet-interactive,
+.leaflet-pane > svg path.leaflet-interactive,
+svg.leaflet-image-layer.leaflet-interactive path {
+ pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
+ pointer-events: auto;
+ }
+
+/* visual tweaks */
+
+.leaflet-container {
+ background: #ddd;
+ outline-offset: 1px;
+ }
+.leaflet-container a {
+ color: #0078A8;
+ }
+.leaflet-zoom-box {
+ border: 2px dotted #38f;
+ background: rgba(255,255,255,0.5);
+ }
+
+
+/* general typography */
+.leaflet-container {
+ font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
+ font-size: 12px;
+ font-size: 0.75rem;
+ line-height: 1.5;
+ }
+
+
+/* general toolbar styles */
+
+.leaflet-bar {
+ box-shadow: 0 1px 5px rgba(0,0,0,0.65);
+ border-radius: 4px;
+ }
+.leaflet-bar a {
+ background-color: #fff;
+ border-bottom: 1px solid #ccc;
+ width: 26px;
+ height: 26px;
+ line-height: 26px;
+ display: block;
+ text-align: center;
+ text-decoration: none;
+ color: black;
+ }
+.leaflet-bar a,
+.leaflet-control-layers-toggle {
+ background-position: 50% 50%;
+ background-repeat: no-repeat;
+ display: block;
+ }
+.leaflet-bar a:hover,
+.leaflet-bar a:focus {
+ background-color: #f4f4f4;
+ }
+.leaflet-bar a:first-child {
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ }
+.leaflet-bar a:last-child {
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border-bottom: none;
+ }
+.leaflet-bar a.leaflet-disabled {
+ cursor: default;
+ background-color: #f4f4f4;
+ color: #bbb;
+ }
+
+.leaflet-touch .leaflet-bar a {
+ width: 30px;
+ height: 30px;
+ line-height: 30px;
+ }
+.leaflet-touch .leaflet-bar a:first-child {
+ border-top-left-radius: 2px;
+ border-top-right-radius: 2px;
+ }
+.leaflet-touch .leaflet-bar a:last-child {
+ border-bottom-left-radius: 2px;
+ border-bottom-right-radius: 2px;
+ }
+
+/* zoom control */
+
+.leaflet-control-zoom-in,
+.leaflet-control-zoom-out {
+ font: bold 18px 'Lucida Console', Monaco, monospace;
+ text-indent: 1px;
+ }
+
+.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
+ font-size: 22px;
+ }
+
+
+/* layers control */
+
+.leaflet-control-layers {
+ box-shadow: 0 1px 5px rgba(0,0,0,0.4);
+ background: #fff;
+ border-radius: 5px;
+ }
+.leaflet-control-layers-toggle {
+ /* background-image (images/layers.png) dropped: no layers control, images vendored out */
+ width: 36px;
+ height: 36px;
+ }
+.leaflet-retina .leaflet-control-layers-toggle {
+ /* background-image (images/layers-2x.png) dropped: see above */
+ background-size: 26px 26px;
+ }
+.leaflet-touch .leaflet-control-layers-toggle {
+ width: 44px;
+ height: 44px;
+ }
+.leaflet-control-layers .leaflet-control-layers-list,
+.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
+ display: none;
+ }
+.leaflet-control-layers-expanded .leaflet-control-layers-list {
+ display: block;
+ position: relative;
+ }
+.leaflet-control-layers-expanded {
+ padding: 6px 10px 6px 6px;
+ color: #333;
+ background: #fff;
+ }
+.leaflet-control-layers-scrollbar {
+ overflow-y: scroll;
+ overflow-x: hidden;
+ padding-right: 5px;
+ }
+.leaflet-control-layers-selector {
+ margin-top: 2px;
+ position: relative;
+ top: 1px;
+ }
+.leaflet-control-layers label {
+ display: block;
+ font-size: 13px;
+ font-size: 1.08333em;
+ }
+.leaflet-control-layers-separator {
+ height: 0;
+ border-top: 1px solid #ddd;
+ margin: 5px -10px 5px -6px;
+ }
+
+/* Default icon URLs */
+.leaflet-default-icon-path {
+ /* background-image (images/marker-icon.png) dropped: app uses L.divIcon only, images vendored out */
+ }
+
+
+/* attribution and scale controls */
+
+.leaflet-container .leaflet-control-attribution {
+ background: #fff;
+ background: rgba(255, 255, 255, 0.8);
+ margin: 0;
+ }
+.leaflet-control-attribution,
+.leaflet-control-scale-line {
+ padding: 0 5px;
+ color: #333;
+ line-height: 1.4;
+ }
+.leaflet-control-attribution a {
+ text-decoration: none;
+ }
+.leaflet-control-attribution a:hover,
+.leaflet-control-attribution a:focus {
+ text-decoration: underline;
+ }
+.leaflet-attribution-flag {
+ display: inline !important;
+ vertical-align: baseline !important;
+ width: 1em;
+ height: 0.6669em;
+ }
+.leaflet-left .leaflet-control-scale {
+ margin-left: 5px;
+ }
+.leaflet-bottom .leaflet-control-scale {
+ margin-bottom: 5px;
+ }
+.leaflet-control-scale-line {
+ border: 2px solid #777;
+ border-top: none;
+ line-height: 1.1;
+ padding: 2px 5px 1px;
+ white-space: nowrap;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ background: rgba(255, 255, 255, 0.8);
+ text-shadow: 1px 1px #fff;
+ }
+.leaflet-control-scale-line:not(:first-child) {
+ border-top: 2px solid #777;
+ border-bottom: none;
+ margin-top: -2px;
+ }
+.leaflet-control-scale-line:not(:first-child):not(:last-child) {
+ border-bottom: 2px solid #777;
+ }
+
+.leaflet-touch .leaflet-control-attribution,
+.leaflet-touch .leaflet-control-layers,
+.leaflet-touch .leaflet-bar {
+ box-shadow: none;
+ }
+.leaflet-touch .leaflet-control-layers,
+.leaflet-touch .leaflet-bar {
+ border: 2px solid rgba(0,0,0,0.2);
+ background-clip: padding-box;
+ }
+
+
+/* popup */
+
+.leaflet-popup {
+ position: absolute;
+ text-align: center;
+ margin-bottom: 20px;
+ }
+.leaflet-popup-content-wrapper {
+ padding: 1px;
+ text-align: left;
+ border-radius: 12px;
+ }
+.leaflet-popup-content {
+ margin: 13px 24px 13px 20px;
+ line-height: 1.3;
+ font-size: 13px;
+ font-size: 1.08333em;
+ min-height: 1px;
+ }
+.leaflet-popup-content p {
+ margin: 17px 0;
+ margin: 1.3em 0;
+ }
+.leaflet-popup-tip-container {
+ width: 40px;
+ height: 20px;
+ position: absolute;
+ left: 50%;
+ margin-top: -1px;
+ margin-left: -20px;
+ overflow: hidden;
+ pointer-events: none;
+ }
+.leaflet-popup-tip {
+ width: 17px;
+ height: 17px;
+ padding: 1px;
+
+ margin: -10px auto 0;
+ pointer-events: auto;
+
+ -webkit-transform: rotate(45deg);
+ -moz-transform: rotate(45deg);
+ -ms-transform: rotate(45deg);
+ transform: rotate(45deg);
+ }
+.leaflet-popup-content-wrapper,
+.leaflet-popup-tip {
+ background: white;
+ color: #333;
+ box-shadow: 0 3px 14px rgba(0,0,0,0.4);
+ }
+.leaflet-container a.leaflet-popup-close-button {
+ position: absolute;
+ top: 0;
+ right: 0;
+ border: none;
+ text-align: center;
+ width: 24px;
+ height: 24px;
+ font: 16px/24px Tahoma, Verdana, sans-serif;
+ color: #757575;
+ text-decoration: none;
+ background: transparent;
+ }
+.leaflet-container a.leaflet-popup-close-button:hover,
+.leaflet-container a.leaflet-popup-close-button:focus {
+ color: #585858;
+ }
+.leaflet-popup-scrolled {
+ overflow: auto;
+ }
+
+.leaflet-oldie .leaflet-popup-content-wrapper {
+ -ms-zoom: 1;
+ }
+.leaflet-oldie .leaflet-popup-tip {
+ width: 24px;
+ margin: 0 auto;
+
+ -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
+ filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
+ }
+
+.leaflet-oldie .leaflet-control-zoom,
+.leaflet-oldie .leaflet-control-layers,
+.leaflet-oldie .leaflet-popup-content-wrapper,
+.leaflet-oldie .leaflet-popup-tip {
+ border: 1px solid #999;
+ }
+
+
+/* div icon */
+
+.leaflet-div-icon {
+ background: #fff;
+ border: 1px solid #666;
+ }
+
+
+/* Tooltip */
+/* Base styles for the element that has a tooltip */
+.leaflet-tooltip {
+ position: absolute;
+ padding: 6px;
+ background-color: #fff;
+ border: 1px solid #fff;
+ border-radius: 3px;
+ color: #222;
+ white-space: nowrap;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ pointer-events: none;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.4);
+ }
+.leaflet-tooltip.leaflet-interactive {
+ cursor: pointer;
+ pointer-events: auto;
+ }
+.leaflet-tooltip-top:before,
+.leaflet-tooltip-bottom:before,
+.leaflet-tooltip-left:before,
+.leaflet-tooltip-right:before {
+ position: absolute;
+ pointer-events: none;
+ border: 6px solid transparent;
+ background: transparent;
+ content: "";
+ }
+
+/* Directions */
+
+.leaflet-tooltip-bottom {
+ margin-top: 6px;
+}
+.leaflet-tooltip-top {
+ margin-top: -6px;
+}
+.leaflet-tooltip-bottom:before,
+.leaflet-tooltip-top:before {
+ left: 50%;
+ margin-left: -6px;
+ }
+.leaflet-tooltip-top:before {
+ bottom: 0;
+ margin-bottom: -12px;
+ border-top-color: #fff;
+ }
+.leaflet-tooltip-bottom:before {
+ top: 0;
+ margin-top: -12px;
+ margin-left: -6px;
+ border-bottom-color: #fff;
+ }
+.leaflet-tooltip-left {
+ margin-left: -6px;
+}
+.leaflet-tooltip-right {
+ margin-left: 6px;
+}
+.leaflet-tooltip-left:before,
+.leaflet-tooltip-right:before {
+ top: 50%;
+ margin-top: -6px;
+ }
+.leaflet-tooltip-left:before {
+ right: 0;
+ margin-right: -12px;
+ border-left-color: #fff;
+ }
+.leaflet-tooltip-right:before {
+ left: 0;
+ margin-left: -12px;
+ border-right-color: #fff;
+ }
+
+/* Printing */
+
+@media print {
+ /* Prevent printers from removing background-images of controls. */
+ .leaflet-control {
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+ }
+ }
diff --git a/resources/leaflet/leaflet.js b/resources/leaflet/leaflet.js
@@ -0,0 +1,6 @@
+/* @preserve
+ * Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com
+ * (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade
+ */
+!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">−</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">×</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});
+//# sourceMappingURL=leaflet.js.map
+\ No newline at end of file
diff --git a/resources/map.js b/resources/map.js
@@ -0,0 +1,431 @@
+// The Leaflet map: event markers, the rotate gesture engine (#map-stage spun as a unit,
+// since Leaflet 1.x can't rotate its own panes), and the loader for vendored Leaflet +
+// the NoGap tile layer. Taps forward to window.panel; rotation is handed to window.initNav.
+
+(function () {
+ var fmt = window.fmt;
+
+ var events = [];
+ var map = null;
+ var IS_TOUCH = window.matchMedia("(pointer: coarse)").matches;
+ var stageAngle = 0; // current map rotation in degrees, set by the rotate gesture
+
+ // Average lat/long of all events, so the page opens framing the events (no location
+ // permission until the compass is tapped). Fallback: Seattle.
+ function eventsCenter() {
+ if (!events.length) { return [47.6062, -122.3321]; }
+ var lat = 0, lng = 0;
+ events.forEach(function (m) { lat += m.latitude; lng += m.longitude; });
+ return [lat / events.length, lng / events.length];
+ }
+
+ // If the URL carries ?<slug>, open that event. Returns true if it selected one.
+ function selectFromUrl(select) {
+ var m = fmt.eventFromUrl(events);
+ if (!m) { return false; }
+ select(m, false);
+ return true;
+ }
+
+ // Rewrite the address bar to the event's deep link (no reload / history entry).
+ function syncUrl(m) {
+ if (window.history && history.replaceState) {
+ history.replaceState(null, "", fmt.eventUrl(m));
+ }
+ }
+
+ function clearUrl() {
+ if (window.history && history.replaceState && window.location.search) {
+ history.replaceState(null, "", window.location.pathname);
+ }
+ }
+
+ function initMap() {
+ // snapshot "now" once so marker filtering, z-ordering, and the intro all agree
+ var now = new Date();
+ // soonest first; passed events removed. With nothing upcoming, fall back to the
+ // 10 most-recently-ended events (most recent first) so the map isn't empty.
+ var upcoming = fmt.upcomingEvents(events, now);
+ var pastMode = upcoming.length === 0;
+ events = pastMode ? fmt.pastEvents(events, now, 10) : upcoming;
+
+ 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);
+
+ // CartoDB tile scale matching display density for crisp tiles. Hardcoded (not
+ // detectRetina, which shifts zoom and shrinks labels). carto serves up to @4x.
+ var dpr = window.devicePixelRatio || 1;
+ var scale = dpr >= 4 ? "@4x" : dpr >= 3 ? "@3x" : dpr >= 2 ? "@2x" : "";
+ var carto = L.tileLayer("https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}" + scale + ".png", {
+ attribution: "",
+ subdomains: "abcd",
+ maxZoom: 19,
+ // NoGap canvas spans keepBuffer; at full DPR each canvas is ratio² in memory,
+ // so a small buffer keeps it under Safari's renderer limit (default 2)
+ keepBuffer: 1
+ });
+
+ carto.addTo(map);
+
+ var mapEl = map.getContainer();
+ map.on("mousedown", function () { mapEl.classList.add("map-grabbing"); });
+ document.addEventListener("mouseup", function () { mapEl.classList.remove("map-grabbing"); });
+
+ initSmoothWheel();
+
+ // background tap (not a marker, not a drag/hold) deselects; Leaflet's "click"
+ // fires only for genuine background taps
+ map.on("click", function () { window.panel.deselectEvent(); clearUrl(); });
+
+ // container may have been sized after init; force a recalc
+ setTimeout(function () {
+ if (map) { map.invalidateSize(); }
+ }, 200);
+
+ function makeIcon(m) {
+ var d = fmt.parseLocal(m.start);
+ return L.divIcon({
+ className: "",
+ html: "<div class=\"event-pin\" style=\"background:" + m.color + "\">" +
+ "<span class=\"pin-month\">" + fmt.MONTH_ABBR[d.getMonth()] + "</span>" +
+ "<span class=\"pin-day\">" + d.getDate() + "</span>" +
+ "</div>",
+ iconSize: [48, 48],
+ iconAnchor: [24, 24]
+ });
+ }
+
+ // Leaflet bakes a latitude term into each marker's z-index, and that spread can
+ // exceed a +1 step, so bump by a large amount to guarantee the newest tap wins.
+ var Z_STEP = 10000;
+ // seed z by date (events sorted soonest-first) so soonest sit on top at load;
+ // topEventZ starts above every seed so the first tap still lifts over all
+ var topEventZ = (events.length + 1) * Z_STEP;
+
+ // Lift the event, type its panel, recenter - shared by marker taps and deep links.
+ function selectMarker(m, animate) {
+ topEventZ += Z_STEP;
+ m._marker.setZIndexOffset(topEventZ);
+ window.panel.selectEvent(m);
+ syncUrl(m);
+ // nav mode: stop following so the pan isn't fought + restart the ease-back
+ if (window.navInteract) { window.navInteract(); }
+ // low easeLinearity bends the default pan into a clear ease-in-out
+ map.panTo([m.latitude, m.longitude], { animate: animate, duration: 0.7, easeLinearity: 0.1 });
+ }
+
+ events.forEach(function (m, i) {
+ var marker = L.marker([m.latitude, m.longitude], { icon: makeIcon(m) }).addTo(map);
+ marker.setZIndexOffset((events.length - i) * Z_STEP);
+ m._marker = marker; // so selectEvent can reach this pin's DOM
+ marker.on("click", function () { selectMarker(m, true); });
+ });
+
+ window.panel.setIntroData(events, selectMarker, pastMode);
+
+ // deep-linked? open it. otherwise show intro.
+ if (!selectFromUrl(selectMarker)) { window.panel.paintIntro(); }
+
+ if (IS_TOUCH) {
+ var rotation = initRotate();
+ if (window.initNav) { window.initNav(map, rotation); }
+ }
+ }
+
+ // Desktop wheel/trackpad. Leaflet's stock scrollWheelZoom debounces and steps, which
+ // feels sluggish; instead ease map._move() toward a goal zoom every frame (the touch
+ // pinch path) anchored under the cursor, and pan via _rawPanBy so markers ride along
+ // (_move only re-renders layers on zoom change). zoomSnap 0 lets fractional zoom flow.
+ function initSmoothWheel() {
+ var el = map.getContainer();
+ var mode = null; // "pinch" | "wheel" | "pan" while a gesture is in flight
+ var goalZoom = 0, cursorPoint = null, centerPoint = null, anchorLatLng = null;
+ var rafId = null, idleTimer = null, moved = false, panning = false;
+ var prevCenter = null, prevZoom = 0;
+
+ function step() {
+ // something else moved the map (marker panTo, drag) - yield to it
+ if (moved && (!map.getCenter().equals(prevCenter) || map.getZoom() !== prevZoom)) {
+ clearTimeout(idleTimer);
+ settle();
+ return;
+ }
+ var zoom = map.getZoom() + (goalZoom - map.getZoom()) * 0.3;
+ // keep the latlng under the cursor fixed while zooming
+ var center = map.unproject(map.project(anchorLatLng, zoom).subtract(cursorPoint.subtract(centerPoint)), zoom);
+ if (!moved) { map._moveStart(true, false); moved = true; }
+ map._move(center, zoom);
+ prevCenter = map.getCenter();
+ prevZoom = map.getZoom();
+ rafId = requestAnimationFrame(step);
+ }
+
+ // end whichever gesture is in flight so tiles settle and moveend fires
+ function settle() {
+ mode = null;
+ if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; }
+ if (moved) { moved = false; map._moveEnd(true); }
+ if (panning) { panning = false; map.fire("moveend"); }
+ }
+
+ function armIdle() {
+ clearTimeout(idleTimer);
+ idleTimer = setTimeout(settle, 200);
+ }
+
+ function beginZoom(clientX, clientY) {
+ if (rafId === null) {
+ map._stop();
+ goalZoom = map.getZoom();
+ centerPoint = map.getSize().divideBy(2);
+ cursorPoint = map.mouseEventToContainerPoint({ clientX: clientX, clientY: clientY });
+ anchorLatLng = map.containerPointToLatLng(cursorPoint);
+ rafId = requestAnimationFrame(step);
+ }
+ armIdle();
+ }
+
+ function looksLikeMouse(e) {
+ if (e.deltaMode !== 0) { return true; }
+ return typeof e.wheelDeltaY === "number" && e.wheelDeltaY !== 0 && e.wheelDeltaY % 120 === 0;
+ }
+
+ el.addEventListener("wheel", function (e) {
+ e.preventDefault();
+ var next;
+ if (e.ctrlKey) { next = "pinch"; }
+ else if (mode === "pan" || mode === "wheel") { next = mode; } // locked while in flight
+ else { next = looksLikeMouse(e) ? "wheel" : "pan"; }
+ if (mode !== null && next !== mode) { clearTimeout(idleTimer); settle(); }
+ mode = next;
+ if (mode === "pan") {
+ if (!panning) { panning = true; map.fire("movestart"); }
+ map._rawPanBy(L.point(e.deltaX, e.deltaY));
+ map.fire("move");
+ armIdle();
+ } else {
+ beginZoom(e.clientX, e.clientY);
+ var dy = e.deltaMode === 1 ? e.deltaY * 20 : e.deltaY; // line deltas (firefox mice) -> px
+ goalZoom = map._limitZoom(goalZoom - dy * (mode === "pinch" ? 0.01 : 0.003));
+ cursorPoint = map.mouseEventToContainerPoint(e);
+ }
+ }, { passive: false });
+
+ // Safari desktop fires gesture* events with a cumulative scale instead of
+ // ctrl+wheel. Touch devices also have GestureEvent - skip them for native pinch.
+ if (!IS_TOUCH && "GestureEvent" in window) {
+ var gestureBaseZoom = 0;
+ el.addEventListener("gesturestart", function (e) {
+ e.preventDefault();
+ if (mode !== null && mode !== "pinch") { clearTimeout(idleTimer); settle(); }
+ mode = "pinch";
+ beginZoom(e.clientX, e.clientY);
+ gestureBaseZoom = goalZoom;
+ });
+ el.addEventListener("gesturechange", function (e) {
+ e.preventDefault();
+ if (rafId === null) { return; }
+ goalZoom = map._limitZoom(gestureBaseZoom + Math.log2(e.scale));
+ cursorPoint = map.mouseEventToContainerPoint(e);
+ armIdle();
+ });
+ el.addEventListener("gestureend", function (e) { e.preventDefault(); });
+ }
+ }
+
+ // Rotate the whole #map-stage (oversized to the panel diagonal) via a CSS transform
+ // and patch Leaflet's coordinate math so its native pinch-zoom/pan still land. Touch only.
+ function initRotate() {
+ var stage = document.getElementById("map-stage");
+ var panel = document.getElementById("left-box");
+ var compass = document.getElementById("compass");
+
+ // Tapping a marker in the rotated stage's clipped corner makes iOS Safari "reveal"
+ // it by auto-scrolling the container (despite overflow:hidden), shifting the stage
+ // off-center. Snap any such scroll back to origin. The reveal may move #left-box or
+ // the document, so neutralize both; the root scroller emits on window.
+ function pinScroll(eventTarget, scroller) {
+ eventTarget.addEventListener("scroll", function () {
+ if (scroller.scrollLeft || scroller.scrollTop) { scroller.scrollTo(0, 0); }
+ }, { passive: true });
+ }
+ pinScroll(panel, panel);
+ var root = document.scrollingElement || document.documentElement;
+ pinScroll(window, root);
+
+ // square of the panel's diagonal so a rotated map never exposes the corners
+ function sizeStage() {
+ var w = panel.clientWidth, h = panel.clientHeight;
+ var diag = Math.ceil(Math.hypot(w, h));
+ stage.style.width = diag + "px";
+ stage.style.height = diag + "px";
+ applyStageTransform();
+ map.invalidateSize();
+ }
+
+ // stage is anchored at the panel center (top/left 50%); translate by half its size
+ // to center, then rotate (translate before rotate so the pivot is the panel center)
+ function applyStageTransform() {
+ var half = (parseFloat(stage.style.width) || 0) / 2;
+ stage.style.transform = "translate(" + (-half) + "px, " + (-half) + "px) rotate(" + stageAngle + "deg)";
+ // pins counter-rotate so their date text stays upright
+ stage.style.setProperty("--pin-counter", (-stageAngle) + "deg");
+ // needle turns with the map so red keeps pointing to true north
+ compass.style.setProperty("--compass-angle", stageAngle + "deg");
+ }
+
+ function normalizeDeg(d) {
+ return ((d + 180) % 360 + 360) % 360 - 180;
+ }
+
+ // invert the stage rotation about the panel center to map a screen point into
+ // stage-local coords (getBoundingClientRect gives only the axis-aligned box)
+ function clientToStage(clientX, clientY) {
+ var r = panel.getBoundingClientRect();
+ var cx = r.left + r.width / 2;
+ var cy = r.top + r.height / 2;
+ var rad = (-stageAngle * Math.PI) / 180;
+ var dx = clientX - cx, dy = clientY - cy;
+ var rx = dx * Math.cos(rad) - dy * Math.sin(rad);
+ var ry = dx * Math.sin(rad) + dy * Math.cos(rad);
+ var half = (parseFloat(stage.style.width) || 0) / 2;
+ return { x: rx + half, y: ry + half };
+ }
+
+ // Leaflet derives container coords from the bounding rect, wrong once rotated.
+ map.mouseEventToContainerPoint = function (e) {
+ var p = clientToStage(e.clientX, e.clientY);
+ return L.point(p.x, p.y);
+ };
+
+ // one-finger pan: Leaflet computes the delta in raw screen px, so rotate it by
+ // -stageAngle so the map tracks the finger.
+ var _dragUpdate = L.Draggable.prototype._updatePosition;
+ L.Draggable.prototype._updatePosition = function () {
+ if (stageAngle && this._startPos && this._newPos) {
+ var rad = (-stageAngle * Math.PI) / 180;
+ var cos = Math.cos(rad), sin = Math.sin(rad);
+ var dx = this._newPos.x - this._startPos.x;
+ var dy = this._newPos.y - this._startPos.y;
+ this._newPos = this._startPos.add(L.point(dx * cos - dy * sin, dx * sin + dy * cos));
+ }
+ _dragUpdate.call(this);
+ };
+
+ // a panel touch sliding onto the map is handed off by iOS as a fresh map touchstart;
+ // flag gestures starting outside the stage so the drag below can refuse it
+ var panelGesture = false;
+ var panelGestureEnded = 0;
+ document.addEventListener("touchstart", function (e) {
+ if (!stage.contains(e.target)) { panelGesture = true; }
+ }, { passive: true, capture: true });
+ function endPanelGesture(e) {
+ if (e.touches.length === 0 && panelGesture) {
+ panelGesture = false;
+ panelGestureEnded = Date.now();
+ }
+ }
+ document.addEventListener("touchend", endPanelGesture, { passive: true, capture: true });
+ document.addEventListener("touchcancel", endPanelGesture, { passive: true, capture: true });
+
+ // the rotated ancestor inflates the pane's bounding box, so Leaflet's cached
+ // "parent scale" is bogus and distorts the drag. Force it to 1:1.
+ var _dragDown = L.Draggable.prototype._onDown;
+ L.Draggable.prototype._onDown = function (e) {
+ if (panelGesture || Date.now() - panelGestureEnded < 250) { return; }
+ _dragDown.call(this, e);
+ if (this._parentScale) { this._parentScale = { x: 1, y: 1 }; }
+ };
+
+ // --- two-finger rotate, layered on Leaflet's native pinch ---
+ var ROTATE_DEADZONE = 8; // deg of twist before rotation engages
+ var rotGesture = null;
+
+ function fingerAngle(t0, t1) {
+ return Math.atan2(t1.clientY - t0.clientY, t1.clientX - t0.clientX) * 180 / Math.PI;
+ }
+
+ // A one-finger touch landing while a zoom animates is dropped (Leaflet won't route
+ // to the drag handler until the anim ends), so end the in-flight zoom at the
+ // earliest capture point. _stop() doesn't cancel the zoom anim; _onZoomTransitionEnd
+ // clears _animatingZoom. Capture phase so it runs first.
+ stage.addEventListener("touchstart", function (e) {
+ if (e.touches.length === 1 && map._animatingZoom) {
+ map._onZoomTransitionEnd();
+ }
+ }, { passive: true, capture: true });
+
+ stage.addEventListener("touchstart", function (e) {
+ // rotation allowed in nav mode too; nav.js treats a manual twist as free-look
+ if (e.touches.length === 2) {
+ rotGesture = { startAngle: fingerAngle(e.touches[0], e.touches[1]), engaged: false, baseAngle: stageAngle };
+ }
+ }, { passive: true });
+
+ stage.addEventListener("touchmove", function (e) {
+ if (!rotGesture || e.touches.length !== 2) { return; }
+ var cur = fingerAngle(e.touches[0], e.touches[1]);
+ var twist = normalizeDeg(cur - rotGesture.startAngle);
+ if (!rotGesture.engaged) {
+ if (Math.abs(twist) < ROTATE_DEADZONE) { return; } // dead-zone keeps pure pinch from rotating
+ rotGesture.engaged = true;
+ rotGesture.startAngle = cur; // take accumulated twist as the new zero so it doesn't jump
+ rotGesture.baseAngle = stageAngle;
+ twist = 0;
+ }
+ stageAngle = normalizeDeg(rotGesture.baseAngle + twist);
+ applyStageTransform();
+ }, { passive: true });
+
+ function endRotate(e) {
+ if (rotGesture && e.touches.length < 2) { rotGesture = null; }
+ }
+ stage.addEventListener("touchend", endRotate);
+ stage.addEventListener("touchcancel", endRotate);
+
+ window.addEventListener("resize", sizeStage);
+ sizeStage();
+
+ // interface consumed by nav.js
+ return {
+ stage: stage,
+ panel: panel,
+ compass: compass,
+ getAngle: function () { return stageAngle; },
+ setAngle: function (deg) { stageAngle = deg; applyStageTransform(); },
+ applyTransform: applyStageTransform,
+ clientToStage: clientToStage,
+ normalizeDeg: normalizeDeg
+ };
+ }
+
+ function mapFailed() {
+ var el = document.getElementById("map-container");
+ el.className = "map-error";
+ el.textContent = "Unable to load map.";
+ }
+
+ // NoGap composites each level's tiles onto one <canvas> so fractional zoom has no
+ // per-tile seams. Loads after Leaflet (extends L.TileLayer); init regardless of failure.
+ function loadNoGapThenInit() {
+ var s = document.createElement("script");
+ s.src = "resources/leaflet/leaflet-nogap.js";
+ s.onload = initMap;
+ s.onerror = initMap;
+ document.body.appendChild(s);
+ }
+
+ // Leaflet is vendored under resources/leaflet/ so the map works offline from first load.
+ function loadLeaflet() {
+ if (window.L) { loadNoGapThenInit(); return; }
+ var s = document.createElement("script");
+ s.src = "resources/leaflet/leaflet.js";
+ s.onload = function () { window.L ? loadNoGapThenInit() : mapFailed(); };
+ s.onerror = mapFailed;
+ document.body.appendChild(s);
+ }
+
+ window.eventsMap = {
+ setEvents: function (data) { events = data; },
+ load: loadLeaflet
+ };
+})();
diff --git a/resources/nav.js b/resources/nav.js
@@ -0,0 +1,474 @@
+// Nav mode: tap the compass to request location + orientation, drop a marker at
+// the user's position, and follow them heading-up. Panning or pinch-rotating away
+// suspends following; after 3s of no interaction the ladybug eases back and follow
+// resumes. Touch only - script.js calls this from initMap once rotation is ready.
+
+(function () {
+ var LADYBUG = "🐞";
+ var FOLLOW_IDLE_MS = 3000;
+ var RING_REWIND_MS = 1200; // arc drain on touch; must stay under FOLLOW_IDLE_MS
+ var RING_FADE_MS = 200; // matches the 0.2s #compass box-shadow transition
+ var RING_LEAD_MS = 100; // arc completes a hair early so takeover feels instant
+ // ease-in-out with a firm landing: a flat tail reads as lag at takeover
+ var RING_FILL_EASE = "cubic-bezier(0.45, 0, 0.6, 0.9)";
+ var FOLLOW_ANCHOR_Y = 0.80; // fraction down the panel: leaves room "ahead"
+ var NAV_TRANSITION_MS = 900;
+ var EASE_TAU_MS = 600; // dead-reckon correction smoothing (bigger = smoother/laggier)
+ var VEL_SMOOTH = 0.5; // per-fix blend of new velocity into the running estimate
+ var MAX_PREDICT_MS = 4000; // stop extrapolating this long after the last fix
+ var SNAP_DIST_M = 500; // a fix this far from the rendered bug just teleports
+
+ window.initNav = function (map, rotation) {
+ var compass = rotation.compass;
+ var panel = rotation.panel;
+ var ringArc = compass.querySelector(".ring-arc");
+
+ // require an orientation sensor: keeps the compass off desktop touchscreens that
+ // can't give a heading.
+ if (!("DeviceOrientationEvent" in window)) { return; }
+ compass.classList.add("compass-enabled");
+
+ // dedicated pane keeps the ladybug above every event square regardless of z math
+ map.createPane("ladybugPane").style.zIndex = 700;
+
+ // Pane for the location->event line, between tilePane (200) and markerPane (600).
+ // We position the line ourselves every frame (see renderLine), so it must NOT carry
+ // leaflet-zoom-animated, or Leaflet's zoom transform would fight our writes.
+ var linePane = map.createPane("linePane");
+ linePane.style.zIndex = 250;
+
+ var heading = 0; // device heading in deg (0 = north)
+ var compassOn = false; // heading-up follow mode
+ var compassStarted = false;
+ var headingReady = false; // a real reading has arrived
+ var pendingEntry = false; // entered nav but waiting for first heading
+
+ var following = false;
+ var followIdleTimer = null;
+ var navAnim = null;
+ var rotAnimating = false;
+
+ var marker = null; // Leaflet marker for the ladybug
+ var lastFix = null; // { lat, lng, t } latest raw reading
+ var vel = { lat: 0, lng: 0 }; // deg/s, smoothed across fixes
+ var predictOn = false;
+ var watching = false;
+
+ // Line to the active event: a standalone SVG overlay. We reproject
+ // the two endpoints every frame via latLngToContainerPoint, so the
+ // line stays glued with constant 4px stroke.
+ var DRAW_MS = 300;
+ var lineSvg = null;
+ var lineEl = null;
+ var activeTarget = null; // { latlng, color }
+ var drawStart = 0;
+ var lineRAF = null;
+
+
+ // the panel-screen point the ladybug is pinned to while following
+ function followAnchorPoint() {
+ var r = panel.getBoundingClientRect();
+ return { x: r.left + r.width / 2, y: r.top + r.height * FOLLOW_ANCHOR_Y };
+ }
+
+ function makeIcon() {
+ return L.divIcon({
+ className: "",
+ html: "<div class=\"ladybug-pin\">" + LADYBUG + "</div>",
+ iconSize: [40, 40],
+ iconAnchor: [20, 20]
+ });
+ }
+
+ // --- line to active event ---
+
+ function ensureLineSvg() {
+ if (lineSvg) { return; }
+ var NS = "http://www.w3.org/2000/svg";
+ lineSvg = document.createElementNS(NS, "svg");
+ lineSvg.setAttribute("class", "ladybug-line-svg");
+ lineEl = document.createElementNS(NS, "line");
+ lineEl.setAttribute("class", "ladybug-line");
+ lineSvg.appendChild(lineEl);
+ linePane.appendChild(lineSvg);
+ }
+
+ function setLineTarget(latlng, color) {
+ ensureLineSvg();
+ activeTarget = { latlng: latlng, color: color };
+ lineEl.setAttribute("stroke", color);
+ drawStart = performance.now();
+ lineSvg.classList.add("visible");
+ renderLine();
+ if (!lineRAF) { lineRAF = requestAnimationFrame(tickLine); }
+ }
+
+ function clearLine() {
+ activeTarget = null;
+ if (lineRAF) { cancelAnimationFrame(lineRAF); lineRAF = null; }
+ if (lineSvg) { lineSvg.classList.remove("visible"); }
+ }
+
+ // linePane's local space is the map pane's space, offset from the container by the
+ // pane's live translation; subtract it to place a container point inside the pane.
+ // Grow the visible end from the ladybug toward the event over DRAW_MS.
+ function renderLine() {
+ if (!activeTarget || !marker || !lineEl) { return; }
+ var off = L.DomUtil.getPosition(map.getPane("mapPane")) || L.point(0, 0);
+ var ac = map.latLngToContainerPoint(marker.getLatLng());
+ var bc = map.latLngToContainerPoint(activeTarget.latlng);
+ var a = { x: ac.x - off.x, y: ac.y - off.y };
+ var b = { x: bc.x - off.x, y: bc.y - off.y };
+ var p = Math.min(1, (performance.now() - drawStart) / DRAW_MS);
+ var e = p * p * (3 - 2 * p); // smoothstep
+ lineEl.setAttribute("x1", a.x);
+ lineEl.setAttribute("y1", a.y);
+ lineEl.setAttribute("x2", a.x + (b.x - a.x) * e);
+ lineEl.setAttribute("y2", a.y + (b.y - a.y) * e);
+ }
+
+ // keeps the line glued during one-finger pan and the ladybug glide (both move the
+ // map outside Leaflet's zoom rAF)
+ function tickLine() {
+ if (!activeTarget || !marker) { clearLine(); return; }
+ renderLine();
+ lineRAF = requestAnimationFrame(tickLine);
+ }
+
+ // two-finger gestures move the map on Leaflet's own rAF; rendering on its move/zoom
+ // events puts the line in the same phase as the markers
+ map.on("move zoom", renderLine);
+
+ window.navLine = { setTarget: setLineTarget, clear: clearLine };
+
+ // --- following ---
+
+ function startFollowing() {
+ following = true;
+ clearTimeout(followIdleTimer);
+ followIdleTimer = null;
+ pinToAnchor();
+ if (compassOn) { animateRotation(); }
+ }
+
+ function stopFollowing() {
+ following = false;
+ pendingEntry = false;
+ if (navAnim) { cancelAnimationFrame(navAnim); navAnim = null; }
+ clearTimeout(followIdleTimer);
+ followIdleTimer = null;
+ }
+
+ // pan (no animation) so the ladybug lands on the anchor point under the current
+ // rotation. Inverting the anchor through the rotation makes the map spin about it.
+ function pinToAnchor() {
+ if (!following || !marker) { return; }
+ var a = followAnchorPoint();
+ var target = rotation.clientToStage(a.x, a.y);
+ var cur = map.latLngToContainerPoint(marker.getLatLng());
+ var dx = cur.x - target.x, dy = cur.y - target.y;
+ if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) { return; }
+ map.panBy([dx, dy], { animate: false });
+ }
+
+ // Dead-reckon: extrapolate the last fix along its velocity and ease the rendered
+ // position toward that moving target, so the ladybug glides between readings. Fresh
+ // fixes steer the target; the ease supplies accel/decel. Extrapolation is capped
+ // so a stalled GPS doesn't walk the bug off; once converged the loop idles.
+ function startPredict() {
+ if (predictOn) { return; }
+ predictOn = true;
+ var prev = performance.now();
+ var step = function (now) {
+ if (!marker || !lastFix) { predictOn = false; return; }
+ var dt = Math.min(now - prev, 100); // clamp tab-switch gaps
+ prev = now;
+ var ahead = Math.min(now - lastFix.t, MAX_PREDICT_MS) / 1000;
+ var tLat = lastFix.lat + vel.lat * ahead;
+ var tLng = lastFix.lng + vel.lng * ahead;
+ var cur = marker.getLatLng();
+ var k = 1 - Math.exp(-dt / EASE_TAU_MS); // frame-rate independent
+ marker.setLatLng([cur.lat + (tLat - cur.lat) * k, cur.lng + (tLng - cur.lng) * k]);
+ if (following) { pinToAnchor(); }
+ var z = map.getZoom();
+ var px = map.project(marker.getLatLng(), z).distanceTo(map.project(L.latLng(tLat, tLng), z));
+ var still = Math.hypot(vel.lat, vel.lng) < 1e-7;
+ if (px <= 0.5 && (still || now - lastFix.t >= MAX_PREDICT_MS)) { predictOn = false; return; }
+ requestAnimationFrame(step);
+ };
+ requestAnimationFrame(step);
+ }
+
+ // --- compass ring (suspend/countdown visuals) ---
+
+ var ringFadeTimer = null;
+ var ringFillTimer = null; // delays the refill until fade/rewind lands
+ var ringWaitUntil = 0; // when the in-flight fade or rewind ends
+
+ function ringSet(offset, transition) {
+ ringArc.style.transition = transition;
+ ringArc.style.strokeDashoffset = offset;
+ }
+
+ // snap the arc to empty, flushed so a transition set right after still runs
+ function ringReset() {
+ clearTimeout(ringFadeTimer);
+ ringFadeTimer = null;
+ clearTimeout(ringFillTimer);
+ ringFillTimer = null;
+ ringArc.style.transition = "none";
+ ringArc.style.strokeDashoffset = 100;
+ ringArc.style.opacity = "";
+ void ringArc.getBoundingClientRect();
+ }
+
+ // touching the map suspends following (free-look). The countdown is NOT started
+ // here (only on release) so holding a finger down keeps control indefinitely.
+ function suspendFollow() {
+ if (!compassOn) { return; }
+ following = false;
+ if (navAnim) { cancelAnimationFrame(navAnim); navAnim = null; }
+ clearTimeout(followIdleTimer); // a new touch cancels a running countdown
+ followIdleTimer = null;
+ clearTimeout(ringFadeTimer);
+ ringFadeTimer = null;
+ clearTimeout(ringFillTimer);
+ ringFillTimer = null;
+ ringArc.style.opacity = "";
+ if (!compass.classList.contains("paused")) {
+ // engaged follow: blue ring fades to the grey track; arc regrows later
+ compass.classList.add("paused");
+ ringSet(100, "none");
+ ringWaitUntil = performance.now() + RING_FADE_MS;
+ } else if (ringArc.style.strokeDashoffset !== "100") {
+ // arc has charge: drain it (a drain already in flight keeps its clock)
+ ringSet(100, "stroke-dashoffset " + RING_REWIND_MS + "ms ease-out");
+ ringWaitUntil = performance.now() + RING_REWIND_MS;
+ }
+ }
+
+ // start the countdown once the LAST finger lifts; if fingers remain, keep waiting
+ function armEaseBack(e) {
+ if (!compassOn) { return; }
+ if (e && e.touches && e.touches.length > 0) { return; }
+ clearTimeout(followIdleTimer);
+ followIdleTimer = setTimeout(easeBack, FOLLOW_IDLE_MS);
+ // arc refills in sync with the countdown; let any in-flight fade/rewind land
+ // first, landing RING_LEAD_MS early so takeover feels instant
+ var wait = Math.max(0, ringWaitUntil - performance.now());
+ var dur = FOLLOW_IDLE_MS - wait - RING_LEAD_MS;
+ clearTimeout(ringFillTimer);
+ if (wait > 0) {
+ ringFillTimer = setTimeout(function () {
+ ringFillTimer = null;
+ ringSet(0, "stroke-dashoffset " + dur + "ms " + RING_FILL_EASE);
+ }, wait);
+ } else {
+ ringReset();
+ ringSet(0, "stroke-dashoffset " + dur + "ms " + RING_FILL_EASE);
+ }
+ }
+
+ // timer fired: the app takes over. The completed arc fades over the same 0.2s the
+ // inset ring fades grey->blue.
+ function easeBack() {
+ followIdleTimer = null;
+ compass.classList.remove("paused");
+ ringArc.style.transition = "opacity 0.2s ease";
+ ringArc.style.opacity = "0";
+ clearTimeout(ringFadeTimer);
+ ringFadeTimer = setTimeout(ringReset, 200);
+ animateToAnchor();
+ }
+
+ // glide the ladybug to the anchor AND rotate heading-up together off one ease, then
+ // engage following. Re-aims at the LIVE heading every frame so the glide lands with
+ // no leftover snap (the heading drifts during the 600ms).
+ function animateToAnchor() {
+ if (!compassOn) { return; }
+ if (navAnim) { cancelAnimationFrame(navAnim); navAnim = null; }
+ if (!marker) { startFollowing(); return; }
+
+ var startAngle = rotation.getAngle();
+ var dAngle = rotation.normalizeDeg(rotation.normalizeDeg(-heading) - startAngle);
+ var t0 = performance.now();
+ var ePrev = 0;
+
+ var step = function (now) {
+ if (!compassOn) { navAnim = null; return; }
+ if (followIdleTimer) { navAnim = null; return; } // user interrupted
+ var p = (now - t0) / NAV_TRANSITION_MS;
+ if (p >= 1) {
+ rotation.setAngle(rotation.normalizeDeg(-heading));
+ navAnim = null;
+ startFollowing();
+ return;
+ }
+ // keep turning the same way if the target slips across the ±180 seam
+ var d = rotation.normalizeDeg(rotation.normalizeDeg(-heading) - startAngle);
+ if (d - dAngle > 180) { d -= 360; } else if (dAngle - d > 180) { d += 360; }
+ dAngle = d;
+ var e = p * p * p * (p * (p * 6 - 15) + 10); // smootherstep
+ // move by the fraction of REMAINING distance this step covers, so cumulative
+ // progress is exactly `e` (frame-invariant despite panBy shifting the frame)
+ var frac = ePrev < 1 ? (e - ePrev) / (1 - ePrev) : 1;
+ ePrev = e;
+ rotation.setAngle(rotation.normalizeDeg(startAngle + dAngle * e));
+ var a = followAnchorPoint();
+ var target = rotation.clientToStage(a.x, a.y);
+ var cur = map.latLngToContainerPoint(marker.getLatLng());
+ map.panBy([(cur.x - target.x) * frac, (cur.y - target.y) * frac], { animate: false });
+ navAnim = requestAnimationFrame(step);
+ };
+ navAnim = requestAnimationFrame(step);
+ }
+
+ // --- rotation (heading-up) ---
+
+ // ease stageAngle toward the live heading; runs only while following.
+ function animateRotation() {
+ if (rotAnimating) { return; }
+ rotAnimating = true;
+ var step = function () {
+ if (!compassOn || !following) { rotAnimating = false; return; }
+ var delta = rotation.normalizeDeg(-heading - rotation.getAngle());
+ if (Math.abs(delta) < 0.05) {
+ rotation.setAngle(rotation.normalizeDeg(-heading));
+ pinToAnchor();
+ rotAnimating = false;
+ return;
+ }
+ rotation.setAngle(rotation.getAngle() + delta * 0.2); // exp ease
+ pinToAnchor(); // re-pin same tick so the ladybug never lags the spin
+ requestAnimationFrame(step);
+ };
+ requestAnimationFrame(step);
+ }
+
+ // --- orientation ---
+
+ function startCompass() {
+ if (compassStarted) { return; }
+ compassStarted = true;
+ var onReading = function (e) {
+ var h;
+ if (typeof e.webkitCompassHeading === "number") {
+ h = e.webkitCompassHeading; // iOS: deg clockwise from north
+ } else if (typeof e.alpha === "number") {
+ h = 360 - e.alpha;
+ } else {
+ return;
+ }
+ heading = h;
+ var first = !headingReady;
+ headingReady = true;
+ if (first && pendingEntry) { pendingEntry = false; animateToAnchor(); return; }
+ if (compassOn) { animateRotation(); }
+ };
+ window.addEventListener("deviceorientationabsolute", onReading, true);
+ window.addEventListener("deviceorientation", onReading, true);
+ }
+
+ function requestOrientationPermission() {
+ var DOE = window.DeviceOrientationEvent;
+ if (DOE && typeof DOE.requestPermission === "function") {
+ return DOE.requestPermission().then(function (s) { return s === "granted"; }).catch(function () { return false; });
+ }
+ return Promise.resolve(true);
+ }
+
+ // --- location ---
+
+ map.on("locationfound", function (e) {
+ var now = performance.now();
+ var firstFix = !marker;
+ if (firstFix) {
+ marker = L.marker(e.latlng, { icon: makeIcon(), interactive: false, pane: "ladybugPane" }).addTo(map);
+ // fade in: add .visible next frame so the opacity transition runs from 0
+ var pin = marker._icon && marker._icon.querySelector(".ladybug-pin");
+ if (pin) { requestAnimationFrame(function () { pin.classList.add("visible"); }); }
+ // event already selected before location came on: draw the line now
+ var active = window.getActiveEvent && window.getActiveEvent();
+ if (active) { setLineTarget(active.latlng, active.color); }
+ } else {
+ var dt = (now - lastFix.t) / 1000;
+ if (map.distance(e.latlng, marker.getLatLng()) >= SNAP_DIST_M) {
+ // huge jump (first good fix after a bad one): no glide across town
+ vel.lat = 0;
+ vel.lng = 0;
+ marker.setLatLng(e.latlng);
+ if (following) { pinToAnchor(); }
+ } else if (dt >= 10) {
+ // stale gap: old velocity means nothing
+ vel.lat = 0;
+ vel.lng = 0;
+ } else if (dt > 0.05) {
+ // velocity from the last two fixes drives the dead-reckoner; blending
+ // damps jitter (sub-50ms bursts keep the running value)
+ vel.lat += ((e.latlng.lat - lastFix.lat) / dt - vel.lat) * VEL_SMOOTH;
+ vel.lng += ((e.latlng.lng - lastFix.lng) / dt - vel.lng) * VEL_SMOOTH;
+ }
+ }
+ lastFix = { lat: e.latlng.lat, lng: e.latlng.lng, t: now };
+ if (!firstFix) { startPredict(); }
+ // first fix: ease from the current view to the anchored ladybug rather than
+ // hard-jumping. animateToAnchor pans relative to the marker, so just engaging
+ // it (once a heading is ready) gives the fly-in for free.
+ if (firstFix && compassOn) {
+ if (headingReady) { animateToAnchor(); }
+ else { pendingEntry = true; }
+ }
+ });
+
+ function startWatching() {
+ if (watching) { return; }
+ watching = true;
+ map.locate({ watch: true, enableHighAccuracy: true, maximumAge: 10000 });
+ }
+
+ // --- compass tap (enter/exit nav mode) ---
+ function enterNav() {
+ requestOrientationPermission().then(function (ok) {
+ if (!ok) { return; } // orientation denied: stay off
+ startCompass();
+ startWatching();
+ compassOn = true;
+ compass.classList.remove("paused");
+ ringReset();
+ compass.classList.add("active");
+ // ease to anchor + rotate heading-up together; defer to the first reading
+ // if none yet, so it rotates too (not move-then-rotate)
+ if (headingReady) { animateToAnchor(); }
+ else { pendingEntry = true; }
+ });
+ }
+
+ function exitNav() {
+ compassOn = false;
+ compass.classList.remove("active", "paused");
+ ringReset();
+ stopFollowing();
+ // angle holds where it is - no snap back to north
+ }
+
+ compass.style.pointerEvents = "auto"; // CSS sets none; nav makes it tappable
+ compass.style.cursor = "pointer";
+ compass.addEventListener("click", function () {
+ if (compassOn) { exitNav(); } else { enterNav(); }
+ });
+
+ // a map gesture in nav mode is a free-look: suspend on touchdown, start the
+ // ease-back only once the finger(s) release
+ var mapEl = map.getContainer();
+ mapEl.addEventListener("touchstart", suspendFollow, { passive: true });
+ mapEl.addEventListener("touchend", armEaseBack, { passive: true });
+ mapEl.addEventListener("touchcancel", armEaseBack, { passive: true });
+
+ // selecting an event counts as an interaction: suspend follow so the pan plays
+ // out, then restart the countdown. No-op outside nav mode (compassOn guards).
+ window.navInteract = function () {
+ suspendFollow();
+ armEaseBack();
+ };
+ };
+})();
diff --git a/resources/panel.js b/resources/panel.js
@@ -0,0 +1,406 @@
+// Right panel: title/body/RSVP text, its color-shift + character-by-character typing, and
+// which event is selected. map.js calls selectEvent / deselectEvent on taps; nav.js reads
+// window.getActiveEvent and draws the location->event line via window.navLine.
+
+(function () {
+ var fmt = window.fmt;
+
+ var intro = {
+ color: "antiquewhite",
+ title: "Upcoming Events"
+ };
+ // flipped on by setIntroData when there are no upcoming events: the intro lists the
+ // most-recently-passed events under a "Past Events" heading instead.
+ var pastMode = false;
+
+ var rightBox = document.getElementById("right-box");
+ var themeColorMeta = document.querySelector('meta[name="theme-color"]');
+ var titleEl = document.getElementById("text-title");
+ var contentEl = document.getElementById("text-content");
+ var actionWrap = document.getElementById("action-wrap");
+ var swapTimer = null;
+ var linkTimer = null;
+ var typeRaf = null;
+ var currentEvent = null;
+ var activePin = null; // .event-pin element of the selected marker (square -> circle)
+ var introEvents = []; // upcoming events for the intro table, soonest first
+ var introDataReady = false; // true once map.js hands over the events (post-fetch)
+ var selectFromIntro = null; // map.js's selectMarker, so table rows can activate an event
+ var introListEl = null; // the current intro grid, so fitIntroColumns can re-measure on resize
+
+ function introTitle() { return pastMode ? "Past Events" : intro.title; }
+
+ var CHAR_MS = readMs("--type-char-ms");
+
+ function readMs(name) {
+ var raw = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
+ var ms = parseFloat(raw);
+ return /s$/.test(raw) && !/ms$/.test(raw) ? ms * 1000 : ms;
+ }
+
+ function clearTyping() {
+ if (typeRaf !== null) { cancelAnimationFrame(typeRaf); typeRaf = null; }
+ }
+
+ // Lay the full text in up front, split into a "shown" span and a trailing "hidden"
+ // (visibility:hidden) span. Layout/wrapping/height are final from frame one, so typing
+ // only moves the boundary
+ function prepTyped(el, text) {
+ el.textContent = "";
+ var shown = document.createElement("span");
+ var hidden = document.createElement("span");
+ hidden.className = "type-hidden";
+ hidden.textContent = text;
+ el.appendChild(shown);
+ el.appendChild(hidden);
+ return { shown: shown, hidden: hidden, text: text };
+ }
+
+ function typeStream(segments) {
+ var prepped = segments.map(function (seg) {
+ // flat segments (RSVP labels) already reserve width via a CSS sizer; flowing
+ // body text gets the split-span treatment to pre-reserve wrapping + height
+ var p = seg.flat
+ ? { shown: seg.el, hidden: null, text: seg.text }
+ : prepTyped(seg.el, seg.text);
+ // list marker stays hidden until this segment's first char reveals
+ p.markerEl = seg.markerEl || null;
+ return p;
+ });
+ // total time scales with text length; progress through it is smoothstep-eased.
+ // rAF-driven (one reflow/frame) so CHAR_MS can sit below the ~1ms timer floor.
+ var total = prepped.reduce(function (n, s) { return n + s.text.length; }, 0);
+ var duration = total * CHAR_MS;
+ var start = null;
+ function frame(now) {
+ if (start === null) { start = now; }
+ var t = duration > 0 ? Math.min(1, (now - start) / duration) : 1;
+ var eased = t * t * (3 - 2 * t); // smoothstep: zero velocity at both ends
+ var revealed = Math.min(total, Math.round(eased * total));
+ var n = revealed;
+ prepped.forEach(function (s) {
+ var len = s.text.length;
+ var c = n <= 0 ? 0 : (n >= len ? len : n);
+ s.shown.textContent = s.text.slice(0, c);
+ if (s.hidden) { s.hidden.textContent = s.text.slice(c); }
+ if (s.markerEl && c > 0) { s.markerEl.classList.add("li-typing"); }
+ n -= len;
+ });
+ typeRaf = revealed < total ? requestAnimationFrame(frame) : null;
+ }
+ typeRaf = requestAnimationFrame(frame);
+ return duration;
+ }
+
+ // Build the event's .ics in memory and trigger a download. On iOS, tapping the file
+ // offers "Add to Calendar"; desktop drops it in Downloads.
+ function downloadICS(m) {
+ if (!m) { return; }
+ var blob = new Blob([fmt.icsContent(m)], { type: "text/calendar;charset=utf-8" });
+ var url = URL.createObjectURL(blob);
+ var a = document.createElement("a");
+ a.href = url;
+ a.download = fmt.icsFilename(m);
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ setTimeout(function () { URL.revokeObjectURL(url); }, 0);
+ }
+
+ var canShare = typeof navigator.share === "function";
+
+ // "Copy Link" -> "Copied Link!" feedback: swap instantly, hold, swap back
+ function flashCopied(a) {
+ a.textContent = "Copied Link!";
+ a._copyTimer = setTimeout(function () {
+ a.textContent = "Copy Link";
+ a._copyTimer = null;
+ }, 3000);
+ }
+
+ var LINKS = [
+ { text: "RSVP by E-mail", onClick: function (_a, m) { window.location.href = fmt.mailtoHref(m); } },
+ { text: "Add to Calendar", onClick: function (a, m) { downloadICS(m); } },
+ // maps:// via location.href: the OS intercepts the scheme and the page stays put,
+ // so no orphaned about:blank tab. https links still get a real tab.
+ { text: "Get Directions", onClick: function (a, m) {
+ var href = fmt.directionsHref(m);
+ if (href.indexOf("maps:") === 0) { window.location.href = href; }
+ else { window.open(href, "_blank"); }
+ } },
+ { text: canShare ? "Share Link" : "Copy Link", onClick: function (a, m) {
+ var url = fmt.eventUrl(m);
+ if (canShare) {
+ navigator.share({ url: url }).catch(function () {});
+ } else if (navigator.clipboard && !a._copyTimer) {
+ navigator.clipboard.writeText(url);
+ flashCopied(a);
+ }
+ } },
+ // on Safari this opens a printable PDF in a new tab instead of the print dialog
+ window.flyer.enabled ? { text: "Print Flyer", onClick: function (a, m) { window.flyer.print(m); } } : null,
+ ].filter(Boolean);
+
+ function buildLinkRows() {
+ actionWrap.innerHTML = "";
+ return LINKS.map(function (def) {
+ var row = document.createElement("div");
+ row.className = "action-row";
+
+ var box = document.createElement("span");
+ box.className = "action-link action-typing";
+
+ var sizer = document.createElement("span");
+ sizer.className = "action-sizer";
+ sizer.textContent = def.text;
+
+ var shown = document.createElement("span");
+ shown.className = "action-shown";
+
+ box.appendChild(sizer);
+ box.appendChild(shown);
+ row.appendChild(box);
+ actionWrap.appendChild(row);
+
+ return { el: shown, row: row, def: def };
+ });
+ }
+
+ // Build the detail <ul> inside #text-content and return its <li>s for typing. Each <li>
+ // carries its text on ._text; the marker + hanging indent come from CSS.
+ function buildDetailList(items) {
+ var ul = document.createElement("ul");
+ ul.className = "detail-list";
+ contentEl.appendChild(ul);
+ return items.map(function (text) {
+ var li = document.createElement("li");
+ li._text = text;
+ ul.appendChild(li);
+ return li;
+ });
+ }
+
+ // Build the intro's upcoming-events table and return the segments to type (one per
+ // cell). Rows are clickable. No events -> a single "No upcoming events." line. A flex
+ // list of <div>s (not a <table>) so columns collapse cleanly.
+ function buildIntroList() {
+ introListEl = null;
+ if (!introEvents.length) {
+ var p = document.createElement("p");
+ contentEl.appendChild(p);
+ return [{ el: p, text: pastMode ? "No past events." : "No upcoming events." }];
+ }
+
+ var list = document.createElement("div");
+ list.className = "intro-list";
+ contentEl.appendChild(list);
+ introListEl = list;
+
+ var segments = [];
+ introEvents.forEach(function (m) {
+ var row = document.createElement("div");
+ row.className = "intro-row";
+ row.addEventListener("click", function () {
+ if (selectFromIntro) { selectFromIntro(m, true); }
+ });
+
+ // each cell types into a child span so its final width is reserved (prepTyped)
+ [
+ { text: m.title, cls: "intro-name" },
+ { text: fmt.shortDate(m, pastMode), cls: "intro-when" },
+ { text: m.venue, cls: "intro-where" }
+ ].forEach(function (col) {
+ var cell = document.createElement("div");
+ cell.className = col.cls;
+ var span = document.createElement("span");
+ cell.appendChild(span);
+ row.appendChild(cell);
+ segments.push({ el: span, text: col.text });
+ });
+
+ list.appendChild(row);
+ });
+
+ return segments;
+ }
+
+ // True if any title cell is clipped at the current column layout. With date + location
+ // held rigid, a long title (or a title squeezed by them) ellipsizes here, which is the
+ // signal to shed a column.
+ function titleClipped() {
+ var cells = introListEl.querySelectorAll(".intro-name");
+ for (var i = 0; i < cells.length; i++) {
+ if (cells[i].scrollWidth - cells[i].clientWidth > 1) { return true; }
+ }
+ return false;
+ }
+
+ // Shed columns by priority until the title fits: drop the date (.cols-2), then the
+ // location (.cols-1). Re-measured on build and on container resize. Reading scrollWidth
+ // forces the reflow between steps.
+ function fitIntroColumns() {
+ if (!introListEl || !introListEl.isConnected) { return; }
+ introListEl.classList.remove("cols-2", "cols-1");
+ if (titleClipped()) {
+ introListEl.classList.add("cols-2");
+ if (titleClipped()) { introListEl.classList.add("cols-1"); }
+ }
+ }
+
+ function swapLinks(links) {
+ links.forEach(function (link) {
+ var def = link.def;
+ var a = document.createElement("a");
+ a.className = "action-link";
+ a.textContent = def.text;
+ if (def.href) {
+ a.href = def.href(currentEvent);
+ } else {
+ a.href = "#";
+ a.addEventListener("click", function (e) {
+ e.preventDefault();
+ def.onClick(a, currentEvent);
+ });
+ }
+ link.row.replaceChild(a, link.row.firstChild);
+ });
+ }
+
+ // Round the active pin into a circle (and square the previous one back) via a class.
+ function setActivePin(m) {
+ if (activePin) { activePin.classList.remove("active"); }
+ activePin = null;
+ var icon = m && m._marker && m._marker._icon;
+ var pin = icon && icon.querySelector(".event-pin");
+ if (pin) { pin.classList.add("active"); activePin = pin; }
+ }
+
+ function selectEvent(m) {
+ if (m === currentEvent) { return; } // already viewing it; don't retype
+ currentEvent = m;
+ setActivePin(m);
+ // draw the location->event line (no-op unless nav mode dropped a ladybug)
+ if (window.navLine) { window.navLine.setTarget(L.latLng(m.latitude, m.longitude), m.color); }
+ transitionTo(m.color, function () {
+ var links = buildLinkRows();
+ // description types into its own <p>, then each detail <li> as its own segment
+ var para = document.createElement("p");
+ contentEl.appendChild(para);
+ var details = buildDetailList(fmt.detailItems(m, true));
+ var segments = [
+ { el: titleEl, text: m.title },
+ { el: para, text: m.description }
+ ];
+ details.forEach(function (li) {
+ segments.push({ el: li, text: li._text, markerEl: li });
+ });
+ links.forEach(function (link) {
+ segments.push({ el: link.el, text: link.def.text, flat: true });
+ });
+ var duration = typeStream(segments);
+ // once the whole stream finishes, make the links clickable
+ linkTimer = setTimeout(function () {
+ swapLinks(links);
+ }, duration);
+ });
+ }
+
+ // nav.js asks for this on its first location fix: if an event is already selected, it
+ // draws the line to it immediately.
+ window.getActiveEvent = function () {
+ if (!currentEvent) { return null; }
+ return { latlng: L.latLng(currentEvent.latitude, currentEvent.longitude), color: currentEvent.color };
+ };
+
+ // Background tap deselects and types the intro back in.
+ function deselectEvent() {
+ if (currentEvent === null) { return; } // already showing the intro
+ currentEvent = null;
+ setActivePin(null);
+ if (window.navLine) { window.navLine.clear(); }
+ transitionTo(intro.color, function () {
+ var segments = [{ el: titleEl, text: introTitle() }];
+ typeStream(segments.concat(buildIntroList()));
+ fitIntroColumns();
+ });
+ }
+
+ // Drive the panel background and the iOS Safari top/bottom bars off one color so the
+ // bars track the selected event.
+ function setEventColor(color) {
+ // :root so the body strips (overscroll/safe-area) and the panel both pick it up
+ document.documentElement.style.setProperty("--event-color", color);
+ if (themeColorMeta) themeColorMeta.setAttribute("content", color);
+ }
+
+ // Shared transition: shift color, fade the text out, then run `paint` once faded.
+ function transitionTo(color, paint) {
+ clearTimeout(swapTimer);
+ clearTimeout(linkTimer);
+ clearTyping();
+
+ setEventColor(color);
+ titleEl.classList.add("fading");
+ contentEl.classList.add("fading");
+ actionWrap.classList.add("fading");
+
+ swapTimer = setTimeout(function () {
+ contentEl.textContent = "";
+ actionWrap.innerHTML = "";
+ // reset scroll now (old text faded + cleared) so the prior event never flashes back
+ rightBox.scrollTop = 0;
+
+ titleEl.classList.remove("fading");
+ contentEl.classList.remove("fading");
+ actionWrap.classList.remove("fading");
+
+ paint();
+ }, 125);
+ }
+
+ // Intro fully shown (no typing). used on root page load, where typing is skipped.
+ function paintIntro() {
+ setEventColor(intro.color);
+ titleEl.textContent = introTitle();
+ contentEl.textContent = "";
+ actionWrap.innerHTML = "";
+ if (!introDataReady) { return; } // pre-fetch: title only, no table yet
+ buildIntroList().forEach(function (seg) { seg.el.textContent = seg.text; });
+ fitIntroColumns();
+ }
+
+ // map.js hands over the upcoming events (soonest first) + selectMarker, before the
+ // first paintIntro / deselect, so the table can build from real data.
+ function setIntroData(events, selectFn, past) {
+ introEvents = events;
+ selectFromIntro = selectFn;
+ pastMode = !!past;
+ introDataReady = true;
+ }
+
+ // Set the panel color up front (before any text paints) so a deep-linked event shows in
+ // its own color instead of fading in from antiquewhite. Zero the color-shift duration
+ // for this one set so it snaps, then restore.
+ function presetEventColor(m) {
+ var root = document.documentElement;
+ root.style.setProperty("--color-shift-ms", "0ms");
+ setEventColor(m.color);
+ void root.offsetWidth; // force reflow so the instant set lands before restore
+ root.style.removeProperty("--color-shift-ms");
+ }
+
+ // Re-fit the intro columns when the panel changes width (rotation, window resize).
+ if (typeof ResizeObserver === "function") {
+ new ResizeObserver(fitIntroColumns).observe(rightBox);
+ }
+
+ window.panel = {
+ selectEvent: selectEvent,
+ deselectEvent: deselectEvent,
+ paintIntro: paintIntro,
+ setIntroData: setIntroData,
+ presetEventColor: presetEventColor,
+ // the event currently shown, or null on the intro (lets flyer.js print on Cmd+P)
+ getSelectedEvent: function () { return currentEvent; }
+ };
+})();
diff --git a/resources/pdf.js b/resources/pdf.js
@@ -0,0 +1,139 @@
+// Minimal single-page PDF writer for the flyer's Safari path: vector text (built-in
+// Times-Roman/Times-Bold, no embedding), filled rects (QR modules), dashed lines (cut
+// marks). No DOM. Pure data in, bytes out, so it also runs under node. Coordinates are
+// raw PDF points, origin bottom-left (y-up); callers flip. Text is WinAnsi-encoded
+// (ASCII/Latin-1 pass through, common Win-1252 punctuation mapped, else "?"); widthOf uses
+// the same encoding via the vendored AFM widths so wrapping matches what renders.
+
+(function (root) {
+ // unicode -> WinAnsi byte for the 0x80-0x9F slots worth mapping
+ var UNI2WIN = {
+ 0x20AC: 0x80, 0x201A: 0x82, 0x0192: 0x83, 0x201E: 0x84, 0x2026: 0x85,
+ 0x2020: 0x86, 0x2021: 0x87, 0x02C6: 0x88, 0x2030: 0x89, 0x0160: 0x8A,
+ 0x2039: 0x8B, 0x0152: 0x8C, 0x017D: 0x8E, 0x2018: 0x91, 0x2019: 0x92,
+ 0x201C: 0x93, 0x201D: 0x94, 0x2022: 0x95, 0x2013: 0x96, 0x2014: 0x97,
+ 0x02DC: 0x98, 0x2122: 0x99, 0x0161: 0x9A, 0x203A: 0x9B, 0x0153: 0x9C,
+ 0x017E: 0x9E, 0x0178: 0x9F
+ };
+
+ // Adobe core AFM advance widths (1/1000 em), codes 32-255 in WinAnsi order.
+ // Zeros mark codes with no WinAnsi glyph; encode() never emits those.
+ 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");
+ 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");
+
+ function parseWidths(s) {
+ return s.split(" ").map(Number);
+ }
+
+ // string -> array of WinAnsi byte values (unmappable code points become "?")
+ function encode(str) {
+ var bytes = [];
+ Array.from(String(str)).forEach(function (ch) {
+ var cp = ch.codePointAt(0);
+ if (cp >= 0x20 && cp <= 0x7E) { bytes.push(cp); return; }
+ if (cp >= 0xA0 && cp <= 0xFF) { bytes.push(cp); return; }
+ bytes.push(UNI2WIN[cp] || 0x3F);
+ });
+ return bytes;
+ }
+
+ function widthOf(str, font, size) {
+ var tbl = font === "timesBold" ? W_TIMES_BOLD : W_TIMES;
+ var w = 0;
+ encode(str).forEach(function (b) {
+ w += tbl[b - 32] || 500;
+ });
+ return w * size / 1000;
+ }
+
+ // PDF literal string body: escape delimiters, octal-escape non-ASCII bytes
+ // (always 3 digits so a following literal digit can't extend the escape)
+ function escapeBytes(bytes) {
+ return bytes.map(function (b) {
+ if (b === 0x28 || b === 0x29 || b === 0x5C) { return "\\" + String.fromCharCode(b); }
+ if (b < 32 || b > 126) { return "\\" + ("00" + b.toString(8)).slice(-3); }
+ return String.fromCharCode(b);
+ }).join("");
+ }
+
+ // compact decimal, never exponential
+ function num(n) {
+ var s = n.toFixed(3).replace(/\.?0+$/, "");
+ return s === "-0" ? "0" : s;
+ }
+
+ function create(widthPt, heightPt) {
+ var ops = [];
+ var lastGray = null;
+ var lastWordSp = 0;
+
+ // x,y position the baseline start. rotate90 renders the run reading down the page
+ // (baseline 0,-1; glyph-up 1,0). wordSpacing (PDF Tw) persists across BT/ET, so
+ // emit only on change.
+ function text(str, x, y, opts) {
+ var f = opts.font === "timesBold" ? "/F2" : "/F1";
+ var ws = opts.wordSpacing || 0;
+ var tw = ws !== lastWordSp ? num(ws) + " Tw " : "";
+ lastWordSp = ws;
+ var pos = opts.rotate90
+ ? "0 -1 1 0 " + num(x) + " " + num(y) + " Tm"
+ : num(x) + " " + num(y) + " Td";
+ ops.push("BT " + f + " " + num(opts.size) + " Tf " + tw + pos +
+ " (" + escapeBytes(encode(str)) + ") Tj ET");
+ lastGray = null; // BT/ET doesn't touch fill color, but stay conservative
+ }
+
+ // filled rect anchored at its bottom-left; gray 0 = black, 1 = white
+ function rect(x, y, w, h, gray) {
+ var g = gray || 0;
+ if (g !== lastGray) {
+ ops.push(num(g) + " g");
+ lastGray = g;
+ }
+ ops.push(num(x) + " " + num(y) + " " + num(w) + " " + num(h) + " re f");
+ }
+
+ function dashedLine(x1, y1, x2, y2, opts) {
+ ops.push("q [" + num(opts.dash[0]) + " " + num(opts.dash[1]) + "] 0 d " +
+ num(opts.width) + " w 0 G " +
+ num(x1) + " " + num(y1) + " m " + num(x2) + " " + num(y2) + " l S Q");
+ }
+
+ // assemble as a binary string (char codes <= 0xFF) so string offsets are byte
+ // offsets, then convert to a Uint8Array
+ function end() {
+ var stream = ops.join("\n");
+ var objects = [
+ "<< /Type /Catalog /Pages 2 0 R >>",
+ "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
+ "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 " + num(widthPt) + " " + num(heightPt) + "]" +
+ " /Resources << /Font << /F1 5 0 R /F2 6 0 R >> >> /Contents 4 0 R >>",
+ "<< /Length " + stream.length + " >>\nstream\n" + stream + "\nendstream",
+ "<< /Type /Font /Subtype /Type1 /BaseFont /Times-Roman /Encoding /WinAnsiEncoding >>",
+ "<< /Type /Font /Subtype /Type1 /BaseFont /Times-Bold /Encoding /WinAnsiEncoding >>"
+ ];
+ var out = "%PDF-1.4\n%\xE2\xE3\xCF\xD3\n";
+ var offsets = [];
+ objects.forEach(function (body, i) {
+ offsets.push(out.length);
+ out += (i + 1) + " 0 obj\n" + body + "\nendobj\n";
+ });
+ var xref = out.length;
+ out += "xref\n0 " + (objects.length + 1) + "\n0000000000 65535 f \n";
+ offsets.forEach(function (off) {
+ out += ("000000000" + off).slice(-10) + " 00000 n \n";
+ });
+ out += "trailer\n<< /Size " + (objects.length + 1) + " /Root 1 0 R >>\n" +
+ "startxref\n" + xref + "\n%%EOF\n";
+ var bytes = new Uint8Array(out.length);
+ for (var i = 0; i < out.length; i++) { bytes[i] = out.charCodeAt(i); }
+ return bytes;
+ }
+
+ return { text: text, rect: rect, dashedLine: dashedLine, end: end };
+ }
+
+ var api = { create: create, widthOf: widthOf };
+ root.pdf = api;
+ if (typeof module !== "undefined" && module.exports) { module.exports = api; }
+})(typeof window !== "undefined" ? window : globalThis);
diff --git a/resources/qr.js b/resources/qr.js
@@ -0,0 +1,561 @@
+// Self-contained QR encoder (byte mode, EC level M, versions 1-40).
+// createMatrix(text) returns a square matrix[row][col] of 0/1 modules
+
+(function () {
+ // Galois Field tables for Reed-Solomon
+ var GF_EXP = new Array(512);
+ var GF_LOG = new Array(256);
+
+ (function initGF() {
+ var x = 1;
+ for (var i = 0; i < 255; i++) {
+ GF_EXP[i] = x;
+ GF_LOG[x] = i;
+ x <<= 1;
+ if (x & 0x100) x ^= 0x11d;
+ }
+ for (var j = 255; j < 512; j++) {
+ GF_EXP[j] = GF_EXP[j - 255];
+ }
+ })();
+
+ function gfMul(a, b) {
+ if (a === 0 || b === 0) return 0;
+ return GF_EXP[GF_LOG[a] + GF_LOG[b]];
+ }
+
+ // Alignment pattern positions lookup table (from QR spec)
+ var ALIGNMENT_POSITIONS = [
+ null,
+ [],
+ [6, 18],
+ [6, 22],
+ [6, 26],
+ [6, 30],
+ [6, 34],
+ [6, 22, 38],
+ [6, 24, 42],
+ [6, 26, 46],
+ [6, 28, 50],
+ [6, 30, 54],
+ [6, 32, 58],
+ [6, 34, 62],
+ [6, 26, 46, 66],
+ [6, 26, 48, 70],
+ [6, 26, 50, 74],
+ [6, 30, 54, 78],
+ [6, 30, 56, 82],
+ [6, 30, 58, 86],
+ [6, 34, 62, 90],
+ [6, 28, 50, 72, 94],
+ [6, 26, 50, 74, 98],
+ [6, 30, 54, 78, 102],
+ [6, 28, 54, 80, 106],
+ [6, 32, 58, 84, 110],
+ [6, 30, 58, 86, 114],
+ [6, 34, 62, 90, 118],
+ [6, 26, 50, 74, 98, 122],
+ [6, 30, 54, 78, 102, 126],
+ [6, 26, 52, 78, 104, 130],
+ [6, 30, 56, 82, 108, 134],
+ [6, 34, 60, 86, 112, 138],
+ [6, 30, 58, 86, 114, 142],
+ [6, 34, 62, 90, 118, 146],
+ [6, 30, 54, 78, 102, 126, 150],
+ [6, 24, 50, 76, 102, 128, 154],
+ [6, 28, 54, 80, 106, 132, 158],
+ [6, 32, 58, 84, 110, 136, 162],
+ [6, 26, 54, 82, 110, 138, 166],
+ [6, 30, 58, 86, 114, 142, 170]
+ ];
+
+ // EC_TABLE: [totalDataCodewords, ecPerBlock, group1Blocks, group1Data, group2Blocks, group2Data]
+ var EC_TABLE = [
+ null,
+ [16, 10, 1, 16, 0, 0], // V1
+ [28, 16, 1, 28, 0, 0], // V2
+ [44, 26, 1, 44, 0, 0], // V3
+ [64, 18, 2, 32, 0, 0], // V4
+ [86, 24, 2, 43, 0, 0], // V5
+ [108, 16, 4, 27, 0, 0], // V6
+ [124, 18, 4, 31, 0, 0], // V7
+ [154, 22, 2, 38, 2, 39], // V8
+ [182, 22, 3, 36, 2, 37], // V9
+ [216, 26, 4, 43, 1, 44], // V10
+ [254, 30, 1, 50, 4, 51], // V11
+ [290, 22, 6, 36, 2, 37], // V12
+ [334, 22, 8, 37, 1, 38], // V13
+ [365, 24, 4, 40, 5, 41], // V14
+ [415, 24, 5, 41, 5, 42], // V15
+ [453, 28, 7, 45, 3, 46], // V16
+ [507, 28, 10, 46, 1, 47], // V17
+ [563, 26, 9, 43, 4, 44], // V18
+ [627, 26, 3, 44, 11, 45], // V19
+ [669, 26, 3, 41, 13, 42], // V20
+ [714, 26, 17, 42, 0, 0], // V21
+ [782, 28, 17, 46, 0, 0], // V22
+ [860, 28, 4, 47, 14, 48], // V23
+ [914, 28, 6, 45, 14, 46], // V24
+ [1000, 28, 8, 47, 13, 48], // V25
+ [1062, 28, 19, 46, 4, 47], // V26
+ [1128, 28, 22, 45, 3, 46], // V27
+ [1193, 28, 3, 45, 23, 46], // V28
+ [1267, 28, 21, 45, 7, 46], // V29
+ [1373, 28, 19, 47, 10, 48],// V30
+ [1455, 28, 2, 46, 29, 47], // V31
+ [1541, 28, 10, 46, 23, 47],// V32
+ [1631, 28, 14, 46, 21, 47],// V33
+ [1725, 28, 14, 46, 23, 47],// V34
+ [1812, 28, 12, 47, 26, 48],// V35
+ [1914, 28, 6, 47, 34, 48], // V36
+ [1992, 28, 29, 46, 14, 47],// V37
+ [2102, 28, 13, 46, 32, 47],// V38
+ [2216, 28, 40, 47, 7, 48], // V39
+ [2334, 28, 18, 47, 31, 48] // V40
+ ];
+
+ function getVersion(byteLength) {
+ for (var v = 1; v <= 40; v++) {
+ var charCountBits = v <= 9 ? 8 : 16;
+ var dataBits = 4 + charCountBits + byteLength * 8;
+ var dataBytes = Math.ceil(dataBits / 8);
+ if (dataBytes <= EC_TABLE[v][0]) return v;
+ }
+ return 40;
+ }
+
+ function createQR(text) {
+ var bytes = new TextEncoder().encode(text);
+ var version = getVersion(bytes.length);
+ var size = version * 4 + 17;
+ var matrix = Array.from({ length: size }, function () { return Array(size).fill(null); });
+
+ addFinderPatterns(matrix, size);
+ addSeparators(matrix, size);
+ addTimingPatterns(matrix, size);
+ addAlignmentPatterns(matrix, version, size);
+ addDarkModule(matrix, version);
+
+ reserveFormatAreas(matrix, size);
+ if (version >= 7) reserveVersionAreas(matrix, size);
+
+ var reserved = createReservedMap(matrix, size);
+
+ var data = encodeData(bytes, version);
+ var ecData = addErrorCorrection(data, version);
+ placeData(matrix, ecData, size);
+
+ var mask = applyBestMask(matrix, size, reserved);
+ addFormatInfo(matrix, size, mask);
+ if (version >= 7) addVersionInfo(matrix, version);
+
+ return matrix;
+ }
+
+ // Finder pattern: 7x7 with specific structure
+ // Outer black border, inner white border, center 3x3 black
+ function addFinderPatterns(matrix, size) {
+ var positions = [
+ [0, 0], // top-left
+ [0, size - 7], // top-right
+ [size - 7, 0] // bottom-left
+ ];
+
+ for (var p = 0; p < positions.length; p++) {
+ var startRow = positions[p][0], startCol = positions[p][1];
+ for (var r = 0; r < 7; r++) {
+ for (var c = 0; c < 7; c++) {
+ var isBlack;
+ if (r === 0 || r === 6 || c === 0 || c === 6) {
+ isBlack = true; // outer border
+ } else if (r === 1 || r === 5 || c === 1 || c === 5) {
+ isBlack = false; // inner white border
+ } else {
+ isBlack = true; // center 3x3
+ }
+ matrix[startRow + r][startCol + c] = isBlack ? 1 : 0;
+ }
+ }
+ }
+ }
+
+ // Separators: 1-module white border around finder patterns
+ function addSeparators(matrix, size) {
+ for (var i = 0; i < 8; i++) {
+ matrix[7][i] = 0;
+ matrix[i][7] = 0;
+ }
+ for (var j = 0; j < 8; j++) {
+ matrix[7][size - 8 + j] = 0;
+ matrix[j][size - 8] = 0;
+ }
+ for (var k = 0; k < 8; k++) {
+ matrix[size - 8][k] = 0;
+ matrix[size - 8 + k][7] = 0;
+ }
+ }
+
+ // Timing patterns: alternating modules on row 6 and column 6
+ function addTimingPatterns(matrix, size) {
+ for (var i = 0; i < size; i++) {
+ var bit = i % 2 === 0 ? 1 : 0;
+ if (matrix[6][i] === null) matrix[6][i] = bit;
+ if (matrix[i][6] === null) matrix[i][6] = bit;
+ }
+ }
+
+ // Alignment patterns: 5x5 with black border, white inner, black center
+ function addAlignmentPatterns(matrix, version, size) {
+ if (version < 2) return;
+
+ var positions = ALIGNMENT_POSITIONS[version];
+
+ for (var ri = 0; ri < positions.length; ri++) {
+ for (var ci = 0; ci < positions.length; ci++) {
+ var row = positions[ri], col = positions[ci];
+ if (isInFinderPattern(row, col, size)) continue; // skip finder overlap
+
+ for (var r = -2; r <= 2; r++) {
+ for (var c = -2; c <= 2; c++) {
+ var isBlack;
+ if (Math.abs(r) === 2 || Math.abs(c) === 2) {
+ isBlack = true; // outer border
+ } else if (r === 0 && c === 0) {
+ isBlack = true; // center
+ } else {
+ isBlack = false; // inner white ring
+ }
+ matrix[row + r][col + c] = isBlack ? 1 : 0;
+ }
+ }
+ }
+ }
+ }
+
+ function isInFinderPattern(row, col, size) {
+ if (row <= 7 && col <= 7) return true;
+ if (row <= 7 && col >= size - 8) return true;
+ if (row >= size - 8 && col <= 7) return true;
+ return false;
+ }
+
+ // Dark module: always at matrix[4*version+9][8] per spec
+ function addDarkModule(matrix, version) {
+ matrix[4 * version + 9][8] = 1;
+ }
+
+ // Reserve format info areas (filled in later)
+ function reserveFormatAreas(matrix, size) {
+ for (var i = 0; i < 9; i++) {
+ if (matrix[8][i] === null) matrix[8][i] = 0;
+ if (matrix[i][8] === null) matrix[i][8] = 0;
+ }
+ for (var j = 0; j < 8; j++) {
+ if (matrix[8][size - 1 - j] === null) matrix[8][size - 1 - j] = 0;
+ }
+ for (var k = 0; k < 7; k++) {
+ if (matrix[size - 1 - k][8] === null) matrix[size - 1 - k][8] = 0;
+ }
+ }
+
+ // Reserve version info areas (version 7+)
+ function reserveVersionAreas(matrix, size) {
+ for (var i = 0; i < 6; i++) {
+ for (var j = 0; j < 3; j++) {
+ matrix[i][size - 11 + j] = 0;
+ }
+ }
+ for (var k = 0; k < 6; k++) {
+ for (var m = 0; m < 3; m++) {
+ matrix[size - 11 + m][k] = 0;
+ }
+ }
+ }
+
+ function createReservedMap(matrix, size) {
+ var reserved = Array.from({ length: size }, function () { return Array(size).fill(false); });
+
+ for (var row = 0; row < size; row++) {
+ for (var col = 0; col < size; col++) {
+ if (matrix[row][col] !== null) {
+ reserved[row][col] = true;
+ }
+ }
+ }
+
+ return reserved;
+ }
+
+ function encodeData(bytes, version) {
+ var bits = [];
+ var charCountBits = version <= 9 ? 8 : 16;
+
+ bits.push(0, 1, 0, 0); // mode: byte
+
+ for (var i = charCountBits - 1; i >= 0; i--) {
+ bits.push((bytes.length >> i) & 1); // character count
+ }
+
+ for (var b = 0; b < bytes.length; b++) {
+ for (var k = 7; k >= 0; k--) {
+ bits.push((bytes[b] >> k) & 1);
+ }
+ }
+
+ // terminator (up to 4 zeros)
+ var capacity = EC_TABLE[version][0] * 8;
+ for (var t = 0; t < 4 && bits.length < capacity; t++) {
+ bits.push(0);
+ }
+
+ while (bits.length % 8 !== 0 && bits.length < capacity) {
+ bits.push(0); // pad to byte boundary
+ }
+
+ // pad codewords
+ var padBytes = [0xEC, 0x11];
+ var padIndex = 0;
+ while (bits.length < capacity) {
+ var pad = padBytes[padIndex++ % 2];
+ for (var pb = 7; pb >= 0; pb--) {
+ bits.push((pad >> pb) & 1);
+ }
+ }
+
+ return bits;
+ }
+
+ function addErrorCorrection(data, version) {
+ var row = EC_TABLE[version];
+ var totalCap = row[0], ecPerBlock = row[1];
+ var g1Count = row[2], g1Data = row[3], g2Count = row[4], g2Data = row[5];
+
+ var dataBytes = [];
+ for (var i = 0; i < data.length; i += 8) {
+ var byte = 0;
+ for (var j = 0; j < 8; j++) byte = (byte << 1) | (data[i + j] || 0);
+ dataBytes.push(byte);
+ }
+
+ while (dataBytes.length < totalCap) {
+ dataBytes.push(dataBytes.length % 2 === 0 ? 0xEC : 0x11); // pad to capacity
+ }
+
+ var blocks = [];
+ var ecBlocks = [];
+ var offset = 0;
+
+ for (var a = 0; a < g1Count; a++) {
+ var b1 = dataBytes.slice(offset, offset + g1Data);
+ blocks.push(b1);
+ ecBlocks.push(generateECBytes(b1, ecPerBlock));
+ offset += g1Data;
+ }
+
+ for (var c = 0; c < g2Count; c++) {
+ var b2 = dataBytes.slice(offset, offset + g2Data);
+ blocks.push(b2);
+ ecBlocks.push(generateECBytes(b2, ecPerBlock));
+ offset += g2Data;
+ }
+
+ var resultBytes = [];
+ var maxDataSize = Math.max(g1Data, g2Data);
+
+ // interleave data byte-by-byte across all blocks
+ for (var d = 0; d < maxDataSize; d++) {
+ for (var e = 0; e < blocks.length; e++) {
+ if (d < blocks[e].length) {
+ resultBytes.push(blocks[e][d]);
+ }
+ }
+ }
+
+ // interleave EC byte-by-byte (EC sizes are always equal)
+ for (var f = 0; f < ecPerBlock; f++) {
+ for (var g = 0; g < ecBlocks.length; g++) {
+ resultBytes.push(ecBlocks[g][f]);
+ }
+ }
+
+ var resultBits = [];
+ for (var rb = 0; rb < resultBytes.length; rb++) {
+ for (var h = 7; h >= 0; h--) resultBits.push((resultBytes[rb] >> h) & 1);
+ }
+ return resultBits;
+ }
+
+ function generateECBytes(data, ecCount) {
+ var gen = [1];
+ for (var i = 0; i < ecCount; i++) {
+ var next = new Array(gen.length + 1).fill(0);
+ for (var j = 0; j < gen.length; j++) {
+ next[j] ^= gen[j];
+ next[j + 1] ^= gfMul(gen[j], GF_EXP[i]);
+ }
+ for (var k = 0; k < next.length; k++) gen[k] = next[k];
+ gen.length = next.length;
+ }
+
+ var remainder = new Array(ecCount).fill(0);
+ for (var d = 0; d < data.length; d++) {
+ var factor = data[d] ^ remainder[0];
+ remainder.shift();
+ remainder.push(0);
+ for (var m = 0; m < ecCount; m++) {
+ remainder[m] ^= gfMul(gen[m + 1], factor);
+ }
+ }
+
+ return remainder;
+ }
+
+ // Place data in zigzag pattern from bottom-right, skipping column 6
+ function placeData(matrix, data, size) {
+ var bitIndex = 0;
+ var up = true;
+
+ for (var col = size - 1; col >= 1; col -= 2) {
+ if (col === 6) col = 5; // skip timing column
+
+ for (var i = 0; i < size; i++) {
+ var row = up ? size - 1 - i : i;
+
+ for (var j = 0; j < 2; j++) {
+ var c = col - j;
+ if (matrix[row][c] === null) {
+ matrix[row][c] = bitIndex < data.length ? data[bitIndex++] : 0;
+ }
+ }
+ }
+ up = !up;
+ }
+ }
+
+ function applyBestMask(matrix, size, reserved) {
+ var bestMask = 0;
+ var bestPenalty = Infinity;
+
+ for (var mask = 0; mask < 8; mask++) {
+ var copy = matrix.map(function (r) { return r.slice(); });
+ applyMask(copy, size, mask, reserved);
+ var penalty = calculatePenalty(copy, size);
+
+ if (penalty < bestPenalty) {
+ bestPenalty = penalty;
+ bestMask = mask;
+ }
+ }
+
+ applyMask(matrix, size, bestMask, reserved);
+ return bestMask;
+ }
+
+ function applyMask(matrix, size, mask, reserved) {
+ for (var row = 0; row < size; row++) {
+ for (var col = 0; col < size; col++) {
+ if (reserved[row][col]) continue;
+
+ var invert = false;
+ switch (mask) {
+ case 0: invert = (row + col) % 2 === 0; break;
+ case 1: invert = row % 2 === 0; break;
+ case 2: invert = col % 3 === 0; break;
+ case 3: invert = (row + col) % 3 === 0; break;
+ case 4: invert = (Math.floor(row / 2) + Math.floor(col / 3)) % 2 === 0; break;
+ case 5: invert = (row * col) % 2 + (row * col) % 3 === 0; break;
+ case 6: invert = ((row * col) % 2 + (row * col) % 3) % 2 === 0; break;
+ case 7: invert = ((row + col) % 2 + (row * col) % 3) % 2 === 0; break;
+ }
+
+ if (invert) matrix[row][col] ^= 1;
+ }
+ }
+ }
+
+ function calculatePenalty(matrix, size) {
+ var penalty = 0;
+
+ // rule 1: 5+ consecutive same-color modules
+ for (var i = 0; i < size; i++) {
+ var rowRun = 1, colRun = 1;
+ for (var j = 1; j < size; j++) {
+ rowRun = matrix[i][j] === matrix[i][j - 1] ? rowRun + 1 : 1;
+ if (rowRun === 5) penalty += 3;
+ else if (rowRun > 5) penalty += 1;
+
+ colRun = matrix[j][i] === matrix[j - 1][i] ? colRun + 1 : 1;
+ if (colRun === 5) penalty += 3;
+ else if (colRun > 5) penalty += 1;
+ }
+ }
+
+ // rule 2: 2x2 blocks of same color
+ for (var r = 0; r < size - 1; r++) {
+ for (var c = 0; c < size - 1; c++) {
+ var v = matrix[r][c];
+ if (v === matrix[r][c + 1] && v === matrix[r + 1][c] && v === matrix[r + 1][c + 1]) {
+ penalty += 3;
+ }
+ }
+ }
+
+ return penalty;
+ }
+
+ function addFormatInfo(matrix, size, mask) {
+ var format = (0 << 3) | mask; // Level M (00) + Mask
+ var rem = format;
+ for (var i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
+ var bits = ((format << 10) | rem) ^ 0x5412;
+
+ function getBit(i) { return (bits >> i) & 1; }
+
+ var coords = [
+ [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]
+ ];
+
+ for (var c = 0; c < 15; c++) {
+ var r = coords[c][0], col = coords[c][1];
+ var bit = getBit(c);
+ matrix[r][col] = bit; // first copy
+ // second copy, mirrored around the other finders
+ if (c < 8) {
+ matrix[8][size - 1 - c] = bit;
+ } else {
+ matrix[size - 1 - (14 - c)][8] = bit;
+ }
+ }
+ }
+
+ function addVersionInfo(matrix, version) {
+ if (version < 7) return;
+
+ // 18-bit BCH Error Corrected Version Codes
+ var versionBits = {
+ 7: 0x07C94, 8: 0x085BC, 9: 0x09A99, 10: 0x0A4D3,
+ 11: 0x0BBF6, 12: 0x0C762, 13: 0x0D847, 14: 0x0E60D,
+ 15: 0x0F928, 16: 0x10B78, 17: 0x1145D, 18: 0x12A17,
+ 19: 0x13532, 20: 0x149A6, 21: 0x15683, 22: 0x168C9,
+ 23: 0x177EC, 24: 0x18EC4, 25: 0x191E1, 26: 0x1AFAB,
+ 27: 0x1B08E, 28: 0x1CC1A, 29: 0x1D33F, 30: 0x1ED75,
+ 31: 0x1F250, 32: 0x209D5, 33: 0x216F0, 34: 0x228BA,
+ 35: 0x2379F, 36: 0x24B0B, 37: 0x2542E, 38: 0x26A64,
+ 39: 0x27541, 40: 0x28C69
+ };
+
+ var bits = versionBits[version];
+ var size = matrix.length;
+
+ for (var i = 0; i < 18; i++) {
+ var bit = (bits >> i) & 1;
+ var a = Math.floor(i / 3);
+ var b = i % 3;
+
+ matrix[size - 11 + b][a] = bit; // bottom-left block
+ matrix[a][size - 11 + b] = bit; // top-right block
+ }
+ }
+
+ window.qr = { createMatrix: createQR };
+})();
diff --git a/resources/styles.css b/resources/styles.css
@@ -0,0 +1,553 @@
+:root {
+ --color-shift-ms: 350ms;
+ --type-char-ms: 0.5ms;
+ --event-color: antiquewhite;
+ --link-pad: 6px;
+}
+
+html, body {
+ margin: 0;
+ height: 100%;
+ overflow: hidden;
+ overscroll-behavior: none;
+ background-color: var(--event-color);
+ transition: background-color var(--color-shift-ms) ease-in-out;
+ font-family: serif;
+}
+
+::selection {
+ background: antiquewhite;
+}
+
+#boxes {
+ display: flex;
+ flex-direction: row;
+ height: 100vh;
+ height: 100dvh;
+}
+#left-box {
+ width: 50%;
+ height: 100%;
+ box-sizing: border-box;
+ position: relative;
+ overflow: hidden;
+ -webkit-user-select: none;
+ user-select: none;
+}
+#right-box {
+ width: 50%;
+ height: 100%;
+ box-sizing: border-box;
+ container: text-box / inline-size;
+ padding: 22px;
+ padding-top: 14px;
+ margin: 0;
+ color: black;
+ overflow-y: scroll;
+ overscroll-behavior: contain;
+ scrollbar-color: #00000060 transparent;
+ background-color: var(--event-color);
+ transition: background-color var(--color-shift-ms) ease-in-out;
+}
+
+#right-box ::selection {
+ background: black;
+ color: var(--event-color);
+}
+#right-box h1 {
+ margin: 0;
+ font-weight: normal;
+ font-size: 38px;
+ margin-bottom: -3px;
+ min-height: 44px;
+}
+#right-box p, #text-content {
+ font-size: 22px;
+ line-height: 29px;
+ margin-top: 16px;
+}
+#text-content > p {
+ margin: 0;
+}
+.detail-list {
+ margin: 16px 0 0;
+ padding-left: 1.2em;
+ list-style-position: outside;
+}
+.detail-list li {
+ margin-top: 2px;
+ list-style: disc;
+}
+.detail-list li::marker {
+ color: transparent;
+}
+.detail-list li.li-typing::marker {
+ color: currentColor;
+}
+
+.intro-list {
+ display: grid;
+ grid-template-columns: minmax(0, max-content) minmax(0, 1fr) max-content minmax(0, 1fr) max-content;
+ column-gap: 16px;
+ row-gap: 2px;
+ font-size: 22px;
+ line-height: 29px;
+}
+.intro-row {
+ display: grid;
+ grid-column: 1 / -1;
+ grid-template-columns: subgrid;
+ align-items: baseline;
+ padding: var(--link-pad);
+ margin: 0 calc(-1 * var(--link-pad));
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+}
+.intro-name {
+ grid-column: 1;
+ min-width: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+.intro-when {
+ grid-column: 3;
+ white-space: nowrap;
+}
+.intro-where {
+ grid-column: 5;
+ min-width: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+@media (hover: hover) {
+ .intro-row:hover {
+ background: black;
+ color: antiquewhite;
+ }
+ .intro-row:hover .intro-name {
+ text-decoration: none;
+ }
+}
+
+/* panel.js drops columns by class when the title would otherwise ellipsize: date first
+ (.cols-2), then location (.cols-1). Title is always shown. */
+.intro-list.cols-2 {
+ grid-template-columns: minmax(0, max-content) minmax(0, 1fr) max-content;
+}
+.intro-list.cols-2 .intro-when {
+ display: none;
+}
+.intro-list.cols-2 .intro-where {
+ grid-column: 3;
+}
+.intro-list.cols-1 {
+ grid-template-columns: minmax(0, 1fr);
+}
+.intro-list.cols-1 .intro-when,
+.intro-list.cols-1 .intro-where {
+ display: none;
+}
+
+#text-content, #text-title, #action-wrap {
+ transition: opacity 0.125s ease-in-out;
+}
+.type-hidden {
+ visibility: hidden;
+}
+#text-content.fading, #text-title.fading, #action-wrap.fading {
+ opacity: 0;
+}
+
+#action-wrap {
+ margin-top: 25px;
+ margin-bottom: 50px;
+ text-align: center;
+}
+.action-row {
+ font-size: 22px;
+ line-height: 29px;
+}
+.action-row + .action-row {
+ margin-top: 18px;
+}
+.action-link {
+ display: inline-block;
+ color: #000000;
+ text-decoration: underline;
+ text-underline-offset: 3px;
+ padding: var(--link-pad);
+ margin: calc(-1 * var(--link-pad));
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+}
+.intro-row, .action-link {
+ position: relative;
+}
+.intro-row::before, .intro-row::after,
+.action-link::before, .action-link::after {
+ content: "";
+ position: absolute;
+ left: 0;
+ right: 0;
+ height: 3px;
+}
+.intro-row::before, .action-link::before {
+ top: -3px;
+}
+.intro-row::after, .action-link::after {
+ bottom: -3px;
+}
+@media (hover: hover) {
+ a.action-link:hover {
+ background: black;
+ color: var(--event-color);
+ text-decoration: none;
+ }
+}
+
+.action-typing {
+ position: relative;
+ display: inline-block;
+ text-align: left;
+ white-space: pre;
+}
+.action-sizer {
+ visibility: hidden;
+}
+.action-shown {
+ position: absolute;
+ left: var(--link-pad);
+ top: var(--link-pad);
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+
+#map-stage {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform-origin: center center;
+ will-change: transform;
+}
+#map-stage:not([style*="width"]) {
+ inset: 0;
+ top: 0;
+ left: 0;
+}
+
+#map-container {
+ height: 100%;
+}
+
+#map-container .leaflet-tile,
+#map-container .leaflet-tile-container,
+#map-container canvas.leaflet-tile-container {
+ pointer-events: none;
+}
+
+#map-container .leaflet-zoom-anim .leaflet-zoom-animated {
+ transition: transform 0.12s cubic-bezier(0, 0, 0.25, 1);
+}
+
+#map-container.map-grabbing,
+#map-container.map-grabbing .leaflet-grab,
+#map-container.map-grabbing .leaflet-interactive {
+ cursor: grabbing !important;
+}
+
+#compass {
+ display: none;
+ position: absolute;
+ right: 12px;
+ bottom: 12px;
+ z-index: 1000;
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.7);
+ -webkit-backdrop-filter: blur(8px);
+ backdrop-filter: blur(8px);
+ -webkit-tap-highlight-color: transparent;
+ box-shadow: 0 1px 3px #00000040;
+ transition: box-shadow 0.12s linear;
+ align-items: center;
+ justify-content: center;
+ pointer-events: none;
+}
+#compass.compass-enabled {
+ display: flex;
+}
+@media (pointer: coarse) {
+ #compass::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: -12px;
+ bottom: -12px;
+ border-radius: 24px 0 0 0;
+ }
+}
+#compass svg {
+ display: block;
+ transform-origin: center center;
+ transform: rotate(var(--compass-angle, 0deg));
+}
+#compass .needle-n { fill: #e8483a; }
+#compass .needle-s { fill: #b8bcc2; }
+#compass .needle-hub { fill: #fafafa; stroke: #b8bcc2; stroke-width: 1; }
+#compass.active {
+ box-shadow: 0 1px 3px #00000040, inset 0 0 0 2.5px cornflowerblue;
+}
+#compass.active.paused {
+ box-shadow: 0 1px 3px #00000040, inset 0 0 0 2.5px #b8bcc2;
+}
+#compass .compass-ring {
+ position: absolute;
+ inset: 0;
+ transform: rotate(-90deg);
+ pointer-events: none;
+}
+#compass .ring-arc {
+ fill: none;
+ stroke: cornflowerblue;
+ stroke-width: 2.5;
+ stroke-dasharray: 100;
+ stroke-dashoffset: 100;
+}
+
+.ladybug-line-svg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 1px;
+ height: 1px;
+ pointer-events: none;
+ overflow: visible;
+ display: none;
+}
+.ladybug-line-svg.visible {
+ display: block;
+}
+.ladybug-line {
+ stroke-width: 4;
+ stroke-linecap: round;
+ fill: none;
+}
+
+.ladybug-pin {
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 30px;
+ line-height: 1;
+ transform: rotate(var(--pin-counter, 0deg));
+ transform-origin: center center;
+ opacity: 0;
+ transition: opacity 0.4s ease;
+}
+.ladybug-pin.visible {
+ opacity: 1;
+}
+
+.map-error {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ padding: 22px;
+ background: #f0eee4;
+ color: black;
+ font-weight: normal;
+ font-size: 38px;
+ text-align: center;
+ -webkit-user-select: none;
+ user-select: none;
+}
+
+.event-pin {
+ width: 48px;
+ height: 48px;
+ box-sizing: border-box;
+ padding: 4px 6px;
+ transform: rotate(var(--pin-counter, 0deg));
+ transform-origin: center center;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ font-family: -apple-system, Arial, Helvetica, sans-serif;
+ line-height: 1.1;
+ color: black;
+ text-align: center;
+ box-shadow: 0 1px 3px #00000040;
+ cursor: pointer;
+ border-radius: 0;
+ transition: border-radius 150ms ease-in-out;
+}
+.event-pin.active {
+ border-radius: 50%;
+}
+.event-pin .pin-month {
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+.event-pin .pin-day {
+ font-size: 18px;
+ font-weight: bold;
+}
+
+@media (orientation: portrait) {
+ #boxes {
+ flex-direction: column;
+ }
+ #left-box, #right-box {
+ width: 100%;
+ height: 50%;
+ }
+}
+
+#flyer {
+ display: none;
+}
+
+#flyer.flyer-measuring {
+ display: flex;
+ flex-direction: column;
+ position: fixed;
+ left: -9999px;
+ top: 0;
+ width: 197.3mm;
+ height: 266.7mm;
+ box-sizing: border-box;
+ font-family: serif;
+ color: #000;
+ background: #fff;
+}
+
+.flyer-body {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ overflow: hidden;
+}
+.flyer-title {
+ margin: 1.5mm 0 7mm;
+ font-weight: normal;
+ font-size: 48pt;
+ line-height: 1.05;
+ overflow-wrap: break-word;
+}
+.flyer-flow {
+ flex: 1 1 auto;
+ min-height: 0;
+ display: flow-root;
+}
+.flyer-desc {
+ margin: 0;
+ text-align: justify;
+}
+.flyer-details {
+ margin: 7mm 0;
+ padding-left: 1.2em;
+ list-style: disc;
+}
+.flyer-details li {
+ margin-top: 0.15em;
+}
+
+.flyer-flow > .flyer-qr {
+ float: right;
+ margin-left: 7mm;
+ margin-bottom: 7mm;
+ margin-top: 2mm;
+}
+
+.flyer-tabs {
+ flex: 0 0 auto;
+ display: flex;
+ height: 78mm;
+ border-top: 1px dashed #000;
+}
+.flyer-tab {
+ flex: 1 1 0;
+ position: relative;
+ overflow: hidden;
+ border-left: 1px dashed #000;
+}
+.flyer-tab:last-child {
+ border-right: 1px dashed #000;
+}
+.flyer-tab-inner {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 78mm;
+ height: 19.73mm;
+ transform: translate(-50%, -50%) rotate(90deg);
+ transform-origin: center center;
+ box-sizing: border-box;
+ padding: 2.5mm 2.5mm 2.5mm 1.5mm;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ gap: 2.5mm;
+ overflow: hidden;
+ font-size: 7pt;
+}
+.flyer-tab-text {
+ flex: 1 1 auto;
+ min-width: 0;
+ text-align: left;
+}
+.flyer-tab-title {
+ font-size: 1.3em;
+ font-weight: bold;
+ line-height: 1.1;
+ overflow-wrap: break-word;
+}
+.flyer-tab-when,
+.flyer-tab-where {
+ margin-top: 0.1em;
+ line-height: 1.15;
+}
+.flyer-tab .flyer-qr {
+ flex: 0 0 auto;
+}
+
+@page {
+ size: auto;
+ margin: 0;
+}
+
+@media print {
+ .flyer-print * {
+ -webkit-print-color-adjust: exact !important;
+ print-color-adjust: exact !important;
+ }
+ html.flyer-print, .flyer-print body {
+ background: #fff !important;
+ }
+ .flyer-print #boxes {
+ display: none !important;
+ }
+ .flyer-print #flyer {
+ display: flex !important;
+ flex-direction: column;
+ width: 197.3mm;
+ height: 266.7mm;
+ margin: 6.35mm auto;
+ box-sizing: border-box;
+ font-family: serif;
+ color: #000;
+ background: #fff;
+ }
+}
diff --git a/serve.py b/serve.py
@@ -0,0 +1,227 @@
+#!/usr/bin/env python3
+"""
+Starts HTTP server for local testing
+Automatically manages a virtual environment for dependencies
+"""
+
+import http.server
+import socketserver
+import socket
+import sys
+import os
+import subprocess
+from pathlib import Path
+
+DEFAULT_PORT = 8000
+SCRIPT_DIR = Path(__file__).parent.absolute()
+VENV_DIR = SCRIPT_DIR / "venv"
+
+
+def setup_venv():
+ """Create and setup virtual environment if it doesn't exist"""
+ # Determine the path to pip and python in the venv
+ if sys.platform == "win32":
+ pip_path = VENV_DIR / "Scripts" / "pip"
+ python_path = VENV_DIR / "Scripts" / "python"
+ else:
+ pip_path = VENV_DIR / "bin" / "pip"
+ python_path = VENV_DIR / "bin" / "python3"
+
+ # Check if venv needs to be created or recreated
+ if not VENV_DIR.exists() or not python_path.exists():
+ if VENV_DIR.exists():
+ print("Virtual environment incomplete, recreating...")
+ import shutil
+ shutil.rmtree(VENV_DIR)
+ else:
+ print("Creating virtual environment...")
+
+ try:
+ subprocess.check_call([sys.executable, "-m", "venv", str(VENV_DIR)])
+ print("Virtual environment created successfully.")
+ except subprocess.CalledProcessError as e:
+ print(f"Error creating virtual environment: {e}")
+ sys.exit(1)
+
+ # Ensure pip is available
+ if not pip_path.exists():
+ print("Installing pip in virtual environment...")
+ try:
+ subprocess.check_call([str(python_path), "-m", "ensurepip", "--upgrade"])
+ except subprocess.CalledProcessError as e:
+ print(f"Error ensuring pip: {e}")
+ sys.exit(1)
+
+ check = subprocess.run(
+ [str(python_path), "-c", "import qrcode"],
+ capture_output=True
+ )
+ if check.returncode != 0:
+ try:
+ subprocess.check_call([str(python_path), "-m", "pip", "install", "-q", "qrcode"])
+ except subprocess.CalledProcessError:
+ print("Note: Could not install qrcode (offline?). QR codes will be unavailable.\n")
+
+ return python_path
+
+
+def run_in_venv():
+ """Re-run this script in the virtual environment"""
+ python_path = setup_venv()
+
+ # Re-run this script with the venv Python
+ try:
+ subprocess.check_call([str(python_path), __file__, "--in-venv"])
+ except (KeyboardInterrupt, subprocess.CalledProcessError):
+ pass
+ sys.exit(0)
+
+
+def get_local_ip():
+ """Get the LAN IP other devices on the network can reach this machine at,
+ or None if it can't be determined."""
+ try:
+ # Connect to a public DNS server (doesn't actually send data) to learn
+ # which local interface/IP would be used to reach the network.
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ s.connect(("8.8.8.8", 80))
+ local_ip = s.getsockname()[0]
+ s.close()
+ return local_ip
+ except Exception:
+ return None
+
+
+def find_available_port(start_port=DEFAULT_PORT, max_attempts=20):
+ """Return the first bindable port at or after start_port. Uses SO_REUSEADDR
+ so a socket lingering in TIME_WAIT from a prior run doesn't block us."""
+ for port in range(start_port, start_port + max_attempts):
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ try:
+ s.bind(("", port))
+ return port
+ except OSError:
+ continue
+ finally:
+ s.close()
+ return None
+
+
+def print_qr_code(url, caption="Scan to connect:"):
+ """Generate and print a QR code using block characters."""
+ try:
+ import qrcode
+
+ qr = qrcode.QRCode(
+ version=1,
+ error_correction=qrcode.constants.ERROR_CORRECT_L,
+ box_size=1,
+ border=1,
+ )
+ qr.add_data(url)
+ qr.make(fit=True)
+
+ matrix = qr.get_matrix()
+
+ # Half-block chars pack two matrix rows per terminal line, keeping the
+ # code compact and roughly square.
+ print(f"\n{caption}\n")
+ for y in range(0, len(matrix), 2):
+ line = " "
+ for x in range(len(matrix[y])):
+ top = matrix[y][x]
+ bottom = matrix[y + 1][x] if y + 1 < len(matrix) else False
+ if top and bottom:
+ line += "█"
+ elif top:
+ line += "▀"
+ elif bottom:
+ line += "▄"
+ else:
+ line += " "
+ print(line)
+ except ImportError:
+ print(f"\n{caption}\n (QR unavailable; open this URL manually) {url}")
+ except Exception as e:
+ print(f"\nCould not generate QR code: {e}\n open manually: {url}")
+
+
+class QuietHandler(http.server.SimpleHTTPRequestHandler):
+ """SimpleHTTPRequestHandler that keeps connections alive, disables caching,
+ silences logging, and swallows broken-pipe noise. Shared by serve.py and
+ https_serve.py."""
+ # HTTP/1.1 keeps the connection alive across requests so loading the app's
+ # files reuses one connection instead of a fresh one per file. Requires a
+ # threaded server, or a held-open connection would block all others.
+ protocol_version = "HTTP/1.1"
+
+ def end_headers(self):
+ self.send_header("Cache-Control", "no-cache")
+ super().end_headers()
+
+ def log_message(self, format, *args):
+ pass
+
+ def handle(self):
+ try:
+ super().handle()
+ except (BrokenPipeError, ConnectionResetError):
+ # Browser cancelled the request (normal for media streaming/preloading)
+ pass
+
+
+def start_server():
+ """Start the HTTP server (runs after venv is set up)"""
+ # Change to script directory
+ os.chdir(SCRIPT_DIR)
+
+ # Find an available port
+ port = find_available_port(DEFAULT_PORT)
+
+ if port is None:
+ print(f"Error: Could not find an available port (tried {DEFAULT_PORT}-{DEFAULT_PORT + 19})")
+ sys.exit(1)
+
+ # Get local IP for network access
+ local_ip = get_local_ip()
+
+ try:
+ with socketserver.ThreadingTCPServer(("", port), QuietHandler) as httpd:
+ httpd.daemon_threads = True
+ local_url = f"http://localhost:{port}"
+
+ print(f"\nServer running on port {port}")
+ print(f"Local access: {local_url}")
+
+ if local_ip:
+ network_url = f"http://{local_ip}:{port}"
+ print(f"Network access: {network_url}")
+ print_qr_code(network_url)
+ else:
+ print("Network access: unavailable (could not determine LAN IP)")
+
+ print("\nPress Ctrl+C to stop the server")
+
+ # Serve forever
+ httpd.serve_forever()
+
+ except KeyboardInterrupt:
+ print("\n\nShutting down server...")
+ sys.exit(0)
+ except Exception as e:
+ print(f"\nError starting server: {e}")
+ sys.exit(1)
+
+
+def main():
+ """Main entry point"""
+ # Check if we're already running in venv
+ if "--in-venv" not in sys.argv:
+ run_in_venv()
+ else:
+ start_server()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/sw.js b/sw.js
@@ -0,0 +1,97 @@
+// Caches the app shell (so it works offline) and map tiles (so panning/zooming
+// reuses previously-viewed tiles instead of refetching). Leaflet is vendored in
+// the shell, so map tiles are the only remaining network dependency.
+
+const SHELL_CACHE = "app-shell-v8";
+const TILE_CACHE = "map-tiles-v1"; // CORS fetches so the cached responses aren't opaque
+const KEEP = new Set([SHELL_CACHE, TILE_CACHE]);
+
+// Every file the app needs to boot with no network.
+const SHELL = [
+ "./",
+ "index.html",
+ "manifest.json",
+ "resources/icon.png",
+ "resources/format.js",
+ "resources/qr.js",
+ "resources/pdf.js",
+ "resources/flyer.js",
+ "resources/panel.js",
+ "resources/map.js",
+ "resources/app.js",
+ "resources/nav.js",
+ "resources/styles.css",
+ "resources/leaflet/leaflet-nogap.js",
+ "resources/demo.json",
+ "resources/leaflet/leaflet.js",
+ "resources/leaflet/leaflet.css",
+];
+
+// Real (gitignored) data; overrides demo.json when present, but often absent. Cached
+// best-effort so a 404 can't fail the install — the app boots fine on demo.json alone.
+const OPTIONAL = ["resources/events.json"];
+
+self.addEventListener("install", (e) => {
+ self.skipWaiting();
+ // Atomic precache of the shell: any failure rejects, so the prior shell cache
+ // survives a bad install. waitUntil resolves only once the full shell is cached,
+ // so activate won't drop the old version until this one is complete.
+ e.waitUntil(caches.open(SHELL_CACHE).then((cache) =>
+ cache.addAll(SHELL).then(() =>
+ Promise.all(OPTIONAL.map((u) => cache.add(u).catch(() => {})))
+ )
+ ));
+});
+
+self.addEventListener("activate", (e) => e.waitUntil(
+ Promise.all([
+ self.clients.claim(),
+ // drop old cache versions (e.g. superseded shells/tiles)
+ caches.keys().then((keys) => Promise.all(
+ keys.filter((k) => !KEEP.has(k)).map((k) => caches.delete(k))
+ )),
+ ])
+));
+
+function isCacheableTile(url) {
+ return url.hostname.endsWith("basemaps.cartocdn.com"); // map tiles
+}
+
+// network-first for same-origin app files: fresh when online, cached when not,
+// so edits show up on reload but the app still boots offline.
+async function shellResponse(request) {
+ const cache = await caches.open(SHELL_CACHE);
+ try {
+ const resp = await fetch(request);
+ if (resp.ok) cache.put(request, resp.clone());
+ return resp;
+ } catch {
+ const hit = await cache.match(request) || await cache.match("index.html");
+ if (hit) return hit;
+ throw new Error("offline and not cached");
+ }
+}
+
+// cache-first for map tiles: serve a cached copy instantly, else fetch and store.
+async function tileResponse(url) {
+ const cache = await caches.open(TILE_CACHE);
+ const hit = await cache.match(url.href);
+ if (hit) return hit;
+ const resp = await fetch(url.href, { mode: "cors", credentials: "omit" });
+ if (resp.ok) cache.put(url.href, resp.clone());
+ return resp;
+}
+
+self.addEventListener("fetch", (event) => {
+ if (event.request.method !== "GET") return;
+ const url = new URL(event.request.url);
+
+ if (isCacheableTile(url)) {
+ event.respondWith(tileResponse(url));
+ return;
+ }
+ // same-origin navigations + assets -> app shell
+ if (url.origin === self.location.origin) {
+ event.respondWith(shellResponse(event.request));
+ }
+});