first push
|
@ -0,0 +1,660 @@
|
|||
### GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc.
|
||||
<http://fsf.org/>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
### Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are 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.
|
||||
|
||||
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.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing
|
||||
under this license.
|
||||
|
||||
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your
|
||||
version supports such interaction) an opportunity to receive the
|
||||
Corresponding Source of your version by providing access to the
|
||||
Corresponding Source from a network server at no charge, through some
|
||||
standard or customary means of facilitating copying of software. This
|
||||
Corresponding Source shall include the Corresponding Source for any
|
||||
work covered by version 3 of the GNU General Public License that is
|
||||
incorporated pursuant to the following paragraph.
|
||||
|
||||
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 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 work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
#### 14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper
|
||||
mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for
|
||||
the specific requirements.
|
||||
|
||||
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 AGPL, see <http://www.gnu.org/licenses/>.
|
|
@ -0,0 +1,20 @@
|
|||
# Meshviewer
|
||||
[![Build Status](https://img.shields.io/travis/com/ffrgb/meshviewer/develop.svg?style=flat-square)](https://travis-ci.com/ffrgb/meshviewer)
|
||||
[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/ffrgb/meshviewer/develop.svg?style=flat-square)](https://scrutinizer-ci.com/g/ffrgb/meshviewer/?branch=develop)
|
||||
[![License: AGPL v3](https://img.shields.io/github/license/ffrgb/meshviewer.svg?style=flat-square)](https://www.gnu.org/licenses/agpl-3.0)
|
||||
|
||||
Meshviewer is an online visualization app to represent nodes and links on a map for Freifunk open mesh network.
|
||||
|
||||
### Demo
|
||||
|
||||
Embedded: https://regensburg.freifunk.net/netz/karte/
|
||||
Standalone: https://regensburg.freifunk.net/meshviewer/
|
||||
|
||||
## Sponsoring / Supporting
|
||||
|
||||
- [BrowserStack](https://www.browserstack.com/) for providing an awesome testing service for hundreds of browsers
|
||||
- [Travis CI](https://travis-ci.com/) for building meshviewer on every push and pull request
|
||||
- [Scrutinizer CI](https://scrutinizer-ci.com/g/ffrgb/meshviewer/) for testing code quality on every push and pull request
|
||||
- [POEditor](https://poeditor.com/join/project/VZBjPNNic9) for providing an easy non-developer translation environment
|
||||
|
||||
These tools need a lot of infrastructures and provide a free account for open source software.
|
|
@ -0,0 +1,40 @@
|
|||
'use strict';
|
||||
|
||||
require.config({
|
||||
paths: {
|
||||
'polyglot': '../node_modules/node-polyglot/build/polyglot',
|
||||
'Navigo': '../node_modules/navigo/lib/navigo',
|
||||
'leaflet': '../node_modules/leaflet/dist/leaflet',
|
||||
'moment': '../node_modules/moment/min/moment.min',
|
||||
// d3 modules indirect dependencies
|
||||
// by d3-zoom: d3-drag
|
||||
'd3-ease': '../node_modules/d3-ease/dist/d3-ease',
|
||||
'd3-transition': '../node_modules/d3-transition/dist/d3-transition',
|
||||
'd3-color': '../node_modules/d3-color/dist/d3-color',
|
||||
'd3-interpolate': '../node_modules/d3-interpolate/dist/d3-interpolate',
|
||||
// by d3-force
|
||||
'd3-collection': '../node_modules/d3-collection/dist/d3-collection',
|
||||
'd3-dispatch': '../node_modules/d3-dispatch/dist/d3-dispatch',
|
||||
'd3-quadtree': '../node_modules/d3-quadtree/dist/d3-quadtree',
|
||||
'd3-timer': '../node_modules/d3-timer/dist/d3-timer',
|
||||
// by d3-drag: d3-selection
|
||||
// d3 modules dependencies
|
||||
'd3-selection': '../node_modules/d3-selection/dist/d3-selection',
|
||||
'd3-force': '../node_modules/d3-force/dist/d3-force',
|
||||
'd3-zoom': '../node_modules/d3-zoom/dist/d3-zoom',
|
||||
'd3-drag': '../node_modules/d3-drag/dist/d3-drag',
|
||||
'snabbdom': '../node_modules/snabbdom/dist/snabbdom-patch',
|
||||
'rbush': '../node_modules/rbush/rbush',
|
||||
'helper': 'utils/helper'
|
||||
},
|
||||
shim: {
|
||||
'd3-drag': ['d3-selection'],
|
||||
'd3-force': ['d3-collection', 'd3-dispatch', 'd3-quadtree', 'd3-timer'],
|
||||
'd3-interpolate': ['d3-color'],
|
||||
'd3-zoom': ['d3-drag', 'd3-ease', 'd3-transition', 'd3-interpolate']
|
||||
}
|
||||
});
|
||||
|
||||
require(['main'], function (main) {
|
||||
main();
|
||||
});
|
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 3.1 KiB |
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square70x70logo src="./mstile-70x70.png"/>
|
||||
<square150x150logo src="./mstile-150x150.png"/>
|
||||
<square310x310logo src="./mstile-310x310.png"/>
|
||||
<wide310x150logo src="./mstile-310x150.png"/>
|
||||
<TileColor>#dc0067</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
After Width: | Height: | Size: 485 B |
After Width: | Height: | Size: 886 B |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 9.3 KiB |
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "Meshviewer",
|
||||
"short_name": "Meshviewer",
|
||||
"icons": [
|
||||
{
|
||||
"src": "./android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "./android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#dc0067",
|
||||
"background_color": "#dc0067",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"result":{"status":"success"},"favicon":{"package_url":"https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/favicon_package_v0.16.zip","files_urls":["https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/android-chrome-192x192.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/android-chrome-512x512.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/apple-touch-icon.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/browserconfig.xml","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/favicon-16x16.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/favicon-32x32.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/favicon.ico","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-144x144.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-150x150.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-310x150.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-310x310.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/mstile-70x70.png","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/safari-pinned-tab.svg","https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/package_files/site.webmanifest"],"html_code":"<link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"./apple-touch-icon.png\">\n<link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"./favicon-32x32.png\">\n<link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"./favicon-16x16.png\">\n<link rel=\"manifest\" href=\"./site.webmanifest\">\n<link rel=\"mask-icon\" href=\"./safari-pinned-tab.svg\" color=\"#dc0067\">\n<link rel=\"shortcut icon\" href=\"./favicon.ico\">\n<meta name=\"apple-mobile-web-app-title\" content=\"<!-- inject:title --><!-- endinject -->\">\n<meta name=\"application-name\" content=\"<!-- inject:title --><!-- endinject -->\">\n<meta name=\"msapplication-TileColor\" content=\"#dc0067\">\n<meta name=\"msapplication-TileImage\" content=\"./mstile-144x144.png\">\n<meta name=\"msapplication-config\" content=\"./browserconfig.xml\">\n<meta name=\"theme-color\" content=\"#dc0067\">","compression":"true","overlapping_markups":["link[rel=\"apple-touch-icon\"]","meta[name=\"apple-mobile-web-app-title\"]","link[rel=\"shortcut\"]","link[rel=\"shortcut icon\"]","link[rel=\"icon\",sizes=\"16x16\"]","link[rel=\"icon\",sizes=\"32x32\"]","meta[name=\"msapplication-TileColor\"]","meta[name=\"msapplication-TileImage\"]","meta[name=\"msapplication-config\"]","meta[name=\"application-name\"]","link[rel=\"manifest\"]","meta[name=\"theme-color\"]","link[rel=\"mask-icon\"]"]},"files_location":{"type":"path","path":"."},"preview_picture_url":"https://realfavicongenerator.net/files/ed9ef5a59ae048602fb9a5b74436696e43a575ce/favicon_preview.png","version":"0.16"}
|
|
@ -0,0 +1,7 @@
|
|||
@mixin icon($name, $code, $prefix: 'ion-') {
|
||||
.#{$prefix}#{$name} {
|
||||
&::before {
|
||||
content: '#{$code}';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
// Needed for standalone scss
|
||||
// @import 'icon-mixin';
|
||||
|
||||
$cache-breaker: unique-id();
|
||||
|
||||
@font-face {
|
||||
font-family: 'ionicons';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src:
|
||||
url('fonts/meshviewer.woff2?rel=#{$cache-breaker}') format('woff2'),
|
||||
url('fonts/meshviewer.woff?rel=#{$cache-breaker}') format('woff'),
|
||||
url('fonts/meshviewer.ttf?rel=#{$cache-breaker}') format('truetype');
|
||||
}
|
||||
|
||||
[class^='ion-'],
|
||||
[class*=' ion-'] {
|
||||
&::before {
|
||||
display: inline-block;
|
||||
font-family: $font-family-icons;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
font-weight: normal;
|
||||
line-height: 1;
|
||||
speak: none;
|
||||
text-rendering: auto;
|
||||
text-transform: none;
|
||||
vertical-align: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include icon('chevron-left', '\f124');
|
||||
@include icon('chevron-right', '\f125');
|
||||
@include icon('pin', '\f3a3');
|
||||
@include icon('wifi', '\f25c');
|
||||
@include icon('eye', '\f133');
|
||||
@include icon('up-b', '\f10d');
|
||||
@include icon('down-b', '\f104');
|
||||
@include icon('locate', '\f2e9');
|
||||
@include icon('close', '\f2d7');
|
||||
@include icon('location', '\f456');
|
||||
@include icon('layer', '\f229');
|
||||
@include icon('filter', '\f38B');
|
||||
@include icon('connection-bars', '\f274');
|
||||
@include icon('share-alt', '\f3ac');
|
||||
@include icon('clipboard', '\f376');
|
||||
@include icon('people', '\f39e');
|
||||
@include icon('person', '\f3a0');
|
||||
@include icon('time', '\f3b3');
|
||||
@include icon('arrow-resize', '\f264');
|
||||
@include icon('arrow-left-c', '\f108');
|
||||
@include icon('arrow-right-c', '\f10b');
|
||||
@include icon('full-enter', '\e901');
|
||||
@include icon('full-exit', '\e900');
|
|
@ -0,0 +1,30 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" data-name="Ebene 1" viewBox="0 0 125 97.5" width="125" height="97.5">
|
||||
<title>
|
||||
Meshviewer
|
||||
</title>
|
||||
<style>
|
||||
path.fade {
|
||||
animation: 1s fade ease-out infinite alternate;
|
||||
}
|
||||
|
||||
path.spin {
|
||||
animation: 2.3s spin linear infinite;
|
||||
transform-origin: 65.4px 56px;
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
to {
|
||||
filter: grayscale(.8);
|
||||
opacity: .2;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path class="fade" d="m 41.426847,11.807177 a 11.89,11.89 0 1 0 -7.75,20.919993 12,12 0 0 0 4,-0.7 l 15.81,16.66 a 13.45,13.45 0 0 1 5.08,-4.82 l -15.25,-16.07 a 11.9,11.9 0 0 0 -1.89,-15.989993 z m -11.25,13.119993 a 5.41,5.41 0 1 1 3.5,1.29 5.35,5.35 0 0 1 -3.5,-1.29 z m 20.86,33.55 -28.39,16 c -0.24,-0.23 -0.49,-0.46 -0.75,-0.67 a 13.38,13.38 0 1 0 4.45,6.62 l 28.13,-15.85 a 14,14 0 0 1 -3.44,-6.1 z m -32.12,30.06 a 6.86,6.86 0 1 1 1.45,-5 6.85,6.85 0 0 1 -1.45,5 z M 119.4569,3.8671829 A 15.09,15.09 0 0 0 96.456847,22.64717 l -23.44,21.69 a 13.58,13.58 0 0 1 4.75,5.14 l 23.390033,-21.61 A 15.1,15.1 0 0 0 119.4569,3.8671829 Z m -3.3,17.0599871 a 8.62,8.62 0 1 1 2,-6.26 8.6,8.6 0 0 1 -2,6.31 z m -4.05,52.4 a 11.23,11.23 0 0 0 -14.690053,0.07 l -18.76,-12.53 a 13.56,13.56 0 0 1 -3.9,5.81 l 19.1,12.74 a 11.24,11.24 0 1 0 18.280053,-6.09 z m -3.72,11.62 a 4.74,4.74 0 0 1 -3.61,1.65 4.74,4.74 0 0 1 -3.59001,-7.82 4.74,4.74 0 0 1 3.61001,-1.65 4.67,4.67 0 0 1 3.06,1.14 4.75,4.75 0 0 1 0.53,6.68 z" fill="#ad2358"/>
|
||||
<path class="spin" fill="#f4c72f" d="m 101.52068,52.899268 a 6.2809967,6.2864323 0 0 0 -8.861411,0.190195 6.2109856,6.2163606 0 0 0 -1.310208,2.102151 L 82.787703,54.711122 A 17.502777,17.517924 0 0 0 67.185227,38.51455 v -8.939146 a 6.3810126,6.3865347 0 1 0 -3.820606,0 v 8.949156 a 17.552785,17.567976 0 0 0 -6.781076,32.753514 l -2.5204,5.896032 a 6.3810126,6.3865347 0 1 0 3.510557,1.521557 l 2.5204,-5.966104 A 17.512779,17.527935 0 0 0 82.637679,58.545045 l 8.491348,0.470482 A 6.2709951,6.276422 0 1 0 101.53068,52.899268 Z M 54.513216,85.602731 a 2.4403873,2.4424992 0 0 1 -2.000317,0 2.4103825,2.4124684 0 0 1 -1.230196,-1.161189 2.4403873,2.4424992 0 0 1 3.160502,-3.3234 2.4103825,2.4124684 0 0 1 1.230195,1.161188 2.4503888,2.4525094 0 0 1 -1.130179,3.323401 z M 63.384624,22.01767 a 2.4403873,2.4424992 0 0 1 1.860295,-0.85087 2.4103825,2.4124684 0 0 1 1.590252,0.590604 2.4403873,2.4424992 0 0 1 -1.590252,4.304404 2.4103825,2.4124684 0 0 1 -1.590252,-0.590604 2.4503888,2.4525094 0 0 1 -0.230037,-3.453534 z m 9.881568,40.86181 a 10.591681,10.600847 0 1 1 2.510398,-7.697876 10.571678,10.580826 0 0 1 -2.480393,7.697876 z m 25.654071,-3.753841 a 2.4403873,2.4424992 0 0 1 -4.200667,-1.831874 2.4103825,2.4124684 0 0 1 0.670107,-1.551588 2.4403873,2.4424992 0 0 1 4.200666,1.831875 2.4103825,2.4124684 0 0 1 -0.640101,1.551587 z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,186 @@
|
|||
module.exports = function () {
|
||||
return {
|
||||
'reverseGeocodingApi': 'https://nominatim.openstreetmap.org/reverse',
|
||||
'maxAge': 60,
|
||||
'maxAgeAlert': 30,
|
||||
'nodeZoom': 18,
|
||||
'labelZoom': 13,
|
||||
'clientZoom': 15,
|
||||
'fullscreen': true,
|
||||
'fullscreenFrame': true,
|
||||
'nodeAttr': [
|
||||
// value can be a node attribute (1 depth) or a a function in utils/node with prefix show
|
||||
{
|
||||
'name': 'node.status',
|
||||
'value': 'Status'
|
||||
},
|
||||
{
|
||||
'name': 'node.databaseid',
|
||||
'value': 'DatabaseID'
|
||||
},
|
||||
|
||||
{
|
||||
'name': 'node.gateway',
|
||||
'value': 'Gateway'
|
||||
},
|
||||
{
|
||||
'name': 'node.coordinates',
|
||||
'value': 'GeoURI'
|
||||
},
|
||||
{
|
||||
"name": "node.contact",
|
||||
"value": "owner"
|
||||
},
|
||||
{
|
||||
'name': 'node.hardware',
|
||||
'value': 'model'
|
||||
},
|
||||
{
|
||||
'name': 'node.primaryMac',
|
||||
'value': 'mac'
|
||||
},
|
||||
{
|
||||
'name': 'node.macs',
|
||||
'value': 'MACs'
|
||||
},
|
||||
{
|
||||
'name': 'node.firmware',
|
||||
'value': 'Firmware'
|
||||
},
|
||||
{
|
||||
'name': 'node.uptime',
|
||||
'value': 'Uptime'
|
||||
},
|
||||
{
|
||||
'name': 'node.firstSeen',
|
||||
'value': 'FirstSeen'
|
||||
},
|
||||
{
|
||||
'name': 'node.systemLoad',
|
||||
'value': 'Load'
|
||||
},
|
||||
{
|
||||
'name': 'node.ram',
|
||||
'value': 'RAM'
|
||||
},
|
||||
{
|
||||
'name': 'node.ipAddresses',
|
||||
'value': 'IPs'
|
||||
},
|
||||
{
|
||||
'name': 'node.update',
|
||||
'value': 'Autoupdate'
|
||||
},
|
||||
{
|
||||
'name': 'node.domain',
|
||||
'value': 'Domain'
|
||||
},
|
||||
{
|
||||
'name': 'node.clients',
|
||||
'value': 'Clients'
|
||||
}
|
||||
],
|
||||
'supportedLocale': [
|
||||
'en',
|
||||
'de',
|
||||
'cz',
|
||||
'fr',
|
||||
'tr',
|
||||
'ru'
|
||||
],
|
||||
// Color configs
|
||||
'icon': {
|
||||
'base': {
|
||||
'fillOpacity': 0.6,
|
||||
'opacity': 0.6,
|
||||
'weight': 2,
|
||||
'radius': 6,
|
||||
'className': 'stroke-first'
|
||||
},
|
||||
'online': {
|
||||
'color': '#1566A9',
|
||||
'fillColor': '#1566A9'
|
||||
},
|
||||
'offline': {
|
||||
'color': '#D43E2A',
|
||||
'fillColor': '#D43E2A',
|
||||
'radius': 3
|
||||
},
|
||||
'lost': {
|
||||
'color': '#D43E2A',
|
||||
'fillColor': '#D43E2A',
|
||||
'radius': 4
|
||||
},
|
||||
'alert': {
|
||||
'color': '#D43E2A',
|
||||
'fillColor': '#D43E2A',
|
||||
'radius': 5
|
||||
},
|
||||
'new': {
|
||||
'color': '#1566A9',
|
||||
'fillColor': '#93E929'
|
||||
}
|
||||
},
|
||||
'client': {
|
||||
'wifi24': 'rgba(220, 0, 103, 0.7)',
|
||||
'wifi5': 'rgba(10, 156, 146, 0.7)',
|
||||
'other': 'rgba(227, 166, 25, 0.7)'
|
||||
},
|
||||
'map': {
|
||||
'labelNewColor': '#459c18',
|
||||
'tqFrom': '#F02311',
|
||||
'tqTo': '#04C714',
|
||||
'highlightNode': {
|
||||
'color': '#ad2358',
|
||||
'weight': 8,
|
||||
'fillOpacity': 1,
|
||||
'opacity': 0.4,
|
||||
'className': 'stroke-first'
|
||||
},
|
||||
'highlightLink': {
|
||||
'weight': 4,
|
||||
'opacity': 1,
|
||||
'dashArray': '5, 10'
|
||||
}
|
||||
},
|
||||
'forceGraph': {
|
||||
'nodeColor': '#fff',
|
||||
'nodeOfflineColor': '#D43E2A',
|
||||
'highlightColor': 'rgba(255, 255, 255, 0.2)',
|
||||
'labelColor': '#fff',
|
||||
'tqFrom': '#770038',
|
||||
'tqTo': '#dc0067',
|
||||
'zoomModifier': 1
|
||||
},
|
||||
'locate': {
|
||||
'outerCircle': {
|
||||
'stroke': false,
|
||||
'color': '#4285F4',
|
||||
'opacity': 1,
|
||||
'fillOpacity': 0.3,
|
||||
'clickable': false,
|
||||
'radius': 16
|
||||
},
|
||||
'innerCircle': {
|
||||
'stroke:': true,
|
||||
'color': '#ffffff',
|
||||
'fillColor': '#4285F4',
|
||||
'weight': 1.5,
|
||||
'clickable': false,
|
||||
'opacity': 1,
|
||||
'fillOpacity': 1,
|
||||
'radius': 7
|
||||
},
|
||||
'accuracyCircle': {
|
||||
'stroke': true,
|
||||
'color': '#4285F4',
|
||||
'weight': 1,
|
||||
'clickable': false,
|
||||
'opacity': 0.7,
|
||||
'fillOpacity': 0.2
|
||||
}
|
||||
},
|
||||
'cacheBreaker': '<!-- inject:cache-breaker -->'
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
module.exports = function () {
|
||||
return {
|
||||
'nodeInfos': [
|
||||
{
|
||||
'name': 'Clientstatistik',
|
||||
'href': 'https://stats.freifunk-leipzig.de/d/GVI39bqGk/nodespublic?orgId=1&refresh=5m&var-nodeid={NODE_ID}',
|
||||
'image': 'https://stats.freifunk-leipzig.de/render/d-solo/GVI39bqGk/statistiken?orgId=1&panelId=2&var-nodeid={NODE_ID}&width=650&height=350&from=now-1d&theme=light',
|
||||
'title': 'Entwicklung der Anzahl der Clients innerhalb des letzten Tages'
|
||||
},
|
||||
{
|
||||
'name': 'Traffic',
|
||||
'href': 'https://stats.freifunk-leipzig.de/d/GVI39bqGk/nodespublic?orgId=1&refresh=5m&var-nodeid={NODE_ID}',
|
||||
'image': 'https://stats.freifunk-leipzig.de/render/d-solo/GVI39bqGk/statistiken?orgId=1&panelId=6&var-nodeid={NODE_ID}&width=650&height=350&from=now-1d&theme=light',
|
||||
'title': 'Traffic innerhalb des letzten Tages'
|
||||
}
|
||||
],
|
||||
// Array of data provider are supported
|
||||
'dataPath': [
|
||||
'https://meshviewer.freifunk-leipzig.de/data/'
|
||||
],
|
||||
'siteName': 'Freifunk Leipzig',
|
||||
'mapLayers': [
|
||||
{
|
||||
'name': 'OpenStreetMap.HOT',
|
||||
'url': 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
|
||||
'config': {
|
||||
'maxZoom': 19,
|
||||
'attribution': '© Openstreetmap France | © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Esri.WorldImagery',
|
||||
'url': '//server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
'config': {
|
||||
'maxZoom': 20,
|
||||
'attribution': 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
|
||||
}
|
||||
}
|
||||
],
|
||||
// Set a visible frame
|
||||
'fixedCenter': [
|
||||
// Northwest
|
||||
[
|
||||
51.4168,
|
||||
12.1983
|
||||
],
|
||||
// Southeast
|
||||
[
|
||||
51.2516,
|
||||
12.4791
|
||||
]
|
||||
],
|
||||
'domainNames': [
|
||||
{
|
||||
'domain': 'l',
|
||||
'name': 'Leipzig (Gluon)'
|
||||
},
|
||||
{
|
||||
'domain': 'meshkit',
|
||||
'name': 'Leipzig (meshkit)'
|
||||
}
|
||||
],
|
||||
'linkList': [
|
||||
{
|
||||
'title': 'Impressum',
|
||||
'href': 'https://freifunk.net/impressum/'
|
||||
},
|
||||
{
|
||||
'title': 'Datenschutz',
|
||||
'href': 'https://freifunk.net/datenschutz/'
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
module.exports = function () {
|
||||
return {
|
||||
'nodeInfos': [
|
||||
{
|
||||
'name': 'Clientstatistik',
|
||||
'href': 'https://stats.freifunk-leipzig.de/d/hRIn3dRWk/nodes?viewPanel=2&orgId=1&refresh=5m&var-nodeid={NODE_ID}from=now-24h&to=now',
|
||||
'href': 'https://stats.freifunk-leipzig.de/d/hRIn3dRWk/nodes?orgId=1&refresh=5m&var-nodeid=={NODE_ID}',
|
||||
'image': 'https://multi.meshviewer.org/graph/000000002/node?panelId=1&var-node={NODE_ID}&from=now-86399s&width=650&height=350&theme=light',
|
||||
'title': 'Entwicklung der Anzahl der Clients innerhalb des letzten Tages'
|
||||
},
|
||||
{
|
||||
'name': 'Hardwareauslastung',
|
||||
'href': 'https://data.meshviewer.org/d/000000002/node?var-node={NODE_ID}',
|
||||
'image': 'https://multi.meshviewer.org/graph/000000002/node?panelId=4&var-node={NODE_ID}&from=now-86399s&width=650&height=350&theme=light',
|
||||
'title': 'Loadavg und Arbeitspeicherauslastung innerhalb des letzten Tages'
|
||||
}
|
||||
],
|
||||
'linkInfos': [
|
||||
{
|
||||
'name': 'Statistik für alle Links zwischen diese Knoten',
|
||||
'href': 'https://data.meshviewer.org/d/nvSNqoHmz/link?var-node={SOURCE_ID}&var-nodetolink={TARGET_ID}',
|
||||
'image': 'https://multi.meshviewer.org/graph/nvSNqoHmz/link?panelId=7&var-node={SOURCE_ID}&var-nodetolink={TARGET_ID}&from=now-86399s&width=650&height=350&theme=light',
|
||||
'title': 'Linkstatistik des letzten Tages, min und max aller Links zwischen diesen Knoten'
|
||||
}
|
||||
],
|
||||
'linkTypeInfos': [
|
||||
{
|
||||
'name': 'Statistik für {TYPE}',
|
||||
'href': 'https://data.meshviewer.org/d/nvSNqoHmz/link?var-node={SOURCE_ID}&var-nodetolink={TARGET_ID}&var-source_mac={SOURCE_MAC}&var-target_mac={TARGET_MAC}',
|
||||
'image': 'https://multi.meshviewer.org/graph/nvSNqoHmz/link?panelId=8&var-node={SOURCE_ID}&var-nodetolink={TARGET_ID}&var-source_mac={SOURCE_MAC}&var-target_mac={TARGET_MAC}&from=now-86399s&width=650&height=350&theme=light',
|
||||
'title': 'Linkstatistik des letzten Tages des einzelnen Links in beide Richtungen'
|
||||
}
|
||||
],
|
||||
// Array of data provider are supported
|
||||
'dataPath': [
|
||||
'https://bhc-pn.de/meshviewer/data/'
|
||||
],
|
||||
'siteName': 'Freifunk Leipzig',
|
||||
'mapLayers': [
|
||||
{
|
||||
'name': 'OpenStreetMap.HOT',
|
||||
'url': 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
|
||||
'config': {
|
||||
'maxZoom': 19,
|
||||
'attribution': '© Openstreetmap France | © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'Esri.WorldImagery',
|
||||
'url': '//server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
'config': {
|
||||
'maxZoom': 20,
|
||||
'attribution': 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
|
||||
}
|
||||
}
|
||||
],
|
||||
// Set a visible frame
|
||||
'fixedCenter': [
|
||||
// Northwest
|
||||
[
|
||||
51.4168,
|
||||
12.1983
|
||||
],
|
||||
// Southeast
|
||||
[
|
||||
51.2516,
|
||||
12.4791
|
||||
]
|
||||
],
|
||||
'domainNames': [
|
||||
{
|
||||
'domain': 'l',
|
||||
'name': 'Leipzig (Gluon)'
|
||||
},
|
||||
{
|
||||
'domain': 'meshkit',
|
||||
'name': 'Leipzig (meshkit)'
|
||||
}
|
||||
],
|
||||
'linkList': [
|
||||
{
|
||||
'title': 'Impressum',
|
||||
'href': 'https://freifunk.net/impressum/'
|
||||
},
|
||||
{
|
||||
'title': 'Datenschutz',
|
||||
'href': 'https://freifunk.net/datenschutz/'
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
module.exports = function () {
|
||||
const build = 'build';
|
||||
|
||||
return {
|
||||
build: build,
|
||||
faviconData: 'assets/faviconData.json',
|
||||
src: {
|
||||
sass: 'scss/**/*.scss',
|
||||
javascript: ['./app.js', 'lib/**/*.js'],
|
||||
json: 'locale/*.json',
|
||||
html: ['html/*.html', './config*.js']
|
||||
},
|
||||
clean: [build + '/*.map', build + '/vendor', build + '/main.css'],
|
||||
browsersync: {
|
||||
open: false,
|
||||
server: {
|
||||
baseDir: build
|
||||
},
|
||||
files: [
|
||||
build + '/*.css',
|
||||
build + '/*.js',
|
||||
build + '/*.html',
|
||||
build + '/locale/*.json'
|
||||
]
|
||||
},
|
||||
requireJs: {
|
||||
prod: {
|
||||
baseUrl: 'lib',
|
||||
name: '../node_modules/almond/almond',
|
||||
mainConfigFile: 'app.js',
|
||||
include: '../app',
|
||||
out: 'app.js',
|
||||
build: true,
|
||||
preserveLicenseComments: true
|
||||
},
|
||||
dev: {
|
||||
baseUrl: 'lib',
|
||||
name: '../node_modules/almond/almond',
|
||||
mainConfigFile: 'app.js',
|
||||
include: '../app',
|
||||
optimize: 'none',
|
||||
out: 'app.js',
|
||||
build: false
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
module.exports = function (gulp, plugins, config, env) {
|
||||
const browserSync = require('browser-sync');
|
||||
|
||||
function getTask(task) {
|
||||
return require('./tasks/' + task)(gulp, plugins, config, env);
|
||||
}
|
||||
|
||||
gulp.task('ws', () =>
|
||||
browserSync(config.browsersync)
|
||||
);
|
||||
|
||||
gulp.task('watch:html', () =>
|
||||
gulp.watch(config.src.html,
|
||||
gulp.parallel(getTask('html'))
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task('watch:javascript', () =>
|
||||
gulp.watch(config.src.javascript,
|
||||
gulp.parallel(getTask('eslint'), getTask('javascript'))
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task('watch:styles', () =>
|
||||
gulp.watch(config.src.sass,
|
||||
gulp.parallel(getTask('sasslint'), getTask('sass'))
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task('watch:json', () =>
|
||||
gulp.watch(config.src.json,
|
||||
gulp.parallel(getTask('jsonMinify'))
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task('watch',
|
||||
gulp.parallel('watch:html', 'watch:styles', 'watch:javascript', 'watch:json')
|
||||
);
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
const del = require('del');
|
||||
module.exports = function (gulp, plugins, config) {
|
||||
return function clean() {
|
||||
return del(config.clean);
|
||||
};
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
module.exports = function (gulp, plugins, config) {
|
||||
return function copy() {
|
||||
gulp.src(['html/*.html', 'assets/favicon/*'])
|
||||
.pipe(gulp.dest(config.build));
|
||||
gulp.src(['assets/logo.svg', 'service-worker.js'])
|
||||
.pipe(gulp.dest(config.build));
|
||||
gulp.src(['polyfill.js'])
|
||||
.pipe(gulp.dest(config.build + '/vendor'));
|
||||
return gulp.src(['assets/fonts/*', 'assets/icons/fonts/*'])
|
||||
.pipe(gulp.dest(config.build + '/fonts'));
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
module.exports = function (gulp, plugins, config, env) {
|
||||
return function eslint() {
|
||||
return gulp.src(['app.js', 'gulpfile.js', 'lib/**/*.js', 'gulp/**/*.js'])
|
||||
.pipe(plugins.eslint())
|
||||
.pipe(plugins.eslint.format())
|
||||
.pipe(env.production(plugins.eslint.failAfterError()));
|
||||
};
|
||||
};
|
|
@ -0,0 +1,65 @@
|
|||
module.exports = function (gulp, plugins, config) {
|
||||
return function javascript(cb) {
|
||||
plugins.realFavicon.generateFavicon({
|
||||
masterPicture: 'assets/logo.svg',
|
||||
dest: 'assets/favicon',
|
||||
iconsPath: '.',
|
||||
design: {
|
||||
ios: {
|
||||
pictureAspect: 'backgroundAndMargin',
|
||||
backgroundColor: '#ffffff',
|
||||
margin: '14%',
|
||||
assets: {
|
||||
ios6AndPriorIcons: false,
|
||||
ios7AndLaterIcons: false,
|
||||
precomposedIcons: false,
|
||||
declareOnlyDefaultIcon: true
|
||||
},
|
||||
appName: 'Meshviewer'
|
||||
},
|
||||
desktopBrowser: {},
|
||||
windows: {
|
||||
pictureAspect: 'whiteSilhouette',
|
||||
backgroundColor: '#dc0067',
|
||||
onConflict: 'override',
|
||||
assets: {
|
||||
windows80Ie10Tile: false,
|
||||
windows10Ie11EdgeTiles: {
|
||||
small: true,
|
||||
medium: true,
|
||||
big: true,
|
||||
rectangle: false
|
||||
}
|
||||
},
|
||||
appName: 'Meshviewer'
|
||||
},
|
||||
androidChrome: {
|
||||
// pictureAspect: 'shadow',
|
||||
themeColor: '#dc0067',
|
||||
manifest: {
|
||||
name: 'Meshviewer',
|
||||
display: 'standalone',
|
||||
orientation: 'portrait',
|
||||
onConflict: 'override',
|
||||
declared: true
|
||||
},
|
||||
assets: {
|
||||
legacyIcon: false,
|
||||
lowResolutionIcons: false
|
||||
}
|
||||
},
|
||||
safariPinnedTab: {
|
||||
pictureAspect: 'silhouette',
|
||||
themeColor: '#dc0067'
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
compression: 2,
|
||||
scalingAlgorithm: 'Mitchell',
|
||||
errorOnImageTooSmall: false
|
||||
},
|
||||
markupFile: config.faviconData
|
||||
});
|
||||
return cb();
|
||||
};
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
const fs = require('fs');
|
||||
|
||||
// stringify functions https://gist.github.com/cowboy/3749767
|
||||
var stringify = function (obj) {
|
||||
var placeholder = '____PLACEHOLDER____';
|
||||
var fns = [];
|
||||
var json = JSON.stringify(obj, function (key, value) {
|
||||
if (typeof value === 'function') {
|
||||
fns.push(value);
|
||||
return placeholder;
|
||||
}
|
||||
return value;
|
||||
}, 2);
|
||||
json = json.replace(new RegExp('"' + placeholder + '"', 'g'), function () {
|
||||
return fns.shift();
|
||||
});
|
||||
return json;
|
||||
};
|
||||
|
||||
module.exports = function (gulp, plugins, config, env) {
|
||||
return function html() {
|
||||
return gulp.src(env.production() ? config.build + '/*.html' : 'html/*.html')
|
||||
.pipe(plugins.realFavicon.injectFaviconMarkups(JSON.parse(fs.readFileSync(config.faviconData)).favicon.html_code))
|
||||
.pipe(env.production(plugins.inlineSource({ compress: false })))
|
||||
.pipe(plugins.inject(gulp.src(['config.js']), {
|
||||
removeTags: true,
|
||||
starttag: '<!-- inject:config -->',
|
||||
transform: function () {
|
||||
delete require.cache[require.resolve('../../config.default')];
|
||||
delete require.cache[require.resolve('../../config')];
|
||||
var buildConfig = Object.assign({}, require('../../config.default')(), require('../../config')());
|
||||
return '<title>' + buildConfig.siteName + ' - loading...</title>' +
|
||||
'<script>window.config =' +
|
||||
stringify(buildConfig)
|
||||
.replace('<!-- inject:cache-breaker -->',
|
||||
Math.random().toString(12).substring(7)) +
|
||||
';</script>';
|
||||
}
|
||||
}))
|
||||
.pipe(plugins.inject(gulp.src(['config.js']), {
|
||||
removeTags: true,
|
||||
starttag: '<!-- inject:title -->',
|
||||
transform: function () {
|
||||
delete require.cache[require.resolve('../../config.default')];
|
||||
delete require.cache[require.resolve('../../config')];
|
||||
var buildConfig = Object.assign({}, require('../../config.default')(), require('../../config')());
|
||||
return buildConfig.siteName;
|
||||
}
|
||||
}))
|
||||
.pipe(plugins.cacheBust({
|
||||
type: 'timestamp'
|
||||
}))
|
||||
.pipe(plugins.htmlmin({
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
minifyJS: true
|
||||
}))
|
||||
.pipe(gulp.dest(config.build));
|
||||
};
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
module.exports = function (gulp, plugins, config, env) {
|
||||
return function javascript() {
|
||||
return gulp.src('app.js')
|
||||
.pipe(env.development(plugins.sourcemaps.init()))
|
||||
.pipe(plugins.requirejsOptimize(env.production() ? config.requireJs.prod : config.requireJs.dev))
|
||||
.on('error', function () {
|
||||
this.emit('end');
|
||||
})
|
||||
.pipe(env.production(plugins.uglify({ output: { comments: 'all' } })))
|
||||
.pipe(env.development(plugins.sourcemaps.write('.', { addComment: true })))
|
||||
.pipe(gulp.dest(config.build));
|
||||
};
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = function (gulp, plugins, config) {
|
||||
return function jsonMinify() {
|
||||
return gulp.src(config.src.json)
|
||||
.pipe(plugins.jsonminify())
|
||||
.pipe(gulp.dest(config.build + '/locale'));
|
||||
};
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
module.exports = function (gulp, plugins, config, env) {
|
||||
return function sass() {
|
||||
return gulp.src('scss/*.scss')
|
||||
.pipe(env.development(plugins.sourcemaps.init()))
|
||||
.pipe(plugins.sass({
|
||||
outputStyle: 'compressed',
|
||||
sourceMap: false
|
||||
}))
|
||||
.on('error', function () {
|
||||
this.emit('end');
|
||||
})
|
||||
.pipe(plugins.autoprefixer({
|
||||
browsers: config.autoprefixer
|
||||
}))
|
||||
.pipe(env.development(plugins.sourcemaps.write('.', { addComment: true })))
|
||||
.pipe(gulp.dest(config.build));
|
||||
};
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
module.exports = function (gulp, plugins, config, env) {
|
||||
return function sasslint() {
|
||||
return gulp.src('scss/**/*.scss')
|
||||
.pipe(plugins.stylelint({
|
||||
syntax: 'scss',
|
||||
failAfterError: env.production(),
|
||||
reporters: [
|
||||
{ formatter: 'string', console: true }
|
||||
]
|
||||
}));
|
||||
};
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = function (gulp, plugins, config, env) {
|
||||
return function setDevelopment(done) {
|
||||
plugins.environments.current(env.development);
|
||||
done();
|
||||
};
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
const gulp = require('gulp');
|
||||
const plugins = require('gulp-load-plugins')();
|
||||
const config = require('./gulp/config')();
|
||||
|
||||
const env = {
|
||||
development: plugins.environments.development,
|
||||
production: plugins.environments.production
|
||||
};
|
||||
|
||||
// Default environment is production
|
||||
plugins.environments.current(env.production);
|
||||
|
||||
function getTask(task) {
|
||||
return require('./gulp/tasks/' + task)(gulp, plugins, config, env);
|
||||
}
|
||||
|
||||
gulp.task('generate-favicon',
|
||||
getTask('favicon')
|
||||
);
|
||||
|
||||
require('./gulp/serve')(gulp, plugins, config, env);
|
||||
gulp.task('serve',
|
||||
gulp.series(
|
||||
getTask('setDevelopment'),
|
||||
gulp.parallel(getTask('eslint'), getTask('sasslint')),
|
||||
gulp.parallel(getTask('copy'), getTask('javascript'), getTask('sass'), getTask('jsonMinify')),
|
||||
getTask('html'),
|
||||
gulp.parallel('watch', 'ws')
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task('default',
|
||||
gulp.series(
|
||||
gulp.parallel(getTask('eslint'), getTask('sasslint')),
|
||||
gulp.parallel(getTask('copy'), getTask('javascript'), getTask('sass'), getTask('jsonMinify')),
|
||||
getTask('html'),
|
||||
getTask('clean')
|
||||
)
|
||||
);
|
|
@ -0,0 +1,48 @@
|
|||
<!DOCTYPE html>
|
||||
<html itemscope itemtype="http://schema.org/WebPage">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<!--<meta name="image" content="https://regensburg.freifunk.net/meshviewer/apple-touch-icon.png">-->
|
||||
|
||||
<meta itemprop="name" content="<!-- inject:title --><!-- endinject --> Meshviewer">
|
||||
<meta name="description" itemprop="description" content="<!-- inject:title --><!-- endinject --> Knotenkarte - Zeigt alle Knoten, Statistiken und Verbindungen auf Karte oder Topologie">
|
||||
<!--Uncomment & adjust local urls-->
|
||||
<!--<meta itemprop="image" content="https://regensburg.freifunk.net/meshviewer/android-chrome-512x512.png">-->
|
||||
|
||||
<!--<meta property="business:contact_data:locality" content="Regensburg">-->
|
||||
<!--<meta property="business:contact_data:region" content="Bayern">-->
|
||||
<meta property="business:contact_data:country_name" content="Germany">
|
||||
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:site" content="@freifunk">
|
||||
|
||||
<meta name="og:title" content="<!-- inject:title --><!-- endinject -->">
|
||||
<meta name="og:description" content="<!-- inject:title --><!-- endinject --> Knotenkarte - Zeigt alle Knoten, Statistiken und Verbindungen auf Karte oder Topologie">
|
||||
<!--<meta name="og:image" content="https://regensburg.freifunk.net/meshviewer/android-chrome-512x512.png">-->
|
||||
<!--<meta name="og:url" content="https://regensburg.freifunk.net/meshviewer/">-->
|
||||
<meta name="og:site_name" content="<!-- inject:title --><!-- endinject -->">
|
||||
<meta name="og:type" content="website">
|
||||
|
||||
<link rel="stylesheet" href="main.css" inline>
|
||||
<link rel="stylesheet" class="css-mode night" media="not" href="night.css" inline>
|
||||
<!-- inject:config -->
|
||||
<!-- contents of html partials will be injected here -->
|
||||
<!-- endinject -->
|
||||
<script src="vendor/polyfill.js" inline></script>
|
||||
<script src="app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="loader">
|
||||
<p>
|
||||
Lade<br />
|
||||
<img inline src="logo.svg" class="spinner" alt="Loading ..."/>
|
||||
<br />
|
||||
Karten & Knoten...
|
||||
</p>
|
||||
<noscript>
|
||||
<strong>JavaScript required</strong>
|
||||
</noscript>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,23 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><!-- inject:title --><!-- endinject --></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<link rel="stylesheet" href="main.css" inline>
|
||||
</head>
|
||||
<body>
|
||||
<div class="loader">
|
||||
<p>
|
||||
You are Offline!<br />
|
||||
<img inline src="logo.svg" class="spinner" alt="Loading ..."/>
|
||||
<br />
|
||||
No connection available.
|
||||
<br /><br /><button onclick="location.reload(true)" class="btn text" aria-label="Try to reload">Try to reload</button><br />
|
||||
</p>
|
||||
<noscript>
|
||||
<strong>JavaScript required</strong>
|
||||
</noscript>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,44 @@
|
|||
define(function () {
|
||||
'use strict';
|
||||
|
||||
return function () {
|
||||
this.render = function render(d) {
|
||||
d.innerHTML = _.t('sidebar.aboutInfo') +
|
||||
'<h4>' + _.t('node.nodes') + '</h4>' +
|
||||
'<p class="legend">' +
|
||||
'<span class="legend-new"><span class="symbol"></span> ' + _.t('sidebar.nodeNew') + '</span>' +
|
||||
'<span class="legend-online"><span class="symbol"></span> ' + _.t('sidebar.nodeOnline') + '</span>' +
|
||||
'<span class="legend-offline"><span class="symbol"></span> ' + _.t('sidebar.nodeOffline') + '</span>' +
|
||||
'</p>' +
|
||||
'<h4>' + _.t('node.clients') + '</h4>' +
|
||||
'<p class="legend">' +
|
||||
'<span class="legend-24ghz"><span class="symbol"></span> 2.4 GHz</span>' +
|
||||
'<span class="legend-5ghz"><span class="symbol"></span> 5 GHz</span>' +
|
||||
'<span class="legend-others"><span class="symbol"></span> ' + _.t('others') + '</span>' +
|
||||
'</p>' +
|
||||
'<h3>AGPL 3</h3>' +
|
||||
|
||||
'<p>Copyright (C) Milan Pässler</p>' +
|
||||
'<p>Copyright (C) Nils Schneider</p>' +
|
||||
|
||||
'<p>This program is free software: you can redistribute it and/or ' +
|
||||
'modify it under the terms of the GNU Affero General Public ' +
|
||||
'License as published by the Free Software Foundation, either ' +
|
||||
'version 3 of the License, or (at your option) any later version.</p>' +
|
||||
|
||||
'<p>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 Affero General Public License for more details.</p>' +
|
||||
|
||||
'<p>You should have received a copy of the GNU Affero General ' +
|
||||
'Public License along with this program. If not, see ' +
|
||||
'<a href="https://www.gnu.org/licenses/">' +
|
||||
'https://www.gnu.org/licenses/</a>.</p>' +
|
||||
|
||||
'<p>The source code is available at ' +
|
||||
'<a href="https://github.com/ffrgb/meshviewer">' +
|
||||
'https://github.com/ffrgb/meshviewer</a>.</p>';
|
||||
};
|
||||
};
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
define([], function () {
|
||||
'use strict';
|
||||
|
||||
return function (tag) {
|
||||
if (!tag) {
|
||||
tag = 'div';
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var container = document.createElement(tag);
|
||||
|
||||
self.add = function add(d) {
|
||||
d.render(container);
|
||||
};
|
||||
|
||||
self.render = function render(el) {
|
||||
el.appendChild(container);
|
||||
};
|
||||
|
||||
return self;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,103 @@
|
|||
define(['filters/nodefilter'], function (NodeFilter) {
|
||||
'use strict';
|
||||
|
||||
return function () {
|
||||
var targets = [];
|
||||
var filterObservers = [];
|
||||
var filters = [];
|
||||
var filteredData;
|
||||
var data;
|
||||
|
||||
function remove(d) {
|
||||
targets = targets.filter(function (e) {
|
||||
return d !== e;
|
||||
});
|
||||
}
|
||||
|
||||
function add(d) {
|
||||
targets.push(d);
|
||||
|
||||
if (filteredData !== undefined) {
|
||||
d.setData(filteredData);
|
||||
}
|
||||
}
|
||||
|
||||
function setData(d) {
|
||||
data = d;
|
||||
refresh();
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (data === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
var filter = filters.reduce(function (a, f) {
|
||||
return function (d) {
|
||||
return a(d) && f.run(d);
|
||||
};
|
||||
}, function () {
|
||||
return true;
|
||||
});
|
||||
|
||||
filteredData = new NodeFilter(filter)(data);
|
||||
|
||||
targets.forEach(function (t) {
|
||||
t.setData(filteredData);
|
||||
});
|
||||
}
|
||||
|
||||
function notifyObservers() {
|
||||
filterObservers.forEach(function (d) {
|
||||
d.filtersChanged(filters);
|
||||
});
|
||||
}
|
||||
|
||||
function addFilter(d) {
|
||||
var newItem = true;
|
||||
|
||||
filters.forEach(function (f) {
|
||||
if (f.getKey && f.getKey() === d.getKey()) {
|
||||
removeFilter(f);
|
||||
newItem = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (newItem) {
|
||||
filters.push(d);
|
||||
notifyObservers();
|
||||
d.setRefresh(refresh);
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
function removeFilter(d) {
|
||||
filters = filters.filter(function (e) {
|
||||
return d !== e;
|
||||
});
|
||||
notifyObservers();
|
||||
refresh();
|
||||
}
|
||||
|
||||
function watchFilters(d) {
|
||||
filterObservers.push(d);
|
||||
|
||||
d.filtersChanged(filters);
|
||||
|
||||
return function () {
|
||||
filterObservers = filterObservers.filter(function (e) {
|
||||
return d !== e;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
add: add,
|
||||
remove: remove,
|
||||
setData: setData,
|
||||
addFilter: addFilter,
|
||||
removeFilter: removeFilter,
|
||||
watchFilters: watchFilters
|
||||
};
|
||||
};
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
define(function () {
|
||||
'use strict';
|
||||
|
||||
return function (distributor) {
|
||||
var container = document.createElement('ul');
|
||||
container.classList.add('filters');
|
||||
var div = document.createElement('div');
|
||||
|
||||
function render(el) {
|
||||
el.appendChild(div);
|
||||
}
|
||||
|
||||
function filtersChanged(filters) {
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
|
||||
filters.forEach(function (d) {
|
||||
var li = document.createElement('li');
|
||||
container.appendChild(li);
|
||||
d.render(li);
|
||||
|
||||
var button = document.createElement('button');
|
||||
button.classList.add('ion-close');
|
||||
button.setAttribute('aria-label', _.t('remove'));
|
||||
button.onclick = function onclick() {
|
||||
distributor.removeFilter(d);
|
||||
};
|
||||
li.appendChild(button);
|
||||
});
|
||||
|
||||
if (container.parentNode === div && filters.length === 0) {
|
||||
div.removeChild(container);
|
||||
} else if (filters.length > 0) {
|
||||
div.appendChild(container);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
render: render,
|
||||
filtersChanged: filtersChanged
|
||||
};
|
||||
};
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
define(['helper'], function (helper) {
|
||||
'use strict';
|
||||
|
||||
return function (name, key, value, f) {
|
||||
var negate = false;
|
||||
var refresh;
|
||||
|
||||
var label = document.createElement('label');
|
||||
var strong = document.createElement('strong');
|
||||
label.textContent = name + ': ';
|
||||
label.appendChild(strong);
|
||||
|
||||
function run(d) {
|
||||
var o = helper.dictGet(d, key.slice(0));
|
||||
|
||||
if (f) {
|
||||
o = f(o);
|
||||
}
|
||||
|
||||
return o === value ? !negate : negate;
|
||||
}
|
||||
|
||||
function setRefresh(r) {
|
||||
refresh = r;
|
||||
}
|
||||
|
||||
function draw(el) {
|
||||
if (negate) {
|
||||
el.classList.add('not');
|
||||
} else {
|
||||
el.classList.remove('not');
|
||||
}
|
||||
|
||||
strong.textContent = value;
|
||||
}
|
||||
|
||||
function render(el) {
|
||||
el.appendChild(label);
|
||||
draw(el);
|
||||
|
||||
label.onclick = function onclick() {
|
||||
negate = !negate;
|
||||
|
||||
draw(el);
|
||||
|
||||
if (refresh) {
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getKey() {
|
||||
return value.concat(name);
|
||||
}
|
||||
|
||||
return {
|
||||
run: run,
|
||||
setRefresh: setRefresh,
|
||||
render: render,
|
||||
getKey: getKey
|
||||
};
|
||||
};
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
define(function () {
|
||||
'use strict';
|
||||
|
||||
return function () {
|
||||
var refreshFunctions = [];
|
||||
var timer;
|
||||
var input = document.createElement('input');
|
||||
|
||||
function refresh() {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(function () {
|
||||
refreshFunctions.forEach(function (f) {
|
||||
f();
|
||||
});
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function run(d) {
|
||||
return d.hostname.toLowerCase().includes(input.value.toLowerCase());
|
||||
}
|
||||
|
||||
function setRefresh(f) {
|
||||
refreshFunctions.push(f);
|
||||
}
|
||||
|
||||
function render(el) {
|
||||
input.type = 'search';
|
||||
input.placeholder = _.t('sidebar.nodeFilter');
|
||||
input.setAttribute('aria-label', _.t('sidebar.nodeFilter'));
|
||||
input.addEventListener('input', refresh);
|
||||
el.classList.add('filter-node');
|
||||
el.classList.add('ion-filter');
|
||||
el.appendChild(input);
|
||||
}
|
||||
|
||||
return {
|
||||
run: run,
|
||||
setRefresh: setRefresh,
|
||||
render: render
|
||||
};
|
||||
};
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
define(function () {
|
||||
'use strict';
|
||||
|
||||
return function (filter) {
|
||||
return function (data) {
|
||||
var n = Object.create(data);
|
||||
n.nodes = {};
|
||||
|
||||
for (var key in data.nodes) {
|
||||
if (data.nodes.hasOwnProperty(key)) {
|
||||
n.nodes[key] = data.nodes[key].filter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
n.links = data.links.filter(function (d) {
|
||||
return filter(d.source) && filter(d.target);
|
||||
});
|
||||
|
||||
return n;
|
||||
};
|
||||
};
|
||||
});
|
|
@ -0,0 +1,288 @@
|
|||
define(['d3-selection', 'd3-force', 'd3-zoom', 'd3-drag', 'd3-timer', 'd3-ease', 'd3-interpolate', 'utils/math', 'forcegraph/draw'],
|
||||
function (d3Selection, d3Force, d3Zoom, d3Drag, d3Timer, d3Ease, d3Interpolate, math, draw) {
|
||||
'use strict';
|
||||
|
||||
return function (linkScale, sidebar) {
|
||||
var self = this;
|
||||
var el;
|
||||
var canvas;
|
||||
var ctx;
|
||||
var force;
|
||||
var forceLink;
|
||||
|
||||
var transform = d3Zoom.zoomIdentity;
|
||||
var intNodes = [];
|
||||
var dictNodes = {};
|
||||
var intLinks = [];
|
||||
var movetoTimer;
|
||||
var initial = 1.8;
|
||||
|
||||
var NODE_RADIUS_DRAG = 10;
|
||||
var NODE_RADIUS_SELECT = 15;
|
||||
var LINK_RADIUS_SELECT = 12;
|
||||
var ZOOM_ANIMATE_DURATION = 350;
|
||||
|
||||
var ZOOM_MIN = 1 / 8;
|
||||
var ZOOM_MAX = 3;
|
||||
|
||||
var FORCE_ALPHA = 0.01;
|
||||
|
||||
draw.setTransform(transform);
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = el.offsetWidth;
|
||||
canvas.height = el.offsetHeight;
|
||||
draw.setMaxArea(canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
function transformPosition(p) {
|
||||
transform.x = p.x;
|
||||
transform.y = p.y;
|
||||
transform.k = p.k;
|
||||
}
|
||||
|
||||
function moveTo(callback, forceMove) {
|
||||
clearTimeout(movetoTimer);
|
||||
if (!forceMove && force.alpha() > 0.3) {
|
||||
movetoTimer = setTimeout(function timerOfMoveTo() {
|
||||
moveTo(callback);
|
||||
}, 300);
|
||||
return;
|
||||
}
|
||||
var result = callback();
|
||||
var x = result[0];
|
||||
var y = result[1];
|
||||
var k = result[2];
|
||||
var end = { k: k };
|
||||
|
||||
end.x = (canvas.width + sidebar.getWidth()) / 2 - x * k;
|
||||
end.y = canvas.height / 2 - y * k;
|
||||
|
||||
var start = { x: transform.x, y: transform.y, k: transform.k };
|
||||
|
||||
var interpolate = d3Interpolate.interpolateObject(start, end);
|
||||
|
||||
var timer = d3Timer.timer(function (t) {
|
||||
if (t >= ZOOM_ANIMATE_DURATION) {
|
||||
timer.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
var v = interpolate(d3Ease.easeQuadInOut(t / ZOOM_ANIMATE_DURATION));
|
||||
transformPosition(v);
|
||||
window.requestAnimationFrame(redraw);
|
||||
});
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
if (d3Selection.event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
var e = transform.invert([d3Selection.event.clientX, d3Selection.event.clientY]);
|
||||
var n = force.find(e[0], e[1], NODE_RADIUS_SELECT);
|
||||
|
||||
if (n !== undefined) {
|
||||
router.fullUrl({ node: n.o.node_id });
|
||||
return;
|
||||
}
|
||||
|
||||
e = { x: e[0], y: e[1] };
|
||||
|
||||
var closedLink;
|
||||
var radius = LINK_RADIUS_SELECT;
|
||||
intLinks
|
||||
.forEach(function (d) {
|
||||
var distance = math.distanceLink(e, d.source, d.target);
|
||||
if (distance < radius) {
|
||||
closedLink = d;
|
||||
radius = distance;
|
||||
}
|
||||
});
|
||||
|
||||
if (closedLink !== undefined) {
|
||||
router.fullUrl({ link: closedLink.o.id });
|
||||
}
|
||||
}
|
||||
|
||||
function redraw() {
|
||||
ctx.save();
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.translate(transform.x, transform.y);
|
||||
ctx.scale(transform.k, transform.k);
|
||||
|
||||
intLinks.forEach(draw.drawLink);
|
||||
intNodes.forEach(draw.drawNode);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
el = document.createElement('div');
|
||||
el.classList.add('graph');
|
||||
|
||||
forceLink = d3Force.forceLink()
|
||||
.distance(function (d) {
|
||||
if (d.o.type.indexOf('vpn') === 0) {
|
||||
return 0;
|
||||
}
|
||||
return 75;
|
||||
})
|
||||
.strength(function (d) {
|
||||
if (d.o.type.indexOf('vpn') === 0) {
|
||||
return 0.02;
|
||||
}
|
||||
return Math.max(0.5, d.o.source_tq);
|
||||
});
|
||||
|
||||
var zoom = d3Zoom.zoom()
|
||||
.scaleExtent([ZOOM_MIN, ZOOM_MAX])
|
||||
.on('zoom', function () {
|
||||
transform = d3Selection.event.transform;
|
||||
draw.setTransform(transform);
|
||||
redraw();
|
||||
});
|
||||
|
||||
force = d3Force.forceSimulation()
|
||||
.force('link', forceLink)
|
||||
.force('charge', d3Force.forceManyBody())
|
||||
.force('x', d3Force.forceX().strength(0.02))
|
||||
.force('y', d3Force.forceY().strength(0.02))
|
||||
.force('collide', d3Force.forceCollide())
|
||||
.on('tick', redraw)
|
||||
.alphaDecay(0.025);
|
||||
|
||||
var drag = d3Drag.drag()
|
||||
.subject(function () {
|
||||
var e = transform.invert([d3Selection.event.x, d3Selection.event.y]);
|
||||
var n = force.find(e[0], e[1], NODE_RADIUS_DRAG);
|
||||
|
||||
if (n !== undefined) {
|
||||
n.x = d3Selection.event.x;
|
||||
n.y = d3Selection.event.y;
|
||||
return n;
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.on('start', function () {
|
||||
if (!d3Selection.event.active) {
|
||||
force.alphaTarget(FORCE_ALPHA).restart();
|
||||
}
|
||||
d3Selection.event.subject.fx = transform.invertX(d3Selection.event.subject.x);
|
||||
d3Selection.event.subject.fy = transform.invertY(d3Selection.event.subject.y);
|
||||
})
|
||||
.on('drag', function () {
|
||||
d3Selection.event.subject.fx = transform.invertX(d3Selection.event.x);
|
||||
d3Selection.event.subject.fy = transform.invertY(d3Selection.event.y);
|
||||
})
|
||||
.on('end', function () {
|
||||
if (!d3Selection.event.active) {
|
||||
force.alphaTarget(0);
|
||||
}
|
||||
d3Selection.event.subject.fx = null;
|
||||
d3Selection.event.subject.fy = null;
|
||||
});
|
||||
|
||||
canvas = d3Selection.select(el)
|
||||
.append('canvas')
|
||||
.on('click', onClick)
|
||||
.call(drag)
|
||||
.call(zoom)
|
||||
.node();
|
||||
|
||||
ctx = canvas.getContext('2d');
|
||||
draw.setCTX(ctx);
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
resizeCanvas();
|
||||
redraw();
|
||||
});
|
||||
|
||||
self.setData = function setData(data) {
|
||||
intNodes = data.nodes.all.map(function (d) {
|
||||
var e = dictNodes[d.node_id];
|
||||
if (!e) {
|
||||
e = {};
|
||||
dictNodes[d.node_id] = e;
|
||||
}
|
||||
|
||||
e.o = d;
|
||||
|
||||
return e;
|
||||
});
|
||||
|
||||
intLinks = data.links.filter(function (d) {
|
||||
return data.nodeDict[d.source.node_id].is_online && data.nodeDict[d.target.node_id].is_online;
|
||||
}).map(function (d) {
|
||||
return {
|
||||
o: d,
|
||||
source: dictNodes[d.source.node_id],
|
||||
target: dictNodes[d.target.node_id],
|
||||
color: linkScale(d.source_tq),
|
||||
color_to: linkScale(d.target_tq)
|
||||
};
|
||||
});
|
||||
|
||||
force.nodes(intNodes);
|
||||
forceLink.links(intLinks);
|
||||
|
||||
force.alpha(initial).velocityDecay(0.15).restart();
|
||||
if (initial === 1.8) {
|
||||
initial = 0.5;
|
||||
}
|
||||
|
||||
resizeCanvas();
|
||||
};
|
||||
|
||||
self.resetView = function resetView() {
|
||||
moveTo(function calcToReset() {
|
||||
draw.setHighlight(null);
|
||||
return [0, 0, (ZOOM_MIN + config.forceGraph.zoomModifier) / 2];
|
||||
}, true);
|
||||
};
|
||||
|
||||
self.gotoNode = function gotoNode(d) {
|
||||
moveTo(function calcToNode() {
|
||||
draw.setHighlight({ type: 'node', id: d.node_id });
|
||||
var n = dictNodes[d.node_id];
|
||||
if (n) {
|
||||
return [n.x, n.y, (ZOOM_MAX + 1) / 2];
|
||||
}
|
||||
return self.resetView();
|
||||
});
|
||||
};
|
||||
|
||||
self.gotoLink = function gotoLink(d) {
|
||||
moveTo(function calcToLink() {
|
||||
draw.setHighlight({ type: 'link', id: d[0].id });
|
||||
var l = intLinks.find(function (link) {
|
||||
return link.o.id === d[0].id;
|
||||
});
|
||||
if (l) {
|
||||
return [(l.source.x + l.target.x) / 2, (l.source.y + l.target.y) / 2, (ZOOM_MAX / 2) + ZOOM_MIN];
|
||||
}
|
||||
return self.resetView();
|
||||
});
|
||||
};
|
||||
|
||||
self.gotoLocation = function gotoLocation() {
|
||||
// ignore
|
||||
};
|
||||
|
||||
self.destroy = function destroy() {
|
||||
force.stop();
|
||||
canvas.parentNode.removeChild(canvas);
|
||||
force = null;
|
||||
|
||||
if (el.parentNode) {
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
};
|
||||
|
||||
self.render = function render(d) {
|
||||
d.appendChild(el);
|
||||
resizeCanvas();
|
||||
};
|
||||
|
||||
return self;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,120 @@
|
|||
define(['helper'], function (helper) {
|
||||
var self = {};
|
||||
|
||||
var ctx;
|
||||
var width;
|
||||
var height;
|
||||
var transform;
|
||||
var highlight;
|
||||
|
||||
var NODE_RADIUS = 15;
|
||||
var LINE_RADIUS = 12;
|
||||
|
||||
function drawDetailNode(d) {
|
||||
if (transform.k > 1 && d.o.is_online) {
|
||||
helper.positionClients(ctx, d, Math.PI, d.o, 15);
|
||||
ctx.beginPath();
|
||||
var name = d.o.node_id;
|
||||
if (d.o) {
|
||||
name = d.o.hostname;
|
||||
}
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = config.forceGraph.labelColor;
|
||||
ctx.fillText(name, d.x, d.y + 20);
|
||||
}
|
||||
}
|
||||
|
||||
function drawHighlightNode(d) {
|
||||
if (highlight && highlight.type === 'node' && d.o.node_id === highlight.id) {
|
||||
ctx.arc(d.x, d.y, NODE_RADIUS * 1.5, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = config.forceGraph.highlightColor;
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
}
|
||||
}
|
||||
|
||||
function drawHighlightLink(d, to) {
|
||||
if (highlight && highlight.type === 'link' && d.o.id === highlight.id) {
|
||||
ctx.lineTo(to[0], to[1]);
|
||||
ctx.strokeStyle = config.forceGraph.highlightColor;
|
||||
ctx.lineWidth = LINE_RADIUS * 2;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.stroke();
|
||||
to = [d.source.x, d.source.y];
|
||||
}
|
||||
return to;
|
||||
}
|
||||
|
||||
self.drawNode = function drawNode(d) {
|
||||
if (d.x < transform.invertX(0) || d.y < transform.invertY(0) || transform.invertX(width) < d.x || transform.invertY(height) < d.y) {
|
||||
return;
|
||||
}
|
||||
ctx.beginPath();
|
||||
|
||||
drawHighlightNode(d);
|
||||
|
||||
if (d.o.is_online) {
|
||||
ctx.arc(d.x, d.y, 8, 0, 2 * Math.PI);
|
||||
if (d.o.is_gateway) {
|
||||
ctx.rect(d.x - 9, d.y - 9, 18, 18);
|
||||
}
|
||||
ctx.fillStyle = config.forceGraph.nodeColor;
|
||||
} else {
|
||||
ctx.arc(d.x, d.y, 6, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = config.forceGraph.nodeOfflineColor;
|
||||
}
|
||||
|
||||
ctx.fill();
|
||||
|
||||
drawDetailNode(d);
|
||||
};
|
||||
|
||||
self.drawLink = function drawLink(d) {
|
||||
var zero = transform.invert([0, 0]);
|
||||
var area = transform.invert([width, height]);
|
||||
if (d.source.x < zero[0] && d.target.x < zero[0] || d.source.y < zero[1] && d.target.y < zero[1] ||
|
||||
d.source.x > area[0] && d.target.x > area[0] || d.source.y > area[1] && d.target.y > area[1]) {
|
||||
return;
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(d.source.x, d.source.y);
|
||||
var to = [d.target.x, d.target.y];
|
||||
|
||||
to = drawHighlightLink(d, to);
|
||||
|
||||
var grd = ctx.createLinearGradient(d.source.x, d.source.y, d.target.x, d.target.y);
|
||||
grd.addColorStop(0.45, d.color);
|
||||
grd.addColorStop(0.55, d.color_to);
|
||||
|
||||
ctx.lineTo(to[0], to[1]);
|
||||
ctx.strokeStyle = grd;
|
||||
if (d.o.type.indexOf('vpn') === 0) {
|
||||
ctx.globalAlpha = 0.2;
|
||||
ctx.lineWidth = 1.5;
|
||||
} else {
|
||||
ctx.globalAlpha = 0.8;
|
||||
ctx.lineWidth = 2.5;
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = 1;
|
||||
};
|
||||
|
||||
self.setCTX = function setCTX(newValue) {
|
||||
ctx = newValue;
|
||||
};
|
||||
|
||||
self.setHighlight = function setHighlight(newValue) {
|
||||
highlight = newValue;
|
||||
};
|
||||
|
||||
self.setTransform = function setTransform(newValue) {
|
||||
transform = newValue;
|
||||
};
|
||||
|
||||
self.setMaxArea = function setMaxArea(newWidth, newHeight) {
|
||||
width = newWidth;
|
||||
height = newHeight;
|
||||
};
|
||||
|
||||
return self;
|
||||
});
|
|
@ -0,0 +1,143 @@
|
|||
define(['d3-interpolate', 'map', 'sidebar', 'tabs', 'container', 'legend',
|
||||
'linklist', 'nodelist', 'simplenodelist', 'infobox/main',
|
||||
'proportions', 'forcegraph', 'title', 'about', 'datadistributor',
|
||||
'filters/filtergui', 'filters/hostname', 'helper'],
|
||||
function (d3Interpolate, Map, Sidebar, Tabs, Container, Legend, Linklist,
|
||||
Nodelist, SimpleNodelist, Infobox, Proportions, ForceGraph,
|
||||
Title, About, DataDistributor, FilterGUI, HostnameFilter, helper) {
|
||||
'use strict';
|
||||
|
||||
return function (language) {
|
||||
var self = this;
|
||||
var content;
|
||||
var contentDiv;
|
||||
|
||||
var linkScale = d3Interpolate.interpolate(config.map.tqFrom, config.map.tqTo);
|
||||
var sidebar;
|
||||
|
||||
var buttons = document.createElement('div');
|
||||
buttons.classList.add('buttons');
|
||||
|
||||
var fanout = new DataDistributor();
|
||||
var fanoutUnfiltered = new DataDistributor();
|
||||
fanoutUnfiltered.add(fanout);
|
||||
|
||||
function removeContent() {
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.removeTarget(content);
|
||||
fanout.remove(content);
|
||||
|
||||
content.destroy();
|
||||
|
||||
content = null;
|
||||
}
|
||||
|
||||
function addContent(K) {
|
||||
removeContent();
|
||||
|
||||
content = new K(linkScale, sidebar, buttons);
|
||||
content.render(contentDiv);
|
||||
|
||||
fanout.add(content);
|
||||
router.addTarget(content);
|
||||
}
|
||||
|
||||
function mkView(K) {
|
||||
return function () {
|
||||
addContent(K);
|
||||
};
|
||||
}
|
||||
|
||||
var loader = document.getElementsByClassName('loader')[0];
|
||||
loader.classList.add('hide');
|
||||
|
||||
contentDiv = document.createElement('div');
|
||||
contentDiv.classList.add('content');
|
||||
document.body.appendChild(contentDiv);
|
||||
|
||||
sidebar = new Sidebar(document.body);
|
||||
|
||||
contentDiv.appendChild(buttons);
|
||||
|
||||
var buttonToggle = document.createElement('button');
|
||||
buttonToggle.classList.add('ion-eye');
|
||||
buttonToggle.setAttribute('aria-label', _.t('button.switchView'));
|
||||
buttonToggle.onclick = function onclick() {
|
||||
var data;
|
||||
if (content.constructor === Map) {
|
||||
data = { view: 'graph', lat: undefined, lng: undefined, zoom: undefined };
|
||||
} else {
|
||||
data = { view: 'map' };
|
||||
}
|
||||
router.fullUrl(data, false, true);
|
||||
};
|
||||
|
||||
buttons.appendChild(buttonToggle);
|
||||
|
||||
if (config.fullscreen || config.fullscreenFrame && window.frameElement) {
|
||||
var buttonFullscreen = document.createElement('button');
|
||||
buttonFullscreen.classList.add('ion-full-enter');
|
||||
buttonFullscreen.setAttribute('aria-label', _.t('button.fullscreen'));
|
||||
buttonFullscreen.onclick = function onclick() {
|
||||
helper.fullscreen(buttonFullscreen);
|
||||
};
|
||||
|
||||
buttons.appendChild(buttonFullscreen);
|
||||
}
|
||||
|
||||
var title = new Title();
|
||||
|
||||
var header = new Container('header');
|
||||
var infobox = new Infobox(sidebar, linkScale);
|
||||
var tabs = new Tabs();
|
||||
var overview = new Container();
|
||||
var legend = new Legend(language);
|
||||
var newnodeslist = new SimpleNodelist('new', 'firstseen', _.t('node.new'));
|
||||
var lostnodeslist = new SimpleNodelist('lost', 'lastseen', _.t('node.missing'));
|
||||
var nodelist = new Nodelist();
|
||||
var linklist = new Linklist(linkScale);
|
||||
var statistics = new Proportions(fanout);
|
||||
var about = new About();
|
||||
|
||||
fanoutUnfiltered.add(legend);
|
||||
fanoutUnfiltered.add(newnodeslist);
|
||||
fanoutUnfiltered.add(lostnodeslist);
|
||||
fanoutUnfiltered.add(infobox);
|
||||
fanout.add(nodelist);
|
||||
fanout.add(linklist);
|
||||
fanout.add(statistics);
|
||||
|
||||
sidebar.add(header);
|
||||
header.add(legend);
|
||||
|
||||
overview.add(newnodeslist);
|
||||
overview.add(lostnodeslist);
|
||||
|
||||
var filterGUI = new FilterGUI(fanout);
|
||||
fanout.watchFilters(filterGUI);
|
||||
header.add(filterGUI);
|
||||
|
||||
var hostnameFilter = new HostnameFilter();
|
||||
fanout.addFilter(hostnameFilter);
|
||||
|
||||
sidebar.add(tabs);
|
||||
tabs.add('sidebar.actual', overview);
|
||||
tabs.add('node.nodes', nodelist);
|
||||
tabs.add('node.links', linklist);
|
||||
tabs.add('sidebar.stats', statistics);
|
||||
tabs.add('sidebar.about', about);
|
||||
|
||||
router.addTarget(title);
|
||||
router.addTarget(infobox);
|
||||
|
||||
router.addView('map', mkView(Map));
|
||||
router.addView('graph', mkView(ForceGraph));
|
||||
|
||||
self.setData = fanoutUnfiltered.setData;
|
||||
|
||||
return self;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
define(['helper', 'snabbdom'], function (helper, V) {
|
||||
'use strict';
|
||||
V = V.default;
|
||||
|
||||
function showStatImg(img, o, d, time) {
|
||||
var subst = {
|
||||
'{SOURCE_ID}': d.source.node_id,
|
||||
'{SOURCE_NAME}': d.source.hostname.replace(/[^a-z0-9\-]/ig, '_'),
|
||||
'{SOURCE_ADDR}': d.source_addr,
|
||||
'{SOURCE_MAC}': d.source_mac ? d.source_mac : d.source_addr,
|
||||
'{TARGET_ID}': d.target.node_id,
|
||||
'{TARGET_NAME}': d.target.hostname.replace(/[^a-z0-9\-]/ig, '_'),
|
||||
'{TARGET_ADDR}': d.target_addr,
|
||||
'{TARGET_MAC}': d.target_mac ? d.target_mac : d.target_addr,
|
||||
'{TYPE}': d.type,
|
||||
'{TIME}': time,
|
||||
'{LOCALE}': _.locale()
|
||||
};
|
||||
|
||||
img.push(V.h('h4', helper.listReplace(o.name, subst)));
|
||||
img.push(helper.showStat(V, o, subst));
|
||||
}
|
||||
|
||||
return function (el, d, linkScale) {
|
||||
var self = this;
|
||||
var header = document.createElement('div');
|
||||
var table = document.createElement('table');
|
||||
var images = document.createElement('div');
|
||||
el.appendChild(header);
|
||||
el.appendChild(table);
|
||||
el.appendChild(images);
|
||||
|
||||
self.render = function render() {
|
||||
var children = [];
|
||||
var img = [];
|
||||
var time = d[0].target.lastseen.format('DDMMYYYYHmmss');
|
||||
|
||||
header = V.patch(header, V.h('div', V.h('h2', [
|
||||
V.h('a', {
|
||||
props: { href: router.generateLink({ node: d[0].source.node_id }) }
|
||||
}, d[0].source.hostname),
|
||||
V.h('span', ' - '),
|
||||
V.h('a', {
|
||||
props: { href: router.generateLink({ node: d[0].target.node_id }) }
|
||||
}, d[0].target.hostname)
|
||||
])));
|
||||
|
||||
helper.attributeEntry(V, children, 'node.hardware', (d[0].source.model ? d[0].source.model + ' – ' : '') +
|
||||
(d[0].target.model ? d[0].target.model : ''));
|
||||
helper.attributeEntry(V, children, 'node.distance', helper.showDistance(d[0]));
|
||||
|
||||
d.forEach(function (link) {
|
||||
children.push(V.h('tr', { props: { className: 'header' } }, [
|
||||
V.h('th', _.t('node.connectionType')),
|
||||
V.h('th', link.type)
|
||||
]));
|
||||
helper.attributeEntry(V, children, 'node.tq', V.h('span',
|
||||
{ style: { color: linkScale((link.source_tq + link.target_tq) / 2) } },
|
||||
helper.showTq(link.source_tq) + ' - ' + helper.showTq(link.target_tq))
|
||||
);
|
||||
|
||||
if (config.linkTypeInfos) {
|
||||
config.linkTypeInfos.forEach(function (o) {
|
||||
showStatImg(img, o, link, time);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (config.linkInfos) {
|
||||
config.linkInfos.forEach(function (o) {
|
||||
showStatImg(img, o, d[0], time);
|
||||
});
|
||||
}
|
||||
|
||||
var elNew = V.h('table', children);
|
||||
table = V.patch(table, elNew);
|
||||
table.elm.classList.add('attributes');
|
||||
images = V.patch(images, V.h('div', img));
|
||||
};
|
||||
|
||||
self.setData = function setData(data) {
|
||||
d = data.links.filter(function (a) {
|
||||
return a.id === d[0].id;
|
||||
});
|
||||
self.render();
|
||||
};
|
||||
return self;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
define(['helper'], function (helper) {
|
||||
'use strict';
|
||||
|
||||
return function (el, d) {
|
||||
var sidebarTitle = document.createElement('h2');
|
||||
sidebarTitle.textContent = _.t('location.location');
|
||||
el.appendChild(sidebarTitle);
|
||||
|
||||
helper.getJSON(config.reverseGeocodingApi + '?format=json&lat=' + d.lat + '&lon=' + d.lng + '&zoom=18&addressdetails=0&accept-language=' + _.locale())
|
||||
.then(function (result) {
|
||||
if (result.display_name) {
|
||||
sidebarTitle.outerHTML += '<p>' + result.display_name + '</p>';
|
||||
}
|
||||
});
|
||||
|
||||
var editLat = document.createElement('input');
|
||||
editLat.setAttribute('aria-label', _.t('location.latitude'));
|
||||
editLat.type = 'text';
|
||||
editLat.value = d.lat.toFixed(9);
|
||||
el.appendChild(createBox('lat', _.t('location.latitude'), editLat));
|
||||
|
||||
var editLng = document.createElement('input');
|
||||
editLng.setAttribute('aria-label', _.t('location.longitude'));
|
||||
editLng.type = 'text';
|
||||
editLng.value = d.lng.toFixed(9);
|
||||
el.appendChild(createBox('lng', _.t('location.longitude'), editLng));
|
||||
|
||||
var editUci = document.createElement('textarea');
|
||||
editUci.setAttribute('aria-label', 'Uci');
|
||||
editUci.value =
|
||||
"uci set gluon-node-info.@location[0]='location'; " +
|
||||
"uci set gluon-node-info.@location[0].share_location='1';" +
|
||||
"uci set gluon-node-info.@location[0].latitude='" + d.lat.toFixed(9) + "';" +
|
||||
"uci set gluon-node-info.@location[0].longitude='" + d.lng.toFixed(9) + "';" +
|
||||
'uci commit gluon-node-info';
|
||||
|
||||
el.appendChild(createBox('uci', 'Uci', editUci));
|
||||
|
||||
function createBox(name, title, inputElem) {
|
||||
var box = document.createElement('div');
|
||||
var heading = document.createElement('h3');
|
||||
heading.textContent = title;
|
||||
box.appendChild(heading);
|
||||
var btn = document.createElement('button');
|
||||
btn.classList.add('ion-clipboard');
|
||||
btn.title = _.t('location.copy');
|
||||
btn.setAttribute('aria-label', _.t('location.copy'));
|
||||
btn.onclick = function onclick() {
|
||||
copy2clip(inputElem.id);
|
||||
};
|
||||
inputElem.id = 'location-' + name;
|
||||
inputElem.readOnly = true;
|
||||
var line = document.createElement('p');
|
||||
line.appendChild(inputElem);
|
||||
line.appendChild(btn);
|
||||
box.appendChild(line);
|
||||
box.id = 'box-' + name;
|
||||
return box;
|
||||
}
|
||||
|
||||
function copy2clip(id) {
|
||||
var copyField = document.querySelector('#' + id);
|
||||
copyField.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
define(['infobox/link', 'infobox/node', 'infobox/location'], function (Link, Node, location) {
|
||||
'use strict';
|
||||
|
||||
return function (sidebar, linkScale) {
|
||||
var self = this;
|
||||
var el;
|
||||
var node;
|
||||
var link;
|
||||
|
||||
function destroy() {
|
||||
if (el && el.parentNode) {
|
||||
el.parentNode.removeChild(el);
|
||||
node = link = el = undefined;
|
||||
sidebar.reveal();
|
||||
}
|
||||
}
|
||||
|
||||
function create() {
|
||||
destroy();
|
||||
sidebar.ensureVisible();
|
||||
sidebar.hide();
|
||||
|
||||
el = document.createElement('div');
|
||||
sidebar.container.children[1].appendChild(el);
|
||||
|
||||
el.scrollIntoView(false);
|
||||
el.classList.add('infobox');
|
||||
el.destroy = destroy;
|
||||
|
||||
var closeButton = document.createElement('button');
|
||||
closeButton.classList.add('close');
|
||||
closeButton.classList.add('ion-close');
|
||||
closeButton.setAttribute('aria-label', _.t('close'));
|
||||
closeButton.onclick = function () {
|
||||
router.fullUrl();
|
||||
};
|
||||
el.appendChild(closeButton);
|
||||
}
|
||||
|
||||
self.resetView = destroy;
|
||||
|
||||
self.gotoNode = function gotoNode(d, nodeDict) {
|
||||
create();
|
||||
node = new Node(el, d, linkScale, nodeDict);
|
||||
node.render();
|
||||
};
|
||||
|
||||
self.gotoLink = function gotoLink(d) {
|
||||
create();
|
||||
link = new Link(el, d, linkScale);
|
||||
link.render();
|
||||
};
|
||||
|
||||
self.gotoLocation = function gotoLocation(d) {
|
||||
create();
|
||||
location(el, d);
|
||||
};
|
||||
|
||||
self.setData = function setData(d) {
|
||||
if (typeof node === 'object') {
|
||||
node.setData(d);
|
||||
}
|
||||
if (typeof link === 'object') {
|
||||
link.setData(d);
|
||||
}
|
||||
};
|
||||
|
||||
return self;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,194 @@
|
|||
define(['sorttable', 'snabbdom', 'd3-interpolate', 'helper', 'utils/node'],
|
||||
function (SortTable, V, d3Interpolate, helper, nodef) {
|
||||
'use strict';
|
||||
V = V.default;
|
||||
|
||||
function showStatImg(o, d) {
|
||||
var subst = {
|
||||
'{NODE_ID}': d.node_id,
|
||||
'{NODE_NAME}': d.hostname.replace(/[^a-z0-9\-]/ig, '_'),
|
||||
'{TIME}': d.lastseen.format('DDMMYYYYHmmss'),
|
||||
'{LOCALE}': _.locale()
|
||||
};
|
||||
return helper.showStat(V, o, subst);
|
||||
}
|
||||
|
||||
return function (el, d, linkScale, nodeDict) {
|
||||
function nodeLink(node) {
|
||||
return V.h('a', {
|
||||
props: {
|
||||
className: node.is_online ? 'online' : 'offline',
|
||||
href: router.generateLink({ node: node.node_id })
|
||||
}, on: {
|
||||
click: function (e) {
|
||||
router.fullUrl({ node: node.node_id }, e);
|
||||
}
|
||||
}
|
||||
}, node.hostname);
|
||||
}
|
||||
|
||||
function nodeIdLink(nodeId) {
|
||||
if (nodeDict[nodeId]) {
|
||||
return nodeLink(nodeDict[nodeId]);
|
||||
}
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
function showGateway(node) {
|
||||
var gatewayCols = [
|
||||
V.h('span', [
|
||||
nodeIdLink(node.gateway_nexthop),
|
||||
V.h('br'),
|
||||
_.t('node.nexthop')
|
||||
]),
|
||||
V.h('span', { props: { className: 'ion-arrow-right-c' } }),
|
||||
V.h('span', [
|
||||
nodeIdLink(node.gateway),
|
||||
V.h('br'),
|
||||
'IPv4'
|
||||
])
|
||||
];
|
||||
|
||||
if (node.gateway6 !== undefined) {
|
||||
gatewayCols.push(V.h('span', [
|
||||
nodeIdLink(node.gateway6),
|
||||
V.h('br'),
|
||||
'IPv6'
|
||||
]));
|
||||
}
|
||||
|
||||
return V.h('td', { props: { className: 'gateway' } }, gatewayCols);
|
||||
}
|
||||
|
||||
function renderNeighbourRow(n) {
|
||||
var icons = [V.h('span', { props: { className: 'icon ion-' + (n.link.type.indexOf('wifi') === 0 ? 'wifi' : 'share-alt'), title: _.t(n.link.type) } })];
|
||||
if (helper.hasLocation(n.node)) {
|
||||
icons.push(V.h('span', { props: { className: 'ion-location', title: _.t('location.location') } }));
|
||||
}
|
||||
|
||||
return V.h('tr', [
|
||||
V.h('td', icons),
|
||||
V.h('td', nodeLink(n.node)),
|
||||
V.h('td', n.node.clients),
|
||||
V.h('td', [V.h('a', {
|
||||
style: {
|
||||
color: linkScale((n.link.source_tq + n.link.target_tq) / 2)
|
||||
},
|
||||
props: {
|
||||
title: n.link.source.hostname + ' - ' + n.link.target.hostname,
|
||||
href: router.generateLink({ link: n.link.id })
|
||||
}, on: {
|
||||
click: function (e) {
|
||||
router.fullUrl({ link: n.link.id }, e);
|
||||
}
|
||||
}
|
||||
}, helper.showTq(n.link.source_tq) + ' - ' + helper.showTq(n.link.target_tq))]),
|
||||
V.h('td', helper.showDistance(n.link))
|
||||
]);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var header = document.createElement('h2');
|
||||
var table = document.createElement('table');
|
||||
var images = document.createElement('div');
|
||||
var neighbours = document.createElement('h3');
|
||||
var headings = [{
|
||||
name: '',
|
||||
sort: function (a, b) {
|
||||
return a.link.type.localeCompare(b.link.type);
|
||||
}
|
||||
}, {
|
||||
name: 'node.nodes',
|
||||
sort: function (a, b) {
|
||||
return a.node.hostname.localeCompare(b.node.hostname);
|
||||
},
|
||||
reverse: false
|
||||
}, {
|
||||
name: 'node.clients',
|
||||
class: 'ion-people',
|
||||
sort: function (a, b) {
|
||||
return a.node.clients - b.node.clients;
|
||||
},
|
||||
reverse: true
|
||||
}, {
|
||||
name: 'node.tq',
|
||||
class: 'ion-connection-bars',
|
||||
sort: function (a, b) {
|
||||
return a.link.source_tq - b.link.source_tq;
|
||||
},
|
||||
reverse: true
|
||||
}, {
|
||||
name: 'node.distance',
|
||||
class: 'ion-arrow-resize',
|
||||
sort: function (a, b) {
|
||||
return (a.link.distance === undefined ? -1 : a.link.distance) -
|
||||
(b.link.distance === undefined ? -1 : b.link.distance);
|
||||
},
|
||||
reverse: true
|
||||
}];
|
||||
var tableNeighbour = new SortTable(headings, 1, renderNeighbourRow);
|
||||
|
||||
el.appendChild(header);
|
||||
el.appendChild(table);
|
||||
el.appendChild(neighbours);
|
||||
el.appendChild(tableNeighbour.el);
|
||||
el.appendChild(images);
|
||||
|
||||
self.render = function render() {
|
||||
V.patch(header, V.h('h2', d.hostname));
|
||||
|
||||
var children = [];
|
||||
|
||||
config.nodeAttr.forEach(function (row) {
|
||||
var field = d[row.value];
|
||||
if (typeof row.value === 'function') {
|
||||
field = row.value(d, nodeDict);
|
||||
} else if (nodef['show' + row.value] !== undefined) {
|
||||
field = nodef['show' + row.value](d);
|
||||
}
|
||||
|
||||
if (field) {
|
||||
if (typeof field !== 'object') {
|
||||
field = V.h('td', field);
|
||||
}
|
||||
children.push(V.h('tr', [
|
||||
row.name !== undefined ? V.h('th', _.t(row.name)) : null,
|
||||
field
|
||||
]));
|
||||
}
|
||||
});
|
||||
|
||||
children.push(V.h('tr', [
|
||||
V.h('th', _.t('node.gateway')),
|
||||
showGateway(d)
|
||||
]));
|
||||
|
||||
var elNew = V.h('table', children);
|
||||
table = V.patch(table, elNew);
|
||||
table.elm.classList.add('attributes');
|
||||
|
||||
V.patch(neighbours, V.h('h3', _.t('node.link', d.neighbours.length) + ' (' + d.neighbours.length + ')'));
|
||||
if (d.neighbours.length > 0) {
|
||||
tableNeighbour.setData(d.neighbours);
|
||||
tableNeighbour.el.elm.classList.add('node-links');
|
||||
}
|
||||
|
||||
if (config.nodeInfos) {
|
||||
var img = [];
|
||||
config.nodeInfos.forEach(function (nodeInfo) {
|
||||
img.push(V.h('h4', nodeInfo.name));
|
||||
img.push(showStatImg(nodeInfo, d));
|
||||
});
|
||||
images = V.patch(images, V.h('div', img));
|
||||
}
|
||||
};
|
||||
|
||||
self.setData = function setData(data) {
|
||||
if (data.nodeDict[d.node_id]) {
|
||||
d = data.nodeDict[d.node_id];
|
||||
}
|
||||
self.render();
|
||||
};
|
||||
return self;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
define(['helper'], function (helper) {
|
||||
'use strict';
|
||||
|
||||
return function (language) {
|
||||
var self = this;
|
||||
var stats = document.createTextNode('');
|
||||
var timestamp = document.createTextNode('');
|
||||
|
||||
self.setData = function setData(d) {
|
||||
var totalNodes = Object.keys(d.nodeDict).length;
|
||||
var totalOnlineNodes = d.nodes.online.length;
|
||||
var totalClients = helper.sum(d.nodes.online.map(function (n) {
|
||||
return n.clients;
|
||||
}));
|
||||
var totalGateways = helper.sum(d.nodes.online.filter(function (n) {
|
||||
return n.is_gateway;
|
||||
}).map(helper.one));
|
||||
|
||||
stats.textContent = _.t('sidebar.nodes', { total: totalNodes, online: totalOnlineNodes }) + ' ' +
|
||||
_.t('sidebar.clients', { smart_count: totalClients }) + ' ' +
|
||||
_.t('sidebar.gateway', { smart_count: totalGateways });
|
||||
|
||||
timestamp.textContent = _.t('sidebar.lastUpdate') + ' ' + d.timestamp.fromNow();
|
||||
};
|
||||
|
||||
self.render = function render(el) {
|
||||
var h1 = document.createElement('h1');
|
||||
h1.textContent = config.siteName;
|
||||
el.appendChild(h1);
|
||||
|
||||
language.languageSelect(el);
|
||||
|
||||
var p = document.createElement('p');
|
||||
p.classList.add('legend');
|
||||
|
||||
p.appendChild(stats);
|
||||
p.appendChild(document.createElement('br'));
|
||||
p.appendChild(timestamp);
|
||||
|
||||
if (config.linkList) {
|
||||
p.appendChild(document.createElement('br'));
|
||||
config.linkList.forEach(function (link) {
|
||||
var a = document.createElement('a');
|
||||
a.innerText = link.title;
|
||||
a.href = link.href;
|
||||
p.appendChild(a);
|
||||
});
|
||||
}
|
||||
|
||||
el.appendChild(p);
|
||||
};
|
||||
|
||||
return self;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
|
||||
'use strict';
|
||||
V = V.default;
|
||||
|
||||
function linkName(d) {
|
||||
return (d.source ? d.source.hostname : d.source.id) + ' – ' + d.target.hostname;
|
||||
}
|
||||
|
||||
var headings = [{
|
||||
name: '',
|
||||
sort: function (a, b) {
|
||||
return a.type.localeCompare(b.type);
|
||||
}
|
||||
}, {
|
||||
name: 'node.nodes',
|
||||
sort: function (a, b) {
|
||||
return linkName(a).localeCompare(linkName(b));
|
||||
},
|
||||
reverse: false
|
||||
}, {
|
||||
name: 'node.tq',
|
||||
class: 'ion-connection-bars',
|
||||
sort: function (a, b) {
|
||||
return (a.source_tq + a.target_tq) / 2 - (b.source_tq + b.target_tq) / 2;
|
||||
},
|
||||
reverse: true
|
||||
}, {
|
||||
name: 'node.distance',
|
||||
class: 'ion-arrow-resize',
|
||||
sort: function (a, b) {
|
||||
return (a.distance === undefined ? -1 : a.distance) -
|
||||
(b.distance === undefined ? -1 : b.distance);
|
||||
},
|
||||
reverse: true
|
||||
}];
|
||||
|
||||
return function (linkScale) {
|
||||
var table = new SortTable(headings, 3, renderRow);
|
||||
|
||||
function renderRow(d) {
|
||||
var td1Content = [V.h('a', {
|
||||
props: {
|
||||
href: router.generateLink({ link: d.id })
|
||||
}, on: {
|
||||
click: function (e) {
|
||||
router.fullUrl({ link: d.id }, e);
|
||||
}
|
||||
}
|
||||
}, linkName(d))];
|
||||
|
||||
return V.h('tr', [
|
||||
V.h('td', V.h('span', { props: { className: 'icon ion-' + (d.type.indexOf('wifi') === 0 ? 'wifi' : 'share-alt'), title: _.t(d.type) } })),
|
||||
V.h('td', td1Content),
|
||||
V.h('td', { style: { color: linkScale((d.source_tq + d.target_tq) / 2) } }, helper.showTq(d.source_tq) + ' - ' + helper.showTq(d.target_tq)),
|
||||
V.h('td', helper.showDistance(d))
|
||||
]);
|
||||
}
|
||||
|
||||
this.render = function render(d) {
|
||||
var h2 = document.createElement('h2');
|
||||
h2.textContent = _.t('node.links');
|
||||
d.appendChild(h2);
|
||||
table.el.elm.classList.add('link-list');
|
||||
d.appendChild(table.el.elm);
|
||||
};
|
||||
|
||||
this.setData = function setData(d) {
|
||||
table.setData(d.links);
|
||||
};
|
||||
};
|
||||
});
|
|
@ -0,0 +1,123 @@
|
|||
define(['moment', 'utils/router', 'leaflet', 'gui', 'helper', 'utils/language'],
|
||||
function (moment, Router, L, GUI, helper, Language) {
|
||||
'use strict';
|
||||
|
||||
return function () {
|
||||
function handleData(data) {
|
||||
var timestamp;
|
||||
var nodes = [];
|
||||
var links = [];
|
||||
var nodeDict = {};
|
||||
|
||||
for (var i = 0; i < data.length; ++i) {
|
||||
nodes = nodes.concat(data[i].nodes);
|
||||
timestamp = data[i].timestamp;
|
||||
links = links.concat(data[i].links);
|
||||
}
|
||||
|
||||
nodes.forEach(function (node) {
|
||||
node.firstseen = moment.utc(node.firstseen).local();
|
||||
node.lastseen = moment.utc(node.lastseen).local();
|
||||
});
|
||||
|
||||
var age = moment().subtract(config.maxAge, 'days');
|
||||
|
||||
var online = nodes.filter(function (d) {
|
||||
return d.is_online;
|
||||
});
|
||||
var offline = nodes.filter(function (d) {
|
||||
return !d.is_online;
|
||||
});
|
||||
|
||||
var newnodes = helper.limit('firstseen', age, helper.sortByKey('firstseen', online));
|
||||
var lostnodes = helper.limit('lastseen', age, helper.sortByKey('lastseen', offline));
|
||||
|
||||
nodes.forEach(function (d) {
|
||||
d.neighbours = [];
|
||||
nodeDict[d.node_id] = d;
|
||||
});
|
||||
|
||||
links.forEach(function (d) {
|
||||
d.source = nodeDict[d.source];
|
||||
d.target = nodeDict[d.target];
|
||||
|
||||
d.id = [d.source.node_id, d.target.node_id].join('-');
|
||||
d.source.neighbours.push({ node: d.target, link: d });
|
||||
d.target.neighbours.push({ node: d.source, link: d });
|
||||
|
||||
try {
|
||||
d.latlngs = [];
|
||||
d.latlngs.push(L.latLng(d.source.location.latitude, d.source.location.longitude));
|
||||
d.latlngs.push(L.latLng(d.target.location.latitude, d.target.location.longitude));
|
||||
|
||||
d.distance = d.latlngs[0].distanceTo(d.latlngs[1]);
|
||||
} catch (e) {
|
||||
// ignore exception
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
now: moment(),
|
||||
timestamp: moment.utc(timestamp).local(),
|
||||
nodes: {
|
||||
all: nodes,
|
||||
online: online,
|
||||
offline: offline,
|
||||
new: newnodes,
|
||||
lost: lostnodes
|
||||
},
|
||||
links: links,
|
||||
nodeDict: nodeDict
|
||||
};
|
||||
}
|
||||
|
||||
var language = new Language();
|
||||
window.router = new Router(language);
|
||||
|
||||
config.dataPath.forEach(function (d, i) {
|
||||
config.dataPath[i] += 'meshviewer.json';
|
||||
});
|
||||
|
||||
language.init(router);
|
||||
|
||||
function update() {
|
||||
return Promise.all(config.dataPath.map(helper.getJSON))
|
||||
.then(handleData);
|
||||
}
|
||||
|
||||
update()
|
||||
.then(function (d) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var count = 0;
|
||||
(function waitForLanguage() {
|
||||
if (Object.keys(_.phrases).length > 0) {
|
||||
resolve(d);
|
||||
} else if (count > 500) {
|
||||
reject(new Error('translation not loaded after 10 seconds'));
|
||||
} else {
|
||||
setTimeout(waitForLanguage.bind(this), 20);
|
||||
}
|
||||
count++;
|
||||
})();
|
||||
});
|
||||
})
|
||||
.then(function (d) {
|
||||
var gui = new GUI(language);
|
||||
gui.setData(d);
|
||||
router.setData(d);
|
||||
router.resolve();
|
||||
|
||||
window.setInterval(function () {
|
||||
update().then(function (n) {
|
||||
gui.setData(n);
|
||||
router.setData(n);
|
||||
});
|
||||
}, 60000);
|
||||
})
|
||||
.catch(function (e) {
|
||||
document.querySelector('.loader').innerHTML += e.message
|
||||
+ '<br /><br /><button onclick="location.reload(true)" class="btn text" aria-label="Try to reload">Try to reload</button><br /> or report to your community';
|
||||
console.warn(e);
|
||||
});
|
||||
};
|
||||
});
|
|
@ -0,0 +1,251 @@
|
|||
define(['map/clientlayer', 'map/labellayer', 'map/button', 'leaflet', 'map/activearea'],
|
||||
function (ClientLayer, LabelLayer, Button, L) {
|
||||
'use strict';
|
||||
|
||||
var options = {
|
||||
worldCopyJump: true,
|
||||
zoomControl: true,
|
||||
minZoom: 0
|
||||
};
|
||||
|
||||
return function (linkScale, sidebar, buttons) {
|
||||
var self = this;
|
||||
var savedView;
|
||||
|
||||
var map;
|
||||
var layerControl;
|
||||
var baseLayers = {};
|
||||
|
||||
function saveView() {
|
||||
savedView = {
|
||||
center: map.getCenter(),
|
||||
zoom: map.getZoom()
|
||||
};
|
||||
}
|
||||
|
||||
function contextMenuOpenLayerMenu() {
|
||||
document.querySelector('.leaflet-control-layers').classList.add('leaflet-control-layers-expanded');
|
||||
}
|
||||
|
||||
function mapActiveArea() {
|
||||
map.setActiveArea({
|
||||
position: 'absolute',
|
||||
left: sidebar.getWidth() + 'px',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0
|
||||
});
|
||||
}
|
||||
|
||||
function setActiveArea() {
|
||||
setTimeout(mapActiveArea, 300);
|
||||
}
|
||||
|
||||
var el = document.createElement('div');
|
||||
el.classList.add('map');
|
||||
|
||||
map = L.map(el, options);
|
||||
mapActiveArea();
|
||||
|
||||
var now = new Date();
|
||||
config.mapLayers.forEach(function (item, i) {
|
||||
if ((typeof item.config.start === 'number' && item.config.start <= now.getHours()) || (typeof item.config.end === 'number' && item.config.end > now.getHours())) {
|
||||
item.config.order = item.config.start * -1;
|
||||
} else {
|
||||
item.config.order = i;
|
||||
}
|
||||
});
|
||||
|
||||
config.mapLayers = config.mapLayers.sort(function (a, b) {
|
||||
return a.config.order - b.config.order;
|
||||
});
|
||||
|
||||
var layers = config.mapLayers.map(function (d) {
|
||||
return {
|
||||
'name': d.name,
|
||||
'layer': L.tileLayer(d.url.replace('{format}', document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') === 0 ? 'webp' : 'png'), d.config)
|
||||
};
|
||||
});
|
||||
|
||||
map.addLayer(layers[0].layer);
|
||||
|
||||
layers.forEach(function (d) {
|
||||
baseLayers[d.name] = d.layer;
|
||||
});
|
||||
|
||||
var button = new Button(map, buttons);
|
||||
|
||||
map.on('locationfound', button.locationFound);
|
||||
map.on('locationerror', button.locationError);
|
||||
map.on('dragend', saveView);
|
||||
map.on('contextmenu', contextMenuOpenLayerMenu);
|
||||
|
||||
if (config.geo) {
|
||||
[].forEach.call(config.geo, function (geo) {
|
||||
geo.json().then(function (result) {
|
||||
if (result) {
|
||||
L.geoJSON(result, geo.option).addTo(map);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
button.init();
|
||||
|
||||
layerControl = L.control.layers(baseLayers, [], { position: 'bottomright' });
|
||||
layerControl.addTo(map);
|
||||
|
||||
map.zoomControl.setPosition('topright');
|
||||
|
||||
var clientLayer = new ClientLayer({ minZoom: config.clientZoom });
|
||||
clientLayer.addTo(map);
|
||||
clientLayer.setZIndex(5);
|
||||
|
||||
var labelLayer = new LabelLayer({ minZoom: config.labelZoom });
|
||||
labelLayer.addTo(map);
|
||||
labelLayer.setZIndex(6);
|
||||
|
||||
sidebar.button.addEventListener('visibility', setActiveArea);
|
||||
|
||||
map.on('zoom', function () {
|
||||
clientLayer.redraw();
|
||||
labelLayer.redraw();
|
||||
});
|
||||
|
||||
map.on('baselayerchange', function (e) {
|
||||
map.options.maxZoom = e.layer.options.maxZoom;
|
||||
clientLayer.options.maxZoom = map.options.maxZoom;
|
||||
labelLayer.options.maxZoom = map.options.maxZoom;
|
||||
if (map.getZoom() > map.options.maxZoom) {
|
||||
map.setZoom(map.options.maxZoom);
|
||||
}
|
||||
|
||||
var style = document.querySelector('.css-mode:not([media="not"])');
|
||||
if (style && e.layer.options.mode !== '' && !style.classList.contains(e.layer.options.mode)) {
|
||||
style.media = 'not';
|
||||
labelLayer.updateLayer();
|
||||
}
|
||||
if (e.layer.options.mode) {
|
||||
var newStyle = document.querySelector('.css-mode.' + e.layer.options.mode);
|
||||
newStyle.media = '';
|
||||
newStyle.appendChild(document.createTextNode(''));
|
||||
labelLayer.updateLayer();
|
||||
}
|
||||
});
|
||||
|
||||
map.on('load', function () {
|
||||
var inputs = document.querySelectorAll('.leaflet-control-layers-selector');
|
||||
[].forEach.call(inputs, function (input) {
|
||||
input.setAttribute('role', 'radiogroup');
|
||||
input.setAttribute('aria-label', input.nextSibling.innerHTML.trim());
|
||||
});
|
||||
});
|
||||
|
||||
var nodeDict = {};
|
||||
var linkDict = {};
|
||||
var highlight;
|
||||
|
||||
function resetMarkerStyles(nodes, links) {
|
||||
Object.keys(nodes).forEach(function (d) {
|
||||
nodes[d].resetStyle();
|
||||
});
|
||||
|
||||
Object.keys(links).forEach(function (d) {
|
||||
links[d].resetStyle();
|
||||
});
|
||||
}
|
||||
|
||||
function setView(bounds, zoom) {
|
||||
map.fitBounds(bounds, { maxZoom: (zoom ? zoom : config.nodeZoom) });
|
||||
}
|
||||
|
||||
function goto(m) {
|
||||
var bounds;
|
||||
|
||||
if ('getBounds' in m) {
|
||||
bounds = m.getBounds();
|
||||
} else {
|
||||
bounds = L.latLngBounds([m.getLatLng()]);
|
||||
}
|
||||
|
||||
setView(bounds);
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
function updateView(nopanzoom) {
|
||||
resetMarkerStyles(nodeDict, linkDict);
|
||||
var m;
|
||||
|
||||
if (highlight !== undefined) {
|
||||
if (highlight.type === 'node' && nodeDict[highlight.o.node_id]) {
|
||||
m = nodeDict[highlight.o.node_id];
|
||||
m.setStyle(config.map.highlightNode);
|
||||
} else if (highlight.type === 'link' && linkDict[highlight.o.id]) {
|
||||
m = linkDict[highlight.o.id];
|
||||
m.setStyle(config.map.highlightLink);
|
||||
}
|
||||
}
|
||||
|
||||
if (!nopanzoom) {
|
||||
if (m) {
|
||||
goto(m);
|
||||
} else if (savedView) {
|
||||
map.setView(savedView.center, savedView.zoom);
|
||||
} else {
|
||||
setView(config.fixedCenter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.setData = function setData(data) {
|
||||
nodeDict = {};
|
||||
linkDict = {};
|
||||
|
||||
clientLayer.setData(data);
|
||||
labelLayer.setData(data, map, nodeDict, linkDict, linkScale);
|
||||
|
||||
updateView(true);
|
||||
};
|
||||
|
||||
self.resetView = function resetView() {
|
||||
button.disableTracking();
|
||||
highlight = undefined;
|
||||
updateView();
|
||||
};
|
||||
|
||||
self.gotoNode = function gotoNode(d) {
|
||||
button.disableTracking();
|
||||
highlight = { type: 'node', o: d };
|
||||
updateView();
|
||||
};
|
||||
|
||||
self.gotoLink = function gotoLink(d) {
|
||||
button.disableTracking();
|
||||
highlight = { type: 'link', o: d[0] };
|
||||
updateView();
|
||||
};
|
||||
|
||||
self.gotoLocation = function gotoLocation(d) {
|
||||
button.disableTracking();
|
||||
map.setView([d.lat, d.lng], d.zoom);
|
||||
};
|
||||
|
||||
self.destroy = function destroy() {
|
||||
button.clearButtons();
|
||||
sidebar.button.removeEventListener('visibility', setActiveArea);
|
||||
map.remove();
|
||||
|
||||
if (el.parentNode) {
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
};
|
||||
|
||||
self.render = function render(d) {
|
||||
d.appendChild(el);
|
||||
map.invalidateSize();
|
||||
};
|
||||
|
||||
return self;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,291 @@
|
|||
define(function () {
|
||||
/**
|
||||
* https://github.com/Mappy/Leaflet-active-area
|
||||
* Apache 2.0 license https://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
var previousMethods = {
|
||||
getCenter: L.Map.prototype.getCenter,
|
||||
setView: L.Map.prototype.setView,
|
||||
setZoomAround: L.Map.prototype.setZoomAround,
|
||||
getBoundsZoom: L.Map.prototype.getBoundsZoom,
|
||||
RendererUpdate: L.Renderer.prototype._update
|
||||
};
|
||||
|
||||
L.Map.include({
|
||||
getBounds: function () {
|
||||
if (this._viewport) {
|
||||
return this.getViewportLatLngBounds();
|
||||
}
|
||||
var bounds = this.getPixelBounds();
|
||||
var sw = this.unproject(bounds.getBottomLeft());
|
||||
var ne = this.unproject(bounds.getTopRight());
|
||||
|
||||
return new L.LatLngBounds(sw, ne);
|
||||
},
|
||||
|
||||
getViewport: function () {
|
||||
return this._viewport;
|
||||
},
|
||||
|
||||
getViewportBounds: function () {
|
||||
var vp = this._viewport;
|
||||
var topleft = L.point(vp.offsetLeft, vp.offsetTop);
|
||||
var vpsize = L.point(vp.clientWidth, vp.clientHeight);
|
||||
|
||||
if (vpsize.x === 0 || vpsize.y === 0) {
|
||||
// Our own viewport has no good size - so we fallback to the container size:
|
||||
vp = this.getContainer();
|
||||
if (vp) {
|
||||
topleft = L.point(0, 0);
|
||||
vpsize = L.point(vp.clientWidth, vp.clientHeight);
|
||||
}
|
||||
}
|
||||
|
||||
return L.bounds(topleft, topleft.add(vpsize));
|
||||
},
|
||||
|
||||
getViewportLatLngBounds: function () {
|
||||
var bounds = this.getViewportBounds();
|
||||
return L.latLngBounds(this.containerPointToLatLng(bounds.min), this.containerPointToLatLng(bounds.max));
|
||||
},
|
||||
|
||||
getOffset: function () {
|
||||
var mCenter = this.getSize().divideBy(2);
|
||||
var vCenter = this.getViewportBounds().getCenter();
|
||||
|
||||
return mCenter.subtract(vCenter);
|
||||
},
|
||||
|
||||
getCenter: function (withoutViewport) {
|
||||
var center = previousMethods.getCenter.call(this);
|
||||
|
||||
if (this.getViewport() && !withoutViewport) {
|
||||
var zoom = this.getZoom();
|
||||
var point = this.project(center, zoom);
|
||||
point = point.subtract(this.getOffset());
|
||||
|
||||
center = this.unproject(point, zoom);
|
||||
}
|
||||
|
||||
return center;
|
||||
},
|
||||
|
||||
setView: function (center, zoom, options) {
|
||||
center = L.latLng(center);
|
||||
zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom);
|
||||
|
||||
if (this.getViewport()) {
|
||||
var point = this.project(center, this._limitZoom(zoom));
|
||||
point = point.add(this.getOffset());
|
||||
center = this.unproject(point, this._limitZoom(zoom));
|
||||
}
|
||||
|
||||
return previousMethods.setView.call(this, center, zoom, options);
|
||||
},
|
||||
|
||||
setZoomAround: function (latlng, zoom, options) {
|
||||
var vp = this.getViewport();
|
||||
|
||||
if (vp) {
|
||||
var scale = this.getZoomScale(zoom);
|
||||
var viewHalf = this.getViewportBounds().getCenter();
|
||||
var containerPoint = latlng instanceof L.Point ? latlng : this.latLngToContainerPoint(latlng);
|
||||
|
||||
var centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale);
|
||||
var newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset));
|
||||
|
||||
return this.setView(newCenter, zoom, { zoom: options });
|
||||
}
|
||||
return previousMethods.setZoomAround.call(this, latlng, zoom, options);
|
||||
},
|
||||
|
||||
getBoundsZoom: function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number
|
||||
bounds = L.latLngBounds(bounds);
|
||||
padding = L.point(padding || [0, 0]);
|
||||
|
||||
var zoom = this.getZoom() || 0;
|
||||
var min = this.getMinZoom();
|
||||
var max = this.getMaxZoom();
|
||||
var nw = bounds.getNorthWest();
|
||||
var se = bounds.getSouthEast();
|
||||
var vp = this.getViewport();
|
||||
var size = (vp ? L.point(vp.clientWidth, vp.clientHeight) : this.getSize()).subtract(padding);
|
||||
var boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom));
|
||||
var snap = L.Browser.any3d ? this.options.zoomSnap : 1;
|
||||
|
||||
var scale = Math.min(size.x / boundsSize.x, size.y / boundsSize.y);
|
||||
|
||||
zoom = this.getScaleZoom(scale, zoom);
|
||||
|
||||
if (snap) {
|
||||
zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level
|
||||
zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap;
|
||||
}
|
||||
|
||||
return Math.max(min, Math.min(max, zoom));
|
||||
}
|
||||
});
|
||||
|
||||
L.Map.include({
|
||||
setActiveArea: function (css, keepCenter, animate) {
|
||||
var center;
|
||||
if (keepCenter && this._zoom) {
|
||||
// save center if map is already initialized
|
||||
// and keepCenter is passed
|
||||
center = this.getCenter();
|
||||
}
|
||||
|
||||
if (!this._viewport) {
|
||||
// Make viewport if not already made
|
||||
var container = this.getContainer();
|
||||
this._viewport = L.DomUtil.create('div', '');
|
||||
container.insertBefore(this._viewport, container.firstChild);
|
||||
}
|
||||
|
||||
if (typeof css === 'string') {
|
||||
this._viewport.className = css;
|
||||
} else {
|
||||
L.extend(this._viewport.style, css);
|
||||
}
|
||||
|
||||
if (center) {
|
||||
this.setView(center, this.getZoom(), { animate: !!animate });
|
||||
}
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
L.Renderer.include({
|
||||
_onZoom: function () {
|
||||
this._updateTransform(this._map.getCenter(true), this._map.getZoom());
|
||||
},
|
||||
|
||||
_update: function () {
|
||||
previousMethods.RendererUpdate.call(this);
|
||||
this._center = this._map.getCenter(true);
|
||||
}
|
||||
});
|
||||
|
||||
L.GridLayer.include({
|
||||
_updateLevels: function () {
|
||||
var zoom = this._tileZoom;
|
||||
var maxZoom = this.options.maxZoom;
|
||||
|
||||
if (zoom === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (var z in this._levels) {
|
||||
if (this._levels[z].el.children.length || z === zoom) {
|
||||
this._levels[z].el.style.zIndex = maxZoom - Math.abs(zoom - z);
|
||||
} else {
|
||||
L.DomUtil.remove(this._levels[z].el);
|
||||
this._removeTilesAtZoom(z);
|
||||
delete this._levels[z];
|
||||
}
|
||||
}
|
||||
|
||||
var level = this._levels[zoom];
|
||||
var map = this._map;
|
||||
|
||||
if (!level) {
|
||||
level = this._levels[zoom] = {};
|
||||
|
||||
level.el = L.DomUtil.create('div', 'leaflet-tile-container leaflet-zoom-animated', this._container);
|
||||
level.el.style.zIndex = maxZoom;
|
||||
|
||||
level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom).round();
|
||||
level.zoom = zoom;
|
||||
|
||||
this._setZoomTransform(level, map.getCenter(true), map.getZoom());
|
||||
|
||||
// force the browser to consider the newly added element for transition
|
||||
L.Util.falseFn(level.el.offsetWidth);
|
||||
}
|
||||
|
||||
this._level = level;
|
||||
|
||||
return level;
|
||||
},
|
||||
|
||||
_resetView: function (e) {
|
||||
var animating = e && (e.pinch || e.flyTo);
|
||||
this._setView(this._map.getCenter(true), this._map.getZoom(), animating, animating);
|
||||
},
|
||||
|
||||
_update: function (center) {
|
||||
var map = this._map;
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
var zoom = map.getZoom();
|
||||
|
||||
if (center === undefined) {
|
||||
center = map.getCenter(this);
|
||||
}
|
||||
if (this._tileZoom === undefined) {
|
||||
return;
|
||||
} // if out of minzoom/maxzoom
|
||||
|
||||
var pixelBounds = this._getTiledPixelBounds(center);
|
||||
var tileRange = this._pxBoundsToTileRange(pixelBounds);
|
||||
var tileCenter = tileRange.getCenter();
|
||||
var queue = [];
|
||||
|
||||
for (var key in this._tiles) {
|
||||
this._tiles[key].current = false;
|
||||
}
|
||||
|
||||
// _update just loads more tiles. If the tile zoom level differs too much
|
||||
// from the map's, let _setView reset levels and prune old tiles.
|
||||
if (Math.abs(zoom - this._tileZoom) > 1) {
|
||||
this._setView(center, zoom);
|
||||
return;
|
||||
}
|
||||
|
||||
// create a queue of coordinates to load tiles from
|
||||
for (var j = tileRange.min.y; j <= tileRange.max.y; j++) {
|
||||
for (var i = tileRange.min.x; i <= tileRange.max.x; i++) {
|
||||
var coords = new L.Point(i, j);
|
||||
coords.z = this._tileZoom;
|
||||
|
||||
if (!this._isValidTile(coords)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var tile = this._tiles[this._tileCoordsToKey(coords)];
|
||||
if (tile) {
|
||||
tile.current = true;
|
||||
} else {
|
||||
queue.push(coords);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort tile queue to load tiles in order of their distance to center
|
||||
queue.sort(function (a, b) {
|
||||
return a.distanceTo(tileCenter) - b.distanceTo(tileCenter);
|
||||
});
|
||||
|
||||
if (queue.length !== 0) {
|
||||
// if its the first batch of tiles to load
|
||||
if (!this._loading) {
|
||||
this._loading = true;
|
||||
// @event loading: Event
|
||||
// Fired when the grid layer starts loading tiles
|
||||
this.fire('loading');
|
||||
}
|
||||
|
||||
// create DOM fragment to append tiles in one batch
|
||||
var fragment = document.createDocumentFragment();
|
||||
|
||||
for (i = 0; i < queue.length; i++) {
|
||||
this._addTile(queue[i], fragment);
|
||||
}
|
||||
|
||||
this._level.el.appendChild(fragment);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,154 @@
|
|||
define(['map/clientlayer', 'map/labellayer', 'leaflet', 'map/locationmarker'],
|
||||
function (ClientLayer, LabelLayer, L, LocationMarker) {
|
||||
'use strict';
|
||||
var self = {};
|
||||
|
||||
var ButtonBase = L.Control.extend({
|
||||
options: {
|
||||
position: 'bottomright'
|
||||
},
|
||||
|
||||
active: false,
|
||||
button: undefined,
|
||||
|
||||
initialize: function (f, o) {
|
||||
L.Util.setOptions(this, o);
|
||||
this.f = f;
|
||||
},
|
||||
|
||||
update: function () {
|
||||
this.button.classList.toggle('active', this.active);
|
||||
},
|
||||
|
||||
set: function (v) {
|
||||
this.active = v;
|
||||
this.update();
|
||||
}
|
||||
});
|
||||
|
||||
var LocateButton = ButtonBase.extend({
|
||||
onAdd: function () {
|
||||
var button = L.DomUtil.create('button', 'ion-locate');
|
||||
button.setAttribute('aria-label', _.t('button.tracking'));
|
||||
L.DomEvent.disableClickPropagation(button);
|
||||
L.DomEvent.addListener(button, 'click', this.onClick, this);
|
||||
|
||||
this.button = button;
|
||||
|
||||
return button;
|
||||
},
|
||||
|
||||
onClick: function () {
|
||||
this.f(!this.active);
|
||||
}
|
||||
});
|
||||
|
||||
var CoordsPickerButton = ButtonBase.extend({
|
||||
onAdd: function () {
|
||||
var button = L.DomUtil.create('button', 'ion-pin');
|
||||
button.setAttribute('aria-label', _.t('button.location'));
|
||||
|
||||
// Click propagation isn't disabled as this causes problems with the
|
||||
// location picking mode; instead propagation is stopped in onClick().
|
||||
L.DomEvent.addListener(button, 'click', this.onClick, this);
|
||||
|
||||
this.button = button;
|
||||
|
||||
return button;
|
||||
},
|
||||
|
||||
onClick: function (e) {
|
||||
L.DomEvent.stopPropagation(e);
|
||||
this.f(!this.active);
|
||||
}
|
||||
});
|
||||
|
||||
return function (map, buttons) {
|
||||
var userLocation;
|
||||
|
||||
var locateUserButton = new LocateButton(function (d) {
|
||||
if (d) {
|
||||
enableTracking();
|
||||
} else {
|
||||
self.disableTracking();
|
||||
}
|
||||
});
|
||||
|
||||
var mybuttons = [];
|
||||
|
||||
function addButton(button) {
|
||||
var el = button.onAdd();
|
||||
mybuttons.push(el);
|
||||
buttons.appendChild(el);
|
||||
}
|
||||
|
||||
self.clearButtons = function clearButtons() {
|
||||
mybuttons.forEach(function (d) {
|
||||
buttons.removeChild(d);
|
||||
});
|
||||
};
|
||||
|
||||
var showCoordsPickerButton = new CoordsPickerButton(function (d) {
|
||||
if (d) {
|
||||
enableCoords();
|
||||
} else {
|
||||
disableCoords();
|
||||
}
|
||||
});
|
||||
|
||||
function enableTracking() {
|
||||
map.locate({
|
||||
watch: true,
|
||||
enableHighAccuracy: true,
|
||||
setView: true
|
||||
});
|
||||
locateUserButton.set(true);
|
||||
}
|
||||
|
||||
self.disableTracking = function disableTracking() {
|
||||
map.stopLocate();
|
||||
self.locationError();
|
||||
locateUserButton.set(false);
|
||||
};
|
||||
|
||||
function enableCoords() {
|
||||
map.getContainer().classList.add('pick-coordinates');
|
||||
map.on('click', showCoordinates);
|
||||
showCoordsPickerButton.set(true);
|
||||
}
|
||||
|
||||
function disableCoords() {
|
||||
map.getContainer().classList.remove('pick-coordinates');
|
||||
map.off('click', showCoordinates);
|
||||
showCoordsPickerButton.set(false);
|
||||
}
|
||||
|
||||
function showCoordinates(e) {
|
||||
router.fullUrl({ zoom: map.getZoom(), lat: e.latlng.lat, lng: e.latlng.lng });
|
||||
disableCoords();
|
||||
}
|
||||
|
||||
self.locationFound = function locationFound(e) {
|
||||
if (!userLocation) {
|
||||
userLocation = new LocationMarker(e.latlng).addTo(map);
|
||||
}
|
||||
|
||||
userLocation.setLatLng(e.latlng);
|
||||
userLocation.setAccuracy(e.accuracy);
|
||||
};
|
||||
|
||||
self.locationError = function locationError() {
|
||||
if (userLocation) {
|
||||
map.removeLayer(userLocation);
|
||||
userLocation = null;
|
||||
}
|
||||
};
|
||||
|
||||
self.init = function init() {
|
||||
addButton(locateUserButton);
|
||||
addButton(showCoordsPickerButton);
|
||||
};
|
||||
|
||||
return self;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
define(['leaflet', 'rbush', 'helper'],
|
||||
function (L, RBush, helper) {
|
||||
'use strict';
|
||||
|
||||
return L.GridLayer.extend({
|
||||
mapRTree: function mapRTree(d) {
|
||||
return {
|
||||
minX: d.location.latitude, minY: d.location.longitude,
|
||||
maxX: d.location.latitude, maxY: d.location.longitude,
|
||||
node: d
|
||||
};
|
||||
},
|
||||
setData: function (data) {
|
||||
var rtreeOnlineAll = new RBush(9);
|
||||
|
||||
this.data = rtreeOnlineAll.load(data.nodes.online.filter(helper.hasLocation).map(this.mapRTree));
|
||||
|
||||
// pre-calculate start angles
|
||||
this.data.all().forEach(function (n) {
|
||||
n.startAngle = (parseInt(n.node.node_id.substr(10, 2), 16) / 255) * 2 * Math.PI;
|
||||
});
|
||||
this.redraw();
|
||||
},
|
||||
createTile: function (tilePoint) {
|
||||
var tile = L.DomUtil.create('canvas', 'leaflet-tile');
|
||||
|
||||
var tileSize = this.options.tileSize;
|
||||
tile.width = tileSize;
|
||||
tile.height = tileSize;
|
||||
|
||||
if (!this.data) {
|
||||
return tile;
|
||||
}
|
||||
|
||||
var ctx = tile.getContext('2d');
|
||||
var s = tilePoint.multiplyBy(tileSize);
|
||||
var map = this._map;
|
||||
|
||||
var margin = 50;
|
||||
var bbox = helper.getTileBBox(s, map, tileSize, margin);
|
||||
|
||||
var nodes = this.data.search(bbox);
|
||||
|
||||
if (nodes.length === 0) {
|
||||
return tile;
|
||||
}
|
||||
|
||||
var startDistance = 10;
|
||||
|
||||
nodes.forEach(function (d) {
|
||||
var p = map.project([d.node.location.latitude, d.node.location.longitude]);
|
||||
|
||||
p.x -= s.x;
|
||||
p.y -= s.y;
|
||||
|
||||
helper.positionClients(ctx, p, d.startAngle, d.node, startDistance);
|
||||
});
|
||||
|
||||
return tile;
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,352 @@
|
|||
define(['leaflet', 'rbush', 'helper', 'moment'],
|
||||
function (L, RBush, helper, moment) {
|
||||
'use strict';
|
||||
|
||||
var groupOnline;
|
||||
var groupOffline;
|
||||
var groupNew;
|
||||
var groupLost;
|
||||
var groupLines;
|
||||
|
||||
var labelLocations = [['left', 'middle', 0 / 8],
|
||||
['center', 'top', 6 / 8],
|
||||
['right', 'middle', 4 / 8],
|
||||
['left', 'top', 7 / 8],
|
||||
['left', 'ideographic', 1 / 8],
|
||||
['right', 'top', 5 / 8],
|
||||
['center', 'ideographic', 2 / 8],
|
||||
['right', 'ideographic', 3 / 8]];
|
||||
var labelShadow;
|
||||
var bodyStyle = { fontFamily: 'sans-serif' };
|
||||
var nodeRadius = 4;
|
||||
|
||||
var cFont = document.createElement('canvas').getContext('2d');
|
||||
|
||||
function measureText(font, text) {
|
||||
cFont.font = font;
|
||||
return cFont.measureText(text);
|
||||
}
|
||||
|
||||
function mapRTree(d) {
|
||||
return { minX: d.position.lat, minY: d.position.lng, maxX: d.position.lat, maxY: d.position.lng, label: d };
|
||||
}
|
||||
|
||||
function prepareLabel(fillStyle, fontSize, offset, stroke) {
|
||||
return function (d) {
|
||||
var font = fontSize + 'px ' + bodyStyle.fontFamily;
|
||||
return {
|
||||
position: L.latLng(d.location.latitude, d.location.longitude),
|
||||
label: d.hostname,
|
||||
offset: offset,
|
||||
fillStyle: fillStyle,
|
||||
height: fontSize * 1.2,
|
||||
font: font,
|
||||
stroke: stroke,
|
||||
width: measureText(font, d.hostname).width
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function calcOffset(offset, loc) {
|
||||
return [offset * Math.cos(loc[2] * 2 * Math.PI),
|
||||
offset * Math.sin(loc[2] * 2 * Math.PI)];
|
||||
}
|
||||
|
||||
function labelRect(p, offset, anchor, label, minZoom, maxZoom, z) {
|
||||
var margin = 1 + 1.41 * (1 - (z - minZoom) / (maxZoom - minZoom));
|
||||
|
||||
var width = label.width * margin;
|
||||
var height = label.height * margin;
|
||||
|
||||
var dx = {
|
||||
left: 0,
|
||||
right: -width,
|
||||
center: -width / 2
|
||||
};
|
||||
|
||||
var dy = {
|
||||
top: 0,
|
||||
ideographic: -height,
|
||||
middle: -height / 2
|
||||
};
|
||||
|
||||
var x = p.x + offset[0] + dx[anchor[0]];
|
||||
var y = p.y + offset[1] + dy[anchor[1]];
|
||||
|
||||
return { minX: x, minY: y, maxX: x + width, maxY: y + height };
|
||||
}
|
||||
|
||||
function mkMarker(dict, iconFunc) {
|
||||
return function (d) {
|
||||
var m = L.circleMarker([d.location.latitude, d.location.longitude], iconFunc(d));
|
||||
|
||||
m.resetStyle = function resetStyle() {
|
||||
m.setStyle(iconFunc(d));
|
||||
};
|
||||
|
||||
m.on('click', function () {
|
||||
router.fullUrl({ node: d.node_id });
|
||||
});
|
||||
m.bindTooltip(helper.escape(d.hostname));
|
||||
|
||||
dict[d.node_id] = m;
|
||||
|
||||
return m;
|
||||
};
|
||||
}
|
||||
|
||||
function addLinksToMap(dict, linkScale, graph) {
|
||||
graph = graph.filter(function (d) {
|
||||
return 'distance' in d && d.type.indexOf('vpn') !== 0;
|
||||
});
|
||||
|
||||
return graph.map(function (d) {
|
||||
var opts = {
|
||||
color: linkScale((d.source_tq + d.target_tq) / 2),
|
||||
weight: 4,
|
||||
opacity: 0.5,
|
||||
dashArray: 'none'
|
||||
};
|
||||
|
||||
var line = L.polyline(d.latlngs, opts);
|
||||
|
||||
line.resetStyle = function resetStyle() {
|
||||
line.setStyle(opts);
|
||||
};
|
||||
|
||||
line.bindTooltip(helper.escape(d.source.hostname + ' – ' + d.target.hostname) +
|
||||
'<br><strong>' + helper.showDistance(d) + ' / ' + helper.showTq(d.source_tq) + ' - ' + helper.showTq(d.target_tq) + '<br>' + d.type + '</strong>');
|
||||
|
||||
line.on('click', function () {
|
||||
router.fullUrl({ link: d.id });
|
||||
});
|
||||
|
||||
dict[d.id] = line;
|
||||
|
||||
return line;
|
||||
});
|
||||
}
|
||||
|
||||
function getIcon(color) {
|
||||
return Object.assign({}, config.icon.base, config.icon[color]);
|
||||
}
|
||||
|
||||
return L.GridLayer.extend({
|
||||
onAdd: function (map) {
|
||||
L.GridLayer.prototype.onAdd.call(this, map);
|
||||
if (this.data) {
|
||||
this.prepareLabels();
|
||||
}
|
||||
},
|
||||
setData: function (data, map, nodeDict, linkDict, linkScale) {
|
||||
var iconOnline = getIcon('online');
|
||||
var iconOffline = getIcon('offline');
|
||||
var iconLost = getIcon('lost');
|
||||
var iconAlert = getIcon('alert');
|
||||
var iconNew = getIcon('new');
|
||||
// Check if init or data is already set
|
||||
if (groupLines) {
|
||||
groupOffline.clearLayers();
|
||||
groupOnline.clearLayers();
|
||||
groupNew.clearLayers();
|
||||
groupLost.clearLayers();
|
||||
groupLines.clearLayers();
|
||||
}
|
||||
|
||||
var lines = addLinksToMap(linkDict, linkScale, data.links);
|
||||
groupLines = L.featureGroup(lines).addTo(map);
|
||||
|
||||
var nodesOnline = helper.subtract(data.nodes.online, data.nodes.new).filter(helper.hasLocation);
|
||||
var nodesOffline = helper.subtract(data.nodes.offline, data.nodes.lost).filter(helper.hasLocation);
|
||||
var nodesNew = data.nodes.new.filter(helper.hasLocation);
|
||||
var nodesLost = data.nodes.lost.filter(helper.hasLocation);
|
||||
|
||||
var markersOnline = nodesOnline.map(mkMarker(nodeDict, function () {
|
||||
return iconOnline;
|
||||
}));
|
||||
|
||||
var markersOffline = nodesOffline.map(mkMarker(nodeDict, function () {
|
||||
return iconOffline;
|
||||
}));
|
||||
|
||||
var markersNew = nodesNew.map(mkMarker(nodeDict, function () {
|
||||
return iconNew;
|
||||
}));
|
||||
|
||||
var markersLost = nodesLost.map(mkMarker(nodeDict, function (d) {
|
||||
var age = moment(data.now).diff(d.lastseen, 'days', true);
|
||||
if (age <= config.maxAgeAlert) {
|
||||
return iconAlert;
|
||||
}
|
||||
if (age <= config.maxAge) {
|
||||
return iconLost;
|
||||
}
|
||||
return null;
|
||||
}));
|
||||
|
||||
groupOffline = L.featureGroup(markersOffline).addTo(map);
|
||||
groupLost = L.featureGroup(markersLost).addTo(map);
|
||||
groupOnline = L.featureGroup(markersOnline).addTo(map);
|
||||
groupNew = L.featureGroup(markersNew).addTo(map);
|
||||
|
||||
this.data = {
|
||||
online: nodesOnline,
|
||||
offline: nodesOffline,
|
||||
new: nodesNew,
|
||||
lost: nodesLost
|
||||
};
|
||||
this.updateLayer();
|
||||
},
|
||||
updateLayer: function () {
|
||||
if (this._map) {
|
||||
this.prepareLabels();
|
||||
}
|
||||
},
|
||||
prepareLabels: function () {
|
||||
var d = this.data;
|
||||
|
||||
// label:
|
||||
// - position (WGS84 coords)
|
||||
// - offset (2D vector in pixels)
|
||||
// - anchor (tuple, textAlignment, textBaseline)
|
||||
// - minZoom (inclusive)
|
||||
// - label (string)
|
||||
// - color (string)
|
||||
|
||||
var labelsOnline = d.online.map(prepareLabel(null, 11, 8, true));
|
||||
var labelsOffline = d.offline.map(prepareLabel(config.icon.offline.color, 9, 5, false));
|
||||
var labelsNew = d.new.map(prepareLabel(config.map.labelNewColor, 11, 8, true));
|
||||
var labelsLost = d.lost.map(prepareLabel(config.icon.lost.color, 11, 8, true));
|
||||
|
||||
var labels = []
|
||||
.concat(labelsNew)
|
||||
.concat(labelsLost)
|
||||
.concat(labelsOnline)
|
||||
.concat(labelsOffline);
|
||||
|
||||
var minZoom = this.options.minZoom;
|
||||
var maxZoom = this.options.maxZoom;
|
||||
|
||||
var trees = [];
|
||||
|
||||
var map = this._map;
|
||||
|
||||
function nodeToRect(z) {
|
||||
return function (n) {
|
||||
var p = map.project(n.position, z);
|
||||
return { minX: p.x - nodeRadius, minY: p.y - nodeRadius, maxX: p.x + nodeRadius, maxY: p.y + nodeRadius };
|
||||
};
|
||||
}
|
||||
|
||||
for (var z = minZoom; z <= maxZoom; z++) {
|
||||
trees[z] = new RBush(9);
|
||||
trees[z].load(labels.map(nodeToRect(z)));
|
||||
}
|
||||
|
||||
labels = labels.map(function (n) {
|
||||
var best = labelLocations.map(function (loc) {
|
||||
var offset = calcOffset(n.offset, loc);
|
||||
var i;
|
||||
|
||||
for (i = maxZoom; i >= minZoom; i--) {
|
||||
var p = map.project(n.position, i);
|
||||
var rect = labelRect(p, offset, loc, n, minZoom, maxZoom, i);
|
||||
var candidates = trees[i].search(rect);
|
||||
|
||||
if (candidates.length > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { loc: loc, z: i + 1 };
|
||||
}).filter(function (k) {
|
||||
return k.z <= maxZoom;
|
||||
}).sort(function (a, b) {
|
||||
return a.z - b.z;
|
||||
})[0];
|
||||
|
||||
if (best !== undefined) {
|
||||
n.offset = calcOffset(n.offset, best.loc);
|
||||
n.minZoom = best.z;
|
||||
n.anchor = best.loc;
|
||||
|
||||
for (var i = maxZoom; i >= best.z; i--) {
|
||||
var p = map.project(n.position, i);
|
||||
var rect = labelRect(p, n.offset, best.loc, n, minZoom, maxZoom, i);
|
||||
trees[i].insert(rect);
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
return undefined;
|
||||
}).filter(function (n) {
|
||||
return n !== undefined;
|
||||
});
|
||||
|
||||
this.margin = 16;
|
||||
|
||||
if (labels.length > 0) {
|
||||
this.margin += labels.map(function (n) {
|
||||
return n.width;
|
||||
}).sort().reverse()[0];
|
||||
}
|
||||
|
||||
this.labels = new RBush(9);
|
||||
this.labels.load(labels.map(mapRTree));
|
||||
|
||||
this.redraw();
|
||||
},
|
||||
createTile: function (tilePoint) {
|
||||
var tile = L.DomUtil.create('canvas', 'leaflet-tile');
|
||||
|
||||
var tileSize = this.options.tileSize;
|
||||
tile.width = tileSize;
|
||||
tile.height = tileSize;
|
||||
|
||||
if (!this.labels) {
|
||||
return tile;
|
||||
}
|
||||
|
||||
var s = tilePoint.multiplyBy(tileSize);
|
||||
var map = this._map;
|
||||
bodyStyle = window.getComputedStyle(document.querySelector('body'));
|
||||
labelShadow = bodyStyle.backgroundColor.replace(/rgb/i, 'rgba').replace(/\)/i, ',0.7)');
|
||||
|
||||
function projectNodes(d) {
|
||||
var p = map.project(d.label.position);
|
||||
|
||||
p.x -= s.x;
|
||||
p.y -= s.y;
|
||||
|
||||
return { p: p, label: d.label };
|
||||
}
|
||||
|
||||
var bbox = helper.getTileBBox(s, map, tileSize, this.margin);
|
||||
var labels = this.labels.search(bbox).map(projectNodes);
|
||||
var ctx = tile.getContext('2d');
|
||||
|
||||
ctx.lineWidth = 5;
|
||||
ctx.strokeStyle = labelShadow;
|
||||
ctx.miterLimit = 2;
|
||||
|
||||
function drawLabel(d) {
|
||||
ctx.font = d.label.font;
|
||||
ctx.textAlign = d.label.anchor[0];
|
||||
ctx.textBaseline = d.label.anchor[1];
|
||||
ctx.fillStyle = d.label.fillStyle === null ? bodyStyle.color : d.label.fillStyle;
|
||||
|
||||
if (d.label.stroke) {
|
||||
ctx.strokeText(d.label.label, d.p.x + d.label.offset[0], d.p.y + d.label.offset[1]);
|
||||
}
|
||||
|
||||
ctx.fillText(d.label.label, d.p.x + d.label.offset[0], d.p.y + d.label.offset[1]);
|
||||
}
|
||||
|
||||
labels.filter(function (d) {
|
||||
return tilePoint.z >= d.label.minZoom;
|
||||
}).forEach(drawLabel);
|
||||
|
||||
return tile;
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
define(['leaflet'], function (L) {
|
||||
'use strict';
|
||||
|
||||
return L.CircleMarker.extend({
|
||||
initialize: function (latlng) {
|
||||
this.accuracyCircle = L.circle(latlng, 0, config.locate.accuracyCircle);
|
||||
this.outerCircle = L.circleMarker(latlng, config.locate.outerCircle);
|
||||
L.CircleMarker.prototype.initialize.call(this, latlng, config.locate.innerCircle);
|
||||
|
||||
this.on('remove', function () {
|
||||
this._map.removeLayer(this.accuracyCircle);
|
||||
this._map.removeLayer(this.outerCircle);
|
||||
});
|
||||
},
|
||||
|
||||
setLatLng: function (latlng) {
|
||||
this.accuracyCircle.setLatLng(latlng);
|
||||
this.outerCircle.setLatLng(latlng);
|
||||
L.CircleMarker.prototype.setLatLng.call(this, latlng);
|
||||
},
|
||||
|
||||
setAccuracy: function (accuracy) {
|
||||
this.accuracyCircle.setRadius(accuracy);
|
||||
},
|
||||
|
||||
onAdd: function (map) {
|
||||
this.accuracyCircle.addTo(map).bringToBack();
|
||||
this.outerCircle.addTo(map);
|
||||
L.CircleMarker.prototype.onAdd.call(this, map);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,101 @@
|
|||
define(['sorttable', 'snabbdom', 'helper'], function (SortTable, V, helper) {
|
||||
'use strict';
|
||||
V = V.default;
|
||||
|
||||
function showUptime(uptime) {
|
||||
// 1000ms are 1 second and 60 second are 1min: 60 * 1000 = 60000
|
||||
var s = uptime / 60000;
|
||||
if (Math.abs(s) < 60) {
|
||||
return Math.round(s) + ' m';
|
||||
}
|
||||
s /= 60;
|
||||
if (Math.abs(s) < 24) {
|
||||
return Math.round(s) + ' h';
|
||||
}
|
||||
s /= 24;
|
||||
return Math.round(s) + ' d';
|
||||
}
|
||||
|
||||
var headings = [{
|
||||
name: ''
|
||||
}, {
|
||||
name: 'node.nodes',
|
||||
sort: function (a, b) {
|
||||
return a.hostname.localeCompare(b.hostname);
|
||||
},
|
||||
reverse: false
|
||||
}, {
|
||||
name: 'node.uptime',
|
||||
class: 'ion-time',
|
||||
sort: function (a, b) {
|
||||
return a.uptime - b.uptime;
|
||||
},
|
||||
reverse: true
|
||||
}, {
|
||||
name: 'node.links',
|
||||
class: 'ion-share-alt',
|
||||
sort: function (a, b) {
|
||||
return a.neighbours.length - b.neighbours.length;
|
||||
},
|
||||
reverse: true
|
||||
}, {
|
||||
name: 'node.clients',
|
||||
class: 'ion-people',
|
||||
sort: function (a, b) {
|
||||
return a.clients - b.clients;
|
||||
},
|
||||
reverse: true
|
||||
}];
|
||||
|
||||
return function () {
|
||||
function renderRow(d) {
|
||||
var td0Content = '';
|
||||
if (helper.hasLocation(d)) {
|
||||
td0Content = V.h('span', { props: { className: 'icon ion-location', title: _.t('location.location') } });
|
||||
}
|
||||
|
||||
var td1Content = V.h('a', {
|
||||
props: {
|
||||
className: ['hostname', d.is_online ? 'online' : 'offline'].join(' '),
|
||||
href: router.generateLink({ node: d.node_id })
|
||||
}, on: {
|
||||
click: function (e) {
|
||||
router.fullUrl({ node: d.node_id }, e);
|
||||
}
|
||||
}
|
||||
}, d.hostname);
|
||||
|
||||
return V.h('tr', [
|
||||
V.h('td', td0Content),
|
||||
V.h('td', td1Content),
|
||||
V.h('td', showUptime(d.uptime)),
|
||||
V.h('td', d.neighbours.length),
|
||||
V.h('td', d.clients)
|
||||
]);
|
||||
}
|
||||
|
||||
var table = new SortTable(headings, 1, renderRow);
|
||||
|
||||
this.render = function render(d) {
|
||||
var h2 = document.createElement('h2');
|
||||
h2.textContent = _.t('node.all');
|
||||
d.appendChild(h2);
|
||||
table.el.elm.classList.add('node-list');
|
||||
d.appendChild(table.el.elm);
|
||||
};
|
||||
|
||||
this.setData = function setData(d) {
|
||||
var data = d.nodes.all.map(function (e) {
|
||||
var n = Object.create(e);
|
||||
if (e.is_online) {
|
||||
n.uptime = d.now - new Date(e.uptime).getTime();
|
||||
} else {
|
||||
n.uptime = e.lastseen - d.now;
|
||||
}
|
||||
return n;
|
||||
});
|
||||
|
||||
table.setData(data);
|
||||
};
|
||||
};
|
||||
});
|
|
@ -0,0 +1,187 @@
|
|||
define(['d3-interpolate', 'snabbdom', 'utils/version', 'filters/genericnode', 'helper'],
|
||||
function (d3Interpolate, V, versionCompare, Filter, helper) {
|
||||
'use strict';
|
||||
V = V.default;
|
||||
|
||||
return function (filterManager) {
|
||||
var self = this;
|
||||
var scale = d3Interpolate.interpolate(config.forceGraph.tqFrom, config.forceGraph.tqTo);
|
||||
var time;
|
||||
|
||||
var statusTable;
|
||||
var fwTable;
|
||||
var hwTable;
|
||||
var geoTable;
|
||||
var autoTable;
|
||||
var gatewayTable;
|
||||
var gateway6Table;
|
||||
var domainTable;
|
||||
|
||||
function count(nodes, key, f) {
|
||||
var dict = {};
|
||||
|
||||
nodes.forEach(function (d) {
|
||||
var v = helper.dictGet(d, key.slice(0));
|
||||
|
||||
if (f !== undefined) {
|
||||
v = f(v);
|
||||
}
|
||||
|
||||
if (v === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dict[v] = 1 + (v in dict ? dict[v] : 0);
|
||||
});
|
||||
|
||||
return Object.keys(dict).map(function (d) {
|
||||
return [d, dict[d], key, f];
|
||||
});
|
||||
}
|
||||
|
||||
function addFilter(filter) {
|
||||
return function () {
|
||||
filterManager.addFilter(filter);
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
function fillTable(name, table, data) {
|
||||
if (!table) {
|
||||
table = document.createElement('table');
|
||||
}
|
||||
|
||||
var max = Math.max.apply(Math, data.map(function (o) {
|
||||
return o[1];
|
||||
}));
|
||||
|
||||
var items = data.map(function (d) {
|
||||
var v = d[1] / max;
|
||||
|
||||
var filter = new Filter(_.t(name), d[2], d[0], d[3]);
|
||||
|
||||
var a = V.h('a', { on: { click: addFilter(filter) } }, d[0]);
|
||||
|
||||
var th = V.h('th', a);
|
||||
var td = V.h('td', V.h('span', {
|
||||
style: {
|
||||
width: 'calc(25px + ' + Math.round(v * 90) + '%)',
|
||||
backgroundColor: scale(v)
|
||||
}
|
||||
}, d[1].toFixed(0)));
|
||||
|
||||
return V.h('tr', [th, td]);
|
||||
});
|
||||
var tableNew = V.h('table', { props: { className: 'proportion' } }, items);
|
||||
return V.patch(table, tableNew);
|
||||
}
|
||||
|
||||
self.setData = function setData(data) {
|
||||
var onlineNodes = data.nodes.online;
|
||||
var nodes = onlineNodes.concat(data.nodes.lost);
|
||||
time = data.timestamp;
|
||||
|
||||
function hostnameOfNodeID(nodeid) {
|
||||
var gateway = data.nodeDict[nodeid];
|
||||
if (gateway) {
|
||||
return gateway.hostname;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var gatewayDict = count(nodes, ['gateway'], hostnameOfNodeID);
|
||||
var gateway6Dict = count(nodes, ['gateway6'], hostnameOfNodeID);
|
||||
|
||||
var statusDict = count(nodes, ['is_online'], function (d) {
|
||||
return d ? 'online' : 'offline';
|
||||
});
|
||||
var fwDict = count(nodes, ['firmware', 'release']);
|
||||
var hwDict = count(nodes, ['model']);
|
||||
var geoDict = count(nodes, ['location'], function (d) {
|
||||
return d && d.longitude && d.latitude ? _.t('yes') : _.t('no');
|
||||
});
|
||||
|
||||
var autoDict = count(nodes, ['autoupdater'], function (d) {
|
||||
if (d.enabled) {
|
||||
return d.branch;
|
||||
}
|
||||
return _.t('node.deactivated');
|
||||
});
|
||||
|
||||
var domainDict = count(nodes, ['domain'], function (d) {
|
||||
if (config.domainNames) {
|
||||
config.domainNames.some(function (t) {
|
||||
if (d === t.domain) {
|
||||
d = t.name;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return d;
|
||||
});
|
||||
|
||||
statusTable = fillTable('node.status', statusTable, statusDict.sort(function (a, b) {
|
||||
return b[1] - a[1];
|
||||
}));
|
||||
fwTable = fillTable('node.firmware', fwTable, fwDict.sort(versionCompare));
|
||||
hwTable = fillTable('node.hardware', hwTable, hwDict.sort(function (a, b) {
|
||||
return b[1] - a[1];
|
||||
}));
|
||||
geoTable = fillTable('node.visible', geoTable, geoDict.sort(function (a, b) {
|
||||
return b[1] - a[1];
|
||||
}));
|
||||
autoTable = fillTable('node.update', autoTable, autoDict.sort(function (a, b) {
|
||||
return b[1] - a[1];
|
||||
}));
|
||||
gatewayTable = fillTable('node.selectedGatewayIPv4', gatewayTable, gatewayDict.sort(function (a, b) {
|
||||
return b[1] - a[1];
|
||||
}));
|
||||
gateway6Table = fillTable('node.selectedGatewayIPv6', gateway6Table, gateway6Dict.sort(function (a, b) {
|
||||
return b[1] - a[1];
|
||||
}));
|
||||
domainTable = fillTable('node.domain', domainTable, domainDict.sort(function (a, b) {
|
||||
return b[1] - a[1];
|
||||
}));
|
||||
};
|
||||
|
||||
self.render = function render(el) {
|
||||
self.renderSingle(el, 'node.status', statusTable);
|
||||
self.renderSingle(el, 'node.firmware', fwTable);
|
||||
self.renderSingle(el, 'node.hardware', hwTable);
|
||||
self.renderSingle(el, 'node.visible', geoTable);
|
||||
self.renderSingle(el, 'node.update', autoTable);
|
||||
self.renderSingle(el, 'node.selectedGatewayIPv4', gatewayTable);
|
||||
self.renderSingle(el, 'node.selectedGatewayIPv6', gateway6Table);
|
||||
self.renderSingle(el, 'node.domain', domainTable);
|
||||
|
||||
if (config.globalInfos) {
|
||||
var images = document.createElement('div');
|
||||
el.appendChild(images);
|
||||
var img = [];
|
||||
var subst = {
|
||||
'{TIME}': time,
|
||||
'{LOCALE}': _.locale()
|
||||
};
|
||||
config.globalInfos.forEach(function (globalInfo) {
|
||||
img.push(V.h('h2', globalInfo.name));
|
||||
img.push(helper.showStat(V, globalInfo, subst));
|
||||
});
|
||||
V.patch(images, V.h('div', img));
|
||||
}
|
||||
};
|
||||
|
||||
self.renderSingle = function renderSingle(el, heading, table) {
|
||||
if (table.children.length > 0) {
|
||||
var h2 = document.createElement('h2');
|
||||
h2.classList.add('proportion-header');
|
||||
h2.textContent = _.t(heading);
|
||||
h2.onclick = function onclick() {
|
||||
table.elm.classList.toggle('hide');
|
||||
};
|
||||
el.appendChild(h2);
|
||||
el.appendChild(table.elm);
|
||||
}
|
||||
};
|
||||
return self;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
define(function () {
|
||||
'use strict';
|
||||
|
||||
return function (el) {
|
||||
var self = this;
|
||||
|
||||
// Needed to avoid render blocking
|
||||
var gridBreakpoints = {
|
||||
lg: [992, 446],
|
||||
xl: [1200, 560]
|
||||
};
|
||||
|
||||
var sidebar = document.createElement('div');
|
||||
sidebar.classList.add('sidebar');
|
||||
el.appendChild(sidebar);
|
||||
|
||||
var button = document.createElement('button');
|
||||
var visibility = new CustomEvent('visibility');
|
||||
sidebar.appendChild(button);
|
||||
|
||||
button.classList.add('sidebarhandle');
|
||||
button.setAttribute('aria-label', _.t('sidebar.toggle'));
|
||||
button.onclick = function onclick() {
|
||||
button.dispatchEvent(visibility);
|
||||
sidebar.classList.toggle('hidden');
|
||||
};
|
||||
|
||||
var container = document.createElement('div');
|
||||
container.classList.add('container');
|
||||
sidebar.appendChild(container);
|
||||
|
||||
self.getWidth = function getWidth() {
|
||||
if (gridBreakpoints.lg[0] > window.innerWidth || sidebar.classList.contains('hidden')) {
|
||||
return 0;
|
||||
} else if (gridBreakpoints.xl[0] > window.innerWidth) {
|
||||
return gridBreakpoints.lg[1];
|
||||
}
|
||||
return gridBreakpoints.xl[1];
|
||||
};
|
||||
|
||||
self.add = function add(d) {
|
||||
d.render(container);
|
||||
};
|
||||
|
||||
self.ensureVisible = function ensureVisible() {
|
||||
sidebar.classList.remove('hidden');
|
||||
};
|
||||
|
||||
self.hide = function hide() {
|
||||
container.children[1].classList.add('hide');
|
||||
container.children[2].classList.add('hide');
|
||||
};
|
||||
|
||||
self.reveal = function reveal() {
|
||||
container.children[1].classList.remove('hide');
|
||||
container.children[2].classList.remove('hide');
|
||||
};
|
||||
|
||||
self.container = sidebar;
|
||||
self.button = button;
|
||||
|
||||
return self;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
define(['moment', 'snabbdom', 'helper'], function (moment, V, helper) {
|
||||
'use strict';
|
||||
V = V.default;
|
||||
|
||||
return function (nodes, field, title) {
|
||||
var self = this;
|
||||
var el;
|
||||
var tbody;
|
||||
|
||||
self.render = function render(d) {
|
||||
el = d;
|
||||
};
|
||||
|
||||
self.setData = function setData(data) {
|
||||
var list = data.nodes[nodes];
|
||||
|
||||
if (list.length === 0) {
|
||||
tbody = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tbody) {
|
||||
var h2 = document.createElement('h2');
|
||||
h2.textContent = title;
|
||||
el.appendChild(h2);
|
||||
|
||||
var table = document.createElement('table');
|
||||
table.classList.add('node-list');
|
||||
el.appendChild(table);
|
||||
|
||||
tbody = document.createElement('tbody');
|
||||
tbody.last = V.h('tbody');
|
||||
table.appendChild(tbody);
|
||||
}
|
||||
|
||||
var items = list.map(function (d) {
|
||||
var td0Content = '';
|
||||
if (helper.hasLocation(d)) {
|
||||
td0Content = V.h('span', { props: { className: 'icon ion-location', title: _.t('location.location') } });
|
||||
}
|
||||
|
||||
var td1Content = V.h('a', {
|
||||
props: {
|
||||
className: ['hostname', d.is_online ? 'online' : 'offline'].join(' '),
|
||||
href: router.generateLink({ node: d.node_id })
|
||||
}, on: {
|
||||
click: function (e) {
|
||||
router.fullUrl({ node: d.node_id }, e);
|
||||
}
|
||||
}
|
||||
}, d.hostname);
|
||||
|
||||
return V.h('tr', [
|
||||
V.h('td', td0Content),
|
||||
V.h('td', td1Content),
|
||||
V.h('td', moment(d[field]).from(data.now))
|
||||
]);
|
||||
});
|
||||
|
||||
var tbodyNew = V.h('tbody', items);
|
||||
tbody = V.patch(tbody, tbodyNew);
|
||||
};
|
||||
|
||||
return self;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
define(['snabbdom'], function (V) {
|
||||
'use strict';
|
||||
V = V.default;
|
||||
|
||||
return function (headings, sortIndex, renderRow) {
|
||||
var self = this;
|
||||
var data;
|
||||
var sortReverse = false;
|
||||
self.el = document.createElement('table');
|
||||
|
||||
function sortTable(i) {
|
||||
sortReverse = i === sortIndex ? !sortReverse : false;
|
||||
sortIndex = i;
|
||||
|
||||
updateView();
|
||||
}
|
||||
|
||||
function sortTableHandler(i) {
|
||||
return function () {
|
||||
sortTable(i);
|
||||
};
|
||||
}
|
||||
|
||||
function updateView() {
|
||||
var children = [];
|
||||
|
||||
if (data.length !== 0) {
|
||||
var th = headings.map(function (d, i) {
|
||||
var name = _.t(d.name);
|
||||
var properties = {
|
||||
onclick: sortTableHandler(i),
|
||||
className: 'sort-header'
|
||||
};
|
||||
|
||||
if (d.class) {
|
||||
properties.className += ' ' + d.class;
|
||||
properties.title = name;
|
||||
name = '';
|
||||
}
|
||||
|
||||
if (sortIndex === i) {
|
||||
properties.className += sortReverse ? ' sort-up' : ' sort-down';
|
||||
}
|
||||
|
||||
return V.h('th', { props: properties }, name);
|
||||
});
|
||||
|
||||
var links = data.slice(0).sort(headings[sortIndex].sort);
|
||||
|
||||
if (headings[sortIndex].reverse ? !sortReverse : sortReverse) {
|
||||
links = links.reverse();
|
||||
}
|
||||
|
||||
children.push(V.h('thead', V.h('tr', th)));
|
||||
children.push(V.h('tbody', links.map(renderRow)));
|
||||
}
|
||||
|
||||
var elNew = V.h('table', children);
|
||||
self.el = V.patch(self.el, elNew);
|
||||
}
|
||||
|
||||
self.setData = function setData(d) {
|
||||
data = d;
|
||||
updateView();
|
||||
};
|
||||
|
||||
return self;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
define(function () {
|
||||
'use strict';
|
||||
|
||||
return function () {
|
||||
var self = this;
|
||||
|
||||
var tabs = document.createElement('ul');
|
||||
tabs.classList.add('tabs');
|
||||
|
||||
var container = document.createElement('div');
|
||||
|
||||
function gotoTab(li) {
|
||||
for (var i = 0; i < tabs.children.length; i++) {
|
||||
tabs.children[i].classList.remove('visible');
|
||||
}
|
||||
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
|
||||
li.classList.add('visible');
|
||||
|
||||
var tab = document.createElement('div');
|
||||
tab.classList.add('tab');
|
||||
container.appendChild(tab);
|
||||
li.child.render(tab);
|
||||
}
|
||||
|
||||
function switchTab() {
|
||||
gotoTab(this);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
self.add = function add(title, d) {
|
||||
var li = document.createElement('li');
|
||||
li.textContent = _.t(title);
|
||||
li.onclick = switchTab;
|
||||
li.child = d;
|
||||
tabs.appendChild(li);
|
||||
|
||||
var anyVisible = false;
|
||||
|
||||
for (var i = 0; i < tabs.children.length; i++) {
|
||||
if (tabs.children[i].classList.contains('visible')) {
|
||||
anyVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyVisible) {
|
||||
gotoTab(li);
|
||||
}
|
||||
};
|
||||
|
||||
self.render = function render(el) {
|
||||
el.appendChild(tabs);
|
||||
el.appendChild(container);
|
||||
};
|
||||
|
||||
return self;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
define(function () {
|
||||
'use strict';
|
||||
|
||||
return function () {
|
||||
function setTitle(d) {
|
||||
var title = [config.siteName];
|
||||
|
||||
if (d !== undefined) {
|
||||
title.unshift(d);
|
||||
}
|
||||
|
||||
document.title = title.join(' - ');
|
||||
}
|
||||
|
||||
this.resetView = function resetView() {
|
||||
setTitle();
|
||||
};
|
||||
|
||||
this.gotoNode = function gotoNode(d) {
|
||||
setTitle(d.hostname);
|
||||
};
|
||||
|
||||
this.gotoLink = function gotoLink(d) {
|
||||
setTitle(d[0].source.hostname + ' \u21D4 ' + d[0].target.hostname);
|
||||
};
|
||||
|
||||
this.gotoLocation = function gotoLocation() {
|
||||
// ignore
|
||||
};
|
||||
|
||||
this.destroy = function destroy() {
|
||||
};
|
||||
|
||||
return this;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,206 @@
|
|||
'use strict';
|
||||
|
||||
define({
|
||||
get: function get(url) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var req = new XMLHttpRequest();
|
||||
req.open('GET', url);
|
||||
|
||||
req.onload = function onload() {
|
||||
if (req.status === 200) {
|
||||
resolve(req.response);
|
||||
} else {
|
||||
reject(Error(req.statusText));
|
||||
}
|
||||
};
|
||||
|
||||
req.onerror = function onerror() {
|
||||
reject(Error('Network Error'));
|
||||
};
|
||||
|
||||
req.send();
|
||||
});
|
||||
},
|
||||
|
||||
getJSON: function getJSON(url) {
|
||||
return require('helper').get(url).then(JSON.parse);
|
||||
},
|
||||
|
||||
sortByKey: function sortByKey(key, d) {
|
||||
return d.sort(function (a, b) {
|
||||
return b[key] - a[key];
|
||||
});
|
||||
},
|
||||
|
||||
limit: function limit(key, m, d) {
|
||||
return d.filter(function (n) {
|
||||
return n[key].isAfter(m);
|
||||
});
|
||||
},
|
||||
|
||||
sum: function sum(a) {
|
||||
return a.reduce(function (b, c) {
|
||||
return b + c;
|
||||
}, 0);
|
||||
},
|
||||
|
||||
one: function one() {
|
||||
return 1;
|
||||
},
|
||||
|
||||
dictGet: function dictGet(dict, key) {
|
||||
var k = key.shift();
|
||||
|
||||
if (!(k in dict)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key.length === 0) {
|
||||
return dict[k];
|
||||
}
|
||||
|
||||
return this.dictGet(dict[k], key);
|
||||
},
|
||||
|
||||
listReplace: function listReplace(s, subst) {
|
||||
for (var key in subst) {
|
||||
if (subst.hasOwnProperty(key)) {
|
||||
var re = new RegExp(key, 'g');
|
||||
s = s.replace(re, subst[key]);
|
||||
}
|
||||
}
|
||||
return s;
|
||||
},
|
||||
|
||||
hasLocation: function hasLocation(d) {
|
||||
return 'location' in d &&
|
||||
Math.abs(d.location.latitude) < 90 &&
|
||||
Math.abs(d.location.longitude) < 180;
|
||||
},
|
||||
|
||||
subtract: function subtract(a, b) {
|
||||
var ids = {};
|
||||
|
||||
b.forEach(function (d) {
|
||||
ids[d.node_id] = true;
|
||||
});
|
||||
|
||||
return a.filter(function (d) {
|
||||
return !ids[d.node_id];
|
||||
});
|
||||
},
|
||||
|
||||
/* Helpers working with links */
|
||||
|
||||
showDistance: function showDistance(d) {
|
||||
if (isNaN(d.distance)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return d.distance.toFixed(0) + ' m';
|
||||
},
|
||||
|
||||
showTq: function showTq(d) {
|
||||
return (d * 100).toFixed(0) + '%';
|
||||
},
|
||||
|
||||
attributeEntry: function attributeEntry(V, children, label, value) {
|
||||
if (value !== undefined) {
|
||||
if (typeof value !== 'object') {
|
||||
value = V.h('td', value);
|
||||
}
|
||||
|
||||
children.push(V.h('tr', [
|
||||
V.h('th', _.t(label)),
|
||||
value
|
||||
]));
|
||||
}
|
||||
},
|
||||
showStat: function showStat(V, o, subst) {
|
||||
var content = V.h('img', { attrs: { src: require('helper').listReplace(o.image, subst), width: o.width, height: o.height, alt: _.t('loading', { name: o.name }) } });
|
||||
|
||||
if (o.href) {
|
||||
return V.h('div', V.h('a', {
|
||||
attrs:
|
||||
{
|
||||
href: require('helper').listReplace(o.href, subst),
|
||||
target: '_blank',
|
||||
title: require('helper').listReplace(o.title, subst)
|
||||
}
|
||||
}, content));
|
||||
}
|
||||
return V.h('div', content);
|
||||
},
|
||||
|
||||
getTileBBox: function getTileBBox(s, map, tileSize, margin) {
|
||||
var tl = map.unproject([s.x - margin, s.y - margin]);
|
||||
var br = map.unproject([s.x + margin + tileSize, s.y + margin + tileSize]);
|
||||
|
||||
return { minX: br.lat, minY: tl.lng, maxX: tl.lat, maxY: br.lng };
|
||||
},
|
||||
positionClients: function positionClients(ctx, p, startAngle, node, startDistance) {
|
||||
if (node.clients === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var radius = 3;
|
||||
var a = 1.2;
|
||||
var mode = 0;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = config.client.wifi24;
|
||||
|
||||
for (var orbit = 0, i = 0; i < node.clients; orbit++) {
|
||||
var distance = startDistance + orbit * 2 * radius * a;
|
||||
var n = Math.floor((Math.PI * distance) / (a * radius));
|
||||
var delta = node.clients - i;
|
||||
|
||||
for (var j = 0; j < Math.min(delta, n); i++, j++) {
|
||||
if (mode !== 1 && i >= (node.clients_wifi24 + node.clients_wifi5)) {
|
||||
mode = 1;
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = config.client.wifi5;
|
||||
} else if (mode === 0 && i >= node.clients_wifi24) {
|
||||
mode = 2;
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = config.client.other;
|
||||
}
|
||||
var angle = 2 * Math.PI / n * j;
|
||||
var x = p.x + distance * Math.cos(angle + startAngle);
|
||||
var y = p.y + distance * Math.sin(angle + startAngle);
|
||||
|
||||
ctx.moveTo(x, y);
|
||||
ctx.arc(x, y, radius, 0, 2 * Math.PI);
|
||||
}
|
||||
}
|
||||
ctx.fill();
|
||||
},
|
||||
fullscreen: function fullscreen(btn) {
|
||||
if (!document.fullscreenElement && !document.webkitFullscreenElement && !document.mozFullScreenElement) {
|
||||
var fel = document.firstElementChild;
|
||||
var func = fel.requestFullscreen
|
||||
|| fel.webkitRequestFullScreen
|
||||
|| fel.mozRequestFullScreen;
|
||||
func.call(fel);
|
||||
btn.classList.remove('ion-full-enter');
|
||||
btn.classList.add('ion-full-exit');
|
||||
} else {
|
||||
func = document.exitFullscreen
|
||||
|| document.webkitExitFullscreen
|
||||
|| document.mozCancelFullScreen;
|
||||
if (func) {
|
||||
func.call(document);
|
||||
btn.classList.remove('ion-full-exit');
|
||||
btn.classList.add('ion-full-enter');
|
||||
}
|
||||
}
|
||||
},
|
||||
escape: function escape(string) {
|
||||
return string.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
});
|
|
@ -0,0 +1,70 @@
|
|||
define(['polyglot', 'moment', 'helper'], function (Polyglot, moment, helper) {
|
||||
'use strict';
|
||||
return function () {
|
||||
var router;
|
||||
|
||||
function languageSelect(el) {
|
||||
var select = document.createElement('select');
|
||||
select.className = 'language-switch';
|
||||
select.setAttribute('aria-label', 'Language');
|
||||
select.addEventListener('change', setSelectLocale);
|
||||
el.appendChild(select);
|
||||
|
||||
// Keep english
|
||||
select.innerHTML = '<option>Language</option>';
|
||||
for (var i = 0; i < config.supportedLocale.length; i++) {
|
||||
select.innerHTML += '<option value="' + config.supportedLocale[i] + '">' + config.supportedLocale[i] + '</option>';
|
||||
}
|
||||
}
|
||||
|
||||
function setSelectLocale(event) {
|
||||
router.fullUrl({ lang: event.target.value }, false, true);
|
||||
}
|
||||
|
||||
function getLocale(input) {
|
||||
var language = input || navigator.languages && navigator.languages[0] || navigator.language;
|
||||
var locale = config.supportedLocale[0];
|
||||
config.supportedLocale.some(function (item) {
|
||||
if (language.indexOf(item) !== -1) {
|
||||
locale = item;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return locale;
|
||||
}
|
||||
|
||||
function setTranslation(json) {
|
||||
_.extend(json);
|
||||
|
||||
if (moment.locale(_.locale()) !== _.locale()) {
|
||||
moment.defineLocale(_.locale(), {
|
||||
longDateFormat: {
|
||||
LT: 'HH:mm',
|
||||
LTS: 'HH:mm:ss',
|
||||
L: 'DD.MM.YYYY',
|
||||
LL: 'D. MMMM YYYY',
|
||||
LLL: 'D. MMMM YYYY HH:mm',
|
||||
LLLL: 'dddd, D. MMMM YYYY HH:mm'
|
||||
},
|
||||
calendar: json.momentjs.calendar,
|
||||
relativeTime: json.momentjs.relativeTime
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function init(r) {
|
||||
router = r;
|
||||
/** global: _ */
|
||||
window._ = new Polyglot({ locale: getLocale(router.getLang()), allowMissing: true });
|
||||
helper.getJSON('locale/' + _.locale() + '.json?' + config.cacheBreaker).then(setTranslation);
|
||||
document.querySelector('html').setAttribute('lang', _.locale());
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
getLocale: getLocale,
|
||||
languageSelect: languageSelect
|
||||
};
|
||||
};
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
define(function () {
|
||||
var self = {};
|
||||
|
||||
self.distance = function distance(a, b) {
|
||||
return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
|
||||
};
|
||||
|
||||
self.distancePoint = function distancePoint(a, b) {
|
||||
return Math.sqrt(self.distance(a, b));
|
||||
};
|
||||
|
||||
self.distanceLink = function distanceLink(p, a, b) {
|
||||
/* http://stackoverflow.com/questions/849211 */
|
||||
var l2 = self.distance(a, b);
|
||||
if (l2 === 0) {
|
||||
return self.distance(p, a);
|
||||
}
|
||||
var t = ((p.x - a.x) * (b.x - a.x) + (p.y - a.y) * (b.y - a.y)) / l2;
|
||||
if (t < 0) {
|
||||
return self.distance(p, a);
|
||||
} else if (t > 1) {
|
||||
return self.distance(p, b);
|
||||
}
|
||||
return self.distancePoint(p, {
|
||||
x: a.x + t * (b.x - a.x),
|
||||
y: a.y + t * (b.y - a.y)
|
||||
});
|
||||
};
|
||||
|
||||
return self;
|
||||
});
|
|
@ -0,0 +1,144 @@
|
|||
define(['snabbdom', 'helper', 'moment'], function (V, helper, moment) {
|
||||
'use strict';
|
||||
V = V.default;
|
||||
|
||||
var self = {};
|
||||
|
||||
function showBar(v, width, warning) {
|
||||
return V.h('span',
|
||||
{ props: { className: 'bar' + (warning ? ' warning' : '') } },
|
||||
[
|
||||
V.h('span',
|
||||
{
|
||||
style: { width: (width * 100) + '%' }
|
||||
}),
|
||||
V.h('label', v)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
self.showStatus = function showStatus(d) {
|
||||
return V.h('td',
|
||||
{ props: { className: d.is_online ? 'online' : 'offline' } },
|
||||
_.t((d.is_online ? 'node.lastOnline' : 'node.lastOffline'), {
|
||||
time: d.lastseen.fromNow(),
|
||||
date: d.lastseen.format('DD.MM.YYYY, H:mm:ss')
|
||||
}));
|
||||
};
|
||||
|
||||
self.showGeoURI = function showGeoURI(d) {
|
||||
if (!helper.hasLocation(d)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return V.h('td',
|
||||
V.h('a',
|
||||
{ props: { href: 'geo:' + d.location.latitude + ',' + d.location.longitude } },
|
||||
Number(d.location.latitude.toFixed(6)) + ', ' + Number(d.location.longitude.toFixed(6))
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
self.showGateway = function showGateway(d) {
|
||||
return d.is_gateway ? _.t('yes') : undefined;
|
||||
};
|
||||
|
||||
self.showFirmware = function showFirmware(d) {
|
||||
return [
|
||||
helper.dictGet(d, ['firmware', 'release']),
|
||||
helper.dictGet(d, ['firmware', 'base'])
|
||||
].filter(function (n) {
|
||||
return n !== null;
|
||||
}).join(' / ') || undefined;
|
||||
};
|
||||
|
||||
self.showUptime = function showUptime(d) {
|
||||
return moment.utc(d.uptime).local().fromNow(true);
|
||||
};
|
||||
|
||||
self.showFirstSeen = function showFirstSeen(d) {
|
||||
return d.firstseen.fromNow(true);
|
||||
};
|
||||
|
||||
self.showLoad = function showLoad(d) {
|
||||
return showBar(d.loadavg.toFixed(2), d.loadavg / (d.nproc || 1), d.loadavg >= d.nproc);
|
||||
};
|
||||
|
||||
self.showRAM = function showRAM(d) {
|
||||
return showBar(Math.round(d.memory_usage * 100) + ' %', d.memory_usage, d.memory_usage >= 0.8);
|
||||
};
|
||||
|
||||
self.showDomain = function showDomain(d) {
|
||||
var rt = d.domain;
|
||||
if (config.domainNames) {
|
||||
config.domainNames.some(function (t) {
|
||||
if (rt === t.domain) {
|
||||
rt = t.name;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
return rt;
|
||||
};
|
||||
|
||||
self.showClients = function showClients(d) {
|
||||
if (!d.is_online) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
var clients = [
|
||||
V.h('span', [
|
||||
d.clients > 0 ? d.clients : _.t('none'),
|
||||
V.h('br'),
|
||||
V.h('i', { props: { className: 'ion-people', title: _.t('node.clients') } })
|
||||
]),
|
||||
V.h('span',
|
||||
{ props: { className: 'legend-24ghz' } },
|
||||
[
|
||||
d.clients_wifi24,
|
||||
V.h('br'),
|
||||
V.h('span', { props: { className: 'symbol', title: '2,4 GHz' } })
|
||||
]),
|
||||
V.h('span',
|
||||
{ props: { className: 'legend-5ghz' } },
|
||||
[
|
||||
d.clients_wifi5,
|
||||
V.h('br'),
|
||||
V.h('span', { props: { className: 'symbol', title: '5 GHz' } })
|
||||
]),
|
||||
V.h('span',
|
||||
{ props: { className: 'legend-others' } },
|
||||
[
|
||||
d.clients_other,
|
||||
V.h('br'),
|
||||
V.h('span', { props: { className: 'symbol', title: _.t('others') } })
|
||||
])
|
||||
];
|
||||
|
||||
return V.h('td', { props: { className: 'clients' } }, clients);
|
||||
};
|
||||
|
||||
self.showIPs = function showIPs(d) {
|
||||
var string = [];
|
||||
var ips = d.addresses;
|
||||
ips.sort();
|
||||
ips.forEach(function (ip, i) {
|
||||
if (i > 0) {
|
||||
string.push(V.h('br'));
|
||||
}
|
||||
|
||||
if (ip.indexOf('fe80:') !== 0) {
|
||||
string.push(V.h('a', { props: { href: 'http://[' + ip + ']/', target: '_blank' } }, ip));
|
||||
} else {
|
||||
string.push(ip);
|
||||
}
|
||||
});
|
||||
return V.h('td', string);
|
||||
};
|
||||
|
||||
self.showAutoupdate = function showAutoupdate(d) {
|
||||
return d.autoupdater.enabled ? _.t('node.activated', { branch: d.autoupdater.branch }) : _.t('node.deactivated');
|
||||
};
|
||||
|
||||
return self;
|
||||
});
|
|
@ -0,0 +1,150 @@
|
|||
define(['Navigo'], function (Navigo) {
|
||||
'use strict';
|
||||
|
||||
return function (language) {
|
||||
var init = false;
|
||||
var objects = {};
|
||||
var targets = [];
|
||||
var views = {};
|
||||
var current = {};
|
||||
var state = { lang: language.getLocale(), view: 'map' };
|
||||
|
||||
function resetView() {
|
||||
targets.forEach(function (t) {
|
||||
t.resetView();
|
||||
});
|
||||
}
|
||||
|
||||
function gotoNode(d) {
|
||||
if (objects.nodeDict[d.nodeId]) {
|
||||
targets.forEach(function (t) {
|
||||
t.gotoNode(objects.nodeDict[d.nodeId], objects.nodeDict);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function gotoLink(d) {
|
||||
var link = objects.links.filter(function (value) {
|
||||
return value.id === d.linkId;
|
||||
});
|
||||
if (link) {
|
||||
targets.forEach(function (t) {
|
||||
t.gotoLink(link);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function view(d) {
|
||||
if (d.view in views) {
|
||||
views[d.view]();
|
||||
state.view = d.view;
|
||||
resetView();
|
||||
}
|
||||
}
|
||||
|
||||
function customRoute(lang, viewValue, node, link, zoom, lat, lng) {
|
||||
current = {
|
||||
lang: lang,
|
||||
view: viewValue,
|
||||
node: node,
|
||||
link: link,
|
||||
zoom: zoom,
|
||||
lat: lat,
|
||||
lng: lng
|
||||
};
|
||||
|
||||
if (lang && lang !== state.lang && lang === language.getLocale(lang)) {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
if (!init || viewValue && viewValue !== state.view) {
|
||||
if (!viewValue) {
|
||||
viewValue = state.view;
|
||||
}
|
||||
view({ view: viewValue });
|
||||
init = true;
|
||||
}
|
||||
|
||||
if (node) {
|
||||
gotoNode({ nodeId: node });
|
||||
} else if (link) {
|
||||
gotoLink({ linkId: link });
|
||||
} else if (lat) {
|
||||
targets.forEach(function (t) {
|
||||
t.gotoLocation({
|
||||
zoom: parseInt(zoom, 10),
|
||||
lat: parseFloat(lat),
|
||||
lng: parseFloat(lng)
|
||||
});
|
||||
});
|
||||
} else {
|
||||
resetView();
|
||||
}
|
||||
}
|
||||
|
||||
var router = new Navigo(null, true, '#!');
|
||||
|
||||
router
|
||||
.on(/^\/?#?!?\/([\w]{2})?\/?(map|graph)?\/?([a-f\d]{12})?([a-f\d\-]{25})?\/?(?:(\d+)\/(-?[\d.]+)\/(-?[\d.]+))?$/, customRoute)
|
||||
.on({
|
||||
'*': function () {
|
||||
router.fullUrl();
|
||||
}
|
||||
});
|
||||
|
||||
router.generateLink = function generateLink(data, full, deep) {
|
||||
var result = '#!';
|
||||
|
||||
if (full) {
|
||||
data = Object.assign({}, state, data);
|
||||
} else if (deep) {
|
||||
data = Object.assign({}, current, data);
|
||||
}
|
||||
|
||||
for (var key in data) {
|
||||
if (!data.hasOwnProperty(key) || data[key] === undefined) {
|
||||
continue;
|
||||
}
|
||||
result += '/' + data[key];
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
router.fullUrl = function fullUrl(data, e, deep) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
router.navigate(router.generateLink(data, !deep, deep));
|
||||
};
|
||||
|
||||
router.getLang = function getLang() {
|
||||
var lang = location.hash.match(/^\/?#!?\/([\w]{2})\//);
|
||||
if (lang) {
|
||||
state.lang = language.getLocale(lang[1]);
|
||||
return lang[1];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
router.addTarget = function addTarget(d) {
|
||||
targets.push(d);
|
||||
};
|
||||
|
||||
router.removeTarget = function removeTarget(d) {
|
||||
targets = targets.filter(function (e) {
|
||||
return d !== e;
|
||||
});
|
||||
};
|
||||
|
||||
router.addView = function addView(k, d) {
|
||||
views[k] = d;
|
||||
};
|
||||
|
||||
router.setData = function setData(data) {
|
||||
objects = data;
|
||||
};
|
||||
|
||||
return router;
|
||||
};
|
||||
});
|
|
@ -0,0 +1,99 @@
|
|||
define(function () {
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
reimplate after node-deb-version-compare under MIT
|
||||
(https://github.com/sdumetz/node-deb-version-compare)
|
||||
*/
|
||||
|
||||
function Version(v) {
|
||||
var version = /^[a-zA-Z]?([0-9]*(?=:))?:(.*)/.exec(v);
|
||||
this.epoch = (version) ? version[1] : 0;
|
||||
version = (version && version[2]) ? version[2] : v;
|
||||
version = version.split('-');
|
||||
this.debian = (version.length > 1) ? version.pop() : '';
|
||||
this.upstream = version.join('-');
|
||||
}
|
||||
|
||||
Version.prototype.compare = function (b) {
|
||||
if ((this.epoch > 0 || b.epoch > 0) && Math.sign(this.epoch - b.epoch) !== 0) {
|
||||
return Math.sign(this.epoch - b.epoch);
|
||||
}
|
||||
if (this.compareStrings(this.upstream, b.upstream) !== 0) {
|
||||
return this.compareStrings(this.upstream, b.upstream);
|
||||
}
|
||||
return this.compareStrings(this.debian, b.debian);
|
||||
};
|
||||
|
||||
Version.prototype.charCode = function (c) { // the lower the charcode the lower the version.
|
||||
// if (c === '~') {return 0;} // tilde sort before anything
|
||||
// else
|
||||
if (/[a-zA-Z]/.test(c)) {
|
||||
return c.charCodeAt(0) - 'A'.charCodeAt(0) + 1;
|
||||
} else if (/[.:+-:]/.test(c)) {
|
||||
return c.charCodeAt(0) + 'z'.charCodeAt(0) + 1;
|
||||
} // charcodes are 46..58
|
||||
return 0;
|
||||
};
|
||||
|
||||
// find index of "val" in "ar".
|
||||
Version.prototype.findIndex = function (ar, fn) {
|
||||
for (var i = 0; i < ar.length; i++) {
|
||||
if (fn(ar[i], i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
Version.prototype.compareChunk = function (a, b) {
|
||||
var ca = a.split('');
|
||||
var cb = b.split('');
|
||||
var diff = this.findIndex(ca, function (c, index) {
|
||||
return !(cb[index] && c === cb[index]);
|
||||
});
|
||||
if (diff === -1) {
|
||||
if (cb.length > ca.length) {
|
||||
if (cb[ca.length] === '~') {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return 0; // no diff found and same length
|
||||
} else if (!cb[diff]) {
|
||||
return (ca[diff] === '~') ? -1 : 1;
|
||||
}
|
||||
return (this.charCode(ca[diff]) > this.charCode(cb[diff])) ? 1 : -1;
|
||||
};
|
||||
|
||||
Version.prototype.compareStrings = function (a, b) {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
var parseA = /([^0-9]+|[0-9]+)/g;
|
||||
var parseB = /([^0-9]+|[0-9]+)/g;
|
||||
var ra = parseA.exec(a);
|
||||
var rb = parseB.exec(b);
|
||||
while (ra !== null && rb !== null) {
|
||||
if ((isNaN(ra[1]) || isNaN(rb[1])) && ra[1] !== rb[1]) { // a or b is not a number and they're not equal. Note : "" IS a number so both null is impossible
|
||||
return this.compareChunk(ra[1], rb[1]);
|
||||
} // both are numbers
|
||||
if (ra[1] !== rb[1]) {
|
||||
return (parseInt(ra[1], 10) > parseInt(rb[1], 10)) ? 1 : -1;
|
||||
}
|
||||
ra = parseA.exec(a);
|
||||
rb = parseB.exec(b);
|
||||
}
|
||||
if (!ra && rb) { // rb doesn't get exec-ed when ra == null
|
||||
return (rb.length > 0 && rb[1].split('')[0] === '~') ? 1 : -1;
|
||||
} else if (ra && !rb) {
|
||||
return (ra[1].split('')[0] === '~') ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
return function compare(a, b) {
|
||||
var va = new Version(a[0]);
|
||||
var vb = new Version(b[0]);
|
||||
return vb.compare(va);
|
||||
};
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
"node": {
|
||||
"all": "Všechny uzly",
|
||||
"nodes": "Uzly",
|
||||
"uptime": "Celková doba provozu",
|
||||
"links": "Odkazy",
|
||||
"clients": "Klienti",
|
||||
"distance": "Vzdálenost",
|
||||
"connectionType": "typ připojení",
|
||||
"tq": "tq",
|
||||
"lastOnline": "poslední on-line %{time} (%{date})",
|
||||
"lastOffline": "lastOffline %{time} (%{date})",
|
||||
"activated": "aktivováno (%{branch})",
|
||||
"deactivated": "deaktivováno",
|
||||
"status": "Stav",
|
||||
"firmware": "Verze firmwaru",
|
||||
"hardware": "Model hardwaru",
|
||||
"visible": "Visible on the map",
|
||||
"update": "Automatický update",
|
||||
"domain": "Domain",
|
||||
"gateway": "Brána",
|
||||
"coordinates": "Souřadnice",
|
||||
"contact": "Kontakt",
|
||||
"primaryMac": "Hlavní MAC",
|
||||
"id": "Identifikace uzlu",
|
||||
"firstSeen": "firstSeen",
|
||||
"systemLoad": "Průměrné zatížení",
|
||||
"ram": "Využití paměti",
|
||||
"ipAddresses": "IP adresa",
|
||||
"nexthop": "Další skok",
|
||||
"selectedGatewayIPv4": "vybranýGatewayIPv4",
|
||||
"selectedGatewayIPv6": "vybranýGatewayIPv6",
|
||||
"link": "Odkaz ||| Odkazy",
|
||||
"node": "Uzel ||| Uzly",
|
||||
"new": "Nové uzly",
|
||||
"missing": "Zmizelé uzly"
|
||||
},
|
||||
"location": {
|
||||
"location": "Poloha",
|
||||
"latitude": "Zeměpisná šířka",
|
||||
"longitude": "Zeměpisná délka",
|
||||
"copy": "Kopírovat"
|
||||
},
|
||||
"sidebar": {
|
||||
"nodeFilter": "nodeFilter",
|
||||
"nodes": "%{total} uzly, %{online} uzly on-line",
|
||||
"clients": "%{smart_count} klienti |||| %{smart_count} klienti",
|
||||
"gateway": " %{smart_count} gateway |||| %{smart_count} gateways",
|
||||
"lastUpdate": "Poslední update",
|
||||
"nodeNew": "nodeNew",
|
||||
"nodeOnline": "Uzel je online",
|
||||
"nodeOffline": "Uzel je offline",
|
||||
"aboutInfo": "aboutInfo",
|
||||
"actual": "aktuální",
|
||||
"stats": "Statistika",
|
||||
"about": "O produktu",
|
||||
"toggle": "přepínat"
|
||||
},
|
||||
"button": {
|
||||
"switchView": "Přepnout zobrazení",
|
||||
"location": "Vybrat souřadnice",
|
||||
"tracking": "Lokalizace"
|
||||
},
|
||||
"momentjs": {
|
||||
"calendar": {
|
||||
"sameDay": "[Today at] LT",
|
||||
"nextDay": "[Tomorrow at] LT",
|
||||
"nextWeek": "dddd [at] LT",
|
||||
"lastDay": "[Yesterday at] LT",
|
||||
"lastWeek": "[Last] dddd [at] LT",
|
||||
"sameElse": "L"
|
||||
},
|
||||
"relativeTime": {
|
||||
"future": "in %s",
|
||||
"past": "%s ago",
|
||||
"s": "Několik sekund",
|
||||
"m": "minuta",
|
||||
"mm": "%d minut",
|
||||
"h": "an hour",
|
||||
"hh": "%d hodin",
|
||||
"d": "den",
|
||||
"dd": "%d dnů",
|
||||
"M": "měsíc",
|
||||
"MM": "%d měsíců",
|
||||
"y": "rok",
|
||||
"yy": "%d let"
|
||||
}
|
||||
},
|
||||
"yes": "ano",
|
||||
"no": "ne",
|
||||
"unknown": "neznámý",
|
||||
"others": "ostatní",
|
||||
"none": "žádný",
|
||||
"remove": "odstranit",
|
||||
"close": "zavřít"
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"node": {
|
||||
"all": "Alle Knoten",
|
||||
"nodes": "Knoten",
|
||||
"uptime": "Laufzeit",
|
||||
"links": "Verbindungen",
|
||||
"clients": "Nutzer",
|
||||
"distance": "Entfernung",
|
||||
"connectionType": "Verbindungsart",
|
||||
"tq": "Übertragungsqualität",
|
||||
"lastOnline": "online, letzte Nachricht %{time} (%{date})",
|
||||
"lastOffline": "offline, letzte Nachricht %{time} (%{date})",
|
||||
"activated": "aktiviert (%{branch})",
|
||||
"deactivated": "deaktiviert",
|
||||
"status": "Status",
|
||||
"firmware": "Firmware-Version",
|
||||
"hardware": "Geräte-Modell",
|
||||
"visible": "Auf der Karte sichtbar",
|
||||
"update": "Auto-Update",
|
||||
"domain": "Domain",
|
||||
"gateway": "Gateway",
|
||||
"coordinates": "Koordinaten",
|
||||
"contact": "Kontakt",
|
||||
"primaryMac": "Primäre MAC",
|
||||
"id": "Knoten ID",
|
||||
"firstSeen": "Erstmals gesehen",
|
||||
"systemLoad": "Systemlast",
|
||||
"ram": "Speicherauslastung",
|
||||
"ipAddresses": "IP Adressen",
|
||||
"nexthop": "Nächster Sprung",
|
||||
"selectedGatewayIPv4": "Gewähltes ipv4 Gateway",
|
||||
"selectedGatewayIPv6": "Gewähltes ipv6 Gateway",
|
||||
"link": "Verbindung |||| Verbindungen",
|
||||
"node": "Knoten",
|
||||
"new": "Neue Knoten",
|
||||
"missing": "Verschwundene Knoten"
|
||||
},
|
||||
"location": {
|
||||
"location": "Standort",
|
||||
"latitude": "Breitengrad",
|
||||
"longitude": "Längengrad",
|
||||
"copy": "Kopieren"
|
||||
},
|
||||
"sidebar": {
|
||||
"nodeFilter": "Knotenfilter",
|
||||
"nodes": "%{total} Knoten, davon %{online} Knoten online",
|
||||
"clients": "mit %{smart_count} Nutzer |||| mit %{smart_count} Nutzern",
|
||||
"gateway": "auf %{smart_count} Gateway |||| auf %{smart_count} Gateways",
|
||||
"lastUpdate": "Letzte Aktualisierung",
|
||||
"nodeNew": "neu",
|
||||
"nodeOnline": "online",
|
||||
"nodeOffline": "offline",
|
||||
"aboutInfo": "<h2>Über Meshviewer</h2><p>Mit Doppelklick kann man in die Karte hinein zoomen und Shift+Doppelklick heraus zoomen.</p>",
|
||||
"actual": "Aktuell",
|
||||
"stats": "Statistiken",
|
||||
"about": "Über",
|
||||
"toggle": "Seitenleiste anzeigen/ausblenden"
|
||||
},
|
||||
"button": {
|
||||
"switchView": "Ansicht wechseln",
|
||||
"location": "Koordinaten wählen",
|
||||
"tracking": "Lokalisierung",
|
||||
"fullscreen": "Vollbildmodus wechseln"
|
||||
},
|
||||
"momentjs": {
|
||||
"calendar": {
|
||||
"sameDay": "[heute um] LT [Uhr]",
|
||||
"nextDay": "[morgen um] LT [Uhr]",
|
||||
"nextWeek": "dddd [um] LT [Uhr]",
|
||||
"lastDay": "[gestern um] LT [Uhr]",
|
||||
"lastWeek": "[letzten] dddd [um] LT [Uhr]",
|
||||
"sameElse": "L"
|
||||
},
|
||||
"relativeTime": {
|
||||
"future": "in %s",
|
||||
"past": "vor %s",
|
||||
"s": "ein paar Sekunden",
|
||||
"m": "einer Minute",
|
||||
"mm": "%d Minuten",
|
||||
"h": "einer Stunde",
|
||||
"hh": "%d Stunden",
|
||||
"d": "einem Tag",
|
||||
"dd": "%d Tagen",
|
||||
"M": "einem Monat",
|
||||
"MM": "%d Monate",
|
||||
"y": "einem Jahr",
|
||||
"yy": "%d Jahre"
|
||||
}
|
||||
},
|
||||
"yes": "ja",
|
||||
"no": "nein",
|
||||
"unknown": "unbekannt",
|
||||
"others": "andere",
|
||||
"none": "keine",
|
||||
"remove": "entfernen",
|
||||
"close": "schließen",
|
||||
"loading": "%{name} graph (wird generiert)"
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"node": {
|
||||
"all": "All nodes",
|
||||
"nodes": "Nodes",
|
||||
"uptime": "Uptime",
|
||||
"links": "Links",
|
||||
"clients": "Clients",
|
||||
"distance": "Distance",
|
||||
"connectionType": "Connection type",
|
||||
"tq": "Transmit quality",
|
||||
"lastOnline": "online, last message %{time} (%{date})",
|
||||
"lastOffline": "offline, last message %{time} (%{date})",
|
||||
"activated": "activated (%{branch})",
|
||||
"deactivated": "deactivated",
|
||||
"status": "Status",
|
||||
"firmware": "Firmware version",
|
||||
"hardware": "Hardware model",
|
||||
"visible": "Visible on the map",
|
||||
"update": "Auto update",
|
||||
"domain": "Domain",
|
||||
"gateway": "Gateway",
|
||||
"coordinates": "Coordinates",
|
||||
"contact": "Contact",
|
||||
"primaryMac": "Primary MAC",
|
||||
"id": "Node ID",
|
||||
"firstSeen": "First seen",
|
||||
"systemLoad": "Load average",
|
||||
"ram": "Memory usage",
|
||||
"ipAddresses": "IP addresses",
|
||||
"nexthop": "Nexthop",
|
||||
"selectedGatewayIPv4": "Selected ipv4-gateway",
|
||||
"selectedGatewayIPv6": "Selected ipv6-gateway",
|
||||
"link": "Link |||| Links",
|
||||
"node": "Node |||| Nodes",
|
||||
"new": "New nodes",
|
||||
"missing": "Disappeared nodes"
|
||||
},
|
||||
"location": {
|
||||
"location": "Location",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"copy": "Copy"
|
||||
},
|
||||
"sidebar": {
|
||||
"nodeFilter": "Node filter",
|
||||
"nodes": "%{total} nodes, including %{online} nodes online",
|
||||
"clients": "with %{smart_count} client |||| with %{smart_count} clients",
|
||||
"gateway": "on %{smart_count} gateway |||| on %{smart_count} gateways",
|
||||
"lastUpdate": "Last update",
|
||||
"nodeNew": "new",
|
||||
"nodeOnline": "online",
|
||||
"nodeOffline": "offline",
|
||||
"aboutInfo": "<h2>About Meshviewer</h2> <p>You can zoom in with double-click and zoom out with shift+double-click</p>",
|
||||
"actual": "Current",
|
||||
"stats": "Statistics",
|
||||
"about": "About",
|
||||
"toggle": "Toggle Sidebar"
|
||||
},
|
||||
"button": {
|
||||
"switchView": "Switch view",
|
||||
"location": "Pick coordinates",
|
||||
"tracking": "Localisation",
|
||||
"fullscreen": "Toggle fullscreen"
|
||||
},
|
||||
"momentjs": {
|
||||
"calendar": {
|
||||
"sameDay": "[Today at] LT",
|
||||
"nextDay": "[Tomorrow at] LT",
|
||||
"nextWeek": "dddd [at] LT",
|
||||
"lastDay": "[Yesterday at] LT",
|
||||
"lastWeek": "[Last] dddd [at] LT",
|
||||
"sameElse": "L"
|
||||
},
|
||||
"relativeTime": {
|
||||
"future": "in %s",
|
||||
"past": "%s ago",
|
||||
"s": "a few seconds",
|
||||
"m": "a minute",
|
||||
"mm": "%d minutes",
|
||||
"h": "an hour",
|
||||
"hh": "%d hours",
|
||||
"d": "a day",
|
||||
"dd": "%d days",
|
||||
"M": "a month",
|
||||
"MM": "%d months",
|
||||
"y": "a year",
|
||||
"yy": "%d years"
|
||||
}
|
||||
},
|
||||
"yes": "yes",
|
||||
"no": "no",
|
||||
"unknown": "unknown",
|
||||
"others": "other",
|
||||
"none": "none",
|
||||
"remove": "remove",
|
||||
"close": "close",
|
||||
"loading": "%{name} graph (is generated)"
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
"node": {
|
||||
"all": "Tous les nœuds",
|
||||
"nodes": "Nœuds",
|
||||
"uptime": "Temps de fonctionnement",
|
||||
"links": "Connexion",
|
||||
"clients": "Clients",
|
||||
"distance": "Distance",
|
||||
"connectionType": "Type de connexion",
|
||||
"tq": "Qualité de transmission",
|
||||
"lastOnline": "en ligne, dernier message %{time} (%{date})",
|
||||
"lastOffline": "hors ligne, dernier message %{time} (%{date})",
|
||||
"activated": "activé (%{branch})",
|
||||
"deactivated": "désactivé",
|
||||
"status": "Statut",
|
||||
"firmware": "Version firmware",
|
||||
"hardware": "Modèle matériel",
|
||||
"visible": "Visible sur la carte",
|
||||
"update": "Mise à jour automatique",
|
||||
"domain": "Domain",
|
||||
"gateway": "Passerelle",
|
||||
"coordinates": "Coordonnées",
|
||||
"contact": "Contact",
|
||||
"primaryMac": "MAC primaire",
|
||||
"id": "ID de nœud",
|
||||
"firstSeen": "Vu pour la première fois",
|
||||
"systemLoad": "Charge moyenne",
|
||||
"ram": "Utilisation de la mémoire",
|
||||
"ipAddresses": "Adresse IP",
|
||||
"nexthop": "Nexthop",
|
||||
"selectedGatewayIPv4": "Selected ipv4-gateway",
|
||||
"selectedGatewayIPv6": "Selected ipv6-gateway",
|
||||
"link": "Connexion |||| Connexions",
|
||||
"node": "Nœud |||| Nœuds",
|
||||
"new": "Nouveaux nœuds",
|
||||
"missing": "Nœuds disparus"
|
||||
},
|
||||
"location": {
|
||||
"location": "Lieu",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude",
|
||||
"copy": "Copier"
|
||||
},
|
||||
"sidebar": {
|
||||
"nodeFilter": "Filtre de nœud",
|
||||
"nodes": "%{total} nœud, dont %{online} nœuds en ligne",
|
||||
"clients": "avec %{smart_count} client |||| avec %{smart_count} clients",
|
||||
"gateway": "sur %{smart_count} passerelle |||| sur %{smart_count} passerelles",
|
||||
"lastUpdate": "Dernière actualisation",
|
||||
"nodeNew": "Nœud est nouveau",
|
||||
"nodeOnline": "Nœud est en ligne",
|
||||
"nodeOffline": "Nœud hors ligne",
|
||||
"aboutInfo": "<h2>Sur Meshviewer</h2> <p>Vous pouvez zoomer avec double-clic et effectuer un zoom arrière avec shift + double-clic</p>",
|
||||
"actual": "Actuel",
|
||||
"stats": "Statistiques",
|
||||
"about": "À propros",
|
||||
"toggle": "Toggle Sidebar"
|
||||
},
|
||||
"button": {
|
||||
"switchView": "Basculer l’affichage",
|
||||
"location": "Choisir les coordonnées",
|
||||
"tracking": "Localisation"
|
||||
},
|
||||
"momentjs": {
|
||||
"calendar": {
|
||||
"sameDay": "[Aujourd'hui à] LT [heures]",
|
||||
"nextDay": "[Demain à] LT [heures]",
|
||||
"nextWeek": "dddd [à] LT [heures]",
|
||||
"lastDay": "[Hier à] LT [heures]",
|
||||
"lastWeek": "[Dernier] dddd [à] LT [heures]",
|
||||
"sameElse": "L"
|
||||
},
|
||||
"relativeTime": {
|
||||
"future": "dans %s",
|
||||
"past": "il y a %s",
|
||||
"s": "quelques secondes",
|
||||
"m": "une minute",
|
||||
"mm": "%d minute",
|
||||
"h": "une heure",
|
||||
"hh": "%d heures",
|
||||
"d": "un jour",
|
||||
"dd": "%d jours",
|
||||
"M": "un mois",
|
||||
"MM": "%d mois",
|
||||
"y": "un an",
|
||||
"yy": "%d ans"
|
||||
}
|
||||
},
|
||||
"yes": "oui",
|
||||
"no": "non",
|
||||
"unknown": "inconnu",
|
||||
"others": "autres",
|
||||
"none": "aucun",
|
||||
"remove": "supprimer",
|
||||
"close": "fermer"
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
"node": {
|
||||
"all": "Все узлы",
|
||||
"nodes": "Узлы",
|
||||
"uptime": "Время работы",
|
||||
"links": "Ссылки",
|
||||
"clients": "Клиенты",
|
||||
"distance": "Расстояние",
|
||||
"connectionType": "Тип подключения",
|
||||
"tq": "Качество связи",
|
||||
"lastOnline": "в сети, последнее сообщение %{time} (%{date})",
|
||||
"lastOffline": "не в сети, последнее сообщение %{time} (%{date})",
|
||||
"activated": "активировано (%{branch})",
|
||||
"deactivated": "деактивировано",
|
||||
"status": "Статус",
|
||||
"firmware": "Версия прошивки",
|
||||
"hardware": "Тип оборудования",
|
||||
"visible": "Видно на карте",
|
||||
"update": "Автообновление",
|
||||
"domain": "Сайт",
|
||||
"gateway": "Шлюз",
|
||||
"coordinates": "Координаты",
|
||||
"contact": "Контакты",
|
||||
"primaryMac": "Основной MAC",
|
||||
"id": "Идентификатор узла",
|
||||
"firstSeen": "Впервые замечен",
|
||||
"systemLoad": "Средняя загрузка",
|
||||
"ram": "Используемая память",
|
||||
"ipAddresses": "IP адреса",
|
||||
"nexthop": "Следующий скачок",
|
||||
"selectedGatewayIPv4": "Выбранный шлюз ipv4",
|
||||
"selectedGatewayIPv6": "Выбранный шлюз ipv6",
|
||||
"link": "Ссылка |||| Ссылки",
|
||||
"node": "Узел |||| Узлы",
|
||||
"new": "Новые узлы",
|
||||
"missing": "Исчезнувшие узлы"
|
||||
},
|
||||
"location": {
|
||||
"location": "Расположение",
|
||||
"latitude": "Широта",
|
||||
"longitude": "Долгота",
|
||||
"copy": "Копировать"
|
||||
},
|
||||
"sidebar": {
|
||||
"nodeFilter": "Фильтр узлов",
|
||||
"nodes": "%{total} узлов, включая %{online} узлов онлайн",
|
||||
"clients": "с %{smart_count} клиентом |||| с %{smart_count} клиентами",
|
||||
"gateway": "на %{smart_count} шлюзе |||| на %{smart_count} шлюзах",
|
||||
"lastUpdate": "Последнее обновление",
|
||||
"nodeNew": "Узел новый",
|
||||
"nodeOnline": "Узел в сети",
|
||||
"nodeOffline": "Узел не в сети",
|
||||
"aboutInfo": "<h2>О Meshviewer</h2> <p>Вы можете увеличить масштаб двойным щелчком мыши и уменьшить с shift + двойной щелчок</p>",
|
||||
"actual": "Текущее",
|
||||
"stats": "Статистика",
|
||||
"about": "О продукте",
|
||||
"toggle": "Включить панель"
|
||||
},
|
||||
"button": {
|
||||
"switchView": "Переключить вид",
|
||||
"location": "Взять координаты",
|
||||
"tracking": "Локализация"
|
||||
},
|
||||
"momentjs": {
|
||||
"calendar": {
|
||||
"sameDay": "[Сегодня в] LT",
|
||||
"nextDay": "[Завтра в] LT",
|
||||
"nextWeek": "dddd [в] LT",
|
||||
"lastDay": "[Вчера в] LT",
|
||||
"lastWeek": "[Последний] dddd [в] LT",
|
||||
"sameElse": "L"
|
||||
},
|
||||
"relativeTime": {
|
||||
"future": "в %s",
|
||||
"past": "%s назад",
|
||||
"s": "несколько секунд",
|
||||
"m": "минута",
|
||||
"mm": "%d минут",
|
||||
"h": "час",
|
||||
"hh": "%d часов",
|
||||
"d": "день",
|
||||
"dd": "%d дней",
|
||||
"M": "месяц",
|
||||
"MM": "%d месяцев",
|
||||
"y": "год",
|
||||
"yy": "%d лет"
|
||||
}
|
||||
},
|
||||
"yes": "да",
|
||||
"no": "нет",
|
||||
"unknown": "неизвестно",
|
||||
"others": "другие",
|
||||
"none": "нет",
|
||||
"remove": "убрать",
|
||||
"close": "закрыть"
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
"node": {
|
||||
"all": "Bütün düğümler",
|
||||
"nodes": "Düğümler",
|
||||
"uptime": "Çalışma süresi",
|
||||
"links": "Bağlantılar",
|
||||
"clients": "Müşteriler",
|
||||
"distance": "Mesafe",
|
||||
"connectionType": "Bağlantı türü",
|
||||
"tq": "İletim kalitesi",
|
||||
"lastOnline": "çevrimiçi, son mesaj %{time} (%{date})",
|
||||
"lastOffline": "çevrimdışı, son mesaj %{time} (%{date})",
|
||||
"activated": "aktif (%{branch})",
|
||||
"deactivated": "devredışı bırakıldı",
|
||||
"status": "Durum",
|
||||
"firmware": "Yazılım versiyonu",
|
||||
"hardware": "Donanım modeli",
|
||||
"visible": "Harita üzerinde görünür",
|
||||
"update": "Otomatik güncelleme",
|
||||
"domain": "Domain",
|
||||
"gateway": "Geçit",
|
||||
"coordinates": "Koordinatlar",
|
||||
"contact": "İlişki",
|
||||
"primaryMac": "Birincil MAC",
|
||||
"id": "Düğüm kimliği",
|
||||
"firstSeen": "İlk görülme",
|
||||
"systemLoad": "Ortalama yük",
|
||||
"ram": "Bellek kullanımı",
|
||||
"ipAddresses": "IP adresleri",
|
||||
"nexthop": "Bir sonraki atlama",
|
||||
"selectedGatewayIPv4": "Seçili Ipv4-ağ geçidi",
|
||||
"selectedGatewayIPv6": "Seçili Ipv6-ağ geçidi",
|
||||
"link": "Bağlantı ||| Bağlantılar",
|
||||
"node": "Düğüm ||| Düğümler",
|
||||
"new": "Yeni düğümler",
|
||||
"missing": "Kaybolan düğümler"
|
||||
},
|
||||
"location": {
|
||||
"location": "Konum",
|
||||
"latitude": "Enlem",
|
||||
"longitude": "Boylam",
|
||||
"copy": "Kopya"
|
||||
},
|
||||
"sidebar": {
|
||||
"nodeFilter": "Düğüm Filtresi",
|
||||
"nodes": "%{total} düğümler, %{online} çevrimiçi düğümler dahil",
|
||||
"clients": "%{smart_count} müşteri ile |||| %{smart_count} müşteriler ile",
|
||||
"gateway": "%{smart_count} geçit üzerinde |||| %{smart_count} geçitler üzerinde",
|
||||
"lastUpdate": "Son güncelleme",
|
||||
"nodeNew": "yeni",
|
||||
"nodeOnline": "çevrimiçi",
|
||||
"nodeOffline": "çevrimdışı",
|
||||
"aboutInfo": "<h2>Meshviewer Hakkında</h2> <p>Çift tıklayarak yakınlaştırabilir ve Shift tuşuna basıp+çift tıklayarak uzaklaştırabilirsiniz</p>",
|
||||
"actual": "Mevcut",
|
||||
"stats": "İstatistikler",
|
||||
"about": "Hakkında",
|
||||
"toggle": "Kenar çubuğunu değiştir"
|
||||
},
|
||||
"button": {
|
||||
"switchView": "Görünümü Değiştir",
|
||||
"location": "Koordinatları seç",
|
||||
"tracking": "Yerelleştirme"
|
||||
},
|
||||
"momentjs": {
|
||||
"calendar": {
|
||||
"sameDay": "[Bugün] LT",
|
||||
"nextDay": "[Yarın] LT",
|
||||
"nextWeek": "dddd [at] LT",
|
||||
"lastDay": "[Dün] LT",
|
||||
"lastWeek": "[Last] dddd [at] LT",
|
||||
"sameElse": "L"
|
||||
},
|
||||
"relativeTime": {
|
||||
"future": "%s içinde",
|
||||
"past": "%s önce",
|
||||
"s": "birkaç saniye",
|
||||
"m": "bir dakika",
|
||||
"mm": "%d dakikalar",
|
||||
"h": "bir saat",
|
||||
"hh": "%d saatler",
|
||||
"d": "bir gün",
|
||||
"dd": "%d günler",
|
||||
"M": "bir ay",
|
||||
"MM": "%d aylar",
|
||||
"y": "bir yıl",
|
||||
"yy": "%d yıllar"
|
||||
}
|
||||
},
|
||||
"yes": "evet",
|
||||
"no": "hayır",
|
||||
"unknown": "bilinmeyen",
|
||||
"others": "diğer",
|
||||
"none": "hiçbiri",
|
||||
"remove": "kaldır",
|
||||
"close": "kapat"
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"name": "meshviewer",
|
||||
"version": "11.1.0",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ffrgb/meshviewer.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/ffrgb/meshviewer/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.0.1",
|
||||
"browser-sync": "^2.26.14",
|
||||
"del": "^6.0.0",
|
||||
"eslint": "^7.20.0",
|
||||
"eslint-config-airbnb-es5": "^1.2.0",
|
||||
"eslint-config-defaults": "^9.0.0",
|
||||
"eslint-plugin-react": "^7.22.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-autoprefixer": "^7.0.1",
|
||||
"gulp-cache-bust": "^1.4.0",
|
||||
"gulp-cli": "^2.3.0",
|
||||
"gulp-environments": "^1.0.1",
|
||||
"gulp-eslint": "^6.0.0",
|
||||
"gulp-htmlmin": "^5.0.1",
|
||||
"gulp-inject": "^5.0.2",
|
||||
"gulp-inline-source": "^4.0.0",
|
||||
"gulp-jsonminify": "^1.1.0",
|
||||
"gulp-load-plugins": "^2.0.6",
|
||||
"gulp-real-favicon": "^0.3.2",
|
||||
"gulp-requirejs-optimize": "^1.3.0",
|
||||
"gulp-sass": "^4.1.0",
|
||||
"gulp-sourcemaps": "^3.0.0",
|
||||
"gulp-stylelint": "^13.0.0",
|
||||
"gulp-uglify": "^3.0.2",
|
||||
"stylelint": "^13.10.0",
|
||||
"stylelint-config-standard": "^20.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"amd": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"almond": "^0.3.3",
|
||||
"d3-drag": "^1.2.5",
|
||||
"d3-force": "^1.2.1",
|
||||
"d3-selection": "^1.4.2",
|
||||
"d3-zoom": "^1.8.3",
|
||||
"leaflet": "^1.7.1",
|
||||
"moment": "^2.28.0",
|
||||
"navigo": "^7.1.2",
|
||||
"node-polyglot": "2.2.2",
|
||||
"rbush": "^3.0.1",
|
||||
"requirejs": "^2.3.6",
|
||||
"snabbdom": "^0.7.4"
|
||||
},
|
||||
"scripts": {
|
||||
"gulp": "./node_modules/gulp-cli/bin/gulp.js"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1% in DE"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
'use strict';
|
||||
// Polyfills for (old) firefox 75
|
||||
// From https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/
|
||||
if (typeof Object.assign !== 'function') {
|
||||
Object.assign = function (target, varArgs) { // .length of function is 2
|
||||
if (target == null) { // TypeError if undefined or null
|
||||
throw new TypeError('Cannot convert undefined or null to object');
|
||||
}
|
||||
|
||||
var to = Object(target);
|
||||
|
||||
for (var index = 1; index < arguments.length; index++) {
|
||||
var nextSource = arguments[index];
|
||||
|
||||
if (nextSource != null) { // Skip over if undefined or null
|
||||
for (var nextKey in nextSource) {
|
||||
// Avoid bugs when hasOwnProperty is shadowed
|
||||
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
|
||||
to[nextKey] = nextSource[nextKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return to;
|
||||
};
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('service-worker.js');
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
// Example of overwriting variables. Take a look at modules/variables
|
||||
// .node-links {
|
||||
// color: $color-primary;
|
||||
// }
|
||||
|
||||
// You can also include additional files for style example https://github.com/ffrgb/meshviewer/tree/ffrgb-config/scss/custom
|
||||
// Include syntax: @include "name" -> Filename: _name.scss
|
||||
|
||||
// SCSS supports css with a lot of additional features like variables or mixins.
|
||||
// Autoprefixer runs in postcss, no need to add browser-prefixes like -webkit, -moz or -ms
|
|
@ -0,0 +1,4 @@
|
|||
// Example of overwriting variables. Take a look at modules/variables
|
||||
//$color-black: #fff;
|
||||
//$color-white: invert($color-white);
|
||||
//$color-primary: invert($color-primary);
|
|
@ -0,0 +1,29 @@
|
|||
// Set variables
|
||||
@import 'modules/variables';
|
||||
@import 'custom/variables';
|
||||
|
||||
// Mixins
|
||||
@import 'mixins/icon';
|
||||
@import 'mixins/font';
|
||||
|
||||
// Add modules
|
||||
@import 'modules/reset';
|
||||
@import 'modules/font/font';
|
||||
@import 'modules/base';
|
||||
@import 'modules/font/icon';
|
||||
@import 'modules/loader';
|
||||
@import 'modules/leaflet';
|
||||
@import 'modules/table';
|
||||
@import 'modules/filter';
|
||||
@import 'modules/sidebar';
|
||||
@import 'modules/map';
|
||||
@import 'modules/forcegraph';
|
||||
@import 'modules/legend';
|
||||
@import 'modules/proportion';
|
||||
@import 'modules/tabs';
|
||||
@import 'modules/node';
|
||||
@import 'modules/infobox';
|
||||
@import 'modules/button';
|
||||
|
||||
// Make adjustments in custom scss
|
||||
@import 'custom/custom';
|
|
@ -0,0 +1,19 @@
|
|||
$font-path: 'fonts' !default;
|
||||
|
||||
@mixin load-font($name, $type, $weight, $style, $alias: '') {
|
||||
@if $alias == '' {
|
||||
$alias: $name;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: '#{$alias}';
|
||||
font-style: $style;
|
||||
font-weight: $weight;
|
||||
src:
|
||||
local('#{$name} #{$type}'),
|
||||
local('#{$name}-#{$type}'),
|
||||
url('#{$font-path}/#{$name}-#{$type}.woff2') format('woff2'),
|
||||
url('#{$font-path}/#{$name}-#{$type}.woff') format('woff'),
|
||||
url('#{$font-path}/#{$name}-#{$type}.ttf') format('truetype');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
@mixin icon($name, $code, $prefix: 'ion-') {
|
||||
.#{$prefix}#{$name} {
|
||||
&::before {
|
||||
content: '#{$code}';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
body {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
background: $color-white;
|
||||
color: $color-black;
|
||||
font-family: $font-family;
|
||||
font-size: $font-size;
|
||||
overflow: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
header {
|
||||
background: transparentize($color-black, .98);
|
||||
border-bottom: 1px solid darken($color-white, 10%);
|
||||
}
|
||||
|
||||
textarea,
|
||||
input {
|
||||
background: transparent;
|
||||
color: $color-black, 100;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
padding: .83em 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.17em;
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
padding-left: $button-distance;
|
||||
padding-right: $button-distance;
|
||||
}
|
||||
|
||||
p,
|
||||
pre,
|
||||
ul,
|
||||
h4 {
|
||||
padding: 0 $button-distance 1em;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $color-online;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus {
|
||||
color: darken($color-online, 15%);
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.67em;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
border: 0;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
button {
|
||||
background: $color-white;
|
||||
border: 0;
|
||||
border-radius: .9em;
|
||||
color: $color-black;
|
||||
cursor: pointer;
|
||||
font-family: $font-family-icons;
|
||||
font-size: $button-font-size;
|
||||
height: 1.8em;
|
||||
line-height: 1.95;
|
||||
opacity: .7;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
transition: box-shadow .5s, background-color .5s, color .5s;
|
||||
width: 1.8em;
|
||||
|
||||
&.text {
|
||||
background: $color-primary;
|
||||
border: 1px solid $color-primary;
|
||||
border-radius: 0;
|
||||
color: $color-white;
|
||||
font: inherit;
|
||||
line-height: initial;
|
||||
padding: 0 20px;
|
||||
width: auto;
|
||||
|
||||
&:hover {
|
||||
background: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.active,
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px $color-primary;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
&[data-tooltip] {
|
||||
&::after {
|
||||
background: $color-black;
|
||||
border-radius: 3px;
|
||||
color: $color-white;
|
||||
content: attr(data-tooltip);
|
||||
font-family: $font-family;
|
||||
font-size: $font-size;
|
||||
padding: 0 12px;
|
||||
position: absolute;
|
||||
transform: translate(45px, 52px);
|
||||
visibility: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
transition: visibility 0s linear .3s;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.close {
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
color: transparentize($color-black, .5);
|
||||
float: right;
|
||||
font-size: $button-font-size;
|
||||
height: auto;
|
||||
line-height: 1.2;
|
||||
margin: $button-distance;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip
|
||||
.content,
|
||||
.sidebar > {
|
||||
button {
|
||||
&[aria-label] {
|
||||
&::after {
|
||||
background: $color-black;
|
||||
border-radius: 3px;
|
||||
color: $color-white;
|
||||
content: attr(aria-label);
|
||||
font-family: $font-family;
|
||||
font-size: $font-size;
|
||||
padding: 0 12px;
|
||||
position: absolute;
|
||||
transform: translate(45px, 52px);
|
||||
visibility: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
transition: visibility 0s linear .3s;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|