Skip to content
Switch branches/tags

Latest commit


Git stats


Failed to load latest commit information.
Latest commit message
Commit time

Table of Contents

  1. Introduction
  2. Installation
  3. Features
  4. Libaries
  5. Surf Database
  6. JSON Web Api
  7. React Native App
  8. Forecast Engine
    a. Retrieving Weather Forecast via Api
    b. Creating Cron Jobs
    c. Calculating Surf Forecast
  9. Things To Improve


Get it on Google Play

Installation Instruction

Start your android/ios emulator

git checkout
react-native start
# for android
react-native run-android
# for ios
react-native run-ios

Main App Features

  • Native application available for Iphone and Android
  • Authentication via email/password or Google Account
  • Takes pictures and Uploades them to a backend server
  • Takes videos with the native camera and zoom functionalities
  • Recording geolocation of picture/videos
  • Listing surfspots pictures filtered by location, date, likes etc..
  • Notifing users based on their location/preferences
  • Videos of surf conditions
  • Surf info retrieved via api
  • WorldWide map with surf information and 1 million surfspots worldwide
  • Backend Application built with Ruby on Rails


  • React native app
  • Libraries:
    • React Native Navigation
    • React Native Camera
    • React Native Base
    • React Native Elements
    • React Native Google Signin
    • React Native Maps
    • React Native WebView
  • Project Structure:
    • app/screens: all the projects screens components
    • app/config: autogenerated file to set up backend server ip
    • app/components: project components to render error messages, posts, google authentication button
    • app/lib: javascript classes which do not use react, responsible for api calls or for handling logic not-relevant to react

The backend is built with ruby on rails, hosting both web application and json-api.

Surf Database

The surf-rails backend includes around 10.000 surfspots from Europe, Oceania, Africa, America and Asia, it's the first public api endpoint, currently the magicseaweed api does not provide this information.

Json Web Api

Each SurfSpot is represented as a location model in Ruby on Rails and it is reverse geocoded with the longitude and latitude. Each location includes information relative to the surf spot which are essential to calculate the surf forecast, for example best_wind_direction and best_swell_direction to display the green and red indicators in the mobile app. The green/red indicator represent wind or waves coming from optimal/non-optimal directions (offshore or onshore).

The Mobile App retrieves the information through the /locations endpoint. The /locations endpoint accepts three type of queries:

  1. Locations filtered by distance from specific coordinates (#set_nearby_locations)
  2. Locations filtered within a bounding box (#set_locations_with_box)
  3. Locations that have videos of the surf conditions (#set_locations_with_cameras)

An example curl request:

curl --location --request GET '' \
--header 'X-User-Email: testing@torino1' \
--header 'X-User-Token: bGEfYLE72KtkGyQ21bcP' \
--header 'Accept: {{Accept}}' \
--header 'Content-Type: {{Content}}' \
--data-raw ''

More updated information and test cases are available in the /locations api endpoint documentation.

React Native App

The react native app retrieves from the /locations api endpoint the locations, forecast, videos information in the Locations component using the Api class. The information are retrieved at the application startup and cached. The same endpoint is used to display the different surfspots during map navigation with the Map component and the Map class.

The MapScreen renders the MapView Component and uses the Map class below to determine if the locations #shouldUpdate().
The map instance variable is used inside the react MapScreen component #handleRegionChange callback to re-render the page relevant surfspots. The method shouldUpdate() will detect if the user scrolled to a new region of the map.

class Map {
  constructor (coords) {
    this._current = coords
    this._previous = coords

  get zoomOut() { return > 5; }
  get zoomIn() { return 0.5 > > 0.1 }
  get noZoom() { return 0.5 < < 1.5; }

  get shift() {
    return this.noZoom && this.scroll

  get scroll() {
    return this.scroll_horizontal || this.scroll_vertical

  get shouldUpdate() { 
    return this.zoomIn || this.zoomOut || this.shift 

Additionally the ReactNative app allows users to record and upload videos. The functionality is included in the Camera Component which uses the Recorder component to record the video and the Player component to upload it.

Forecast Engine

The forecast information are retrieved from the stormglass api endpoint /weather/point. The application targets a specific country (Indonesia, Bali) with the plan and potential to expand to other areas in the world. Forecast are retrieved only for locations featured in the landing page with at least one uploaded surfing video, limiting background jobs memory usage and video playback server expenses.

The forecast are calculated with Sidekiq cron jobs. The #set_job method inside Location model starts the process which consist of the following steps:

STEP 1 Retrieving Information via Api

Runs the WeeklyForecastWorker to retrieve the forecast from the api and persist them in the Forecast model json weather column.

The #set_job method is called from the Location class.

class Post < ApplicationRecord
  before_save :set_forecast
  def set_forecast
    location =
    location.set_job unless location.with_forecast

class Location < ApplicationRecord
  def set_job
    # seeds for the first time the weather data
    WeeklyForecastWorker.perform_async({ id: })

Each Location has_one :forecast, the result of the api call is cached in the weather jsonb column for later postprocessing.

class WeeklyForecastWorker
  include Sidekiq::Worker

  def perform(args)
    # calls execute_job

  def execute_job
    return unless @location.storm.success?
    # saves the stormglass weather information in the database
      weather: @location.storm.getWaves,
      tides: @location.storm.getTides,

STEP 2 Creating Cron Jobs

Creates a cron job to repeat the WeeklyForecastWorker 3 times a week.

class Location
  def weekly_cron_tab
    first_day = @now.wday
    second_day = first_day.next_day
    # returns a string for ex. 56 8 * * 4,6,2
    # to repeat the cron job 3 times a week
    "#{@now.minute} #{@now.hour - 1} * * #{first_day},#{second_day},#{second_day.next_day}"

  def set_job
    @now =
    location_text = "Location name: #{}, id: #{}"
    # Loads in Sidekiq the cron job
          name: "#{location_text} update forecast data - every 3 days",
          id: "#{location_text} update forecast data - every 3 days",
          cron: weekly_cron_tab,
          class: 'WeeklyForecastWorker',
          args: { id: id }

STEP 3 Calculating Surf Forecast

Surf Forecast calculations are handled by the Weather plain ruby class. The Weather class inherits from Array and it is used to parse the jsonb column, calculates the hourly and daily average wave height, wave period, wind speed, wind direction...

class Forecast < ApplicationRecord
  belongs_to :location

  # overwrites the default wether column attribute reader
  def weather
    # returns an instance of Weather class || [])
=> [
        "seaLevel"=>[{"value"=>1.14, "source"=>"sg"}],
        "windSpeed"=>[{"value"=>10.14, "source"=>"sg"}, {"value"=>12.44, "source"=>"icon"}],
        "..." => "...",
=> Weather

The Weather class creates accessors to retrieve informations from the json structure

class Weather < Array
  # a symbol for each property to retrieve from the json response
  KEYS = %w(time swellHeight waveHeight windSpeed windDirection waveDirection 
  swellDirection swellPeriod)

  # generates def swellHeightRange get method
  # returns the current hour swellHeight as a range 
  # for example 1-5 (meters) of swell
  # 1 is the minimum for that hour and 5 is the maximum
  %w(swellHeight waveHeight windSpeed swellPeriod).each do |method|
    define_method("#{method}Range") { current[method].minMaxString }

  # generates def waveHeights
  # returns an array of the current hour
  # wave heights based on different sources
  # for ex. [1,1.5,1,3]
  %w(waveHeight swellPeriod).each do |method|
    define_method(method.pluralize) do 
      current[method].collect { |x| x["value"] } 
 # generates def time, def swellHeight 
  KEYS.each do |method|
    define_method(method) { current.value(method) }

  # generates def swellHeight_at(time), def windSpeed_at(time)
  KEYS.each do |method|
    define_method("#{method}_at(time)".to_sym) do
      select { |row| row["time"] == time }.first.value(method)

The Forecast Model uses the Weather class #daily method to calculate the daily average forecast.

class Forecast < ApplicationRecord
  belongs_to :location
  KEYS = %w(swellHeight waveHeight windSpeed windDirection 
  waveDirection swellDirection swellPeriod)

  def weather || [])
  # generates the following methods
  # def get_swell_height(days) def get_wave_height(days) 
  # def get_wind_speed(days) def get_wind_direction(days) 
  # def get_wave_direction(days) def get_swell_direction(days) 
  # def get_swell_period(days)
  KEYS.each do |field|
    define_method("get_#{field.duckTyped}(days)") do |days|
      weather.daily(field, days)

  # uses above get methods to calculate daily forecast
  def get_daily(days)
    result ||= do |field|
      # send(:get_wind_speed(days), days)
      daily_forecast = send("get_#{field.duckTyped}(days)".to_sym, days)
      [field, daily_forecast]
    size = result.first.second.size - 1
    result += [["days", days.in_words[0..size]]]

The DailyForecastWorkers schedules the calculation of daily forecast.
Forecast are calculated based on the Location timezone, as surfing is possible only at daytime.

class Location < ApplicationRecord
  # returns a list of local dates based on the location Timezome
  #  => [Sun, 26 Jan 2020 19:47:22 WITA +08:00, ...]
  def week_days
    @week_days ||= ( + 6).map do |day|
  def get_daily
    # uses the local timezones to calculate the forecast
    daily = forecast.get_daily(week_days)
    # checks if wind/swell directions are optimal
    %w(wind swell).each do |attr|
      daily["optimal_#{attr}"] = daily["#{attr}Direction"].map do |value|
        send("optimal_#{attr}?(#{attr})", value.in_word)

The Weather #daily method is responsible for calculating the dailyAverage swell and wind based of the location timezone.

class Weather < Array
  def daily(key, week_days)
    return nil unless available? { |day| dailyAverage(key, day) }.delete_if { |x| x.nil? }

Things to Improve

  1. Full Test Coverage (jest unit test and detox e2e tests) of the react native mobile app
  2. Refactor the React application
  3. Fixing rspec tests
  4. Speeding up rspec tests with vcr
  5. Optimizing weather engine
    a. retrieve weather forecast based on closest buoy and not gps
    location belongs_to :weather_station
    b. refactor the weather class and avoid code repetition between location and forecast model
    c. Weather class does not have a constructor
    d. Refactor json response structure
  6. Continuos Integration
  7. Fastlane


React Native application for surfcheck







No releases published


No packages published