diff --git a/README.md b/README.md index 9d5d759..d377490 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,13 @@ An extremely simple and ultra lightweight package for parsing through Hurdat2 da # Usage ## Installation This package can be installed using a simple `npm install` command -``` +```bash npm install hurdatparser ``` +You can import it using `import` +```javascript +import { Hurdat, Util, Point } from "hurdatparser" +``` ## [Documentation](https://github.com/wilburcoding/hurdatparser/tree/main/docs) # Contributing diff --git a/TODO.md b/TODO.md index 6672c4a..3cf55bf 100644 --- a/TODO.md +++ b/TODO.md @@ -1,2 +1,5 @@ -- [ ] Add support for multiple basins +- [x] Add support for multiple basins +- [ ] Add JTWC data support +- [ ] PAGASA data support +- [ ] Add SPC storm report data? - [ ] Proofread docs and create examples diff --git a/docs/hurdat.md b/docs/hurdat.md index a167d5a..9babd92 100644 --- a/docs/hurdat.md +++ b/docs/hurdat.md @@ -22,36 +22,6 @@ Initalize and load Hurdat2 data const hurdat = new Hurdat("path/to/data.txt"); ``` -## `Hurdat.funcFilter(func)` - -A method to filter storms using the passed function - -#### Parameters - -`func` - Function used to filter storms - -#### Returns - -Array of `Storm` filtered - -#### Example Usage - -Finds storms named "Henri" - -```javascript -hurdat.funcFilter(function(storm) { - return storm.name == "Henri"; -}); -``` - -Find storms with forming in July - -```javascript -hurdat.funcFilter(function(storm) { - return storm.formed.getMonth() == 6; -}); -``` - ## `Hurdat.filter(query)` A method to quickly query for storms @@ -59,18 +29,21 @@ A method to quickly query for storms #### Parameters -`query` - An object containing different conditions to query for +`query` - An function with one parameter to filter items or an object containing different conditions to query for +#### Query object fields **Note**: All `query` parameters are optional - `season` (Number) - Look for storms in a certain year - `namematch` (Regular expression) - Look for storms with names matching a certain regular expression - `number` (Number or Array of numbers) - Look for storms whose number is equal to the provided number or is in the provided array -- `peakwind` (Number or Array of numbers) - Look for storms whose highest wind speed (kt) is equal to the provided number or is greater than or equal to the first item of the provided array (minimum) and less than or equal to the second item of the provided array (maximum) -- `peakpressure` (Number or Array of numbers) - Look for storms whose lowest pressure (mb) is equal to the provided number or is greater than or equal to the first item of the provided array (minimum) and less than or equal to the second item of the provided array (maximum) +- `peakwind` (Number or Array of 2 numbers) - Look for storms whose highest wind speed (kt) is equal to the provided number or is greater than or equal to the first item of the provided array (minimum) and less than or equal to the second item of the provided array (maximum) +- `peakpressure` (Number or Array of 2 numbers) - Look for storms whose lowest pressure (mb) is equal to the provided number or is greater than or equal to the first item of the provided array (minimum) and less than or equal to the second item of the provided array (maximum) - `date` (Array of 2 Date objects) - Look for storms that were active during period of time in the provided array - **Note**: This feature is currently experimental and could lead to inaccurate results -- `landfallnum` (Number or Array of numbers) - Look for storms whose number of landfalls is equal to the provided number or is greater than or equal to the first item of the provided array (beginning of date range) and less than or equal to the second item of the provided array (ending of date range) +- `landfallnum` (Number or Array of 2 numbers) - Look for storms whose number of landfalls is equal to the provided number or is greater than or equal to the first item of the provided array (beginning of date range) and less than or equal to the second item of the provided array (ending of date range) - `point` (Array of 4 numbers) - Look for storms that have passed through an area (Item 1 of the provided array is the minimum latitude, Item 2 is the maximum latitude, Item 3 is the minimum longitude, Item 4 is the maximum longitude) - `landfall` (Array of 4 numbers) - Look for storms that have made landfall in an area (Item 1 of the provided array is the minimum latitude, Item 2 is the maximum latitude, Item 3 is the minimum longitude, Item 4 is the maximum longitude) +- `distancekm` (Number or Array of 2 numbers) - Look for storms whose track distance in kilometers is equal to the provided number or is greater than or equal to the first item of the provided array (beginning of range) and less than or equal to the second item of the provided array (ending of range) +- `distancemi` (Number or Array of 2 numbers) - Look for storms whose track distance in miles is equal to the provided number or is greater than or equal to the first item of the provided array (beginning of range) and less than or equal to the second item of the provided array (ending of range) #### Returns @@ -87,7 +60,7 @@ hurdat.filter({"season" : "2005","landfallnum" : [3, 5]}); Find storms that made landfall on Long Island before 1950 ```javascript -hurdat.filter({"date" : [new Date(1800, 1, 1), new Date(1950, 1, 1)], "landfall" : [40.54, 41.21, -71.75, -74.18]}); +hurdat.filter({"date" : [new Date(1800, 0, 1), new Date(1950, 0, 1)], "landfall" : [40.54, 41.21, -71.75, -74.18]}); ``` Find storms that made at least `5` landfalls and no more than `10` landfalls that had a peak intensity of `80` to `100` knots @@ -95,3 +68,17 @@ Find storms that made at least `5` landfalls and no more than `10` landfalls tha ```javascript hurdat.filter({"peakwind": [80, 100], "landfallnum": [5, 10]}) ``` + +Find storms that have a track distance of at least 2,500 miles and less than or equal to 10,000 miles after 2000 + +```javascript +hurdat.filter({"distancemi": [2500, 10000], "date": [new Date(2000, 0, 1), new Date(2500, 0, 1)]}) +``` + +Example usage of using a function to filter storms with the name Henri + +```javascript +hurdat.filter(function(storm) { + return storm.name == "HENRI"; +}) +``` diff --git a/docs/point.md b/docs/point.md index d9bf5ba..b71df8a 100644 --- a/docs/point.md +++ b/docs/point.md @@ -48,3 +48,34 @@ Get latitude value for Point point.getLat(); ``` +## `Util.setLat(newlat)` + +Set latitude value for Point + +#### Parameters + +`newlat` - new latitude value + +#### Example Usage + +Set latitude value for Point to 40 + +```javascript +point.setLat(40); +``` + +## `Util.setLong(newlong)` + +Set longtiude value for Point + +#### Parameters + +`newlong` - new latitude value + +#### Example Usage + +Set longitude value for Point to 57 + +```javascript +point.setLong(57); +``` diff --git a/docs/util.md b/docs/util.md index 6fe1f32..fa79a22 100644 --- a/docs/util.md +++ b/docs/util.md @@ -55,6 +55,32 @@ Convert 50 kt to mph util.ktToMph(50); ``` +## `Util.coordDist(point1, point2, unit)` + +Get distance between 2 coordinates + +#### Parameters + +`point1` - First point to calculate distance between +`point2` - Second point to calculate distance between +`unit` (Optional) - Specify unit for result. km" for kilometers and "mi" for miles. Default value is "km" + +#### Returns + +Distance between provided coordinates + +#### Example usage + +Calculate the distance between (59, 14) and (60, 14) in miles +```javascript +console.log(util.coordDist(new Point(59, 13), new Point(60, 14), "mi")) +``` + +Calculate the distance between (58, 5) and (59, 8) in kilometers +```javascript +console.log(util.coordDist(new Point(58, 5), new Point(59, 8))) +``` + ## `Util.ktToKph(kt)` Convert knots to kph diff --git a/src/index.js b/src/index.js index 919fd4e..19e3d71 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,6 @@ import axios from 'axios'; import * as fs from 'fs'; class Point { constructor(lat, long) { - if (lat < -90 || lat > 90 || Number(lat) != lat) { throw new Error("Parameter latitude must be a Number in range -90 and 90") } @@ -21,41 +20,42 @@ class Point { getCoord() { return [this.lat, this.long] } + setLat(newlat) { + if (newlat < -90 || newlat > 90 || Number(newlat) != newlat) { + throw new Error("Parameter latitude must be a Number in range -90 and 90") + } + this.lat = newlat; + } + setLong(newlong) { + if (newlong < -180 || newlong > 180 || Number(newlong) != newlong) { + throw new Error("Parameter latitude must be a Number in range -90 and 90") + } + this.long = newlong; + } } class Hurdat { constructor(filename) { - var self = this - var data = fs.readFileSync(filename, 'utf8') - //if (err) throw new Error("Unable to load file"); - var raw = data.split("\n"); - self.storms = [] - var stormdata = [] - var stormheader = "" - for (var item of raw) { - if (item.substring(0, 2) == "AL" || item.substring(0,2)=="EP" || item.substring(0,2)=="CP") { - // - if (stormheader != "") { - self.storms.push(new Storm(stormheader, stormdata)) - stormdata = [] - } - stormheader = item - - } else { - stormdata.push(item) - } - } - - - } - funcFilter(func) { try { - if (func instanceof Function) { - return this.storms.filter((storm) => func(storm)) + var self = this + var data = fs.readFileSync(filename, 'utf8') + var raw = data.split("\n"); + self.storms = [] + var stormdata = [] + var stormheader = "" + for (var item of raw) { + if (item.split(",").length < 8) { + if (stormheader != "") { + self.storms.push(new Storm(stormheader, stormdata)) + stormdata = [] + } + stormheader = item + } else { + stormdata.push(item) + } } - throw new Error("Parameter must be a function") } catch (e) { - console.log(e) - throw new Error("Error filtering items") + console.error(e) + throw new Error("Unable to parse data file. File may be invalid") } } filter(query) { @@ -65,9 +65,7 @@ class Hurdat { var matches = true; if (Object.keys(query).includes("season")) { if (Number(query["season"]) == query["season"]) { - //is number matches = matches && storm.year == query["season"] - } else { throw new Error("Query value for season must be number") } @@ -119,10 +117,10 @@ class Hurdat { } } if (Object.keys(query).includes("date")) { - if (query["date"].length==2 && query["date"][0] instanceof Date && query["date"][1] instanceof Date) { + if (query["date"].length == 2 && query["date"][0] instanceof Date && query["date"][1] instanceof Date) { //will be fixed - matches = matches && (query["date"][0].getTime() <= storm.formed.getTime() && query["date"][1].getTime() >= storm.dissipated.getTime()) + matches = matches && (query["date"][0].getTime() <= storm.formed.getTime() && query["date"][1].getTime() >= storm.dissipated.getTime()) } else { throw new Error("Query values for date must be date opjects") } @@ -141,6 +139,35 @@ class Hurdat { throw new Error("Query value for landfallnum must be Number or an array with 2 elements, a minimum and a maximum") } } + if (Object.keys(query).includes("distancekm")) { + if (Number(query["distancekm"]) == query["distancekm"]) { + //is number + matches = matches && storm.distance.km == query["distancekm"] + } else if (query["distancekm"].constructor === Array) { + if (query["distancekm"].length == 2 && query["distancekm"][1] >= query["distancekm"][0]) { + matches = matches && (storm.distance.km >= query["distancekm"][0] && storm.distance.km <= query["distancekm"][1]) + } else { + throw new Error("Invalid array range provided. Parameter should be an array with 2 elements of type Number, a minimum and a maximum") + } + } else { + throw new Error("Query value for distancekm must be Number or an array with 2 elements, a minimum and a maximum") + } + } + //merge with distancekm possible in future + if (Object.keys(query).includes("distancemi")) { + if (Number(query["distancemi"]) == query["distancemi"]) { + //is number + matches = matches && storm.distance.mi == query["distancemi"] + } else if (query["distancemi"].constructor === Array) { + if (query["distancemi"].length == 2 && query["distancemi"][1] >= query["distancemi"][0]) { + matches = matches && (storm.distance.mi >= query["distancemi"][0] && storm.distance.mi <= query["distancemi"][1]) + } else { + throw new Error("Invalid array range provided. Parameter should be an array with 2 elements of type Number, a minimum and a maximum") + } + } else { + throw new Error("Query value for distancemi must be Number or an array with 2 elements, a minimum and a maximum") + } + } if (Object.keys(query).includes("point")) { if (query["point"].constructor === Array) { if (query["point"].length == 4 && query["point"][1] >= query["point"][0] && query["point"][3] >= query["point"][2]) { @@ -167,10 +194,8 @@ class Hurdat { for (var item of storm.entries) { var point = item.point if (point.getLat() >= query["landfall"][0] && point.getLat() <= query["landfall"][1] && point.getLong() >= query["landfall"][2] && point.getLong() <= query["landfall"][3] && item.identifier == "L") { - isin = true; break - } } matches = matches && isin @@ -184,11 +209,13 @@ class Hurdat { return matches }) + } else if (query instanceof Function) { + return this.storms.filter((storm) => query(storm)) } else { - throw new Error("Parameter must be of type dictionary") + throw new Error("Filter must be a function or object with query fields") } } catch (e) { - console.log(e) + console.error(e); throw new Error("Invalid parameter or query") } } @@ -202,7 +229,6 @@ class Storm { var id = data[0].trim() this.id = id this.peakwind = null - this.peakpressure = null this.number = parseInt(id.substring(2, 4)) this.year = parseInt(id.substring(4, 9)) @@ -220,11 +246,28 @@ class Storm { this.peakpressure = lastentry } } + this.formed = this.entries[0].date this.dissipated = this.entries[this.entries.length - 1].date + var distkm = 0; + var distmi = 0; + for (var i = 0; i < this.entries.length - 1; i++) { + var radlat1 = Math.PI * this.entries[i].point.getLat() / 180; + var radlat2 = Math.PI * this.entries[i + 1].point.getLat() / 180; + var theta = this.entries[i].point.getLong() - this.entries[i + 1].point.getLong() + var radtheta = Math.PI * theta / 180 + var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta); + dist = Math.acos(dist) * 180 / Math.PI * 60 * 1.1515 + distkm += dist * 1.609344 + distmi += dist * 0.8684 + } + this.distance = { + mi: distmi, + km: distkm + } } catch (e) { - console.log(e) + console.error(e) throw new Error("An error record while parsing storm data") } } @@ -243,29 +286,38 @@ class Entry { this.point = new Point((data[4][data[4].length - 1] == "N" ? parseFloat(data[4].substring(0, data[4].length - 1)) : parseFloat(data[4].substring(0, data[4].length - 1)) * -1), (data[5][data[5].length - 1] == "E" ? parseFloat(data[5].substring(0, data[5].length - 1)) : parseFloat(data[5].substring(0, data[5].length - 1)) * -1)) this.wind = (data[6].trim() != "" ? parseInt(data[6].trim()) : null) this.pressure = parseInt(data[7].trim()) + for (var i = 0; i < data.length; i++) { + if (data[i] == -999 || data[i] == -99) { + data[i]=null; + } else { + if (i >= 8 && i <= 20) { + data[i]=parseInt(data[i]) + } + } + } this.radius = { "34": { - "NE": (data[8].trim() != "-999" ? parseInt(data[8].trim()) : null), - "SE": (data[9].trim() != "-999" ? parseInt(data[9].trim()) : null), - "SW": (data[10].trim() != "-999" ? parseInt(data[10].trim()) : null), - "NW": (data[11].trim() != "-999" ? parseInt(data[11].trim()) : null) + "NE": data[8], + "SE": data[9], + "SW": data[10], + "NW": data[11] }, "50": { - "NE": (data[12].trim() != "-999" ? parseInt(data[12].trim()) : null), - "SE": (data[13].trim() != "-999" ? parseInt(data[13].trim()) : null), - "SW": (data[14].trim() != "-999" ? parseInt(data[14].trim()) : null), - "NW": (data[15].trim() != "-999" ? parseInt(data[15].trim()) : null) + "NE": data[12], + "SE": data[13], + "SW": data[14], + "NW": data[15] }, "64": { - "NE": (data[16].trim() != "-999" ? parseInt(data[16].trim()) : null), - "SE": (data[17].trim() != "-999" ? parseInt(data[17].trim()) : null), - "SW": (data[18].trim() != "-999" ? parseInt(data[18].trim()) : null), - "NW": (data[19].trim() != "-999" ? parseInt(data[19].trim()) : null) + "NE": data[16], + "SE": data[17], + "SW": data[18], + "NW": data[19] }, - "max": parseInt(data[20].trim()) + "max": data[20] } } catch (e) { - console.log(e) + console.error(e) throw new Error("An error record while parsing entry data") } } @@ -273,27 +325,27 @@ class Entry { } class Util { constructor() { } - download(filename,source) { - if (source==="natl") { - axios.get('https://www.nhc.noaa.gov/data/hurdat/hurdat2-1851-2021-100522.txt').then(function(response) { - const text = response.data; + download(filename, source) { + if (source === "natl") { + axios.get('https://www.nhc.noaa.gov/data/hurdat/hurdat2-1851-2021-100522.txt').then(function(response) { + const text = response.data; - fs.writeFile(filename, text, function(err) { - if (err) { - throw new Error("Error saving file") - } + fs.writeFile(filename, text, function(err) { + if (err) { + throw new Error("Error saving file") + } + }); }); - }); - } else if (source==="pac") { + } else if (source === "pac") { axios.get('https://www.nhc.noaa.gov/data/hurdat/hurdat2-nepac-1949-2021-091522.txt').then(function(response) { - const text = response.data; + const text = response.data; - fs.writeFile(filename, text, function(err) { - if (err) { - throw new Error("Error saving file") - } + fs.writeFile(filename, text, function(err) { + if (err) { + throw new Error("Error saving file") + } + }); }); - }); } else { throw new Error("Invalid data type. Source must be \"natl\" (North Atlantic) or \"pac\" (Eastern and Central Pacific)") } @@ -340,15 +392,27 @@ class Util { } throw new Error("Invalid Parameter (Parameter needs to be a Point)") } - inside(minlat,maxlat,minlong,maxlong,point) { + inside(minlat, maxlat, minlong, maxlong, point) { if (Number(minlat) == minlat && Number(maxlat) == maxlat && Number(minlong) == minlong && Number(maxlong) == maxlong && point instanceof Point) { - return (point.getLat() >= minlat && point.getLat() <= maxlat && point.getLong() >= minlong && point.getLong() <= maxlong) - } + return (point.getLat() >= minlat && point.getLat() <= maxlat && point.getLong() >= minlong && point.getLong() <= maxlong) + } throw new Error("Invalid parameters. minlat, maxlat, minlong, and maxlong need to be of type Number, point needs to be of type Point") } - + coordDist(point1, point2, unit = "km") { + //uses haversine formula + //https://www.htmlgoodies.com/javascript/calculate-the-distance-between-two-points-in-your-web-apps/ + //https://stackoverflow.com/questions/18883601/function-to-calculate-distance-between-two-coordinates + var radlat1 = Math.PI * point1.getLat() / 180; + var radlat2 = Math.PI * point2.getLat() / 180; + var theta = point1.getLong() - point2.getLong() + var radtheta = Math.PI * theta / 180 + var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta); + dist = Math.acos(dist) * 180 / Math.PI * 60 * 1.1515 + if (unit == "km") { dist = dist * 1.609344 } + if (unit == "mi") { dist = dist * 0.8684 } + return dist + } } - export { Hurdat, Util,