// url.cpp // // Copyright (C) 2002-present, the Celestia Development Team // Original version written by Chris Teyssier (chris@tux.teyssier.org) // // 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. #include #include #include #include #include #include #include #include #include "celestiacore.h" #include "url.h" using celestia::util::GetLogger; namespace { std::string getBodyName(Universe* universe, Body* body) { std::string name = body->getName(); PlanetarySystem* parentSystem = body->getSystem(); const Body* parentBody = nullptr; if (parentSystem != nullptr) parentBody = parentSystem->getPrimaryBody(); else assert(0); // TODO: Figure out why the line below was added. //parentBody = body->getOrbitBarycenter(); while (parentBody != nullptr) { name = parentBody->getName() + ":" + name; parentSystem = parentBody->getSystem(); if (parentSystem == nullptr) break; parentBody = parentSystem->getPrimaryBody(); } auto *star = body->getSystem()->getStar(); if (star != nullptr) name = universe->getStarCatalog()->getStarName(*star) + ":" + name; return name; } // We use std::string here because we pass result to C API (gettext()) std::string getBodyShortName(const std::string &body) { if (!body.empty()) { auto pos = body.rfind(":"); if (pos != std::string::npos) return D_(body.substr(pos + 1).c_str()); } return D_(body.c_str()); } celestia::compat::string_view getCoordSysName(ObserverFrame::CoordinateSystem mode) { switch (mode) { case ObserverFrame::Universal: return "Freeflight"; case ObserverFrame::Ecliptical: return "Follow"; case ObserverFrame::BodyFixed: return "SyncOrbit"; case ObserverFrame::Chase: return "Chase"; case ObserverFrame::PhaseLock: return "PhaseLock"; case ObserverFrame::Equatorial: return "Unknown"; case ObserverFrame::ObserverLocal: return "Unknown"; default: return "Unknown"; } } } // anon namespace Url::Url(CelestiaCore *core) : m_appCore(core) { } Url::Url(const CelestiaState &appState, int version, Url::TimeSource timeSource) : m_appCore(appState.m_appCore), m_state(appState), m_version(version), m_timeSource(timeSource) { assert(version == 3); std::ostringstream u; switch (m_state.m_coordSys) { case ObserverFrame::Universal: m_nBodies = 0; break; case ObserverFrame::PhaseLock: m_nBodies = 2; break; default: m_nBodies = 1; } u << Url::proto() << getCoordSysName(m_state.m_coordSys); if (appState.m_coordSys != ObserverFrame::Universal) { u << '/' << m_state.m_refBodyName; if (appState.m_coordSys == ObserverFrame::PhaseLock) u << '/' << m_state.m_targetBodyName; } m_date = astro::Date(m_state.m_tdb); u << '/' << m_date.toCStr(astro::Date::ISO8601); // observer position u << fmt::format("?x={}&y={}&z={}", m_state.m_observerPosition.x.toString(), m_state.m_observerPosition.y.toString(), m_state.m_observerPosition.z.toString()); // observer orientation u << fmt::format("&ow={}&ox={}&oy={}&oz={}", m_state.m_observerOrientation.w(), m_state.m_observerOrientation.x(), m_state.m_observerOrientation.y(), m_state.m_observerOrientation.z()); if (!m_state.m_trackedBodyName.empty()) u << "&track=" << m_state.m_trackedBodyName; if (!m_state.m_selectedBodyName.empty()) u << "&select=" << m_state.m_selectedBodyName; u << fmt::format("&fov={}&ts={}<d={}&p={}", m_state.m_fieldOfView, m_state.m_timeScale, m_state.m_lightTimeDelay ? 1 : 0, m_state.m_pauseState ? 1 : 0); // ShowTintedIllumination == 0x04000000, the last 1.6 parameter // we keep only old parameters and clear new ones int rf = static_cast(m_state.m_renderFlags & 0x04ffffffull); // 1.6 uses ShowPlanets to control display of all types of solar // system objects. So set it if any one is displayed. if ((m_state.m_renderFlags & Renderer::ShowSolarSystemObjects) != 0) rf |= static_cast(Renderer::ShowPlanets); // But we need to store actual value of the bit which controls // planets display. 27th bit is unused in 1.6. if ((m_state.m_renderFlags & Renderer::ShowPlanets) != 0) rf |= (1<<27); int nrf = static_cast(m_state.m_renderFlags >> 27); u << fmt::format("&rf={}&nrf={}&lm={}", rf, nrf, m_state.m_labelMode); // Append the url settings: time source and version u << "&tsrc=" << (int) m_timeSource; u << "&ver=" << m_version; m_url = u.str(); m_valid = true; evalName(); } bool Url::goTo() { if (!m_valid) return false; assert(m_appCore != nullptr); auto *sim = m_appCore->getSimulation(); auto *renderer = m_appCore->getRenderer(); sim->update(0.0); sim->setFrame(m_ref.getCoordinateSystem(), m_ref.getRefObject(), m_ref.getTargetObject()); sim->getActiveObserver()->setFOV(celmath::degToRad(m_state.m_fieldOfView)); m_appCore->setZoomFromFOV(); sim->setTimeScale(m_state.m_timeScale); sim->setPauseState(m_state.m_pauseState); m_appCore->setLightDelayActive(m_state.m_lightTimeDelay); if (!m_state.m_selectedBodyName.empty()) { auto body = m_state.m_selectedBodyName; std::replace(body.begin(), body.end(), ':', '/'); auto sel = sim->findObjectFromPath(body); sim->setSelection(sel); } else { sim->setSelection(Selection()); } if (!m_state.m_trackedBodyName.empty()) { auto body = m_state.m_trackedBodyName; std::replace(body.begin(), body.end(), ':', '/'); auto sel = sim->findObjectFromPath(body); sim->setTrackedObject(sel); } else { if (!sim->getTrackedObject().empty()) sim->setTrackedObject(Selection()); } renderer->setRenderFlags(m_state.m_renderFlags); renderer->setLabelMode(m_state.m_labelMode); switch (m_timeSource) { case UseUrlTime: sim->setTime(m_state.m_tdb); break; case UseSimulationTime: // Leave the current simulation time unmodified break; case UseSystemTime: sim->setTime(astro::UTCtoTDB(astro::Date::systemDate())); break; default: break; } // Position and orientation stored in frame coordinates; convert them // to universal and set the observer position. double tdb = sim->getTime(); auto coord = sim->getObserver().getFrame()->convertToUniversal(m_state.m_observerPosition, m_state.m_tdb); Eigen::Quaterniond q = m_state.m_observerOrientation.cast(); q = sim->getObserver().getFrame()->convertToUniversal(q, m_state.m_tdb); sim->setObserverPosition(coord); sim->setObserverOrientation(q.cast()); return true; } std::string Url::getAsString() const { return m_url; } std::string Url::getEncodedObjectName(const Selection& selection, const CelestiaCore* appCore) { auto *universe = appCore->getSimulation()->getUniverse(); std::string name; Body* parentBody = nullptr; switch (selection.getType()) { case Selection::Type_Body: name = getBodyName(universe, selection.body()); break; case Selection::Type_Star: name = universe->getStarCatalog()->getStarName(*selection.star()); break; case Selection::Type_DeepSky: name = universe->getDSOCatalog()->getDSOName(selection.deepsky()); break; case Selection::Type_Location: name = selection.location()->getName(); parentBody = selection.location()->getParentBody(); if (parentBody != nullptr) name = getBodyName(universe, parentBody) + ":" + name; break; default: return {}; } return Url::encodeString(name); } std::string Url::decodeString(celestia::compat::string_view str) { celestia::compat::string_view::size_type a = 0, b = 0; std::string out; b = str.find('%'); while (b != celestia::compat::string_view::npos && a < str.length()) { auto s = str.substr(a, b - a); out.append(s.data(), s.length()); celestia::compat::string_view c_code = str.substr(b + 1, 2); uint8_t c; if (to_number(c_code, c, 16)) { out += static_cast(c); } else { GetLogger()->warn(_("Incorrect hex value \"{}\"\n"), c_code); out += '%'; out.append(c_code.data(), c_code.length()); } a = b + 1 + c_code.length(); b = str.find('%', a); } if (a < str.length()) { auto s = str.substr(a); out.append(s.data(), s.length()); } return out; } std::string Url::encodeString(celestia::compat::string_view str) { std::ostringstream enc; for (const auto _ch : str) { auto ch = static_cast(_ch); bool encode = false; if (ch <= 32 || ch >= 128) { encode = true; } else { switch (ch) { case '%': case '?': case '"': case '#': case '+': case ',': case '=': case '@': case '[': case ']': encode = true; break; } } if (encode) enc << fmt::sprintf("%%%02x", ch); else enc << _ch; } return enc.str(); } struct Mode { celestia::compat::string_view modeStr; ObserverFrame::CoordinateSystem mode; int nBodies; }; static Mode modes[] = { { "Freeflight", ObserverFrame::Universal, 0 }, { "Follow", ObserverFrame::Ecliptical, 1 }, { "SyncOrbit", ObserverFrame::BodyFixed, 1 }, { "Chase", ObserverFrame::Chase, 1 }, { "PhaseLock", ObserverFrame::PhaseLock, 2 }, }; auto ParseURLParams(celestia::compat::string_view paramsStr) -> std::map; bool Url::parse(celestia::compat::string_view urlStr) { constexpr auto npos = celestia::compat::string_view::npos; // proper URL string must start with protocol (cel://) if (urlStr.compare(0, Url::proto().length(), Url::proto()) != 0) { GetLogger()->error(_("URL must start with \"{}\"!\n"), Url::proto()); return false; } // extract @path and @params from the URL auto pos = urlStr.find('?'); auto pathStr = urlStr.substr(Url::proto().length(), pos - Url::proto().length()); while (pathStr.back() == '/') pathStr.remove_suffix(1); celestia::compat::string_view paramsStr; if (pos != npos) paramsStr = urlStr.substr(pos + 1); pos = pathStr.find('/'); if (pos == npos) { GetLogger()->error(_("URL must have at least mode and time!\n")); return false; } auto modeStr = pathStr.substr(0, pos); int nBodies = -1; CelestiaState state; auto lambda = [modeStr](Mode &m) { return compareIgnoringCase(modeStr, m.modeStr) == 0; }; auto it = std::find_if(std::begin(modes), std::end(modes), lambda); if (it == std::end(modes)) { GetLogger()->error(_("Unsupported URL mode \"{}\"!\n"), modeStr); return false; } state.m_coordSys = it->mode; nBodies = it->nBodies; auto timepos = nBodies == 0 ? pos : pathStr.rfind('/'); auto timeStr = pathStr.substr(timepos + 1); Selection bodies[2]; if (nBodies > 0) { auto bodiesStr = pathStr.substr(pos + 1, timepos - pos - 1); pos = bodiesStr.find('/'); if (nBodies == 1) { if (pos != npos) { GetLogger()->error(_("URL must contain only one body\n")); return false; } auto body = Url::decodeString(bodiesStr); std::replace(body.begin(), body.end(), ':', '/'); bodies[0] = m_appCore->getSimulation()->findObjectFromPath(body); state.m_refBodyName = std::move(body); } else if (nBodies == 2) { if (pos == npos || bodiesStr.find('/', pos + 1) != npos) { GetLogger()->error(_("URL must contain 2 bodies\n")); return false; } auto body = Url::decodeString(bodiesStr.substr(0, pos)); std::replace(body.begin(), body.end(), ':', '/'); bodies[0] = m_appCore->getSimulation()->findObjectFromPath(body); state.m_refBodyName = std::move(body); body = Url::decodeString(bodiesStr.substr(pos + 1)); std::replace(body.begin(), body.end(), ':', '/'); bodies[1] = m_appCore->getSimulation()->findObjectFromPath(body); state.m_targetBodyName = std::move(body); } } ObserverFrame ref; switch (nBodies) { case 0: ref = ObserverFrame(); break; case 1: ref = ObserverFrame(state.m_coordSys, bodies[0]); break; case 2: ref = ObserverFrame(state.m_coordSys, bodies[0], bodies[1]); break; default: break; } auto params = ParseURLParams(paramsStr); // Version labelling of cel URLs was only added in Celestia 1.5, cel URL // version 2. Assume any URL without a version is version 1. int version = 1; if (params.count("ver") != 0) { auto &p = params["ver"]; if (!to_number(p, version)) { GetLogger()->error(_("Invalid URL version \"{}\"!\n"), p); return false; } } if (version != 3 && version != 4) { GetLogger()->error(_("Unsupported URL version: {}\n"), version); return false; } m_ref = ref; m_state = state; m_nBodies = nBodies; if (version == 4 && !initVersion4(params, timeStr)) return false; else if (!initVersion3(params, timeStr)) return false; m_valid = true; evalName(); return true; } auto ParseURLParams(celestia::compat::string_view paramsStr) -> std::map { std::map params; if (paramsStr.empty()) return params; constexpr auto npos = celestia::compat::string_view::npos; for (auto iter = paramsStr;;) { auto pos = iter.find('&'); auto kv = iter.substr(0, pos); auto vpos = kv.find('='); if (vpos == npos) { GetLogger()->error(_("URL parameter must look like key=value\n")); break; } params[kv.substr(0, vpos)] = Url::decodeString(kv.substr(vpos + 1)); if (pos == npos) break; iter.remove_prefix(pos + 1); } return params; } bool Url::initVersion3(std::map ¶ms, celestia::compat::string_view timeStr) { m_version = 3; if (!astro::parseDate(std::string(timeStr), m_date)) return false; m_state.m_tdb = (double) m_date; if (params.count("x") == 0 || params.count("y") == 0 || params.count("z") == 0) return false; m_state.m_observerPosition = UniversalCoord(BigFix(params["x"]), BigFix(params["y"]), BigFix(params["z"])); float ow, ox, oy, oz; if (to_number(params["ow"], ow) && to_number(params["ox"], ox) && to_number(params["oy"], oy) && to_number(params["oz"], oz)) m_state.m_observerOrientation = Eigen::Quaternionf(ow, ox, oy, oz); else return false; if (params.count("select") != 0) m_state.m_selectedBodyName = params["select"]; if (params.count("track") != 0) m_state.m_trackedBodyName = params["track"]; if (params.count("ltd") != 0) m_state.m_lightTimeDelay = params["ltd"] != "0"; if (params.count("fov") != 0 && !to_number(params["fov"], m_state.m_fieldOfView)) return false; if (params.count("ts") != 0 && !to_number(params["ts"], m_state.m_timeScale)) return false; if (params.count("p") != 0) m_state.m_pauseState = params["p"] != "0"; // Render settings bool hasNewRenderFlags = false; uint64_t newFlags = 0ull, oldFlags = 0ull; if (params.count("nrf") != 0) { hasNewRenderFlags = true; int nrf; if (!to_number(params["nrf"], nrf)) return false; newFlags = static_cast(nrf) << 27; } if (params.count("rf") != 0) { // old renderer flags are int int rf; if (!to_number(params["rf"], rf)) return false; // older celestia versions don't know about the new renderer flags if (hasNewRenderFlags) { oldFlags = static_cast(rf & 0x04ffffff); // get actual Renderer::ShowPlanets value in 27th bit // clear ShowPlanets if 27th bit is unset if ((rf & (1<<27)) == 0) oldFlags &= ~Renderer::ShowPlanets; } else { oldFlags = static_cast(rf); // new options enabled by default in 1.7 oldFlags |= Renderer::ShowPlanetRings | Renderer::ShowFadingOrbits; // old ShowPlanets == new ShowSolarSystemObjects if ((oldFlags & Renderer::ShowPlanets) != 0) oldFlags |= Renderer::ShowSolarSystemObjects; } m_state.m_renderFlags = newFlags | oldFlags; } if (params.count("lm") != 0 && !to_number(params["lm"], m_state.m_labelMode)) return false; int tsrc = 0; if (params.count("tsrc") != 0 && !to_number(params["tsrc"], tsrc)) return false; if (tsrc >= 0 && tsrc < TimeSourceCount) m_timeSource = static_cast(tsrc); return true; } bool Url::initVersion4(std::map ¶ms, celestia::compat::string_view timeStr) { if (params.count("rf") != 0) { uint64_t rf; if (!to_number(params["rf"], rf)) return false; int nrf = rf >> 27; int _rf = rf & 0x07ffffff; if ((rf & Renderer::ShowPlanets) != 0) _rf |= (1 << 27); // Set the 27th bits to ShowPlanets params["nrf"] = std::to_string(nrf); params["rf"] = std::to_string(_rf); } return initVersion3(params, timeStr); } void Url::evalName() { std::string name; if (!m_state.m_refBodyName.empty()) name += fmt::sprintf(" %s", getBodyShortName(m_state.m_refBodyName)); if (!m_state.m_targetBodyName.empty()) name += fmt::sprintf(" %s", getBodyShortName(m_state.m_targetBodyName)); if (!m_state.m_trackedBodyName.empty()) name += fmt::sprintf(" -> %s", getBodyShortName(m_state.m_trackedBodyName)); if (!m_state.m_selectedBodyName.empty()) name += fmt::sprintf(" [%s]", getBodyShortName(m_state.m_selectedBodyName)); m_name = std::move(name); }