Files
wx_wherigo/main/src/ui/map_sim_frame.cpp
T
mars3142 92045ec6df some tweaks
still non playable cartridges

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-06-02 23:21:56 +02:00

380 lines
13 KiB
C++

#include "ui/map_sim_frame.h"
#include "lua/game_engine.h"
extern "C" {
#include <lua.h>
}
#include <wx/button.h>
#include <wx/log.h>
#include <wx/msgdlg.h>
#include <wx/sizer.h>
#include <wx/webview.h>
enum { ID_PlayBtn = wxID_HIGHEST + 100, ID_ClearBtn, ID_SimTimer };
wxBEGIN_EVENT_TABLE(MapSimFrame, wxFrame)
EVT_BUTTON(ID_PlayBtn, MapSimFrame::OnPlay)
EVT_BUTTON(ID_ClearBtn, MapSimFrame::OnClear)
EVT_TIMER(ID_SimTimer, MapSimFrame::OnSimTimer) wxEND_EVENT_TABLE()
// Escape a string for safe embedding inside a JS single-quoted string
static wxString jsEscape(const std::string &s) {
wxString out;
for (const unsigned char c : s) {
if (c == '\'')
out += "\\'";
else if (c == '\\')
out += "\\\\";
else if (c == '\n')
out += "\\n";
else if (c == '\r')
out += "\\r";
else
out += static_cast<char>(c);
}
return out;
}
MapSimFrame::MapSimFrame(wxWindow *parent, double centerLat, double centerLon,
const std::vector<ZoneInfo> &zoneInfos)
: wxFrame(parent, wxID_ANY, "GPS-Simulation", wxDefaultPosition,
wxSize(900, 700)),
m_simTimer(this, ID_SimTimer), m_zoneInfos(zoneInfos) {
auto *sizer = new wxBoxSizer(wxVERTICAL);
m_webView = wxWebView::New(this, wxID_ANY);
sizer->Add(m_webView, 1, wxEXPAND);
auto *btnSizer = new wxBoxSizer(wxHORIZONTAL);
btnSizer->Add(new wxButton(this, ID_PlayBtn, "▶ Route abspielen"), 0, wxALL,
5);
btnSizer->Add(new wxButton(this, ID_ClearBtn, "✕ Wegpunkte löschen"), 0,
wxALL, 5);
sizer->Add(btnSizer, 0, wxALIGN_CENTER);
SetSizer(sizer);
// Register JS → C++ message handler (wxWidgets 3.1.5+)
m_webView->AddScriptMessageHandler("wx");
m_webView->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED,
&MapSimFrame::OnScriptMessage, this);
m_webView->Bind(wxEVT_WEBVIEW_LOADED, &MapSimFrame::OnWebViewLoaded, this);
wherigo::GameEngine::getInstance().Bind(
wherigo::EVT_GAME_STATE_CHANGED, &MapSimFrame::OnGameStateChanged, this);
Bind(wxEVT_CLOSE_WINDOW, &MapSimFrame::OnClose, this);
// ── Build Leaflet page ───────────────────────────────────────────────────
wxString html;
html << "<!DOCTYPE html><html><head>"
"<meta charset='utf-8'><title>GPS-Sim</title>"
"<link rel='stylesheet' "
"href='https://unpkg.com/leaflet/dist/leaflet.css'/>"
"<link rel='stylesheet' "
"href='https://unpkg.com/leaflet.markercluster/dist/"
"MarkerCluster.css'/>"
"<link rel='stylesheet' "
"href='https://unpkg.com/leaflet.markercluster/dist/"
"MarkerCluster.Default.css'/>"
"<script src='https://unpkg.com/leaflet/dist/leaflet.js'></script>"
"<script "
"src='https://unpkg.com/leaflet.markercluster/dist/"
"leaflet.markercluster.js'></script>"
"<style>"
" html,body,#map{height:100%;margin:0;padding:0;}"
" .zone-dot{"
" width:14px;height:14px;border-radius:50%;"
" background:#e44;border:2px solid #900;"
" box-shadow:0 1px 3px rgba(0,0,0,.4);"
" }"
" .zone-cluster{"
" background:#e44;border:3px solid #900;border-radius:50%;"
" color:#fff;font-weight:bold;font-size:13px;"
" display:flex;align-items:center;justify-content:center;"
" box-shadow:0 2px 5px rgba(0,0,0,.4);"
" }"
" .zone-label{"
" background:transparent;border:none;font-weight:bold;"
" font-size:12px;color:#900;"
" text-shadow:0 0 3px #fff,0 0 3px #fff,0 0 3px #fff;"
" white-space:nowrap;pointer-events:none;"
" }"
"</style>"
"</head><body>"
"<div id='map' style='height:calc(100vh - 50px);'></div>"
"<script>"
<< wxString::Format("var map=L.map('map').setView([%.8f,%.8f],16);",
centerLat, centerLon)
<< "L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',"
" {maxZoom:19,attribution:'&copy; OpenStreetMap'}).addTo(map);"
"var routeMarkers=[];"
"var routeLine=null;"
// Zone cluster group with custom cluster icon
"var zoneCluster=L.markerClusterGroup({"
" maxClusterRadius:60,"
" iconCreateFunction:function(c){"
" var n=c.getChildCount();"
" var s=n<10?32:n<100?38:44;"
" return L.divIcon({"
" html:'<div class=\"zone-cluster\" "
"style=\"width:'+s+'px;height:'+s+'px;\">'+n+'</div>',"
" className:'',"
" iconSize:[s,s],iconAnchor:[s/2,s/2]"
" });"
" }"
"});";
html << "map.addLayer(zoneCluster);"
// Dynamic zone sync: adds new active zones, removes deactivated ones
"var zoneMarkerMap={};"
"function syncZones(zones){"
" var active={};"
" zones.forEach(function(z){"
" active[z.name]=true;"
" if(!zoneMarkerMap[z.name]){"
" var zm=L.marker([z.lat,z.lon],{"
" icon:L.divIcon({"
" html:'<div class=\"zone-dot\"></div>',"
" className:'',"
" iconSize:[14,14],iconAnchor:[7,7]"
" })"
" });"
" zm.bindTooltip(z.name,{"
" permanent:true,direction:'top',"
" offset:[0,-10],className:'zone-label'"
" });"
" zoneCluster.addLayer(zm);"
" zoneMarkerMap[z.name]=zm;"
" }"
" });"
" Object.keys(zoneMarkerMap).forEach(function(name){"
" if(!active[name]){"
" zoneCluster.removeLayer(zoneMarkerMap[name]);"
" delete zoneMarkerMap[name];"
" }"
" });"
"}"
// Player position marker (hidden until first position update)
"var playerMarker=null;"
"function updatePlayerPos(lat,lon){"
" if(!playerMarker){"
" playerMarker=L.marker([lat,lon],{"
" icon:L.divIcon({"
" html:'<div style=\"width:18px;height:18px;border-radius:50%;"
" background:#0077cc;border:3px solid #fff;"
" box-shadow:0 0 0 2px #0077cc,0 2px 6px "
"rgba(0,0,0,.5);\"></div>',"
" className:'',"
" iconSize:[18,18],iconAnchor:[9,9]"
" })"
" }).addTo(map);"
" "
"playerMarker.bindTooltip('Spieler',{permanent:false,direction:'top'}"
");"
" } else {"
" playerMarker.setLatLng([lat,lon]);"
" }"
" map.panTo([lat,lon]);"
"}"
// Click handler: add numbered route waypoint (NOT in cluster)
"function updateLine(){"
" if(routeLine){map.removeLayer(routeLine);routeLine=null;}"
" if(routeMarkers.length>1){"
" var lls=routeMarkers.map(function(m){return m.getLatLng();});"
" "
"routeLine=L.polyline(lls,{color:'#0077cc',weight:2,dashArray:'6,4'})"
".addTo(map);"
" }"
"}"
"map.on('click',function(e){"
" var n=routeMarkers.length+1;"
" var m=L.marker(e.latlng,{"
" icon:L.divIcon({"
" html:'<div "
"style=\"background:#0077cc;color:#fff;border-radius:50%;width:20px;"
" "
"height:20px;line-height:20px;text-align:center;font-size:11px;"
" font-weight:bold;border:2px solid "
"#005299;\">'+n+'</div>',"
" className:'',"
" iconSize:[20,20],iconAnchor:[10,10]"
" })"
" }).addTo(map);"
" routeMarkers.push(m);"
" updateLine();"
" "
"window.wx.postMessage(JSON.stringify({lat:e.latlng.lat,lon:e.latlng."
"lng}));"
"});"
"</script></body></html>";
m_webView->SetPage(html, "");
}
// ── JS → C++ message
// ──────────────────────────────────────────────────────────
static bool parseLatLon(const wxString &json, double &lat, double &lon) {
auto extractNum = [&](const wxString &key, double &val) -> bool {
int pos = json.Find(key);
if (pos == wxNOT_FOUND)
return false;
pos += static_cast<int>(key.length());
while (pos < static_cast<int>(json.length()) &&
(json[pos] == ' ' || json[pos] == ':'))
++pos;
const int start = pos;
while (pos < static_cast<int>(json.length()) &&
(wxIsdigit(json[pos]) || json[pos] == '.' || json[pos] == '-'))
++pos;
return json.Mid(start, pos - start).ToDouble(&val);
};
return extractNum("\"lat\"", lat) && extractNum("\"lon\"", lon);
}
void MapSimFrame::OnScriptMessage(wxWebViewEvent &event) {
double lat = 0, lon = 0;
if (parseLatLon(event.GetString(), lat, lon)) {
AddSimPoint(lat, lon);
wxLogDebug("MapSim: waypoint %zu added (%.6f, %.6f)", m_route.size(), lat,
lon);
}
}
// ── Route management
// ──────────────────────────────────────────────────────────
void MapSimFrame::AddSimPoint(double lat, double lon) {
m_route.push_back({lat, lon});
}
void MapSimFrame::StartSimulation() {
if (m_route.empty())
return;
m_simIndex = 0;
m_simTimer.Start(500);
}
// ── Button handlers
// ───────────────────────────────────────────────────────────
void MapSimFrame::OnPlay(wxCommandEvent &) {
if (m_route.empty()) {
wxMessageBox("Bitte zuerst Wegpunkte auf der Karte setzen.", "Hinweis",
wxOK | wxICON_INFORMATION);
return;
}
m_simIndex = 0;
m_simTimer.Start(500);
wxLogDebug("MapSim: starting playback of %zu waypoints", m_route.size());
}
void MapSimFrame::OnClear(wxCommandEvent &) {
m_simTimer.Stop();
m_route.clear();
m_simIndex = 0;
// Remove all route markers and the connecting line from the map
m_webView->RunScript(
"routeMarkers.forEach(function(m){map.removeLayer(m);});"
"routeMarkers=[];"
"if(routeLine){map.removeLayer(routeLine);routeLine=null;}",
nullptr);
wxLogDebug("MapSim: route cleared");
}
void MapSimFrame::OnSimTimer(wxTimerEvent &) {
if (m_simIndex >= m_route.size()) {
wxCommandEvent dummy;
OnClear(dummy);
return;
}
const auto &pt = m_route[m_simIndex++];
SendPositionToEngine(pt.lat, pt.lon);
}
// ── Zone sync
// ─────────────────────────────────────────────────────────────────
void MapSimFrame::syncZonesOnMap() {
if (!m_pageLoaded)
return;
lua_State *L = wherigo::GameEngine::getInstance().getLuaState();
if (!L)
return;
wxString js = "syncZones([";
bool first = true;
lua_getglobal(L, "_G");
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_istable(L, -1)) {
lua_getfield(L, -1, "ClassName");
const bool isZone =
lua_isstring(L, -1) && !strcmp(lua_tostring(L, -1), "Zone");
lua_pop(L, 1);
if (isZone) {
lua_getfield(L, -1, "Active");
const bool active = lua_toboolean(L, -1);
lua_pop(L, 1);
if (active) {
lua_getfield(L, -1, "Name");
const std::string name =
lua_isstring(L, -1) ? lua_tostring(L, -1) : "";
lua_pop(L, 1);
lua_getfield(L, -1, "OriginalPoint");
if (lua_istable(L, -1)) {
lua_getfield(L, -1, "latitude");
lua_getfield(L, -2, "longitude");
const double lat = lua_tonumber(L, -2);
const double lon = lua_tonumber(L, -1);
lua_pop(L, 2);
if (!first)
js += ",";
js += wxString::Format("{name:'%s',lat:%.8f,lon:%.8f}",
jsEscape(name), lat, lon);
first = false;
}
lua_pop(L, 1); // pop OriginalPoint
}
}
}
lua_pop(L, 1);
}
lua_pop(L, 1); // pop _G
js += "]);";
m_webView->RunScript(js, nullptr);
}
void MapSimFrame::OnWebViewLoaded(wxWebViewEvent &) {
m_pageLoaded = true;
syncZonesOnMap();
}
void MapSimFrame::OnGameStateChanged(wxEvent &) { syncZonesOnMap(); }
void MapSimFrame::OnClose(wxCloseEvent &event) {
wherigo::GameEngine::getInstance().Unbind(
wherigo::EVT_GAME_STATE_CHANGED, &MapSimFrame::OnGameStateChanged, this);
event.Skip();
}
// ── Engine integration
// ────────────────────────────────────────────────────────
void MapSimFrame::SendPositionToEngine(double lat, double lon) {
wxLogDebug("MapSim: position → engine (%.6f, %.6f)", lat, lon);
wherigo::GameEngine::getInstance().updatePlayerPosition(lat, lon, 0.0);
m_webView->RunScript(
wxString::Format("updatePlayerPos(%.8f,%.8f);", lat, lon), nullptr);
}