Compare commits
No commits in common. "master" and "gh-pages" have entirely different histories.
396
LICENSE
|
@ -1,396 +0,0 @@
|
|||
Attribution 4.0 International
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||
does not provide legal services or legal advice. Distribution of
|
||||
Creative Commons public licenses does not create a lawyer-client or
|
||||
other relationship. Creative Commons makes its licenses and related
|
||||
information available on an "as-is" basis. Creative Commons gives no
|
||||
warranties regarding its licenses, any material licensed under their
|
||||
terms and conditions, or any related information. Creative Commons
|
||||
disclaims all liability for damages resulting from their use to the
|
||||
fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and
|
||||
conditions that creators and other rights holders may use to share
|
||||
original works of authorship and other material subject to copyright
|
||||
and certain other rights specified in the public license below. The
|
||||
following considerations are for informational purposes only, are not
|
||||
exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are
|
||||
intended for use by those authorized to give the public
|
||||
permission to use material in ways otherwise restricted by
|
||||
copyright and certain other rights. Our licenses are
|
||||
irrevocable. Licensors should read and understand the terms
|
||||
and conditions of the license they choose before applying it.
|
||||
Licensors should also secure all rights necessary before
|
||||
applying our licenses so that the public can reuse the
|
||||
material as expected. Licensors should clearly mark any
|
||||
material not subject to the license. This includes other CC-
|
||||
licensed material, or material used under an exception or
|
||||
limitation to copyright. More considerations for licensors:
|
||||
wiki.creativecommons.org/Considerations_for_licensors
|
||||
|
||||
Considerations for the public: By using one of our public
|
||||
licenses, a licensor grants the public permission to use the
|
||||
licensed material under specified terms and conditions. If
|
||||
the licensor's permission is not necessary for any reason--for
|
||||
example, because of any applicable exception or limitation to
|
||||
copyright--then that use is not regulated by the license. Our
|
||||
licenses grant only permissions under copyright and certain
|
||||
other rights that a licensor has authority to grant. Use of
|
||||
the licensed material may still be restricted for other
|
||||
reasons, including because others have copyright or other
|
||||
rights in the material. A licensor may make special requests,
|
||||
such as asking that all changes be marked or described.
|
||||
Although not required by our licenses, you are encouraged to
|
||||
respect those requests where reasonable. More considerations
|
||||
for the public:
|
||||
wiki.creativecommons.org/Considerations_for_licensees
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Attribution 4.0 International Public License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution 4.0 International Public License ("Public License"). To the
|
||||
extent this Public License may be interpreted as a contract, You are
|
||||
granted the Licensed Rights in consideration of Your acceptance of
|
||||
these terms and conditions, and the Licensor grants You such rights in
|
||||
consideration of benefits the Licensor receives from making the
|
||||
Licensed Material available under these terms and conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
d. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
e. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
f. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
g. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
h. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
i. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
j. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
k. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
4. If You Share Adapted Material You produce, the Adapter's
|
||||
License You apply must not prevent recipients of the Adapted
|
||||
Material from complying with this Public License.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material; and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
|
61
README.md
|
@ -1,61 +0,0 @@
|
|||
# Coronamap
|
||||
|
||||
Coronamap is an interactive thematic map that animates the spread of the coronavirus.
|
||||
|
||||
Check out the [deployment](../../deployments "Deployment").
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/img/desktop.png" width=55% alt="Coronmap on laptop">
|
||||
<img src="docs/assets/img/mobile.png" width=20% alt="Coronamap on phone"/>
|
||||
</p>
|
||||
|
||||
## Preview
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/img/animation.gif" width="75%" alt="Coronamap preview"/>
|
||||
</p>
|
||||
Disease data: <a href="https://github.com/CSSEGISandData/COVID-19">Johns Hopkins CSSE</a> (https://github.com/CSSEGISandData/COVID-19)
|
||||
|
||||
### Date Slider
|
||||
|
||||
<img src="docs/assets/img/timecontrol.png" height="32px" alt="The date slider consisting of several form controls, as follows">
|
||||
|
||||
- <img src="docs/assets/img/timecontrol-play.png" height="28px" alt="Right-pointing triangle button">: Play
|
||||
- <img src="docs/assets/img/timecontrol-reverse.png" height="28px" alt="Left-pointing triangle button">: Reverse
|
||||
- <img src="docs/assets/img/timecontrol-forward.png" height="28px" alt="Right-pointing double triangle button">: Move to the next day
|
||||
- <img src="docs/assets/img/timecontrol-backward.png" height="28px" alt="Left-pointing double triangle button">: Move to the previous day
|
||||
- <img src="docs/assets/img/timecontrol-loop.png" height="28px" alt="Button with circular arrow pointing in a clockwise direction">: Loop
|
||||
- <img src="docs/assets/img/timecontrol-dateslider.png" height="28px" alt="Range input type positioned to the middle">: Date progress bar
|
||||
- <img src="docs/assets/img/timecontrol-fps.png" height="28px" alt="Range input type labeled with fps value and a clock icon to the left">: Playback speed
|
||||
|
||||
### Colored Geographical Areas
|
||||
|
||||
<img src="docs/assets/img/choropleth-legend.png" alt="A yellow to red gradient color ramp legend labeled with the range of infected cases" height="32px">
|
||||
|
||||
The yellow-orange-red sequential color scheme shows the *number of infected cases*.
|
||||
|
||||
### Circle Markers
|
||||
|
||||
<img src="docs/assets/img/circlemarker.png" alt="" width=10> <img src="docs/assets/img/circlemarker.png" alt="" width=15> <img src="docs/assets/img/circlemarker.png" alt="" width=20> <img src="docs/assets/img/circlemarker.png" alt="" width=25> <img src="docs/assets/img/circlemarker.png" alt="" width=30>
|
||||
|
||||
The size of a circle marker scales to the *number of deaths*.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
* Node.js
|
||||
```bash
|
||||
$ yum install nodejs
|
||||
$ yum install npm
|
||||
```
|
||||
|
||||
* Install npm packages
|
||||
```bash
|
||||
$ npm install
|
||||
```
|
||||
|
||||
### Development
|
||||
* Watch for updates to code and compile automatically: `npm run develop`
|
||||
* Build the optimized production: `npm run build`
|
||||
* Run all unit tests: `npm run test`
|
Before Width: | Height: | Size: 17 MiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 808 B |
Before Width: | Height: | Size: 123 KiB |
Before Width: | Height: | Size: 162 KiB |
Before Width: | Height: | Size: 527 B |
Before Width: | Height: | Size: 573 B |
Before Width: | Height: | Size: 562 B |
Before Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 502 B |
Before Width: | Height: | Size: 489 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 738 B |
Before Width: | Height: | Size: 225 B |
Before Width: | Height: | Size: 275 B |
Before Width: | Height: | Size: 5 KiB |
4463
package-lock.json
generated
17
package.json
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"ts-loader": "^6.2.1",
|
||||
"typescript": "^3.8.3",
|
||||
"webpack": "^4.42.0",
|
||||
"webpack-cli": "^3.3.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mapbox/geojson-merge": "^1.1.1",
|
||||
"@types/node": "^13.11.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo 'test not implemented'",
|
||||
"develop": "webpack --mode development --watch",
|
||||
"build": "webpack --mode production"
|
||||
}
|
||||
}
|
|
@ -1,200 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
import { TimeDimension } from "./TimeDimension";
|
||||
var geojsonMerge = require('@mapbox/geojson-merge');
|
||||
|
||||
export abstract class AbstractReader {
|
||||
public static readonly BASE_URL: string = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/";
|
||||
private confirmedGeoJson: object;
|
||||
private deathsGeoJson: object;
|
||||
private recoveredGeoJson: object;
|
||||
|
||||
public constructor() { }
|
||||
|
||||
public abstract init(): void;
|
||||
|
||||
public abstract loadGeoJsonFile(): object;
|
||||
|
||||
// Retrieve csv data from url
|
||||
public readCsv(scope: string): string {
|
||||
var response;
|
||||
$.ajax({
|
||||
url: scope,
|
||||
async: false,
|
||||
success: function (data) {
|
||||
response = data;
|
||||
},
|
||||
dataType: 'text'
|
||||
});
|
||||
return response;
|
||||
};
|
||||
|
||||
public csvToObject(csv: string): object {
|
||||
return $.csv.toObjects(csv);
|
||||
}
|
||||
|
||||
public replaceColumnKeys(csv: any, dict: object): string {
|
||||
var header = csv.split("\n")[0];
|
||||
for (let key of Object.keys(dict)) {
|
||||
header = header.replace(key, dict[key]);
|
||||
}
|
||||
return header + '\r\n' + csv.split("\n").slice(1).join("\n");
|
||||
}
|
||||
|
||||
// Find different data between geojson and csv, and equalize different region names between geojson and csv data
|
||||
public replaceColumnValues(csv: any, dict: object, colName: string): object {
|
||||
var regex = new RegExp('^' + Object.keys(dict).map(_ => _.replace(/[+?^*${}()|[\]\\]/ig, '\\$&')).join('$|^') + '$', 'gi');
|
||||
|
||||
$(csv).each(function(index, value) {
|
||||
value[colName] = value[colName].replace(regex, function(match) { return dict[match]; });
|
||||
});
|
||||
|
||||
return csv;
|
||||
}
|
||||
|
||||
// Return property values in csv, not in geojson, or empty object if there is no different values between geojson and csv
|
||||
public comparePropertyValues(csv: any, geoJson: object, colName: string, propertyKey: string): object {
|
||||
return $(csv.map(_ => _[colName])).not(geoJson).get().filter(function(v, i, _) { return _.indexOf(v) >= i; });
|
||||
}
|
||||
|
||||
public setPropertyValues(csv: any, geoJson: any, colName: string, geoJsonKey: string): object {
|
||||
for (let value of geoJson.features) {
|
||||
var id = 0;
|
||||
for (let csvIdx in csv) {
|
||||
// If geojson properties name value matches to csv column value
|
||||
if (value.properties[geoJsonKey] == csv[csvIdx][colName]) {
|
||||
// Set properties in geojson with new values
|
||||
value.properties[id] = csv[csvIdx];
|
||||
id++;
|
||||
}
|
||||
}
|
||||
};
|
||||
this.pushTotalNumberOfCaseToProperties(geoJson);
|
||||
return geoJson;
|
||||
}
|
||||
|
||||
public static getProperty(features: any, propertyNames: string[]): any[] {
|
||||
var values = [];
|
||||
for (let i in Object.values(features)) {
|
||||
if (features[i].properties['Country/Region']) {
|
||||
for (let pname of propertyNames) {
|
||||
if (!values[pname]) {
|
||||
values[pname] = [];
|
||||
}
|
||||
values[pname].push(features[i].properties[pname]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
public static mergeGeoJsons(geoJson: object, otherGeoJson: object): object {
|
||||
var mergedGeoJson = geojsonMerge.merge([
|
||||
geoJson,
|
||||
otherGeoJson
|
||||
]);
|
||||
return mergedGeoJson;
|
||||
}
|
||||
|
||||
public setConfirmedGeoJson(geoJson: object): void {
|
||||
this.confirmedGeoJson = geoJson;
|
||||
}
|
||||
|
||||
public getConfirmedGeoJson(): object {
|
||||
return this.confirmedGeoJson;
|
||||
}
|
||||
|
||||
public setDeathsGeoJson(geoJson: object): void {
|
||||
this.deathsGeoJson = geoJson;
|
||||
}
|
||||
|
||||
public getDeathsGeoJson(): object {
|
||||
return this.deathsGeoJson;
|
||||
}
|
||||
|
||||
public setRecoveredGeoJson(geoJson: object): void {
|
||||
this.recoveredGeoJson = geoJson;
|
||||
}
|
||||
|
||||
public getRecoveredGeoJson(): object {
|
||||
return this.recoveredGeoJson;
|
||||
}
|
||||
|
||||
public replaceText(text: string, from: string, to: string, repeat: boolean = false): string {
|
||||
return text.replace(repeat ? '\/' + from + '\/g' : from, to);
|
||||
}
|
||||
|
||||
public getNumberOfCase(features: any, time: string, latlong: [number, number]): number {
|
||||
for (let featIdx = 0, len = Object.keys(features).length; featIdx < len; featIdx++) {
|
||||
var prop = features[featIdx]['properties'];
|
||||
|
||||
if (prop.Lat == latlong[0] && prop.Long == latlong[1]) {
|
||||
return prop[time];
|
||||
}
|
||||
|
||||
for (let propIdx = 0, len = Object.keys(prop).length; propIdx < len; propIdx++) {
|
||||
if (prop[propIdx]) {
|
||||
if (prop[propIdx].Lat == latlong[0] && prop[propIdx].Long == latlong[1]) {
|
||||
return prop[propIdx][time];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public subtractRecoveredFromConfirmed(confirmedGeoJson: any, recoveredGeoJson: any): object {
|
||||
var clonedConfirmedGeoJson = confirmedGeoJson;
|
||||
// Iterate each country in confirmed geojson
|
||||
for (let confirmedCountry of Object.values(clonedConfirmedGeoJson)) {
|
||||
// Iterate each country in recovered geojson
|
||||
for (let recoveredCountry of Object.values(recoveredGeoJson)) {
|
||||
// For each key from each country
|
||||
for (let key of Object.keys(confirmedCountry)) {
|
||||
// Find the exact same country and state names in two geojsons
|
||||
if (confirmedCountry['Province/State'] === recoveredCountry['Province/State'] && confirmedCountry['Country/Region'] === recoveredCountry['Country/Region']) {
|
||||
// If key is date
|
||||
if (moment(key).isValid()) {
|
||||
// Subtract number of recovered cases from number of confirmed cases on the date
|
||||
confirmedCountry[key] -= recoveredCountry[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return clonedConfirmedGeoJson;
|
||||
}
|
||||
|
||||
private pushTotalNumberOfCaseToProperties(geoJson: any): void {
|
||||
// Iterate geojson
|
||||
for (let featIdx = 0, len = Object.keys(geoJson.features).length; featIdx < len; featIdx++) {
|
||||
var totalCaseByDate = {};
|
||||
// Iterate properties
|
||||
for (let keyIdx = 0, len = Object.keys(geoJson.features[featIdx].properties).length; keyIdx < len; keyIdx++) {
|
||||
var key = Object.keys(geoJson.features[featIdx].properties)[keyIdx];
|
||||
// Cities and counties are object
|
||||
if(geoJson.features[featIdx].properties[key] && typeof geoJson.features[featIdx].properties[key] === 'object') {
|
||||
// Iterate keys in each object
|
||||
for (let objectKey of Object.keys(geoJson.features[featIdx].properties[key])) {
|
||||
// If key is date
|
||||
if (moment(objectKey).isValid()) {
|
||||
var date = moment(objectKey).format(TimeDimension.DATE_FORMAT);
|
||||
// Stack number of cases in this city or county
|
||||
if (!totalCaseByDate[date]) {
|
||||
totalCaseByDate[date] = parseInt(geoJson.features[featIdx].properties[key][objectKey]);
|
||||
} else if (totalCaseByDate[date] > 0) {
|
||||
totalCaseByDate[date] += parseInt(geoJson.features[featIdx].properties[key][objectKey]);
|
||||
}
|
||||
} else if (objectKey === 'Country/Region') {
|
||||
totalCaseByDate[objectKey] = geoJson.features[featIdx].properties[key][objectKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add total number of cases in scope of properties
|
||||
for (let country in totalCaseByDate) {
|
||||
geoJson.features[featIdx].properties[country] = totalCaseByDate[country];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
import { Map } from "./Map";
|
||||
import { ITemporal } from "./ITemporal";
|
||||
|
||||
export enum ChoroplethMode {
|
||||
Quantile = "q",
|
||||
Equidistant = "e",
|
||||
Kmeans = "k"
|
||||
}
|
||||
|
||||
export class Choropleth implements ITemporal {
|
||||
public static readonly DEFAULT_SCALE: string[] = ['#fff', '#ffefac', '#fbc750', '#f6b340', '#f09e33', '#e98828', '#e1731e', '#d95b17', '#d14211', '#c81e0d'];
|
||||
public static readonly DEFAULT_BORDER_COLOR: string = '#666';
|
||||
public static readonly DEFAULT_FILL_OPACITY: number = 0.6;
|
||||
public static readonly HIGHLIGHT_FILL_OPACITY: number = 0.65;
|
||||
public static readonly DEFAULT_MODE: ChoroplethMode = ChoroplethMode.Quantile;
|
||||
public static readonly DEFAULT_WEIGHT: number = 1;
|
||||
public static readonly HIGHLIGHT_WEIGHT: number = 1.75;
|
||||
public static readonly LEGEND_CONTROL_POSITION: string = 'bottomleft';
|
||||
public static readonly LEGEND_ELEMENT_CLASS: string = 'choropleth-legend';
|
||||
public static readonly LEGEND_COLOR_WIDTHS: number[] = [1, 1, 1.5, 1.5, 1.5, 2, 2.5, 3, 5];
|
||||
private scale: string[];
|
||||
private borderColor: string;
|
||||
private fillOpacity: number;
|
||||
private mode: ChoroplethMode;
|
||||
|
||||
public constructor(scale?: string[], borderColor?: string, fillOpacity?: number, mode?: ChoroplethMode) {
|
||||
this.scale = !scale || scale.length < 1 ? Choropleth.DEFAULT_SCALE : scale;
|
||||
this.borderColor = !borderColor ? Choropleth.DEFAULT_BORDER_COLOR : borderColor;
|
||||
this.fillOpacity = !fillOpacity ? Choropleth.DEFAULT_FILL_OPACITY : fillOpacity;
|
||||
this.mode = !mode ? Choropleth.DEFAULT_MODE : mode;
|
||||
}
|
||||
|
||||
public update(geoJson: object): any {
|
||||
var choroplethLayer = L.choropleth(geoJson, {
|
||||
// Set properties to use
|
||||
valueProperty: Map.getInstance().getTimeDimension().getCurrentTime(),
|
||||
scale: this.scale,
|
||||
mode: this.mode,
|
||||
step: this.scale.length,
|
||||
style: {
|
||||
color: this.borderColor,
|
||||
weight: 1,
|
||||
fillOpacity: this.fillOpacity
|
||||
},
|
||||
onEachFeature: onEachFeature
|
||||
});
|
||||
|
||||
this.createLegend(choroplethLayer);
|
||||
|
||||
return choroplethLayer;
|
||||
}
|
||||
|
||||
public createLegend(choroplethLayer: any): void {
|
||||
// Adapted from Tim Wisniewski's example: https://github.com/timwis/leaflet-choropleth/blob/gh-pages/examples/legend/demo.js retrieved in March 2020.
|
||||
var legend = L.control({ position: Choropleth.LEGEND_CONTROL_POSITION });
|
||||
legend.onAdd = function() {
|
||||
var div: Element;
|
||||
if ($('div.' + Choropleth.LEGEND_ELEMENT_CLASS).length > 0) {
|
||||
div = document.querySelectorAll('div.' + Choropleth.LEGEND_ELEMENT_CLASS)[0];
|
||||
} else {
|
||||
div = L.DomUtil.create('div', Choropleth.LEGEND_ELEMENT_CLASS);
|
||||
}
|
||||
|
||||
var limits = Choropleth.pushAverageOfEveryElement(choroplethLayer.options.limits);
|
||||
limits[0] = 0;
|
||||
choroplethLayer.options.limits = limits;
|
||||
|
||||
var colors = Choropleth.DEFAULT_SCALE;
|
||||
colors = colors.slice(1, colors.length);
|
||||
choroplethLayer.options.colors = colors;
|
||||
|
||||
var labels = [];
|
||||
|
||||
// Add min and max labels
|
||||
div.innerHTML = '<div class="min"><span>Cases:</span> ' + limits[0] + '</div><div class="max">' + limits[limits.length - 1] + '</div></div>';
|
||||
// Style
|
||||
limits.forEach(function (limit, index) {
|
||||
labels.push('<li style="background-color:' + colors[index]
|
||||
+ ';width:' + Choropleth.LEGEND_COLOR_WIDTHS[index] + 'vw;"></li>');
|
||||
});
|
||||
div.innerHTML += '<ul>' + labels.join('') + '</ul>';
|
||||
|
||||
return div;
|
||||
}
|
||||
legend.addTo(Map.getInstance().getMap());
|
||||
}
|
||||
|
||||
public setScale(scale: string[]): void {
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
public getScale(): string[] {
|
||||
return this.scale;
|
||||
}
|
||||
|
||||
public setBorderColor(borderColor: string): void {
|
||||
this.borderColor = borderColor;
|
||||
}
|
||||
|
||||
public getBorderColor(): string {
|
||||
return this.borderColor;
|
||||
}
|
||||
|
||||
public setFillOpacity(fillOpacity: number): void {
|
||||
this.fillOpacity = fillOpacity;
|
||||
}
|
||||
|
||||
public getFillOpacity(): number {
|
||||
return this.fillOpacity;
|
||||
}
|
||||
|
||||
public setMode(mode: ChoroplethMode): void {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public getMode(): ChoroplethMode {
|
||||
return this.mode;
|
||||
}
|
||||
|
||||
private static pushAverageOfEveryElement(array: number[]): number[] {
|
||||
var newArray = [];
|
||||
for (let element in array) {
|
||||
var index = parseInt(element);
|
||||
newArray.push(array[index]);
|
||||
newArray.push((array[index] + array[index + 1]) / 2);
|
||||
}
|
||||
newArray.pop();
|
||||
return newArray;
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from Leaflet's example: https://leafletjs.com/examples/choropleth retrieved in March 2020.
|
||||
export function onEachFeature(_, layer) {
|
||||
layer.on({
|
||||
mouseover: highlightFeature,
|
||||
mouseout: resetHighlight,
|
||||
click: zoomToFeature
|
||||
});
|
||||
}
|
||||
|
||||
function resetHighlight(e) {
|
||||
// Iterate each map layer and run resetStyle() function
|
||||
$.each(Map.getInstance().getTimeDimension().getLayers(), function(index, value) {
|
||||
try {
|
||||
var layer = e.target;
|
||||
layer.setStyle({
|
||||
weight: Choropleth.DEFAULT_WEIGHT,
|
||||
color: Choropleth.DEFAULT_BORDER_COLOR,
|
||||
fillOpacity: Choropleth.DEFAULT_FILL_OPACITY,
|
||||
});
|
||||
if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
|
||||
layer.bringToFront();
|
||||
}
|
||||
} catch(e) { }
|
||||
});
|
||||
}
|
||||
|
||||
function zoomToFeature(e) {
|
||||
Map.getInstance().getMap().fitBounds(e.target.getBounds());
|
||||
}
|
||||
|
||||
function highlightFeature(e) {
|
||||
var layer = e.target;
|
||||
layer.setStyle({
|
||||
weight: Choropleth.HIGHLIGHT_WEIGHT,
|
||||
fillOpacity: Choropleth.HIGHLIGHT_FILL_OPACITY,
|
||||
});
|
||||
|
||||
if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
|
||||
layer.bringToFront();
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
import { Map } from "./Map";
|
||||
import { ITemporal } from "./ITemporal";
|
||||
import { AbstractReader } from "./AbstractReader";
|
||||
|
||||
export class CircleMarker implements ITemporal {
|
||||
public static readonly PANE_NAME: string = 'circle-marker-pane';
|
||||
public static readonly COLOR: string = '#a80808';
|
||||
public static readonly OPACITY: number = 0.27;
|
||||
public static readonly BORDER_WEIGHT: number = 0.4;
|
||||
public static readonly RADIUS_NOISE: number = 0.0015;
|
||||
public static readonly RADIUS_MAX: number = 20;
|
||||
public static readonly RADIUS_MIN: number = 2.5;
|
||||
private circles: object[];
|
||||
private fileReader: AbstractReader;
|
||||
private confirmedFeatures: object;
|
||||
|
||||
public constructor(fileReader: AbstractReader, confirmedFeatures: object) {
|
||||
this.fileReader = fileReader;
|
||||
this.confirmedFeatures = confirmedFeatures;
|
||||
}
|
||||
|
||||
public update(geoJson: any) {
|
||||
Map.getInstance().getMap().createPane(CircleMarker.PANE_NAME);
|
||||
|
||||
// Clear all circles
|
||||
this.circles = [];
|
||||
|
||||
// Create circles
|
||||
var features: object[];
|
||||
// Extract a portion of states from end of geojson
|
||||
features = geoJson.features.slice(1).slice(-50);
|
||||
this.createCircleMarkers(features, false);
|
||||
|
||||
// Extract all except states at the end of geojson
|
||||
features = geoJson.features.slice(0, -50);
|
||||
this.createCircleMarkers(features, true);
|
||||
|
||||
return L.layerGroup(this.circles).addTo(Map.getInstance().getMap());
|
||||
}
|
||||
|
||||
public getCircles(): any[] {
|
||||
return this.circles;
|
||||
}
|
||||
|
||||
public createTooltip(marker: any, contentText: string, options?: {}): any {
|
||||
var tooltip = L.tooltip(options).setContent(contentText);
|
||||
// Attach tooltip to circle marker
|
||||
marker.bindTooltip(tooltip).openPopup();
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
private createCircleMarkers(features: any, recursive: boolean = false): void {
|
||||
if (!features || features.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!recursive) {
|
||||
for (let stateIdx = 0, len = Object.keys(features).length; stateIdx < len; stateIdx++) {
|
||||
var state = features[stateIdx].properties;
|
||||
var currentTime = Map.getInstance().getTimeDimension().getCurrentTime();
|
||||
var numOfDeathsAtCurrentTime = state[currentTime];
|
||||
if (numOfDeathsAtCurrentTime && numOfDeathsAtCurrentTime > 0) {
|
||||
try {
|
||||
// Create circle marker
|
||||
var circleMarker = L.circleMarker([state['Lat'], state['Long']], {
|
||||
radius: this.setRadius(numOfDeathsAtCurrentTime * CircleMarker.RADIUS_NOISE),
|
||||
color: CircleMarker.COLOR,
|
||||
fillOpacity: CircleMarker.OPACITY,
|
||||
weight: CircleMarker.BORDER_WEIGHT,
|
||||
pane: CircleMarker.PANE_NAME,
|
||||
}
|
||||
);
|
||||
// Get geographic name
|
||||
var countryName = state['Country/Region'] ? state['Country/Region'] : '';
|
||||
var stateName = state['Province/State'] ? state['Province/State'] : state['name'];
|
||||
if (!stateName) {
|
||||
stateName = '';
|
||||
} else if (countryName) {
|
||||
// Add a comma between country name and state name if a geographical area has both
|
||||
stateName += ', ';
|
||||
}
|
||||
// Show number of confirmed cases
|
||||
var numOfConfirmedCases = this.fileReader.getNumberOfCase(this.confirmedFeatures, currentTime, [state['Lat'], state['Long']]);
|
||||
|
||||
this.createTooltip(circleMarker,
|
||||
'<b>' + stateName + countryName + '</b><br/>' +
|
||||
'Confirmed: ' + numOfConfirmedCases + '<br/>' +
|
||||
'Deaths: ' + numOfDeathsAtCurrentTime
|
||||
, {
|
||||
opacity: 1
|
||||
});
|
||||
this.circles.push(circleMarker);
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var countryFeatures = [];
|
||||
for (let countryIdx = 0, len = Object.keys(features).length; countryIdx < len; countryIdx++) {
|
||||
for (let stateIdx = 0, len = Object.keys(features[countryIdx].properties).length; stateIdx < len; stateIdx++) {
|
||||
var state = features[countryIdx].properties[stateIdx];
|
||||
if (state && typeof state === 'object') {
|
||||
var feature: any = {};
|
||||
feature.properties = state;
|
||||
countryFeatures.push(feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.createCircleMarkers(countryFeatures, false);
|
||||
}
|
||||
}
|
||||
|
||||
private setRadius(radius: number): number {
|
||||
if (radius > CircleMarker.RADIUS_MAX) {
|
||||
return CircleMarker.RADIUS_MAX;
|
||||
} else if (radius < CircleMarker.RADIUS_MIN) {
|
||||
return CircleMarker.RADIUS_MIN;
|
||||
}
|
||||
return radius;
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
import { Map } from "./Map";
|
||||
import { Database } from "./Database";
|
||||
import { AbstractReader } from "./AbstractReader";
|
||||
import { WorldFileReader } from "./WorldReader";
|
||||
import { StatesFileReader } from "./StatesReader";
|
||||
import { TimeDimension } from "./TimeDimension";
|
||||
import { ITemporal } from "./ITemporal";
|
||||
import { Choropleth } from "./Choropleth";
|
||||
import { CircleMarker } from "./CircleMarker";
|
||||
import { View } from "./View";
|
||||
moment.suppressDeprecationWarnings = true;
|
||||
|
||||
// Measure the time it takes to fully load page
|
||||
const t0 = performance.now();
|
||||
|
||||
var map = Map.getInstance();
|
||||
map.init();
|
||||
var worldFr: AbstractReader = new WorldFileReader();
|
||||
var usFr: AbstractReader = new StatesFileReader();
|
||||
var db = new Database();
|
||||
db.isExpired().then(function(value) {
|
||||
if (value) {
|
||||
worldFr.init();
|
||||
usFr.init();
|
||||
db.clear();
|
||||
db.setExpiryTime();
|
||||
db.setItem(Database.KEY_CONFIRMED_GEOJSON, AbstractReader.mergeGeoJsons(worldFr.getConfirmedGeoJson(), usFr.getConfirmedGeoJson()));
|
||||
db.setItem(Database.KEY_DEATHS_GEOJSON, AbstractReader.mergeGeoJsons(worldFr.getDeathsGeoJson(), usFr.getDeathsGeoJson()));
|
||||
}
|
||||
}).then(() => {
|
||||
db.getItem(Database.KEY_CONFIRMED_GEOJSON).then(function(mergedConfirmedGeoJson) {
|
||||
var choropleth = new Choropleth();
|
||||
var timedimension = new TimeDimension(Array<ITemporal>(choropleth));
|
||||
map.attachTimeDimension(timedimension);
|
||||
timedimension.update(mergedConfirmedGeoJson);
|
||||
|
||||
db.getItem(Database.KEY_DEATHS_GEOJSON).then(function(mergedDeathsGeoJson) {
|
||||
var circleMarker = new CircleMarker(worldFr, mergedConfirmedGeoJson.features);
|
||||
timedimension = new TimeDimension(Array<ITemporal>(circleMarker));
|
||||
map.attachTimeDimension(timedimension);
|
||||
timedimension.update(mergedDeathsGeoJson);
|
||||
|
||||
var view = new View(mergedConfirmedGeoJson, mergedDeathsGeoJson);
|
||||
view.init();
|
||||
timedimension = new TimeDimension(Array<ITemporal>(view));
|
||||
map.attachTimeDimension(timedimension);
|
||||
timedimension.update();
|
||||
|
||||
// Remove spinner when the page is fully loaded
|
||||
document.getElementById('spinner').outerHTML = '';
|
||||
|
||||
console.log(`Performance: ${performance.now() - t0} milliseconds.`);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,85 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
export class Database {
|
||||
public static readonly DB_NAME: string = 'CoronamapStorage';
|
||||
public static readonly KEY_EXPIRY_TIME: string = 'Expiry Time';
|
||||
public static readonly KEY_CONFIRMED_GEOJSON: string = 'Confirmed Json';
|
||||
public static readonly KEY_DEATHS_GEOJSON: string = 'Deaths Json';
|
||||
// Stored data expires next day 3am UTC when the automated update is available by public data
|
||||
public static readonly DEFAULT_EXPIRY_TIME: number = new Date().setUTCHours(24 + 3, 0, 0, 0);
|
||||
// Use temporary storage if false
|
||||
private static hasLocalStorage: boolean = true;
|
||||
// Temporary storage expires after page refresh
|
||||
private static temporaryStorage: Object = {};
|
||||
|
||||
public constructor() {
|
||||
localforage.config({
|
||||
driver: localforage.INDEXEDDB,
|
||||
name: Database.DB_NAME
|
||||
});
|
||||
}
|
||||
|
||||
public setItem(key: string, value: any): void {
|
||||
if (Database.hasLocalStorage) {
|
||||
localforage.setItem(key, value);
|
||||
} else {
|
||||
Database.temporaryStorage[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public async getItem(key: string): Promise<any> {
|
||||
if (Database.hasLocalStorage) {
|
||||
return localforage.getItem(key).then((value) => {
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}).catch((err) => {
|
||||
Database.hasLocalStorage = false;
|
||||
});
|
||||
} else {
|
||||
return Database.temporaryStorage[key];
|
||||
}
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
if (Database.hasLocalStorage) {
|
||||
localforage.clear();
|
||||
} else {
|
||||
Database.temporaryStorage = new Object();
|
||||
}
|
||||
}
|
||||
|
||||
public supports(): boolean {
|
||||
return localforage.supports(localforage.INDEXEDDB);
|
||||
}
|
||||
|
||||
public async isExpired(): Promise<any> {
|
||||
var extime;
|
||||
if (Database.hasLocalStorage) {
|
||||
extime = await this.getItem(Database.KEY_EXPIRY_TIME);
|
||||
}
|
||||
if (extime) {
|
||||
return new Date().getTime() > extime;
|
||||
}
|
||||
// Expire if database does not contain expiry time
|
||||
return true;
|
||||
}
|
||||
|
||||
public setExpiryTime(extime?: any): void {
|
||||
if (Database.hasLocalStorage) {
|
||||
this.isExpired().then(function(value) {
|
||||
if (value) {
|
||||
localforage.setItem(Database.KEY_EXPIRY_TIME, !extime ? Database.DEFAULT_EXPIRY_TIME : extime);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getStorage(): object {
|
||||
if (Database.hasLocalStorage) {
|
||||
return localforage._dbInfo;
|
||||
} else {
|
||||
return Database.temporaryStorage;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
export interface ITemporal {
|
||||
|
||||
// Return a layer, or empty layer L.layerGroup().addTo(Map.getInstance().getMap());
|
||||
update(geoJson: object): any;
|
||||
}
|
106
src/Map.ts
|
@ -1,106 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
import { TimeDimension } from "./TimeDimension";
|
||||
|
||||
export class Map {
|
||||
public static readonly BUTTON_CONTROL_POSITION: string = 'bottomright';
|
||||
public static readonly TIMEDIMENSION_POSITION: string = 'topright';
|
||||
public static readonly SCALE_CONTROL_POSITION: string = 'bottomright';
|
||||
public static readonly LINK_VIEW_SOURCE: string = 'https://github.com/7ae/coronamap';
|
||||
private static instance: Map = null;
|
||||
private map: any;
|
||||
private timeDimension: any;
|
||||
|
||||
private constructor() { }
|
||||
|
||||
public static getInstance(): Map {
|
||||
if (this.instance === null) {
|
||||
this.instance = new Map();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
this.map = L.map('map', {
|
||||
zoomControl: true,
|
||||
zoomDelta: 0.5,
|
||||
zoomSnap: 0,
|
||||
minZoom: 1.5,
|
||||
maxZoom: 6,
|
||||
worldCopyJump: true,
|
||||
timeDimension: true,
|
||||
timeDimensionOptions: {
|
||||
timeInterval: '2020-01-22/' + new Date(Date.now() - 864e5).toJSON().slice(0, 10),
|
||||
period: 'P1D',
|
||||
buffer: 1,
|
||||
},
|
||||
}).setView([30, 0], 2.0);
|
||||
|
||||
// Add scale to map
|
||||
L.control.scale({position: Map.SCALE_CONTROL_POSITION, metric: false}).addTo(this.map);
|
||||
|
||||
this.createTimeDimensionControl();
|
||||
|
||||
// Load map tiles
|
||||
var osmLayer = L.tileLayer('https://{s}.tile.osm.org/{z}/{x}/{y}.png', {
|
||||
attribution: 'Disease data © <a href="https://systems.jhu.edu/">Johns Hopkins CSSE</a> Map © <a href="https://osm.org/copyright">OpenStreetMap</a> contributors, <a href="https://leafletjs.com">Leaflet</a>',
|
||||
tileSize: 512,
|
||||
zoomOffset: -1
|
||||
}).addTo(this.map);
|
||||
this.map.attributionControl.setPrefix('<a href="' + Map.LINK_VIEW_SOURCE + '">View Source</a>');
|
||||
this.createFullscreenControl(Map.BUTTON_CONTROL_POSITION);
|
||||
}
|
||||
|
||||
// Attach fullscreen control to zoom buttons
|
||||
private createFullscreenControl(controlPosition: string): void {
|
||||
// Move zoom buttons
|
||||
this.map.zoomControl.setPosition(controlPosition);
|
||||
// Attach fullscreen control to zoom buttons
|
||||
L.control.fullscreen({
|
||||
position: controlPosition,
|
||||
forceSeparateButton: false
|
||||
}).addTo(this.map);
|
||||
}
|
||||
|
||||
// Attach timedimension control to the map
|
||||
private createTimeDimensionControl(): void {
|
||||
L.Control.TimeDimensionCustom = L.Control.TimeDimension.extend({
|
||||
//@override
|
||||
_getDisplayDateFormat: function(date: any) {
|
||||
return moment(date).add(1, 'days').format('dddd, LL');
|
||||
}
|
||||
});
|
||||
var timeDimensionControl = new L.Control.TimeDimensionCustom({
|
||||
position: Map.TIMEDIMENSION_POSITION,
|
||||
minSpeed: 0.25,
|
||||
maxSpeed: 2,
|
||||
speedStep: 0.25,
|
||||
timeSliderDragUpdate: true,
|
||||
autoPlay: false,
|
||||
loopButton: true,
|
||||
playReverseButton: true,
|
||||
timeZones: ['Local'],
|
||||
playerOptions: {
|
||||
loop: true,
|
||||
startOver: true,
|
||||
}
|
||||
});
|
||||
this.map.addControl(timeDimensionControl);
|
||||
}
|
||||
|
||||
public getMap(): any {
|
||||
return this.map;
|
||||
}
|
||||
|
||||
public attachTimeDimension(td: TimeDimension): void {
|
||||
this.timeDimension = td;
|
||||
}
|
||||
|
||||
public dettachTimeDimension(): void {
|
||||
this.timeDimension = null;
|
||||
}
|
||||
|
||||
public getTimeDimension(): TimeDimension {
|
||||
return this.timeDimension;
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
import { AbstractReader } from "./AbstractReader";
|
||||
import * as usStatesGeoJSONFile from '../dist/src/us-states.json';
|
||||
|
||||
export class StatesFileReader extends AbstractReader {
|
||||
public static readonly CONFIRMED_FILE_SCOPE: string = 'time_series_covid19_confirmed_US.csv';
|
||||
public static readonly DEATHS_FILE_SCOPE: string = 'time_series_covid19_deaths_US.csv';
|
||||
public static readonly CENTER_OF_LAT_KEY: string = "Lat";
|
||||
public static readonly CENTER_OF_LONG_KEY: string = "Long";
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
var centerOfLatLong = this.readCsv("https://raw.githubusercontent.com/jayinsf/coronamap/master/dist/src/states.csv");
|
||||
|
||||
var confirmed: any = this.readCsv(AbstractReader.BASE_URL + StatesFileReader.CONFIRMED_FILE_SCOPE);
|
||||
var deaths: any = this.readCsv(AbstractReader.BASE_URL + StatesFileReader.DEATHS_FILE_SCOPE);
|
||||
|
||||
confirmed = this.replaceColumnKeys(confirmed);
|
||||
deaths = this.replaceColumnKeys(deaths);
|
||||
|
||||
confirmed = this.csvToObject(confirmed);
|
||||
deaths = this.csvToObject(deaths);
|
||||
|
||||
confirmed = this.replaceColumnValues(confirmed);
|
||||
deaths = this.replaceColumnValues(deaths);
|
||||
|
||||
super.setConfirmedGeoJson(this.setPropertyValues(confirmed, this.loadGeoJsonFile()));
|
||||
super.setDeathsGeoJson(this.setPropertyValues(deaths, this.loadGeoJsonFile()));
|
||||
|
||||
this.setConfirmedGeoJson(this.addCenterOfCordinates(this.csvToObject(centerOfLatLong), this.getConfirmedGeoJson()));
|
||||
this.setDeathsGeoJson(this.addCenterOfCordinates(this.csvToObject(centerOfLatLong), this.getDeathsGeoJson()));
|
||||
}
|
||||
|
||||
// Load geojson file and return copy of geojson object
|
||||
public loadGeoJsonFile(): object {
|
||||
return JSON.parse(JSON.stringify(usStatesGeoJSONFile.default));
|
||||
}
|
||||
|
||||
public replaceColumnKeys(csv: any): string {
|
||||
const dict = {
|
||||
"Province_State": "Province/State",
|
||||
"Country_Region": "Country/Region",
|
||||
"Long_": "Long",
|
||||
}
|
||||
return super.replaceColumnKeys(csv, dict);
|
||||
}
|
||||
|
||||
public replaceColumnValues(csv: any): object {
|
||||
const dict = {}
|
||||
return super.replaceColumnValues(csv, dict, 'Province/State');
|
||||
}
|
||||
|
||||
public setPropertyValues(csv: any, geoJson: object): object {
|
||||
return super.setPropertyValues(csv, geoJson, 'Province/State', 'name');
|
||||
}
|
||||
|
||||
public addCenterOfCordinates(csv: any, geoJson: any): object {
|
||||
for (let feature of geoJson.features) {
|
||||
for (let csvRow of csv) {
|
||||
if (feature.id == csvRow.state) {
|
||||
feature.properties[StatesFileReader.CENTER_OF_LAT_KEY] = csvRow.latitude;
|
||||
feature.properties[StatesFileReader.CENTER_OF_LONG_KEY] = csvRow.longitude;
|
||||
}
|
||||
}
|
||||
}
|
||||
return geoJson;
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
import { Map } from "./Map";
|
||||
import { ITemporal } from "./ITemporal";
|
||||
|
||||
export class TimeDimension {
|
||||
public static readonly DATE_FORMAT: string = 'M/D/YY';
|
||||
public static readonly WAIT_ON_LOAD_TIMER: number = 2;
|
||||
private map: Map;
|
||||
private temporal: Array<ITemporal>;
|
||||
private layerGroup: any;
|
||||
private layers: {[key: number]: any};
|
||||
private hasTimeChanged: boolean;
|
||||
private currentTime: string;
|
||||
|
||||
public constructor(temporal: ITemporal[]) {
|
||||
this.map = Map.getInstance();
|
||||
this.temporal = temporal;
|
||||
this.layerGroup = L.layerGroup().addTo(this.map.getMap());
|
||||
this.layers = {};
|
||||
this.hasTimeChanged = false;
|
||||
}
|
||||
|
||||
public update(geoJson: object = null) {
|
||||
setInterval(() => {
|
||||
this.hasTimeChanged = this.currentTime !== this.getCurrentTime();
|
||||
this.currentTime = this.getCurrentTime();
|
||||
if (!this.hasTimeChanged) {
|
||||
return;
|
||||
}
|
||||
this.clearLayerGroup();
|
||||
|
||||
for (let singleTemporal of this.temporal) {
|
||||
let layer = singleTemporal.update(geoJson).addTo(this.layerGroup);
|
||||
let layerId = L.stamp(layer) < Number.MAX_VALUE ? L.stamp(layer) : 0;
|
||||
this.layers[layerId] = layer;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public getLayers(): {[key: number]: any} {
|
||||
return this.layers;
|
||||
}
|
||||
|
||||
public clearLayerGroup(): void {
|
||||
if(this.layers) {
|
||||
for (let i in this.layerGroup._layers) {
|
||||
if (this.layerGroup.hasLayer(i)) {
|
||||
this.layerGroup.removeLayer(i);
|
||||
}
|
||||
}
|
||||
this.layers = {};
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentTime(): string {
|
||||
var currentTimeInMillisecond = this.map.getMap().timeDimension.getCurrentTime();
|
||||
var currentTime = this.millisecondToDate(currentTimeInMillisecond);
|
||||
return moment(currentTime).format(TimeDimension.DATE_FORMAT);
|
||||
}
|
||||
|
||||
private millisecondToDate(millisecond: number): string {
|
||||
return new Date(millisecond).toJSON().slice(0, 10);
|
||||
}
|
||||
}
|
100
src/View.ts
|
@ -1,100 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
import { Map } from "./Map";
|
||||
import { AbstractReader } from "./AbstractReader";
|
||||
|
||||
export class View {
|
||||
private table: any;
|
||||
private confirmedWorldFeatures: any;
|
||||
private deathsWorldFeatures: any;
|
||||
private confirmedUSFeatures: any;
|
||||
private deathsUSFeatures: any;
|
||||
|
||||
constructor(confirmedGeoJson: any, deathsGeoJson: any) {
|
||||
document.getElementById('table').style.visibility = 'visible';
|
||||
|
||||
this.confirmedWorldFeatures = confirmedGeoJson.features.slice(0, -50);
|
||||
this.deathsWorldFeatures = deathsGeoJson.features.slice(0, -50);
|
||||
|
||||
this.confirmedUSFeatures = confirmedGeoJson.features.slice(1).slice(-50);
|
||||
this.deathsUSFeatures = deathsGeoJson.features.slice(1).slice(-50);
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
this.table = $('#datatables').DataTable({
|
||||
autoWidth: false,
|
||||
columnDefs: [{
|
||||
targets: -1,
|
||||
className: 'all'
|
||||
}],
|
||||
paging: false,
|
||||
|
||||
"columns": [
|
||||
{ "data": "country" },
|
||||
{ "data": "confirmed" },
|
||||
{ "data": "death" },
|
||||
],
|
||||
|
||||
//adds icons
|
||||
dom: 'Bfrtip',
|
||||
buttons: [
|
||||
{ extend: 'csvHtml5', text: '<i class="far fa-file-alt"></i>', titleAttr: 'CSV' },
|
||||
{ extend: 'excelHtml5', text: '<i class="far fa-file-excel"></i>', titleAttr: 'Excel' },
|
||||
{ extend: 'pdfHtml5', text: '<i class="far fa-file-pdf"></i>', titleAttr: 'PDF' },
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
public update(): object {
|
||||
this.table.clear().draw();
|
||||
//extracts all except states at the end of geojson
|
||||
this.createRows(this.confirmedWorldFeatures, this.deathsWorldFeatures);
|
||||
this.createRows(this.confirmedUSFeatures, this.deathsUSFeatures, true);
|
||||
return L.layerGroup().addTo(Map.getInstance().getMap());
|
||||
}
|
||||
|
||||
public createRows(confirmedFeatures: object, deathsFeatures: object, isStates: boolean = false): any {
|
||||
var currentTime = Map.getInstance().getTimeDimension().getCurrentTime();
|
||||
var confirmed = AbstractReader.getProperty(confirmedFeatures, ['Country/Region', currentTime]);
|
||||
var deaths = AbstractReader.getProperty(deathsFeatures, [currentTime]);
|
||||
|
||||
if (isStates) {
|
||||
this.createOneRow([
|
||||
{
|
||||
'country': confirmed['Country/Region'][0],
|
||||
'confirmed': this.sumArrayItems(confirmed[currentTime]).toLocaleString(),
|
||||
'death': this.sumArrayItems(deaths[currentTime]).toLocaleString(),
|
||||
}
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
var dataset: object[] = [];
|
||||
for (let i in confirmed['Country/Region']) {
|
||||
// Fix DataTables warning: table id=datatables - Requested unknown parameter 'confirmed' for row 0, column 1. For more information about this error, please see http://datatables.net/tn/4
|
||||
try {
|
||||
dataset.push({
|
||||
'country': confirmed['Country/Region'][i],
|
||||
'confirmed': confirmed[currentTime][i].toLocaleString(),
|
||||
'death': deaths[currentTime][i].toLocaleString()
|
||||
});
|
||||
} catch (e) {
|
||||
// Throw exception if today's disease data is not found on public
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
this.createOneRow(dataset);
|
||||
}
|
||||
|
||||
private createOneRow(dataset: object[]): void {
|
||||
this.table.rows.add(dataset).draw();
|
||||
}
|
||||
|
||||
private sumArrayItems(arr: number[]): number {
|
||||
var sum = 0;
|
||||
for (let i in arr) {
|
||||
sum += arr[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
import { AbstractReader } from "./AbstractReader";
|
||||
import * as worldCountriesGeoJSONFile from '../dist/src/world-countries.json';
|
||||
|
||||
export class WorldFileReader extends AbstractReader {
|
||||
public static readonly CONFIRMED_FILE_SCOPE = 'time_series_covid19_confirmed_global.csv';
|
||||
public static readonly DEATHS_FILE_SCOPE = 'time_series_covid19_deaths_global.csv';
|
||||
public static readonly RECOVERED_FILE_SCOPE = 'time_series_covid19_recovered_global.csv';
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
var confirmed: any = this.readCsv(AbstractReader.BASE_URL + WorldFileReader.CONFIRMED_FILE_SCOPE);
|
||||
var deaths: any = this.readCsv(AbstractReader.BASE_URL + WorldFileReader.DEATHS_FILE_SCOPE);
|
||||
var recovered: any = this.readCsv(AbstractReader.BASE_URL + WorldFileReader.RECOVERED_FILE_SCOPE);
|
||||
|
||||
confirmed = this.replaceText(confirmed, 'Greenland,Denmark', 'Greenland,Greenland');
|
||||
deaths = this.replaceText(deaths, 'Greenland,Denmark', 'Greenland,Greenland');
|
||||
recovered = this.replaceText(recovered, 'Greenland,Denmark', 'Greenland,Greenland');
|
||||
|
||||
confirmed = this.csvToObject(confirmed);
|
||||
deaths = this.csvToObject(deaths);
|
||||
recovered = this.csvToObject(recovered);
|
||||
|
||||
confirmed = this.replaceColumnValues(confirmed);
|
||||
deaths = this.replaceColumnValues(deaths);
|
||||
recovered = this.replaceColumnValues(recovered);
|
||||
|
||||
confirmed = this.subtractRecoveredFromConfirmed(confirmed, recovered);
|
||||
|
||||
super.setConfirmedGeoJson(this.setPropertyValues(confirmed, this.loadGeoJsonFile()));
|
||||
super.setDeathsGeoJson(this.setPropertyValues(deaths, this.loadGeoJsonFile()));
|
||||
super.setRecoveredGeoJson(this.setPropertyValues(recovered, this.loadGeoJsonFile()));
|
||||
}
|
||||
|
||||
public loadGeoJsonFile(): object {
|
||||
return JSON.parse(JSON.stringify(worldCountriesGeoJSONFile.default));
|
||||
}
|
||||
|
||||
public replaceColumnValues(csv: any): object {
|
||||
const dict = {
|
||||
"Burma": "Myanmar",
|
||||
"Czechia": "Czech Republic",
|
||||
"Korea, South": "South Korea",
|
||||
"Congo (Kinshasa)": "Democratic Republic of the Congo",
|
||||
"Congo (Brazzaville)": "Republic of the Congo",
|
||||
"Taiwan*": 'Taiwan',
|
||||
"occupied Palestinian territory": "Palestine",
|
||||
"Bahamas, The": "Bahamas",
|
||||
"Gambia, The": "Gambia",
|
||||
}
|
||||
return super.replaceColumnValues(csv, dict, 'Country/Region');
|
||||
}
|
||||
|
||||
// Append csv data to geojson
|
||||
public setPropertyValues(csv: any, geoJson: object): object {
|
||||
return super.setPropertyValues(csv, geoJson, 'Country/Region', 'name');
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 337 B After Width: | Height: | Size: 337 B |
Before Width: | Height: | Size: 426 B After Width: | Height: | Size: 426 B |
Before Width: | Height: | Size: 334 B After Width: | Height: | Size: 334 B |
17
src/vendor.d.ts
vendored
|
@ -1,17 +0,0 @@
|
|||
// JQuery
|
||||
declare var $: any;
|
||||
|
||||
// Leaflet.js
|
||||
declare var L: any;
|
||||
|
||||
// Files with .json extension
|
||||
declare module "*.json" {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
|
||||
// momentjs
|
||||
declare var moment: any;
|
||||
|
||||
// localforage
|
||||
declare var localforage: any;
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": false,
|
||||
"module": "es6",
|
||||
"target": "es5",
|
||||
"allowJs": true,
|
||||
"watch": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"exclude": [
|
||||
"test.ts",
|
||||
"node_modules",
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 737 B After Width: | Height: | Size: 737 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 730 KiB |
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB |
Before Width: | Height: | Size: 896 KiB After Width: | Height: | Size: 896 KiB |
Before Width: | Height: | Size: 215 B After Width: | Height: | Size: 215 B |
Before Width: | Height: | Size: 139 B After Width: | Height: | Size: 139 B |
|
@ -1,26 +0,0 @@
|
|||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/Coronamap.ts',
|
||||
devtool: 'inline-source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
node: {
|
||||
fs: 'empty'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.tsx'],
|
||||
},
|
||||
output: {
|
||||
filename: 'bundle.js',
|
||||
path: path.resolve(__dirname, 'dist/js'),
|
||||
},
|
||||
|
||||
};
|