Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Preview

Preview

Installation

smashing install bc4014fa61f08b31f3d42a5e78c49d9b

Configuration

  • ratp.css: adjust background-color to your preference.
  • ratp.rb: configure with the desired stops and directions.

Pro-tip: while you can still set the desired transports in ratp.rb, it is a better idea to separate the code from the data by placing the data in the config/settings.rb file instead. This will make the upgrades easier.

To do so, the config directory should be created at the root level of the dashboard (at the same level of the jobs folder), and inside it, you should create a file called settings.rb. In that file, you would define your transports:

require_relative '../jobs/ratp_utils.rb'

TRANSPORTS = [
  Transport.new(Type::BUS, '22', 'Ranelagh', 'Opera'),
  Transport.new(Type::BUS, '52', 'Ranelagh', 'Opera'),
  Transport.new(Type::METRO, '9', 'Ranelagh', 'Mairie de Montreuil'),
  Transport.new(Type::METRO, '9', 'Ranelagh', 'Pont de Sevres')
]

The settings.rb file is automatically picked up before the widget.

Usage

<li data-row="1" data-col="1" data-sizex="1" data-sizey="1">
  <div data-id="ratp" data-view="Ratp" data-title="RATP"></div>
</li>

Credits

Uses Pierre Grimaud's RATP REST APIs.

class Dashing.Ratp extends Dashing.Widget
onData: (data) ->
for result in data.results
transportId = result.key
currentResult = result.value
transportId1 = transportId + '-1'
transportId2 = transportId + '-2'
element = $("##{transportId}-1").val()
if not element?
# First time, build the table
$('.widget-ratp table')
.append(@createRow(currentResult.type, currentResult.id, transportId1))
.append(@createRow(currentResult.type, currentResult.id, transportId2))
@update(transportId1, currentResult.d1, currentResult.t1)
@update(transportId2, currentResult.d2, currentResult.t2)
createRow: (type, id, transportId) ->
cellIcon = $ '<td>'
cellIcon.addClass 'transport'
imgIcon = $ '<img>'
imgIcon.attr 'src', "https://www.ratp.fr/sites/default/files/network/#{type}/ligne#{id}.svg"
imgIcon.addClass type
imgIcon.addClass 'icon'
imgIcon.on 'error', ->
console.log "Unable to retrieve #{imgIcon.attr 'src'}"
cellIcon.html id # If image is not available, fall back to text
cellIcon.append imgIcon
cellDest = $ '<td>'
cellDest.addClass 'dest'
cellDest.attr 'id', transportId + '-dest'
spanDest = $ '<span>'
spanDest.attr 'id', transportId + '-dest-span'
cellDest.append spanDest
cellTime = $ '<td>'
cellTime.addClass 'time'
cellTime.attr 'id', transportId + '-time'
spanTime = $ '<span>'
spanTime.attr 'id', transportId + '-time-span'
cellTime.append spanTime
row = $ '<tr>'
row.attr 'id', transportId
row.append cellIcon
row.append cellDest
row.append cellTime
return row
update: (id, newDest, newTime) ->
@fadeUpdate(id, 'dest', newDest, 15)
@fadeUpdate(id, 'time', newTime, 10)
fadeUpdate: (id, type, newValue, minSize) ->
spanId = "##{id}-#{type}-span"
if newValue == '[ND]'
$(spanId).addClass 'grayed'
else
$(spanId).removeClass 'grayed'
tdId = "##{id}-#{type}"
oldValue = $(spanId).html()
if oldValue != newValue
$(spanId).fadeOut(->
$(tdId).css('font-size', '')
$(this).html(newValue).fadeIn(->
while $(tdId)[0]?.offsetWidth < $(tdId)[0]?.scrollWidth && $(tdId).css('font-size').replace('px','') > minSize
$(tdId).css('font-size','-=0.5')
)
)
<h1 data-bind="title" data-showif="title"></h1>
<table>
<!-- Elements will be added dynamically -->
</table>
<p id="ratp-updated-at" class="updated-at" data-bind="updatedAtMessage"></p>
require_relative 'ratp_utils'
# Uncomment and define transports below
# (or alternatively, define them in config/settings.rb)
# TRANSPORTS = [
# Transport.new(Type::BUS, '22', 'Ranelagh', 'Opera'),
# Transport.new(Type::BUS, '52', 'Ranelagh', 'Opera'),
# Transport.new(Type::METRO, '9', 'Ranelagh', 'Mairie de Montreuil'),
# Transport.new(Type::METRO, '9', 'Ranelagh', 'Pont de Sevres')
# ]
# RATP_UPDATE_INTERVAL = '10s'
# Do not modify beyond this point.
unless defined? TRANSPORTS
TRANSPORTS = [].freeze
puts('WARN: RATP Transports not defined. See README for more info!')
end
unless defined? RATP_UPDATE_INTERVAL
RATP_UPDATE_INTERVAL = '10s'.freeze
end
stations = {}
directions = {}
SCHEDULER.every RATP_UPDATE_INTERVAL, first_in: 0 do |job|
begin
results = []
TRANSPORTS.each do |transport|
line_key = line_key(transport)
if stations[line_key].nil?
stations[line_key] = read_stations(transport.type[:api], transport.number)
next if stations[line_key].nil?
end
if stations[line_key][transport.stop].nil?
raise ConfigurationError, "Invalid stop '#{transport.stop}', possible values are #{stations[line_key].keys}"
end
if directions[line_key].nil?
directions[line_key] = read_directions(transport.type[:api], transport.number, stations[line_key])
next if directions[line_key].nil?
end
if directions[line_key][transport.destination].nil?
raise ConfigurationError, "Invalid destination '#{transport.destination}', possible values are #{directions[line_key].keys}"
end
type = transport.type[:api]
id = transport.number
stop = stations[line_key][transport.stop]
dir = directions[line_key][transport.destination]
timings = read_timings(type, id, stop, dir)
next if timings.nil?
first_destination, first_time, second_destination, second_time = timings
first_time_parsed, second_time_parsed = reword(first_time, second_time)
ui_type = transport.type[:ui]
stop_escaped = stop.delete('+')
key = "#{ui_type}-#{id}-#{stop_escaped}-#{dir}"
results.push(
key: key,
value: {
type: ui_type,
id: id,
d1: first_destination, t1: first_time_parsed,
d2: second_destination, t2: second_time_parsed
}
)
end
send_event('ratp', results: results)
rescue ConfigurationError => e
warn("ERROR: #{e}")
job.unschedule
end
end
// ----------------------------------------------------------------------------
// Sass declarations
// ----------------------------------------------------------------------------
$background-color: #607d8b;
$rounded-width: 1em;
$rounded-height: 1em;
$rect-width: 1.72em;
$rect-height: 1em;
// ----------------------------------------------------------------------------
// Widget-ratp styles
// ----------------------------------------------------------------------------
.widget-ratp {
background-color: $background-color;
.bus, .noctilien {
width: $rect-width;
height: $rect-height;
}
.metro, .tram, .rer {
width: $rounded-width;
height: $rounded-height;
}
table {
border-collapse: separate;
border-spacing: 4px 0px;
}
td {
border-bottom: 1px dotted #CCC;
text-align: left;
white-space: nowrap;
max-width: 0;
overflow: hidden;
}
.transport {
width: 12%;
text-align: center;
}
.dest {
width: 62%;
}
.icon {
vertical-align: text-bottom;
}
.grayed {
color: #a2a2a2;
}
}
require 'net/http'
require 'json'
API_HOME = 'https://api-ratp.pierre-grimaud.fr/v3'.freeze
SINGLETONS_REPLACEMENTS = {
"Train a l'approche" => 'Approche',
"Train à l'approche" => 'Approche',
"A l'approche" => 'Approche',
'Train a quai' => 'Quai',
'Train à quai' => 'Quai',
'Train retarde' => 'Retardé',
"A l'arret" => 'Arrêt',
'Train arrete' => 'Arrêté',
'Service Termine' => 'Terminé',
'Service termine' => 'Terminé',
'PERTURBATIONS' => 'Perturbé',
'BUS SUIVANT DEVIE' => 'Dévié',
'DERNIER PASSAGE' => 'Terminé',
'PREMIER PASSAGE' => ''
}.freeze
PAIR_REPLACEMENTS = {
['INTERROMPU', 'ARRET NON DESSERVI'] => ['Interrompu', 'N/Desservi'],
['INTERROMPU', 'INTERROMPU'] => ['Interrompu', 'Interrompu'],
['INTERROMPU', 'MANIFESTATION'] => ['Interrompu', 'Manifestation'],
['INTERROMPU', 'INTEMPERIES'] => ['Interrompu', 'Intempéries'],
['ARRET NON DESSERVI', 'ARRET NON DESSERVI'] => ['N/Desservi', 'N/Desservi'],
['ARRET NON DESSERVI', 'MANIFESTATION'] => ['N/Desservi', 'Manifestation'],
['ARRET NON DESSERVI', 'DEVIATION'] => ['N/Desservi', 'Déviation'],
['ARRET NON DESSERVI', 'ARRET REPORTE'] => ['N/Desservi', 'Reporté'],
['ARRET NON DESSERVI', 'INTEMPERIES'] => ['N/Desservi', 'Intempéries'],
['NON ASSURE', 'NON ASSURE'] => ['Non Assuré', 'Non Assuré'],
['NON ASSURE', 'MANIFESTATION'] => ['Non Assuré', 'Manifestation'],
['NON ASSURE', 'INTEMPERIES'] => ['Non Assuré', 'Intempéries'],
['CIRCULATION DENSE', 'MANIFESTATION'] => ['Circul Dense', 'Manifestation'],
['INTEMPERIES', 'INTEMPERIES'] => ['Intempéries', 'Intempéries'],
['INFO INDISPO ....'] => ['Indispo', 'Indispo'],
['SERVICE TERMINE'] => ['Terminé', 'Terminé'],
['TERMINE'] => ['Terminé', 'Terminé'],
['SERVICE', 'NON COMMENCE'] => ['N/Commencé', 'N/Commencé'],
['SERVICE NON COMMENCE'] => ['N/Commencé', 'N/Commencé'],
['NON COMMENCE'] => ['N/Commencé', 'N/Commencé'],
['BUS PERTURBE', '59 mn'] => %w[Perturbé Perturbé]
}.freeze
NA_UI = '[ND]'.freeze
Transport = Struct.new(:type, :number, :stop, :destination)
class Type
METRO = { api: 'metros', ui: 'metro' }.freeze
BUS = { api: 'bus', ui: 'bus' }.freeze
RER = { api: 'rers', ui: 'rer' }.freeze
TRAM = { api: 'tramways', ui: 'tram' }.freeze
NOCTILIEN = { api: 'noctiliens', ui: 'noctilien' }.freeze
end
class ConfigurationError < StandardError
end
private def line_key(transport)
transport.type[:api] + '-' + transport.number
end
private def get_as_json(path)
response = Net::HTTP.get_response(URI(path))
JSON.parse(response.body)
end
def read_stations(type, id)
url = "#{API_HOME}/stations/#{type}/#{id}?_format=json"
begin
json = get_as_json(url)
rescue StandardError => e
warn("ERROR: Unable to read stations for #{type} #{id} (#{url}): #{e}")
return nil
end
raise ConfigurationError, "#{type} #{id}: #{json['result']['message']}" if json['result']['code'] == 400
stations = station_name_to_slug_mapping(json)
stations
end
private def station_name_to_slug_mapping(json)
stations = {}
json['result']['stations'].each do |station|
stations[station['name']] = station['slug']
end
stations
end
def read_directions(type, id, stations)
url = "#{API_HOME}/destinations/#{type}/#{id}?_format=json"
begin
json = get_as_json(url)
rescue StandardError => e
warn("ERROR: Unable to read directions for #{type} #{id} (#{url}): #{e}")
return nil
end
raise ConfigurationError, "#{type} #{id}: #{json['result']['message']}" if json['result']['code'] == 400
# Workaround a bug on RATP or API side - sometimes, only one direction is returned (https://api-ratp.pierre-grimaud.fr/v3/destinations/bus/72?_format=json)
if type == 'bus' && json['result']['destinations'].length == 1
alt_destinations = get_as_json("https://www.ratp.fr/api/getLine/busratp/#{id}")
alt_destination_names = alt_destinations['name'].split('/')
destinations = [{ name: alt_destination_names[0].strip, way: 'A' },
{ name: alt_destination_names[1].strip, way: 'R' }]
json['result']['destinations'] = destinations
json = JSON.parse(JSON(json))
end
destinations = destination_name_to_way_mapping(json, type, id, stations)
destinations
end
private def destination_name_to_way_mapping(json, type, id, stations)
destinations = json['result']['destinations']
if [Type::BUS[:api], Type::TRAM[:api]].include?(type)
return find_bus_directions(destinations, stations, type, id)
else
return find_regular_directions(destinations)
end
end
private def find_bus_directions(destinations, stations, type, id)
# Bug on RATP side for buses & trams - not all directions are correct
# We take one destination, and check for destination A
# If we get ambiguous station (400), it means that A is the direction itself
# Otherwise, it means that A is the other direction
# Yet it happens, sometimes, that both return a timing
possible_directions = %w[A R]
destinations.each_index do |index_destination|
destination = destinations[index_destination]
other_destination = destinations[(index_destination + 1) % 2]
slug = stations[destination['name']]
possible_directions.each_index do |index_direction|
direction = possible_directions[index_direction]
other_direction = possible_directions[(index_direction + 1) % 2]
schedule = get_as_json("#{API_HOME}/schedules/#{type}/#{id}/#{slug}/#{direction}?_format=json")
if schedule['result']['code'] == 400
return {
destination['name'] => direction,
other_destination['name'] => other_direction
}
end
end
end
# Otherwise, fallback to default
find_regular_directions(destinations)
end
private def find_regular_directions(destinations)
directions = {}
destinations.each do |destination|
directions[destination['name']] = destination['way']
end
directions
end
def read_timings(type, id, stop, dir)
url = "#{API_HOME}/schedules/#{type}/#{id}/#{stop}/#{dir}?_format=json"
begin
json = get_as_json(url)
rescue StandardError => e
warn("ERROR: Unable to fetch timings for #{type} #{id} (#{url}): #{e}")
return [NA_UI, NA_UI,
NA_UI, NA_UI]
end
if json['result']['schedules'].nil?
warn("ERROR: Schedules not available for #{type} #{id} (#{url}), json = #{json}")
return [NA_UI, NA_UI,
NA_UI, NA_UI]
end
schedules = json['result']['schedules']
if schedules.length >= 2
[schedules[0]['destination'], schedules[0]['message'],
schedules[1]['destination'], schedules[1]['message']]
elsif schedules.length == 1
if !schedules[0].key?('code')
[schedules[0]['destination'], schedules[0]['message'],
'', '']
else
warn("ERROR: #{schedules[0]['code']} for #{type} #{id} (#{url}), json = #{json}")
[schedules[0]['destination'], NA_UI,
schedules[0]['destination'], NA_UI]
end
else
warn("ERROR: Unable to parse timings for #{type} #{id} (#{url}), json = #{json}")
[schedules[0]['destination'], NA_UI,
schedules[0]['destination'], NA_UI]
end
end
private def reword(first_time, second_time)
PAIR_REPLACEMENTS.each do |source_message, target_message|
if (source_message.length == 1 &&
(first_time == source_message[0] || second_time == source_message[0])) ||
(source_message.length == 2 &&
((first_time == source_message[0] && second_time == source_message[1]) ||
(first_time == source_message[1] && second_time == source_message[0])))
return target_message
end
end
first_time_parsed = shortcut(first_time)
second_time_parsed = shortcut(second_time)
[first_time_parsed, second_time_parsed]
end
private def shortcut(text)
SINGLETONS_REPLACEMENTS[text] || text
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.