#include "glfw_view.hpp" #include "glfw_backend.hpp" #include "glfw_renderer_frontend.hpp" #include "ny_route.hpp" #include "test_writer.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if !defined(MBGL_LAYER_CUSTOM_DISABLE_ALL) #include "example_custom_drawable_style_layer.hpp" #endif #ifdef _MSC_VER #pragma warning(push) #pragma warning(disable : 4244) #pragma warning(disable : 4267) #endif #include #include #include #ifdef _MSC_VER #pragma warning(pop) #endif #if !defined(__APPLE__) #define GLFW_INCLUDE_ES3 #endif #if MLN_RENDER_BACKEND_VULKAN #define GLFW_INCLUDE_VULKAN #endif #define GL_GLEXT_PROTOTYPES #include #include #include #include #include #include #include #include using namespace std::numbers; #ifdef ENABLE_LOCATION_INDICATOR namespace { const std::string mbglPuckAssetsPath{MLN_ASSETS_PATH}; mln::Color premultiply(mln::Color c) { c.r *= c.a; c.g *= c.a; c.b *= c.a; return c; } std::array toArray(const mln::LatLng &crd) { return {crd.latitude(), crd.longitude(), 0}; } } // namespace #endif // ENABLE_LOCATION_INDICATOR class SnapshotObserver final : public mln::MapSnapshotterObserver { public: ~SnapshotObserver() override = default; void onDidFinishLoadingStyle() override { MLN_TRACE_FUNC(); if (didFinishLoadingStyleCallback) { didFinishLoadingStyleCallback(); } } std::function didFinishLoadingStyleCallback; }; namespace { enum class TileLodProfile { Default, // Default Tile LOD parameters NoLod, // Disable LOD Reduced, // Reduce LOD away from camera Aggressive, // Aggressively reduce LOD away from camera at the detriment of quality DistanceBased, // Use distance-based algorithm }; constexpr TileLodProfile nextTileLodProfile(TileLodProfile current) { switch (current) { case TileLodProfile::Default: return TileLodProfile::NoLod; case TileLodProfile::NoLod: return TileLodProfile::Reduced; case TileLodProfile::Reduced: return TileLodProfile::Aggressive; case TileLodProfile::Aggressive: return TileLodProfile::DistanceBased; case TileLodProfile::DistanceBased: return TileLodProfile::Default; default: return TileLodProfile::Default; } } void cycleTileLodProfile(mln::Map &map) { // TileLodProfile::Default parameters static const auto defaultRadius = map.getTileLodMinRadius(); static const auto defaultScale = map.getTileLodScale(); static const auto defaultTilePitchThreshold = map.getTileLodPitchThreshold(); static const auto defaultTileLodMode = map.getTileLodMode(); static TileLodProfile profile = TileLodProfile::Default; profile = nextTileLodProfile(profile); switch (profile) { case TileLodProfile::Default: map.setTileLodMinRadius(defaultRadius); map.setTileLodScale(defaultScale); map.setTileLodPitchThreshold(defaultTilePitchThreshold); map.setTileLodMode(defaultTileLodMode); mln::Log::Info(mln::Event::General, "Tile LOD profile: default"); break; case TileLodProfile::NoLod: // When LOD is off we set a maximum PitchThreshold map.setTileLodPitchThreshold(std::numbers::pi); mln::Log::Info(mln::Event::General, "Tile LOD profile: disabled"); break; case TileLodProfile::Reduced: map.setTileLodMinRadius(2); map.setTileLodScale(1.5); map.setTileLodPitchThreshold(std::numbers::pi / 4); mln::Log::Info(mln::Event::General, "Tile LOD profile: reduced"); break; case TileLodProfile::Aggressive: map.setTileLodMinRadius(1); map.setTileLodScale(2); map.setTileLodPitchThreshold(0); mln::Log::Info(mln::Event::General, "Tile LOD profile: aggressive"); break; case TileLodProfile::DistanceBased: map.setTileLodScale(1); map.setTileLodPitchThreshold(0); map.setTileLodMode(mln::TileLodMode::Distance); mln::Log::Info(mln::Event::General, "Tile LOD profile: distance-based"); break; } map.triggerRepaint(); } void tileLodZoomShift(mln::Map &map, bool positive) { constexpr auto tileLodZoomShiftStep = 0.25; auto shift = positive ? tileLodZoomShiftStep : -tileLodZoomShiftStep; shift = map.getTileLodZoomShift() + shift; shift = mln::util::clamp(shift, -2.5, 2.5); mln::Log::Info(mln::Event::OpenGL, "Zoom shift: " + std::to_string(shift)); map.setTileLodZoomShift(shift); map.triggerRepaint(); } void addFillExtrusionLayer(mln::style::Style &style, bool visible) { MLN_TRACE_FUNC(); using namespace mln::style; using namespace mln::style::expression::dsl; // Satellite-only style does not contain building extrusions data. if (!style.getSource("composite")) { return; } if (auto layer = style.getLayer("3d-buildings")) { layer->setVisibility(VisibilityType(!visible)); return; } auto extrusionLayer = std::make_unique("3d-buildings", "composite"); extrusionLayer->setSourceLayer("building"); extrusionLayer->setMinZoom(15.0f); extrusionLayer->setFilter(Filter(eq(get("extrude"), literal("true")))); extrusionLayer->setFillExtrusionColor(PropertyExpression(interpolate(linear(), number(get("height")), 0.f, toColor(literal("#160e23")), 50.f, toColor(literal("#00615f")), 100.f, toColor(literal("#55e9ff"))))); extrusionLayer->setFillExtrusionOpacity(0.6f); extrusionLayer->setFillExtrusionHeight(PropertyExpression(get("height"))); extrusionLayer->setFillExtrusionBase(PropertyExpression(get("min_height"))); style.addLayer(std::move(extrusionLayer)); } } // namespace void glfwError(int error, const char *description) { mln::Log::Error(mln::Event::OpenGL, std::string("GLFW error (") + std::to_string(error) + "): " + description); } GLFWView::GLFWView(bool fullscreen_, bool benchmark_, const mln::ResourceOptions &resourceOptions, const mln::ClientOptions &clientOptions) : fullscreen(fullscreen_), benchmark(benchmark_), snapshotterObserver(std::make_unique()), mapResourceOptions(resourceOptions.clone()), mapClientOptions(clientOptions.clone()) { MLN_TRACE_FUNC(); glfwSetErrorCallback(glfwError); std::srand(static_cast(std::time(nullptr))); #if defined(MLN_RENDER_BACKEND_WEBGPU) #ifdef __linux__ // Force X11 platform for WebGPU compatibility (Dawn doesn't support Wayland yet) // For now, always use X11 when WebGPU might be used glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11); #endif #endif if (!glfwInit()) { mln::Log::Error(mln::Event::OpenGL, "failed to initialize glfw"); exit(1); } GLFWmonitor *monitor = nullptr; if (fullscreen) { monitor = glfwGetPrimaryMonitor(); auto videoMode = glfwGetVideoMode(monitor); width = videoMode->width; height = videoMode->height; } #if __APPLE__ glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GL_TRUE); #endif if (mln::gfx::Backend::GetType() != mln::gfx::Backend::Type::OpenGL) { glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); } else { #if __APPLE__ glfwWindowHint(GLFW_COCOA_GRAPHICS_SWITCHING, GL_TRUE); #endif #if MBGL_WITH_EGL glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API); #endif #ifdef DEBUG glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE); #endif glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); glfwWindowHint(GLFW_RED_BITS, 8); glfwWindowHint(GLFW_GREEN_BITS, 8); glfwWindowHint(GLFW_BLUE_BITS, 8); glfwWindowHint(GLFW_ALPHA_BITS, 8); glfwWindowHint(GLFW_STENCIL_BITS, 8); glfwWindowHint(GLFW_DEPTH_BITS, 16); } window = glfwCreateWindow(width, height, "MapLibre Native", monitor, nullptr); if (!window) { glfwTerminate(); mln::Log::Error(mln::Event::OpenGL, "failed to initialize window"); exit(1); } glfwSetWindowUserPointer(window, this); glfwSetCursorPosCallback(window, onMouseMove); glfwSetMouseButtonCallback(window, onMouseClick); glfwSetWindowSizeCallback(window, onWindowResize); glfwSetFramebufferSizeCallback(window, onFramebufferResize); glfwSetScrollCallback(window, onScroll); glfwSetKeyCallback(window, onKey); glfwSetWindowFocusCallback(window, onWindowFocus); #if defined(__APPLE__) && defined(MLN_RENDER_BACKEND_VULKAN) glfwSetWindowRefreshCallback(window, onWindowRefresh); #endif glfwGetWindowSize(window, &width, &height); bool capFrameRate = !benchmark; // disable VSync in benchmark mode backend = GLFWBackend::Create(window, capFrameRate); #if defined(__APPLE__) && !defined(MLN_RENDER_BACKEND_VULKAN) int fbW, fbH; glfwGetFramebufferSize(window, &fbW, &fbH); backend->setFramebufferSize({static_cast(fbW), static_cast(fbH)}); #endif pixelRatio = static_cast(backend->getFramebufferSize().width) / width; glfwMakeContextCurrent(nullptr); printf("\n"); printf( "======================================================================" "==========\n"); printf("\n"); printf("- Press `S` to cycle through bundled styles\n"); printf("- Press `X` to reset the transform\n"); printf("- Press `N` to reset north\n"); printf("- Press `R` to enable the route demo\n"); printf("- Press `E` to insert an example building extrusion layer\n"); printf("- Press `O` to toggle online connectivity\n"); printf("- Press `Z` to cycle through north orientations\n"); printf("- Press `X` to cycle through the viewport modes\n"); printf("- Press `I` to delete existing database and re-initialize\n"); printf( "- Press `A` to cycle through Mapbox offices in the world + dateline " "monument\n"); printf("- Press `B` to cycle through the color, stencil, and depth buffer\n"); printf( "- Press `D` to cycle through camera bounds: inside, crossing IDL at " "left, crossing IDL at right, and " "disabled\n"); printf("- Press `T` to add custom geometry source\n"); printf("- Press `F` to enable feature-state demo\n"); printf("- Press `U` to toggle pitch bounds\n"); printf("- Press `H` to take a snapshot of a current map.\n"); printf( "- Press `J` to take a snapshot of a current map with an extrusions " "overlay.\n"); printf("- Press `Y` to start a camera fly-by demo\n"); printf("\n"); printf( "- Press `1` through `6` to add increasing numbers of point " "annotations for testing\n"); printf( "- Press `7` through `0` to add increasing numbers of shape " "annotations for testing\n"); printf("\n"); printf("- Press `Q` to query annotations\n"); printf("- Press `C` to remove annotations\n"); printf("- Press `K` to add a random custom runtime imagery annotation\n"); printf("- Press `L` to add a random line annotation\n"); printf("- Press `W` to pop the last-added annotation off\n"); printf("- Press `V` to toggle custom drawable layer\n"); printf("- Press `B` to toggle rendering stats\n"); printf("- Press `P` to pause tile requests\n"); printf("\n"); printf("- Hold `Control` + mouse drag to rotate\n"); printf("- Hold `Shift` + mouse drag to tilt\n"); printf("\n"); printf("- Press `F1` to generate a render test for the current view\n"); printf("\n"); printf("- Press `Tab` to cycle through the map debug options\n"); printf("- Press `F6` to cycle through Tile LOD modes\n"); printf("- Press `F7` to lower the zoom level without changing the camera\n"); printf("- Press `F8` to higher the zoom level without changing the camera\n"); printf("- Press `Esc` to quit\n"); printf("\n"); printf( "======================================================================" "==========\n"); printf("\n"); } GLFWView::~GLFWView() { MLN_TRACE_FUNC(); glfwDestroyWindow(window); glfwTerminate(); } void GLFWView::setMap(mln::Map *map_) { MLN_TRACE_FUNC(); map = map_; map->addAnnotationImage(makeImage("default_marker", 22, 22, 1)); } void GLFWView::setRenderFrontend(GLFWRendererFrontend *rendererFrontend_) { MLN_TRACE_FUNC(); rendererFrontend = rendererFrontend_; } mln::gfx::RendererBackend &GLFWView::getRendererBackend() { MLN_TRACE_FUNC(); return backend->getRendererBackend(); } void GLFWView::onKey(GLFWwindow *window, int key, int /*scancode*/, int action, int mods) { MLN_TRACE_FUNC(); auto *view = reinterpret_cast(glfwGetWindowUserPointer(window)); if (action == GLFW_RELEASE) { if (key != GLFW_KEY_R || key != GLFW_KEY_S) view->animateRouteCallback = nullptr; switch (key) { case GLFW_KEY_ESCAPE: glfwSetWindowShouldClose(window, true); break; case GLFW_KEY_TAB: view->cycleDebugOptions(); break; case GLFW_KEY_X: if (!mods) view->map->jumpTo( mln::CameraOptions().withCenter(mln::LatLng{}).withZoom(0.0).withBearing(0.0).withPitch(0.0)); break; case GLFW_KEY_O: view->onlineStatusCallback(); break; case GLFW_KEY_S: if (view->changeStyleCallback) view->changeStyleCallback(); break; case GLFW_KEY_N: if (!mods) view->map->easeTo(mln::CameraOptions().withBearing(0.0), mln::AnimationOptions{{mln::Milliseconds(500)}}); break; case GLFW_KEY_Z: view->nextOrientation(); break; case GLFW_KEY_Q: { auto result = view->rendererFrontend->getRenderer()->queryPointAnnotations( {{}, {static_cast(view->getSize().width), static_cast(view->getSize().height)}}); printf("visible point annotations: %zu\n", result.size()); auto features = view->rendererFrontend->getRenderer()->queryRenderedFeatures( mln::ScreenBox{{view->getSize().width * 0.5, view->getSize().height * 0.5}, {view->getSize().width * 0.5 + 1.0, view->getSize().height * 0.5 + 1}}, {}); printf("Rendered features at the center of the screen: %zu\n", features.size()); } break; case GLFW_KEY_P: view->pauseResumeCallback(); break; case GLFW_KEY_C: view->clearAnnotations(); break; case GLFW_KEY_V: view->toggleCustomDrawableStyle(); break; case GLFW_KEY_B: view->map->enableRenderingStatsView(!view->map->isRenderingStatsViewEnabled()); break; case GLFW_KEY_I: view->resetDatabaseCallback(); break; case GLFW_KEY_K: view->addRandomCustomPointAnnotations(1); break; case GLFW_KEY_L: view->addRandomLineAnnotations(1); break; case GLFW_KEY_A: { // XXX Fix precision loss in flyTo: // https://github.com/mapbox/mapbox-gl-native/issues/4298 static const std::vector places = { mln::LatLng{-16.796665, -179.999983}, // Dateline monument mln::LatLng{12.9810542, 77.6345551}, // Mapbox Bengaluru, India mln::LatLng{-13.15607, -74.21773}, // Mapbox Peru mln::LatLng{37.77572, -122.4158818}, // Mapbox SF, USA mln::LatLng{38.91318, -77.03255}, // Mapbox DC, USA }; static size_t nextPlace = 0; mln::CameraOptions cameraOptions; cameraOptions.center = places[nextPlace++]; cameraOptions.zoom = 20; cameraOptions.pitch = 30; mln::AnimationOptions animationOptions(mln::Seconds(10)); view->map->flyTo(cameraOptions, animationOptions); nextPlace = nextPlace % places.size(); } break; case GLFW_KEY_R: { view->show3DExtrusions = true; view->toggle3DExtrusions(view->show3DExtrusions); if (view->animateRouteCallback) break; view->animateRouteCallback = [](mln::Map *routeMap) { static mapbox::cheap_ruler::CheapRuler ruler{40.7}; // New York static mapbox::geojson::geojson route{mapbox::geojson::parse(mln::platform::glfw::route)}; const auto &geometry = route.get>(); const auto &lineString = geometry.get>(); static double routeDistance = ruler.lineDistance(lineString); static double routeProgress = 0; routeProgress += 0.0005; if (routeProgress > 1.0) { routeProgress = 0.0; } auto camera = routeMap->getCameraOptions(); auto point = ruler.along(lineString, routeProgress * routeDistance); const mln::LatLng center{point.y, point.x}; auto latLng = *camera.center; double bearing = ruler.bearing({latLng.longitude(), latLng.latitude()}, point); double easing = bearing - *camera.bearing; easing += easing > 180.0 ? -360.0 : easing < -180 ? 360.0 : 0; bearing = *camera.bearing + (easing / 20); routeMap->jumpTo( mln::CameraOptions().withCenter(center).withZoom(18.0).withBearing(bearing).withPitch(60.0)); }; view->animateRouteCallback(view->map); } break; case GLFW_KEY_E: view->toggle3DExtrusions(!view->show3DExtrusions); break; case GLFW_KEY_D: { static const std::vector bounds = { mln::LatLngBounds::hull(mln::LatLng{-45.0, -170.0}, mln::LatLng{45.0, 170.0}), // inside mln::LatLngBounds::hull(mln::LatLng{-45.0, -200.0}, mln::LatLng{45.0, -160.0}), // left IDL mln::LatLngBounds::hull(mln::LatLng{-45.0, 160.0}, mln::LatLng{45.0, 200.0}), // right IDL mln::LatLngBounds()}; static size_t nextBound = 0u; static mln::AnnotationID boundAnnotationID = std::numeric_limits::max(); mln::LatLngBounds bound = bounds[nextBound++]; nextBound = nextBound % bounds.size(); view->map->setBounds(mln::BoundOptions().withLatLngBounds(bound)); if (bound == mln::LatLngBounds()) { view->map->removeAnnotation(boundAnnotationID); boundAnnotationID = std::numeric_limits::max(); } else { mln::Polygon rect; rect.push_back({ mln::Point{bound.west(), bound.north()}, mln::Point{bound.east(), bound.north()}, mln::Point{bound.east(), bound.south()}, mln::Point{bound.west(), bound.south()}, }); auto boundAnnotation = mln::FillAnnotation{ rect, 0.5f, {view->makeRandomColor()}, {view->makeRandomColor()}}; if (boundAnnotationID == std::numeric_limits::max()) { boundAnnotationID = view->map->addAnnotation(boundAnnotation); } else { view->map->updateAnnotation(boundAnnotationID, boundAnnotation); } } } break; case GLFW_KEY_T: view->toggleCustomSource(); break; case GLFW_KEY_F: { using namespace mln; using namespace mln::style; using namespace mln::style::expression::dsl; auto &style = view->map->getStyle(); if (!style.getSource("states")) { std::string url = "https://maplibre.org/maplibre-gl-js/docs/assets/us_states.geojson"; auto source = std::make_unique("states"); source->setURL(url); style.addSource(std::move(source)); mln::CameraOptions cameraOptions; cameraOptions.center = mln::LatLng{42.619626, -103.523181}; cameraOptions.zoom = 3; cameraOptions.pitch = 0; cameraOptions.bearing = 0; view->map->jumpTo(cameraOptions); } auto layer = style.getLayer("state-fills"); if (!layer) { auto fillLayer = std::make_unique("state-fills", "states"); fillLayer->setFillColor(mln::Color{0.0, 0.0, 1.0, 0.5}); fillLayer->setFillOpacity(PropertyExpression( createExpression(R"(["case", ["boolean", ["feature-state", "hover"], false], 1, 0.5])"))); style.addLayer(std::move(fillLayer)); } else { layer->setVisibility(layer->getVisibility() == mln::style::VisibilityType::Visible ? mln::style::VisibilityType::None : mln::style::VisibilityType::Visible); } layer = style.getLayer("state-borders"); if (!layer) { auto borderLayer = std::make_unique("state-borders", "states"); borderLayer->setLineColor(mln::Color{0.0, 0.0, 1.0, 1.0}); borderLayer->setLineWidth(PropertyExpression( createExpression(R"(["case", ["boolean", ["feature-state", "hover"], false], 2, 1])"))); style.addLayer(std::move(borderLayer)); } else { layer->setVisibility(layer->getVisibility() == mln::style::VisibilityType::Visible ? mln::style::VisibilityType::None : mln::style::VisibilityType::Visible); } } break; case GLFW_KEY_F1: { bool success = TestWriter() .withInitialSize(mln::Size(view->width, view->height)) .withStyle(view->map->getStyle()) .withCameraOptions(view->map->getCameraOptions()) .write(view->testDirectory); if (success) { mln::Log::Info(mln::Event::General, "Render test created!"); } else { mln::Log::Error(mln::Event::General, "Fail to create render test! Base directory does not " "exist or permission denied."); } } break; case GLFW_KEY_U: { auto bounds = view->map->getBounds(); if (bounds.minPitch == mln::util::rad2deg(mln::util::PITCH_MIN) && bounds.maxPitch == mln::util::rad2deg(mln::util::PITCH_MAX)) { mln::Log::Info(mln::Event::General, "Limiting pitch bounds to [30, 40] degrees"); view->map->setBounds(mln::BoundOptions().withMinPitch(30).withMaxPitch(40)); } else { mln::Log::Info(mln::Event::General, "Resetting pitch bounds to [0, 60] degrees"); view->map->setBounds(mln::BoundOptions().withMinPitch(0).withMaxPitch(60)); } } break; case GLFW_KEY_H: { view->makeSnapshot(); } break; case GLFW_KEY_J: { // Snapshot with overlay view->makeSnapshot(true); } break; case GLFW_KEY_G: { view->toggleLocationIndicatorLayer(); } break; case GLFW_KEY_Y: { view->freeCameraDemoPhase = 0; view->freeCameraDemoStartTime = mln::Clock::now(); view->invalidate(); } break; case GLFW_KEY_F6: { cycleTileLodProfile(*view->map); } break; case GLFW_KEY_F7: { tileLodZoomShift(*view->map, false); } break; case GLFW_KEY_F8: { tileLodZoomShift(*view->map, true); } break; } } if (action == GLFW_RELEASE || action == GLFW_REPEAT) { switch (key) { case GLFW_KEY_W: view->popAnnotation(); break; case GLFW_KEY_1: view->addRandomPointAnnotations(1); break; case GLFW_KEY_2: view->addRandomPointAnnotations(10); break; case GLFW_KEY_3: view->addRandomPointAnnotations(100); break; case GLFW_KEY_4: view->addRandomPointAnnotations(1000); break; case GLFW_KEY_5: view->addRandomPointAnnotations(10000); break; case GLFW_KEY_6: view->addRandomPointAnnotations(100000); break; case GLFW_KEY_7: view->addRandomShapeAnnotations(1); break; case GLFW_KEY_8: view->addRandomShapeAnnotations(10); break; case GLFW_KEY_9: view->addRandomShapeAnnotations(100); break; case GLFW_KEY_0: view->addRandomShapeAnnotations(1000); break; case GLFW_KEY_M: view->addAnimatedAnnotation(); break; } } } namespace mln { namespace util { template <> struct Interpolator { mln::LatLng operator()(const mln::LatLng &a, const mln::LatLng &b, const double t) { return { interpolate(a.latitude(), b.latitude(), t), interpolate(a.longitude(), b.longitude(), t), }; } }; } // namespace util } // namespace mln void GLFWView::updateFreeCameraDemo() { MLN_TRACE_FUNC(); const mln::LatLng trainStartPos = {60.171367, 24.941359}; const mln::LatLng trainEndPos = {60.185147, 24.936668}; const mln::LatLng cameraStartPos = {60.167443, 24.927176}; const mln::LatLng cameraEndPos = {60.185107, 24.933366}; const double cameraStartAlt = 1000.0; const double cameraEndAlt = 150.0; const double duration = 8.0; // Interpolate between starting and ending points std::chrono::duration deltaTime = mln::Clock::now() - freeCameraDemoStartTime; freeCameraDemoPhase = deltaTime.count() / duration; auto trainPos = mln::util::interpolate(trainStartPos, trainEndPos, freeCameraDemoPhase); auto cameraPos = mln::util::interpolate(cameraStartPos, cameraEndPos, freeCameraDemoPhase); auto cameraAlt = mln::util::interpolate(cameraStartAlt, cameraEndAlt, freeCameraDemoPhase); mln::FreeCameraOptions camera; // Update camera position and focus point on the map with interpolated values camera.setLocation({cameraPos, cameraAlt}); camera.lookAtPoint(trainPos); map->setFreeCameraOptions(camera); if (freeCameraDemoPhase > 1.0) { freeCameraDemoPhase = -1.0; } } mln::Color GLFWView::makeRandomColor() const { const auto r = static_cast(std::rand()) / static_cast(RAND_MAX); const auto g = static_cast(std::rand()) / static_cast(RAND_MAX); const auto b = static_cast(std::rand()) / static_cast(RAND_MAX); return {r, g, b, 1.0f}; } mln::Point GLFWView::makeRandomPoint() const { const double x = width * static_cast(std::rand()) / RAND_MAX; const double y = height * static_cast(std::rand()) / RAND_MAX; mln::LatLng latLng = map->latLngForPixel({x, y}); return {latLng.longitude(), latLng.latitude()}; } std::unique_ptr GLFWView::makeImage(const std::string &id, int width, int height, float pixelRatio) { MLN_TRACE_FUNC(); const int r = static_cast(255 * (static_cast(std::rand()) / RAND_MAX)); const int g = static_cast(255 * (static_cast(std::rand()) / RAND_MAX)); const int b = static_cast(255 * (static_cast(std::rand()) / RAND_MAX)); const int w = static_cast(std::ceil(pixelRatio * width)); const int h = static_cast(std::ceil(pixelRatio * height)); mln::PremultipliedImage image({static_cast(w), static_cast(h)}); auto data = reinterpret_cast(image.data.get()); const int dist = (w / 2) * (w / 2); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { const int dx = x - w / 2; const int dy = y - h / 2; const int diff = dist - (dx * dx + dy * dy); if (diff > 0) { const int a = std::min(0xFF, diff) * 0xFF / dist; // Premultiply the rgb values with alpha data[w * y + x] = (a << 24) | ((a * r / 0xFF) << 16) | ((a * g / 0xFF) << 8) | (a * b / 0xFF); } } } return std::make_unique(id, std::move(image), pixelRatio); } void GLFWView::nextOrientation() { using NO = mln::NorthOrientation; switch (map->getMapOptions().northOrientation()) { case NO::Upwards: map->setNorthOrientation(NO::Rightwards); break; case NO::Rightwards: map->setNorthOrientation(NO::Downwards); break; case NO::Downwards: map->setNorthOrientation(NO::Leftwards); break; default: map->setNorthOrientation(NO::Upwards); break; } } void GLFWView::addRandomCustomPointAnnotations(int count) { MLN_TRACE_FUNC(); for (int i = 0; i < count; i++) { static int spriteID = 1; const auto name = std::string{"marker-"} + mln::util::toString(spriteID++); map->addAnnotationImage(makeImage(name, 22, 22, 1)); spriteIDs.push_back(name); annotationIDs.push_back(map->addAnnotation(mln::SymbolAnnotation{makeRandomPoint(), name})); } } void GLFWView::addRandomPointAnnotations(int count) { MLN_TRACE_FUNC(); for (int i = 0; i < count; ++i) { annotationIDs.push_back(map->addAnnotation(mln::SymbolAnnotation{makeRandomPoint(), "default_marker"})); } } void GLFWView::addRandomLineAnnotations(int count) { MLN_TRACE_FUNC(); for (int i = 0; i < count; ++i) { mln::LineString lineString; for (int j = 0; j < 3; ++j) { lineString.push_back(makeRandomPoint()); } annotationIDs.push_back(map->addAnnotation(mln::LineAnnotation{lineString, 1.0f, 2.0f, {makeRandomColor()}})); } } void GLFWView::addRandomShapeAnnotations(int count) { MLN_TRACE_FUNC(); for (int i = 0; i < count; ++i) { mln::Polygon triangle; triangle.push_back({makeRandomPoint(), makeRandomPoint(), makeRandomPoint()}); annotationIDs.push_back( map->addAnnotation(mln::FillAnnotation{triangle, 0.5f, {makeRandomColor()}, {makeRandomColor()}})); } } void GLFWView::addAnimatedAnnotation() { MLN_TRACE_FUNC(); const double started = glfwGetTime(); animatedAnnotationIDs.push_back(map->addAnnotation(mln::SymbolAnnotation{{0, 0}, "default_marker"})); animatedAnnotationAddedTimes.push_back(started); } void GLFWView::updateAnimatedAnnotations() { MLN_TRACE_FUNC(); const double time = glfwGetTime(); for (size_t i = 0; i < animatedAnnotationIDs.size(); i++) { auto dt = time - animatedAnnotationAddedTimes[i]; const double period = 10; const double x = dt / period * 360 - 180; const double y = std::sin(dt / period * pi * 2.0) * 80; map->updateAnnotation(animatedAnnotationIDs[i], mln::SymbolAnnotation{{x, y}, "default_marker"}); } } void GLFWView::cycleDebugOptions() { auto debug = map->getDebug(); if (debug & mln::MapDebugOptions::Overdraw) debug = mln::MapDebugOptions::NoDebug; else if (debug & mln::MapDebugOptions::Collision) debug = mln::MapDebugOptions::Overdraw; else if (debug & mln::MapDebugOptions::Timestamps) debug = debug | mln::MapDebugOptions::Collision; else if (debug & mln::MapDebugOptions::ParseStatus) debug = debug | mln::MapDebugOptions::Timestamps; else if (debug & mln::MapDebugOptions::TileBorders) debug = debug | mln::MapDebugOptions::ParseStatus; else debug = mln::MapDebugOptions::TileBorders; map->setDebug(debug); } void GLFWView::clearAnnotations() { MLN_TRACE_FUNC(); for (const auto &id : annotationIDs) { map->removeAnnotation(id); } annotationIDs.clear(); for (const auto &id : animatedAnnotationIDs) { map->removeAnnotation(id); } animatedAnnotationIDs.clear(); } void GLFWView::popAnnotation() { if (annotationIDs.empty()) { return; } map->removeAnnotation(annotationIDs.back()); annotationIDs.pop_back(); } void GLFWView::toggleCustomDrawableStyle() { #if !defined(MBGL_LAYER_CUSTOM_DISABLE_ALL) auto &style = map->getStyle(); const std::string identifier = "ExampleCustomDrawableStyleLayer"; const auto &existingLayer = style.getLayer(identifier); if (!existingLayer) { style.addLayer(std::make_unique( identifier, std::make_unique(MLN_ASSETS_PATH))); } else { style.removeLayer(identifier); } #endif } void GLFWView::makeSnapshot(bool withOverlay) { MLN_TRACE_FUNC(); if (!snapshotter || snapshotter->getStyleURL() != map->getStyle().getURL()) { snapshotter = std::make_unique(map->getMapOptions().size(), map->getMapOptions().pixelRatio(), mapResourceOptions, mapClientOptions, *snapshotterObserver); snapshotter->setStyleURL(map->getStyle().getURL()); } auto snapshot = [&] { snapshotter->setCameraOptions(map->getCameraOptions()); snapshotter->snapshot([](const std::exception_ptr &ptr, mln::PremultipliedImage image, const mln::MapSnapshotter::Attributions &, const mln::MapSnapshotter::PointForFn &, const mln::MapSnapshotter::LatLngForFn &) { if (!ptr) { std::ostringstream oss; oss << "Made snapshot './snapshot.png' with size w:" << image.size.width << "px h:" << image.size.height << "px"; mln::Log::Info(mln::Event::General, oss.str()); std::ofstream file("./snapshot.png", std::ios::out | std::ios::binary); file << mln::encodePNG(image); } else { mln::Log::Error(mln::Event::General, "Failed to make a snapshot!"); } }); }; if (withOverlay) { snapshotterObserver->didFinishLoadingStyleCallback = [&] { addFillExtrusionLayer(snapshotter->getStyle(), withOverlay); snapshot(); }; } else { snapshot(); } } void GLFWView::onScroll(GLFWwindow *window, double /*xOffset*/, double yOffset) { MLN_TRACE_FUNC(); auto *view = reinterpret_cast(glfwGetWindowUserPointer(window)); double delta = yOffset * 40; bool isWheel = delta != 0 && std::fmod(delta, 4.000244140625) == 0; double absDelta = delta < 0 ? -delta : delta; double scale = 2.0 / (1.0 + std::exp(-absDelta / 100.0)); // Make the scroll wheel a bit slower. if (!isWheel) { scale = (scale - 1.0) / 2.0 + 1.0; } // Zooming out. if (delta < 0 && scale != 0) { scale = 1.0 / scale; } view->map->scaleBy(scale, mln::ScreenCoordinate{view->lastX, view->lastY}); #if defined(MLN_RENDER_BACKEND_OPENGL) && !defined(MBGL_LAYER_CUSTOM_DISABLE_ALL) if (view->puck && view->puckFollowsCameraCenter) { mln::LatLng mapCenter = view->map->getCameraOptions().center.value(); view->puck->setLocation(toArray(mapCenter)); } #endif } void GLFWView::onWindowResize(GLFWwindow *window, int width, int height) { MLN_TRACE_FUNC(); auto *view = reinterpret_cast(glfwGetWindowUserPointer(window)); view->width = width; view->height = height; view->map->setSize({static_cast(view->width), static_cast(view->height)}); #ifdef __APPLE__ int fbW, fbH; glfwGetFramebufferSize(window, &fbW, &fbH); view->backend->setFramebufferSize({static_cast(fbW), static_cast(fbH)}); #endif } void GLFWView::onFramebufferResize(GLFWwindow *window, int width, int height) { MLN_TRACE_FUNC(); auto *view = reinterpret_cast(glfwGetWindowUserPointer(window)); view->backend->setFramebufferSize({static_cast(width), static_cast(height)}); // This is only triggered when the framebuffer is resized, but not the // window. It can happen when you move the window between screens with a // different pixel ratio. We are forcing a repaint my invalidating the view, // which triggers a rerender with the new framebuffer dimensions. view->invalidate(); #if defined(__APPLE__) && defined(MLN_RENDER_BACKEND_VULKAN) // Render continuously while resizing // Untested elsewhere view->render(); #if defined(MLN_RENDER_BACKEND_OPENGL) glfwSwapBuffers(window); #endif #endif } void GLFWView::onMouseClick(GLFWwindow *window, int button, int action, int modifiers) { MLN_TRACE_FUNC(); auto *view = reinterpret_cast(glfwGetWindowUserPointer(window)); if (button == GLFW_MOUSE_BUTTON_RIGHT || (button == GLFW_MOUSE_BUTTON_LEFT && modifiers & GLFW_MOD_CONTROL)) { view->rotating = action == GLFW_PRESS; view->map->setGestureInProgress(view->rotating); } else if (button == GLFW_MOUSE_BUTTON_LEFT && (modifiers & GLFW_MOD_SHIFT)) { view->pitching = action == GLFW_PRESS; view->map->setGestureInProgress(view->pitching); } else if (button == GLFW_MOUSE_BUTTON_LEFT) { view->tracking = action == GLFW_PRESS; view->map->setGestureInProgress(view->tracking); if (action == GLFW_RELEASE) { double now = glfwGetTime(); if (now - view->lastClick < 0.4 /* ms */) { if (modifiers & GLFW_MOD_SHIFT) { view->map->scaleBy(0.5, mln::ScreenCoordinate{view->lastX, view->lastY}, mln::AnimationOptions{{mln::Milliseconds(500)}}); } else { view->map->scaleBy(2.0, mln::ScreenCoordinate{view->lastX, view->lastY}, mln::AnimationOptions{{mln::Milliseconds(500)}}); } } view->lastClick = now; } } } void GLFWView::onMouseMove(GLFWwindow *window, double x, double y) { MLN_TRACE_FUNC(); auto *view = reinterpret_cast(glfwGetWindowUserPointer(window)); if (view->tracking) { const double dx = x - view->lastX; const double dy = y - view->lastY; if (dx || dy) { view->map->moveBy(mln::ScreenCoordinate{dx, dy}); } } else if (view->rotating) { view->map->rotateBy({view->lastX, view->lastY}, {x, y}); } else if (view->pitching) { const double dy = y - view->lastY; if (dy) { view->map->pitchBy(dy / 2); } } view->lastX = x; view->lastY = y; #if defined(MLN_RENDER_BACKEND_OPENGL) && !defined(MBGL_LAYER_CUSTOM_DISABLE_ALL) if (view->puck && view->puckFollowsCameraCenter) { mln::LatLng mapCenter = view->map->getCameraOptions().center.value(); view->puck->setLocation(toArray(mapCenter)); } #endif auto &style = view->map->getStyle(); if (style.getLayer("state-fills")) { auto screenCoordinate = mln::ScreenCoordinate{view->lastX, view->lastY}; const mln::RenderedQueryOptions queryOptions({{{"state-fills"}}, {}}); auto result = view->rendererFrontend->getRenderer()->queryRenderedFeatures(screenCoordinate, queryOptions); using namespace mln; FeatureState newState; if (!result.empty()) { FeatureIdentifier id = result[0].id; std::optional idStr = featureIDtoString(id); if (idStr) { if (view->featureID && (*view->featureID != *idStr)) { newState["hover"] = false; view->rendererFrontend->getRenderer()->setFeatureState("states", {}, *view->featureID, newState); view->featureID = std::nullopt; } if (!view->featureID) { newState["hover"] = true; view->featureID = featureIDtoString(id); view->rendererFrontend->getRenderer()->setFeatureState("states", {}, *view->featureID, newState); } } } else { if (view->featureID) { newState["hover"] = false; view->rendererFrontend->getRenderer()->setFeatureState("states", {}, *view->featureID, newState); view->featureID = std::nullopt; } } view->invalidate(); } } void GLFWView::onWindowFocus(GLFWwindow *window, int focused) { MLN_TRACE_FUNC(); if (focused == GLFW_FALSE) { // Focus lost. auto *view = reinterpret_cast(glfwGetWindowUserPointer(window)); view->rendererFrontend->getRenderer()->reduceMemoryUse(); } } #if defined(__APPLE__) && defined(MLN_RENDER_BACKEND_VULKAN) void GLFWView::onWindowRefresh(GLFWwindow *window) { // Untested elsewhere if (auto *view = reinterpret_cast(glfwGetWindowUserPointer(window))) { view->invalidate(); view->render(); #if defined(MLN_RENDER_BACKEND_OPENGL) glfwSwapBuffers(window); #endif } } #endif void GLFWView::render() { if (dirty && rendererFrontend) { MLN_TRACE_ZONE(ReRender); dirty = false; const double started = glfwGetTime(); if (animateRouteCallback) { MLN_TRACE_ZONE(animateRouteCallback); animateRouteCallback(map); } updateAnimatedAnnotations(); mln::gfx::BackendScope scope{backend->getRendererBackend()}; rendererFrontend->render(); if (freeCameraDemoPhase >= 0.0) { updateFreeCameraDemo(); } report(static_cast(1000 * (glfwGetTime() - started))); if (benchmark) { invalidate(); } } } void GLFWView::run() { MLN_TRACE_FUNC(); auto callback = [&] { MLN_TRACE_ZONE(GLFWView_runLoop_callback); { MLN_TRACE_ZONE(glfwWindowShouldClose); if (glfwWindowShouldClose(window)) { runLoop.stop(); return; } } { MLN_TRACE_ZONE(glfwPollEvents); glfwPollEvents(); } render(); #ifndef __APPLE__ runLoop.updateTime(); #endif }; // Cap frame rate to 60hz if benchmark mode is disabled auto tickDuration = mln::Milliseconds(1000 / 60); if (benchmark) { // frameTick.start internally uses libuv which uses milliseconds resolution // tickDuration is set to 1ms in benchmark mode which limits FPS to 1000 // 1000 should more than enough for benchmarking purposes tickDuration = mln::Milliseconds(1); } frameTick.start(mln::Duration::zero(), tickDuration, callback); #if defined(__APPLE__) while (!glfwWindowShouldClose(window)) runLoop.run(); #else runLoop.run(); #endif } float GLFWView::getPixelRatio() const { return pixelRatio; } mln::Size GLFWView::getSize() const { return {static_cast(width), static_cast(height)}; } void GLFWView::invalidate() { MLN_TRACE_FUNC(); dirty = true; glfwPostEmptyEvent(); } void GLFWView::report(float duration) { frames++; frameTime += duration; const double currentTime = glfwGetTime(); if (currentTime - lastReported >= 1) { frameTime /= frames; std::ostringstream oss; oss.precision(2); oss << "Frame time: " << std::fixed << frameTime << "ms (" << 1000 / frameTime << "fps)"; mln::Log::Info(mln::Event::Render, oss.str()); frames = 0; frameTime = 0; lastReported = currentTime; } } void GLFWView::setChangeStyleCallback(std::function callback) { changeStyleCallback = std::move(callback); } void GLFWView::setShouldClose() { MLN_TRACE_FUNC(); glfwSetWindowShouldClose(window, true); glfwPostEmptyEvent(); } void GLFWView::setWindowTitle(const std::string &title) { MLN_TRACE_FUNC(); glfwSetWindowTitle(window, (std::string{"MapLibre Native (GLFW): "} + title).c_str()); } void GLFWView::onDidFinishLoadingStyle() { MLN_TRACE_FUNC(); #if defined(MLN_RENDER_BACKEND_OPENGL) && !defined(MBGL_LAYER_CUSTOM_DISABLE_ALL) puck = nullptr; #endif if (show3DExtrusions) { toggle3DExtrusions(show3DExtrusions); } } void GLFWView::toggle3DExtrusions(bool visible) { MLN_TRACE_FUNC(); show3DExtrusions = visible; addFillExtrusionLayer(map->getStyle(), show3DExtrusions); } void GLFWView::toggleCustomSource() { MLN_TRACE_FUNC(); if (!map->getStyle().getSource("custom")) { mln::style::CustomGeometrySource::Options options; options.cancelTileFunction = [](const mln::CanonicalTileID &) { }; options.fetchTileFunction = [&](const mln::CanonicalTileID &tileID) { double gridSpacing = 0.1; mln::FeatureCollection features; const mln::LatLngBounds bounds(tileID); for (double y = ceil(bounds.north() / gridSpacing) * gridSpacing; y >= floor(bounds.south() / gridSpacing) * gridSpacing; y -= gridSpacing) { mapbox::geojson::line_string gridLine; gridLine.emplace_back(bounds.west(), y); gridLine.emplace_back(bounds.east(), y); features.emplace_back(gridLine); } for (double x = floor(bounds.west() / gridSpacing) * gridSpacing; x <= ceil(bounds.east() / gridSpacing) * gridSpacing; x += gridSpacing) { mapbox::geojson::line_string gridLine; gridLine.emplace_back(x, bounds.south()); gridLine.emplace_back(x, bounds.north()); features.emplace_back(gridLine); } auto source = static_cast(map->getStyle().getSource("custom")); if (source) { source->setTileData(tileID, features); source->invalidateTile(tileID); } }; map->getStyle().addSource(std::make_unique("custom", options)); } auto *layer = map->getStyle().getLayer("grid"); if (!layer) { auto lineLayer = std::make_unique("grid", "custom"); lineLayer->setLineColor(mln::Color{1.0, 0.0, 0.0, 1.0}); map->getStyle().addLayer(std::move(lineLayer)); } else { layer->setVisibility(layer->getVisibility() == mln::style::VisibilityType::Visible ? mln::style::VisibilityType::None : mln::style::VisibilityType::Visible); } } void GLFWView::toggleLocationIndicatorLayer() { MLN_TRACE_FUNC(); #ifdef ENABLE_LOCATION_INDICATOR puck = static_cast(map->getStyle().getLayer("puck")); static const mln::LatLng puckLocation{35.683389, 139.76525}; // A location on the crossing of 4 tiles if (puck == nullptr) { auto puckLayer = std::make_unique("puck"); puckLayer->setLocationTransition( mln::style::TransitionOptions(mln::Duration::zero(), mln::Duration::zero())); // Note: This is used here for demo purpose. // SDKs should not use this, or else the location // will "jump" to positions. puckLayer->setLocation(toArray(puckLocation)); puckLayer->setAccuracyRadius(50); puckLayer->setAccuracyRadiusColor( premultiply(mln::Color{0.0f, 1.0f, 0.0f, 0.2f})); // Note: these must be fed premultiplied puckLayer->setBearingTransition(mln::style::TransitionOptions(mln::Duration::zero(), mln::Duration::zero())); puckLayer->setBearing(mln::style::Rotation(0.0)); puckLayer->setAccuracyRadiusBorderColor(premultiply(mln::Color{0.0f, 1.0f, 0.2f, 0.4f})); puckLayer->setTopImageSize(0.18f); puckLayer->setBearingImageSize(0.26f); puckLayer->setShadowImageSize(0.2f); puckLayer->setImageTiltDisplacement(7.0f); // set to 0 for a "flat" puck puckLayer->setPerspectiveCompensation(0.9f); map->getStyle().addImage(std::make_unique( "puck.png", mln::decodeImage(mln::util::read_file(mbglPuckAssetsPath + "puck.png")), 1.0f)); map->getStyle().addImage(std::make_unique( "puck_shadow.png", mln::decodeImage(mln::util::read_file(mbglPuckAssetsPath + "puck_shadow.png")), 1.0f)); map->getStyle().addImage(std::make_unique( "puck_hat.png", mln::decodeImage(mln::util::read_file(mbglPuckAssetsPath + "puck_hat.png")), 1.0f)); puckLayer->setBearingImage(mln::style::expression::Image("puck.png")); puckLayer->setShadowImage(mln::style::expression::Image("puck_shadow.png")); puckLayer->setTopImage(mln::style::expression::Image("puck_hat.png")); puck = puckLayer.get(); map->getStyle().addLayer(std::move(puckLayer)); } else { bool visible = puck->getVisibility() == mln::style::VisibilityType::Visible; if (visible) { if (!puckFollowsCameraCenter) { mln::LatLng mapCenter = map->getCameraOptions().center.value(); puck->setLocation(toArray(mapCenter)); puckFollowsCameraCenter = true; } else { puckFollowsCameraCenter = false; puck->setVisibility(mln::style::VisibilityType(mln::style::VisibilityType::None)); } } else { puck->setLocation(toArray(puckLocation)); puck->setVisibility(mln::style::VisibilityType(mln::style::VisibilityType::Visible)); puckFollowsCameraCenter = false; } } #endif } using Nanoseconds = std::chrono::nanoseconds; void GLFWView::onWillStartRenderingFrame() { MLN_TRACE_FUNC(); #ifdef ENABLE_LOCATION_INDICATOR puck = static_cast(map->getStyle().getLayer("puck")); if (puck) { uint64_t ns = mln::Clock::now().time_since_epoch().count(); const double bearing = static_cast(ns % 2000000000) / 2000000000.0 * 360.0; puck->setBearing(mln::style::Rotation(bearing)); } #endif }