Skip to content

Instantly share code, notes, and snippets.

@aymanebarka
Forked from ChadiEM/README-ratp.md
Created February 7, 2021 17:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aymanebarka/06afca0f52544f3e42a04675bbfd335a to your computer and use it in GitHub Desktop.
Save aymanebarka/06afca0f52544f3e42a04675bbfd335a to your computer and use it in GitHub Desktop.

Preview

Preview

Installation

smashing install bc4014fa61f08b31f3d42a5e78c49d9b

Configuration

  • ratp.scss: adjust background-color and font-size 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', 'Gare Saint-Lazare'),
  Transport.new(Type::METRO, '9', 'Ranelagh', 'Mairie de Montreuil'),
  Transport.new(Type::TRAM, '2', 'Porte de Versailles', 'Pont de Bezons'),
]

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>

You can omit data-title to free up space for more destinations.

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
@createRow(currentResult.type, currentResult.id, transportId1)
.insertBefore($('.widget-ratp #placeholder'))
@createRow(currentResult.type, currentResult.id, transportId2)
.insertBefore($('.widget-ratp #placeholder'))
@update(transportId1, currentResult.d1, currentResult.t1, currentResult.status)
@update(transportId2, currentResult.d2, currentResult.t2, currentResult.status)
createRow: (type, id, transportId) ->
cellIcon = $ '<span>'
cellIcon.addClass 'transport'
cellIcon.attr 'id', transportId + '-icon'
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 = $ '<span>'
cellDest.addClass 'dest'
cellDest.attr 'id', transportId + '-dest'
cellTime = $ '<span>'
cellTime.addClass 'time'
cellTime.attr 'id', transportId + '-time'
row = $ '<div>'
row.attr 'id', transportId
row.addClass 'item'
row.append cellIcon
row.append cellDest
row.append cellTime
return row
update: (id, newDest, newTime, status) ->
@iconUpdate(id, status)
@fadeUpdate(id, 'dest', newDest)
@fadeUpdate(id, 'time', newTime)
iconUpdate: (id, status) ->
iconId = "##{id}-icon"
if status == 'critical'
$(iconId).addClass 'critical'
else
$(iconId).removeClass 'critical'
if status == 'alerte'
$(iconId).addClass 'alert'
else
$(iconId).removeClass 'alert'
fadeUpdate: (id, type, newValue) ->
spanId = "##{id}-#{type}"
if newValue == '[ND]'
$(spanId).addClass 'grayed'
else
$(spanId).removeClass 'grayed'
oldValue = $(spanId).html()
if oldValue != newValue
$(spanId).fadeOut(->
if type == 'time'
if !(newValue.includes ' mn') && !(newValue.match /\d{1,2}:\d{1,2}/)
$(spanId).addClass 'condensed'
else
$(spanId).removeClass 'condensed'
$(this).html(newValue).fadeIn()
)
<div id="ratpContainer">
<h1 data-bind="title" data-showif="title"></h1>
<!-- Elements will be dynamically added -->
<span id="placeholder"></span>
<p id="ratp-updated-at" class="updated-at" data-bind="updatedAtMessage"></p>
</div>
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)
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)
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
stop = stations[line_key][transport.stop]
dir = directions[line_key][transport.destination]
timings = read_timings(transport, stop, dir)
next if timings.nil?
status = read_status(transport)
next if status.nil?
first_destination, first_time, second_destination, second_time = timings
first_time_parsed, second_time_parsed = reword(first_time, second_time)
stop_escaped = stop.delete('+')
key = "#{line_key}-#{stop_escaped}-#{dir}"
results.push(
key: key,
value: {
type: transport.type[:ui],
id: transport.number,
d1: first_destination, t1: first_time_parsed,
d2: second_destination, t2: second_time_parsed,
status: status
}
)
end
send_event('ratp', results: results)
rescue ConfigurationError => e
warn("ERROR: RATP: #{e}")
job.unschedule
end
end
// ----------------------------------------------------------------------------
// Sass declarations
// ----------------------------------------------------------------------------
$background-color: #607d8b;
$font-size: 0.85em;
$rounded-width: 1.72em;
$rounded-height: 1.3em;
$rect-width: 1.72em;
$rect-height: 1em;
// ----------------------------------------------------------------------------
// Widget-ratp styles
// ----------------------------------------------------------------------------
.widget-ratp {
font-size: $font-size;
background-color: $background-color;
h1 {
margin-top: 12px;
}
.alert {
background-color: rgba(255, 165, 0, 0.4);
}
.critical {
background-color: rgba(255, 0, 0, 0.3);
}
.bus,
.noctilien {
width: $rect-width;
height: $rect-height;
}
.metro,
.tram,
.rer {
width: $rounded-width;
height: $rounded-height;
}
.dest {
padding-left: 0.2em;
padding-right: 0.5em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
flex: 1 1 auto;
}
.time {
white-space: nowrap;
}
.grayed {
color: #a2a2a2;
}
.condensed {
font-size: 0.8em;
}
#ratpContainer {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-around;
}
.item {
padding-left: 0.2em;
padding-right: 0.2em;
display: flex;
align-items: center;
flex: 1 1 auto;
border: 1px solid rgba(0, 0, 0, 0.125);
flex-direction: row;
justify-content: space-between;
}
.updated-at {
position: relative;
margin-top: 10px;
margin-bottom: 10px;
bottom: 0;
}
}
require 'net/http'
require 'json'
API_V4 = 'https://api-ratp.pierre-grimaud.fr/v4'.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' => '',
'TRAFIC REDUIT' => 'Trafic Réduit'
}.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'],
['SERVICE', 'NON ASSURE'] => ['Non Assuré', 'Non Assuré'],
['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) do
def to_s
"#{type[:ui]} #{number}"
end
end
class Type
METRO = { apiv4: 'metros', ui: 'metro' }.freeze
BUS = { apiv4: 'buses', ui: 'bus' }.freeze
RER = { apiv4: 'rers', ui: 'rer' }.freeze
TRAM = { apiv4: 'tramways', ui: 'tram' }.freeze
NOCTILIEN = { apiv4: 'noctiliens', ui: 'noctilien' }.freeze
end
# Due to bugs, some transports have the A/R destinations swapped - this is a hardcoded, non-exhaustive list of swaps
TRANSPORTS_TO_SWAP = [
{ type: Type::TRAM, id: '2' },
{ type: Type::TRAM, id: '5' },
{ type: Type::TRAM, id: '7' }
].freeze
class ConfigurationError < StandardError
end
private def line_key(transport)
transport.type[:apiv4] + '-' + transport.number
end
private def get_as_json(path)
response = Net::HTTP.get_response(URI(path))
JSON.parse(response.body)
end
def read_stations(transport)
url = "#{API_V4}/stations/#{transport.type[:apiv4]}/#{transport.number}?_format=json"
begin
json = get_as_json(url)
rescue StandardError => e
warn("ERROR: RATP: Unable to read stations for #{transport} (#{url}): #{e}")
return nil
end
raise ConfigurationError, "Unable to read stations: #{transport}: #{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(transport)
url = "#{API_V4}/destinations/#{transport.type[:apiv4]}/#{transport.number}?_format=json"
begin
json = get_as_json(url)
rescue StandardError => e
warn("ERROR: RATP: Unable to read directions for #{transport} (#{url}): #{e}")
return nil
end
raise ConfigurationError, "Unable to read directions: #{transport}: #{json['result']['message']}" if json['result']['code'] == 400
destinations = destination_name_to_way_mapping(json, transport)
destinations
end
private def destination_name_to_way_mapping(json, transport)
directions = {}
json['result']['destinations'].each do |destination|
directions[destination['name']] = destination['way']
end
TRANSPORTS_TO_SWAP.each do |transport_to_swap|
if transport_to_swap[:type] == transport[:type] && transport_to_swap[:id] == transport[:number]
directions.each { |dir, way| directions[dir] = way == 'A' ? 'R' : 'A' }
end
end
directions
end
def read_timings(transport, stop, dir)
url = "#{API_V4}/schedules/#{transport.type[:apiv4]}/#{transport.number}/#{stop}/#{dir}"
begin
json = get_as_json(url)
rescue StandardError => e
warn("ERROR: RATP: Unable to fetch timings for #{transport} (#{url}): #{e}")
return [NA_UI, NA_UI,
NA_UI, NA_UI]
end
if json['result']['schedules'].nil?
warn("ERROR: RATP: Schedules not available for #{transport} (#{url}), json = #{json}")
return [NA_UI, NA_UI,
NA_UI, NA_UI]
end
schedules = json['result']['schedules']
if schedules.length == 4 &&
(['PREMIER DEPART', 'DEUXIEME DEPART'].include?(schedules[1]['message']) ||
['PREMIER DEPART', 'DEUXIEME DEPART'].include?(schedules[3]['message']))
# T2, Porte de Versailles, dir Bezons
premier = schedules[1]['message'] == 'PREMIER DEPART' ? 0 : 2
deuxieme = (premier + 2) % 4
[schedules[premier]['destination'], schedules[premier]['message'],
schedules[deuxieme]['destination'], schedules[deuxieme]['message']]
elsif 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: RATP: #{schedules[0]['code']} for #{transport} (#{url}), json = #{json}")
[schedules[0]['destination'], NA_UI,
schedules[0]['destination'], NA_UI]
end
else
warn("ERROR: RATP: Unable to parse timings for #{transport} (#{url}), json = #{json}")
[schedules[0]['destination'], NA_UI,
schedules[0]['destination'], NA_UI]
end
end
def read_status(transport)
return 'indispo' if transport.type == Type::BUS
url = "#{API_V4}/traffic/#{transport.type[:apiv4]}/#{transport.number}"
begin
json = get_as_json(url)
rescue StandardError => e
warn("ERROR: RATP: Unable to read status for #{transport} (#{url}): #{e}")
return NA_UI
end
if json['result']['slug'].nil?
warn("ERROR: RATP: Status not available for #{transport} (#{url}), json = #{json}")
return NA_UI
end
json['result']['slug']
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