🌊 Modern swell buoy charts + Aggregated surf forecast from Surfline & Spitcast APIs
ruby 3.3.x
node 20.x
yarn 1.x
postgresql
brew bundle
pg_ctl -D /home/linuxbrew/.linuxbrew/var/postgres start
gem install bundler -v=$(cat Gemfile.lock | tail -1 | tr -d " ")
bundle
yarn
cp config/database.yml.example config/database.yml
bin/rails db:create db:schema:load:with_data db:seed
bin/rails buoys:update forecasts:update
bin/foreman start -f Procfile.dev
.env.development
:
SURFLINE_EMAIL=xxx
SURFLINE_PASSWORD=yyy
Then run bin/rails forecasts:update
againbin/rails db:migrate:with_data
to include Data MigrationsContributing new spots is easy! Make sure you're signed into your Github account and edit the seeds file:
CA = Region.find_or_create_by(name: 'California')
LA = Subregion.find_or_create_by(name: 'Los Angeles', region: CA)
LA.timezone = 'America/Los_Angeles'
LA.save!
You can get valid timezone names from this list.rails console
, then run SpotFinder.new('{string}').formatted_spots
.seeds.rb
, then make sure to assign each spot to the right subregion & delete extraneous fields (msw_name
, match_type
, distance
).Use the following as a template. Delete the lines for surfline_v2_id
, msw_id
, etc, if that spot doesn't exist on that particular site.
{
name: 'County Line',
lat: 34.051,
lon: -118.964,
surfline_v2_id: '590927576a2e4300134fbed8',
msw_id: 277,
subregion: LA,
},
Surfline's new API is undocumented but easy to reverse engineer using their new website's code. Thankfully its structure is much more sane than the old API.
https://services.surfline.com/kbyg/spots/forecasts/{type}?{params}
For reference, I believe kbyg
stands for "Know Before You Go," which is their tagline.
Type | Data |
---|---|
rating | array of human-readable and numeric (0-6) ratings |
wave | array of min/max sizes & optimal scores |
wind | array of wind directions/speeds & optimal scores |
tides | array of types & heights |
weather | array of sunrise/set times, array of temperatures/weather conditions |
Param | Values | Effect |
---|---|---|
spotId | string | Surfline spot id that you want data for. A typical Surfline URL is https://www.surfline.com/surf-report/venice-breakwater/590927576a2e4300134fbed8 where 590927576a2e4300134fbed8 is the spotId |
days | integer | Number of forecast days to get (Max 6 w/o access token, Max 17 w/ premium token) |
intervalHours | integer | Minimum of 1 (hour) |
maxHeights | boolean | true seems to remove min & optimal values from the wave data output |
sds | boolean | If true, use the new LOTUS forecast engine |
accesstoken | string | Auth token to get premium data access (optional) |
Anywhere there is an optimalScore
the value can be interpreted as follows:
Value | Meaning |
---|---|
0 | Suboptimal |
1 | Good |
2 | Optimal |
However, I have never seen a score of 1 in any of their API responses (only 0 or 2), which is unfortunate when it comes to granularity of ratings. Hopefully this changes in the future.
Surfline's old API is undocumented and unauthenticated, but was used via javascript on their website, so it was fairly easy to reverse-engineer. However, they have updated their site & apps to use the new API, and it appears that they've stopped including some critical data in the responses for the old API, so it's disabled in this app for now (and probably forever).
It returned JSON, but with a very odd structure, with each item that is time-sensitive containing an array of daily arrays of values that correspond to timestamps provided in a separate set of arrays. For example (lots of data left out for brevity):
"Surf": {
"dateStamp": [
[
"January 24, 2016 04:00:00",
"January 24, 2016 10:00:00",
"January 24, 2016 16:00:00",
"January 24, 2016 22:00:00"
],
[
"January 25, 2016 04:00:00",
"January 25, 2016 10:00:00",
"January 25, 2016 16:00:00",
"January 25, 2016 22:00:00"
]
],
"surf_min": [
[
2.15,
1.8,
1.4,
1
],
[
0.7,
0.4,
0.3,
0.3
]
],
}
Requests are structured as follows:
https://api.surfline.com/v1/forecasts/{spot_id}?{params}
This is a breakdown of the params available:
Param | Values | Effect |
---|---|---|
spot_id | integer | Surfline spot id that you want data for. A typical legacy Surfline URL is https://www.surfline.com/surf-report/venice-beach-southern-california_4211/ where 4211 is the spot_id . You can also get this from a v2 API response's legacyId property. |
resources | string | Any comma-separated list of "surf,analysis,wind,weather,tide,sort". There could be more available that I haven't discovered. "Sort" gives an array of swells, periods & heights that are used for the tables on spot forecast pages. To see the whole list, just set 'all'. |
days | integer | Number of days of forecast to get. This seems to cap out at 16 for Wind and 25 for Surf. |
getAllSpots | boolean | false returns an object containing the single spot you requested, true returns an array of data for all spots in the same region as your spot, in this case "South Los Angeles" |
units | string | e returns American units (ft/mi), m uses metric |
usenearshore | boolean | The best that I can gather, you want this set to true to use the more accurate nearshore models that take into account how each spot's unique bathymetry affects the incoming swells. |
interpolate | boolean | Provide "forecasts" every 3 hours instead of ever 6. These interpolations seem to be simple averages of the values of the 6-hour forecasts. |
showOptimal | boolean | Includes arrays of 0's & 1's indicating whether each wind & swell forecast is optimal for this spot or not. Unfortunately the optimal swell data is only provided if you include the "sort" resource - it is not included in the "surf" resource. |
callback | string | jsonp callback function name |
Spitcast has a documented API.
I've also asked Jack from Spitcast a few questions and added his responses below:
size
value?
I actually take the API number and create the max by adding 1/6 the height (in feet), and then create the min by subtracting 1/6 the height."
MagicSeaweed was acquired by Surfline an shut down in 2023. Prior to this, MagicSeaweed had a well-documented JSON API that required requesting an API key via email. This was a straightforward process and they got back to me quickly with my key.
I've asked MagicSeaweed a few questions and added their responses below:
All of the forecasting services (including Surfline v1 vs v2) use different systems for rating waves. I've attempted to normalize them all to a 0-5 (6-point) scale as best as possible, which is perhaps easier to understand when mapped onto the commonly-used Poor-Good scale (some throw Epic in there at the top end, but I went with Very Good):
Each forecasting service is massaged onto that scale as follows:
Surfline v2: "Seven point" decimal ratings (0-6). These are massaged by multiplying by 5/6 to get a 0-5 scale.
Spitcast: ratings in text form:
These are massaged by multiplying by 5/1.5 (essentially 3.3Ì…) to get a 0-5 scale.
For record-keeping, these are the formulae for formerly-supported services:
fadedRating
(0-5) & solidRating
(0-5). I simply subtract fadedRating (which is essentially the negative effect of wind) from solidRating.optimalWind
boolean. I take the max swell rating at any given time for that spot, multiply it by 5, and then halve it if the wind is not optimal.It took me a long time to land on a solution here, but I've finally settled on storing all timestamps in the database in the spot's local time. This defies Rails convention, but makes intuitive sense. If you pull up the forecasts table and look at the timestamp, that's the actual local time at that spot that it's forecast for (even though Rails & Postgres both think it's being stored in UTC). This is typically the format that the forecasting service gives it to us in, and what users want to see it in, so there's no point in doing all sorts of fancy conversion when it should be the same all the way through the pipeline. Now, you may ask, why am I still using the Rails default of TIMESTAMP WITHOUT TIMEZONE
, and the answer is that shockingly enough, TIMESTAMP WITH TIMEZONE
doesn't actually store timezone data!
optimal_wind
boolean that is being crudely integrated into the display_swell_rating
method - improvements welcome.