Ursprung
d91b30715d
Commit
0c5f71fc85
@ -207,15 +207,23 @@ endif()
|
|||||||
|
|
||||||
include(cmake/add-resource.cmake)
|
include(cmake/add-resource.cmake)
|
||||||
|
|
||||||
|
add_resource(resource-www
|
||||||
|
FILES
|
||||||
|
www/css/normalize.css
|
||||||
|
www/css/throttle.css
|
||||||
|
www/js/throttle.js
|
||||||
|
www/throttle.html
|
||||||
|
)
|
||||||
|
|
||||||
add_resource(resource-shared
|
add_resource(resource-shared
|
||||||
BASE_DIR ../
|
BASE_DIR ../
|
||||||
FILES
|
FILES
|
||||||
shared/gfx/appicon.ico
|
shared/gfx/appicon.ico
|
||||||
)
|
)
|
||||||
|
|
||||||
add_dependencies(traintastic-server resource-shared)
|
add_dependencies(traintastic-server resource-www resource-shared)
|
||||||
if(BUILD_TESTING)
|
if(BUILD_TESTING)
|
||||||
add_dependencies(traintastic-server-test resource-shared)
|
add_dependencies(traintastic-server-test resource-www resource-shared)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
### OPTIONS ###
|
### OPTIONS ###
|
||||||
|
|||||||
@ -37,7 +37,7 @@ guard = '_'.join(namespaces).upper() + '_' + re.sub(r'[\.]+','_', os.path.basena
|
|||||||
|
|
||||||
is_binary = input_file_ext not in ['html', 'css', 'js']
|
is_binary = input_file_ext not in ['html', 'css', 'js']
|
||||||
|
|
||||||
with open(input_file, 'rb') as f:
|
with open(input_file, 'rb' if is_binary else 'r') as f:
|
||||||
contents = f.read()
|
contents = f.read()
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(sys.argv[3]), exist_ok=True)
|
os.makedirs(os.path.dirname(sys.argv[3]), exist_ok=True)
|
||||||
@ -64,6 +64,7 @@ constexpr std::array<std::byte, {size}> {variable}{{{{
|
|||||||
}}}};
|
}}}};
|
||||||
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
''')
|
''')
|
||||||
|
|
||||||
@ -79,8 +80,9 @@ else: # text
|
|||||||
namespace {'::'.join(namespaces)}
|
namespace {'::'.join(namespaces)}
|
||||||
{{
|
{{
|
||||||
|
|
||||||
constexpr std::string_view {variable} = R"({contents})";
|
constexpr std::string_view {variable} = R"~#!({contents})~#!";
|
||||||
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
''')
|
''')
|
||||||
|
|||||||
@ -32,6 +32,18 @@
|
|||||||
#include "../log/log.hpp"
|
#include "../log/log.hpp"
|
||||||
#include "../log/logmessageexception.hpp"
|
#include "../log/logmessageexception.hpp"
|
||||||
#include "../utils/setthreadname.hpp"
|
#include "../utils/setthreadname.hpp"
|
||||||
|
|
||||||
|
//#define SERVE_FROM_FS // Development option, NOT for production!
|
||||||
|
#ifdef SERVE_FROM_FS
|
||||||
|
#include "../utils/readfile.hpp"
|
||||||
|
|
||||||
|
static const auto www = std::filesystem::absolute(std::filesystem::path(__FILE__).parent_path() / ".." / ".." / "www");
|
||||||
|
#else
|
||||||
|
#include <resource/www/throttle.html.hpp>
|
||||||
|
#include <resource/www/css/throttle.css.hpp>
|
||||||
|
#include <resource/www/js/throttle.js.hpp>
|
||||||
|
#endif
|
||||||
|
#include <resource/www/css/normalize.css.hpp>
|
||||||
#include <resource/shared/gfx/appicon.ico.hpp>
|
#include <resource/shared/gfx/appicon.ico.hpp>
|
||||||
|
|
||||||
#define IS_SERVER_THREAD (std::this_thread::get_id() == m_thread.get_id())
|
#define IS_SERVER_THREAD (std::this_thread::get_id() == m_thread.get_id())
|
||||||
@ -45,6 +57,8 @@ namespace
|
|||||||
static constexpr std::string_view serverHeader{"Traintastic-server/" TRAINTASTIC_VERSION_FULL};
|
static constexpr std::string_view serverHeader{"Traintastic-server/" TRAINTASTIC_VERSION_FULL};
|
||||||
static constexpr std::string_view contentTypeTextPlain{"text/plain"};
|
static constexpr std::string_view contentTypeTextPlain{"text/plain"};
|
||||||
static constexpr std::string_view contentTypeTextHtml{"text/html"};
|
static constexpr std::string_view contentTypeTextHtml{"text/html"};
|
||||||
|
static constexpr std::string_view contentTypeTextCss{"text/css"};
|
||||||
|
static constexpr std::string_view contentTypeTextJavaScript{"text/javascript"};
|
||||||
static constexpr std::string_view contentTypeImageXIcon{"image/x-icon"};
|
static constexpr std::string_view contentTypeImageXIcon{"image/x-icon"};
|
||||||
|
|
||||||
http::message_generator notFound(const http::request<http::string_body>& request)
|
http::message_generator notFound(const http::request<http::string_body>& request)
|
||||||
@ -131,6 +145,16 @@ http::message_generator textHtml(const http::request<http::string_body>& request
|
|||||||
return text(request, contentTypeTextHtml, body);
|
return text(request, contentTypeTextHtml, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
http::message_generator textCss(const http::request<http::string_body>& request, std::string_view body)
|
||||||
|
{
|
||||||
|
return text(request, contentTypeTextCss, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
http::message_generator textJavaScript(const http::request<http::string_body>& request, std::string_view body)
|
||||||
|
{
|
||||||
|
return text(request, contentTypeTextJavaScript, body);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Server::Server(bool localhostOnly, uint16_t port, bool discoverable)
|
Server::Server(bool localhostOnly, uint16_t port, bool discoverable)
|
||||||
@ -330,6 +354,9 @@ http::message_generator Server::handleHTTPRequest(http::request<http::string_bod
|
|||||||
"</head>"
|
"</head>"
|
||||||
"<body>"
|
"<body>"
|
||||||
"<h1>Traintastic <small>v" TRAINTASTIC_VERSION_FULL "</small></h1>"
|
"<h1>Traintastic <small>v" TRAINTASTIC_VERSION_FULL "</small></h1>"
|
||||||
|
"<ul>"
|
||||||
|
"<li><a href=\"/throttle\">Web throttle</a></li>"
|
||||||
|
"</ul>"
|
||||||
"</body>"
|
"</body>"
|
||||||
"</html>");
|
"</html>");
|
||||||
}
|
}
|
||||||
@ -337,6 +364,37 @@ http::message_generator Server::handleHTTPRequest(http::request<http::string_bod
|
|||||||
{
|
{
|
||||||
return binary(request, contentTypeImageXIcon, Resource::shared::gfx::appicon_ico);
|
return binary(request, contentTypeImageXIcon, Resource::shared::gfx::appicon_ico);
|
||||||
}
|
}
|
||||||
|
if(request.target() == "/css/normalize.css")
|
||||||
|
{
|
||||||
|
return textCss(request, Resource::www::css::normalize_css);
|
||||||
|
}
|
||||||
|
if(request.target() == "/css/throttle.css")
|
||||||
|
{
|
||||||
|
#ifdef SERVE_FROM_FS
|
||||||
|
const auto css = readFile(www / "css" / "throttle.css");
|
||||||
|
return css ? textCss(request, *css) : notFound(request);
|
||||||
|
#else
|
||||||
|
return textCss(request, Resource::www::css::throttle_css);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
if(request.target() == "/js/throttle.js")
|
||||||
|
{
|
||||||
|
#ifdef SERVE_FROM_FS
|
||||||
|
const auto js = readFile(www / "js" / "throttle.js");
|
||||||
|
return js ? textJavaScript(request, *js) : notFound(request);
|
||||||
|
#else
|
||||||
|
return textJavaScript(request, Resource::www::js::throttle_js);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
if(request.target() == "/throttle")
|
||||||
|
{
|
||||||
|
#ifdef SERVE_FROM_FS
|
||||||
|
const auto html = readFile(www / "throttle.html");
|
||||||
|
return html ? textHtml(request, *html) : notFound(request);
|
||||||
|
#else
|
||||||
|
return textHtml(request, Resource::www::throttle_html);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
if(target == "/version")
|
if(target == "/version")
|
||||||
{
|
{
|
||||||
return textPlain(request, TRAINTASTIC_VERSION_FULL);
|
return textPlain(request, TRAINTASTIC_VERSION_FULL);
|
||||||
|
|||||||
@ -22,8 +22,15 @@
|
|||||||
|
|
||||||
#include "webthrottleconnection.hpp"
|
#include "webthrottleconnection.hpp"
|
||||||
#include "server.hpp"
|
#include "server.hpp"
|
||||||
|
#include "../traintastic/traintastic.hpp"
|
||||||
#include "../core/eventloop.hpp"
|
#include "../core/eventloop.hpp"
|
||||||
|
#include "../core/objectproperty.tpp"
|
||||||
|
#include "../hardware/throttle/webthrottle.hpp"
|
||||||
#include "../log/log.hpp"
|
#include "../log/log.hpp"
|
||||||
|
#include "../train/train.hpp"
|
||||||
|
#include "../train/trainerror.hpp"
|
||||||
|
#include "../train/trainlist.hpp"
|
||||||
|
|
||||||
WebThrottleConnection::WebThrottleConnection(Server& server, std::shared_ptr<boost::beast::websocket::stream<boost::beast::tcp_stream>> ws)
|
WebThrottleConnection::WebThrottleConnection(Server& server, std::shared_ptr<boost::beast::websocket::stream<boost::beast::tcp_stream>> ws)
|
||||||
: WebSocketConnection(server, std::move(ws), "webthrottle")
|
: WebSocketConnection(server, std::move(ws), "webthrottle")
|
||||||
{
|
{
|
||||||
@ -35,6 +42,12 @@ WebThrottleConnection::WebThrottleConnection(Server& server, std::shared_ptr<boo
|
|||||||
WebThrottleConnection::~WebThrottleConnection()
|
WebThrottleConnection::~WebThrottleConnection()
|
||||||
{
|
{
|
||||||
assert(isEventLoopThread());
|
assert(isEventLoopThread());
|
||||||
|
|
||||||
|
for(auto& it : m_throttles)
|
||||||
|
{
|
||||||
|
it.second->destroy();
|
||||||
|
it.second.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebThrottleConnection::doRead()
|
void WebThrottleConnection::doRead()
|
||||||
@ -102,7 +115,138 @@ void WebThrottleConnection::doWrite()
|
|||||||
void WebThrottleConnection::processMessage(const nlohmann::json& message)
|
void WebThrottleConnection::processMessage(const nlohmann::json& message)
|
||||||
{
|
{
|
||||||
assert(isEventLoopThread());
|
assert(isEventLoopThread());
|
||||||
(void)message;
|
|
||||||
|
const auto& world = Traintastic::instance->world.value();
|
||||||
|
const auto action = message.value("action", "");
|
||||||
|
const auto throttleId = message.value<uint32_t>("throttle_id", 0);
|
||||||
|
|
||||||
|
if(throttleId == 0)
|
||||||
|
{
|
||||||
|
if(action == "get_train_list")
|
||||||
|
{
|
||||||
|
auto response = nlohmann::json::object();
|
||||||
|
response.emplace("event", "train_list");
|
||||||
|
auto list = nlohmann::json::array();
|
||||||
|
if(world)
|
||||||
|
{
|
||||||
|
for(const auto& train : *world->trains)
|
||||||
|
{
|
||||||
|
auto item = nlohmann::json::object();
|
||||||
|
item.emplace("id", train->id.value());
|
||||||
|
item.emplace("name", train->name.value());
|
||||||
|
list.emplace_back(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.emplace("list", list);
|
||||||
|
sendMessage(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const auto& throttle = getThrottle(throttleId);
|
||||||
|
|
||||||
|
if(action == "acquire")
|
||||||
|
{
|
||||||
|
auto train = std::dynamic_pointer_cast<Train>(world->getObjectById(message.value("train_id", "")));
|
||||||
|
if(train)
|
||||||
|
{
|
||||||
|
nlohmann::json object;
|
||||||
|
|
||||||
|
const auto ec = throttle->acquire(train, message.value("steal", false));
|
||||||
|
if(!ec)
|
||||||
|
{
|
||||||
|
m_trainPropertyChanged.emplace(throttleId, train->propertyChanged.connect(
|
||||||
|
[this, throttleId](BaseProperty& property)
|
||||||
|
{
|
||||||
|
const auto name = property.name();
|
||||||
|
if(name == "direction" || name == "speed" || name == "throttle_speed")
|
||||||
|
{
|
||||||
|
auto event = nlohmann::json::object();
|
||||||
|
event.emplace("event", name);
|
||||||
|
event.emplace("throttle_id", throttleId);
|
||||||
|
if(dynamic_cast<AbstractUnitProperty*>(&property))
|
||||||
|
{
|
||||||
|
event.update(property.toJSON());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
event.emplace("value", property.toJSON());
|
||||||
|
}
|
||||||
|
sendMessage(event);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
object = nlohmann::json::object();
|
||||||
|
object.emplace("id", train->id.toJSON());
|
||||||
|
object.emplace("name", train->name.toJSON());
|
||||||
|
object.emplace("direction", train->direction.toJSON());
|
||||||
|
object.emplace("speed", train->speed.toJSON());
|
||||||
|
object.emplace("throttle_speed", train->throttleSpeed.toJSON());
|
||||||
|
}
|
||||||
|
else // error
|
||||||
|
{
|
||||||
|
auto error = nlohmann::json::object();
|
||||||
|
error.emplace("event", "message");
|
||||||
|
error.emplace("throttle_id", throttleId);
|
||||||
|
error.emplace("type", "error");
|
||||||
|
if(ec == TrainError::AlreadyAcquired)
|
||||||
|
{
|
||||||
|
error.emplace("tag", "already_acquired");
|
||||||
|
}
|
||||||
|
error.emplace("text", ec.message());
|
||||||
|
sendMessage(error);
|
||||||
|
}
|
||||||
|
auto response = nlohmann::json::object();
|
||||||
|
response.emplace("event", "train");
|
||||||
|
response.emplace("throttle_id", throttleId);
|
||||||
|
response.emplace("train", object);
|
||||||
|
sendMessage(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(action == "set_name")
|
||||||
|
{
|
||||||
|
throttle->name = message.value("value", "");
|
||||||
|
}
|
||||||
|
else if(throttle->acquired())
|
||||||
|
{
|
||||||
|
if(action == "estop")
|
||||||
|
{
|
||||||
|
throttle->emergencyStop();
|
||||||
|
}
|
||||||
|
else if(action == "stop")
|
||||||
|
{
|
||||||
|
throttle->stop();
|
||||||
|
}
|
||||||
|
else if(action == "faster")
|
||||||
|
{
|
||||||
|
throttle->faster();
|
||||||
|
}
|
||||||
|
else if(action == "slower")
|
||||||
|
{
|
||||||
|
throttle->slower();
|
||||||
|
}
|
||||||
|
else if(action == "reverse")
|
||||||
|
{
|
||||||
|
throttle->setDirection(Direction::Reverse);
|
||||||
|
}
|
||||||
|
else if(action == "forward")
|
||||||
|
{
|
||||||
|
throttle->setDirection(Direction::Forward);
|
||||||
|
}
|
||||||
|
else if(action == "release")
|
||||||
|
{
|
||||||
|
throttle->release(message.value("stop", true));
|
||||||
|
|
||||||
|
m_trainPropertyChanged.erase(throttleId);
|
||||||
|
|
||||||
|
auto response = nlohmann::json::object();
|
||||||
|
response.emplace("event", "train");
|
||||||
|
response.emplace("throttle_id", throttleId);
|
||||||
|
response.emplace("train", nullptr);
|
||||||
|
sendMessage(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebThrottleConnection::sendMessage(const nlohmann::json& message)
|
void WebThrottleConnection::sendMessage(const nlohmann::json& message)
|
||||||
@ -120,3 +264,35 @@ void WebThrottleConnection::sendMessage(const nlohmann::json& message)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::shared_ptr<WebThrottle>& WebThrottleConnection::getThrottle(uint32_t throttleId)
|
||||||
|
{
|
||||||
|
assert(isEventLoopThread());
|
||||||
|
|
||||||
|
static const std::shared_ptr<WebThrottle> noThrottle;
|
||||||
|
|
||||||
|
if(auto it = m_throttles.find(throttleId); it != m_throttles.end())
|
||||||
|
{
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(const auto& world = Traintastic::instance->world.value())
|
||||||
|
{
|
||||||
|
auto [it, inserted] = m_throttles.emplace(throttleId, WebThrottle::create(*world));
|
||||||
|
if(inserted) /*[[likely]]*/
|
||||||
|
{
|
||||||
|
m_throttleReleased.emplace(throttleId, it->second->released.connect(
|
||||||
|
[this, throttleId]()
|
||||||
|
{
|
||||||
|
auto response = nlohmann::json::object();
|
||||||
|
response.emplace("event", "train");
|
||||||
|
response.emplace("throttle_id", throttleId);
|
||||||
|
response.emplace("train", nullptr);
|
||||||
|
sendMessage(response);
|
||||||
|
}));
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return noThrottle;
|
||||||
|
}
|
||||||
|
|||||||
@ -37,6 +37,9 @@ class WebThrottleConnection : public WebSocketConnection
|
|||||||
protected:
|
protected:
|
||||||
boost::beast::flat_buffer m_readBuffer;
|
boost::beast::flat_buffer m_readBuffer;
|
||||||
std::queue<std::string> m_writeQueue;
|
std::queue<std::string> m_writeQueue;
|
||||||
|
std::map<uint32_t, std::shared_ptr<WebThrottle>> m_throttles;
|
||||||
|
std::map<uint32_t, boost::signals2::scoped_connection> m_throttleReleased;
|
||||||
|
std::map<uint32_t, boost::signals2::scoped_connection> m_trainPropertyChanged;
|
||||||
|
|
||||||
void doRead() final;
|
void doRead() final;
|
||||||
void doWrite() final;
|
void doWrite() final;
|
||||||
@ -44,6 +47,8 @@ protected:
|
|||||||
void processMessage(const nlohmann::json& message);
|
void processMessage(const nlohmann::json& message);
|
||||||
void sendMessage(const nlohmann::json& message);
|
void sendMessage(const nlohmann::json& message);
|
||||||
|
|
||||||
|
const std::shared_ptr<WebThrottle>& getThrottle(uint32_t throttleId);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
const std::string id;
|
const std::string id;
|
||||||
|
|
||||||
|
|||||||
39
server/src/utils/readfile.cpp
Normale Datei
39
server/src/utils/readfile.cpp
Normale Datei
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* server/src/utils/readfile.cpp
|
||||||
|
*
|
||||||
|
* This file is part of the traintastic source code.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2025 Reinder Feenstra
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "readfile.hpp"
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
std::optional<std::string> readFile(const std::filesystem::path& filename)
|
||||||
|
{
|
||||||
|
std::ifstream file(filename, std::ios::in | std::ios::binary | std::ios::ate);
|
||||||
|
if(!file.is_open())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
const size_t size = file.tellg();
|
||||||
|
std::string contents;
|
||||||
|
contents.resize(size);
|
||||||
|
file.seekg(std::ios::beg);
|
||||||
|
file.read(contents.data(), size);
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
32
server/src/utils/readfile.hpp
Normale Datei
32
server/src/utils/readfile.hpp
Normale Datei
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* server/src/utils/readfile.hpp
|
||||||
|
*
|
||||||
|
* This file is part of the traintastic source code.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2025 Reinder Feenstra
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef TRAINTASTIC_SERVER_UTILS_READFILE_HPP
|
||||||
|
#define TRAINTASTIC_SERVER_UTILS_READFILE_HPP
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
std::optional<std::string> readFile(const std::filesystem::path& filename);
|
||||||
|
|
||||||
|
#endif
|
||||||
349
server/www/css/normalize.css
vendored
Normale Datei
349
server/www/css/normalize.css
vendored
Normale Datei
@ -0,0 +1,349 @@
|
|||||||
|
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||||
|
|
||||||
|
/* Document
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the line height in all browsers.
|
||||||
|
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the margin in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the `main` element consistently in IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the font size and margin on `h1` elements within `section` and
|
||||||
|
* `article` contexts in Chrome, Firefox, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0.67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in Firefox.
|
||||||
|
* 2. Show the overflow in Edge and IE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box; /* 1 */
|
||||||
|
height: 0; /* 1 */
|
||||||
|
overflow: visible; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pre {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-level semantics
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the gray background on active links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove the bottom border in Chrome 57-
|
||||||
|
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: none; /* 1 */
|
||||||
|
text-decoration: underline; /* 2 */
|
||||||
|
text-decoration: underline dotted; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||||
|
* 2. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace; /* 1 */
|
||||||
|
font-size: 1em; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||||
|
* all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the border on images inside links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Change the font styles in all browsers.
|
||||||
|
* 2. Remove the margin in Firefox and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit; /* 1 */
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
line-height: 1.15; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the overflow in IE.
|
||||||
|
* 1. Show the overflow in Edge.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input { /* 1 */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||||
|
* 1. Remove the inheritance of text transform in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select { /* 1 */
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type="button"],
|
||||||
|
[type="reset"],
|
||||||
|
[type="submit"] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner border and padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
[type="button"]::-moz-focus-inner,
|
||||||
|
[type="reset"]::-moz-focus-inner,
|
||||||
|
[type="submit"]::-moz-focus-inner {
|
||||||
|
border-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore the focus styles unset by the previous rule.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button:-moz-focusring,
|
||||||
|
[type="button"]:-moz-focusring,
|
||||||
|
[type="reset"]:-moz-focusring,
|
||||||
|
[type="submit"]:-moz-focusring {
|
||||||
|
outline: 1px dotted ButtonText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the padding in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: 0.35em 0.75em 0.625em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the text wrapping in Edge and IE.
|
||||||
|
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||||
|
* 3. Remove the padding so developers are not caught out when they zero out
|
||||||
|
* `fieldset` elements in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
color: inherit; /* 2 */
|
||||||
|
display: table; /* 1 */
|
||||||
|
max-width: 100%; /* 1 */
|
||||||
|
padding: 0; /* 3 */
|
||||||
|
white-space: normal; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the default vertical scrollbar in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Add the correct box sizing in IE 10.
|
||||||
|
* 2. Remove the padding in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="checkbox"],
|
||||||
|
[type="radio"] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="number"]::-webkit-inner-spin-button,
|
||||||
|
[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
* 2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
outline-offset: -2px; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
* 2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button; /* 1 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
details {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add the correct display in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Misc
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the correct display in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
192
server/www/css/throttle.css
Normale Datei
192
server/www/css/throttle.css
Normale Datei
@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of Traintastic,
|
||||||
|
* see <https://github.com/traintastic/traintastic>.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2024-2025 Reinder Feenstra
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.flex-column
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-row
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-resize
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-fixed
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide
|
||||||
|
{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stretch
|
||||||
|
{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-right
|
||||||
|
{
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right
|
||||||
|
{
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p1
|
||||||
|
{
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pr1
|
||||||
|
{
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
html
|
||||||
|
{
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body
|
||||||
|
{
|
||||||
|
background-color: #121212;
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
header
|
||||||
|
{
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1
|
||||||
|
{
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#open_settings
|
||||||
|
{
|
||||||
|
font-size: 200%;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 2.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#body
|
||||||
|
{
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings
|
||||||
|
{
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
background-color: #121212;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings label
|
||||||
|
{
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.control,
|
||||||
|
select.control
|
||||||
|
{
|
||||||
|
background-color: #bb86fc;
|
||||||
|
border: none;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0.25rem;
|
||||||
|
height: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.control:disabled
|
||||||
|
{
|
||||||
|
background-color: gray !important;
|
||||||
|
cursor: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.control.active
|
||||||
|
{
|
||||||
|
background-color: #03dac6;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.red
|
||||||
|
{
|
||||||
|
background-color: #cf6679;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.square
|
||||||
|
{
|
||||||
|
width: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label
|
||||||
|
{
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text]
|
||||||
|
{
|
||||||
|
background-color: rgba(255, 255, 255, .07);
|
||||||
|
border: solid 2px #bb86fc;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
font-size: 150%;
|
||||||
|
color: white;
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
margin: 0.5rem 0 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.message
|
||||||
|
{
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.message.error
|
||||||
|
{
|
||||||
|
background-color: #cf6679;
|
||||||
|
}
|
||||||
403
server/www/js/throttle.js
Normale Datei
403
server/www/js/throttle.js
Normale Datei
@ -0,0 +1,403 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of Traintastic,
|
||||||
|
* see <https://github.com/traintastic/traintastic>.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2024-2025 Reinder Feenstra
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function Throttle(parent, id)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
this.message = null;
|
||||||
|
this.messageTimeout = null;
|
||||||
|
|
||||||
|
var createButton = function (action, innerText, className)
|
||||||
|
{
|
||||||
|
var e = document.createElement('button');
|
||||||
|
e.innerText = innerText;
|
||||||
|
e.className = className;
|
||||||
|
e.setAttribute('throttle-id', id);
|
||||||
|
e.setAttribute('action', action);
|
||||||
|
e.onclick = function ()
|
||||||
|
{
|
||||||
|
tm.send({
|
||||||
|
'throttle_id': parseInt(this.getAttribute('throttle-id')),
|
||||||
|
'action': this.getAttribute('action'),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
var createDiv = function (className, children)
|
||||||
|
{
|
||||||
|
var e = document.createElement('div');
|
||||||
|
e.className = className;
|
||||||
|
children.forEach(function (c, _)
|
||||||
|
{
|
||||||
|
e.appendChild(c);
|
||||||
|
});
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
|
||||||
|
var createTrainSelect = function (className)
|
||||||
|
{
|
||||||
|
var e = document.createElement('select');
|
||||||
|
e.className = className;
|
||||||
|
e.setAttribute('throttle-id', id);
|
||||||
|
e.setAttribute('name', 'train_select');
|
||||||
|
e.onchange = function ()
|
||||||
|
{
|
||||||
|
if(this.value != '')
|
||||||
|
{
|
||||||
|
tm.send({
|
||||||
|
'throttle_id': parseInt(this.getAttribute('throttle-id')),
|
||||||
|
'action': 'acquire',
|
||||||
|
'train_id': this.value,
|
||||||
|
'steal': false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tm.send({
|
||||||
|
'throttle_id': parseInt(this.getAttribute('throttle-id')),
|
||||||
|
'action': 'release',
|
||||||
|
'stop': localStorage.throttleStopOnRelease != 'false',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
var createSpan = function (id, text = '', className = '')
|
||||||
|
{
|
||||||
|
var e = document.createElement('span');
|
||||||
|
e.id = id;
|
||||||
|
e.className = className;
|
||||||
|
e.innerText = text;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
var addRemoveClass = function (e, add, className)
|
||||||
|
{
|
||||||
|
if(add && !e.classList.contains(className))
|
||||||
|
{
|
||||||
|
e.classList.add(className);
|
||||||
|
}
|
||||||
|
else if(!add)
|
||||||
|
{
|
||||||
|
e.classList.remove(className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatSpeed = function (value, unit)
|
||||||
|
{
|
||||||
|
if(unit == 'kmph')
|
||||||
|
{
|
||||||
|
return value.toFixed(0) + ' km/h';
|
||||||
|
}
|
||||||
|
if(unit == 'mph')
|
||||||
|
{
|
||||||
|
return value.toFixed(0) + ' mph';
|
||||||
|
}
|
||||||
|
if(unit == 'mps')
|
||||||
|
{
|
||||||
|
return value.toFixed(1) + ' m/s';
|
||||||
|
}
|
||||||
|
return value + ' ' + unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
var throttle = createDiv('p1 stretch', [
|
||||||
|
createTrainSelect('control stretch'),
|
||||||
|
createDiv('flex-row', [
|
||||||
|
createSpan('', 'Speed:', 'text-right pr1'),
|
||||||
|
createSpan('throttle-' + id + '-actual-speed', '', 'stretch'),
|
||||||
|
createSpan('', 'Target:', 'stretch text-right pr1'),
|
||||||
|
createSpan('throttle-' + id + '-target-speed', '', 'stretch'),
|
||||||
|
/* not yet supported:
|
||||||
|
createSpan('', 'Limit:', 'stretch text-right pr1'),
|
||||||
|
createSpan('throttle-' + id + '-target-limit', '∞', 'stretch'),
|
||||||
|
*/
|
||||||
|
]),
|
||||||
|
createDiv('flex-row', [
|
||||||
|
createDiv('flex-resize', [
|
||||||
|
// TODO: function buttons
|
||||||
|
]),
|
||||||
|
createDiv('flex-resize', [
|
||||||
|
createDiv('flex-column stretch', [
|
||||||
|
createButton('faster', '+', 'control stretch'),
|
||||||
|
createButton('slower', '-', 'control stretch'),
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
createDiv('flex-row', [
|
||||||
|
createDiv('flex-resize', [createButton('reverse', '<', 'control stretch')]),
|
||||||
|
createDiv('flex-resize', [createButton('stop', '0', 'control stretch')]),
|
||||||
|
createDiv('flex-resize', [createButton('forward', '>', 'control stretch')]),
|
||||||
|
]),
|
||||||
|
createButton('estop', 'EStop', 'control stretch red'),
|
||||||
|
]);
|
||||||
|
parent.appendChild(throttle);
|
||||||
|
|
||||||
|
this.setTrainList = function (list)
|
||||||
|
{
|
||||||
|
var train_select = throttle.querySelector('select[name=train_select]');
|
||||||
|
|
||||||
|
train_select.innerHTML = '';
|
||||||
|
train_select.appendChild(document.createElement('option'));
|
||||||
|
list.forEach(function (train, _)
|
||||||
|
{
|
||||||
|
var option = document.createElement('option');
|
||||||
|
option.value = train['id'];
|
||||||
|
option.innerText = train['name'];
|
||||||
|
train_select.appendChild(option);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.showMessage = function (message)
|
||||||
|
{
|
||||||
|
var layout = [];
|
||||||
|
if(message['tag'] == 'already_acquired')
|
||||||
|
{
|
||||||
|
var e = document.createElement('button');
|
||||||
|
e.innerText = 'Steal';
|
||||||
|
e.className = 'right';
|
||||||
|
e.setAttribute('throttle-id', id);
|
||||||
|
e.setAttribute('train-id', throttle.querySelector('select[name=train_select]').value);
|
||||||
|
e.onclick = function ()
|
||||||
|
{
|
||||||
|
var throttleId = parseInt(this.getAttribute('throttle-id'));
|
||||||
|
tm.send({
|
||||||
|
'throttle_id': throttleId,
|
||||||
|
'action': 'acquire',
|
||||||
|
'train_id': this.getAttribute('train-id'),
|
||||||
|
'steal': true,
|
||||||
|
});
|
||||||
|
tm.throttles[throttleId].clearMessage();
|
||||||
|
};
|
||||||
|
layout.push(e)
|
||||||
|
}
|
||||||
|
text = document.createElement('p');
|
||||||
|
text.innerText = message['text'];
|
||||||
|
layout.push(text);
|
||||||
|
this.clearMessage();
|
||||||
|
this.message = createDiv('message ' + message['type'], layout);
|
||||||
|
throttle.appendChild(this.message);
|
||||||
|
this.messageTimeout = window.setTimeout(function (throttle) { throttle.clearMessage(); }, 5000, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearMessage = function ()
|
||||||
|
{
|
||||||
|
if(this.message)
|
||||||
|
{
|
||||||
|
throttle.removeChild(this.message);
|
||||||
|
window.clearTimeout(this.messageTimeout);
|
||||||
|
this.message = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setTrain = function (train)
|
||||||
|
{
|
||||||
|
var buttons = throttle.querySelectorAll('button.control');
|
||||||
|
if(train)
|
||||||
|
{
|
||||||
|
this.setDirection(train.direction);
|
||||||
|
this.setSpeed(train.speed.value, train.speed.unit);
|
||||||
|
this.setThrottleSpeed(train.throttle_speed.value, train.throttle_speed.unit);
|
||||||
|
buttons.forEach(function (button) { button.disabled = false; });
|
||||||
|
throttle.querySelector('select[name=train_select]').value = train.id;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
buttons.forEach(function (button) { button.disabled = true; });
|
||||||
|
throttle.querySelector('select[name=train_select]').value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setDirection = function (direction)
|
||||||
|
{
|
||||||
|
addRemoveClass(
|
||||||
|
throttle.querySelector('button[action=forward]'),
|
||||||
|
direction == 'forward',
|
||||||
|
'active');
|
||||||
|
addRemoveClass(
|
||||||
|
throttle.querySelector('button[action=reverse]'),
|
||||||
|
direction == 'reverse',
|
||||||
|
'active');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setSpeed = function (value, unit)
|
||||||
|
{
|
||||||
|
document.getElementById('throttle-' + this.id + '-actual-speed').innerText = formatSpeed(value, unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setThrottleSpeed = function (value, unit)
|
||||||
|
{
|
||||||
|
document.getElementById('throttle-' + this.id + '-target-speed').innerText = formatSpeed(value, unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setTrain(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
var tm = new function ()
|
||||||
|
{
|
||||||
|
this.ws = null;
|
||||||
|
this.throttles = []
|
||||||
|
|
||||||
|
this.init = function ()
|
||||||
|
{
|
||||||
|
if(localStorage.throttleName)
|
||||||
|
{
|
||||||
|
document.getElementById('throttle_name').value = localStorage.throttleName;
|
||||||
|
}
|
||||||
|
if(localStorage.throttleStopOnRelease)
|
||||||
|
{
|
||||||
|
document.getElementById('stop_train_on_release').value = localStorage.throttleStopOnRelease;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('open_settings').onclick = function ()
|
||||||
|
{
|
||||||
|
var settings = document.getElementById('settings');
|
||||||
|
if(settings.classList.contains('hide'))
|
||||||
|
{
|
||||||
|
settings.classList.remove('hide');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
document.getElementById('close_settings').onclick();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('close_settings').onclick = function ()
|
||||||
|
{
|
||||||
|
localStorage.throttleName = document.getElementById('throttle_name').value;
|
||||||
|
localStorage.throttleStopOnRelease = document.getElementById('stop_train_on_release').value == 'on';
|
||||||
|
document.getElementById('settings').classList.add('hide');
|
||||||
|
if(tm.throttles.length == 0)
|
||||||
|
{
|
||||||
|
tm.add();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if(localStorage.throttleName && localStorage.throttleStopOnRelease)
|
||||||
|
{
|
||||||
|
document.getElementById('settings').classList.add('hide');
|
||||||
|
this.add();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.add = function ()
|
||||||
|
{
|
||||||
|
this.connect();
|
||||||
|
var e = document.getElementById('throttles');
|
||||||
|
var id = 1;
|
||||||
|
while(id in this.throttles) { id++; }
|
||||||
|
this.throttles[id] = new Throttle(e, id);
|
||||||
|
return this.throttles[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connect = function ()
|
||||||
|
{
|
||||||
|
if(!this.ws)
|
||||||
|
{
|
||||||
|
this.ws = new WebSocket((window.location.protocol == 'https' ? 'wss' : 'ws') + '://' + window.location.host + window.location.pathname);
|
||||||
|
this.ws.onopen = function (ev)
|
||||||
|
{
|
||||||
|
tm.send({ 'action': 'get_train_list' });
|
||||||
|
tm.throttles.forEach(function (throttle, _)
|
||||||
|
{
|
||||||
|
tm.send({
|
||||||
|
'throttle_id': throttle.id,
|
||||||
|
'action': 'set_name',
|
||||||
|
'value': localStorage.throttleName + ' #' + throttle.id,
|
||||||
|
});
|
||||||
|
var trainId = localStorage['throttle' + throttle.id + 'TrainId'];
|
||||||
|
if(trainId)
|
||||||
|
{
|
||||||
|
tm.send({
|
||||||
|
'throttle_id': throttle.id,
|
||||||
|
'action': 'acquire',
|
||||||
|
'train_id': trainId,
|
||||||
|
'steal': false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
this.ws.onmessage = function (ev)
|
||||||
|
{
|
||||||
|
var msg = JSON.parse(ev.data);
|
||||||
|
console.log('RX', msg);
|
||||||
|
if(msg['event'] == 'train_list')
|
||||||
|
{
|
||||||
|
tm.throttles.forEach(function (throttle, _)
|
||||||
|
{
|
||||||
|
throttle.setTrainList(msg['list']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if(msg['event'] == 'message')
|
||||||
|
{
|
||||||
|
var throttleId = msg['throttle_id'];
|
||||||
|
if(throttleId)
|
||||||
|
{
|
||||||
|
tm.throttles[throttleId].showMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(msg['event'] == 'train')
|
||||||
|
{
|
||||||
|
var throttleId = msg['throttle_id'];
|
||||||
|
var train = msg['train'];
|
||||||
|
var item = 'throttle' + throttleId + 'TrainId';
|
||||||
|
if(train)
|
||||||
|
{
|
||||||
|
localStorage.setItem(item, train.id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
localStorage.removeItem(item);
|
||||||
|
}
|
||||||
|
tm.throttles[throttleId].setTrain(train);
|
||||||
|
}
|
||||||
|
else if(msg['event'] == 'direction')
|
||||||
|
{
|
||||||
|
tm.throttles[msg['throttle_id']].setDirection(msg['value']);
|
||||||
|
}
|
||||||
|
else if(msg['event'] == 'speed')
|
||||||
|
{
|
||||||
|
tm.throttles[msg['throttle_id']].setSpeed(msg['value'], msg['unit']);
|
||||||
|
}
|
||||||
|
else if(msg['event'] == 'throttle_speed')
|
||||||
|
{
|
||||||
|
tm.throttles[msg['throttle_id']].setThrottleSpeed(msg['value'], msg['unit']);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.ws.onclose = function ()
|
||||||
|
{
|
||||||
|
setTimeout(function () { tm.connect(); }, 1000);
|
||||||
|
}
|
||||||
|
this.ws.onerror = function (err)
|
||||||
|
{
|
||||||
|
console.error('WebSocket error: ', err.message);
|
||||||
|
this.ws.close();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.send = function (msg)
|
||||||
|
{
|
||||||
|
console.log('TX', msg);
|
||||||
|
return this.ws.send(JSON.stringify(msg));
|
||||||
|
};
|
||||||
|
}();
|
||||||
36
server/www/throttle.html
Normale Datei
36
server/www/throttle.html
Normale Datei
@ -0,0 +1,36 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<title>Throttle | Traintastic</title>
|
||||||
|
<link rel="stylesheet" href="/css/normalize.css">
|
||||||
|
<link rel="stylesheet" href="/css/throttle.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body onload="tm.init()">
|
||||||
|
<header>
|
||||||
|
<div id="open_settings" class="right">⋮</div>
|
||||||
|
<h1>Traintastic</h1>
|
||||||
|
</header>
|
||||||
|
<div id="body">
|
||||||
|
<div id="settings">
|
||||||
|
<div>
|
||||||
|
<label for="throttle_name">Name:</label>
|
||||||
|
<input type="text" id="throttle_name">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="stop_train_on_release">Stop train on release:</label>
|
||||||
|
<input type="checkbox" id="stop_train_on_release" checked="checked">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button id="close_settings">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="throttles" class="flex-row"></div>
|
||||||
|
</div>
|
||||||
|
<script src="/js/throttle.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Laden…
x
In neuem Issue referenzieren
Einen Benutzer sperren