aboutsummaryrefslogtreecommitdiffstats
path: root/app/javascript/routes
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/routes')
-rw-r--r--app/javascript/routes/actions/index.js62
-rw-r--r--app/javascript/routes/components/App.js25
-rw-r--r--app/javascript/routes/components/BSelect2.js125
-rw-r--r--app/javascript/routes/components/OlMap.js169
-rw-r--r--app/javascript/routes/components/StopPoint.js94
-rw-r--r--app/javascript/routes/components/StopPointList.js69
-rw-r--r--app/javascript/routes/containers/AddStopPoint.js20
-rw-r--r--app/javascript/routes/containers/VisibleStopPoints.js58
-rw-r--r--app/javascript/routes/form_helper.js55
-rw-r--r--app/javascript/routes/index.js80
-rw-r--r--app/javascript/routes/reducers/index.js8
-rw-r--r--app/javascript/routes/reducers/stopPoints.js144
-rw-r--r--app/javascript/routes/show.js102
13 files changed, 1011 insertions, 0 deletions
diff --git a/app/javascript/routes/actions/index.js b/app/javascript/routes/actions/index.js
new file mode 100644
index 000000000..13b2d60b2
--- /dev/null
+++ b/app/javascript/routes/actions/index.js
@@ -0,0 +1,62 @@
+const actions = {
+ addStop : () => {
+ return {
+ type: 'ADD_STOP'
+ }
+ },
+ moveStopUp : (index) => {
+ return {
+ type: 'MOVE_STOP_UP',
+ index
+ }
+ },
+ moveStopDown : (index) => {
+ return {
+ type: 'MOVE_STOP_DOWN',
+ index
+ }
+ },
+ deleteStop : (index) => {
+ return {
+ type: 'DELETE_STOP',
+ index
+ }
+ },
+ updateInputValue : (index, text) => {
+ return {
+ type : 'UPDATE_INPUT_VALUE',
+ index,
+ text
+ }
+ },
+ updateSelectValue: (e, index) => {
+ return {
+ type :'UPDATE_SELECT_VALUE',
+ select_id: e.currentTarget.id,
+ select_value: e.currentTarget.value,
+ index
+ }
+ },
+ toggleMap: (index) =>({
+ type: 'TOGGLE_MAP',
+ index
+ }),
+ toggleEdit: (index) =>({
+ type: 'TOGGLE_EDIT',
+ index
+ }),
+ closeMaps: () => ({
+ type : 'CLOSE_MAP'
+ }),
+ selectMarker: (index, data) =>({
+ type: 'SELECT_MARKER',
+ index,
+ data
+ }),
+ unselectMarker: (index) => ({
+ type: 'UNSELECT_MARKER',
+ index
+ })
+}
+
+module.exports = actions
diff --git a/app/javascript/routes/components/App.js b/app/javascript/routes/components/App.js
new file mode 100644
index 000000000..0f5786407
--- /dev/null
+++ b/app/javascript/routes/components/App.js
@@ -0,0 +1,25 @@
+import React, { Component, PropTypes } from 'react'
+import AddStopPoint from '../containers/AddStopPoint'
+import VisibleStopPoints from'../containers/VisibleStopPoints'
+import clone from '../../helpers/clone'
+const I18n = clone(window , "I18n", true)
+
+export default class App extends Component {
+
+ getChildContext() {
+ return { I18n }
+ }
+
+ render() {
+ return (
+ <div>
+ <VisibleStopPoints />
+ <AddStopPoint />
+ </div>
+ )
+ }
+}
+
+App.childContextTypes = {
+ I18n: PropTypes.object
+}
diff --git a/app/javascript/routes/components/BSelect2.js b/app/javascript/routes/components/BSelect2.js
new file mode 100644
index 000000000..340d9df95
--- /dev/null
+++ b/app/javascript/routes/components/BSelect2.js
@@ -0,0 +1,125 @@
+import _ from'lodash'
+import React, { Component, PropTypes } from 'react'
+import Select2 from 'react-select2'
+
+
+// get JSON full path
+var origin = window.location.origin
+var path = window.location.pathname.split('/', 3).join('/')
+
+
+export default class BSelect3 extends Component {
+ constructor(props, context) {
+ super(props, context)
+ }
+ onChange(e) {
+ this.props.onChange(this.props.index, {
+ text: e.currentTarget.textContent,
+ stoparea_id: e.currentTarget.value,
+ user_objectid: e.params.data.user_objectid,
+ longitude: e.params.data.longitude,
+ latitude: e.params.data.latitude,
+ name: e.params.data.name,
+ short_name: e.params.data.short_name,
+ city_name: e.params.data.city_name,
+ area_type: e.params.data.area_type,
+ zip_code: e.params.data.zip_code,
+ comment: e.params.data.comment
+ })
+ }
+
+ parsedText(data) {
+ let a = data.replace('</em></small>', '')
+ let b = a.split('<small><em>')
+ if (b.length > 1) {
+ return (
+ <span>
+ {b[0]}
+ <small><em>{b[1]}</em></small>
+ </span>
+ )
+ } else {
+ return (
+ <span>{data}</span>
+ )
+ }
+ }
+
+ render() {
+ if(this.props.value.edit)
+ return (
+ <div className='select2-bootstrap-append'>
+ <BSelect2 {...this.props} onSelect={ this.onChange.bind(this) }/>
+ </div>
+ )
+ else
+ if(!this.props.value.stoparea_id)
+ return (
+ <div>
+ <BSelect2 {...this.props} onSelect={ this.onChange.bind(this) }/>
+ </div>
+ )
+ else
+ return (
+ <a
+ className='navlink'
+ href={origin + path + '/stop_areas/' + this.props.value.stoparea_id}
+ title="Voir l'arrêt"
+ >
+ {this.parsedText(this.props.value.text)}
+ </a>
+ )
+ }
+}
+
+class BSelect2 extends Component{
+ componentDidMount() {
+ this.refs.newSelect.el.select2('open')
+ }
+
+ render() {
+ return (
+ <Select2
+ value={ this.props.value.stoparea_id }
+ onSelect={ this.props.onSelect }
+ ref='newSelect'
+ options={{
+ placeholder: this.context.I18n.routes.edit.select2.placeholder,
+ allowClear: true,
+ language: 'fr', /* Doesn't seem to work... :( */
+ theme: 'bootstrap',
+ width: '100%',
+ ajax: {
+ url: origin + path + '/autocomplete_stop_areas.json',
+ dataType: 'json',
+ delay: '500',
+ data: function(params) {
+ return {
+ q: params.term,
+ target_type: 'zdep'
+ };
+ },
+ processResults: function(data, params) {
+ return {
+ results: data.map(
+ item => _.assign(
+ {},
+ item,
+ { text: item.name + ", " + item.zip_code + " " + item.short_city_name + " <small><em>(" + item.user_objectid + ")</em></small>" }
+ )
+ )
+ };
+ },
+ cache: true
+ },
+ escapeMarkup: function (markup) { return markup; },
+ minimumInputLength: 3
+ }}
+ />
+ )
+ }
+}
+
+BSelect2.contextTypes = {
+ I18n: PropTypes.object
+}
diff --git a/app/javascript/routes/components/OlMap.js b/app/javascript/routes/components/OlMap.js
new file mode 100644
index 000000000..2c01dfa7f
--- /dev/null
+++ b/app/javascript/routes/components/OlMap.js
@@ -0,0 +1,169 @@
+import _ from 'lodash'
+import React, { Component, PropTypes } from 'react'
+
+export default class OlMap extends Component{
+ constructor(props, context){
+ super(props, context)
+ }
+
+ fetchApiURL(id){
+ const origin = window.location.origin
+ const path = window.location.pathname.split('/', 3).join('/')
+ return origin + path + "/autocomplete_stop_areas/" + id + "/around?target_type=zdep"
+ }
+
+ componentDidUpdate(prevProps, prevState){
+ if(prevProps.value.olMap.isOpened == false && this.props.value.olMap.isOpened == true){
+ var source = new ol.source.Vector({
+ format: new ol.format.GeoJSON(),
+ url: this.fetchApiURL(this.props.value.stoparea_id)
+ })
+ var feature = new ol.Feature({
+ geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(this.props.value.longitude), parseFloat(this.props.value.latitude)]))
+ })
+
+ var defaultStyles = new ol.style.Style({
+ image: new ol.style.Circle(({
+ radius: 4,
+ fill: new ol.style.Fill({
+ color: '#004d87'
+ })
+ }))
+ })
+ var selectedStyles = new ol.style.Style({
+ image: new ol.style.Circle(({
+ radius: 6,
+ fill: new ol.style.Fill({
+ color: '#da2f36'
+ })
+ }))
+ })
+
+ var centerLayer = new ol.layer.Vector({
+ source: new ol.source.Vector({
+ features: [feature]
+ }),
+ style: selectedStyles,
+ zIndex: 2
+ })
+ var vectorLayer = new ol.layer.Vector({
+ source: source,
+ style: defaultStyles,
+ zIndex: 1
+ });
+
+ var map = new ol.Map({
+ target: 'stoppoint_map' + this.props.index,
+ layers: [
+ new ol.layer.Tile({
+ source: new ol.source.OSM()
+ }),
+ vectorLayer,
+ centerLayer
+ ],
+ controls: [ new ol.control.ScaleLine() ],
+ interactions: ol.interaction.defaults({
+ dragPan: false,
+ doubleClickZoom: false,
+ shiftDragZoom: false,
+ mouseWheelZoom: false
+ }),
+ view: new ol.View({
+ center: ol.proj.fromLonLat([parseFloat(this.props.value.longitude), parseFloat(this.props.value.latitude)]),
+ zoom: 18
+ })
+ });
+
+ // Selectable marker
+ var select = new ol.interaction.Select({
+ style: selectedStyles
+ });
+
+ map.addInteraction(select);
+
+ select.on('select', function(e) {
+ feature.setStyle(defaultStyles);
+ centerLayer.setZIndex(0);
+
+ if(e.selected.length != 0) {
+
+ if(e.selected[0].getGeometry() == feature.getGeometry()) {
+ if(e.selected[0].style_.image_.fill_.color_ != '#da2f36'){
+ feature.setStyle(selectedStyles);
+ centerLayer.setZIndex(2);
+ e.preventDefault()
+ return false
+ }
+ }
+ let data = _.assign({}, e.selected[0].getProperties(), {geometry: undefined});
+
+ this.props.onSelectMarker(this.props.index, data)
+ } else {
+ this.props.onUnselectMarker(this.props.index)
+ }
+ }, this);
+ }
+ }
+
+ render() {
+ if (this.props.value.olMap.isOpened) {
+ return (
+ <div className='map_container'>
+ <div className='map_metas'>
+ <p>
+ <strong>{this.props.value.olMap.json.name}</strong>
+ </p>
+ <p>
+ <strong>{this.context.I18n.routes.edit.stop_point_type} : </strong>
+ {this.props.value.olMap.json.area_type}
+ </p>
+ <p>
+ <strong>{this.context.I18n.routes.edit.short_name} : </strong>
+ {this.props.value.olMap.json.short_name}
+ </p>
+ <p>
+ <strong>{this.context.I18n.id_reflex} : </strong>
+ {this.props.value.olMap.json.user_objectid}
+ </p>
+
+ <p><strong>{this.context.I18n.routes.edit.map.coordinates} : </strong></p>
+ <p style={{paddingLeft: 10, marginTop: 0}}>
+ <em>{this.context.I18n.routes.edit.map.proj}.: </em>WSG84<br/>
+ <em>{this.context.I18n.routes.edit.map.lat}.: </em>{this.props.value.olMap.json.latitude} <br/>
+ <em>{this.context.I18n.routes.edit.map.lon}.: </em>{this.props.value.olMap.json.longitude}
+ </p>
+ <p>
+ <strong>{this.context.I18n.routes.edit.map.postal_code} : </strong>
+ {this.props.value.olMap.json.zip_code}
+ </p>
+ <p>
+ <strong>{this.context.I18n.routes.edit.map.city} : </strong>
+ {this.props.value.olMap.json.city_name}
+ </p>
+ <p>
+ <strong>{this.context.I18n.routes.edit.map.comment} : </strong>
+ {this.props.value.olMap.json.comment}
+ </p>
+ {(this.props.value.stoparea_id != this.props.value.olMap.json.stoparea_id) &&(
+ <div className='btn btn-outline-primary btn-sm'
+ onClick= {() => {this.props.onUpdateViaOlMap(this.props.index, this.props.value.olMap.json)}}
+ >{this.context.I18n.actions.select}</div>
+ )}
+ </div>
+ <div className='map_content'>
+ <div id={"stoppoint_map" + this.props.index} className='map'></div>
+ </div>
+ </div>
+ )
+ } else {
+ return false
+ }
+ }
+}
+
+OlMap.PropTypes = {
+}
+
+OlMap.contextTypes = {
+ I18n: PropTypes.object
+}
diff --git a/app/javascript/routes/components/StopPoint.js b/app/javascript/routes/components/StopPoint.js
new file mode 100644
index 000000000..606121f99
--- /dev/null
+++ b/app/javascript/routes/components/StopPoint.js
@@ -0,0 +1,94 @@
+import React, { PropTypes } from 'react'
+import BSelect2 from './BSelect2'
+import OlMap from './OlMap'
+
+export default function StopPoint(props, {I18n}) {
+ return (
+ <div className='nested-fields'>
+ <div className='wrapper'>
+ <div style={{width: 90}}>
+ <span>{props.value.user_objectid}</span>
+ </div>
+
+ <div>
+ <BSelect2 id={'route_stop_points_' + props.id} value={props.value} onChange={props.onChange} index={props.index} />
+ </div>
+
+ <div>
+ <select className='form-control' value={props.value.for_boarding} id="for_boarding" onChange={props.onSelectChange}>
+ <option value="normal">{I18n.routes.edit.stop_point.boarding.normal}</option>
+ <option value="forbidden">{I18n.routes.edit.stop_point.boarding.forbidden}</option>
+ </select>
+ </div>
+
+ <div>
+ <select className='form-control' value={props.value.for_alighting} id="for_alighting" onChange={props.onSelectChange}>
+ <option value="normal">{I18n.routes.edit.stop_point.alighting.normal}</option>
+ <option value="forbidden">{I18n.routes.edit.stop_point.alighting.forbidden}</option>
+ </select>
+ </div>
+
+ <div className='actions-5'>
+ <div
+ className={'btn btn-link' + (props.value.stoparea_id ? '' : ' disabled')}
+ onClick={props.onToggleMap}
+ >
+ <span className='fa fa-map-marker'></span>
+ </div>
+
+ <div
+ className={'btn btn-link' + (props.first ? ' disabled' : '')}
+ onClick={props.onMoveUpClick}
+ >
+ <span className='fa fa-arrow-up'></span>
+ </div>
+ <div
+ className={'btn btn-link' + (props.last ? ' disabled' : '')}
+ onClick={props.onMoveDownClick}
+ >
+ <span className='fa fa-arrow-down'></span>
+ </div>
+
+ <div
+ className='btn btn-link'
+ onClick={props.onToggleEdit}
+ >
+ <span className={'fa' + (props.value.edit ? ' fa-check' : ' fa-pencil')}></span>
+ </div>
+ <div
+ className='btn btn-link'
+ onClick={props.onDeleteClick}
+ >
+ <span className='fa fa-trash text-danger'></span>
+ </div>
+ </div>
+ </div>
+
+ <OlMap
+ value = {props.value}
+ index = {props.index}
+ onSelectMarker = {props.onSelectMarker}
+ onUnselectMarker = {props.onUnselectMarker}
+ onUpdateViaOlMap = {props.onUpdateViaOlMap}
+ />
+ </div>
+ )
+}
+
+StopPoint.PropTypes = {
+ onToggleMap: PropTypes.func.isRequired,
+ onToggleEdit: PropTypes.func.isRequired,
+ onDeleteClick: PropTypes.func.isRequired,
+ onMoveUpClick: PropTypes.func.isRequired,
+ onMoveDownClick: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onSelectChange: PropTypes.func.isRequired,
+ first: PropTypes.bool,
+ last: PropTypes.bool,
+ index: PropTypes.number,
+ value: PropTypes.object
+}
+
+StopPoint.contextTypes = {
+ I18n: PropTypes.object
+} \ No newline at end of file
diff --git a/app/javascript/routes/components/StopPointList.js b/app/javascript/routes/components/StopPointList.js
new file mode 100644
index 000000000..68af16f57
--- /dev/null
+++ b/app/javascript/routes/components/StopPointList.js
@@ -0,0 +1,69 @@
+import React, { PropTypes } from 'react'
+import StopPoint from './StopPoint'
+
+export default function StopPointList({ stopPoints, onDeleteClick, onMoveUpClick, onMoveDownClick, onChange, onSelectChange, onToggleMap, onToggleEdit, onSelectMarker, onUnselectMarker, onUpdateViaOlMap }, {I18n}) {
+ return (
+ <div className='subform'>
+ <div className='nested-head'>
+ <div className="wrapper">
+ <div style={{width: 100}}>
+ <div className="form-group">
+ <label className="control-label">{I18n.reflex_id}</label>
+ </div>
+ </div>
+ <div>
+ <div className="form-group">
+ <label className="control-label">{I18n.simple_form.labels.stop_point.name}</label>
+ </div>
+ </div>
+ <div>
+ <div className="form-group">
+ <label className="control-label">{I18n.simple_form.labels.stop_point.for_boarding}</label>
+ </div>
+ </div>
+ <div>
+ <div className="form-group">
+ <label className="control-label">{I18n.simple_form.labels.stop_point.for_alighting}</label>
+ </div>
+ </div>
+ <div className='actions-5'></div>
+ </div>
+ </div>
+ {stopPoints.map((stopPoint, index) =>
+ <StopPoint
+ key={'item-' + index}
+ onDeleteClick={() => onDeleteClick(index)}
+ onMoveUpClick={() => {
+ onMoveUpClick(index)
+ }}
+ onMoveDownClick={() => onMoveDownClick(index)}
+ onChange={ onChange }
+ onSelectChange={ (e) => onSelectChange(e, index) }
+ onToggleMap={() => onToggleMap(index)}
+ onToggleEdit={() => onToggleEdit(index)}
+ onSelectMarker={onSelectMarker}
+ onUnselectMarker={onUnselectMarker}
+ onUpdateViaOlMap={onUpdateViaOlMap}
+ first={ index === 0 }
+ last={ index === (stopPoints.length - 1) }
+ index={ index }
+ value={ stopPoint }
+ />
+ )}
+ </div>
+ )
+}
+
+StopPointList.PropTypes = {
+ stopPoints: PropTypes.array.isRequired,
+ onDeleteClick: PropTypes.func.isRequired,
+ onMoveUpClick: PropTypes.func.isRequired,
+ onMoveDownClick: PropTypes.func.isRequired,
+ onSelectChange: PropTypes.func.isRequired,
+ onSelectMarker: PropTypes.func.isRequired,
+ onUnselectMarker : PropTypes.func.isRequired
+}
+
+StopPointList.contextTypes = {
+ I18n: PropTypes.object
+} \ No newline at end of file
diff --git a/app/javascript/routes/containers/AddStopPoint.js b/app/javascript/routes/containers/AddStopPoint.js
new file mode 100644
index 000000000..fd9227ff3
--- /dev/null
+++ b/app/javascript/routes/containers/AddStopPoint.js
@@ -0,0 +1,20 @@
+import React from 'react'
+import { connect } from 'react-redux'
+import actions from '../actions'
+
+let AddStopPoint = ({ dispatch }) => {
+ return (
+ <div className="nested-linker">
+ <form onSubmit={e => {
+ e.preventDefault()
+ dispatch(actions.closeMaps())
+ dispatch(actions.addStop())
+ }}>
+ <button type="submit" className="btn btn-outline-primary">
+ Ajouter un arrêt
+ </button>
+ </form>
+ </div>
+ )
+}
+export default AddStopPoint = connect()(AddStopPoint)
diff --git a/app/javascript/routes/containers/VisibleStopPoints.js b/app/javascript/routes/containers/VisibleStopPoints.js
new file mode 100644
index 000000000..67d77af50
--- /dev/null
+++ b/app/javascript/routes/containers/VisibleStopPoints.js
@@ -0,0 +1,58 @@
+import actions from '../actions'
+import { connect } from 'react-redux'
+import StopPointList from '../components/StopPointList'
+
+const mapStateToProps = (state) => {
+ return {
+ stopPoints: state.stopPoints
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ onDeleteClick: (index) =>{
+ dispatch(actions.deleteStop(index))
+ dispatch(actions.closeMaps())
+ },
+ onMoveUpClick: (index) =>{
+ dispatch(actions.moveStopUp(index))
+ dispatch(actions.closeMaps())
+ },
+ onMoveDownClick: (index) =>{
+ dispatch(actions.moveStopDown(index))
+ dispatch(actions.closeMaps())
+ },
+ onChange: (index, text) =>{
+ dispatch(actions.updateInputValue(index, text))
+ dispatch(actions.closeMaps())
+ dispatch(actions.toggleEdit(index))
+ },
+ onSelectChange: (e, index) =>{
+ dispatch(actions.updateSelectValue(e, index))
+ dispatch(actions.closeMaps())
+ },
+ onToggleMap: (index) =>{
+ dispatch(actions.toggleMap(index))
+ },
+ onToggleEdit: (index) =>{
+ dispatch(actions.toggleEdit(index))
+ },
+ onSelectMarker: (index, data) =>{
+ dispatch(actions.selectMarker(index, data))
+ },
+ onUnselectMarker: (index) =>{
+ dispatch(actions.unselectMarker(index))
+ },
+ onUpdateViaOlMap: (index, data) =>{
+ dispatch(actions.updateInputValue(index, data))
+ dispatch(actions.toggleMap(index))
+ }
+ }
+}
+
+const VisibleStopPoints = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(StopPointList)
+
+export default VisibleStopPoints
diff --git a/app/javascript/routes/form_helper.js b/app/javascript/routes/form_helper.js
new file mode 100644
index 000000000..8a3277234
--- /dev/null
+++ b/app/javascript/routes/form_helper.js
@@ -0,0 +1,55 @@
+const formHelper = {
+ addInput: (name, value, index) => {
+ let form = document.querySelector('form')
+ let input = document.createElement('input')
+ let formatedName = `route[stop_points_attributes][${index.toString()}][${name}]`
+ input.setAttribute('type', 'hidden')
+ input.setAttribute('name', formatedName)
+ input.setAttribute('value', value)
+ form.appendChild(input)
+ },
+ addError: (ids) => {
+ ids.forEach((id) => {
+ if (!$(id).parents('.form-group').hasClass('has-error')) {
+ $(id).parents('.form-group').addClass('has-error')
+ $(id).parent().append(`<span class='help-block small'>${'doit être rempli(e)'}</span>`)
+ }
+ })
+ },
+ cleanInputs: (ids) => {
+ ids.forEach((id) =>{
+ $(id).parents('.form-group').removeClass('has-error')
+ $(id).siblings('span').remove()
+ })
+ },
+ handleForm: (...ids) => {
+ let filledInputs = []
+ let blankInputs = []
+ ids.forEach(id => {
+ $(id).val() == "" ? blankInputs.push(id) : filledInputs.push(id)
+ })
+
+ if (filledInputs.length > 0) formHelper.cleanInputs(filledInputs)
+ if (blankInputs.length > 0) formHelper.addError(blankInputs)
+ },
+ handleStopPoints: (event, state) => {
+ if (state.stopPoints.length >= 2) {
+ state.stopPoints.map((stopPoint, i) => {
+ formHelper.addInput('id', stopPoint.stoppoint_id ? stopPoint.stoppoint_id : '', i)
+ formHelper.addInput('stop_area_id', stopPoint.stoparea_id, i)
+ formHelper.addInput('position', i, i)
+ formHelper.addInput('for_boarding', stopPoint.for_boarding, i)
+ formHelper.addInput('for_alighting', stopPoint.for_alighting, i)
+ })
+ if ($('.alert.alert-danger').length > 0) $('.alert.alert-danger').remove()
+ } else {
+ event.preventDefault()
+ let msg = "L'itinéraire doit comporter au moins deux arrêts"
+ if ($('.alert.alert-danger').length == 0) {
+ $('#stop_points').find('.subform').after(`<div class='alert alert-danger'><span class='fa fa-lg fa-exclamation-circle'></span><span>" ${msg} "</span></div>`)
+ }
+ }
+ }
+}
+
+export default formHelper \ No newline at end of file
diff --git a/app/javascript/routes/index.js b/app/javascript/routes/index.js
new file mode 100644
index 000000000..febae7d54
--- /dev/null
+++ b/app/javascript/routes/index.js
@@ -0,0 +1,80 @@
+import React from 'react'
+import { render } from 'react-dom'
+import { Provider } from 'react-redux'
+import { createStore } from 'redux'
+import reducers from './reducers'
+import App from './components/App'
+import { handleForm, handleStopPoints } from './form_helper'
+import clone from '../helpers/clone'
+let datas = clone(window, "itinerary_stop", true)
+datas = JSON.parse(decodeURIComponent(datas))
+
+// logger, DO NOT REMOVE
+// var applyMiddleware = require('redux').applyMiddleware
+// var createLogger = require('redux-logger')
+// var thunkMiddleware = require('redux-thunk').default
+// var promise = require('redux-promise')
+
+const getInitialState = () => {
+ let state = []
+
+ datas.map(function(v, i) {
+ let fancyText = v.name.replace("&#39;", "\'")
+ if(v.zip_code && v.city_name)
+ fancyText += ", " + v.zip_code + " " + v.city_name.replace("&#39;", "\'")
+
+ state.push({
+ stoppoint_id: v.stoppoint_id,
+ stoparea_id: v.stoparea_id,
+ user_objectid: v.user_objectid,
+ short_name: v.short_name ? v.short_name.replace("&#39;", "\'") : '',
+ area_type: v.area_type,
+ index: i,
+ edit: false,
+ city_name: v.city_name ? v.city_name.replace("&#39;", "\'") : '',
+ zip_code: v.zip_code,
+ name: v.name ? v.name.replace("&#39;", "\'") : '',
+ registration_number: v.registration_number,
+ text: fancyText,
+ for_boarding: v.for_boarding || "normal",
+ for_alighting: v.for_alighting || "normal",
+ longitude: v.longitude || 0,
+ latitude: v.latitude || 0,
+ comment: v.comment ? v.comment.replace("&#39;", "\'") : '',
+ olMap: {
+ isOpened: false,
+ json: {}
+ }
+ })
+ })
+
+ return state
+}
+
+var initialState = {stopPoints: getInitialState()}
+// const loggerMiddleware = createLogger()
+let store = createStore(
+ reducers,
+ initialState
+ // applyMiddleware(thunkMiddleware, promise, loggerMiddleware)
+)
+
+render(
+ <Provider store={store}>
+ <App />
+ </Provider>,
+ document.getElementById('stop_points')
+)
+
+document.querySelector('input[name=commit]').addEventListener('click', (event)=>{
+ let state = store.getState()
+
+ let name = $("#route_name").val()
+ let publicName = $("#route_published_name").val()
+ if (name == "" || publicName == "") {
+ event.preventDefault()
+ handleForm("#route_name", "#route_published_name")
+ }
+
+ handleStopPoints(event, state)
+})
diff --git a/app/javascript/routes/reducers/index.js b/app/javascript/routes/reducers/index.js
new file mode 100644
index 000000000..eb01ea9f7
--- /dev/null
+++ b/app/javascript/routes/reducers/index.js
@@ -0,0 +1,8 @@
+import { combineReducers } from 'redux'
+import stopPoints from './stopPoints'
+
+const stopPointsApp = combineReducers({
+ stopPoints
+})
+
+export default stopPointsApp
diff --git a/app/javascript/routes/reducers/stopPoints.js b/app/javascript/routes/reducers/stopPoints.js
new file mode 100644
index 000000000..eeec06327
--- /dev/null
+++ b/app/javascript/routes/reducers/stopPoints.js
@@ -0,0 +1,144 @@
+import _ from 'lodash'
+import formHelper from '../form_helper'
+
+const stopPoint = (state = {}, action, length) => {
+ switch (action.type) {
+ case 'ADD_STOP':
+ return {
+ text: '',
+ index: length,
+ edit: true,
+ for_boarding: 'normal',
+ for_alighting: 'normal',
+ olMap: {
+ isOpened: false,
+ json: {}
+ }
+ }
+ default:
+ return state
+ }
+}
+
+const updateFormForDeletion = (stop) =>{
+ if (stop.stoppoint_id !== undefined){
+ let now = Date.now()
+ formHelper.addInput('id', stop.stoppoint_id, now)
+ formHelper.addInput('_destroy', 'true', now)
+ }
+}
+
+const stopPoints = (state = [], action) => {
+ switch (action.type) {
+ case 'ADD_STOP':
+ return [
+ ...state,
+ stopPoint(undefined, action, state.length)
+ ]
+ case 'MOVE_STOP_UP':
+ return [
+ ...state.slice(0, action.index - 1),
+ _.assign({}, state[action.index], { stoppoint_id: state[action.index - 1].stoppoint_id }),
+ _.assign({}, state[action.index - 1], { stoppoint_id: state[action.index].stoppoint_id }),
+ ...state.slice(action.index + 1)
+ ]
+ case 'MOVE_STOP_DOWN':
+ return [
+ ...state.slice(0, action.index),
+ _.assign({}, state[action.index + 1], { stoppoint_id: state[action.index].stoppoint_id }),
+ _.assign({}, state[action.index], { stoppoint_id: state[action.index + 1].stoppoint_id }),
+ ...state.slice(action.index + 2)
+ ]
+ case 'DELETE_STOP':
+ updateFormForDeletion(state[action.index])
+ return [
+ ...state.slice(0, action.index),
+ ...state.slice(action.index + 1).map((stopPoint)=>{
+ stopPoint.index--
+ return stopPoint
+ })
+ ]
+ case 'UPDATE_INPUT_VALUE':
+ return state.map( (t, i) => {
+ if (i === action.index) {
+ return _.assign(
+ {},
+ t,
+ {
+ stoppoint_id: t.stoppoint_id,
+ text: action.text.text,
+ stoparea_id: action.text.stoparea_id,
+ user_objectid: action.text.user_objectid,
+ latitude: action.text.latitude,
+ longitude: action.text.longitude,
+ name: action.text.name,
+ short_name: action.text.short_name,
+ area_type: action.text.area_type,
+ city_name: action.text.city_name,
+ comment: action.text.comment,
+ registration_number: action.text.registration_number
+ }
+ )
+ } else {
+ return t
+ }
+ })
+ case 'UPDATE_SELECT_VALUE':
+ return state.map( (t, i) => {
+ if (i === action.index) {
+ let stopState = _.assign({}, t)
+ stopState[action.select_id] = action.select_value
+ return stopState
+ } else {
+ return t
+ }
+ })
+ case 'TOGGLE_EDIT':
+ return state.map((t, i) => {
+ if (i === action.index){
+ return _.assign({}, t, {edit: !t.edit})
+ } else {
+ return t
+ }
+ })
+ case 'TOGGLE_MAP':
+ return state.map( (t, i) => {
+ if (i === action.index){
+ let val = !t.olMap.isOpened
+ let jsonData = val ? _.assign({}, t, {olMap: undefined}) : {}
+ let stateMap = _.assign({}, t.olMap, {isOpened: val, json: jsonData})
+ return _.assign({}, t, {olMap: stateMap})
+ }else {
+ let emptyMap = _.assign({}, t.olMap, {isOpened: false, json : {}})
+ return _.assign({}, t, {olMap: emptyMap})
+ }
+ })
+ case 'SELECT_MARKER':
+ return state.map((t, i) => {
+ if (i === action.index){
+ let stateMap = _.assign({}, t.olMap, {json: action.data})
+ return _.assign({}, t, {olMap: stateMap})
+ } else {
+ return t
+ }
+ })
+ case 'UNSELECT_MARKER':
+ return state.map((t, i) => {
+ if (i === action.index){
+ let stateMap = _.assign({}, t.olMap, {json: {}})
+ return _.assign({}, t, {olMap: stateMap})
+ } else {
+ return t
+ }
+ })
+ case 'CLOSE_MAP':
+ return state.map( (t, i) => {
+ let emptyMap = _.assign({}, t.olMap, {isOpened: false, json: {}})
+ return _.assign({}, t, {olMap: emptyMap})
+ })
+ default:
+ return state
+ }
+}
+
+export default stopPoints \ No newline at end of file
diff --git a/app/javascript/routes/show.js b/app/javascript/routes/show.js
new file mode 100644
index 000000000..e88469900
--- /dev/null
+++ b/app/javascript/routes/show.js
@@ -0,0 +1,102 @@
+const clone = require('../helpers/clone')
+let route = clone(window, "route", true)
+route = JSON.parse(decodeURIComponent(route))
+
+const geoColPts = []
+const geoColLns= []
+const geoColEdges = [
+ new ol.Feature({
+ geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(route[0].longitude), parseFloat(route[0].latitude)]))
+ }),
+ new ol.Feature({
+ geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(route[route.length - 1].longitude), parseFloat(route[route.length - 1].latitude)]))
+ })
+]
+route.forEach(function(stop, i){
+ if (i < route.length - 1){
+ geoColLns.push(new ol.Feature({
+ geometry: new ol.geom.LineString([
+ ol.proj.fromLonLat([parseFloat(route[i].longitude), parseFloat(route[i].latitude)]),
+ ol.proj.fromLonLat([parseFloat(route[i+1].longitude), parseFloat(route[i+1].latitude)])
+ ])
+ }))
+ }
+ geoColPts.push(new ol.Feature({
+ geometry: new ol.geom.Point(ol.proj.fromLonLat([parseFloat(stop.longitude), parseFloat(stop.latitude)]))
+ })
+ )
+})
+var edgeStyles = new ol.style.Style({
+ image: new ol.style.Circle(({
+ radius: 5,
+ stroke: new ol.style.Stroke({
+ color: '#007fbb',
+ width: 2
+ }),
+ fill: new ol.style.Fill({
+ color: '#007fbb',
+ width: 2
+ })
+ }))
+})
+var defaultStyles = new ol.style.Style({
+ image: new ol.style.Circle(({
+ radius: 4,
+ stroke: new ol.style.Stroke({
+ color: '#007fbb',
+ width: 2
+ }),
+ fill: new ol.style.Fill({
+ color: '#ffffff',
+ width: 2
+ })
+ }))
+})
+var lineStyle = new ol.style.Style({
+ stroke: new ol.style.Stroke({
+ color: '#007fbb',
+ width: 3
+ })
+})
+
+var vectorPtsLayer = new ol.layer.Vector({
+ source: new ol.source.Vector({
+ features: geoColPts
+ }),
+ style: defaultStyles,
+ zIndex: 2
+})
+var vectorEdgesLayer = new ol.layer.Vector({
+ source: new ol.source.Vector({
+ features: geoColEdges
+ }),
+ style: edgeStyles,
+ zIndex: 3
+})
+var vectorLnsLayer = new ol.layer.Vector({
+ source: new ol.source.Vector({
+ features: geoColLns
+ }),
+ style: [lineStyle],
+ zIndex: 1
+})
+
+var map = new ol.Map({
+ target: 'route_map',
+ layers: [
+ new ol.layer.Tile({
+ source: new ol.source.OSM()
+ }),
+ vectorPtsLayer,
+ vectorEdgesLayer,
+ vectorLnsLayer
+ ],
+ controls: [ new ol.control.ScaleLine(), new ol.control.Zoom(), new ol.control.ZoomSlider() ],
+ interactions: ol.interaction.defaults({
+ zoom: true
+ }),
+ view: new ol.View({
+ center: ol.proj.fromLonLat([parseFloat(route[0].longitude), parseFloat(route[0].latitude)]),
+ zoom: 13
+ })
+});