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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|